ant*_*rez 31 c linux system-calls zombie-process waitpid
上下文就是这个Redis问题.我们有一个wait3()等待AOF重写子项在磁盘上创建新AOF版本的调用.当孩子完成后,通知父母wait3()以便用新的AOF替换旧的AOF.
但是,在上述问题的上下文中,用户向我们通知了一个错误.我修改了一点Redis 3.0的实现,以便在wait3()返回-1 时清楚地记录,而不是因为这种意外情况而崩溃.所以这就是显而易见的事情:
wait3() 当有待等待的孩子时,我们会打电话.SIGCHLD应设置为SIG_DFL,没有代码Redis的所有设置这个信号,所以它的默认行为.wait3()按预期成功运行.wait3()开始返回-1.AFAIK在我们调用的当前代码中wait3()是不可能的,因为当没有挂起的子节点时,因为当创建AOF子节点时,我们设置server.aof_child_pid为pid的值,并且我们仅在成功wait3()调用之后重置它.
所以wait3()应该没有理由失败-1 ECHILD,但它确实如此,所以可能僵尸孩子不是出于某种意想不到的原因而创建的.
假设1:在某些奇怪的条件下Linux可能会丢弃僵尸孩子,例如因为内存压力?看起来不合理,因为僵尸只附加了元数据但谁知道.
请注意,我们打电话wait3()给WNOHANG.并且鉴于默认情况下SIGCHLD设置为SIG_DFL,唯一应该导致失败并返回-1的条件,并且ECHLD应该没有可用于报告信息的僵尸.
假设2:可能发生的其他事情但是没有解释,如果它发生,是在第一个孩子死后,SIGCHLD处理程序被设置为SIG_IGN,导致wait3()返回-1和ECHLD.
假设3:有没有办法从外部移除僵尸儿童?也许这个用户有某种脚本可以在后台删除僵尸进程,以便信息不再可用wait3()?据我所知,如果父母不等待它(使用或处理信号)并且如果没有被忽略,则永远不可能移除僵尸,但也许有一些特定于Linux的方式.waitpidSIGCHLD
假设4:实际上有在Redis的代码一些bug,使我们成功wait3()的孩子第一次不正确复位状态,后来我们叫wait3()连连,但不再有僵尸,所以它返回-1.分析代码看起来不可能,但也许我错了.
另一件重要的事情:我们过去从未见过这一点.这显然只发生在这个特定的Linux系统中.
更新:Yossi Gottlieb提出SIGCHLDRedis进程中的另一个线程由于某种原因(通常不会发生,仅在此系统上)接收.我们已经SIGALRM在bio.c线程中屏蔽了,也许我们也可以尝试SIGCHLD从I/O线程屏蔽.
调用wait3()的地方:
/* Check if a background saving or AOF rewrite in progress terminated. */
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
int statloc;
pid_t pid;
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
int exitcode = WEXITSTATUS(statloc);
int bysignal = 0;
if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);
if (pid == -1) {
redisLog(LOG_WARNING,"wait3() returned an error: %s. "
"rdb_child_pid = %d, aof_child_pid = %d",
strerror(errno),
(int) server.rdb_child_pid,
(int) server.aof_child_pid);
} else if (pid == server.rdb_child_pid) {
backgroundSaveDoneHandler(exitcode,bysignal);
} else if (pid == server.aof_child_pid) {
backgroundRewriteDoneHandler(exitcode,bysignal);
} else {
redisLog(REDIS_WARNING,
"Warning, detected child with unmatched pid: %ld",
(long)pid);
}
updateDictResizePolicy();
}
} else {
Run Code Online (Sandbox Code Playgroud)
选定部分backgroundRewriteDoneHandler:
void backgroundRewriteDoneHandler(int exitcode, int bysignal) {
if (!bysignal && exitcode == 0) {
int newfd, oldfd;
char tmpfile[256];
long long now = ustime();
mstime_t latency;
redisLog(REDIS_NOTICE,
"Background AOF rewrite terminated with success");
... more code to handle the rewrite, never calls return ...
} else if (!bysignal && exitcode != 0) {
server.aof_lastbgrewrite_status = REDIS_ERR;
redisLog(REDIS_WARNING,
"Background AOF rewrite terminated with error");
} else {
server.aof_lastbgrewrite_status = REDIS_ERR;
redisLog(REDIS_WARNING,
"Background AOF rewrite terminated by signal %d", bysignal);
}
cleanup:
aofClosePipes();
aofRewriteBufferReset();
aofRemoveTempFile(server.aof_child_pid);
server.aof_child_pid = -1;
server.aof_rewrite_time_last = time(NULL)-server.aof_rewrite_time_start;
server.aof_rewrite_time_start = -1;
/* Schedule a new rewrite if we are waiting for it to switch the AOF ON. */
if (server.aof_state == REDIS_AOF_WAIT_REWRITE)
server.aof_rewrite_scheduled = 1;
}
Run Code Online (Sandbox Code Playgroud)
如您所见,所有代码路径必须执行cleanup重置server.aof_child_pid为-1 的代码.
21353:C 29 Nov 04:00:29.957*AOF重写:写入时复制使用的内存为8 MB
27848:M 29 Nov 04:00:30.133 ^ @ wait3()返回错误:没有子进程.rdb_child_pid = -1,aof_child_pid = 21353
你可以看到aof_child_pid不是-1.
TLDR:您目前依赖于signal(2)未指明的行为; 使用sigaction(小心)代替.
首先,SIGCHLD很奇怪.从手册页的sigaction;
POSIX.1-1990不允许设置行动
SIGCHLD来SIG_IGN.POSIX.1-2001允许这种可能性,因此SIGCHLD可以使用忽略来防止僵尸的创建(参见wait(2)).然而,用于忽略的历史BSD和System V行为是SIGCHLD不同的,因此确保终止的孩子不会成为僵尸的唯一完全可移植的方法是捕获SIGCHLD信号并执行wait(2)或类似.
以下是wait(2)手册页中的内容:
POSIX.1-2001指定如果处置
SIGCHLD设置为SIG_IGN或者设置了SA_NOCLDWAIT标志SIGCHLD(参见sigaction(2)),那么终止的子节点不会成为僵尸,并且会阻止wait()或waitpid()将阻止所有子节点终止,然后将errno设置为失败ECHILD.(最初的POSIX标准将设置的行为保留SIGCHLD为SIG_IGN未指定.请注意,即使默认处置SIGCHLD为"ignore",也要明确设置处理SIG_IGN结果,以便对僵尸进程子进行不同处理.)Linux 2.6符合此规范.但是,Linux 2.4(及更早版本)不会:如果在忽略时进行wait()或waitpid()调用SIGCHLD,则调用的行为就好像SIGCHLD没有被忽略一样,即调用阻塞,直到下一个子进程终止,然后返回进程ID和那个孩子的状况.
请注意,这样的效果是,如果信号的处理行为类似于SIG_IGN设置,那么(在Linux 2.6+下)您将看到您正在看到的行为 - wait()即将返回-1并ECHLD因为孩子将被自动收获.
其次,信号处理pthreads(我认为你在这里使用)是非常困难的.它的工作方式(我相信你知道)是过程导向的信号被发送到信号未被屏蔽的过程中的任意线程.但是虽然线程有自己的信号掩码,但是有一个进程范围的动作处理程序.
将这两件事放在一起,我认为你遇到了我以前碰过的问题.我在使用SIGCHLD处理时遇到了问题signal()(这在pthreads之前已被弃用,这是公平的),这是通过移动sigaction并仔细设置每个线程信号掩码来修复的.我当时得出的结论是,C库正在仿效sigaction我正在告诉它的事情signal(),但却被绊倒了pthreads.
请注意,您当前依赖于未指定的行为.从手册页的signal(2):
的影响
signal()在多线程程序是不确定的.
这是我建议你做的:
sigaction()和pthread_sigmask().明确设置您关心的所有信号的处理(即使您认为这是当前的默认值),即使将它们设置为SIG_IGN或SIG_DFL.我这样做时会阻止信号(可能过于谨慎但我从某处复制了这个例子).这是我正在做的(大致):
sigset_t set;
struct sigaction sa;
/* block all signals */
sigfillset (&set);
pthread_sigmask (SIG_BLOCK, &set, NULL);
/* Set up the structure to specify the new action. */
memset (&sa, 0, sizeof (struct sigaction));
sa.sa_handler = handlesignal; /* signal handler for INT, TERM, HUP, USR1, USR2 */
sigemptyset (&sa.sa_mask);
sa.sa_flags = 0;
sigaction (SIGINT, &sa, NULL);
sigaction (SIGTERM, &sa, NULL);
sigaction (SIGHUP, &sa, NULL);
sigaction (SIGUSR1, &sa, NULL);
sigaction (SIGUSR2, &sa, NULL);
sa.sa_handler = SIG_IGN;
sigemptyset (&sa.sa_mask);
sa.sa_flags = 0;
sigaction (SIGPIPE, &sa, NULL); /* I don't care about SIGPIPE */
sa.sa_handler = SIG_DFL;
sigemptyset (&sa.sa_mask);
sa.sa_flags = 0;
sigaction (SIGCHLD, &sa, NULL); /* I want SIGCHLD to be handled by SIG_DFL */
pthread_sigmask (SIG_UNBLOCK, &set, NULL);
Run Code Online (Sandbox Code Playgroud)
在任何pthread操作之前,尽可能设置所有信号处理程序和掩码等.在可能的情况下,不要更改信号处理程序和掩码(您可能需要在fork()调用之前和之后执行此操作).
如果你需要一个信号处理程序SIGCHLD(而不是依赖SIG_DFL),如果可能的话让任何线程接收它,并使用自管方法或类似方法来警告主程序.
如果您必须具有执行/不处理某些信号的线程,请尝试将自己限制pthread_sigmask在相关线程中而不是sig*调用.
为了防止你遇到我遇到的下一个问题,请确保在你fork()完成之后再次设置从头开始(在孩子中)的信号处理,而不是依赖于你可能从父进程继承的任何东西.如果有一件事比混有pthread的信号更糟糕的话,它的信号与pthread混合在一起fork().
注意我无法完全解释为什么 change(1)有效,但它已经修复了对我来说非常相似的问题,并且毕竟依赖于以前"未指定"的东西.它最接近你的"假设2",但我认为它对传统信号函数的模拟真的是不完全的(特别是模仿之前的活泼行为signal()导致它首先被它取代sigaction()- 但这只是猜测).
顺便提一下,我建议您使用wait4()或(因为您没有使用rusage)waitpid()而不是wait3(),因此您可以指定要等待的特定PID.如果你有其他的东西可以生成孩子(我有一个图书馆这样做),你可能最终会等待错误的事情.那就是说,我不认为这就是这里发生的事情.