近两天做实验,需要在Linux下编写一个反复调用其他程序(application)并等待的程序(invoker)。出现了一个有意思的问题,调了两天终于把这个事情解决了。特此记录一下。
这个程序想要实现的功能是:invoker循环带arguments调用application,并为application设定一个超时时间,如10秒。10秒钟到后,强制结束application并重新开始下一次调用。
原始程序的主要代码精简后如下:
static void init_signal() {
sigemptyset (&sig_mask);
sigaddset (&sig_mask, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &sig_mask, NULL) < 0) PFATAL("sigprocmask() failed");
}
static void run_target(char* argv[]) {
pid_t pid;
pid = fork();
if (pid < 0) PFATAL("fork() failed");
if (!pid) { // Child process
execv(argv[0], argv);
PFATAL("execv() returned");
}
// Parent process
do {
if (sigtimedwait(&sig_mask, NULL, &timeout) < 0) {
// Interrupted by a signal other than SIGCHLD.
if (errno == EINTR) continue;
// Timeout, kill child.
else if (errno == EAGAIN) {
if (kill (pid, SIGKILL) == -1)
PFATAL("kill() failed");
}
else PFATAL ("sigtimedwait()");
}
break;
} while (1);
if (waitpid(pid, NULL, 0) <= 0)
PFATAL("waitpid() failed");
}
本程序的实现主要使用了sigtimedwait系统调用。它的作用是给定一个时长,在给定时长内接收sig_mask
所代表的信号,并返回信号ID。如果超时没接收到信号,返回值小于0并且标记errno
为EAGAIN
。如果被其他信号打断,标记errno
为EINTR
。
那么上面的实现大概为:用fork+execve调用目标application。parent调用sigtimedwait进行等待。如果程序十秒内结束就立即退出循环,否则调用kill函数杀掉目标进程。然后再调用waitpid回收进程。看起来似乎没啥毛病。
然而在反复的目标application执行过程中,我发现,application超时之后竟然没被杀死。之后我做了几个testcase来复现了一下,并用strace进行跟踪,跟踪结果精简后如下:
rt_sigprocmask(SIG_BLOCK, [CHLD], NULL, 8) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f910f1e4810) = 569769
rt_sigtimedwait([CHLD], NULL, {tv_sec=10, tv_nsec=0}, 8) = -1 EAGAIN (Resource temporarily unavailable)
kill(569769, SIGKILL) = 0
wait4(569769, NULL, 0, NULL) = 569769
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f910f1e4810) = 574383
rt_sigtimedwait([CHLD], NULL, {tv_sec=10, tv_nsec=0}, 8) = 17 (SIGCHLD)
wait4(574383,
然后卡住了。看完这个strace之后我百思不得其解,为什么明明569769都被kill了之后,还能产生一个SIGCHLD信号。
后来在做了大量测试之后,突然醒悟:正常结束的进程会产生SIGCHLD信号,被kill的进程也会产生。对于timeout的程序,SIGCHLD没有被第19行的sigtimedwait所捕获。所以留到了下一次执行第19行时再处理。如果恰好这个程序也timeout,那么就会一直卡在第32行的wait。
因此解决方案是,在kill之后再捕获一次SIGCHLD。
更正版的源代码如下:
static void init_signal() {
sigemptyset (&sig_mask);
sigaddset (&sig_mask, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &sig_mask, NULL) < 0) PFATAL("sigprocmask() failed");
}
static void run_target(char* argv[]) {
pid_t pid;
pid = fork();
if (pid < 0) PFATAL("fork() failed");
if (!pid) { // Child process
execv(argv[0], argv);
PFATAL("execv() returned");
}
// Parent process
do {
if (sigtimedwait(&sig_mask, NULL, &timeout) < 0) {
// Interrupted by a signal other than SIGCHLD.
if (errno == EINTR) continue;
// Timeout, kill child.
else if (errno == EAGAIN) {
if (kill (pid, SIGKILL) == -1)
PFATAL("kill() failed");
/* The kill of child also incurs a pending SIGCHLD signal of parent process,
* so we must capture it or the signal will affect the next timeout capture,
* the program will get stuck. */
sigtimedwait(&sig_mask, NULL, &timeout);
}
else PFATAL ("sigtimedwait()");
}
break;
} while (1);
if (waitpid(pid, NULL, 0) <= 0)
PFATAL("waitpid() failed");
}
新的SIGCHLD捕获代码加在了第29行。
值得注意的是,我曾经尝试用usleep(小秒数)+sigtimedwait(timeout为0)的方式去捕获。这样做是行不通的,照样会产生阻塞。而usleep时间高一点做捕获就没有问题(例如10秒),但我不能采用这种方式,因为太影响效率了。
我猜测出现这种情况的原因是:如果usleep采用小秒数的话,sigtimedwait从kill返回+sleep+sigtimedwait检查signal是否pending的时间,要少于kernel kill之后往task_struct里塞信号的时间。这会导致程序捕获不到SIGCHLD,相当于加的没用。
而直接使用sigtimedwait(有timeout)的方式则会在内核态里面阻塞,这样就不用再担心usleep的时间长短问题了。在这里面我的timeout设的是10秒。kernel从kill到塞信号的时间肯定不会长于10秒,所以这样做是没问题的。
参考资料:
[1] https://stackoverflow.com/a/20173592
可能另一种做法会简单一点:
sigaction(SIGALRM);
alarm(10);
waitpid();
alarm(0);
if (errno == EINTR) {
kill(9);
waitpid();
}
这样完全不和 SIGCHLD 发生关系, 只用 wait 来阻塞, 可以免去不必要的麻烦.
感谢指点!