天天看點

vfork建立的子程序與父程序位址空間關系

存儲unix程式設計

在《UNIX環境進階程式設計》一書的第八章中,有一道課後習題如下:

回憶圖7-3典型的存儲空間布局。由于對應于每個函數調用的棧幀通常存儲在棧中,并在調用 vfork後,子程序運作在父程序的位址空間中,如果不是在main函數中而是在另一個函數中調用vfork,以後子程序從該函數傳回時,将會發生什麼情況?

作者Rich Stevens是一位大師,留下這麼一題必有其深意,于是結合《深入了解計算機系統》中的知識,寫了個程式驗證了下,受益良多。

         首先回憶下程式運作的棧幀結構(見下圖):

vfork建立的子程式與父程式位址空間關系

從圖中可知,如果一個函數調用用了另外一個函數,那麼被調用者的棧幀則會被壓入棧頂被設定為“目前幀”,首先執行被調用者,執行完成後,調用者的棧幀被彈出程式棧,然後從“傳回位址”傳回到調用者的位址空間中。

于是猜想,如果在main函數中,調用了一個函數foo,則“目前幀”為foo的棧幀,這時,若調用vfork建立一個子程序,那麼根據vfork的語義,子程序不會完全複制父程序的位址空間,它會在父程序的位址空間中運作(這也是為什麼vfork能保證子程序先運作,而fork不能保證。因為vfork建立的子程序是與父程序共享位址空間,為了避免競争,是以就讓子程序先運作,而父程序後運作;而fork建立的子程序是父程序的副本,是以不會帶來競争問題,誰先誰後也就無所謂了),是以它共享的是“目前幀”的位址空間,是以當子程序傳回時,隻會改變foo的資料,而不會改變main棧幀中的資料。

下面就來寫個程式驗證一下:

#include <stdio.h>  
#include <unistd.h>  
#include <sys/types.h>  
  
int glob = 88;               //a global var  
void foo(int);  
  
int main(int argc,char *arg[])  
{  
         int var = 100;            //a local var in main  
         foo(var);  
         if(printf("In main var:%d  glob:%d pid:%d/n",var,glob,getpid())<0)  
           perror("main printf");  
         exit(0);  
}  
  
void foo(int var)  
{  
         pid_t pid;  
         int loc = 66;                 //a local var in foo  
         printf("Before vfork/n");  
         if((pid = vfork())<0)  
           perror("vfork");  
         else if(pid == 0)            //child process  
         {  
                   loc++;  
                   var++;  
                   glob++;  
                   printf("pid:%d/n",getpid());  
                   exit(0);  
         }                   
  
         /*parent process continues here*/  
         printf("In foo var:%d  glob:%d  loc:%d  pid:%d/n",var,glob,loc,getpid());  
  
}  
           

運作此程式,得到結果為(見下圖):

vfork建立的子程式與父程式位址空間關系

果然,可以看到在foo和main中,程序号都是一樣的,也就說明foo和main在同一程序中。但是各個變量的值卻有差異:在foo傳回後mian函數中的局部變量var依然是初始值,并沒有增加,推其原因,就是因為子程序共享的是foo的棧幀資料,而非main函數的棧幀,是以自然也就不會改變main棧幀中的資料。(var 是值傳遞!)

書中正文中說:子程序不會完全複制父程序的位址空間,它會在父程序的位址空間中運作。是以可以進一步得出一個結論:vfork建立的子程序,共享的是父程序目前棧幀的位址空間。

(1)、堆棧幀到底是什麼

堆棧幀(stack frame)(或活動記錄(activation record))是一塊堆棧保留區域,用于存放被傳遞的實際參數、子程式的傳回值、局部變量以及被儲存的寄存器。

實際上堆棧幀就相當于子函數的緩存,當子函數使用的堆棧個數最大時,其所擁有的所有部分構成了這個函數的堆棧幀。

esp是棧指針,是cpu機制決定的,push、pop指令會自動調整esp的值;

ebp隻是存取某時刻的esp,這個時刻就是進入一個函數内後,cpu會将esp的值賦給ebp,此時就可以通過ebp對棧進行操作,比如擷取函數參數,局部變量等,實際上使用esp也可以;

既然使用esp也可以,那麼為什麼要設定ebp呢?

答案是為了友善程式員。

因為esp在函數運作時會不斷的變化,是以儲存一個一進入某個函數的esp到ebp中會友善程式員通路參數和局部變量,而且還友善調試器分析函數調用過程中的堆棧情況。前面說了,這個ebp不是必須要有的,你非要使用esp來通路函數參數和局部變量也是可行的,隻不過這樣會麻煩一些。

是以:

esp是棧頂指針,隻要有push,esp就減小,pop,esp就增加;

ebp是對于一個棧幀來說,即一次函數調用,ebp指向該函數參數,位址,局部變量壓棧後最後的esp,友善通路這個函數調用裡面的這些變量。

繼續閱讀