一文讀懂Linux進程、進程組、會話、殭屍

作者簡介

herongwei,北交碩士畢業,現就職於搜狗公司,後端開發工程師。從事 C++,Golang ,Linux 後端開發。

追求技術,熱愛編程與分享,希望能和大家多多交流學習~

座右銘:    認真的人,自帶光芒!

GitHub:     https://github.com/rongweihe

個人博客:   https://rongweihe.github.io/

1、前言

在研究 Linux 實現之前,首先要對進程、進程組、會話,線程有個整體的瞭解:一個會話包含多個進程組,一個進程組包含多個進程,一個進程包含多個線程。

2、進程控制

進程是 Linux 操作系統環境的基礎,它控制着系統上幾乎所有的活動。每個進程都有自己唯一的標識:進程 ID,也有自己的生命週期。進程都有父進程,父進程也有父進程,從而形成了一個以 init 進程 (PID = 1)爲根的家族樹。除此以外,進程還有其他層次關係:進程組和會話。

一個典型的進程的生命週期如下圖所示。

3、進程ID

Linux 下每個進程都會有一個非負整數表示的唯一進程 ID,簡稱 pid。

Linux 提供了getpid 函數來獲取進程的 pid,同時還提供了 getppid 函數來獲取父進程的 pid,相關接口定義如下:

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); 
pid_t getppid(void); 

每個進程都有自己的父進程,父進程又會有自己的父進程,最終都會追溯到 1 號進程即 init 進程。這就決定了操作系統上所有的進程必然會組成樹狀結構,就像一個家族的家譜一樣。可以通過 pstree 的命 令來查看進程的家族樹,如下圖所示。

procfs 文件系統會在 /proc 下爲每個進程創建一個目錄,名字是該進程的 pid。目錄下有很多文件, 用於記錄進程的運行情況和統計信息等。因爲進程有創建,也有終止,所以 /proc/ 下記錄進程信息的目錄(以及目錄下的文件)也會發生變化。如下圖所示:

4、進程創建

Linux 下創建新進程的系統調用是 fork。其定義如下:

#include <sys/types.h>
#include <unistd.h>
pid_t fork( void );

該函數的每次調用都會返回兩次:在父進程中返回的是子進程的 PID,在子進程中則返回 0 。該返回值是後續代碼用來判斷當前進程是父進程還是子進程的依據。

fork 函數會複製當前進程,在內核進程表中創建一個新的進程表項。新的進程表項有很多屬性和原進程相同,比如堆指針、棧指針和標誌寄存器的值。但也有許多屬性被賦予了新的值,比如該進程的 PPID 被設置成了原進程的 PID,信號位圖被清除(也就是原進程設置的信號處理的函數不再對新進程起作用)。

子進程的代碼與父進程完全相同,同時它還會複製父進程的教據(堆數據、棧數據和靜態數據)。

數據的複製採用的是所謂的寫時複製(copy on writte),即只有在任一進程(父進程或子進程)對數據執行了寫操作時,複製纔會發生(先是缺頁中斷,然後操作系統給子進程分配內存並複製父進程的數據)。

如果我們在程序中分配了大量內存,那麼使用 fork 時也應當謹慎,避免沒必要的內存分配和數據複製。

此外,創建子進程後,父進程中打開的文件描述符默認在子進程中也是打開的,且文件描述符的引用計數加 1,不僅如此,父進程的用戶根目錄、當前工作目錄等變量的引用計數均會加 1。

5、進程層次

進程組和會話在進程之間形成了兩級的層次關係:

進程組是一組相關進程的集合,會話是一組相關進程組的集合。

一個進程會有如下 ID:

1)PID:進程的唯一標識。如果一個進程含有多個線程,所有線程調用 getpid 函數會返回相同的值。

2)PGID:進程組 ID。每個進程都會有進程組 ID,表示該進程所屬的進程組。默認情況下新創建的進程會繼承父進程的進程組 ID。

3)SID:會話 ID。每個進程也都有會話 ID。默認情況下,新創建的進程會繼承父進程的會話 ID。

4)PPID:是程序的父進程號。

可以調用如下指令來查看所有進程的層次關係(To print a process tree):

1、ps -ejH
2、ps axjf

也可以調用以下函數獲取進程組 ID 跟會話 ID :

1、pid_t getpgrp(void);
2、pid_t getsid(pid_t pid); 

前面提到,新進程默認會繼承父進程的進程組 ID 和會話 ID,那麼看到這裏,有同學可能會問了:如果都是默認情況的話,那麼一層層往上計算,所有的進程應該有共同的進程組 ID 和會話 ID ,但是當調用 ps axjf 命令查看,實際情況並非如此,系統中存在很多不同的會話,每個會話下也有不同的進程組。

如下圖所示:

這是什麼原因呢?

《Linux環境編程:從應用到內核》這本書裏,有一個解釋,說的比較生動形象:

1)可以打個比方,以家族企業的創業爲例,每個進程可以比喻成家族企業的每個成員。

2)如果從創業之初,所有家族成員都安分守己,循規蹈矩,默認情況下,就只會有一個公司、一個部門。但是也有些“叛逆”的子弟,願意爲家族公司開疆拓土,願意成立新的部門。

3)這些新的部門就是新創建的進程組。如果有子弟“離經叛道”,甚至不願意呆在家族公司裏,他別開天地,另創了一個公司,那這個新公司就是新創建的會話組。由此可見,系統必須要有改變和設置進程組 ID 和會話 ID 的函數接口,否則,系統中只會存在一個會話、一個進程組。

這樣一來,是不是就比較好理解了。

6、進程的狀態

在 Linux 中,進程具有以下可能的狀態:

new-表示正在創建進程。

ready-表示該進程就緒,等待分配處理器。

running -表示該進程正在執行中。

waiting-表示該進程正在等待某些事件發生(例如I / O完成或信號接收)。此外,內核還區分了兩種類型的等待進程。可中斷的等待過程–可以被信號中斷,而不可中斷的等待過程–直接在硬件條件下等待,並且不能被任何事件/信號中斷。

terminated-表示該進程已完成執行。

如下圖所示:

在 Linux 中,常用的 ps-efps aux top 命令,也可以用 top 命令查看,如下圖所示:

另外幾個查看進程的命令, htop, jobs, fgbg 命令也可以學習一下。

此外,我們還必須瞭解其它三種進程:

孤兒進程:一個父進程退出,而它的一個或多個子進程還在運行,那麼那些子進程將成爲孤兒進程。孤兒進程將被 init 進程(進程號爲1)所收養,並由 init 進程對它們完成狀態收集工作。

殭屍進程:一個進程使用 fork 創建子進程,如果子進程先退出,而父進程並沒有調用 wait 或 waitpid 獲取子進程的狀態信息,那麼子進程的進程描述符仍然保存在系統中。這種進程稱之爲僵死進程。

守護進程:(英語:daemon)是一種在後臺執行的程序。此類程序會被以進程的形式初始化。守護進程程序的名稱通常以字母“d”結尾:例如,syslogd 就是指管理系統日誌的守護進程

Linux 系統中,每當出現一個孤兒進程的時候,內核就把孤兒進程的父進程設置爲init,而 init 進程會循環地 調用 wait() ,處理已經退出的子進程。這樣,當一個孤兒進程淒涼地結束了其生命週期的時候,init 進程就會代表收容所一樣處理它的一切善後工作。因此孤兒進程並不會有什麼危害。

而對於殭屍進程,如果進程不調用 wait / waitpid的話, 那麼保留的那段信息就不會釋放,其進程號就會一直被佔用,但是系統所能使用的進程號是有限的,如果大量的產生僵死進程,將因爲沒有可用的進程號而導致系統不能產生新的進程,此即爲殭屍進程的危害,應當避免。

解決:僵死進程並不是問題的根源,罪魁禍首是產生出大量僵死進程的那個父進程。因此,當我們要消滅系統中大量的僵死進程時,要做的就是把產生大量僵死進程的那個父進程殺死(通過 kill 發送 SIGTERM 或者 SIGKILL 信號)。殺死之後,產生的僵死進程就變成了孤兒進程,這些孤兒進程會被 init 進程接管。

7、進程組

進程組和會話是爲了支持 shell 作業控制而引入的概念。

修改進程組 ID 的接口如下

int setpgid(pid_t pid, pid_t pgid); 

這個函數的含義是,找到進程 ID 爲 pid 的進程,將其進程組 ID 修改爲 pgid,如果 pid 的值爲 0,則表示要修改調用進程的進程組 ID。該接口一般用來創建一個新的進程組。

下面三個函數接口含義一致,都是創立新的進程組,並且指定的進程會成爲進程組的首進程。

如果參數 pid 和 pgid 的值不匹配,那麼 setpgid 函數會將一個進程從原來所屬的進程組遷移到 pgid 對應的進程組。

setpgid(0,0)
setpgid(getpid(),0)
setpgid(getpid(),getpid()) 

setpgid 函數有一些限制:

1)pid 參數必須指定爲調用 setpgid 函數的進程或其子進程,不能隨意修改不相關進程的進程組 ID,如果違反這條規則,則返回 -1,並置 errno 爲 ESRCH。

2)pid 參數可以指定調用進程的子進程,但是子進程如果已經執行了exec函數,則不能修改子進程的進程組 ID。如果違反這條規則,則返回-1,並置 errno 爲 EACCESS。

3)在進程組間移動,調用進程,pid 指定的進程及目標進程組必須在同一個會話之內。這個比較好理解,不加入公司(會話),就無法加入公司下屬的部門(進程組),否則就是部門要造反的節奏。如果違反這條規則,則返回-1,並置 errno 爲 EPERM。

4)pid 指定的進程,不能是會話首進程。如果違反這條規則,則返回 -1,並置 errno 爲 EPERM。

有了創建進程組的接口,新創建的進程組就不必繼承父進程的進程組 ID 了。

最常見的創建進程組的場景就是在 shell 中執行管道命令。

代碼如下:

cmd1 | cmd2 | cmd3

下面用一個簡單的命令 ps ax|grep nfsd 來說明,其進程之間的關係如下圖所示。

ps 進程和 grep 進程都是 bash 創建的子進程,兩者通過管道協同完成一項工作,它們隸屬於同一個進程組,其中 ps 進程是進程組的組長。

進程組的概念並不難理解,可以將人與人之間的關係做類比。一起工作的同事,自然比毫不相干的路人更加親近。shell 中協同工作的進程屬於同一個進程組,就如同協同工作的人屬於同一個部門一樣。

引入了進程組的概念,可以更方便地管理這一組進程了。比如這項工作放棄了,不必向每個進程一一發送信號,可以直接將信號發送給進程組,進程組內的所有進程都會收到該信號。

8、會話

當有新的用戶登錄 Linux 時,登錄進程會爲這個用戶創建一個會話。

用戶的登錄 shell 就是會話的首進程。會話的首進程 ID 會作爲整個會話的 ID。會話是一個或多個進程組的集合,包括了登錄用戶的所有活動。

在登錄 shell 時,用戶可能會使用管道,讓多個進程互相配合完成一項工作,這一組進程屬於同一個進程組。

前臺進程和後臺進程:

用戶在 shell 中可以同時執行多個命令。對於耗時很久的命令(如編譯大型工程),用戶不必在傻傻的等待命令運行完畢才執行下一個命令。

用戶在執行命令時,可以在命令的結尾添加“&”符號,表示將命令放入後臺執行。這樣該命令對應的進程組即爲後臺進程組。

在任意時刻,可能同時存在多個後臺進程組,但是不管什麼時候都只能有一個前臺進程組。只有在前臺進程組中進程才能在控制終端讀取輸入。當用戶在終端輸入信號生成終端字符(如 ctrl+c、ctrl+z、ctr+\等)時,對應的信號只會發送給前臺進程組。

shell 中可以存在多個進程組,無論是前臺進程組還是後臺進程組,它們或多或少存在一定的聯繫,爲了更好地控制這些進程組(或者稱爲作業),系統引入了會話的概念。

會話的意義在於將很多的工作集中在一個終端,選取其中一個作爲前臺來直接接收終端的輸入及信號,其他的工作則放在後臺執行。

可以使用下面幾個函數來通知內核哪一個進程組是前臺進程組,以便設備驅動程序知道該把終端輸入和終端產生的信號發往何處。

#include <unistd.h>
#include <termios.h>
pid_t tcgetpgrp(int fd);    /* 返回值:若成功,返回前臺進程組 ID;否則,返回 -1 */
int tcsetpgrp(int fd, pid_t pgrpid);   /* 返回值:若成功,返回 0;否則,返回 -1 */
pid_t tcgetsid(int fd);/* 返回值:若成功,返回會話首進程的進程組 ID;否則,返回 -1 */

一個或多個進程組的集合組成了會話,以用戶登錄系統爲例,可能存在如下圖所示的情況。

Linux 系統提供 setsid 函數來創建會話,其接口定義如下:

#include <unistd.h>
pid_t setsid(void); 

如果這個函數的調用進程不是進程組組長,那麼調用該函數會發生以下事情:

1)創建一個新會話,會話 ID 等於進程 ID,調用進程成爲會話的首進程。

2)創建一個進程組,進程組 ID 等於進程 ID,調用進程成爲進程組的組長。

3)該進程沒有控制終端,如果調用 setsid 前,該進程有控制終端,這種聯繫就會斷掉。

調用 setsid 函數的進程不能是進程組的組長,否則調用會失敗,返回-1,並置 errno 爲 EPERM。

這個限制的合理在於如果允許進程組組長遷移到新的會話,而進程組的其他成員仍然在老的會話中,那麼,就會出現同一個進程組的進程分屬不同的會話之中的情況,這就破壞了進程組和會話的嚴格的層次關係。

9、總結

最後來張圖,幫助大家更好的瞭解 PID、PGID、PPID、Session 。

參考資料:《Linux 環境編程:從應用到內核》。

(END)

相關閱讀:


宋寶華:讓Linux的段錯誤(segmentation fault)不再是一個錯誤

宋寶華:世上最好的共享內存(Linux共享內存最透徹的一篇)

Linux中父進程爲何要苦苦地知道子進程的死亡原因?

宋寶華: 一圖理解終端、會話、 進程組、進程關係

相關課程:

《Linux系統編程兩駕馬車》之《多進程編程》課程

Linux閱碼場原創精華文章彙總

更多精彩,盡在"Linux閱碼場",掃描下方二維碼關注

點一點右下角”在看”,爲大神打Call~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章