Linux 系统调用 —— fork 内核源码剖析
系统调用流程简述
fork() 函数是系统调用对应的 API,这个系统调用会触发一个int 0x80 的中断;
当用户态进程调用 fork() 时,先将 eax(寄存器) 的值置为 2(即 __NR_fork 系统调用号);
执行 int $0x80,cpu 进入内核态;
执行 SAVE_ALL,保存所有寄存器到当前进程内核栈中;

进入 sys_call,将 eax 的值压栈,根据系统调用号查找 system_call_table ,调用对应的函数;
函数返回,执行 RESTORE_ALL,恢复保存的寄存器;执行 iret,cpu 切换至用户态;

从 eax 中取出返回值,fork() 返回;
详见:系统调用的工作机制
fork 在内核中做了什么
当我们调用 fork()、clone()、vfork() 时,实际上在内核中调用的都是同一个函数 —— do_fork()
这里的三个系统调用的区别就在于调用 do_fork() 时传入的参数不同
do_fork() 中第一个参数 clone_flags 是一个 32bit 的标志,其中不同的 bit 置 1 代表不同的选项,表示新的子进程与父进程之间共享哪些资源
其中 sys_fork() 调用 do_fork() 只设置了 SIGCHLD 选项,sys_vfork() 设置了 CLONE_VM | CLONE_VFORK | SIGCHLD 选项,而 sys_clone() 的参数来自上层,通过 ebx 传入;
下面简述下 do_fork() 的执行过程
do_fork()
- 查找 pidmap_array 位图,为子进程分配新的 pid;
- 调用 copy_process() ,将新的 pid 传入参数,这个函数是创建进程的关键步骤,该函数返回新的 task_struct 地址;
copy_process()
- 创建 task_struct 结构体指针;
- 检查参数;
- 调用 dup_task_struct() ,将父进程 task_struct 传入参数,为子进程获取进程描述符;
dup_task_struct()
- 创建 task_struct 、thread_info 结构体指针;
struct task_struct *tsk;
struct thread_info *ti;
- 调用 alloc_task_struct() 宏为新进程获取进程描述符,并保存至 tsk 中;
tsk = alloc_task_struct();
if (!tsk)
return NULL;
- 调用 alloc_thread_info() 宏获取一块空闲内存区,保存在 ti 中(这块内存的大小为 8K/4k,用来存放新进程的 thread_info 结构体和内核栈)
struct task_struct
{
struct thread_info * thread_info; // 指向 thread_info 的指针
struct mm_struct * mm; // 进程地址空间
pid_t pid;
struct list_head children; // 子进程链表
...
}
struct thread_info
{
struct task_struct task; // 指向 task_struct 的指针
_u32 cpu; // 当前所在的cpu
mm_segment_t addr_limit; // 线程地址空间
// user_thread 0-0xBFFFFFFF
// kernel_thread 0-0xFFFFFFFF
...
}
// thread_info 和 stack 共享一块内存
union thread_union
{
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
- 复制进程描述符和 thread_info,并将两者中的互指指针初始化;
*ti = *current->thread_info;
ti->task = tsk;
- 将新进程描述符的使用计数器 usage 置为2,表示描述符正在被使用而其对应的进程处于活动状态;
新进程的进程描述符创建完成,返回至 copy_process()
- 检查当前用户所拥有的进程数是否超过了限制的值(1024),有root权限除外;若超过了限制则返回错误码,否则增加用户拥有的进程计数;
atomic_inc(p->user->process);
检查系统中的进程数量是否超过了 max_threads;
max_threads的数量由系统内存容量决定,所有的thread_info描述符和内核栈所占用空间不能超过系统内存的1/8;拷贝所有的进程信息:
- 其中最重要的是 copy_mm() ,该函数通过建立新进程所有页表和内存描述符来创建进程地址空间;
struct mm_struct
{
struct vm_area_struct * mmap; // 指向线性区对象的链表头
struct rb_root mm_rb; // 指向线性区对象的红黑树的根
pgd_t * pgd; // 指向页全局目录
atomic_t mm_users; // 次使用计数器,存放共享 mm_struct 数据结构轻量级进程的个数
atomic_t mm_count; // 主使用计数器,每当 mm_count 递减,内核就要检查它是否为0,如果是就要解除这个内存描述符
}
copy_mm()
- 创建 mm_struct * mm, oldmm 结构体指针(内存描述符);
oldmm = current->mm; //oldmm 初始化为父进程的 mm_struct
- 检查 clone_flags 是否设置了 CLONE_VM 位;
若设置了 CLONE_VM 位,则表示创建线程,与父进程共享地址空间
atomic_inc(&oldmm->mm_users); // 父进程的地址空间引用计数加一
mm = oldmm; // 将父进程地址空间赋给子进程
- 否则,就要创建新的地址空间,并从当前进程复制 mm 的内容
mm = allocate_mm();
memcpy(mm, oldmm, sizeof(*mm));
- 调用 dup_mmap() 复制父进程的线性区和页表
dup_mmap()
- 复制父进程每个 vm_area_struct 线性区描述符,插入到子进程的线性区链表和红黑树中;
struct vm_area_struct
{
struct mm_struct * vm_mm; // 指向线性区所在的内存描述符
unsigned long vm_start; // 当前线性区起始地址
unsigned long vm_end; // 线性区尾地址
struct vm_area_struct * vm_next; // 下一个线性区
pgprot_t vm_page_prot; // 线性区访问权限
struct rb_node vm_rb; // 用于红黑树搜索的节点
}
- 用 copy_page_range() 创建新的页表,在新的 vm_area_struct 中链接并复制父进程的页表条目;
copy_page_range()
- 创建新的页表;
- 复制父进程的页表来初始化子进程的新页表;
私有/可写的页( VM_SHARED 标志关闭/ VM_MAYWRITE 标志打开)所对应的权限父子进程都设为只读,以便于 Copy-on-write 机制处理。
新进程的线性区和页表复制完成,返回至copy_process()
- 调用 copy_thread() 用父进程的内核栈初始化子进程的内核栈
copy_thread()
- 将eax的值强制设为0(fork / clone 系统调用的返回值)
childregs->eax = 0;
sched_fork()
- 调用 sched_fork() 完成对新进程调度程序数据结构的初始化,将新进程状态设为 TASK_RUNNING
- 为了公平起见,父子进程共享父进程的时间片
进程创建完成,返回至 do_fork()
- 如果设置 CLONE_STOPPED,就将子进程设置 TASK_STOPPED 状态并挂起;
否则调用 wake_up_new_task() 调整父子进程的调度参数;
wake_up_new_task()
- 如果父子进程运行在同一个 cpu 上,并且不能共享同一组页表 (CLONE_VM 位为 0),就把子进程插入运行队列中的父进程之前;
如果子进程创建之后调用 exec 执行新程序,就可以避免写时拷贝机制执行不必要的页面复制;
否则,如果运行在不同的cpu上,或父子共享同一组页表,就将子进程插入运行队列的队尾。
返回至 do_fork()
- 返回子进程的 pid
2017/8/3 补充
fork() 和 vfork() 参数是写死的,而 clone() 是可选的,它可以选择当前创建的进程哪些部分是共享的,哪些部分是独立的;
vfork() 是历史的产物,当调用 fork() 的时候,需要将父进程的线性区和页表都拷贝一份,而调用 exec() 执行新程序后,又要把所有页表删除重置新的页表,建立映射关系,效率很低;
所以要有 vfork(),vfork() 的 clone_flags 位置了 CLONE_VM ,表示共享父进程的地址空间,vfork() 中创建的进程没有分配自己的地址空间,而是通过一个 mm_struct 指针指向父进程的地址空间,这个进程是为了在之后调用 exec() 执行新的程序;
而在有了 Copy-on-write 技术后,fork() 出的子进程只创建了自己的地址空间,然后用父进程的地址空间初始化,每个页表的项置为父进程的页表项,共享父进程的物理页面,并将所有 私有/可写 页面改为只读;
当我们改变父子进程的数据后,cpu 在运行过程中会发生一个缺页错误,cpu 转交控制权给操作系统,操作系统查找 VMA 发现该页权限为只读,但所在段又是可写的,产生一个矛盾,这就是识别 Copy-on-write 的方法,接着 OS 给子进程分配一个新的物理页,并将页表该页的地址修改成新的物理页地址;
这样 fork() 后再调用 exec() 就不用那么麻烦了,可以直接将新的物理页与子进程的虚拟空间建立映射
小结
综上,fork 在创建子进程时,主要做了这些工作
- 为子进程分配新的 pid,并通过父进程 PCB(task_struct)创建新的子进程 PCB
- 检查进程数是否达到上限(分别检查用户限制和系统限制)
- 拷贝所有的进程信息(打开的文件 / 信号处理 / 进程地址空间等),这里需要拷贝的选项由调用 do_fork() 时传入的参数 clone_flags 决定
- 用父进程的内核栈初始化子进程的内核栈,设置子进程的返回值为 0(eax = 0)
- 设置新进程的状态(TASK_RUNNING / TASK_STOPPED),调整父子进程调度
- 父进程 fork 返回子进程的 pid
Linux 系统调用 —— fork 内核源码剖析的更多相关文章
- 《Unix内核源码剖析》
<Unix内核源码剖析> 基本信息 作者: (日)青柳隆宏 译者: 殷中翔 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787115345219 上架时间:2014-2 ...
- Linux内核分析(一)---linux体系简介|内核源码简介|内核配置编译安装
原文:Linux内核分析(一)---linux体系简介|内核源码简介|内核配置编译安装 Linux内核分析(一) 从本篇博文开始我将对linux内核进行学习和分析,整个过程必将十分艰辛,但我会坚持到底 ...
- linux0.11内核源码剖析:第一篇 内存管理、memory.c【转】
转自:http://www.cnblogs.com/v-July-v/archive/2011/01/06/1983695.html linux0.11内核源码剖析第一篇:memory.c July ...
- (升级版)Spark从入门到精通(Scala编程、案例实战、高级特性、Spark内核源码剖析、Hadoop高端)
本课程主要讲解目前大数据领域最热门.最火爆.最有前景的技术——Spark.在本课程中,会从浅入深,基于大量案例实战,深度剖析和讲解Spark,并且会包含完全从企业真实复杂业务需求中抽取出的案例实战.课 ...
- ARM Linux内核源码剖析索引
start_kernel -->asm-offset.h 生成 -->proc_info_list -->machine_desc -->__vet_atags --> ...
- UNIX V6内核源码剖析——进程
进程的概念 1. 什么是进程 2. 进程的并行执行 3. 进程的运行状态 4. 用户模式和内核模式 cpu具有2种模式——内核模式和用户模式,通过PSW来切换. 切换时, 映射到虚拟地址的物理内存区域 ...
- UNIX V6内核源码剖析——unix v6 全貌
1. UNIX V6 运行硬件环境——PDP-11/40 PDP-11/40指令和数据都是以16比特为单位.对它而言,一个字的宽度为16比特. PDP-11/40以及周边设备的寄存器被映射到内存最高位 ...
- 转载 :Linux有问必答:如何在Debian或Ubuntu上安装完整的内核源码
http://linux.cn/article-5015-1.html 问题:我需要为我的Debian或Ubuntu下载并安装完整树结构的内核源码以供编译一个定制的内核.那么在Debian或Ubunt ...
- 源码剖析Linux epoll实现机制及Linux上惊群
转载:https://blog.csdn.net/tgxallen/article/details/78086360 看源码是对一个技术认识最直接且最有效的方式了,之前用Linux Epoll做过一个 ...
随机推荐
- go map数据结构和源码详解
目录 1. 前言 2. go map的数据结构 2.1 核心结体体 2.2 数据结构图 3. go map的常用操作 3.1 创建 3.2 插入或更新 3.3 删除 3.4 查找 3.5 range迭 ...
- PHP 类中使用全局变量和全局常量
<?php $global_var = "var"; define('global_const', 'const'); class Test { public $_var; ...
- List、Set集合系列之剖析HashSet存储原理(HashMap底层)
目录 List接口 1.1 List接口介绍 1.2 List接口中常用方法 List的子类 2.1 ArrayList集合 2.2 LinkedList集合 Set接口 3.1 Set接口介绍 Se ...
- TypeError: expected string or bytes-like object
在写Python代码的时候,遇到了"TypeError: a bytes-like object is required, not 'str'"错误,此处实验机器的Python环境 ...
- Java抽象类、接口、内部类
抽象类的概念: 1.Java中可以定义没有方法体的方法,还方法的具体实现由子类完成,该方法称为抽象方法,包含抽象方法的类就是抽象类: 2.如,shape类计算周长和面积的方法无法确定,那么就可以将这样 ...
- NOIP模拟34
考试的时候被T2卡了一年....考虑了一下正解的式子,然后没去给左边分解因数,去给后面乘倍数...理论复杂度O(n^2),实际好像卡不掉的样子.但是由于我智障的打了一棵主席树,他M了.... 预计得分 ...
- 爬虫学习--常用的正则表达式 Day3
在做爬虫经常遇到需要用正则校验数据时候,往往是在网上去找很久,结果找来的还是不很符合要求.所以我最近把开发中常用的一些正则表达式整理了一下,给自己留个底,也给朋友们做个参考. 一.校验数字的表达式 1 ...
- 14 Zabbix4.4.0系统实现监控checkpoint设备
点击返回:自学Zabbix之路 点击返回:自学Zabbix4.0之路 点击返回:自学zabbix集锦 14 Zabbix4.4.0系统实现监控checkpoint设备 1. 前期规划信息 2. 配置 ...
- CSP-S 95 (sb lsc yy赛)
sb lsc 终于改完题了!(心力交悴.png)
- 『题解』洛谷P3384 【模板】树链剖分
Problem Portal Portal1: Luogu Description 如题,已知一棵包含\(N\)个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作: 操作\(1\): ...