本文介紹了Linux下的進(jìn)程的一些概念,并著重講解了與Linux進(jìn)程管理相關(guān)的重要系統(tǒng)調(diào)用wait,waitpid和exec函數(shù)族,輔助一些例程說明了它們的特點和使用方法。 在前面的文章中,我們已經(jīng)了解了父進(jìn)程和子進(jìn)程的概念,并已經(jīng)掌握了系統(tǒng)調(diào)用exit的用法,但可能很少有人意識到,在一個進(jìn)程調(diào)用了exit之后,該進(jìn)程并非馬上就消失掉,而是留下一個稱為僵尸進(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)程列表中保留一個位置,記載該進(jìn)程的退出狀態(tài)等信息供其他進(jìn)程收集,除此之外,僵尸進(jìn)程不再占有任何內(nèi)存空間。從這點來看,僵尸進(jìn)程雖然有一個很酷的名字,但它的影響力遠(yuǎn)遠(yuǎn)抵不上那些真正的僵尸兄弟,真正的僵尸總能令人感到恐怖,而僵尸進(jìn)程卻除了留下一些供人憑吊的信息,對系統(tǒng)毫無作用。 也許讀者們還對這個新概念比較好奇,那就讓我們來看一眼Linux里的僵尸進(jìn)程究竟長什么樣子。 當(dāng)一個進(jìn)程已退出,但其父進(jìn)程還沒有調(diào)用系統(tǒng)調(diào)用wait(稍后介紹)對其進(jìn)行收集之前的這段時間里,它會一直保持僵尸狀態(tài),利用這個特點,我們來寫一個簡單的小程序:
sleep的作用是讓進(jìn)程休眠指定的秒數(shù),在這60秒內(nèi),子進(jìn)程已經(jīng)退出,而父進(jìn)程正忙著睡覺,不可能對它進(jìn)行收集,這樣,我們就能保持子進(jìn)程60秒的僵尸狀態(tài)。 編譯這個程序:
后臺運行程序,以使我們能夠執(zhí)行下一條命令
列一下系統(tǒng)內(nèi)的進(jìn)程
看到中間的"Z"了嗎?那就是僵尸進(jìn)程的標(biāo)志,它表示1578號進(jìn)程現(xiàn)在就是一個僵尸進(jìn)程。 我們已經(jīng)學(xué)習(xí)了系統(tǒng)調(diào)用exit,它的作用是使進(jìn)程退出,但也僅僅限于將一個正常的進(jìn)程變成一個僵尸進(jìn)程,并不能將其完全銷毀。僵尸進(jìn)程雖然對其他進(jìn)程幾乎沒有什么影響,不占用CPU時間,消耗的內(nèi)存也幾乎可以忽略不計,但有它在那里呆著,還是讓人覺得心里很不舒服。而且Linux系統(tǒng)中進(jìn)程數(shù)目是有限制的,在一些特殊的情況下,如果存在太多的僵尸進(jìn)程,也會影響到新進(jìn)程的產(chǎn)生。那么,我們該如何來消滅這些僵尸進(jìn)程呢? 先來了解一下僵尸進(jìn)程的來由,我們知道,Linux和UNIX總有著剪不斷理還亂的親緣關(guān)系,僵尸進(jìn)程的概念也是從UNIX上繼承來的,而UNIX的先驅(qū)們設(shè)計這個東西并非是因為閑來無聊想煩煩其他的程序員。僵尸進(jìn)程中保存著很多對程序員和系統(tǒng)管理員非常重要的信息,首先,這個進(jìn)程是怎么死亡的?是正常退出呢,還是出現(xiàn)了錯誤,還是被其它進(jìn)程強(qiáng)迫退出的?其次,這個進(jìn)程占用的總系統(tǒng)CPU時間和總用戶CPU時間分別是多少?發(fā)生頁錯誤的數(shù)目和收到信號的數(shù)目。這些信息都被存儲在僵尸進(jìn)程中,試想如果沒有僵尸進(jìn)程,進(jìn)程一退出,所有與之相關(guān)的信息都立刻歸于無形,而此時程序員或系統(tǒng)管理員需要用到,就只好干瞪眼了。 那么,我們?nèi)绾问占@些信息,并終結(jié)這些僵尸進(jìn)程呢?就要靠我們下面要講到的waitpid調(diào)用和wait調(diào)用。這兩者的作用都是收集僵尸進(jìn)程留下的信息,同時使這個進(jìn)程徹底消失。下面就對這兩個調(diào)用分別作詳細(xì)介紹。
wait的函數(shù)原型是:
進(jìn)程一旦調(diào)用了wait,就立即阻塞自己,由wait自動分析是否當(dāng)前進(jìn)程的某個子進(jìn)程已經(jīng)退出,如果讓它找到了這樣一個已經(jīng)變成僵尸的子進(jìn)程,wait就會收集這個子進(jìn)程的信息,并把它徹底銷毀后返回;如果沒有找到這樣一個子進(jìn)程,wait就會一直阻塞在這里,直到有一個出現(xiàn)為止。 參數(shù)status用來保存被收集進(jìn)程退出時的一些狀態(tài),它是一個指向int類型的指針。但如果我們對這個子進(jìn)程是如何死掉的毫不在意,只想把這個僵尸進(jìn)程消滅掉,(事實上絕大多數(shù)情況下,我們都會這樣想),我們就可以設(shè)定這個參數(shù)為NULL,就象下面這樣:
如果成功,wait會返回被收集的子進(jìn)程的進(jìn)程ID,如果調(diào)用進(jìn)程沒有子進(jìn)程,調(diào)用就會失敗,此時wait返回-1,同時errno被置為ECHILD。 下面就讓我們用一個例子來實戰(zhàn)應(yīng)用一下wait調(diào)用,程序中用到了系統(tǒng)調(diào)用fork,如果你對此不大熟悉或已經(jīng)忘記了,請參考上一篇文章《進(jìn)程管理相關(guān)的系統(tǒng)調(diào)用(一)》。
編譯并運行:
可以明顯注意到,在第2行結(jié)果打印出來前有10秒鐘的等待時間,這就是我們設(shè)定的讓子進(jìn)程睡眠的時間,只有子進(jìn)程從睡眠中蘇醒過來,它才能正常退出,也就才能被父進(jìn)程捕捉到。其實這里我們不管設(shè)定子進(jìn)程睡眠的時間有多長,父進(jìn)程都會一直等待下去,讀者如果有興趣的話,可以試著自己修改一下這個數(shù)值,看看會出現(xiàn)怎樣的結(jié)果。 如果參數(shù)status的值不是NULL,wait就會把子進(jìn)程退出時的狀態(tài)取出并存入其中,這是一個整數(shù)值(int),指出了子進(jìn)程是正常退出還是被非正常結(jié)束的(一個進(jìn)程也可以被其他進(jìn)程用信號結(jié)束,我們將在以后的文章中介紹),以及正常結(jié)束時的返回值,或被哪一個信號結(jié)束的等信息。由于這些信息被存放在一個整數(shù)的不同二進(jìn)制位中,所以用常規(guī)的方法讀取會非常麻煩,人們就設(shè)計了一套專門的宏(macro)來完成這項工作,下面我們來學(xué)習(xí)一下其中最常用的兩個: 1,WIFEXITED(status) 這個宏用來指出子進(jìn)程是否為正常退出的,如果是,它會返回一個非零值。 (請注意,雖然名字一樣,這里的參數(shù)status并不同于wait唯一的參數(shù)--指向整數(shù)的指針status,而是那個指針?biāo)赶虻恼麛?shù),切記不要搞混了。) 2,WEXITSTATUS(status) 當(dāng)WIFEXITED返回非零值時,我們可以用這個宏來提取子進(jìn)程的返回值,如果子進(jìn)程調(diào)用exit(5)退出,WEXITSTATUS(status)就會返回5;如果子進(jìn)程調(diào)用exit(7),WEXITSTATUS(status)就會返回7。請注意,如果進(jìn)程不是正常退出的,也就是說,WIFEXITED返回0,這個值就毫無意義。 下面通過例子來實戰(zhàn)一下我們剛剛學(xué)到的內(nèi)容:
編譯并運行:
父進(jìn)程準(zhǔn)確捕捉到了子進(jìn)程的返回值3,并把它打印了出來。 當(dāng)然,處理進(jìn)程退出狀態(tài)的宏并不止這兩個,但它們當(dāng)中的絕大部分在平時的編程中很少用到,就也不在這里浪費篇幅介紹了,有興趣的讀者可以自己參閱Linux man pages去了解它們的用法。 有時候,父進(jìn)程要求子進(jìn)程的運算結(jié)果進(jìn)行下一步的運算,或者子進(jìn)程的功能是為父進(jìn)程提供了下一步執(zhí)行的先決條件(如:子進(jìn)程建立文件,而父進(jìn)程寫入數(shù)據(jù)),此時父進(jìn)程就必須在某一個位置停下來,等待子進(jìn)程運行結(jié)束,而如果父進(jìn)程不等待而直接執(zhí)行下去的話,可以想見,會出現(xiàn)極大的混亂。這種情況稱為進(jìn)程之間的同步,更準(zhǔn)確地說,這是進(jìn)程同步的一種特例。進(jìn)程同步就是要協(xié)調(diào)好2個以上的進(jìn)程,使之以安排好地次序依次執(zhí)行。解決進(jìn)程同步問題有更通用的方法,我們將在以后介紹,但對于我們假設(shè)的這種情況,則完全可以用wait系統(tǒng)調(diào)用簡單的予以解決。請看下面這段程序:
這段程序只是個例子,不能真正拿來執(zhí)行,但它卻說明了一些問題,首先,當(dāng)fork調(diào)用成功后,父子進(jìn)程各做各的事情,但當(dāng)父進(jìn)程的工作告一段落,需要用到子進(jìn)程的結(jié)果時,它就停下來調(diào)用wait,一直等到子進(jìn)程運行結(jié)束,然后利用子進(jìn)程的結(jié)果繼續(xù)執(zhí)行,這樣就圓滿地解決了我們提出的進(jìn)程同步問題。
waitpid系統(tǒng)調(diào)用在Linux函數(shù)庫中的原型是:
從本質(zhì)上講,系統(tǒng)調(diào)用waitpid和wait的作用是完全相同的,但waitpid多出了兩個可由用戶控制的參數(shù)pid和options,從而為我們編程提供了另一種更靈活的方式。下面我們就來詳細(xì)介紹一下這兩個參數(shù): 從參數(shù)的名字pid和類型pid_t中就可以看出,這里需要的是一個進(jìn)程ID。但當(dāng)pid取不同的值時,在這里有不同的意義。
options提供了一些額外的選項來控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED兩個選項,這是兩個常數(shù),可以用"|"運算符把它們連接起來使用,比如:
如果我們不想使用它們,也可以把options設(shè)為0,如:
如果使用了WNOHANG參數(shù)調(diào)用waitpid,即使沒有子進(jìn)程退出,它也會立即返回,不會像wait那樣永遠(yuǎn)等下去。 而WUNTRACED參數(shù),由于涉及到一些跟蹤調(diào)試方面的知識,加之極少用到,這里就不多費筆墨了,有興趣的讀者可以自行查閱相關(guān)材料。 看到這里,聰明的讀者可能已經(jīng)看出端倪了--wait不就是經(jīng)過包裝的waitpid嗎?沒錯,察看<內(nèi)核源碼目錄>/include/unistd.h文件349-352行就會發(fā)現(xiàn)以下程序段:
waitpid的返回值比wait稍微復(fù)雜一些,一共有3種情況:
當(dāng)pid所指示的子進(jìn)程不存在,或此進(jìn)程存在,但不是調(diào)用進(jìn)程的子進(jìn)程,waitpid就會出錯返回,這時errno被設(shè)置為ECHILD;
編譯并運行:
父進(jìn)程經(jīng)過10次失敗的嘗試之后,終于收集到了退出的子進(jìn)程。 因為這只是一個例子程序,不便寫得太復(fù)雜,所以我們就讓父進(jìn)程和子進(jìn)程分別睡眠了10秒鐘和1秒鐘,代表它們分別作了10秒鐘和1秒鐘的工作。父子進(jìn)程都有工作要做,父進(jìn)程利用工作的簡短間歇察看子進(jìn)程的是否退出,如退出就收集它。
也許有不少讀者從本系列文章一推出就開始讀,一直到這里還有一個很大的疑惑:既然所有新進(jìn)程都是由fork產(chǎn)生的,而且由fork產(chǎn)生的子進(jìn)程和父進(jìn)程幾乎完全一樣,那豈不是意味著系統(tǒng)中所有的進(jìn)程都應(yīng)該一模一樣了嗎?而且,就我們的常識來說,當(dāng)我們執(zhí)行一個程序的時候,新產(chǎn)生的進(jìn)程的內(nèi)容應(yīng)就是程序的內(nèi)容才對。是我們理解錯了嗎?顯然不是,要解決這些疑惑,就必須提到我們下面要介紹的exec系統(tǒng)調(diào)用。 說是exec系統(tǒng)調(diào)用,實際上在Linux中,并不存在一個exec()的函數(shù)形式,exec指的是一組函數(shù),一共有6個,分別是:
其中只有execve是真正意義上的系統(tǒng)調(diào)用,其它都是在此基礎(chǔ)上經(jīng)過包裝的庫函數(shù)。 exec函數(shù)族的作用是根據(jù)指定的文件名找到可執(zhí)行文件,并用它來取代調(diào)用進(jìn)程的內(nèi)容,換句話說,就是在調(diào)用進(jìn)程內(nèi)部執(zhí)行一個可執(zhí)行文件。這里的可執(zhí)行文件既可以是二進(jìn)制文件,也可以是任何Linux下可執(zhí)行的腳本文件。 與一般情況不同,exec函數(shù)族的函數(shù)執(zhí)行成功后不會返回,因為調(diào)用進(jìn)程的實體,包括代碼段,數(shù)據(jù)段和堆棧等都已經(jīng)被新的內(nèi)容取代,只留下進(jìn)程ID等一些表面上的信息仍保持原樣,頗有些神似"三十六計"中的"金蟬脫殼"??瓷先ミ€是舊的軀殼,卻已經(jīng)注入了新的靈魂。只有調(diào)用失敗了,它們才會返回一個-1,從原程序的調(diào)用點接著往下執(zhí)行。 現(xiàn)在我們應(yīng)該明白了,Linux下是如何執(zhí)行新程序的,每當(dāng)有進(jìn)程認(rèn)為自己不能為系統(tǒng)和擁護(hù)做出任何貢獻(xiàn)了,他就可以發(fā)揮最后一點余熱,調(diào)用任何一個exec,讓自己以新的面貌重生;或者,更普遍的情況是,如果一個進(jìn)程想執(zhí)行另一個程序,它就可以fork出一個新進(jìn)程,然后調(diào)用任何一個exec,這樣看起來就好像通過執(zhí)行應(yīng)用程序而產(chǎn)生了一個新進(jìn)程一樣。 事實上第二種情況被應(yīng)用得如此普遍,以至于Linux專門為其作了優(yōu)化,我們已經(jīng)知道,fork會將調(diào)用進(jìn)程的所有內(nèi)容原封不動的拷貝到新產(chǎn)生的子進(jìn)程中去,這些拷貝的動作很消耗時間,而如果fork完之后我們馬上就調(diào)用exec,這些辛辛苦苦拷貝來的東西又會被立刻抹掉,這看起來非常不劃算,于是人們設(shè)計了一種"寫時拷貝(copy-on-write)"技術(shù),使得fork結(jié)束后并不立刻復(fù)制父進(jìn)程的內(nèi)容,而是到了真正實用的時候才復(fù)制,這樣如果下一條語句是exec,它就不會白白作無用功了,也就提高了效率。 上面6條函數(shù)看起來似乎很復(fù)雜,但實際上無論是作用還是用法都非常相似,只有很微小的差別。在學(xué)習(xí)它們之前,先來了解一下我們習(xí)以為常的main函數(shù)。 下面這個main函數(shù)的形式可能有些出乎我們的意料:
它可能與絕大多數(shù)教科書上描述的都不一樣,但實際上,這才是main函數(shù)真正完整的形式。 參數(shù)argc指出了運行該程序時命令行參數(shù)的個數(shù),數(shù)組argv存放了所有的命令行參數(shù),數(shù)組envp存放了所有的環(huán)境變量。環(huán)境變量指的是一組值,從用戶登錄后就一直存在,很多應(yīng)用程序需要依靠它來確定系統(tǒng)的一些細(xì)節(jié),我們最常見的環(huán)境變量是PATH,它指出了應(yīng)到哪里去搜索應(yīng)用程序,如/bin;HOME也是比較常見的環(huán)境變量,它指出了我們在系統(tǒng)中的個人目錄。環(huán)境變量一般以字符串"XXX=xxx"的形式存在,XXX表示變量名,xxx表示變量的值。 值得一提的是,argv數(shù)組和envp數(shù)組存放的都是指向字符串的指針,這兩個數(shù)組都以一個NULL元素表示數(shù)組的結(jié)尾。 我們可以通過以下這個程序來觀看傳到argc、argv和envp里的都是什么東西:
編譯它:
運行時,我們故意加幾個沒有任何作用的命令行參數(shù):
我們看到,程序?qū)?./main"作為第1個命令行參數(shù),所以我們一共有3個命令行參數(shù)。這可能與大家平時習(xí)慣的說法有些不同,小心不要搞錯了。 現(xiàn)在回過頭來看一下exec函數(shù)族,先把注意力集中在execve上:
對比一下main函數(shù)的完整形式,看出問題了嗎?是的,這兩個函數(shù)里的argv和envp是完全一一對應(yīng)的關(guān)系。execve第1個參數(shù)path是被執(zhí)行應(yīng)用程序的完整路徑,第2個參數(shù)argv就是傳給被執(zhí)行應(yīng)用程序的命令行參數(shù),第3個參數(shù)envp是傳給被執(zhí)行應(yīng)用程序的環(huán)境變量。 留心看一下這6個函數(shù)還可以發(fā)現(xiàn),前3個函數(shù)都是以execl開頭的,后3個都是以execv開頭的,它們的區(qū)別在于,execv開頭的函數(shù)是以"char *argv[]"這樣的形式傳遞命令行參數(shù),而execl開頭的函數(shù)采用了我們更容易習(xí)慣的方式,把參數(shù)一個一個列出來,然后以一個NULL表示結(jié)束。這里的NULL的作用和argv數(shù)組里的NULL作用是一樣的。 在全部6個函數(shù)中,只有execle和execve使用了char *envp[]傳遞環(huán)境變量,其它的4個函數(shù)都沒有這個參數(shù),這并不意味著它們不傳遞環(huán)境變量,這4個函數(shù)將把默認(rèn)的環(huán)境變量不做任何修改地傳給被執(zhí)行的應(yīng)用程序。而execle和execve會用指定的環(huán)境變量去替代默認(rèn)的那些。 還有2個以p結(jié)尾的函數(shù)execlp和execvp,咋看起來,它們和execl與execv的差別很小,事實也確是如此,除execlp和execvp之外的4個函數(shù)都要求,它們的第1個參數(shù)path必須是一個完整的路徑,如"/bin/ls";而execlp和execvp的第1個參數(shù)file可以簡單到僅僅是一個文件名,如"ls",這兩個函數(shù)可以自動到環(huán)境變量PATH制定的目錄里去尋找。 知識介紹得差不多了,接下來我們看看實際的應(yīng)用:
程序里調(diào)用了2個Linux常用的系統(tǒng)命令,echo和env。echo會把后面跟的命令行參數(shù)原封不動的打印出來,env用來列出所有環(huán)境變量。 由于各個子進(jìn)程執(zhí)行的順序無法控制,所以有可能出現(xiàn)一個比較混亂的輸出--各子進(jìn)程打印的結(jié)果交雜在一起,而不是嚴(yán)格按照程序中列出的次序。 編譯并運行:
果然不出所料,execle輸出的結(jié)果跑到了execlp前面。 大家在平時的編程中,如果用到了exec函數(shù)族,一定記得要加錯誤判斷語句。因為與其他系統(tǒng)調(diào)用比起來,exec很容易受傷,被執(zhí)行文件的位置,權(quán)限等很多因素都能導(dǎo)致該調(diào)用的失敗。最常見的錯誤是:
下面就讓我用一些形象的比喻,來對進(jìn)程短暫的一生作一個小小的總結(jié): 隨著一句fork,一個新進(jìn)程呱呱落地,但它這時只是老進(jìn)程的一個克隆。 然后隨著exec,新進(jìn)程脫胎換骨,離家獨立,開始了為人民服務(wù)的職業(yè)生涯。 人有生老病死,進(jìn)程也一樣,它可以是自然死亡,即運行到main函數(shù)的最后一個"}",從容地離我們而去;也可以是自殺,自殺有2種方式,一種是調(diào)用exit函數(shù),一種是在main函數(shù)內(nèi)使用return,無論哪一種方式,它都可以留下遺書,放在返回值里保留下來;它還甚至能可被謀殺,被其它進(jìn)程通過另外一些方式結(jié)束他的生命。 進(jìn)程死掉以后,會留下一具僵尸,wait和waitpid充當(dāng)了殮尸工,把僵尸推去火化,使其最終歸于無形。 這就是進(jìn)程完整的一生。
本文重點介紹了系統(tǒng)調(diào)用wait、waitpid和exec函數(shù)族,對與進(jìn)程管理相關(guān)的系統(tǒng)調(diào)用的介紹就在這里告一段落,在下一篇文章,也是與進(jìn)程管理相關(guān)的系統(tǒng)調(diào)用的最后一篇文章中,我們會通過兩個很酷的實際例子,來重溫一下最近學(xué)過的知識。
|
|