探秘linux内核之fork、vfork以及clone之间的差异

探秘linux内核之fork、vfork以及clone之间的差异

前言

之所以要写这个主题的原因是因为本人学习过Unix C中一直有个疑惑是为什么父进程采用fork系统调用创建子进程后父子进程完成的先后顺序不定,而父进程采用vfork系统调用创建子进程后,则一定是子进程先于父进程完成?内核很重要,学习起来着实不易。由于Linux内核的开源化,并且博采众长,不断完善,被全世界广泛使用。本人为linux内核菜鸟,学习内核是为了更深一步了解前人的优秀成果,助长自己的C代码能力。本科就读软件学院,专业是软件工程软件开发方向,我们那一届学院的整体方向是JAVA Web开发路线,所以JAVA基础,JSP,Struts/Hibernate/Spring三大框架都完整学习了下来,当然计算机专业四大核心课程(数据结构/组成原理/操作系统/计算机网络)都会有安排。读研期间,发现这一套不管用了(不能说无用,因为未来说不定还能够用上),很多东西都忘了,研究生期间需要更多的是linux环境下的C/C++开发,原因是很多科研工作的开源代码是C/C++为主,当然JAVA也有,比如Spark等。C语言相当强大,UNIX/Linux内核源码C语言占绝大部分,汇编语言占少量。这段期间陆续学习了UNIX环境高级编程,就想着乘一波热探秘下linux内核,感兴趣的可以多多交流。探秘内核源码的同时,这次双11给自己购买了3本书籍如下:(1)毛德操老师的《linux内核源代码情景分析》,内核版本为2.4.0;(2)D. P. BOVET大牛的《深入理解linux内核》,版本2.6.11;(3)张天飞的《奔跑吧linux内核》,内核版本4.X,本人采用的是4.19.82。也不知道能不能够坚持学习下来,就与君共勉吧。

fork、vfork、clone之间的差异

直接切入主题前,需要进行下简短说明,fork/vfork/clone这三者均为系统调用,进程或线程通过这三种系统调用的某一种来创建子进程/线程,这三种调用的底层实现均为linux内核源码kernel/fork.c中的do_fork()函数来实现,它们间的唯一的差别就是传入的参数值有差异。
简短看下do_fork的函数接口:

#ifndef CONFIG_HAVE_COPY_THREAD_TLS
/* For compatibility with architectures that call do_fork directly rather than
 * using the syscall entry points below. */
long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)

可看出do_fork有5个参数:
(1)clone_flags:表示创建进程的各种标志位集合;
(2)stack_start:表示用户态栈的起始地址;
(3)stack_size:对应栈大小,通常设置为0;
(4)parent_tidptr和child_tidptr:分别指向用户态空间的两个指针,指向父进程和子进程的PID。
我们来看看clone_flags究竟有哪些标志?见linux-4.19.82\include\uapi\linux\sched.h

/*
 * cloning flags:
 */
#define CSIGNAL		0x000000ff	/* signal mask(信号掩码) to be sent at exit */
#define CLONE_VM	0x00000100	/* set if VM shared between processes(进程间共享内存) */
#define CLONE_FS	0x00000200	/* set if fs info shared between processes(进程间共享文件系统信息,比如根目录和当前工作目录) */
#define CLONE_FILES	0x00000400	/* set if open files shared between processes (进程间共享文件描述符)*/
#define CLONE_SIGHAND	0x00000800	/* set if signal handlers and blocked signals shared(共享信号处理等信息) */
#define CLONE_PTRACE	0x00002000	/* set if we want to let tracing continue on the child too(表明父进程被trace,子进程也跟着被trace) */
#define CLONE_VFORK	0x00004000	/* set if the parent wants the child to wake it up on mm_release(这个标志很重要,他是fork和vfork差异的主要原因,
表明父进程会被挂起,直到子进程释放了虚拟内存资源,这就是为什么vfork()调用顺序是一定的,子进程先于父进程完成) */
#define CLONE_PARENT	0x00008000	/* set if we want to have the same parent as the cloner (有点意思,我们一般都是父进程创建子进程,这里表明的是新进程和创建它的进程是兄弟关系!!)*/
#define CLONE_THREAD	0x00010000	/* Same thread group?(父子进程将共享相同的线程群) */
...

由上面可知,我们会有一个问题是:父进程通过系统调用创建子进程后,它们之间主要共享了哪些资源?
从源码的cloning flags中我们可以知道存在同一虚拟地址空间中的父子进程主要共享了4种资源:(1)共享内存空间;(2)共享文件系统信息如根/当前工作目录等;(3)共享文件描述符;(4)共享相同的信号处理函数。

为了说明三种系统调用的区别,需要结合它们的调用实参进行说明。参考
fork实现:
do_fork(SIGCHLD, 0, 0, NULL, NULL);
vfork实现:
do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL, NULL);
clone实现:
do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);

参数SIGCHLD是子进程终止后用于通知父进程进行回收资源的,也就是说子进程终止后会发送SIGCHLD信号给父进程,父进程通过调用waitpid()或wait()函数回收子进程资源。
从上面fork和vfork的系统具体实现可以看出,vfork比fork多了2个标志位,分别是CLONE_VFORK和CLONE_VM;其中CLONE_VFORK表明父进程创建子进程后会被挂起,直到子进程结束释放了虚拟内存资源,这就是为什么vfork()调用顺序是一定的,子进程先于父进程完成;那你一定会好奇,父进程为什么需要等待子进程呢?这是因为虚拟内存资源是父子进程共享的,这是临界资源是独占的,也就是标志位CLONE_VM的功劳;简单理解就是父进程调用了vfork()创建了子进程就是休眠等待,直到子进程结束释放共享资源唤醒父进程继续运行就好了。 而fork()系统调用会使得子进程建立一个基于父进程的完整副本,为了简便,子进程和父进程采用写时拷贝(copy-on-write,COW)机制,子进程只是拷贝了父进程的页表,并没有真正复制页表内容。只有当子进程需要写入新数据后才会采用COW为子进程创建一个副本。因此它们之间的完成先后并没有严格规定,就可能出现父进程或许先于子进程完成,或者晚于子进程完成的啦。

谈完fork和vfork的差别后,那么简单讲一下clone系统调用的功能:其功能主要是用于进程创建线程。三者区别显而易见,从clone的调用实参可以看出,需要指定新的栈地址newsp,从用户空间传入内核空间。

后记

个人感触是探秘了linux内核源码可以加深对某些结论的理解,比如为何通过fork和vfork等调用的不同,父子进程间执行的顺序会有差异化。由于本人是linux内核菜鸟,写的不到位的地方,请多多指教。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值