首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >rCore:进程管理详解

rCore:进程管理详解

原创
作者头像
武状元
发布于 2026-11-14 23:03:48
发布于 2026-11-14 23:03:48
60
举报

​

在之前的ch4中,实现了虚拟地址功能,不用通过汇编直接给每个APP分配物理地址然后运行了。但是对每个APP而言,系统并不能很好的控制他们,因此我们需要实现让内核能够控制每个APP。这就是进程的作用,这里的系统还没有实现线程,因此一个进程只能运行一个APP。

1.进程控制块

代码语言:javascript
复制
pub struct TaskControlBlock {
    // Immutable
    /// Process identifier
    pub pid: PidHandle,

    /// Kernel stack corresponding to PID
    pub kernel_stack: KernelStack,

    /// Mutable
    inner: UPSafeCell<TaskControlBlockInner>,
}

这里提供了进程的pid,内核栈还有进程的信息,使用inner来实现,因为一些信息会被修改,比如上下文,状态,所以需要线程安全(名词就叫这个,这里没有线程)。

代码语言:javascript
复制
pub struct TaskControlBlockInner {
    /// The physical page number of the frame where the trap context is placed
    pub trap_cx_ppn: PhysPageNum,

    /// Application data can only appear in areas
    /// where the application address space is lower than base_size
    pub base_size: usize,

    /// Save task context
    pub task_cx: TaskContext,

    /// Maintain the execution status of the current process
    pub task_status: TaskStatus,

    /// Application address space
    pub memory_set: MemorySet,

    /// Parent process of the current process.
    /// Weak will not affect the reference count of the parent
    pub parent: Option<Weak<TaskControlBlock>>,

    /// A vector containing TCBs of all child processes of the current process
    pub children: Vec<Arc<TaskControlBlock>>,

    /// It is set when active exit or execution error occurs
    pub exit_code: i32,

    /// Heap bottom
    pub heap_bottom: usize,

    /// Program break
    pub program_brk: usize,

    /// Current stride of this process, used by the stride scheduler.
    pub stride: usize,

    /// Scheduling pass: `BIG_STRIDE / priority`.
    pub pass: usize,

    /// Priority of this process, always `>= 2`.
    pub priority: isize,
}

inner里面主要有页表(memory_set),trap时保存上下文的页表,内核上下文内容,任务状态等信息。这些信息在进程切换的时候会改变。TaskContext里面主要是ra,sp,s0~s11这些寄存器,在恢复现场的时候,这些寄存器里面的东西都要被保存。trap_cx_ppn这个是内核的地址,由于切换的时候是在内核的页表里面,所以不能用用户的地址,也不能当变量直接存在用户的属性中。

代码语言:javascript
复制
pub enum TaskStatus {
    /// uninitialized
    UnInit,
    /// ready to run
    Ready,
    /// running
    Running,
    /// exited
    Zombie,
}

进程的四个状态的作用如下:

  • UnInit:占位,本章没有实际使用;
  • Ready:已经创建好、随时可以被调度器选中,正在就绪队列里排队;
  • Running:正在 CPU 上跑,不在就绪队列里;
  • Zombie:已经退出但还没被父进程 waitpid 回收。它不能立刻销毁,因为父进程还要读它的 exit_code。

2.进程切换

进程切换实际在内核发生,具体有

  • 用户态被打断,进内核
  • 内核里决定让出 CPU
  • 把当前进程从 CPU 上摘下来,放回队列
  • 调度器调度

这里进入内核实际是走的trap的方式,sscause寄存器会保存trap的原因,然后trap的时候就会运行调度的程序,这里内核会让出这个CPU,修改进程的状态,然后放入队列中,最后调度器调度。整体过程并不复杂,但是需要注意实现的步骤,而且这里需要进入内核来实现,内核中要保证修改的原子性。

时序如下:

代码语言:javascript
复制
用户态 P 运行
  │ 时钟中断
  ▼
__alltraps(跳板页): 存 TrapContext → satp 切内核 → sp 切 P 的内核栈
  ▼
trap_handler(内核页表,P 的栈): suspend_current_and_run_next()
  ├─ take_current_task()       P 离开 PROCESSOR
  ├─ P.status = Ready
  ├─ add_task(P)               回就绪队列
  └─ schedule(&mut P.task_cx)
       └─ __switch(P.task_cx, idle_task_cx)   存 P 的内核现场
  ▼
run_tasks(boot stack): fetch_task() 选 stride 最小的 Q,Q.status = Running
       └─ __switch(idle_task_cx, Q.task_cx)   存调度器现场,载入 Q
  ▼
Q 的内核栈: ra = trap_return(首次)或 schedule() 的返回点(恢复)
  ▼
trap_return → jr 跳板页 __restore → satp 切 Q 的页表 → 恢复 TrapContext → sret
  ▼
用户态 Q 运行

这里把相应的函数实现后,后面实现睡眠锁等需要阻塞方式实现的函数的时候只需要修改进程的状态,将进程放入队列中,然后运行下一个进程就可以了。

其实我感觉这里实现的进程的部分与freertos的任务非常相似,只不过freertos的资源是需要我们写入参数来分配的,而进程是内核分配资源,freertos就像是个小内核一样,只不过它管理的部分并不多,只是管理任务的切换,内存等资源分配。想到这里我才彻底明白为什么说进程是操作系统分配资源的最小单位,而freertos中创建任务又为什么要分配栈的大小等部分。

3.用户态使用的函数

这里新加的函数如下:

  • fork
  • exec
  • waitpid

为了实现waitpid功能,rCore把会使用的命令的名字和相应的地址保存了起来,这样我们就可以使用相应的名字来调用相应的程序。程序是应用的 ELF 格式数据,这里通过elf格式的文件保存了起来。

fork的作用就是复制一个进程,父子从同一点分叉新建 TCB + 深拷贝 MemorySet + 复制 trap_cx

exec是用来替换自己的程序映像,不新建进程原地换 MemorySet + 重置 trap_cx

waitpid回收子进程,拿它的退出码找僵尸子进程。

这里fork实现比较有意思,它修改了保存的上下文,实现如下:

代码语言:javascript
复制
pub fn sys_fork() -> isize {
    let current_task = current_task().unwrap();
    let new_task = current_task.fork();
    let new_pid = new_task.pid.0;
    // modify trap context of new_task, because it returns immediately after switching
    let trap_cx = new_task.inner_exclusive_access().get_trap_cx();
    // we do not have to move to next instruction since we have done it before
    // for child process, fork returns 0
    trap_cx.x[10] = 0;
    // add new task to scheduler
    add_task(new_task);
    new_pid as isize

修改后,这样可以让父进程返回子进程的pid,子进程会获取0,这样可以分辨父子进程。

4.调度器实现

调度器的全部状态就是一个Vec:

代码语言:javascript
复制
pub struct TaskManager {
    ready_queue: Vec<Arc<TaskControlBlock>>,
}

lazy_static! {
    pub static ref TASK_MANAGER: UPSafeCell<TaskManager> =
        unsafe { UPSafeCell::new(TaskManager::new()) };
}

  1. 里面只放 Ready 的进程。 Running 的进程在 PROCESSOR.current 里,不在这个 Vec 里。
  2. Vec 是无序的。 顺序由 push 决定,没有任何排序——排序的责任完全落在 fetch 的扫描上。

插入进程就是直接在尾部插入

代码语言:javascript
复制
pub fn add(&mut self, task: Arc<TaskControlBlock>) {
    self.ready_queue.push(task);
}

fetch则是扫描一遍然后获取最低的stride,然后将该任务拿出来运行

代码语言:javascript
复制
pub fn fetch(&mut self) -> Option<Arc<TaskControlBlock>> {
    let mut min_stride = usize::MAX;
    let mut min_idx = None;
    for (idx, task) in self.ready_queue.iter().enumerate() {
        let stride = task.inner_exclusive_access().stride;
        if stride < min_stride {
            min_stride = stride;
            min_idx = Some(idx);
        }
    }
    let idx = min_idx?;
    let task = self.ready_queue.remove(idx);
    {
        let mut inner = task.inner_exclusive_access();
        inner.stride += inner.pass;
    }
    Some(task)
}

拆开看有四个决策点:

  • 比较用严格 <,不是 <=。 后果是平局时第一个被遍历到的胜出,也就是 Vec 里靠前的那个。而 add 是 push 到尾部,所以"让出 CPU 后被重新排到队尾"天然构成 FIFO 轮转。如果写成 <=,平局时永远选最后一个,队列靠前的进程会长期挨饿。
  • 记账在"取出时",不在"放回时"。 这是全函数最关键的一行。如果挪到 suspend_current_and_run_next 或 add_task 里,那么走 exec、走 exit、被 waitpid 回收的进程永远不经过放回路径,这次调度就白嫖了。放在取出时,每一次调度恰好记一次账,与进程后来怎么结束无关。
  • remove(idx) 是 O(n)。 它要把后面的元素整体前移。加上扫描本身也是 O(n),fetch 整体是 O(n)。同时 remove 会保持剩余元素的相对顺序,这一点对 ① 里的平局规则很重要——如果用 swap_remove,队列顺序会被打乱,FIFO 语义就没了。

5.总结

第五章新增的其实不多:一个 TaskControlBlock 的分层结构、一个四状态的状态机、一个 stride 调度器、一个 Processor 来管"谁在跑"。但把这些东西拼在一起之后,fork/exec/waitpid 这三件看起来玄乎的事情就都落地了

  • fork 的关键在于 TrapContext 被整页复制了,所以子进程天然从 ecall 的下一条指令继续执行,只需要把 a0 改成 0 来区分父子;
  • exec 的关键在于 memory_set = memory_set 这一行赋值触发了旧地址空间的完整析构,不需要任何显式的资源回收代码;
  • waitpid 的关键在于 Arc 的引用计数——父进程 children 里那个 Arc 就是僵尸进程的"生命维持器",remove 掉之后 TCB 自然析构,PidHandle、KernelStack、MemorySet 的 Drop 依次执行,一个物理页都不会漏;

​

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 1.进程控制块
  • 2.进程切换
  • 3.用户态使用的函数
  • 4.调度器实现
  • 5.总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档