UNIX 進(jìn)程揭秘
分配給系統(tǒng)管理員的許多工作之一是確保用戶的程序正確運(yùn)行。因?yàn)橄到y(tǒng)上存在其他并發(fā)運(yùn)行的程序,所以此任務(wù)變得更加復(fù)雜。由于種種原因,這些程序可能會(huì)失敗、掛起或行為異常。在構(gòu)建更可靠的系統(tǒng)時(shí),了解 Unix® 環(huán)境如何創(chuàng)建、管理和銷毀這些作業(yè)是至關(guān)重要的步驟。
開發(fā)人員還必須積極了解內(nèi)核如何管理進(jìn)程,因?yàn)榕c系統(tǒng)的其他部分和睦相處的應(yīng)用程序會(huì)占用更少的資源,并且不會(huì)頻繁地給系統(tǒng)管理員帶來麻煩。由于導(dǎo)致僵死進(jìn)程(將在稍后對(duì)其進(jìn)行描述)而頻繁重新啟動(dòng)的應(yīng)用程序明顯是不可取的。對(duì)控制進(jìn)程的 UNIX 系統(tǒng)調(diào)用的了解可以使開發(fā)人員編寫能夠在后臺(tái)自動(dòng)運(yùn)行的軟件,而不是需要一個(gè)始終保持在用戶屏幕上的終端會(huì)話。
管理這些程序的基本構(gòu)件就是進(jìn)程。進(jìn)程是賦予某個(gè)由操作系統(tǒng)執(zhí)行的程序的名稱。如果您熟悉 ps 命令,則您應(yīng)該熟悉進(jìn)程清單,如清單 1 所示。
清單 1. ps 命令的輸出
sunbox#ps -ef UID PID PPID CSTIME TTY TIME CMDroot 0 0 0 20:15:23 ? 0:14 schedroot 1 0 0 20:15:24 ? 0:00 /sbin/initroot 2 0 0 20:15:24 ? 0:00 pageoutroot 3 0 0 20:15:24 ? 0:00 fsflush daemon 240 1 0 20:16:37 ? 0:00 /usr/lib/nfs/statd...
前三列對(duì)這里的討論非常重要。第一列列出用于運(yùn)行該進(jìn)程的用戶身份,第二列列出進(jìn)程的 ID,第三列列出該進(jìn)程的父進(jìn)程 ID。最后一列是進(jìn)程的描述,通常是所運(yùn)行的二進(jìn)制文件的名稱。每個(gè)進(jìn)程都被分配一個(gè)標(biāo)識(shí)符,稱為進(jìn)程標(biāo)識(shí)符(Process IdentifIEr,PID)。進(jìn)程還有父進(jìn)程,在大多數(shù)情況下就是啟動(dòng)它的進(jìn)程的 PID。
父 PID (PPID) 的存在意味著這是一個(gè)由別的進(jìn)程創(chuàng)建的進(jìn)程。最初創(chuàng)建進(jìn)程的原始進(jìn)程名為 init,它始終被賦予 PID 1。init 是將在內(nèi)核啟動(dòng)時(shí)啟動(dòng)的第一個(gè)實(shí)際進(jìn)程。啟動(dòng)系統(tǒng)的其余部分是 init 的工作。init 和其他具有 PPID 0 的進(jìn)程屬于內(nèi)核。
使用 fork 系統(tǒng)調(diào)用
fork(2) 系統(tǒng)調(diào)用創(chuàng)建一個(gè)新進(jìn)程。清單 2 顯示了一個(gè)簡(jiǎn)單 C 代碼片段中使用的 fork。
清單 2. 簡(jiǎn)單的 fork(2) 用法
sunbox$ cat fork1.c#include <unistd.h>#include <stdio.h>int main (void) {pid_t p; /* fork returns type pid_t */p = fork();printf("fork returned %dn", p);}sunbox$ gcc fork1.c -o fork1sunbox$ ./fork1fork returned 0fork returned 698
fork1.c 中的代碼不過就是發(fā)出 fork 調(diào)用,并通過一個(gè) printf 調(diào)用來打印整數(shù)結(jié)果。雖然只發(fā)出了一個(gè)調(diào)用,但是打印了兩次輸出。這是因?yàn)樵?fork 調(diào)用中創(chuàng)建了一個(gè)新進(jìn)程。現(xiàn)在有兩個(gè)單獨(dú)的進(jìn)程在從該調(diào)用返回結(jié)果。這通常被描述為“調(diào)用一次,返回兩次。
fork 返回的值非常有趣。其中一個(gè)返回 0;另一個(gè)返回一個(gè)非零值。獲得 0 的進(jìn)程稱為子進(jìn)程,非零結(jié)果屬于原始進(jìn)程,即父進(jìn)程。您將使用返回值來確定哪個(gè)是父進(jìn)程,哪個(gè)是子進(jìn)程。由于兩個(gè)進(jìn)程都在同一空間中繼續(xù)運(yùn)行,唯一有實(shí)際意義的區(qū)別是從 fork 返回的值。
0 和非零返回值的基本原理在于,子進(jìn)程始終可以通過 getppid(2) 調(diào)用來找出其父進(jìn)程是誰,但是父進(jìn)程要找出它的所有子進(jìn)程卻很困難。因此,要告訴父進(jìn)程關(guān)于其新的子進(jìn)程的信息,而子進(jìn)程可在需要時(shí)查找其父進(jìn)程。
考慮到 fork 的返回值,現(xiàn)在該代碼可以檢查確定它是父進(jìn)程還是子進(jìn)程,并進(jìn)行相應(yīng)的操作。清單 3 顯示了一個(gè)基于 fork 的結(jié)果來打印不同輸出的程序。
清單 3. 更完整的 fork 用法示例
sunbox$ cat fork2.c#include <unistd.h>#include <stdio.h>int main (void) {pid_t p;printf("Original program, pid=%dn", getpid());p = fork();if (p == 0) {printf("In child process, pid=%d, ppid=%dn",getpid(), getppid());} else {printf("In parent, pid=%d, fork returned=%dn",getpid(), p);}}sunbox$ gcc fork2.c -o fork2sunbox$ ./fork2Original program, pid=767In child process, pid=768, ppid=767In parent, pid=767, fork returned=768
清單 3 在每個(gè)步驟打印出 PID,并且該代碼檢查從 fork 返回的值來確定哪個(gè)進(jìn)程是父進(jìn)程,哪個(gè)進(jìn)程是子進(jìn)程。對(duì)所打印的 PID 進(jìn)行比較,可以看到原始進(jìn)程是父進(jìn)程 (PID 767),并且子進(jìn)程 (PID 768) 知道其父進(jìn)程是誰。請(qǐng)注意子進(jìn)程如何通過 getppid 來知道其父進(jìn)程以及父進(jìn)程如何使用 fork 來定位其子進(jìn)程。
現(xiàn)在您已經(jīng)了解了復(fù)制某個(gè)進(jìn)程的方法,下面讓我們研究如何運(yùn)行一個(gè)不同的進(jìn)程。fork 只是進(jìn)程機(jī)制中的一半。exec 系列系統(tǒng)調(diào)用運(yùn)行實(shí)際的程序。
使用 exec 系列系統(tǒng)調(diào)用
exec 的工作是將當(dāng)前進(jìn)程替換為一個(gè)新進(jìn)程。請(qǐng)注意“替換這個(gè)措詞的含義。在您調(diào)用 exec 以后,當(dāng)前進(jìn)程就消失了,新進(jìn)程就啟動(dòng)了。如果希望創(chuàng)建一個(gè)單獨(dú)的進(jìn)程,您必須首先運(yùn)行 fork,然后在子進(jìn)程中執(zhí)行 (exec) 新的二進(jìn)制文件。清單 4 顯示了這樣一種情況。
清單 4. 通過將 fork 與 exec 配合使用來運(yùn)行不同的程序
sunbox$ cat exec1.c#include <unistd.h>#include <stdio.h>int main (void) {/* Define a null terminated array of the command to run followed by any parameters, in this case none */char *arg[] = { "/usr/bin/ls", 0 };/* fork, and exec within child process */if (fork() == 0) {printf("In child process:n");execv(arg[0], arg);printf("I will never be calledn");}printf("Execution continues in parent processn");}sunbox$ gcc exec1.c -o exec1sunbox$ ./exec1In child process:fork1.c exec1fork2 exec1.c fork1fork2.c Execution continues in parent process
清單 4 中的代碼首先定義一個(gè)數(shù)組,其中第一個(gè)元素是要執(zhí)行的二進(jìn)制文件的路徑,其余元素充當(dāng)命令行參數(shù)。根據(jù)手冊(cè)頁的描述,該數(shù)組以 Null 結(jié)尾。在從 fork 系統(tǒng)調(diào)用返回以后,將指示子進(jìn)程執(zhí)行 (execv) 新的二進(jìn)制文件。
execv 調(diào)用首先取得一個(gè)指向要運(yùn)行的二進(jìn)制文件名稱的指針,然后取得一個(gè)指向您前面聲明的參數(shù)數(shù)組的指針。該數(shù)組的第一個(gè)元素實(shí)際上是二進(jìn)制文件的名稱,因此參數(shù)實(shí)際上是從第二個(gè)元素開始的。請(qǐng)注意,該子進(jìn)程一直沒有從 execv 調(diào)用返回。這表明正在運(yùn)行的進(jìn)程已被新進(jìn)程所替換。
還存在其他執(zhí)行 (exec) 某個(gè)進(jìn)程的系統(tǒng)調(diào)用,它們的區(qū)別在于接受參數(shù)的方式和是否需要傳遞環(huán)境變量。execv(2) 是替換當(dāng)前映像的較簡(jiǎn)單方法之一,因?yàn)樗恍枰P(guān)于環(huán)境的信息,并且它使用以 Null 結(jié)尾的數(shù)組。其他選項(xiàng)包括 execl(2)(它單獨(dú)接受各個(gè)參數(shù))或 execvp(2)(它也接受一個(gè)以 Null 結(jié)尾的環(huán)境變量數(shù)組)。使問題復(fù)雜化的是,并非所有操作系統(tǒng)都支持所有變體。關(guān)于使用哪一種變體的決定取決于平臺(tái)、編碼風(fēng)格和是否需要定義任何環(huán)境變量。
調(diào)用 fork 時(shí),打開的文件會(huì)發(fā)生什么情況呢?
當(dāng)某個(gè)進(jìn)程復(fù)制它自身時(shí),內(nèi)核生成所有打開的文件描述符的副本。文件描述符是指向打開的文件或設(shè)備的整數(shù),并用于執(zhí)行讀取和寫入。如果在調(diào)用 fork 前,某個(gè)程序已經(jīng)打開了一個(gè)文件,如果兩個(gè)進(jìn)程都嘗試執(zhí)行讀取或?qū)懭耄瑫?huì)發(fā)生什么情況呢?一個(gè)進(jìn)程會(huì)改寫另一個(gè)進(jìn)程中的數(shù)據(jù)嗎?是否會(huì)讀取該文件的兩個(gè)副本?清單 5 對(duì)此進(jìn)行了研究,它打開兩個(gè)文件——一個(gè)文件用于讀取,另一個(gè)文件用于寫入——并讓父進(jìn)程和子進(jìn)程同時(shí)執(zhí)行讀取和寫入。
清單 5. 同時(shí)對(duì)同一文件執(zhí)行讀取和寫入的兩個(gè)進(jìn)程
#include <stdio.h>#include <strings.h>#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int main(void) {int fd_in, fd_out;char buf[1024];memset(buf, 0, 1024); /* clear buffer*/fd_in = open("/tmp/infile", O_RDONLY);fd_out = open("/tmp/outfile", O_WRONLY|O_CREAT);fork(); /* It doesn't matter about child vs parent */while (read(fd_in, buf, 2) > 0) { /* Loop through the infile */printf("%d: %s", getpid(), buf);/* Write a line */sprintf(buf, "%d Hello, world!nr", getpid());write(fd_out, buf, strlen(buf));sleep(1);memset(buf, 0, 1024); /* clear buffer*/}sleep(10);}sunbox$ gcc fdtest1.c -o fdtest1sunbox$ ./fdtest12875: 12874: 22875: 32874: 42875: 52874: 62874: 7sunbox$ cat /tmp/outfile2875 Hello, world!2874 Hello, world!2875 Hello, world!2874 Hello, world!2875 Hello, world!2874 Hello, world!2874 Hello, world!
清單 5 是用于打開文件的簡(jiǎn)單程序,并派生 (fork) 為父進(jìn)程和子進(jìn)程。每個(gè)進(jìn)程從同一文件描述符(它只是一個(gè)包含數(shù)字 1 至 7 的文本文件)執(zhí)行讀取操作,并連同 PID 一起打印所讀取的內(nèi)容。在讀取一行之后,將 PID 寫到輸出文件。當(dāng)輸入文件中沒有其他字符可供讀取時(shí),循環(huán)結(jié)束。
清單 5 的輸出表明,當(dāng)一個(gè)進(jìn)程從該文件讀取時(shí),兩個(gè)進(jìn)程的文件指針都在移動(dòng)。同樣地,當(dāng)向某個(gè)文件寫入時(shí),下一個(gè)字符被寫到文件結(jié)尾。這是非常有意義的,因?yàn)閮?nèi)核跟蹤打開文件的信息。文件描述符只不過是進(jìn)程的標(biāo)識(shí)符。
您可能還知道,標(biāo)準(zhǔn)輸出(屏幕)也是一個(gè)文件描述符。此文件描述符在 fork 調(diào)用期間被復(fù)制,這就是兩個(gè)進(jìn)程都能對(duì)屏幕執(zhí)行寫入操作的原因。
父進(jìn)程或子進(jìn)程的終止
進(jìn)程必須在某個(gè)時(shí)候終止。問題只是哪個(gè)進(jìn)程首先終止:父進(jìn)程還是子進(jìn)程。
父進(jìn)程在子進(jìn)程之前終止
如果父進(jìn)程在子進(jìn)程之前終止,孤立的子進(jìn)程需要知道它們的父進(jìn)程是誰。記住,每個(gè)進(jìn)程都有父進(jìn)程,并且您可以跟蹤從每個(gè)子進(jìn)程一直到 PID 1(或稱為 init)的整個(gè)進(jìn)程家族樹。當(dāng)某個(gè)父進(jìn)程終止時(shí),init 將接納所有子進(jìn)程,如清單 6 所示。
清單 6. 在子進(jìn)程之前終止的父進(jìn)程
#include <unistd.h>#include <stdio.h>int main(void) {int i;if (fork()) {/* Parent */sleep(2);_exit(0);}for (i=0; i < 5; i++) {printf("My parent is %dn", getppid());sleep(1);}}sunbox$ gcc dIE1.c -o die1sunbox$ ./die1My parent is 2920My parent is 2920sunbox$ My parent is 1My parent is 1My parent is 1
在此例中,父進(jìn)程調(diào)用 fork,等待兩秒鐘,然后退出。子進(jìn)程在五秒鐘內(nèi)繼續(xù)打印其父 PID。可以看到,PPID 在父進(jìn)程終止后更改為 1。Shell 提示符的返回也是非常有趣的。由于子進(jìn)程在后臺(tái)運(yùn)行,父進(jìn)程一終止,控制即返回到 Shell。
子進(jìn)程在父進(jìn)程之前終止
清單 7 與清單 6 相反——即在父進(jìn)程之前終止的子進(jìn)程。為更好地說明所發(fā)生的事情,進(jìn)程本身中沒有打印任何內(nèi)容。而有趣的信息來自于進(jìn)程清單。
清單 7. 子進(jìn)程在父進(jìn)程之前終止
sunbox$ cat dIE2.c#include <unistd.h>#include <stdio.h>int main(void) {int i;if (!fork()) {/* Child exits immediately*/_exit(0);}/* Parent waits around for a minute */sleep(60);}sunbox$ gcc die2.c -o die2sunbox$ ./die2 &[1] 2934sunbox$ ps -ef | grep 2934sean 2934 2885 0 21:43:05 pts/1 0:00 ./die2sean 2935 2934 0- ? 0:00 <defunct>sunbox$ ps -ef | grep 2934[1]+ Exit 199./die2
die2 使用 & 操作符在后臺(tái)運(yùn)行,然后顯示一個(gè)進(jìn)程清單,并且僅顯示正在運(yùn)行的進(jìn)程及其子進(jìn)程。PID 2934 是父進(jìn)程,PID 2935 是派生 (fork) 并立即終止的進(jìn)程。盡管子進(jìn)程提前退出,但它仍然在進(jìn)程表中作為失效 (defunct) 進(jìn)程存在,或稱為僵死 (zombie) 進(jìn)程。當(dāng)父進(jìn)程在 60 秒以后終止時(shí),兩個(gè)進(jìn)程都消失了。
當(dāng)子進(jìn)程終止時(shí),會(huì)使用一個(gè)名為 SIGCHLD 的信號(hào)來通知其父進(jìn)程。該通知的確切機(jī)制現(xiàn)在對(duì)您并不重要。重要的是父進(jìn)程必須以某種方式確認(rèn)子進(jìn)程的終止。子進(jìn)程從終止時(shí)起就一直處于僵死狀態(tài),直到父進(jìn)程確認(rèn)該信號(hào)為止。僵死進(jìn)程不運(yùn)行或消耗 CPU 周期;它只是占用進(jìn)程表空間。當(dāng)父進(jìn)程終止時(shí),內(nèi)核最終能夠回收未確認(rèn)的子進(jìn)程以及父進(jìn)程。這意味著可消除僵死進(jìn)程的唯一方法是終止父進(jìn)程。處理僵死進(jìn)程的最好方法是首先確保它們不會(huì)發(fā)生。清單 8 中的代碼實(shí)現(xiàn)了一個(gè)處理傳入的 SIGCHLD 信號(hào)的信號(hào)處理程序。
清單 8. 實(shí)際操作中的信號(hào)處理程序
#include <unistd.h>#include <stdio.h>#include <sys/types.h>#include <sys/wait.h>void sighandler(int sig) {printf("In signal handler for signal %dn", sig);/* wait() is the key to acknowledging the SIGCHLD */wait(0);}int main(void) {int i;/* Assign a signal handler to SIGCHLD */sigset(SIGCHLD, &sighandler);if (!fork()) {/* Child */_exit(0);}sleep(60);}sunbox$ gcc dIE3.c -o die3sunbox$ ./die3 &[1] 3116sunbox$ In signal handler for signal 18ps -ef | grep 3116sean 3116 2885 0 22:37:26 pts/1 0:00 ./die3
由于使用了 sigset 函數(shù)(它向信號(hào)處理程序分配一個(gè)函數(shù)指針),清單 8 比前一個(gè)示例稍微復(fù)雜一點(diǎn),。每當(dāng)進(jìn)程接收到某個(gè)已處理的信號(hào)時(shí),就會(huì)調(diào)用通過 sigset 分配的函數(shù)。對(duì)于 SIGCHLD 信號(hào),應(yīng)用程序必須調(diào)用 wait(3c) 函數(shù),以等待子進(jìn)程退出。由于該進(jìn)程已經(jīng)退出,這相當(dāng)于向內(nèi)核確認(rèn)了子進(jìn)程的終止。實(shí)際上,父進(jìn)程所做的工作可能不只是確認(rèn)該信息。它還可能需要清理子進(jìn)程的數(shù)據(jù)。
在執(zhí)行 die3 以后,代碼檢查了進(jìn)程清單,并干凈地執(zhí)行子進(jìn)程。然后使用值 18 (SIGCHLD) 來調(diào)用信號(hào)處理程序,確認(rèn)子進(jìn)程的退出,并且父進(jìn)程返回到 sleep(60)。
總結(jié)
Unix 進(jìn)程是在某個(gè)進(jìn)程調(diào)用 fork 時(shí)創(chuàng)建的,fork 將正在運(yùn)行的可執(zhí)行進(jìn)程一分為二。然后該進(jìn)程可以執(zhí)行 exec 系列中的某個(gè)系統(tǒng)調(diào)用,從而將當(dāng)前運(yùn)行的映像替換為新的映像。
當(dāng)父進(jìn)程終止時(shí),其所有子進(jìn)程將由 PID 為 1 的 init 接納。如果子進(jìn)程在父進(jìn)程之前終止,則會(huì)向父進(jìn)程發(fā)送一個(gè)信號(hào),然后子進(jìn)程轉(zhuǎn)變?yōu)榻┧罓顟B(tài),直到該信號(hào)得到確認(rèn),或父進(jìn)程被終止。
現(xiàn)在您已了解了進(jìn)程是如何創(chuàng)建和銷毀的,您已經(jīng)為處理運(yùn)行您系統(tǒng)的進(jìn)程作了更好的準(zhǔn)備,尤其是大量使用多進(jìn)程的系統(tǒng),例如 Apache。如果您需要執(zhí)行某些故障排除,能夠跟蹤某個(gè)特定進(jìn)程的進(jìn)程樹還允許您將任何應(yīng)用程序追溯到創(chuàng)建它的進(jìn)程。

網(wǎng)公網(wǎng)安備