默认分类 11 0

    类似System()的不阻塞进程调用的实现

    类似System()的不阻塞进程调用的实现

    在一些情况下,我们希望在代码中创建系统调用的同时,让该进程独立于执行它的软件运行。常规的system( )系统调用会在执行代码的时候阻塞并返回命令的返回值。因此,在工程中我们重新实现了一个执行命令的函数 exec_cmd,通过两次 vfork 来创建子进程和孙子进程,以确保孙子进程在独立的环境中执行命令。

    代码实现

    
     OSI_INT32 exec_cmd(const char *cmdstring)
    {
        pid_t pid;
        pid_t ppid;
        int status=0;
        int i;
        int fdlimit = 1024;
        struct rlimit rl;
    
        /* 基本检查 */
        if (cmdstring == NULL) {
            return status;
        }
    
        /* 第一次vfork */
        /* fork子进程与父进程共享内存,内存受限,使用vfork */
        if ((pid = vfork()) < 0) {
            status = -1;
    
        } else if (pid == 0) {
            /* 第二次vfork */
            if ((ppid = vfork()) < 0) {
                status = -1;
    
            } else if (ppid == 0) {
                /* 孙子进程 */
                /* 获取fd限制 */
                if (getrlimit(RLIMIT_FSIZE, &rl) == -1) {
                    OSI_Debug(DLEVEL_ERROR, "getrlimit fd limit error!");
                } else {
                    fdlimit = rl.rlim_cur;
                }
    
                /* 关闭除了0,1,2以外的其他文件描述符 */
                for (i=3; i<fdlimit; i++) {
                    close (i);
                }
    
                /* 执行命令 */
                execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
                _exit(127);
            }
    
            /* ust exit in child, then the grand child is free(father is 1(init)) */
            /* 强制退出 */
            _exit(0);
    
        } else {
            /* 等待子进程退出 */
            while (waitpid(pid, &status, 0) < 0)
            {
                if (errno == EINTR)
                {
                    continue;
                }
    
                status = -1;
                break;
            }
        }
    
        /* 返回执行结果 */
        return status;
    }
    1. 基本检查

       if (cmdstring == NULL) {
           return status;
       }

      检查输入的命令字符串是否为空,如果为空则直接返回。

    2. 第一次 vfork
       if ((pid = vfork()) < 0) {
           status = -1;
       } else if (pid == 0) {
           /* 子进程 */
           if ((ppid = vfork()) < 0) {
               status = OSI_ERROR;
           } else if (ppid == 0) {
               /* 孙子进程 */
               if (getrlimit(RLIMIT_FSIZE, &rl) == -1) {
                   printf( "getrlimit fd limit error!");
               } else {
                   fdlimit = rl.rlim_cur;
               }
               for (i = 3; i < fdlimit; i++) {
                   close(i);
               }
               execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
               _exit(127);
           }
           _exit(0);
       } else {
           /* 父进程 */
           while (waitpid(pid, &status, 0) < 0) {
               if (errno == EINTR) {
                   continue;
               }
               status = -1;
               break;
           }
       }
    • 第一次 vfork:创建子进程。
    • 第二次 vfork:在子进程中创建孙子进程。
    • 孙子进程:获取文件描述符限制,关闭所有非标准输入输出的文件描述符,执行命令。
    • 子进程:退出,使孙子进程成为孤儿进程,由 init 进程接管。
    • 父进程:等待子进程退出,并检查状态。

    实现原理

    进程创建和退出过程
    1. 父进程调用 vfork 创建子进程

      • 父进程调用 vfork 创建子进程。
      • 子进程是父进程的副本,共享父进程的地址空间。
    2. 子进程调用 vfork 创建孙子进程

      • 子进程调用 vfork 创建孙子进程。
      • 孙子进程是子进程的副本,共享子进程的地址空间。
    3. 孙子进程执行命令

      • 孙子进程调用 getrlimit 获取文件描述符限制。
      • 孙子进程关闭多余的文件描述符。
      • 孙子进程调用 execl 执行 /bin/sh 命令。
      • 如果 execl 成功,孙子进程的内存空间被替换,孙子进程执行新的命令。
      • 如果 execl 失败,孙子进程调用 _exit(127) 退出。
    4. 子进程退出

      • 子进程在创建孙子进程后立即调用 _exit(0) 退出。
    5. 父进程等待子进程结束

      • 父进程调用 waitpid 等待子进程结束,并处理 EINTR 错误。
    孙子进程的状态
    • 孙子进程的父进程:在孙子进程创建后,子进程立即调用 _exit(0) 退出。此时,孙子进程的父进程(子进程)已经终止,但孙子进程仍然存在。

      孙子进程在子进程退出后,会被操作系统接管,成为孤儿进程。孤儿进程会被 init 进程(进程ID为1)收养,init 进程会负责处理孤儿进程的退出状态。

    关闭文件描述符
    1. 获取文件描述符限制

      • getrlimit(RLIMIT_FSIZE, &rl) 获取当前进程的文件描述符限制。
      • rl.rlim_cur 是当前文件描述符的软限制。
    2. 关闭文件描述符

      • 从 3 开始关闭文件描述符,因为文件描述符 0、1 和 2 通常被用于标准输入、标准输出和标准错误。
      • close(i) 关闭文件描述符 i
      • 文件描述符 0、1 和 2 分别对应标准输入(stdin)、标准输出(stdout)和标准错误(stderr),这些是进程的基本输入输出通道,通常不应该被关闭。从 3 开始关闭文件描述符,确保不会影响这些基本的输入输出通道。