天天看点

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,方便访问这个函数调用里面的这些变量。

继续阅读