想象一下,我有一个启动多个子进程的进程。父母需要知道孩子什么时候退出。
我可以使用waitpid
,但是如果/当父需要退出时,我无法告诉waitpid
中被阻止的线程正常退出并加入它。让事情自己清理是件好事,但这可能不是什么大不了的事。
我可以将waitpid
与WNOHANG
一起使用,然后休眠一段时间以防止忙碌等待。然而,我只能知道一个孩子是否经常退出。在我的情况下,我知道一个孩子何时立即离开可能并不是非常关键,但我想尽快知道......
我可以使用SIGCHLD
的信号处理程序,并在信号处理程序中执行当子项退出时要执行的任何操作,或者将消息发送到其他线程以执行某些操作。但是使用信号处理程序会稍微混淆代码流。
我真正想做的是在一些超时时使用waitpid
,比如5秒。由于退出进程不是一个时间关键的操作,我可以懒惰地通知线程退出,同时在waitpid
其余时间仍然阻塞它,随时准备做出反应。 在linux中有这样的调用吗?在其他选择中,哪一个最好?
编辑:
基于回复的另一种方法是在SIGCHLD
\ pthread
的所有主题中阻止_sigmask()
。然后在一个帖子中,在寻找sigtimedwait()
的同时继续致电SIGCHLD
。这意味着我可以超时调用并检查线程是否应该退出,如果没有,则保持阻塞状态等待信号。一旦SIGCHLD
被传递到该线程,我们就可以立即对它做出反应,并且在等待线程中,而不使用信号处理程序。
答案 0 :(得分:41)
请勿将alarm()
与wait()
混合使用。您可以通过这种方式丢失错误信息。
使用自管技巧。这会将任何信号转换为select()
能够发生的事件:
int selfpipe[2];
void selfpipe_sigh(int n)
{
int save_errno = errno;
(void)write(selfpipe[1], "",1);
errno = save_errno;
}
void selfpipe_setup(void)
{
static struct sigaction act;
if (pipe(selfpipe) == -1) { abort(); }
fcntl(selfpipe[0],F_SETFL,fcntl(selfpipe[0],F_GETFL)|O_NONBLOCK);
fcntl(selfpipe[1],F_SETFL,fcntl(selfpipe[1],F_GETFL)|O_NONBLOCK);
memset(&act, 0, sizeof(act));
act.sa_handler = selfpipe_sigh;
sigaction(SIGCHLD, &act, NULL);
}
然后,类似waitpid的函数如下所示:
int selfpipe_waitpid(void)
{
static char dummy[4096];
fd_set rfds;
struct timeval tv;
int died = 0, st;
tv.tv_sec = 5;
tv.tv_usec = 0;
FD_ZERO(&rfds);
FD_SET(selfpipe[0], &rfds);
if (select(selfpipe[0]+1, &rfds, NULL, NULL, &tv) > 0) {
while (read(selfpipe[0],dummy,sizeof(dummy)) > 0);
while (waitpid(-1, &st, WNOHANG) != -1) died++;
}
return died;
}
您可以在selfpipe_waitpid()
中看到如何控制超时,甚至可以与其他基于select()
的IO混合使用。
答案 1 :(得分:29)
分叉一个中间子,它分叉真正的子进程和超时进程并等待其所有(两个)子进程。当一个人退出时,它会杀死另一个并退出。
pid_t intermediate_pid = fork();
if (intermediate_pid == 0) {
pid_t worker_pid = fork();
if (worker_pid == 0) {
do_work();
_exit(0);
}
pid_t timeout_pid = fork();
if (timeout_pid == 0) {
sleep(timeout_time);
_exit(0);
}
pid_t exited_pid = wait(NULL);
if (exited_pid == worker_pid) {
kill(timeout_pid, SIGKILL);
} else {
kill(worker_pid, SIGKILL); // Or something less violent if you prefer
}
wait(NULL); // Collect the other process
_exit(0); // Or some more informative status
}
waitpid(intermediate_pid, 0, 0);
非常简单:)
如果您确定程序中没有其他模块正在自行填充子进程,您甚至可以省略中间孩子。
答案 2 :(得分:15)
这是一个有趣的问题。 我发现sigtimedwait可以做到。
编辑2016/08/29: 感谢Mark Edington的建议。我在Ubuntu 16.04上测试了你的例子,它按预期工作。
注意:这仅适用于子进程。遗憾的是,Linux / Unix中似乎没有Window的WaitForSingleObject(unrelated_process_handle,timeout)的等效方式,以便在超时时通知无关的进程终止。
好的,Mark Edington的示例代码是here:
/* The program creates a child process and waits for it to finish. If a timeout
* elapses the child is killed. Waiting is done using sigtimedwait(). Race
* condition is avoided by blocking the SIGCHLD signal before fork().
*/
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
static pid_t fork_child (void)
{
int p = fork ();
if (p == -1) {
perror ("fork");
exit (1);
}
if (p == 0) {
puts ("child: sleeping...");
sleep (10);
puts ("child: exiting");
exit (0);
}
return p;
}
int main (int argc, char *argv[])
{
sigset_t mask;
sigset_t orig_mask;
struct timespec timeout;
pid_t pid;
sigemptyset (&mask);
sigaddset (&mask, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &mask, &orig_mask) < 0) {
perror ("sigprocmask");
return 1;
}
pid = fork_child ();
timeout.tv_sec = 5;
timeout.tv_nsec = 0;
do {
if (sigtimedwait(&mask, NULL, &timeout) < 0) {
if (errno == EINTR) {
/* Interrupted by a signal other than SIGCHLD. */
continue;
}
else if (errno == EAGAIN) {
printf ("Timeout, killing child\n");
kill (pid, SIGKILL);
}
else {
perror ("sigtimedwait");
return 1;
}
}
break;
} while (1);
if (waitpid(pid, NULL, 0) < 0) {
perror ("waitpid");
return 1;
}
return 0;
}
答案 3 :(得分:6)
该功能可以通过信号中断,因此您可以在调用waitpid()之前设置定时器,并在定时器信号被提升时以EINTR退出。编辑:它应该像调用waitpid()之前调用alarm(5)一样简单。
答案 4 :(得分:2)
如果您要使用信号(根据Steve的建议),您可以在想要退出时手动发送信号。这将导致waitpid返回EINTR,然后线程可以退出。无需定期报警/重启。
答案 5 :(得分:2)
我认为select
会在孩子发出EINTR
信号时返回SIGCHLD
。
我相信这应该有效:
while(1)
{
int retval = select(0, NULL, NULL, NULL, &tv, &mask);
if (retval == -1 && errno == EINTR) // some signal
{
pid_t pid = (waitpid(-1, &st, WNOHANG) == 0);
if (pid != 0) // some child signaled
}
else if (retval == 0)
{
// timeout
break;
}
else // error
}
注意:您可以使用pselect
覆盖当前sigmask
并避免中断不需要的信号。
答案 6 :(得分:2)
由于情况,我绝对需要在主线程中运行,并且使用自管道技巧或eventfd并不是很简单,因为我的epoll循环在另一个线程中运行。所以我通过将其他堆栈溢出处理程序整理在一起来想出这个。请注意,通常以其他方式执行此操作会更安全,但这很简单。如果有人关心它是如何真的非常糟糕那么我就是全部耳朵。
注意:绝对有必要阻止任何线程中的信号处理,除了你要运行它的那个。我默认这样做,因为我认为处理随机线程中的信号很麻烦
static void ctlWaitPidTimeout(pid_t child, useconds_t usec, int *timedOut) {
int rc = -1;
static pthread_mutex_t alarmMutex = PTHREAD_MUTEX_INITIALIZER;
TRACE("ctlWaitPidTimeout: waiting on %lu\n", (unsigned long) child);
/**
* paranoid, in case this was called twice in a row by different
* threads, which could quickly turn very messy.
*/
pthread_mutex_lock(&alarmMutex);
/* set the alarm handler */
struct sigaction alarmSigaction;
struct sigaction oldSigaction;
sigemptyset(&alarmSigaction.sa_mask);
alarmSigaction.sa_flags = 0;
alarmSigaction.sa_handler = ctlAlarmSignalHandler;
sigaction(SIGALRM, &alarmSigaction, &oldSigaction);
/* set alarm, because no alarm is fired when the first argument is 0, 1 is used instead */
ualarm((usec == 0) ? 1 : usec, 0);
/* wait for the child we just killed */
rc = waitpid(child, NULL, 0);
/* if errno == EINTR, the alarm went off, set timedOut to true */
*timedOut = (rc == -1 && errno == EINTR);
/* in case we did not time out, unset the current alarm so it doesn't bother us later */
ualarm(0, 0);
/* restore old signal action */
sigaction(SIGALRM, &oldSigaction, NULL);
pthread_mutex_unlock(&alarmMutex);
TRACE("ctlWaitPidTimeout: timeout wait done, rc = %d, error = '%s'\n", rc, (rc == -1) ? strerror(errno) : "none");
}
static void ctlAlarmSignalHandler(int s) {
TRACE("ctlAlarmSignalHandler: alarm occured, %d\n", s);
}
编辑:我已经过渡到使用一个与我现有的基于epoll()的eventloop很好地集成的解决方案,使用timerfd。因为我正在使用epoll,所以我并没有真正失去任何平台独立性,而且我获得了额外的睡眠,因为我知道多线程和UNIX信号的不合理组合不会再次伤害我的程序。
答案 7 :(得分:1)
我可以为SIGCHLD使用信号处理程序,并在信号处理程序中执行当子项退出时要执行的任何操作,或者将消息发送到其他线程以执行某些操作。但是使用信号处理程序会稍微混淆代码流。
为了避免竞争条件,你应该避免做一些比改变信号处理程序中的volatile标志更复杂的事情。
我认为您的最佳选择是向父母发送信号。 waitpid()然后将errno设置为EINTR并返回。此时,您检查waitpid返回值和errno,注意您已收到信号并采取适当的措施。
答案 8 :(得分:0)
您可以直接使用SIGCHLD调用sigtimedwait(),而不是直接调用waitpid(),它会在子项退出后发送到父进程),然后等待它被传递到当前线程,就像函数名建议的那样:支持timeout参数。
请检查以下代码段以获取详细信息
static bool waitpid_with_timeout(pid_t pid, int timeout_ms, int* status) {
sigset_t child_mask, old_mask;
sigemptyset(&child_mask);
sigaddset(&child_mask, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &child_mask, &old_mask) == -1) {
printf("*** sigprocmask failed: %s\n", strerror(errno));
return false;
}
timespec ts;
ts.tv_sec = MSEC_TO_SEC(timeout_ms);
ts.tv_nsec = (timeout_ms % 1000) * 1000000;
int ret = TEMP_FAILURE_RETRY(sigtimedwait(&child_mask, NULL, &ts));
int saved_errno = errno;
// Set the signals back the way they were.
if (sigprocmask(SIG_SETMASK, &old_mask, NULL) == -1) {
printf("*** sigprocmask failed: %s\n", strerror(errno));
if (ret == 0) {
return false;
}
}
if (ret == -1) {
errno = saved_errno;
if (errno == EAGAIN) {
errno = ETIMEDOUT;
} else {
printf("*** sigtimedwait failed: %s\n", strerror(errno));
}
return false;
}
pid_t child_pid = waitpid(pid, status, WNOHANG);
if (child_pid != pid) {
if (child_pid != -1) {
printf("*** Waiting for pid %d, got pid %d instead\n", pid, child_pid);
} else {
printf("*** waitpid failed: %s\n", strerror(errno));
}
return false;
}
return true;
}
答案 9 :(得分:0)
如果您的程序仅在现代Linux内核(5.3或更高版本)上运行,则首选方法是使用pidfd_open
(https://lwn.net/Articles/789023/ https://man7.org/linux/man-pages/man2/pidfd_open.2.html)。
此系统调用返回代表进程的文件描述符,然后您可以select
,poll
或epoll
进行操作,就像等待其他类型的文件描述符一样。< / p>
例如,
int fd = pidfd_open(pid, 0);
struct pollfd pfd = {fd, POLLIN, 0};
poll(&pfd, 1, 1000) == 1;