Linux 0.11 进程 0: fork 创建进程 1

1,101 阅读4分钟

main-fork.png

这篇笔记详细分析 fork 系统调用的流程:复制父进程以创建子进程。

fork 创建进程 1

// main
if (!fork()) {   /* we count on this going ok */
    init();      // 在新建的子进程(任务1)中执行。
}
for(;;) pause(); // 任务0 fork 回来到这里

系统调用流程

fork 函数的调用流程:

fork-inside.png

  • 系统调用都是 _syscall{0,1,2,3} 的封装
  • _syscall 宏把参数放到寄存器,发 0x80 中断陷入内核
    • 0x80 会
  • 0x80 中断的响应程序是 _system_call
  • _system_call 从 eax 拿系统调用号,查表调用对应的系统调用函数 sys_xxx(参数压到栈)
  • 这里 fork 最终的系统调用的处理函数是 sys_fork

sys_fork.png

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:父建子

copy_process.png

参数来自运行过程中累积的压栈:int 0x80 -> system_call -> sys_fork

  • int 0x80 自动把 5 个寄存器值压到内核栈(非用户栈)

这个函数过程略复杂,它主要的工作是,为新进程创建必要的东西,并从父进程复制内容去填充:

copy_process_flow.png

新建 task_struct

  1. get_free_page 获取新页作为 task_union,其中前面的 task_struct 放到 task 数组中:

get_free_page.png


注意:mem_map 和页表作用不同:

  • mem_map 记账:哪里的哪个页给出去了,给了几个人
  • 页表:线性页、物理页的映射关系

复制 task_struct

  1. *p = *current 复制父进程的整个 task_struct。然后再个性化修改 p->xxx = xxx

copy_task_struct.png

copy_mem

  1. copy_mem 复制父进程的段、页给子进程:

    • 设置 LDT:code、data
      • 基址 = task号 * 64M
      • 限长 = 640K
    • 复制页表:copy_page_tables
      • 新页目录项 -> 新页表 (管 4MB)
      • 新页表 -> 从父页表逐项复制页表项
        • 特殊情况:进程 0 新建进程 1:只复制前 160 项
        • else:复制全部 1024 项
        • 子(新的:to)与父(旧的:from)的页表指向相同页面(共享物理:me m_map 引用计数++)
      • 新老页表均置只读(for cow)

理一下页和段的关系,以及 copy_page_tables 到底发生了什么:

page_and_seg.png

  • 页目录项(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)
  • 复制时的 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 项。
  • 上图跟物理地址没关系。线性和物理的映射是另一回事。
    • 瓜分线性空间是页表的表现
    • 线性 => 物理是页表的功能
      • 任意线性可以关联到任意物理

其他事项

  1. 共享文件:

    • 父打开的文件:f_count++
    • pwd、root、executable:i_count++
  2. 在 GDT 中设置新的 task 1:

    • 新 TSS
    • 新 LDT
  3. 新进程 -> 就绪态,返回 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]]