什么是daemon進程?
Unix/Linux中的daemon進程類似於Windows中的后台服務進程,一直在后台運行運行,例如http服務進程nginx,ssh服務進程sshd等。
注意,其英文拼寫為daemon而不是deamon。
為什么daemon進程需要特殊的編寫步驟?
daemon進程和普通進程不一樣嗎?為什么要單獨提出如何編寫daemon進程呢?
不知道你是否有過這樣的經歷,在Linux上面打開一個terminal,輸入編譯命令進行編譯,編譯的時間可能比較長,
這時候你不小心關閉了這個terminal,編譯就中斷了。因為編譯腳本是作為當前terminal的一個子進程來執行的,當terminal退出后,
子進程也就退出了。而作為daemon進程,我們希望一旦啟動就能在后台一直運行,不會隨着terminal的退出而結束。
那么如何能做到這一點呢?有人說用下面的命令行嗎?
> make &
讓編譯命令make到后台執行,這樣只是造成了make在后台一直運行的假象,它依然沒有脫離和terminal之間的父子關系;
當terminal退出后,make依然會退出。所以針對daemon進程就要用特殊的步驟來編寫,以保證在terminal中執行后,
即使terminal退出,daemon進程仍然在后台運行。
如何編寫daemon進程?
對於可以用多種方法解決的問題,我們一般只需熟練掌握其中一種最適合自己的即可;
但是需要知道還有其它的方法,以備不時之需,這里我將介紹三種創建daemon進程的方法。
1. 首先給出經典名著APUE中的方法:
#include "apue.h" #include <syslog.h> #include <fcntl.h> #include <sys/resource.h> void daemonize(const char *cmd){ int i, fd0, fd1, fd2; pid_t pid; struct rlimit rl; struct sigaction sa; /* * Clear file creation mask. */ umask(0);//注釋1 /* * Get maximum number of file descriptors. */ if (getrlimit(RLIMIT_NOFILE, &rl) < 0) err_quit("%s: can't get file limit", cmd); /* * Become a session leader to lose controlling TTY. */ if ((pid = fork()) < 0)//注釋2 err_quit("%s: can't fork", cmd); else if (pid != 0) /* parent */ exit(0); setsid();//注釋3 /* * Ensure future opens won't allocate controlling TTYs. */ sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGHUP, &sa, NULL) < 0) err_quit("%s: can't ignore SIGHUP", cmd); if ((pid = fork()) < 0)//注釋4 err_quit("%s: can't fork", cmd); else if (pid != 0) /* parent */ exit(0); /* * Change the current working directory to the root so * we won't prevent file systems from being unmounted. */ if (chdir("/") < 0)//注釋5 err_quit("%s: can't change directory to /", cmd); /* * Close all open file descriptors. */ if (rl.rlim_max == RLIM_INFINITY) rl.rlim_max = 1024; for (i = 0; i < rl.rlim_max; i++) close(i);//注釋6 /* * Attach file descriptors 0, 1, and 2 to /dev/null. */ fd0 = open("/dev/null", O_RDWR);//注釋7 fd1 = dup(0);//注釋7 fd2 = dup(0);//注釋7 /* * Initialize the log file. */ openlog(cmd, LOG_CONS, LOG_DAEMON); if (fd0 != 0 || fd1 != 1 || fd2 != 2) { syslog(LOG_ERR, "unexpected file descriptors %d %d %d",fd0, fd1, fd2); exit(1); } }
下面是針對上面例子的詳細解釋:
* 注釋1:因為我們從shell創建的daemon子進程,所以daemon子進程會繼承shell的umask,如果不清除的話,會導致daemon進程創建文件時屏蔽某些權限。
* 注釋2:fork后讓父進程退出,子進程獲得新的pid,肯定不為進程組組長,這是setsid前提。
* 注釋3:調用setsid來創建新的進程會話。這使得daemon進程成為會話首進程,脫離和terminal的關聯。
* 注釋4:最好在這里再次fork。這樣使得daemon進程不再是會話首進程,那么永遠沒有機會獲得控制終端。如果這里不fork的話,會話首進程依然可能打開控制終端。
* 注釋5:將當前工作目錄切換到根目錄。父進程繼承過來的當前目錄可能mount在一個文件系統上,如果不切換到根目錄,那么這個文件系統不允許unmount。
* 注釋6:在子進程中關閉從父進程中繼承過來的那些不需要的文件描述符。可以通過_SC_OPEN_MAX來判斷最高文件描述符(不是很必須).
* 注釋7:打開/dev/null復制到0,1,2,因為dameon進程已經和terminal脫離了,所以需要重新定向標准輸入,標准輸出和標准錯誤(不是很必須).
針對這個例子,首先要說明的是,不管在Unix還是Linux上按照這個例子寫的daemon肯定沒問題。
不過我對其中的一些步驟的必要性一直持懷疑態度:
1) 第二個fork是必須的嗎?
根據APUE中的說法是,這是為了防止后面打開終端的時候又關聯到了daemon進程上,這樣當終端關閉后,daemon進程就退出了,
不過個人感覺這種說法有可能已經不再適用了,畢竟大名鼎鼎的nginx也沒有fork兩次。不過目前我還不知道怎么用實驗來證明這個結論。
2) setsid()是必須的嗎?
按照書上說的是每個進程都屬於一個進程組(Process Group),每個進程組都屬於一個進程會話(Process Session)。
這三者的關系如下圖所示,當terminal退出的時候,以最初login shell為首的進程回話就結束了。
這時候,屬於這個session的所有進程都會收到SIGHUP信號,導致進程退出。
執行了第一次fork(),父進程退出了,子進程變成孤兒進程過繼給了1號init進程,但是它仍然屬於當前登錄shell所控制的session,
調用setsid()的目的是讓daemon進程形成獨立的Session,這樣當terminal退出的時候就影響不到這個daemon進程了。
但是我在各種Unix,Linux系統上做了實驗,不調用setsid(), 並且只fork()一次,然后將當前終端關閉,重新打開一個新的終端,
發現daemon進程仍然存在,並沒有像書中所說會隨着terminal的退出而退出,請高人指點迷津。
2. 利用系統庫函數daemon()創建daemon進程
Linux系統還專門提供了一個用來創建daemon進程的系統函數:
int daemon(int nochdir, int noclose);
從api的文檔描述看該api也調用了fork(),估計內部實現和上面的代碼邏輯類似,從其參數作用也可以看出這一點,
這個api有兩個參數,其作用分別對應上面代碼中的注釋5和注釋7。下面是用這個api創建daemon進程的簡單示例:
#include <unistd.h> #include <stdlib.h> int main(void) { if(daemon(0,0) == -1) exit(EXIT_FAILURE); while(1) { sleep(60); } return 0; }
3. 使用第三方工具supervisor
簡單的說supervisor是一個python工具,可以通過編寫配置文件來對指定的進程進行管理,比如啟動進程,停止進程以及進程退出后自動重啟等;
這樣一來,即使一個普通進程通過supervisor管理之后也會變成和daemon進程一樣的行為,不會隨着terminal的關閉而退出。
后續我會寫一篇關於supervisor的文章專門介紹其具體使用方法。
參考資料
http://www.cnblogs.com/mickole/p/3188321.html
https://dirtysalt.github.io/apue.html#orgheadline174