簡體   English   中英

wait3(waitpid別名)返回-1,並且errno設置為ECHILD時不應該

[英]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時清楚地記錄,而不是因為這種意外情況而崩潰。 所以這就是顯而易見的事情:

  1. 當我們有待等待的子項時,將調用wait3()
  2. SIGCHLD應設置為SIG_DFL ,Redis中根本沒有設置此信號的代碼,因此這是默認行為。
  3. 當第一次AOF重寫發生時, wait3()成功地按預期工作。
  4. 從第二次AOF重寫開始(第二個子項創建), 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

附錄: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;
}

如您所見,所有代碼路徑必須執行將server.aof_child_pid重置為-1的cleanup代碼。

Redis在問題期間記錄的錯誤

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()將返回-1ECHLD因為孩子將自動返回收獲。

其次,使用pthreads信號處理(我認為你在這里使用)是非常困難的。 它的工作方式(我相信你知道)是過程導向的信號被發送到信號未被屏蔽的過程中的任意線程。 但是雖然線程有自己的信號掩碼,但是有一個進程范圍的動作處理程序。

將這兩件事放在一起,我認為你遇到了我以前碰過的問題。 我在使用signal()遇到了SIGCHLD處理的問題(這在pthreads之前已被棄用,這是公平的),這是通過轉移到sigaction並仔細設置每個線程信號掩碼來修復的。 我當時的結論是,C庫正在模仿(帶有sigaction )我告訴它對signal()做什么,但是被pthreads絆倒了。

請注意,您當前依賴於未指定的行為 signal(2) 手冊頁 signal(2)

signal()在多線程進程中的影響未指定。

這是我建議你做的:

  1. 轉到sigaction()pthread_sigmask() 顯式設置您關心的所有信號的處理(即使您認為這是當前的默認值),即使將它們設置為SIG_IGNSIG_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);
  1. 在任何pthread操作之前,盡可能設置所有信號處理程序和掩碼等。 在可能的情況下,不要更改信號處理程序和掩碼(您可能需要在fork()調用之前和之后執行此操作)。

  2. 如果您需要SIGCHLD的信號處理程序(而不是依賴於SIG_DFL ),如果可能,請讓任何線程接收它,並使用自管道方法或類似方法來警告主程序。

  3. 如果必須具有處理/不處理某些信號的線程,請嘗試將自己限制在相關線程中的pthread_sigmask而不是sig*調用。

  4. 為了防止你遇到我遇到的下一個問題,請確保在你有fork()之后,再次設置從頭開始(在孩子中)的信號處理,而不是依賴於你可能繼承的任何東西。父進程。 如果有一件事比混有pthread的信號更糟糕的話,那就是用fork()與pthread混合的信號。

注意我無法完全解釋為什么 change(1)有效,但它已經修復了對我來說非常相似的問題,並且畢竟依賴於以前“未指定”的東西。 它最接近你的'假設2',但我認為這是傳統信號函數的非常不完整的仿真(特別是模仿signal()的先前的行為,這是導致它首先被sigaction()取代的原因 - 但是這個只是一個猜測)。

順便提一下,我建議你使用wait4()或(因為你沒有使用rusagewaitpid()而不是wait3() ,所以你可以指定一個特定的PID來等待。 如果你有其他的東西可以生成孩子(我有一個圖書館這樣做),你可能最終會等待錯誤的事情。 那就是說,我不認為這就是這里發生的事情。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM