1. 程式人生 > >linux程序管理之程序建立(三)

linux程序管理之程序建立(三)

在linux系統中,許多程序在誕生之初都與其父程序共同用一個儲存空間。但是子程序又可以建立自己的儲存空間,並與父程序“分道揚鑣”,成為與父程序一樣真正意義上的程序。

 linux系統執行的第一個程序是在初始化階段“捏造出來的”。而此後的執行緒或程序都是由一個已存在的程序像細胞分裂一樣通過系統呼叫複製出來的,稱為“fork()”或者“clone()”。

1.fork()

關於fork()和exec()的介紹在之前的一篇博文中做了介紹,

一個現有程序可以呼叫fork()函式建立一個新程序。由fork建立的新程序被稱為子程序(child process)。fork函式被呼叫一次但返回兩次。兩次返回的唯一區別是子程序中返回0值而父程序中返回子程序ID。

子程序是父程序的副本,它將獲得父程序資料空間、堆、棧等資源的副本。注意,子程序持有的是上述儲存空間的“副本”,這意味著父子程序間不共享這些儲存空間。

UNIX將複製父程序的地址空間內容給子程序,因此,子程序有了獨立的地址空間。在不同的UNIX (Like)系統下,我們無法確定fork之後是子程序先執行還是父程序先執行,這依賴於系統的實現。所以在移植程式碼的時候我們不應該對此作出任何的假設。

由於在複製時複製了父程序的堆疊段,所以兩個程序都停留在fork函式中,等待返回。因此fork函式會返回兩次,一次是在父程序中返回,另一次是在子程序中返回,這兩次的返回值是不一樣的。

呼叫fork之後,資料、堆疊有兩份,程式碼仍然為一份但是這個程式碼段成為兩個程序的共享程式碼段都從fork函式中返回,箭頭表示各自的執行處。當父子程序有一個想要修改資料或者堆疊時,兩個程序真正分裂。

fork函式的特點概括起來就是“呼叫一次,返回兩次”,在父程序中呼叫一次,在父程序和子程序中各返回一次。

fork的另一個特性是所有由父程序開啟的描述符都被複制到子程序中。父、子程序中相同編號的檔案描述符在核心中指向同一個file結構體,也就是說,file結構體的引用計數要增加。

2.vfork()

vfork()會產生一個新的子程序。但是vfork建立的子程序與父程序共享資料段,而且由vfork建立的。子程序將先於父程序執行。

vfork()用法與fork()相似.但是也有區別,具體區別歸結為以下幾點:

1. fork():子程序拷貝父程序的資料段,程式碼段. vfork():子程序與父程序共享資料段.

2. fork():父子程序的執行次序不確定.

vfork():保證子程序先執行,在呼叫exec或exit之前與父程序資料是共享的,在它呼叫exec或exit之後父程序才可能被排程執行。

3. vfork()保證子程序先執行,在她呼叫exec或exit之後父程序才可能被排程執行。如果在呼叫這兩個函式之前子程序依賴於父程序的進一步動作,則會導致死鎖。

4.當需要改變共享資料段中變數的值,則拷貝父程序。

從這裡可見,vfork()和fork()之間的一個區別是:vfork 保證子程序先執行,在她呼叫exec 或exit 之後父程序才可能被排程執行。如果在呼叫這兩個函式之前子程序依賴於父程序的進一步動作,則會導致死鎖。

我們來看下面這段程式碼:

#include<sys/types.h>   
#include<unistd.h>    
#include<stdio.h>    
int main()      
{      
    pid_t pid;      
    int cnt = 0;      
    pid = fork();     
    if(pid<0)     
    printf("error in fork!\n");     
    else if(pid == 0)     
    {     
        cnt++;     
        printf("cnt=%d\n",cnt);     
        printf("I am the child process,ID is %d\n",getpid());     
    }     
    else   
    {     
        cnt++;     
        printf("cnt=%d\n",cnt);     
        printf("I am the parent process,ID is %d\n",getpid());     
    }     
    return 0;   
}

 

執行結果為:

cnt=1  
I am the child process,ID is 5077  
cnt=1  
I am the parent process,ID is 5076  

 

為什麼不是2 呢?因為我們一次強調fork ()函式子程序拷貝父程序的資料段程式碼段,所以 

cnt++;      

printf("cnt= %d\n",cnt);

return 0  


將被父子程序各執行一次,但是子程序執行時使自己的資料段裡面的(這個資料段是從父程序那copy 過來的一模一樣)count+1,同樣父程序執行時使自己的資料段裡面的count+1,  他們互不影響,與是便出現瞭如上的結果。

那麼再來看看vfork ()吧。如果將上面程式中的fork ()改成vfork(),執行結果是什麼  樣子的呢?

cnt=1  
I am the child process,ID is 4711  
cnt=1  
I am the parent process,ID is 4710  
段錯誤 

 

本來vfock()是共享資料段的,結果應該是2,為什麼不是預想的2 呢?

上面程式中的fork ()改成vfork()後,vfork ()建立子程序並沒有呼叫exec 或exit,  所以最終將導致死鎖。 

那麼,對程式做下面的修改,

#include<sys/types.h>    
#include<unistd.h>    
#include<stdio.h>    
int main()      
{     
    pid_t pid;      
    int cnt = 0;     
    pid = vfork();     
    if(pid<0)    
        printf("error in fork!\n");    
    else if(pid == 0)     
    {     
        cnt++;     
        printf("cnt=%d\n",cnt);     
        printf("I am the child process,ID is %d\n",getpid());    
        _exit(0);     
    }     
    else   
    {     
        cnt++;     
        printf("cnt=%d\n",cnt);     
        printf("I am the parent process,ID is %d\n",getpid());     
    }     
    return 0;     
}  

 

如果沒有_exit(0)的話,子程序沒有呼叫exec 或exit,所以父程序是不可能執行的,在子程序呼叫exec 或exit 之後父程序才可能被排程執行。 
所以我們加上_exit(0);使得子程序退出,父程序執行,這樣else 後的語句就會被父程序執行,又因在子程序呼叫exec 或exit之前與父程序資料是共享的,所以子程序退出後把父程序的資料段count改成1 了,子程序退出後,父程序又執行,最終就將count變成了2。
執行結果:

cnt=1  
I am the child process,ID is 4711  
cnt=2  
I am the parent process,ID is 4710

 

3.擴充套件

有這樣一段程式碼:

#include <stdio.h>
 
#include <stdlib.h>
 
#include <unistd.h>
 
int main(void) {
 
    int var;
 
    var = 88;
 
    if ((pid = vfork()) < 0) {
 
        printf("vfork error");
 
        exit(-1);
 
    } else if (pid == 0) { /* 子程序 */
 
        var++;
 
        return 0;
 
    }
 
    printf("pid=%d, glob=%d, var=%d\n", getpid(), glob, var);
 
    return 0;
 
}

上述程式碼一執行就掛掉了,但如果把子程序的return改成exit(0)就沒事。這是為什麼呢?

首先說一下fork和vfork的差別:

  • fork 是 建立一個子程序,並把父程序的記憶體資料copy到子程序中。
  • vfork是 建立一個子程序,並和父程序的記憶體資料share一起用。

這兩個的差別是,一個是copy,一個是share。

你 man vfork 一下,你可以看到,vfork是這樣的工作的,

1)保證子程序先執行。 2)當子程序呼叫exit()或exec()後,父程序往下執行。

那麼,為什麼要幹出一個vfork這個玩意? 原因在man page也講得很清楚了:

Historic Description

Under Linux, fork(2) is implemented using copy-on-write pages, so the only penalty incurred by fork(2) is the time and memory required to duplicate the parent’s page tables, and to create a unique task structure for the child. However, in the bad old days a fork(2) would require making a complete copy of the caller’s data space, often needlessly, since usually immediately afterwards an exec(3) is done. Thus, for greater efficiency, BSD introduced the vfork() system call, which did not fully copy the address space of the parent process, but borrowed the parent’s memory and thread of control until a call to execve(2) or an exit occurred. The parent process was suspended while the child was using its resources. The use of vfork() was tricky: for example, not modifying data in the parent process depended on knowing which variables are held in a register.

意思是這樣的—— 起初只有fork,但是很多程式在fork一個子程序後就exec一個外部程式,於是fork需要copy父程序的資料這個動作就變得毫無意了,這樣幹顯得很重(因為拷貝了所有內容)。

所以,BSD搞出了個父子程序共享的 vfork,這樣成本比較低。因此,vfork本就是為了exec而生。

為什麼return會掛掉,exit()不會?

從上面我們知道,結束子程序的呼叫是exit()而不是return,如果你在vfork中return了,那麼,這就意味main()函式return了,注意因為函式棧父子程序共享,所以整個程式的棧就跪了。

如果你在子程序中return,那麼基本是下面的過程:

1)子程序的main() 函式 return了,於是程式的函式棧發生了變化。

2)而main()函式return後,通常會呼叫 exit()或相似的函式(如:_exit(),exitgroup())

3)這時,父程序收到子程序exit(),開始從vfork返回,但是父程序的棧都被子程序給return幹廢掉了,父程序無法執行

(注:棧會返回一個詭異一個棧地址,對於某些核心版本的實現,直接報“棧錯誤”就給跪了,然而,對於某些核心版本的實現,於是有可能會再次呼叫main(),於是進入了一個無限迴圈的結果,直到vfork 呼叫返回 error)

好了,現在再回到 return 和 exit,return會釋放區域性變數,並彈棧,回到上級函式執行。exit直接退掉。如果你用c++ 你就知道,return會呼叫區域性物件的解構函式,exit不會。(注:exit不是系統呼叫,是glibc對系統呼叫 _exit()或_exitgroup()的封裝)

可見,子程序呼叫exit() 沒有修改函式棧,所以,父程序得以順利執行

關於fork的優化

很明顯,fork太重,而vfork又太危險,所以,就有人開始優化fork這個系統呼叫。優化的技術用到了著名的寫時拷貝(COW)

也就是說,對於fork後並不是馬上拷貝記憶體,而是隻有你在需要改變的時候,才會從父程序中拷貝到子程序中,這樣fork後立馬執行exec的成本就非常小了。所以,Linux的Man Page中並不鼓勵使用vfork() ——

“ It is rather unfortunate that Linux revived this specter from the past. The BSD man page states: “This system call will be eliminated when proper system sharing mechanisms are implemented. Users should not depend on the memory sharing semantics of vfork() as it will, in that case, be made synonymous to fork(2).””

於是,從BSD4.4開始,他們讓vfork和fork變成一樣的了

但在後來,NetBSD 1.3 又把傳統的vfork給撿了回來,說是vfork的效能在 Pentium Pro 200MHz 的機器(這機器好古董啊)上有可以提高几秒鐘的效能。詳情見——“NetBSD Documentation: Why implement traditional vfork()

今天的Linux下,fork和vfork還是各是各的,不過,還是建議你不要用vfork,除非你非常關注效能。

4.圖說

在最後,放兩張fork()和vfork()的圖,我們自己體會。。。

fork():

vfork():

寫時拷貝:

參考:

https://www.cnblogs.com/lovemdx/p/3308057.html

https://www.cnblogs.com/1932238825qq/p/7373443.html