1.8.1 簡介
2002 年 3 月 01 日
本文介紹了Linux下的進(jìn)程概念,并著重講解了與Linux進(jìn)程管理相關(guān)的4個(gè)重要系統(tǒng)調(diào)用getpid,fork,exit和_exit,輔助一些例程說明了它們的特點(diǎn)和使用方法。
關(guān)于進(jìn)程的一些必要知識(shí)
先 看一下進(jìn)程在大學(xué)課本里的標(biāo)準(zhǔn)定義:“進(jìn)程是可并發(fā)執(zhí)行的程序在一個(gè)數(shù)據(jù)集合上的運(yùn)行過程。”這個(gè)定義非常嚴(yán)謹(jǐn),而且難懂,如果你沒有一下子理解這句話, 就不妨看看筆者自己的并不嚴(yán)謹(jǐn)?shù)慕忉尅N覀兇蠹叶贾?,硬盤上的一個(gè)可執(zhí)行文件經(jīng)常被稱作程序,在Linux系統(tǒng)中,當(dāng)一個(gè)程序開始執(zhí)行后,在開始執(zhí)行到 執(zhí)行完畢退出這段時(shí)間里,它在內(nèi)存中的部分就被稱作一個(gè)進(jìn)程。
當(dāng)然,這個(gè)解釋并不完善,但好處是容易理解,在以下的文章中,我們將會(huì)對(duì)進(jìn)程作一些更全面的認(rèn)識(shí)。
Linux進(jìn)程簡介
Linux 是一個(gè)多任務(wù)的操作系統(tǒng),也就是說,在同一個(gè)時(shí)間內(nèi),可以有多個(gè)進(jìn)程同時(shí)執(zhí)行。如果讀者對(duì)計(jì)算機(jī)硬件體系有一定了解的話,會(huì)知道我們大家常用的單CPU計(jì) 算機(jī)實(shí)際上在一個(gè)時(shí)間片斷內(nèi)只能執(zhí)行一條指令,那么Linux是如何實(shí)現(xiàn)多進(jìn)程同時(shí)執(zhí)行的呢?原來Linux使用了一種稱為“進(jìn)程調(diào)度(process scheduling)”的手段,首先,為每個(gè)進(jìn)程指派一定的運(yùn)行時(shí)間,這個(gè)時(shí)間通常很短,短到以毫秒為單位,然后依照某種規(guī)則,從眾多進(jìn)程中挑選一個(gè)投 入運(yùn)行,其他的進(jìn)程暫時(shí)等待,當(dāng)正在運(yùn)行的那個(gè)進(jìn)程時(shí)間耗盡,或執(zhí)行完畢退出,或因某種原因暫停,Linux就會(huì)重新進(jìn)行調(diào)度,挑選下一個(gè)進(jìn)程投入運(yùn)行。 因?yàn)槊總€(gè)進(jìn)程占用的時(shí)間片都很短,在我們使用者的角度來看,就好像多個(gè)進(jìn)程同時(shí)運(yùn)行一樣了。
在Linux中,每個(gè)進(jìn)程在創(chuàng)建 時(shí)都會(huì)被分配一個(gè)數(shù)據(jù)結(jié)構(gòu),稱為進(jìn)程控制塊(Process Control Block,簡稱PCB)。PCB中包含了很多重要的信息,供系統(tǒng)調(diào)度和進(jìn)程本身執(zhí)行使用,其中最重要的莫過于進(jìn)程ID(process ID)了,進(jìn)程ID也被稱作進(jìn)程標(biāo)識(shí)符,是一個(gè)非負(fù)的整數(shù),在Linux操作系統(tǒng)中唯一地標(biāo)志一個(gè)進(jìn)程,在我們最常使用的I386架構(gòu)(即PC使用的架 構(gòu))上,一個(gè)非負(fù)的整數(shù)的變化范圍是0-32767,這也是我們所有可能取到的進(jìn)程ID。其實(shí)從進(jìn)程ID的名字就可以看出,它就是進(jìn)程的身份證號(hào)碼,每個(gè) 人的身份證號(hào)碼都不會(huì)相同,每個(gè)進(jìn)程的進(jìn)程ID也不會(huì)相同。
一個(gè)或多個(gè)進(jìn)程可以合起來構(gòu)成一個(gè)進(jìn)程組(process group),一個(gè)或多個(gè)進(jìn)程組可以合起來構(gòu)成一個(gè)會(huì)話(session)。這樣我們就有了對(duì)進(jìn)程進(jìn)行批量操作的能力,比如通過向某個(gè)進(jìn)程組發(fā)送信號(hào)來實(shí)現(xiàn)向該組中的每個(gè)進(jìn)程發(fā)送信號(hào)。
最后,讓我們通過ps命令親眼看一看自己的系統(tǒng)中目前有多少進(jìn)程在運(yùn)行:
$ps -aux (以下是在我的計(jì)算機(jī)上的運(yùn)行結(jié)果,你的結(jié)果很可能與這不同。) USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.1 0.4 1412 520 ? S May15 0:04 init [3] root 2 0.0 0.0 0 0 ? SW May15 0:00 [keventd] root 3 0.0 0.0 0 0 ? SW May15 0:00 [kapm-idled] root 4 0.0 0.0 0 0 ? SWN May15 0:00 [ksoftirqd_CPU0] root 5 0.0 0.0 0 0 ? SW May15 0:00 [kswapd] root 6 0.0 0.0 0 0 ? SW May15 0:00 [kreclaimd] root 7 0.0 0.0 0 0 ? SW May15 0:00 [bdflush] root 8 0.0 0.0 0 0 ? SW May15 0:00 [kupdated] root 9 0.0 0.0 0 0 ? SW< May15 0:00 [mdrecoveryd] root 13 0.0 0.0 0 0 ? SW May15 0:00 [kjournald] root 132 0.0 0.0 0 0 ? SW May15 0:00 [kjournald] root 673 0.0 0.4 1472 592 ? S May15 0:00 syslogd -m 0 root 678 0.0 0.8 2084 1116 ? S May15 0:00 klogd -2 rpc 698 0.0 0.4 1552 588 ? S May15 0:00 portmap rpcuser 726 0.0 0.6 1596 764 ? S May15 0:00 rpc.statd root 839 0.0 0.4 1396 524 ? S May15 0:00 /usr/sbin/apmd -p root 908 0.0 0.7 2264 1000 ? S May15 0:00 xinetd -stayalive root 948 0.0 1.5 5296 1984 ? S May15 0:00 sendmail: accepti root 967 0.0 0.3 1440 484 ? S May15 0:00 gpm -t ps/2 -m /d wnn 987 0.0 2.7 4732 3440 ? S May15 0:00 /usr/bin/cserver root 1005 0.0 0.5 1584 660 ? S May15 0:00 crond wnn 1025 0.0 1.9 3720 2488 ? S May15 0:00 /usr/bin/tserver xfs 1079 0.0 2.5 4592 3216 ? S May15 0:00 xfs -droppriv -da daemon 1115 0.0 0.4 1444 568 ? S May15 0:00 /usr/sbin/atd root 1130 0.0 0.3 1384 448 tty1 S May15 0:00 /sbin/mingetty tt root 1131 0.0 0.3 1384 448 tty2 S May15 0:00 /sbin/mingetty tt root 1132 0.0 0.3 1384 448 tty3 S May15 0:00 /sbin/mingetty tt root 1133 0.0 0.3 1384 448 tty4 S May15 0:00 /sbin/mingetty tt root 1134 0.0 0.3 1384 448 tty5 S May15 0:00 /sbin/mingetty tt root 1135 0.0 0.3 1384 448 tty6 S May15 0:00 /sbin/mingetty tt root 8769 0.0 0.6 1744 812 ? S 00:08 0:00 in.telnetd: 192.1 root 8770 0.0 0.9 2336 1184 pts/0 S 00:08 0:00 login -- lei lei 8771 0.1 0.9 2432 1264 pts/0 S 00:08 0:00 -bash lei 8809 0.0 0.6 2764 808 pts/0 R 00:09 0:00 ps -aux
|
以上除標(biāo)題外,每一行都代表一個(gè)進(jìn)程。在各列中,PID一列代表了各進(jìn)程的進(jìn)程ID,COMMAND一列代表了進(jìn)程的名稱或在Shell中調(diào)用的命令行,對(duì)其他列的具體含義,我就不再作解釋,有興趣的讀者可以去參考相關(guān)書籍。
getpid
在2.4.4版內(nèi)核中,getpid是第20號(hào)系統(tǒng)調(diào)用,其在Linux函數(shù)庫中的原型是:
#include<sys/types.h> /* 提供類型pid_t的定義 */ #include<unistd.h> /* 提供函數(shù)的定義 */ pid_t getpid(void);
|
getpid的作用很簡單,就是返回當(dāng)前進(jìn)程的進(jìn)程ID,請(qǐng)大家看以下的例子:
/* getpid_test.c */ #include<unistd.h> main() { printf("The current process ID is %d\n",getpid()); }
|
細(xì)心的讀者可能注意到了,這個(gè)程序的定義里并沒有包含 頭文件sys/types.h,這是因?yàn)槲覀冊(cè)诔绦蛑袥]有用到pid_t類型,pid_t類型即為進(jìn)程ID的類型。事實(shí)上,在i386架構(gòu)上(就是我們一 般PC計(jì)算機(jī)的架構(gòu)),pid_t類型是和int類型完全兼容的,我們可以用處理整形數(shù)的方法去處理pid_t類型的數(shù)據(jù),比如,用"%d"把它打印出 來。
編譯并運(yùn)行程序getpid_test.c:
$gcc getpid_test.c -o getpid_test $./getpid_test The current process ID is 1980 (你自己的運(yùn)行結(jié)果很可能與這個(gè)數(shù)字不一樣,這是很正常的。)
|
再運(yùn)行一遍:
$./getpid_test The current process ID is 1981
|
正如我們所見,盡管是同一個(gè)應(yīng)用程序,每一次運(yùn)行的時(shí)候,所分配的進(jìn)程標(biāo)識(shí)符都不相同。
fork
在2.4.4版內(nèi)核中,fork是第2號(hào)系統(tǒng)調(diào)用,其在Linux函數(shù)庫中的原型是:
#include<sys/types.h> /* 提供類型pid_t的定義 */ #include<unistd.h> /* 提供函數(shù)的定義 */ pid_t fork(void);
|
只看fork的名字,可能難得有幾個(gè)人可以猜到它是做什么用的。fork系統(tǒng)調(diào)用的作用是復(fù)制一個(gè)進(jìn)程。當(dāng)一個(gè)進(jìn)程調(diào)用它,完成后就出現(xiàn)兩個(gè)幾乎一模一樣的進(jìn)程,我們也由此得到了一個(gè)新進(jìn)程。據(jù)說fork的名字就是來源于這個(gè)與叉子的形狀頗有幾分相似的工作流程。
在 Linux中,創(chuàng)造新進(jìn)程的方法只有一個(gè),就是我們正在介紹的fork。其他一些庫函數(shù),如system(),看起來似乎它們也能創(chuàng)建新的進(jìn)程,如果能看 一下它們的源碼就會(huì)明白,它們實(shí)際上也在內(nèi)部調(diào)用了fork。包括我們?cè)诿钚邢逻\(yùn)行應(yīng)用程序,新的進(jìn)程也是由shell調(diào)用fork制造出來的。 fork有一些很有意思的特征,下面就讓我們通過一個(gè)小程序來對(duì)它有更多的了解。
/* fork_test.c */ #include<sys/types.h> #inlcude<unistd.h> main() { pid_t pid; /*此時(shí)僅有一個(gè)進(jìn)程*/ pid=fork(); /*此時(shí)已經(jīng)有兩個(gè)進(jìn)程在同時(shí)運(yùn)行*/ if(pid<0) printf("error in fork!"); else if(pid==0) printf("I am the child process, my process ID is %d\n",getpid()); else printf("I am the parent process, my process ID is %d\n",getpid()); }
|
編譯并運(yùn)行:
$gcc fork_test.c -o fork_test $./fork_test I am the parent process, my process ID is 1991 I am the child process, my process ID is 1992
|
看這個(gè)程序的時(shí)候,頭腦中必須首先了解一個(gè)概念:在語句pid=fork()之前,只有一個(gè)進(jìn)程在執(zhí)行這段代碼,但在這條語句之后,就變成兩個(gè)進(jìn)程在執(zhí)行了,這兩個(gè)進(jìn)程的代碼部分完全相同,將要執(zhí)行的下一條語句都是if(pid==0)……。
兩 個(gè)進(jìn)程中,原先就存在的那個(gè)被稱作“父進(jìn)程”,新出現(xiàn)的那個(gè)被稱作“子進(jìn)程”。父子進(jìn)程的區(qū)別除了進(jìn)程標(biāo)志符(process ID)不同外,變量pid的值也不相同,pid存放的是fork的返回值。fork調(diào)用的一個(gè)奇妙之處就是它僅僅被調(diào)用一次,卻能夠返回兩次,它可能有三 種不同的返回值:
- 在父進(jìn)程中,fork返回新創(chuàng)建子進(jìn)程的進(jìn)程ID;
- 在子進(jìn)程中,fork返回0;
- 如果出現(xiàn)錯(cuò)誤,fork返回一個(gè)負(fù)值;
fork出錯(cuò)可能有兩種原因:(1)當(dāng)前的進(jìn)程數(shù)已經(jīng)達(dá)到了系統(tǒng)規(guī)定的上限,這時(shí)errno的值被設(shè)置為EAGAIN。(2)系統(tǒng)內(nèi)存不足,這時(shí)errno的值被設(shè)置為ENOMEM。(關(guān)于errno的意義,請(qǐng)參考本系列的第一篇文章。)
fork系統(tǒng)調(diào)用出錯(cuò)的可能性很小,而且如果出錯(cuò),一般都為第一種錯(cuò)誤。如果出現(xiàn)第二種錯(cuò)誤,說明系統(tǒng)已經(jīng)沒有可分配的內(nèi)存,正處于崩潰的邊緣,這種情況對(duì)Linux來說是很罕見的。
說 到這里,聰明的讀者可能已經(jīng)完全看懂剩下的代碼了,如果pid小于0,說明出現(xiàn)了錯(cuò)誤;pid==0,就說明fork返回了0,也就說明當(dāng)前進(jìn)程是子進(jìn) 程,就去執(zhí)行printf("I am the child!"),否則(else),當(dāng)前進(jìn)程就是父進(jìn)程,執(zhí)行printf("I am the parent!")。完美主義者會(huì)覺得這很冗余,因?yàn)閮蓚€(gè)進(jìn)程里都各有一條它們永遠(yuǎn)執(zhí)行不到的語句。不必過于為此耿耿于懷,畢竟很多年以前,UNIX的鼻 祖?zhèn)冊(cè)诋?dāng)時(shí)內(nèi)存小得無法想象的計(jì)算機(jī)上就是這樣寫程序的,以我們?nèi)缃竦?#8220;海量”內(nèi)存,完全可以把這幾個(gè)字節(jié)的顧慮拋到九霄云外。
說 到這里,可能有些讀者還有疑問:如果fork后子進(jìn)程和父進(jìn)程幾乎完全一樣,而系統(tǒng)中產(chǎn)生新進(jìn)程唯一的方法就是fork,那豈不是系統(tǒng)中所有的進(jìn)程都要一 模一樣嗎?那我們要執(zhí)行新的應(yīng)用程序時(shí)候怎么辦呢?從對(duì)Linux系統(tǒng)的經(jīng)驗(yàn)中,我們知道這種問題并不存在。至于采用了什么方法,我們把這個(gè)問題留到后面 具體討論。
exit
在2.4.4版內(nèi)核中,exit是第1號(hào)調(diào)用,其在Linux函數(shù)庫中的原型是:
#include<stdlib.h> void exit(int status);
|
不像fork那么難理解,從exit的名字就能看出,這個(gè)系統(tǒng)調(diào)用是用來終止一個(gè)進(jìn)程的。無論在程序中的什么位置,只要執(zhí)行到exit系統(tǒng)調(diào)用,進(jìn)程就會(huì)停止剩下的所有操作,清除包括PCB在內(nèi)的各種數(shù)據(jù)結(jié)構(gòu),并終止本進(jìn)程的運(yùn)行。請(qǐng)看下面的程序:
/* exit_test1.c */ #include<stdlib.h> main() { printf("this process will exit!\n"); exit(0); printf("never be displayed!\n"); }
|
編譯后運(yùn)行:
$gcc exit_test1.c -o exit_test1 $./exit_test1 this process will exit!
|
我們可以看到,程序并沒有打印后面的"never be displayed!\n",因?yàn)樵诖酥埃趫?zhí)行到exit(0)時(shí),進(jìn)程就已經(jīng)終止了。
exit 系統(tǒng)調(diào)用帶有一個(gè)整數(shù)類型的參數(shù)status,我們可以利用這個(gè)參數(shù)傳遞進(jìn)程結(jié)束時(shí)的狀態(tài),比如說,該進(jìn)程是正常結(jié)束的,還是出現(xiàn)某種意外而結(jié)束的,一般 來說,0表示沒有意外的正常結(jié)束;其他的數(shù)值表示出現(xiàn)了錯(cuò)誤,進(jìn)程非正常結(jié)束。我們?cè)趯?shí)際編程時(shí),可以用wait系統(tǒng)調(diào)用接收子進(jìn)程的返回值,從而針對(duì)不 同的情況進(jìn)行不同的處理。關(guān)于wait的詳細(xì)情況,我們將在以后的篇幅中進(jìn)行介紹。
exit和_exit
作為系統(tǒng)調(diào)用而言,_exit和exit是一對(duì)孿生兄弟,它們究竟相似到什么程度,我們可以從Linux的源碼中找到答案:
#define __NR__exit __NR_exit /* 摘自文件include/asm-i386/unistd.h第334行 */
“__NR_”是在Linux的源碼中為每個(gè)系統(tǒng)調(diào)用加上的前綴,請(qǐng)注意第一個(gè)exit前有2條下劃線,第二個(gè)exit前只有1條下劃線。
這時(shí)隨便一個(gè)懂得C語言并且頭腦清醒的人都會(huì)說,_exit和exit沒有任何區(qū)別,但我們還要講一下這兩者之間的區(qū)別,這種區(qū)別主要體現(xiàn)在它們?cè)诤瘮?shù)庫中的定義。_exit在Linux函數(shù)庫中的原型是:
#include<unistd.h> void _exit(int status);
|
和exit比較一下,exit()函數(shù)定義在 stdlib.h中,而_exit()定義在unistd.h中,從名字上看,stdlib.h似乎比unistd.h高級(jí)一點(diǎn),那么,它們之間到底有什 么區(qū)別呢?讓我們先來看流程圖,通過下圖,我們會(huì)對(duì)這兩個(gè)系統(tǒng)調(diào)用的執(zhí)行過程產(chǎn)生一個(gè)較為直觀的認(rèn)識(shí)。
從 圖中可以看出,_exit()函數(shù)的作用最為簡單:直接使進(jìn)程停止運(yùn)行,清除其使用的內(nèi)存空間,并銷毀其在內(nèi)核中的各種數(shù)據(jù)結(jié)構(gòu);exit()函數(shù)則在這 些基礎(chǔ)上作了一些包裝,在執(zhí)行退出之前加了若干道工序,也是因?yàn)檫@個(gè)原因,有些人認(rèn)為exit已經(jīng)不能算是純粹的系統(tǒng)調(diào)用。
exit()函數(shù)與_exit()函數(shù)最大的區(qū)別就在于exit()函數(shù)在調(diào)用exit系統(tǒng)調(diào)用之前要檢查文件的打開情況,把文件緩沖區(qū)中的內(nèi)容寫回文件,就是圖中的“清理I/O緩沖”一項(xiàng)。
在 Linux的標(biāo)準(zhǔn)函數(shù)庫中,有一套稱作“高級(jí)I/O”的函數(shù),我們熟知的printf()、fopen()、fread()、fwrite()都在此列, 它們也被稱作“緩沖I/O(buffered I/O)”,其特征是對(duì)應(yīng)每一個(gè)打開的文件,在內(nèi)存中都有一片緩沖區(qū),每次讀文件時(shí),會(huì)多讀出若干條記錄,這樣下次讀文件時(shí)就可以直接從內(nèi)存的緩沖區(qū)中讀 取,每次寫文件的時(shí)候,也僅僅是寫入內(nèi)存中的緩沖區(qū),等滿足了一定的條件(達(dá)到一定數(shù)量,或遇到特定字符,如換行符\n和文件結(jié)束符EOF),再將緩沖區(qū) 中的內(nèi)容一次性寫入文件,這樣就大大增加了文件讀寫的速度,但也為我們編程帶來了一點(diǎn)點(diǎn)麻煩。如果有一些數(shù)據(jù),我們認(rèn)為已經(jīng)寫入了文件,實(shí)際上因?yàn)闆]有滿 足特定的條件,它們還只是保存在緩沖區(qū)內(nèi),這時(shí)我們用_exit()函數(shù)直接將進(jìn)程關(guān)閉,緩沖區(qū)中的數(shù)據(jù)就會(huì)丟失,反之,如果想保證數(shù)據(jù)的完整性,就一定 要使用exit()函數(shù)。
請(qǐng)看以下例程:
/* exit2.c */ #include<stdlib.h> main() { printf("output begin\n"); printf("content in buffer"); exit(0); }
|
編譯并運(yùn)行:
$gcc exit2.c -o exit2 $./exit2 output begin content in buffer /* _exit1.c */ #include<unistd.h> main() { printf("output begin\n"); printf("content in buffer"); _exit(0); }
|
編譯并運(yùn)行:
$gcc _exit1.c -o _exit1 $./_exit1 output begin
|
在Linux中,標(biāo)準(zhǔn)輸入和標(biāo)準(zhǔn)輸出都是作為文件處理的,雖然是一類特殊的文件,但從程序員的角度來看,它們和硬盤上存儲(chǔ)數(shù)據(jù)的普通文件并沒有任何區(qū)別。與所有其他文件一樣,它們?cè)诖蜷_后也有自己的緩沖區(qū)。
請(qǐng)讀者結(jié)合前面的敘述,思考一下為什么這兩個(gè)程序會(huì)得出不同的結(jié)果。相信如果您理解了我前面所講的內(nèi)容,會(huì)很容易的得出結(jié)論。
1.7 背景
在 前面的文章中,我們已經(jīng)了解了父進(jìn)程和子進(jìn)程的概念,并已經(jīng)掌握了系統(tǒng)調(diào)用exit的用法,但可能很少有人意識(shí)到,在一個(gè)進(jìn)程調(diào)用了exit之后,該進(jìn)程 并非馬上就消失掉,而是留下一個(gè)稱為僵尸進(jìn)程(Zombie)的數(shù)據(jù)結(jié)構(gòu)。在Linux進(jìn)程的5種狀態(tài)中,僵尸進(jìn)程是非常特殊的一種,它已經(jīng)放棄了幾乎所 有內(nèi)存空間,沒有任何可執(zhí)行代碼,也不能被調(diào)度,僅僅在進(jìn)程列表中保留一個(gè)位置,記載該進(jìn)程的退出狀態(tài)等信息供其他進(jìn)程收集,除此之外,僵尸進(jìn)程不再占有 任何內(nèi)存空間。從這點(diǎn)來看,僵尸進(jìn)程雖然有一個(gè)很酷的名字,但它的影響力遠(yuǎn)遠(yuǎn)抵不上那些真正的僵尸兄弟,真正的僵尸總能令人感到恐怖,而僵尸進(jìn)程卻除了留 下一些供人憑吊的信息,對(duì)系統(tǒng)毫無作用。
也許讀者們還對(duì)這個(gè)新概念比較好奇,那就讓我們來看一眼Linux里的僵尸進(jìn)程究竟長什么樣子。
當(dāng)一個(gè)進(jìn)程已退出,但其父進(jìn)程還沒有調(diào)用系統(tǒng)調(diào)用wait(稍后介紹)對(duì)其進(jìn)行收集之前的這段時(shí)間里,它會(huì)一直保持僵尸狀態(tài),利用這個(gè)特點(diǎn),我們來寫一個(gè)簡單的小程序:
/* zombie.c */ #include <sys/types.h> #include <unistd.h> main() { pid_t pid; pid=fork(); if(pid<0) /* 如果出錯(cuò) */ printf("error occurred!\n"); else if(pid==0) /* 如果是子進(jìn)程 */ exit(0); else /* 如果是父進(jìn)程 */ sleep(60); /* 休眠60秒,這段時(shí)間里,父進(jìn)程什么也干不了 */ wait(NULL); /* 收集僵尸進(jìn)程 */ }
|
sleep的作用是讓進(jìn)程休眠指定的秒數(shù),在這60秒內(nèi),子進(jìn)程已經(jīng)退出,而父進(jìn)程正忙著睡覺,不可能對(duì)它進(jìn)行收集,這樣,我們就能保持子進(jìn)程60秒的僵尸狀態(tài)。
編譯這個(gè)程序:
后臺(tái)運(yùn)行程序,以使我們能夠執(zhí)行下一條命令
列一下系統(tǒng)內(nèi)的進(jìn)程
$ ps -ax ... ... 1177 pts/0 S 0:00 -bash 1577 pts/0 S 0:00 ./zombie 1578 pts/0 Z 0:00 [zombie <defunct>] 1579 pts/0 R 0:00 ps -ax
|
看到中間的"Z"了嗎?那就是僵尸進(jìn)程的標(biāo)志,它表示1578號(hào)進(jìn)程現(xiàn)在就是一個(gè)僵尸進(jìn)程。
我 們已經(jīng)學(xué)習(xí)了系統(tǒng)調(diào)用exit,它的作用是使進(jìn)程退出,但也僅僅限于將一個(gè)正常的進(jìn)程變成一個(gè)僵尸進(jìn)程,并不能將其完全銷毀。僵尸進(jìn)程雖然對(duì)其他進(jìn)程幾乎 沒有什么影響,不占用CPU時(shí)間,消耗的內(nèi)存也幾乎可以忽略不計(jì),但有它在那里呆著,還是讓人覺得心里很不舒服。而且Linux系統(tǒng)中進(jìn)程數(shù)目是有限制 的,在一些特殊的情況下,如果存在太多的僵尸進(jìn)程,也會(huì)影響到新進(jìn)程的產(chǎn)生。那么,我們?cè)撊绾蝸硐麥邕@些僵尸進(jìn)程呢?
先 來了解一下僵尸進(jìn)程的來由,我們知道,Linux和UNIX總有著剪不斷理還亂的親緣關(guān)系,僵尸進(jìn)程的概念也是從UNIX上繼承來的,而UNIX的先驅(qū)們 設(shè)計(jì)這個(gè)東西并非是因?yàn)殚e來無聊想煩煩其他的程序員。僵尸進(jìn)程中保存著很多對(duì)程序員和系統(tǒng)管理員非常重要的信息,首先,這個(gè)進(jìn)程是怎么死亡的?是正常退出 呢,還是出現(xiàn)了錯(cuò)誤,還是被其它進(jìn)程強(qiáng)迫退出的?其次,這個(gè)進(jìn)程占用的總系統(tǒng)CPU時(shí)間和總用戶CPU時(shí)間分別是多少?發(fā)生頁錯(cuò)誤的數(shù)目和收到信號(hào)的數(shù) 目。這些信息都被存儲(chǔ)在僵尸進(jìn)程中,試想如果沒有僵尸進(jìn)程,進(jìn)程一退出,所有與之相關(guān)的信息都立刻歸于無形,而此時(shí)程序員或系統(tǒng)管理員需要用到,就只好干 瞪眼了。
那么,我們?nèi)绾问占@些信息,并終結(jié)這些僵尸進(jìn)程呢?就要靠我們下面要講到的waitpid調(diào)用和wait調(diào)用。這兩者的作用都是收集僵尸進(jìn)程留下的信息,同時(shí)使這個(gè)進(jìn)程徹底消失。下面就對(duì)這兩個(gè)調(diào)用分別作詳細(xì)介紹。
wait的函數(shù)原型是:
#include <sys/types.h> /* 提供類型pid_t的定義 */ #include <sys/wait.h> pid_t wait(int *status)
|
進(jìn)程一旦調(diào)用了wait,就立即阻塞自己,由wait自動(dòng)分析是否當(dāng)前進(jìn)程的某個(gè)子進(jìn)程已經(jīng)退出,如果讓它找到了這樣一個(gè)已經(jīng)變成僵尸的子進(jìn) 程,wait就會(huì)收集這個(gè)子進(jìn)程的信息,并把它徹底銷毀后返回;如果沒有找到這樣一個(gè)子進(jìn)程,wait就會(huì)一直阻塞在這里,直到有一個(gè)出現(xiàn)為止。
參數(shù)status用來保存被收集進(jìn)程退出時(shí)的一些狀態(tài),它是一個(gè)指向int類型的指針。但如果我們對(duì)這個(gè)子進(jìn)程是如何死掉的毫不在意,只想把這個(gè)僵尸進(jìn)程消滅掉,(事實(shí)上絕大多數(shù)情況下,我們都會(huì)這樣想),我們就可以設(shè)定這個(gè)參數(shù)為NULL,就象下面這樣:
如果成功,wait會(huì)返回被收集的子進(jìn)程的進(jìn)程ID,如果調(diào)用進(jìn)程沒有子進(jìn)程,調(diào)用就會(huì)失敗,此時(shí)wait返回-1,同時(shí)errno被置為ECHILD。
1.8.2 實(shí)戰(zhàn)
下面就讓我們用一個(gè)例子來實(shí)戰(zhàn)應(yīng)用一下wait調(diào)用,程序中用到了系統(tǒng)調(diào)用fork,如果你對(duì)此不大熟悉或已經(jīng)忘記了,請(qǐng)參考上一篇文章《進(jìn)程管理相關(guān)的系統(tǒng)調(diào)用(一)》。
/* wait1.c */ #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> main() { pid_t pc,pr; pc=fork(); if(pc<0) /* 如果出錯(cuò) */ printf("error ocurred!\n"); else if(pc==0){ /* 如果是子進(jìn)程 */ printf("This is child process with pid of %d\n",getpid()); sleep(10); /* 睡眠10秒鐘 */ } else{ /* 如果是父進(jìn)程 */ pr=wait(NULL); /* 在這里等待 */ printf("I catched a child process with pid of %d\n"),pr); } exit(0); }
|
編譯并運(yùn)行:
$ cc wait1.c -o wait1 $ ./wait1 This is child process with pid of 1508 I catched a child process with pid of 1508
|
可以明顯注意到,在第2行結(jié)果打印出來前有10秒鐘的等待時(shí)間,這就是我們?cè)O(shè)定的讓子進(jìn)程睡眠的時(shí)間,只有子進(jìn)程從睡眠中蘇醒過來,它才能正常退 出,也就才能被父進(jìn)程捕捉到。其實(shí)這里我們不管設(shè)定子進(jìn)程睡眠的時(shí)間有多長,父進(jìn)程都會(huì)一直等待下去,讀者如果有興趣的話,可以試著自己修改一下這個(gè)數(shù) 值,看看會(huì)出現(xiàn)怎樣的結(jié)果。
1.8.3 參數(shù)status
如果參數(shù)status的值不是NULL,wait就會(huì)把子進(jìn)程退出時(shí)的狀態(tài)取出并存入其中,這是一個(gè)整數(shù)值(int),指出了子進(jìn)程是正常退出還是 被非正常結(jié)束的(一個(gè)進(jìn)程也可以被其他進(jìn)程用信號(hào)結(jié)束,我們將在以后的文章中介紹),以及正常結(jié)束時(shí)的返回值,或被哪一個(gè)信號(hào)結(jié)束的等信息。由于這些信息 被存放在一個(gè)整數(shù)的不同二進(jìn)制位中,所以用常規(guī)的方法讀取會(huì)非常麻煩,人們就設(shè)計(jì)了一套專門的宏(macro)來完成這項(xiàng)工作,下面我們來學(xué)習(xí)一下其中最 常用的兩個(gè):
1,WIFEXITED(status) 這個(gè)宏用來指出子進(jìn)程是否為正常退出的,如果是,它會(huì)返回一個(gè)非零值。
(請(qǐng)注意,雖然名字一樣,這里的參數(shù)status并不同于wait唯一的參數(shù)--指向整數(shù)的指針status,而是那個(gè)指針?biāo)赶虻恼麛?shù),切記不要搞混了。)
2,WEXITSTATUS(status) 當(dāng)WIFEXITED返回非零值時(shí),我們可以用這個(gè)宏來提取子進(jìn)程的返回值,如果子進(jìn)程調(diào)用exit(5)退出,WEXITSTATUS(status) 就會(huì)返回5;如果子進(jìn)程調(diào)用exit(7),WEXITSTATUS(status)就會(huì)返回7。請(qǐng)注意,如果進(jìn)程不是正常退出的,也就是 說,WIFEXITED返回0,這個(gè)值就毫無意義。
下面通過例子來實(shí)戰(zhàn)一下我們剛剛學(xué)到的內(nèi)容:
/* wait2.c */ #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> main() { int status; pid_t pc,pr; pc=fork(); if(pc<0) /* 如果出錯(cuò) */ printf("error ocurred!\n"); else if(pc==0){ /* 子進(jìn)程 */ printf("This is child process with pid of %d.\n",getpid()); exit(3); /* 子進(jìn)程返回3 */ } else{ /* 父進(jìn)程 */ pr=wait(&status); if(WIFEXITED(status)){ /* 如果WIFEXITED返回非零值 */ printf("the child process %d exit normally.\n",pr); printf("the return code is %d.\n",WEXITSTATUS(status)); }else /* 如果WIFEXITED返回零 */ printf("the child process %d exit abnormally.\n",pr); } }
|
編譯并運(yùn)行:
$ cc wait2.c -o wait2 $ ./wait2 This is child process with pid of 1538. the child process 1538 exit normally. the return code is 3.
|
父進(jìn)程準(zhǔn)確捕捉到了子進(jìn)程的返回值3,并把它打印了出來。
當(dāng)然,處理進(jìn)程退出狀態(tài)的宏并不止這兩個(gè),但它們當(dāng)中的絕大部分在平時(shí)的編程中很少用到,就也不在這里浪費(fèi)篇幅介紹了,有興趣的讀者可以自己參閱Linux man pages去了解它們的用法。
1.9 waitpid
1.9.1 簡介
waitpid系統(tǒng)調(diào)用在Linux函數(shù)庫中的原型是:
#include <sys/types.h> /* 提供類型pid_t的定義 */ #include <sys/wait.h> pid_t waitpid(pid_t pid,int *status,int options)
|
從本質(zhì)上講,系統(tǒng)調(diào)用waitpid和wait的作用是完全相同的,但waitpid多出了兩個(gè)可由用戶控制的參數(shù)pid和options,從而為我們編程提供了另一種更靈活的方式。下面我們就來詳細(xì)介紹一下這兩個(gè)參數(shù):
pid
從參數(shù)的名字pid和類型pid_t中就可以看出,這里需要的是一個(gè)進(jìn)程ID。但當(dāng)pid取不同的值時(shí),在這里有不同的意義。
- pid>0時(shí),只等待進(jìn)程ID等于pid的子進(jìn)程,不管其它已經(jīng)有多少子進(jìn)程運(yùn)行結(jié)束退出了,只要指定的子進(jìn)程還沒有結(jié)束,waitpid就會(huì)一直等下去。
- pid=-1時(shí),等待任何一個(gè)子進(jìn)程退出,沒有任何限制,此時(shí)waitpid和wait的作用一模一樣。
- pid=0時(shí),等待同一個(gè)進(jìn)程組中的任何子進(jìn)程,如果子進(jìn)程已經(jīng)加入了別的進(jìn)程組,waitpid不會(huì)對(duì)它做任何理睬。
- pid<-1時(shí),等待一個(gè)指定進(jìn)程組中的任何子進(jìn)程,這個(gè)進(jìn)程組的ID等于pid的絕對(duì)值。
options
options提供了一些額外的選項(xiàng)來控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED兩個(gè)選項(xiàng),這是兩個(gè)常數(shù),可以用"|"運(yùn)算符把它們連接起來使用,比如:
ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
|
如果我們不想使用它們,也可以把options設(shè)為0,如:
如果使用了WNOHANG參數(shù)調(diào)用waitpid,即使沒有子進(jìn)程退出,它也會(huì)立即返回,不會(huì)像wait那樣永遠(yuǎn)等下去。
而WUNTRACED參數(shù),由于涉及到一些跟蹤調(diào)試方面的知識(shí),加之極少用到,這里就不多費(fèi)筆墨了,有興趣的讀者可以自行查閱相關(guān)材料。
看到這里,聰明的讀者可能已經(jīng)看出端倪了--wait不就是經(jīng)過包裝的waitpid嗎?沒錯(cuò),察看<內(nèi)核源碼目錄>/include/unistd.h文件349-352行就會(huì)發(fā)現(xiàn)以下程序段:
static inline pid_t wait(int * wait_stat) { return waitpid(-1,wait_stat,0); }
|
1.9.2 返回值和錯(cuò)誤
waitpid的返回值比wait稍微復(fù)雜一些,一共有3種情況:
- 當(dāng)正常返回的時(shí)候,waitpid返回收集到的子進(jìn)程的進(jìn)程ID;
- 如果設(shè)置了選項(xiàng)WNOHANG,而調(diào)用中waitpid發(fā)現(xiàn)沒有已退出的子進(jìn)程可收集,則返回0;
- 如果調(diào)用中出錯(cuò),則返回-1,這時(shí)errno會(huì)被設(shè)置成相應(yīng)的值以指示錯(cuò)誤所在;
當(dāng)pid所指示的子進(jìn)程不存在,或此進(jìn)程存在,但不是調(diào)用進(jìn)程的子進(jìn)程,waitpid就會(huì)出錯(cuò)返回,這時(shí)errno被設(shè)置為ECHILD;
/* waitpid.c */ #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> main() { pid_t pc, pr; pc=fork(); if(pc<0) /* 如果fork出錯(cuò) */ printf("Error occured on forking.\n"); else if(pc==0){ /* 如果是子進(jìn)程 */ sleep(10); /* 睡眠10秒 */ exit(0); } /* 如果是父進(jìn)程 */ do{ pr=waitpid(pc, NULL, WNOHANG); /* 使用了WNOHANG參數(shù),waitpid不會(huì)在這里等待 */ if(pr==0){ /* 如果沒有收集到子進(jìn)程 */ printf("No child exited\n"); sleep(1); } }while(pr==0); /* 沒有收集到子進(jìn)程,就回去繼續(xù)嘗試 */ if(pr==pc) printf("successfully get child %d\n", pr); else printf("some error occured\n"); }
|
編譯并運(yùn)行:
$ cc waitpid.c -o waitpid $ ./waitpid No child exited No child exited No child exited No child exited No child exited No child exited No child exited No child exited No child exited No child exited successfully get child 1526
|
父進(jìn)程經(jīng)過10次失敗的嘗試之后,終于收集到了退出的子進(jìn)程。
因?yàn)檫@只是一個(gè)例子程序,不便寫得太復(fù)雜,所以我們就讓父進(jìn)程和子進(jìn)程分別睡眠了10秒鐘和1秒鐘,代表它們分別作了10秒鐘和1秒鐘的工作。父子進(jìn)程都有工作要做,父進(jìn)程利用工作的簡短間歇察看子進(jìn)程的是否退出,如退出就收集它。
http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part3/index.html