如何在 C 的 pthread 中为 system() 调用启用 SIGINT 信号?

How to enable SIGINT signal for system() call in a pthread in C?

下面关于 system() 的手册说它通过 system() 调用阻止任何二进制程序 运行 的 SIGINT 和 SIGQUIT 信号。 https://man7.org/linux/man-pages/man3/system.3.html#:~:text=The%20system()%20library%20function,the%20command%20has%20been%20completed.

伪代码:

thread_1()
{
...
system("binary application");

}


main() {
...
pid = pthread_create(thread_1);
pthread_cancel(pid);

}

pthread_cancel 向线程 1 发出 SIGINT,这会终止线程 1,但不会终止二进制应用程序。

如何让“二进制应用程序”接收到SIGINT信号?

不是使用 system(),而是 fork() 一个 child 进程,并且 execl("/bin/sh", "-c", "system-command-goes-here", (char *)0); 在那个 child 进程中。

当您调用 fork() 时,它会 returns 两次:一次在 parent 中带有正值 – child 的进程标识符“pid”过程;并且一次在 child 中具有零值。

要向 child 进程发送一个 INT 信号,只需使用 kill(pid, SIGINT);

你可以在线程中使用pthread_cleanup_push(kill_int, (intptr_t)pid)来杀死child进程,如果线程退出(被取消或杀死),用

static void kill_int(void *pidptr)
{
    const pid_t  pid = (intptr_t)pidptr;
    pid_t        p;

    if (pid > 1)
        kill(pid, SIGINT);
}

这里有一些您可能会觉得有用的 Public 域辅助函数。 run.h:

/* SPDX-License-Identifier: CC0-1.0 */

#ifndef   RUN_H
#define   RUN_H
#include <unistd.h>
#include <sys/types.h>

/* Execute command.  First parameter is the binary to execute (path or name),
   and the second parameter is the argument array.  First element in the
   argument array is command name, and the last element must be (char *)0.
   Returns the child process ID if successful, -1 with errno set if error.
*/
pid_t run(const char *, const char *[]);

/* Execute shell command.  The parameter is the shell command,
   otherwise this behaves like run().
*/
pid_t run_sh(const char *);

/* Check if child process has exited.
   Returns the PID of the child if it has returned,
   with the status (use WIFEXITED(), WEXITSTATUS(), WIFSIGNALED(), WTERMSIG())
   stored at the location specified by the int pointer, if not NULL.
   Returns 0 if the child hasn't exited yet, or
   -1 if an error occurred (with errno set).

   try_reap() tries to reap a specific child,
   try_reap_any() checks if any child processes have exited, and
   try_reap_group() checks if a child belonging to a process group has exited.
*/
pid_t try_reap(pid_t, int *);
pid_t try_reap_any(int *);
pid_t try_reap_group(pid_t, int *);

/* Wait until a specific child exits.
   Returns the child PID with status set if not NULL,
   or -1 if an error occurs.
*/
pid_t reap(pid_t, int *);

/* Wait until all child processes have exited.
   If non-NULL, the callback is called for each reaped child.
   If the callback returns nonzero, the function returns immediately
   without waiting for other children.
   Returns 0 if success, callback return value if it returns nonzero,
   or -1 with errno set if an error occurs.
*/
pid_t reap_all(int (*report)(pid_t, int));
pid_t reap_group(pid_t, int (*report)(pid_t, int));

#endif /* RUN_H */

执行,run.c:

/* SPDX-License-Identifier: CC0-1.0 */

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

#ifndef  RUN_FAILURE_EXIT_STATUS
#define  RUN_FAILURE_EXIT_STATUS  69
#endif

static inline int has_slash(const char *cmd)
{
    while (*cmd)
        if (*(cmd++) == '/')
            return 1;
    return 0;
}

pid_t run(const char *cmd, const char *args[])
{
    int    ctrl[2] = { -1, -1 };
    int    cause;
    pid_t  child, p;

    /* Sanity checks. */
    if (!cmd || !*cmd || !args) {
        errno = EINVAL;
        return -1;
    }

    /* Create a close-on-exec control pipe. */
    if (pipe2(ctrl, O_CLOEXEC) == -1) {
        /* Failed; errno already set. */
        return -1;
    }

    /* Fork the child process. */
    child = fork();
    if (child == (pid_t)-1) {
        /* Failed; errno set. */
        cause = errno;
        close(ctrl[0]);
        close(ctrl[1]);
        errno = cause;
        return -1;
    } else
    if (!child) {
        
        /* This is the child process. */

        /* Close parent end of control pipe. */
        close(ctrl[0]);

        /* Try and execute the command. */
        if (has_slash(cmd))
            execv(cmd, (char *const *)args);
        else
            execvp(cmd, (char *const *)args);

        /* Failed. Try and report cause to parent. */
        cause = errno;
        {
            const char       *ptr = (const char *)(&cause);
            const char *const end = (const char *)(&cause) + sizeof cause;
            ssize_t           n;

            while (ptr < end) {
                n = write(ctrl[1], ptr, (size_t)(end - ptr));
                if (n > 0) {
                    ptr += n;
                } else
                if (n != -1 || errno != EINTR)
                    break;
            }
        }

        exit(RUN_FAILURE_EXIT_STATUS);
    }

    /* This is the parent process. */

    /* Close child end of control pipe. */
    close(ctrl[1]);

    /* Try reading from the control pipe. */
    {
        char       *ptr = (char *)(&cause) + sizeof cause;
        char *const end = (char *)(&cause) + sizeof cause;
        int         err = 0;
        ssize_t     n;

        while (ptr < end) {
            n = read(ctrl[0], ptr, (size_t)(end - ptr));
            if (n > 0) {
                ptr += n;
            } else
            if (!n) {
                break;
            } else
            if (n != -1) {
                err = EIO;
                break;
            } else
            if (errno != EINTR) {
                err = errno;
                break;
            }
        }

        /* If we failed, and didn't get a full cause,
           use the error from the read. */
        if (err && ptr != end)
            cause = err;

    }

    /* Close parent end of the control pipe. */
    close(ctrl[0]);

    /* If we failed, reap the child and exit. */
    if (cause) {
        do {
            p = waitpid(child, NULL, 0);
        } while (p == -1 && errno == EINTR);
        errno = cause;
        return -1;
    }

    /* Everything looks okay! */
    return child;
}

pid_t run_shell(const char *command)
{
    const char *args[4] = { "sh", "-c", command, (char *)0 };
    return run("/bin/sh", args);
}

pid_t try_reap(const pid_t pid, int *status)
{
    int   temp_status;
    pid_t p;

    if (pid <= 1) {
        errno = EINVAL;
        return -1;
    }

    do {
        p = waitpid(pid, &temp_status, WNOHANG);
    } while (p == -1 && errno == EINTR);
    if (status && p > 0)
        *status = temp_status;
    return p;
}

pid_t try_reap_any(int *status)
{
    int   temp_status;
    pid_t p;

    do {
        p = waitpid(-1, &temp_status, WNOHANG);
    } while (p == -1 && errno == EINTR);
    if (status && p > 0)
        *status = temp_status;
    return p;
}

pid_t try_reap_group(pid_t pgid, int *status)
{
    int   temp_status;
    pid_t p;

    if (pgid <= 1) {
        errno = EINVAL;
        return -1;
    }

    do {
        p = waitpid(-1, &temp_status, WNOHANG);
    } while (p == -1 && errno == EINTR);
    if (status && p > 0)
        *status = temp_status;
    return p;
}

pid_t reap(const pid_t pid, int *status)
{
    int   temp_status;
    pid_t p;

    if (pid <= 1) {
        errno = EINVAL;
        return -1;
    }

    do {
        p = waitpid(pid, &temp_status, 0);
    } while (p == -1 && errno == EINTR);
    if (status && p > 0)
        *status = temp_status;
    return p;
}

int reap_all(int (*report)(pid_t pid, int status))
{
    int   status, retval;
    pid_t p;

    while (1) {
        p = waitpid(-1, &status, 0);
        if (p == -1) {
            if (errno == ECHILD)
                return 0;
            else
            if (errno != EINTR)
                return -1;
        } else
        if (p > 0 && report) {
            retval = report(p, status);
            if (retval)
                return retval;
        }
    }
}

int reap_group(pid_t pgid, int (*report)(pid_t pid, int status))
{
    int   status, retval;
    pid_t p;

    if (pgid <= 1) {
        errno = EINVAL;
        return -1;
    }

    while (1) {
        p = waitpid(-pgid, &status, 0);
        if (p == -1) {
            if (errno == ECHILD)
                return 0;
            else
            if (errno != EINTR)
                return -1;
        } else
        if (p > 0 && report) {
            retval = report(p, status);
            if (retval)
                return retval;
        }
    }
}

这是一个使用示例,example.c,其中 运行s 由 command-line 参数指定的二进制文件:

/* SPDX-License-Identifier: CC0-1.0 */

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

#include "run.h"

int main(int argc, char *argv[])
{
    pid_t  child, p;
    int    status;

    if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        const char *argv0 = (argc > 0 && argv && argv[0]) ? argv[0] : "(this)";
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv0);
        fprintf(stderr, "       %s COMMAND [ ARGS ... ]\n", argv0);
        fprintf(stderr, "\n");
        return EXIT_FAILURE;
    }

    child = run(argv[1], (const char **)(argv + 1));
    if (child == -1) {
        fprintf(stderr, "%s: Cannot execute: %s.\n", argv[1], strerror(errno));
        return EXIT_FAILURE;
    }
    fprintf(stderr, "%s: Started process %d.\n", argv[1], (int)child);

    p = reap(child, &status);
    if (p == -1) {
        fprintf(stderr, "%s: Cannot reap child: %s.\n", argv[1], strerror(errno));
        return EXIT_FAILURE;
    } else
    if (p != child) {
        fprintf(stderr, "%s: Internal bug: reaped the wrong child process (%d, expected %d).\n", argv[1], (int)p, (int)child);
        return EXIT_FAILURE;
    }

    if (WIFEXITED(status)) {
        if (WEXITSTATUS(status) == EXIT_SUCCESS) {
            fprintf(stderr, "%s: Exited successfully.\n", argv[1]);
            return EXIT_SUCCESS;
        } else {
            fprintf(stderr, "%s: Exited with status %d.\n", argv[1], WEXITSTATUS(status));
            return WEXITSTATUS(status);
        }
    } else
    if (WIFSIGNALED(status)) {
        fprintf(stderr, "%s: Died from signal %d.\n", argv[1], WTERMSIG(status));
        return EXIT_FAILURE;
    } else {
        fprintf(stderr, "%s: Child process vanished!\n", argv[1]);
        return EXIT_FAILURE;
    }
}

为了将所有这些联系在一起,Makefile:

CC      := gcc
CFLAGS  := -Wall -O2
LDFLAGS :=
PROGS   := example

all: $(PROGS)

clean:
    rm -f *.o $(PROGS)

%.o:%.c
    $(CC) $(CFLAGS) -c $^

example: run.o example.o
    $(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@

注意这个论坛吃Tabs,所以你需要运行 sed -e 's|^ *|\t|' -i Makefile来修复缩进。要编译,只需 运行 make。 至 运行,运行 例如

./example date

parent 进程检查 child 进程退出的方式和原因,并将报告进程标识符 (pid) 和退出状态。

手册页还说:

(These signals will be handled according to their defaults inside the child process that executes command.)

所以回答你的问题“如何让‘二进制应用程序’接收SIGINT信号?”;没关系,反正都会的。阻塞发生在调用命令的线程中,而不是命令进程中。

编辑:

要回答@Hanu 下面的评论,请使用 wait() set of system calls: you can get the pid of the command inside the system() call from there, and you can safely close your child thread or take action depending on the result of wait(). But I don't know what resources you would need to clean if the process has terminated: Linux will free all the resources associated with the process called by system: there is a distinction between how the OS cleans pthreads when they finish and process resources - see this SO answer .

The below manual on system() says it blocks SIGINT and SIGQUIT signal for any binary program run through system() call.

不,不是。它是这样说的:

During execution of the command, SIGCHLD will be blocked, and SIGINT and SIGQUIT will be ignored, in the process that calls system(). (These signals will be handled according to their defaults inside the child process that executes command.)

(强调已添加。)影响 signal-handling 的是调用 system() 的进程,而不是运行命令的(单独)进程。而且,这是有目的的,你不应该轻易去干涉。

pthread_cancel issues SIGINT to thread 1

存疑。 POSIX 没有记录线程取消是如何实现的,但是发送 SIGINT 是不太可能的选择,因为它的默认行为是终止 进程 pthread_cancel() 的 Linux 手册确实说它是通过信号实现的,但它也说如果实时信号可用,则使用第一个实时信号,否则使用 SIGUSR2。当 system() 是 运行.

时,这些都没有被记录为被阻止

which kill the thread 1, but not the binary application.

是的,在该函数处于 运行 时强制终止调用 system() 的线程不会终止正在执行指定命令的单独进程。这与信号被阻止无关。如果您想在命令完成之前终止命令为 运行 的进程,那么您可能正在寻找 kill() 函数。为此,您需要找到要杀死的 child 的 PID,这可能需要相当大的努力和麻烦。

总的来说,如果您不喜欢 system() 的语义,那么您最好基于 fork()exec*() 推出自己的版本,这样您就可以有你想要的控制水平。