fork原理--Linux实现

fork的一些特征是怎么实现的?
比如:
1. 为什么父进程返回子进程pid, 子进程返回0?
2. 子进程是如何做到与父进程“一模一样的”?
3. 子进程同父进程一样,都是从调用fork处继续向下执行,而不是子进程从头执行?

下面直接看源码来分析。
注:如果不额外说明,为了阅读方便,以下展示源码都已删减。

从父进程调用fork开始; 就不写程序了。
大家都知道,这是一个系统调用,当父进程调用fork()时,大体过程如下:
fork() -> int 0x80 -> 调用system_call这个汇编函数 --> 调用其对应的内核函数sys_fork();
(详细的系统调用过程可以去另一篇文章了解一下)
为什么要列出这个过程呢,因为这个过程反映了压栈的顺序,这里先不详述,下面会用到这个调用过程。

好了,现在我们知道,最终是调用了sys_fork, 看一下它的实现,这是一个汇编函数:
fork原理--Linux实现
kernel/syscall.s

其实sys_fork的实现就分两个步骤(两个C函数):

  1. find_empty_process
  2. copy_process

其实从名字上也能看出来这两个函数要干嘛了,我们来详细看一下:
find_empty_process:
fork原理--Linux实现
kernel/fork.c

Linux把所有的进程(的指针)放在task这个数组里,所以只需要一个下标就可以表示一个进程了,find_empty_process就是在task数组中找一个空位置,返回这个位置的下标(作为子进程的pid),失败返回负数。
注意:C函数的返回值存放在eax寄存器中,也就是说find_empty_process调用完成后,返回值(子进程pid或负数)放在eax中,所以sys_fork那个汇编函数中
在call find_empty_process后,
通过testl eax, eax 判断find_empty_process的返回值(eax)是否为负数,是负数则返回(没有找到空闲位置,子进程没地方放,自然没必要进行下面的复制操作)。
如果find_empty_process找到了一个空闲位置(子进程pid),则进行压栈操作,调用copy_process进行复制操作(这是fork函数的核心)。
fork原理--Linux实现
kernel/fork.c

首先看参数,这辈子头一次见这么长的参数列表,但是这些参数是什么时候赋值或者说压栈的呢?
这些参数实际上包含了从中断开始所有的相关压栈操作,也就是说从中断(int 0x80发生那一该)就已经在为这个函数的参数作准备了。

文章开始的调用顺序应该没忘了,就是它们为copy_process一个个压入参数。
从int 0x80: 发生中断自动压栈:ip, cs, eflags, sp, ss
然后在system_call:
在这里插入图片描述fork原理--Linux实现
kernel/syscall.s

一直到sys_fork在call coyp_process之前的几个push, 正好和copy_process的参数是一一对应的。

我们这里只关注重要的参数:nr, ip.
nr是第一个参数,肯定是call copy_process之前最后一条压栈指令压入的(参数从右向左压栈),对应push eax, 也就是find_empty_process的返回值(task数组的空闲位置)。
ip是发生中断时压入的,int 0x80的下一条指令的地址,也就是中断返回后的地址。
这里不要认为是中断处理程序的地址。
fork原理--Linux实现
这里压入的是instructor1, 而不是system_call, 所以下面把这个ip赋值给子进程ip后,子进程再执行,就是从instrutor1位置(可以认为是fork()后一条指令了,因为fork()几乎 就触发了一条int 0x80指令)
fork原理--Linux实现
int 0x80压入的就是fork()后一条指令地址(这就是为什么子进程会和父进程一样从fork()之后继续执行)。

我们进入copy_process的函数体,看它是如何复制进程的。

fork原理--Linux实现
上面代码再粘一遍。

123行申请一页内存,这是为子进程的pcb申请的空间;
125行,task[nr] = p, 直接把子进程放入调度队列,子进程信息还没有构建,直接调度不会有问题吗?
当然不会,进入中断时会关闭中断,没有时钟中断,当然不会引起调度,可以放心向下执行下面的操作。
127行,*p = *current, p是指向子进程的pcb, current是指向当前进程(也就是父进程:调用fork的进程)的pcb, 这句是直接把父进程的pcb复制给子进程。
然后下面的大部分代码都是对子进程pcb进行修改的(父子进程pcb不可能完全一样,pid, 栈等肯定不一样)。
129行设置子进程pid
130行设置子进程的内核栈,指向pcb页末。(进程初始化都会这么设置)
fork原理--Linux实现
然后设置ip。
134行设置 eax值为0, 前边讲汇编函数时讲到了函数返回值是放在eax中的,现在把子进程eax的值置为0, 将来eax被调度,取出eax后就是子进程fork的返回值(0)。

138行调用copy_mem主要是复制页表(此版本所有进程共享页目录表,所以只需要复制页表)。
fork原理--Linux实现
71行设置ldt[1], 这是代码段(子进程的)。
72行设置ldt[2], 这是数据段。
然后调用copy_page_table()复制页表。由于本文不是讲解内存管理,所以对这个函数不做详细介绍。
这里只需要知道copy_mem只复制了页表!!!
然后copy_process最后也只是更新了打开文件数(全都+1),设置tss,ldt,运行状态。就结束了。也就是说整个fork结束了,那么只复制了页表,父子进程不会相互影响吗?答案是肯定的。
fork只需要页表,而不复制所有物理内存页面,父子进程共享!!!。
fork原理--Linux实现
copy_page_table非常复杂,只需要关注一下177行,this_page &= ~2, 这行的意思是把父进程的物理内存置为只读(此时父子进程共享),那么什么时候真正进行复制这些物理内存页面呢?
写的时候。是的,这就是LInux的写时复制机制。(见文章

本文主要是笔记加入个人理解,如有错误,还望指正。


参考:《Linux 内核完全剖析》