會話、進程組、線程組總體關系示意圖
待插入
Session(會話)與進程組
Shell 分前后台來控制的不是進程而是作業(Job)或者進程組(Process Group)。一個前台作業可以由多個進程組成,一個后台作業也可以由多個進程組成,Shell可以同時運行一個前台作業和任意多個后台作業,這稱為作業 控制(Job Control)。例如用以下命令啟動5個進程(這個例子出自[APUE2e]):
其中proc1和proc2屬於同一個后台進程組,proc3、proc4、proc5屬於同一個前台進程組,Shell進程本身屬於一個單獨的進 程組。這些進程組的控制終端相同,它們屬於同一個Session。當用戶在控制終端輸入特殊的控制鍵(例如Ctrl-C)時,內核會發送相應的信號(例如 SIGINT)給前台進程組的所有進程。各進程、進程組、Session的關系如下圖所示。
現在我們從Session和進程組的角度重新來看登錄和執行命令的過程。
1. getty或telnetd進程在打開終端設備之前調用setsid函數創建一個新的Session,該進程稱為Session Leader,該進程的id也可以看作Session的id,然后該進程打開終端設備作為這個Session中所有進程的控制終端。在創建新 Session的同時也創建了一個新的進程組,該進程是這個進程組的Process Group Leader,該進程的id也是進程組的id。
2. 在登錄過程中,getty或telnetd進程變成login,然后變成Shell,但仍然是同一個進程,仍然是Session Leader。
3. 由Shell進程fork出的子進程本來具有和Shell相同的Session、進程組和控制終端,但是Shell調用setpgid函數將作業中的某個 子進程指定為一個新進程組的Leader,然后調用setpgid將該作業中的其它子進程也轉移到這個進程組中。如果這個進程組需要在前台運行,就調用 tcsetpgrp函數將它設置為前台進程組,由於一個Session只能有一個前台進程組,所以Shell所在的進程組就自動變成后台進程組。
在上面的例子中,proc3、proc4、proc5被Shell放到同一個前台進程組,其中有一個進程是該進程組的Leader,Shell調用 wait等待它們運行結束。一旦它們全部運行結束,Shell就調用tcsetpgrp函數將自己提到前台繼續接受命令。但是注意,如果proc3、 proc4、proc5中的某個進程又fork出子進程,子進程也屬於同一進程組,但是Shell並不知道子進程的存在,也不會調用wait等待它結束。 換句話說,proc3 | proc4 | proc5是Shell的作業,而這個子進程不是,這是作業和進程組在概念上的區別。一旦作業運行結束,Shell就把自己提到前台,如果原來的前台進程 組還存在(如果這個子進程還沒終止),則它自動變成后台進程組(回顧一下例 30.3 “fork”)。
下面看兩個例子。
這個作業由ps和cat兩個進程組成,在前台運行。從PPID列可以看出這兩個進程的父進程是bash。從PGRP列可以看出,bash在id為 6994的進程組中,這個id等於bash的進程id,所以它是進程組的Leader,而兩個子進程在id為8762的進程組中,ps是這個進程組的 Leader。從SESS可以看出三個進程都在同一Session中,bash是Session Leader。從TPGID可以看出,前台進程組的id是8762,也就是兩個子進程所在的進程組。
這個作業由ps和cat兩個進程組成,在后台運行,bash不等作業結束就打印提示信息[1] 8835然后給出提示符接受新的命令,[1]是作業的編號,如果同時運行多個作業可以用這個編號區分,8835是該作業中某個進程的id。請讀者自己分析ps命令的輸出結果。
2.2. 與作業控制有關的信號
我們通過實驗來理解與作業控制有關的信號。
將cat放到后台運行,由於cat需要讀標准輸入(也就是終端輸入),而后台進程是不能讀終端輸入的,因此內核發SIGTTIN信號給進程,該信號的默認處理動作是使進程停止。
jobs命令可以查看當前有哪些作業。fg命令可以將某個作業提至前台運行,如果該作業的進程組正在后台運行則提至前台運行,如果該作業處於停止狀 態,則給進程組的每個進程發SIGCONT信號使它繼續運行。參數%1表示將第1個作業提至前台運行。cat提到前台運行后,掛起等待終端輸入,當輸入 hello並回車后,cat打印出同樣的一行,然后繼續掛起等待輸入。如果輸入Ctrl-Z則向所有前台進程發SIGTSTP信號,該信號的默認動作是使 進程停止。
bg命令可以讓某個停止的作業在后台繼續運行,也需要給該作業的進程組的每個進程發SIGCONT信號。cat進程繼續運行,又要讀終端輸入,然而它在后台不能讀終端輸入,所以又收到SIGTTIN信號而停止。
用kill命令給一個停止的進程發SIGTERM信號,這個信號並不會立刻處理,而要等進程准備繼續運行之前處理,默認動作是終止進程。但如果給一個停止的進程發SIGKILL信號就不同了。
SIGKILL信號既不能被阻塞也不能被忽略,也不能用自定義函數捕捉,只能按系統的默認動作立刻處理。與此類似的還有SIGSTOP信號,給一個 進程發SIGSTOP信號會使進程停止,這個默認的處理動作不能改變。這樣保證了不管什么樣的進程都能用SIGKILL終止或者用SIGSTOP停止,當 系統出現異常時管理員總是有辦法殺掉有問題的進程或者暫時停掉懷疑有問題的進程。
上面講了如果后台進程試圖從控制終端讀,會收到SIGTTIN信號而停止,如果試圖向控制終端寫呢?通常是允許寫的。如果覺得后台進程向控制終端輸出信息干擾了用戶使用終端,可以設置一個終端選項禁止后台進程寫。
首先用stty命令設置終端選項,禁止后台進程寫,然后啟動一個后台進程准備往終端寫,這時進程收到一個SIGTTOU信號,默認處理動作也是停止進程。
進程組與線程組
進程可有多線程組成,同一個進程的各個線程屬於統一個線程組,每個線程對應於一個輕量級進程,他們的tgid就是領頭線程或進程的pid,getpid()返回的是tgid。
進程與線程組的關系可詳見我的另一片博文《linux內核——進程,輕量級進程,線程,線程組》