收发信号思想是 Linux 程序设计特性之一,一个信号可以认为是一种软中断,通过用来向进程通知异步事件。
本文讲述的 信号处理内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解信号编程。
遵循 POSIX.1 - 2008
标准 c 库,libc, -lc
这个接口依 _POSIX_C_SOURCE 特性测试宏。
#include
pid_t wait(int *_Nullable wstatus);
pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
/* This is the glibc and POSIX interface; see
NOTES for information on the raw system call. */
waitid():
Since glibc 2.26:
_XOPEN_SOURCE >= 500 || _POSIX_C_SOURCE >= 200809L
glibc 2.25 and earlier:
_XOPEN_SOURCE
|| /* Since glibc 2.12: */ _POSIX_C_SOURCE >= 200809L
|| /* glibc <= 2.19: */ _BSD_SOURCE
所有这些系统调用都是调用进程等待子进程状态变化并获得变化进程相关信息的。这里的状态变化包括:子进程终止、子进程被信号打断、子进程被信号恢复。对于终止的子进程,进程 wait 操作能够让系统释放子进程的相关资源,如果没进行 wait,那么终止的子进程会停留在僵尸状态(参考后面注意章节)。
如果子进程早已经有状态变化,那么这些调用会立马返回。否则会一直阻塞直到子进程状态变化或者一个信号处理函数打断当前调用(我们假设该信号没有设置 sigaction(2) 的 SA_RESTART标记)。本文中会将子进程状态变化但没有被 wait 的状态称为 waitable。
wait() 和 waitpid()
wait() 系统调用会暂停当前调用线程直到任何一个子进程终止。 wait(&wstatus) 的相当于
waitpid(-1, &wstatus, 0)
waitpid() 系统调用暂停当前调用线程直到 pid 指定的子进程状态变化。默认情况下,waitpid() 只等待终止的子进程,但是这种行为会受到 options 参数的影响,后面会讲到。
pid 的值可以是:
< -1 表示等待所有进程组 ID 是 pid 绝对值的子进程
-1 表示等待任何子进程
0 表示等待调用时刻进程组 ID 和调用进程的进程组 ID 相同的任何子进程
> 0 表示等待进程 ID 为 pid 的子进程
options 的值为下面值的位或:
WNOHANG
如果没有子进程退出,那么就立即返回
WUNTRACED
子进程停止时也返回(但是没有被 ptrace(2) 跟踪)。被跟踪的子进程的停止状态在没有指定这个状态时也会返回。
下面是只适用于 Linux 的一些选项:
如果 wstatus 不为 NULL,wait() 和 waitpid() 会将状态信息存到它指向的整型数里,这个数能通过以下宏来检查。
WIFEXITED(wstatus)
如果子进程正常终止返回 true,即 通过 exit(3)、_exit(2) 或者main() 函数返回。
WEXISTSTATUS(wstatus)
返回子进程退出的状态,状态包含的是子进程调用 exit(3)、_exit(2) 或者 main() 返回时参数 status 的低 8 位,只有 WIFEXISTED 返回 true 时才适用。
WIFSIGNALED(wstatus)
如果子进程被信号终止返回 true。
WTERMSIG(wstatus)
返回导致子进程退出的信号编号,只能在 WIFSIGNALED 返回 true 时适用。
WCOREDUMP(wstatus)
如果子进程产生 core dump 返回 true(参考 core(5))。这个宏只有在 WIFSIGNALED 返回 true 时适用。
这个宏没有在 POSIX.1-2001 中描述,并且在一些 UNIX 实现上也不可用(比如 AIX、SunOS)。因此使用时应该加上 #ifdef WCOREDUMP ... #endif
WIFSTOPPED(wstatus)
子进程被信号停止时返回 true,这个只有在 WUNTRACED 调用或者子进程正在被 trace 时适用。
WSTOPSIG(wstatus)
返回导致子进程停止的信号,这个宏只能在 WIFSTOPPED 返回 true 时适用。
WIFCONTINUED(wstatus)
从 Linux 2.6.10 起,子进程收到 SIGCONT 信号继续执行时返回 true。
waitpid()
waitpid() 系统调用(Linux 2.6.9 后)提供了对子进程状态变化更加精确的控制。
idtype 和 id 参数用来选择要等待的子进程,如下:
idtype == P_PID
等待进程 ID 号为 id 的子进程
idtype == P_PIDFD(Linux 5.4 后)
等待由文件描述符 id 指定的子进程。
idtype == P_PGID
等待进程组 ID 为 id 的子进程。Linux 5.4 后,如果 id 为 0,就等待调用时刻和调用进程在统一进程组的所有子进程。
idtype == P_ALL
等待任何子进程,忽略 id。
要等待的状态由 options 指定,是下面标记的位或:
WEXITED
等待已经终止的子进程
WSTOPPED
等待被信号停止的子进程
WCONTINUED
等待被 SIGCONT 信号恢复的子进程(之前被停止过)
下面这些标记可能也可以用在 options 中:
WNOHANG
和 waitpid() 一样
WNOWAIT
让子进程继续处于等待状态,后面调用可以继续获取子进程状态信息。
一旦成功返回,waitpid() 会填充 infop 指向的 siginfo_t 结构:
si_pid 子进程进程 id
si_uid 子进程的真实用户 ID(大多数实现没有设置这个字段)
si_signo 总是 SIGCHLD
si_status _exit(2)、exit(3)的返回状态,或者导致子进程终止、停止、继续的信号。si_code 字段可以用来决定如何翻译这个字段。
si_code 可以设置为:CLD_EXITED(子进程调用了 _exit(2)),CLD_KILLED(子进程被信号杀死),CLD_DUMPED(子进程被信号杀死,并且 core dump),CLD_TRAPPED(trace 子进程停止),以及 CLD_CONTINUED(子进程被 SIGCONT 信号继续)。
如果在 options 里指定了 WNOHANG,并且没有子进程处于可等待状态,那么 waitpid() 立即返回 0 并且infop 指向的 siginfo_t 结构填充是实现定义的。为了和有一个可等待的子进程的情况区分,在调用和检查这个字段是否为非零前需要将 si_pid 字段清零。
POSIX.1-2008 技术勘误 1(2013) 中添加了一些要求,当指定 WNOHANG 时并且没有子进程处于可等待状态,那么 waitpid() 应该将清空 si_pid 和 si_signo 字段。在 Linux 以及其服从这个要求的系统实现上,在调用 waitpid() 不用将 si_pid 清零,然而在这一点上,并不是所有实现都遵循 POSIX.1 规范。
wait():成功时返回终止的子进程的 ID,失败时返回 -1。
waitpid():成功时返回状态变化的子进程的 ID,如果指定了 WNOHANG,并且通过 pid 指定了一个或者多个子进程,但是都没有发生状态变化,那么会返回 0。失败时返回 -1。
waitid():成功或者指定了 WNOHANG 并且通过 id 指定的子进程没有状态变化时返回 0,失败时返回 -1。
失败时,会更新 errno。
错误代码如下:
EAGAIN | PID 文件描述符 id 是非阻塞的并且它指向的进程没有终止 |
ECHILD | 调用进程没有任何没等待的子进程(wait()) |
ECHILD | (waitpid() 或者 waitid())pid 指定的进程或者 idtype 和 id 指定的进程不存在,或者不是调用进程的子进程。(这个可能在子进程将 SIGCHILD 信号设置为 SIG_IGN 的情况) |
EINTR | WNOHANG 没有指定并且捕获了一个非阻塞信号或者 SIGCHILD |
EINVAL | options 参数不合法 |
ESRCH | pid 等于 INT_MIN(wait()、waitpid()) |
wait() 实际上是 libc 实现的库函数,它会调用 wait4(2)。
在一些架构上,没有 waitpid() 系统调用,这个接口同样是 C 库通过 wait4(2) 系统调用实现的。
waitid() 系统调用有第五个参数,类型是 struct rusage *,如果这个参数不为空,那么会用它来返回子进程的资源使用信息,这个和 wait4(2) 类似。参考 getrusage(2) 获取更详细信息。
一个终止了但是没有被 wait 的子进程会称为僵尸进程。内核维护关于僵尸进程的最小信息集合(PID、终止状态、资源使用情况),用来让父进程稍后进行 wait 获取子进程的相关信息。只要僵尸进程没有通过 wait 从系统移除,它就会占用内核进程表的一个位置,如果这个表满了,就无法继续创建进程了。如果父进程终止了,那么它的僵尸子进程就会被 1 号 init 进程接管(或者通过 prctl(2) PR_SET_CHILD_SUBREAPER 指定的最近的 subreaper 领养),1 号进程会自动进程 wait 来移除这些僵尸。
POSIX.1-2001 指定了如果 SIGCHLD 的处置函数被设置为 SIG_IGN 或者设置了 SIGCHLD 的 SA_NOCLDWAIT 标记,那么子进程终止后不会成为僵尸进程,wait() 或者 waitpid() 调用会阻塞直至所有子进程终止,然后将 errrno 设置为ECHILD 失败。(原来的 POSIX 标准并没有定义这种情况的行为,注意尽管 SIGCHLD 的默认处置函数是 SIG_IGN,明确的 SIG_IGN 设置会僵尸子进程的不同对待行为。)
Linux 2.6 遵循 POSIX 要求。然而,Linux 2.4 及更早版本并没有:如果 wait() 或者 waitpid() 在 SIGCHLD 被忽略的情况下调用,那么调用行为和 SIGCHLD 没设置是一样的,也就是说调用会一直阻塞直到下一个子进程终止,然后返回对应的进程 ID 和状态信息。
根据 POSIX.1-2008,应用调用 waitid() 时,必须保证 infop 指向 siinfo_t 结构(也就是说不能是空指针)。在 Linux 上,如果 infop 为 NULL,waitid() 会成功并且返回等待的进程 ID。应用程序应该避免依赖这个不一致的、非标准的、非必须的特性 。
下面程序演示了 fork(2) 和 waitpid() 的用法。程序创建了一个子进程,如果没有提供命令行参数,那么子进程会使用 pause(2) 来停止,允许用户向其发送信号。否则如果指定了命令行参数,那么子进程立即返回,使用命令行提供的整数作为返回状态值。父进程循环执行,通过 waitpid() 来监视子进程,使用 W*() 宏来分析返回的状态值。
下面 shell 会话演示了程序的使用:
$ ./a.out &
Child PID is 32360
[1] 32359
$ kill -STOP 32360
stopped by signal 19
$ kill -CONT 32360
continued
$ kill -TERM 32360
killed by signal 15
[1]+ Done ./a.out
$
下面是程序源码:
#include
#include
#include
#include
#include
int
main(int argc, char *argv[])
{
int wstatus;
pid_t cpid, w;
cpid = fork();
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0) { /* Code executed by child */
printf("Child PID is %jd\n", (intmax_t) getpid());
if (argc == 1)
pause(); /* Wait for signals */
_exit(atoi(argv[1]));
} else { /* Code executed by parent */
do {
w = waitpid(cpid, &wstatus, WUNTRACED | WCONTINUED);
if (w == -1) {
perror("waitpid");
exit(EXIT_FAILURE);
}
if (WIFEXITED(wstatus)) {
printf("exited, status=%d\n", WEXITSTATUS(wstatus));
} else if (WIFSIGNALED(wstatus)) {
printf("killed by signal %d\n", WTERMSIG(wstatus));
} else if (WIFSTOPPED(wstatus)) {
printf("stopped by signal %d\n", WSTOPSIG(wstatus));
} else if (WIFCONTINUED(wstatus)) {
printf("continued\n");
}
} while (!WIFEXITED(wstatus) && !WIFSIGNALED(wstatus));
exit(EXIT_SUCCESS);
}
}