Fork
参照
前言
Unix 标准的复制进程的系统调用时 fork(即分叉),但是 Linux,BSD 等操作系统并不止实现这一个,确切的说 linux 实现了三个,fork,vfork,clone(确切说 vfork 创造出来的是轻量级进程,也叫线程,是共享资源的进程)
| 系统调用 | 描述 |
|---|---|
| fork | fork 创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容 task_struct 内容 |
| vfork | vfork 创建的子进程与父进程共享数据段,而且由 vfork()创建的子进程将先于父进程运行 |
| clone | Linux 上创建线程一般使用的是 pthread 库 实际上 linux 也给我们提供了创建线程的系统调用,就是 clone |
关于用户空间使用 fork, vfork 和 clone, 请参见
fork, vfork 和 clone 的系统调用的入口地址分别是 sys_fork, sys_vfork 和 sys_clone, 而他们的定义是依赖于体系结构的, 因为在用户空间和内核空间之间传递参数的方法因体系结构而异
系统调用的参数传递
系统调用的实现与 C 库不同, 普通 C 函数通过将参数的值压入到进程的栈中进行参数的传递。由于系统调用是通过中断进程从用户态到内核态的一种特殊的函数调用,没有用户态或者内核态的堆栈可以被用来在调用函数和被调函数之间进行参数传递。系统调用通过 CPU 的寄存器来进行参数传递。在进行系统调用之前,系统调用的参数被写入 CPU 的寄存器,而在实际调用系统服务例程之前,内核将 CPU 寄存器的内容拷贝到内核堆栈中,实现参数的传递。
因此不同的体系结构可能采用不同的方式或者不同的寄存器来传递参数,而上面函数的任务就是从处理器的寄存器中提取用户空间提供的信息, 并调用体系结构无关的**_do_fork(或者早期的 do_fork)函数, 负责进程的复制**
即不同的体系结构可能需要采用不同的方式或者寄存器来存储函数调用的参数, 因此 linux 在设计系统调用的时候, 将其划分成体系结构相关的层次和体系结构无关的层次, 前者复杂提取出依赖与体系结构的特定的参数, 后者则依据参数的设置执行特定的真正操作
fork, vfork, clone 系统调用的实现
关于 do_fork 和_do_frok
The commit 3033f14ab78c32687 (“clone: support passing tls argument via C rather than pt_regs magic”) introduced _do_fork() that allowed to pass @tls parameter.
linux2.5.32 以后, 添加了 TLS(Thread Local Storage)机制, clone 的标识 CLONE_SETTLS 接受一个参数来设置线程的本地存储区。sys_clone 也因此增加了一个 int 参数来传入相应的点 tls_val。sys_clone 通过 do_fork 来调用 copy_process 完成进程的复制,它调用特定的 copy_thread 和 copy_thread 把相应的系统调用参数从 pt_regs 寄存器列表中提取出来,但是会导致意外的情况。
only one code path into copy_thread can pass the CLONE_SETTLS flag, and that code path comes from sys_clone with its architecture-specific argument-passing order.
前面我们说了, 在实现函数调用的时候,我 iosys_clone 等将特定体系结构的参数从寄存器中提取出来, 然后到达 do_fork 这步的时候已经应该是体系结构无关了, 但是我们 sys_clone 需要设置的 CLONE_SETTLS 的 tls 仍然是个依赖与体系结构的参数, 这里就会出现问题。
因此linux-4.2 之后选择引入一个新的 CONFIG_HAVE_COPY_THREAD_TLS,和一个新的 COPY_THREAD_TLS 接受 TLS 参数为 额外的长整型(系统调用参数大小)的争论。改变 sys_clone 的 TLS 参数 unsigned long,并传递到 copy_thread_tls。
|
|
我们会发现,新版本的系统中 clone 的 TLS 设置标识会通过 TLS 参数传递, 因此_do_fork 替代了老版本的 do_fork。
老版本的 do_fork 只有在如下情况才会定义
- 只有当系统不支持通过 TLS 参数通过参数传递而是使用 pt_regs 寄存器列表传递时
- 未定义 CONFIG_HAVE_COPY_THREAD_TLS 宏
| 参数 | 描述 |
|---|---|
| clone_flags | 与 clone()参数 flags 相同, 用来控制进程复制过的一些属性信息, 描述你需要从父进程继承那些资源。该标志位的 4 个字节分为两部分。最低的一个字节为子进程结束时发送给父进程的信号代码,通常为 SIGCHLD;剩余的三个字节则是各种 clone 标志的组合(本文所涉及的标志含义详见下表),也就是若干个标志之间的或运算。通过 clone 标志可以有选择的对父进程的资源进行复制; |
| stack_start | 与 clone()参数 stack_start 相同, 子进程用户态堆栈的地址 |
| regs | 是一个指向了寄存器集合的指针, 其中以原始形式, 保存了调用的参数, 该参数使用的数据类型是特定体系结构的 struct pt_regs,其中按照系统调用执行时寄存器在内核栈上的存储顺序, 保存了所有的寄存器, 即指向内核态堆栈通用寄存器值的指针,通用寄存器的值是在从用户态切换到内核态时被保存到内核态堆栈中的(指向 pt_regs 结构体的指针。当系统发生系统调用,即用户进程从用户态切换到内核态时,该结构体保存通用寄存器中的值,并被存放于内核态的堆栈中) |
| stack_size | 用户状态下栈的大小, 该参数通常是不必要的, 总被设置为 0 |
| parent_tidptr | 与 clone 的 ptid 参数相同, 父进程在用户态下 pid 的地址,该参数在 CLONE_PARENT_SETTID 标志被设定时有意义 |
| child_tidptr | 与 clone 的 ctid 参数相同, 子进程在用户太下 pid 的地址,该参数在 CLONE_CHILD_SETTID 标志被设定时有意义 |
其中 clone_flags 如下表所示
sys_fork 的实现
不同体系结构下的 fork 实现 sys_fork 主要是通过标志集合区分, 在大多数体系结构上, 典型的 fork 实现方式与如下
早期实现
| 架构 | 实现 |
|---|---|
| arm | arch/arm/kernel/sys_arm.c, line 239 |
| i386 | arch/i386/kernel/process.c, line 710 |
| x86_64 | arch/x86_64/kernel/process.c, line 706 |
|
|
新版本
http://lxr.free-electrons.com/source/kernel/fork.c?v=4.5#L1785
|
|
我们可以看到唯一使用的标志是 SIGCHLD。这意味着在子进程终止后将发送信号 SIGCHLD 信号通知父进程,
由于写时复制(COW)技术, 最初父子进程的栈地址相同, 但是如果操作栈地址闭并写入数据, 则 COW 机制会为每个进程分别创建一个新的栈副本
如果 do_fork 成功, 则新建进程的 pid 作为系统调用的结果返回, 否则返回错误码
sys_vfork 的实现
早期实现
| 架构 | 实现 |
|---|---|
| arm | arch/arm/kernel/sys_arm.c, line 254 |
| i386 | arch/i386/kernel/process.c, line 737 |
| x86_64 | arch/x86_64/kernel/process.c, line 728 |
|
|
新版本
http://lxr.free-electrons.com/source/kernel/fork.c?v=4.5#L1797
|
|
可以看到 sys_vfork 的实现与 sys_fork 只是略微不同, 前者使用了额外的标志 CLONE_VFORK | CLONE_VM
sys_clone 的实现
早期实现
| 架构 | 实现 |
|---|---|
| arm | arch/arm/kernel/sys_arm.c, line 247 |
| i386 | arch/i386/kernel/process.c, line 715 |
| x86_64 | arch/x86_64/kernel/process.c, line 711 |
sys_clone 的实现方式与上述系统调用类似, 但实际差别在于 do_fork 如下调用
casmlinkage int sys_clone(struct pt_regs regs)
{
/* 注释中是i385下增加的代码, 其他体系结构无此定义
unsigned long clone_flags;
unsigned long newsp;
clone_flags = regs.ebx;
newsp = regs.ecx;*/
if (!newsp)
newsp = regs.esp;
return do_fork(clone_flags, newsp, ®s, 0);
}123456789101112
新版本
http://lxr.free-electrons.com/source/kernel/fork.c?v=4.5#L1805
|
|
我们可以看到 sys_clone 的标识不再是硬编码的, 而是通过各个寄存器参数传递到系统调用, 因而我们需要提取这些参数。
另外,clone 也不再复制进程的栈, 而是可以指定新的栈地址, 在生成线程时, 可能需要这样做, 线程可能与父进程共享地址空间, 但是线程自身的栈可能在另外一个地址空间
另外还指令了用户空间的两个指针(parent_tidptr 和 child_tidptr), 用于与线程库通信
创建子进程的流程
_do_fork 和早起 do_fork 的流程
_do_fork 和 do_fork 在进程的复制的时候并没有太大的区别, 他们就只是在进程 tls 复制的过程中实现有细微差别
所有进程复制(创建)的 fork 机制最终都调用了 kernel/fork.c 中的_do_fork(一个体系结构无关的函数),
其定义在 http://lxr.free-electrons.com/source/kernel/fork.c?v=4.2#L1679
_do_fork 以调用 copy_process 开始, 后者执行生成新的进程的实际工作, 并根据指定的标志复制父进程的数据。在子进程生成后, 内核必须执行下列收尾操作:
- 调用 copy_process 为子进程复制出一份进程信息
- 如果是 vfork(设置了 CLONE_VFORK 和 ptrace 标志)初始化完成处理信息
- 调用 wake_up_new_task 将子进程加入调度器,为之分配 CPU
- 如果是 vfork,父进程等待子进程完成 exec 替换自己的地址空间
对比,我们从《深入 linux 内核架构》中找到了早期的 do_fork 流程图,基本一致,可以用来参考学习和对比
|
|
copy_process 流程
http://lxr.free-electrons.com/source/kernel/fork.c?v=4.5#L1237
- 调用 dup_task_struct 复制当前的 task_struct
- 检查进程数是否超过限制
- 初始化自旋锁、挂起信号、CPU 定时器等
- 调用 sched_fork 初始化进程数据结构,并把进程状态设置为 TASK_RUNNING
- 复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
- 调用 copy_thread_tls 初始化子进程内核栈
- 为新进程分配并设置新的 pid
对比,我们从《深入 linux 内核架构》中找到了早期的 do_fork 流程图,基本一致,可以用来参考学习和对比
主要的区别其实就是最后的 copy_thread 更改成为 copy_thread_tls
|
|
dup_task_struct 流程
http://lxr.free-electrons.com/source/kernel/fork.c?v=4.5#L334
|
|
- 调用 alloc_task_struct_node 分配一个 task_struct 节点
- 调用 alloc_thread_info_node 分配一个 thread_info 节点,其实是分配了一个 thread_union 联合体,将栈底返回给 ti
|
|
- 最后将栈底的值 ti 赋值给新节点的栈
- 最终执行完 dup_task_struct 之后,子进程除了 tsk->stack 指针不同之外,全部都一样!
sched_fork 流程
|
|
我们可以看到 sched_fork 大致完成了两项重要工作,
- 一是将子进程状态设置为 TASK_RUNNING,
- 二是为其分配 CPU
copy_thread 和 copy_thread_tls 流程
我们可以看到 linux-4.2 之后增加了 copy_thread_tls 函数和 CONFIG_HAVE_COPY_THREAD_TLS 宏
但是如果未定义 CONFIG_HAVE_COPY_THREAD_TLS 宏默认则使用 copy_thread 同时将定义 copy_thread_tls 为 copy_thread
单独将这个函数是因为这个复制操作与其他操作都不相同, 这是一个特定于体系结构的函数,用于复制进程中特定于线程(thread-special)的数据, 重要的就是填充 task_struct->thread 的各个成员,这是一个 thread_struct 类型的结构, 其定义是依赖于体系结构的。它包含了所有寄存器(和其他信息),内核在进程之间切换时需要保存和恢复的进程的信息。
该函数用于设置子进程的执行环境,如子进程运行时各 CPU 寄存器的值、子进程的内核栈的起始地址(指向内核栈的指针通常也是保存在一个特别保留的寄存器中)
|
|
下面我们来看 32 位架构的 copy_thread_tls 函数,他与原来的 copy_thread 变动并不大, 只是多了后面 TLS 的设置信息
|
|
copy_thread_tls 这段代码为我们解释了两个相当重要的问题!
一是,为什么 fork 在子进程中返回 0,原因是 childregs->ax = 0;这段代码将子进程的 eax 赋值为 0 二是,p->thread.ip = (unsigned long) ret_from_fork;将子进程的 ip 设置为 ret_form_fork 的首地址,因此子进程是从 ret_from_fork 开始执行的
总结
fork, vfork 和 clone 的系统调用的入口地址分别是 sys_fork, sys_vfork 和 sys_clone, 而他们的定义是依赖于体系结构的, 而他们最终都调用了_do_fork(linux-4.2 之前的内核中是 do_fork),在_do_fork 中通过 copy_process 复制进程的信息,调用 wake_up_new_task 将子进程加入调度器中
fork 系统调用对应的 kernel 函数是 sys_fork,此函数简单的调用 kernel 函数_do_fork。一个简化版的_do_fork 执行如下:
- copy_process()此函数会做 fork 的大部分事情,它主要完成讲父进程的运行环境复制到新的子进程,比如信号处理、文件描述符和进程的代码数据等。
- wake_up_new_task()。计算此进程的优先级和其他调度参数,将新的进程加入到进程调度队列并设此进程为可被调度的,以后这个进程可以被进程调度模块调度执行。
简化的 copy_process()流程
- dup_task_struct()。分配一个新的进程控制块,包括新进程在 kernel 中的堆栈。新的进程控制块会复制父进程的进程控制块,但是因为每个进程都有一个 kernel 堆栈,新进程的堆栈将被设置成新分配的堆栈。
- 初始化一些新进程的统计信息,如此进程的运行时间
- copy_semundo()复制父进程的 semaphore undo_list 到子进程。
- copy_files()、copy_fs()。复制父进程文件系统相关的环境到子进程
- copy_sighand()、copy_signal()。复制父进程信号处理相关的环境到子进程。
- copy_mm()。复制父进程内存管理相关的环境到子进程,包括页表、地址空间和代码数据。
- copy_thread()/copy_thread_tls。设置子进程的执行环境,如子进程运行时各 CPU 寄存器的值、子进程的 kernel 栈的起始地址。
- sched_fork()。设置子进程调度相关的参数,即子进程的运行 CPU、初始时间片长度和静态优先级等。
- 将子进程加入到全局的进程队列中
- 设置子进程的进程组 ID 和对话期 ID 等。
简单的说,copy_process()就是将父进程的运行环境复制到子进程并对某些子进程特定的环境做相应的调整。
此外应用程序使用系统调用 exit()来结束一个进程,此系统调用接受一个退出原因代码,父进程可以使用 wait()系统调用来获取此代码,从而知道子进程退出的原因。对应到 kernel,此系统调用 sys_exit_group(),它的基本流程如下:
- 将信号 SIGKILL 加入到其他线程的信号队列中,并唤醒这些线程。
- 此线程执行 do_exit()来退出。
do_exit()完成线程退出的任务,其主要功能是将线程占用的系统资源释放,do_exit()的基本流程如下: \1. 将进程内存管理相关的资源释放
- 将进程 ICP semaphore 相关资源释放
- **exit_files()、**exit_fs()。将进程文件管理相关的资源释放。
- exit_thread()。只要目的是释放平台相关的一些资源。
- exit_notify()。在 Linux 中进程退出时要将其退出的原因告诉父进程,父进程调用 wait()系统调用后会在一个等待队列上睡眠。
- schedule()。调用进程调度器,因为此进程已经退出,切换到其他进程。
进程的创建到执行过程如下图所示
-
No backlinks found.