上下文是Redis issue。我们有一个wait3()
调用,等待AOF重写子级在磁盘上创建新的AOF版本。当 child 完成后,将通过wait3()
通知 parent ,以便用新的AOF替换旧的AOF。
但是,在上述问题的背景下,用户通知了我们一个错误。我修改了Redis 3.0的实现,以清楚地记录wait3()
返回-1的时间,而不是由于此意外情况而崩溃。因此,显然是这样的:
当我们有未决 child 等待时,会调用
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 :可能发生的其他事情,但如果发生,则没有任何解释,是在第一个 child 去世后,
SIGCHLD
处理程序设置为SIG_IGN
,导致wait3()
返回-1和ECHLD
。假设3 :是否可以通过某种方式从外部移除僵尸 child ?也许此用户具有某种脚本,可以在后台删除僵尸进程,以使该信息不再可用于
wait3()
?据我所知,如果 parent 不等待僵尸(使用waitpid
或处理信号)并且不忽略SIGCHLD
,则永远不可能删除僵尸,但是也许有一些Linux特定的方式。假设4 :Redis代码中实际上存在一些错误,因此我们在没有正确重置状态的情况下第一次成功对 child 进行了
wait3()
,后来我们一次又一次地调用wait3()
,但是不再有僵尸,因此它返回-1 。分析代码似乎是不可能的,但也许我错了。另一个重要的事情:我们过去从未观察到这一点。显然,仅在此特定的Linux系统中会发生。
更新:Yossi Gottlieb提出,由于某种原因,Redis进程中的另一个线程会接收到
SIGCHLD
(通常不会发生,仅在此系统上会发生)。我们已经在SIGALRM
线程中屏蔽了bio.c
,也许我们也可以尝试从I/O线程中屏蔽SIGCHLD
。附录:Redis代码的选定部分
在其中调用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 {
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;
}
如您所见,所有代码路径都必须执行cleanup
代码,将server.aof_child_pid
重置为-1。Redis在问题期间记录的错误
如您所见,
aof_child_pid
不是-1。 最佳答案
TLDR:您当前依赖于signal
(2)的未指定行为;请谨慎使用sigaction
。
首先,SIGCHLD
很奇怪。从manual page中获取sigaction
;
这是wait
(2)的manual page的内容:
请注意,这样做的效果是,如果信号的处理方式像设置了SIG_IGN
一样,那么(在Linux 2.6+下)您将看到所看到的行为-即wait()
将返回-1
和ECHLD
,因为子级将被自动收割。
其次,众所周知,使用pthreads
(我认为您在此处使用)进行信号处理非常困难。它的工作方式(如我所确定的那样)是,进程定向信号被发送到进程中具有未屏蔽信号的任意线程。但是,尽管线程具有其自己的信号掩码,但存在一个进程范围的 Action 处理程序。
将这两件事放在一起,我认为您遇到的是我之前遇到的问题。我在使SIGCHLD
处理与signal()
一起工作时遇到了问题(这很公平,因为在pthreads之前已弃用),已通过移动到sigaction
并仔细设置每个线程信号掩码来解决。我当时的结论是C库正在(用sigaction
)模拟我告诉它与signal()
一起使用的内容,但被pthreads
绊倒了。
请注意,您当前正在依赖未指定的行为。从signal(2)
的manual page中:
这是我建议您执行的操作:
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);
pthread
操作之前,请尽可能设置所有信号处理程序和掩码等。尽可能不要更改信号处理程序和掩码(您可能需要在fork()
调用之前和之后进行此操作)。 SIGCHLD
的信号处理程序(而不是依赖SIG_DFL
),请尽可能让任何线程接收它,并使用self-pipe方法或类似的方法来警告主程序。 pthread_sigmask
而不是sig*
调用中。 fork()
之后,重新设置从头开始的信号处理(在子级中),而不要依赖于您可能从父级继承的任何东西处理。如果有比混合了pthread的信号更糟糕的事情,那就是混合了fork()
的pthread的信号。 请注意,我无法完全解释更改(1)为何有效的原因,但是它已经解决了对我来说看起来非常相似的问题,并且毕竟依赖于以前“未指定”的内容。它最接近您的“假设2”,但我认为这实际上是对遗留信号功能的不完全仿真(特别是仿真
signal()
的先前行为,这是首先导致它被sigaction()
取代的原因-但这只是一个猜测)。顺便说一句,我建议您使用
wait4()
或(因为您没有使用rusage
)waitpid()
而不是wait3()
,因此您可以指定要等待的特定PID。如果您还有其他产生 child 的东西(我有图书馆做过),您可能最终会等待错误的消息。就是说,我认为这不是正在发生的事情。关于c - wait3(waitpid别名)在不应将errno设置为ECHILD的情况下返回-1,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/33994543/