C- wait() & waitpid() & status变量 & errno

基本概念

wait()waitpid() 都是 UNIX 和 Linux 系统上用于让父进程等待其子进程终止的系统调用。当子进程终止后,它们使父进程能够收集子进程的退出状态。这两个调用也帮助防止僵尸进程的产生。

1. wait()

wait() 系统调用挂起父进程的执行,直到其一个子进程终止。一旦子进程终止,wait() 将返回子进程的 PID,并提供其退出状态。

原型:

pid_t wait(int *status);

参数:

  • status: 一个指向整数的指针,用于存储子进程的退出状态。

返回值:

  • 如果成功,返回已终止的子进程的 PID。
  • 如果调用进程没有子进程或有错误发生,则返回 -1。

2. waitpid()

waitpid()wait() 的扩展,它提供了更多的控制,允许父进程等待特定的子进程或者满足某些条件的子进程。

原型:

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

参数:

  • pid:
    • 如果 > 0,则 waitpid() 只等待与 pid 匹配的子进程。

    • 如果 = 0,则 waitpid() 等待与调用进程同一进程组的任意一个子进程。更具体地说,当 pid = 0 时:

      • 如果调用进程的进程组中有一个子进程结束,waitpid() 就会立即返回这个子进程的状态信息,并不会继续等待其他子进程。

      • 如果调用进程的进程组中没有子进程结束,waitpid() 的行为将取决于它的 options 参数。如果 WNOHANG 被设置为选项,那么 waitpid() 不会阻塞,而是立即返回0。如果 WNOHANG 没有被设置,那么 waitpid() 会阻塞,直到进程组中的一个子进程结束。

    • 如果 = -1,则 waitpid() 等待任意一个子进程,行为与 wait() 类似。更具体地说,当 pid = -1 时:

      • 如果存在已经结束的子进程,waitpid() 会立即返回,并返回该子进程的状态信息。

      • 如果没有子进程已经结束,waitpid() 的行为取决于 options 参数:

        • 如果设置了 WNOHANG 选项,waitpid() 不会阻塞,而是立即返回 0
        • 如果没有设置 WNOHANG 选项,waitpid() 会阻塞,直到一个子进程结束。
    • 如果 < -1,则 waitpid() 等待进程组 ID 为 pid 绝对值的任何子进程。

  • status: 同上,用于存储子进程的退出状态。
  • options: 提供额外的选项来控制 waitpid() 的行为。常见的选项包括:
    • WNOHANG: 使 waitpid() 成为非阻塞的,如果没有子进程已经退出,则立即返回 0。
    • WUNTRACED: 如果子进程被停止(例如,因为收到 SIGSTOP 信号),并且之前尚未报告,则也返回。

所以,如果希望等待所有的子进程都结束,需要在一个循环中多次调用 waitpid(-1, &status, 0),直到它返回 -1errno 被设置为 ECHILD,表示没有更多的子进程。(关于errnoECHILD会在文章末尾讲到 )

返回值:

  • 如果成功,返回已终止或被停止的子进程的 PID。
  • 如果使用了 WNOHANG 选项并且没有子进程已经退出,则返回 0。
  • 如果出现错误或调用进程没有子进程,则返回 -1。

工作原理

当子进程终止时,内核不立即删除其所有信息。它保留一部分信息(例如退出状态),供父进程查询。在父进程调用 wait()waitpid() 并获得子进程的退出状态后,子进程的这部分信息才会被完全删除。

如果子进程在父进程调用 wait()waitpid() 之前已经终止,则这两个调用将立即返回。

使用场景

  • 防止僵尸进程:当子进程终止并退出后,但其父进程尚未收集其退出状态,子进程会变成僵尸进程。使用 wait()waitpid() 可以避免这种情况。

  • 同步:父进程可能需要等待子进程完成某些任务后再继续执行。这时可以使用 wait()waitpid() 来同步父子进程的执行。

这两个系统调用是进程管理和进程同步中的关键工具,并且是 UNIX 和 Linux 系统编程的基础。

wait() 和 waitpid() 实例

下面,为 wait()waitpid() 分别提供一个简单的 C 语言示例。

1. 使用 wait()

这个例子中,父进程创建一个子进程。子进程会休眠几秒后退出,而父进程会通过 wait() 等待子进程完成。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        // fork 失败
        perror("Fork failed");
        exit(1);
    }

    if (pid == 0) {
        // 子进程
        printf("Child process with ID %d\n", getpid());
        sleep(3);
        printf("Child process exiting...\n");
        exit(0);
    } else {
        // 父进程
        printf("Parent waiting for child to exit...\n");
        wait(NULL);
        printf("Parent process exiting...\n");
    }

    return 0;
}

2. 使用 waitpid()WNOHANG

这个例子创建一个子进程,父进程会尝试使用 waitpid()WNOHANG 选项进行非阻塞的等待。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        // fork 失败
        perror("Fork failed");
        exit(1);
    }

    if (pid == 0) {
        // 子进程
        printf("Child process with ID %d\n", getpid());
        sleep(3);
        printf("Child process exiting...\n");
        exit(0);
    } else {
        // 父进程
        int status;
        while (waitpid(pid, &status, WNOHANG) == 0) {
            printf("Child is still running...\n");
            sleep(1);
        }
        printf("Child has exited. Parent process exiting...\n");
    }

    return 0;
}

这里,父进程每秒检查一次子进程的状态,直到子进程退出为止。

在上面的 waitpid() 示例中,没有为 status 变量提供初始值。但在这种情况下,它是可以接受的,因为 waitpid() 将更新 status 变量的值以反映子进程的退出状态。当 waitpid() 返回子进程的 PID(说明子进程已经退出)时,status 会被设置为相应的退出状态。

status变量

status 变量保存了子进程终止时的相关状态信息。我们可以使用一系列的宏来解析这个状态。这些宏包括:

  1. WIFEXITED(status): 如果子进程正常退出返回 true。这意味着子进程通过 exit()_exit() 调用正常结束。

    • WEXITSTATUS(status): 如果 WIFEXITED(status)true,此宏可以用来获取子进程传递给 exit()_exit() 的退出状态。
  2. WIFSIGNALED(status): 如果子进程是由于一个未捕获的信号而终止的,则返回 true

    • WTERMSIG(status): 如果 WIFSIGNALED(status)true,此宏返回导致子进程终止的信号的编号。
  3. WIFSTOPPED(status): 如果子进程被停止,则返回 true(例如,由于接收到 SIGSTOP 或其他停止信号)。

    • WSTOPSIG(status): 如果 WIFSTOPPED(status)true,此宏返回导致子进程停止的信号的编号。

举个例子,如果子进程通过 exit(3) 正常退出,那么 WIFEXITED(status) 将返回 true,且 WEXITSTATUS(status) 将返回 3

如果子进程由于接收到信号 SIGKILL 而被终止,那么 WIFSIGNALED(status) 将返回 true,且 WTERMSIG(status) 将返回 9(因为 SIGKILL 的信号编号是9)。

需要注意的是,status 变量本身的值可能是一个复杂的组合,包含了退出状态、终止的信号等信息,所以我们通常使用上述的宏来解析和检索特定的信息。

status 实例

下面,我们通过一个简单的示例,来展示如何使用上述的宏来检查子进程的退出状态。

这个示例创建一个子进程,子进程会尝试除以零(这会导致程序接收到 SIGFPE 信号并终止)。父进程将使用 waitpid() 来等待子进程并检查其退出状态。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        // fork 失败
        perror("Fork failed");
        exit(1);
    }

    if (pid == 0) {
        // 子进程
        printf("Child process with ID %d attempting to divide by zero...\n", getpid());
        int result = 1 / 0;  // 这将导致 SIGFPE 信号
        printf("This won't be printed.\n");
        exit(0);
    } else {
        // 父进程
        int status;
        waitpid(pid, &status, 0);

        if (WIFEXITED(status)) {
            printf("Child exited with status %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child was terminated by signal %d\n", WTERMSIG(status));
        }
    }

    return 0;
}

在这个例子中,当运行程序时,我们会看到“Child was terminated by signal”消息,因为子进程由于接收到 SIGFPE 信号而被终止。

majn@tiger:~/C_Project/process_project$ ./status_demo 
Child process with ID 668989 attempting to divide by zero...
Child was terminated by signal 8

查看所有的信号

在 Linux 中,我们可以使用 kill 命令的 -l 选项来查看所有的信号:

kill -l

这将列出所有可用的信号及其对应的编号。

majn@tiger:~/C_Project/process_project$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

另外,信号的定义和相关的数字编码也在头文件 <signal.h> 中。我们可以直接查看这个文件(其位置可能因系统和安装而异,但通常在 /usr/include/ 下)。使用以下命令可以查找和显示这个文件的内容:

cat /usr/include/asm-generic/signal.h

或者:

cat /usr/include/bits/signum-generic.h

具体的位置可能会根据我们的系统和架构有所不同。如果使用的是不同的 Linux 分发版或架构,可能需要稍微搜索一下。

errno 和 ECHILD

errnoECHILD 都是与 UNIX 和 Linux 系统编程紧密相关的概念。下面是它们的详细介绍:

errno

  1. 定义errno 是一个全局变量,用于指示许多库函数的错误状态。当这些函数遇到错误时,它们通常返回一个错误代码(如 -1),并设置 errno 为一个特定的值,以指示特定的错误原因。

  2. 使用:为了正确使用 errno,在调用可能会设置它的函数之前,通常应先将其设置为 0。调用函数后,如果函数返回错误,可以检查 errno 来确定错误的原因。

  3. 线程安全性:在多线程环境中,为了线程安全,errno 实际上是一个宏,通常映射到一个线程局部存储变量。

  4. 与 perror() 一起使用:可以使用 perror() 函数打印与当前 errno 值关联的错误消息。

ECHILD

  1. 定义ECHILD 是一个错误码,用于表示“没有子进程”。在某些情况下,当进程尝试等待一个不存在的子进程时,可能会设置此错误。

  2. 使用场景:例如,当一个进程尝试使用 wait()waitpid() 等待子进程,但它没有任何子进程或已经等待了所有子进程,wait()waitpid() 将返回 -1,并将 errno 设置为 ECHILD

  3. 来源ECHILD 的定义通常可以在 <errno.h><sys/errno.h> 中找到。

示例:

#include <sys/wait.h>
#include <stdio.h>
#include <errno.h>

int main() {
    int status;
    if (wait(&status) == -1) {
        if (errno == ECHILD) {
            perror("c_program wait error");
            // 输出是 "c_program wait error: No child processes"
        }
    }
    return 0;
}

在上面的例子中,如果主进程没有子进程,尝试等待子进程时会产生 ECHILD 错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青衫客36

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值