fork与vfock系统调用的区别

转载 2012年03月26日 23:02:32

原文链接 http://blog.csdn.net/yunsongice/article/details/5516382

fork()与vfock()都是创建一个进程,那他们有什么区别呢?总结有以下三点区别:

1. fork():子进程拷贝父进程的数据段,代码段
   vfork():子进程与父进程共享数据段
2. fork():父子进程的执行次序不确定
   vfork保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。
3. vfork 保证子进程先运行,在她调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

下面通过几个例子加以说明:

第一:子进程拷贝父进程的代码段的例子:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
    pid_t pid;
    pid=fork();
    if(pid<0)
        printf("error in fork!");
    else if(pid==0)
        printf("I am the child process,ID is %d/n",getpid());
    else
        printf("I am the parent process,ID is %d/n",getpid());
}
运行结果:
[root@localhost chenyunsong]# gcc fork1.c -o fork1
[root@localhost chenyunsong]# ./fork1
I am the child process,ID is 4238
I am the parent process,ID is 4237

为什么两条语句都会打印呢?上一篇博文已经粗略地谈到了,这里再详细说明一下。

首先,./fork1是个可执行文件,当我们在Linux shell环境下运行它时,在执行pid=fork()指令之前,CPU中的寄存器都是跟fork1相关的,并且是处于用户态,例如cs指向用户态代码段基地址;eip指向用户态代码段中下一条指令的偏移位置,即fork()库函数;ds指向用户态数据段基地址;esi/edi指向用户态数据段中当前操作数的全部/部分偏移位置;ss指向用户态数据段中堆栈段的基地址;esp指向用户态栈顶单元的偏移位置;ebp指向用户态堆栈段中当前单元的全部/部分偏移量,也可能存放32位或16位操作数或运算结果。

好了,fork()以后,产生中断,进入内核态,CS和eip这对寄存器包含下一条将要执行的指令(sys_fork(regs))的逻辑地址。进入内核态后,控制单元必须开始使用与新的特权级相关的指令段和栈。这个过程包括从TSS段中装载ss0和esp0寄存器,在新的内核态堆栈中保存用户态的ss 和esp的值,用引起异常的指令地址装载CS和eip寄存器(sys_fork(regs)),在内核态堆栈中保存eflags、CS及eip的内容。

随后,SAVE_ALL,将所有其他的寄存器(大多数的通用寄存器)保存到内核态堆栈中。这时,cs:eip、ss:esp都是内核态了,但是,注意了,这时候ds段寄存器仍然是__USER_DS,因为上一博文讲过了,这是效率问题。至于如何使用用户态的静态数据,Linux用的是另一种拷贝机制,我们会在相关的专题中专门讨论,也是很重要的,但这里不在话下。

走到fork()系统调用的实务函数do_fork()函数,用于从已存在的进程中创建一个新的进程,新的进程称为子进程,而原进程称为父进程,fork()的返回值有两个:子进程返回0,父进程返回子进程的进程号,进程号都是非零的正整数,所以父进程返回的值一定大于零,在pid=fork()语句之前只有父进程在运行,而在pid=fork()之后,父进程和新创建的子进程都在运行,所以如果pid==0,那么肯定是子进程,为什么?

因为do_fork()结束之后,我们有了处于可运行状态的完整的子进程。但是,它还没有实际运行,要等到调度程序schedule()决定何时把CPU交给这个子进程。以后的博文中会详细阐述Linux的调度机制,这里只需要知道有可能立即就切换即子进程先运行,也有可能要等一段时间切换,即父进程先运行。在调度程序的进程切换中,调度程序继续完善子进程:把子进程描述符thread字段的值(TSS值)装入几个CPU寄存器(详见上一篇博文)。特别是把thread.esp装入esp寄存器,把函数ret_from_fork()的地址装入eip寄存器(这些事儿以后才做,之前eax寄存器的值还是父进程执行do_fork函数后返回的子进程pid的值)。这个汇编语言函数调用 schedule_tail()函数,用存放在栈中的值再装入所有寄存器,使进程返回到用户态。这样,eax寄存器就装过两个值:一个是父进程的值——子进程的PID(这个时候是父进程在运行,因为在切换前eax保留的是子进程pid的值);一个是子进程的值 0(这个时候是子进程在运行,将eax原先的内容覆盖了);。

若pid!=0(事实上肯定大于0),那么是父进程在运行。而我们知道fork()函数子进程是拷贝父进程的代码段的,所以子进程中同样有
if(pid<0)
    printf("error in fork!");
else if(pid==0)
    printf("I am the child process,ID is %d/n",getpid());
else
    printf("I am the parent process,ID is %d/n",getpid());
这么一段代码,所以上面这段代码会被父进程和子进程各执行一次,最终由于子进程的pid==0,而打印出第一句话,父进程的pid>0,而打印出第二句话。于是得到了上面的运行结果。

注意,并不是子进程执行一段,父进程执行一段,而是都执行,所以其只是根据pid(C编译器会定位到eax寄存器)的值来判断具体执行哪一段。

再来看一个拷贝数据段的例子:
#include <unistd.h>
#include <stdio.h>
int main(void)
{
    pid_t pid;
    int count=0;
    pid=fork();
    count++;
    printf("count= %d/n",count);
    return 0;
}

大家觉着打印出的值应该是多少呢?是不是2 呢?先来看下运行结果吧
[root@localhost chenyunsong]# gcc fork2.c -o fork2
[root@localhost chenyunsong]# ./fork2
count= 1
count= 1

为什么不是2 呢?因为我们一次强调fork()函数子进程拷贝父进程的数据段代码段,所以
count++;
printf("count= %d/n",count);
return 0
将被父子进程各执行一次,但是子进程执行时使自己的数据段里面的(这个数据段是从父进程那copy 过来的一模一样)count+1,同样父进程执行时使自己的数据段里面的count+1,他们互不影响,与是便出现了如上的结果。

那么再来看看vfork()吧。如果将上面程序中的fork()改成vfork(),运行结果是什么样子的呢?
[root@localhost chenyunsong]# gcc fork2.c -o fork2
[root@localhost chenyunsong]# ./fork2
count= 1
count= 1

还是错误。本来vfock()是共享数据段的,结果应该是2,为什么不是预想的2呢?先看一个知识点:vfork和fork 之间的另一个区别是: vfork保证子进程先运行,在她调用exec 或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

这样上面程序中的fork()改成vfork()后,vfork()创建子进程并没有调用exec 或exit,所以最终将导致死锁。

怎么改呢?看下面程序:
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main(void)
{
    pid_t pid;
    int count=0;
    pid=vfork();
    if(pid==0)
    {
        count++;
        _exit(0);
    }else{
        count++;
    }
    printf("count= %d/n",count);
    return 0;
}

如果没有_exit(0)的话,子进程没有调用exec或exit,所以父进程是不可能执行的,在子进程调用exec或exit之后父进程才可能被调度运行。

所以我们加上_exit(0);使得子进程退出,父进程执行,这样else后的语句就会被父进程执行,又因在子进程调用exec或exit之前与父进程数据是共享的,所以子进程退出后把父进程的数据段count改成1 了,子进程退出后,父进程又执行,最终就将count 变成了2,看下实际运行结果:
[root@localhost chenyunsong]# gcc fork2.c -o fork2
[root@localhost chenyunsong]# ./fork2
count= 2

网上抄的一段,可以再加深一下理解:

为什么会有vfork,因为以前的fork很傻,当它创建一个子进程时,将会创建一个新的地址空间,并且拷贝父进程的资源,而往往在子进程中会执行exec 调用,这样,前面的拷贝工作就是白费力气了,在还没有人想到写时拷贝(write-on-copy)机制情况下,聪明的人就想出了vfork,它产生的子进程刚开始暂时与父进程共享地址空间(其实就是线程的概念了),因为这时候子进程在父进程的地址空间中运行,所以子进程不能进行写操作,并且在儿子“ 霸占”着老子的房子时候,要委屈老子一下了,让他在外面歇着(阻塞),一旦儿子执行了exec或者exit后,相当于儿子买了自己的房子了,这时候就相当于分家了。

Linux系统调用fork()用法详解

Linux系统调用fork()用法详解        这学期在学操作系统,老师布置了一个关于进程的实验题,是在Linux系统中实现的,其中有涉及到fork()函数的调用,恰好我研究Ubuntu 也有...

Linux kernel 分析之九:fork()系统调用

不仅进入系统调用时要伪装现场,fork系统调用时返回时也需要伪装现场。因为是“无中生有”。 例如在fork创建新进程时,系统要保证新进程与旧进程一样,从相同的代码开始执行。比如: #include #...

秒杀linux下系统调用fork()面试题

秒杀linux下系统调用fork()面试题 第一道题(在之前博客也写过这道题:http://blog.csdn.net/chdhust/article/details/8535915...

进程系统调用——fork函数深入理解

原创作品 转载请注明出处http://blog.csdn.net/always2015/article/details/45008785当我们在一个现代系统上运行一个程序的时候,我们会得到一个假象,...

关于系统调用fork()

一、fork入门知识 一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程, 也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量...

秒杀linux下系统调用fork()面试题

秒杀linux下系统调用fork()面试题 第一道题(在之前博客也写过这道题:http://blog.csdn.net/chdhust/article/details/8535915...

fork系统调用

fork系统调用                                           分类:             Linux进程编程              201...

fork系统调用底层实现源码剖析

本文主要针对在linux 2.6.11.12内核源码中,fork系统调用的实现过程。 假设我们的程序源文件名为main.c,内容是: Void main() { Pid_t pid = fork();...

fork和pause等系统调用的实现原理(Linux0.11内核)

点击打开链接
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:fork与vfock系统调用的区别
举报原因:
原因补充:

(最多只允许输入30个字)