[RustSBI output]
[kernel] Hello, world!
AAAAAAAAAA [1/5]
BBBBBBBBBB [1/2]
....
CCCCCCCCCC [2/3]
AAAAAAAAAA [3/5]
Test write_b OK!
[kernel] Application exited with code 0
CCCCCCCCCC [3/3]
...
[kernel] Application exited with code 0
[kernel] Panicked at src/task/mod.rs:106 All applications completed!
构建应用
└── user
├── build.py(新增:使用 build.py 构建应用使得它们占用的物理地址区间不相交)
├── Makefile(修改:使用 build.py 构建应用)
└── src (各种应用程序)
改进OS:Loader
模块加载和执行程序
├── os
│ └── src
│ ├── batch.rs(移除:功能分别拆分到 loader 和 task 两个子模块)
│ ├── config.rs(新增:保存内核的一些配置)
│ ├── loader.rs(新增:将应用加载到内存并进行管理)
│ ├── main.rs(修改:主函数进行了修改)
│ ├── syscall(修改:新增若干 syscall)
改进OS:TaskManager
模块管理/切换程序的执行
├── os
│ └── src
│ ├── task(新增:task 子模块,主要负责任务管理)
│ │ ├── context.rs(引入 Task 上下文 TaskContext)
│ │ ├── mod.rs(全局任务管理器和提供给其他模块的接口)
│ │ ├── switch.rs(将任务切换的汇编代码解释为 Rust 接口 __switch)
│ │ ├── switch.S(任务切换的汇编代码)
│ │ └── task.rs(任务控制块 TaskControlBlock 和任务状态 TaskStatus 的定义)
user/src/bin/
├── 00write_a.rs # 5次显示 AAAAAAAAAA 字符串
├── 01write_b.rs # 2次显示 BBBBBBBBBB 字符串
└── 02write_c.rs # 3次显示 CCCCCCCCCC 字符串
BASE_ADDRESS
都是不同的。应用起始地址 = 基址 + 数字编号 * 0x20000
//00write_a.rs
fn main() -> i32 {
for i in 0..HEIGHT {
for _ in 0..WIDTH {
print!("A");
}
println!(" [{}/{}]", i + 1, HEIGHT);
yield_(); //放弃处理器
}
println!("Test write_a OK!");
0
}
const SYSCALL_YIELD: usize = 124;
const SYSCALL_YIELD: usize = 124;
pub fn sys_yield() -> isize {
syscall(SYSCALL_YIELD, [0, 0, 0])
}
pub fn yield_() -> isize {
sys_yield()
}
fn get_base_i(app_id: usize) -> usize {
APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT
}
let base_i = get_base_i(i);
// load app from data section to memory
let src = (app_start[i]..app_start[i + 1]);
let dst = (base_i.. base_i+src.len());
dst.copy_from_slice(src);
执行时机
执行方式
这就完成了支持把应用都放到内存中的锯齿螈OS
任务运行状态
pub enum TaskStatus {
UnInit,
Ready,
Running,
Exited,
}
任务切换
1// os/src/task/context.rs
2 pub struct TaskContext {
3 ra: usize, //函数返回地址
4 sp: usize, //用户栈指针
5 s: [usize; 12], //属于Callee函数保存的寄存器集s0~s11
6}
任务上下文
1// os/src/task/context.rs
2 pub struct TaskContext {
3 ra: usize,
4 sp: usize,
5 s: [usize; 12],
6}
// os/src/trap/context.rs
pub struct TrapContext {
pub x: [usize; 32],
pub sstatus: Sstatus,
pub sepc: usize,
}
任务(Task)上下文 vs 系统调用(Trap)上下文
任务切换是来自两个不同应用在内核中的 Trap 控制流之间的切换
回顾:控制流
回顾:控制流
CSAPP:
回顾:控制流
CSAPP:
回顾:控制流上下文(执行环境的状态)
计算机组成原理:站在硬件的角度来看普通控制流或异常控制流的具体执行过程,我们会发现从控制流起始的某条指令执行开始,指令可访问的所有物理资源的内容,包括自带的所有通用寄存器、特权级相关特殊寄存器、以及指令访问的内存等,会随着指令的执行而逐渐发生变化。
对于当前实践的OS,没有虚拟资源,而物理资源内容就是通用寄存器/CSR寄存器
回顾:控制流上下文(执行环境的状态)
OS面临的挑战
在分属不同任务的两个Trap控制流之间进行hacker级操作,即进行Trap上下文切换,从而实现任务切换。
__switch
__switch
从实现的角度讲, __switch 函数和一个普通的函数之间的核心差别仅仅是它会换栈 。
Trap 控制流之间的切换 -- 函数__switch
阶段 [1]:在 Trap 控制流 A 调用 __switch 之前,A 的内核栈上只有 Trap 上下文和 Trap 处理函数的调用栈信息,而 B 是之前被切换出去的;
阶段 [2]:A 在 A 任务上下文空间在里面保存 CPU 当前的寄存器快照;
阶段 [3]:这一步极为关键,读取 next_task_cx_ptr 指向的 B 任务上下文,根据 B 任务上下文保存的内容来恢复 ra 寄存器、s0~s11 寄存器以及 sp 寄存器。只有这一步做完后, __switch 才能做到一个函数跨两条控制流执行,即 通过换栈也就实现了控制流的切换 。
阶段 [4]:上一步寄存器恢复后,可以看到通过恢复 sp 寄存器换到了任务 B 的内核栈上,进而实现了控制流的切换。这就是为什么 __switch 能做到一个函数跨两条控制流执行。此后,当 CPU 执行 ret 汇编伪指令完成 __switch 函数返回后,任务 B 可以从调用 __switch 的位置继续向下执行。
__switch
的实现 1// os/src/task/switch.rs
2
3global_asm!(include_str!("switch.S"));
4
5use super::TaskContext;
6
7extern "C" {
8 pub fn __switch(
9 current_task_cx_ptr: *mut TaskContext,
10 next_task_cx_ptr: *const TaskContext
11 );
12}
__switch
的实现12 __switch:
13 # 阶段 [1]
14 # __switch(
15 # current_task_cx_ptr: *mut TaskContext,
16 # next_task_cx_ptr: *const TaskContext
17 # )
18 # 阶段 [2]
19 # save kernel stack of current task
20 sd sp, 8(a0)
21 # save ra & s0~s11 of current execution
22 sd ra, 0(a0)
23 .set n, 0
24 .rept 12
25 SAVE_SN %n
26 .set n, n + 1
27 .endr
__switch
的实现28 # 阶段 [3]
29 # restore ra & s0~s11 of next execution
30 ld ra, 0(a1)
31 .set n, 0
32 .rept 12
33 LOAD_SN %n
34 .set n, n + 1
35 .endr
36 # restore kernel stack of next task
37 ld sp, 8(a1)
38 # 阶段 [4]
39 ret
pub struct TaskControlBlock {
pub task_status: TaskStatus,
pub task_cx: TaskContext,
}
struct TaskManagerInner {
tasks: [TaskControlBlock; MAX_APP_NUM],
current_task: usize,
}
sys_yield
和sys_exit
系统调用pub fn sys_yield() -> isize {
suspend_current_and_run_next();
0
}
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_yield
和sys_exit
系统调用// os/src/task/mod.rs
pub fn suspend_current_and_run_next() {
mark_current_suspended();
run_next_task();
}
pub fn exit_current_and_run_next() {
mark_current_exited();
run_next_task();
}
sys_yield
和sys_exit
系统调用 fn run_next_task(&self) {
......
unsafe {
__switch(
current_task_cx_ptr, //当前任务上下文
next_task_cx_ptr, //下个任务上下文
);
}
如果能搞定,我们就实现了支持多道程序协作调度的始初龙操作系统
内核程序设计 -- 基本思路
// os/src/sbi.rs
pub fn set_timer(timer: usize) {
sbi_call(SBI_SET_TIMER, timer, 0, 0);
}
// os/src/timer.rs
pub fn set_next_trigger() {
set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
}
pub fn rust_main() -> ! {
trap::enable_timer_interrupt();
timer::set_next_trigger();
}
// os/src/trap/mod.rs trap_handler函数
......
match scause.cause() {
Trap::Interrupt(Interrupt::SupervisorTimer) => {
set_next_trigger();
suspend_current_and_run_next();
}
}
这样我们就实现了分时多任务的腔骨龙操作系统
锯齿螈 始初龙 腔骨龙
J. Lyons & Co.是一家成立于1884年的英国连锁餐厅,食品制造业和酒店集团。
https://baike.baidu.com/item/EDSAC/7639053 电子延迟存储自动计算器(英文:Electronic Delay Storage Automatic Calculator、EDSAC)是英国的早期计算机。1946年,英国剑桥大学数学实验室的莫里斯·威尔克斯教授和他的团队受冯·诺伊曼的First Draft of a Report on the EDVAC的启发,以EDVAC为蓝本,设计和建造EDSAC,1949年5月6日正式运行,是世界上第一台实际运行的存储程序式电子计算机。 是EDSAC在工程实施中同样遇到了困难:不是技术,而是资金缺乏。在关键时刻,威尔克斯成功地说服了伦敦一家面包公司J.Lyons&Co。.的老板投资该项目,终于使计划绝处逢生。1949年5月6日,EDSAC首次试运行成功,它从带上读人一个生成平方表的程序并执行,正确地打印出结果。作为对投资的回报,LyOHS公司取得了批量生产EDSAC的权利,这就是于1951年正式投入市场的LEO计算机(Lyons Electronic Office),这通常被认为是世界上第一个商品化的计算机型号,因此这也成了计算机发展史上的一件趣事:第一家生产出商品化计算机的厂商原先竟是面包房。Lyons公司后来成为英国著名的“国际计算机有限公司”即ICL的一部分。