找回密码
 立即注册
首页 业界区 安全 [rCore学习笔记 024]多道程序与协作式调度 ...

[rCore学习笔记 024]多道程序与协作式调度

腥狩频 10 小时前
写在前面

本随笔是非常菜的菜鸡写的。如有问题请及时提出。
可以联系:1160712160@qq.com
GitHhub:https://github.com/WindDevil (目前啥也没有
本节重点

主要是对 任务 的概念进行进一步扩展和延伸:形成

  • 任务运行状态:任务从开始到结束执行过程中所处的不同运行状态:未初始化、准备执行、正在执行、已退出
  • 任务控制块:管理程序的执行过程的任务上下文,控制程序的执行与暂停
  • 任务相关系统调用:应用程序和操作系统之间的接口,用于程序主动暂停 sys_yield 和主动退出 sys_exit
这里主要看具体实现,这些概念之前学习RTOS的时候使用是会使用了,但是具体怎么实现还不好说.
多道程序背景与 yield 系统调用

尽管 CPU 可以一直在跑应用了,但是其利用率仍有上升的空间.
随着应用需求的不断复杂,有的时候会在内核的监督下访问一些外设,它们也是计算机系统的另一个非常重要的组成部分,即 输入/输出 (I/O, Input/Output) .
CPU 会把 I/O 请求传递给外设,待外设处理完毕之后,CPU 便可以从外设读到其发出的 I/O 请求的处理结果.
我们暂时考虑 CPU 只能 单向地 通过读取外设提供的寄存器信息来获取外设处理 I/O 的完成状态。
多道程序的思想在于:

  • 内核同时管理多个应用。如果外设处理 I/O 的时间足够长,那我们可以先进行任务切换去执行其他应用
  • 在某次切换回来之后,应用再次读取设备寄存器,发现 I/O 请求已经处理完毕了,那么就可以根据返回的 I/O 结果继续向下执行了
这样的话,只要同时存在的 应用足够多 ,就能 一定程度 上隐藏 I/O 外设处理相对于 CPU 的延迟,保证 CPU 不必浪费时间在等待外设上,而是几乎一直在进行计算。
这种任务切换,是让应用 主动 调用 sys_yield 系统调用来实现的,这意味着应用主动交出 CPU 的使用权给其他应用。
这一段的描述相当是一种多任务的轮询,但是在我的脑海中, 外部中断 还是比多任务轮询要好得多的. 但是怎么合理地 利用 外部中断提高实时性,就是一个问题.
至于主动调用sys_yield就是一件很难的事情,也就是为啥叫做 协作式 , 就是系统的性能要依赖程序员在设计APP的时候释放CPU.(我自己都想拉满CPU,谁想管你死活捏)
这里提到了 一种多道程序执行的典型情况 :
1.png

这张图很好解释:

  • 这张图的 横轴 是时间轴
  • 这张图的 纵轴 是运行实体(任务和IO硬件)
  • 可以看到是有三个运行实体

    • I/O Device : 这个是IO硬件
    • I/O Task : 这个是请求IO硬件的任务
    • Other Task : 这个是不请求IO硬件的其它任务

  • 可以看到最开始是 IO Task 在运行.
  • I/O Start yield 时刻,IO Task 请求了IO硬件,然后释放了CPU.
  • Other Task 接手CPU,同时 IO Device 继续处理硬件上的问题.
  • 一直执行到 Not Complete yileld again 时段的开头,Other Task 执行完毕,把CPU释放.
  • IO Task 接手之后检查IO硬件状态,仍然没有处理完毕.
  • Not Complete yileld again 时段的结尾, IO Task 释放CPU.
  • Other Task 再次接手CPU,同时 IO Device 继续处理硬件上的问题.
  • Other Task 执行期间,发生了 I/O Complete 时刻,但是此时软件感知不到.
  • Continue 时刻, ,Other Task 执行完毕,把CPU释放.
  • IO Task 接手之后检查IO硬件状态,处理完毕,因此继续执行.
上面我们是通过“避免无谓的外设等待来提高 CPU 利用率”这一切入点来引入 sys_yield 。但其实调用 sys_yield 不一定与外设有关 。随着内核功能的逐渐复杂,我们还会遇到 其他需要等待的事件 ,我们都可以立即调用 sys_yield 来避免等待过程造成的浪费。
sys_yield 的缺点

这一部分和我最开始考虑的关于实时性问题的思考是有一定关联的.
当应用调用它主动交出 CPU 使用权之后,它下一次再被允许使用 CPU 的时间点与内核的调度策略与当前的总体应用执行情况有关,很有可能远远迟于该应用等待的事件(如外设处理完请求)达成的时间点。这就会造成该应用的响应延迟不稳定或者很长。比如,设想一下,敲击键盘之后隔了数分钟之后才能在屏幕上看到字符,这已经超出了人类所能忍受的范畴。但也请不要担心,我们后面会有更加优雅的解决方案。
sys_yield 的标准接口

思考我们之前提到的两种syscall.
内核层 实现的:
  1. //os/syscall/mod
  2. const SYSCALL_WRITE: usize = 64;
  3. const SYSCALL_EXIT: usize = 93;
  4. mod fs;
  5. mod process;
  6. use fs::*;
  7. use process::*;
  8. /// handle syscall exception with `syscall_id` and other arguments
  9. pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
  10.     match syscall_id {
  11.         SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
  12.         SYSCALL_EXIT => sys_exit(args[0] as i32),
  13.         _ => panic!("Unsupported syscall_id: {}", syscall_id),
  14.     }
  15. }
复制代码
用户层 实现的:
  1. //user/syscall
  2. use core::arch::asm;
  3. const SYSCALL_WRITE: usize = 64;
  4. const SYSCALL_EXIT: usize = 93;
  5. fn syscall(id: usize, args: [usize; 3]) -> isize {
  6.     let mut ret: isize;
  7.     unsafe {
  8.         asm!(
  9.             "ecall",
  10.             inlateout("x10") args[0] => ret,
  11.             in("x11") args[1],
  12.             in("x12") args[2],
  13.             in("x17") id
  14.         );
  15.     }
  16.     ret
  17. }
  18. pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
  19.     syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
  20. }
  21. pub fn sys_exit(exit_code: i32) -> isize {
  22.     syscall(SYSCALL_EXIT, [exit_code as usize, 0, 0])
  23. }
复制代码
这里如果能理解到这里的同名的syscall,sys_write,sys_exit不是同一个函数,说明才 理解到位 .
现在要 继续实现 一个 系统调用 sys_yield.
于是要在 用户层 实现接口:
  1. // user/src/syscall.rs
  2. pub fn sys_yield() -> isize {
  3.     syscall(SYSCALL_YIELD, [0, 0, 0])
  4. }
  5. // user/src/lib.rs
  6. pub fn yield_() -> isize { sys_yield() }
复制代码
SYSCALL_YIELD同样是一个 需要定义 的常量.
这里有个小问题,由于yield是rust的 关键字 ,因此定义函数名字的时候 增加了一个_ .
于是在 内核层 的syscall里边也需要增加一个判别,现在我只写成伪代码,因为具体我也 不知道 参数怎么填写:
  1. pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
  2.     match syscall_id {
  3.                 // 这里是伪代码
  4.             SYSCALL_YIELD => sys_yield(...)
  5.             // 这里是伪代码
  6.         SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
  7.         SYSCALL_EXIT => sys_exit(args[0] as i32),
  8.         _ => panic!("Unsupported syscall_id: {}", syscall_id),
  9.     }
  10. }
复制代码
任务控制块与任务运行状态

思考上一章实现的AppManager,它包含了三部分:

  • 应用的 数量 .
  • 当前 运行应用.
  • 应用的 入口地址 .
但是考虑当前的任务的状态,可能 不是 简单地如上图两任务的情况一样,而是存在更多的任务和更复杂的情景.
想到我们本节 开头 时候所说,要建立一个 任务运行状态 的概念,把任务归类为如下几种状态:

  • 未初始化
  • 准备执行
  • 正在执行
  • 已退出
因此可以使用rust构建这样一个结构体:
  1. // os/src/task/task.rs
  2. #[derive(Copy, Clone, PartialEq)]
  3. pub enum TaskStatus {
  4.     UnInit, // 未初始化
  5.     Ready, // 准备运行
  6.     Running, // 正在运行
  7.     Exited, // 已退出
  8. }
复制代码
#[derive]这个注解有点类似于 Kotlin ,可以让 编译器自动 帮你实现一些方法:

  • 实现了 Clone Trait 之后就可以调用 clone 函数完成拷贝;
  • 实现了 PartialEq Trait 之后就可以使用 == 运算符比较该类型的两个实例,从逻辑上说只有 两个相等的应用执行状态才会被判为相等,而事实上也确实如此。
  • Copy 是一个标记 Trait,决定该类型在按值传参/赋值的时候采用移动语义还是复制语义。
回想起上一节提到的TaskContext,我们的 任务控制块 中需要保存的两部分也就知道了:

  • TaskContext保存任务上下文
  • TaskStatus保存任务状态
因此用rust构建这样一个结构体:
  1. // os/src/task/task.rs
  2. #[derive(Copy, Clone)]
  3. pub struct TaskControlBlock {
  4.     pub task_status: TaskStatus,
  5.     pub task_cx: TaskContext,
  6. }
复制代码
任务管理器

那么有了TaskControlBlock,就可以实现一个任务管理器.
任务管理器需要管理多个任务,于是就需要知道:

  • app 总数
  • 当前 的任务
  • 每个任务的 控制块

    • 任务 状态
    • 任务 上下文

这里使用了 常量和变量分离的方法 来实现它.
  1. // os/src/task/mod.rs
  2. pub struct TaskManager {
  3.     num_app: usize,
  4.     inner: UPSafeCell<TaskManagerInner>,
  5. }
  6. struct TaskManagerInner {
  7.     tasks: [TaskControlBlock; MAX_APP_NUM],
  8.     current_task: usize,
  9. }
复制代码
这是因为num_app是常量不需要变化,而inner是变量,需要用UPSafeCell,保证其 内部可变性单核时 安全的借用能力.
这里在官方文档里提到了:

  • 在第二章的AppManager是可以通过current_app推测上/下任务 的.
  • 但是在TaskManger里的TaskManagerInner的current_task是 只能 感知当前任务.
为TaskManager创建全局实例TASK_MANAGER,仍然使用 懒初始化 的方法:
  1. // os/src/task/mod.rs
  2. lazy_static! {
  3.     pub static ref TASK_MANAGER: TaskManager = {
  4.         let num_app = get_num_app();
  5.         let mut tasks = [
  6.             TaskControlBlock {
  7.                 task_cx: TaskContext::zero_init(),
  8.                 task_status: TaskStatus::UnInit
  9.             };
  10.             MAX_APP_NUM
  11.         ];
  12.         for i in 0..num_app {
  13.             tasks[i].task_cx = TaskContext::goto_restore(init_app_cx(i));
  14.             tasks[i].task_status = TaskStatus::Ready;
  15.         }
  16.         TaskManager {
  17.             num_app,
  18.             inner: unsafe { UPSafeCell::new(TaskManagerInner {
  19.                 tasks,
  20.                 current_task: 0,
  21.             })},
  22.         }
  23.     };
  24. }
复制代码
这个初始化顺序是:

  • 使用 上一节实现 的 get_num_app来获取任务数量
  • 创建一个TaskControlBlock的 数组 ,大小为 设定好的 MAX_APP_NUM.
  • 然后通过 上一节实现的 init_app_cx 来获取每个 已经加载到内存 的任务上下文.
  • 把所有的任务都 初始化Ready 状态.
  • 然后用 匿名函数 的方式得到的 task 和初始化为0的current_task创建一个匿名 TaskManagerInner,随后包裹在 UPSafeCell 之中,和num_app一起创建一个TaskManager,传给TASK_MANAGER.
实现 sys_yield 和 sys_exit 系统调用

类似于上一章实现的 内核层 的syscall函数中会根据 函数代码 调用函数.
我们需要理解到的一点就是:

  • 应用层 的 syscall 函数只是使用 ecall 触发Trap.
  • 内核层 的 syscall函数才是真的具体实现.
我们现在讲的是 内核层具体实现 调用的函数,其作用是在syscall中作为一个 分支 :
  1. // os/src/syscall/process.rs
  2. use crate::task::suspend_current_and_run_next;
  3. pub fn sys_yield() -> isize {
  4.     suspend_current_and_run_next();
  5.     0
  6. }
复制代码
这个是sys_yield,用于暂停当前的应用并切换到下个应用.
看它的具体实现实际上是 抽象化 了suspend_current_and_run_next接口,使得接口名称 一致 .
这时候要考虑我们上一章实现的sys_exit:
  1. //! App management syscalls
  2. use crate::loader::run_next_app;
  3. use crate::println;
  4. /// task exits and submit an exit code
  5. pub fn sys_exit(exit_code: i32) -> ! {
  6.     println!("[kernel] Application exited with code {}", exit_code);
  7.     run_next_app()
  8. }
复制代码
打印了LOG之后,使用run_next_app切换到下一个APP.
那么考虑到现在run_next_app已经不适合于当前的有 任务调度 的系统,所以也要对sys_exit的具体实现进行修改.
  1. // os/src/syscall/process.rs
  2. use crate::task::exit_current_and_run_next;
  3. pub fn sys_exit(exit_code: i32) -> ! {
  4.     println!("[kernel] Application exited with code {}", exit_code);
  5.     exit_current_and_run_next();
  6.     panic!("Unreachable in sys_exit!");
  7. }
复制代码
可以看到现在的具体实现是 抽象化 了exit_current_and_run_next接口,使得接口名称 一致 .
接下来我们只需要 具体实现 ,刚刚提到的两个接口就行了:
  1. // os/src/task/mod.rs
  2. pub fn suspend_current_and_run_next() {
  3.     mark_current_suspended();
  4.     run_next_task();
  5. }
  6. pub fn exit_current_and_run_next() {
  7.     mark_current_exited();
  8.     run_next_task();
  9. }
复制代码
这里摘抄出具体实现,但是具体实现中还是有三个函数 有待实现 :

  • mark_current_suspended
  • mark_current_exited
  • run_next_task
他们的具体实现要和上一章和上一节的实现对比:

  • 上一章: 加载应用 然后 修改程序指针 直接开始运行 .
  • 上一节:直接 修改程序指针 直接开始运行.
这一章的实现是不同的,是通过 修改用户的状态 ,解决.
  1. // os/src/task/mod.rs
  2. fn mark_current_suspended() {
  3.     TASK_MANAGER.mark_current_suspended();
  4. }
  5. fn mark_current_exited() {
  6.     TASK_MANAGER.mark_current_exited();
  7. }
  8. impl TaskManager {
  9.     fn mark_current_suspended(&self) {
  10.         let mut inner = self.inner.borrow_mut();
  11.         let current = inner.current_task;
  12.         inner.tasks[current].task_status = TaskStatus::Ready;
  13.     }
  14.     fn mark_current_exited(&self) {
  15.         let mut inner = self.inner.borrow_mut();
  16.         let current = inner.current_task;
  17.         inner.tasks[current].task_status = TaskStatus::Exited;
  18.     }
  19. }
复制代码
然后再通过run_next_task来(根据状态) 决定(可以叫调度吗?对的...不对...对的对的...不对) 下一步要运行哪个Task.
  1. // os/src/task/mod.rs
  2. fn run_next_task() {
  3.     TASK_MANAGER.run_next_task();
  4. }
  5. impl TaskManager {
  6.     fn run_next_task(&self) {
  7.         if let Some(next) = self.find_next_task() {
  8.             let mut inner = self.inner.exclusive_access();
  9.             let current = inner.current_task;
  10.             inner.tasks[next].task_status = TaskStatus::Running;
  11.             inner.current_task = next;
  12.             let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
  13.             let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
  14.             drop(inner);
  15.             // before this, we should drop local variables that must be dropped manually
  16.             unsafe {
  17.                 __switch(
  18.                     current_task_cx_ptr,
  19.                     next_task_cx_ptr,
  20.                 );
  21.             }
  22.             // go back to user mode
  23.         } else {
  24.             panic!("All applications completed!");
  25.         }
  26.     }
  27. }
复制代码
这里也是分为两部分:

  • run_next_task是对TASK_MANAGER.run_next_task();的封装.
  • 对TaskManager结构体的run_next_task方法的实现.

    • 首先就是if let这种模式匹配写法,最开始没有掌握rust的开发技术,因此不懂.

      • 这时候查阅Rust圣经.关于if let的部分.

        • 当只需要进行一次匹配的时候就可以使用这个方法.
        • 使用匹配是为了解决用简单的 ==  不能解决 复杂类型 匹配的情况.
        • 使用if let而不是match是为了解决只有None和非None两种情况的简单写法.

      • 查阅Rust圣经.关于Some的部分.

        • Option枚举有两种可能

          • Some代表有值,Some包裹的内容就是它的值

            • 一个在 定义 枚举类型的时候是 Some(T),T代表的是类型.Some(i32)就代表可以存储i32类型的值.
            • 在实例的时候Some(T)可以被实例化Some(3),就代表这个值存在且值为3.

          • None代表没值


      • 因此这一段的结果意思是:

        • 如果self.find_next_task()的结果不是None,那么对应的返回值应该是Some(next).
        • 下面的逻辑里的next就是返回的Some()里包裹的next.代表 下一个任务的任务号 .


    • 随后获取TaskManager.inner的单线程可变借用.
    • 从上一步的结果中获取 当前任务.
    • 下一个任务 的状态改为 运行中 .
    • 把当前任务号改为 刚刚获取到的下一个任务号 .
    • 分别获取 当前和下一个 任务上下文.
    • 主动释放获取到的TaskManager.inner.

      • 因为如果不去主动释放要等函数运行结束才能继续访问这个TaskManager.inner里的内容.
      • __switch需要操作TaskManager.inner里的task.task_cx的内容.

    • 使用 上一节实现的 __switch 完成任务栈切换,如果已经忘了可以回去看看.

可以看到find_next_task是一个重要的方法,它的实现是这样的:
  1. // os/src/task/mod.rs
  2. impl TaskManager {
  3.     fn find_next_task(&self) -> Option<usize> {
  4.         let inner = self.inner.exclusive_access();
  5.         let current = inner.current_task;
  6.         (current + 1..current + self.num_app + 1)
  7.             .map(|id| id % self.num_app)
  8.             .find(|id| {
  9.                 inner.tasks[*id].task_status == TaskStatus::Ready
  10.             })
  11.     }
  12. }
复制代码
它在获取TaskManager.inner的单线程可变借用之后对current_task为开头( 不包含它本身 )把整个数组看成一个 环形队列 然后逐个去 查询状态 , 直到找到 第一个 状态为准备的任务.
这里关于Rust语言,每次我们遇到不会了的,不是光把它搞懂,还要把它上一层的偏概念性的东西搞懂.
这里用到的就是 闭包迭代器 的知识:

  • 迭代器跟 for 循环颇为相似,都是去遍历一个集合,但是实际上它们存在不小的差别,其中最主要的差别就是:是否通过索引来访问集合

    • Iterator Trait 的 map 方法: Rust中的迭代器(Iterator)有一个map方法,它接收一个闭包(closure),并将迭代器中的每个元素传递给这个闭包。map方法会生成一个新的迭代器,其中的元素是闭包返回的结果。
    • 迭代器有一个find方法,它接收一个闭包作为参数。该闭包定义了要查找的条件,当迭代器中的元素满足这个条件时,find方法就会返回一个Option类型的结果,其中包含找到的第一个匹配项或者None如果没有任何元素满足条件。

  • 闭包一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值.

    • 有点像是某种C里的 函数宏 ,用 do...while封装起来的这种.因此可以偷取别的作用域的变量来用.

这张图太好了:
2.png

第一次进入用户态

回想上一章,我们使用run_next_app调用了__restore调用sret回到用户态.
目前我们要第一次进入用户态应该也需要sret才可以.
但是思考一下上一章我们学到的__switch的实现,显然它是 不改变 特权级的.
因此第一次进入用户态还是要依赖__restore.
为了使用__restore则需要构建Trap上下文,把 上一节 实现的init_app_cx,移动到loader.rs:
  1. // os/src/loader.rs
  2. pub fn init_app_cx(app_id: usize) -> usize {
  3.     KERNEL_STACK[app_id].push_context(
  4.         TrapContext::app_init_context(get_base_i(app_id), USER_STACK[app_id].get_sp()),
  5.     )
  6. }
复制代码
再给TaskContext构造一个 构建第一次执行任务的上下文 的方法:
  1. // os/src/task/context.rs
  2. impl TaskContext {
  3.     pub fn goto_restore(kstack_ptr: usize) -> Self {
  4.         extern "C" { fn __restore(); }
  5.         Self {
  6.             ra: __restore as usize,
  7.             sp: kstack_ptr,
  8.             s: [0; 12],
  9.         }
  10.     }
  11. }
复制代码
在这个操作之中,

  • 传入了一个 内核栈指针 .
  • 使用如下内容构建一个 TaskContext.

    • 内核栈指针作为 任务上下文的栈指针 .
    • __restore的函数地址作为 函数调用完毕返回地址 .也就是说 __switch的ret执行完毕之后执行__restore.
    • 空的s0~s12.

需要注意的是, __restore 的实现需要做出变化:它 不再需要 在开头 mv sp, a0 了。因为在 __switch 之后,sp 就已经正确指向了我们需要的 Trap 上下文地址。
然后在创建 TaskManager 的全局实例 TASK_MANAGER 的时候为 每个任务上下文 , 初始化为由如下内容组成的TaskContext:

  • 链接进去 的任务内存位置决定的 每个任务的内核栈指针 作为栈指针.
  • __restore作为 函数调用完毕返回地址 .
  • 空的s0~s12.
为TaskContext构建一个 执行第一个任务 的方法:
  1. impl TaskManager {
  2.     fn run_first_task(&self) -> ! {
  3.         let mut inner = self.inner.exclusive_access();
  4.         let task0 = &mut inner.tasks[0];
  5.         task0.task_status = TaskStatus::Running;
  6.         let next_task_cx_ptr = &task0.task_cx as *const TaskContext;
  7.         drop(inner);
  8.         let mut _unused = TaskContext::zero_init();
  9.         // before this, we should drop local variables that must be dropped manually
  10.         unsafe {
  11.             __switch(
  12.                 &mut _unused as *mut TaskContext,
  13.                 next_task_cx_ptr,
  14.             );
  15.         }
  16.         panic!("unreachable in run_first_task!");
  17.     }
复制代码
这段代码可以这样理解:

  • 获取 单线程的借用 .
  • 获取第一个 任务块的指针 .
  • 随后把这个任务设置为 运行状态 .
  • 获取这个任务的 上下文 .
  • 由于后续要使用__switch因此需要 主动释放 这个借用.
  • 使用__switch调用

    • 由zero_init构建的一个 全空 的上下文.
    • 第一个任务 的上下文.

这时候这个执行顺序有点乱了,我尝试画一个流程图.
首先是这章实现的结构体TaskManager的结构:
3.png

初始化 的流程为:
4.png

初始化后的TASK_MANAGER:
5.png

调用run_fist_app之后发生了什么:
6.png

这时候考虑APP发生挂起的时候会发生什么:
7.png

尝试构建本章实验

首先先看用户层的APP,回想对于用户层 有影响 的改动:

  • 实现yield接口以挂起应用程序.
  • 这里 注意 修改exit的实现并 影响用户层.
其实从 写代码的用户 来看对用户层没有任何影响的.
但是如果我们不调用yield我们怎么体现我们这章的内容呢?那不是还是 顺序执行 ?
"诶呦,这不是显得您枪法准吗?"------不执行yeild的应用程序
8.png

"不对啊,我是主角啊!"------协作式调度
9.png

创建APP

于是我们需要重新设计几个APP,而且可以在 执行耗时操作 之后主动使用yield释放CPU.
这里官方代码设计了00write_a.rs,01write_b.rs,02write_c.rs,三个APP.
分别用于打印HEIGHT行有WIDTH个的A,B和C.并且都调用了yield,用于挂起应用,这样和顺序执行相比就会有区别.
这里我们把原来的user/src/bin下的APP全部都移到user/src/temp下,然后在user/src/bin里创建如上三个文件.
实现用户层yield

和上一章一样,只要明白用户层的调用是套壳的就很容易写出来:

  • 约定调用号
  • 调用syscall
  • 封装成函数
在user/src/syscall.rs里完成 前两步 :
  1. const SYSCALL_YIELD: usize = 124;
  2. pub fn sys_yield() -> isize {
  3.     syscall(SYSCALL_YIELD, [0, 0, 0])
  4. }
复制代码
这个传入的参数其实也很 暧昧 , 因为实际上这里这么写相当于 默认了 不需要传输其他参数到内核层.
.
这里理解为 只是一步挂起操作不需要更多信息 .
在user/src/lib.rs里完成 第三步:
  1. pub fn yield_() -> isize {
  2.     sys_yield()
  3. }
复制代码
编译APP

这时候进行编译,以得到.bin文件:
  1. cd user
  2. make clean
  3. make build
复制代码
实现内核层的操作

syscall模块

对应用户层,真正执行用户层的指令的是内核层:

  • 对应约定好的调用号调用函数
  • 实现需要调用的函数

    • 修改sys_exit
    • 实现sys_yield

在os/src/syscall/mod.rs里完成 第一步 :
  1. const SYSCALL_YIELD: usize = 124;
  2. pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
  3.     match syscall_id {
  4.         SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
  5.         SYSCALL_EXIT => sys_exit(args[0] as i32),
  6.         SYSCALL_YIELD => sys_yield(),
  7.         _ => panic!("Unsupported syscall_id: {}", syscall_id),
  8.     }
  9. }
复制代码
在os/src/syscall/process.rs里完成 第二步:
  1. use crate::task::{exit_current_and_run_next, suspend_current_and_run_next};
  2. /// task exits and submit an exit code
  3. pub fn sys_exit(exit_code: i32) -> ! {
  4.     println!("[kernel] Application exited with code {}", exit_code);
  5.     exit_current_and_run_next();
  6.     panic!("Unreachable in sys_exit!");
  7. }
  8. /// current task gives up resources for other tasks
  9. pub fn sys_yield() -> isize {
  10.     suspend_current_and_run_next();
  11.     0
  12. }
复制代码
这里调用的exit_current_and_run_next和suspend_current_and_run_next都需要在task模块里是实现.
task模块

10.png

回忆有关于task的冗杂的数据结构,我们全部都要在这个模块里实现.

  • 首先需要创建os/src/task文件夹
  • 在文件夹中创建context.rs用来实现数据结构TaskContext.

    • 储存如图上所示的上下文
    • 因为TaskContext在第一次初始化的时候需要, __swtich调用一个空的TaskContext和第一个任务的TaskContext进行切换

      • 实现一个生成 空结构 的方法
      • 实现一个 每个任务的初始化 方法


  • 在文件夹中创建task.rs实现枚举类型 TaskControlBlock用来保存单个任务信息.

    • 实现TaskStatus枚举任务状态
    • 用TaskContext保存任务上下文.

  • 封装__switch,实现上下文切换.
  • 在文件夹中创建mod.rs用来实现数据结构TaskManager

    • 储存num_app
    • 实现数据结构TaskManagerInner

      • 储存当前任务current_task
      • 实现数据结构TaskControlBlock,储存TaskControlBlock的数组tasks

    • 用UPSafeCell包裹TaskManagerInner
    • 它需要有几个功能,要实现成对应的方法:

      • 挂起/关闭任务

        • 挂起并执行下一个任务

          • 标记挂起
          • 寻找下一个任务
          • 执行下一个任务

        • 关闭并执行下一个任务

          • 标记关闭
          • 寻找下一个任务
          • 执行下一个任务


      • 执行 第一个 任务


  • 在mod.rs中懒初始化一个全局的TaskManager
context.rs

如上所述:
  1. //! Implementation of [`TaskContext`]
  2. /// Task Context
  3. #[derive(Copy, Clone)]
  4. #[repr(C)]
  5. pub struct TaskContext {
  6.     /// return address ( e.g. __restore ) of __switch ASM function
  7.     ra: usize,
  8.     /// kernel stack pointer of app
  9.     sp: usize,
  10.     /// callee saved registers:  s 0..11
  11.     s: [usize; 12],
  12. }
  13. impl TaskContext {
  14.     /// init task context
  15.     pub fn zero_init() -> Self {
  16.         Self {
  17.             ra: 0,
  18.             sp: 0,
  19.             s: [0; 12],
  20.         }
  21.     }
  22.     /// set task context {__restore ASM funciton, kernel stack, s_0..12 }
  23.     pub fn goto_restore(kstack_ptr: usize) -> Self {
  24.         extern "C" {
  25.             fn __restore();
  26.         }
  27.         Self {
  28.             ra: __restore as usize,
  29.             sp: kstack_ptr,
  30.             s: [0; 12],
  31.         }
  32.     }
  33. }
复制代码
这里需要注意的就是有类似于JAVA里的直接对象传递,深浅拷贝,C++的拷贝构造相关的东西.
这周复杂的结构体一定要使用#[derive(Copy, Clone)]为它实现拷贝方法.#[derive(Copy, Clone)] 是 Rust 语言中的一个属性宏,用于自动为结构体(struct)或枚举(enum)实现 Copy 和 Clone trait。

  • Copy trait 表示类型的数据可以被复制,而不需要所有权转移。当一个值类型实现了 Copy,意味着你可以无需任何额外成本地将该值复制给另一个变量。这通常适用于简单的数据类型,如整数、浮点数等,因为它们的复制成本较低。
  • Clone trait 则提供了一个 .clone() 方法,用于创建类型的一个副本。与 Copy 不同,Clone 可以用于更复杂的数据类型,包括那些包含指针或者需要较深拷贝的数据结构。使用 .clone() 方法会调用分配器,因此可能有额外的性能开销。
task.rs

如上所述:
  1. //! Types related to task management
  2. use super::TaskContext;
  3. #[derive(Copy, Clone)]
  4. pub struct TaskControlBlock {
  5.     pub task_status: TaskStatus,
  6.     pub task_cx: TaskContext,
  7. }
  8. #[derive(Copy, Clone, PartialEq)]
  9. pub enum TaskStatus {
  10.     UnInit,
  11.     Ready,
  12.     Running,
  13.     Exited,
  14. }
复制代码
switch.rs

如上所述,需要封装一个switch函数来保证在调用__switch时的自动上下文保存.
因此需要创建os/src/task/switch.rs和os/src/task/switch.S用以实现.
switch.rs内容,明确 指定 了传入的两个参数的指针类型:
  1. use super::TaskContext;
  2. use core::arch::global_asm;
  3. global_asm!(include_str!("switch.S"));
  4. extern "C" {
  5.     pub fn __switch(current_task_cx_ptr: *mut TaskContext, next_task_cx_ptr: *const TaskContext);
  6. }
复制代码
switch.S内容,就是上一节讲的__switch函数:
  1. .altmacro
  2. .macro SAVE_SN n
  3.     sd s\n, (\n+2)*8(a0)
  4. .endm
  5. .macro LOAD_SN n
  6.     ld s\n, (\n+2)*8(a1)
  7. .endm
  8.     .section .text
  9.     .globl __switch
  10. __switch:
  11.     # __switch(
  12.     #     current_task_cx_ptr: *mut TaskContext,
  13.     #     next_task_cx_ptr: *const TaskContext
  14.     # )
  15.     # save kernel stack of current task
  16.     sd sp, 8(a0)
  17.     # save ra & s0~s11 of current execution
  18.     sd ra, 0(a0)
  19.     .set n, 0
  20.     .rept 12
  21.         SAVE_SN %n
  22.         .set n, n + 1
  23.     .endr
  24.     # restore ra & s0~s11 of next execution
  25.     ld ra, 0(a1)
  26.     .set n, 0
  27.     .rept 12
  28.         LOAD_SN %n
  29.         .set n, n + 1
  30.     .endr
  31.     # restore kernel stack of next task
  32.     ld sp, 8(a1)
  33.     ret
复制代码
因为每次调用__restore的时候已经由__switch把sp指向TrapContext了,因此需要去掉:
  1. mv sp, a0
复制代码
在把它引用为一个函数时也要变化:
  1. // 之前
  2. extern "C" {
  3.         fn __restore(cx_addr: usize);
  4. }
  5. // 现在
  6. extern "C" {
  7.         fn __restore();
  8. }
复制代码
mod.rs

如上所述,实现这两个结构体:
  1. pub struct TaskManager {
  2.     num_app: usize,
  3.     inner: UPSafeCell<TaskManagerInner>,
  4. }
  5. struct TaskManagerInner {
  6.     current_task: usize,
  7.     tasks: [TaskControlBlock; MAX_APP_NUM],
  8. }
复制代码
实现的方法:
  1. impl TaskManager
  2. {
  3.     fn run_first_task(&self) ->!
  4.     {
  5.         let mut inner = self.inner.exclusive_access();
  6.         let task0 = &mut inner.tasks[0];
  7.         task0.task_status = TaskStatus::Running;
  8.         let next_task_cx_ptr = &task0.task_cx as *const TaskContext;
  9.         let mut _unused = TaskContext::zero_init();
  10.         drop(inner);
  11.         unsafe
  12.         {
  13.             __switch(&mut _unused as *mut TaskContext, next_task_cx_ptr)
  14.         }
  15.         panic!("unreachable in run_first_task");   
  16.     }
  17.     fn mark_current_suspended(&self)
  18.     {
  19.         let mut inner = self.inner.exclusive_access();
  20.         let current = inner.current_task;
  21.         inner.tasks[current].task_status = TaskStatus::Ready;
  22.     }
  23.     fn mark_current_exited(&self)
  24.     {
  25.         let mut inner = self.inner.exclusive_access();
  26.         let current = inner.current_task;
  27.         inner.tasks[current].task_status = TaskStatus::Exited;
  28.     }
  29.     fn find_next_task(&self) -> Option<usize>
  30.     {
  31.         let inner = self.inner.exclusive_access();
  32.         let current = inner.current_task;
  33.         (current + 1..current+1+self.num_app)
  34.             .map(|id| id%self.num_app)
  35.             .find(|id| inner.tasks[*id].task_status == TaskStatus::Ready)
  36.     }
  37.     fn run_next_task(&self)
  38.     {
  39.         if let Some(next) = self.find_next_task()
  40.         {
  41.             let mut inner = self.inner.exclusive_access();
  42.             let current = inner.current_task;
  43.             inner.current_task = next;
  44.             inner.tasks[next].task_status = TaskStatus::Running;
  45.             let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
  46.             let next_task_cx_ptr = &mut inner.tasks[next].task_cx as *const TaskContext;
  47.             drop(inner);
  48.             unsafe
  49.             {
  50.                 __switch(current_task_cx_ptr, next_task_cx_ptr)
  51.             }
  52.         }
  53.         else
  54.         {
  55.             println!("All applications completed!");
  56.             shutdown(false);
  57.         }
  58.     }
  59. }
  60. lazy_static!
  61. {
  62.     /// 全局单例的任务管理器
  63.     pub static ref TASK_MANAGER: TaskManager =
  64.     {
  65.         let num_app = get_num_app();
  66.         let mut tasks = [TaskControlBlock{
  67.             task_status: TaskStatus::UnInit,
  68.             task_cx: TaskContext::zero_init(),
  69.         }
  70.         ; MAX_APP_NUM];
  71.         for(i,task) in tasks.iter_mut().enumerate()
  72.         {
  73.             task.task_cx = TaskContext::goto_restore(init_app_cx(i));
  74.             task.task_status = TaskStatus::Ready;
  75.         }
  76.         TaskManager
  77.         {
  78.             num_app,
  79.             inner: unsafe
  80.             {
  81.                 UPSafeCell::new(TaskManagerInner
  82.                 {
  83.                     tasks,
  84.                     current_task: 0,
  85.                 })
  86.             }
  87.         }
  88.     };
  89. }
复制代码
这里注意使用__switch之前一定要主动drop(inner).
懒初始化的单例对象:
  1. lazy_static!
  2. {
  3.     pub static ref TASK_MANAGER: TaskManager =
  4.     {
  5.         let num_app = get_num_app();
  6.         let mut tasks = [TaskControlBlock{
  7.             task_status: TaskStatus::UnInit,
  8.             task_cx: TaskContext::zero_init(),
  9.         }
  10.         ; MAX_APP_NUM];
  11.         for(i,task) in tasks.iter_mut().enumerate()
  12.         {
  13.             task.task_cx = TaskContext::goto_restore(init_app_cx(i));
  14.             task.task_status = TaskStatus::Ready;
  15.         }
  16.         TaskManager
  17.         {
  18.             num_app,
  19.             inner: unsafe
  20.             {
  21.                 UPSafeCell::new(TaskManagerInner
  22.                 {
  23.                     tasks,
  24.                     current_task: 0,
  25.                 })
  26.             }
  27.         }
  28.     };
  29. }
复制代码
对这个单例对象的操作的封装:
  1. pub fn run_first_task() {
  2.     TASK_MANAGER.run_first_task();
  3. }
  4. fn mark_current_suspended()
  5. {
  6.     TASK_MANAGER.mark_current_suspended();
  7. }
  8. fn mark_current_exited()
  9. {
  10.     TASK_MANAGER.mark_current_exited();
  11. }
  12. fn run_next_task()
  13. {
  14.     TASK_MANAGER.run_next_task();
  15. }
  16. pub fn suspend_current_and_run_next()
  17. {
  18.     mark_current_suspended();
  19.     run_next_task();
  20. }
  21. pub fn exit_current_and_run_next()
  22. {
  23.     mark_current_exited();
  24.     run_next_task();
  25. }
复制代码
修改Trap调用

因为现在已经没有run_next_app了只有新实现的suspend_current_and_run_next和exit_current_and_run_next.
因此把出现非法调用的切换APP的API修改为exit_current_and_run_next..
  1. // use crate::loader::run_next_app;
  2. #[no_mangle]
  3. /// handle an interrupt, exception, or system call from user space
  4. pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
  5.     let scause = scause::read(); // get trap cause
  6.     let stval = stval::read(); // get extra value
  7.     match scause.cause() {
  8.         Trap::Exception(Exception::UserEnvCall) => {
  9.             cx.sepc += 4;
  10.             cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
  11.         }
  12.         Trap::Exception(Exception::StoreFault) | Trap::Exception(Exception::StorePageFault) => {
  13.             println!("[kernel] PageFault in application, kernel killed it.");
  14.             exit_current_and_run_next();
  15.         }
  16.         Trap::Exception(Exception::IllegalInstruction) => {
  17.             println!("[kernel] IllegalInstruction in application, kernel killed it.");
  18.             exit_current_and_run_next();
  19.         }
  20.         _ => {
  21.             panic!(
  22.                 "Unsupported trap {:?}, stval = {:#x}!",
  23.                 scause.cause(),
  24.                 stval
  25.             );
  26.         }
  27.     }
  28.     cx
  29. }
复制代码
这里在查看参考代码的时候看到一个很好的操作,虽然参考代码是出现权限错误就直接报panic.
  1. #[no_mangle]
  2. /// handle an interrupt, exception, or system call from user space
  3. pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
  4.     let scause = scause::read(); // get trap cause
  5.     let stval = stval::read(); // get extra value
  6.     match scause.cause() {
  7.         Trap::Exception(Exception::UserEnvCall) => {
  8.             cx.sepc += 4;
  9.             cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
  10.         }
  11.         Trap::Exception(Exception::StoreFault) | Trap::Exception(Exception::StorePageFault) => {
  12.             println!("[kernel] PageFault in application, bad addr = {:#x}, bad instruction = {:#x}, kernel killed it.", stval, cx.sepc);
  13.             panic!("[kernel] Cannot continue!");
  14.             //run_next_app();
  15.         }
  16.         Trap::Exception(Exception::IllegalInstruction) => {
  17.             println!("[kernel] IllegalInstruction in application, kernel killed it.");
  18.             panic!("[kernel] Cannot continue!");
  19.             //run_next_app();
  20.         }
  21.         _ => {
  22.             panic!(
  23.                 "Unsupported trap {:?}, stval = {:#x}!",
  24.                 scause.cause(),
  25.                 stval
  26.             );
  27.         }
  28.     }
  29.     cx
  30. }
复制代码
这里的:
  1. println!("[kernel] PageFault in application, bad addr = {:#x}, bad instruction = {:#x}, kernel killed it.", stval, cx.sepc);
复制代码
sepc保存的是 发生异常的指令 .
stval保存的是了陷入(trap)的附加信息:

  • 地址例外中出错 的地址
  • 发生非法指令例外的指令本身
  • 对于其他异常,它的值为 0
有了这两个寄存器的硬件帮助,我们实现一些东西也变得简单了起来,这里可以用于完成 第二章 作业第五题.
修改main函数

把run_next_app改成run_first_task即可:
  1. /// the rust entry-point of os
  2. #[no_mangle]
  3. pub fn rust_main() -> ! {
  4.     extern "C" {
  5.         fn stext(); // begin addr of text segment
  6.         fn etext(); // end addr of text segment
  7.         fn srodata(); // start addr of Read-Only data segment
  8.         fn erodata(); // end addr of Read-Only data ssegment
  9.         fn sdata(); // start addr of data segment
  10.         fn edata(); // end addr of data segment
  11.         fn sbss(); // start addr of BSS segment
  12.         fn ebss(); // end addr of BSS segment
  13.         fn boot_stack_lower_bound(); // stack lower bound
  14.         fn boot_stack_top(); // stack top
  15.     }
  16.     clear_bss();
  17.     logging::init();
  18.     println!("[kernel] Hello, world!");
  19.     trace!(
  20.         "[kernel] .text [{:#x}, {:#x})",
  21.         stext as usize,
  22.         etext as usize
  23.     );
  24.     debug!(
  25.         "[kernel] .rodata [{:#x}, {:#x})",
  26.         srodata as usize, erodata as usize
  27.     );
  28.     info!(
  29.         "[kernel] .data [{:#x}, {:#x})",
  30.         sdata as usize, edata as usize
  31.     );
  32.     warn!(
  33.         "[kernel] boot_stack top=bottom={:#x}, lower_bound={:#x}",
  34.         boot_stack_top as usize, boot_stack_lower_bound as usize
  35.     );
  36.     error!("[kernel] .bss [{:#x}, {:#x})", sbss as usize, ebss as usize);
  37.     trap::init();
  38.     println!("[kernel] trap init end");
  39.     loader::load_apps();
  40.     task::run_first_task();
  41.     panic!("Unreachable in rust_main!");
  42. }
复制代码
编译运行
  1. make run
复制代码
运行结果:
  1. [rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
  2. .______       __    __      _______.___________.  _______..______   __
  3. |   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
  4. |  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
  5. |      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
  6. |  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
  7. | _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
  8. [rustsbi] Implementation     : RustSBI-QEMU Version 0.2.0-alpha.2
  9. [rustsbi] Platform Name      : riscv-virtio,qemu
  10. [rustsbi] Platform SMP       : 1
  11. [rustsbi] Platform Memory    : 0x80000000..0x88000000
  12. [rustsbi] Boot HART          : 0
  13. [rustsbi] Device Tree Region : 0x87000000..0x87000f02
  14. [rustsbi] Firmware Address   : 0x80000000
  15. [rustsbi] Supervisor Address : 0x80200000
  16. [rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
  17. [rustsbi] pmp02: 0x80000000..0x80200000 (---)
  18. [rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
  19. [rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
  20. [kernel] Hello, world!
  21. [kernel] trap init end
  22. AAAAAAAAAA [1/5]
  23. BBBBBBBBBB [1/2]
  24. CCCCCCCCCC [1/3]
  25. AAAAAAAAAA [2/5]
  26. BBBBBBBBBB [2/2]
  27. CCCCCCCCCC [2/3]
  28. AAAAAAAAAA [3/5]
  29. Test write_b OK!
  30. [kernel] Application exited with code 0
  31. CCCCCCCCCC [3/3]
  32. AAAAAAAAAA [4/5]
  33. Test write_c OK!
  34. [kernel] Application exited with code 0
  35. AAAAAAAAAA [5/5]
  36. Test write_a OK!
  37. [kernel] Application exited with code 0
  38. All applications completed!
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册