gpt4 book ai didi

c - prctl(PR_SET_PDEATHSIG) 竞争条件

转载 作者:太空狗 更新时间:2023-10-29 11:41:50 27 4
gpt4 key购买 nike

据我所知,在父进程死亡时终止子进程的最佳方法是通过 prctl(PR_SET_PDEATHSIG)(至少在 Linux 上):How to make child process die after parent exits?

man prctl 中提到了这一点:

This value is cleared for the child of a fork(2) and (since Linux 2.4.36 / 2.6.23) when executing a set-user-ID or set-group-ID binary, or a binary that has associated capabilities (see capabilities(7)). This value is preserved across execve(2).

所以,下面的代码有一个竞争条件:

parent.c:

#include <unistd.h>

int main(int argc, char **argv) {
int f = fork();
if (fork() == 0) {
execl("./child", "child", NULL, NULL);
}
return 0;
}

child.c:

#include <sys/prctl.h>
#include <signal.h>

int main(int argc, char **argv) {
prctl(PR_SET_PDEATHSIG, SIGKILL); // ignore error checking for now
// ...
return 0;
}

即,父计数在 prctl() 在子进程中执行之前死亡(因此子进程不会收到 SIGKILL)。解决此问题的正确方法是在 exec() 之前的父级中使用 prctl():

parent.c:

#include <unistd.h>
#include <sys/prctl.h>
#include <signal.h>

int main(int argc, char **argv) {
int f = fork();
if (fork() == 0) {
prctl(PR_SET_PDEATHSIG, SIGKILL); // ignore error checking for now
execl("./child", "child", NULL, NULL);
}
return 0;
}

child.c:

int main(int argc, char **argv) {
// ...
return 0;
}

但是,如果 ./child 是一个 setuid/setgid 二进制文件,那么这个避免竞争条件的技巧就不起作用(exec() setuid/根据上面引用的手册页,setgid binary 导致 PDEATHSIG 丢失),而且您似乎被迫采用第一个(不合理的)解决方案。

如果 childprctl(PR_SET_PDEATH_SIG) 的 setuid/setgid 二进制文件,有没有什么办法可以以一种非活泼的方式进行?

最佳答案

让父进程设置管道更为常见。父进程保持写端打开(pipefd[1]),关闭读端(pipefd[0])。子进程关闭写端(pipefd[1]),并设置读端(pipefd[1])非阻塞。

这样,子进程就可以使用read(pipefd[0], buffer, 1) 来检查父进程是否还活着。如果父级仍在运行,它将返回 -1errno == EAGAIN(或 errno == EINTR)。

现在,在Linux中,子进程也可以设置读取结束异步,这样子进程在父进程退出时会发送一个信号(默认为SIGIO):

fcntl(pipefd[0], F_SETSIG, desired_signal);
fcntl(pipefd[0], F_SETOWN, getpid());
fcntl(pipefd[0], F_SETFL, O_NONBLOCK | O_ASYNC);

desired_signal 使用 siginfo 处理程序。如果 info->si_code == POLL_IN && info->si_fd == pipefd[0],则父进程退出或向管道写入内容。因为 read() 是异步信号安全的,并且管道是非阻塞的,所以您可以在信号处理程序中使用 read(pipefd[0], &buffer, sizeof buffer) 是否 parent 写了一些东西,或者如果 parent 退出(关闭管道)。在后一种情况下,read() 将返回 0

据我所知,这种方法没有竞争条件(如果您使用实时信号,这样信号就不会丢失,因为用户发送的信号已经挂起),尽管它非常特定于 Linux。设置信号处理程序后,在子进程生命周期的任何时候,子进程始终可以显式检查父进程是否仍然存在,而不会影响信号生成。

因此,用伪代码概括一下:

Construct pipe
Fork child process

Child process:
Close write end of pipe
Install pipe signal handler (say, SIGRTMIN+0)
Set read end of pipe to generate pipe signal (F_SETSIG)
Set own PID as read end owner (F_SETOWN)
Set read end of pipe nonblocking and async (F_SETFL, O_NONBLOCK | O_ASYNC)
If read(pipefd[0], buffer, sizeof buffer) == 0,
the parent process has already exited.

Continue with normal work.

Child process pipe signal handler:
If siginfo->si_code == POLL_IN and siginfo->si_fd == pipefd[0],
parent process has exited.
To immediately die, use e.g. raise(SIGKILL).

Parent process:
Close read end of pipe

Continue with normal work.

我不希望你相信我的话。

下面是一个粗略的示例程序,您可以用来自己检查此行为。它很长,但只是因为我希望它很容易看到运行时发生的事情。要在普通程序中实现它,您只需要几十行代码。 example.c:

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

static volatile sig_atomic_t done = 0;

static void handle_done(int signum)
{
if (!done)
done = signum;
}

static int install_done(const int signum)
{
struct sigaction act;

memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_handler = handle_done;
act.sa_flags = 0;
if (sigaction(signum, &act, NULL) == -1)
return errno;

return 0;
}

static int deathfd = -1;

static void death(int signum, siginfo_t *info, void *context)
{
if (info->si_code == POLL_IN && info->si_fd == deathfd)
raise(SIGTERM);
}

static int install_death(const int signum)
{
struct sigaction act;

memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_sigaction = death;
act.sa_flags = SA_SIGINFO;
if (sigaction(signum, &act, NULL) == -1)
return errno;

return 0;
}

int main(void)
{
pid_t child, p;
int pipefd[2], status;
char buffer[8];

if (install_done(SIGINT)) {
fprintf(stderr, "Cannot set SIGINT handler: %s.\n", strerror(errno));
return EXIT_FAILURE;
}

if (pipe(pipefd) == -1) {
fprintf(stderr, "Cannot create control pipe: %s.\n", strerror(errno));
return EXIT_FAILURE;
}

child = fork();
if (child == (pid_t)-1) {
fprintf(stderr, "Cannot fork child process: %s.\n", strerror(errno));
return EXIT_FAILURE;
}

if (!child) {
/*
* Child process.
*/

/* Close write end of pipe. */
deathfd = pipefd[0];
close(pipefd[1]);

/* Set a SIGHUP signal handler. */
if (install_death(SIGHUP)) {
fprintf(stderr, "Child process: cannot set SIGHUP handler: %s.\n", strerror(errno));
return EXIT_FAILURE;
}

/* Set SIGTERM signal handler. */
if (install_done(SIGTERM)) {
fprintf(stderr, "Child process: cannot set SIGTERM handler: %s.\n", strerror(errno));
return EXIT_FAILURE;
}

/* We want a SIGHUP instead of SIGIO. */
fcntl(deathfd, F_SETSIG, SIGHUP);

/* We want the SIGHUP delivered when deathfd closes. */
fcntl(deathfd, F_SETOWN, getpid());

/* Make the deathfd (read end of pipe) nonblocking and async. */
fcntl(deathfd, F_SETFL, O_NONBLOCK | O_ASYNC);

/* Check if the parent process is dead. */
if (read(deathfd, buffer, sizeof buffer) == 0) {
printf("Child process (%ld): Parent process is already dead.\n", (long)getpid());
return EXIT_FAILURE;
}

while (1) {
status = __atomic_fetch_and(&done, 0, __ATOMIC_SEQ_CST);
if (status == SIGINT)
printf("Child process (%ld): SIGINT caught and ignored.\n", (long)getpid());
else
if (status)
break;
printf("Child process (%ld): Tick.\n", (long)getpid());
fflush(stdout);
sleep(1);

status = __atomic_fetch_and(&done, 0, __ATOMIC_SEQ_CST);
if (status == SIGINT)
printf("Child process (%ld): SIGINT caught and ignored.\n", (long)getpid());
else
if (status)
break;
printf("Child process (%ld): Tock.\n", (long)getpid());
fflush(stdout);
sleep(1);
}

printf("Child process (%ld): Exited due to %s.\n", (long)getpid(),
(status == SIGINT) ? "SIGINT" :
(status == SIGHUP) ? "SIGHUP" :
(status == SIGTERM) ? "SIGTERM" : "Unknown signal.\n");
fflush(stdout);

return EXIT_SUCCESS;
}

/*
* Parent process.
*/

/* Close read end of pipe. */
close(pipefd[0]);

while (!done) {
fprintf(stderr, "Parent process (%ld): Tick.\n", (long)getpid());
fflush(stderr);
sleep(1);
fprintf(stderr, "Parent process (%ld): Tock.\n", (long)getpid());
fflush(stderr);
sleep(1);

/* Try reaping the child process. */
p = waitpid(child, &status, WNOHANG);
if (p == child || (p == (pid_t)-1 && errno == ECHILD)) {
if (p == child && WIFSIGNALED(status))
fprintf(stderr, "Child process died from %s. Parent will now exit, too.\n",
(WTERMSIG(status) == SIGINT) ? "SIGINT" :
(WTERMSIG(status) == SIGHUP) ? "SIGHUP" :
(WTERMSIG(status) == SIGTERM) ? "SIGTERM" : "an unknown signal");
else
fprintf(stderr, "Child process has exited, so the parent will too.\n");
fflush(stderr);
break;
}
}

if (done) {
fprintf(stderr, "Parent process (%ld): Exited due to %s.\n", (long)getpid(),
(done == SIGINT) ? "SIGINT" :
(done == SIGHUP) ? "SIGHUP" : "Unknown signal.\n");
fflush(stderr);
}

/* Never reached! */
return EXIT_SUCCESS;
}

使用例如编译并运行上面的代码

gcc -Wall -O2 example.c -o example
./example

父进程打印到标准输出,子进程打印到标准错误。如果按Ctrl+C,父进程将退出;子进程将忽略该信号。子进程使用 SIGHUP 而不是 SIGIO(尽管实时信号,比如 SIGRTMIN+0 会更安全);如果由退出的父进程生成,SIGHUP 信号处理程序将在子进程中引发 SIGTERM

为了使终止原因易于查看,子进程捕获 SIGTERM,并退出下一次迭代(一秒后)。如果需要,处理程序可以使用例如raise(SIGKILL) 立即终止自身。

父进程和子进程都显示它们的进程 ID,因此您可以轻松地从另一个终端窗口发送 SIGINT/SIGHUP/SIGTERM 信号. (子进程忽略从进程外部发送的 SIGINTSIGHUP。)

关于c - prctl(PR_SET_PDEATHSIG) 竞争条件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42496478/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com