Lab3
待整理
第四讲 多道程序与分时多任务
代码树解释
── 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
详细解释
顶层结构
build.rs: 编译脚本,用于编译期间的自定义构建步骤。Cargo.toml: Rust 项目的配置文件,定义了项目的依赖和元数据。Makefile: 使用make构建项目的配置文件。
src 目录
batch.rs: 原来负责批处理功能的模块,现已移除,其功能被拆分到loader和task两个子模块中。config.rs: 新增的模块,用于保存内核的一些配置。console.rs: 控制台相关功能。logging.rs: 日志记录相关功能。sync: 同步相关功能。entry.asm: 程序入口的汇编代码。lang_items.rs: 定义了一些语言项目(如 panic 处理)。link_app.S: 用于链接用户应用程序的汇编代码。linker.ld: 链接脚本,用于定义内存布局。loader.rs: 新增的模块,负责将应用加载到内存并进行管理。main.rs: 主函数,进行了修改以适应新增和修改的功能。sbi.rs: 修改后的模块,引入了新的 SBI 调用set_timer。syscall: 系统调用相关功能,新增了若干系统调用。fs.rs: 文件系统相关系统调用。mod.rs: 系统调用模块入口。process.rs: 进程相关系统调用。
task: 新增的任务管理子模块。context.rs: 引入了任务上下文TaskContext。mod.rs: 定义了全局任务管理器并提供接口给其他模块。switch.rs: 将任务切换的汇编代码解释为 Rust 接口__switch。switch.S: 任务切换的汇编代码。task.rs: 定义了任务控制块TaskControlBlock和任务状态TaskStatus。
timer.rs: 新增的计时器相关模块。trap: 中断和异常处理相关功能。context.rs: 中断和异常处理上下文。mod.rs: 修改了时钟中断的相应处理。trap.S: 中断和异常处理的汇编代码。
通过这些模块的拆分和新增,内核实现了多道程序与分时多任务的功能,提高了系统的并发能力和响应速度。
我们将详细深入讲解 loader.rs 文件中的 load_apps 函数每个细节。
loader.rs 文件
#![allow(unused)] fn main() { // os/src/loader.rs pub fn load_apps() { extern "C" { fn _num_app(); } let num_app_ptr = _num_app as usize as *const usize; let num_app = unsafe { num_app_ptr.read() }; 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); } } }
详细解释
1. 获取应用程序数量
#![allow(unused)] fn main() { extern "C" { fn _num_app(); } let num_app_ptr = _num_app as usize as *const usize; let num_app = unsafe { num_app_ptr.read() }; }
extern "C": 这里声明了一个外部函数_num_app,它的定义在其他地方(通常是汇编代码中)。_num_app as usize as *const usize: 将_num_app的函数指针转换为usize类型,再转换为指向usize的指针。num_app_ptr.read(): 读取指针指向的值,即应用程序的数量。由于这个操作涉及到裸指针,需要使用unsafe块来保证内存安全。
2. 获取应用程序起始地址数组
#![allow(unused)] fn main() { let app_start = unsafe { core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) }; }
core::slice::from_raw_parts: 将一个原始指针和长度转换为一个切片。num_app_ptr.add(1): 指针向后移动一位,跳过应用程序数量的存储位置,指向应用程序起始地址数组的开始。num_app + 1: 切片的长度为num_app + 1,包括所有应用程序的起始地址和结束位置。
3. 清除指令缓存
#![allow(unused)] fn main() { unsafe { core::arch::asm!("fence.i"); } }
core::arch::asm!("fence.i"): 使用内联汇编清除指令缓存(I-cache),确保后续加载的指令能够被正确执行。fence.i是 RISC-V 指令,用于指令序列之间的隔离。
4. 加载应用程序
#![allow(unused)] fn main() { 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); } }
for i in 0..num_app: 遍历每个应用程序。get_base_i(i): 计算每个应用程序的起始地址base_i。
清除目标内存区域
#![allow(unused)] fn main() { (base_i..base_i + APP_SIZE_LIMIT) .for_each(|addr| unsafe { (addr as *mut u8).write_volatile(0) }); }
(base_i..base_i + APP_SIZE_LIMIT): 创建从base_i到base_i + APP_SIZE_LIMIT的地址范围。for_each: 对该范围内的每个地址执行操作。(addr as *mut u8).write_volatile(0): 将地址addr转换为可变指针,并写入0。使用write_volatile确保编译器不会优化掉这段代码,确保每个字节都被清零。
加载应用程序数据
#![allow(unused)] fn main() { 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); }
core::slice::from_raw_parts: 将应用程序在数据段中的起始地址转换为切片。src: 来源数据切片,表示应用程序的二进制数据。core::slice::from_raw_parts_mut: 将目标地址转换为可变切片。dst: 目标数据切片,表示应用程序在内存中的位置。copy_from_slice(src): 将src中的数据复制到dst。
get_base_i 函数
每个应用程序被加载到以物理地址 base_i 开头的一段物理内存上,而 base_i 的计算方式如下:
#![allow(unused)] fn main() { fn get_base_i(app_id: usize) -> usize { APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT } }
APP_BASE_ADDRESS: 应用程序的基地址,通常在config模块中定义。这里设置为0x80400000。APP_SIZE_LIMIT: 每个应用程序的内存大小限制。这里设置为0x20000。APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT: 计算第app_id个应用程序的起始地址,确保每个应用程序都有一个独立的内存区域。
config.rs 文件中的常数定义
#![allow(unused)] fn main() { // os/src/config.rs pub const APP_BASE_ADDRESS: usize = 0x80400000; pub const APP_SIZE_LIMIT: usize = 0x20000; }
APP_BASE_ADDRESS: 基地址,设置为0x80400000。APP_SIZE_LIMIT: 每个应用程序的大小限制,设置为0x20000。
通过这些步骤,内核实现了多道程序的加载和执行,为系统带来了并发处理能力。每个应用程序都有独立的内存区域,确保了它们可以同时驻留在内存中并被正确执行。
多道程序放置与加载中的硬件和操作系统流程
1. 获取应用程序数量
- 代码位置:
loader.rs - 函数名:
load_apps
#![allow(unused)] fn main() { extern "C" { fn _num_app(); } let num_app_ptr = _num_app as usize as *const usize; let num_app = unsafe { num_app_ptr.read() }; }
硬件状态:
- 内存读取: CPU 从固定的内存位置读取应用程序数量。
- 内存位置: 该位置由
_num_app函数指向,可能是由汇编代码或链接器设置的一个标记位置。
操作系统状态:
- 内核数据准备: 内核通过读取
num_app_ptr获取应用程序数量并存储在num_app变量中。 - 内核流程: 准备遍历和加载多个应用程序。
2. 获取应用程序起始地址数组
- 代码位置:
loader.rs - 函数名:
load_apps
#![allow(unused)] fn main() { let app_start = unsafe { core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) }; }
硬件状态:
- 内存读取: CPU 访问内存中存储的应用程序起始地址数组。
- 内存位置: 起始地址数组从
num_app_ptr开始的下一个位置开始。
操作系统状态:
- 内核数据准备: 内核通过
core::slice::from_raw_parts获取应用程序起始地址和结束地址,存储在app_start切片中。 - 内核流程: 准备按照这些地址加载应用程序。
3. 清除指令缓存
- 代码位置:
loader.rs - 函数名:
load_apps
#![allow(unused)] fn main() { unsafe { core::arch::asm!("fence.i"); } }
硬件状态:
- CPU 操作: 执行
fence.i指令,清除指令缓存(I-cache)。 - 指令缓存: 确保指令缓存中的旧指令不会影响新加载的应用程序。
操作系统状态:
- 内核初始化: 确保内存中的新指令可以被正确执行,准备加载应用程序。
4. 加载应用程序
- 代码位置:
loader.rs - 函数名:
load_apps
#![allow(unused)] fn main() { 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); } }
硬件状态:
- 内存操作: CPU 清除目标内存区域 (
base_i..base_i + APP_SIZE_LIMIT) 并写入应用程序数据。 - 内存读取和写入: CPU 从
app_start读取应用程序数据,并写入base_i开始的内存区域。
操作系统状态:
- 内核数据准备: 内核计算每个应用程序的基地址 (
get_base_i)。 - 内核初始化: 清除目标内存区域,确保没有残留数据。
- 内核加载: 复制应用程序数据到目标内存区域,确保每个应用程序都在独立的内存区域中正确加载。
5. 计算应用程序的基地址
- 代码位置:
loader.rs - 函数名:
get_base_i
#![allow(unused)] fn main() { fn get_base_i(app_id: usize) -> usize { APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT } }
- 代码位置:
config.rs - 常量定义:
APP_BASE_ADDRESS和APP_SIZE_LIMIT
#![allow(unused)] fn main() { pub const APP_BASE_ADDRESS: usize = 0x80400000; pub const APP_SIZE_LIMIT: usize = 0x20000; }
硬件状态:
- 内存布局: 计算每个应用程序在物理内存中的起始地址。
操作系统状态:
- 内核地址计算: 使用
APP_BASE_ADDRESS和APP_SIZE_LIMIT,通过应用程序编号app_id计算每个应用程序的基地址base_i。 - 内存管理: 确保每个应用程序都有独立的内存区域,防止地址冲突。
流程总结
-
获取应用程序数量:
- 硬件: 从固定内存位置读取数量。
- 操作系统: 读取并存储在
num_app变量中。
-
获取应用程序起始地址数组:
- 硬件: 读取内存中的地址数组。
- 操作系统: 存储在
app_start切片中。
-
清除指令缓存:
- 硬件: 执行
fence.i指令,清除 I-cache。 - 操作系统: 确保新指令可以正确执行。
- 硬件: 执行
-
加载应用程序:
- 硬件: 清除目标内存区域并写入应用程序数据。
- 操作系统: 计算基地址,初始化内存,复制数据。
-
计算应用程序基地址:
- 硬件: 基于常量计算地址。
- 操作系统: 确保内存布局合理,防止地址冲突。
通过这些步骤,操作系统成功实现了多道程序的加载和运行,为系统提供了多任务并发处理的能力。每个应用程序都有独立的内存区域,确保它们可以同时驻留在内存中并被正确执行。
任务切换
任务切换是操作系统的核心机制之一,使得应用可以在运行中主动或被动地交出 CPU 的使用权,内核可以选择另一个程序继续执行。任务切换的关键在于保证用户程序在两次运行期间,任务上下文(如寄存器、栈等)保持一致。
任务切换的设计与实现
任务切换与 Trap 控制流切换相比,有如下异同:
- 不同点:
- 不涉及特权级切换,部分由编译器完成。
- 相同点:
- 对应用是透明的。
任务切换实质上是来自两个不同应用在内核中的 Trap 控制流之间的切换。当一个应用 Trap 到 S 态 OS 内核中进行进一步处理时,其 Trap 控制流可以调用一个特殊的 __switch 函数。在 __switch 返回之后,Trap 控制流将继续从调用该函数的位置继续向下执行。
__switch 函数
任务切换通过 __switch 函数实现。在 __switch 函数中,保存 CPU 的某些寄存器,它们就是任务上下文 (Task Context)。
下面是 __switch 的实现:
# 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
流程细节
-
函数调用:
- 函数名:
__switch - 参数:
current_task_cx_ptr(当前任务的上下文指针,通过寄存器a0传入)next_task_cx_ptr(下一个任务的上下文指针,通过寄存器a1传入)
- 函数名:
-
保存当前任务上下文:
-
保存栈指针(sp):
sd sp, 8(a0)- 硬件状态: 将当前任务的栈指针
sp保存到current_task_cx_ptr指向的内存位置。 - 操作系统状态: 当前任务的栈状态被保存。
- 硬件状态: 将当前任务的栈指针
-
保存返回地址(ra)和保存寄存器(s0~s11):
sd ra, 0(a0) .set n, 0 .rept 12 SAVE_SN %n .set n, n + 1 .endr- 硬件状态: 将返回地址
ra和保存寄存器s0~s11保存到current_task_cx_ptr指向的内存位置。 - 操作系统状态: 当前任务的寄存器状态被保存。
- 硬件状态: 将返回地址
-
-
恢复下一个任务上下文:
-
恢复返回地址(ra)和保存寄存器(s0~s11):
ld ra, 0(a1) .set n, 0 .rept 12 LOAD_SN %n .set n, n + 1 .endr- 硬件状态: 从
next_task_cx_ptr指向的内存位置恢复返回地址ra和保存寄存器s0~s11。 - 操作系统状态: 下一个任务的寄存器状态被恢复。
- 硬件状态: 从
-
恢复栈指针(sp):
ld sp, 8(a1)- 硬件状态: 从
next_task_cx_ptr指向的内存位置恢复栈指针sp。 - 操作系统状态: 下一个任务的栈状态被恢复。
- 硬件状态: 从
-
-
返回(ret):
- 硬件状态: 返回到下一个任务的执行点。
- 操作系统状态: CPU 开始执行下一个任务。
总结
任务切换的关键步骤:
- 保存当前任务的上下文:
- 保存当前任务的栈指针
sp和寄存器ra,s0~s11到current_task_cx_ptr。
- 保存当前任务的栈指针
- 恢复下一个任务的上下文:
- 从
next_task_cx_ptr恢复下一个任务的栈指针sp和寄存器ra,s0~s11。
- 从
硬件状态变化:
- 内存操作: 多次读写内存,用于保存和恢复任务上下文。
- 寄存器操作: 读写多个寄存器值,包括
sp,ra,s0~s11。
操作系统状态变化:
- 上下文切换: 当前任务的上下文被保存,下一个任务的上下文被恢复。
- 任务执行: 切换到下一个任务的执行点,继续执行下一个任务的代码。
通过 __switch 函数,内核能够有效地在不同的任务之间切换,确保每个任务在两次运行之间保持上下文一致。这是实现多任务并发运行的基础。
任务切换中的硬件和操作系统流程细节
任务切换是操作系统中的核心机制,它使得应用可以在运行中主动或被动地交出 CPU 的使用权,从而允许另一个程序继续执行。在这一过程中,操作系统需要确保用户程序两次运行期间,任务上下文(如寄存器、栈等)保持一致。
代码位置和函数名
- 代码位置:
os/src/task/switch.Sos/src/task/switch.rs - 函数名:
__switch - 相关变量:
current_task_cx_ptr,next_task_cx_ptr
详细流程
1. 获取当前任务和下一个任务的上下文指针
- 操作系统状态:
- 函数调用:
__switch(current_task_cx_ptr: *mut TaskContext, next_task_cx_ptr: *const TaskContext) - 变量传递:
current_task_cx_ptr和next_task_cx_ptr分别通过寄存器a0和a1传入。
- 函数调用:
2. 保存当前任务的上下文
- 函数名:
__switch
-
保存栈指针(sp):
- 硬件状态: CPU 将当前任务的栈指针
sp保存到current_task_cx_ptr指向的内存位置(通过sd sp, 8(a0))。 - 操作系统状态: 当前任务的栈状态被保存。
- 硬件状态: CPU 将当前任务的栈指针
-
保存返回地址(ra)和保存寄存器(s0~s11):
- 硬件状态: CPU 将返回地址
ra和保存寄存器s0~s11保存到current_task_cx_ptr指向的内存位置(通过sd ra, 0(a0)和SAVE_SN宏)。 - 操作系统状态: 当前任务的寄存器状态被保存。
- 硬件状态: CPU 将返回地址
3. 恢复下一个任务的上下文
- 函数名:
__switch
-
恢复返回地址(ra)和保存寄存器(s0~s11):
- 硬件状态: CPU 从
next_task_cx_ptr指向的内存位置恢复返回地址ra和保存寄存器s0~s11(通过ld ra, 0(a1)和LOAD_SN宏)。 - 操作系统状态: 下一个任务的寄存器状态被恢复。
- 硬件状态: CPU 从
-
恢复栈指针(sp):
- 硬件状态: CPU 从
next_task_cx_ptr指向的内存位置恢复栈指针sp(通过ld sp, 8(a1))。 - 操作系统状态: 下一个任务的栈状态被恢复。
- 硬件状态: CPU 从
4. 返回到下一个任务的执行点
-
函数名:
__switch -
指令:
ret -
硬件状态: CPU 执行
ret指令,跳转到恢复的返回地址ra,开始执行下一个任务。 -
操作系统状态: CPU 切换到下一个任务的执行点,继续执行下一个任务的代码。
总结
硬件状态变化
- 内存读写: 多次读写内存,用于保存和恢复任务上下文(寄存器和栈指针)。
- 寄存器操作: 读写多个寄存器值,包括
sp,ra,s0~s11。 - 指令执行: 执行
ret指令,切换到下一个任务的执行点。
操作系统状态变化
- 上下文切换: 当前任务的上下文被保存,下一个任务的上下文被恢复。
- 任务执行: 切换到下一个任务的执行点,继续执行下一个任务的代码。
通过 __switch 函数,操作系统实现了不同任务之间的切换,确保每个任务在两次运行之间保持上下文一致。这是实现多任务并发运行的基础。
任务上下文(TaskContext)和任务切换(__switch)的实现
任务上下文(TaskContext)
在任务切换中,保存和恢复任务的上下文是关键。上下文保存了任务在切换前的状态,以便在切换回来时能从中断点继续执行。具体来说,上下文包括返回地址(ra)、栈指针(sp)、以及保存寄存器(s0~s11)。
TaskContext 结构体
#![allow(unused)] fn main() { // os/src/task/context.rs #[repr(C)] pub struct TaskContext { ra: usize, sp: usize, s: [usize; 12], } }
详细解释
- #[repr(C)]: 这个属性保证结构体的内存布局与 C 语言兼容,以确保在汇编代码中能正确访问这些字段。
- ra: usize: 保存返回地址寄存器(Return Address)。
ra寄存器记录了函数返回后应该跳转到的地址。 - sp: usize: 保存栈指针寄存器(Stack Pointer)。
sp寄存器指向当前栈顶位置。 - s: [usize; 12]: 保存被调用者保存寄存器(Saved Registers)。
s0~s11是被调用者保存寄存器,在函数调用过程中需要保持它们的值。
__switch 的 Rust 封装
#![allow(unused)] fn main() { // os/src/task/switch.rs core::arch::global_asm!(include_str!("switch.S")); extern "C" { pub fn __switch( current_task_cx_ptr: *mut TaskContext, next_task_cx_ptr: *const TaskContext); } }
详细解释
-
core::arch::global_asm!(include_str!("switch.S")):
- 将
switch.S中的汇编代码包含进来,确保汇编函数__switch可被 Rust 代码调用。
- 将
-
extern "C":
- 声明外部函数
__switch,表示该函数是用 C ABI 调用的汇编函数。
- 声明外部函数
-
pub fn __switch(current_task_cx_ptr: *mut TaskContext, next_task_cx_ptr: *const TaskContext):
- 该函数接受两个参数:
current_task_cx_ptr和next_task_cx_ptr,分别是当前任务和下一个任务的上下文指针。 current_task_cx_ptr是一个可变指针,指向当前任务的上下文,表示需要保存的当前任务的寄存器状态。next_task_cx_ptr是一个不可变指针,指向下一个任务的上下文,表示需要恢复的下一个任务的寄存器状态。
- 该函数接受两个参数:
通过 TaskContext 结构体和 __switch 汇编函数的实现,操作系统能够在不同任务之间进行切换,确保每个任务在切换过程中保持上下文一致。这是实现多任务并发运行的关键机制。
管理多道程序
内核需要管理多个任务以实现多道程序的并发执行。管理任务的关键在于维护任务信息,包括任务运行状态、任务控制块、以及任务相关的系统调用。
任务运行状态
任务运行状态包括:
- 未初始化: 任务尚未准备好执行。
- 准备执行: 任务已经准备好,可以执行。
- 正在执行: 任务当前正在 CPU 上执行。
- 已退出: 任务已经完成,不再需要执行。
这些状态帮助内核跟踪每个任务的执行进度和调度需求。
任务控制块(Task Control Block)
任务控制块 (TCB) 是用于维护每个任务的状态和上下文的结构体。TCB 包含了任务的上下文(如寄存器、栈指针等)以及任务的运行状态。
任务相关系统调用
系统调用是用户程序与内核交互的接口,任务相关的系统调用包括:
- 主动暂停(sys_yield): 任务主动交出 CPU 使用权,让其他任务执行。
- 主动退出(sys_exit): 任务主动退出,表示任务已完成。
yield 系统调用
sys_yield 系统调用允许任务主动交出 CPU 使用权,让内核调度其他任务执行。这在多道程序中尤为重要,可以避免 CPU 资源的浪费。
#![allow(unused)] fn main() { // user/src/syscall.rs pub fn sys_yield() -> isize { syscall(SYSCALL_YIELD, [0, 0, 0]) } // user/src/lib.rs // yield 是 Rust 的关键字 pub fn yield_() -> isize { sys_yield() } }
sys_yield 系统调用流程
-
调用
sys_yield函数:- 函数名:
sys_yield - 定义位置:
user/src/syscall.rs - 功能: 应用主动交出 CPU 使用权,并切换到其他应用。
- 返回值: 总是返回 0。
- syscall ID: 124
- 函数名:
-
用户库封装
sys_yield:- 函数名:
yield_ - 定义位置:
user/src/lib.rs - 功能: 用户调用
yield_函数,内部调用sys_yield实现。
- 函数名:
硬件和操作系统流程细节
-
应用调用
yield_函数:- 操作系统状态: 应用调用
yield_,实际上调用了sys_yield系统调用。
- 操作系统状态: 应用调用
-
sys_yield系统调用实现:- 操作系统状态: 内核处理
sys_yield系统调用,将当前任务的上下文保存到 TCB 中,并选择下一个任务执行。 - 硬件状态:
- 上下文切换: 内核保存当前任务的寄存器和栈指针。
- CPU 调度: 内核调度器选择下一个任务,将其上下文恢复到寄存器和栈指针。
- 操作系统状态: 内核处理
-
内核调度下一个任务:
- 操作系统状态: 内核根据调度策略选择下一个任务,将其状态从 "准备执行" 切换为 "正在执行"。
- 硬件状态:
- 恢复上下文: 内核恢复下一个任务的寄存器和栈指针。
- CPU 执行: CPU 开始执行下一个任务。
多道程序的典型执行情况
通过 sys_yield 系统调用,任务可以在需要等待外设返回结果时主动交出 CPU 使用权,让其他任务执行。以下是一个典型的多道程序执行过程:
-
蓝色应用请求外设:
- 操作系统状态: 蓝色应用向外设提交请求,外设开始工作,需要一段时间才能返回结果。
- 硬件状态: 外设开始处理请求。
-
蓝色应用调用
sys_yield:- 操作系统状态: 蓝色应用调用
sys_yield系统调用,主动交出 CPU 使用权。 - 硬件状态: 内核保存蓝色应用的上下文,调度绿色应用执行。
- 操作系统状态: 蓝色应用调用
-
绿色应用执行:
- 操作系统状态: 内核将绿色应用的上下文恢复到寄存器,调度绿色应用执行。
- 硬件状态: CPU 开始执行绿色应用的代码。
-
外设返回结果前的多次
sys_yield:- 操作系统状态: 蓝色应用在外设返回结果前多次调用
sys_yield,内核多次在蓝色应用和其他任务之间进行调度。 - 硬件状态: CPU 多次在不同任务之间切换。
- 操作系统状态: 蓝色应用在外设返回结果前多次调用
-
外设返回结果:
- 操作系统状态: 蓝色应用最终等待到外设返回结果,可以继续执行。
- 硬件状态: CPU 继续执行蓝色应用的代码。
通过上述流程,操作系统实现了多道程序的并发执行,充分利用了 CPU 资源,提高了系统的响应速度和效率。
任务控制块与任务运行状态
任务控制块(Task Control Block,TCB)和任务运行状态是操作系统管理任务的核心组件。下面我们深入讲解它们的实现和作用。
任务运行状态
任务运行状态用于描述任务在生命周期中的不同阶段。定义在 os/src/task/task.rs 中:
#![allow(unused)] fn main() { // os/src/task/task.rs #[derive(Copy, Clone, PartialEq)] pub enum TaskStatus { UnInit, // 未初始化 Ready, // 准备运行 Running, // 正在运行 Exited, // 已退出 } }
详细解释
- UnInit: 任务未初始化,尚未准备好执行。
- Ready: 任务已准备好,可以运行,但尚未开始执行。
- Running: 任务当前正在 CPU 上执行。
- Exited: 任务已经完成执行并退出。
这些状态帮助内核跟踪每个任务的执行进度和调度需求。
任务控制块(Task Control Block)
任务控制块是用于维护每个任务状态和上下文的数据结构。定义在 os/src/task/task.rs 中:
#![allow(unused)] fn main() { // os/src/task/task.rs #[derive(Copy, Clone)] pub struct TaskControlBlock { pub task_status: TaskStatus, pub task_cx: TaskContext, } }
详细解释
- task_status: 任务状态(TaskStatus),表示任务当前的运行状态。
- task_cx: 任务上下文(TaskContext),包含任务的寄存器和栈指针等信息。
任务管理器
任务管理器是内核中用于管理多个任务控制块的全局结构。定义在 os/src/task/mod.rs 中:
#![allow(unused)] fn main() { // os/src/task/mod.rs pub struct TaskManager { num_app: usize, inner: UPSafeCell<TaskManagerInner>, } struct TaskManagerInner { tasks: [TaskControlBlock; MAX_APP_NUM], current_task: usize, } }
详细解释
-
TaskManager
- num_app: 应用程序数量,在
TaskManager初始化后保持不变。 - inner: 包含实际任务管理数据的结构体,用
UPSafeCell包装以保证线程安全。
- num_app: 应用程序数量,在
-
TaskManagerInner
- tasks: 任务控制块数组,包含所有任务的状态和上下文。
- current_task: 当前正在执行的任务编号。
这种设计将不变的字段(如 num_app)和变化的字段(如 tasks 和 current_task)分离,保证了代码的可读性和维护性。
硬件和操作系统流程细节
任务状态管理
-
任务初始化:
- 操作系统状态: 将任务状态设置为
TaskStatus::UnInit,准备初始化任务。
- 操作系统状态: 将任务状态设置为
-
任务准备运行:
- 操作系统状态: 将任务状态设置为
TaskStatus::Ready,表示任务已准备好,可以调度执行。
- 操作系统状态: 将任务状态设置为
-
任务开始运行:
- 操作系统状态: 将任务状态设置为
TaskStatus::Running,表示任务正在 CPU 上执行。 - 硬件状态: CPU 开始执行任务的代码。
- 操作系统状态: 将任务状态设置为
-
任务退出:
- 操作系统状态: 将任务状态设置为
TaskStatus::Exited,表示任务已完成执行并退出。 - 硬件状态: CPU 结束任务的执行。
- 操作系统状态: 将任务状态设置为
任务控制块管理
-
保存任务上下文:
- 操作系统状态: 在任务切换时,内核将当前任务的上下文保存到对应的
TaskControlBlock中。 - 硬件状态: CPU 将寄存器值写入内存中的
TaskContext结构体。
- 操作系统状态: 在任务切换时,内核将当前任务的上下文保存到对应的
-
恢复任务上下文:
- 操作系统状态: 在任务切换时,内核将下一个任务的上下文从对应的
TaskControlBlock中恢复。 - 硬件状态: CPU 从内存中的
TaskContext结构体读取寄存器值,并恢复到寄存器中。
- 操作系统状态: 在任务切换时,内核将下一个任务的上下文从对应的
任务管理器
-
初始化任务管理器:
- 操作系统状态: 在内核启动时,初始化
TaskManager和TaskManagerInner,并设置应用程序数量num_app。
- 操作系统状态: 在内核启动时,初始化
-
管理任务控制块数组:
- 操作系统状态: 任务管理器维护一个包含所有任务控制块的数组
tasks,并跟踪当前正在执行的任务编号current_task。
- 操作系统状态: 任务管理器维护一个包含所有任务控制块的数组
-
调度任务:
- 操作系统状态: 调度器选择下一个任务,将其状态从 "准备运行" 切换为 "正在运行",并通过任务控制块恢复其上下文。
- 硬件状态: CPU 切换到下一个任务的上下文,继续执行任务代码。
初始化 TaskManager 的全局实例 TASK_MANAGER
为了管理所有任务,操作系统需要一个全局的任务管理器实例 TASK_MANAGER。我们使用 lazy_static 宏来实现这个全局实例的懒加载初始化。
代码位置
#![allow(unused)] fn main() { // os/src/task/mod.rs }
代码详解
#![allow(unused)] fn main() { 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, }) }, } }; } }
详细解释
-
使用
lazy_static!宏:- 宏名:
lazy_static! - 作用: 实现全局静态变量的懒初始化,即在第一次使用时进行初始化。
- 宏名:
-
获取应用总数:
- 函数名:
get_num_app - 作用: 获取链接到内核的应用程序总数。
- 位置: 调用
loader子模块提供的接口。
#![allow(unused)] fn main() { let num_app = get_num_app(); } - 函数名:
-
初始化任务控制块数组:
- 结构体:
TaskControlBlock - 数组大小:
MAX_APP_NUM - 初始状态:
task_cx: 使用TaskContext::zero_init()初始化。task_status: 设置为TaskStatus::UnInit。
#![allow(unused)] fn main() { let mut tasks = [TaskControlBlock { task_cx: TaskContext::zero_init(), task_status: TaskStatus::UnInit, }; MAX_APP_NUM]; } - 结构体:
-
遍历并初始化每个任务控制块:
- 迭代器:
tasks.iter_mut().enumerate().take(num_app) - 初始化任务上下文: 使用
TaskContext::goto_restore(init_app_cx(i))。 - 设置任务状态: 将任务状态设置为
TaskStatus::Ready。
#![allow(unused)] fn main() { 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; } }- 函数:
init_app_cx(i)- 作用: 初始化每个应用程序的上下文。
- 函数:
TaskContext::goto_restore- 作用: 设置任务上下文的初始值,确保任务能够从正确的位置恢复执行。
- 迭代器:
-
创建并返回
TaskManager实例:- 结构体:
TaskManager - 字段:
num_app: 应用程序总数。inner: 包含实际任务管理数据的结构体,用UPSafeCell包装以保证线程安全。
- 内部结构体:
TaskManagerInner- 字段:
tasks: 任务控制块数组。current_task: 当前正在执行的任务编号。
- 字段:
#![allow(unused)] fn main() { TaskManager { num_app, inner: unsafe { UPSafeCell::new(TaskManagerInner { tasks, current_task: 0, }) }, } } - 结构体:
任务管理器的初始化流程
-
获取应用总数:
- 操作系统状态: 调用
get_num_app获取链接到内核的应用程序总数。 - 位置:
loader子模块提供的接口。
- 操作系统状态: 调用
-
初始化任务控制块数组:
- 操作系统状态: 创建并初始化一个大小为
MAX_APP_NUM的任务控制块数组。 - 初始状态: 每个任务控制块的
task_cx初始化为零,task_status设置为TaskStatus::UnInit。
- 操作系统状态: 创建并初始化一个大小为
-
遍历并初始化每个任务控制块:
- 操作系统状态: 使用迭代器遍历任务控制块数组的前
num_app个元素。 - 任务上下文初始化:
- 函数:
init_app_cx(i)- 作用: 初始化应用程序上下文。
- 函数:
TaskContext::goto_restore- 作用: 设置任务上下文的初始值,确保任务能够从正确的位置恢复执行。
- 函数:
- 设置任务状态: 将任务状态设置为
TaskStatus::Ready。
- 操作系统状态: 使用迭代器遍历任务控制块数组的前
-
创建
TaskManager实例:- 操作系统状态: 创建并初始化
TaskManager实例。 - 字段:
num_app: 应用程序总数。inner: 包含实际任务管理数据的结构体,用UPSafeCell包装以保证线程安全。
- 操作系统状态: 创建并初始化
-
返回
TaskManager实例:- 操作系统状态: 返回初始化后的
TaskManager实例,赋值给全局变量TASK_MANAGER。
- 操作系统状态: 返回初始化后的
总结
通过以上步骤,操作系统完成了 TaskManager 全局实例 TASK_MANAGER 的初始化。TASK_MANAGER 负责管理所有任务控制块,并通过其内部结构体 TaskManagerInner 来维护任务状态和上下文。这个设计确保了任务的正确初始化和调度,为多任务并发执行提供了基础。
ASK_MANAGER, TaskControlBlock 和 TaskContext 的对比
-
TASK_MANAGER:
- 位置:
os/src/task/mod.rs - 类型: 全局静态实例,使用
lazy_static!宏初始化。 - 功能: 管理所有任务控制块(TCB),负责任务调度和切换。
- 结构:
num_app: 应用程序数量。inner: 包含实际任务管理数据的结构体TaskManagerInner。
- 位置:
-
TaskControlBlock:
- 位置:
os/src/task/task.rs - 类型: 结构体。
- 功能: 维护单个任务的状态和上下文。
- 结构:
task_status: 任务状态(TaskStatus)。task_cx: 任务上下文(TaskContext)。
- 位置:
-
TaskContext:
- 位置:
os/src/task/context.rs - 类型: 结构体。
- 功能: 保存任务的寄存器和栈指针等上下文信息。
- 结构:
ra: 返回地址寄存器(Return Address)。sp: 栈指针寄存器(Stack Pointer)。s: 被调用者保存寄存器(Saved Registers),包含s0~s11。
- 位置:
实现 sys_yield 和 sys_exit
sys_yield 和 sys_exit 是两个重要的系统调用,分别用于让任务主动放弃 CPU 使用权和退出任务。它们依赖于任务管理器提供的接口来实现任务的调度和状态管理。
sys_yield 的实现
sys_yield 通过 suspend_current_and_run_next 接口实现,这个接口的作用是暂停当前的任务并切换到下一个任务。
代码位置和实现
#![allow(unused)] fn main() { // os/src/syscall/process.rs use crate::task::suspend_current_and_run_next; pub fn sys_yield() -> isize { suspend_current_and_run_next(); 0 } }
-
函数名:
sys_yield- 作用: 应用主动交出 CPU 使用权,让内核调度其他任务执行。
- 返回值: 总是返回 0。
-
调用
suspend_current_and_run_next接口- 位置:
os/src/task/mod.rs - 作用: 暂停当前任务并切换到下一个任务。
- 位置:
sys_exit 的实现
sys_exit 通过 exit_current_and_run_next 接口实现,这个接口的作用是退出当前的任务并切换到下一个任务。
代码位置和实现
#![allow(unused)] fn main() { // os/src/syscall/process.rs use crate::task::exit_current_and_run_next; pub fn sys_exit(exit_code: i32) -> ! { println!("[kernel] Application exited with code {}", exit_code); exit_current_and_run_next(); panic!("Unreachable in sys_exit!"); } }
-
函数名:
sys_exit- 参数:
exit_code(i32),表示任务退出时的状态码。 - 作用: 应用主动退出,并让内核调度其他任务执行。
- 返回值: 永远不返回(
!表示返回类型为 Never)。
- 参数:
-
调用
exit_current_and_run_next接口- 位置:
os/src/task/mod.rs - 作用: 退出当前任务并切换到下一个任务。
- 位置:
-
打印退出信息:
- 输出:
"[kernel] Application exited with code {}"。 - 作用: 在任务退出时打印退出状态码。
- 输出:
-
触发 panic:
- 作用: 理论上不应到达此处,触发 panic 以捕获错误。
suspend_current_and_run_next 和 exit_current_and_run_next 的实现
这两个函数都是先修改当前任务的运行状态,然后尝试切换到下一个任务。
代码位置和实现
#![allow(unused)] fn main() { // os/src/task/mod.rs pub fn suspend_current_and_run_next() { TASK_MANAGER.mark_current_suspended(); TASK_MANAGER.run_next_task(); } pub fn exit_current_and_run_next() { TASK_MANAGER.mark_current_exited(); TASK_MANAGER.run_next_task(); } }
-
函数名:
suspend_current_and_run_next- 作用: 暂停当前任务并切换到下一个任务。
- 步骤:
- 调用
TASK_MANAGER.mark_current_suspended()将当前任务状态标记为暂停。 - 调用
TASK_MANAGER.run_next_task()切换到下一个任务。
- 调用
-
函数名:
exit_current_and_run_next- 作用: 退出当前任务并切换到下一个任务。
- 步骤:
- 调用
TASK_MANAGER.mark_current_exited()将当前任务状态标记为退出。 - 调用
TASK_MANAGER.run_next_task()切换到下一个任务。
- 调用
修改任务运行状态和任务切换
修改运行状态:mark_current_suspended
修改运行状态主要涉及到任务控制块数组中的当前任务状态。在任务管理器 TaskManager 中实现一个方法来修改当前任务的状态,例如 mark_current_suspended。
代码位置和实现
#![allow(unused)] fn main() { // os/src/task/mod.rs impl TaskManager { fn mark_current_suspended(&self) { let mut inner = self.inner.exclusive_access(); let current = inner.current_task; inner.tasks[current].task_status = TaskStatus::Ready; } } }
详细解释
-
获取内部任务管理器的可变引用:
- 方法:
self.inner.exclusive_access() - 作用: 获取
TaskManagerInner的可变引用,以便修改内部状态。
- 方法:
-
获取当前任务的索引:
- 变量:
current - 作用: 从
TaskManagerInner中获取当前任务的索引。
- 变量:
-
修改当前任务的状态:
- 变量:
inner.tasks[current].task_status - 作用: 将当前任务的状态修改为
TaskStatus::Ready,表示任务暂停,准备再次运行。
- 变量:
切换到下一个任务:run_next_task
run_next_task 方法负责切换到下一个准备运行的任务。
代码位置和实现
#![allow(unused)] fn main() { // os/src/task/mod.rs impl TaskManager { 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!"); } } } }
详细解释
-
寻找下一个准备运行的任务:
- 方法:
self.find_next_task() - 作用: 寻找一个状态为
TaskStatus::Ready的任务,并返回其 ID。
- 方法:
-
获取内部任务管理器的可变引用:
- 方法:
self.inner.exclusive_access() - 作用: 获取
TaskManagerInner的可变引用,以便修改内部状态。
- 方法:
-
设置下一个任务的状态:
- 变量:
inner.tasks[next].task_status - 作用: 将下一个任务的状态设置为
TaskStatus::Running,表示任务正在运行。
- 变量:
-
更新当前任务索引:
- 变量:
inner.current_task - 作用: 更新当前任务的索引为下一个任务的索引。
- 变量:
-
获取当前和下一个任务的上下文指针:
- 变量:
current_task_cx_ptr和next_task_cx_ptr - 作用: 分别获取当前任务和下一个任务的上下文指针,以便在任务切换时使用。
- 变量:
-
手动 drop 内部任务管理器的可变引用:
- 方法:
drop(inner) - 作用: 手动释放
inner的可变引用,以确保TASK_MANAGER的inner字段回到未被借用的状态。
- 方法:
-
任务切换:
- 函数:
__switch - 作用: 使用汇编代码实现的任务切换函数,切换当前任务的上下文到下一个任务的上下文。
- 函数:
-
处理所有任务完成的情况:
- 作用: 如果没有找到准备运行的任务,
find_next_task返回None,内核会触发panic,表示所有任务都已经完成。
- 作用: 如果没有找到准备运行的任务,
寻找下一个任务:find_next_task
find_next_task 方法用于寻找下一个准备运行的任务,并返回其 ID。
代码位置和实现
#![allow(unused)] fn main() { // os/src/task/mod.rs impl TaskManager { 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) } } }
详细解释
-
获取内部任务管理器的可变引用:
- 方法:
self.inner.exclusive_access() - 作用: 获取
TaskManagerInner的可变引用,以便读取内部状态。
- 方法:
-
获取当前任务的索引:
- 变量:
current - 作用: 从
TaskManagerInner中获取当前任务的索引。
- 变量:
-
遍历任务数组寻找准备运行的任务:
- 方法:
current + 1..current + self.num_app + 1: 从当前任务的下一个任务开始遍历,循环遍历任务数组。.map(|id| id % self.num_app): 确保任务索引在任务数组范围内循环。.find(|id| inner.tasks[*id].task_status == TaskStatus::Ready): 找到第一个状态为TaskStatus::Ready的任务,并返回其 ID。
- 方法:
总结
通过 mark_current_suspended 方法,我们可以将当前任务的状态修改为 Ready,表示任务暂停,准备再次运行。run_next_task 方法负责切换到下一个准备运行的任务,并调用 __switch 进行上下文切换。find_next_task 方法用于寻找下一个准备运行的任务,确保任务切换的正确进行。
修改运行状态和任务切换的关键步骤
-
修改当前任务的状态:
- 获取
TaskManagerInner的可变引用。 - 修改当前任务的状态为
Ready或其他状态。
- 获取
-
任务切换:
- 寻找下一个准备运行的任务。
- 更新任务状态和当前任务索引。
- 获取上下文指针。
- 调用汇编实现的
__switch进行上下文切换。
通过这些步骤,操作系统实现了多任务的调度和切换,确保任务能够有效地并发执行。
第一次进入用户态
背景
在第二章中,CPU 第一次从内核态进入用户态的方法是通过在内核栈上压入构造好的 Trap 上下文,并通过调用 __restore 函数恢复上下文。本章在此基础上进行扩展,详细解释任务上下文的初始化和任务切换的实现。
初始化任务控制块
在任务管理器中初始化任务控制块时,我们使用 init_app_cx 函数向内核栈压入一个 Trap 上下文,并返回压入 Trap 上下文后栈指针(sp)的值。goto_restore 函数保存传入的 sp,并将返回地址(ra)设置为 __restore 的入口地址。
代码位置和实现
#![allow(unused)] fn main() { // os/src/task/mod.rs 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; } }
-
初始化任务上下文:
- 函数名:
init_app_cx - 作用: 向内核栈压入一个 Trap 上下文,并返回压入 Trap 上下文后栈指针的值。
- 函数名:
-
设置任务上下文:
- 函数名:
TaskContext::goto_restore - 作用: 保存传入的栈指针,并将返回地址设置为
__restore的入口地址。
- 函数名:
TaskContext 实现
TaskContext 结构体用于保存任务上下文,包括返回地址(ra)、栈指针(sp)和保存寄存器(s0~s11)。
代码位置和实现
#![allow(unused)] fn main() { // os/src/task/context.rs impl TaskContext { pub fn goto_restore(kstack_ptr: usize) -> Self { extern "C" { fn __restore(); } Self { ra: __restore as usize, sp: kstack_ptr, s: [0; 12], } } } }
-
返回地址(ra):
- 设置为
__restore的入口地址:- 函数名:
__restore - 作用: 恢复 Trap 上下文并进入用户态。
- 函数名:
- 设置为
-
栈指针(sp):
- 设置为
init_app_cx返回的值:- 作用: 指向内核栈上的 Trap 上下文。
- 设置为
-
保存寄存器(s0~s11):
- 初始化为 0:
- 作用: 在任务初始化时,保存寄存器的值为 0。
- 初始化为 0:
运行第一个任务
在 rust_main 函数中,我们调用 task::run_first_task 来执行第一个应用。该函数切换到第一个任务并进入用户态。
代码位置和实现
#![allow(unused)] fn main() { // os/src/task/mod.rs 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!"); } }
-
获取第一个任务的上下文指针:
- 变量:
next_task_cx_ptr - 作用: 获取第一个任务的上下文指针。
- 变量:
-
设置任务状态:
- 变量:
task0.task_status - 作用: 将第一个任务的状态设置为
TaskStatus::Running。
- 变量:
-
获取任务上下文指针
- 获取任务上下文引用:
- 代码:
&task0.task_cx - 作用: 获取第一个任务的任务上下文
task_cx的引用。
- 代码:
- 转换为指针:
- 代码:
as *const TaskContext - 作用: 将任务上下文的引用转换为原生指针(const pointer),指向
TaskContext结构体。 - 原因:
__switch函数接受指向TaskContext的原生指针。
- 代码:
- 变量:
next_task_cx_ptr- 类型:
*const TaskContext - 作用: 保存指向第一个任务的任务上下文的指针。
- 类型:
- 获取任务上下文引用:
-
手动释放
TaskManagerInner的可变引用:- 方法:
drop(inner) - 作用: 手动释放
inner的可变引用,以确保TASK_MANAGER的inner字段回到未被借用的状态。
- 方法:
-
任务切换:
- 函数名:
__switch - 参数:
_unused和next_task_cx_ptr - 作用: 切换到第一个任务的上下文,进入用户态。
- 声明未使用的任务上下文:
- 代码:
let mut _unused = TaskContext::zero_init(); - 作用: 创建一个未使用的任务上下文
_unused,用作__switch的第一个参数。 - 原因: 在第一次任务切换时,没有真正的上一个任务上下文,所以使用一个占位符
_unused。
- 代码:
- 转换为指针:
- 代码:
&mut _unused as *mut TaskContext - 作用: 将未使用的任务上下文的引用转换为原生指针(mut pointer),指向
TaskContext结构体。 - 原因:
__switch函数接受指向TaskContext的原生指针。
- 代码:
- 调用
__switch:- 代码:
__switch(&mut _unused as *mut TaskContext, next_task_cx_ptr); - 作用: 调用
__switch函数进行任务切换。 - 参数:
&mut _unused as *mut TaskContext: 指向未使用的任务上下文的指针,作为当前任务的上下文指针。next_task_cx_ptr: 指向第一个任务的任务上下文的指针,作为下一个任务的上下文指针。
- 代码:
- 使用
unsafe块:- 原因: 调用
__switch函数涉及到直接操作原生指针,这在 Rust 中是unsafe的,需要用unsafe块包裹。
- 原因: 调用
- 声明未使用的任务上下文:
- 函数名:
__restore 函数的实现
在 __switch 中恢复 sp 后,sp 将指向 init_app_cx 构造的 Trap 上下文,后面就回到第二章的情况了。此外,__restore 的实现需要做出变化:它不再需要在开头 mv sp, a0,因为在 __switch 之后,sp 就已经正确指向了我们需要的 Trap 上下文地址。
总结
通过初始化任务控制块和任务上下文,我们能够将 CPU 从内核态切换到用户态。具体步骤包括:
-
初始化任务控制块:
- 使用
init_app_cx向内核栈压入 Trap 上下文,并返回栈指针。 - 使用
TaskContext::goto_restore设置任务上下文,包括返回地址和栈指针。
- 使用
-
运行第一个任务:
- 调用
task::run_first_task切换到第一个任务的上下文,并进入用户态。
- 调用
-
任务切换:
- 在
__switch中切换任务上下文,恢复 sp 后进入用户态。
- 在
通过这些步骤,操作系统能够正确地初始化任务并在首次运行时切换到用户态,确保任务能够正确执行。
分时多任务系统
分时多任务系统通过时间片轮转算法 (Round-Robin, RR) 来实现任务调度。在这种系统中,每个任务只能连续执行一个时间片(可能在毫秒量级),然后内核强制性切换到下一个任务。
关键概念
-
时间片 (Time Slice):
- 是任务连续执行的时间度量单位。
- 一般在毫秒量级。
-
时间片轮转算法 (Round-Robin, RR):
- 每个任务按顺序轮流执行一个时间片。
-
时钟中断:
- 计时器到达设定时间时触发,用于实现时间片轮转调度。
时钟中断与计时器
RISC-V 架构要求处理器维护时钟计数器 mtime 和比较寄存器 mtimecmp。当 mtime 的值超过 mtimecmp 时,会触发时钟中断。
获取当前时间
get_time 函数用于获取当前的 mtime 计数器值。
代码位置和实现
#![allow(unused)] fn main() { // os/src/timer.rs use riscv::register::time; pub fn get_time() -> usize { time::read() } }
详细解释
-
导入
riscv::register::time模块:- 模块:
riscv::register::time - 作用: 提供读取 RISC-V 时钟计数器
mtime的功能。
- 模块:
-
定义
get_time函数:- 返回类型:
usize - 作用: 返回当前的
mtime计数器值。
- 返回类型:
-
读取当前时间:
- 方法:
time::read() - 作用: 读取
mtime计数器的当前值并返回。
- 方法:
设置时钟中断
set_timer 函数用于设置 mtimecmp 的值,从而在指定时间后触发时钟中断。
代码位置和实现
#![allow(unused)] fn main() { // os/src/sbi.rs const SBI_SET_TIMER: usize = 0; pub fn set_timer(timer: usize) { sbi_call(SBI_SET_TIMER, timer, 0, 0); } }
详细解释
-
定义常量
SBI_SET_TIMER:- 类型:
usize - 值: 0
- 作用: SBI 调用
set_timer的函数编号。
- 类型:
-
定义
set_timer函数:- 参数:
timer(类型usize),表示设置的mtimecmp的值。 - 作用: 调用 SBI 接口设置
mtimecmp的值。
- 参数:
-
调用
sbi_call函数:- 参数:
SBI_SET_TIMER: SBI 调用编号。timer: 设置的mtimecmp的值。- 其他两个参数为 0。
- 作用: 通过 SBI 接口调用设置
mtimecmp的值。
- 参数:
定时触发
set_next_trigger 函数用于计算下一个时钟中断的触发时间,并设置 mtimecmp 的值。
代码位置和实现
#![allow(unused)] fn main() { // os/src/timer.rs use crate::config::CLOCK_FREQ; const TICKS_PER_SEC: usize = 100; pub fn set_next_trigger() { set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC); } }
详细解释
-
导入
CLOCK_FREQ常量:- 模块:
config - 作用: 表示平台的时钟频率,单位为赫兹。
- 模块:
-
定义常量
TICKS_PER_SEC:- 类型:
usize - 值: 100
- 作用: 表示每秒的时钟中断次数(即每 10ms 一次)。
- 类型:
-
定义
set_next_trigger函数:- 作用: 设置下一个时钟中断的触发时间。
-
计算下一个时钟中断时间:
- 方法:
get_time() + CLOCK_FREQ / TICKS_PER_SEC - 作用: 获取当前时间
get_time(),加上CLOCK_FREQ / TICKS_PER_SEC的增量,计算出 10ms 后的时间。
- 方法:
-
设置时钟中断:
- 函数:
set_timer - 参数: 计算出的下一个时钟中断时间。
- 作用: 设置
mtimecmp的值,使得 10ms 后触发时钟中断。
- 函数:
时钟中断处理
当 mtime 超过 mtimecmp 的值时,会触发时钟中断。时钟中断处理程序需要执行以下步骤:
- 保存当前任务的上下文。
- 调度下一个任务。
- 恢复下一个任务的上下文。
时钟中断处理流程
-
触发时钟中断:
- 硬件: 当
mtime超过mtimecmp的值时,硬件触发时钟中断。 - 作用: 通知内核需要进行任务调度。
- 硬件: 当
-
保存当前任务的上下文:
- 内核: 保存当前任务的寄存器、栈指针等上下文信息。
- 作用: 保持当前任务的状态,以便以后恢复。
-
调度下一个任务:
- 内核: 调用任务调度算法(如 RR 算法)选择下一个任务。
- 作用: 确定下一个任务,并准备切换上下文。
-
恢复下一个任务的上下文:
- 内核: 恢复下一个任务的寄存器、栈指针等上下文信息。
- 作用: 切换到下一个任务,开始执行。
-
重新设置时钟中断:
- 内核: 调用
set_next_trigger函数,设置下一个时钟中断时间。 - 作用: 确保下一个时钟中断能够正确触发,实现持续的任务调度。
- 内核: 调用
总结
通过上述步骤和代码实现,我们构建了一个分时多任务系统。该系统利用时钟中断和时间片轮转算法,实现任务的定时调度和上下文切换,确保每个任务能够公平地获得 CPU 使用权,并在特定时间片后强制切换任务,提高系统的响应速度和资源利用率。
计时需求和新系统调用
为了满足后续的计时需求,我们需要设计一个能够以微秒为单位返回当前计时器值的函数,并新增一个系统调用,使应用能够获取当前时间。
以微秒为单位返回当前计时器值
在 timer 子模块中,我们设计了 get_time_us 函数,用于以微秒为单位返回当前计时器的值。
代码位置和实现
#![allow(unused)] fn main() { // os/src/timer.rs use riscv::register::time; use crate::config::CLOCK_FREQ; const MICRO_PER_SEC: usize = 1_000_000; pub fn get_time_us() -> usize { time::read() / (CLOCK_FREQ / MICRO_PER_SEC) } }
详细解释
-
常量定义:
- 常量:
MICRO_PER_SEC - 类型:
usize - 值: 1_000_000(表示一秒中的微秒数)
- 常量:
-
函数定义:
get_time_us- 返回类型:
usize - 作用: 以微秒为单位返回当前计时器的值
- 返回类型:
-
读取当前时间:
- 函数:
time::read() - 作用: 读取 RISC-V 时钟计数器
mtime的当前值
- 函数:
-
计算当前时间(微秒):
- 公式:
time::read() / (CLOCK_FREQ / MICRO_PER_SEC) - 作用: 将
mtime值转换为微秒数
- 公式:
新增系统调用:获取当前时间
为了使应用能够获取当前时间,我们设计了一个新的系统调用 sys_get_time,并定义了 TimeVal 结构体来存储时间值。
系统调用定义
#![allow(unused)] fn main() { /// 功能:获取当前的时间,保存在 TimeVal 结构体 ts 中,_tz 在我们的实现中忽略 /// 返回值:返回是否执行成功,成功则返回 0 /// syscall ID:169 fn sys_get_time(ts: *mut TimeVal, _tz: usize) -> isize; }
结构体 TimeVal 的定义
#![allow(unused)] fn main() { // os/src/syscall/process.rs #[repr(C)] pub struct TimeVal { pub sec: usize, pub usec: usize, } }
系统调用实现
#![allow(unused)] fn main() { // os/src/syscall/process.rs use crate::timer::get_time_us; pub fn sys_get_time(ts: *mut TimeVal, _tz: usize) -> isize { if ts.is_null() { return -1; } let us = get_time_us(); let time_val = TimeVal { sec: us / 1_000_000, usec: us % 1_000_000, }; unsafe { *ts = time_val; } 0 } }
详细解释
-
引入
get_time_us函数:- 模块:
timer - 作用: 获取当前时间(微秒)
- 模块:
-
函数定义:
sys_get_time- 参数:
ts: 指向TimeVal结构体的指针,用于存储当前时间_tz: 时区参数,在我们的实现中被忽略
- 返回值:
isize,表示执行结果,成功返回 0
- 参数:
-
检查指针是否为空:
- 判断:
ts.is_null() - 作用: 检查传入的指针是否为
null,如果是则返回错误码-1
- 判断:
-
获取当前时间(微秒):
- 函数:
get_time_us() - 作用: 获取当前时间,单位为微秒
- 函数:
-
计算时间值:
- 变量:
us - 公式:
sec: us / 1_000_000:将微秒转换为秒usec: us % 1_000_000:取余数,得到剩余的微秒数
- 变量:
-
创建
TimeVal结构体:- 结构体:
TimeVal - 字段:
sec: 秒数usec: 微秒数
- 结构体:
-
写入时间值到指针:
- 代码:
*ts = time_val; - 作用: 使用
unsafe块将计算的时间值写入传入的指针所指向的内存位置
- 代码:
-
返回成功码:
- 返回值:
0 - 作用: 表示系统调用成功执行
- 返回值:
总结
通过上述步骤和代码实现,我们完成了以微秒为单位返回当前计时器值的函数 get_time_us,以及一个新的系统调用 sys_get_time,使应用能够获取当前时间。
关键步骤
-
定义
get_time_us函数:- 获取当前
mtime计数器值并转换为微秒数。
- 获取当前
-
定义
TimeVal结构体:- 用于存储秒和微秒两个时间字段。
-
实现
sys_get_time系统调用:- 检查指针合法性。
- 获取当前时间并计算秒和微秒。
- 将时间值写入传入的
TimeVal结构体指针。
通过这些步骤,我们能够实现一个分时多任务系统中的计时功能,并通过系统调用使用户应用能够获取当前时间。
解释涉及的公式及其原因
1. 设置下一个时钟中断触发时间
#![allow(unused)] fn main() { pub fn set_next_trigger() { set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC); } }
详细解释
-
get_time():- 作用: 获取当前的
mtime计数器值。mtime是 RISC-V 架构中的一个硬件计数器,用于计时。
- 作用: 获取当前的
-
CLOCK_FREQ / TICKS_PER_SEC:- 公式:
CLOCK_FREQ表示时钟频率(每秒的计数值),TICKS_PER_SEC表示每秒的时钟中断次数。 - 作用: 计算每个时间片的时钟计数器增量。
CLOCK_FREQ是一秒内的时钟计数器增量,TICKS_PER_SEC是每秒的时间片数,CLOCK_FREQ / TICKS_PER_SEC就是每个时间片对应的时钟计数器增量。
- 公式:
-
get_time() + CLOCK_FREQ / TICKS_PER_SEC:- 作用: 计算下一个时钟中断触发的时间点。当前时间加上一个时间片的增量就是下一个时钟中断触发的时间点。
-
set_timer():- 作用: 将计算出的下一个触发时间点设置到
mtimecmp中,以便触发时钟中断。
- 作用: 将计算出的下一个触发时间点设置到
原因
- 这个公式确保每个时间片后触发一次时钟中断,以实现时间片轮转调度。
CLOCK_FREQ / TICKS_PER_SEC确保时间片的长度固定,从而实现公平的任务调度。
2. 获取当前时间(微秒)
#![allow(unused)] fn main() { pub fn get_time_us() -> usize { time::read() / (CLOCK_FREQ / MICRO_PER_SEC) } }
详细解释
-
time::read():- 作用: 读取当前的
mtime计数器值。
- 作用: 读取当前的
-
CLOCK_FREQ / MICRO_PER_SEC:- 公式:
CLOCK_FREQ表示时钟频率,MICRO_PER_SEC表示一秒内的微秒数(1,000,000)。 - 作用: 计算每微秒对应的时钟计数器增量。
CLOCK_FREQ是一秒内的时钟计数器增量,MICRO_PER_SEC是一秒内的微秒数,CLOCK_FREQ / MICRO_PER_SEC就是每微秒对应的时钟计数器增量。
- 公式:
-
time::read() / (CLOCK_FREQ / MICRO_PER_SEC):- 作用: 将当前的
mtime计数器值转换为微秒数。通过除以每微秒的计数器增量,可以得到当前的时间(单位为微秒)。
- 作用: 将当前的
原因
- 这个公式确保
mtime计数器值可以被转换为更精细的时间单位(微秒),从而实现高精度的计时功能。
3. 定义 TimeVal 结构体及其初始化
#![allow(unused)] fn main() { pub struct TimeVal { pub sec: usize, pub usec: usize, } let time_val = TimeVal { sec: us / 1_000_000, usec: us % 1_000_000, }; }
详细解释
-
结构体
TimeVal:- 字段:
sec: 秒数usec: 微秒数
- 字段:
-
初始化
TimeVal结构体:- 变量:
us - 类型:
usize - 作用: 表示当前时间(单位为微秒)
- 变量:
-
sec: us / 1_000_000:- 公式:
us / 1_000_000 - 作用: 将微秒数转换为秒数。通过将微秒数除以 1,000,000(每秒的微秒数),得到当前的秒数。
- 公式:
-
usec: us % 1_000_000:- 公式:
us % 1_000_000 - 作用: 获取当前秒数之外的剩余微秒数。通过取余操作,得到当前秒数之外的微秒数。
- 公式:
原因
- 这个公式将微秒数分解为秒数和微秒数,使得时间表示更加精确和易于理解。
TimeVal结构体将时间分为两个部分,便于系统调用返回更高精度的时间值。
总结
这些公式和计算方法确保了系统能够以高精度计时,并实现时间片轮转调度。通过这些机制,操作系统可以准确地管理任务执行时间和调度,确保系统的公平性和响应速度。
- 时间片轮转调度:
set_next_trigger计算下一个时钟中断触发时间,以实现时间片轮转调度。
- 高精度计时:
get_time_us将mtime计数器值转换为微秒,以实现高精度计时。
- 时间表示:
TimeVal结构体将时间分为秒和微秒,提供更高精度的时间表示。
这些机制和计算方法共同构成了一个高效的分时多任务操作系统。
RISC-V 架构中的嵌套中断问题
在 RISC-V 架构中,嵌套中断(Nested Interrupt)指在处理一个中断的过程中,又被同特权级或高特权级的中断打断。默认情况下,RISC-V 硬件会屏蔽同特权级的中断,以避免嵌套中断。
1. Trap 和中断处理
在 RISC-V 中,当 Trap(包括中断、异常和系统调用)发生时,系统进入某个特权级(如 S 特权级),并进行相应的处理。在这个过程中,默认情况下,当前特权级的中断会被屏蔽。
2. S 特权级的中断屏蔽机制
sstatus 寄存器
sstatus 是一个控制和状态寄存器,包含多个字段,其中 sie 和 spie 与中断屏蔽相关:
- sstatus.sie: S 特权级中断使能位。若为 1,表示使能 S 特权级的中断;若为 0,表示屏蔽 S 特权级的中断。
- sstatus.spie: 保存先前的 S 特权级中断使能状态。
Trap 处理流程
-
Trap 发生:
- 动作:
sstatus.sie被保存在sstatus.spie中,同时sstatus.sie被置零。 - 结果: 屏蔽所有 S 特权级的中断,确保当前 Trap 处理过程中不会被其他 S 特权级中断打断。
- 动作:
-
Trap 处理完毕:
- 动作:
sret指令执行时,将sstatus.sie恢复为sstatus.spie的值。 - 结果: 恢复 S 特权级中断使能状态。
- 动作:
3. 嵌套中断与嵌套 Trap
嵌套中断
嵌套中断指在处理一个中断的过程中,又被同特权级或高特权级的中断打断。
- 默认情况:
- 硬件会避免同特权级中断的嵌套,因为
sstatus.sie在 Trap 处理过程中被置零。
- 硬件会避免同特权级中断的嵌套,因为
- 手动设置:
- 可以通过手动设置
sstatus寄存器来允许同特权级中断的嵌套。
- 可以通过手动设置
- 高特权级中断:
- 高特权级的中断仍可以打断低特权级的中断处理,这种情况是无法避免的。
嵌套 Trap
嵌套 Trap 指在处理一个 Trap 的过程中,再次发生 Trap。嵌套中断是嵌套 Trap 的一种情况。
- 例子:
- 在处理系统调用时发生页面缺失异常。
- 在处理中断时发生另一种类型的 Trap(如非法指令异常)。
处理嵌套中断的机制
RISC-V 硬件和操作系统提供机制来处理嵌套中断,以确保系统的稳定性和响应性。
1. sstatus CSR 的设置
操作系统可以通过手动设置 sstatus 寄存器来控制中断的屏蔽和使能。
代码示例
#![allow(unused)] fn main() { use riscv::register::sstatus; fn enable_nested_interrupts() { unsafe { sstatus::set_sie(); } } fn disable_nested_interrupts() { unsafe { sstatus::clear_sie(); } } }
2. Trap 和中断处理流程
操作系统在处理 Trap 和中断时,可以根据需要选择是否允许嵌套中断。
示例流程
-
进入 Trap 处理程序:
- 屏蔽同特权级中断:
sstatus.sie被置零。 - 根据需要决定是否启用嵌套中断:调用
enable_nested_interrupts。
- 屏蔽同特权级中断:
-
处理 Trap:
- 处理异常、系统调用或中断。
- 可能发生新的 Trap(如嵌套中断或异常)。
-
退出 Trap 处理程序:
- 恢复中断使能状态:
sret指令将sstatus.sie恢复为sstatus.spie的值。
- 恢复中断使能状态:
3. 操作系统中的嵌套中断处理
操作系统可以在设计中考虑嵌套中断的处理,以提高系统的响应性和鲁棒性。
设计策略
-
确定哪些中断可以嵌套:
- 某些高优先级中断可以嵌套,例如时钟中断或紧急系统事件。
- 低优先级中断可能不允许嵌套。
-
实现嵌套中断处理:
- 在中断处理程序中,根据中断优先级决定是否允许嵌套。
- 保存和恢复上下文时,确保不会影响正在处理的中断或 Trap。
总结
在 RISC-V 架构中,默认情况下同特权级的中断在 Trap 处理过程中会被屏蔽,以避免嵌套中断。通过手动设置 sstatus CSR,可以允许嵌套中断的发生。而高特权级的中断可以打断低特权级的中断处理,这是无法避免的。
嵌套中断与嵌套 Trap 是操作系统设计中需要处理的复杂问题。通过合理设计中断处理机制和使用适当的硬件控制寄存器,操作系统可以有效管理和处理嵌套中断,确保系统的稳定性和响应速度。
抢占式调度
抢占式调度是一种调度算法,通过时钟中断和计时器强制任务切换。利用 RISC-V 架构中的时钟中断机制,我们可以实现时间片轮转调度算法(Round-Robin, RR),确保每个任务都能公平地获得 CPU 资源。
实现抢占式调度的步骤
-
时钟中断处理:
- 当触发 S 特权级时钟中断时,重新设置计时器,暂停当前任务,并切换到下一个任务。
-
启用时钟中断:
- 在执行第一个应用前,启用 S 特权级时钟中断,并设置第一个 10ms 的计时器。
时钟中断处理
在 trap_handler 函数中新增一个分支,用于处理 S 特权级时钟中断。
代码位置和实现
#![allow(unused)] fn main() { // os/src/trap/mod.rs match scause.cause() { Trap::Interrupt(Interrupt::SupervisorTimer) => { set_next_trigger(); suspend_current_and_run_next(); } } }
详细解释
-
识别时钟中断:
- 方法:
scause.cause() - 枚举:
Trap::Interrupt(Interrupt::SupervisorTimer) - 作用: 识别 S 特权级时钟中断。
- 方法:
-
重新设置计时器:
- 函数:
set_next_trigger() - 作用: 设置下一个时钟中断触发时间,以便在 10ms 后再次触发中断。
- 函数:
-
暂停当前任务并切换到下一个任务:
- 函数:
suspend_current_and_run_next() - 作用: 暂停当前任务的执行,并切换到下一个任务。
- 函数:
启用时钟中断
在执行第一个应用前,启用 S 特权级时钟中断,并设置第一个 10ms 的计时器。
代码位置和实现
#![allow(unused)] fn main() { // os/src/main.rs #[no_mangle] pub fn rust_main() -> ! { // ... trap::enable_timer_interrupt(); timer::set_next_trigger(); // ... } }
详细解释
-
启用 S 特权级时钟中断:
- 函数:
trap::enable_timer_interrupt() - 作用: 设置 S 特权级时钟中断使能位,使得时钟中断不会被屏蔽。
- 函数:
-
设置第一个 10ms 的计时器:
- 函数:
timer::set_next_trigger() - 作用: 设置下一个时钟中断触发时间,为 10ms 后。
- 函数:
启用时钟中断的具体实现
在 trap 模块中,实现 enable_timer_interrupt 函数。
#![allow(unused)] fn main() { // os/src/trap/mod.rs use riscv::register::sie; pub fn enable_timer_interrupt() { unsafe { sie::set_stimer(); } } }
详细解释
-
导入
sie模块:- 模块:
riscv::register::sie - 作用: 提供设置和清除 S 特权级中断使能位的功能。
- 模块:
-
启用 S 特权级时钟中断:
- 函数:
sie::set_stimer() - 作用: 设置 S 特权级时钟中断使能位,使得时钟中断不会被屏蔽。
- 函数:
核心函数和机制
suspend_current_and_run_next
暂停当前任务并切换到下一个任务。
代码位置和实现
#![allow(unused)] fn main() { // os/src/task/mod.rs pub fn suspend_current_and_run_next() { TASK_MANAGER.mark_current_suspended(); TASK_MANAGER.run_next_task(); } }
详细解释
-
标记当前任务为暂停:
- 函数:
TASK_MANAGER.mark_current_suspended() - 作用: 将当前任务的状态设置为
TaskStatus::Ready。
- 函数:
-
运行下一个任务:
- 函数:
TASK_MANAGER.run_next_task() - 作用: 调度并运行下一个准备好的任务。
- 函数:
set_next_trigger
设置下一个时钟中断触发时间。
代码位置和实现
#![allow(unused)] fn main() { // os/src/timer.rs use crate::config::CLOCK_FREQ; const TICKS_PER_SEC: usize = 100; pub fn set_next_trigger() { set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC); } }
详细解释
-
获取当前时间:
- 函数:
get_time() - 作用: 获取当前的
mtime计数器值。
- 函数:
-
计算下一个时钟中断触发时间:
- 公式:
CLOCK_FREQ / TICKS_PER_SEC - 作用: 计算 10ms 的计时器增量。
- 公式:
-
设置下一个时钟中断触发时间:
- 函数:
set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC) - 作用: 设置
mtimecmp的值,使得 10ms 后触发时钟中断。
- 函数:
总结
通过利用 RISC-V 架构中的时钟中断机制,我们实现了抢占式调度。这一调度算法通过时钟中断定期强制任务切换,确保每个任务都能公平地获得 CPU 资源,实现了时间片轮转调度算法。
关键步骤
-
时钟中断处理:
- 在
trap_handler中新增分支,处理 S 特权级时钟中断。 - 重新设置计时器,暂停当前任务并切换到下一个任务。
- 在
-
启用时钟中断:
- 在执行第一个应用前,启用 S 特权级时钟中断。
- 设置第一个 10ms 的计时器。
-
核心函数:
suspend_current_and_run_next: 暂停当前任务并切换到下一个任务。set_next_trigger: 设置下一个时钟中断触发时间。
通过这些机制,我们实现了一个高效的抢占式调度系统,确保多任务操作系统中的任务能够公平地竞争 CPU 资源。
多道程序与分时多任务操作系统的全流程描述
1. 系统初始化
文件: src/main.rs
主要步骤:
rust_main函数初始化系统,包括启用时钟中断和设置第一个 10ms 的计时器。
关键函数和变量:
trap::enable_timer_interrupt()timer::set_next_trigger()task::run_first_task()
2. 启用 S 特权级时钟中断
文件: src/trap/mod.rs
主要步骤:
enable_timer_interrupt函数设置 S 特权级时钟中断使能位,确保时钟中断不会被屏蔽。
关键函数和变量:
sie::set_stimer()
3. 设置计时器
文件: src/timer.rs
主要步骤:
set_next_trigger函数设置下一个时钟中断触发时间。get_time函数获取当前的mtime计数器值。set_timer函数设置mtimecmp的值。
关键函数和变量:
CLOCK_FREQTICKS_PER_SECset_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC)
4. 加载应用程序
文件: src/loader.rs
主要步骤:
load_apps函数将应用程序加载到内存。init_app_cx函数初始化任务控制块,构造 Trap 上下文,并返回应用程序的初始栈指针。
关键函数和变量:
get_num_app()TaskContext::goto_restore()
5. 任务上下文和任务控制块
文件: src/task/context.rs
主要步骤:
TaskContext结构体定义任务上下文,包括返回地址(ra)、栈指针(sp)和保存寄存器(s0~s11)。TaskControlBlock结构体定义任务控制块,包括任务状态和上下文。
关键函数和变量:
TaskContext::goto_restore()
6. 任务管理器
文件: src/task/mod.rs
主要步骤:
TASK_MANAGER全局实例初始化任务管理器。suspend_current_and_run_next函数暂停当前任务并切换到下一个任务。run_next_task函数调度并运行下一个任务。find_next_task函数查找下一个准备运行的任务。
关键函数和变量:
TaskManagerTaskManagerInnermark_current_suspended()mark_current_exited()
7. 任务切换
文件: src/task/switch.S 和 src/task/switch.rs
主要步骤:
__switch函数使用汇编实现任务上下文的保存和恢复,进行任务切换。
关键函数和变量:
__switch
8. 时钟中断处理
文件: src/trap/mod.rs
主要步骤:
trap_handler函数处理 S 特权级时钟中断,重新设置计时器,并进行任务切换。
关键函数和变量:
scause.cause()Trap::Interrupt(Interrupt::SupervisorTimer)set_next_trigger()suspend_current_and_run_next()
总结
通过以上步骤,我们实现了一个多道程序与分时多任务的操作系统。关键步骤包括系统初始化、启用时钟中断、设置计时器、加载应用程序、管理任务上下文和任务控制块、实现任务切换以及处理时钟中断。这些步骤确保了操作系统能够高效地调度任务,实现公平的时间片轮转调度。