一個 Linux 後台程式編程案例分析

Linux 下的一個進程打開一個日誌文件,不定期地往該文件里寫入日誌。此時可以在控制台使用 mv 命令給該日誌文件改個名字或者用 rm 命令把這個日誌文件刪除掉。Linux 下是允許這麼乾的!對於改日誌文件名的情形還好一點,後續的日誌還是會寫入更名後的文件里,只是會影響後面日誌文件自動清理功能(比如把日誌文件名改得像個進程文件名);而對於刪除文件的情形,就直接導致後續的日誌無法計入日誌文件,一直到第二天凌晨日誌文件切換時才回復正常。

為此,增加了一個日誌文件保護功能,放在一個獨立的執行緒 logGuardEntry 里運行。這個保護功能主要是定期檢查當前所使用的日誌文件是否存在,以及日誌記錄是否正常,若檢測到異常,則關閉當前日誌輸出的文件句柄,並重新打開所使用的文件,文件不存在則重建。

用 debug 版調試運行,功能正常後生成 release 版,卻測試發現增加的日誌文件保護功能沒有起作用。用 gdb 掛上進程查看執行緒棧,想看下 logGuardEntry 執行緒內部出了什麼狀況,結果發現根本就沒有 logGuardEntry 這個執行緒!

仔細排查,才發現問題和 daemonInit 函數調用有關係。main 函數里相關調用示意如下:

int main()
{
    ...
    startLogWriter(...);
    ...
#ifndef _DEBUG
    daemonInit();
#endif
    ...
}

startLogWriter 函數體末尾有一行:

startThread(logGuradEntry,...);

 即啟動一個以 logGuardEntry 為入口函數的執行緒,實施日誌文件保護的功能。

daemonInit 函數里有如下程式碼段:

void daemonInit()
{
    ...
    pid = fork();
    if (pid != 0) {
        exit(0);
    }
    ...
}

這是讓程式以守護進程運行的通常做法,即讓主進程退出,而讓子進程經過進一步處理成為守護進程繼續運行。但是 fork 調用生成的子進程在 fork() 這條語句完成時,只會有一個執行緒,即調用 fork 的執行緒(上面就是 main 所在的執行緒,即主執行緒)。這是 Linux 出於某種合理的考慮而這樣設計的。上面的 main 函數里,先調用了 startLogWriter,裡面會啟動一個 logGuard 執行緒,這時主進程有兩個執行緒在運行;而隨後調用 daemonInit,導致主進程退出,而子進程卻丟掉了 logGuard 執行緒,導致測試時發現日誌保護功能根本不起作用。

當然,這個問題改起來很簡單,把 startLogWriter 調用放到 daemonInit 之後就好了,即:

int main()
{
    ...
#ifndef _DEBUG
    daemonInit();
#endif
    startLogWriter(...);
    ...
}

就是說,對於以守護進程運行的後台程式而言,daemonInit 調用盡量早一些做,尤其不要在調用 daemonInit 之前啟動工作執行緒。

由 fork 調用的工作機制,不禁會想:子進程是不是可以沒有 main() 函數所在的執行緒,即所謂主執行緒(比如,把上面的 daemonInit 調用挪到 logGuard 執行緒里調用)?

實際試驗了一下,果然是可以的。以下是用 gdb 掛上進程看到的內情:

 

Tags: