• 正文
    • 守護進程
    • 查看守護進程
    • 進程組、會話、控制終端
    • 進程組、對話期和控制終端關(guān)系
    • 守護進程創(chuàng)建流程
    • 代碼實現(xiàn)
  • 相關(guān)推薦
申請入駐 產(chǎn)業(yè)圖譜

搞懂進程組、會話、控制終端關(guān)系,才能明白守護進程干嘛的?

2020/10/19
144
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

守護進程

概念:

守護進程,也就是通常所說的 Daemon 進程,是 Linux 中的后臺服務進程。周期性的執(zhí)行某種任務或等待處理某些發(fā)生的事件。

Linux 系統(tǒng)有很多守護進程,大多數(shù)服務都是用守護進程實現(xiàn)的。比如:像我們的 tftp,samba,nfs 等相關(guān)服務。

UNIX 的守護進程一般都命名為*d 的形式,如 httpd,telnetd 等等。

生命周期:

守護進程會長時間運行,常常在系統(tǒng)啟動時就開始運行,直到系統(tǒng)關(guān)閉時才終止。

守護進程不依賴于終端

從終端開始運行的進程都會依附于這個終端,這個終端稱為這些進程的控制終端。當控制終端被關(guān)閉時,相應的進程都會被自動關(guān)閉。咱們平常寫進程時,一個死循環(huán)程序,咱們不知道有 ctrl+c 的時候,怎么關(guān)閉它呀,是不是關(guān)閉終端呀。也就是說關(guān)閉終端的同時也關(guān)閉了我們的程序,但是對于守護進程來說,其生命周期守護需要突破這種限制,它從開始運行,直到整個系統(tǒng)關(guān)閉才會退出,所以守護進程不能依賴于終端。

查看守護進程

ps axj

a: 顯示所有

x:顯示沒有控制終端的進程

j:顯示與作業(yè)有關(guān)的信息(顯示的列):會話期 ID(SID),進程組 ID(PGID),控制終端(TT),終端進程組 ID(TRGID)

? 所有的守護進程都是以超級用戶啟動的(UID 為 0);

? 沒有控制終端(TTY 為?);

? 終端進程組 ID 為 -1(TPGID 表示終端進程組 ID,該值表示與控制終端相關(guān)的前臺進程組,如果未和任何終端相關(guān),其值為 -1;

? 所有的守護進程的父進程:

歷史上,Linux 的啟動一直采用 init 進程;下面的命令用來啟動服務。

這種方法有兩個缺點:
1.?啟動時間長。init 進程是串行啟動,只有前一個進程啟動完,才會啟動下一個進程。
2.?啟動腳本復雜。init 進程只是執(zhí)行啟動腳本,不管其他事情。腳本需要自己處理各種情況,
這往往使得腳本變得很長。

Systemd

就是為了解決這些問題而誕生的。它的設計目標是,為系統(tǒng)的啟動和管理提供一套完整的解決方案。

根據(jù) Linux 慣例,字母 d 是守護進程(daemon)的縮寫。Systemd 這個名字的含義,就是它要守護整個系統(tǒng)。

進程組、會話、控制終端

? 進程組

shell 里的每個進程都屬于一個進程組,創(chuàng)建進程組的目的是用于簡化向組內(nèi)所有進程發(fā)送信號的操作,即如果一個信號是發(fā)給一個進程組,則這個組內(nèi)的所有進程都會受到該信號【方便管理】。

? PGID 進程組 ID

進程組內(nèi)的所有進程都有相同的 PGID,等于該組組長的 PID。(進程組組長:進程組中有一個進程擔當組長。進程組 ID(PGID)等于進程組組長的進程 ID。已知一個進程,要得到該進程所屬的進程組 ID 可以調(diào)用 getpgrp。一個進程可以通過另一個系統(tǒng)調(diào)用 setpgrp 來加入一個已經(jīng)存在的進程組或者創(chuàng)建一個新的進程組。

如果內(nèi)核支持 _POSIX_JOB_CONTROL(該宏被定義)則內(nèi)核會為 Shell 上的每一條命令行(可能由多個命令通過管道等連接)創(chuàng)建一個進程組。從這點上看,進程組不是進程的概念,而是 shell 上才有,所以在 task_struct 里并沒有存儲進程組 id 之類的變量。

進程組的生命周期到組中最后一個進程終止或其加入其他進程組(離開本進程組)為止。

會話

一般一個用戶登錄后新建一個會話,每個會話也有一個 ID 來標識(SID)。登錄后的第一個進程叫做會話領(lǐng)頭進程(session leader),通常是一個 shell/bash。對于會話領(lǐng)頭進程,其 PID=SID。

控制終端

一個會話一般會擁有一個控制終端用于執(zhí)行 IO 操作。會話的領(lǐng)頭進程打開一個終端之后, 該終端就成為該會話的控制終端。與控制終端建立連接的會話領(lǐng)頭進程也稱為控制進程 (controlling process) 。一個會話只能有一個控制終端。

前臺進程組

該進程組中的進程能夠向終端設備進行讀、寫操作的進程組。例如登陸 shell(例如 bash)通過調(diào)用 int tcsetpgrp(int fd, pid_t pgrp); 函數(shù)設置為某個進程組 pgrp 關(guān)聯(lián)終端設備 fd,該函數(shù)執(zhí)行成功后,該進程組 pgrp 成為前臺進程組。

后臺進程組

該進程組中的進程只能夠向終端設備寫。

終端進程組 ID

每個進程還有一個屬性,終端進程組 ID(TPGID),用來標識一個進程是否處于一個和終端相關(guān)的進程組中。前臺進程組中的進程的 TPGID=PGID,后臺進程組的 PGID≠TPGID。若該進程和任何終端無關(guān),其值為 -1。通過比較他們來判斷一個進程是屬于前臺進程組,還是后臺進程組。

進程組、對話期和控制終端關(guān)系

進程組、對話期和控制終端關(guān)系

  1. 每個會話有且只有一個前臺進程組,但會有 0 個或者多個后臺進程組。產(chǎn)生在控制終端上的輸入(Input)和信號(Signal)將發(fā)送給會話的前臺進程組中的所有進程。對于輸出(Output)來說,則是在前臺和后臺共享的,即前臺和后臺的打印輸出都會顯示在屏幕上。終端上的連接斷開時 (比如網(wǎng)絡斷開或 Modem 斷開), 掛起信號將發(fā)送到控制進程(controlling process) 。一個用戶登錄后創(chuàng)建一個會話。一個會話中只存在一個前臺進程組,但可以存在多個后臺進程組。第一次登陸后第一個創(chuàng)建的進程是 shell,也就是會話的領(lǐng)頭進程,該領(lǐng)頭進程缺省處于一個前臺進程組中并打開一個控制終端可以進行數(shù)據(jù)的讀寫。當在 shell 里運行一行命令后(不帶&)創(chuàng)建一個新的進程組,命令行中如果有多個命令會創(chuàng)建多個進程,這些進程都處于該新建進程組中,shell 將該新建的進程組設置為前臺進程組并將自己暫時設置為后臺進程組。

舉例

  1. 打開第一個終端執(zhí)行命令:
ping?127.0.0.1?-aq?|?grep?icmp?&??//?通過管道將兩個命令串接起來 ping?–q 不顯示 timeout 信息,將其設置到后臺并 running
  1. 在第一個終端繼續(xù)執(zhí)行命令,在前臺再新建一個進程組?!咀⒁鉀]有&】
ping?127.0.0.1?-aq?|?grep?icmp?// 在前臺再新建一個進程組,
  1. 開啟第二個終端并運行
?ps?axj?|?grep?pts/0??????即過濾只看 pts/0 里的會話

?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,說明大家都在一個 Session 里

? 有三個進程組 PGID 2111,2503 和 2538。我們可以看到用|連起來的 ping 和 grep 是在一個進程組里的。

? 2538 這個進程組是一個前臺的進程組,因為其 PGID==TGPID, 2503 這個進程組是一個后臺進程組

  1. 在第一個終端中執(zhí)行 Ctrl+C 在第二個終端里繼續(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 那個前臺進程組的所有進程都消失了,說明信號會發(fā)給前臺進程組的所有進程

? 2111,即 bash 所在的那個進程組成為了前臺進程組。

守護進程創(chuàng)建流程

守護進程創(chuàng)建流程如下:

1.?創(chuàng)建子進程,父進程退出?
2.?在子進程中創(chuàng)建新會話?
3.?改變當前目錄為根目錄?
4.?重設文件權(quán)限掩碼?
5.?關(guān)閉文件描述符?

1. 創(chuàng)建子進程,父進程退出

由于守護進程是脫離控制終端的,因此,完成第一步后就會在 shell 終端里造成一程序已經(jīng)運行完畢的假象。之后的所有后續(xù)工作都在子進程中完成,而用戶在 shell 終端里則可以執(zhí)行其他的命令,從而在形式上做到了與控制終端的脫離。

由于父進程已經(jīng)先于子進程退出,會造成子進程沒有父進程,從而變成一個孤兒進程。在 Linux 中,每當系統(tǒng)發(fā)現(xiàn)一個孤兒進程,就會自動由 1 號進程收養(yǎng)。原先的子進程就會變成 init 進程的子進程。

2. 在子進程中創(chuàng)建新會話

setsid()函數(shù)的作用。一個進程調(diào)用 setsid()函數(shù)后,會發(fā)生如下事件:

??首先內(nèi)核會創(chuàng)建一個新的會話,并讓該進程成為該會話的 leader 進程,
??同時伴隨該 session 的建立,一個新的進程組也會被創(chuàng)建,同時該進程成為該進程組的組長。
??該進程此時還沒有和任何控制終端關(guān)聯(lián)。若需要則要另外調(diào)用 tcsetpgrp,前面講前臺進程組時介紹過。

調(diào)用 setsid()有以下 3 個作用:

??讓進程擺脫原會話的控制。
??讓進程擺脫原進程組的控制。
??讓進程擺脫原控制終端的控制。

那么,在創(chuàng)建守護進程時為什么要調(diào)用 setsid()函數(shù)呢?

讀者可以回憶一下創(chuàng)建守護進程的第一步,在那里調(diào)用了 fork()函數(shù)來創(chuàng)建子進程再令父進程退出。由于在調(diào)用 fork()函數(shù)時,子進程全盤復制了父進程的會話期、進程組和控制終端等,雖然父進程退出了,但原先的會話期、進程組和控制終端等并沒有改變,因此,還不是真正意義上的獨立。而 setsid()函數(shù)能夠使進程完全獨立出來,從而脫離所有其他進程和終端的控制。

詳細見 man 2 setsid。

3. 改變當前目錄為根目

這一步也是必要的步驟。使用 fork()創(chuàng)建的子進程繼承了父進程的當前工作目錄。

由于在進程運行過程中,當前目錄所在的文件系統(tǒng)(如“/mnt/usb”等)是不能卸載的,這對以后的使用會造成諸多的麻煩(如系統(tǒng)由于某種原因要進入單用戶模式)。

因此,通常的做法是讓“/”作為守護進程的當前工作目錄,這樣就可以避免上述問題。當然,如有特殊需要,也可以把當前工作目錄換成其他的路徑,如 /tmp。改變工作目錄的常見函數(shù)是 chdir()。

4. 重設文件權(quán)限掩碼

文件權(quán)限掩碼是指屏蔽掉文件權(quán)限中的對應位。

例如,有一個文件權(quán)限掩碼是 050,它就屏蔽了文件組擁有者的可讀與可執(zhí)行權(quán)限。由于使用 fork()函數(shù)新建的子進程繼承了父進程的文件權(quán)限掩碼,這就給該子進程使用文件帶來了諸多的麻煩。

因此,把文件權(quán)限掩碼設置為 0,可以大大增強該守護進程的靈活性。設置文件權(quán)限掩碼的函數(shù)是 umask()。在這里,通常的使用方法為 umask(0)。即賦予最大的能力。

5. 關(guān)閉文件描述符

同文件權(quán)限掩碼一樣,用 fork()函數(shù)新建的子進程會從父進程那里繼承一些已經(jīng)打開的文件。這些被打開的文件可能永遠不會被守護進程讀或?qū)懀鼈円粯酉南到y(tǒng)資源,而且可能導致所在的文件系統(tǒng)無法被卸載。

在上面的第(2)步之后,守護進程已經(jīng)與所屬的控制終端失去了聯(lián)系,因此,從終端輸入的字符不可能達到守護進程,守護進程中用常規(guī)方法(如 printf())輸出的字符也不可能在終端上顯示出來。

所以,文件描述符為 0、1 和 2 的 3 個文件(常說的輸入、輸出和報錯這 3 個文件)已經(jīng)失去了存在的價值,也應被關(guān)閉。

代碼實現(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);
?}?
?
?/*?第一步,父進程退出?*/
?if?(pid?>?0)?{
??exit(0);?
?}
?/*?第二步?*/
?setsid();
?/*?第三步?*/??
?chdir("/");??
?/*?第四步?*/
?umask(0);
?/*?第五步?*/??
?for(i?=?0;?i?<?getdtablesize();?i++)?
?{
??close(i);
?}
?
?/*?這時創(chuàng)建完守護進程,以下開始正式進入守護進程實際工作
??*?注意:由于此時守護進程完全脫離了控制終端,因此,不能像其他普通進程
??*?一樣通過 printf 或者 perror 將錯誤信息輸出到控制終端,一種通用的辦
??*?法是使用 syslog 服務,將程序中的出錯信息輸入到系統(tǒng)日志文件中。
??*?本程序著重演示創(chuàng)建守護進程的步驟,暫不演示 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é)果

由上圖可見:? 守護進程 ./run 的 UID 為 0;? 沒有控制終端(TTY 為?);? 終端進程組 ID 為 -1;? 守護進程的父進程為 1516,即 systemd。

 

相關(guān)推薦

登錄即可解鎖
  • 海量技術(shù)文章
  • 設計資源下載
  • 產(chǎn)業(yè)鏈客戶資源
  • 寫文章/發(fā)需求
立即登錄

公眾號『一口Linux』號主彭老師,擁有15年嵌入式開發(fā)經(jīng)驗和培訓經(jīng)驗。曾任職ZTE,某研究所,華清遠見教學總監(jiān)。擁有多篇網(wǎng)絡協(xié)議相關(guān)專利和軟件著作。精通計算機網(wǎng)絡、Linux系統(tǒng)編程、ARM、Linux驅(qū)動、龍芯、物聯(lián)網(wǎng)。原創(chuàng)內(nèi)容基本從實際項目出發(fā),保持原理+實踐風格,適合Linux驅(qū)動新手入門和技術(shù)進階。