你的代码中有太多的错误(由于未在struct sigaction
中清除信号掩码),任何人都无法解释你正在遇到的效果。
相反,考虑以下可用的示例代码,例如example.c
:
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
static pid_t internal_child_pid = 0;
static inline void set_child_pid(pid_t p) { __atomic_store_n(&internal_child_pid, p, __ATOMIC_SEQ_CST); }
static inline pid_t get_child_pid(void) { return __atomic_load_n(&internal_child_pid, __ATOMIC_SEQ_CST); }
static void forward_handler(int signum, siginfo_t *info, void *context)
{
const pid_t target = get_child_pid();
if (target != 0 && info->si_pid != target)
kill(target, signum);
}
static int forward_signal(const int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_sigaction = forward_handler;
act.sa_flags = SA_SIGINFO | SA_RESTART;
if (sigaction(signum, &act, NULL))
return errno;
return 0;
}
int main(int argc, char *argv[])
{
int status;
pid_t p, r;
if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
fprintf(stderr, " %s COMMAND [ ARGS ... ]\n", argv[0]);
fprintf(stderr, "\n");
return EXIT_FAILURE;
}
if (forward_signal(SIGINT) ||
forward_signal(SIGHUP) ||
forward_signal(SIGTERM) ||
forward_signal(SIGQUIT) ||
forward_signal(SIGUSR1) ||
forward_signal(SIGUSR2)) {
fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
p = fork();
if (p == (pid_t)-1) {
fprintf(stderr, "Cannot fork(): %s.\n", strerror(errno));
return EXIT_FAILURE;
}
if (!p) {
execvp(argv[1], argv + 1);
fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
return EXIT_FAILURE;
}
set_child_pid(p);
while (1) {
status = 0;
r = waitpid(p, &status, 0);
if (r == -1) {
if (errno == EINTR)
continue;
fprintf(stderr, "Error waiting for child to exit: %s.\n", strerror(errno));
status = EXIT_FAILURE;
break;
}
if (r == p) {
if (WIFEXITED(status)) {
if (WEXITSTATUS(status))
fprintf(stderr, "Command failed [%d]\n", WEXITSTATUS(status));
else
fprintf(stderr, "Command succeeded [0]\n");
} else
if (WIFSIGNALED(status))
fprintf(stderr, "Command exited due to signal %d (%s)\n", WTERMSIG(status), strsignal(WTERMSIG(status)));
else
fprintf(stderr, "Command process died from unknown causes!\n");
break;
}
}
return status;
}
使用例如编译它。
gcc -Wall -O2 example.c -o example
并且可以通过例如以下方式运行。
./example sqlite3
你会注意到,即使你直接运行 sqlite3
,Ctrl+C 也不会中断它;相反,你只会在屏幕上看到 ^C
。这是因为 sqlite3
设置了终端,使得 Ctrl+C 不会引起信号,而只是被视为普通输入。
你可以使用 .quit
命令退出 sqlite3
,或在行首按下 Ctrl+D。
你会看到原始程序在返回命令行之前会输出一个 Command ... []
行。因此,父进程不会被信号杀死/损坏/打扰。
你可以使用 ps f
查看终端进程树,并找出父进程和子进程的 PID,然后向任意一个发送信号以观察发生了什么。
请注意,由于无法捕获、阻止或忽略 SIGSTOP
信号,因此反映作业控制信号(例如当你使用 Ctrl+Z 时)可能不那么简单。对于正确的作业控制,父进程需要设置新会话和新进程组,并暂时与终端分离。这也是完全可能的,但有点超出本文的范围,因为它涉及到会话、进程组和终端的详细行为。
让我们解构上面的示例程序。
该示例程序首先安装了一些信号反射器,然后 fork 了一个子进程,该子进程执行命令 sqlite3
。(你可以指定任何可执行文件和任何参数字符串给程序。)
internal_child_pid
变量、set_child_pid()
和 get_child_pid()
函数用于原子地管理子进程。__atomic_store_n()
和 __atomic_load_n()
是编译器提供的内建函数;对于 GCC,请参见此处以获取详细信息。它们避免了在只部分分配子进程 PID 时发生信号的问题。在某些常见架构中,这种情况不会发生,但这是一个谨慎的示例,因此使用原子访问来确保只有完整的(旧的或新的)值才能被看到。如果在过渡期间暂时阻止相关信号,我们可以完全避免使用这些。再次强调,我决定使用原子访问更简单,并且在实践中可能会有趣。
forward_handler()
函数原子地获取子进程PID,然后验证其是否非零(以确保我们有一个子进程),并且不会转发由子进程发送的信号(仅确保我们不会造成信号风暴,两个进程互相轰炸对方的信号)。siginfo_t
结构中的各个字段在man 2 sigaction
man页面中列出。
forward_signal()
函数为指定的信号signum
安装上述处理程序。请注意,我们首先使用memset()
将整个结构清除为零。以这种方式清除它可以确保未来兼容性,如果结构中的某些填充被转换为数据字段。
struct sigaction
中的.sa_mask
字段是一个无序的信号集。在执行信号处理程序的线程中,屏蔽了掩码中设置的信号的传递。(对于上面的示例程序,我们可以安全地说,在运行信号处理程序时这些信号被阻止;只是在多线程程序中,这些信号仅在用于运行处理程序的特定线程中被阻止。)
重要的是要使用sigemptyset(&act.sa_mask)
清除信号掩码。仅将结构设置为零是不够的,即使它在许多机器上实践中可能有效。(我不知道,我甚至没有检查过。我更喜欢强健和可靠,而不是懒惰和脆弱!)
使用了SA_SIGINFO
标志,因为处理程序使用三参数形式(并使用siginfo_t
的si_pid
字段)。只有在OP希望使用它时才会存在SA_RESTART
标志;这仅意味着如果可能,C库和内核会尝试避免返回errno == EINTR
错误,如果一个信号被传递使用当前在系统调用(如wait()
)中阻塞的线程。您可以删除SA_RESTART
标志,并在父进程的循环中的适当位置添加一个调试fprintf(stderr, "Hey!\n");
以查看然后会发生什么。
如果没有错误,则sigaction()
函数将返回0,否则将返回-1,并设置errno
。如果成功分配forward_handler
,则forward_signal()
函数返回0,否则返回非零的错误号。有些人不喜欢这种返回值(他们更喜欢仅返回错误的-1,而不是errno
值本身),但出于某种不合理的原因,我已经喜欢上了这种习惯用法。如果您愿意,请更改它。
现在我们来到了main()
函数。
如果你没有输入任何参数或者只输入一个-h
或--help
参数运行程序,它将会输出一个使用摘要信息。再次强调,这样做只是我喜欢的一种方式--getopt()
和getopt_long()
更常用于解析命令行选项。对于这种简单的程序,我只是硬编码实现了参数检查。
在这种情况下,我故意让使用说明非常简短。添加一个关于程序具体功能的段落会更好。这些类型的文本--尤其是代码中的注释(解释代码应该做什么的意图,而不是描述代码实际做了什么)--非常重要。离我第一次写代码拿到报酬已经过去超过20年了,我仍然在学习如何更好地注释--描述--我的代码,所以我认为越早开始这样做就越好。
fork()
部分应该很熟悉了。如果它返回-1
,则说明fork失败(可能是由于限制或类似的原因),现在最好打印出errno
消息。返回值将在子进程中为0
,在父进程中为子进程的进程ID。
execlp()
函数有两个参数:要执行的二进制文件名称(指定在PATH环境变量中的目录将用于查找这样的二进制文件),以及一个指向该二进制文件参数的指针数组。第一个参数将是新二进制文件中的argv[0]
,也就是命令本身的名称。
execlp(argv[1], argv + 1);
调用实际上很容易解析,如果你将它与上面的描述进行比较。 argv[1]
命名要执行的二进制文件。 argv+1
基本上等同于(char **)(&argv[1])
,即一个从argv[1]
开始而不是argv[0]
的指针数组。再次强调,我只是喜欢execlp(argv[n], argv + n)
的习惯用法,因为它允许我们在不必担心解析命令行或通过Shell执行它(有时是绝对不希望的)的情况下,执行在命令行中指定的另一个命令。
man 7 signal
页面解释了在 fork()
和 exec()
中会发生什么事情。简单来说,在 fork()
中信号处理程序被继承,但在 exec()
中重置为默认值,这正好是我们想要的。
如果我们首先进行 fork ,然后再安装信号处理程序,那么在子进程存在但父进程仍具有默认处理程序(大多数是终止)的情况下,就会出现一个窗口。
相反,我们可以在 fork 前在父进程中使用例如 sigprocmask()
来阻塞这些信号。阻塞信号意味着它被“等待”;直到信号被解除阻塞之前,它将不会被发送。在子进程中,信号可以保持阻塞,因为通过 exec()
信号处理程序会被重置为默认值。在父进程中,我们可以在 fork 前或后安装信号处理程序,最后解除信号的阻塞。这样做的好处在于,我们既不需要原子机制,也不需要检查子进程的 pid 是否为零,因为在任何信号被发送之前,子进程的 pid 就会被设置为其实际值!
while
循环基本上只是一个围绕 waitpid()
调用的循环,直到我们启动的确切子进程退出,或者发生一些有趣的事情(子进程以某种方式消失)。这个循环包含相当仔细的错误检查,以及正确的 EINTR
处理,如果信号处理程序是在没有使用 SA_RESTART
标志的情况下安装的。
如果我们 fork 的子进程退出了,我们将检查它的退出状态和/或死亡原因,并向标准错误打印诊断消息。
最后,程序以可怕的 hack 结束:我们不返回 EXIT_SUCCESS
或 EXIT_FAILURE
,而是返回我们在子进程退出时使用 waitpid 获得的整个状态字。我保留了这个,是因为在实践中有时会使用它,当您希望返回与子进程返回的相同或类似的退出状态代码时。所以,这是为了说明问题。如果您曾经发现自己处于需要使程序返回与 fork 和执行的子进程相同的退出状态的情况下,这仍然比设置机制让进程自杀更好,使用相同信号杀死子进程。只需在那里放置一个突出的注释,并在安装说明中添加一条注释,以便那些在可能不需要的体系结构上编译程序的人可以进行修复。
sqlite3
不会阻塞HUP
、TERM
、QUIT
、USR1
或USR2
信号;而且它似乎处理或忽略了INT
信号(如果在其他地方通过kill
发送,则在终端上显示为^C
)。请注意,您始终可以使用例如kill -HUP $(ps -C sqlite3 -o pid=)
向所有正在运行的sqlite3
进程发送HUP信号;这就是我用于测试的方法。 - Nominal Animal