【rCore】用rust从零开始写一个操作系统 开源操作系统训练营:ch3 多道程序与分时多任务

需要实现什么

本章的目标是实现分时多任务系统,它能并发地执行多个用户程序,并调度这些程序。为此需要实现
一次性加载所有用户程序,减少任务切换开销;
支持任务切换机制,保存切换前后程序上下文;
支持程序主动放弃处理器,实现 yield 系统调用;
以时间片轮转算法调度用户程序,实现资源的时分复用。

拥有什么

实现了用户程序加载、用户态与内核态间的相互转换、trap处理机制、用户程序运行完成后切换至下一个程序的批处理系统。

思路

区别:

  1. 程序保存在内存中而非作为数据段链接到内核里。
  2. 同时管理多个应用程序。

定时触发时钟中断:
通过M特权级获取时间 -> 设置mtimecmp触发中断 -> 捕获中断运行下一个app

应用程序管理:
单例管理所有程序 -> 将程序保存在内存中 -> 实现应用加载、切换、退出 -> 实现yield让步

本章代码树

── os
   ├── build.rs
   ├── Cargo.toml
   ├── Makefile
   └── src
       ├── batch.rs(移除:功能分别拆分到 loader 和 task 两个子模块)
       ├── config.rs(新增:保存内核的一些配置)
       ├── console.rs
       ├── logging.rs
       ├── sync
       ├── entry.asm
       ├── lang_items.rs
       ├── link_app.S
       ├── linker.ld
       ├── loader.rs(新增:将应用加载到内存并进行管理)
       ├── main.rs(修改:主函数进行了修改)
       ├── sbi.rs(修改:引入新的 sbi call set_timer)
       ├── syscall(修改:新增若干 syscall)
       │   ├── fs.rs
       │   ├── mod.rs
       │   └── process.rs
       ├── task(新增:task 子模块,主要负责任务管理)
       │   ├── context.rs(引入 Task 上下文 TaskContext)
       │   ├── mod.rs(全局任务管理器和提供给其他模块的接口)
       │   ├── switch.rs(将任务切换的汇编代码解释为 Rust 接口 __switch)
       │   ├── switch.S(任务切换的汇编代码)
       │   └── task.rs(任务控制块 TaskControlBlock 和任务状态 TaskStatus 的定义)
       ├── timer.rs(新增:计时器相关)
       └── trap
           ├── context.rs
           ├── mod.rs(修改:时钟中断相应处理)
           └── trap.S

实现过程

定时触发时钟中断

通过M特权级获取时间

// os/src/timer.rs
use riscv::register::time;
pub fn get_time() -> usize {
    time::read()
}

设置mtimecmp触发中断

RISC-V 要求处理器维护时钟计数器 mtime,还有另外一个 CSR mtimecmp 。 一旦计数器 mtime 的值超过了 mtimecmp,就会触发一次时钟中断。

// os\src\sbi.rs
/// use sbi call to set timer
pub fn set_timer(timer: usize) {
    sbi_call(SBI_SET_TIMER, timer, 0, 0);
}

捕获中断运行下一个app

重新设置mtimecmp后运行下一个app。

timer 子模块的 set_next_trigger 函数对 set_timer 进行了封装, 它首先读取当前 mtime 的值,然后计算出 10ms 之内计数器的增量,再将 mtimecmp 设置为二者的和。 这样,10ms 之后一个 S 特权级时钟中断就会被触发。

trap_handler详解见 ch2 。

// os\src\trap\mod.rs
/// trap handler
#[no_mangle]
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
		...
        Trap::Interrupt(Interrupt::SupervisorTimer) => {
            set_next_trigger();
            suspend_current_and_run_next();
        }
        ...
}

// os\src\timer.rs
/// Set the next timer interrupt
pub fn set_next_trigger() {
    set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
}

应用程序管理

单例统一管理所有应用程序

TaskManager保存任务(进程)数、所有任务信息task、当前任务id。
Task包括任务状态和任务上下文TaskContext。
TaskContext包括ra、sp、s0-11寄存器。

lazy_static! {
    /// Global variable: TASK_MANAGER
    pub static ref TASK_MANAGER: TaskManager = {
        let num_app = get_num_app();
        let mut tasks = [TaskControlBlock {
            task_cx: TaskContext::zero_init(),
            task_status: TaskStatus::UnInit,

            syscall_times: [0; MAX_SYSCALL_NUM],
            first_time: 0,
        }; MAX_APP_NUM];
        for (i, task) in tasks.iter_mut().enumerate() {
            task.task_cx = TaskContext::goto_restore(init_app_cx(i));
            task.task_status = TaskStatus::Ready;
        }
        TaskManager {
            num_app,
            inner: unsafe {
                UPSafeCell::new(TaskManagerInner {
                    tasks,
                    current_task: 0,
                })
            },
        }
    };
}

将程序保存在内存中

// os\src\loader.rs
/// Load nth user app at
/// [APP_BASE_ADDRESS + n * APP_SIZE_LIMIT, APP_BASE_ADDRESS + (n+1) * APP_SIZE_LIMIT).
pub fn load_apps() {
    extern "C" {
        fn _num_app();
    }
    let num_app_ptr = _num_app as usize as *const usize;
    let num_app = get_num_app();
    let app_start = unsafe { core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) };
    // clear i-cache first
    unsafe {
        asm!("fence.i");
    }
    // load apps
    for i in 0..num_app {
        let base_i = get_base_i(i);
        // clear region
        (base_i..base_i + APP_SIZE_LIMIT)
            .for_each(|addr| unsafe { (addr as *mut u8).write_volatile(0) });
        // load app from data section to memory
        let src = unsafe {
            core::slice::from_raw_parts(app_start[i] as *const u8, app_start[i + 1] - app_start[i])
        };
        let dst = unsafe { core::slice::from_raw_parts_mut(base_i as *mut u8, src.len()) };
        dst.copy_from_slice(src);
    }
}
/// Get base address of app i.
fn get_base_i(app_id: usize) -> usize {
    APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT
}

实现应用加载、切换、退出

在 __switch(os\src\task\switch.S) 中保存 CPU 的某些寄存器,它们就是 任务上下文 (Task Context)。内核先把 current_task_cx_ptr 中包含的寄存器值逐个保存,再把 next_task_cx_ptr 中包含的寄存器值逐个恢复。 __switch 函数的主要作用是保存当前任务的上下文( Task Context,栈指针和寄存器值),并加载下一个任务的上下文,以便可以无缝地切换到下一个任务的执行。

# 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
    .section .text
    .globl __switch
__switch:
    # __switch(
    #     current_task_cx_ptr: *mut TaskContext,
    #     next_task_cx_ptr: *const TaskContext
    # )
    # 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
    # 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)
    ret
    
这段代码是一个汇编程序,特定于某种RISC-V或其他类似指令集的架构,用于实现任务上下文切换(task context switch)。它保存了当前执行任务的上下文(如寄存器和栈指针),并加载下一个任务的上下文以恢复其执行。以下是对代码的逐行解释:

# 开始的行是注释,它们解释了代码的功能和目的。
2-9. 定义了两个宏 SAVE_SN 和 LOAD_SN,分别用于保存和加载寄存器(s0 到 s11)的值。这些宏使用了一个名为 n 的变量作为参数,该变量在宏内部被替换为具体的寄存器编号。

SAVE_SN 宏使用 sd(store doubleword,即存储双字)指令将寄存器 s\n(例如 s0、s1 等)的值存储到由 a0 指向的内存地址的偏移位置。
LOAD_SN 宏使用 ld(load doubleword,即加载双字)指令从由 a1 指向的内存地址的偏移位置加载值到寄存器 s\n。
10-11. 声明了一个文本段(.text)和一个全局符号 __switch。

12-35. 定义了 __switch 函数,该函数实现了任务上下文切换。

第18行:保存当前任务的栈指针(sp)到由 a0 指向的内存地址的偏移8的位置。
第20行:保存当前执行的返回地址(ra)到由 a0 指向的内存地址的偏移0的位置。
第22-25行:使用 .rept(repeat)伪指令重复12次,以保存寄存器 s0 到 s11 的值。这里使用了前面定义的 SAVE_SN 宏。
第27行:从由 a1 指向的内存地址的偏移0的位置加载下一个任务的返回地址(ra)。
第29-32行:使用 .rept 伪指令重复12次,以加载寄存器 s0 到 s11 的值。这里使用了前面定义的 LOAD_SN 宏。
第34行:从由 a1 指向的内存地址的偏移8的位置加载下一个任务的栈指针(sp)。
第35行:ret 指令返回,此时将开始执行下一个任务的代码。
总之,这个 __switch 函数的主要作用是保存当前任务的上下文(栈指针和寄存器值),并加载下一个任务的上下文,以便可以无缝地切换到下一个任务的执行。这是操作系统中多任务执行的核心机制之一。
// os\src\task\mod.rs
impl TaskManager {
    /// Run the first task in task list.
    ///
    /// Generally, the first task in task list is an idle task (we call it zero process later).
    /// But in ch3, we load apps statically, so the first task is a real app.
    fn run_first_task(&self) -> ! {
        let mut inner = self.inner.exclusive_access();
        let task0 = &mut inner.tasks[0];
        task0.task_status = TaskStatus::Running;
        let next_task_cx_ptr = &task0.task_cx as *const TaskContext;
        drop(inner);
        let mut _unused = TaskContext::zero_init();
        // before this, we should drop local variables that must be dropped manually
        unsafe {
            __switch(&mut _unused as *mut TaskContext, next_task_cx_ptr);
        }
        panic!("unreachable in run_first_task!");
    }

    /// Change the status of current `Running` task into `Ready`.
    fn mark_current_suspended(&self) {
        let mut inner = self.inner.exclusive_access();
        let current = inner.current_task;
        inner.tasks[current].task_status = TaskStatus::Ready;
    }

    /// Change the status of current `Running` task into `Exited`.
    fn mark_current_exited(&self) {
        let mut inner = self.inner.exclusive_access();
        let current = inner.current_task;
        inner.tasks[current].task_status = TaskStatus::Exited;
    }

    /// Find next task to run and return task id.
    /// In this case, we only return the first `Ready` task in task list.
    fn find_next_task(&self) -> Option<usize> {
        let inner = self.inner.exclusive_access();
        let current = inner.current_task;
        (current + 1..current + self.num_app + 1)
            .map(|id| id % self.num_app)
            .find(|id| inner.tasks[*id].task_status == TaskStatus::Ready)
    }

    /// Switch current `Running` task to the task we have found,
    /// or there is no `Ready` task and we can exit with all applications completed
    fn run_next_task(&self) {
        if let Some(next) = self.find_next_task() {
            let mut inner = self.inner.exclusive_access();
            let current = inner.current_task;
            inner.tasks[next].task_status = TaskStatus::Running;
            inner.current_task = next;

            let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
            let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
            drop(inner);
            // before this, we should drop local variables that must be dropped manually
            unsafe {
                __switch(current_task_cx_ptr, next_task_cx_ptr);
            }
            // go back to user mode
        } else {
            panic!("All applications completed!");
        }
    }
}
加载
/// Run the first task in task list.
pub fn run_first_task() {
    TASK_MANAGER.run_first_task();
}
切换
/// Suspend the current 'Running' task and run the next task in task list.
pub fn suspend_current_and_run_next() {
    mark_current_suspended();
    run_next_task();
}
退出
/// Exit the current 'Running' task and run the next task in task list.
pub fn exit_current_and_run_next() {
    mark_current_exited();
    run_next_task();
}

实现yield让步

实现系统调用,相当于主动切换。

// os\src\syscall\process.rs
/// current task gives up resources for other tasks
pub fn sys_yield() -> isize {
    trace!("kernel: sys_yield");
    suspend_current_and_run_next();
    0
}

chapter3练习

编程作业

获取任务信息

思路:每个task记录系统调用次数、任务第一次被调度时刻。第一次调度任务时设置任务第一次被调度时刻为当前时刻(初始设为-1,run_next_task检测若为-1设置为当前时刻)(这一项直接设置为0好像也没关系),每次触发系统调用时使用TASK_MANAGER更改当前task的syscall_times。

简答作业

  1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。 请同学们可以自行测试这些内容(运行 三个 bad 测例 (ch2b_bad_*.rs) ), 描述程序出错行为,同时注意注明你使用的 sbi 及其版本。

程序出错行为:
ch2b_bad_address: [kernel] PageFault in application, bad addr = 0x0, bad instruction = 0x804003ac, kernel killed it.
ch2b_bad_instructions: [kernel] IllegalInstruction in application, kernel killed it.
ch2b_bad_register: [kernel] IllegalInstruction in application, kernel killed it.
报错来源:
// os\src\trap\mod.rs
Trap::Exception(Exception::StoreFault) | Trap::Exception(Exception::StorePageFault) => {
println!(“[kernel] PageFault in application, bad addr = {:#x}, bad instruction = {:#x}, kernel killed it.”, stval, cx.sepc);
exit_current_and_run_next();
}
Trap::Exception(Exception::IllegalInstruction) => {
println!(“[kernel] IllegalInstruction in application, kernel killed it.”);
exit_current_and_run_next();
}
os的trap_handler捕获非法物理地址和非法指令错误,终止当前程序运行下一个。

  1. 深入理解 trap.S 中两个函数 __alltraps 和 __restore 的作用,并回答如下问题:

    1. L40:刚进入 __restore 时,a0 代表了什么值。请指出 __restore 的两种使用情景。

      当 trap_handler 返回或系统调用结束之后,使用 __restore 从保存在内核栈上的 Trap 上下文恢复寄存器。最后通过一条 sret 指令回到应用程序执行。

    2. L43-L48:这几行汇编代码特殊处理了哪些寄存器?这些寄存器的的值对于进入用户态有何意义?请分别解释。

       ld t0, 32*8(sp)
       ld t1, 33*8(sp)
       ld t2, 2*8(sp)
       csrw sstatus, t0
       csrw sepc, t1
       csrw sscratch, t2
      

      从内核栈上加载 sstatus、sepc 和 sscratch 的值。使用 csrw 指令将这些值写回到相应的寄存器中。其中 sstatus 是系统状态寄存器,sepc 是程序计数器寄存器,sscratch 用于保存用户栈地址。

    3. L50-L56:为何跳过了 x2 和 x4?

       ld x1, 1*8(sp)
       ld x3, 3*8(sp)
       .set n, 5
       .rept 27
          LOAD_GP %n
          .set n, n+1
       .endr
      

      x2、x4分别为sp和tp寄存器,栈指针寄存器sp保存在上下文中,线程指针寄存器tp没有实质性用途。

    4. L60:该指令之后,sp 和 sscratch 中的值分别有什么意义?

       csrrw sp, sscratch, sp
      

      将sp和sscratch的值交换回来,使sp指向内核栈,sscratch指向用户栈。

    5. __restore:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态?

      L61 sret

    6. L13:该指令之后,sp 和 sscratch 中的值分别有什么意义?

       csrrw sp, sscratch, sp	
      

      通过csrrw sp, sscratch, sp指令,将sscratch寄存器(通常包含用户栈指针)的值保存到当前栈指针(sp),并将sp寄存器的值写入sscratch。这样,sp现在指向内核栈,而sscratch包含用户栈的指针。

    7. 从 U 态进入 S 态是哪一条指令发生的?

      ecall

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值