您的位置:首页 > 理论基础 > 计算机网络

fork() vfork() return退出与exit退出 http://blog.chinaunix.net/uid-24261911-id-2620651.html

2015-09-22 14:19 471 查看
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”,把“返回地址”放回去。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: