linux 进程创建clone、fork与vfork

目录:

1、clone、fork与vfork介绍

2、fork说明

3、vfork说明

4、clone说明
5、fork,vfork,clone的区别

内容:

1、clone、fork与vfork介绍

Linux下的进程与线程相同点是都有进程控制块(PCB,具体的类是task_struct)。区别在于一个有独立的进程资源,一个是共享的进程资源。除了内核线程是完全没有用户空间。进程资源包括进程的PCB、线程的系统堆栈、进程的用户空间、进程打开的设备(文件描述符集)等。

Linux的用户进程不能直接被创建出来,因为不存在这样的API。它只能从某个进程中复制出来,有的需要通过exec这样的API来切换到实际想要运行的程序文件。

复制的API包括三种:fork、clone、vfork。

在linux源码中这三个调用的执行过程是执行fork(),vfork(),clone()时,通过一个系统调用表映射到sys_fork(),sys_vfork(),sys_clone(),再在这三个函数中去调用do_fork()去做具体的创建进程工作。这三个API的内部实际都是调用一个内核内部函数do_fork,只是填写的参数不同而已。

1.fork, vfork and clone三者最终都会调用do_fork函数,三者的差别就是参数上的不同而已。
fork的实现:
   do_fork(CLONE_SIGCHLD,...)
clone的实现:
    do_fork(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGCHLD,...)
vfork的实现:
     do_fork(CLONE_VFORK|CLONE_VM|CLONE_SIGCHLD,...)

实际上产生效果的也是这些参数:

CLONE_VM标识:表示共享地址空间(变量等)

CLONE_FILES标志:表示共享文件描述符表

CLONE_VFORK标识:标识父进程会被阻塞,子进程会把父进程的地址空间锁住,直到子进程退出或执行exec时才释放该锁

SIGCHLD标识:共享信号

2. Linux使用copy on wirte的技术,Linux中的fork代价仅仅是创建子进程的页表结构和创建一个task_struct结构。
3. 为了优化那些:fork然后就是exec的程序,Linux提供了vfork。vfork时,父进程会被阻塞,直到子进程调用了exec或exit,因为此时不复制页表结构。
4. clone()系统调用是fork()的推广形式,它允许新进程共享父进程的存储空间、文件描述符和信号处理程序

2、fork
共享资源:

fork创建一个进程时,子进程只是完全复制父进程的资源,复制出来的子进程有自己的task_struct结构和pid,但却复制父进程其它资源(用户空间、文件描述符集)。

写时复制:

fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程后,其子进程仅仅是为了调用exec执行另一个可执行文件,那么在fork过程中对于虚存空间的复制将是一个多余的过程。但由于现在Linux中是采取了copy-on-write(COW写时复制)技术,为了降低开销,fork最初并不会真的产生两个不同的拷贝,因为在那个时候,大量的数据其实完全是一样的。写时复制是在推迟真正的数据拷贝。若后来确实发生了写入,那意味着parent和child的数据不一致了,于是产生复制动作,每个进程拿到属于自己的那一份,这样就可以降低系统调用的开销。所以有了写时复制。

返回值:

fork()调用执行一次返回两个值,对于父进程,fork函数返回子程序的进程号,而对于子程序,fork函数则返回零,这就是一个函数返回两次的本质。

共享代码段:

在fork之后,子进程和父进程都会继续执行fork调用之后的指令。子进程是父进程的副本。它将获得父进程的数据空间,堆和栈的副本,这些都是副本,父子进程并不共享这部分的内存。也就是说,子进程对父进程中的同名变量进行修改并不会影响其在父进程中的值。但是父子进程又共享一些东西,简单说来就是程序的正文段。正文段存放着由cpu执行的机器指令,通常是read-only的。

(1)调用方法
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
正确返回:在父进程中返回子进程的进程号,在子进程中返回0
错误返回:-1

(2) 函数调用的用途
一个进程希望复制自身,从而父子进程能同时执行不同段的代码。

下面是一个验证的例子:

例1:fork.c

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<errno.h>

int main()
{
int a = 5;
int b = 2;
pid_t pid;
pid = fork();
if(pid == 0) {
a = a-4;
printf("I'm a child process with PID [%d],the value of a: %d,the value of b:%d.\n",pid,a,b);
}else if(pid < 0) {
perror("fork");
}else {
printf("I'm a parent process, with PID [%d], the value of a: %d, the value of b:%d.\n", pid, a, b);
}
return 0;
}

3、vfork

vfork系统调用不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。
因此,上面的例子如果改用vfork()的话,那么两次打印a,b的值是相同的,所在地址也是相同的。
但此处有一点要注意的是用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。
Vfork也是在父进程中返回子进程的进程号,在子进程中返回0。

执行时机:
用 vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec,将一个新的可执行文件载入到地址空间并执行之)或exit。
vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,
所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。

(1) 调用方法
与fork函数完全相同
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
正确返回:在父进程中返回子进程的进程号,在子进程中返回0
错误返回:-1
2. vfork函数调用的用途
用vfork创建的进程主要目的是用exec函数执行另外的程序。

下面这个例子可以验证子进程调用exec时父进程是否真的已经结束阻塞:
 
例2:execl.c
 
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdio.h>
#include<errno.h>
#include<string.h>
 
int main()
{
int a = 1;
int b = 2;
pid_t pid;
int status;
pid = vfork();
if(pid == -1) {
  perror("Fork failed to creat a process");
  exit(1);
}else if(pid == 0) {
//     sleep(3);
  if(execl("/bin/example","example",NULL)<0) {
      perror("Exec failed");
      exit(1);
  }
  exit(0);
//  }else 
//     if(pid != wait(&status)) {
//     perror("A Signal occured before the child exited");
  }else 
      printf("parent process,the value of a :%d, b:%d, addr of a: %p,b: %p\n",a,b,&a,&b);
  exit(0);
}
Example.c
#include<stdio.h>
 
int main()
{
int a = 1;
int b = 2;
sleep(3);
printf("Child process,the value of a is %d,b is %d,the address a %p,b %p\n",a,b,&a,&b);
return 0;
}
#gcc –o execl execl.c
#./ execl
运行结果:
Parent process,the value of a:1,b:2,addr of a:0xbfaa710c,b:0xbfaa7108
Child process ,The value of a is 1,b is 2,the address a 0xbfb73d90,b 0xbfb73d8c
如果将注释掉的三行加入程序的话,由于父进程wait()而阻塞,因此即使此时子进程阻塞,父进程也得不到运行,因此运行结果如下:
The value of a is 1,b is 2,the address a 0xbfb73d90,b 0xbfb73d8c
Parent process,the value of a:1,b:2,addr ofa:0xbfaa710c, b:0xbf aa7108
另外还应注意的是在它调用exec后父进程才可能调度运行,因此sleep(3)函数必须放在example程序中才能生效。

#gcc –o fork fork.c
#./fork
运行结果:
I’m a child process with PID[0],the value of a:1,the value of b:2.
I’m a parent process with PID[19824],the value of a:5,the value of b:2.
可见,子进程中将变量a 的值改为1,而父进程中则保持不变。

4、clone

系统调用fork()和vfork()是无参数的,而clone()则带有参数。

fork()是全部复制,vfork()是共享内存,而clone()是则可以将父进程资源有选择地复制给子进程,

而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的clone_flags来决定。
另外,clone()返回的是子进程的pid。

(1)调用方法
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
正确返回:返回所创建进程的PID,函数中的flags标志用于设置创建子进程时的相关选项。
错误返回:-1

关于参数

CLONE_VM标识:表示共享地址空间(变量等)
CLONE_FILES标志:表示共享文件描述符表

CLONE_VFORK标识:标识父进程会被阻塞,子进程会把父进程的地址空间锁住,直到子进程退出或执行exec时才释放该锁

SIGCHLD标识:共享信号

(2)clone()函数调用的用途
用于有选择地设置父子进程之间需共享的资源

下面来看一个例子:
 
例3:clone.c
 
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
 
int variable,fd;
int do_something() {
       variable = 42;
       printf("in child process\n");
       close(fd);
//     _exit(0);
       return 0;
}
 
int main(int argc, char *argv[]) {
       void *child_stack;
       char tempch;
       variable = 9;
       fd = open("/test.txt",O_RDONLY);
       child_stack = (void *)malloc(16384);
       printf("The variable was %d\n",variable);
       clone(do_something, child_stack+10000, CLONE_VM |CLONE_FILES,NULL);
       sleep(3);        /* 延时以便子进程完成关闭文件操作、修改变量 */
       printf("The variable is now %d\n",variable);
       if(read(fd,&tempch,1) < 1) {
              perror("File Read Error");
              exit(1);
       }
       printf("We could read from the file\n");
       return 0;
}
#gcc –o clone clone.c 
#./clone
运行结果:
the value was 9
in child process
The variable is now 42
File Read Error
 
从程序的输出结果可以看出:
子进程将文件关闭并将变量修改(调用clone时用到的CLONE_VM、CLONE_FILES标志将使得变量和文件描述符表被共享),
父进程随即就感觉到了,这就是clone的特点。由于此处没有设置标志CLONE_VFORK,因此子进程在运行时父进程也不会阻塞,两者同时运行。

5、fork,vfork,clone的区别
(1)拷贝内容

对于fork,子进程拷贝父进程的数据段和堆栈段,共享方式访问代码段,由于在Linux中采用的“写时复制”技术,也就是说,fork执行时并不真正复制用户空间的所有页面,而只是复制页面表。这样,无论父进程还是子进程,当发生用户空间的写操作时,都会引发“写复制”操作,而另行分配一块可用的用户空间,使其完全独立。这是一种提高效率的非常有效的方法。

对于vfork,共享所有的父进程资源,子进程与父进程共享内存空间, 子进程对虚拟地址空间任何数据的修改同样为父进程所见,故而是真正意义上的共享,因此对共享数据的保护必须有上层应用来保证。所以才需要等到exec(只是用另一个新程序替换了当前进程的正文,数据,堆和栈段)或子进程退出后父进程才能被调度。

对于clone,通过参clone_flags 的设置来决定哪些资源共享,哪些资源拷贝,一般只有进程的PCB和线程的系统堆栈被复制了,(也就是共享了进程的用户空间、进程打开的设备(文件描述符集),但需要依赖共享标识的参数 CLONE_VM(共享地址空间)|CLONE_FS|CLONE_FILES(共享文件描述符集)|CLONE_SIGCHLD(共享信号))。

ps:

在四项进程资源的复制中(进程资源包括进程的PCB、线程的系统堆栈、进程的用户空间、进程打开的设备(文件描述符集)),用户空间是相对庞大的,如果完全复制则效率会很低。

(2)访问次序控制
fork不对父子进程的执行次序进行任何限制,fork返回后,子进程和父进程都从调用fork函数的下一条语句开始行,
但父子进程运行顺序是不定的,它取决于内核的调度算法;

而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,
父子进程的执行次序才不再有限制;

clone中由标志CLONE_VFORK来决定子进程在执行时父进程是阻塞还是运行,若没有设置该标志,则父子进程同时运行,
设置了该标志,则父进程挂起,直到子进程结束为止。
---------------------
风云来

上一篇:linux内核学习之六 进程创建过程学习


下一篇:UNIX环境编程学习笔记(19)——进程管理之fork 函数的深入学习