1. 程式人生 > >linux進程管理之進程創建(三)

linux進程管理之進程創建(三)

while 變量 call 兩次返回 pen lan under strong ()

在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

linux進程管理之進程創建(三)