文章目录
【总体思路】
- 编译:应用程序和内核独立编译,合并为一个镜像
- 编译:应用程序需要各自的起始地址
- 构造:系统调用服务请求接口,任务的管理与初始化
- 构造:任务控制块,任务的上下文/状态管理
- 运行:特权级切换,任务与OS相互切换
- 运行:任务通过系统调用/中断实现主动/被动切换
为此需要实现
- 一次性加载所有用户程序,减少任务切换开销;
- 支持任务切换机制,保存切换前后程序上下文;
- 支持程序主动放弃处理器,实现 yield 系统调用;
- 以时间片轮转算法调度用户程序,实现资源的时分复用。
【结构框架】
── os3-ref
├── 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
【多道程序放置与加载】
😃 多道程序放置
对于Dunkleosteus OS
内核让所有的应用都共享同一个固定的起始地址。 正因如此,内存中同时最多只能驻留一个应用。
要一次加载运行多个程序,就要求每个用户程序被内核加载到内存中的起始地址都不同。 为此,我们编写脚本 user/build.py
为每个应用定制各自的起始地址。
import os
base_address = 0x80400000
step = 0x20000
linker = "src/linker.ld"
app_id = 0
apps = os.listdir("build/app")
apps.sort()
chapter = os.getenv("CHAPTER")
for app in apps:
app = app[: app.find(".")]
os.system(
"cargo rustc --bin %s --release -- -Clink-args=-Ttext=%x"
% (app, base_address + step * app_id)
)
print(
"[build.py] application %s start with address %s"
% (app, hex(base_address + step * app_id))
)
if chapter == '3':
app_id = app_id + 1
- 第 8 行:
os.listdir()
方法用于返回指定的文件夹包含的文件或文件夹的名字的列表。 - 第 9 行:对文件的列表进行排序。
- 第 10 行:返回环境变量键的值,在这里就是返回
CHAPTER
这个Key
的Value。
- 第 13 行:取得app这个文件的名字。
- 第 14 行:system函数可以将字符串转化成命令在服务器上运行;其原理是每一条system函数执行时,其会创建一个子进程在系统上执行命令行,子进程的执行结果无法影响主进程。
- 第 15 - 16 行:执行
cargo rustc --bin %s --release -- -Clink-args=-Ttext=%x
系统命令,可以举个例子,假设hello_world
为内部运行的第一个函数,则运行cargo rustc --bin hello_world --release -- -Clink-args=-Ttext=0x80400000
- cargo rustc [OPTIONS] [–] [args]…
- cargo rustc --bin:针对这个目录生成二进制文件
- cargo rustc --release:生成的target文件夹下不再有debug目录,替代的是release目录
- -Clink-args=-Ttext=0x80420000:使用我们的链接脚本,-Ttext链接时将初始地址重定向为0x80420000。
- 第 22 - 23 行:在
OS
的makefile
中增加CHAPTER ?= 3
,对应于22行的条件判断使得app_id
会不停的增加,下个应用的地址就会在之上的偏移0x20000
。下一个链接时的地址空间就为0x80420000。
它的思路很简单,对于每一个应用程序,使用
cargo rustc
单独编译, 用-Clink-args=-Ttext=xxxx
选项指定链接时.text
段(代码段)的地址为0x80400000 + app_id * 0x20000
。最后将这些编译好的bin
文件放在/user/build/bin
目录下,此时这些文件相当于可执行文件,但还没有被加载进内存中执行,所以需要后续的程序加载任务。
😃 多道程序加载
与Dunkleosteus OS
不同的是将batch.rs
拆分为了loader.rs
和task
,loader.rs
负责启动时加载应用程序,task
负责调度和切换。
//! os/src/loader.rs
//! Loading user applications into memory
//! For chapter 3, user applications are simply part of the data included in the
//! kernel binary, so we only need to copy them to the space allocated for each
//! app to load them. We also allocate fixed spaces for each task's
//! [`KernelStack`] and [`UserStack`].
use crate::config::*;
use crate::trap::TrapContext;
#[repr(align(4096))]
#[derive(Copy, Clone)]
/// kernel stack structure
struct KernelStack {
data: [u8; KERNEL_STACK_SIZE],
}
#[repr(align(4096))]
#[derive(Copy, Clone)]
/// user stack structure
struct UserStack {
data: [u8; USER_STACK_SIZE],
}
/// kernel stack instance
static KERNEL_STACK: [KernelStack; MAX_APP_NUM] = [KernelStack {
data: [0; KERNEL_STACK_SIZE],
}; MAX_APP_NUM];
/// user stack instance
static USER_STACK: [UserStack; MAX_APP_NUM] = [UserStack {
data: [0; USER_STACK_SIZE],
}; MAX_APP_NUM];
impl KernelStack {
fn get_sp(&self) -> usize {
self.data.as_ptr() as usize + KERNEL_STACK_SIZE
}
pub fn push_context(&self, trap_cx: TrapContext) -> usize {
let trap_cx_ptr = (self.get_sp() - core::mem::size_of::<TrapContext>()) as *mut TrapContext;
unsafe {
*trap_cx_ptr = trap_cx;
}
trap_cx_ptr as usize
}
}
impl UserStack {
fn get_sp(&self) -> usize {
self.data.as_ptr() as usize + USER_STACK_SIZE
}
}
/// Get base address of app i.
fn get_base_i(app_id: usize) -> usize {
APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT
}
/// Get the total number of applications.
pub fn get_num_app() -> usize {
extern "C" {
fn _num_app();
}
unsafe { (_num_app as usize as *const usize).read_volatile() }
}
/// 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 {
core::arch::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 app info with entry and sp and save `TrapContext` in kernel stack
pub fn init_app_cx(app_id: usize) -> usize {
KERNEL_STACK[app_id].push_context(TrapContext::app_init_context(
get_base_i(app_id),
USER_STACK[app_id].get_sp(),
))
}
-
第 14 - 16 行:声明内核栈结构体,内部大小为
8K
。 -
第 21 - 23 行:声明用户栈结构体,内部大小为
4K
。 -
第 26 - 28 行:为内核栈实例化,共有
MAX_APP_NUM
个内核栈,并初始化内核栈为0。 -
第 31 - 33 行:为用户栈实例化,共有
MAX_APP_NUM
个用户栈,并初始化用户栈为0。 -
第 35 - 46 行:为内核栈实现
get_sp
和push_context
方法,get_sp
方法获取当前app
内核栈的指针。push_context
方法将传入的trap_cx
保存在当前app
内核栈的开头。- 第 37 行:返回当前
app
的内核栈指针,指向8k
大小的开头处。 - 第 40 行:获得
trap_cx_ptr
,地址就是8k
大小的开头处减去TrapContext
大小的位置。 - 第 41 - 43 行:将
trap_cx
放入的trap_cx_ptr
的地址处。所以现在从8k大小的开头处 - trap_cx_ptr
之间存放的就是传入进来的trap_cx
。 - 第 44 行:返回
trap_cx_ptr
这个位置。
- 第 37 行:返回当前
-
第 48 - 52 行:为用户栈实现
get_sp
方法,也就是返回栈指针,指向当前app
其用户栈4k
大小的开头处。 -
第 55 - 57 行:获取
app_id
的基地址,地址为0x80400000+ app_id * 0x20000
。这个地址与我们上面python
脚本链接时候的文件地址保持一致,需要从这个地方加载之前编译好的bin
文件。 -
第 60 - 65 行:从
link_app.S
汇编文件_num_app
字段的第一行.quad 12
的中获得当前app
的总数量。 -
第 69 - 93 行:load_apps函数,将所有用户程序在内核初始化的时候一并加载进内存。第 i 个应用被加载到以物理地址
base_i
开头的一段物理内存上。- 第 70 - 72 行:引出
_num_app
这个字段。 - 第 73 行:得到
_num_app
指针。 - 第 74 行:获取
app
的总体数量。 - 第 75 行:指向
app_0_start
。 - 第 77 - 79 行:清除
icache
。 - 第 82 行:获取
app_i
的基地址。( i 从0-num_app
) - 第 84 - 85 行:是把从82行获得的基地址到基地址+
0x20000
这段区域清零。这段地址目的是存放第i+1
个app
。 - 第 87 - 89 行:获取当前
app
代码段的切片。 - 第 90 行:获取该
app
应该存放基地址的切片,大小为0x20000
。 - 第 91 行:将该
app
切片存入进去。
- 第 70 - 72 行:引出
-
第 96 - 101 行:获取应用程序信息的入口和sp,并保存
TrapContext
在内核堆栈。- 第 98 行:
get_base_i
获取当前app_id
运行时的基地址。 - 第 99 行:获取
app_id
的用户栈地址。 - 第 97 行:对
TrapContext
进行初始化,将传入的app_id
运行时的基地址放到sepc
(记录Trap
发生之前执行的最后一条指令的地址)字段。传入的栈指针放到x[2]
。SPP
设为User
状态。再将其放到内核栈的开头部分。
- 第 98 行:
load_apps
函数主要是将user/build/bin
目录下编译好的各个APP
的.bin
文件拷贝进以0x80400000
为起始的内存地址中,每个app
之间相距0x20000
大小。这下就可以通过将入口地址改到这些地址分别执行相应的之前编好的app
了。
init_app_cx
函数依据传入进去的app_id
初始化对应的Trap
上下文到内核栈空间中。首先将app_id
对应的app
运行起始地址拷贝到sepc
字段,再将app_id
对应的用户栈sp
指针拷贝到x[2]
字段。之后将整个TrapContext
拷贝到app_id
所对应的内核栈头部。
loader.rs
中为每个app
都声明了对应的内核栈和用户栈。内核栈每个空间大小8k
,内核栈每个空间大小4k
。
【任务切换】
即应用在运行中主动或被动地交出 CPU 的使用权,内核可以选择另一个程序继续执行。 内核需要保证用户程序两次运行期间,任务上下文(如寄存器、栈等)保持一致。
😃 任务切换的具体实现
任务切换与上一章提及的 Trap 控制流切换相比,有如下异同:
- 与 Trap 切换不同,它不涉及特权级切换(因为完全是在S模式下进行的切换),部分由编译器完成;
- 与 Trap 切换相同,它对应用是透明的。
我们需要在 __switch
中保存 CPU 的某些寄存器,它们就是 任务上下文 (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
-
第 4 行:定义
SAVE_SN
宏,调用携带的参数为n。 -
第 5 行:
SAVE_SN
宏具有的汇编语句为sd s\n, (\n+2)*8(a0)
-
第 6 行:结束宏定义。
-
第 7 行:定义
LOAD_SN
宏,调用携带的参数为n。 -
第 8 行:
LOAD_SN
宏具有的汇编语句为ld s\n, (\n+2)*8(a1)
-
第 9 行:结束宏定义。
-
第 10 行:进入代码段。
-
第 11 行:声明全局符号
__switch
。 -
第 18 - 25 行:保存当前的上下文(14个寄存器值)到
current_task_cx_ptr
这个传入的参数所指向的地址中。- 第 18 行:存储
sp
为a0
中的sp
。 - 第 20 行:存储
ra
为a0
中的ra
。 - 第 21 - 25 行:通过
SAVE_SN
这个宏定义来把s0 - s11
依次保存到a0
中所对应的寄存器中,通过修改保存地址的方式确定保存到对应的位置。
上述所述之所以将当前的状态保存到
a0
当中去是因为a0
里面现在存在的是当调用__switch
时所传入的第一个参数也就是current_task_cx_ptr
。前面的保存就是将当前的这14个寄存器的值保存到所传入的这个结构体中。同理所示,a1
保存的是next_task_cx_ptr
传入的第二个参数。 - 第 18 行:存储
-
第 27 - 34 行:从
next_task_cx_ptr
这个传入的参数所指向的地址中取出当前的上下文(14个寄存器值)。- 第 27 行:从
a1
中恢复ra
寄存器值。它记录了__switch
函数返回之后应该跳转到哪里继续执行,return address
。 - 第 28 - 32 行:从
a1
中恢复s0 - s11
寄存器值。 - 第 34 行:从
a1
中恢复sp
寄存器值。
- 第 27 行:从
-
第 35 行:返回
__switch
函数主要做的事情就是将依据传入的两个参数,将当前的状态(14个寄存器)保存到第一个参数中,再将第二个参数中所保存的状态(14个寄存器)恢复到当前寄存器中。当返回的时候就会从下一个应用所传入的ra
字段加载运行。也就顺利成章的完成了任务的切换。
😃 任务切换的结构体声明
context.rs
中保存了任务切换所需要的结构体声明和一些方法。
// os/src/task/context.rs
//! Implementation of [`TaskContext`]
#[derive(Copy, Clone)]
#[repr(C)]
/// task context structure containing some registers
pub struct TaskContext {
ra: usize,
sp: usize,
s: [usize; 12],
}
impl TaskContext {
pub fn zero_init() -> Self {
Self {
ra: 0,
sp: 0,
s: [0; 12],
}
}
pub fn goto_restore(kstack_ptr: usize) -> Self {
extern "C" {
fn __restore();
}
Self {
ra: __restore as usize,
sp: kstack_ptr,
s: [0; 12],
}
}
}
- 第 7 - 11 行:为任务切换所需要结构体的声明。
- 第 8 行:
ra
寄存器保存的是return address
返回后继续执行所需要的地址。 - 第 9 行:
sp
寄存器保存的是栈指针。 - 第 10 行:
s0 - s11
这12个寄存器为保存寄存器,也就是函数执行的上下文。
- 第 8 行:
- 第 14 - 20 行:为
TaskContext
实现了zero_init
这个方法,将里面的值初始化为0。 - 第 21 - 30 行:
goto_restore
保存传入的sp
,并将ra
设置为__restore
的入口地址,构造任务上下文后返回。这样,任务管理器中各个应用的任务上下文就得到了初始化。
😃 任务切换的外部封装
switch.rs
对汇编代码__switch
函数进行了封装,封装成rust函数可以供我们方便使用。
// os/src/task/switch.rs
//! Rust wrapper around `__switch`.
//!
//! Switching to a different task's context happens here. The actual
//! implementation must not be in Rust and (essentially) has to be in assembly
//! language (Do you know why?), so this module really is just a wrapper around
//! `switch.S`.
core::arch::global_asm!(include_str!("switch.S"));
use super::TaskContext;
extern "C" {
/// Switch to the context of `next_task_cx_ptr`, saving the current context
/// in `current_task_cx_ptr`.
pub fn __switch(
current_task_cx_ptr: *mut TaskContext,
next_task_cx_ptr: *const TaskContext);
}
【管理多道程序】
而内核为了管理任务,需要维护任务信息,相关内容包括:
- 任务运行状态:未初始化、准备执行、正在执行、已退出
- 任务控制块:维护任务状态和任务上下文
- 任务相关系统调用:程序主动暂停
sys_yield
和主动退出sys_exit
😃 yield 系统调用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J5NzMVjp-1663242684630)(C:\Users\11932\AppData\Roaming\Typora\typora-user-images\image-20220908112436254.png)]
上图描述了一种多道程序执行的典型情况。其中横轴为时间线,纵轴为正在执行的实体。 开始时,蓝色应用向外设提交了一个请求,外设随即开始工作, 但是它要一段时间后才能返回结果。蓝色应用于是调用 sys_yield
交出 CPU 使用权, 内核让绿色应用继续执行。一段时间后 CPU 切换回蓝色应用,发现外设仍未返回结果, 于是再次 sys_yield
。直到第二次切换回蓝色应用,外设才处理完请求,于是蓝色应用终于可以向下执行了。
😃 实现 sys_yield 和 sys_exit
// os/src/syscall/process.rs
//! Process management syscalls
use crate::config::{MAX_APP_NUM, MAX_SYSCALL_NUM};
use crate::task::{exit_current_and_run_next, suspend_current_and_run_next, TaskStatus};
use crate::timer::get_time_us;
#[repr(C)]
#[derive(Debug)]
pub struct TimeVal {
pub sec: usize,
pub usec: usize,
}
pub struct TaskInfo {
status: TaskStatus,
syscall_times: [u32; MAX_SYSCALL_NUM],
time: usize,
}
/// task exits and submit an exit code
pub fn sys_exit(exit_code: i32) -> ! {
info!("[kernel] Application exited with code {}", exit_code);
exit_current_and_run_next();
panic!("Unreachable in sys_exit!");
}
/// current task gives up resources for other tasks
pub fn sys_yield() -> isize {
suspend_current_and_run_next();
0
}
/// get time with second and microsecond
pub fn sys_get_time(ts: *mut TimeVal, _tz: usize) -> isize {
let us = get_time_us();
unsafe {
*ts = TimeVal {
sec: us / 1_000_000,
usec: us % 1_000_000,
};
}
0
}
/// YOUR JOB: Finish sys_task_info to pass testcases
pub fn sys_task_info(ti: *mut TaskInfo) -> isize {
-1
}
- 第 10 - 13 行:
TimeVal
结构体定义。 - 第 22 - 26 行:
sys_exit
基于task
子模块提供的exit_current_and_run_next
接口,它的含义是退出当前的应用并切换到下个应用。 - 第 29 - 32 行:
sys_yield
的实现用到了task
子模块提供的suspend_current_and_run_next
接口,这个接口如字面含义,就是暂停当前的应用并切换到下个应用。 - 第 35 - 44 行:
sys_get_time
获取us
级的时间。并针对时间设置TimeVal
结构体的值。
😃 任务控制块与任务运行状态
任务运行状态在os/src/task/task.rs
这个文件下:
// os/src/task/task.rs
//! Types related to task management
use super::TaskContext;
#[derive(Copy, Clone)]
/// task control block structure
pub struct TaskControlBlock {
pub task_status: TaskStatus,
pub task_cx: TaskContext,
// LAB1: Add whatever you need about the Task.
}
#[derive(Copy, Clone, PartialEq)]
/// task status: UnInit, Ready, Running, Exited
pub enum TaskStatus {
UnInit,// 未初始化
Ready,// 准备运行
Running,// 正在运行
Exited,// 已退出
}
- 第8 - 12行:声明了TCB控制块结构体,内部拥有两个成员分别为当前任务的状态和当前任务的上下文。
- 第 16 - 21 行:声明了枚举类型的任务运行状态,共有四种任务状态。
😃 任务管理器
/os/src/task/mod.rs
这个文件是进行任务调度的上层实现。
//! /os/src/task/mod.rs
//! Task management implementation
//! Everything about task management, like starting and switching tasks is
//! implemented here.
//!
//! A single global instance of [`TaskManager`] called `TASK_MANAGER` controls
//! all the tasks in the operating system.
//!
//! Be careful when you see [`__switch`]. Control flow around this function
//! might not be what you expect.
mod context;
mod switch;
#[allow(clippy::module_inception)]
mod task;
use crate::config::{MAX_APP_NUM, MAX_SYSCALL_NUM};
use crate::loader::{get_num_app, init_app_cx};
use crate::sync::UPSafeCell;
use lazy_static::*;
pub use switch::__switch;
pub use task::{TaskControlBlock, TaskStatus};
pub use context::TaskContext;
/// The task manager, where all the tasks are managed.
///
/// Functions implemented on `TaskManager` deals with all task state transitions
/// and task context switching. For convenience, you can find wrappers around it
/// in the module level.
///
/// Most of `TaskManager` are hidden behind the field `inner`, to defer
/// borrowing checks to runtime. You can see examples on how to use `inner` in
/// existing functions on `TaskManager`.
pub struct TaskManager {
/// total number of tasks
num_app: usize,
/// use inner value to get mutable access
inner: UPSafeCell<TaskManagerInner>,
}
/// The task manager inner in 'UPSafeCell'
struct TaskManagerInner {
/// task list
tasks: [TaskControlBlock; MAX_APP_NUM],
/// id of current `Running` task
current_task: usize,
}
lazy_static! {
/// a `TaskManager` instance through lazy_static!
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,
}; MAX_APP_NUM];
for (i, t) in tasks.iter_mut().enumerate().take(num_app) {
t.task_cx = TaskContext::goto_restore(init_app_cx(i));
t.task_status = TaskStatus::Ready;
}
TaskManager {
num_app,
inner: unsafe {
UPSafeCell::new(TaskManagerInner {
tasks,
current_task: 0,
})
},
}
};
}
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!");
}
}
// LAB1: Try to implement your function to update or get task info!
}
/// Run the first task in task list.
pub fn run_first_task() {
TASK_MANAGER.run_first_task();
}
/// 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() {
TASK_MANAGER.run_next_task();
}
/// Change the status of current `Running` task into `Ready`.
fn mark_current_suspended() {
TASK_MANAGER.mark_current_suspended();
}
/// Change the status of current `Running` task into `Exited`.
fn mark_current_exited() {
TASK_MANAGER.mark_current_exited();
}
/// 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();
}
// LAB1: Public functions implemented here provide interfaces.
// You may use TASK_MANAGER member functions to handle requests.
- 第 35 - 40 行:创建一个
TaskManager
结构体,这里用到了变量与常量分离的编程风格:字段num_app
表示应用数目,它在TaskManager
初始化后将保持不变; 而包裹在TaskManagerInner
内的任务控制块数组tasks
,以及正在执行的应用编号current_task
会在执行过程中变化。TaskManager
结构体为控制TCB的最外围的结构体。 - 第 43 - 48 行:第一个字段为任务控制块数组,其为每一个任务创建了一个TCB,第二个字段表明当前正在运行的任务。方便内核管理这些任务。
TaskManagerInner
结构体为TaskManager
结构体的子集,包含了所有任务的TCB。 - 第 50 - 72 行:声明了
TASK_MANAGER
,在第一次使用的时候对其进行初始化,主要对TCB进行初始化操作。- 第 53 行:调用
loader
子模块提供的get_num_app
接口获取链接到内核的应用总数。 - 第 54 - 57 行:创建
tasks
数组,含有config.rs
中MAX_APP_NUM
数量的TCB
。 - 第 58 - 61 行:依次对每个任务控制块进行初始化,将其运行状态设置为
Ready
,并在它的内核栈栈顶压入一些初始化上下文(trap
上下文),然后更新它的task_cx
。 - 第 62 -70 行:返回
TaskManager
结构体。并把上文所初始化的tasks
字段加入进来。
- 第 53 行:调用
- 第 74 - 140 行:为
TaskManager
实现了run_first_task
、mark_current_suspended
、mark_current_exited
、find_next_task
、run_next_task
这5个方法-
第 79 - 91 行:实现了
run_first_task
这个方法,这个函数将第一个任务加载进内核中执行。- 第 80 行:调用
exclusive_access
方法获取inner
其内部对象的可变引用,也就是TaskManagerInner
这个数据段。 - 第 81 行:获取
tasks0
第一个应用的TCB
,类型为TaskControlBlock
。 - 第 82 行:修改
task0
的TCB
中当前状态为running
态。 - 第 83 行:设置
next_task_cx_ptr
为task0
的task_cx
。 - 第 84 行:
drop inner
。切换任务之前,我们要手动 drop 掉我们获取到的TaskManagerInner
可变引用。 因为函数还没有返回,inner
不会自动销毁。我们只有令TASK_MANAGER
的inner
字段回到未被借用的状态,下次任务切换时才能再借用。 - 第 85 行:初始化一个
_unused
的TaskContext
类型的上下文为0。 - 第 88 行:通过
__switch
进行上下文的切换,将当前状态保存在_unused
中,并载入next_task_cx_ptr
所表明的第一个任务的上下文。
- 第 80 行:调用
-
第 94 - 98 行:实现了
mark_current_suspended
这个方法,这个方法设置当前app
的状态为ready
。 -
第 101 - 105 行:实现了
mark_current_exited
这个方法,这个方法设置当前app
的状态为exited
。 -
第 110 - 116 行:实现了
find_next_task
这个方法,这个方法查找之后的第一个找到的当前状态为ready
的方法。判断依据为每个app
所对应的task_status
字段。 -
第 120 - 137 行:实现了
run_next_task
这个方法,这个方法将找到下一个处于Ready
状态的app
将其进行运行(上下文切换、状态修改、保存上一个app
的上下文)。- 第 121 行:判断
find_next_task
方法返回的值是否存在,表明是否还有处于Ready
的app
。 - 第 122 行:调用
exclusive_access
方法获取inner
其内部对象的可变引用,也就是TaskManagerInner
这个数据段。 - 第 123 行:获取当前正在运行的任务ID。
- 第 124 行:设置下一个任务的当前状态为
Running
。 - 第 125 行:设置
current_task
字段的ID为下一个即将运行的任务。 - 第 126 行:设置当前任务的上下文为
current_task_cx_ptr
。 - 第 127 行:设置下一个任务的上下文为
next_task_cx_ptr
。 - 第 128 行:
drop inner
。 - 第 131 行:通过
__switch
进行上下文的切换,将当前状态保存在当前任务的上下文中,并载入next_task_cx_ptr
所表明的下一个任务的上下文。
- 第 121 行:判断
-
第 143 - 145 行:创建
run_first_task
函数,运行第一个app
,为TaskManager
方法run_first_task
的外部封装。 -
第 149 - 151 行:创建
run_next_task
函数,运行下一个app
,为TaskManager
方法run_next_task
的外部封装 -
第 154 - 156 行:创建
mark_current_suspended
函数,设置当前app
的状态为Ready
,为TaskManager
方法mark_current_suspended
的外部封装 -
第 159 - 161 行:创建
mark_current_exited
函数,设置当前app
的状态为Exited
,为TaskManager
方法mark_current_exited
的外部封装 -
第 164 - 167 行:创建
suspend_current_and_run_next
函数,设置当前app
的状态为Ready
并开始运行下一个app
。 -
第 170 - 173 行:创建
exit_current_and_run_next
函数,设置当前app
的状态为exited
并开始运行下一个app
。
-
此文件主要是由内核使用的负责管理任务之间的切换还有各个任务的状态。每个任务通过他的task_status字段来判断其处的状态。任务之间的切换首先修改下一个任务与当前任务的运行状态,其次保存将要运行任务的ID,最后进行上下文的切换也就是寄存器的保存与恢复。
【分时多任务系统】
现代的任务调度算法基本都是抢占式的,它要求每个应用只能连续执行一段时间,然后内核就会将它强制性切换出去。 一般将 时间片 (Time Slice) 作为应用连续执行时长的度量单位,每个时间片可能在毫秒量级。 简单起见,我们使用 时间片轮转算法 (RR, Round-Robin) 来对应用进行调度。
😃 时钟中断与计时器
实现调度算法需要计时。RISC-V 要求处理器维护时钟计数器 mtime
,还有另外一个 CSR mtimecmp
。 一旦计数器 mtime
的值超过了 mtimecmp
,就会触发一次时钟中断。
os/src/sbi.rs
这个文件在原来sbi
的基础上新增了SBI_SET_TIMER
这个ID
,依据OpenSBI
文档ID
对应为0。用于设置mtimecmp
的值
// os/src/sbi.rs
//! SBI call wrappers
#![allow(unused)]
const SBI_SET_TIMER: usize = 0;
const SBI_CONSOLE_PUTCHAR: usize = 1;
const SBI_CONSOLE_GETCHAR: usize = 2;
const SBI_SHUTDOWN: usize = 8;
#[inline(always)]
/// general sbi call
fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
let mut ret;
unsafe {
core::arch::asm!(
"li x16, 0",
"ecall",
inlateout("x10") arg0 => ret,
in("x11") arg1,
in("x12") arg2,
in("x17") which,
);
}
ret
}
/// use sbi call to set timer
pub fn set_timer(timer: usize) {
sbi_call(SBI_SET_TIMER, timer, 0, 0);
}
/// use sbi call to putchar in console (qemu uart handler)
pub fn console_putchar(c: usize) {
sbi_call(SBI_CONSOLE_PUTCHAR, c, 0, 0);
}
/// use sbi call to getchar from console (qemu uart handler)
pub fn console_getchar() -> usize {
sbi_call(SBI_CONSOLE_GETCHAR, 0, 0, 0)
}
/// use sbi call to shutdown the kernel
pub fn shutdown() -> ! {
sbi_call(SBI_SHUTDOWN, 0, 0, 0);
panic!("It should shutdown!");
}
- 第 6 行:
SBI_SET_TIMER
的ID
为0。 - 第 29 - 31 行:
sbi
子模块有一个set_timer
调用,用来设置mtimecmp
的值。
此文件在原有基础上新增了
SBI_SET_TIMER
这个ecall
指令主要设置定时器阈值,为M
模式下的SBI
。
运行在 M 特权级的 SEE 已经预留了相应的接口,基于此编写的 get_time
函数可以取得当前 mtime
计数器的值;
// os/src/timer.rs
//! RISC-V timer-related functionality
use crate::config::CLOCK_FREQ;
use crate::sbi::set_timer;
use riscv::register::time;
const TICKS_PER_SEC: usize = 100;
const MICRO_PER_SEC: usize = 1_000_000;
/// read the `mtime` register
pub fn get_time() -> usize {
time::read()
}
/// get current time in microseconds
pub fn get_time_us() -> usize {
time::read() / (CLOCK_FREQ / MICRO_PER_SEC)
}
/// set the next timer interrupt
pub fn set_next_trigger() {
set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
}
- 第 12 - 14 行:获取当前
mtime
计数器的值。time
计数器统计自CPU复位以来共运行了多少时间,驱动time
计数器是已知的固定频率的时钟。 - 第 16 - 19 行:依据
get_time
的值进行计算得到us
值。 - 第 22 - 24 行:
timer
子模块的set_next_trigger
函数对set_timer
进行了封装, 它首先读取当前mtime
的值,然后计算出 10ms 之内计数器的增量,再将mtimecmp
设置为二者的和。 这样,10ms 之后一个 S 特权级时钟中断就会被触发。
主要为时钟部分的上层封装,增加了读取当前time值和设置下一个中断的函数。
😃 增加获取当前时间的系统调用
文件夹位置os/src/syscall/process.rs
具体内容参考:) 实现 sys_yield 和 sys_exit
😃 RISC-V 架构中的嵌套中断问题
默认情况下,当 Trap 进入某个特权级之后,在 Trap 处理的过程中同特权级的中断都会被屏蔽。
- 当 Trap 发生时,
sstatus.sie
会被保存在sstatus.spie
字段中,同时sstatus.sie
置零, 这也就在 Trap 处理的过程中屏蔽了所有 S 特权级的中断; - 当 Trap 处理完毕
sret
的时候,sstatus.sie
会恢复到sstatus.spie
内的值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e12HVk6H-1663242684630)(C:\Users\11932\AppData\Roaming\Typora\typora-user-images\image-20220913155807437.png)]
也就是说,如果不去手动设置 sstatus
CSR ,在只考虑 S 特权级中断的情况下,是不会出现 嵌套中断 (Nested Interrupt) 的。
嵌套中断与嵌套 Trap
嵌套中断可以分为两部分:在处理一个中断的过程中又被同特权级/高特权级中断所打断。默认情况下硬件会避免前一部分, 也可以通过手动设置来允许前一部分的存在;而从上面介绍的规则可以知道,后一部分则是无论如何设置都不可避免的。
嵌套 Trap 则是指处理一个 Trap 过程中又再次发生 Trap ,嵌套中断算是嵌套 Trap 的一种。
😃 抢占式调度
总体思路就是当发生时钟中断的时候就保存上下文,运行下一个
app
//! Trap handling functionality
//!
//! For rCore, we have a single trap entry point, namely `__alltraps`. At
//! initialization in [`init()`], we set the `stvec` CSR to point to it.
//!
//! All traps go through `__alltraps`, which is defined in `trap.S`. The
//! assembly language code does just enough work restore the kernel space
//! context, ensuring that Rust code safely runs, and transfers control to
//! [`trap_handler()`].
//!
//! It then calls different functionality based on what exactly the exception
//! was. For example, timer interrupts trigger task preemption, and syscalls go
//! to [`syscall()`].
mod context;
use crate::syscall::syscall;
use crate::task::{exit_current_and_run_next, suspend_current_and_run_next};
use crate::timer::set_next_trigger;
use riscv::register::{
mtvec::TrapMode,
scause::{self, Exception, Interrupt, Trap},
sie, stval, stvec,
};
core::arch::global_asm!(include_str!("trap.S"));
/// initialize CSR `stvec` as the entry of `__alltraps`
pub fn init() {
extern "C" {
fn __alltraps();
}
unsafe {
stvec::write(__alltraps as usize, TrapMode::Direct);
}
}
/// timer interrupt enabled
pub fn enable_timer_interrupt() {
unsafe {
sie::set_stimer();
}
}
#[no_mangle]
/// handle an interrupt, exception, or system call from user space
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
let scause = scause::read(); // get trap cause
let stval = stval::read(); // get extra value
match scause.cause() {
Trap::Exception(Exception::UserEnvCall) => {
cx.sepc += 4;
cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
}
Trap::Exception(Exception::StoreFault) | Trap::Exception(Exception::StorePageFault) => {
error!("[kernel] PageFault in application, bad addr = {:#x}, bad instruction = {:#x}, core dumped.", stval, cx.sepc);
exit_current_and_run_next();
}
Trap::Exception(Exception::IllegalInstruction) => {
error!("[kernel] IllegalInstruction in application, core dumped.");
exit_current_and_run_next();
}
Trap::Interrupt(Interrupt::SupervisorTimer) => {
set_next_trigger();
suspend_current_and_run_next();
}
_ => {
panic!(
"Unsupported trap {:?}, stval = {:#x}!",
scause.cause(),
stval
);
}
}
cx
}
pub use context::TrapContext
- 第 29 - 36 行:修改
stvec
寄存器来指向正确的 Trap 处理入口点。- 第 31 行:引入外部符号
__alltraps
- 第 34 行:将
stvec
(trap处理代码的地址)设置为Direct
模式指向__alltraps
的地址。
- 第 31 行:引入外部符号
- 第 39 - 43 行:设置
sie.stie
(s-mode下时钟中断的enable比特位), 使得 S 特权级时钟中断不会被屏蔽。 - 第 63 - 66 行:新增分支,触发了 S 特权级时钟中断时,重新设置计时器, 调用
suspend_current_and_run_next
函数暂停当前应用并切换到下一个。
分时多任务系统在此主要是用了定时器中断进行任务的时间片轮流执行。通过设置定时器的阈值与处理中断函数进行任务切换达到这一目的,为OS层面的。
【实验设计 chapter3】
【获取任务信息】
我们希望引入一个新的系统调用 sys_task_info
以获取当前任务的信息,定义如下:
fn sys_task_info(ti: *mut TaskInfo) -> isize
- syscall ID: 410
- 查询当前正在执行的任务信息,任务信息包括任务控制块相关信息(任务状态)、任务使用的系统调用及调用次数、任务总运行时长(单位ms)。
struct TaskInfo {
status: TaskStatus,
syscall_times: [u32; MAX_SYSCALL_NUM],
time: usize
}
- 参数:
- ti: 待查询任务信息
- 返回值:执行成功返回0,错误返回-1
- 说明:
- 相关结构已在框架中给出,只需添加逻辑实现功能需求即可。
- 在我们的实验中,系统调用号一定小于 500,所以直接使用一个长为
MAX_SYSCALL_NUM=500
的数组做桶计数。 - 运行时间 time 返回系统调用时刻距离任务第一次被调度时刻的时长,也就是说这个时长可能包含该任务被其他任务抢占后的等待重新调度的时间。
【思路】
-
status
:由于查询的是当前任务的状态,因此TaskStatus
一定是Running
。 -
syscall_times
:记录当前app
使用系统调用的次数。-
在
TCB
中添加一个数组用来记录系统调用的次数pub struct TaskControlBlock { pub task_status: TaskStatus, pub task_cx: TaskContext, // LAB1: Add whatever you need about the Task. pub syscall_count: [u32; MAX_SYSCALL_NUM], }
-
为
app
实现刷新syscall
计数,获取当前info
的方法// LAB1: Try to implement your function to update or get task info! fn update_task_info(&self , syscall_id:usize) { let mut inner = self.inner.exclusive_access(); let current = inner.current_task; inner.tasks[current].syscall_count[syscall_id] = inner.tasks[current].syscall_count[syscall_id]+ 1; } fn git_task_info(&self) -> num:[u32; MAX_SYSCALL_NUM]{ let mut inner = self.inner.exclusive_access(); let current = inner.current_task; sys_count:[u32; MAX_SYSCALL_NUM] = inner.tasks[current].syscall_count; sys_count }
-
对方法进行外部封装
// LAB1: Public functions implemented here provide interfaces. // You may use TASK_MANAGER member functions to handle requests. fn update_task_info(syscall_id : usize) { TASK_MANAGER.update_task_info(syscall_id); } fn git_task_info() -> num:[u32; MAX_SYSCALL_NUM]{ num:[u32; MAX_SYSCALL_NUM] = TASK_MANAGER.git_task_info(); num }
-
在系统调用处进行系统调用的计数
/// handle syscall exception with `syscall_id` and other arguments pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize { // LAB1: You may need to update syscall info here. update_task_info(syscall_id); match syscall_id { SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]), SYSCALL_EXIT => sys_exit(args[0] as i32), SYSCALL_YIELD => sys_yield(), SYSCALL_GET_TIME => sys_get_time(args[0] as *mut TimeVal, args[1]), SYSCALL_TASK_INFO => sys_task_info(args[0] as *mut TaskInfo), _ => panic!("Unsupported syscall_id: {}", syscall_id), } }
-
新增
sys_task_info
系统调用处理函数static mut old_time: usize = 0; /// YOUR JOB: Finish sys_task_info to pass testcases pub fn sys_task_info(ti: *mut TaskInfo) -> isize { unsafe { let cur_time:usize = get_time_us(); let cha = cur_time - old_time; old_time = cur_time; *ti = TaskInfo { status: TaskStatus::Running, syscall_times: git_task_info(), time: cha, }; } 0 }
-
-
time
:记录任务总运行时长,在第一次切换到相应任务的时候进行记录时间。时间是ms为单位。-
在切换下一个
app
的时候查看它是否time
未被记录过,没有则记录一下时间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; if inner.tasks[current].first_time == 0{ inner.tasks[current].first_time = get_time_us()/1000; } 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!"); } }
-
增加获取当前任务时间的方法
fn git_task_time_info(&self) -> usize{ let mut inner = self.inner.exclusive_access(); let current = inner.current_task; let sys_init_time:usize = inner.tasks[current].first_time; sys_init_time }
-
对此方法进行外部封装
pub fn git_task_time_info() -> usize{ let num:usize = TASK_MANAGER.git_task_time_info(); num }
-
在
sys_task_info
的time
部分将当前时间与第一次存入的时间差值进行保存/// YOUR JOB: Finish sys_task_info to pass testcases pub fn sys_task_info(ti: *mut TaskInfo) -> isize { unsafe { let cur_time:usize = get_time_us()/1000; *ti = TaskInfo { status: TaskStatus::Running, syscall_times: git_task_info(), time: cur_time - git_task_time_info(), }; } 0 }
-
😃 个人总结
- 多道程序系统是在
Dunkleosteus OS
的基础上将多个APP
加载到紧挨着的不同的地址空间从而实现多个app同时处理。- 多道程序的切换呢主要是依据
TCB
的控制类似于状态机,内部的切换实质上是上下文的保存与恢复(保存一些寄存器的值简称上下文)。- 分时多任务系统的实现主要在多道程序基础上增加了时间片轮转调度算法,内部的实现是依据中断进行的,通过中断处理函数进行多道程序的调度。
- 截至当前时间节点,从裸机开始,到应用与系统分开再到现在可以多任务分时调度运行。特权级分层的效果越来越明显,我们也是越来越不信任应用程序。😃