Linux 下的一個進程打開一個日志文件,不定期地往該文件里寫入日志。此時可以在控制台使用 mv 命令給該日志文件改個名字或者用 rm 命令把這個日志文件刪除掉。Linux 下是允許這么干的!對於改日志文件名的情形還好一點,后續的日志還是會寫入更名后的文件里,只是會影響后面日志文件自動清理功能(比如把日志文件名改得像個進程文件名);而對於刪除文件的情形,就直接導致后續的日志無法計入日志文件,一直到第二天凌晨日志文件切換時才回復正常。
為此,增加了一個日志文件保護功能,放在一個獨立的線程 logGuardEntry 里運行。這個保護功能主要是定期檢查當前所使用的日志文件是否存在,以及日志記錄是否正常,若檢測到異常,則關閉當前日志輸出的文件句柄,並重新打開所使用的文件,文件不存在則重建。
用 debug 版調試運行,功能正常后生成 release 版,卻測試發現增加的日志文件保護功能沒有起作用。用 gdb 掛上進程查看線程棧,想看下 logGuardEntry 線程內部出了什么狀況,結果發現根本就沒有 logGuardEntry 這個線程!
仔細排查,才發現問題和 daemonInit 函數調用有關系。main 函數里相關調用示意如下:
int main() { ... startLogWriter(...); ... #ifndef _DEBUG daemonInit(); #endif ... }
startLogWriter 函數體末尾有一行:
startThread(logGuardEntry,...);
即啟動一個以 logGuardEntry 為入口函數的線程,實施日志文件保護的功能。
daemonInit 函數里有如下代碼段:
void daemonInit() { ... pid = fork(); if (pid != 0) { exit(0); } ... }
這是讓程序以守護進程運行的通常做法,即讓主進程退出,而讓子進程經過進一步處理成為守護進程繼續運行。但是 fork 調用生成的子進程在 fork() 這條語句完成時,只會有一個線程,即調用 fork 的線程(上面就是 main 所在的線程,即主線程)。這是 Linux 出於某種合理的考慮而這樣設計的。上面的 main 函數里,先調用了 startLogWriter,里面會啟動一個 logGuardEntry 線程,這時主進程有兩個線程在運行;而隨后調用 daemonInit,導致主進程退出,而子進程卻丟掉了 logGuardEntry 線程,導致測試時發現日志保護功能根本不起作用。
當然,這個問題改起來很簡單,把 startLogWriter 調用放到 daemonInit 之后就好了,即:
int main() { ... #ifndef _DEBUG daemonInit(); #endif startLogWriter(...); ... }
就是說,對於以守護進程運行的后台程序而言,daemonInit 調用盡量早一些做,尤其不要在調用 daemonInit 之前啟動工作線程。
由 fork 調用的工作機制,不禁會想:子進程是不是可以沒有 main() 函數所在的線程,即所謂主線程(比如,把上面的 daemonInit 調用挪到 logGuardEntry 線程里調用)?
實際試驗了一下,果然是可以的。以下是用 gdb 掛上進程看到的內情: