關於fork()返回值

fork簡介:      
   fork英文原意是“分岔,分支”的意思,而在操作系統中,乃是著名的Unix(或類Unix,如Linux,Minix)中用於創建子進程的系統調用。

【NOTE1】
fork () 的作用是什麼?換句話說,你用 fork () 的目的是什麼?
――是爲了產生一個新的進程,地球人都知道 :)
產生一個什麼樣的進程?
――和你本來調用 fork () 的那個進程基本一樣的進程,其實就是你原來進程的副本;
真的完全一樣嗎?
――當然不能完全一樣,你要兩個除了 pid 之外其它一模一樣的進程幹什麼,就算memory
再多也不用這麼擺譜吧?
哪裏不一樣?
――當然最重要的是 fork () 之後執行的代碼不一樣,you know, i know :)
怎麼實現呢?
――如果是 Windows,它會讓你在 fork () 裏面提供一大堆東西,指明這個那個什麼的……
我用的是 unix 啊
――所以很簡單,unix 會讓兩個進程(不錯,原來是一個,unix 替你複製了一個,現在有兩個)
在 fork () 之後產生不同:返回值不同。其中一個進程(使用新的 pid)裏面的 fork () 返回零,
這個進程就是“子進程”;而另一個進程(使用原來的 pid)中的 fork () 返回前面那個子進程的
pid,他自己被稱爲“父進程”

然後呢?
――寫代碼的人又不笨,當然就根據返回值是否非零來判斷了,現在我是在子進程裏面呢,還是在
父進程裏面?在子進程裏面就執行子進程該執行的代碼,在父進程裏面就執行父進程的代碼……

    有鐵桿 windows fans 藉此說明,windows 好啊,子進程用子進程的代碼,父進程用父進程的,
你 unix 笨了吧,子進程包含父進程、子進程的代碼,父進程包含父進程子進程的代碼,豈不是多佔用內存了嗎?
――據我所知,unix 代碼段都是可重入代碼,也就是說,進程複製,並不複製代碼段,若干個進程
共享同一代碼段,增加的只是全局共享數據和對文件描述符的引用等,另外就是堆棧。你一個代碼
長達 10M 的進程,fork () 出三四個子進程,只是增加一點內存佔用(如果你沒有使用很多全局變量
的話),而不是佔用 40M 以上的內存。

【NOTE2】
程序   從   fork   開始分支   (稱分支不準確),   一路是主進程   pid   >   0   (pid   是子進程ID)   一路是子進程   pid   ==   0   自此分成兩個任務
其實fork的時候已經兩個分支了,數據段被複制了一份,因此pid有兩份      
執行pid=fork()時,返回值賦給pid在兩個進程中運行,      
fork會返回給父進程的那個>0的值,告訴調用者新建進程的pid   
子進程的fork返回值是0    
更不用說if...else的比較也是在兩個進程中都做的了   

【NOTE3】
fork的精闢剖析
程序如下:
#include <unistd.h>;
#include <sys/types.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 %dn",getpid());
else
printf("i am the parent process, my process id is %dn",getpid());
}

結果是

[root@localhost c]# ./a.out

i am the child process, my process id is 4286

i am the parent process, my process id is 4285

一:
要搞清楚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...

爲什麼看上去程序中互斥的兩個分支都被執行了?在一個程序的一次執行中,這當然是不可能的;但是你看到的兩行輸出是來自兩個進程,這兩個進程來自同一個程序的兩次執行。

fork之後,操作系統會複製一個與父進程完全相同的子進程,雖說是父子關係,但是在操作系統看來,他們更像兄弟關係,這2個進程共享代碼空間,但是數據 空間是互相獨立的,子進程數據空間中的內容是父進程的完整拷貝,指令指針也完全相同,但只有一點不同,如果fork成功,子進程中fork的返回值是0, 父進程中fork的返回值是子進程的進程號,如果fork不成功,父進程會返回錯誤。

可以這樣想象,2個進程一直同時運行,而且步調一致,在fork之後,他們分別作不同的工作,也就是分岔了。這也是fork爲什麼叫fork的原因。

在程序段裏用了fork()之後程序出了分岔,派生出了兩個進程。具體哪個先運行就看該系統的調度算法了。

如果需要父子進程協同,可以通過原語的辦法解決。

二:
進程的創建:
創建一個進程的系統調用很簡單.我們只要調用fork函數就可以了.
#include <unistd.h>
pid_t fork();

當一個進程調用了fork以後,系統會創建一個子進程.這個子進程和父進程不同的地方只有他的進程ID和父進程ID,其他的都是一樣.就象父進程克隆 (clone)自己一樣.當然創建兩個一模一樣的進程是沒有意義的.爲了區分父進程和子進程,我們必須跟蹤fork的返回值. 當fork掉用失敗的時候(內存不足或者是用戶的最大進程數已到)fork返回-1,否則fork的返回值有重要的作用.對於父進程fork返回子進程的 ID,而對於fork子進程返回0.我們就是根據這個返回值來區分父子進程的. 父進程爲什麼要創建子進程呢?前面我們已經說過了Linux是一個多用戶操作系統,在同一時間會有許多的用戶在爭奪系統的資源.有時進程爲了早一點完成任 務就創建子進程來爭奪資源. 一旦子進程被創建,父子進程一起從fork處繼續執行,相互競爭系統的資源.有時候我們希望子進程繼續執行,而父進程阻塞,直到子進程完成任務.這個時候 我們可以調用wait或者waitpid系統調用.

   總結一下有三:

1,派生子進程的進程,即父進程,其pid不變;

2,對子進程來說,fork返回給它0,但它的pid絕對不會是0;之所以fork返回0給它,是因爲它隨時可以調用getpid()來獲取自己的pid;

3,fork之後父子進程除非採用了同步手段,否則不能確定誰先運行,也不能確定誰先結束。認爲子進程結束後父進程才從fork返回的,這是不對的,fork不是這樣的,vfork才這樣。


【NOTE4】

首先必須有一點要清楚,函數的返回值是儲存在寄存器eax中的
其次,當fork返回時,新進程會返回0是因爲在初始化任務結構時,將eax設置爲0;
在fork中,把子進程加入到可運行的隊列中,由進程調度程序在適當的時機調度運行。也就是從此時開始,當前進程分裂爲兩個併發的進程。
無論哪個進程被調度運行,都將繼續執行fork函數的剩餘代碼,執行結束後返回各自的值。

【NOTE5】
對於fork來說,父子進程共享同一段代碼空間,所以給人的感覺好像是有兩次返回,其實對於調用fork的父進程來說,如果fork出來的子進程沒有得到 調度,那麼父進程從fork系統調用返回,同時分析sys_fork知道,fork返回的是子進程的id。再看fork出來的子進程,由 copy_process函數可以看出,子進程的返回地址爲ret_from_fork(和父進程在同一個代碼點上返回),返回值直接置爲0。所以當子進 程得到調度的時候,也從fork返回,返回值爲0。
    關鍵注意兩點:1.fork返回後,父進程或子進程的執行位置。(首先會將當前進程eax的值做爲返回值)2.兩次返回的pid存放的位置。(eax中)

進程調用copy_process得到lastpid的值(放入eax中,fork正常返回後,父進程中返回的就是lastpid)
子進程任務狀態段tss的eax被設置成0,
fork.c 中
p-&gt;tss.eax=0;(如果子進程要執行就需要進程切換,當發生切換時,子進程tss中的eax值就調入eax寄存器,子進程執行時首先會將eax的內容做爲返回值)
當子進程開始執行時,copy_process返回eax的值。
fork()後,就是兩個任務同時進行,父進程用他的tss,子進程用自己的tss,在切換時,各用各的eax中的值.

所以,“一次調用兩次返回”是2個不同的進程!
看這一句:pid=fork()
當執行這一句時,當前進程進入fork()運行,此時,fork()內會用一段嵌入式彙編進行系統調用:int 0x80(具體代碼可參見內核版本0.11的unistd.h文件的133行_syscall0函數)。這時進入內核根據此前寫入eax的系統調用功能號 便會運行sys_fork系統調用。接着,sys_fork中首先會調用C函數find_empty_process產生一個新的進程,然後會調用C函數 copy_process將父進程的內容複製給子進程,但是子進程tss中的eax值賦值爲0(這也是爲什麼子進程中返回0的原因),當賦值完成後, copy_process會返回新進程(該子進程)的pid,這個值會被保存到eax中。這時子進程就產生了,此時子進程與父進程擁有相同的代碼空間,程 序指針寄存器eip指向相同的下一條指令地址,當fork正常返回調用其的父進程後,因爲eax中的值是新創建的子進程號,所以,fork()返回子進程 號,執行else(pid&gt;0);當產生進程切換運行子進程時,首先會恢復子進程的運行環境即裝入子進程的tss任務狀態段,其中的eax 值(copy_process中置爲0)也會被裝入eax寄存器,所以,當子進程運行時,fork返回的是0執行if(pid==0)。

【NOTE5】
理解它關鍵在於理解堆棧的切換和壓棧,彈棧!
關於子進程的返回:
子進程複製了父進程的棧內容,從高到低
SS                             ;堆棧段寄存器
ESP                           ;堆棧指針寄存器
EFLAGS                    ;標識寄存器
CS                             ;代碼段寄存器
EIP -----此是int 0x80 的下一條指令,也是子進程開始執行的地方!!!!    ;指令指針寄存器
DS                            ;數據段寄存器
ES                            ;附加段寄存器
FS                            ;附加段寄存器
EDX                         ;DX數據寄存器
ECX                         ;CX計數器
EBX                         ;BX基址寄存器
GS                           ;附加段寄存器
ESI                          ;源地址寄存器
EDI                          ;目的地址寄存器
EBP                         ;基址寄存器
EAX(0)                     ;AX累加器

由於 EAX = 0,所以子進程返回 0 給 fork.

注:新進程的用戶棧設爲其父進程的用戶棧(最後彈出的SS,ESP)。如果父子進程以copy_on_write方式共用用戶堆棧
(Linux之下就是這樣的),而且在此之前父進程修改了該堆棧(如果父進程先返回,這幾乎是肯定的),那麼,系統已經爲父進程創建了該用戶棧的副本,父進程原來的用戶棧留給了子進程。那麼新進程的系統棧已經清空,新進程回到了用戶態,返回到了函數fork。

【NOTE6】
關於fork的討論與評價:
fork好不好?相比其他操作系統如Windows,Windows會有諸如CreateProcess這樣的函數來創建一個與生俱來兩手空空的獨立的新進程。然後還有一大堆參數,指手畫腳的告訴你這個那個是什麼。。。

煩!!!

K.I.S.S. (Keep it simple,stupid.)是Unix的至高原則。

fork 起源於 Unix 操作系統。那是貝爾實驗室的 K&R (這兩人是Unix和C語言之父) 的一項天才發明!!!   Linux由於與生俱來就與Unix血濃於水,所以繼承了它的這個天才發明。

這種方法效率是很高的。因爲複製的代價是很低的。在計算機網絡的實現中,以及在 client/server 系統中的server 一方的實現中,fork 常常是最自然,最有效,最適宜的手段。很多人甚至懷疑,到底是先有 fork 還是先有 client/server,因爲 fork 似乎就是專門爲此而設計的 !更重要的好處是,這樣有利於父子進程間通過 pipe 建立起一種簡單有效的進程間通信管道,並且產生了操作系統的用戶界面即 shell 的管道機制。這一點,對於 Unix 的發展和應用推廣,對於 Uinx 程序設計環境的形成,對於 Unix 程序設計風格的形成,都有非常深遠的影響。可以說這是一項天才的發明,它在很大程度上改變了操作系統的發展方向。

發佈了33 篇原創文章 · 獲贊 11 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章