[英]wait3 (waitpid alias) returns -1 with errno set to ECHILD when it should not
上下文就是這個Redis問題 。 我們有一個wait3()
調用,等待AOF重寫子wait3()
在磁盤上創建新的AOF版本。 當孩子完成后,通過wait3()
通知父母,以便用舊的AOF替換舊的AOF。
但是,在上述問題的上下文中,用戶向我們通知了一個錯誤。 我修改了一點Redis 3.0的實現,以便在wait3()
返回-1時清楚地記錄,而不是因為這種意外情況而崩潰。 所以這就是顯而易見的事情:
wait3()
。 SIGCHLD
應設置為SIG_DFL
,Redis中根本沒有設置此信號的代碼,因此這是默認行為。 wait3()
成功地按預期工作。 wait3()
開始返回-1。 AFAIK在當前代碼中我們無法調用wait3()
而沒有掛起的子wait3()
,因為在創建AOF子wait3()
時,我們將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()
但是不再有僵屍,所以它返回-1。 分析代碼看起來不可能,但也許我錯了。
另一件重要的事情: 我們過去從未見過這一點 。 這顯然只發生在這個特定的Linux系統中。
更新 :Yossi Gottlieb提出由於某種原因(在正常情況下,僅在此系統上發生),Redis進程中的另一個線程收到SIGCHLD
。 我們已經在bio.c
線程中屏蔽了SIGALRM
,也許我們也可以嘗試從I / O線程屏蔽SIGCHLD
。
調用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;
}
如您所見,所有代碼路徑必須執行將server.aof_child_pid
重置為-1的cleanup
代碼。
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))。 盡管如此,忽略SIGCHLD
的歷史BSD和System V行為也有所不同,因此確保被終止的孩子不會變成僵屍的唯一完全可移植的方法是捕獲SIGCHLD
信號並執行wait
(2)或類似。
以下是wait
(2)的手冊頁 :
POSIX.1-2001指定如果
SIGCHLD
的處置設置為SIG_IGN
或為SIGCHLD
設置SA_NOCLDWAIT
標志(請參閱sigaction
(2)),則終止的子節點不會成為僵屍並且調用wait()
或waitpid()
將阻止所有孩子終止,然后將errno設置為ECHILD
失敗。 (原始POSIX標准將SIGCHLD
設置為SIG_IGN
的行為未指定。注意,即使SIGCHLD
的默認處置是“忽略”,顯式設置SIG_IGN
的處置SIG_IGN
導致對僵屍進程子進程的不同處理。)Linux 2.6符合此規格。 但是,Linux 2.4(及更早版本)不會:如果在忽略SIGCHLD
時進行wait()
或waitpid()
調用,則調用的行為就像SIGCHLD
未被忽略一樣,即調用阻塞直到下一個子句終止,然后返回該子進程的ID和狀態。
請注意,如果信號的處理行為類似於SIG_IGN
,那么(在Linux 2.6+下)您將看到您所看到的行為 - 即wait()
將返回-1
和ECHLD
因為孩子將自動返回收獲。
其次,使用pthreads
信號處理(我認為你在這里使用)是非常困難的。 它的工作方式(我相信你知道)是過程導向的信號被發送到信號未被屏蔽的過程中的任意線程。 但是雖然線程有自己的信號掩碼,但是有一個進程范圍的動作處理程序。
將這兩件事放在一起,我認為你遇到了我以前碰過的問題。 我在使用signal()
遇到了SIGCHLD
處理的問題(這在pthreads之前已被棄用,這是公平的),這是通過轉移到sigaction
並仔細設置每個線程信號掩碼來修復的。 我當時的結論是,C庫正在模仿(帶有sigaction
)我告訴它對signal()
做什么,但是被pthreads
絆倒了。
請注意,您當前依賴於未指定的行為 。 從signal(2)
手冊頁 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);
在任何pthread
操作之前,盡可能設置所有信號處理程序和掩碼等。 在可能的情況下,不要更改信號處理程序和掩碼(您可能需要在fork()
調用之前和之后執行此操作)。
如果您需要SIGCHLD
的信號處理程序(而不是依賴於SIG_DFL
),如果可能,請讓任何線程接收它,並使用自管道方法或類似方法來警告主程序。
如果必須具有處理/不處理某些信號的線程,請嘗試將自己限制在相關線程中的pthread_sigmask
而不是sig*
調用。
為了防止你遇到我遇到的下一個問題,請確保在你有fork()
之后,再次設置從頭開始(在孩子中)的信號處理,而不是依賴於你可能繼承的任何東西。父進程。 如果有一件事比混有pthread的信號更糟糕的話,那就是用fork()
與pthread混合的信號。
注意我無法完全解釋為什么 change(1)有效,但它已經修復了對我來說非常相似的問題,並且畢竟依賴於以前“未指定”的東西。 它最接近你的'假設2',但我認為這是傳統信號函數的非常不完整的仿真(特別是模仿signal()
的先前的行為,這是導致它首先被sigaction()
取代的原因 - 但是這個只是一個猜測)。
順便提一下,我建議你使用wait4()
或(因為你沒有使用rusage
) waitpid()
而不是wait3()
,所以你可以指定一個特定的PID來等待。 如果你有其他的東西可以生成孩子(我有一個圖書館這樣做),你可能最終會等待錯誤的事情。 那就是說,我不認為這就是這里發生的事情。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.