这篇笔记详细分析 fork 系统调用的流程:复制父进程以创建子进程。
fork 创建进程 1
// main
if (!fork()) { /* we count on this going ok */
init(); // 在新建的子进程(任务1)中执行。
}
for(;;) pause(); // 任务0 fork 回来到这里
系统调用流程
fork 函数的调用流程:
- 系统调用都是
_syscall{0,1,2,3}
的封装 _syscall
宏把参数放到寄存器,发 0x80 中断陷入内核- 0x80 会
- 0x80 中断的响应程序是
_system_call
_system_call
从 eax 拿系统调用号,查表调用对应的系统调用函数sys_xxx
(参数压到栈)- 这里
fork
最终的系统调用的处理函数是sys_fork
find_empty_proces
find_empty_process
:找空闲进程槽位(task[]
)-> 获取 pid
- 确定 pid:自增进程 id
[0,-1)
last_pid
:开机以来历史累计的进程数
- 确定任务号:
task[]
中空闲项的索引[0, 64)
task
中是正在跑 or 将要跑 or 可以在满足条件后跑的进程
- 返回任务号 ->
%eax
copy_process
copy_process
:父建子
参数来自运行过程中累积的压栈:int 0x80
-> system_call
-> sys_fork
。
- int 0x80 自动把 5 个寄存器值压到内核栈(非用户栈)
这个函数过程略复杂,它主要的工作是,为新进程创建必要的东西,并从父进程复制内容去填充:
新建 task_struct
get_free_page
获取新页作为task_union
,其中前面的task_struct
放到task
数组中:
注意:mem_map
和页表作用不同:
mem_map
记账:哪里的哪个页给出去了,给了几个人- 页表:线性页、物理页的映射关系
复制 task_struct
*p = *current
复制父进程的整个task_struct
。然后再个性化修改p->xxx = xxx
。
copy_mem
-
copy_mem
复制父进程的段、页给子进程:- 设置 LDT:code、data
基址 = task号 * 64M
限长 = 640K
- 复制页表:
copy_page_tables
- 新页目录项 -> 新页表 (管 4MB)
- 新页表 -> 从父页表逐项复制页表项
- 特殊情况:进程 0 新建进程 1:只复制前 160 项
- else:复制全部 1024 项
- 子(新的:to)与父(旧的:from)的页表指向相同页面(共享物理:me m_map 引用计数++)
- 新老页表均置只读(for cow)
- 设置 LDT:code、data
理一下页和段的关系,以及 copy_page_tables
到底发生了什么:
- 页目录项(
pg_dir[i]
)中有指向页表的指针,而页表可以在任意位置- 随便拿一个空闲页,写到
pg_dir
里登记上,就当上页表了。 - 不是整个系统只有 4 个页表(head.s 里建的 pg0~4):那四个是 kernel (and task 0)有 4 个直接映射到物理内存的(4 * 4MB = 16 MB)
- 注意 task 0 只拥有第一个页目录项,即第一个页表 pg0 的前 160 项(
160 * 4KB == 640 KB
)
- 随便拿一个空闲页,写到
- 一个 task 槽位对应
pg_dir
中的 16 个页目录项- 即一个进程最多可以有 16 个页表
- 一个页表 1024 项,一个页 4KB
- 所以一个进程最多
16 * 1024 * 4KB = 64 MB
- 段是在线性空间(4GB)上的
- 一个进程的 task 号刚好对应该进程的段基址:
task号 * 64M
- 一个进程 64 MB <=> 16个页表 <=> 16个页表项,瓜分的也是线性空间(4GB)
- 一个进程的 task 号刚好对应该进程的段基址:
- 复制时的 size 参数,传的是
data_limit=get_limit(0x17)
这个值,拿到里面做size=(size+0x3fffff)) >> 22
算出有父进程有几个页目录项,即有几张页表。- 当前:进程 0 自己也只拥有第 1 个页目录项(pg0)及其对应页表中的前 160 项(160 页 == 640 KB):这人段限长就 640 KB,超出去也不是他的了。
- 当前 task 0 段限长 640 KB,拿到里面
size=(640*1024+0x3fffff)) >> 22
,就是 1,即只复制第一个页目录项。 - 所以 0 建 1 的时候还特殊处理了只复制表中前 160 项;以后再新建进程都是复制整个表的全部 1024 项。
- 上图跟物理地址没关系。线性和物理的映射是另一回事。
- 瓜分线性空间是页表的表现
- 线性 => 物理是页表的功能
- 任意线性可以关联到任意物理
其他事项
-
共享文件:
- 父打开的文件:
f_count++
- pwd、root、executable:
i_count++
- 父打开的文件:
-
在 GDT 中设置新的 task 1:
- 新 TSS
- 新 LDT
-
新进程 -> 就绪态,返回
last_pid
作为新(子)进程的 PID。
(然后就一路返回 copy_process
-> _sys_fork
-> _system_call
-> fork
-> main
, fork 就结束了)
after fork()
- task 0 死循环 pause:怠速运行:[[5.after-fork-0-schedule]]
- task 1 跑去做 init:[[6.after-fork-1-init]]