文章结构:

  • wait能力介绍
  • wait()函数讲解
  • 示例代码及操作演示
  • # wait能力介绍

    在上一篇 【C/C++】多进程:子进程的创建fork() 中演示了子进程的创建。

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

    #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 < -1 取该pid的绝对值,如果任意子进程的进程组ID等于该绝对值,则该组进程中任一子进程中的进程状态发生变化都会触发 waitpid() 的回调。

    pid == -1 监听范围扩大到任意子进程。

    pid == 0 监听限制为子进程的进程组ID与父进程相等。

    pid > 0 监听限制为指定子进程进程ID值。

  • status
  • 值可以为 NULL 。当不为 NULL 时,用于存储触发状态变化的信息号值和 exit(code) 中的 code 值。

    wait.h 头文件定义了几个宏用于解析 status 的值,常见的有:

    WIFEXITED(status)

    WEXITSTATUS(status)
    当子进程调用 exit(code) _exit(code) 或正常运行到 main() 函数结尾时正常结束运行,则返回 true

    WIFEXITED(status) true 时,获取 exit(code) _exit(code) code 值。
    其中 code 只能为0或正数,不支持负数。 WIFSIGNALED(status)

    WTERMSIG(status) 当子进程被信号量杀死时则返回 true

    WIFSIGNALED(status) true 时,获取该信号量的值。 WIFSTOPPED(status)

    WSTOPSIG(status) 当子进程被信号量暂停执行时则返回 true

    WIFSTOPPED(status) true 时,获取该信号量的值。
  • options
  • 值可以是以下常量的任意值或任意常量与0的 OR 计算值。

    WNOHANG 调用 wait 时指定的 pid 仍未结束运行,则 wait 立即返回0。 WUNTRACED 当子进程被暂停时,则 wait 立即返回子进程的 pid 。 WCONTINUED
    当被暂停的子进程又被信号量恢复后,则 wait 立即返回子进程的 pid
    Linux 2.6.10及以后生效。在Mac 0S X 10.9.5上未生效。

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

    # 示例代码及操作演示

    由于涉及到两个进程,在终端命令行下的日志打印会出现混乱,所以通过重定向标准输入输出流将两个进程的日志分别输出到两个文件中去,父进程的日志输出到 main.txt 中去,子进程的日志输出到 child.txt 中去。涉及到重定向标准输入输出流,具体细节见 【C/C++】文件创建、打开、读、写、复制、关闭、删除等操作汇总

    涉及到的 ps kill 命令可以通过 man ps man kill 去查找细节。

    接下来的代码将演示 fork() 一个子进程后,会通过 ps 命令查询进程状态,及 kill 命令向子进程发送信号量改变进程状态;父进程通过 wait 监听子进程状态。

    wait.c 源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    #include <sys/wait.h>
    #include <errno.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <time.h>

    #ifndef CHILD_COUNT
    #define CHILD_COUNT 10000
    #endif


    static void printTime() {
    time_t calendar_time = time(NULL);
    struct tm * tm_local = localtime(&calendar_time);
    char str_f_t [50];
    strftime(str_f_t, sizeof(str_f_t), "%G-%m-%d %H:%M:%S", tm_local);
    printf("%s ", str_f_t);
    }

    int main/*09*/(int argc, char *argv[]) {
    pid_t cpid, w;
    int status;
    cpid = fork();
    if (cpid == -1) {
    perror("fork");
    exit(EXIT_FAILURE);
    }
    if (cpid == 0) {
    /* Code executed by child */
    FILE* fChild = freopen("/Users/sodino/workspace/xcode/Define/Define/child.txt", "w", stdout);
    printTime();
    printf("Child PID is %ld argc=%d\n", (long) getpid(), argc);
    int count = 0;
    do{
    sleep(1);
    printTime();
    printf("sleep count=%d\n", count);
    fflush(fChild);

    count ++;
    if (count > CHILD_COUNT) {
    break;
    }
    }while (1);
    fflush(fChild);
    fclose(fChild);

    // code值将从父进程的WEXITSTATUS()宏定义中获知
    // _exit(123);
    exit(123);
    } else {
    FILE * fMain = freopen("/Users/sodino/workspace/xcode/Define/Define/main.txt", "w", stdout);

    // 如果子进程还在,则返回0;如果子进程已经运行结束,则返回子进程pid
    w = waitpid(cpid, &status, WNOHANG);
    if (w == 0) {
    printTime();
    printf("Child PID=%d has not yet changed state\n", cpid);
    } else {
    printTime();
    printf("Child PID=%d has been terminated.\n", cpid);
    }

    int ifExited, ifSignaled, ifStopped, ifContinued;
    /* Code executed by parent */
    do {
    // 在mac上·WCONTINUED·无效,从头文件来看,只适用于thread
    w = waitpid(cpid, &status, WUNTRACED | WCONTINUED);
    // w = waitpid(cpid, &status, WUNTRACED);
    if (w == -1) {
    printTime();
    printf("Parent w=-1, error=%s \n", strerror(errno));
    exit(EXIT_FAILURE);
    } if (w == 0) {
    printTime();
    printf("w == 0 ignore. continue.\n");
    continue;
    }

    ifExited = WIFEXITED(status);
    ifSignaled = WIFSIGNALED(status);
    ifStopped = WIFSTOPPED(status);
    ifContinued = WIFCONTINUED(status);

    printTime();
    printf("pid=%ld w=%d exitCode=%d status=%d ifExited=%d ifSignaled=%d ifStopped=%d ifContinued=%d \n", (long)getpid(),
    // w, status, WEXITSTATUS(w), ifExited, ifSignaled, ifStopped, ifContinued);
    w, status, _WSTATUS(w), ifExited, ifSignaled, ifStopped, ifContinued);

    printTime();
    if (ifExited) {
    printf("PID=%ld exited, status=%d\n", (long)w, WEXITSTATUS(status));
    } else if (ifSignaled) {
    printf("PID=%ld killed by signal %d\n", (long)w, WTERMSIG(status));
    } else if (ifStopped) {
    printf("PID=%ld stopped by signal %d\n", (long)w, WSTOPSIG(status));
    } else if (ifContinued) {
    printf("PID=%ld continued\n", (long)w);
    }
    fflush(fMain);
    if (ifExited || ifSignaled) {
    printTime();
    printf("isExited=%d isSingaled=%d\n", ifExited, ifSignaled);
    fflush(fMain);
    break;
    }
    } while (1);

    printTime();
    printf("Main PID %ld exit.\n", (long)getpid());

    fclose(fMain);
    exit(EXIT_SUCCESS);
    }
    }

    进入到 wait.c 的目录下,执行如下命令编译并运行:

    gcc wait.c // 生成可执行文件a.out
    ./a.out    // 运行可执行文件

    使用 ps -j 命令,查看进程可以看到 fork() 执行后,进程列表中有两个名为 a.out 的进程。如下图:
    two.processes

    可以看到PID=678的进程其父进程是PID=677。两个进程的进程组ID(PGID)都为677。

    再看上图中的 STAT 列,两个进程都是 S+ ,其中 S 表示进程处于 sleeping 状态,原因是父进程在 wait ,而子进程除了打印日志外大部分时间都是在执行 sleep() ;另一个 + 表示这两个进程都是在当前控制台的前台进程。

    接使用 kill -SIGSTOP 678 命令向子进程发送暂停信号,再 ps -j 查询一下进程状态,发现子进程678已经从 S+ 变为 T+ ,即已经进入STOP状态了。

    而另一方面,查看 child.txt ,发现该文件已经不会继续生成日志了。查看 main.txt 文件,日志内容如下:

    2015-04-16 22:53:15 Child PID=678 has not yet changed state
    2015-04-16 23:11:48 pid=677 w=678 exitCode=4479 status=38 ifExited=0 ifSignaled=0 ifStopped=1 ifContinued=0 
    2015-04-16 23:11:48 PID=678 stopped by signal 17

    第一句 Child PID=678 has not yet changed state 是父进程执行 waitpid:WNOHANG 的返回结果,表示当时子进程仍未退出,正在运行。第二句则输出了 ifStopped=1 表示 waitpid:WUNTRACED 已经监听到子进程被外部发送的信号量导致进程状态发生变化了。第三句 PID=678 stopped by signal 17 表示信号量值是17,通过以下命令可能验证SIGSTOP信号量值为17:

    sodino:Define sodino$ kill -l SIGSTOP
    17

    接下来,要继续使用 kill -SIGCONT 678 命令来恢复子进程的运行,操作见下图:
    kill.sigcont

    由上图可见子进程从暂停状态又恢复运行了。查看 child.txt 日志则发现日志又恢复输出了:

    ... ...
    2015-04-16 23:11:46 sleep count=1110
    2015-04-16 23:11:47 sleep count=1111
    2015-04-16 23:23:20 sleep count=1112
    2015-04-16 23:23:21 sleep count=1113
    ... ...

    注意看时间,从23:11分到23:23分这个时间段内进程状态是停止的所以没有日志输出(这段时间在写博客给你们看呀..)。查看 main.txt 则没有发现 waitpid 有回调,个人认为这是在我的mac上C版本及运行环境问题吧。

    最后再使用命令 kill -SIGTERM 678 后,可以发现 ps 命令已经查询不到刚才的 a.out 进程了。
    kill.sigterm