wait()
和 waitpid()
都是 UNIX 和 Linux 系统上用于让父进程等待其子进程终止的系统调用。当子进程终止后,它们使父进程能够收集子进程的退出状态。这两个调用也帮助防止僵尸进程的产生。
wait()
系统调用挂起父进程的执行,直到其一个子进程终止。一旦子进程终止,wait()
将返回子进程的 PID,并提供其退出状态。
原型:
pid_t wait(int *status);
参数:
status
: 一个指向整数的指针,用于存储子进程的退出状态。返回值:
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)
,直到它返回 -1
且 errno
被设置为 ECHILD
,表示没有更多的子进程。(关于errno
和 ECHILD
会在文章末尾讲到 )
返回值:
WNOHANG
选项并且没有子进程已经退出,则返回 0。当子进程终止时,内核不立即删除其所有信息。它保留一部分信息(例如退出状态),供父进程查询。在父进程调用 wait()
或 waitpid()
并获得子进程的退出状态后,子进程的这部分信息才会被完全删除。
如果子进程在父进程调用 wait()
或 waitpid()
之前已经终止,则这两个调用将立即返回。
防止僵尸进程:当子进程终止并退出后,但其父进程尚未收集其退出状态,子进程会变成僵尸进程。使用 wait()
或 waitpid()
可以避免这种情况。
同步:父进程可能需要等待子进程完成某些任务后再继续执行。这时可以使用 wait()
或 waitpid()
来同步父子进程的执行。
这两个系统调用是进程管理和进程同步中的关键工具,并且是 UNIX 和 Linux 系统编程的基础。
下面,为 wait()
和 waitpid()
分别提供一个简单的 C 语言示例。
wait()
这个例子中,父进程创建一个子进程。子进程会休眠几秒后退出,而父进程会通过 wait()
等待子进程完成。
#include
#include
#include
#include
#include
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;
}
waitpid()
与 WNOHANG
这个例子创建一个子进程,父进程会尝试使用 waitpid()
的 WNOHANG
选项进行非阻塞的等待。
#include
#include
#include
#include
#include
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
变量保存了子进程终止时的相关状态信息。我们可以使用一系列的宏来解析这个状态。这些宏包括:
WIFEXITED(status): 如果子进程正常退出返回 true
。这意味着子进程通过 exit()
或 _exit()
调用正常结束。
WIFEXITED(status)
为 true
,此宏可以用来获取子进程传递给 exit()
或 _exit()
的退出状态。WIFSIGNALED(status): 如果子进程是由于一个未捕获的信号而终止的,则返回 true
。
WIFSIGNALED(status)
为 true
,此宏返回导致子进程终止的信号的编号。WIFSTOPPED(status): 如果子进程被停止,则返回 true
(例如,由于接收到 SIGSTOP 或其他停止信号)。
WIFSTOPPED(status)
为 true
,此宏返回导致子进程停止的信号的编号。举个例子,如果子进程通过 exit(3)
正常退出,那么 WIFEXITED(status)
将返回 true
,且 WEXITSTATUS(status)
将返回 3
。
如果子进程由于接收到信号 SIGKILL
而被终止,那么 WIFSIGNALED(status)
将返回 true
,且 WTERMSIG(status)
将返回 9
(因为 SIGKILL 的信号编号是9)。
需要注意的是,status
变量本身的值可能是一个复杂的组合,包含了退出状态、终止的信号等信息,所以我们通常使用上述的宏来解析和检索特定的信息。
下面,我们通过一个简单的示例,来展示如何使用上述的宏来检查子进程的退出状态。
这个示例创建一个子进程,子进程会尝试除以零(这会导致程序接收到 SIGFPE
信号并终止)。父进程将使用 waitpid()
来等待子进程并检查其退出状态。
#include
#include
#include
#include
#include
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
另外,信号的定义和相关的数字编码也在头文件
中。我们可以直接查看这个文件(其位置可能因系统和安装而异,但通常在 /usr/include/
下)。使用以下命令可以查找和显示这个文件的内容:
cat /usr/include/asm-generic/signal.h
或者:
cat /usr/include/bits/signum-generic.h
具体的位置可能会根据我们的系统和架构有所不同。如果使用的是不同的 Linux 分发版或架构,可能需要稍微搜索一下。
errno
和 ECHILD
都是与 UNIX 和 Linux 系统编程紧密相关的概念。下面是它们的详细介绍:
定义:errno
是一个全局变量,用于指示许多库函数的错误状态。当这些函数遇到错误时,它们通常返回一个错误代码(如 -1
),并设置 errno
为一个特定的值,以指示特定的错误原因。
使用:为了正确使用 errno
,在调用可能会设置它的函数之前,通常应先将其设置为 0。调用函数后,如果函数返回错误,可以检查 errno
来确定错误的原因。
线程安全性:在多线程环境中,为了线程安全,errno
实际上是一个宏,通常映射到一个线程局部存储变量。
与 perror() 一起使用:可以使用 perror()
函数打印与当前 errno
值关联的错误消息。
定义:ECHILD
是一个错误码,用于表示“没有子进程”。在某些情况下,当进程尝试等待一个不存在的子进程时,可能会设置此错误。
使用场景:例如,当一个进程尝试使用 wait()
或 waitpid()
等待子进程,但它没有任何子进程或已经等待了所有子进程,wait()
或 waitpid()
将返回 -1
,并将 errno
设置为 ECHILD
。
来源:ECHILD
的定义通常可以在
或
中找到。
示例:
#include
#include
#include
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
错误。