進程的創建和終止
大多數系統的進程能夠並發執行,它們可以動態創建和刪除。因此,操作系統必須提供機制,用於創建進程和終止進程。
進程創建
進程在執行過程中可能創建多個新的進程。創建進程稱為父進程,而新的進程稱為子進程。每個新進程可以再創建其他進程,從而形成進程樹。
大多數的操作系統(包括 UNIX、
Linux 和 Windows)對進程的識別采用的是唯一的進程標識符(pid),pid 通常是一個整數值。系統內的每個進程都有一個唯一 pid,它可以用作索引,以便訪問內核中的進程的各種屬性。
圖 1 典型Linux系統的一個進程樹
圖 1 顯示了 Linux 操作系統的一個典型進程樹,包括進程的名稱和 pid(我們通常使用進程這個術語,不過 Linux 偏愛"任務"這個術語)。進程 init(它的 pid 總是 1),作為所有用戶進程的根進程或父進程。一旦系統啟動后,進程init可以創建各種用戶進程,如 Web 服務器、打印服務器、ssh 服務器等。
在圖 1 中,kthreadd 和 sshd 為 init 的兩個子進程。kthreadd 進程負責創建額外進程,以便執行內核任務(這里為 khelper 和 pdflush)。sshd 進程負責管理通過 ssh 連到系統的客戶端。login 進程負責管理直接登錄到系統的客戶端。在這個例子中,客戶已登錄,並且使用 bash 外殼,它所分配的 pid 為 8416。采用 bash 命令行界面,這個進程還創建了進程 ps 和 emacs 編輯器。
對於 UNIX 和 Linux 系統,我們可以通過 ps 命令得到一個進程列表。例如,命令
ps -el
可以列出系統中的所有當前活動進程的完整信息。通過遞歸跟蹤父進程一直到進程 init,可以輕松構造類似圖 1 所示的進程樹。
一般來說,當一個進程創建子進程時,該子進程需要一定的資源(CPU 時間、內存、文件、I/O 設備等)來完成任務。子進程可以從操作系統那里直接獲得資源,也可以只從父進程那里獲得資源子集。父進程可能要在子進程之間分配資源或共享資源(如內存或文件)。限制子進程只能使用父進程的資源,可以防止創建過多進程,導致系統超載。
除了提供各種物理和邏輯資源外,父進程也可能向子進程傳遞初始化數據(或輸入)。例如,假設有一個進程,其功能是在終端屏幕上顯示文件如 image.jpg 的狀態。當該進程被創建時,它會從父進程處得到輸入,即文件名稱 image.jpg。通過這個名稱,它會打開文件,進而寫出內容。它也可以得到輸出設備名稱。另外,有的操作系統會向子進程傳遞資源。對於這種系統,新進程可得到兩個打開文件,即 image.jpg 和終端設備,並且可以在這兩者之間進行數據傳輸。
當進程創建新進程時,可有兩種執行可能:
-
父進程與子進程並發執行。
-
父進程等待,直到某個或全部子進程執行完。
新進程的地址空間也有兩種可能:
-
子進程是父進程的復制品(它具有與父進程同樣的程序和數據)。
-
子進程加載另一個新程序。
為了說明這些不同,首先看一看 UNIX 操作系統。在 UNIX 中,正如以前所述,每個進程都用一個唯一的整型進程標識符來標識。通過系統調用 fork(),可創建新進程。新進程的地址空間復制了原來進程的地址空間。這種機制允許父進程與子進程輕松通信。這兩個進程(父和子)都繼續執行處於系統調用 fork() 之后的指令,但有一點不同:對於新(子)進程,系統調用 fork() 的返回值為 0;而對於父進程,返回值為子進程的進程標識符(非零)。
通常,在系統調用 fork() 之后,有個進程使用系統調用 exec(),以用新程序來取代進程的內存空間。系統調用 exec() 加載二進制文件到內存中(破壞了包含系統調用 exec() 的原來程序的內存內容),並開始執行。采用這種方式,這兩個進程能相互通信,並能按各自方法運行。父進程能夠創建更多子進程,或者如果在子進程運行時沒有什么可做,那么它采用系統調用 wait() 把自己移出就緒隊列,直到子進程終止。因為調用 exec() 用新程序覆蓋了進程的地址空間,所以調用 exec() 除非出現錯誤,不會返回控制。
-
#include <sys/types.h>
-
#include <stdio.h>
-
#include <unistd.h>
-
int main()
-
{
-
pid_t pid;
-
/* fork a child process */
-
pid = fork();
-
if (pid < 0) { /* error occurred */\
-
fprintf(stderr, "Fork Failed");
-
return 1;
-
}
-
else if (pid == 0) { /* child process */
-
execlp("/bin/ls","ls",NULL);
-
}
-
else { /* parent process */
-
/* parent will wait for the child to complete */
-
wait(NULL);
-
printf("Child Complete");
-
}
-
return 0;
-
}
以上所示的 C 程序說明了上述 UNIX 系統調用例子中。這里有兩個不同進程,但運行同一程序。這兩個進程的唯一差別是:子進程的 pid 值為0,而父進程的 pid 值大於0(實際上,它就是子進程的 pid)。子進程繼承了父進程的權限、調度屬性以及某些資源,諸如打開文件。
通過系統調用 execlp()(這是系統調用 exec() 的一個版本),子進程采用 UNIX 命令 /bin/ls(用來列出目錄清單)來覆蓋其地址空間。通過系統調用 wait(),父進程等待子進程的完成。當子進程完成后(通過顯示或隱式調用 exit()),父進程會從 wait() 調用處開始繼續,並且結束時會調用系統調用 exit()。這可用圖 2 表示。
圖 2 通過系統調用 fork() 創建進程
當然,沒有什么可以阻止子進程不調用 exec(),而是繼續作為父進程的副本來執行。在這種情況下,父進程和子進程會並發執行,並采用同樣的代碼指令。由於子進程是父進程的一個副本,這兩個進程都有各自的數據副本。
作為另一個例子,接下來看一看 Windows 的進程創建。進程創建采用 Windows API 函數 CreateProcess(),它類似於 fork()(這是父進程用於創建子進程的)。不過,fork() 讓子進程繼承了父進程的地址空間,而 CreateProcess() 在進程創建時要求將一個特定程序加載到子進程的地址空間。再者,fork() 不需要傳遞任何參數,而 CreateProcess() 需要傳遞至少 10 個參數。
-
#include <stdio.h>
-
#include <windows.h>
-
int main(VOID)
-
{
-
STARTUPINFO si;
-
PR0CESS_INF0RMATI0N pi;
-
/* allocate memory */
-
ZeroMemory(&si, sizeof(si));
-
si.cb = sizeof(si);
-
ZeroMemory(&pi, sizeof(pi));
-
/* create child process */
-
if (!CreateProcess(NULL, /* use command line */
-
"C: \\WIND0WS\\system32\\mspaint. exe" , /* command */ NULL, /* don,t inherit process handle */
-
NULL, /* don^ inherit thread handle */
-
FALSE, /* disable handle inheritance */
-
0, /* no creation flags */
-
NULL, /* use parentJs environment block */
-
NULL, /* use parent1s existing directory */
-
&si,
-
&pi))
-
{
-
fprintf (stderr, "Create Process Failed");
-
return -1;
-
}
-
/* parent will wait for the child to complete */
-
WaitForSingleObject(pi.hProcess,INFINITE);
-
printf("Child Complete");
-
/* close handles */
-
CloseHandle(pi.hProcess);
-
CloseHandle(pi.hThread);
-
}
以上所示的 C 程序演示了函數 CreateProcess(),它創建了一個子進程,並且加載了應用程序 mspaint.exe。這里選擇了 10 個參數中的許多默認值來傳遞給 CreateProcess()。
傳遞給 CreateProcess() 的兩個參數,為結構 STARTUPINFO 和 PROCESS-INFORMATION 的實例:
-
結構 STARTUPINFO 指定新進程的許多特性,如窗口大小和外觀、標准輸入與輸出的文件句柄等;
-
結構 PR0CESS_INF0RMATI0N 含新進程及其線程的句柄與標識符。
在進行 CeateProcess() 之前,調用函數 ZeroMemory() 來為這些結構分配內存。
函數 CreateProcess() 的頭兩個參數是應用程序名稱和命令行參數。如果應用程序名稱為 NULL(這里就是 NULL),那么命令行參數指定了所要加載的應用程序。在這個例子中,加載的是 Microsoft Windows 的 mspaint.exe 應用程序。
除了這兩個初始參數之外,這里使用系統默認參數來繼承進程和線程句柄,並指定沒有創建標志;另外,這里還使用了父進程的已有環境塊和啟動目錄。最后,提供了兩個指向程序剛開始時所創建的結構 STARTUPINFO 和 PROCESS_INFORMATION 的指針。
在 UNIX 系統調用例子中,父進程通過調用 wait() 系統調用等待子進程的完成;而在 Windows 中與此相當的是 WaitForSingleObject(),用於等待進程完成,它的參數指定了子進程的句柄即 pi.hProcess。一旦子進程退出,控制會從函數 WaitForSingleObject() 回到父進程。
進程終止
當進程完成執行最后語句並且通過系統調用 exit() 請求操作系統刪除自身時,進程終止。這時,進程可以返回狀態值(通常為整數)到父進程(通過系統調用 wait())。所有進程資源,如物理和虛擬內存、打開文件和 I/O 緩沖區等,會由操作系統釋放。
在其他情況下也會出現進程終止。進程通過適當系統調用(如 Windows 的 Terminate-Process()),可以終止另一進程。通常,只有終止進程的父進程才能執行這一系統調用。否則,用戶可以任意終止彼此的作業。記住,如果終止子進程,則父進程需要知道這些子進程的標識符。因此,當一個進程創建新進程時,新創建進程的標識符要傳遞到父進程。
父進程終止子進程的原因有很多,如:
-
子進程使用了超過它所分配的資源。(為判定是否發生這種情況,父進程應有一個機制,以檢查子進程的狀態)。
-
分配給子進程的任務,不再需要。
-
父進程正在退出,而且操作系統不允許無父進程的子進程繼續執行。
有些系統不允許子進程在父進程已終止的情況下存在。對於這類系統,如果一個進程終止(正常或不正常),那么它的所有子進程也應終止。這種現象,稱為級聯終止,通常由操作系統來啟動。
為了說明進程執行和終止,下面以 Linux 和 UNIX 系統為例:可以通過系統調用 exit() 來終止進程,還可以將退出狀態作為參數來提供。
/* exit with status 1 */
exit(1);
事實上,在正常終止時,exit() 可以直接調用(如上所示),也可以間接調用(通過 main() 的返回語句)。
父進程可以通過系統調用 wait(),等待子進程的終止。系統調用 wait() 可以通過參數,讓父進程獲得子進程的退出狀態;這個系統調用也返回終止子進程的標識符,這樣父進程能夠知道哪個子進程已經終止了:
pid_t pid;
int status;
pid = wait(festatus);
當一個進程終止時,操作系統會釋放其資源。不過,它位於進程表中的條目還是在的,直到它的父進程調用 wait();這是因為進程表包含了進程的退出狀態。當進程已經終止,但是其父進程尚未調用 wait(),這樣的進程稱為僵屍進程。
所有進程終止時都會過渡到這種狀態,但是一般而言僵屍只是短暫存在。一旦父進程調用了 wait(),僵屍進程的進程標識符和它在進程表中的條目就會釋放。
如果父進程沒有調用 wait() 就終止,以致於子進程成為孤兒進程,那么這會發生什么?Linux 和 UNIX 對這種情況的處理是:將 init 進程作為孤兒進程的父進程。進程 init 定期調用 wait(),以便收集任何孤兒進程的退出狀態,並釋放孤兒進程標識符和進程表條目。