rCore 第三章学习笔记:多道程序与分时多任务

2025/3/93414 字

第三章结果架构图(引用自rCore 第三章)

一、学习进度与实践成果

本来好几天之前就学完了 rCore 第三章,但被实践硬控了差不多一周的时间。不过好在实践终于完成了,还发了个 GitHub,成就感还是不错的。

结合这几天的思考总结一下,虽然标题是第三章的笔记,但也会结合第二章的一些内容。

二、系统调用与批处理系统

首先,为什么要在应用程序的基础之上再套一层叫做系统调用的东西呢?

因为如果没有系统调用,各个程序员各有各的写法,代码风格不统一还好说,关键是有大量的重复功能会被反复写。所以就出现了最初的操作系统的雏形,提供一些常用的、通用的功能的统一实现,方便程序员开发各自的用户应用程序的时候进行调用。

解决了调用的问题,但此时要运行多个应用程序,还是需要人手动一个个地把程序加载到内存中,非常麻烦。那能不能让计算机自动加载提供的一系列应用程序呢?

有的,兄弟有的。通过第二章实现的批处理系统,我们可以在一个应用程序执行完毕之后自动切换到下一个应用程序,一直到所有程序都运行结束。

但仅仅是这样的话马上就会有问题,就是一旦某个应用程序中有错误,就会导致整个计算机崩溃而停止运行。并且,用户还可以执行一些恶性代码破坏整个系统或者是影响其它的应用程序。都自动化了还是不能省心。

所以还必须将用户程序和管理用户程序的程序分离开来,限制用户程序的运行。这就是第二章提到的特权分级。

有了第二章的特权分级和批处理系统,终于可以挂机执行一批任务了。

三、多道程序设计

但是还是有让人不满意的地方,就是在当年计算机资源十分宝贵的时候,计算机的利用率却不高,这是为什么呢?

因为在一个应用程序执行的过程中,时常会执行到一些有关 IO 这种耗时比较长但 CPU 只是要等待的任务,人们又想,能不能把这个等待的时间利用起来,先让计算机在这个等待的时候去执行别的任务呢?

也就是实现各个任务之间的自由切换。

切换的理论基础

要实现各个任务之间的切换,我们先想一下这在理论上可行吗。

首先,我们运行的应用程序的任务,是一串存储在内存中的二进制指令。而让 CPU 切换当前运行的指令是可以做到的,也就是修改指令寄存器的值就可以了。

那么直接改指令寄存器的值到下一个任务可以实现切换吗?

不行,因为虽然执行的代码是切换了,但是一个程序的正确执行还需要正确的“状态”,也就是当前程序用到的栈和寄存器,在切换代码之前,我们必须要先把状态正确地切换到应用程序需要的状态。一般把这种状态称作上下文(context)。

因此,切换这个操作需要提供目的任务的 context。

rust
#[derive(Clone, Copy)]
#[repr(C)]
pub struct TaskContext {
    ra: usize,
    sp: usize,
    s: [usize; 12],
}

并且当一个应用程序只是因为等待被切换出去之后,我们还希望能够再次切换回这个还没执行完毕的应用程序,因此,切换还需要将被切换的任务的 context 保存起来。

为了方便管理和保存各个任务的 context,我们就先创建了一个叫做 TaskControllBlock 的数据结构

rust
#[derive(Copy, Clone)]
pub struct TaskControlBlock {
    pub task_status: TaskStatus,
    pub task_cx: TaskContext,
}

可以看到,除了 context 之外,我们还弄了个 task_status 用来记录当前这个 task 到底是被挂起,还是在运行,还是已经结束,或者是还没加载好。

rust
#[derive(Copy, Clone, PartialEq)]
pub enum TaskStatus {
    UnInit, // 未初始化
    Ready, // 准备运行
    Running, // 正在运行
    Exited, // 已退出
}

然后给每一个存在的任务一个 TCB,放到一起,用一个统一的 TaskManager 进行管理

rust
pub struct TaskManager {
    num_app: usize,
    inner: UPSafeCell<TaskManagerInner>,
}

struct TaskManagerInner {
    tasks: [TaskControlBlock; MAX_APP_NUM],
    current_task: usize,
}

这里提一嘴,猜猜为什么直接用一个 TaskManager 包含 num_app,current_task 和 tasks,因为这三个变量中,num_app 和其它两个变量不同的是,它是在加载完毕之后就不变的,并且要在初始化静态变量的时候就使用,不用也不好包括在 UPSafeCell 里面。

切换函数的实现

有了这些准备,我们就可以开始实现应用程序的切换函数 __switch,因为这个函数涉及到寄存器的操作,直接用汇编写会比较方便。

先来想想切换的过程是什么,很自然。

第一步,在 TaskManager 中找到一个 Ready 的任务,获得它的编号(这个“找”的过程终于可以用上一般的操作系统课上用到的调度算法了,这边用的就是最简单的遍历找,找到第一个就行)

rust
    pub fn find_a_task(&self) -> Option<usize> {
        let inner = self.inner.exclusive_access();
  
        let mut curr = (inner.current_task + 1) % self.num;
        while inner.tcbs[curr].task_status != TaskStatus::Ready {
            curr = (curr + 1) % self.num;
            if curr == inner.current_task {
                return None;
            }
        }

        info!("[kernel] found a task {} to run", curr);
        Some(curr)
    }

第二步,把当前正在运行的任务标记为 Ready,然后保存当前任务的 context 到它自己的 TCB 中。

rust
    pub fn suspend_task(&self, task_id: usize) {
        let mut inner = self.inner.exclusive_access();
        inner.tcbs[task_id].task_status = TaskStatus::Ready;
    }

第三步,将找到的要运行的任务的状态记为 Running,并且把其 TCB 中的 context 加载到对应的寄存器上。然后使用 ret命令跳转到 ra 记录的返回地址。

rust
    pub fn run_next_task(&self) {
        extern "C" {
            pub fn __switch(
                current_task_cx_ptr: *mut TaskContext,
                next_task_cx_ptr: *const TaskContext
            );
        }

        let next_task = self.find_a_task();

        let mut inner = self.inner.exclusive_access();

        let current_task = inner.current_task;
        match next_task {
            Some(task_id) => {
                inner.current_task = task_id;
                inner.tcbs[task_id].task_status = TaskStatus::Running;

                let current_task_cx = &inner.tcbs[current_task].task_cx as *const _ as *const TaskContext;
                let next_task_cx = &inner.tcbs[task_id].task_cx as *const _ as *const TaskContext;

                drop(inner);

                unsafe {
                    __switch(
                        current_task_cx as *mut TaskContext,
                        next_task_cx,
                    )
                }
            }
            None => {
                // no more task to run
                crate::sbi::shutdown(false);
            }
        }
    }

下面是关于寄存器操作的汇编实现

plaintext
# os/src/task/switch.S

.altmacro
.macro SAVE_SN n
    sd s\n, (\n+2)*8(a0)
.endm
.macro LOAD_SN n
    ld s\n, (\n+2)*8(a1)
.endm

__switch:
    # 阶段 [1]
    # __switch(
    #     current_task_cx_ptr: *mut TaskContext,
    #     next_task_cx_ptr: *const TaskContext
    # )
    # 阶段 [2]
    # save kernel stack of current task
    sd sp, 8(a0)
    # save ra & s0~s11 of current execution
    sd ra, 0(a0)
    .set n, 0
    .rept 12
        SAVE_SN %n
        .set n, n + 1
    .endr
    # 阶段 [3]
    # restore ra & s0~s11 of next execution
    ld ra, 0(a1)
    .set n, 0
    .rept 12
        LOAD_SN %n
        .set n, n + 1
    .endr
    # restore kernel stack of next task
    ld sp, 8(a1)
    # 阶段 [4]
    ret

之后,只需要在系统调用中添加 sys_yield()

rust
// os/src/syscall/process.rs
pub fn sys_yield() -> isize {
    info!("[kernel] Yielding to next application");
    suspend_and_run_next();
    0
}

// os/src/task/mod.rs
pub fn suspend_and_run_next() {
    let current_task = TASK_MANAGER.get_current_app_id();
    TASK_MANAGER.suspend_task(current_task);
    TASK_MANAGER.run_next_task();
}

就可以自由地调用这个系统调用切换运行的应用程序了!

如何开始运行第一个应用

那么,你有没有想过,我们该如何开始运行第一个应用呢?也就是如何把 task 从 UnInit 变到 Ready 状态?

同样的,我们先看 Ready 状态需要什么。

当然是在自己的 TCB 中有对应的 context,而对于每一个应用的初始的 context ,可以初始化为 ra:应用的起始地址,sp :应用程序栈,ss:全部设置为 0。

吗???

当然并不是,千万不要忽略我们在上一章实现的特权分级,仔细想想,当我们的操作系统启动的时候,我们处于的可以内核态,而应用程序运行在用户态,要从内核态切换到用户态,唯一的方法就是上一章实现的 __restore(这里我用 __init 代替,具体原因去看二到三章的实践篇)

plaintext
__init:
    # case1: start running app by __restore
    # case2: back to U after handling trap
    # now sp->kernel stack(after allocated), sscratch->user stack
    # restore sstatus/sepc
    ld t0, 32*8(sp)
    ld t1, 33*8(sp)
    ld t2, 2*8(sp)
    csrw sstatus, t0
    csrw sepc, t1
    csrw sscratch, t2
    # restore general-purpuse registers except sp/tp
    ld x1, 1*8(sp)
    ld x3, 3*8(sp)
    .set n, 5
    .rept 27
        LOAD_GP %n
        .set n, n+1
    .endr
    # release TrapContext on kernel stack
    addi sp, sp, 34*8
    # now sp->kernel stack, sscratch->user stack
    csrrw sp, sscratch, sp
    sret

所以我们的切换路径应该是

初始内核态–__switch–>应用0内核态–__init–>应用0用户态–system_call–>应用0内核态–__switch–>其它应用内核态。。。

所以我们初始 __switch 的目的地应当是各个应用各自的内核态,那 context 应当初始化为:

ra:__init 地址,sp:应用内核栈指针,ss:全0

rust
// os/src/task/mod.rs
lazy_static! {
    static ref TASK_MANAGER: TaskManager = unsafe {
        extern "C" {
            fn __init();
        }

        let app_manager = APP_MANAGER.exclusive_access();
        let app_num = app_manager.num_app;
  
        let mut tcbs = [TaskControlBlock::uninit(); MAX_APP_NUM];

        for i in 0..app_num {  
            tcbs[i].task_status = TaskStatus::Ready;
            tcbs[i].task_cx = TaskContext::new(
                __init as usize,
                KERNEL_STACKS[i].push_context(TrapContext::app_init_context(get_base_i(i), USER_STACKS[i].get_sp())) as *const _ as usize,
                [0; 12],
            );
            // warn!("kernel stack for app {}", i);
        }

        TaskManager {
            num: app_num,
            inner: UPSafeCell::new(TaskManagerInner {
                tcbs: tcbs,
                current_task: 0,
            }),
        }
    };
}

做到这一步,我们已经可以顺利进入应用0的内核态的 __init 部分,只要在系统初始化后调用特殊的 __switch。也可以实现 应用0内核态–__switch–>其它应用内核态 的过程。

rust
// os/src/task/task.rs
    pub fn run_first_task(&self) {
        extern "C" {
            pub fn __switch(
                current_task_cx_ptr: *mut TaskContext,
                next_task_cx_ptr: *const TaskContext
            );
        }
        warn!("[kernal] __switch: {:#x}", __switch as usize);

        let mut inner = self.inner.exclusive_access();
        inner.current_task = 0;
        inner.tcbs[0].task_status = TaskStatus::Running;

        let mut unused_task_cx = TaskContext {
            ra: 0,
            sp: 0,
            s: [0; 12],
        };
        let next_task_cx = &inner.tcbs[0].task_cx as *const _ as *const TaskContext;

        drop(inner);

        info!("[kernel] running first task");

        unsafe {
            __switch(
                &mut unused_task_cx as *mut _ as *mut TaskContext,
                next_task_cx,
            )
        }
    }

并且,得益于第二章的工作,我们可以实现 应用0用户态–system_call–>应用0内核态 的过程。

不过,还有最后一环没有完成,就是 应用0内核态–__init–>应用0用户态。

注意到,要从内核态到达用户态,需要对应应用的内核栈中有相应的 TrapContext 才行

所以,在初始化各个任务的时候,应当初始化各个应用程序的 TrapContext 为:

x:全0,除了 x[2]代表 sp需要设置为用户栈指针,sstatus:用户态,sepc:应用程序起始地址

rust
            tcbs[i].task_cx = TaskContext::new(
                __init as usize,
                KERNEL_STACKS[i].push_context(TrapContext::app_init_context(get_base_i(i), USER_STACKS[i].get_sp())) as *const _ as usize,
                [0; 12],
            );

至此,多道程序系统构造完毕。

四、分时多任务系统

要升级到这个系统的原因也很简单,就是因为之前应用程序的切换需要应用自己调用切换的系统调用,这需要写应用程序的人有相当的自觉。因此上一种方式并不是十分可靠,所以分时多任务系统就是在协作式调度的基础上加上操作系统定时的自动切换。

切换的函数我们已经实现,关键是如何时间定时触发。

首先我们知道,我们目前的 cpu 已经用于执行程序去了,因此不能再靠 cpu 进行计时的操作。

这就需要计算机还有其它的专门用于计时的硬件。

RISC-V 架构也规定了必须要有时钟的存在。

时钟的作用就是维护一个寄存器 mtime,以一个固定的频率给这个寄存器不断加1。

因此只要我们知道了当前硬件的时钟的频率和 mtime寄存器的值,我们就可以计时。

不过除了计时还不够,我们的最终目的是要实现每隔一段时间触发一次切换。

这就需要时钟中断出场了。

还有另外一个 64 位的 CSR mtimecmp的作用是:一旦计数器 mtime的值超过了 mtimecmp,就会触发一次时钟中断。这使得我们可以方便的通过设置 mtimecmp 的值来决定下一次时钟中断何时触发。

rust
// os/src/timer/timer.rs
use super::{INTERVAL_MS, CLOCK_FREQ};

use riscv::register::{time, sie};
use crate::sbi::set_timer;

pub fn get_time() -> usize {
    time::read()
}

pub fn enable_timer_interrupt() {
    unsafe { sie::set_stimer(); }
}

pub fn set_next_trigger() {
    set_timer(get_time() + CLOCK_FREQ * INTERVAL_MS / 1000);
}
rust
// os/src/timer/mod.rs
mod timer;

const INTERVAL_MS: usize = 10;

#[cfg(feature = "board_qemu")]
pub const CLOCK_FREQ: usize = 12500000;

pub fn get_time_ms() -> usize {
    timer::get_time() / (CLOCK_FREQ / 1000)
}

pub fn start(){
    timer::enable_timer_interrupt();
    timer::set_next_trigger();
}

pub fn go_on() {
    timer::set_next_trigger();
}

通过调用 start()和 go_on(),我们就可以实现一定间隔触发一次时钟中断了。

最后还要在 trap_handler 处处理时钟中断。

rust
// os/src/trap/mod.rs
        Trap::Interrupt(Interrupt::SupervisorTimer) => {
            go_on();
            suspend_and_run_next();
        }

到这里,分时多任务系统也实现完毕。

💬 评论区

写评论

暂无评论,来发表第一条吧。

© 2026 Corn 的自留地