我們先來了解一下什麼是守護進程?
守護進程
守護進程也稱精靈進程(Daemon)
它是運⾏在後臺的⼀種特殊進程。它獨⽴於控制終端並且週期性地執⾏某種任務或等待處理某些發⽣的事件。守護進程是⼀種很有⽤的進程。Linux的⼤多數服務器就是⽤守護進程實現的。⽐如, Internet服務器inetd, Web服務器httpd等。同時,守護進程完成許多系統任務。⽐如,作業規劃進程crond等。
Linux系統啓動時會啓動很多系統服務進程,這些系統服務進程沒有控制終端,不能直接和⽤戶交互。其它進程都是在⽤戶登錄或運⾏程序時創建,在運⾏結束或⽤戶註銷時終⽌,但系統服務進程不受⽤戶登錄註銷的影響,它們⼀直在運⾏着。
守護進程的特點
- 守護進程7*24小時不間斷運行,即時刻運行着,不受用戶登錄註銷的影響。
- 守護進程自成進程組。
- 守護進程的父進程爲序號爲1,是孤兒進程。
- 守護進程自成會話。關於會話,新啓一個終端即新建一個會話,且bash作爲該會話的首進程;關閉一個終端即關閉一個會話。
- 守護進程沒有控制終端,不能直接和用戶交互。
- 守護進程命名上以d結尾。
查看守護進程的命令
ps axj
凡是TPGID⼀欄寫着-1的都是沒有控制終端的進程,也就是守護進程。
在COMMAND⼀列⽤[]括起來的 名字表⽰內核線程,這些線程在內核⾥創建,沒有⽤戶空間代碼,因此沒有程序⽂件名和命令⾏。
init進程我們已經很熟悉了
udevd負責維護/dev⽬錄下的 設備⽂件
acpid負責電源管理
syslogd負責維護/var/log下的⽇志⽂件
可以看出,守護進程通 常採⽤以d結尾的名字,表⽰Daemon。下面來看一下詳細的參數信息
ps axj | grep -E "d$"
守護進程存在的原因
daemon函數存在的原因是因爲控制終端由於某些原因(如斷開終端鏈接)會發送一些信號的原因。而接收處理這些信號的缺省動作會讓進程退出。這些信號會由於終端上敲一些特殊按鍵而產生。
守護進程和後臺進程的區別
- 守護進程是後臺進程,後臺進程不一定是守護進程
- 守護進程運行是與終端無關的,是不能往終端上打消息的
- 守護進程的會話組和當前目錄,文件描述符都是獨立的。後臺運行只是終端進行了一次fork,讓程序在後臺執行
setsid函數
創建守護進程最關鍵的⼀步是調⽤setsid函數創建⼀個新的Session,併成爲Session Leader。
#include <unistd.h>
pid_t setsid(void);
該函數調⽤成功時返回新創建的Session的id(也就是當前進程的id),出錯返回-1。
該函數成功調⽤的結果是:
- 創建⼀個新的Session,當前進程成爲Session Leader,當前進程的id就是Session的id。
- 創建⼀個新的進程組,當前進程成爲進程組的Leader,當前進程的id就是進程組的id。
- 如果當前進程原本有⼀個控制終端,則它失去這個控制終端,成爲⼀個沒有控制終端的進程。所謂失去控制終端是指,原來的控制終端仍然是打開的,仍然可以讀寫,但只是⼀個普通的打開⽂件⽽不是控制終端了
注意,調⽤這個函數之前,當前進程不允許是進程組的Leader,否則該函數返回-1。要保證當前進程不是進程組的Leader也很容易,只要先fork再調⽤setsid就⾏了。 fork創建的⼦進程和⽗進程在同⼀個進程組中,進程組的Leader必然是該組的第⼀個進程,所以⼦進程不可能是該組的第⼀個進程,在⼦進程中調⽤setsid就不會有問題了。
守護進程的創建
- 代碼如下:
1 #include <stdio.h>
2 #include <signal.h>
3 #include <unistd.h>
4 #include <stdlib.h>
5 #include <fcntl.h>
6 #include <sys/stat.h>
7
8 void mydaemon()
9 {
10 int i = 0;
11 int fd0;
12 struct sigaction sa;
13 pid_t id;
14 umask(0);
15 if((id = fork()) < 0)
16 {
17 perror("fork");
18 return;
19 }
20 else if(id != 0) // father
21 {
22 exit(0);
23 }
24 setsid();
25
26 sa.sa_handler = SIG_IGN;
27 sigemptyset(&sa.sa_mask);
28 sa.sa_flags = 0;
29 if(sigaction(SIGCHLD, &sa, NULL) < 0)
30 {
31 return;
32 }
33 if((id = fork()) < 0)
34 {
35 perror("fork");
36 return;
37 }
38 else if(id != 0)
39 {
40 exit(0);
41 }
42 if(chdir("/") < 0)
43 {
44 perror("chdir");
45 return;
46 }
47 close(0);
48 fd0 = open("/dev/null", O_RDWR);
49 dup2(fd0, 1);
50 dup2(fd0, 2);
51 }
52
53 int main()
54 {
55 mydaemon();
56 while(1);
57 return 0;
58 }
- 步驟
- 調⽤umask將⽂件模式創建屏蔽字設置爲0.
- 調⽤fork,⽗進程退出( exit) 。
(1)如果該守護進程是作爲⼀條簡單的shell命令 啓動的,那麼⽗進程終⽌使得shell認爲該命令已經執⾏完畢。
(2)保證⼦進程不是⼀個進程組的組長進程。- 調⽤setsid創建⼀個新會話。
(1)調⽤進程成爲新會話的⾸進程。
(2)調⽤ 進程成爲⼀個進程組的組長進程 。
(3)調⽤進程沒有控制終端。(再次fork⼀次,保證 daemon進程,之後不會打開tty設備)- 將當前⼯作⽬錄更改爲根⽬錄。
- 關閉不在需要的⽂件描述符。
- 其他:忽略SIGCHLD信號。
創建過程中fork幾次?
在上述代碼中,我fork了兩次。也有人在創建過程中fork一次的,但是結果相同。下面來看看有何區別。
- 要知道在創建守護進程的時候fork一次和fork兩次兩者有什麼區別,就要先知道第一次fork和第二次fork都起到了什麼作用:
- (1)調用一次fork的作用:
- 第一次fork的作用是讓shell認爲這條命令已經終止,不用掛在終端輸入上,還有就是爲了後面的setsid服務,因爲調用setsid函數的進程不能是進程組組長,如果不fork出子進程,則此時的父進程是進程組組長,就無法調用setsid。當子進程調用完setsid函數之後,子進程是會話組長也是進程組組長,並且脫離了控制終端,此時,不管控制終端如何操作,新的進程都不會收到一些信號使得進程退出。
- (2)第二次fork的作用:
- 雖然當前關閉了和終端的聯繫,但是後期可能會誤操作打開了終端。只有會話首進程能打開終端設備,也就是再fork一次,再把父進程退出,再次fork的子進程作爲守護進程繼續運行,保證了該精靈進程不是對話期的首進程。
由此可見,第二次不是必須的,而是可選的,市面上有些開源項目也是fork一次。