编程作业

思路

rCore 的第一个实验,主要是为了熟悉如何进行内核编程,实现起来比较简单。

要求实现一个系统调用,填充传入的 TaskInfo 结构体已获取当前任务的一些信息,包含三个字段:任务状态、任务使用的系统调用及调用次数、系统调用时刻距离任务第一次被调度时刻的时长(单位 ms)。

首先是任务状态,这个比较简单,直接查看当前任务的任务控制块的字段值即可。

对于系统调用次数,可以在任务控制块中添加新的字段来存储相关信息,例如按提示所说,一个长度为 MAX_SYSCALL_NUM 的整型数组。在函数 syscall/mod.rs:syscall 中,在内核对用户态传入的系统调用号进行分发处理前,增加对应的系统调用桶的计数。注意,由于本次系统调用 sys_task_info 也要进行计数,因此不能在执行了特定的系统调用后再来增加计数,否则本次 sys_task_info 系统调用次数将无法被统计。

最后是距离任务第一次被调度时刻的时长,一种实现方式是:为任务控制块添加新的字段:time,表示任务第一次被调度的时间。先为 time 设定一个初始值(例如 0),表示该值未被更改过,每当一个任务即将被调度时(如 TASK_MANAGER.run_next_task() 进行任务切换时),查看 time 值,判断是否未被更改:如果是,则赋予当前时间;否则,不做任何操作。这样,就成功保存了任务初次被调度的时刻。在调用 sys_task_info 时,将当前时间减去保存的初次被调度时间,即为时长,注意单位为毫秒。

代码

由于代码是直接在原项目代码的基础上进行修改,因此为了查看方便,本次及之后实验的代码将以 git diff 的形式展现,加号(绿色)代表添加的代码,减号(红色)代表删除的代码。

diff --git a/os/src/syscall/mod.rs b/os/src/syscall/mod.rs
index e6e8f98..7f63ed6 100644
--- a/os/src/syscall/mod.rs
+++ b/os/src/syscall/mod.rs
@@ -26,8 +26,13 @@ mod process;
 
 use fs::*;
 use process::*;
+
+use crate::task::inc_syscall_times;
+
 /// handle syscall exception with `syscall_id` and other arguments
 pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
+    // increase syscall times before handle specific syscall
+    inc_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),
diff --git a/os/src/syscall/process.rs b/os/src/syscall/process.rs
index f1cd424..8d01cf4 100644
--- a/os/src/syscall/process.rs
+++ b/os/src/syscall/process.rs
@@ -1,8 +1,9 @@
 //! Process management syscalls
+
 use crate::{
     config::MAX_SYSCALL_NUM,
-    task::{exit_current_and_run_next, suspend_current_and_run_next, TaskStatus},
-    timer::get_time_us,
+    task::{exit_current_and_run_next, get_current_tcb, suspend_current_and_run_next, TaskStatus},
+    timer::{get_time_ms, get_time_us},
 };
 
 #[repr(C)]
@@ -53,5 +54,19 @@ pub fn sys_get_time(ts: *mut TimeVal, _tz: usize) -> isize {
 /// YOUR JOB: Finish sys_task_info to pass testcases
 pub fn sys_task_info(_ti: *mut TaskInfo) -> isize {
     trace!("kernel: sys_task_info");
-    -1
+    let cur_tcb = get_current_tcb();
+    // labour in vain! The status must be Running
+    let status = cur_tcb.task_status;
+    let syscall_times = cur_tcb.syscall_times;
+    let time = get_time_ms() - cur_tcb.time;
+
+    unsafe {
+        *_ti = TaskInfo {
+            status,
+            syscall_times,
+            time,
+        }
+    }
+
+    0
 }
diff --git a/os/src/task/mod.rs b/os/src/task/mod.rs
index c1636ef..4a14e2f 100644
--- a/os/src/task/mod.rs
+++ b/os/src/task/mod.rs
@@ -14,9 +14,11 @@ mod switch;
 #[allow(clippy::module_inception)]
 mod task;
 
-use crate::config::MAX_APP_NUM;
+
+use crate::config::{MAX_APP_NUM, MAX_SYSCALL_NUM};
 use crate::loader::{get_num_app, init_app_cx};
 use crate::sync::UPSafeCell;
+use crate::timer::get_time_ms;
 use lazy_static::*;
 use switch::__switch;
 pub use task::{TaskControlBlock, TaskStatus};
@@ -54,6 +56,8 @@ lazy_static! {
         let mut tasks = [TaskControlBlock {
             task_cx: TaskContext::zero_init(),
             task_status: TaskStatus::UnInit,
+            syscall_times: [0u32; MAX_SYSCALL_NUM],
+            time: 0usize,
         }; MAX_APP_NUM];
         for (i, task) in tasks.iter_mut().enumerate() {
             task.task_cx = TaskContext::goto_restore(init_app_cx(i));
@@ -122,6 +126,11 @@ impl TaskManager {
             let mut inner = self.inner.exclusive_access();
             let current = inner.current_task;
             inner.tasks[next].task_status = TaskStatus::Running;
+            // if the task is being called for the first time,
+            if inner.tasks[next].time == 0usize {
+                // then set it to the current time
+                inner.tasks[next].time = get_time_ms();
+            }
             inner.current_task = next;
             let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
             let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
@@ -135,6 +144,24 @@ impl TaskManager {
             panic!("All applications completed!");
         }
     }
+
+    /// Get task control block
+    pub fn get_current_tcb(&self) -> TaskControlBlock {
+        let inner = self.inner.exclusive_access();
+        let current = inner.current_task;
+        inner.tasks[current].clone()
+    }
+
+    /// increase syscall times
+    pub fn inc_syscall_times(&self, syscall_id: usize) -> bool {
+        if syscall_id >= MAX_SYSCALL_NUM {
+            return false;
+        }
+        let mut inner = self.inner.exclusive_access();
+        let current = inner.current_task;
+        inner.tasks[current].syscall_times[syscall_id] += 1;
+        true
+    }
 }
 
 /// Run the first task in task list.
@@ -169,3 +196,13 @@ pub fn exit_current_and_run_next() {
     mark_current_exited();
     run_next_task();
 }
+
+/// Get current task control block
+pub fn get_current_tcb() -> TaskControlBlock {
+    TASK_MANAGER.get_current_tcb()
+}
+
+/// increase syscall times
+pub fn inc_syscall_times(syscall_id: usize) -> bool {
+    TASK_MANAGER.inc_syscall_times(syscall_id)
+}
\ No newline at end of file
diff --git a/os/src/task/task.rs b/os/src/task/task.rs
index e6580c9..4e4c19b 100644
--- a/os/src/task/task.rs
+++ b/os/src/task/task.rs
@@ -1,6 +1,7 @@
 //! Types related to task management
 
 use super::TaskContext;
+use crate::config::MAX_SYSCALL_NUM;
 
 /// The task control block (TCB) of a task.
 #[derive(Copy, Clone)]
@@ -9,6 +10,10 @@ pub struct TaskControlBlock {
     pub task_status: TaskStatus,
     /// The task context
     pub task_cx: TaskContext,
+    /// The syscall times
+    pub syscall_times: [u32; MAX_SYSCALL_NUM],
+    /// The first time the task was scheduled
+    pub time: usize,
 }
 
 /// The status of a task

time 的设置有一点小 bug,就是第一个被调度任务的 time 应该在 run_first_task 中设置,否则它的 time 将会晚一个调度周期,testcase 没测出来。。。写博客时才注意到,懒得改了😓

问答作业

t1

Q: 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。 请同学们可以自行测试这些内容(运行 三个 bad 测例 (ch2b_bad_*.rs) ), 描述程序出错行为,同时注意注明你使用的 sbi 及其版本。

A: 略。

t2

深入理解 trap.S 中两个函数 __alltraps__restore 的作用,并回答如下问题:

Q1: L40:刚进入 __restore 时,a0 代表了什么值。请指出 __restore 的两种使用情景。

A1: a0 代表了内核栈的栈指针,即 __alltraps 保存的上下文的首地址。

情景一:表示内核第一次从内核态切换到用户态(或者说将 CPU 所有权转交给用户程序),在初始化或加载应用程序时,内核通过 __restore 恢复必要的状态。

情景二:当 call trap_handler 执行结束后,程序计数器指向它的下一条指令,即 __restore 的起始地址,表示 Trap 处理完成,将恢复 Trap 时的上下文状态,返回用户态。

Q2: L43-L48:这几行汇编代码特殊处理了哪些寄存器?这些寄存器的的值对于进入用户态有何意义?请分别解释。

ld t0, 32*8(sp)
ld t1, 33*8(sp)
ld t2, 2*8(sp)
csrw sstatus, t0
csrw sepc, t1
csrw sscratch, t2

A2: 特殊处理了 t0t1t2sstatussepcsscratch 寄存器。

t0t1t2 用来暂存从内核栈中读取的之前保存的 CSR 寄存器的值,再使用 csrw 指令将暂存值写入对应的 CSR 寄存器中。

其中 sstatusSPP 字段给出 Trap 发生之前 CPU 处在哪个特权级等信息,sepc 当 Trap 是异常时(exception)记录之前执行的最后一条指令地址,sscratch 则存储着用户栈的栈指针,用于在 sret 返回用户态前使用 csrrw 指令将当前栈指针从内核栈切换到用户栈。

Q3: L50-L56:为何跳过了 x2x4

ld x1, 1*8(sp)
ld x3, 3*8(sp)
.set n, 5
.rept 27
   LOAD_GP %n
   .set n, n+1
.endr

A3: 如下图所示,x2sp 寄存器,保存了内核栈的栈指针。而 x4 寄存器为线程寄存器,在本实验中不会用到。

Q4: L60:该指令之后,spsscratch 中的值分别有什么意义?

csrrw sp, sscratch, sp

A4: 该指令的作用是先将 sscratch 的值读取到 sp 中,再将 sp 的值写入 sscratch 中,这两个操作是在同一周期内完成的,无需中间寄存器的参与,作用相当于 swap(sp, sscratch). 在返回用户态之前交换 spsscratch 的值,即将当前栈指针由用户栈切换到内核栈。

Q5: __restore:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态?

A5: 状态切换发生在 sret,该指令会负责从栈中恢复之前保存的程序计数器值,并更新特权级相关的寄存器(如 mstatus)。

Q6: L13:该指令之后,spsscratch 中的值分别有什么意义?

csrrw sp, sscratch, sp

A6: 与 L60 相反,作用是将当前栈指针由内核栈切换到用户栈,便于后续的 Trap 处理。

Q7: 从 U 态进入 S 态是哪一条指令发生的?

A7: 根据 Trap 的类型不同而定。对于系统调用而言,是 ecall 指令,对于其它的异常情况(如非法操作、页面缺失等)则是在硬件检测到后自动触发异常,从而进入内核态。