Linux系统编程之sigtimedwait使用问题记录

近两天做实验,需要在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并且标记errnoEAGAIN。如果被其他信号打断,标记errnoEINTR

那么上面的实现大概为:用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

Linux系统编程之sigtimedwait使用问题记录》有2个想法

  1. gray

    可能另一种做法会简单一点:

    sigaction(SIGALRM);
    alarm(10);
    waitpid();
    alarm(0);
    if (errno == EINTR) {
    kill(9);
    waitpid();
    }

    这样完全不和 SIGCHLD 发生关系, 只用 wait 来阻塞, 可以免去不必要的麻烦.

发表评论

您的电子邮箱地址不会被公开。