6

子プロセスをログに記録してリープする SIGCHLD のハンドラーをインストールするマルチスレッド アプリケーションがあります。
私が見ている問題は、 への呼び出しを行っているときに始まりますsystem()system()子プロセスが終了するのを待つ必要があり、終了コードが必要なため、子プロセス自体を取得します。これが、sigprocmask()SIGCHLD のブロックを呼び出す理由です。しかし、私のマルチスレッド アプリケーションでは、SIGCHLD はまだ別のスレッドで呼び出されており、その子はsystem()そうする前にリープされます。

これは POSIX の既知の問題ですか?

私が考えたこれを回避する1つの方法は、他のすべてのスレッドでSIGCHLDをブロックすることですが、すべてのスレッドがコードによって直接作成されるわけではないため、これは私の場合は現実的ではありません。
他にどのようなオプションがありますか?

4

4 に答える 4

5

はい、それは既知の (または少なくとも強く関連している) 問題です。

子プロセスが終了するのを待っている間に SIGCHLD をブロックすると、system() がステータス自体を取得する前に、アプリケーションがシグナルをキャッチして system() の子プロセスからステータスを取得するのを防ぐことができます。.... アプリケーションが SIGCHLD シグナルをキャッチしている場合、成功した system() 呼び出しが戻る前に、そのようなシグナルを受信することに注意してください。

( のドキュメントからsystem()、強調が追加されました。)

したがって、あなたの実装がたまたま SIGCHLD をキューに入れない限り、POSIXly は不運です。そうであれば、もちろん、フォークした pid の記録を保持し、期待していたものだけを取得することができます。

Linux でも、 signalfd も複数の SIGCHLD を折りたたむように見えるため、運が悪いです。

しかし、UNIX では、自分の子供を管理し、サードパーティのルーチンの子供を無視するために利用できる巧妙で巧妙すぎるテクニックがたくさんあります。継承されたパイプの I/O 多重化は、SIGCHLD キャッチの代替手段の 1 つです。小さな専用の「spawn-helper」を使用して、別のプロセスで fork と reap を行います。

于 2013-07-09T17:55:55.897 に答える
5

制御できないスレッドがあるため、プリロードされたライブラリを作成して、独自の実装で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がシステム ライブラリ ディレクトリにインストールされていない場合は、フル パスを指定する必要があります。)

考えられるアプローチの 1 つは、保留中の子のロックなし配列 (C コンパイラによって提供されるアトミック ビルトインを使用) を使用することです。の代わりにwaitpid()system()実装はエントリの 1 つを割り当て、そこに子 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;
}

上記を say として保存すると、コンパイルして使用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()の配置を変更しないようにする必要がある場合があることに注意してください (によってインストールされた後)ライブラリ)SIGCHLDlibchild.so

それらの制御不能なスレッドが を使用する場合、上記と非常によく似たコードを2 つの部分に分割するだけで、popen()それ (および ) を挿入できます。pclose()system()

(なぜ私のsystem()コードがわざわざexec()親プロセスに失敗を報告するのか疑問に思っているなら、それは私が通常、コマンドを文字列の配列として受け取るこのコードの変形を使用しているためです。このようにして、コマンドが見つからなかった場合に正しく報告します。または不十分な特権などのために実行できませんでした. この特定のケースでは、コマンドは常に/bin/shです. ただし、子出口と *child_pid[] で最新の PID を持つことの間の競合を回避するために、とにかく通信ソケットが必要であるため* 配列、「余分な」コードを残すことにしました。)

于 2013-07-10T00:35:35.290 に答える
0

まだ答えを探している人のために、この問題を解決する簡単な方法があります:

フラグ WNOHANG|WNOWAIT を指定した waitid 呼び出しを使用するように SIGCHLD ハンドラを書き直して、子の PID をリープする前にチェックします。必要に応じて、/proc/PID/stat (または同様の OS インターフェイス) でコマンド名を確認できます。

于 2014-06-21T04:38:49.603 に答える