如何等待非子进程的退出

45

对于子进程,可以使用wait()waitpid()函数来暂停当前进程的执行,直到子进程退出。但是这些函数不能用于非子进程。

是否有另一个函数可以等待任何进程的退出?

14个回答

37

没有与wait()相当的操作。通常的做法是使用kill(pid, 0)进行轮询,并查看返回值为-1和errnoESRCH来表示进程已经退出。

更新:自Linux内核5.3以来,引入了pidfd_open系统调用,它可以为给定的pid创建文件描述符,可以轮询该描述符获取pid退出时的通知。


2
这样的忙等循环是否可以接受? - CsTamas
1
好的,你不想让它太忙;在每个没有找到进程的kill()之后,你应该使用usleep()暂停一段时间。然后你必须在轮询的繁忙程度和进程消失前需要多长时间才能注意到之间取得平衡。 - chaos
哦,看来我没注意到的时候,usleep()已经过时了。现在似乎应该使用nanosleep()代替它。 - chaos
2
@Sam Hocevar:关于竞态条件的内容以及如何在没有它的情况下完成此操作,都没有提到。这并没有真正帮助到我。 - chaos
10
@chaos: 没有任何保证kill(pid,0)会向你感兴趣的进程发送信号。在你调用nanosleep期间,它可能已经死亡并被另一个正在运行的进程替代。我不需要更详细地解释了:已经提出了三个好建议(FIFO,信号量和ptrace方法,在我看来,尽管非常特定于平台,但优于其他所有方法)。 - sam hocevar
显示剩余3条评论

16
到目前为止,在Linux上有三种方法可以实现此操作:
  • 轮询:每隔一段时间检查进程是否存在,可以使用kill命令或测试/proc/$pid的存在与否,如其他答案中所述。
  • 使用ptrace系统调用,像调试器一样附加到该进程,以便在其退出时得到通知,如a3nm的回答中所述。
  • 使用netlink接口监听PROC_EVENT_EXIT消息 - 这样内核会在每个进程退出时告知您的程序,并等待正确的进程ID。我只在一个网站上看过这种描述。
无耻的广告:我正在开发一个程序(当然是开源的;GPLv2),可执行以上三种方法之一。

15

在BSD和OS X上,您可以使用带有EVFILT_PROC+NOTE_EXIT的kqueue来完美实现此功能,无需轮询。不幸的是,Linux没有类似的功能。


8
Linux应该感到羞耻,因为他们没有移植kqueue。 - Lothar

8
您也可以创建一个套接字或FIFO并在它们上读取。 FIFO特别简单:将子进程的标准输出连接到FIFO并读取。读取将阻塞,直到子进程退出(出于任何原因)或发出某些数据为止。因此,您需要一个小循环来丢弃不需要的文本数据。
如果您可以访问子进程的源代码,请在启动时打开FIFO进行写入,然后简单地忘记它。当子进程终止时,操作系统将清除打开的文件描述符,您等待的“父”进程将唤醒。
现在,这可能是一个您没有启动或拥有的进程。在这种情况下,您可以使用脚本替换二进制可执行文件,该脚本启动真正的二进制文件,但还添加了如上所述的监视功能。

1
不是一个孩子,特别是它可能没有考虑到这种跟踪,并且无法修改源代码。 - Lothar
@Lothar 我认为展示一些非显而易见的解决方案是很好的,特别是因为被接受的答案不可靠。此外,任何进程都可以以某种方式变成子进程。例如,您可以用一个脚本替换二进制文件,监视原始二进制文件并在现在的子进程死亡时发送信号。 - Aaron Digulla

5
这里有一种在Linux中等待任何进程退出(或被杀死)的方法(不一定是子进程),而无需轮询:
使用inotify等待/proc 'pid'文件夹被删除将是完美的解决方案,但不幸的是inotify不能与像/proc这样的伪文件系统一起工作。 但是,我们可以将其与进程的可执行文件一起使用。只要进程仍然存在,该文件就会保持打开状态。因此,我们可以使用带有IN_CLOSE_NOWRITE选项的inotify来阻塞,直到文件关闭。当然,它也可能因其他原因关闭(例如,如果另一个具有相同可执行文件的进程退出),因此我们必须通过其他方式过滤这些事件。
我们可以使用kill(pid,0),但它不能保证是否仍然是同一进程。如果我们对此真的很谨慎,我们可以做些其他事情。
下面是一种应该100%安全防止pid重用问题的方法:我们打开伪目录/proc/'pid',并保持打开状态,直到我们完成。如果同时创建了一个具有相同pid的新进程,则我们持有的目录文件描述符仍将引用原始进程(或者如果旧进程停止存在则变为无效),但永远不会引用重新使用pid的新进程。然后,我们可以检查原始进程是否仍然存在,例如通过检查openat()在目录中是否存在名为“cmdline”的文件。当进程退出或被杀死时,这些伪文件也将停止存在,因此openat()将失败。
以下是示例代码:
// return -1 on error, or 0 if everything went well
int wait_for_pid(int pid)
{
    char path[32];
    int in_fd = inotify_init();
    sprintf(path, "/proc/%i/exe", pid);
    if (inotify_add_watch(in_fd, path, IN_CLOSE_NOWRITE) < 0) {
        close(in_fd);
        return -1;
    }
    sprintf(path, "/proc/%i", pid);
    int dir_fd = open(path, 0);
    if (dir_fd < 0) {
        close(in_fd);
        return -1;
    }

    int res = 0;
    while (1) {
        struct inotify_event event;
        if (read(in_fd, &event, sizeof(event)) < 0) {
            res = -1;
            break;
        }
        int f = openat(dir_fd, "fd", 0);
        if (f < 0) break;
        close(f);
    }

    close(dir_fd);
    close(in_fd);
    return res;
}

有一个bug!由于/proc/PID/exe通常指向文件系统上的脚本或可执行文件,任何其他进程访问相同的文件都会触发inotifywait!!请查看我的答案 - F. Hauri - Give Up GitHub

3
您可以使用ptrace(2)附加到进程。从shell中,strace -p PID >/dev/null 2>&1似乎可行。这样���以避免繁忙等待,虽然会减慢被跟踪的进程,并且不适用于所有进程(仅适用于您自己的进程,比仅适用于子进程好一点)。

1
知识永远不会伤害,但对于shell来说,我建议使用“标准”的方式,定期轮询;请参阅问题1058047。尽管这可能是一种罕见的情况,但strace可能会造成忙循环。例如$ (read) &; strace -p $!。请注意,(read)&本身是无害的。 - teika kazura

1

也许可以等待/proc/[pid]或/proc/[pid]/[something]消失?

有poll()和其他文件事件等待函数,也许可以帮助解决问题?


是的,这是个好主意。除非相同的进程ID被如此快地重用 - 但这种情况可能很少发生。 - CsTamas
@CsTamas,这里有一种保护机制,即进程标识符的数量(32768)远大于可以运行的进程数量。因此,除非你睡了一段时间,否则获得相同的进程标识符的可能性非常低。 - Alexis Wilke

1

PR_SET_PDEATHSIG 可以用于等待父进程终止


1

我不知道还有其他的解决方案。除了混沌的解决方案,如果你可以更改要等待的程序,你可以使用信号量。

库函数包括sem_open(3)sem_init(3)sem_wait(3)...

sem_wait(3)执行等待操作,因此你不必像混沌的解决方案那样进行忙等待。当然,使用信号量会使你的程序更加复杂,这可能不值得麻烦。


这些信号量几乎没有用处,即使没有进程打开它们,它们仍然存在。我记得定期调用ipcrm来清理一些崩溃进程的残留物。 - user678269

1
自Linux内核5.3以来,有一个pidfd_open系统调用,它为给定的pid创建一个fd,可以轮询该fd以获取pid退出时的通知。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接