天天看點

程序程式設計—fork,getpid,exit,_exit

一、要搞清楚fork的執行過程,就必須先講清楚作業系統中的“程序(process)”概念。一個程序,主要包含三個元素:

o. 一個可以執行的程式;

o. 和該程序相關聯的全部資料(包括變量,記憶體空間,緩沖區等等);

o. 程式的執行上下文(execution context)。

    不妨簡單了解為,一個程序表示的,就是一個可執行程式的一次執行過程中的一個狀态。作業系統對程序的管理,典型的情況,是通過程序表完成的。程序表中的每一個表項,記錄的是目前作業系統中一個程序的情況。對于單 CPU的情況而言,每一特定時刻隻有一個程序占用 CPU,但是系統中可能同時存在多個活動的(等待執行或繼續執行的)程序。

    一個稱為“程式計數器(program counter, pc)”的寄存器,指出目前占用 CPU的程序要執行的下一條指令的位置。

    當分給某個程序的 CPU時間已經用完,作業系統将該程序相關的寄存器的值,儲存到該程序在程序表中對應的表項裡面;把将要接替這個程序占用 CPU的那個程序的上下文,從程序表中讀出,并更新相應的寄存器(這個過程稱為“上下文交換(process context switch)”,實際的上下文交換需要涉及到更多的資料,那和fork無關,不再多說,主要要記住程式寄存器pc指出程式目前已經執行到哪裡,是程序上下文的重要内容,換出 CPU的程序要儲存這個寄存器的值,換入CPU的程序,也要根據程序表中儲存的本程序執行上下文資訊,更新這個寄存器)。

    好了,有這些概念打底,可以說fork了。當你的程式執行到下面的語句:

    pid=fork();

    作業系統建立一個新的程序(子程序),并且在程序表中相應為它建立一個新的表項。新程序和原有程序的可執行程式是同一個程式;上下文和資料,絕大部分就是原程序(父程序)的拷貝,但它們是兩個互相獨立的程序!此時程式寄存器pc,在父、子程序的上下文中都聲稱,這個程序目前執行到fork調用即将傳回(此時子程序不占有CPU,子程序的pc不是真正儲存在寄存器中,而是作為程序上下文儲存在程序表中的對應表項内)。問題是怎麼傳回,在父子程序中就分道揚镳。

    父程序繼續執行,作業系統對fork的實作,使這個調用在父程序中傳回剛剛建立的子程序的pid(一個正整數),是以下面的if語句中pid<0, pid==0的兩個分支都不會執行。是以輸出i am the parent process...

    子程序在之後的某個時候得到排程,它的上下文被換入,占據 CPU,作業系統對fork的實作,使得子程序中fork調用傳回0。是以在這個程序(注意這不是父程序了哦,雖然是同一個程式,但是這是同一個程式的另外一次執行,在作業系統中這次執行是由另外一個程序表示的,從執行的角度說和父程序互相獨立)中pid=0。這個程序繼續執行的過程中,if語句中pid<0不滿足,但是pid==0是true。是以輸出i am the child process...

    我想你比較困惑的就是,為什麼看上去程式中互斥的兩個分支都被執行了。在一個程式的一次執行中,這當然是不可能的;但是你看到的兩行輸出是來自兩個程序,這兩個程序來自同一個程式的兩次執行。

二、本文介紹了Linux下的程序概念,并着重講解了與Linux程序管理相關的4個重要系統調用getpid,fork,exit和_exit,輔助一些例程說明了它們的特點和使用方法。

關于程序的一些必要知識

先看一下程序在大學課本裡的标準定義:“程序是可并發執行的程式在一個資料集合上的運作過程。”這個定義非常嚴謹,而且難懂,如果你沒有一下子了解這句話,就不妨看看筆者自己的并不嚴謹的解釋。我們大家都知道,硬碟上的一個可執行檔案經常被稱作程式,在Linux系統中,當一個程式開始執行後,在開始執行到執行完畢退出這段時間裡,它在記憶體中的部分就被稱作一個程序。

當然,這個解釋并不完善,但好處是容易了解,在以下的文章中,我們将會對程序作一些更全面的認識。

Linux程序簡介

Linux 是一個多任務的作業系統,也就是說,在同一個時間内,可以有多個程序同時執行。如果讀者對計算機硬體體系有一定了解的話,會知道我們大家常用的單CPU計算機實際上在一個時間片斷内隻能執行一條指令,那麼Linux是如何實作多程序同時執行的呢?原來Linux使用了一種稱為“程序排程(process scheduling)”的手段,首先,為每個程序指派一定的運作時間,這個時間通常很短,短到以毫秒為機關,然後依照某種規則,從衆多程序中挑選一個投入運作,其他的程序暫時等待,當正在運作的那個程序時間耗盡,或執行完畢退出,或因某種原因暫停,Linux就會重新進行排程,挑選下一個程序投入運作。因為每個程序占用的時間片都很短,在我們使用者的角度來看,就好像多個程序同時運作一樣了。

在Linux中,每個程序在建立時都會被配置設定一個資料結構,稱為程序控制塊(Process Control Block,簡稱PCB)。PCB中包含了很多重要的資訊,供系統排程和程序本身執行使用,其中最重要的莫過于程序ID(process ID)了,程序ID也被稱作程序辨別符,是一個非負的整數,在Linux作業系統中唯一地标志一個程序,在我們最常使用的I386架構(即PC使用的架構)上,一個非負的整數的變化範圍是0-32767,這也是我們所有可能取到的程序ID。其實從程序ID的名字就可以看出,它就是程序的身份證号碼,每個人的身份證号碼都不會相同,每個程序的程序ID也不會相同。

一個或多個程序可以合起來構成一個程序組(process group),一個或多個程序組可以合起來構成一個會話(session)。這樣我們就有了對程序進行批量操作的能力,比如通過向某個程序組發送信号來實作向該組中的每個程序發送信号。

三、getpid

在2.4.4版核心中,getpid是第20号系統調用,其在Linux函數庫中的原型是:

         #include<sys/types.h>

         #include<unistd.h>

pid_t getpid(void);

getpid的作用很簡單,就是傳回目前程序的程序ID,請大家看以下的例子:

         #include<unistd.h>

             pid_t fork(void);

隻看fork的名字,可能難得有幾個人可以猜到它是做什麼用的。fork系統調用的作用是複制一個程序。當一個程序調用它,完成後就出現兩個幾乎一模一樣的程序,我們也由此得到了一個新程序。據說fork的名字就是來源于這個與叉子的形狀頗有幾分相似的工作流程。

在Linux 中,創造新程序的方法隻有一個,就是我們正在介紹的fork。其他一些庫函數,如system(),看起來似乎它們也能建立新的程序,如果能看一下它們的源碼就會明白,它們實際上也在内部調用了fork。包括我們在指令行下運作應用程式,新的程序也是由shell調用fork制造出來的。fork有一些很有意思的特征,下面就讓我們通過一個小程式來對它有更多的了解。

#include<sys/types.h>

#inlcude<unistd.h>

main()

{

         pid_t pid;          

         pid="fork"();        

         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());

}

編譯并運作:

$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

看這個程式的時候,頭腦中必須首先了解一個概念:在語句pid=fork()之前,隻有一個程序在執行這段代碼,但在這條語句之後,就變成兩個程序在執行了,這兩個程序的代碼部分完全相同,将要執行的下一條語句都是if(pid==0)……。

兩個程序中,原先就存在的那個被稱作“父程序”,新出現的那個被稱作“子程序”。父子程序的差別除了程序标志符(process ID)不同外,變量pid的值也不相同,pid存放的是fork的傳回值。fork調用的一個奇妙之處就是它僅僅被調用一次,卻能夠傳回兩次,它可能有三種不同的傳回值:

在父程序中,fork傳回新建立子程序的程序ID;

在子程序中,fork傳回0;

如果出現錯誤,fork傳回一個負值;

fork出錯可能有兩種原因:

(1)目前的程序數已經達到了系統規定的上限,這時errno的值被設定為EAGAIN。

(2)系統記憶體不足,這時errno的值被設定為ENOMEM。(關于errno的意義,請參考本系列的第一篇文章。)

fork系統調用出錯的可能性很小,而且如果出錯,一般都為第一種錯誤。如果出現第二種錯誤,說明系統已經沒有可配置設定的記憶體,正處于崩潰的邊緣,這種情況對Linux來說是很罕見的。

說到這裡,聰明的讀者可能已經完全看懂剩下的代碼了,如果pid小于0,說明出現了錯誤;pid==0,就說明fork傳回了0,也就說明目前程序是子程序,就去執行printf("I am the child!"),否則(else),目前程序就是父程序,執行printf("I am the parent!")。完美主義者會覺得這很備援,因為兩個程序裡都各有一條它們永遠執行不到的語句。不必過于為此耿耿于懷,畢竟很多年以前,UNIX的鼻祖們在當時記憶體小得無法想象的計算機上就是這樣寫程式的,以我們如今的“海量”記憶體,完全可以把這幾個位元組的顧慮抛到九霄雲外。

說到這裡,可能有些讀者還有疑問:如果fork後子程序和父程序幾乎完全一樣,而系統中産生新程序唯一的方法就是fork,那豈不是系統中所有的程序都要一模一樣嗎?那我們要執行新的應用程式時候怎麼辦呢?從對Linux系統的經驗中,我們知道這種問題并不存在。至于采用了什麼方法,我們把這個問題留到後面具體讨論。

五、exit

在2.4.4版核心中,exit是第1号調用,其在Linux函數庫中的原型是:

         #include<stdlib.h>

               void exit(int status);

不像fork那麼難了解,從exit的名字就能看出,這個系統調用是用來終止一個程序的。無論在程式中的什麼位置,隻要執行到exit系統調用,程序就會停止剩下的所有操作,清除包括PCB在内的各種資料結構,并終止本程序的運作。請看下面的程式:

#include<stdlib.h>

main()

{

         printf("this process will exit!/n");

         exit(0);

         printf("never be displayed!/n");

}

編譯後運作:

$gcc exit_test1.c -o exit_test1

$./exit_test1

this process will exit!

我們可以看到,程式并沒有列印後面的"never be displayed!/n",因為在此之前,在執行到exit(0)時,程序就已經終止了。

exit 系統調用帶有一個整數類型的參數status,我們可以利用這個參數傳遞程序結束時的狀态,比如說,該程序是正常結束的,還是出現某種意外而結束的,一般來說,0表示沒有意外的正常結束;其他的數值表示出現了錯誤,程序非正常結束。我們在實際程式設計時,可以用wait系統調用接收子程序的傳回值,進而針對不同的情況進行不同的處理。關于wait的詳細情況,我們将在以後的篇幅中進行介紹。

六、exit和_exit

作為系統調用而言,_exit和exit是一對孿生兄弟,它們究竟相似到什麼程度,我們可以從Linux的源碼中找到答案:

#define __NR__exit __NR_exit

“__NR_”是在Linux的源碼中為每個系統調用加上的字首,請注意第一個exit前有2條下劃線,第二個exit前隻有1條下劃線。

這時随便一個懂得C語言并且頭腦清醒的人都會說,_exit和exit沒有任何差別,但我們還要講一下這兩者之間的差別,這種差別主要展現在它們在函數庫中的定義。_exit在Linux函數庫中的原型是:

         #include<unistd.h>

          void _exit(int status);

和exit比較一下,exit()函數定義在 stdlib.h中,而_exit()定義在unistd.h中,從名字上看,stdlib.h似乎比unistd.h進階一點,那麼,它們之間到底有什麼差別呢?讓我們先來看流程圖,通過下圖,我們會對這兩個系統調用的執行過程産生一個較為直覺的認識。

_exit()函數的作用最為簡單:直接使程序停止運作,清除其使用的記憶體空間,并銷毀其在核心中的各種資料結構;exit()函數則在這些基礎上作了一些包裝,在執行退出之前加了若幹道工序,也是因為這個原因,有些人認為exit已經不能算是純粹的系統調用。

exit()函數與_exit()函數最大的差別就在于exit()函數在調用exit系統調用之前要檢查檔案的打開情況,把檔案緩沖區中的内容寫回檔案,就是圖中的“清理I/O緩沖”一項。

在Linux 的标準函數庫中,有一套稱作“進階I/O”的函數,我們熟知的printf()、fopen()、fread()、fwrite()都在此列,它們也被稱作“緩沖I/O(buffered I/O)”,其特征是對應每一個打開的檔案,在記憶體中都有一片緩沖區,每次讀檔案時,會多讀出若幹條記錄,這樣下次讀檔案時就可以直接從記憶體的緩沖區中讀取,每次寫檔案的時候,也僅僅是寫入記憶體中的緩沖區,等滿足了一定的條件(達到一定數量,或遇到特定字元,如換行符/n和檔案結束符EOF),再将緩沖區中的内容一次性寫入檔案,這樣就大大增加了檔案讀寫的速度,但也為我們程式設計帶來了一點點麻煩。如果有一些資料,我們認為已經寫入了檔案,實際上因為沒有滿足特定的條件,它們還隻是儲存在緩沖區内,這時我們用_exit()函數直接将程序關閉,緩沖區中的資料就會丢失,反之,如果想保證資料的完整性,就一定要使用exit()函數。

請看以下例程:

#include<stdlib.h>

main()

{

         printf("output begin/n"); 

printf("content in buffer");

         exit(0);

}

編譯并運作:

$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);

}

編譯并運作:

$gcc _exit1.c -o _exit1

$./_exit1

output begin

在Linux中,标準輸入和标準輸出都是作為檔案處理的,雖然是一類特殊的檔案,但從程式員的角度來看,它們和硬碟上存儲資料的普通檔案并沒有任何差別。與所有其他檔案一樣,它們在打開後也有自己的緩沖區。

請讀者結合前面的叙述,思考一下為什麼這兩個程式會得出不同的結果。相信如果您了解了我前面所講的内容,會很容易的得出結論。

繼續閱讀