本文是参加2025春夏季开源操作系统训练营时对第二阶段文档第5章做的笔记。
文档:https://learningos.cn/rCore-Camp-Guide-2025S
完整版文档:https://rcore-os.cn/rCore-Tutorial-Book-v3
之前的章节中,所有的应用都是在内核初始化阶段被一并加载到内存中的,之后也无法对应用的执行进行动态增删。本章增加了一个终端、将任务进一步抽象成进程:任务是正在执行的程序,进程是程序的一次执行过程。
任务的概念是正在执行的程序,主角是程序。而相比于任务,进程的含义是在操作系统管理下的程序的一次执行过程。
相关的系统调用:
fork
:创建一个与当前进程完全相同的进程,是当前进程的子进程waitpid
:当前进程等待一个子进程变为僵尸进程,回收其全部资源并收集其返回值exec
:将当前进程的地址空间清空,加载一个特定的可执行文件,返回用户态后开始执行user_shell
就是使用以上这几个系统调用实现的。为了加入进程管理,需要增加或修改一些数据结构:
把链接应用程序的 link_app.S
改为按应用的名字来链接,在加载器中,也把所有应用的名字保存起来。
为每个进程提供一个唯一的进程标识符,当它的生命周期结束后对应的整数会被编译器自动回收:
1 | // os/src/task/pid.rs |
前一章中,每个应用的内核栈按照应用编号从小到大的顺序将它们作为逻辑段从高地址到低地址放在内核地址空间中,且两两之间保留一个guard page,这一章将应用编号改为进程标识符,而且增加了 Drop
Trait的实现,当 KernelStack
生命周期结束后,保存它的物理页帧也将会被编译器自动回收。
内核中一般会将每个进程的执行状态、资源控制等元数据均保存在一个被称为进程控制块(PCB, Process Control Block) 的结构中,在内核看来,它就等价于一个进程。在前一章的基础上,把任务控制块 TaskControlBlock
进行一些小改动并让它直接承担PCB的功能:
pid
和创建时确定的 KernelStack
,是不可变的。parent
使用 Option<Weak<TaskControlBlock>>
,children
使用 Vec<Arc<TaskControlBlock>>
。把任务管理器 TaskManager
中对于CPU的监控职能拆分到 processor
中,TaskManager
仅负责管理所有任务(进程),TaskManager
中使用一个队列来维护任务,任务调度使用简单的FIFO算法,后面的练习中会加入stride算法(也很简单)。
Processor
负责维护CPU的状态,包括当前处理器上正在执行的任务、当前处理器上的 idle 控制流的任务上下文。
idle控制流是尝试从任务管理器中选出一个任务来在当前 CPU 核上执行。在内核初始化完毕之后,会通过调用 run_tasks
函数来进入 idle 控制流:
1 | ///The main part of process execution and scheduling |
循环调用 fetch_task
直到顺利从任务管理器中取出一个任务,随后通过任务切换的方式来执行。fetch_task
使用了 TaskManager
的任务调度函数。
在内核初始化完毕之后,会将初始进程 initproc
的PCB加入任务管理器,它会启动用户shell等待用户操作。PCB的创建过程主要是:从ELF文件中解析地址空间、进行一些trap相关的配置、初始化trap上下文。
用户shell的实现使用了 fork
, exec
, wait
等系统调用,fork
系统调用中,基本上和PCB的创建过程相同,不同点是地址空间是从其它的用户的地址空间复制来的,还有多了维护父子进程关系的部分。复制地址空间的部分在 mm
模块中加入了两个辅助函数。最后,在 fork
系统调用返回时,要将子进程的Trap上下文中用来存放系统调用返回值的a0寄存器修改为0。
exec
系统调用加载一个新的ELF文件替换原来的地址空间并返回,实现起来比较简单,生成一个新的地址空间并替换,再修改一些trap相关的配置即可。在 trap_handler
中,由于 exec
系统调用中地址空间改变,trap上下文失效,在返回后需要重新获取。
最后是进程退出时的处理,当一个进程调用 sys_exit
系统调用主动退出,或者出错由内核终止之后,会在内核中调用 exit_current_and_run_next
函数,它比前几章增加一个进程的返回值,将进程控制块中的状态修改为 TaskStatus::Zombie
即僵尸进程,将当前进程的所有子进程挂在初始进程 initproc
下面,再将当前进程占用的资源进行早期回收。在任务切换之后,不会再回到僵尸进程继续执行。
跟OSTEP中进程调度的部分差不多,没有单独记。