1. 終端
在Linux系統中,用戶通過終端登錄系統後得到一個Shell進程,這個終端成爲Shell進程的控制終端(Controlling Terminal),Shell進程啓動的其他進程的控制終端也是這個終端。默認情況下(沒有重定向),每個進程的標準輸入、標準輸出和標準錯誤輸出都指向控制終端,進程從標準輸入讀也就是讀用戶的鍵盤輸入,進程往標準輸出或標準錯誤輸出寫也就是輸出到顯示器上。此外在控制終端輸入一些特殊的控制鍵可以給前臺進程發信號,例如Ctrl+C表示SIGINT,Ctrl+\表示SIGQUIT。
2. 會話與進程組
每次用戶登錄終端時會產生一個會話(session)。從用戶登錄開始到用戶退出爲止,這段時間內在該終端執行的進程都屬於這一個會話。
每個進程除了有一進程ID之外,還屬於一個進程組(Process Group)。進程組是一個或多個進程的集合,每個進程組有一個唯一的進程組ID。多個進程屬於進程組的情況是多個進程用管道“|”號連接進行執行。如果在命令行執行單個進程時這個進程組只有這一個進程。
3. 控制終端
在終端(包括telnet等僞終端)登錄就會產生一個會話,此會話擁有這一個單獨的控制終端。
建立與控制終端連接的會話首進程,被稱之爲控制進程,也就是Shell進程。
一個會話中的幾個進程組可被分成一個前臺進程組以及一個或幾個後臺進程組。
4. 作業控制
前後臺運行的進程組又稱爲作業(Job),一個前臺作業可以由多個進程組成,一個後臺作業也可以由多個進程組成,Shell進程可以同時運行一個前臺作業和任意多個後臺作業,這稱爲作業控制(Job Control)。
作業控制允許在一個終端上起動多個作業(進程組),控制哪一個作業可以存取該終端,以及哪些作業在後臺運行。
從Shell進程使用作業控制功能角度觀察,可以在前臺啓動一個作業或後臺啓動多個作業。一個作業只是幾個進程的集合,通常用管道連接各進程。
例如下面的命令在後臺(&表示在後臺運行)啓動了兩個作業,這兩個後臺作業所調用的進程都在後臺運行。
cat *.c | pg &
make all &
實際上有三個特殊字符可使終端驅動程序產生信號,信號將送至前臺進程組的所有進程,而後臺進程組作業則不受影響,它們是:
① 中斷字符(DELETE或Ctrl-C)產生SIGINT信號。
② 退出字符(Ctrl-\)產生SIGQUIT信號。
③ 掛起字符(Ctrl-Z)產生SIGTSTP信號。
5. 會話與進程組的關係
例如用以下命令啓動5個進程。
$ proc1 | proc2 &
$ proc3 | proc4 | proc5
其中proc1和proc2屬於同一個後臺進程組,proc3、proc4、proc5屬於同一個前臺進程組,Shell進程本身屬於一個單獨的進程組。這些進程組的控制終端相同,它們屬於同一個session。其進程、進程組、session的關係如下圖12-12所示。
圖12-12 會話與進程組關係圖
現在從session和進程組的角度重新來看登錄和執行命令的過程。在上面的例子中,proc3、proc4、proc5被Shell放到同一個前臺進程組,其中proc3進程是該進程組的組長,Shell進程調用wait等待它們運行結束,一旦它們全部運行結束,Shell進程就調用tcsetpgrp函數將自己提到前臺繼續接收命令。但是注意,如果proc3、proc4、proc5中的某個進程又fork出子進程,子進程也屬於同一進程組,但是Shell進程並不知道子進程的存在,也不會調用wait等待它結束。換句話說,proc3 | proc4 | proc5是Shell進程的作業,而這個子進程不是,這是作業和進程組在概念上的區別。一旦作業運行結束,Shell進程就把自己提到前臺。
6. 進程組和會話實例說明
(1)進程組與會話實例一
$ ps -o pid,ppid,pgrp,session,tpgid,comm | cat
PID PPID PGRP SESS TPGID COMMAND
6994 6989 6994 6994 8762 bash
8762 6994 8762 6994 8762 ps
8763 6994 8762 6994 8762 cat
這個作業由ps和cat兩個進程組成,在前臺運行。從PPID列可以看出這兩個進程的父進程是bash;從PGRP列可以看出,bash在進程組ID爲6994的進程組中,這個進程組ID等於bash的進程ID,所以它是進程組的組長;而兩個子進程在8762的進程組中,ps是這個進程組的組長;從SESS列可以看出這三個進程都在同一session中,會話ID爲6994,bash是會話首進程;從TPGID列可以看出,前臺進程組ID是8762,也就是ps和cat這兩個進程所在的進程組,其中ps進程爲進程組的組長。
(2)進程組與會話實例二
$ ps -o pid,ppid,pgrp,session,tpgid,comm | cat &
[1] 8835
$ PID PPID PGRP SESS TPGID COMMAND
6994 6989 6994 6994 6994 bash
8834 6994 8834 6994 6994 ps
8835 6994 8834 6994 6994 cat
這個作業由ps和cat兩個進程組成,在後臺運行。bash不等作業結束就打印提示信息“[1] 8835”,然後給出提示符接受新的命令,“1”是作業的編號,如果同時運行多個作業可以用這個編號區分,“8835”是該作業中某個進程的進程ID。
7. 進程組函數
(1)進程組函數說明
每個進程組有一個組長進程,組長進程ID等於其進程組ID。只要在某個進程組中有一個進程存在,則該進程組就存在,這與其組長進程是否終止無關。從進程組創建開始到其中最後一個進程離開爲止的時間區間稱爲進程組的生命期。
(2)取進程組ID函數原型
getpgrp(取得進程組識別碼)
所需頭文件 | #include <unistd.h> |
函數說明 | getpgrp()用來取得目前進程所屬的組識別碼。此函數相當於調用getpgid(0) |
函數原型 | pid_t getpgrp(void) |
函數返回值 | 返回目前進程所屬的組識別碼 |
(3)設置進程組ID函數原型
setpgrp(設置進程組識別碼)
所需頭文件 | #include <unistd.h> |
函數說明 | setpgid()將參數pid 指定進程所屬的組識別碼設爲參數pgid 指定的組識別碼。如果參數pid 爲0,則會用來設置目前進程的組識別碼,如果參數pgid爲0,則會以目前進程的進程識別碼來取代 |
函數原型 | int setpgid(pid_t pid,pid_t pgid) |
函數返回值 | 執行成功則返回組識別碼,如果有錯誤則返回-1,錯誤原因存於errno中 |
錯誤代碼 | EINVAL:參數pgid小於0 EPERM:進程權限不足,無法完成調用 ESRCH:找不到符合參數pid指定的進程 |
進程調用setpgid(pid, pgid)可以參加一個現存的組或者創建一個新進程組,這時pid進程的進程組ID設置爲pgid。如果這兩個參數相等,則pid指定的進程變成進程組組長。
一個進程只能爲它自己或它的子進程設置進程組ID。在它的子進程調用了 exec後,它就不再能改變該子進程的進程組ID。如果設置的pid等於0,則使用調用者的進程ID;如果設置pgid等於0,則使用pid進程的進程組ID。
在大多數作業控制中,在fork之後調用此函數,使父進程設置其子進程的進程組ID,然後使子進程設置其自己的進程組ID。這些調用中有一個是冗餘的,但這樣做可以保證父、子進程在進一步操作之前,子進程都進入了該進程組。如果不這樣做的話,那麼就產生一個競態條件,因爲它依賴於哪一個進程先執行。
在發送一個信號時,信號可以發送給一個進程或送給一個進程組。
8. 會話函數
(1)會話函數原型
setsid(創建一個新的會話)
所需頭文件 | #include <sys/types.h> #include <unistd.h> |
函數說明 | 創建一個新的會話 |
函數原型 | pid_t setsid(void) |
函數返回值 | 若成功則爲進程組 ID,若出錯則爲-1 |
(2)會話說明
用戶每次用戶登錄會產生一個會話。如果調用setsid此函數的進程不是一個進程組的組長,則此函數會創建一個新會話,所產生的結果如下:
此進程變成該新會話的會話首進程(會話首進程是創建該會話的進程),此進程是該新會話中的唯一進程。
此進程成爲一個新進程組的組長進程,新進程組ID是此進程的進程ID。
此進程沒有控制終端,如果在調用setsid之前,此進程有一個控制終端,那麼這種聯繫也被解除。
如果此調用進程已經是一個進程組的組長,則此函數返回出錯。爲了保證不處於這種情況,通常先調用fork,然後使其父進程終止,而子進程則繼續。因爲子進程繼承了父進程的進程組ID,而其進程ID則是新分配的,兩者不可能相等,所以這就保證了子進程不是一個進程組的組長。
(3)setsid函數的作用如下
① 讓進程擺脫原會話的控制。
② 讓進程擺脫原進程組的控制。
③ 讓進程擺脫原控制終端的控制。
9. 孤兒進程
一個父進程已終止的進程稱爲孤兒進程 (orphan process),這種進程由1號進程init收養。
10. 殭屍進程
(1)殭屍進程的產生
一個進程在調用exit函數結束自己生命的時候,其實它並沒有真正的被完全銷燬,而是留下一個稱爲殭屍進程(Zombie)的數據結構。
在Linux進程的狀態中,殭屍進程是非常特殊的一種,它已經釋放了幾乎所有內存空間,沒有任何可執行代碼,也不能被調度,僅僅在進程列表中保留一個表項,記載該進程的退出狀態等信息供其他進程收集。除此之外,殭屍進程不再佔有任何內存空間。它需要它的父進程來爲它收屍,如果它的父進程沒有設置SIGCHLD信號處理函數、或者沒有設置SIGCHLD信號爲忽略(SIG_IGN)、或者沒有調用wait(或waitpid)等待子進程結束,那麼它就一直保持殭屍狀態。如果這時父進程結束了,那麼init進程會自動接手這個子進程,殭屍進程消失。但是如果如果父進程是一個循環,不會結束,那麼子進程就會一直保持殭屍狀態,這就是爲什麼系統中有時會有很多的殭屍進程。
(2)怎樣來清除殭屍進程
清除殭屍進程有三種處理方法,具體說明如下:
改寫父進程,在子進程死後爲它收屍。具體做法是接管SIGCHLD信號,子進程死後,會發送SIGCHLD信號給父進程,父進程調用wait(或waitpid)函數爲子進程收屍。
在父進程中設置SIGCHLD信號處理函數或者設置SIGCHLD信號爲忽略(SIG_IGN)。
把父進程殺掉,父進程死後,殭屍進程成爲“孤兒進程”,過繼給1號進程init,init始終會負責清理殭屍進程,它產生的所有殭屍進程也跟着消失。
摘錄自《深入淺出Linux工具與編程》