Dameon進程又被稱做守護進程,一般來說他有以下2個特點:
1.生命周期非常長,一旦啟動,一般不會終止,直到系統推出,不過dameon進程可以通過stop或者發送信號將其殺死
2.在后台執行,不跟任何控制終端關聯,終端信號比如:SIGINT,SIGQUIT,SIGTSTP,以及關閉終端都不會影響deamon
如何編寫Daemon進程,需要遵循以下規則:
(1)
執行fork()函數,父進程退出,子進程繼續
執行這一步,原因有二:
·父進程有可能是進程組的組長(在命令行啟動的情況下),從而不能夠執行后面要執行的setsid函數,子進程繼承了父進程的進程組ID,並且擁有自己的進程ID,一定不會是進程組的組長,所以子進程一定可以執行后面要執行的setsid函數。
·如果daemon是從終端命令行啟動的,那么父進程退出會被shell檢測到,shell會顯示shell提示符,讓子進程在后台執行。
(2)
子進程執行如下三個步驟,以擺脫與環境的關系
1)
修改進程的當前目錄為根目錄(/)。
這樣做是有原因的,因為daemon一直在運行,如果當前工作路徑上包含有根文件系統以外的其他文件系統,那么這些文件系統將無法卸載。因此,常規是將當前工作目錄切換成根目錄,當然也可以是其他目錄,只要確保該目錄所在的文件系統不會被卸載即可。
chdir("/")
2)
調用setsid函數。這個函數的目的是切斷與控制終端的所有關系,並且創建一個新的會話。
這一步比較關鍵,因為這一步確保了子進程不再歸屬於控制終端所關聯的會話。因此無論終端是否發送SIGINT、SIGQUIT或SIGTSTP信號,也無論終端是否斷開,都與要創建的daemon進程無關,不會影響到daemon進程的繼續執行。
3)
設置文件模式創建掩碼為0。
umask(0)
這是為了讓daemon進程創建的文件權限屬性跟shell脫離關系,因為默認情況下,進程的umask來源於父進程shell的umask.如果不執行umask(0),那么父進程shell的umask就會影響daemon進程的umask.如果用戶改變了shell的umask,那么也就改變了dameon的umask,就會使得daemon進程每次執行的umask信息可能不一致
(3)
再次執行fork,父進程退出,子進程繼續
執行完前面兩步之后,可以說已經比較圓滿了:新建會話,進程是會話的首進程,也是進程組的首進程。進程ID、進程組ID和會話ID,三者的值相同,進程和終端無關聯。那么這里為何還要再執行一次fork函數呢?
原因是,daemon進程有可能會打開一個終端設備,即daemon進程可能會根據需要,執行類似如下的代碼:
int fd = open("/dev/console", O_RDWR);
這個打開的終端設備是否會成為daemon進程的控制終端,取決於兩點:
·daemon進程是不是會話的首進程。
·系統實現。(BSD風格的實現不會成為daemon進程的控制終端,但是POSIX標准說這由具體實現來決定)。
既然如此,為了確保萬無一失,只有確保daemon進程不是會話的首進程,才能保證打開的終端設備不會自動成為控制終端。因此,不得不執行第二次fork,fork之后,父進程退出,子進程繼續。這時,子進程不再是會話的首進程,也不是進程組的首進程了。
(4)
關閉標准輸入(stdin)、標准輸出(stdout)和標准錯誤(stderr)
因為文件描述符0、1和2指向的就是控制終端。daemon進程已經不再與任意控制終端相關聯,因此這三者都沒有意義。一般來講,關閉了之后,會打開/dev/null,並執行dup2函數,將0、1和2重定向到/dev/null。這個重定向是有意義的,防止了后面的程序在文件描述符0、1和2上執行I/O庫函數而導致報錯。
至此,即完成了daemon進程的創建,進程可以開始自己真正的工作了。
上述步驟比較繁瑣,對於C語言而言,glibc提供了daemon函數,從而幫我們將程序轉化成daemon進程。
#include <unistd.h>
int daemon(int nochdir, int noclose);
該函數有兩個入參,分別控制一種行為,具體如下。
其中的
nochdir,用來控制是否將當前工作目錄切換到根目錄。
·0:將當前工作目錄切換到/。
·1:保持當前工作目錄不變。
而
noclose,用來控制是否將標准輸入、標准輸出和標准錯誤重定向到/dev/null。
·0:將標准輸入、標准輸出和標准錯誤重定向到/dev/null。
·1:保持標准輸入、標准輸出和標准錯誤不變。
一般情況下,這兩個入參都要為0。
ret = daemon(0,0)
成功時,daemon函數返回0;失敗時,返回-1,並置errno。因為daemon函數內部會調用fork函數和setsid函數,所以出錯時errno可以查看fork函數和setsid函數的出錯情形。
glibc的daemon函數做的事情,和前面討論的大體一致,但是做得並不徹底,沒有執行第二次的fork。
進程的終止
在不考慮線程的情況下,進程的退出有以下5種方式。
正常退出有3種:
·從main函數return返回
·調用exit
·調用_exit
異常退出有兩種:
·
調用abort
·接收到信號,由信號終止
_exit函數的接口定義如下:
#include <unistd.h>
void _exit(int status);
用戶調用_exit函數,本質上是調用exit_group系統調用。這點在前面已經詳細介紹過,在此就不再贅述了。
exit函數
exit函數更常見一些,其接口定義如下:
#include <stdlib.h>
void exit(int status);
exit()函數的最后也會調用_exit()函數,但是exit在調用_exit之前,還做了其他工作:
1)執行用戶通過調用atexit函數或on_exit定義的清理函數。
2)關閉所有打開的流(stream),所有緩沖的數據均被寫入(flush),通過tmpfile創建的臨時文件都會被刪除。
3)調用_exit。
圖4-11給出了exit函數和_exit函數的差異。

下面介紹exit函數和_exit函數的不同之處。
首先是exit函數會執行用戶注冊的清理函數。用戶可以通過調用atexit()函數或on_exit()函數來定義清理函數。這些清理函數在調用return或調用exit時會被執行。執行順序與函數注冊的順序相反。當進程收到致命信號而退出時,注冊的清理函數不會被執行;當進程調用_exit退出時,注冊的清理函數不會被執行;當執行到某個清理函數時,若收到致命信號或清理函數調用了_exit()函數,那么該清理函數不會返回,從而導致排在后面的需要執行的清理函數都會被丟棄。
其次是exit函數會沖刷(flush)標准I/O庫的緩沖並關閉流。glibc提供的很多與I/O相關的函數都提供了緩沖區,用於緩存大塊數據。
緩沖有三種方式:無緩沖(_IONBF)、行緩沖(_IOLBF)和全緩沖(_IOFBF)。
·無緩沖:就是沒有緩沖區,每次調用stdio庫函數都會立刻調用read/write系統調用。
·行緩沖:對於輸出流,收到換行符之前,一律緩沖數據,除非緩沖區滿了。對於輸入流,每次讀取一行數據。
·全緩沖:就是緩沖區滿之前,不會調用read/write系統調用來進行讀寫操作。
對於后兩種緩沖,可能會出現這種情況:進程退出時,緩沖區里面可能還有未沖刷的數據。如果不沖刷緩沖區,緩沖區的數據就會丟失。比如行緩沖遲遲沒有等到換行符,又或者全緩沖沒有等到緩沖區滿。尤其是后者,很容易出現,因為glibc的緩沖區默認是8192字節。exit函數在關閉流之前,會沖刷緩沖區的數據,確保緩沖區里的數據不會丟失。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void foo()
{
fprintf(stderr,"foo says bye.\n");
}
void bar()
{
fprintf(stderr,"bar says bye.\n");
}
int main(int argc, char **argv)
{
atexit(foo);
atexit(bar);
fprintf(stdout,"Oops ... forgot a newline!");
sleep(2);
if (argc > 1 && strcmp(argv[1],"exit") == 0)
exit(0);
if (argc > 1 && strcmp(argv[1],"_exit") == 0)
_exit(0);
return 0;
- }
注意上面的示例代碼,fprintf打印的字符串是沒有換行符的,對於標准輸出流stdout,采用的是行緩沖,收到換行符之前是不會有輸出的。輸出情況如下:
manu@manu-hacks:exit$ ./test exit //調用exit結束,輸出了緩沖區的字符
bar says bye.
foo says bye.
Oops ... forgot a newline!manu@manu-hacks:exit$ //調用return 輸出了緩沖區字符
manu@manu-hacks:exit$
manu@manu-hacks:exit$ ./test
bar says bye.
foo says bye.
Oops ... forgot a newline!manu@manu-hacks:exit$ //直接調用_exit沒有輸出緩沖區的字符
manu@manu-hacks:exit$
manu@manu-hacks:exit$ ./test _exit
manu@manu-hacks:~/code/self/c/exit$
盡管緩沖區里的數據沒有等到換行符,但是無論是調用return返回還是調用exit返回,緩沖區里的數據都會被沖刷,“Oops...forgot a newline!”都會被輸出。因為exit()函數會負責此事。從測試代碼的輸出也可以看出,exit()函數首先執行的是用戶注冊的清理函數,然后才執行了緩沖區的沖刷。
第三,存在臨時文件,exit函數會負責將臨時文件刪除.
exit函數的最后調用了_exit()函數,最終殊途同歸,走向內核清理。
return退出
return是一種更常見的終止進程的方法。執行return(n)等同於執行exit(n),因為調用main()的運行時函數會將main的返回值當作exit的參數。