写在前面
本随笔是非常菜的菜鸡写的。如有问题请及时提出。
可以联系:1160712160@qq.com
GitHhub:https://github.com/WindDevil (目前啥也没有
编程题
第一题
扩展内核,能够显示操作系统切换任务的过程。
首先先回忆一下操作系统切换任务的过程.
因此只需要在这些关键节点加上println!
即可.
首先是在Trap
的时候输出,这里其余的都有了,只增加了Interrupt::SupervisorTimer
的输出:
rust
// os/src/trap/mod.rs
#[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) => {
println!("[kernel] PageFault in application, kernel killed it.");
exit_current_and_run_next();
}
Trap::Exception(Exception::IllegalInstruction) => {
println!("[kernel] IllegalInstruction in application, kernel killed it.");
exit_current_and_run_next();
}
Trap::Interrupt(Interrupt::SupervisorTimer) => {
println!("\nTimer interrupt,time slice used up!");
set_next_trigger();
println!("Next timer set!");
suspend_current_and_run_next();
}
_ => {
panic!(
"Unsupported trap {:?}, stval = {:#x}!",
scause.cause(),
stval
);
}
}
cx
}
修改Task
,在修改当前任务的状态之后输出:
rust
// os/src/task/mod.rs
/// 当前任务主动放弃 CPU 使用权
pub fn suspend_current_and_run_next()
{
mark_current_suspended();
println!("\nTask {} suspended", TASK_MANAGER.inner.exclusive_access().current_task);
run_next_task();
}
/// 当前任务退出
pub fn exit_current_and_run_next()
{
mark_current_exited();
println!("\nTask {} exited", TASK_MANAGER.inner.exclusive_access().current_task);
run_next_task();
}
在寻找到下一个task
后,输出下一个task
.
rust
// os/src/task/mod.rs
impl TaskManager
{
... ...
fn run_next_task(&self)
{
if let Some(next) = self.find_next_task()
{
println!("\nFound next task {}", next);
let mut inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.current_task = next;
inner.tasks[next].task_status = TaskStatus::Running;
let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
let next_task_cx_ptr = &mut inner.tasks[next].task_cx as *const TaskContext;
drop(inner);
unsafe
{
__switch(current_task_cx_ptr, next_task_cx_ptr);
}
println!("\nTask {} has been switched out", next);
}
else
{
println!("All applications completed!");
shutdown(false);
}
}
}
这时候make run
,输出:
shell
[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name : riscv-virtio,qemu
[rustsbi] Platform SMP : 1
[rustsbi] Platform Memory : 0x80000000..0x88000000
[rustsbi] Boot HART : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[kernel] trap init end
power_3 [10000/200000]
power_3 [20000/200000]
power_3 [30000/200000]
power_3 [40000/200000]
power_3 [50000/200000]
power_3 [60000/200000]
power_3 [70000/200000]
power_3 [80000/200000]
power_3 [90000/200000]
power_3 [100000/200000]
power_3 [110000/200000]
power_3 [120000/200000]
power_3 [130000/200000]
power_3 [140000/200000]
power_3 [150000/200000]
power_3 [160000/200000]
power_3 [170000/200000]
power_3 [180000/200000]
power_3 [190000/200000]
power_3 [200000/200000]
3^200000 = 871008973(MOD 998244353)
Test power_3 OK!
[kernel] Application exited with code 0
Task 0 exited
Found next task 1
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0
Task 1 exited
Found next task 2
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0
Task 2 exited
Found next task 3
Task 3 suspended
Found next task 3
Task 3 has been switched out
Timer interrupt,time slice used up!
Next timer set!
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Test sleep OK!
[kernel] Application exited with code 0
Task 3 exited
All applications completed!
这里为了输出更符合标准,可能需要在每句字符串前边加上[kernel]
, 这样可以分辨这句LOG是从哪里输出出来的.
第二题
扩展内核,能够统计每个应用执行后的完成时间:用户态完成时间和内核态完成时间。
这个只需要略微明白mtime
的作用就行,另外,如果你是一直看我博客到现在的,我现在提醒你可以去做第一章没有做完的作业了.
原本的打算是实现一个线程安全的数组,但是这样的写法说明我对OOP的理解学到了直肠里,主打一个脑子不好使.
其实只需在每一个任务控制块里边加入一个任务运行开始时间的变量就行.
rust
// os/src/task/task.rs
#[derive(Copy, Clone)]
pub struct TaskControlBlock
{
pub task_status: TaskStatus,
pub task_cx: TaskContext,
pub task_start_time: isize,
}
不要忘记初始化的时候初始化这个变量:
rust
// os/src/task/mod.rs
lazy_static!
{
/// 全局单例的任务管理器
pub static ref TASK_MANAGER: TaskManager =
{
let num_app = get_num_app();
let mut tasks = [TaskControlBlock{
task_status: TaskStatus::UnInit,
task_cx: TaskContext::zero_init(),
task_start_time: -1,
}
; MAX_APP_NUM];
for(i,task) in tasks.iter_mut().enumerate()
{
task.task_cx = TaskContext::goto_restore(init_app_cx(i));
task.task_status = TaskStatus::Ready;
}
TaskManager
{
num_app,
inner: unsafe
{
UPSafeCell::new(TaskManagerInner
{
tasks,
current_task: 0,
})
}
}
};
}
这里注意要初始化为-1
,判断有没有初始化过.
然后要在切换任务的时候获取当前时间,再退出时输出任务时间:
rust
impl TaskManager
{
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;
let mut _unused = TaskContext::zero_init();
inner.tasks[0].task_start_time = get_time_us() as isize;
drop(inner);
unsafe
{
__switch(&mut _unused as *mut TaskContext, next_task_cx_ptr)
}
panic!("unreachable in run_first_task");
}
fn mark_current_suspended(&self)
{
let mut inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.tasks[current].task_status = TaskStatus::Ready;
}
fn mark_current_exited(&self)
{
let mut inner = self.inner.exclusive_access();
let current = inner.current_task;
println!("\nTask {} RunTime:{}~{} {}us", current, get_time_us(),inner.tasks[current].task_start_time,get_time_us() as isize -inner.tasks[current].task_start_time);
inner.tasks[current].task_status = TaskStatus::Exited;
}
fn find_next_task(&self) -> Option<usize>
{
let inner = self.inner.exclusive_access();
let current = inner.current_task;
(current + 1..current+1+self.num_app)
.map(|id| id%self.num_app)
.find(|id| inner.tasks[*id].task_status == TaskStatus::Ready)
}
fn run_next_task(&self)
{
if let Some(next) = self.find_next_task()
{
println!("\nFound next task {}", next);
let mut inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.current_task = next;
if inner.tasks[next].task_start_time == -1
{
inner.tasks[next].task_start_time = get_time_us() as isize;
}
inner.tasks[next].task_status = TaskStatus::Running;
let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
let next_task_cx_ptr = &mut inner.tasks[next].task_cx as *const TaskContext;
drop(inner);
unsafe
{
__switch(current_task_cx_ptr, next_task_cx_ptr);
}
println!("\nTask {} has been switched out", next);
}
else
{
println!("All applications completed!");
shutdown(false);
}
}
}
lazy_static!
{
/// 全局单例的任务管理器
pub static ref TASK_MANAGER: TaskManager =
{
let num_app = get_num_app();
let mut tasks = [TaskControlBlock{
task_status: TaskStatus::UnInit,
task_cx: TaskContext::zero_init(),
task_start_time: -1,
}
; MAX_APP_NUM];
for(i,task) in tasks.iter_mut().enumerate()
{
task.task_cx = TaskContext::goto_restore(init_app_cx(i));
task.task_status = TaskStatus::Ready;
}
TaskManager
{
num_app,
inner: unsafe
{
UPSafeCell::new(TaskManagerInner
{
tasks,
current_task: 0,
})
}
}
};
}
然后make run
:
shell
[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name : riscv-virtio,qemu
[rustsbi] Platform SMP : 1
[rustsbi] Platform Memory : 0x80000000..0x88000000
[rustsbi] Boot HART : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[kernel] trap init end
power_3 [10000/200000]
power_3 [20000/200000]
power_3 [30000/200000]
power_3 [40000/200000]
power_3 [50000/200000]
power_3 [60000/200000]
power_3 [70000/200000]
power_3 [80000/200000]
power_3 [90000/200000]
power_3 [100000/200000]
power_3 [110000/200000]
power_3 [120000/200000]
power_3 [130000/200000]
power_3 [140000/200000]
power_3 [150000/200000]
power_3 [160000/200000]
power_3 [170000/200000]
power_3 [180000/200000]
power_3 [190000/200000]
power_3 [200000/200000]
3^200000 = 871008973(MOD 998244353)
Test power_3 OK!
[kernel] Application exited with code 0
Task 0 RunTime:12921~5597 7330us
Task 0 exited
Found next task 1
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [
Timer interrupt,time slice used up!
Next timer set!
Task 1 suspended
Found next task 2
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0
Task 2 RunTime:19099~16545 2554us
Task 2 exited
Found next task 3
Task 3 suspended
Found next task 1
Task 2 has been switched out
80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0
Task 1 RunTime:21086~13182 7904us
Task 1 exited
Found next task 3
Task 1 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Test sleep OK!
[kernel] Application exited with code 0
Task 3 RunTime:23113~19237 3876us
Task 3 exited
All applications completed!
但是这样似乎记录的是任务创建和任务结束之间的时间差,和我们目标中的还不一样,我们希望统计的是用户态和内核态的完成时间.
那么不仅没有区分用户态和内核态,而且还没有再任务不运行的时候停止计时.
这里再加一个成员就行:
rust
// os/src/task/task.rs
... ...
pub struct TaskControlBlock
{
... ...
pub task_start_time: isize,
pub task_running_time: isize,
}
... ...
在两种进行任务切换的情况下,计算任务运行时的差值:
rust
// os/src/task/mod.rs
... ...
fn mark_current_suspended(&self)
{
... ...
inner.tasks[current].task_running_time += get_time_us() as isize - inner.tasks[current].task_start_time;
... ...
}
fn mark_current_exited(&self)
{
... ...
inner.tasks[current].task_running_time += get_time_us() as isize - inner.tasks[current].task_start_time;
println!("\nTask {} RunTime:{} us", current, inner.tasks[current].task_running_time);
... ...
}
... ...
这里注意要每次运行的时候都更新task_start_time
:
rust
fn run_next_task(&self)
{
if let Some(next) = self.find_next_task()
{
... ...
inner.tasks[next].task_start_time = get_time_us() as isize;
... ...
}
else
{
... ...
}
}
也要注意初始化这个变量为0
:
rust
lazy_static!
{
/// 全局单例的任务管理器
pub static ref TASK_MANAGER: TaskManager =
{
... ...
let mut tasks = [TaskControlBlock{
... ...
task_running_time: 0,
}
; MAX_APP_NUM];
for(i,task) in tasks.iter_mut().enumerate()
{
... ...
}
TaskManager
{
... ...
}
};
}
这时候进行运行:
shell
[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name : riscv-virtio,qemu
[rustsbi] Platform SMP : 1
[rustsbi] Platform Memory : 0x80000000..0x88000000
[rustsbi] Boot HART : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[kernel] trap init end
power_3 [10000/200000]
power_3 [20000/200000]
power_3 [30000/200000]
power_3 [40000/200000]
power_3 [50000/200000]
power_3 [60000/200000]
power_3 [70000/200000]
power_3 [80000/200000]
power_3 [90000/200000]
power_3 [100000/200000]
power_3 [110000/200000]
power_3 [120000/200000]
power_3 [130000/200000]
power_3 [140000/200000]
power_3 [150000/200000]
power_3 [160000/200000]
power_3 [170000/200000]
power_3 [180000/200000]
power_3 [190000/200000]
power_3 [200000/200000]
3^200000 = 871008973(MOD 998244353)
Test power_3 OK!
[kernel] Application exited with code 0
Task 0 RunTime:4285us
Task 0 exited
Found next task 1
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0
Task 1 RunTime:4896us
Task 1 exited
Found next task 2
power_7 [
Timer interrupt,time slice used up!
Next timer set!
Task 2 suspended
Found next task 3
Task 3 suspended
Found next task 2
Task 3 has been switched out
10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0
Task 2 RunTime:4611us
Task 2 exited
Found next task 3
Task 2 has been switched out
Test sleep OK!
[kernel] Application exited with code 0
Task 3 RunTime:291us
Task 3 exited
All applications completed!
这里看到,Task 3 RunTime:291us
,和user/src/bin/03sleep.rs
的内容相匹配,由于我们的系统频率为os/src/boards/qemu.rs
所记述的10000000
,因此任务的作用是是睡眠300us:
rust
#![no_std]
#![no_main]
#[macro_use]
extern crate user_lib;
use user_lib::{get_time, yield_};
#[no_mangle]
fn main() -> i32 {
let current_timer = get_time();
let wait_for = current_timer + 3000;
while get_time() < wait_for {
yield_();
}
println!("Test sleep OK!");
0
}
但是似乎搞得又不太对,只是记录了每个任务的执行时间 ,那么内核态时间 和用户态时间呢?
看了很久参考答案,发现根本没有成功得到停表的精髓,其实就是只用了一个时间戳,然后只计算时间戳和当前时间之间的插值而已,别想的太复杂.
这里看到参考答案晕了的原因是没有发现__switch前后的内核时间应该分别记录到两个任务中 ,同一个函数的时间可以不记录在同一个地方.
这里是不发生任务切换 的情况,这时候脑海中就一直想为什么参考答案要在mark_current_suspended
和mark_current_exited
中加停表呢?:
如果发生任务切换,那么更复杂一些:
这时候应该看到,我们应该在__swtich
前后刷新停表,并且记录时间.
这是时候修改TaskManager
的方法,这里refresh_stop_watch
的位置没有加到mark_current_suspended
和mark_current_exited
中 ,这是因为理解不同 ,我认为发生了__switch
之后才算真正完成了切换,而答案中则认为Task状态发生改变就是发生了切换:
rust
impl TaskManager
{
... ...
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;
let mut _unused = TaskContext::zero_init();
inner.stop_watch = get_time_us();
drop(inner);
unsafe
{
__switch(&mut _unused as *mut TaskContext, next_task_cx_ptr)
}
panic!("unreachable in run_first_task");
}
... ...
fn run_next_task(&self)
{
if let Some(next) = self.find_next_task()
{
println!("\nFound next task {}", next);
let mut inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.current_task = next;
inner.tasks[next].task_status = TaskStatus::Running;
let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
let next_task_cx_ptr = &mut inner.tasks[next].task_cx as *const TaskContext;
inner.refresh_stop_watch();
drop(inner);
unsafe
{
__switch(current_task_cx_ptr, next_task_cx_ptr);
}
println!("\nTask {} has been switched out", next);
}
else
{
println!("All applications completed!");
shutdown(false);
}
}
... ...
/// 统计内核时间,从现在开始算的是用户时间
fn user_time_start(&self) {
let mut inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.tasks[current].kernel_time += inner.refresh_stop_watch();
}
/// 统计用户时间,从现在开始算的是内核时间
fn user_time_end(&self) {
let mut inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.tasks[current].user_time += inner.refresh_stop_watch();
}
}
其中用到的refresh_stop_watch
是给TaskManagerInner
实现的方法,这里不给TaskManager
实现这个方法而是给TaskManagerInner
实现这个方法的原因是,因为记录的时候往往要拿到inner
的所有权,才能获得当前任务信息 ,如果是给这时候如果是TaskManager
的方法,那么肯定会需要再度获取inner
的所有权,会造成所有权处理的混乱,并且会增加时间计算不准确:
rust
impl TaskManagerInner
{
fn refresh_stop_watch(&mut self) -> usize
{
let start_time = self.stop_watch;
self.stop_watch = get_time_us();
self.stop_watch - start_time
}
}
这里封装user_time_start
和user_time_end
两个函数:
rust
/// 获取当前任务的内核态运行时间
pub fn user_time_start()
{
TASK_MANAGER.user_time_start();
}
/// 获取当前任务的用户态运行时间
pub fn user_time_end()
{
TASK_MANAGER.user_time_end();
}
同时在trap
模块中加入记录时间的函数:
rust
#[no_mangle]
/// handle an interrupt, exception, or system call from user space
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
user_time_start();
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) => {
println!("[kernel] PageFault in application, kernel killed it.");
exit_current_and_run_next();
}
Trap::Exception(Exception::IllegalInstruction) => {
println!("[kernel] IllegalInstruction in application, kernel killed it.");
exit_current_and_run_next();
}
Trap::Interrupt(Interrupt::SupervisorTimer) => {
println!("\nTimer interrupt,time slice used up!");
set_next_trigger();
println!("Next timer set!");
suspend_current_and_run_next();
}
_ => {
panic!(
"Unsupported trap {:?}, stval = {:#x}!",
scause.cause(),
stval
);
}
}
user_time_end();
cx
}
这时候执行make run
:
shell
[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name : riscv-virtio,qemu
[rustsbi] Platform SMP : 1
[rustsbi] Platform Memory : 0x80000000..0x88000000
[rustsbi] Boot HART : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[kernel] trap init end
power_3 [10000/200000]
power_3 [20000/200000]
power_3 [30000/200000]
power_3 [40000/200000]
power_3 [50000/200000]
power_3 [60000/200000]
power_3 [70000/200000]
power_3 [80000/200000]
power_3 [90000/200000]
power_3 [100000/200000]
power_3 [110000/200000]
power_3 [120000/200000]
power_3 [130000/200000]
power_3 [140000/200000]
power_3 [150000/200000]
power_3 [160000/200000]
power_3 [170000/200000]
power_3 [180000/200000]
power_3 [190000/200000]
power_3 [200000/200000]
3^200000 = 871008973(MOD 998244353)
Test power_3 OK!
[kernel] Application exited with code 0
Task 0 exited,kernel time:1926 user time:3263
Task 0 exited
Found next task 1
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0
Task 1 exited,kernel time:1065 user time:1955
Task 1 exited
Found next task 2
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
Timer interrupt,time slice used up!
Next timer set!
Task 2 suspended
Found next task 3
Task 3 suspended
Found next task 2
Task 3 has been switched out
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0
Task 2 exited,kernel time:1009 user time:2060
Task 2 exited
Found next task 3
Task 2 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Test sleep OK!
[kernel] Application exited with code 0
Task 3 exited,kernel time:83 user time:355
Task 3 exited
All applications completed!
第三题
编写浮点应用程序A,并扩展内核,支持面向浮点应用的正常切换与抢占。
也许这就是缘分吧,今天刚看到这个视频.
这让我想起之前硬汉说的有关于MCU 原子操作和临界段保护 的问题.
那么我们知道的是浮点应用计算是分为好几步的,而不是原子操作,也就是进行任务切换的时候可能会存在浮点数计算计算到一半的情况.
那么回想之前的做法,我们需要进行保存上下文的操作.
首先可以查阅技术手册.理解RISC-V的浮点数计算相关知识.
RV32F 和 RV32D 的浮点寄存器。单精度寄存器占用了 32 个双精度寄存器中最右边的一半。
可一看到这里的寄存器分为了:
- 浮点临时寄存器
- 浮点保存寄存器
- 浮点参数和返回值寄存器
查阅技术手册:
RISC-V 有足够多的寄存器来达到两全其美的结果:既能将操作数存放在寄存器中,同 时也能减少保存和恢复寄存器的次数。其中的关键在于,在函数调用的过程中不保留部分寄 存器存储的值,称它们为临时寄存器;另一些寄存器则对应地称为保存寄存器。不再调用其 它函数的函数称为叶函数。当一个叶函数只有少量的参数和局部变量时,它们可以都被存储 在寄存器中,而不会"溢出(spilling)"到内存中。但如果函数参数和局部变量很多,程序 还是需要把寄存器的值保存在内存中,不过这种情况并不多见。
函数调用中其它的寄存器,要么被当做保存寄存器来使用,在函数调用前后值不变;要 么被当做临时寄存器使用,在函数调用中不保留。函数会更改用来保存返回值的寄存器,因 此它们和临时寄存器类似;用来给函数传递参数的寄存器也不需要保留,因此它们也类似于 临时寄存器。对于其它一些寄存器,调用者需要保证它们在函数调用前后保持不变:比如用 于存储返回地址的寄存器和存储栈指针的寄存器。
这张图列出了寄存器的 RISC-V 应用程序 二进制接口(ABI)名称和它们在函数调用中是否保留的规定。
这里我思考了是不是临时寄存器就不需要保存上下文,而保存寄存器就需要保存上下文 这个问题,但是回头一想,在trap.S
里我们选择了保存x0~x31
(一部分特殊的寄存器暂时不用保存),这里吧也是有临时寄存器和保存寄存器的.
那么都需要保存了.查阅技术手册:
fsd
:这个指令通常用来存储浮点数。它的全称可以是"float store double",但确切的名字取决于具体的架构。fsd
指令会将一个浮点寄存器中的值存储到指定的内存地址上。例如,在MIPS架构中,fsd
会将一个双精度浮点数从浮点寄存器写入内存。fld
:这个指令通常用于加载浮点数。它的全称可以是"float load double"。fld
指令从内存地址读取一个双精度浮点数值,并将其加载到指定的浮点寄存器中。这同样是在某些架构如MIPS中使用的命令。
这时候的trap.S
asm
.altmacro
.macro SAVE_GP n
sd x\n, \n*8(sp)
.endm
.macro LOAD_GP n
ld x\n, \n*8(sp)
.endm
.section .text
.globl __alltraps
.globl __restore
.align 2
__alltraps:
csrrw sp, sscratch, sp
# now sp->kernel stack, sscratch->user stack
# allocate a TrapContext on kernel stack
addi sp, sp, -34*8
# save general-purpose registers
sd x1, 1*8(sp)
# skip sp(x2), we will save it later
sd x3, 3*8(sp)
# skip tp(x4), application does not use it
# save x5~x31
.set n, 5
.rept 27
SAVE_GP %n
.set n, n+1
.endr
# we can use t0/t1/t2 freely, because they were saved on kernel stack
csrr t0, sstatus
csrr t1, sepc
sd t0, 32*8(sp)
sd t1, 33*8(sp)
# read user stack from sscratch and save it on the kernel stack
csrr t2, sscratch
# 浮点寄存器
fsd f0, 34*8(sp)
fsd f1, 35*8(sp)
fsd f2, 36*8(sp)
fsd f3, 37*8(sp)
fsd f4, 38*8(sp)
fsd f5, 39*8(sp)
fsd f6, 40*8(sp)
fsd f7, 41*8(sp)
fsd f8, 42*8(sp)
fsd f9, 43*8(sp)
fsd f10, 44*8(sp)
fsd f11, 45*8(sp)
fsd f12, 46*8(sp)
fsd f13, 47*8(sp)
fsd f14, 48*8(sp)
fsd f15, 49*8(sp)
fsd f16, 50*8(sp)
fsd f17, 51*8(sp)
fsd f18, 52*8(sp)
fsd f19, 53*8(sp)
fsd f20, 54*8(sp)
fsd f21, 55*8(sp)
fsd f22, 56*8(sp)
fsd f23, 57*8(sp)
fsd f24, 58*8(sp)
fsd f25, 59*8(sp)
fsd f26, 60*8(sp)
fsd f27, 61*8(sp)
fsd f28, 62*8(sp)
fsd f29, 63*8(sp)
fsd f30, 64*8(sp)
fsd f31, 65*8(sp)
sd t2, 2*8(sp)
# set input argument of trap_handler(cx: &mut TrapContext)
mv a0, sp
call trap_handler
__restore:
# case1: start running app by __restore
# case2: back to U after handling trap
# now sp->kernel stack(after allocated), sscratch->user stack
# restore sstatus/sepc
ld t0, 32*8(sp)
ld t1, 33*8(sp)
ld t2, 2*8(sp)
csrw sstatus, t0
csrw sepc, t1
csrw sscratch, t2
# restore general-purpuse registers except sp/tp
ld x1, 1*8(sp)
ld x3, 3*8(sp)
.set n, 5
.rept 27
LOAD_GP %n
.set n, n+1
.endr
# 浮点寄存器
fld f0, 34*8(sp)
fld f1, 35*8(sp)
fld f2, 36*8(sp)
fld f3, 37*8(sp)
fld f4, 38*8(sp)
fld f5, 39*8(sp)
fld f6, 40*8(sp)
fld f7, 41*8(sp)
fld f8, 42*8(sp)
fld f9, 43*8(sp)
fld f10, 44*8(sp)
fld f11, 45*8(sp)
fld f12, 46*8(sp)
fld f13, 47*8(sp)
fld f14, 48*8(sp)
fld f15, 49*8(sp)
fld f16, 50*8(sp)
fld f17, 51*8(sp)
fld f18, 52*8(sp)
fld f19, 53*8(sp)
fld f20, 54*8(sp)
fld f21, 55*8(sp)
fld f22, 56*8(sp)
fld f23, 57*8(sp)
fld f24, 58*8(sp)
fld f25, 59*8(sp)
fld f26, 60*8(sp)
fld f27, 61*8(sp)
fld f28, 62*8(sp)
fld f29, 63*8(sp)
fld f30, 64*8(sp)
fld f31, 65*8(sp)
# release TrapContext on kernel stack
addi sp, sp, 34*8
# now sp->kernel stack, sscratch->user stack
csrrw sp, sscratch, sp
sret
但是这时候参阅练习参考答案,发现仍有不周到之处.
此外,支持浮点指令可能还需要(包括但不限于)以下条件:
- 机器本身支持浮点指令
- Rust 编译目标包含浮点指令
- 在
os/Makefile
中的TARGET := riscv64gc-unknown-none-elf
支持浮点指令,而对应的riscv64imac
则不支持。 - 如果机器本身支持但使用
riscv64imac
作为编译目标,仍然可以通过强行插入指令的方式来支持浮点,如fld fs0, 280(sp)
在 RISCV 指令集中表示为机器码0x2472
,就可以在上面的trap.S
中插入
.short 0x2472 # fld fs0, 280(sp)
来支持浮点指令
- 在
- 需要通过控制浮点控制状态寄存器(如
fcsr
)来检查FPU状态。详见 https://five-embeddev.com/riscv-isa-manual/latest/machine.html#machine-trap-vector-base-address-register-mtvec
这时候就需要参阅linux或rCore的源码了.
在rCore的仓库中发现了这个:rcore-os/trapframe-rs: Handle TrapFrame across kernel and user space on multiple ISAs. (github.com),但是它似乎也没有对浮点寄存器进行保存.
那么这时候需要看Linux的上下文保存了.
我们看到在Linux源码中似乎也没有进行保存.
那么其实对于我的能力而言,这里的内容有两个部分没有完成:
- 实际设计一个任务来测试保存和不保存浮点寄存器的区别
- 没有找到Linux浮点寄存器保存相关的内容
第四题
编写应用程序或扩展内核,能够统计任务切换的大致开销
这个其实上来是一脸懵的,首先先思考一下任务切换开销到底开销在哪了:
- 对于CPU时间的占用
- 对于内存的占用
这题其实是没什么头绪的,但是看了答案豁然开朗,只需要统计__switch
的总耗时即可.
这里注意,是所有的开销,而不是单纯的一个的开销
但是仔细看答案的实现,参考hangx-ma在评论区的评论和他和Mars在评论区的讨论:
rust
/// 切换的开始时间
static mut SWITCH_TIME_START: usize = 0;
/// 切换的总时间
static mut SWITCH_TIME_COUNT: usize = 0;
unsafe fn __switch(current_task_cx_ptr: *mut TaskContext, next_task_cx_ptr: *const TaskContext) {
SWITCH_TIME_START = get_time_us();
switch::__switch(current_task_cx_ptr, next_task_cx_ptr);
SWITCH_TIME_COUNT += get_time_us() - SWITCH_TIME_START;
}
fn get_switch_time_count() -> usize {
unsafe { SWITCH_TIME_COUNT }
}
对第一次启动的任务 ,后续会转到__restore
函数,这样就会导致函数烂尾 ,就是一个反常识 的情况,但是如果用汇编的方式来理解就很好理解了,__restore
会直接回到用户态,这时候后边的代码就被跳过了.
回顾trap.S
:
asm
__alltraps:
... ...
call trap_handler
__restore:
... ...
sret
把trap_handler
的内容中函数的内容更换为:
asm
__alltraps:
... ...
# 一些代码逻辑
goto __restore # 实际上RISCV没有goto这个控制转移指令
# 后续的代码逻辑
__restore:
... ...
sret
这里一定要特别注意一点,这是汇编,也就是__alltraps
不会执行到call trap_handler
就停止了,它会一直执行__restore
到sret
.
最后在完整回顾一下trap
流程:
引用评论区的讨论
- 进入执行switch::__switch这个rust函数时, ra 寄存器会被设置为 当前指令的下一个指令,所以在这个函数执行时, switch::__switch 的下一个指令的地址会被存储在当前task A 的TaskContext.ra 中,
- 当执行 switch::__switch 的 ret 指令时, pc 寄存器会设置为 task B 的 TaskContext.ra, 此时 task B 的 TaskContext.ra 有且只能取 两个值中的一个:1. 如果 task B 是初次运行,那么task B 的 TaskContext.ra 的值应为 __restore, 然后回到用户态,从头开始执行 task B, 注意,这个 task switch 时间是没有被记录的; 2. 如果 task B 不是初次运行, 那么 task B 的 TaskContext.ra 的值应为 switch::__switch 的下一条指令的地址, 所以执行 ret 指令后 会执行
SWITCH_TIME_COUNT += get_time_us() - SWITCH_TIME_START;
, 所以, 这个 task switch 时间被记录下来了 - 基于以上 2 点, 我认为官方对 task switch 时间的统计方法没有问题,只是缺少了一部分
这样就很好理解了,在官方解答的基础上,只需要添加一个在__restore
后进行时间统计的功能就行了.
做出如下修改:
rust
// os/src/task/mod.rs
/// 切换的开始时间
pub static mut SWITCH_TIME_START: usize = 0;
/// 切换的总时间
pub static mut SWITCH_TIME_COUNT: usize = 0;
unsafe fn __switch(current_task_cx_ptr: *mut TaskContext, next_task_cx_ptr: *const TaskContext) {
SWITCH_TIME_START = get_time_us();
switch::__switch(current_task_cx_ptr, next_task_cx_ptr);
SWITCH_TIME_COUNT += get_time_us() - SWITCH_TIME_START;
}
fn get_switch_time_count() -> usize {
unsafe { SWITCH_TIME_COUNT }
}
impl TaskManager
{
fn run_next_task(&self)
{
if let Some(next) = self.find_next_task()
{
.... ...
}
else
{
println!("All applications completed!");
println!("task switch time: {} us", get_switch_time_count());
shutdown(false);
}
}
}
rust
// os/src/task/context.rs
impl TaskContext {
... ...
/// set task context {__restore ASM funciton, kernel stack, s_0..12 }
pub fn goto_restore(kstack_ptr: usize) -> Self {
extern "C" {
fn __pre_restore();
}
Self {
ra: __pre_restore as usize,
sp: kstack_ptr,
s: [0; 12],
}
}
}
rust
// os/src/trap/mod.rs
/// 计算时间
#[no_mangle]
pub unsafe extern "C" fn switch_cost(cx: &mut TrapContext) -> &mut TrapContext {
SWITCH_TIME_COUNT += get_time_us() - SWITCH_TIME_START;
cx
}
asm
// os/src/trap/trap.S
... ...
__pre_restore:
mv a0, sp
call switch_cost
mv sp, a0
j __restore
随后make run
:
shell
[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name : riscv-virtio,qemu
[rustsbi] Platform SMP : 1
[rustsbi] Platform Memory : 0x80000000..0x88000000
[rustsbi] Boot HART : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[kernel] trap init end
power_3 [10000/200000]
power_3 [20000/200000]
power_3 [30000/200000]
power_3 [40000/200000]
power_3 [50000/200000]
power_3 [60000/200000]
power_3 [70000/200000]
power_3 [80000/200000]
power_3 [90000/200000]
power_3 [100000/200000]
power_3 [110000/200000]
power_3 [120000/200000]
power_3 [130000/200000]
power_3 [140000/200000]
power_3 [150000/200000]
power_3 [160000/200000]
power_3 [170000/200000]
power_3 [180000/200000]
power_3 [190000/200000]
power_3 [200000/200000]
3^200000 = 871008973(MOD 998244353)
Test power_3 OK!
[kernel] Application exited with code 0
Task 0 exited,kernel time:1635 user time:3015
Task 0 exited
Found next task 1
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0
Task 1 exited,kernel time:955 user time:1320
Task 1 exited
Found next task 2
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0
Task 2 exited,kernel time:923 user time:1449
Task 2 exited
Found next task 3
Timer interrupt,time slice used up!
Next timer set!
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Task 3 suspended
Found next task 3
Task 3 has been switched out
Test sleep OK!
[kernel] Application exited with code 0
Task 3 exited,kernel time:104 user time:1506
Task 3 exited
All applications completed!
task switch time: 35 us
第五题
这题对应了问答题的第六题 ,在sie
位设置为1的时候实际上内核态还是可以接收到中断的.
参考答案似乎说的很有道理,但是和第三题一样实际上:
- 没有对应测试APP
- 最后也是开放性答案,没有讲清楚具体怎么解决问题
TODO
第六题
TODO 似乎是一个看起来很简单但是执行起来很难的问题.
问答题
第一题
协作式调度与抢占式调度的区别是什么?
协作式调度是经由APP本身在执行耗时操作的时候主动执行yield
释放CPU.
抢占式调度是除了APP本身主动放弃CPU以外,使用定时器中断每隔一个时间片强制切换任务.
第二题
中断、异常和系统调用有何异同之处?
中断和异常都是Trap,而系统调用是一种异常.就是Environment call from U-mode
这个异常.
第三题
RISC-V支持哪些中断/异常?
这个第二章作业题其实已经解释过了, 我直接把图贴过来.
但是这里发现Exception Code
有很多是空着的.
这里直接看riscv-privileged对Supervisor cause register (scause)
的描述,它刚好记载了S
特权级所有进入Trap
的可能情况.
第四题
如何判断进入操作系统内核的起因是由于中断还是异常?
这里想到关于trap_handler
的代码:
rust
#[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) => {
println!("[kernel] PageFault in application, bad addr = {:#x}, bad instruction = {:#x}, kernel killed it.", stval, cx.sepc);
exit_current_and_run_next();
}
Trap::Exception(Exception::IllegalInstruction) => {
println!("[kernel] IllegalInstruction in application, kernel killed it.");
exit_current_and_run_next();
}
Trap::Interrupt(Interrupt::SupervisorTimer) => {
set_next_trigger();
suspend_current_and_run_next();
}
_ => {
panic!(
"Unsupported trap {:?}, stval = {:#x}!",
scause.cause(),
stval
);
}
}
cx
}
可以看到是处理了scause.cause
,应该是scause
寄存器的cause
位.
和 上一题 对应起来就可以理解了.
第五题
在 RISC-V 中断机制中,PLIC 和 CLINT 各起到了什么作用?
这里在我们已有的参考书里搜都找不到.
因此使用一个trick,这样搜索.得到一本关于PLIC 的参考手册.
同样的方法得到一本关于 CLINT 的参考手册.
那么要回答这个问题只需要看手册的 Introduction 部分就行了.
关于PLIC
:
This document contains the RISC-V platform-level interrupt controller (PLIC) specification (was removed from RISC-V Privileged Spec v1.11-draft) , which defines an interrupt controller specifically designed to work in the context of RISC-V systems. The PLIC multiplexes various device interrupts onto the external interrupt lines of Hart contexts, with hardware support for interrupt priorities. This specification defines the general PLIC architecture and the operation parameters. PLIC supports up-to 1023 interrupts (0 is reserved) and 15872 contexts, but the actual number of interrupts and context depends on the PLIC implementation. However, the implement must adhere to the offset of each register within the PLIC operation parameters. The PLIC which claimed as PLIC-Compliant standard PLIC should follow the implementations mentioned in sections below.
机翻:
这份文档包含了 RISC-V 平台级中断控制器 (PLIC) 的规格说明(该规格曾从 RISC-V 特权规格 v1.11 草案中移除),它定义了一个专门为 RISC-V 系统设计的中断控制器。PLIC 将各种设备中断复用到 Hart 上下文的外部中断线上,并提供了对中断优先级的硬件支持。本规格说明定义了通用的 PLIC 架构和操作参数。PLIC 支持最多 1023 个中断(0 保留不用)和 15872 个上下文,但实际的中断数量和上下文数量取决于 PLIC 的具体实现。然而,实现必须遵循 PLIC 操作参数中每个寄存器的偏移量。声称符合 PLIC 标准的 PLIC 应当遵循下文中提到的实现。
关于CLINT
:
This RISC-V ACLINT specification defines a set of memory mapped devices which provide interprocessor interrupts (IPI) and timer functionalities for each HART on a multi-HART RISC-V platform. These HART-level IPI and timer functionalities are required by operating systems, bootloaders and firmwares running on a multi-HART RISC-V platform. The SiFive Core-Local Interruptor (CLINT) device has been widely adopted in the RISC-V world to provide machine-level IPI and timer functionalities. Unfortunately, the SiFive CLINT has a unified register map for both IPI and timer functionalities and it does not provide supervisor-level IPI functionality. The RISC-V ACLINT specification takes a more modular approach by defining separate memory mapped devices for IPI and timer functionalities. This modularity allows RISC-V platforms to omit some of the RISC-V ACLINT devices for when the platform has an alternate mechanism. In addition to modularity, the RISC-V ACLINT specification also defines a dedicated memory mapped device for supervisor-level IPIs. The Table 1 below shows the list of devices defined by the RISC-V ACLINT specification.
机翻:
RISC-V ACLINT 规格说明定义了一组内存映射设备,这些设备为多 Hart RISC-V 平台上每个 Hart 提供了处理器间中断 (IPI) 和定时器功能。这些 Hart 级别的 IPI 和定时器功能是多 Hart RISC-V 平台上运行的操作系统、引导加载程序和固件所必需的。SiFive 的 Core-Local Interruptor (CLINT) 设备已在 RISC-V 领域被广泛采用,用于提供机器级别的 IPI 和定时器功能。不幸的是,SiFive CLINT 设备具有统一的寄存器映射,既用于 IPI 又用于定时器功能,并且不提供监督级别 (supervisor-level) 的 IPI 功能。RISC-V ACLINT 规格说明采取了更为模块化的方法,通过定义独立的内存映射设备来分别提供 IPI 和定时器功能。这种模块化允许 RISC-V 平台省略某些 RISC-V ACLINT 设备,当平台有替代机制时。除了模块化之外,RISC-V ACLINT 规格说明还定义了一个专门的内存映射设备用于监督级别的 IPI 功能。下表 1 显示了 RISC-V ACLINT 规格说明定义的设备列表。
总结:
CLINT 处理时钟中断 (MTI
) 和核间的软件中断 (MSI
);PLIC 处理外部来源的中断 (MEI
)。
第六题
基于RISC-V 的操作系统支持中断嵌套?请给出进一步的解释说明。
~~这里根据本章学到的知识,得出的结论应该是支持中断嵌套. ~~
模糊的记忆是:当RISC-V
接收到中断后会修改对应寄存器的值以屏蔽和此中断同等级和低等级的所有中断.
RISC-V原生不支持中断嵌套。(在S态的内核中)只有 sstatus
的 SIE
位为 1 时,才会开启中断,再由 sie
寄存器控制哪些中断可以触发。触发中断时,sstatus.SPIE
置为 sstatus.SIE
,而 sstatus.SIE
置为0;当执行 sret
时,sstatus.SIE
置为 sstatus.SPIE
,而 sstatus.SPIE
置为1。这意味着触发中断时,因为 sstatus.SIE
为0,所以无法再次触发中断。
这里不知道是不是因为提了 操作系统 因此不考虑M
态的中断会出现在操作系统中,因此是不支持中断嵌套的.
第七题
本章提出的任务的概念与前面提到的进程的概念之间有何区别与联系?
说实话 前面似乎没有提到进程的概念 .
感觉需要等到第五章. #TODO
这里直接抄答案了:
- 联系:任务和进程都有自己独立的栈、上下文信息,任务是进程的"原始版本",在第五章会将目前的用户程序从任务升级为进程。
- 区别:任务之间没有地址空间隔离,实际上是能相互访问到的;进程之间有地址空间隔离,一个进程无法访问到另一个进程的地址。
第八题
简单描述一下任务的地址空间中有哪些类型的数据和代码。
可参照 user/src/linker.ld
:
.text
:任务的代码段,其中开头的.text.entry
段包含任务的入口地址.rodata
:只读数据,包含字符串常量,如测例中的println!("Test power_3 OK!");
实际打印的字符串存在这里.data
:需要初始化的全局变量.bss
:未初始化或初始为0的全局变量。
除此之外,在内核中为每个任务构造的用户栈 os/src/loader.rs:USER_STACK
也属于各自任务的地址。
第九题
任务控制块保存哪些内容?
这个直接照抄我们之前画的图就行了.
看最后的TaskControlBlock
的结构.
第十题
任务上下文切换需要保存与恢复哪些内容?
这个同第九题,直接看TaskContext
的内容.
第十一题
特权级上下文和任务上下文有何异同?
任务上下文的内容同第九题即可,特权级上下文的内容直接照抄第二章的内容.
rust
// os/src/trap/context.rs
#[repr(C)]
pub struct TrapContext {
pub x: [usize; 32],
pub sstatus: Sstatus,
pub sepc: usize,
}
可以看到TaskContext
和TrapContext
都保存了一些寄存器内容,而TrapContext
保存了更多的寄存器.
特权级上下文切换可以发生在中断异常时,所以它不符合函数调用约定,需要保存所有通用寄存器。同时它又涉及特权级切换,所以还额外保留了一些 CSR,在切换时还会涉及更多的 CSR。
第十二题
上下文切换为什么需要用汇编语言实现?
这题还真不知道,也是之前脑子里的疑惑了.
上下文切换过程中,需要我们直接控制所有的寄存器。C 和 Rust 编译器在编译代码的时候都会"自作主张"使用通用寄存器,以及我们不知道的情况下访问栈,这是我们需要避免的。
切换到内核的时候,保存好用户态状态之后,我们将栈指针指向内核栈,相当于构建好一个高级语言可以正常运行的环境,这时候就可以由高级语言接管了。
第十三题
有哪些可能的时机导致任务切换?
- 系统调用
- 主动
yield
- 时间片时间到了
第十四题
在设计任务控制块时,为何采用分离的内核栈和用户栈,而不用一个栈?
用户程序可以任意修改栈指针,将其指向任意位置,而内核在运行的时候总希望在某一个合法的栈上,所以需要用分开的两个栈。
此外,利用后面的章节的知识可以保护内核和用户栈,让用户无法读写内核栈上的内容,保证安全。
第十五题
我们已经在 rCore 里实现了不少操作系统的基本功能:特权级、上下文切换、系统调用......为了让大家对相关代码更熟悉,我们来以另一个操作系统为例,比较一下功能的实现。看看换一段代码,你还认不认识操作系统。
阅读 Linux 源代码,特别是 riscv
架构相关的代码,回答以下问题:
-
Linux 正常运行的时候,
stvec
指向哪个函数?是哪段代码设置的stvec
的值? -
Linux 里进行上下文切换的函数叫什么?(对应 rCore 的
__switch
) -
Linux 里,和 rCore 中的
TrapContext
和TaskContext
这两个类型大致对应的结构体叫什么? -
Linux 在内核态运行的时候,
tp
寄存器的值有什么含义?sscratch
的值是什么? -
Linux 在用户态运行的时候,
sscratch
的值有什么含义? -
Linux 在切换到内核态的时候,保存了和用户态程序相关的什么状态?
-
Linux 在内核态的时候,被打断的用户态程序的寄存器值存在哪里?在 C 代码里如何访问?
-
Linux 是如何根据系统调用编号找到对应的函数的?(对应 rCore 的
syscall::syscall()
函数的功能) -
Linux 用户程序调用
ecall
的参数是怎么传给系统调用的实现的?系统调用的返回值是怎样返回给用户态的?
阅读代码的时候,可以重点关注一下如下几个文件,尤其是第一个 entry.S
,当然也可能会需要读到其它代码:
arch/riscv/kernel/entry.S
(与 rCore 的switch.S
对比)arch/riscv/include/asm/current.h
arch/riscv/include/asm/processor.h
arch/riscv/include/asm/switch_to.h
arch/riscv/kernel/process.c
arch/riscv/kernel/syscall_table.c
arch/riscv/kernel/traps.c
include/linux/sched.h
此外,推荐使用 https://elixir.bootlin.com 阅读 Linux 源码,方便查找各个函数、类型、变量的定义及引用情况。
一些提示:
- Linux 支持各种架构,查找架构相关的代码的时候,请认准文件名中的
arch/riscv
。 - 为了同时兼容 RV32 和 RV64,Linux 在汇编代码中用了几个宏定义。例如,
REG_L
在 RV32 上是lw
,而在 RV64 上是ld
。同理,REG_S
在 RV32 上是sw
,而在 RV64 上是sd
。 - 如果看到
#ifdef CONFIG_
相关的预处理指令,是 Linux 根据编译时的配置启用不同的代码。一般阅读代码时,要么比较容易判断出这些宏有没有被定义,要么其实无关紧要。比如,Linux 内核确实应该和 rCore 一样,是在 S-mode 运行的,所以CONFIG_RISCV_M_MODE
应该是没有启用的。 - 汇编代码中可能会看到有些
TASK_
和 PT_ 开头的常量,找不到定义。这些常量并没有直接写在源码里,而是自动生成的。
在汇编语言中需要用到的很多struct
里偏移量的常量定义可以在arch/riscv/kernel/asm-offsets.c
文件里找到。其中,OFFSET(NAME, struct_name, field)
指的是NAME
的值定义为field
这一项在struct_name
结构体里,距离结构体开头的偏移量。最终这些代码会生成asm/asm-offsets.h
供汇编代码使用。 #include <asm/unistd.h>
在arch/riscv/include/uapi/asm/unistd.h
,#include <asm-generic/unistd.h>
在include/uapi/asm-generic/unistd.h
。
这里我们也可以完全不接受建议,使用github1s
观看,虽然老登从来不接受github上的issue,但是确实在上边公开了自己的代码.
第一个小问
Linux 正常运行的时候, stvec
指向哪个函数?是哪段代码设置的 stvec
的值?
查阅RISC-V手册 (ustc.edu.cn) :
stvec
发生例外的指令的 PC 被存入 sepc,且 PC 被设置为 stvec。
在源码的riscv
文件夹中搜索stvec
:
- 在
csr.h
里边明确定义了#define CSR_STVEC 0x105
,用宏来代替物理地址. - 使用
#if #else
宏来决定CSR_TVEC
宏究竟是跟着CSR_STVEC
还是CSR_MTVEC
- 寻找谁修改了
CSR_STVEC
或者CSR_TVEC
就是当前的任务了
对于CSR_STVEC
可以看到在\arch\riscv\kernel\kexec_relocate.S
里有一段代码是写入CSR_STVEC
的:
asm
SYM_CODE_START(riscv_kexec_relocate)
/*
* s0: Pointer to the current entry
* s1: (const) Phys address to jump to after relocation
* s2: (const) Phys address of the FDT image
* s3: (const) The hartid of the current hart
* s4: (const) kernel_map.va_pa_offset, used when switching MMU off
* s5: Pointer to the destination address for the relocation
* s6: (const) Physical address of the main loop
*/
...
/*
* When we switch SATP.MODE to "Bare" we'll only
* play with physical addresses. However the first time
* we try to jump somewhere, the offset on the jump
* will be relative to pc which will still be on VA. To
* deal with this we set stvec to the physical address at
* the start of the loop below so that we jump there in
* any case.
*/
la s6, 1f
sub s6, s6, s4
csrw CSR_STVEC, s6
...
/*
* Switch to physical addressing
* This will also trigger a jump to CSR_STVEC
* which in this case is the address of the new
* kernel.
*/
csrw CSR_STVEC, a2
csrw CSR_SATP, zero
...
SYM_CODE_END(riscv_kexec_relocate)
这段代码经过多方查阅是一段关于kexec
的代码,kexec
是 Linux 内核提供的一种机制,允许在不重启硬件的情况下加载一个新的内核映像.
在\arch\riscv\kvm\vcpu_switch.S
里有一个函数有操作是写入CSR_STVEC
的:
asm
SYM_FUNC_START(__kvm_riscv_switch_to)
...
/* Save Host STVEC and change it to return path */
csrrw t4, CSR_STVEC, t4
...
/* Restore Host STVEC */
csrw CSR_STVEC, t1
...
SYM_FUNC_END(__kvm_riscv_switch_to)
这里需要注意的就是这里的神似魔法的宏展开操作:SYM_CODE_START
,SYM_CODE_END
,SYM_FUNC_START
,SYM_FUNC_END
.
原本还打算自己宏展开看一下的,发现自己实在是个SB,脑容量确实有限,因此直接找到了开源文档中的介绍.
大概意思是汇编代码 也是需要区分代码 和函数 的,但是汇编器不强制要求这一点,这里通过宏操作把对这两方面的注解更改成了一键式.
SYM_FUNC_*
代表类似于C语言的函数,有参数调用之类.
SYM_CODE_*
使用特殊堆栈调用的特殊功能。无论是具有特殊栈内容的中断处理程序、中继机制,还是启动函数.
知道了这些,我们只需要关注riscv_kexec_relocate
和__kvm_riscv_switch_to
在哪里被调用.
riscv_kexec_relocate
只在如下函数中被调用,可以看到是一个初始化kexec
的函数,而且非常巧思 地直接把这段asm
当作一段数组拷贝进一个地址,这样就可以实现动态函数:
C
/*
* machine_kexec_prepare - Initialize kexec
*
* This function is called from do_kexec_load, when the user has
* provided us with an image to be loaded. Its goal is to validate
* the image and prepare the control code buffer as needed.
* Note that kimage_alloc_init has already been called and the
* control buffer has already been allocated.
*/
int
machine_kexec_prepare(struct kimage *image)
{
struct kimage_arch *internal = &image->arch;
struct fdt_header fdt = {0};
void *control_code_buffer = NULL;
unsigned int control_code_buffer_sz = 0;
int i = 0;
/* Find the Flattened Device Tree and save its physical address */
for (i = 0; i < image->nr_segments; i++) {
if (image->segment[i].memsz <= sizeof(fdt))
continue;
if (image->file_mode)
memcpy(&fdt, image->segment[i].buf, sizeof(fdt));
else if (copy_from_user(&fdt, image->segment[i].buf, sizeof(fdt)))
continue;
if (fdt_check_header(&fdt))
continue;
internal->fdt_addr = (unsigned long) image->segment[i].mem;
break;
}
if (!internal->fdt_addr) {
pr_err("Device tree not included in the provided image\n");
return -EINVAL;
}
/* Copy the assembler code for relocation to the control page */
if (image->type != KEXEC_TYPE_CRASH) {
control_code_buffer = page_address(image->control_code_page);
control_code_buffer_sz = page_size(image->control_code_page);
if (unlikely(riscv_kexec_relocate_size > control_code_buffer_sz)) {
pr_err("Relocation code doesn't fit within a control page\n");
return -EINVAL;
}
memcpy(control_code_buffer, riscv_kexec_relocate,
riscv_kexec_relocate_size);
/* Mark the control page executable */
set_memory_x((unsigned long) control_code_buffer, 1);
}
return 0;
}
在\arch\riscv\kvm\vcpu_exit.c
里有一个函数是操作CSR_STVEC
的:
C
/**
* kvm_riscv_vcpu_unpriv_read -- Read machine word from Guest memory
*
* @vcpu: The VCPU pointer
* @read_insn: Flag representing whether we are reading instruction
* @guest_addr: Guest address to read
* @trap: Output pointer to trap details
*/
unsigned long kvm_riscv_vcpu_unpriv_read(struct kvm_vcpu *vcpu,
bool read_insn,
unsigned long guest_addr,
struct kvm_cpu_trap *trap)
{
register unsigned long taddr asm("a0") = (unsigned long)trap;
register unsigned long ttmp asm("a1");
unsigned long flags, val, tmp, old_stvec, old_hstatus;
local_irq_save(flags);
old_hstatus = csr_swap(CSR_HSTATUS, vcpu->arch.guest_context.hstatus);
old_stvec = csr_swap(CSR_STVEC, (ulong)&__kvm_riscv_unpriv_trap);
if (read_insn) {
/*
* HLVX.HU instruction
* 0110010 00011 rs1 100 rd 1110011
*/
asm volatile ("\n"
".option push\n"
".option norvc\n"
"add %[ttmp], %[taddr], 0\n"
HLVX_HU(%[val], %[addr])
"andi %[tmp], %[val], 3\n"
"addi %[tmp], %[tmp], -3\n"
"bne %[tmp], zero, 2f\n"
"addi %[addr], %[addr], 2\n"
HLVX_HU(%[tmp], %[addr])
"sll %[tmp], %[tmp], 16\n"
"add %[val], %[val], %[tmp]\n"
"2:\n"
".option pop"
: [val] "=&r" (val), [tmp] "=&r" (tmp),
[taddr] "+&r" (taddr), [ttmp] "+&r" (ttmp),
[addr] "+&r" (guest_addr) : : "memory");
if (trap->scause == EXC_LOAD_PAGE_FAULT)
trap->scause = EXC_INST_PAGE_FAULT;
} else {
/*
* HLV.D instruction
* 0110110 00000 rs1 100 rd 1110011
*
* HLV.W instruction
* 0110100 00000 rs1 100 rd 1110011
*/
asm volatile ("\n"
".option push\n"
".option norvc\n"
"add %[ttmp], %[taddr], 0\n"
#ifdef CONFIG_64BIT
HLV_D(%[val], %[addr])
#else
HLV_W(%[val], %[addr])
#endif
".option pop"
: [val] "=&r" (val),
[taddr] "+&r" (taddr), [ttmp] "+&r" (ttmp)
: [addr] "r" (guest_addr) : "memory");
}
csr_write(CSR_STVEC, old_stvec);
csr_write(CSR_HSTATUS, old_hstatus);
local_irq_restore(flags);
return val;
}
直接去看它的注释:描述了一个名为 kvm_riscv_vcpu_unpriv_read
的函数,该函数用于从Guest(虚拟机)内存中读取机器字。
这里大部分内容都是关于kexec
的.
根据本题给出的线索,显然正常运行过程中是用不到这个工具的.
对于CSR_TVEC
似乎搜索的结果很让人乐观.
只在\arch\riscv\kernel\head.S
和\arch\riscv\kernel\suspend.c
找到了相关的调用.
在\arch\riscv\kernel\suspend.c
中是对CSR的保存操作:
C
void suspend_save_csrs(struct suspend_context *context)
{
if (riscv_cpu_has_extension_unlikely(smp_processor_id(), RISCV_ISA_EXT_XLINUXENVCFG))
context->envcfg = csr_read(CSR_ENVCFG);
context->tvec = csr_read(CSR_TVEC);
context->ie = csr_read(CSR_IE);
/*
* No need to save/restore IP CSR (i.e. MIP or SIP) because:
*
* 1. For no-MMU (M-mode) kernel, the bits in MIP are set by
* external devices (such as interrupt controller, timer, etc).
* 2. For MMU (S-mode) kernel, the bits in SIP are set by
* M-mode firmware and external devices (such as interrupt
* controller, etc).
*/
#ifdef CONFIG_MMU
context->satp = csr_read(CSR_SATP);
#endif
}
void suspend_restore_csrs(struct suspend_context *context)
{
csr_write(CSR_SCRATCH, 0);
if (riscv_cpu_has_extension_unlikely(smp_processor_id(), RISCV_ISA_EXT_XLINUXENVCFG))
csr_write(CSR_ENVCFG, context->envcfg);
csr_write(CSR_TVEC, context->tvec);
csr_write(CSR_IE, context->ie);
#ifdef CONFIG_MMU
csr_write(CSR_SATP, context->satp);
#endif
}
重点显然在\arch\riscv\kernel\head.S
文件中:
asm
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Copyright (C) 2012 Regents of the University of California
*/
#include <asm/asm-offsets.h>
#include <asm/asm.h>
#include <linux/init.h>
#include <linux/linkage.h>
#include <asm/thread_info.h>
#include <asm/page.h>
#include <asm/pgtable.h>
#include <asm/csr.h>
#include <asm/hwcap.h>
#include <asm/image.h>
#include <asm/scs.h>
#include <asm/xip_fixup.h>
#include "efi-header.S"
__HEAD
SYM_CODE_START(_start)
/*
* Image header expected by Linux boot-loaders. The image header data
* structure is described in asm/image.h.
* Do not modify it without modifying the structure and all bootloaders
* that expects this header format!!
*/
#ifdef CONFIG_EFI
/*
* This instruction decodes to "MZ" ASCII required by UEFI.
*/
c.li s4,-13
j _start_kernel
#else
/* jump to start kernel */
j _start_kernel
/* reserved */
.word 0
#endif
.balign 8
#ifdef CONFIG_RISCV_M_MODE
/* Image load offset (0MB) from start of RAM for M-mode */
.dword 0
#else
#if __riscv_xlen == 64
/* Image load offset(2MB) from start of RAM */
.dword 0x200000
#else
/* Image load offset(4MB) from start of RAM */
.dword 0x400000
#endif
#endif
/* Effective size of kernel image */
.dword _end - _start
.dword __HEAD_FLAGS
.word RISCV_HEADER_VERSION
.word 0
.dword 0
.ascii RISCV_IMAGE_MAGIC
.balign 4
.ascii RISCV_IMAGE_MAGIC2
#ifdef CONFIG_EFI
.word pe_head_start - _start
pe_head_start:
__EFI_PE_HEADER
#else
.word 0
#endif
.align 2
#ifdef CONFIG_MMU
.global relocate_enable_mmu
relocate_enable_mmu:
/* Relocate return address */
la a1, kernel_map
XIP_FIXUP_OFFSET a1
REG_L a1, KERNEL_MAP_VIRT_ADDR(a1)
la a2, _start
sub a1, a1, a2
add ra, ra, a1
/* Point stvec to virtual address of intruction after satp write */
la a2, 1f
add a2, a2, a1
csrw CSR_TVEC, a2
/* Compute satp for kernel page tables, but don't load it yet */
srl a2, a0, PAGE_SHIFT
la a1, satp_mode
XIP_FIXUP_OFFSET a1
REG_L a1, 0(a1)
or a2, a2, a1
/*
* Load trampoline page directory, which will cause us to trap to
* stvec if VA != PA, or simply fall through if VA == PA. We need a
* full fence here because setup_vm() just wrote these PTEs and we need
* to ensure the new translations are in use.
*/
la a0, trampoline_pg_dir
XIP_FIXUP_OFFSET a0
srl a0, a0, PAGE_SHIFT
or a0, a0, a1
sfence.vma
csrw CSR_SATP, a0
.align 2
1:
/* Set trap vector to spin forever to help debug */
la a0, .Lsecondary_park
csrw CSR_TVEC, a0
/* Reload the global pointer */
load_global_pointer
/*
* Switch to kernel page tables. A full fence is necessary in order to
* avoid using the trampoline translations, which are only correct for
* the first superpage. Fetching the fence is guaranteed to work
* because that first superpage is translated the same way.
*/
csrw CSR_SATP, a2
sfence.vma
ret
#endif /* CONFIG_MMU */
#ifdef CONFIG_SMP
.global secondary_start_sbi
secondary_start_sbi:
/* Mask all interrupts */
csrw CSR_IE, zero
csrw CSR_IP, zero
/* Load the global pointer */
load_global_pointer
/*
* Disable FPU & VECTOR to detect illegal usage of
* floating point or vector in kernel space
*/
li t0, SR_FS_VS
csrc CSR_STATUS, t0
/* Set trap vector to spin forever to help debug */
la a3, .Lsecondary_park
csrw CSR_TVEC, a3
/* a0 contains the hartid & a1 contains boot data */
li a2, SBI_HART_BOOT_TASK_PTR_OFFSET
XIP_FIXUP_OFFSET a2
add a2, a2, a1
REG_L tp, (a2)
li a3, SBI_HART_BOOT_STACK_PTR_OFFSET
XIP_FIXUP_OFFSET a3
add a3, a3, a1
REG_L sp, (a3)
.Lsecondary_start_common:
#ifdef CONFIG_MMU
/* Enable virtual memory and relocate to virtual address */
la a0, swapper_pg_dir
XIP_FIXUP_OFFSET a0
call relocate_enable_mmu
#endif
call .Lsetup_trap_vector
scs_load_current
call smp_callin
#endif /* CONFIG_SMP */
.align 2
.Lsecondary_park:
/*
* Park this hart if we:
* - have too many harts on CONFIG_RISCV_BOOT_SPINWAIT
* - receive an early trap, before setup_trap_vector finished
* - fail in smp_callin(), as a successful one wouldn't return
*/
wfi
j .Lsecondary_park
.align 2
.Lsetup_trap_vector:
/* Set trap vector to exception handler */
la a0, handle_exception
csrw CSR_TVEC, a0
/*
* Set sup0 scratch register to 0, indicating to exception vector that
* we are presently executing in kernel.
*/
csrw CSR_SCRATCH, zero
ret
SYM_CODE_END(_start)
SYM_CODE_START(_start_kernel)
/* Mask all interrupts */
csrw CSR_IE, zero
csrw CSR_IP, zero
#ifdef CONFIG_RISCV_M_MODE
/* flush the instruction cache */
fence.i
/* Reset all registers except ra, a0, a1 */
call reset_regs
/*
* Setup a PMP to permit access to all of memory. Some machines may
* not implement PMPs, so we set up a quick trap handler to just skip
* touching the PMPs on any trap.
*/
la a0, .Lpmp_done
csrw CSR_TVEC, a0
li a0, -1
csrw CSR_PMPADDR0, a0
li a0, (PMP_A_NAPOT | PMP_R | PMP_W | PMP_X)
csrw CSR_PMPCFG0, a0
.align 2
.Lpmp_done:
/*
* The hartid in a0 is expected later on, and we have no firmware
* to hand it to us.
*/
csrr a0, CSR_MHARTID
#endif /* CONFIG_RISCV_M_MODE */
/* Load the global pointer */
load_global_pointer
/*
* Disable FPU & VECTOR to detect illegal usage of
* floating point or vector in kernel space
*/
li t0, SR_FS_VS
csrc CSR_STATUS, t0
#ifdef CONFIG_RISCV_BOOT_SPINWAIT
li t0, CONFIG_NR_CPUS
blt a0, t0, .Lgood_cores
tail .Lsecondary_park
.Lgood_cores:
/* The lottery system is only required for spinwait booting method */
#ifndef CONFIG_XIP_KERNEL
/* Pick one hart to run the main boot sequence */
la a3, hart_lottery
li a2, 1
amoadd.w a3, a2, (a3)
bnez a3, .Lsecondary_start
#else
/* hart_lottery in flash contains a magic number */
la a3, hart_lottery
mv a2, a3
XIP_FIXUP_OFFSET a2
XIP_FIXUP_FLASH_OFFSET a3
lw t1, (a3)
amoswap.w t0, t1, (a2)
/* first time here if hart_lottery in RAM is not set */
beq t0, t1, .Lsecondary_start
#endif /* CONFIG_XIP */
#endif /* CONFIG_RISCV_BOOT_SPINWAIT */
#ifdef CONFIG_XIP_KERNEL
la sp, _end + THREAD_SIZE
XIP_FIXUP_OFFSET sp
mv s0, a0
mv s1, a1
call __copy_data
/* Restore a0 & a1 copy */
mv a0, s0
mv a1, s1
#endif
#ifndef CONFIG_XIP_KERNEL
/* Clear BSS for flat non-ELF images */
la a3, __bss_start
la a4, __bss_stop
ble a4, a3, .Lclear_bss_done
.Lclear_bss:
REG_S zero, (a3)
add a3, a3, RISCV_SZPTR
blt a3, a4, .Lclear_bss
.Lclear_bss_done:
#endif
la a2, boot_cpu_hartid
XIP_FIXUP_OFFSET a2
REG_S a0, (a2)
/* Initialize page tables and relocate to virtual addresses */
la tp, init_task
la sp, init_thread_union + THREAD_SIZE
XIP_FIXUP_OFFSET sp
addi sp, sp, -PT_SIZE_ON_STACK
scs_load_init_stack
#ifdef CONFIG_BUILTIN_DTB
la a0, __dtb_start
XIP_FIXUP_OFFSET a0
#else
mv a0, a1
#endif /* CONFIG_BUILTIN_DTB */
/* Set trap vector to spin forever to help debug */
la a3, .Lsecondary_park
csrw CSR_TVEC, a3
call setup_vm
#ifdef CONFIG_MMU
la a0, early_pg_dir
XIP_FIXUP_OFFSET a0
call relocate_enable_mmu
#endif /* CONFIG_MMU */
call .Lsetup_trap_vector
/* Restore C environment */
la tp, init_task
la sp, init_thread_union + THREAD_SIZE
addi sp, sp, -PT_SIZE_ON_STACK
scs_load_current
#ifdef CONFIG_KASAN
call kasan_early_init
#endif
/* Start the kernel */
call soc_early_init
tail start_kernel
#ifdef CONFIG_RISCV_BOOT_SPINWAIT
.Lsecondary_start:
/* Set trap vector to spin forever to help debug */
la a3, .Lsecondary_park
csrw CSR_TVEC, a3
slli a3, a0, LGREG
la a1, __cpu_spinwait_stack_pointer
XIP_FIXUP_OFFSET a1
la a2, __cpu_spinwait_task_pointer
XIP_FIXUP_OFFSET a2
add a1, a3, a1
add a2, a3, a2
/*
* This hart didn't win the lottery, so we wait for the winning hart to
* get far enough along the boot process that it should continue.
*/
.Lwait_for_cpu_up:
/* FIXME: We should WFI to save some energy here. */
REG_L sp, (a1)
REG_L tp, (a2)
beqz sp, .Lwait_for_cpu_up
beqz tp, .Lwait_for_cpu_up
fence
tail .Lsecondary_start_common
#endif /* CONFIG_RISCV_BOOT_SPINWAIT */
SYM_CODE_END(_start_kernel)
#ifdef CONFIG_RISCV_M_MODE
SYM_CODE_START_LOCAL(reset_regs)
li sp, 0
li gp, 0
li tp, 0
li t0, 0
li t1, 0
li t2, 0
li s0, 0
li s1, 0
li a2, 0
li a3, 0
li a4, 0
li a5, 0
li a6, 0
li a7, 0
li s2, 0
li s3, 0
li s4, 0
li s5, 0
li s6, 0
li s7, 0
li s8, 0
li s9, 0
li s10, 0
li s11, 0
li t3, 0
li t4, 0
li t5, 0
li t6, 0
csrw CSR_SCRATCH, 0
#ifdef CONFIG_FPU
csrr t0, CSR_MISA
andi t0, t0, (COMPAT_HWCAP_ISA_F | COMPAT_HWCAP_ISA_D)
beqz t0, .Lreset_regs_done_fpu
li t1, SR_FS
csrs CSR_STATUS, t1
fmv.s.x f0, zero
fmv.s.x f1, zero
fmv.s.x f2, zero
fmv.s.x f3, zero
fmv.s.x f4, zero
fmv.s.x f5, zero
fmv.s.x f6, zero
fmv.s.x f7, zero
fmv.s.x f8, zero
fmv.s.x f9, zero
fmv.s.x f10, zero
fmv.s.x f11, zero
fmv.s.x f12, zero
fmv.s.x f13, zero
fmv.s.x f14, zero
fmv.s.x f15, zero
fmv.s.x f16, zero
fmv.s.x f17, zero
fmv.s.x f18, zero
fmv.s.x f19, zero
fmv.s.x f20, zero
fmv.s.x f21, zero
fmv.s.x f22, zero
fmv.s.x f23, zero
fmv.s.x f24, zero
fmv.s.x f25, zero
fmv.s.x f26, zero
fmv.s.x f27, zero
fmv.s.x f28, zero
fmv.s.x f29, zero
fmv.s.x f30, zero
fmv.s.x f31, zero
csrw fcsr, 0
/* note that the caller must clear SR_FS */
.Lreset_regs_done_fpu:
#endif /* CONFIG_FPU */
#ifdef CONFIG_RISCV_ISA_V
csrr t0, CSR_MISA
li t1, COMPAT_HWCAP_ISA_V
and t0, t0, t1
beqz t0, .Lreset_regs_done_vector
/*
* Clear vector registers and reset vcsr
* VLMAX has a defined value, VLEN is a constant,
* and this form of vsetvli is defined to set vl to VLMAX.
*/
li t1, SR_VS
csrs CSR_STATUS, t1
csrs CSR_VCSR, x0
vsetvli t1, x0, e8, m8, ta, ma
vmv.v.i v0, 0
vmv.v.i v8, 0
vmv.v.i v16, 0
vmv.v.i v24, 0
/* note that the caller must clear SR_VS */
.Lreset_regs_done_vector:
#endif /* CONFIG_RISCV_ISA_V */
ret
SYM_CODE_END(reset_regs)
#endif /* CONFIG_RISCV_M_MODE */
这个文件巨长无比,而且还是asm
写的,越是这个时候越应该冷静下来.
先看第一部分_start
:
asm
SYM_CODE_START(_start)
/*
* Image header expected by Linux boot-loaders. The image header data
* structure is described in asm/image.h.
* Do not modify it without modifying the structure and all bootloaders
* that expects this header format!!
*/
#ifdef CONFIG_EFI
/*
* This instruction decodes to "MZ" ASCII required by UEFI.
*/
c.li s4,-13
j _start_kernel
#else
/* jump to start kernel */
j _start_kernel
/* reserved */
.word 0
#endif
.balign 8
#ifdef CONFIG_RISCV_M_MODE
/* Image load offset (0MB) from start of RAM for M-mode */
.dword 0
#else
#if __riscv_xlen == 64
/* Image load offset(2MB) from start of RAM */
.dword 0x200000
#else
/* Image load offset(4MB) from start of RAM */
.dword 0x400000
#endif
#endif
/* Effective size of kernel image */
.dword _end - _start
.dword __HEAD_FLAGS
.word RISCV_HEADER_VERSION
.word 0
.dword 0
.ascii RISCV_IMAGE_MAGIC
.balign 4
.ascii RISCV_IMAGE_MAGIC2
#ifdef CONFIG_EFI
.word pe_head_start - _start
pe_head_start:
__EFI_PE_HEADER
#else
.word 0
#endif
.align 2
#ifdef CONFIG_MMU
.global relocate_enable_mmu
relocate_enable_mmu:
/* Relocate return address */
la a1, kernel_map
XIP_FIXUP_OFFSET a1
REG_L a1, KERNEL_MAP_VIRT_ADDR(a1)
la a2, _start
sub a1, a1, a2
add ra, ra, a1
/* Point stvec to virtual address of intruction after satp write */
la a2, 1f
add a2, a2, a1
csrw CSR_TVEC, a2
/* Compute satp for kernel page tables, but don't load it yet */
srl a2, a0, PAGE_SHIFT
la a1, satp_mode
XIP_FIXUP_OFFSET a1
REG_L a1, 0(a1)
or a2, a2, a1
/*
* Load trampoline page directory, which will cause us to trap to
* stvec if VA != PA, or simply fall through if VA == PA. We need a
* full fence here because setup_vm() just wrote these PTEs and we need
* to ensure the new translations are in use.
*/
la a0, trampoline_pg_dir
XIP_FIXUP_OFFSET a0
srl a0, a0, PAGE_SHIFT
or a0, a0, a1
sfence.vma
csrw CSR_SATP, a0
.align 2
1:
/* Set trap vector to spin forever to help debug */
la a0, .Lsecondary_park
csrw CSR_TVEC, a0
/* Reload the global pointer */
load_global_pointer
/*
* Switch to kernel page tables. A full fence is necessary in order to
* avoid using the trampoline translations, which are only correct for
* the first superpage. Fetching the fence is guaranteed to work
* because that first superpage is translated the same way.
*/
csrw CSR_SATP, a2
sfence.vma
ret
#endif /* CONFIG_MMU */
#ifdef CONFIG_SMP
.global secondary_start_sbi
secondary_start_sbi:
/* Mask all interrupts */
csrw CSR_IE, zero
csrw CSR_IP, zero
/* Load the global pointer */
load_global_pointer
/*
* Disable FPU & VECTOR to detect illegal usage of
* floating point or vector in kernel space
*/
li t0, SR_FS_VS
csrc CSR_STATUS, t0
/* Set trap vector to spin forever to help debug */
la a3, .Lsecondary_park
csrw CSR_TVEC, a3
/* a0 contains the hartid & a1 contains boot data */
li a2, SBI_HART_BOOT_TASK_PTR_OFFSET
XIP_FIXUP_OFFSET a2
add a2, a2, a1
REG_L tp, (a2)
li a3, SBI_HART_BOOT_STACK_PTR_OFFSET
XIP_FIXUP_OFFSET a3
add a3, a3, a1
REG_L sp, (a3)
.Lsecondary_start_common:
#ifdef CONFIG_MMU
/* Enable virtual memory and relocate to virtual address */
la a0, swapper_pg_dir
XIP_FIXUP_OFFSET a0
call relocate_enable_mmu
#endif
call .Lsetup_trap_vector
scs_load_current
call smp_callin
#endif /* CONFIG_SMP */
.align 2
.Lsecondary_park:
/*
* Park this hart if we:
* - have too many harts on CONFIG_RISCV_BOOT_SPINWAIT
* - receive an early trap, before setup_trap_vector finished
* - fail in smp_callin(), as a successful one wouldn't return
*/
wfi
j .Lsecondary_park
.align 2
.Lsetup_trap_vector:
/* Set trap vector to exception handler */
la a0, handle_exception
csrw CSR_TVEC, a0
/*
* Set sup0 scratch register to 0, indicating to exception vector that
* we are presently executing in kernel.
*/
csrw CSR_SCRATCH, zero
ret
SYM_CODE_END(_start)
中间多次变动了CSR_TVEC
,但是为了功利地 知道最后CSR_TVEC
被设置为了什么值,我们可以直接看最后在setup_trap_vector
处的注释Set trap vector to exception handler
,最后还是把CSR_TVEC
设置为了handle_exception
函数的地址.
这里暂时 #TODO 勉强可以确定是\arch\riscv\kernel\head.S
里的setup_trap_vector
把stval
设置为了\arch\riscv\kernel\entry.S
里的handle_exception
.
第二个小问
Linux 里进行上下文切换的函数叫什么?(对应 rCore 的 __switch
)
根据题目的提示应该在entry.S
里找.
在没有提示的情况下,我们应该专注于Linux Kernel
的启动流程才能达成.
根据注释很容易找到这个函数__switch_to
.
asm
/*
* Integer register context switch
* The callee-saved registers must be saved and restored.
*
* a0: previous task_struct (must be preserved across the switch)
* a1: next task_struct
*
* The value of a0 and a1 must be preserved by this function, as that's how
* arguments are passed to schedule_tail.
*/
SYM_FUNC_START(__switch_to)
/* Save context into prev->thread */
li a4, TASK_THREAD_RA
add a3, a0, a4
add a4, a1, a4
REG_S ra, TASK_THREAD_RA_RA(a3)
REG_S sp, TASK_THREAD_SP_RA(a3)
REG_S s0, TASK_THREAD_S0_RA(a3)
REG_S s1, TASK_THREAD_S1_RA(a3)
REG_S s2, TASK_THREAD_S2_RA(a3)
REG_S s3, TASK_THREAD_S3_RA(a3)
REG_S s4, TASK_THREAD_S4_RA(a3)
REG_S s5, TASK_THREAD_S5_RA(a3)
REG_S s6, TASK_THREAD_S6_RA(a3)
REG_S s7, TASK_THREAD_S7_RA(a3)
REG_S s8, TASK_THREAD_S8_RA(a3)
REG_S s9, TASK_THREAD_S9_RA(a3)
REG_S s10, TASK_THREAD_S10_RA(a3)
REG_S s11, TASK_THREAD_S11_RA(a3)
/* Save the kernel shadow call stack pointer */
scs_save_current
/* Restore context from next->thread */
REG_L ra, TASK_THREAD_RA_RA(a4)
REG_L sp, TASK_THREAD_SP_RA(a4)
REG_L s0, TASK_THREAD_S0_RA(a4)
REG_L s1, TASK_THREAD_S1_RA(a4)
REG_L s2, TASK_THREAD_S2_RA(a4)
REG_L s3, TASK_THREAD_S3_RA(a4)
REG_L s4, TASK_THREAD_S4_RA(a4)
REG_L s5, TASK_THREAD_S5_RA(a4)
REG_L s6, TASK_THREAD_S6_RA(a4)
REG_L s7, TASK_THREAD_S7_RA(a4)
REG_L s8, TASK_THREAD_S8_RA(a4)
REG_L s9, TASK_THREAD_S9_RA(a4)
REG_L s10, TASK_THREAD_S10_RA(a4)
REG_L s11, TASK_THREAD_S11_RA(a4)
/* The offset of thread_info in task_struct is zero. */
move tp, a1
/* Switch to the next shadow call stack */
scs_load_current
ret
SYM_FUNC_END(__switch_to)
第三个小问
Linux 里,和 rCore 中的 TrapContext
和 TaskContext
这两个类型大致对应的结构体叫什么?
既让上一小问找到了__switch_to
,那么保存任务上下文只需要看那段代码段就行了,必然是和任务上下文有关的.
REG_*的宏魔法
这里我们发现REG_L
我们不认识,显然是用了 "宏魔法".
C
#ifdef __ASSEMBLY__
#define __ASM_STR(x) x
#else
#define __ASM_STR(x) #x
#endif
#if __riscv_xlen == 64
#define __REG_SEL(a, b) __ASM_STR(a)
#elif __riscv_xlen == 32
#define __REG_SEL(a, b) __ASM_STR(b)
#else
#error "Unexpected __riscv_xlen"
#endif
#define REG_L __REG_SEL(ld, lw)
#define REG_S __REG_SEL(sd, sw)
只能说这里的宏还是非常有东西的,太有参考价值了.
__ASM_STR(x)
:
- 这个宏定义的目的是将宏参数转换为字符串字面量。
- 如果是在汇编语言文件中 (
__ASSEMBLY__
定义为 1),则直接返回参数x
。 - 如果不是在汇编语言文件中 (
__ASSEMBLY__
未定义或为 0),则使用预处理器的字符串化操作符#
将参数x
转换为字符串字面量。
- 如果是在汇编语言文件中 (
__REG_SEL(a, b)
:
- 这个宏定义用于选择不同的寄存器指令,取决于
__riscv_xlen
的值。- 如果
__riscv_xlen
等于 64,则返回第一个参数a
。 - 如果
__riscv_xlen
等于 32,则返回第二个参数b
。 - 如果
__riscv_xlen
的值既不是 64 也不是 32,则触发编译错误。
- 如果
REG_L
和 REG_S
:
REG_L
和REG_S
分别代表长整型(long)和短整型(short)的加载和存储指令。REG_L
选择适当的长整型加载指令。REG_S
选择适当的长整型存储指令。
实际上实现的效果REG_S
还是把寄存器内容存到内存中,REG_L
还是把内存内容转存到寄存器中.
TASK_THREAD_*_*宏
还有一个TASK_THREAD_*_*
也是宏,我们一样可以按图索骥找到他们的实现,这里代码就不全贴出来了,原理都是一样的,在\arch\riscv\kernel\asm-offsets.c
文件:
C
/*
* THREAD_{F,X}* might be larger than a S-type offset can handle, but
* these are used in performance-sensitive assembly so we can't resort
* to loading the long immediate every time.
*/
DEFINE(TASK_THREAD_RA_RA,
offsetof(struct task_struct, thread.ra)
- offsetof(struct task_struct, thread.ra)
);
这里用到的offsetof
也是一个宏:
C
#define offsetof(TYPE, FIELD) ((size_t) &((TYPE *)0)->FIELD)
看到这个宏直接访问了NULL
的成员我直接震惊了,这是什么操作?
原来它也是一个宏魔法:
(TYPE *)0
:- 创建一个类型为
TYPE
的指针,并将其初始化为0
(NULL 指针)。 - 这实际上创建了一个"空"的
TYPE
结构体实例,但它的地址是0
。
- 创建一个类型为
&((TYPE *)0)->FIELD
:- 计算
FIELD
成员的地址,即使这个结构体实例的地址是0
。 - 由于地址是
0
,所以结果就是FIELD
成员相对于结构体起始地址的偏移量。
- 计算
((size_t) &((TYPE *)0)->FIELD)
:- 最终结果是一个
size_t
类型的值,表示FIELD
成员相对于结构体起始地址的偏移量(以字节为单位)。
- 最终结果是一个
DEFINE
本身也是一个宏:
C
#define DEFINE(sym, val) \
asm volatile("\n.ascii \"->" #sym " %0 " #val "\"" : : "i" (val))
这里难理解的原因是因为不懂asm
关键字的操作:
asm volatile
:asm
关键字用于插入汇编指令。volatile
确保编译器不会优化掉这条指令,即使它看起来没有副作用。
\n.ascii
:\n
是新行指令,用于在汇编语言中添加新的一行。.ascii
是汇编指令,用于定义字符串常量。
"->" #sym " %0 " #val "
:"->"
是固定的字符串,用于表示输出格式的开头。#sym
和#val
是宏参数,被转换为字符串。这个很重要 ,因为很多同学C语言基础不牢,还要自称"国一大神",实际上#
和##
都是C的宏.%0
是一个占位符,表示后面将要用实际值替换的位置。#sym
和#val
会被宏替换为实际的字符串,例如"sym"
和"val"
。
"i" (val)
:"i"
是输入操作符,表示后面的参数将作为立即数传递给汇编指令。(val)
是实际要传递给.ascii
指令的值。
这里查询asm
关键字的作用.
C
asm(
内嵌汇编指令
:输出操作数
:输入操作数
:破坏描述
);
这时候发现我们看这段代码的时候看的方式错了,应该这样看:
C
asm volatile(
"\n.ascii \"->" #sym " %0 " #val "\""
:
: "i" (val)
)
这个真的很难理解,这时候要动脑想办法 而不是硬刚,我们可以查询资料,发现这里边就有很好很合适的例子:
C
DEFINE(TEST_A,offsetof(struct test,a));
它会被展开为:
C
asm volatile(
"\n.ascii \"->" "TEST_A" " %0 " "offsetof(struct test,a)" "\""
:
: "i" (offsetof(struct test,a))
);
去掉最外边代表字符串的"
:\n.ascii \"-> "TEST_A" %0 "offsetof(struct test,a)" \"
这里发现TEST_A
和offsetof(struct test,a)
外层都套了一个"
,原因不是多此一举,因为如果不这样,它们会被识别成字符串的一部分.
最后得到的asm
代码是:
asm
.ascii "->TEST_A $x offsetof(struct test,a)"
这里注意:
- 首先有一个换行
$x
是一个立即数 ,所以%0
这个占位符在输入一个数的时候会被替换为立即数,当然有的时候立即数用#x
来表示,那这就得看编译器的作为了.
利用预编译 过程,就可以把.c
文件预编译成.s
文件,这样就可以实现动态获悉结构体大小的作用了.
并且上述资料为这个宏的由来提供了很好的背景:汇编器根本不认识C的语法,我们仍然需要C的计算结果.
这里发现.ascii
是一个输出调试信息的命令,那么例如TASK_THREAD_RA_RA
是什么呢?
在网络上继续搜索查询到了相关资料.
这里点明其中的重点:也就是先用 .c 文件生成 .s 文件,然后再用 sed
命令对其中特定的行进行替换,进而重定向到目标文件中,也就是 asm-offset.h。
也就是这个.s
文件也不重要,重要的是利用了汇编嵌入立即数的机制.(像我这种笨蛋还是一个个计算大小然后写入的命)
最后这些宏储存的是结构体struct task_struct
的成员的偏移量.
保存任务上下文的结构体
那么我们可以看出上一题的汇编代码 在保存的恰是struct task_struct
的内容.
保存陷入上下文的结构体
这个就难了,也没什么线索.只能尝试搜索一下trap
.
没想到真搜到了,还是在\arch\riscv\kernel\entry.S
:
asm
/*
* The ret_from_exception must be called with interrupt disabled. Here is the
* caller list:
* - handle_exception
* - ret_from_fork
*/
SYM_CODE_START_NOALIGN(ret_from_exception)
REG_L s0, PT_STATUS(sp)
#ifdef CONFIG_RISCV_M_MODE
/* the MPP value is too large to be used as an immediate arg for addi */
li t0, SR_MPP
and s0, s0, t0
#else
andi s0, s0, SR_SPP
#endif
bnez s0, 1f
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
call stackleak_erase_on_task_stack
#endif
/* Save unwound kernel stack pointer in thread_info */
addi s0, sp, PT_SIZE_ON_STACK
REG_S s0, TASK_TI_KERNEL_SP(tp)
/* Save the kernel shadow call stack pointer */
scs_save_current
/*
* Save TP into the scratch register , so we can find the kernel data
* structures again.
*/
csrw CSR_SCRATCH, tp
1:
#ifdef CONFIG_RISCV_ISA_V_PREEMPTIVE
move a0, sp
call riscv_v_context_nesting_end
#endif
REG_L a0, PT_STATUS(sp)
/*
* The current load reservation is effectively part of the processor's
* state, in the sense that load reservations cannot be shared between
* different hart contexts. We can't actually save and restore a load
* reservation, so instead here we clear any existing reservation --
* it's always legal for implementations to clear load reservations at
* any point (as long as the forward progress guarantee is kept, but
* we'll ignore that here).
*
* Dangling load reservations can be the result of taking a trap in the
* middle of an LR/SC sequence, but can also be the result of a taken
* forward branch around an SC -- which is how we implement CAS. As a
* result we need to clear reservations between the last CAS and the
* jump back to the new context. While it is unlikely the store
* completes, implementations are allowed to expand reservations to be
* arbitrarily large.
*/
REG_L a2, PT_EPC(sp)
REG_SC x0, a2, PT_EPC(sp)
csrw CSR_STATUS, a0
csrw CSR_EPC, a2
REG_L x1, PT_RA(sp)
REG_L x3, PT_GP(sp)
REG_L x4, PT_TP(sp)
REG_L x5, PT_T0(sp)
restore_from_x6_to_x31
REG_L x2, PT_SP(sp)
#ifdef CONFIG_RISCV_M_MODE
mret
#else
sret
#endif
SYM_CODE_END(ret_from_exception)
可以看到这个代码段的名字叫做ret_from_exception
,可见是处理中断之后的.那么根据注释,现在就是在恢复Trap上下文.
只需要按图索骥找到PT_*
寄存器对应的结构体的定义即可,例如:
C
OFFSET(PT_ORIG_R2, pt_regs, orig_r2);
可见保存Trap上下文的结构体是pt_regs
.
理解宏魔法的方法
编译一遍Linux Kernel
然后查看,这样是最妙的.
也可以像我一样使用这两个指令来解决:
shell
gcc -S -o empty.S empty.c
gcc -E -o empty.i empty.c
以empty.c
为例,这两个指令分别是生成编译过程的汇编版本 和预处理后的版本.
第四小问
Linux 在内核态运行的时候, tp
寄存器的值有什么含义? sscratch
的值是什么?
这时候去查The RISC-V Instruction Set Manual Volume I: User-Level ISA.
tp
寄存器就是x4
寄存器,代表Thread pointer
.
在源码中搜索thread pointer
找到arch\riscv\kernel\entry.S
里的:
asm
SYM_CODE_START(handle_exception)
/*
* If coming from userspace, preserve the user thread pointer and load
* the kernel thread pointer. If we came from the kernel, the scratch
* register will contain 0, and we should continue on the current TP.
*/
csrrw tp, CSR_SCRATCH, tp
bnez tp, .Lsave_context
根据注释,当在内核态 的时候sscratch
寄存器是0
.
这时候再重点看csrrw tp, CSR_SCRATCH, tp
这一句是交换tp
和CSR_SCRATCH
的值.与我脑中不同的是csrrw
的用法csrrw rd, csr, zimm[4:0]
:
rd
:目标寄存器,用于存放从 CSR 读取的原始值。csr
:控制和状态寄存器的地址。zimm[4:0]
:立即数,其中的值将被写入 CSR。
那么这时候tp
寄存器的值是什么呢?明明搜的是tp
结果的出来的结论是sscratch
的.
这时候我们关注进入内核态的相关代码 ,其实上边注释给了我们思路,我们就搜索came from the kernel
.
发现就在这段代码里还有一句came from the kernel
.
asm
/*
* Set the scratch register to 0, so that if a recursive exception
* occurs, the exception vector knows it came from the kernel
*/
csrw CSR_SCRATCH, x0
但是内容只是印证了sscratch
寄存器的值被设置为0
.
这让我们对整段代码块产生了兴趣:
asm
SYM_CODE_START(handle_exception)
/*
* If coming from userspace, preserve the user thread pointer and load
* the kernel thread pointer. If we came from the kernel, the scratch
* register will contain 0, and we should continue on the current TP.
*/
csrrw tp, CSR_SCRATCH, tp
bnez tp, .Lsave_context
.Lrestore_kernel_tpsp:
csrr tp, CSR_SCRATCH
REG_S sp, TASK_TI_KERNEL_SP(tp)
#ifdef CONFIG_VMAP_STACK
addi sp, sp, -(PT_SIZE_ON_STACK)
srli sp, sp, THREAD_SHIFT
andi sp, sp, 0x1
bnez sp, handle_kernel_stack_overflow
REG_L sp, TASK_TI_KERNEL_SP(tp)
#endif
.Lsave_context:
REG_S sp, TASK_TI_USER_SP(tp)
REG_L sp, TASK_TI_KERNEL_SP(tp)
addi sp, sp, -(PT_SIZE_ON_STACK)
REG_S x1, PT_RA(sp)
REG_S x3, PT_GP(sp)
REG_S x5, PT_T0(sp)
save_from_x6_to_x31
/*
* Disable user-mode memory access as it should only be set in the
* actual user copy routines.
*
* Disable the FPU/Vector to detect illegal usage of floating point
* or vector in kernel space.
*/
li t0, SR_SUM | SR_FS_VS
REG_L s0, TASK_TI_USER_SP(tp)
csrrc s1, CSR_STATUS, t0
csrr s2, CSR_EPC
csrr s3, CSR_TVAL
csrr s4, CSR_CAUSE
csrr s5, CSR_SCRATCH
REG_S s0, PT_SP(sp)
REG_S s1, PT_STATUS(sp)
REG_S s2, PT_EPC(sp)
REG_S s3, PT_BADADDR(sp)
REG_S s4, PT_CAUSE(sp)
REG_S s5, PT_TP(sp)
/*
* Set the scratch register to 0, so that if a recursive exception
* occurs, the exception vector knows it came from the kernel
*/
csrw CSR_SCRATCH, x0
/* Load the global pointer */
load_global_pointer
/* Load the kernel shadow call stack pointer if coming from userspace */
scs_load_current_if_task_changed s5
#ifdef CONFIG_RISCV_ISA_V_PREEMPTIVE
move a0, sp
call riscv_v_context_nesting_start
#endif
move a0, sp /* pt_regs */
/*
* MSB of cause differentiates between
* interrupts and exceptions
*/
bge s4, zero, 1f
/* Handle interrupts */
call do_irq
j ret_from_exception
1:
/* Handle other exceptions */
slli t0, s4, RISCV_LGPTR
la t1, excp_vect_table
la t2, excp_vect_table_end
add t0, t1, t0
/* Check if exception code lies within bounds */
bgeu t0, t2, 3f
REG_L t1, 0(t0)
2: jalr t1
j ret_from_exception
3:
la t1, do_trap_unknown
j 2b
SYM_CODE_END(handle_exception)
我们查看这段代码里所有对tp
的操作.
可以看到第一条就很容易得知了bnez tp, .Lsave_context
:
bnez
:bnez
是一个条件分支指令,代表 "Branch if Not Equal to Zero"(如果非零则分支)。它用于测试寄存器中的值是否非零,如果是,则跳转到指定的目标地址。
tp
:tp
是 RISC-V 架构中的一个专用寄存器,通常用于存储当前线程的栈顶地址。
.Lsave_context
:.Lsave_context
是一个标号,代表目标地址。当tp
的值非零时,程序将跳转到这个标号所标记的地址继续执行。
这段指令的作用是检查 tp
寄存器中的值是否非零。如果 tp
的值非零,则程序将跳转到标号 .Lsave_context
所指向的位置继续执行;否则,程序将继续按顺序执行下一条指令。
那么.Lsave_context
的内容是:
asm
.Lsave_context:
REG_S sp, TASK_TI_USER_SP(tp)
REG_L sp, TASK_TI_KERNEL_SP(tp)
addi sp, sp, -(PT_SIZE_ON_STACK)
REG_S x1, PT_RA(sp)
REG_S x3, PT_GP(sp)
REG_S x5, PT_T0(sp)
save_from_x6_to_x31
/*
* Disable user-mode memory access as it should only be set in the
* actual user copy routines.
*
* Disable the FPU/Vector to detect illegal usage of floating point
* or vector in kernel space.
*/
li t0, SR_SUM | SR_FS_VS
REG_L s0, TASK_TI_USER_SP(tp)
csrrc s1, CSR_STATUS, t0
csrr s2, CSR_EPC
csrr s3, CSR_TVAL
csrr s4, CSR_CAUSE
csrr s5, CSR_SCRATCH
REG_S s0, PT_SP(sp)
REG_S s1, PT_STATUS(sp)
REG_S s2, PT_EPC(sp)
REG_S s3, PT_BADADDR(sp)
REG_S s4, PT_CAUSE(sp)
REG_S s5, PT_TP(sp)
/*
* Set the scratch register to 0, so that if a recursive exception
* occurs, the exception vector knows it came from the kernel
*/
csrw CSR_SCRATCH, x0
/* Load the global pointer */
load_global_pointer
/* Load the kernel shadow call stack pointer if coming from userspace */
scs_load_current_if_task_changed s5
#ifdef CONFIG_RISCV_ISA_V_PREEMPTIVE
move a0, sp
call riscv_v_context_nesting_start
#endif
move a0, sp /* pt_regs */
/*
* MSB of cause differentiates between
* interrupts and exceptions
*/
bge s4, zero, 1f
/* Handle interrupts */
call do_irq
j ret_from_exception
1:
/* Handle other exceptions */
slli t0, s4, RISCV_LGPTR
la t1, excp_vect_table
la t2, excp_vect_table_end
add t0, t1, t0
/* Check if exception code lies within bounds */
bgeu t0, t2, 3f
REG_L t1, 0(t0)
2: jalr t1
j ret_from_exception
3:
la t1, do_trap_unknown
j 2b
重点看前两句就知道了:
asm
REG_S sp, TASK_TI_USER_SP(tp)
REG_L sp, TASK_TI_KERNEL_SP(tp)
相当于分别储存了用户栈指针和载入了内核栈指针.
这两个宏还是神奇的,同上边讲的宏魔法是一样的方式,访问了tp
为头指针的某个地址偏置的内容.
C
OFFSET(TASK_TI_KERNEL_SP, task_struct, thread_info.kernel_sp);
OFFSET(TASK_TI_USER_SP, task_struct, thread_info.user_sp);
这也证明了这时候tp
指向的就是task_struct
.那也就是当前任务的task_struct
了.
至于为什么当前任务是储存在tp
中的,我想我们需要更进一步的努力,这里我找了很多资料,实际上应该和tp
寄存器在RISC-V里的规定和内核的启动流程有关系.
我们只能说,顾名思义,既然叫这个名字也被反复代码佐证调用,应该不会有异议,但是这样的碎片化学习也确实不扎实.
第五小问
Linux 在用户态运行的时候, sscratch
的值有什么含义?
懂了第四问第五问也就很好了解,sscratch
是暂存地当前任务的task_struct
.
asm
/*
* If coming from userspace, preserve the user thread pointer and load
* the kernel thread pointer. If we came from the kernel, the scratch
* register will contain 0, and we should continue on the current TP.
*/
csrrw tp, CSR_SCRATCH, tp
bnez tp, .Lsave_context
... ...
/*
* Set the scratch register to 0, so that if a recursive exception
* occurs, the exception vector knows it came from the kernel
*/
csrw CSR_SCRATCH, x0
... ...
可以看到,如果是来自于用户态sscratch
不是0
,那么把tp
和sscratch
调换后,tp
不是0
就进行上下文保存,如果是来自内核态,sscratch
是0
,tp
也被调换为0
,因此可以做分支操作.
第六小问
Linux 在切换到内核态的时候,保存了和用户态程序相关的什么状态?
这个也好说,还是看这段代码:
asm
.Lsave_context:
REG_S sp, TASK_TI_USER_SP(tp)
REG_L sp, TASK_TI_KERNEL_SP(tp)
addi sp, sp, -(PT_SIZE_ON_STACK)
REG_S x1, PT_RA(sp)
REG_S x3, PT_GP(sp)
REG_S x5, PT_T0(sp)
save_from_x6_to_x31
/*
* Disable user-mode memory access as it should only be set in the
* actual user copy routines.
*
* Disable the FPU/Vector to detect illegal usage of floating point
* or vector in kernel space.
*/
li t0, SR_SUM | SR_FS_VS
REG_L s0, TASK_TI_USER_SP(tp)
csrrc s1, CSR_STATUS, t0
csrr s2, CSR_EPC
csrr s3, CSR_TVAL
csrr s4, CSR_CAUSE
csrr s5, CSR_SCRATCH
REG_S s0, PT_SP(sp)
REG_S s1, PT_STATUS(sp)
REG_S s2, PT_EPC(sp)
REG_S s3, PT_BADADDR(sp)
REG_S s4, PT_CAUSE(sp)
REG_S s5, PT_TP(sp)
/*
* Set the scratch register to 0, so that if a recursive exception
* occurs, the exception vector knows it came from the kernel
*/
csrw CSR_SCRATCH, x0
/* Load the global pointer */
load_global_pointer
/* Load the kernel shadow call stack pointer if coming from userspace */
scs_load_current_if_task_changed s5
#ifdef CONFIG_RISCV_ISA_V_PREEMPTIVE
move a0, sp
call riscv_v_context_nesting_start
#endif
move a0, sp /* pt_regs */
/*
* MSB of cause differentiates between
* interrupts and exceptions
*/
bge s4, zero, 1f
/* Handle interrupts */
call do_irq
j ret_from_exception
1:
/* Handle other exceptions */
slli t0, s4, RISCV_LGPTR
la t1, excp_vect_table
la t2, excp_vect_table_end
add t0, t1, t0
/* Check if exception code lies within bounds */
bgeu t0, t2, 3f
REG_L t1, 0(t0)
2: jalr t1
j ret_from_exception
3:
la t1, do_trap_unknown
j 2b
实际上是保存了所有的通用寄存器x0~x31 ,这个操作和rCore的操作是相同的.
同样在CSR
方面保存了SEPC
,SSTATUS
,SCAUSE
,STVAL
和SSCRATCH
.
第七小问
Linux 在内核态的时候,被打断的用户态程序的寄存器值存在哪里?在 C 代码里如何访问?
和第六小问是同一个问题,是被保存在了sp
指向的位置.
asm
... ...
REG_S x1, PT_RA(sp)
REG_S x3, PT_GP(sp)
REG_S x5, PT_T0(sp)
save_from_x6_to_x31
... ...
REG_S s0, PT_SP(sp)
REG_S s1, PT_STATUS(sp)
REG_S s2, PT_EPC(sp)
REG_S s3, PT_BADADDR(sp)
REG_S s4, PT_CAUSE(sp)
REG_S s5, PT_TP(sp)
而在代码的开头,
asm
REG_S sp, TASK_TI_USER_SP(tp)
REG_L sp, TASK_TI_KERNEL_SP(tp)
实际上是存储在了内核栈里边.
第八问
Linux 是如何根据系统调用编号找到对应的函数的?(对应 rCore 的 syscall::syscall()
函数的功能)
这个部分就很难说,查了半天,最后发现还是和汇编有关系,而且仍有部分代码是随编译生成.
深入Linux内核架构这本书里讲述了关于这部分是知识:
发出系统调用的机制,实际上是进行一个从用户空间到内核空间的可控切换,在所有支持的平台 上都有所不同。但标准文件负责与系统调用相关的以下两方面任务。 它定义了预处理器常数,将所有系统调用的描述符关联到符号常数。这些常数的名称诸如 __NR_chdir和__NR_send等。因为各体系结构会尽力与特定的本地操作系统(例如,Alpha上 的OSF/1、Sparc上的Solaris)所用的描述符保持一致,各体系结构所用的数值可能是不同的。 它定义了在内核内部调用系统调用所用的函数。通常,为此使用了一种预处理器机制,连同 用于自动生成代码的内联汇编。
搜索NR_chdir
发现,在\include\uapi\asm-generic\unistd.h
中有这样的注释:
C
/*
* This file contains the system call numbers, based on the
* layout of the x86-64 architecture, which embeds the
* pointer to the syscall in the table.
*
* As a basic principle, no duplication of functionality
* should be added, e.g. we don't use lseek when llseek
* is present. New architectures should use this file
* and implement the less feature-full calls in user space.
*/
这里是列出了x86-64 的syscall
代码.而不是risc-v
的.
查看题目中的提示,我们找到arch/riscv/kernel/syscall_table.c
这个文件,发现这个数组似乎是动态生成的:
C
void * const sys_call_table[__NR_syscalls] = {
[0 ... __NR_syscalls - 1] = __riscv_sys_ni_syscall,
#include <asm/syscall_table.h>
};
这里还利用了宏展开.把asm/syscall_table.h
里的内容展开过来:
C
#include <asm/bitsperlong.h>
#if __BITS_PER_LONG == 64
#include <asm/syscall_table_64.h>
#else
#include <asm/syscall_table_32.h>
#endif
但是找不到syscall_table_64.h
这个文件.因此应该是Make
过程中生成的.
第九问
Linux 用户程序调用 ecall
的参数是怎么传给系统调用的实现的?系统调用的返回值是怎样返回给用户态的?
这个似乎和RISC-V有关系,和使用的什么操作系统是没关系的.
在rCore中,使用a0~a7
寄存器来作为函数调用参数,返回值会给回a0
.
在Linux中也是如此.
实验练习
实践作业
这里首先还是切换到~/App/rCore-Tutorial-v3
,随后切换分支到要求的分支ch3-lab
:
shell
git checkout ch3-lab
开幕雷击之上来就报错:
rust
#[cfg(feature = "board_k210")]
pub const CLOCK_FREQ: usize = 403000000 / 62;
#[cfg(feature = "board_qemu")]
pub const CLOCK_FREQ: usize = 12500000;
这里的意思是必须实现board_qemu
或者board_k210
才能根据这两个进行定义CLOCK_FREQ
.
这里我们只需要创建os/src/board_qemu.rs
,然后在main.rs
里边在mod config
之前 通过mod board_qemu
引用这个空的文件即可.
作业要求我们实现一个新的系统调用,实现这个接口的过程很简单,重点是实现功能.
这里越实现越觉得奇怪.
首先是发现APP层的文件里并没有调用我们需要的sys_task_info
.
其次,发现参考书中提到的:相关结构已在框架中给出,只需添加逻辑实现功能需求即可.根本就没有在这个分支中实现.
TODO 这里需要给rCore-Tutorial提一个issue.
这里看评论区:
这位仁兄提到可以使用2023年的指导书,这使我想到是不是可以使用2024年的指导书,我还真的翻到了.
这时候需要把相关代码clone
下来.
shell
cd ~/App
git clone https://github.com/LearningOS/rCore-Tutorial-Code-2024S.git
cd rCore-Tutorial-Code-2024S/
git clone https://github.com/LearningOS/rCore-Tutorial-Test-2024S.git user
这里要注意就是要clone
两次的,因为还有一个是测试相关(user
文件)的.
切换到第三章作业,并且尝试运行:
shell
git checkout ch3
make run
运行成功:
shell
[rustsbi] RustSBI version 0.3.0-alpha.2, adapting to RISC-V SBI v1.0.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name : riscv-virtio,qemu
[rustsbi] Platform SMP : 1
[rustsbi] Platform Memory : 0x80000000..0x88000000
[rustsbi] Boot HART : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[kernel] PageFault in application, bad addr = 0x0, bad instruction = 0x804003ac, kernel killed it.
[kernel] IllegalInstruction in application, kernel killed it.
[kernel] IllegalInstruction in application, kernel killed it.
Hello, world from user mode program!
power_3 [10000/200000]
power_3 [20000/200000]
power_3 [30000/200000]
power_3 [40000/200000]
power_3 [50000/200000]
power_3 [60000/200000]
power_3 [70000/200000]
power_3 [80000/200000]
power_3 [90000/200000]
power_3 [100000/200000]
power_3 [110000/200000]
power_3 [120000/200000]
power_3 [130000/200000]
power_3 [140000/200000]
power_3 [150000/200000]
power_3 [160000/200000]
power_3 [170000/200000]
power_3 [180000/200000]
power_3 [190000/200000]
power_3 [200000/200000]
3^200000 = 871008973(MOD 998244353)
Test power_3 OK!
power_5 [10000/140000]
power_5 [20000/140000]
power_5 [30000/140000]
power_5 [40000/140000]
power_5 [50000/140000]
power_5 [60000/140000]
power_5 [70000/140000]
power_5 [80000/140000]
power_5 [90000/140000]
power_5 [100000/140000]
power_5 [110000/140000]
power_5 [120000/140000]
power_5 [130000/140000]
power_5 [140000/140000]
5^140000 = 386471875(MOD 998244353)
Test power_5 OK!
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
AAAAAAAAAA [1/5]
BBBBBBBBBB [1/5]
CCCCCCCCCC [1/5]
BBBBBBBBBB [2/5]
CCCCCCCCCC [2/5]
AAAAAAAAAA [2/5]
BBBBBBBBBB [3/5]
CCCCCCCCCC [3/5]
AAAAAAAAAA [3/5]
BBBBBBBBBB [4/5]
CCCCCCCCCC [4/5]
AAAAAAAAAA [4/5]
BBBBBBBBBB [5/5]
CCCCCCCCCC [5/5]
AAAAAAAAAA [5/5]
Test write B OK!
Test write C OK!
Test write A OK!
[kernel] Panicked at src/task/mod.rs:135 All applications completed!
发现一个非常严重的问题,这个测试虽然成功输出了,但是运行的APP似乎不是我们需要的APP.
我们通过观察rCore-Tutorial-Code-2024S/os/Makefile
的内容:
makefile
... ...
CHAPTER ?= $(shell git rev-parse --abbrev-ref HEAD | sed -E 's/ch([0-9])/\1/')
TEST ?= $(CHAPTER)
BASE ?= 1
... ...
kernel:
@make -C ../user build TEST=$(TEST) CHAPTER=$(CHAPTER) BASE=$(BASE)
... ...
可以看到如上指令是对APP层进行编译的指令.
git rev-parse --abbrev-ref HEAD | sed -E 's/ch([0-9])/\1/'
可以获取当前我们使用的是那个章节的代码TEST
,CHAPTER
,BASE
则分别是对于APP层编译使用的参数
这时候我们手动进行编译:
shell
cd ../user
make clean
make build TEST=3 CHAPTER=3 BASE=1
cd /build/bin
ls
发现编译结果确实是和我们猜想的只编译第三章的APP不同:
shell
ch2b_bad_address.bin ch2b_bad_register.bin ch2b_power_3.bin ch2b_power_7.bin ch3b_yield1.bin
ch2b_bad_instructions.bin ch2b_hello_world.bin ch2b_power_5.bin ch3b_yield0.bin ch3b_yield2.bin
可以看到是编译了ch2~3
所有的b
结尾的APP.
我们去看user/Makefile
:
makefile
... ...
BASE ?= 0
CHAPTER ?= 0
TEST ?= $(CHAPTER)
ifeq ($(TEST), 0) # No test, deprecated, previously used in v3
APPS := $(filter-out $(wildcard $(APP_DIR)/ch*.rs), $(wildcard $(APP_DIR)/*.rs))
else ifeq ($(TEST), 1) # All test
APPS := $(wildcard $(APP_DIR)/ch*.rs)
else
TESTS := $(shell seq $(BASE) $(TEST))
ifeq ($(BASE), 0) # Normal tests only
APPS := $(foreach T, $(TESTS), $(wildcard $(APP_DIR)/ch$(T)_*.rs))
else ifeq ($(BASE), 1) # Basic tests only
APPS := $(foreach T, $(TESTS), $(wildcard $(APP_DIR)/ch$(T)b_*.rs))
else # Basic and normal
APPS := $(foreach T, $(TESTS), $(wildcard $(APP_DIR)/ch$(T)*.rs))
endif
endif
... ...
好像确实是这么设计的(没绷住).
那么其实我们也可以明白,和我们这个实验测试相关的user/src/bin/ch3_taskinfo.rs
是属于 不是b
结尾的APP,我们需要编译条件是BASE=0
或者BASE=2
都可以运行到.
这里我们看到os/src/syscall/process.rs
里的:
rust
/// YOUR JOB: Finish sys_task_info to pass testcases
pub fn sys_task_info(_ti: *mut TaskInfo) -> isize {
trace!("kernel: sys_task_info");
-1
}
同时 加入编译条件LOG
:
rust
make run LOG=TRACE BASE=0
这时候发现日志信息太多了,稍微修改一下源码:
rust
/// YOUR JOB: Finish sys_task_info to pass testcases
pub fn sys_task_info(_ti: *mut TaskInfo) -> isize {
info!("kernel: sys_task_info");
-1
}
编译语句改为:
rust
make run LOG=INFO BASE=0
这时候的输出:
shell
[rustsbi] RustSBI version 0.3.0-alpha.2, adapting to RISC-V SBI v1.0.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name : riscv-virtio,qemu
[rustsbi] Platform SMP : 1
[rustsbi] Platform Memory : 0x80000000..0x88000000
[rustsbi] Boot HART : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[ INFO] [kernel] .data [0x80237000, 0x80244000)
[ WARN] [kernel] boot_stack top=bottom=0x80254000, lower_bound=0x80244000
[ERROR] [kernel] .bss [0x80254000, 0x80275000)
get_time OK! 6
current time_msec = 6
time_msec = 107 after sleeping 100 ticks, delta = 101ms!
Test sleep1 passed!
[ INFO] kernel: sys_task_info
Panicked at src/bin/ch3_taskinfo.rs:19, assertion `left == right` failed
left: 0
right: -1
Test sleep OK!
[kernel] Panicked at src/task/mod.rs:135 All applications completed!
可以看到是有的断言语句,而且没有达到要求:
rust
// user/src/bin/ch3_taskinfo.rs
#![no_std]
#![no_main]
extern crate user_lib;
use user_lib::{
get_time, println, sleep, task_info, TaskInfo, TaskStatus, SYSCALL_EXIT, SYSCALL_GETTIMEOFDAY,
SYSCALL_TASK_INFO, SYSCALL_WRITE, SYSCALL_YIELD,
};
#[no_mangle]
pub fn main() -> usize {
let t1 = get_time() as usize;
let mut info = TaskInfo::new();
get_time();
sleep(500);
let t2 = get_time() as usize;
// 注意本次 task info 调用也计入
assert_eq!(0, task_info(&mut info));
let t3 = get_time() as usize;
assert!(3 <= info.syscall_times[SYSCALL_GETTIMEOFDAY]);
assert_eq!(1, info.syscall_times[SYSCALL_TASK_INFO]);
assert_eq!(0, info.syscall_times[SYSCALL_WRITE]);
assert!(0 < info.syscall_times[SYSCALL_YIELD]);
assert_eq!(0, info.syscall_times[SYSCALL_EXIT]);
assert!(t2 - t1 <= info.time + 1);
assert!(info.time < t3 - t1 + 100);
assert!(info.status == TaskStatus::Running);
// 想想为什么 write 调用是两次
println!("string from task info test\n");
let t4 = get_time() as usize;
assert_eq!(0, task_info(&mut info));
let t5 = get_time() as usize;
assert!(5 <= info.syscall_times[SYSCALL_GETTIMEOFDAY]);
assert_eq!(2, info.syscall_times[SYSCALL_TASK_INFO]);
assert_eq!(2, info.syscall_times[SYSCALL_WRITE]);
assert!(0 < info.syscall_times[SYSCALL_YIELD]);
assert_eq!(0, info.syscall_times[SYSCALL_EXIT]);
assert!(t4 - t1 <= info.time + 1);
assert!(info.time < t5 - t1 + 100);
assert!(info.status == TaskStatus::Running);
println!("Test task info OK!");
0
}
那么我们面向结果编程(不是),首先就是为了满足这个需求.
这时候仔细看题目要求.
获取任务信息
ch3 中,我们的系统已经能够支持多个任务分时轮流运行,我们希望引入一个新的系统调用 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 返回系统调用时刻距离任务第一次被调度时刻的时长,也就是说这个时长可能包含该任务被其他任务抢占后的等待重新调度的时间。
- 由于查询的是当前任务的状态,因此 TaskStatus 一定是 Running。(助教起初想设计根据任务 id 查询,但是既不好定义任务 id 也不好写测例,遂放弃 QAQ)
- 调用
sys_task_info
也会对本次调用计数。
- 提示:
- 大胆修改已有框架!除了配置文件,你几乎可以随意修改已有框架的内容。
- 程序运行时间可以通过调用
get_time()
获取,注意任务运行总时长的单位是 ms。 - 系统调用次数可以考虑在进入内核态系统调用异常处理函数之后,进入具体系统调用函数之前维护。
- 阅读 TaskManager 的实现,思考如何维护内核控制块信息(可以在控制块可变部分加入需要的信息)。
- 虽然系统调用接口采用桶计数,但是内核采用相同的方法进行维护会遇到什么问题?是不是可以用其他结构计数?
实验要求
- 完成分支: ch3。
- 实验目录要求
├── os(内核实现)
│ ├── Cargo.toml(配置文件)
│ └── src(所有内核的源代码放在 os/src 目录下)
│ ├── main.rs(内核主函数)
│ └── ...
├── reports (不是 report)
│ ├── lab1.md/pdf
│ └── ...
├── ...
- 通过所有测例: CI 使用的测例与本地相同,测试中,user 文件夹及其它与构建相关的文件将被替换,请不要试图依靠硬编码通过测试。
默认情况下,makefile 仅编译基础测例 (
BASE=1
),即无需修改框架即可正常运行的测例。 你需要在编译时指定BASE=0
控制框架仅编译实验测例(在 os 目录执行make run BASE=0
), 或指定BASE=2
控制框架同时编译基础测例和实验测例。 - 如果本地在线编译访问 github 遇到问题,可以对 os/Cargo.toml 中的依赖进行如下替换: riscv = { git = "https://gitee.com/rcore-os/riscv", features = ["inline-asm"] }
virtio-drivers =
- 本地离线运行 CI 脚本的方法(注意 CI 脚本会修改代码仓库的文件,请在运行前暂存改动): git clone git@git.tsinghua.edu.cn:os-lab/2024s/public/rcore-tutorial-checker-2024s.git ci-user
git clone git@git.tsinghua.edu.cn:os-lab/2024s/public/rcore-tutorial-test-2024s.git ci-user/user
cd ci-user && make test CHAPTER=$ID OFFLINE=1
TODO 这里还是没有想出来:虽然系统调用接口采用桶计数,但是内核采用相同的方法进行维护会遇到什么问题?是不是可以用其他结构计数?
这里直接从syscall
模块开始看我写的实现:
rust
// os/src/syscall/process.rs
/// YOUR JOB: Finish sys_task_info to pass testcases
pub fn sys_task_info(_ti: *mut TaskInfo) -> isize {
info!("kernel: sys_task_info");
unsafe {
*_ti = TaskInfo {
status: get_current_task_status(),
syscall_times: get_current_task_syscall_times(),
time: get_time_ms()-get_current_task_first_start_time() as usize,
};
}
0
}
在os/src/syscall/mod.rs
中,也要添加一个记录函数:
rust
// os/src/syscall/mod.rs
/// handle syscall exception with `syscall_id` and other arguments
pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
record_syscall_times(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),
}
}
四个函数是我在task
模块里的新实现:
rust
/// Get the status of the current 'Running' task.
pub fn get_current_task_status() -> TaskStatus {
TASK_MANAGER.get_current_task_status()
}
/// Get the first start time of the current 'Running' task.
pub fn get_current_task_first_start_time() -> isize {
TASK_MANAGER.get_current_task_first_start_time()
}
/// Record the syscall times of the current 'Running' task.
pub fn record_syscall_times(syscall_id: usize) {
TASK_MANAGER.record_syscall_times(syscall_id);
}
/// Get the syscall times of the current 'Running' task.
pub fn get_current_task_syscall_times() -> [u32; MAX_SYSCALL_NUM] {
TASK_MANAGER.get_current_task_syscall_times()
}
这个也是仿照原来的实现对TASK_MANAGER
的包裹.
那么TASK_MANAGER
调用的属于TaskManager
的方法,也是对于struct
的成员的访问:
rust
impl TaskManager {
... ...
fn get_current_task_status(&self) -> TaskStatus {
let inner = self.inner.exclusive_access();
inner.tasks[inner.current_task].task_status
}
fn get_current_task_first_start_time(&self) -> isize {
let inner = self.inner.exclusive_access();
inner.tasks[inner.current_task].task_first_start_time
}
fn record_syscall_times(&self, syscall_id: usize) {
let mut inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.tasks[current].syscall_times[syscall_id] += 1;
}
fn get_current_task_syscall_times(&self) -> [u32; MAX_SYSCALL_NUM] {
let inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.tasks[current].syscall_times
}
... ...
}
其中比较重要的还是record_syscall_times
和get_current_task_syscall_times
.
因为这里涉及到了对TaskControlBlock
添加的新成员.
rust
// os/src/task/task.rs
/// The task control block (TCB) of a task.
#[derive(Copy, Clone)]
pub struct TaskControlBlock {
/// The task status in it's lifecycle
pub task_status: TaskStatus,
/// The task context
pub task_cx: TaskContext,
/// The time when the task first starts
pub task_first_start_time: isize,
/// The numbers of syscall called by task
pub syscall_times: [u32; MAX_SYSCALL_NUM],
}
这时候运行:
shell
make run LOG=INFO BASE=0
输出:
shell
[rustsbi] RustSBI version 0.3.0-alpha.2, adapting to RISC-V SBI v1.0.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name : riscv-virtio,qemu
[rustsbi] Platform SMP : 1
[rustsbi] Platform Memory : 0x80000000..0x88000000
[rustsbi] Boot HART : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[ INFO] [kernel] .data [0x80236000, 0x80243000)
[ WARN] [kernel] boot_stack top=bottom=0x80253000, lower_bound=0x80243000
[ERROR] [kernel] .bss [0x80253000, 0x8027c000)
get_time OK! 8
current time_msec = 8
time_msec = 109 after sleeping 100 ticks, delta = 101ms!
Test sleep1 passed!
[ INFO] kernel: sys_task_info
string from task info test
[ INFO] kernel: sys_task_info
Test task info OK!
Test sleep OK!
[kernel] Panicked at src/task/mod.rs:143 All applications completed!
尝试使用官方的打分工具
使用如下指令得到打分工具:
shell
git clone https://github.com/LearningOS/rCore-Tutorial-Code-2024S.git
cd rCore-Tutorial-Code-2024S
rm -rf ci-user
git clone https://github.com/LearningOS/rCore-Tutorial-Checker-2024S.git ci-user
git clone https://github.com/LearningOS/rCore-Tutorial-Test-2024S.git ci-user/user
git checkout ch$ID
# check&grade OS in ch$ID with more tests
cd ci-user && make test CHAPTER=3
这里$ID
我已经换成3.
运行得到的结果反而是会卡住.
这里参考HangX-Ma的笔记,把原本三个获取信息的方法更改为一个:
rust
// os/src/task/mod.rs
impl TaskManager
{
fn get_current_task_block(&self) -> TaskControlBlock {
let inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.tasks[current]
}
}
/// Get the current task block.
pub fn get_current_task_block() -> TaskControlBlock {
TASK_MANAGER.get_current_task_block()
}
这时候:
rust
// os/src/syscall/process.rs
/// YOUR JOB: Finish sys_task_info to pass testcases
pub fn sys_task_info(_ti: *mut TaskInfo) -> isize {
info!("kernel: sys_task_info");
let task_block = get_current_task_block();
unsafe {
*_ti = TaskInfo {
status: task_block.task_status,
syscall_times: task_block.syscall_times,
time: get_time_ms()-task_block.task_first_start_time as usize,
};
}
0
}
重新执行打分系统:
shell
cd ci-user
make test CHAPTER=3
得到:
shell
[rustsbi] RustSBI version 0.3.0-alpha.2, adapting to RISC-V SBI v1.0.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name : riscv-virtio,qemu
[rustsbi] Platform SMP : 1
[rustsbi] Platform Memory : 0x80000000..0x88000000
[rustsbi] Boot HART : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[kernel] PageFault in application, bad addr = 0x0, bad instruction = 0x804003ac, kernel killed it.
[kernel] IllegalInstruction in application, kernel killed it.
[kernel] IllegalInstruction in application, kernel killed it.
Hello, world from user mode program!
power_3 [10000/200000]
power_3 [20000/200000]
power_3 [30000/200000]
power_3 [40000/200000]
power_3 [50000/200000]
power_3 [60000/200000]
power_3 [70000/200000]
power_3 [80000/200000]
power_3 [90000/200000]
power_3 [100000/200000]
power_3 [110000/200000]
power_3 [120000/200000]
power_3 [130000/200000]
power_3 [140000/200000]
power_3 [150000/200000]
power_3 [160000/200000]
power_3 [170000/200000]
power_3 [180000/200000]
power_3 [190000/200000]
power_3 [200000/200000]
3^200000 = 871008973(MOD 998244353)
Test power_3 OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!
power_5 [10000/140000]
power_5 [20000/140000]
power_5 [30000/140000]
power_5 [40000/140000]
power_5 [50000/140000]
power_5 [60000/140000]
power_5 [70000/140000]
power_5 [80000/140000]
power_5 [90000/140000]
power_5 [100000/140000]
power_5 [110000/140000]
power_5 [120000/140000]
power_5 [130000/140000]
power_5 [140000/140000]
5^140000 = 386471875(MOD 998244353)
Test power_5 OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!
get_time OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703! 15
current time_msec = 16
AAAAAAAAAA [1/5]
BBBBBBBBBB [1/5]
CCCCCCCCCC [1/5]
AAAAAAAAAA [2/5]
BBBBBBBBBB [2/5]
CCCCCCCCCC [2/5]
AAAAAAAAAA [3/5]
BBBBBBBBBB [3/5]
CCCCCCCCCC [3/5]
AAAAAAAAAA [4/5]
BBBBBBBBBB [4/5]
CCCCCCCCCC [4/5]
AAAAAAAAAA [5/5]
BBBBBBBBBB [5/5]
CCCCCCCCCC [5/5]
Test write A OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!
Test write B OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!
Test write C OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!
time_msec = 117 after sleeping 100 ticks, delta = 101ms!
Test sleep1 passed108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!
string from task info test
Test task info OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!
Test sleep OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!
[kernel] Panicked at src/task/mod.rs:143 All applications completed!
make[1]: Leaving directory '/home/winddevil/App/rCore-Tutorial-Code-2024S/os'
python3 check/ch3.py < stdout-ch3
['get_time OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703! (\\d+)', 'Test sleep OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!', 'current time_msec = (\\d+)', 'time_msec = (\\d+) after sleeping (\\d+) ticks, delta = (\\d+)ms!', 'Test sleep1 passed108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!', 'string from task info test', 'Test task info OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!'] []
[PASS] found <get_time OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703! (\d+)>
[PASS] found <Test sleep OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!>
[PASS] found <current time_msec = (\d+)>
[PASS] found <time_msec = (\d+) after sleeping (\d+) ticks, delta = (\d+)ms!>
[PASS] found <Test sleep1 passed108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!>
[PASS] found <string from task info test>
[PASS] found <Test task info OK108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703!>
Test passed108555400344366892959246160303963725110509874186131795664545912711533192633763728051953567001905031813167241237815709164221805013426241793040257117282631636437781402128911587233237614961549283815838886474811079129885051215412843617893644241703: 7/7
最后是全部PASS的结局.
但是为什么我原来的方法就不可以,现在还没有搞明白,可以在这个链接和我讨论,求求了.
问答作业
第一题
这个题在第二章作业已经有了,我们不再重复了吧.
第二题
这题在第一张的实验练习:问答作业 这块是已经做过的,只需要理解是什么时候进入S
态的即可.
这里文档中给出的链接定位非常准确,如下所示函数是rustsbi
的入口:
rust
extern "C" fn rust_main(hartid: usize, opqaue: usize)
我们找到这一行输出了关于进入S
态的说明.
随后找到了execute_supervisor这个函数,一直追踪下去发现了这个操作,设置mstatus
的mpp
位.同样还有设置mepc
的操作.
找到手册里对于mpp
的描述:
发生异常之前的权限模式保留在 mstatus 的 MPP 域中,再把权限模式更改为 M
处理程序用 mret 指令(M 模 式特有的指令)返回。mret 将 PC 设置为 mepc,通过将 mstatus 的 MPIE 域复制到 MIE 来恢复之前的中断使能设置,并将权限模式设置为 mstatus 的 MPP 域中的值。
这时候我们只需要找到到底是哪里调用了mret
就行了.
这里我们可以看到这里调用了resume
,而它调用了do_resume
.
从这里一直往下追踪do_resume
的嵌套即可,可以看到to_supervisor_restore
,最终调用了mert
,也就是从这时候开始正式进入了S
态.
说点看到的别的部分,这里回想起上一章学到的关于RISC-V的把中断和同步异常交给 S 模式处理而完全绕过 M 模式,我们想到了mideleg(Machine Interrupt Delegation,机器中断委托)
,这时候在源码里搜索找到了这个函数:
rust
// 委托终端;把S的中断全部委托给S层
fn delegate_interrupt_exception() {
use riscv::register::{medeleg, mideleg, mie};
unsafe {
mideleg::set_sext();
mideleg::set_stimer();
mideleg::set_ssoft();
mideleg::set_uext();
mideleg::set_utimer();
mideleg::set_usoft();
medeleg::set_instruction_misaligned();
medeleg::set_breakpoint();
medeleg::set_user_env_call();
medeleg::set_instruction_page_fault();
medeleg::set_load_page_fault();
medeleg::set_store_page_fault();
medeleg::set_instruction_fault();
medeleg::set_load_fault();
medeleg::set_store_fault();
mie::set_mext();
// 不打开mie::set_mtimer
mie::set_msoft();
}
}