上下文是这个Redis问题。我们有一个wait3()
调用,等待AOF重写子进程在磁盘上创建新的AOF版本。当子进程完成后,父进程通过wait3()
被通知以将旧的AOF替换为新的AOF。
但是,在上述问题的背景下,用户向我们报告了一个错误。我修改了Redis 3.0的实现,以便在wait3()
返回-1而不是因此意外情况崩溃时清晰地记录日志。所以显然会发生以下情况:
- 当有挂起的子进程需要等待时,调用
wait3()
。 SIGCHLD
应该设置为SIG_DFL
,但Redis中根本没有设置这个信号的代码,因此这是默认行为。- 从第二次AOF重写(第二个子进程创建)开始,
wait3()
开始返回-1。 - 我认为在当前的代码中不可能在没有挂起子进程的情况下调用
wait3()
,因为当创建AOF子进程时,我们将server.aof_child_pid
设置为pid的值,而只有在成功的wait3()
调用后才会重置它。
因此,wait3()
没有理由以-1和ECHILD
失败,但实际上确实失败了,所以可能出现某些意外情况导致僵尸子进程未创建。
假设1:Linux在某些奇怪的条件下是否有可能丢弃僵尸子进程,例如因为内存压力?看起来不合理,因为僵尸进程只是附加了元数据,但谁知道呢。
请注意,我们使用WNOHANG
调用wait3()
。并且考虑到SIGCHLD
默认设置为SIG_DFL
,唯一导致失败并返回-1和ECHLD
的条件应该是没有可用的僵尸进程来报告信息。
假设2:另一件可能发生的事情,但如果发生,就没有解释,那就是在第一个子进程死亡后,将SIGCHLD
处理程序设置为SIG_IGN
,导致wait3()
返回-1和ECHLD
。
假设3:有没有一种方法可以从外部删除僵尸子进程?也许该用户有某种脚本会在后台中删除僵尸进程,因此然后信息对wait3()
不再可用?我的理解是,如果父进程不等待它(使用waitpid
或处理信号),并且如果SIGCHLD
不被忽略,那么永远不应该能够删除僵尸进程,但是也许有一些Linux特定的方式。
假设4:实际上,在Redis代码中存在某些错误,使我们第一次成功地wait3()
了子进程而没有正确重置状态,而后来我们一遍又一遍地调用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
的选定部分:
<code>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;
}
</code>
正如您所看到的,所有的代码路径都必须执行
cleanup
代码以将server.aof_child_pid
重置为-1。
Redis在问题期间记录的错误
21353:C 29 Nov 04:00:29.957 * AOF rewrite: 8 MB of memory used by copy-on-write
27848:M 29 Nov 04:00:30.133 ^@ wait3() returned an error: No child processes. rdb_child_pid = -1, aof_child_pid = 21353
如您所见,aof_child_pid
不是-1。
signal()
替换为sigaction()
。 - alkSIG_DFL
)。因此,假设2是可能发生的。只需用sigaction()
替换signal()
调用(它不会重置为 SIG_DFL),以查看是否属实。 - P.Prdb_child_pid / server.aof_child_pid
标识的进程? - nos