大多數(shù)系統(tǒng)的進程能夠并發(fā)執(zhí)行,它們可以動態(tài)創(chuàng)建和刪除。因此,操作系統(tǒng)必須提供機制,用于創(chuàng)建進程和終止進程。
進程創(chuàng)建
進程在執(zhí)行過程中可能創(chuàng)建多個新的進程。
創(chuàng)建進程稱為父進程,而新的進程稱為子進程。每個新進程可以再創(chuàng)建其他進程,從而形成
進程樹。
大多數(shù)的操作系統(tǒng)(包括 UNIX、Linux 和 Windows)對進程的識別采用的是唯一的
進程標識符(pid),pid 通常是一個整數(shù)值。系統(tǒng)內(nèi)的每個進程都有一個唯一 pid,它可以用作索引,以便訪問內(nèi)核中的進程的各種屬性。
圖 1 典型Linux系統(tǒng)的一個進程樹
圖 1 顯示了 Linux 操作系統(tǒng)的一個典型進程樹,包括進程的名稱和 pid(我們通常使用進程這個術(shù)語,不過 Linux 偏愛"
任務(wù)"這個術(shù)語)。進程 init(它的 pid 總是 1),作為所有用戶進程的根進程或父進程。一旦系統(tǒng)啟動后,進程init可以創(chuàng)建各種用戶進程,如 Web 服務(wù)器、打印服務(wù)器、ssh 服務(wù)器等。
在圖 1 中,kthreadd 和 sshd 為 init 的兩個子進程。kthreadd 進程負責(zé)創(chuàng)建額外進程,以便執(zhí)行內(nèi)核任務(wù)(這里為 khelper 和 pdflush)。sshd 進程負責(zé)管理通過 ssh 連到系統(tǒng)的客戶端。login 進程負責(zé)管理直接登錄到系統(tǒng)的客戶端。在這個例子中,客戶已登錄,并且使用 bash 外殼,它所分配的 pid 為 8416。采用 bash 命令行界面,這個進程還創(chuàng)建了進程 ps 和 emacs 編輯器。
對于 UNIX 和 Linux 系統(tǒng),我們可以通過 ps 命令得到一個進程列表。例如,命令
ps -el
可以列出系統(tǒng)中的所有當前活動進程的完整信息。通過遞歸跟蹤父進程一直到進程 init,可以輕松構(gòu)造類似圖 1 所示的進程樹。
一般來說,當一個進程創(chuàng)建子進程時,該子進程需要一定的資源(CPU 時間、內(nèi)存、文件、I/O 設(shè)備等)來完成任務(wù)。子進程可以從操作系統(tǒng)那里直接獲得資源,也可以只從父進程那里獲得資源子集。父進程可能要在子進程之間分配資源或共享資源(如內(nèi)存或文件)。限制子進程只能使用父進程的資源,可以防止創(chuàng)建過多進程,導(dǎo)致系統(tǒng)超載。
除了提供各種物理和邏輯資源外,父進程也可能向子進程傳遞初始化數(shù)據(jù)(或輸入)。例如,假設(shè)有一個進程,其功能是在終端屏幕上顯示文件如 image.jpg 的狀態(tài)。當該進程被創(chuàng)建時,它會從父進程處得到輸入,即文件名稱 image.jpg。通過這個名稱,它會打開文件,進而寫出內(nèi)容。它也可以得到輸出設(shè)備名稱。另外,有的操作系統(tǒng)會向子進程傳遞資源。對于這種系統(tǒng),新進程可得到兩個打開文件,即 image.jpg 和終端設(shè)備,并且可以在這兩者之間進行數(shù)據(jù)傳輸。
當進程創(chuàng)建新進程時,可有兩種執(zhí)行可能:
- 父進程與子進程并發(fā)執(zhí)行。
- 父進程等待,直到某個或全部子進程執(zhí)行完。
新進程的地址空間也有兩種可能:
- 子進程是父進程的復(fù)制品(它具有與父進程同樣的程序和數(shù)據(jù))。
- 子進程加載另一個新程序。
為了說明這些不同,首先看一看 UNIX 操作系統(tǒng)。在 UNIX 中,正如以前所述,每個進程都用一個唯一的整型進程標識符來標識。通過系統(tǒng)調(diào)用
fork(),可創(chuàng)建新進程。新進程的地址空間復(fù)制了原來進程的地址空間。這種機制允許父進程與子進程輕松通信。這兩個進程(父和子)都繼續(xù)執(zhí)行處于系統(tǒng)調(diào)用 fork() 之后的指令,但有一點不同:對于新(子)進程,系統(tǒng)調(diào)用 fork() 的返回值為 0;而對于父進程,返回值為子進程的進程標識符(非零)。
通常,在系統(tǒng)調(diào)用 fork() 之后,有個進程使用系統(tǒng)調(diào)用 exec(),以用新程序來取代進程的內(nèi)存空間。系統(tǒng)調(diào)用 exec() 加載二進制文件到內(nèi)存中(破壞了包含系統(tǒng)調(diào)用 exec() 的原來程序的內(nèi)存內(nèi)容),并開始執(zhí)行。采用這種方式,這兩個進程能相互通信,并能按各自方法運行。父進程能夠創(chuàng)建更多子進程,或者如果在子進程運行時沒有什么可做,那么它采用系統(tǒng)調(diào)用 wait() 把自己移出就緒隊列,直到子進程終止。因為調(diào)用 exec() 用新程序覆蓋了進程的地址空間,所以調(diào)用 exec() 除非出現(xiàn)錯誤,不會返回控制。
- #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 系統(tǒng)調(diào)用例子中。這里有兩個不同進程,但運行同一程序。這兩個進程的唯一差別是:子進程的 pid 值為0,而父進程的 pid 值大于0(實際上,它就是子進程的 pid)。子進程繼承了父進程的權(quán)限、調(diào)度屬性以及某些資源,諸如打開文件。
通過系統(tǒng)調(diào)用 execlp()(這是系統(tǒng)調(diào)用 exec() 的一個版本),子進程采用 UNIX 命令 /bin/ls(用來列出目錄清單)來覆蓋其地址空間。通過系統(tǒng)調(diào)用 wait(),父進程等待子進程的完成。當子進程完成后(通過顯示或隱式調(diào)用 exit()),父進程會從 wait() 調(diào)用處開始繼續(xù),并且結(jié)束時會調(diào)用系統(tǒng)調(diào)用 exit()。這可用圖 2 表示。
圖 2 通過系統(tǒng)調(diào)用 fork() 創(chuàng)建進程
當然,沒有什么可以阻止子進程不調(diào)用 exec(),而是繼續(xù)作為父進程的副本來執(zhí)行。在這種情況下,父進程和子進程會并發(fā)執(zhí)行,并采用同樣的代碼指令。由于子進程是父進程的一個副本,這兩個進程都有各自的數(shù)據(jù)副本。
作為另一個例子,接下來看一看 Windows 的進程創(chuàng)建。進程創(chuàng)建采用 Windows API 函數(shù) CreateProcess(),它類似于 fork()(這是父進程用于創(chuàng)建子進程的)。不過,fork() 讓子進程繼承了父進程的地址空間,而 CreateProcess() 在進程創(chuàng)建時要求將一個特定程序加載到子進程的地址空間。再者,fork() 不需要傳遞任何參數(shù),而 CreateProcess() 需要傳遞至少 10 個參數(shù)。
- #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 程序演示了函數(shù) CreateProcess(),它創(chuàng)建了一個子進程,并且加載了應(yīng)用程序 mspaint.exe。這里選擇了 10 個參數(shù)中的許多默認值來傳遞給 CreateProcess()。
傳遞給 CreateProcess() 的兩個參數(shù),為結(jié)構(gòu) STARTUPINFO 和 PROCESS-INFORMATION 的實例:
- 結(jié)構(gòu) STARTUPINFO 指定新進程的許多特性,如窗口大小和外觀、標準輸入與輸出的文件句柄等;
- 結(jié)構(gòu) PR0CESS_INF0RMATI0N 含新進程及其線程的句柄與標識符。
在進行 CeateProcess() 之前,調(diào)用函數(shù) ZeroMemory() 來為這些結(jié)構(gòu)分配內(nèi)存。
函數(shù) CreateProcess() 的頭兩個參數(shù)是應(yīng)用程序名稱和命令行參數(shù)。如果應(yīng)用程序名稱為 NULL(這里就是 NULL),那么命令行參數(shù)指定了所要加載的應(yīng)用程序。在這個例子中,加載的是 Microsoft Windows 的 mspaint.exe 應(yīng)用程序。
除了這兩個初始參數(shù)之外,這里使用系統(tǒng)默認參數(shù)來繼承進程和線程句柄,并指定沒有創(chuàng)建標志;另外,這里還使用了父進程的已有環(huán)境塊和啟動目錄。最后,提供了兩個指向程序剛開始時所創(chuàng)建的結(jié)構(gòu) STARTUPINFO 和 PROCESS_INFORMATION 的指針。
在 UNIX 系統(tǒng)調(diào)用例子中,父進程通過調(diào)用 wait() 系統(tǒng)調(diào)用等待子進程的完成;而在 Windows 中與此相當?shù)氖?WaitForSingleObject(),用于等待進程完成,它的參數(shù)指定了子進程的句柄即 pi.hProcess。一旦子進程退出,控制會從函數(shù) WaitForSingleObject() 回到父進程。
進程終止
當進程完成執(zhí)行最后語句并且通過系統(tǒng)調(diào)用
exit() 請求操作系統(tǒng)刪除自身時,進程終止。這時,進程可以返回狀態(tài)值(通常為整數(shù))到父進程(通過系統(tǒng)調(diào)用 wait())。所有進程資源,如物理和虛擬內(nèi)存、打開文件和 I/O 緩沖區(qū)等,會由操作系統(tǒng)釋放。
在其他情況下也會出現(xiàn)進程終止。進程通過適當系統(tǒng)調(diào)用(如 Windows 的 Terminate-Process()),可以終止另一進程。通常,只有終止進程的父進程才能執(zhí)行這一系統(tǒng)調(diào)用。否則,用戶可以任意終止彼此的作業(yè)。記住,如果終止子進程,則父進程需要知道這些子進程的標識符。因此,當一個進程創(chuàng)建新進程時,新創(chuàng)建進程的標識符要傳遞到父進程。
父進程終止子進程的原因有很多,如:
- 子進程使用了超過它所分配的資源。(為判定是否發(fā)生這種情況,父進程應(yīng)有一個機制,以檢查子進程的狀態(tài))。
- 分配給子進程的任務(wù),不再需要。
- 父進程正在退出,而且操作系統(tǒng)不允許無父進程的子進程繼續(xù)執(zhí)行。
有些系統(tǒng)不允許子進程在父進程已終止的情況下存在。對于這類系統(tǒng),如果一個進程終止(正?;虿徽#?,那么它的所有子進程也應(yīng)終止。這種現(xiàn)象,稱為
級聯(lián)終止,通常由操作系統(tǒng)來啟動。
為了說明進程執(zhí)行和終止,下面以 Linux 和 UNIX 系統(tǒng)為例:可以通過系統(tǒng)調(diào)用 exit() 來終止進程,還可以將退出狀態(tài)作為參數(shù)來提供。
/* exit with status 1 */
exit(1);
事實上,在正常終止時,exit() 可以直接調(diào)用(如上所示),也可以間接調(diào)用(通過 main() 的返回語句)。
父進程可以通過系統(tǒng)調(diào)用 wait(),等待子進程的終止。系統(tǒng)調(diào)用 wait() 可以通過參數(shù),讓父進程獲得子進程的退出狀態(tài);這個系統(tǒng)調(diào)用也返回終止子進程的標識符,這樣父進程能夠知道哪個子進程已經(jīng)終止了:
pid_t pid;
int status;
pid = wait(festatus);
當一個進程終止時,操作系統(tǒng)會釋放其資源。不過,它位于進程表中的條目還是在的,直到它的父進程調(diào)用 wait();這是因為進程表包含了進程的退出狀態(tài)。當進程已經(jīng)終止,但是其父進程尚未調(diào)用 wait(),這樣的進程稱為
僵尸進程。
所有進程終止時都會過渡到這種狀態(tài),但是一般而言僵尸只是短暫存在。一旦父進程調(diào)用了 wait(),僵尸進程的進程標識符和它在進程表中的條目就會釋放。
如果父進程沒有調(diào)用 wait() 就終止,以致于子進程成為孤兒進程,那么這會發(fā)生什么?Linux 和 UNIX 對這種情況的處理是:將 init 進程作為孤兒進程的父進程。進程 init 定期調(diào)用 wait(),以便收集任何孤兒進程的退出狀態(tài),并釋放孤兒進程標識符和進程表條目。