0%

system-shell

  • fork
  • waitpid
  • execl

fork

一个进程,包括代码、数据和分配给进程的资源。fork 函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。

一个进程调用 fork 函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

当执行 fork 函数后,会生成一个子进程,子进程的执行从 fork 的返回值开始且代码继续往下执行。

所以 fork 执行一次后会有两次返回值:第一次为原来的进程,即父进程会有一次返回值,表示新生成的子进程的进程ID;第二次为子进程的起始执行,返回值为0。如果返回值为-1,则表示创建子进程失败,可能通过 errno 定位失败原因。

示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main (int argc, char ** argv)
{
    int flag = 0;
    pid_t pId = fork();
    if (pId == -1) {
        perror("fork error");
        exit(EXIT_FAILURE);
    } else if (pId == 0) {
        int myPid = getpid();
        int parentPid = getppid();

        printf("Child:SelfID=%d ParentID=%d \n", myPid, parentPid);
        flag = 123;
        printf("Child:flag=%d %p \n", flag, &flag);
        int count = 0;
        do{
            count ++;
            sleep(1);
            printf("Child count=%d \n", count);
            if (count >= 5) {
                break;
            }
        }while (1);
        return EXIT_SUCCESS;
    } else {
        printf("Parent:SelfID=%d MyChildPID=%d \n", getpid(), pId);
        flag = 456;
        printf("Parent:flag=%d %p \n", flag, &flag); // 连地址都一样,说明是真的完全拷贝,但值已经是不同的了..
        int count = 0;
        do{
            count ++;
            sleep(1);
            printf("Parent count=%d \n", count);
            if (count >= 2) {
                break;
            }
        }while (1);
    }

    return EXIT_SUCCESS;
}

以上代码中,使用 fork 创建了一个子进程。返回值 pId 有两个作用:一是判断 fork是否正常执行;二是判断 fork 正常执行后如何区分父子进程。

在父子进程中,都各自打印出自己的进程ID及父/子进程ID。

通过flag的值可以验证创建的子进程是完全复制父进程的堆栈段(因为flag是在main()方法内声明的)的,两个进程都输出了flag=0的信息。接下来进程可以各自对flag再次更新值,做到了互不干扰。但从打印的int指针地址来看,指针地址值都是一样的,再次印证了子进程是对父进程的完全复制。

接下来,父进程只执行了两次打印,然后就结束且进程销毁退出了;但父进程的结束并不影响子进程的运行,子进程一直打印到数字5才正常退出。所以验证了 fork 出来的进程是各自独立的,完全按照自己的代码逻辑运行直至执行完毕。

wait

父进程监听子进程状态 wait 的使用

wait能力介绍

创建子进程后,父进程具有监听子进程的运行状态的能力,用到的函数为:

#include <sys/wait.h>

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

以上函数用于等待子进程子进程的状态变化回调并且获取状态变化信息。所能获取到的状态变化包括:子进程运行结束、子进程被信号量暂停、子进程被信号量恢复运行。

父进程执行了wait函数后,如果子进程已经发生了状态变化,则wait函数立即就会有返回结果;否则wait函数会一直阻塞直至子进程状态发生变化。

通常意义上,如果子进程已经发生了状态变化,但还未被父进程或其它系统回调执行wait,则把此时的子进程称为是可等待的(waitable)。

子进程运行结束后,父进行执行wait函数可以推动系统释放与子进程相关的资源;否则子进程将会被维持在僵尸进程的状态下一直存在。   

wait函数讲解

函数 wait(int * status) 是对 waitpid 的封装,限定了只有在任一子进程运行结束时才会有返回,否则调用进程会一起处于阻塞状态暂停执行。

wait(int * status) 等同于如下代码:

waitpid(-1, &status, 0);

waitpid() 会阻塞调用进程直至任一子进程的运行状态发生变化。接下来对 waitpid() 的三个参数进行讲解:   

PID

  • pid < -1取该pid的绝对值,如果任意子进程的进程组ID等于该绝对值,则该组进程中任一子进程中的进程状态发生变化都会触发waitpid()的回调。
  • pid == -1监听范围扩大到任意子进程。
  • pid == 0监听限制为子进程的进程组ID与父进程相等。
  • pid > 0监听限制为指定子进程进程ID值。

STATUS

值可以为NULL。当不为NULL时,用于存储触发状态变化的信息号值和exit(code)中的code值。
wait.h头文件定义了几个宏用于解析status的值,常见的有:

  • WIFEXITED(status) 当子进程调用exit(code)或_exit(code)或正常运行到main()函数结尾时正常结束运行,则返回true
  • WEXITSTATUS(status) 当WIFEXITED(status)为true时,获取exit(code)或_exit(code)的code值。其中code只能为0或正数,不支持负数。
  • WIFSIGNALED(status) 当子进程被信号量杀死时则返回true
  • WTERMSIG(status) 当WIFSIGNALED(status)为true时,获取该信号量的值
  • WIFSTOPPED(status) 当子进程被信号量暂停执行时则返回true
  • WSTOPSIG(status) 当WIFSTOPPED(status)为true时,获取该信号量的值

Options

值可以是以下常量的任意值或任意常量与0的OR计算值

  • WNOHANG 调用wait时指定的pid仍未结束运行,则wait立即返回0。waitpid将不阻塞如果指定的pid并未结束
  • WUNTRACED 当子进程被暂停时,则wait立即返回子进程的pid。
  • WCONTINUED 当被暂停的子进程又被信号量恢复后,则wait立即返回子进程的pid

wait() 函数在正常执行时会返回被终止进程的pid值,当执行发生错误后会返回-1。

waitpid() 函数在正常执行时会返回进程状态发生变化的进程pid值;如果函数 options 中包含了 WNOHANG 常量,则会在指定pid的子进程未退出且进程状态也未发生变化时直接返回0,如果子进程已经退出了,则返回子进程的pid;否则当执行发生错误后会返回-1。

execl

exec 系列函数包括

#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, ...
                /* (char  *) NULL */);
int execlp(const char *file, const char *arg, ...
                /* (char  *) NULL */);
int execle(const char *path, const char *arg, ...
                /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
                char *const envp[]);
  • l 的exec函数:execl,execlp,execle,表示后边的参数以可变参数的形式给出且都以一个空指针结束
  • p 的exec函数:execlp,execvp,表示第一个参数path不用输入完整路径,只有给出命令名即可,它会在环境变量PATH当中查找命令
  • 不带 l 的exec函数:execv,execvp表示命令所需的参数以 char *arg[] 形式给出且arg最后一个元素必须 是NULL
  • e 的exec函数:execle表示,将环境变量传递给需要替换的进程

例如:

execl("/bin/ls","ls","-l",NULL);

char *argv[] = {"ls","-l",NULL};
ret = execvp("ls",argv);

Example

if((pid = fork()) < 0){
    return -1;
}
else if(0 == pid){
    execl("/bin/sh", "sh", "-c", cmd, NULL);
    exit(127);
}
else {
    while(waitpid(pid, &status, WNOHANG) == 0){
        //执行脚本未结束,执行一些中间处理
        usleep(10000);
    }

    //脚本执行完成
    //do finish
}