我有一个多线程应用程序,它为SIGCHLD安装一个处理程序,用于记录和重新获取子进程。
当我打电话给system()
时,我看到的问题就出现了。 system()
需要等待子进程结束并自己收回,因为它需要退出代码。这就是它调用sigprocmask()
来阻止SIGCHLD的原因。但是在我的多线程应用程序中,SIGCHLD仍然在另一个线程中被调用,并且在system()
有机会之前收回了孩子。
这是POSIX中的已知问题吗?
我想到的一种方法是在所有其他线程中阻止SIGCHLD,但在我的情况下这并不现实,因为并非所有线程都是由我的代码直接创建的。
我还有其他选择吗?
答案 0 :(得分:4)
是的,这是一个已知的(或至少是强烈暗示的)问题。
在等待子进程终止时阻塞SIGCHLD会阻止应用程序捕获信号并从system()的子进程获取状态,然后system()才能获得状态。 .... 请注意,如果应用程序正在捕获SIGCHLD信号,它将在成功的system()调用返回之前收到此类信号。
(来自system()
的文档,重点补充。)
所以,POSIXly你运气不好,除非你的实现恰好排队SIGCHLD。如果确实如此,你当然可以记录你所分配的pids,然后只收获你期望的那些。
Linux也是,你运气不好,如signalfd appears also to collapse multiple SIGCHLDs。
然而,在UNIX上,您有许多聪明且过于聪明的技术可用于管理您自己的孩子并忽略第三方例程。继承管道的I / O多路复用是SIGCHLD捕获的一种替代方法,就像使用一个小的,专用的“spawn-helper”来在一个单独的进程中进行分叉和收割一样。答案 1 :(得分:4)
由于你有无法控制的线程,我建议你编写一个预加载的库来插入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()
,{{1}等等,以确保这些线程不会更改pthread_sigmask()
处理程序的处置(在SIGCHLD
库安装之后)。
如果这些失控线程使用libchild.so
,您可以将其(和popen()
)与上面的pclose()
非常相似的代码插入,只需分成两部分。
(如果您想知道为什么我的system()
代码无法向父进程报告system()
失败,那是因为我通常使用此代码的变体将该命令作为字符串数组;这样就可以正确地报告是否找不到命令,或者由于权限不足而无法执行等等。在这种特殊情况下,命令总是exec()
。但是,因为无论如何都需要通信套接字来避免在子出口和* child_pid [] *数组中拥有最新的PID之间竞争,我决定留下“额外”代码。)
答案 2 :(得分:0)
对于那些仍在寻找答案的人来说,有一种更简单的方法可以解决这个问题:
重写SIGCHLD处理程序以使用带有标志WNOHANG | WNOWAIT的waitid调用来检查子PID之前的PID。您可以选择检查/ proc / PID / stat(或类似的OS接口)以获取命令名称。
答案 3 :(得分:-1)
将system()
替换为proc_system()
。