Chinaunix首页 | 论坛 | 博客
  • 博客访问: 185979
  • 博文数量: 26
  • 博客积分: 1416
  • 博客等级: 上尉
  • 技术积分: 176
  • 用 户 组: 普通用户
  • 注册时间: 2010-07-18 15:59
文章分类
文章存档

2011年(3)

2010年(23)

我的朋友

分类: LINUX

2010-09-17 18:54:10

fork() 与 vfork()的区别:
 
1、fork:子进程拷贝父进程的数据段
   vfork:子进程与父进程共享数据段
2、fork:父子进程的执行次序不确定
   vfork:子进程先运行,父进程后运行
 
 
fork:

//fork: fork1.c

  1 #include <unistd.h>
  2 #include <stdio.h>
  3 #include <stdlib.h>
  4
  5 main(void)
  6 {
  7 pid_t pid;
  8 int count=0;
  9
 10 pid=fork();
 11 if(pid==0)
 12 {
 13 printf("child: count=%d\n",count);
 14 printf("child: getpid=%d\n",getpid());
 15 count=1;
 16 printf("child: count=%d\n",count);
 17 }
 18 else
 19 {
 20 printf("\nfather: pid=%d\n",pid);
 21 printf("father: count=%d\n",count);
 22 }
 23 return(0);
 24 }

运行结果:

[root@localhost part1_linux]# gcc fork1.c
[root@localhost part1_linux]# ./a.out
child: count=0
child: getpid=10581
child: count=1

father: pid=10581
father: count=0

 
 
 
vfork:

//vfor: fork2.c 

  1 #include <unistd.h>
  2 #include <stdio.h>
  3 #include <stdlib.h>
  4
  5 main(void)
  6 {
  7 pid_t pid;
  8 int count=0;
  9
 10 pid=vfork();
 11 if(pid==0)
 12 {
 13 printf("child: count=%d\n",count);
 14 printf("child: getpid=%d\n",getpid());
 15 count=1;
 16 printf("child: count=%d\n",count);
 17 // return 0;//会出现段错误

 18 exit(0); //ok

 19 }
 20 else
 21 {
 22 printf("\nfather: pid=%d\n",pid);
 23 printf("father: count=%d\n",count);
 24 }
 25 return(0);
 26 }

运行结果:

[root@localhost part1_linux]# gcc fork2.c
[root@localhost part1_linux]# ./a.out
child: count=0
child: getpid=9911
child: count=1

father: pid=9911
father: count=1



运行结果说明:vfrok时父、子进程共享数据段,fork时是进行拷贝

如果,vfork子进程中,使用return返回时,出现段错误,结果如下:

[root@localhost part1_linux]# gcc fork2.c
[root@localhost part1_linux]# ./a.out
child: count=0
child: getpid=10864
child: count=1

father: pid=10864
father: count=0
段错误

×××××××××××××××××××××扩展××××××××××××××××××××××××××××××××××××××××

具体原因,还是有些模糊,参考:《神奇的vfork》http://hi.baidu.com/_kouu/blog/item/3e92640e3b6393e4ab645784.html

 

         神奇的vfork
2009-07-20 23:23

一段神奇的代码

在论坛里看到下面一段代码:
int createproc();
int main()
{
   pid_t pid=createproc();
   printf("%d\n", pid);
   exit(0);
}
int createproc()
{
   pid_t pid;
   if(!(pid=vfork()))
{
     printf("child proc:%d\n", pid);
     return pid;
   }
   else return -1;
}

输出结果:
child proc:0
0
child proc:0
Killed


感觉非常奇怪,为什么vfork以后,父子进程都走了“子进程”的分支呢?

什么是vfork?

什么是vfork,网络上介绍它的文档很多,随便一搜就是一大堆。简单来说,vfork和fork完成了基本上相同的功能,把进程做了一次复制,变成两个进程。
在shell中,执行命令时,shell程序就是通过“复制”形成了父子进程。子进程生成后,执行exec系列函数,载入新的可执行文件,开始执行。
由于复制完成后,子进程马上就要载入新的程序来运行了,在此之前从父进程那里复制来的内存空间都不需要了。所以,“复制”过程中,复制内存空间是件费力不讨好的事情。
所以,fork有了“写时复制”技术。“复制”的时候内存并没有被复制,而是共享的。直到父子进程之一去写某块内存时,它才被复制。(内核先将这些内存设为只读,当它们被写时,CPU出现访存异常。内核捕捉异常,复制空间,并改属性为可写。)

上面说到的内存空间是实际存储用户数据的空间,利用“写时复制”避免了干前面提到的那件费力不讨好的事情。
但是,“写时复制”其实还是有复制,进程的mm结构、页表都还是被复制了(“写时复制”也必须由这些信息来支撑。否则内核捕捉到CPU访存异常,怎么区分这是“写时复制”引起的,还是真正的越权访问呢?)。
而vfork就把事情做绝了,所有有关于内存的东西都不复制了,父子进程的内存是完全共享的。但是这样一来又有问题了,虽然用户程序可以设计很多方法来避免父子进程间的访存冲突。但是关键的一点,父子进程共用着栈,这可不由用户程序控制的。一个进程进行了关于函数调用或返回的操作,则另一个进程的调用栈(实际上就是同一个栈)也被影响了。这样的程序没法运行下去。

所以,vfork有个限制,子进程生成后,父进程在vfork中被内核挂起,直到子进程有了自己的内存空间(exec**)或退出(_exit)。并且,在此之前,子进程不能从调用vfork的函数中返回(同时,不能修改栈上变量、不能继续调用除_exit或exec系列之外的函数,否则父进程的数据可能被改写)。
尽管限制很多,但并不妨碍实现前面提到的关于shell程序的那个“需求”。

问题的思考

说到这里,可以看出文章开头的那段代码是存在问题的了。子进程不但调用了printf,还从createproc函数中返回了。
但是,子进程的违规为什么会使父进程走上“child proc”这条路呢?父进程在子进程退出前被阻塞在vfork里面,vfork的返回值是如何变成0的呢?

前面一直在说vfork,其实它是两个东西,(libc)函数vfork系统调用vfork。用户程序调用的是库函数,而库函数再去调用系统调用。用户程序中几乎所有的系统调用都是通过库函数去调用的。因为不同体系结构下(甚至相同体系结构),系统调用的指令和参数传递规则都可能不同,这些细节被库函数隐藏了。

前面提到,父进程被挂起在vfork中,这是指的系统调用vfork。在系统调用中,进程使用的是内核栈(每个进程有着自己独有的内核栈)。此时,父进程在内核里面是安全的,随便子进程怎么违规。内核会保证系统调用vfork的完整性,系统调用的返回值也不会有问题(它是通过寄存器传回用户空间的,跟栈无关)。
而vfork的返回值变成0的问题,则是在库函数vfork中产生的。既然子进程已经违规了,库函数没办法保证程序的正确性。而库函数vfork是否返回0也是不确定的,可能不同版本的libc、不同的程序上下文、不同的系统、等等、都会有不同的返回值(或者就直接“段错误”了)。还有可能是,父进程中库函数vfork并没有返回0,但是栈上的返回地址被改写了,从函数createproc返回,返回到printf("child proc")这句话去了。

再深入一点

vfork后,库函数没法保证子进程在进行函数调用或返回的操作后程序还正常,但是库函数vfork本身就是一个函数呀,从系统调用vfork返回后,库函数vfork接着又返回了。这时,程序的正确性又是如何保证的呢?

关于函数调用,一般而言:调用前-调用者将需要传递的参数放到栈上;调用时-调用者使用call指令,该指令自动将返回地址入栈;调用后,在被调用的函数中,第一件事是做调用栈的调整,如createproc函数如是做:
08048487 :
8048487:       55                      push   %ebp
8048488:       89 e5                 mov    %esp,%ebp
804848a:       83 ec 28            sub    $0x28,%esp
......

其中ESP是当前栈的指针,而EBP是上一层调用栈的指针。调用栈调整之前,EBP保存着上上一层栈的指针,这个值不能丢,需要放在栈上,以便函数返回时恢复。

每层调用都有自己的调用栈,“深”的调用不会影响到之前的调用栈。所以,vfork后子进程调用其他函数应该是没有问题的(但是可能会改写掉属于父进程的某些数据,造成逻辑上的错误),只要它不从调用vfork的函数中返回就行了。
但是,库函数vfork本身却不是这样做的。在这个函数中没有使用栈上的内存空间,它没有去进行调用栈的切换,如:
000983f0 <__vfork>:
983f0:       59                      pop    %ecx
983f1:       65 8b 15 6c 00 00 00    mov    %gs:0x6c,%edx
983f8:       89 d0                   mov    %edx,%eax
983fa:       f7 d8                   neg    %eax
......

9840e:       cd 80                   int    $0x80
98410:       51                      push   %ecx
......

所以父进程在库函数中运行时,不用担心栈上的数据已经被子进程修改(它根本不去使用栈上的数据)。
然而call/ret指令却不得不使用栈(因为返回地址自动会被CPU放在栈上),如果子进程在vfork后调用其他函数,会使得父进程在进入库函数vfork时通过call指令在栈上留下的“返回地址”被擦掉。
事情的确是这样。于是库函数vfork为了解决这个问题,做了一些手脚,它并没有让栈上的“返回地址”一直留在栈上。注意上面的汇编代码,进入库函数vfork的第一条指令就是“pop %ecx”,把放在栈上的“返回地址”弹到了ECX中去,保存起来。然后在系统调用vfork返回后(int 0x80是用于系统调用的指令),再“push %ecx”,把“返回地址”放回去。


阅读(4011) | 评论(0) | 转发(2) |
给主人留下些什么吧!~~