2013-07-09 126 views
5

我有一个多线程的应用程序,安装一个处理程序SIGCHLD该日志和收割子进程。
我看到的问题开始于我拨打system()的电话时。 system()需要等待子进程结束并收获,因为它需要退出代码。这就是为什么它调用sigprocmask()来阻止SIGCHLD。但是在我的多线程应用程序中,SIGCHLD仍然在不同的线程中调用,并且在system()有机会这样做之前收获孩子。Linux操作系统:系统()+ SIGCHLD处理多线程+

这是在POSIX一个已知的问题?解决此我想到了

一种方法是阻止SIGCHLD在所有其他线程,但是这不是我的情况真的很现实,因为不直接由我的代码创建的所有线程。
我还有什么其他选择?

+0

不应该system()阻止SIGCHLD吗?而你的意思是“SIGCHLD在不同的线程中调用”? – LostBoy

+0

也许你可以使用一个库(Glib,Pocolib,Boost,QtCore等),它比'system(3)'具有更好的进程控制能力。 –

+1

@LostBoy'system()'确实阻塞了SIGCHLD,但仅限于调用线程。它使用'sigprocmask()',它被记录为“一个进程中的每个线程都有自己的信号掩码。” – shoosh

回答

3

是的,这是一个众所周知的(或至少是强烈暗示)的问题。

在等待孩子终止时阻塞SIGCHLD可防止应用程序在系统()获取状态之前从系统()的子进程捕获信号并获取状态。 .... 注意,如果应用程序捕获SIGCHLD信号,它将一个成功的系统()调用返回前收到这样的信号。

(从system()的文件,加上强调)。

所以,POSIXly你的运气了,除非你实现恰好排队SIGCHLD。如果是这样,你当然可以记录你分叉的pid,然后只收获你期望的pid。

Linuxly也一样,你的运气,因为signalfd appears also to collapse multiple SIGCHLDs

然而,UNIXly有许多聪明和太聪明的技术可用来管理自己的孩子,并忽略第三方例程。继承管道的I/O复用是SIGCHLD捕获的一种替代方法,就像使用一个小型的专用“产卵助手”在独立进程中进行分叉和收获一样。

+1

我也遇到过这个问题。在多线程库中使用UNIX信号处理是正确的,这应该不会干扰其他代码。信号处理以及使用进程ID而不是进程句柄是UNIX不可修复的两个重要方面。 – Lothar

3

由于您有无法控制的线程,我建议您编写一个预加载的库来调用system()调用(也可能是popen()等)与您自己的实现。我也会在库中包含你的SIGCHLD处理程序。

如果你不想通过env LD_PRELOAD=libwhatever.so yourprogram运行您的程序,你可以像

const char *libs; 

libs = getenv("LD_PRELOAD"); 
if (!libs || !*libs) { 
    setenv("LD_PRELOAD", "libwhatever.so", 1); 
    execv(argv[0], argv); 
    _exit(127); 
} 

在程序的开始添加的东西,有一个与LD_PRELOAD正确设置它重新执​​行本身。 (请注意,有怪癖考虑,如果你的程序是setuid或setgid;参见man ld.so对细节特别是,如果libwhatever.so没有安装在系统库目录,你必须指定一个完整路径。)

一个可能的方法可能会使用待定子项的无锁数组(由C编译器提供的原子内置函数)。而不是waitpid(),您的system()实施分配其中一个条目,将子PID保留在那里,然后等待子信号退出,而不是调用waitpid()

下面是一个示例实现:

#define _GNU_SOURCE 
#define _POSIX_C_SOURCE 200809L 
#include <stdlib.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <sys/socket.h> 
#include <sys/wait.h> 
#include <fcntl.h> 
#include <signal.h> 
#include <semaphore.h> 
#include <dlfcn.h> 
#include <errno.h> 

/* Maximum number of concurrent children waited for. 
*/ 
#define MAX_CHILDS 256 

/* Lockless array of child processes waited for. 
*/ 
static pid_t child_pid[MAX_CHILDS] = { 0 }; /* 0 is not a valid PID */ 
static sem_t child_sem[MAX_CHILDS]; 
static int child_status[MAX_CHILDS]; 

/* Helper function: allocate a child process. 
* Returns the index, or -1 if all in use. 
*/ 
static inline int child_get(const pid_t pid) 
{ 
    int i = MAX_CHILDS; 
    while (i-->0) 
     if (__sync_bool_compare_and_swap(&child_pid[i], (pid_t)0, pid)) { 
      sem_init(&child_sem[i], 0, 0); 
      return i; 
     } 
    return -1; 
} 

/* Helper function: release a child descriptor. 
*/ 
static inline void child_put(const int i) 
{ 
    sem_destroy(&child_sem[i]); 
    __sync_fetch_and_and(&child_pid[i], (pid_t)0); 
} 

/* SIGCHLD signal handler. 
* Note: Both waitpid() and sem_post() are async-signal safe. 
*/ 
static void sigchld_handler(int signum __attribute__((unused)), 
          siginfo_t *info __attribute__((unused)), 
          void *context __attribute__((unused))) 
{ 
    pid_t p; 
    int status, i; 

    while (1) { 
     p = waitpid((pid_t)-1, &status, WNOHANG); 
     if (p == (pid_t)0 || p == (pid_t)-1) 
      break; 

     i = MAX_CHILDS; 
     while (i-->0) 
      if (p == __sync_fetch_and_or(&child_pid[i], (pid_t)0)) { 
       child_status[i] = status; 
       sem_post(&child_sem[i]); 
       break; 
      } 

     /* Log p and status? */ 
    } 
} 

/* Helper function: close descriptor, without affecting errno. 
*/ 
static inline int closefd(const int fd) 
{ 
    int result, saved_errno; 

    if (fd == -1) 
     return EINVAL; 

    saved_errno = errno; 

    do { 
     result = close(fd); 
    } while (result == -1 && errno == EINTR); 
    if (result == -1) 
     result = errno; 
    else 
     result = 0; 

    errno = saved_errno; 

    return result; 
} 

/* Helper function: Create a close-on-exec socket pair. 
*/ 
static int commsocket(int fd[2]) 
{ 
    int result; 

    if (socketpair(AF_UNIX, SOCK_STREAM, 0, fd)) { 
     fd[0] = -1; 
     fd[1] = -1; 
     return errno; 
    } 

    do { 
     result = fcntl(fd[0], F_SETFD, FD_CLOEXEC); 
    } while (result == -1 && errno == EINTR); 
    if (result == -1) { 
     closefd(fd[0]); 
     closefd(fd[1]); 
     return errno; 
    } 

    do { 
     result = fcntl(fd[1], F_SETFD, FD_CLOEXEC); 
    } while (result == -1 && errno == EINTR); 
    if (result == -1) { 
     closefd(fd[0]); 
     closefd(fd[1]); 
     return errno; 
    } 

    return 0; 
} 

/* New system() implementation. 
*/ 
int system(const char *command) 
{ 
    pid_t child; 
    int  i, status, commfd[2]; 
    ssize_t n; 

    /* Allocate the child process. */ 
    i = child_get((pid_t)-1); 
    if (i < 0) { 
     /* "fork failed" */ 
     errno = EAGAIN; 
     return -1; 
    } 

    /* Create a close-on-exec socket pair. */ 
    if (commsocket(commfd)) { 
     child_put(i); 
     /* "fork failed" */ 
     errno = EAGAIN; 
     return -1; 
    } 

    /* Create the child process. */ 
    child = fork(); 
    if (child == (pid_t)-1) 
     return -1; 

    /* Child process? */ 
    if (!child) { 
     char *args[4] = { "sh", "-c", (char *)command, NULL }; 

     /* If command is NULL, return 7 if sh is available. */ 
     if (!command) 
      args[2] = "exit 7"; 

     /* Close parent end of comms socket. */ 
     closefd(commfd[0]); 

     /* Receive one char before continuing. */ 
     do { 
      n = read(commfd[1], &status, 1); 
     } while (n == (ssize_t)-1 && errno == EINTR); 
     if (n != 1) { 
      closefd(commfd[1]); 
      _exit(127); 
     } 

     /* We won't receive anything else. */ 
     shutdown(commfd[1], SHUT_RD); 

     /* Execute the command. If successful, this closes the comms socket. */ 
     execv("/bin/sh", args); 

     /* Failed. Return the errno to the parent. */ 
     status = errno; 
     { 
      const char  *p = (const char *)&status; 
      const char *const q = (const char *)&status + sizeof status; 

      while (p < q) { 
       n = write(commfd[1], p, (size_t)(q - p)); 
       if (n > (ssize_t)0) 
        p += n; 
       else 
       if (n != (ssize_t)-1) 
        break; 
       else 
       if (errno != EINTR) 
        break; 
      } 
     } 

     /* Explicitly close the socket pair. */ 
     shutdown(commfd[1], SHUT_RDWR); 
     closefd(commfd[1]); 
     _exit(127); 
    } 

    /* Parent process. Close the child end of the comms socket. */ 
    closefd(commfd[1]); 

    /* Update the child PID in the array. */ 
    __sync_bool_compare_and_swap(&child_pid[i], (pid_t)-1, child); 

    /* Let the child proceed, by sending a char via the socket. */ 
    status = 0; 
    do { 
     n = write(commfd[0], &status, 1); 
    } while (n == (ssize_t)-1 && errno == EINTR); 
    if (n != 1) { 
     /* Release the child entry. */ 
     child_put(i); 
     closefd(commfd[0]); 

     /* Kill the child. */ 
     kill(child, SIGKILL); 

     /* "fork failed". */ 
     errno = EAGAIN; 
     return -1; 
    } 

    /* Won't send anything else over the comms socket. */ 
    shutdown(commfd[0], SHUT_WR); 

    /* Try reading an int from the comms socket. */ 
    { 
     char  *p = (char *)&status; 
     char *const q = (char *)&status + sizeof status; 

     while (p < q) { 
      n = read(commfd[0], p, (size_t)(q - p)); 
      if (n > (ssize_t)0) 
       p += n; 
      else 
      if (n != (ssize_t)-1) 
       break; 
      else 
      if (errno != EINTR) 
       break; 
     } 

     /* Socket closed with nothing read? */ 
     if (n == (ssize_t)0 && p == (char *)&status) 
      status = 0; 
     else 
     if (p != q) 
      status = EAGAIN; /* Incomplete error code, use EAGAIN. */ 

     /* Close the comms socket. */ 
     shutdown(commfd[0], SHUT_RDWR); 
     closefd(commfd[0]); 
    } 

    /* Wait for the command to complete. */ 
    sem_wait(&child_sem[i]); 

    /* Did the command execution fail? */ 
    if (status) { 
     child_put(i); 
     errno = status; 
     return -1; 
    } 

    /* Command was executed. Return the exit status. */ 
    status = child_status[i]; 
    child_put(i); 

    /* If command is NULL, then the return value is nonzero 
    * iff the exit status was 7. */ 
    if (!command) { 
     if (WIFEXITED(status) && WEXITSTATUS(status) == 7) 
      status = 1; 
     else 
      status = 0; 
    } 

    return status; 
} 

/* Library initialization. 
* Sets the sigchld handler, 
* makes sure pthread library is loaded, and 
* unsets the LD_PRELOAD environment variable. 
*/ 
static void init(void) __attribute__((constructor)); 
static void init(void) 
{ 
    struct sigaction act; 
    int    saved_errno; 

    saved_errno = errno; 

    sigemptyset(&act.sa_mask); 
    act.sa_sigaction = sigchld_handler; 
    act.sa_flags = SA_NOCLDSTOP | SA_RESTART | SA_SIGINFO; 

    sigaction(SIGCHLD, &act, NULL); 

    (void)dlopen("libpthread.so.0", RTLD_NOW | RTLD_GLOBAL); 

    unsetenv("LD_PRELOAD"); 

    errno = saved_errno; 
} 

如果保存上面说child.c,你可以把它编译成libchild.so使用

gcc -W -Wall -O3 -fpic -fPIC -c child.c -lpthread 
gcc -W -Wall -O3 -shared -Wl,-soname,libchild.so child.o -ldl -lpthread -o libchild.so 

如果你有一个测试程序,它system()电话在各种线程中,您可以使用system()插入(和孩子自动收获)使用

env LD_PRELOAD=/path/to/libchild.so test-program 

请注意,根据正是那些不是你的控制之下线程做,你可能需要干预更多的功能,包括signal()sigaction()sigprocmask()pthread_sigmask(),等等,以确保这些线程不会改变您的SIGCHLD处理程序的处置(由libchild.so库安装后)。

如果这些外的控制线程使用popen(),您可以设置是(和pclose())非常相似的代码上面system(),只是分成两个部分。 (如果您想知道为什么我的system()代码不能向父进程报告exec()失败,那是因为我通常使用此代码的变体,它将该命令作为字符串数组使用;这样,它就能正确地报告if该命令没有找到,或者由于权限不足等原因无法执行。在这种特殊情况下,该命令始终为/bin/sh。但是,由于无论如何都需要使用通信套接字来避免在子出口和最新的PID在* child_pid [] *数组中,我决定留下“额外”代码。)

+0

这是很棒的东西,谢谢。我最终做的是类似的,但有点简单。在我对'system()'的重写中,我设置了一个原子布尔值,它可以防止SIGCHLD处理收获僵尸,而在原始的system()结束后收获剩余的僵尸 – shoosh

0

对于那些仍在寻找答案的人,有一种更简单的方法来解决此问题:

重写SIGCHLD处理程序以使用带标志的waitid调用WNOHANG | WNOWAIT在收集它们之前检查孩子的PID。您可以选择检查/ proc/PID/stat(或类似的OS界面)以查找命令名称。