UNIX 環(huán)境高級編程|進(jìn)程控制(上)
GitHub: https://github.com/storagezhang
Emai: debugzhang@163.com
本文為《UNIX 環(huán)境高級編程》第 8 章學(xué)習(xí)筆記
對在 UNIX 環(huán)境中的高級編程而言,完整地了解 UNIX 的進(jìn)程控制是非常重要的。
其中必須熟練掌握的只有幾個函數(shù)——fork、exec 系列、_exit、wait 和 waitpid。很多應(yīng)用程序都使用這些簡單的函數(shù)。fork 函數(shù)也給了我們一個了解競爭條件的機(jī)會。
本章說明了 system 函數(shù)和進(jìn)程會計(jì),這也使我們能進(jìn)一步了解所有這些進(jìn)程控制函數(shù)。
本章還說明了 exec 函數(shù)的另一種變體:解釋器文件以及它們的工作方式。
對各種不同的用戶 ID 和組 ID(實(shí)際、有效和保存的)的理解,對編寫安全的設(shè)置用戶 ID 程序是至關(guān)重要的。
8.2 進(jìn)程標(biāo)識
每個進(jìn)程都有一個非負(fù)整數(shù)表示的唯一進(jìn)程 ID。
因?yàn)檫M(jìn)程 ID 標(biāo)識總是唯一的,常將其用作其他標(biāo)識符的一部分以保證其唯一性。
雖然是唯一的,但是進(jìn)程 ID 是可復(fù)用的。
當(dāng)一個進(jìn)程終止后,其進(jìn)程 ID 就成為復(fù)用的候選者。
大多數(shù) UNIX 系統(tǒng)實(shí)現(xiàn)延遲復(fù)用算法,使得賦予新建進(jìn)程的 ID 不同于最近終止進(jìn)程所使用的 ID。
這防止了將新進(jìn)程誤認(rèn)為是使用同一 ID 的某個已終止的先前進(jìn)程。
系統(tǒng)中有一些專用進(jìn)程,但具體細(xì)節(jié)隨實(shí)現(xiàn)而不同。
ID 為 0 的進(jìn)程通常是調(diào)度進(jìn)程,常常被稱為交換進(jìn)程。
該進(jìn)程是內(nèi)核的一部分,它并不執(zhí)行任何磁盤上的程序,因此也被稱為系統(tǒng)進(jìn)程。
ID 為 1 的進(jìn)程通常是 init 進(jìn)程,在自舉過程結(jié)束時由內(nèi)核調(diào)用。
該進(jìn)程的程序文件在 UNIX 早期版本是 /etc/init,在較新的版本中是 /sbin/init。
該進(jìn)程負(fù)責(zé)在自舉內(nèi)核后啟動一個 UNIX 系統(tǒng)。
該進(jìn)程通常讀取與系統(tǒng)有關(guān)的初始化文件(/etc/rc* 文件,/etc/inittab 文件以及 /etc/init.d 中的文件),并將系統(tǒng)引導(dǎo)到一個狀態(tài)(如多用戶)。
該進(jìn)程永遠(yuǎn)不會終止。
該進(jìn)程是一個普通的用戶進(jìn)程(與交換進(jìn)程不同,它不是內(nèi)核中的系統(tǒng)進(jìn)程),但是它以超級用戶特權(quán)運(yùn)行。
每個 UNIX 系統(tǒng)實(shí)現(xiàn)都有它自己的一套提供操作系統(tǒng)服務(wù)的內(nèi)核進(jìn)程。
除了進(jìn)程 ID,每個進(jìn)程還有一些其他標(biāo)識符。下列函數(shù)返回這些標(biāo)識符:
#include
注意:這些函數(shù)都沒有出錯返回。
8.3 函數(shù) fork
一個現(xiàn)有的進(jìn)程可以調(diào)用 fork 函數(shù)創(chuàng)建一個新進(jìn)程:
#include
由 fork 創(chuàng)建的新進(jìn)程被稱為子進(jìn)程。
fork 函數(shù)被調(diào)用一次,但返回兩次。
將子進(jìn)程 ID 返回給父進(jìn)程的理由是:一個進(jìn)程的子進(jìn)程可以有多個,并且沒有一個函數(shù)使一個進(jìn)程可以獲得其所有子進(jìn)程的進(jìn)程 ID。
子進(jìn)程繼續(xù)執(zhí)行 fork 調(diào)用之后的指令。
子進(jìn)程是父進(jìn)程的副本。
獲得父進(jìn)程數(shù)據(jù)空間、堆和棧的副本。
父進(jìn)程和子進(jìn)程并不共享這些存儲空間部分。
父進(jìn)程和子進(jìn)程共享正文段。
由于在 fork 之后經(jīng)常跟隨著 exec,所以現(xiàn)在的很多實(shí)現(xiàn)并不執(zhí)行一個父進(jìn)程數(shù)據(jù)段、棧和堆的完全副本。
作為替代,使用了寫時復(fù)制(Copy-On-Write,COW)技術(shù),
這些區(qū)域由父進(jìn)程和子進(jìn)程共享,而且內(nèi)核將它們的訪問權(quán)限改變?yōu)橹蛔x。
如果父子進(jìn)程中任一個試圖修改這些區(qū)域,則內(nèi)核只為修改區(qū)域的那塊內(nèi)存制作一個副本,通常是虛擬存儲系統(tǒng)中的一“頁”。
一般來說,在 fork 之后是父進(jìn)程先執(zhí)行還是子進(jìn)程先執(zhí)行是不確定的,這取決于內(nèi)核所使用的調(diào)度算法。如果要求父進(jìn)程和子進(jìn)程之間相互同步,則要求某種形式的進(jìn)程間通信。
文件共享
在重定向父進(jìn)程的標(biāo)準(zhǔn)輸出時,子進(jìn)程的標(biāo)準(zhǔn)輸出也被重定向。
實(shí)際上,fork 的一個特性是父進(jìn)程的所有打開文件描述符都被復(fù)制到子進(jìn)程中。
這里的“復(fù)制”,指對每個文件描述符來說,就好像執(zhí)行了 dup 函數(shù)。父進(jìn)程和子進(jìn)程每個相同的打開描述符共享一個文件表項(xiàng)。
父進(jìn)程和子進(jìn)程共享同一個文件偏移量。
在 fork 之后處理文件描述符有以下兩種常見的情況:
父進(jìn)程等待子進(jìn)程完成。
父進(jìn)程無需對其描述符做任何處理。
當(dāng)子進(jìn)程終止后,它曾經(jīng)進(jìn)行過讀、寫操作的任一共享描述符的文件偏移量已做了相應(yīng)更新。
父進(jìn)程和子進(jìn)程各自執(zhí)行不同的程序段。
父進(jìn)程和子進(jìn)程各自關(guān)閉它們不需要使用的文件描述符,這樣就不會干擾對方使用的文件描述符。這種方法是網(wǎng)絡(luò)服務(wù)進(jìn)程經(jīng)常使用的。
除了打開文件之外,父進(jìn)程的很多其他屬性也由子進(jìn)程繼承:
實(shí)際用戶 ID、實(shí)際組 ID、有效用戶 ID、有效組 ID
附屬組 ID
進(jìn)程組 ID
會話 ID
控制終端
設(shè)置用戶 ID 標(biāo)志和設(shè)置組 ID 標(biāo)志
當(dāng)前工作目錄
根目錄
文件模式創(chuàng)建屏蔽字
信號屏蔽和安排
對任一打開文件描述符的執(zhí)行時關(guān)閉標(biāo)志
環(huán)境
連接的共享存儲段
存儲映像
資源限制
父進(jìn)程和子進(jìn)程之間的區(qū)別具體如下:
fork 的返回值不同
進(jìn)程 ID 不同
各自的父進(jìn)程 ID 不同
子進(jìn)程的 tms_utime,tms_stime,tms_cutime 和 tms_ustime 的值設(shè)置為 0
子進(jìn)程不繼承父進(jìn)程設(shè)置的文件鎖
子進(jìn)程的未處理鬧鐘被清除
子進(jìn)程的未處理信號集設(shè)置為空集
使 fork 失敗的兩個主要原因是:
系統(tǒng)中已經(jīng)有了太多的進(jìn)程。
該實(shí)際用戶 ID 的進(jìn)程總數(shù)超過了系統(tǒng)限制。
CHILD_MAX 規(guī)定了每個實(shí)際用戶 ID 在任一時刻可擁有的最大進(jìn)程數(shù)。
fork 有兩種用法:
一個父進(jìn)程希望復(fù)制自己,使父進(jìn)程和子進(jìn)程同時執(zhí)行不同的代碼段。
這在網(wǎng)絡(luò)服務(wù)進(jìn)程中是常見的:
父進(jìn)程等待客戶端的服務(wù)請求。
當(dāng)這種請求到達(dá)時,父進(jìn)程調(diào)用 fork,使子進(jìn)程處理此請求。
父進(jìn)程繼續(xù)等待下一個服務(wù)請求。
一個進(jìn)程要執(zhí)行一個不同的程序。
這對 shell 是常見的情況:
子進(jìn)程從 fork 返回后立即調(diào)用 exec。
8.4 函數(shù) vfork
vfork 函數(shù)的調(diào)用序列和返回值與 fork 相同,但兩者的語義不同:
vfork 函數(shù)用于創(chuàng)建一個新進(jìn)程,而該新進(jìn)程的目的是執(zhí)行一個新程序。
vfork 與 fork 一樣都創(chuàng)建一個子進(jìn)程,但是它并不將父進(jìn)程的地址空間完全復(fù)制到子進(jìn)程中,因?yàn)樽舆M(jìn)程會立即調(diào)用 exec(或 exit),于是也就不會引用該地址空間。
不過在子進(jìn)程調(diào)用 exec 或 exit 之前,它在父進(jìn)程的空間中運(yùn)行。
這種優(yōu)化工作方式在某些 UNIX 系統(tǒng)的實(shí)現(xiàn)中提高了效率,但如果子進(jìn)程修改數(shù)據(jù)(除了用于存放 vfork 返回值的變量)、進(jìn)行函數(shù)調(diào)用或者沒有 exec 或 exit 就返回都可能會帶來未知的結(jié)果。
vfokr 保證子進(jìn)程先運(yùn)行,在它調(diào)用 exec 或 exit 之后父進(jìn)程才可能被調(diào)度運(yùn)行,當(dāng)子進(jìn)程低啊用這兩個函數(shù)中的任意一個時,父進(jìn)程會恢復(fù)運(yùn)行。
如果在調(diào)用這兩個函數(shù)之前子進(jìn)程依賴于父進(jìn)程的進(jìn)一步動作,則會導(dǎo)致死鎖。
8.5 函數(shù) exit
進(jìn)程有 8 種方式使進(jìn)程終止,其中 5 種為正常終止,3 種為異常終止:
正常終止:
在 main 函數(shù)內(nèi)執(zhí)行 return 語句。
這等效于調(diào)用 exit。
調(diào)用 exit 函數(shù)。
此函數(shù)由 ISO C 定義,其操作包括調(diào)用個終止處理程序(終止處理程序在調(diào)用 atexit 函數(shù)時登記),然后關(guān)閉所有標(biāo)準(zhǔn) I/O 流等。
因?yàn)?ISO C 并不處理文件描述符、多進(jìn)程以及作業(yè)控制,所以這一定義對 UNIX 系統(tǒng)而言是不完整的。
調(diào)用 _exit 或 _Exit 函數(shù)。
ISO C 定義 _Exit,其目的是為進(jìn)程提供一種無需運(yùn)行終止處理程序或信號處理程序而終止的方法。
在 UNIX 系統(tǒng)中,_exit 和 _Exit 是同義的,并不沖洗標(biāo)準(zhǔn) I/O 流。
_exit 是由 POSIX.1 說明的,它由 exit 調(diào)用,處理 UNIX 系統(tǒng)特定的細(xì)節(jié)。
進(jìn)程的最后一個線程在其啟動例程中執(zhí)行 return 語句。
該線程的返回值不用作進(jìn)程的返回值。
當(dāng)最后一個線程從其啟動例程返回時,該進(jìn)程以終止?fàn)顟B(tài) 0 返回。
進(jìn)程的最后一個線程調(diào)用 pthread_exit 函數(shù)。
進(jìn)程的終止?fàn)顟B(tài)總是 0,這與傳送給 pthread_exit 的參數(shù)無關(guān)。
異常終止:
調(diào)用 abort。
它產(chǎn)生 SIGABRT 信號,是下一種異常終止的一個特例。
接到一個信號。
信號可由進(jìn)程自身(如調(diào)用 abort 函數(shù))、其他進(jìn)程或內(nèi)核產(chǎn)生。
最后一個線程對“取消”請求作出響應(yīng)。
默認(rèn)情況下,“取消”以延遲的方式發(fā)生:一個線程要求取消另一個線程,若干時間之后,目標(biāo)線程終止。
不管進(jìn)程如何終止,最后都會執(zhí)行內(nèi)核中的同一段代碼。這段代碼為相應(yīng)進(jìn)程關(guān)閉所有打開描述符,釋放它對所使用的存儲器等。
對上述任意一種終止情形,我們都希望終止進(jìn)程能夠通知其父進(jìn)程它是如何終止的。
對于 3 個終止函數(shù),實(shí)現(xiàn)這一點(diǎn)的方法是,將其退出狀態(tài)作為參數(shù)傳遞給函數(shù)。
在異常終止情況下,該終止進(jìn)程的父進(jìn)程都能用 wait 或 waitpid 函數(shù)取得其終止?fàn)顟B(tài)。
注意,退出狀態(tài)是傳遞給 3 個終止函數(shù)的參數(shù),或 main 的返回值,在最后調(diào)用 _exit 時,內(nèi)核將退出狀態(tài)轉(zhuǎn)換成終止?fàn)顟B(tài)。
如果子進(jìn)程先終止:
子進(jìn)程將其終止?fàn)顟B(tài)返回給父進(jìn)程。
如果父進(jìn)程先終止:
對于父進(jìn)程已經(jīng)終止的所有進(jìn)程,它們的父進(jìn)程都改為 init 進(jìn)程。
我們稱這些進(jìn)程由 init 進(jìn)程收養(yǎng),其操作過程大致是:
在一個進(jìn)程終止時,內(nèi)核逐個檢查所有活動進(jìn)程,以判斷它是否是正要終止進(jìn)程的子進(jìn)程,如果是,則該進(jìn)程的父進(jìn)程 ID 就更改為 1。
這種處理方法保證了每個進(jìn)程有一個父進(jìn)程。
內(nèi)核為每個終止子進(jìn)程保存了一定量的信息,所以當(dāng)終止進(jìn)程的父進(jìn)程調(diào)用 wait 函數(shù)或者 waitpid 函數(shù)時,可以得到這些信息。
這些信息至少包括:終止進(jìn)程的進(jìn)程 ID、該進(jìn)程的終止?fàn)顟B(tài)、該進(jìn)程使用的 CPU 時間總量。
一個已經(jīng)終止、但是等待父進(jìn)程對它進(jìn)行善后處理的進(jìn)程稱作僵死進(jìn)程,在 ps 命令中顯示為 Z。
所謂善后處理,就是父進(jìn)程調(diào)用 wait 函數(shù)或者 waitpid 函數(shù)讀取終止進(jìn)程的殘留信息
一旦父進(jìn)程進(jìn)行了善后處理,則終止進(jìn)程的所有占用資源(包括殘留信息)都得到釋放,該進(jìn)程被徹底銷毀
對于 init 進(jìn)程:
任何時候只要有一個子進(jìn)程終止,就立即調(diào)用 wait 函數(shù)取得其終止?fàn)顟B(tài)。
這種做法防止系統(tǒng)中塞滿了僵死進(jìn)程。
8.6 函數(shù) wait 和 waitpid
當(dāng)一個進(jìn)程正?;虍惓=K止時,內(nèi)核就向其父進(jìn)程發(fā)送 SIGCHLD 信號。
因?yàn)樽舆M(jìn)程終止是一個異步事件,所以這種信號是內(nèi)核向父進(jìn)程發(fā)送的異步進(jìn)程。
父進(jìn)程可以選擇忽略該信號。這是系統(tǒng)的默認(rèn)動作。
父進(jìn)程也可以提供一個該信號發(fā)生時即被調(diào)用執(zhí)行的函數(shù)(信號處理程序)。
如果進(jìn)程由于接收到 SIGCHLD 信號而調(diào)用 wait,我們期望 wait 會立即返回。
但是如果在隨機(jī)時間點(diǎn)調(diào)用 wait,則進(jìn)程可能會阻塞。
#include
區(qū)別:
在一個子進(jìn)程終止前,wait 使其調(diào)用者阻塞,而 waitpid 有一選項(xiàng),可使調(diào)用者不阻塞。
waitpid 并不等待在其調(diào)用之后的第一個終止子進(jìn)程,它有若干個選項(xiàng),可以控制它所等待的進(jìn)程。
如果子進(jìn)程已經(jīng)終止,并且是一個僵死進(jìn)程,則 wait 立即返回并取得該子進(jìn)程的狀態(tài);否則 wait 使其調(diào)用者阻塞,直到一個子進(jìn)程終止。
如果調(diào)用者阻塞而且它有多個子進(jìn)程,則在其某一子進(jìn)程終止時,wait 就立即返回。
因?yàn)?wait 返回終止子進(jìn)程的進(jìn)程 ID,所以它總能了解是哪一個子進(jìn)程終止了。
waitpid 可等待一個特定進(jìn)程,而 wait 則返回任一終止子進(jìn)程的狀態(tài)。
waitpid 提供了一個 wait 的非阻塞版本。有時希望獲取一個子進(jìn)程的狀態(tài),但不想阻塞。
參數(shù):
staloc:存放子進(jìn)程終止?fàn)顟B(tài)的地址。
如果不關(guān)心子進(jìn)程的終止?fàn)顟B(tài),可以將該參數(shù)設(shè)為空指針。
pid:
如果 pid==-1:則等待任一子進(jìn)程終止。
如果 pid>0:則等待進(jìn)程 ID 等于 pid 的那個子進(jìn)程終止。
如果 pid==0:則等待組 ID 等于調(diào)用進(jìn)程組 ID 的任一子進(jìn)程終止。
如果 pid<0:等待組 ID 等于 pid 絕對值的任一子進(jìn)程終止。
options:使我們進(jìn)一步控制 waitpid 的操作。
或者是0,或者是下列常量按位或的結(jié)果:
有 4 個互斥的宏可用來取得進(jìn)程終止的原因,它們的名字都以 WIF 開始?;谶@ 4 個宏中哪一個值為真,就可選用其他宏來取得退出狀態(tài)、信號編號等。
8.7 函數(shù) waitid
Single UNIX Specification 包括了另一個取得進(jìn)程終止?fàn)顟B(tài)的函數(shù)——waitid,此函數(shù)類似于 waitpid,但提供了更多的靈活性。
#include
參數(shù):
idtype:可以為下列常量:
id:指定要等待的子進(jìn)程 ID,其作用與 idtype 的值相關(guān)。
infop:一個緩沖區(qū)的地址。
該緩沖區(qū)由 waitid 填寫,包含了造成子進(jìn)程狀態(tài)改變的有關(guān)信號的詳細(xì)信息。
options:指示調(diào)用者關(guān)注哪些狀態(tài)變化??梢允窍铝谐A康陌次换颍?/p>
WCONTINUED、WEXITED 或 WSTOPPED 這 3 個常量之一必須在 options 參數(shù)中指定。
8.8 函數(shù) wait3 和 wait4
函數(shù) wait3 和 wait4 提供的功能比 wait、waitpid 和 waitid 所提供的功能要多一個,這與附加參數(shù)有關(guān)。該參數(shù)允許內(nèi)核返回由終止進(jìn)程及其所有子進(jìn)程使用的資源概況。
#include
參數(shù):
statloc:存放子進(jìn)程終止?fàn)顟B(tài)的緩沖區(qū)的地址。
如果不關(guān)心子進(jìn)程的終止?fàn)顟B(tài),可以將它設(shè)為空指針。
rusage:存放由 wait3 和 wait4 返回的終止子進(jìn)程的資源統(tǒng)計(jì)信息的緩沖區(qū)地址。
資源統(tǒng)計(jì)信息包括用戶 CPU 時間總量、系統(tǒng) CPU 時間總量、缺頁次數(shù)、接收到信號的次數(shù)等。
pid 和 options 參數(shù)的意義與 waitpid 相同。
8.9 競爭條件
當(dāng)多個進(jìn)程都企圖對共享數(shù)據(jù)進(jìn)行某種處理,而最后的結(jié)果又取決于進(jìn)程運(yùn)行的順序時,我們認(rèn)為發(fā)生了競爭條件。
如果在 fork 之后的某種邏輯顯式或隱式地依賴于在 fork 之后是父進(jìn)程先運(yùn)行還是子進(jìn)程先運(yùn)行,那么 fork 函數(shù)就會是競爭條件活躍的滋生地。
如果一個進(jìn)程希望等待一個子進(jìn)程終止,則它必須調(diào)用 wait 函數(shù)中的一個。如果一個進(jìn)程要等待其父進(jìn)程終止,則可使用下列形式的循環(huán):
while (getppid() != 1) { sleep(1); }
這種形式的循環(huán)稱為輪詢,它的問題是浪費(fèi)了 CPU 時間,因?yàn)檎{(diào)用者每隔 1s 都被喚醒,然后進(jìn)行條件測試。
為了避免競爭條件和輪詢,在多個進(jìn)程之間需要有某種形式的信號發(fā)送和接收的方法。在 UNIX 中可以使用信號機(jī)制,各種形式的進(jìn)程間通信(IPC)也可以使用。
Linux Unix
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。