sys_fork的实现
1. 进程控制块
在之前我们没有提进程的概念,都是以任务的概念来说的,从这章开始正式进入进程,在内核中一个TaskControlBlock
即代表了一个进程,系统中同一时间存在的每个进程都被一个不同的 进程标识符 (PID, Process Identifier) 所标识。在内核初始化完毕之后会创建一个进程——即 用户初始进程 (Initial Process) ,它是目前在内核中以硬编码方式创建的唯一一个进程。其他所有的进程都是通过一个名为 fork
的系统调用来创建的。
因此我们首先更新一下TCB
的内容:
typedef struct TaskControlBlock |
新增了pid
以及父进程的TCB
指针,每个进程都需要为其分配一个pid
的值,我将初始的第一个进程的pid
值设置成了1,当有新的进程被创建时,相应的pid
值加一就行,代码实现如下:
int nextpid = 1; |
如果使用app_init
函数进行进程初始化的话就去调用此函数为进程分配一个pid
号:
顺便这里修复一个bug
,在调用tcx_init
函数初始化任务上下文时,传入的参数应该是该进程的内核栈的地址才对,用于更新sp
之前传入错了。
2. fork的实现
fork
函数的功能直观体现如下:
int main() |
父进程在调用fork
函数后会生成一个新的子进程,此子进程和父进程的代码一样,执行逻辑是不一样的,当父进程fork
成功后,fork
返回的是子进程的pid
号,而子进程中返回的是0
代表子进程创建成功
在实现 fork 的时候,分为两个步骤:
- 新建一个进程,为其分配页表,分配
trap
页,映射跳板页 - 拷贝父进程的数据和子进程的一致
父进程和子进程的虚拟地址空间是完全相同的,但是由于页表不一样,所以映射到的实际物理内存是不一样的。
首先来实现第一步,为子进程创建页表,分配与映射trap
页,映射跳板页:
struct TaskControlBlock* allocproc() |
去进程数组里查找未初始化的进程,找到了则跳转到
found
处为此进程分配
pid
,将其任务状态设置成Ready
,这样就可以进入调度了调用
proc_trap
函数,为应用程序分配一页内存用与存放trap
,同时初始化任务上下文调用
proc_pagetable
函数,为用户程序创建页表,映射跳板页和trap
上下文页
在此之前,实现了一个内核支持进程的初始化函数,将所有进程状态设置成了UnInit
void procinit() |
这样当创建新的进程时就去tasks
数组里查找未初始化的。
然后来实现第二步,为子进程创建一个和父进程几乎完全相同的应用地址空间,定义了uvmcopy
函数,此函数的入参为父进程的根页表和子进程的根页表和一个sz
的参数,我们对父进程的页表进行索引,去查找父进程的虚拟地址空间中那些页是映射了的,如果此虚拟地址被映射了,则为子进程创建相同的映射,不同的是需要为子进程分配进行映射的物理地址页。
int uvmcopy(PageTable* old, PageTable* new, u64 sz) |
这个sz
的值是父进程所占的虚拟地址空间的最大值,是在load_app
函数中进行赋值的:
如下图:
我们从0x0
开始依次遍历,通过find_pte(old,vpn);
函数去一页一页遍历直到遍历到base_size
大小,如果此页未被映射,则find_pte(old,vpn);
会返回0,反之如果此页被映射了,则会返回映射的pte
的值,我们此时就能对子进程进行映射了,同时根据父进程的pte
找到存储父进程应用数据的物理页,拷贝数据到子进程的物理页中
这里find_pte
函数有个小bug
:
将判断页表为空的这一步提到了返回pte
的前面。
有了上面这两部我们再来看__sys_fork()
的实现:
父进程就是当前正在执行的进程,通过
current_proc()
函数拿到了当前进程的PCB
指针创建一个新的子进程
拷贝父进程的内存数据,创建一个和父进程相同的虚拟地址空间
拷贝父进程的
trap
页的数据将子进程的
trap
返回值设置为0,然后复制父进程的TCB
的信息设置子进程的返回地址和内核栈
将
_top++
__sys_fork()
的返回值为子进程的pid
号
int __sys_fork() |
我们在子进程内核栈上压入一个初始化的任务上下文,使得内核一旦通过任务切换到该进程,就会跳转到 trap_return
来进入用户态。而在复制地址空间的时候,子进程的 Trap 上下文也是完全从父进程复制过来的,这可以保证子进程进入用户态和其父进程回到用户态的那一瞬间 CPU 的状态是完全相同的。而两个进程的应用数据由于地址空间复制的原因也是完全相同的,
我们再来看看从父进程trap进内核执行fork的过程:
- 首先父进程执行
fork
函数进入内核的trap_handler
函数进行分发,这里将cx->sepc += 8;
向前移动到了sys_call
的上一步,这也是一个小bug
。因为在__sys_fork()
内部会对父进程的trap
页数据进行拷贝,为了保证子进程从内核返回后能正确返回到调用fork
函数的下一指令开始执行,所以需要先修改trap
页数据
- 然后执行
__sys_fork()
函数,此函数执行完毕后,内核中就多了一个子进程,父进程就从trap
返回了,此时返回到用户态的值就是__sys_fork()
的返回值即子进程的pid
值
- 然后进行调度,当调度到子进程时会从内核态返回到用户态,由于在
__sys_fork()
函数中我们将子进程的trap
页的a0
寄存器的值设置成了0,所以用户态接收到的值就是0
3. 测试
- 首先修改一下
main
函数,初始化所有进程
- 然后修改应用程序,我直接修改
time.c
|
- 在
app.c
中新建一个系统调用函数,#define __NR_clone 220
int sys_fork() |
编译运行,先编译time
应用程序,再编译内核:
可以看见来回打印father
和child
,测试成功
参考链接
进程管理机制的设计实现 - rCore-Tutorial-Book-v3 3.6.0-alpha.1 文档 (rcore-os.cn)
[xv6 fork的实现 | Blurred code](https://www.blurredcode.com/2020/11/xv6fork的实现/#:~:text=fork的实现 在xv6中的fork的实现是 int fork(void) { int child_pid %3D,0 for child_process fork () return child_pid%3B })