Chinaunix首页 | 论坛 | 博客
  • 博客访问: 15256
  • 博文数量: 5
  • 博客积分: 1480
  • 博客等级: 上尉
  • 技术积分: 50
  • 用 户 组: 普通用户
  • 注册时间: 2009-08-19 21:23
文章分类
文章存档

2011年(1)

2010年(3)

2009年(1)

我的朋友
最近访客

分类: LINUX

2010-03-15 21:10:49

在学习vfork的时候遇到了return与exit区别的问题:

 

#include <stdio.h>
#include <errno.h>
#include <unistd.h>

int main(void)
{
        pid_t pid;
        int ret = 0;

        if ((pid = vfork()) < 0)
                printf("Fork error:%s\n", strerror(errno));
        else if (pid == 0){
                ret++;
                printf("This is child!The ret is %d\n", ret);
                exit (0);
// return 0;

        }
        else{
                printf("This is father!The ret is %d\n", ret);
        }

        return 0;
// exit(0);

}

如果不使用exit,而使用return那么在fedora下程序在反复的执行,而在ubuntu下则会输出下面这样的结果
 

This is child!The ret is 1
This is father!The ret is -1209189331


这个问题的出现主要就是对于函数返回的深层机制理解不足导致的,在kouu的帮助下发现了下面这篇文章较好的介绍了这个问题,先转载如下,供自己和他人查阅。

------------------------------------------------------------------------

在论坛里看到下面一段代码:
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后,库函数没法保证子进程在进行函数调用或返回的操作后程序还正常,但是库函数vfork本身就是一个函数呀,从系统调用vfork返回后,库函数vfork接着又返回了。这时,程序的正确性又是如何保证的呢?

关于函数调用,一般而言:调用前-调用参数入栈;调用时-返回地址入栈;调用后EBP寄存器值入栈(EBP保存着当前调用栈的位置)。
每层调用都有自己的调用栈,“深”的调用不会影响到之前的调用栈。所以,vfork后子进程调用其他函数应该是没有问题的(但是可能会改写掉属于父进程的某些数据,造成逻辑上的错误),只要它不从调用vfork的函数中返回就行了。
但是,库函数vfork本身却不是这样做的。在这个函数中没有使用栈上的内存空间,它没有去进行调用栈的切换(没有改变EBP)。所以父进程在库函数中运行时,不用担心栈上的数据已经被子进程修改(它根本不去使用栈上的数据)。

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

0008bc70 <__vfork>:
    ......
   8bc88:       59                           pop    %ecx
   8bc89:       b8 be 00 00 00        mov    $0xbe,%eax
   8bc8e:       cd 80                      int    $0x80
   8bc90:       51                           push   %ecx

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