守護(hù)進(jìn)程
概念:
守護(hù)進(jìn)程,也就是通常所說(shuō)的 Daemon 進(jìn)程,是 Linux 中的后臺(tái)服務(wù)進(jìn)程。周期性的執(zhí)行某種任務(wù)或等待處理某些發(fā)生的事件。
Linux 系統(tǒng)有很多守護(hù)進(jìn)程,大多數(shù)服務(wù)都是用守護(hù)進(jìn)程實(shí)現(xiàn)的。比如:像我們的 tftp,samba,nfs 等相關(guān)服務(wù)。
UNIX 的守護(hù)進(jìn)程一般都命名為*d 的形式,如 httpd,telnetd 等等。
生命周期:
守護(hù)進(jìn)程會(huì)長(zhǎng)時(shí)間運(yùn)行,常常在系統(tǒng)啟動(dòng)時(shí)就開(kāi)始運(yùn)行,直到系統(tǒng)關(guān)閉時(shí)才終止。
守護(hù)進(jìn)程不依賴于終端
從終端開(kāi)始運(yùn)行的進(jìn)程都會(huì)依附于這個(gè)終端,這個(gè)終端稱為這些進(jìn)程的控制終端。當(dāng)控制終端被關(guān)閉時(shí),相應(yīng)的進(jìn)程都會(huì)被自動(dòng)關(guān)閉。咱們平常寫(xiě)進(jìn)程時(shí),一個(gè)死循環(huán)程序,咱們不知道有 ctrl+c 的時(shí)候,怎么關(guān)閉它呀,是不是關(guān)閉終端呀。也就是說(shuō)關(guān)閉終端的同時(shí)也關(guān)閉了我們的程序,但是對(duì)于守護(hù)進(jìn)程來(lái)說(shuō),其生命周期守護(hù)需要突破這種限制,它從開(kāi)始運(yùn)行,直到整個(gè)系統(tǒng)關(guān)閉才會(huì)退出,所以守護(hù)進(jìn)程不能依賴于終端。
查看守護(hù)進(jìn)程
ps axj
a: 顯示所有
x:顯示沒(méi)有控制終端的進(jìn)程
j:顯示與作業(yè)有關(guān)的信息(顯示的列):會(huì)話期 ID(SID),進(jìn)程組 ID(PGID),控制終端(TT),終端進(jìn)程組 ID(TRGID)
? 所有的守護(hù)進(jìn)程都是以超級(jí)用戶啟動(dòng)的(UID 為 0);
? 沒(méi)有控制終端(TTY 為?);
? 終端進(jìn)程組 ID 為 -1(TPGID 表示終端進(jìn)程組 ID,該值表示與控制終端相關(guān)的前臺(tái)進(jìn)程組,如果未和任何終端相關(guān),其值為 -1;
? 所有的守護(hù)進(jìn)程的父進(jìn)程:
歷史上,Linux 的啟動(dòng)一直采用 init 進(jìn)程;下面的命令用來(lái)啟動(dòng)服務(wù)。
這種方法有兩個(gè)缺點(diǎn):
1.?啟動(dòng)時(shí)間長(zhǎng)。init 進(jìn)程是串行啟動(dòng),只有前一個(gè)進(jìn)程啟動(dòng)完,才會(huì)啟動(dòng)下一個(gè)進(jìn)程。
2.?啟動(dòng)腳本復(fù)雜。init 進(jìn)程只是執(zhí)行啟動(dòng)腳本,不管其他事情。腳本需要自己處理各種情況,
這往往使得腳本變得很長(zhǎng)。
Systemd
就是為了解決這些問(wèn)題而誕生的。它的設(shè)計(jì)目標(biāo)是,為系統(tǒng)的啟動(dòng)和管理提供一套完整的解決方案。
根據(jù) Linux 慣例,字母 d 是守護(hù)進(jìn)程(daemon)的縮寫(xiě)。Systemd 這個(gè)名字的含義,就是它要守護(hù)整個(gè)系統(tǒng)。
進(jìn)程組、會(huì)話、控制終端
? 進(jìn)程組
shell 里的每個(gè)進(jìn)程都屬于一個(gè)進(jìn)程組,創(chuàng)建進(jìn)程組的目的是用于簡(jiǎn)化向組內(nèi)所有進(jìn)程發(fā)送信號(hào)的操作,即如果一個(gè)信號(hào)是發(fā)給一個(gè)進(jìn)程組,則這個(gè)組內(nèi)的所有進(jìn)程都會(huì)受到該信號(hào)【方便管理】。
? PGID 進(jìn)程組 ID
進(jìn)程組內(nèi)的所有進(jìn)程都有相同的 PGID,等于該組組長(zhǎng)的 PID。(進(jìn)程組組長(zhǎng):進(jìn)程組中有一個(gè)進(jìn)程擔(dān)當(dāng)組長(zhǎng)。進(jìn)程組 ID(PGID)等于進(jìn)程組組長(zhǎng)的進(jìn)程 ID。已知一個(gè)進(jìn)程,要得到該進(jìn)程所屬的進(jìn)程組 ID 可以調(diào)用 getpgrp。一個(gè)進(jìn)程可以通過(guò)另一個(gè)系統(tǒng)調(diào)用 setpgrp 來(lái)加入一個(gè)已經(jīng)存在的進(jìn)程組或者創(chuàng)建一個(gè)新的進(jìn)程組。
如果內(nèi)核支持 _POSIX_JOB_CONTROL(該宏被定義)則內(nèi)核會(huì)為 Shell 上的每一條命令行(可能由多個(gè)命令通過(guò)管道等連接)創(chuàng)建一個(gè)進(jìn)程組。從這點(diǎn)上看,進(jìn)程組不是進(jìn)程的概念,而是 shell 上才有,所以在 task_struct 里并沒(méi)有存儲(chǔ)進(jìn)程組 id 之類(lèi)的變量。
進(jìn)程組的生命周期到組中最后一個(gè)進(jìn)程終止或其加入其他進(jìn)程組(離開(kāi)本進(jìn)程組)為止。
會(huì)話
一般一個(gè)用戶登錄后新建一個(gè)會(huì)話,每個(gè)會(huì)話也有一個(gè) ID 來(lái)標(biāo)識(shí)(SID)。登錄后的第一個(gè)進(jìn)程叫做會(huì)話領(lǐng)頭進(jìn)程(session leader),通常是一個(gè) shell/bash。對(duì)于會(huì)話領(lǐng)頭進(jìn)程,其 PID=SID。
控制終端
一個(gè)會(huì)話一般會(huì)擁有一個(gè)控制終端用于執(zhí)行 IO 操作。會(huì)話的領(lǐng)頭進(jìn)程打開(kāi)一個(gè)終端之后, 該終端就成為該會(huì)話的控制終端。與控制終端建立連接的會(huì)話領(lǐng)頭進(jìn)程也稱為控制進(jìn)程 (controlling process) 。一個(gè)會(huì)話只能有一個(gè)控制終端。
前臺(tái)進(jìn)程組
該進(jìn)程組中的進(jìn)程能夠向終端設(shè)備進(jìn)行讀、寫(xiě)操作的進(jìn)程組。例如登陸 shell(例如 bash)通過(guò)調(diào)用 int tcsetpgrp(int fd, pid_t pgrp); 函數(shù)設(shè)置為某個(gè)進(jìn)程組 pgrp 關(guān)聯(lián)終端設(shè)備 fd,該函數(shù)執(zhí)行成功后,該進(jìn)程組 pgrp 成為前臺(tái)進(jìn)程組。
后臺(tái)進(jìn)程組
該進(jìn)程組中的進(jìn)程只能夠向終端設(shè)備寫(xiě)。
終端進(jìn)程組 ID
每個(gè)進(jìn)程還有一個(gè)屬性,終端進(jìn)程組 ID(TPGID),用來(lái)標(biāo)識(shí)一個(gè)進(jìn)程是否處于一個(gè)和終端相關(guān)的進(jìn)程組中。前臺(tái)進(jìn)程組中的進(jìn)程的 TPGID=PGID,后臺(tái)進(jìn)程組的 PGID≠TPGID。若該進(jìn)程和任何終端無(wú)關(guān),其值為 -1。通過(guò)比較他們來(lái)判斷一個(gè)進(jìn)程是屬于前臺(tái)進(jìn)程組,還是后臺(tái)進(jìn)程組。
進(jìn)程組、對(duì)話期和控制終端關(guān)系
進(jìn)程組、對(duì)話期和控制終端關(guān)系
- 每個(gè)會(huì)話有且只有一個(gè)前臺(tái)進(jìn)程組,但會(huì)有 0 個(gè)或者多個(gè)后臺(tái)進(jìn)程組。產(chǎn)生在控制終端上的輸入(Input)和信號(hào)(Signal)將發(fā)送給會(huì)話的前臺(tái)進(jìn)程組中的所有進(jìn)程。對(duì)于輸出(Output)來(lái)說(shuō),則是在前臺(tái)和后臺(tái)共享的,即前臺(tái)和后臺(tái)的打印輸出都會(huì)顯示在屏幕上。終端上的連接斷開(kāi)時(shí) (比如網(wǎng)絡(luò)斷開(kāi)或 Modem 斷開(kāi)), 掛起信號(hào)將發(fā)送到控制進(jìn)程(controlling process) 。一個(gè)用戶登錄后創(chuàng)建一個(gè)會(huì)話。一個(gè)會(huì)話中只存在一個(gè)前臺(tái)進(jìn)程組,但可以存在多個(gè)后臺(tái)進(jìn)程組。第一次登陸后第一個(gè)創(chuàng)建的進(jìn)程是 shell,也就是會(huì)話的領(lǐng)頭進(jìn)程,該領(lǐng)頭進(jìn)程缺省處于一個(gè)前臺(tái)進(jìn)程組中并打開(kāi)一個(gè)控制終端可以進(jìn)行數(shù)據(jù)的讀寫(xiě)。當(dāng)在 shell 里運(yùn)行一行命令后(不帶&)創(chuàng)建一個(gè)新的進(jìn)程組,命令行中如果有多個(gè)命令會(huì)創(chuàng)建多個(gè)進(jìn)程,這些進(jìn)程都處于該新建進(jìn)程組中,shell 將該新建的進(jìn)程組設(shè)置為前臺(tái)進(jìn)程組并將自己暫時(shí)設(shè)置為后臺(tái)進(jìn)程組。
舉例
- 打開(kāi)第一個(gè)終端執(zhí)行命令:
ping?127.0.0.1?-aq?|?grep?icmp?&??//?通過(guò)管道將兩個(gè)命令串接起來(lái) ping?–q 不顯示 timeout 信息,將其設(shè)置到后臺(tái)并 running
- 在第一個(gè)終端繼續(xù)執(zhí)行命令,在前臺(tái)再新建一個(gè)進(jìn)程組?!咀⒁鉀](méi)有&】
ping?127.0.0.1?-aq?|?grep?icmp?// 在前臺(tái)再新建一個(gè)進(jìn)程組,
- 開(kāi)啟第二個(gè)終端并運(yùn)行
?ps?axj?|?grep?pts/0??????即過(guò)濾只看 pts/0 里的會(huì)話
?PPID???PID??PGID???SID?TTY??????TPGID?STAT???UID???TIME?COMMAND
?2109??2111??2111??2111?pts/0?????2538?Ss????1000???0:01?bash
?2111??2503??2503??2111?pts/0?????2538?S?????1000???0:00?ping?127.0.0.1?-aq
?2111??2504??2503??2111?pts/0?????2538?S?????1000???0:00?grep?--color=auto?icmp
?2111??2538??2538??2111?pts/0?????2538?S+????1000???0:00?ping?127.0.0.2?-aq
?2111??2539??2538??2111?pts/0?????2538?S+????1000???0:00?grep?--color=auto?timeo
? SID 都是 2111,說(shuō)明大家都在一個(gè) Session 里
? 有三個(gè)進(jìn)程組 PGID 2111,2503 和 2538。我們可以看到用|連起來(lái)的 ping 和 grep 是在一個(gè)進(jìn)程組里的。
? 2538 這個(gè)進(jìn)程組是一個(gè)前臺(tái)的進(jìn)程組,因?yàn)槠?PGID==TGPID, 2503 這個(gè)進(jìn)程組是一個(gè)后臺(tái)進(jìn)程組
- 在第一個(gè)終端中執(zhí)行 Ctrl+C 在第二個(gè)終端里繼續(xù) ps axj | grep pts/0
?PPID???PID??PGID???SID?TTY??????TPGID?STAT???UID???TIME?COMMAND
?2109??2111??2111??2111?pts/0?????2111?Ss+???1000???0:01?bash
?2111??2503??2503??2111?pts/0?????2111?S?????1000???0:00?ping?127.0.0.1?-aq
?2111??2504??2503??2111?pts/0?????2111?S?????1000???0:00?grep?--color=auto?icmp
? 2538 那個(gè)前臺(tái)進(jìn)程組的所有進(jìn)程都消失了,說(shuō)明信號(hào)會(huì)發(fā)給前臺(tái)進(jìn)程組的所有進(jìn)程
? 2111,即 bash 所在的那個(gè)進(jìn)程組成為了前臺(tái)進(jìn)程組。
守護(hù)進(jìn)程創(chuàng)建流程
守護(hù)進(jìn)程創(chuàng)建流程如下:
1.?創(chuàng)建子進(jìn)程,父進(jìn)程退出?
2.?在子進(jìn)程中創(chuàng)建新會(huì)話?
3.?改變當(dāng)前目錄為根目錄?
4.?重設(shè)文件權(quán)限掩碼?
5.?關(guān)閉文件描述符?
1. 創(chuàng)建子進(jìn)程,父進(jìn)程退出
由于守護(hù)進(jìn)程是脫離控制終端的,因此,完成第一步后就會(huì)在 shell 終端里造成一程序已經(jīng)運(yùn)行完畢的假象。之后的所有后續(xù)工作都在子進(jìn)程中完成,而用戶在 shell 終端里則可以執(zhí)行其他的命令,從而在形式上做到了與控制終端的脫離。
由于父進(jìn)程已經(jīng)先于子進(jìn)程退出,會(huì)造成子進(jìn)程沒(méi)有父進(jìn)程,從而變成一個(gè)孤兒進(jìn)程。在 Linux 中,每當(dāng)系統(tǒng)發(fā)現(xiàn)一個(gè)孤兒進(jìn)程,就會(huì)自動(dòng)由 1 號(hào)進(jìn)程收養(yǎng)。原先的子進(jìn)程就會(huì)變成 init 進(jìn)程的子進(jìn)程。
2. 在子進(jìn)程中創(chuàng)建新會(huì)話
setsid()函數(shù)的作用。一個(gè)進(jìn)程調(diào)用 setsid()函數(shù)后,會(huì)發(fā)生如下事件:
??首先內(nèi)核會(huì)創(chuàng)建一個(gè)新的會(huì)話,并讓該進(jìn)程成為該會(huì)話的 leader 進(jìn)程,
??同時(shí)伴隨該 session 的建立,一個(gè)新的進(jìn)程組也會(huì)被創(chuàng)建,同時(shí)該進(jìn)程成為該進(jìn)程組的組長(zhǎng)。
??該進(jìn)程此時(shí)還沒(méi)有和任何控制終端關(guān)聯(lián)。若需要?jiǎng)t要另外調(diào)用 tcsetpgrp,前面講前臺(tái)進(jìn)程組時(shí)介紹過(guò)。
調(diào)用 setsid()有以下 3 個(gè)作用:
??讓進(jìn)程擺脫原會(huì)話的控制。
??讓進(jìn)程擺脫原進(jìn)程組的控制。
??讓進(jìn)程擺脫原控制終端的控制。
那么,在創(chuàng)建守護(hù)進(jìn)程時(shí)為什么要調(diào)用 setsid()函數(shù)呢?
讀者可以回憶一下創(chuàng)建守護(hù)進(jìn)程的第一步,在那里調(diào)用了 fork()函數(shù)來(lái)創(chuàng)建子進(jìn)程再令父進(jìn)程退出。由于在調(diào)用 fork()函數(shù)時(shí),子進(jìn)程全盤(pán)復(fù)制了父進(jìn)程的會(huì)話期、進(jìn)程組和控制終端等,雖然父進(jìn)程退出了,但原先的會(huì)話期、進(jìn)程組和控制終端等并沒(méi)有改變,因此,還不是真正意義上的獨(dú)立。而 setsid()函數(shù)能夠使進(jìn)程完全獨(dú)立出來(lái),從而脫離所有其他進(jìn)程和終端的控制。
詳細(xì)見(jiàn) man 2 setsid。
3. 改變當(dāng)前目錄為根目
這一步也是必要的步驟。使用 fork()創(chuàng)建的子進(jìn)程繼承了父進(jìn)程的當(dāng)前工作目錄。
由于在進(jìn)程運(yùn)行過(guò)程中,當(dāng)前目錄所在的文件系統(tǒng)(如“/mnt/usb”等)是不能卸載的,這對(duì)以后的使用會(huì)造成諸多的麻煩(如系統(tǒng)由于某種原因要進(jìn)入單用戶模式)。
因此,通常的做法是讓“/”作為守護(hù)進(jìn)程的當(dāng)前工作目錄,這樣就可以避免上述問(wèn)題。當(dāng)然,如有特殊需要,也可以把當(dāng)前工作目錄換成其他的路徑,如 /tmp。改變工作目錄的常見(jiàn)函數(shù)是 chdir()。
4. 重設(shè)文件權(quán)限掩碼
文件權(quán)限掩碼是指屏蔽掉文件權(quán)限中的對(duì)應(yīng)位。
例如,有一個(gè)文件權(quán)限掩碼是 050,它就屏蔽了文件組擁有者的可讀與可執(zhí)行權(quán)限。由于使用 fork()函數(shù)新建的子進(jìn)程繼承了父進(jìn)程的文件權(quán)限掩碼,這就給該子進(jìn)程使用文件帶來(lái)了諸多的麻煩。
因此,把文件權(quán)限掩碼設(shè)置為 0,可以大大增強(qiáng)該守護(hù)進(jìn)程的靈活性。設(shè)置文件權(quán)限掩碼的函數(shù)是 umask()。在這里,通常的使用方法為 umask(0)。即賦予最大的能力。
5. 關(guān)閉文件描述符
同文件權(quán)限掩碼一樣,用 fork()函數(shù)新建的子進(jìn)程會(huì)從父進(jìn)程那里繼承一些已經(jīng)打開(kāi)的文件。這些被打開(kāi)的文件可能永遠(yuǎn)不會(huì)被守護(hù)進(jìn)程讀或?qū)懀鼈円粯酉南到y(tǒng)資源,而且可能導(dǎo)致所在的文件系統(tǒng)無(wú)法被卸載。
在上面的第(2)步之后,守護(hù)進(jìn)程已經(jīng)與所屬的控制終端失去了聯(lián)系,因此,從終端輸入的字符不可能達(dá)到守護(hù)進(jìn)程,守護(hù)進(jìn)程中用常規(guī)方法(如 printf())輸出的字符也不可能在終端上顯示出來(lái)。
所以,文件描述符為 0、1 和 2 的 3 個(gè)文件(常說(shuō)的輸入、輸出和報(bào)錯(cuò)這 3 個(gè)文件)已經(jīng)失去了存在的價(jià)值,也應(yīng)被關(guān)閉。
代碼實(shí)現(xiàn)
/*
?關(guān)注一口 Linux
*/
#include??
#include??
#include??
#include?
#include?
#include??
#include?
#include?
int?main()
{
?pid_t?pid;
?int?i,?fd;
?char?*buf?=?"This?is?a?Daemonn";
?pid?=?fork();
?if?(pid?<?0)?{
??printf("Error?forkn");
??exit(1);
?}?
?
?/*?第一步,父進(jìn)程退出?*/
?if?(pid?>?0)?{
??exit(0);?
?}
?/*?第二步?*/
?setsid();
?/*?第三步?*/??
?chdir("/");??
?/*?第四步?*/
?umask(0);
?/*?第五步?*/??
?for(i?=?0;?i?<?getdtablesize();?i++)?
?{
??close(i);
?}
?
?/*?這時(shí)創(chuàng)建完守護(hù)進(jìn)程,以下開(kāi)始正式進(jìn)入守護(hù)進(jìn)程實(shí)際工作
??*?注意:由于此時(shí)守護(hù)進(jìn)程完全脫離了控制終端,因此,不能像其他普通進(jìn)程
??*?一樣通過(guò) printf 或者 perror 將錯(cuò)誤信息輸出到控制終端,一種通用的辦
??*?法是使用 syslog 服務(wù),將程序中的出錯(cuò)信息輸入到系統(tǒng)日志文件中。
??*?本程序著重演示創(chuàng)建守護(hù)進(jìn)程的步驟,暫不演示 syslog。
??*/
?while(1)?{
??if?((fd?=?open("/tmp/daemon.log",?
????O_CREAT|O_WRONLY|O_APPEND,?0600))?<?0)?{
???exit(1);
??}
??write(fd,?buf,?strlen(buf)?+?1);
??close(fd);
??sleep(10);
?}
?
?exit(0);
}
執(zhí)行結(jié)果
由上圖可見(jiàn):? 守護(hù)進(jìn)程 ./run 的 UID 為 0;? 沒(méi)有控制終端(TTY 為?);? 終端進(jìn)程組 ID 為 -1;? 守護(hù)進(jìn)程的父進(jìn)程為 1516,即 systemd。