2013-07-09 122 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界面)以查找命令名稱。