首页 服务器系统 Linux

「技术干货」fork,vfork,clone,Linux系统调用

前言:fork,vfork,clone都是linux系统调用,这三个函数分别调用sys_fork,sys_vfork,sys_clone,最终都会调用到do_fork函数。差别就在于参数的传递和一些准备工作的不同

一,进程的四要素

linux进程所必须的四个要素:

  • 程序代码,有一段程序供其执行: 代码不一定是进程专有,可以与其它进程共享
  • 有自己的专用系统堆栈空间:
  • 有进程控制块(task_struct):
  • 有独立的存储空间:

以上4条,缺一不可。如果缺少第四条,那么就称其为"线程"。如果完全没有用户空间,称其为”内核线程“;如果共享用户空间,则称其为”用户线程"。

二,fork

系统调用fork,允许父进程创建一个新的进程(子进程)。新的子进程是父进程的翻版:完全继承父进程的栈、数据段、堆和执行文本的拷贝。其接口如下:

NAME
       fork - create a child process

SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>

       pid_t fork(void);

完成对其调用后将存在两个进程,且每个进程都会从fork的返回处继续执行。这两个进程将执行相同的程序文本段,但却各自拥有不同的栈段、数据段以及堆段拷贝。子进程的栈、数据以及栈段开始时是对父进程内存相应各部分的完全复制。执行fork之后,每个进程均可修改各自的栈数据以及堆中的变量而不影响另一进程。

为调用进程创建一个一模一样的新进程,但父子进程需要改变时候,执行一个copy,但是任何修改都造成分裂,如:chroot, open, 写memory,mmap,sigaction….

更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取。

fork的示例

考虑以下代码的输出,假设test.txt中的内容”abcdefghijklmnopqrst…”

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include<fcntl.h>

int main(void)
{
        char str[10];
        int count = 1;
        int fd = open("test.txt", O_RDWR);
        if(fork() == 0)
        {
                int cnt = read(fd, str, 10);
                printf("Child process : %s\n", (char *)str);
                printf("This is son, his count is: %d (%p). and his pid is: %d\n", ++count, &count, getpid());
        }
        else
        {
                int cnt = read(fd, str, 10);
                printf("Child process : %s\n", (char *)str);
                printf("This is father, his count is: %d (%p), his pid is: %d\n", count, &count, getpid());
        }

    return 0;
}

输出为:

  • 从结果来看,子进程和父进程的PID不同,内存资源count是值的复制,子进程改变了count的值,而父进程中的count的值没有改变,这个过程请参考之前章节的写时复制技术。
  • 两个进程共享了同一个指向文件的结构体,所以当子进程输出“abcdefghij”后,父进程就接着输出"klmnopqrst"

三,vfork

vfork也是创建子进程,但是子进程共享父进程的空间。在vfork创建子进程之后,父进程阻塞,直到子进程执行exec()或者exit()。vfork设计的最初是因为fork没有实现COW机制,很多情况下fork之后会紧跟着exec,而exec的执行相当于前面fork复制的空间全部变得无用,所以设计了vfork。而现在fork使用了COW,唯一的代价仅仅是复制父进程页表的代价,所以vfork的功能就变得越来越不重要。

NAME
       vfork - create a child process and block parent
SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>
       pid_t vfork(void);

vfork因为如下两个特性而更具效率,也是区别与fork所在:

无需为子进程复制虚拟内存页或页表,相反,子进程共享父进程的内存,直至其成功执行exec或调用exit退出

在子进程调用exit或exec之前,将暂停执行父进程,所以在使用vfork时,一般立即在vfork之后调用exec,如果exec调用失败,子进程应调用exit退出。

vfork示例:

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


int main(void)
{
	int count =1;
	int child;

	printf("Before create son, the father's count is %d\n",count);
	if(!(child = vfork()))
	{
		int i = 0;
		for( i = 0; i< 3; i++)
		{
			count++;
			printf("This is son This i is: %d count: %d\n", i, count);
			if(i == 2)
			{
				printf("This is son This pid is: %d count: %d\n", getpid(), count);	
				exit(1);
			}
		}
	}
	else
	{
		printf("This is father This pid is: %d count: %d\n", getpid(), count); 
	}
    return 0;
}

输出:

用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程

子进程在vfork()返回后直接运行在父进程的栈空间,并使用父进程的内存和数据。这意味着子进程可能破坏父进程的数据结构或栈,造成失败。为了避免这些问题,需要确保一旦调用vfork(),子进程就不从当前的栈框架中返回,并且如果子进程改变了父进程的数据结构就不能调用exit函数。子进程还必须避免改变全局数据结构或全局变量中的任何信息,因为这些改变都有可能使父进程不能继续。

值得注意的是用vfork创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,父进程就讲一直阻塞,出现异常

大家可以实际将上述例子中exit(1)这个注释掉后,会出现什么情况。对于Vfork和fork是类似的,除了下面两点:

  • 1、阻塞父进程
  • 2、不复制父进程的页表

之所以vfork要阻塞父进程是因为vfork后父子进程使用的是完全相同的mm_struct,也就是由完全相同的虚拟地址空间,包括栈也相同,所以两个进程就不能同时运行,否则栈就会乱掉。所以vfork后,父进程是阻塞的,直到调用了exec系列或者exit后,这个时候,子进程的mm需要释放,不再与父进程公用,这个时候就可以解除父进程的阻塞状态。

四,clone

clone是Linux为创建线程设计的,所以可以说clone是fork的升级版本,不仅可以创建进程或线程,还可以指定创建新的命名空间,有选择的继承父进程的内存、甚至可以将创建出来的进程编程父进程的兄弟进程等。

clone函数功能强大,待有很多参数,提供了一个非诚灵活自由的常见进程的方法,因此它创建进程要比前面两种方法更为复杂。clone可以有选择继承父进程的资源,你可以选择像vfork一样和父进程共享一个虚拟存储空间,也可以不和父进程共享,甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。

NAME
       clone, __clone2 - create a child process

SYNOPSIS
       /* Prototype for the glibc wrapper function */

       #define _GNU_SOURCE
       #include <sched.h>

       int clone(int (*fn)(void *), void *child_stack,
                 int flags, void *arg, ...
                 /* pid_t *ptid, void *newtls, pid_t *ctid */ );

fn为函数指针

此指针指向一个函数体,即想要创建进程的静态程序

child_stack

为给子进程分配系统堆栈的指针

arg

传给子进程的参数一般为(0)

flags

要复制资源的标志,描述你需要从父进程继承那些资源(是资源复制还是共享,在这里设置参数)

下面是flaga可以取得值:

  • CLONE_PARENT 建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”
  • CLONE_FS 子进程与父进程共享相同的文件系统,包括root、当前目录、umask
  • CLONE_FILES 子进程与父进程共享相同的文件描述符(file descriptor)表
  • CLONE_NEWNS 在新的namespace启动子进程,namespace描述了进程的文件hierarchy
  • CLONE_SIGHAND 子进程与父进程共享相同的信号处理(signal handler)表
  • CLONE_PTRACE 若父进程被trace,子进程也被trace
  • CLONE_VFORK 父进程被挂起,直至子进程释放虚拟内存资源
  • CLONE_VM 子进程与父进程运行于相同的内存空间
  • CLONE_PID 子进程在创建时PID与父进程一致
  • CLONE_THREAD Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群

#define _GNU_SOURCE
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sched.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
#define FIBER_STACK 8192

int a;
void *stack;

int do_something()
{
	a = 10;
	printf("This is son, the pid is: %d, the a is: %d\n",getpid(), a);
	free(stack);
	exit(1);
}

int main(void)
{
	void *stack;
	a = 1;
	stack = malloc(FIBER_STACK);
	if(!stack)
	{
		printf("The stack failed\n");
		exit(0);
	}

	printf("Create son thread \n");
	clone(&do_something, (char *)stack + FIBER_STACK, CLONE_VM | CLONE_VFORK, 0);
	printf("This is father, the pid is: %d, the a is: %d\n",getpid(), a);
    return 0;
}

输出结果:

inux创建线程的API,本质上去调 clone。要求把P2的所有资源的指针,都指向P1。线程,也被称为 Light weight process。而Linux在clone线程时也十分灵活,可以选择共享/不共享部分资源。

POSIX标准要求,进程里面如果有多个线程,在用户空间 getpid() 看到的都是同一个id,这个id其实是TGID。一个进程里面创建了多个线程,在/proc 下 的是 tgid,/proc/tgid/task/{pidx,y,z}pthread_self() 看到的是用户空间pthread线程库里获得的id 。

五,总结

下面是三个接口的优缺点对比:

相关推荐