2013-07-09 91 views
5

Tôi có một ứng dụng đa luồng cài đặt trình xử lý cho SIGCHLD ghi nhật ký và gặt hái các tiến trình con.
Sự cố tôi thấy bắt đầu khi tôi thực hiện cuộc gọi đến system(). system() cần phải chờ quá trình con kết thúc và gặt hái chính nó vì nó cần mã thoát. Đây là lý do tại sao nó gọi sigprocmask() để chặn SIGCHLD. Nhưng trong ứng dụng đa luồng của tôi, SIGCHLD vẫn được gọi trong một chủ đề khác và đứa trẻ bị gặt hái trước khi system() có cơ hội làm như vậy.Linux: hệ thống() + SIGCHLD xử lý + đa luồng

Đây có phải là sự cố đã biết trong POSIX không?

Một cách xung quanh điều này tôi nghĩ là để chặn SIGCHLD trong tất cả các chủ đề khác nhưng điều này là không thực sự thực tế trong trường hợp của tôi vì không phải tất cả các chủ đề được tạo trực tiếp bởi mã của tôi.
Tôi có các tùy chọn nào khác?

+0

Không nên hệ thống() chặn SIGCHLD? Và bạn có ý nghĩa gì bởi "SIGCHLD được gọi trong một chủ đề khác"? – LostBoy

+0

Có lẽ bạn có thể sử dụng một thư viện (Glib, Pocolib, Boost, QtCore, ...) để cho phép bạn kiểm soát tốt hơn các quy trình hơn là 'hệ thống (3)' có. –

+1

@LostBoy 'system()' không chặn SIGCHLD nhưng chỉ cho chuỗi gọi. nó sử dụng 'sigprocmask()' được viết thành "Mỗi luồng trong một tiến trình có mặt nạ tín hiệu riêng." – shoosh

Trả lời

3

Có, đó là một vấn đề đã biết (hoặc ít nhất là mạnh mẽ).

Chặn SIGCHLD trong khi chờ đứa trẻ chấm dứt ngăn không cho ứng dụng bắt tín hiệu và lấy trạng thái từ quá trình con của hệ thống() trước khi hệ thống() có thể tự nhận trạng thái. .... Lưu ý rằng nếu ứng dụng đang bắt tín hiệu SIGCHLD, nó sẽ nhận được tín hiệu như vậy trước khi trả về cuộc gọi hệ thống thành công().

(Từ tài liệu cho system(), nhấn mạnh thêm.)

Vì vậy, POSIXly bạn ra khỏi may mắn, trừ khi thực hiện của bạn xảy ra để xếp hàng SIGCHLD. Nếu có, bạn có thể giữ một bản ghi các pids mà bạn đã chia đôi, và sau đó chỉ gặt hái những cái bạn đang mong đợi.

Linux cũng vậy, bạn không may mắn, như signalfd appears also to collapse multiple SIGCHLDs.

Tuy nhiên, UNIX có rất nhiều kỹ thuật thông minh và quá thông minh để quản lý con của riêng bạn và bỏ qua những thói quen của bên thứ ba. Sự ghép kênh I/O của các ống thừa kế là một thay thế cho việc bắt SIGCHLD, cũng như sử dụng một "người trợ giúp đẻ trứng" chuyên dụng nhỏ để làm việc cày xới và gặt hái trong một quá trình riêng biệt.

+1

Tôi cũng chạy vào nó. Dường như không thể xử lý tín hiệu UNIX đúng trong một thư viện đa luồng mà không nên can thiệp vào mã khác. Xử lý tín hiệu và sử dụng quá trình id thay vì xử lý quá trình là hai khu vực quan trọng mà UNIX bị hỏng ngoài sửa chữa. – Lothar

3

Vì bạn có chủ đề bạn không thể kiểm soát, tôi khuyên bạn nên viết thư viện được tải sẵn để chuyển cuộc gọi system() (và có thể cả popen() v.v.) với việc triển khai của riêng bạn. Tôi cũng sẽ bao gồm trình xử lý SIGCHLD của bạn trong thư viện.

Nếu bạn không muốn chạy chương trình của bạn qua env LD_PRELOAD=libwhatever.so yourprogram, bạn có thể thêm một cái gì đó giống như

const char *libs; 

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

lúc bắt đầu chương trình của bạn, để có nó lại thực hiện bản thân với LD_PRELOAD một cách thích hợp thiết lập. (Lưu ý rằng có những quirks để xem xét nếu chương trình của bạn là setuid hoặc setgid; xem man ld.so để biết chi tiết. Cụ thể, nếu libwhatever.so không được cài đặt trong thư mục hệ thống thư viện, bạn phải chỉ định đường dẫn đầy đủ.)

Một cách tiếp cận có thể sẽ sử dụng một mảng không khóa (sử dụng các trình xây dựng nguyên tử được cung cấp bởi trình biên dịch C) của các trẻ đang chờ xử lý.Thay vì waitpid(), việc triển khai system() của bạn phân bổ một trong các mục nhập, gắn con PID vào đó và đợi trên một semaphore để trẻ thoát ra thay vì gọi waitpid().

Đây là một thực hiện ví dụ:

#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; 
} 

Nếu bạn lưu trên như nói child.c, bạn có thể biên dịch nó thành libchild.so sử dụng

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 

Nếu bạn có một chương trình thử nghiệm mà không system() cuộc gọi trong các chủ đề khác nhau, bạn có thể chạy nó với system() interposed (và trẻ em tự động gặt hái) bằng cách sử dụng

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

Lưu ý rằng tùy thuộc vào chính xác những gì những chủ đề mà không phải là dưới sự kiểm soát của bạn làm, bạn có thể cần phải xen chức năng hơn nữa, bao gồm signal(), sigaction(), sigprocmask(), pthread_sigmask(), và như vậy, để đảm bảo những chủ đề không thay đổi bố trí của bạn xử lý SIGCHLD (sau khi cài đặt bởi các thư viện libchild.so).

Nếu những chủ đề không kiểm soát đó sử dụng popen(), bạn có thể xen vào (và pclose()) với mã rất giống với system() ở trên, chỉ chia làm hai phần.

(Nếu bạn tự hỏi tại sao mã số system() của mình làm phiền để báo cáo lỗi exec() cho quy trình gốc, đó là vì tôi thường sử dụng một biến thể của mã này nhận lệnh như một chuỗi các chuỗi; lệnh không được tìm thấy, hoặc không thể được thực hiện do không đủ đặc quyền, vv Trong trường hợp cụ thể này lệnh luôn là /bin/sh. Tuy nhiên, vì ổ cắm truyền thông là cần thiết để tránh việc chạy đua giữa lối ra của trẻ và có cập nhật PID trong mảng * child_pid [] *, tôi quyết định để lại mã "phụ".)

+0

đây là công cụ tuyệt vời, cảm ơn bạn.Những gì tôi đã làm là tương tự nhưng đơn giản hơn một chút. Trong quá trình ghi đè lên 'system()', tôi đặt một boolean nguyên tử ngăn cản việc xử lý SIGCHLD từ việc gặt hái những thây ma và thay vào đó, gặt mọi zombie còn lại ngay sau khi hệ thống 'system()' kết thúc – shoosh

0

Đối với những người vẫn đang tìm câu trả lời, có cách dễ dàng hơn để giải quyết vấn đề này:

Viết lại trình xử lý SIGCHLD để sử dụng cuộc gọi chờ với cờ WNOHANG | WNOWAIT để kiểm tra PID của trẻ trước khi gặt chúng. Bạn có thể tùy chọn kiểm tra/proc/PID/stat (hoặc giao diện hệ điều hành tương tự) cho tên lệnh.