進程控制

2020-08-14 21:08:15

進程控制

1 引言

本章介紹UNIX系統的進程控制,包括建立新進程、執行進程和進程終止。還將說明進程屬性的各種ID——實際,有效和儲存的使用者ID和組ID,以及它們如何受進程控制原語影響。本章還包括瞭直譯器檔案和system函數。本章最後講述了大多數UNIX系統所提供的進程會計機制 機製,這種機制 機製使我們能夠從另一個角度瞭解進程的控制功能。

2 進程標識(進程ID)

每個進程都有一個非負整數作爲其唯一標識,稱爲進程ID。
雖然是唯一的,但是進程ID是複用的。當一個進程結束時,其進程ID就成爲複用的候選者。大多數UNIX系統實現延遲複用演算法,以保證剛剛結束的進程ID不會被新執行的進程使用作爲其進程ID,這避免了新的進程被認爲是某個已終止的舊的進程。
UNIX系統中有一些專用進程,但其隨具體實現不同而不同。進程ID爲0的進程通常是排程進程,常被稱爲交換進程(swapper)。該進程是內核的一部分,它並不執行磁碟上的任何程式,因此也被稱爲系統進程。進程ID1通常是init進程,在自舉過程結束後,由內核呼叫。該進程負責在自舉內核後啓動一個UNIX系統。init通常讀取與系統有關的初始化檔案(/etc/rc*檔案或/etc/inittab檔案,以及在/etc/init.d中的檔案),並將系統引導到一個狀態(如多使用者),但是它以超級使用者特權執行。
除了進程ID,每個進程還有一些其他的識別符號。下列函數返回這些識別符號。

#include <unistd.h>

pid_t getpid(void);
// 返回值:呼叫進程的進程ID
pid_t getppid(void);
// 返回值:呼叫進程的父進程ID
uid_t getuid(void);
// 返回值:呼叫進程的實際使用者ID
uid_t geteuid(void);
// 返回值:呼叫進程的有效使用者ID
gid_t getgid(void);
// 返回值:呼叫進程的實際組ID
gid_t getegid(void);
// 返回值:呼叫進程的有效組ID

3. 函數fork

一個現有的進程可以通過fork函數建立一個新的進程。

pid_t fork(void);
// 返回值:在子進程中返回0,在父進程中返回子進程ID

fork函數建立一個新進程,稱爲子進程。fork函數呼叫一次,但返回兩次,分別在父進程和子進程返回。在父進程中返回子進程的進程ID,因爲沒有方法能獲取父進程的所有子進程;在子進程中返回0,因爲子進程能夠通過getppid的方式獲取父進程ID,同時0是內核交換進程的進程ID,不可能被其他進程使用。
fork函數建立子進程時,會拷貝數據空間,堆疊的副本供子進程使用,父進程與子進程共用正文段,不共用儲存空間。
由於在fork後經常跟隨exec,所以現在的很多實現並不執行一個進程的數據段,堆疊的完全副本。作爲替代,使用寫時復刻(Copy-On-Write, COW)技術.這些區域由父進程和子進程共用,而且內核將它們的存取許可權改爲只讀,如果父進程或者子進程中的任一個試圖改變這些區域,則內核只爲修改區域的那部分記憶體區域製作一個副本。
一般來說,先執行父進程還是先執行子進程是不一定的,這取決與內核所使用的排程演算法。如果要求父進程和子進程相互同步,則要求某種形式的進程間通訊。
當寫標準輸出時,我們將buf的長度減一作爲輸出位元組數,這裏我們不期望寫終止null位元組。strlen計算不包含終止null字元的字串長度,而sizeof則計算包含終止null字元的字串長度。兩者的另一差別是,使用strlen需要進行一次函數呼叫,而sizeof是在編譯時計算緩衝區長度,因爲編譯時,緩衝區已用已知字串進行初始化,其長度是固定的。
write函數是不帶緩衝的,在fork前呼叫write函數,僅寫標準輸出一次。printf寫標準輸出流,當標準輸出流關聯裝置/控制檯時,是行緩衝的,所以緩衝區遇換行符沖洗,printf數據中有換行符,直接進行無緩衝I/O。當標準輸出流關聯檔案時,是全緩衝的,當緩衝區滿或者主動沖洗時才進行實際I/O。所以fork
函數呼叫時,同樣拷貝了標準輸出流的緩衝區,所以在實際I/O時,由父進程/子進程各輸出一次。

檔案共用

fork的一個特性是父進程開啓的所有檔案描述符都複製到子進程中。我們說「複製」是因爲對於每個檔案描述符都好像使用了dup函數。父進程和子進程都擁有自己的檔案描述符,它們位於各自的進程表項中檔案描述符項,但是它們關聯相同的檔案表項,這意味着父進程,子進程共用檔案表項,包括檔案狀態標註,當前檔案偏移量和v節點指針。

說明檔案表項由內核管理,不屬於進程數據空間。

父進程,子進程對當前檔案偏移量的修改相互影響。
fork後處理檔案描述符由以下兩種情況,

  1. 父進程等待子進程完成。這種情況下,父進程無需對檔案描述符做任何處理。當子進程終止後,它所讀、寫的任一共用檔案描述符的當前檔案偏移量都以做了相應更新。
  2. 父進程,子進程執行不同的程式段。在fork後,父進程,子進程關閉各自不需要的檔案描述符,以免相互影響。這種方法是網路服務經常使用的。

除了開啓檔案之外,父進程的很多其他屬性也由子進程繼承,包括:

  • 實際使用者ID,實際組ID,有效使用者ID,有效組ID
  • 附屬組ID
  • 行程羣組ID
  • 對談ID
  • 控制終端
  • 設定使用者ID標誌和設定組ID標誌
  • 當前工作目錄
  • 根目錄
  • 檔案模式建立遮蔽字
  • 信號遮蔽和安排
  • 對任一開啓檔案描述符的執行時關閉(exec to close)標誌
  • 環境
  • 連線的共用儲存段
  • 儲存印象
  • 資源限制

父進程和子進程的區別如下:

  • fork的返回值不同;
  • 進程ID不同
  • 父進程ID不同
  • 子進程的tms_utime, tms_stime, tms_cutime, tms_ustime的值設定爲0
  • 子進程不繼承父進程設定的檔案鎖
  • 子進程的未處理鬧鐘被清除
  • 子進程的未處理信號集設定爲空集

fork有以下兩種用法:
(1) 一個父進程希望複製自己,使在父進程和子進程中執行不同的程式段。這在網路服務中是常見的,父進程等待用戶端的服務請求。當這種請求到達時,父進程呼叫fork,使子進程處理此請求。父進程則繼續等待下一個服務請求。
(2) 一個進程要執行一個不同的程式。在這種情況下,子進程從fork返回後,立即呼叫exec

4. 函數exit

進程有5種正常終止方式和3種異常終止方式,5種正常終止方式如下:
(1) 從main函數中執行return語句返回,這種情況等同於呼叫exit(main)
(2) 呼叫exit函數。exit函數屬於ISO C標準,執行終止處理程式,並關閉開啓的標準I/O流,清理I/O緩衝區。因爲ISO C並不關閉檔案描述符,多進程(父進程和子進程)以及作業控制,所以這一定義對於UNIX系統是不完整的。
(3) 呼叫_exit_Exit函數。_Exit函數屬於ISO C標準,目的是爲了提供一種不執行終止處理程式和和信號處理程式的終止進程的方式,關於是否沖洗標準I/O流則取決於實現。_exit函數屬於POSIX.1標準,在UNIX系統中,其作用與_Exit函數相同,並不沖洗標準I/O流。_exit函數由exit呼叫,它負責處理UNIX系統特定的細節。

在大多數UNIX系統的實現中,exit(3)是標準C庫中的一個函數,_exit(2)是一個系統呼叫。

(4) 進程的最後一個執行緒在其啓動例程中執行return語句返回。但是,其執行緒的返回值不作爲進程的返回值,當最後一個執行緒從啓動例程返回時,該進程以終止狀態0返回。
(5) 進程中的最後一個執行緒呼叫pthread_exit函數。如同前面一樣,進程以終止狀態0返回,與傳遞給pthread_exit的參數無關。

3種異常終止方式如下:
(1) 呼叫abort。它產生SIGABRT信號,這是下一種異常終止的一種特例。
(2) 當進程接收到某縣信號時。信號可由進程本身,其他進程,內核產生。
(3) 最後一個執行緒對取消請求作出響應。預設情況下,取消以延時方式發生:一個進程要求取消另一個進程,一段時間後,目標進程終止。

不管進程時如何終止的,最後都會執行內核中一段相同的程式碼,這段程式碼爲進程關閉所有檔案描述符,並釋放它所使用的記憶體。

對於上述的任何一種終止方式,我們都希望其父進程知道子進程是如何終止的。父進程能通過子進程的終止狀態瞭解子進程的終止情況。終止狀態是呼叫_exit函數時產生的狀態,退出狀態爲exit_exit_Exit函數呼叫時傳入的參數。在通過呼叫exit_exit_Exit函數終止進程的情況下,內核將退出狀態轉化爲終止狀態。在異常終止進程時,內核產生一個異常終止原因的終止狀態。在任一種情況下,父進程呼叫waitwaitpid獲得子進程的終止狀態(正常終止獲得退出狀態)。

父進程呼叫fork函數產生子進程,子進程終止後,如過父進程還在執行,子進程將終止狀態返回父進程;如果父進程在子進程之前結束,則init進程變爲該子進程的父進程。init進程在每一個進程終止時,都會查詢其子進程是否還在執行,如果仍在執行,則將這個進程的父進程設爲init進程,這使得每個進程都有一個父進程。

init的父進程是什麼?

當子進程終止時,關閉標準I/O流,釋放佔用儲存區,但子進程還保留其他一些資訊供父進程查詢,其至少包括子進程ID,終止狀態和CUP時間總量。父進程呼叫waitwaitpid獲取子進程的保留資訊,並且清理其仍佔有的資源,如果父進程不呼叫waitwaitpid則會導致子進程終止後仍佔有的資源無法釋放,這樣的子進程被稱爲僵死進程(zombie)。內核會釋放子進程呼叫的終止進程所佔用的資源(使用的所有儲存區),關閉所有開啓的檔案(關閉檔案描述符)。
當子進程中終止後被完全清理了,父進程就無法存取子進程的終止狀態。
對於init進程收養的進程終止時,init進程會呼叫一個wait函數獲取其終止狀態。

5. 函數waitwaitpid

函數waitwaitpid用於當進程終止後,父進程獲取子進程的終止狀態。

當進程終止時,無論是正常終止還是異常終止,內核都會向其父進程發送一個SIGCHLD信號,用於通知。子進程的終止對於父進程來說是非同步發生的,可能發生於父進程執行期間的任何時間點。所以父進程需要對信號進行非同步處理。可以設定SIGCHLD信號處理程式,當捕獲到此信號時,呼叫此程式。對於父進程來說,其預設處理方式是忽略該信號。

對於waitwaitpid函數:
當所有子進程都處於執行狀態,則阻塞;
當有進程終止,則獲取進程終止狀態並返回;
當沒有子進程時,立刻返回出錯。
對於wait函數,當父進程捕獲到SIGCHLD信號時執行,期望立刻返回,非此時間點呼叫,則阻塞。

#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
// 返回值,如成功返回子進程ID,否則返回0或-1

waitwaitpid的區別:

  • wait獲取呼叫等待後第一個終止的子進程的終止狀態,waitpid等待獲取指定pid子進程的終止狀態。
  • wait正常呼叫後,必然阻塞,waitpid使用選項可以不阻塞。

statloc參數指定終止子進程終止狀態的儲存位置,如果不關心終止狀態則傳入空指針。

對於waitpid函數:
pid == -1:等待任一子進程,等價於wait
pid > 0:等待進程ID與pid相等的進程。
pid == 0:等待組ID等於呼叫行程羣組ID的任一子進程。
pid < -1:等待組ID等於pid絕對值的任一子進程。

options參數控制waitpid函數的操作。此參數值爲0,或爲下列常數安慰或的運算結果:

  • WCONTINUED
  • WNOHANG:若由pid指定的子進程並不是立即可用的,則不阻塞,此時返回值爲0
  • WUNTRACED

waitpid提供wait函數沒有的3個功能:
(1) 等待指定pid的子進程終止。
(2) 子進程不可用時,不阻塞當前進程。
(3) 支援作業控制。

6. 競爭條件

當多個進程試圖在同一時間對共用數據做某種處理,就會導致出現競爭條件。其最終的結果取決進程執行時的內核排程。在進程開始執行後,其機器指令的執行時間點取決與系統負責和內核的排程演算法。

7. 函數exec

函數exec用於將當前進程替換爲一個新的進程執行,呼叫exec函數後,將當前進程的正文段,數據段,堆和棧全部替換爲新進程的相應部分,並從新進程的main函數開始執行。函數exec並不建立新的進程,進程其他屬性保持不變,如進程ID。

#include <unistd.h>
int execl(const char *pathname, const char *arg0, const char *arg1... /*(char *)0*/);
int execv(const char *pathname, char * const argv[]);
int execle(const char *pathname, const char *arg0, const char *arg1... /*(char *)0, char *const envp[]*/ );
int execve(const char *pathname, char * const argv[], char * const envp[]);
int execlp(const char *filename, const char *arg0, .../*(char *)0*/);
int execvp(const char *filename, char * const argv[]);
int fexecve(int fd, char * const argv[], char * const envp[]);
// 返回值,如出錯,返回-1.若成功,不返回

有三種方式確定執行進程:

  • 執行路徑名:輸入執行程式的路徑,
  • 執行程式檔名:若filename中含有"/",則視爲路徑名,否則,視爲程式檔名,其執行程式搜尋路徑爲$PATH。execlpexecvp中找到的檔案不是連線編譯器產生的可執行程式,則嘗試視爲shell腳步。
  • 檔案描述符:主要用於使用檔案描述符判斷執行程式是否符合期望,避免TOCTTOU錯誤,在執行程式前,程式檔案被替換。

呼叫進程傳參使用參數列表或者參數表陣列。
呼叫進程環境表若呼叫exec時傳入,則使用傳入環境表,否則使用呼叫進程環境表。

在執行exec後,進程ID沒有改變。但新程式從呼叫進程繼承以下屬性:

  • 進程ID和父進程ID
  • 實際使用者ID和實際組ID
  • 附屬組ID
  • 行程羣組ID
  • 對談ID
  • 控制終端
  • 鬧鐘尚餘留的時間
  • 當前工作目錄
  • 根目錄
  • 檔案模式建立遮蔽字
  • 檔案鎖
  • 進程信號遮蔽
  • 未處理信號
  • 資源限制
  • nice值
  • tms_utime, tms_stime, tms_cutime, tms_ustime

對開啓檔案的處理,與每個檔案描述符的執行時關閉(close-on-exec)標誌值有關。若使用fcntl設定了執行時關閉標誌,則在執行exec時關閉該檔案描述符,否則遵循系統預設設定,不設定該標誌,執行exec後仍保持檔案描述符開啓。

POSIX.1明確要求在執行exec時關閉目錄流。這通常是由opendir函數實現的,它呼叫fcntl爲對應開啓目錄流的函數的檔案描述符設定執行時關閉標誌。

exec前後實際使用者ID和實際組ID保持不變,而有效使用者ID和有效組ID是否取決於所執行程式檔案的設定使用者ID位和設定組ID位是否設定。如果新程式的設定使用者ID位已設定,則有效使用者ID變爲程式檔案所有者的ID;否則,有效使用者ID不變。對組ID的處理方式與此相同。