uCore 实验第 3 章 - 多道程序与分时多任务

// 启动时初始化进程表
void proc_init(void)
{
    struct proc *p;
    for (p = pool; p < &pool[NPROC]; p++) {
        p->state = UNUSED;
        // p - pool 是 p 指向的 proc 在 pool 中的下标,因此 p - pool 变化情况是 0, 1, 2, ..., NPROC - 1
        p->kstack    = (uint64)kstack[p - pool];
        p->ustack    = (uint64)ustack[p - pool];
        p->trapframe = (struct trapframe *)trapframe[p - pool];
        /*
		* LAB1: you may need to initialize your new fields of proc here
		*/
    }
    idle.kstack  = (uint64)boot_stack_top;
    idle.pid     = 0;
    current_proc = &idle;
}

p - pool 表示什么?
假设我们有一个名为 pool 的数组,其中包含了多个类型为 struct proc 的元素,并且有一个指针 p 指向其中的某个元素。
当 p 指向 pool 数组的第一个元素时,p - pool 的结果将是 0,因为指针相对于数组首地址的偏移量为 0。
当 p 指向 pool 数组的第二个元素时,p - pool 的结果将是 1,因为指针相对于数组首地址的偏移量为 1。
以此类推,当 p 指向 pool 数组的第 N 个元素时,p - pool 的结果将是 N-1,因为指针相对于数组首地址的偏移量为 N-1。
总结来说,如果 p 是指向 pool 数组中第 N 个元素的指针,那么 p - pool 的结果将是 N-1。

原调度函数每次都会从 pool 数组的第一个元素开始遍历,这样会导致每次都是从第一个进程开始运行,而不是从上次运行的进程开始运行。需要修改为如下:

// 调度程序永不返回。它循环执行以下操作:
//  - 选择要运行的进程。
//  - 切换以启动运行该进程。
//  - 最终,该进程通过切换将控制权
//    传递回调度程序。
void scheduler(void)
{
    struct proc *p;
    struct proc *last_checked_proc = pool; // 初始化指针为 pool

    for (;;) {
        for (p = last_checked_proc; p < &pool[NPROC];
             p++) { // 将 p 初始化为 last_checked_proc
            if (p->state == RUNNABLE) {
                /*
                * LAB1:你可能需要在这里初始化进程的起始时间
                */
                p->state     = RUNNING;
                current_proc = p;
                swtch(&idle.context, &p->context);
            }
        }
        last_checked_proc = pool + 1; // 更新 last_checked_proc 的值为下一个位置
    }
}

LAB1

---
 os/loader.c           |   5 +-
 os/proc.c             |  15 +-
 os/proc.h             |  23 +-
 os/syscall.c          |  55 ++++-
 os/syscall_ids.h      |   5 +-
 os/timer.h            |   2 +
 9 files changed, 374 insertions(+), 291 deletions(-)

diff --git a/os/loader.c b/os/loader.c
index b45e85d..b21b0a4 100644
--- a/os/loader.c
+++ b/os/loader.c
@@ -1,6 +1,7 @@
 #include "loader.h"
 #include "defs.h"
 #include "trap.h"
+#include <string.h>
 
 static uint64  app_num;
 static uint64 *app_info_ptr;
@@ -49,8 +50,10 @@ int run_all_app()
         trapframe->sp  = (uint64)p->ustack + USER_STACK_SIZE;
         p->state       = RUNNABLE;
         /*
-		* LAB1: you may need to initialize your new fields of proc here
+		* LAB1: 初始化系统调用数以及进程开始时间
 		*/
+        memset(p->syscall_times, 0, MAX_SYSCALL_NUM * sizeof(uint32));
+        p->start_time = 0;
     }
     return 0;
 }
\ No newline at end of file
diff --git a/os/proc.c b/os/proc.c
index fee3886..0c69ae5 100644
--- a/os/proc.c
+++ b/os/proc.c
@@ -2,6 +2,7 @@
 #include "defs.h"
 #include "loader.h"
 #include "trap.h"
+#include "timer.h"
 
 struct proc pool[NPROC];
 char        kstack[NPROC][PAGE_SIZE];
@@ -33,9 +34,8 @@ void proc_init(void)
         p->kstack    = (uint64)kstack[p - pool];
         p->ustack    = (uint64)ustack[p - pool];
         p->trapframe = (struct trapframe *)trapframe[p - pool];
-        /*
-		* LAB1: you may need to initialize your new fields of proc here
-		*/
+        memset(p->syscall_times, 0, MAX_SYSCALL_NUM * sizeof(uint32));
+        p->start_time = 0;
     }
     idle.kstack  = (uint64)boot_stack_top;
     idle.pid     = 0;
@@ -47,6 +47,7 @@ int allocpid()
     static int PID = 1;
     return PID++;
 }
+
 // 在进程表中寻找一个未使用的进程。
 // 如果找到,则初始化在内核中运行所需的状态。
 // 如果没有空闲的进程,或者内存分配失败,则返回 0。
@@ -80,14 +81,18 @@ void scheduler(void)
 {
     struct proc *p;
     struct proc *last_checked_proc = pool; // 初始化指针为 pool
-
     for (;;) {
         for (p = last_checked_proc; p < &pool[NPROC];
              p++) { // 将 p 初始化为 last_checked_proc
             if (p->state == RUNNABLE) {
                 /*
-                * LAB1:你可能需要在这里初始化进程的起始时间
+                * LAB1:在这里初始化进程的开始时间
                 */
+                if (p->start_time == 0) {
+                    uint64 cycle = get_cycle();
+                    p->start_time =
+                        (cycle % CPU_FREQ) * MILLISECONDS_PER_SECOND / CPU_FREQ;
+                }
                 p->state     = RUNNING;
                 current_proc = p;
                 swtch(&idle.context, &p->context);
diff --git a/os/proc.h b/os/proc.h
index d208c5d..53576bf 100644
--- a/os/proc.h
+++ b/os/proc.h
@@ -3,7 +3,8 @@
 
 #include "types.h"
 
-#define NPROC (16)
+#define NPROC           (16)  // 最大进程数
+#define MAX_SYSCALL_NUM (500) // 最大系统调用数
 
 // Saved registers for kernel context switches.
 struct context {
@@ -42,14 +43,28 @@ struct proc {
     uint64            kstack;    // 进程内核栈虚拟地址 (内核页表)
     struct trapframe *trapframe; // 进程中断帧
     struct context    context; // 用于保存进程内核态的寄存器信息,进程切换时使用
-                               /*
-	* LAB1: you may need to add some new fields here
+    /*
+	* LAB1: 添加一些新的成员用于新的 sys_task_info 系统调用
 	*/
+    uint32 syscall_times[MAX_SYSCALL_NUM]; // 系统调用次数统计 TODO: 后续改为指针
+    uint64 start_time;                     // 进程开始运行时间
 };
 
 /*
-* LAB1: you may need to define struct for TaskInfo here
+* LAB1: 定义 TaskInfo 结构体
 */
+typedef enum {
+    UnInit,
+    Ready,
+    Running,
+    Exited,
+} TaskStatus;
+
+typedef struct {
+    TaskStatus status;
+    uint32     syscall_times[MAX_SYSCALL_NUM];
+    int        time; // 进程运行时间统计
+} TaskInfo;
 
 struct proc *curr_proc();
 void         exit(int);
diff --git a/os/syscall.c b/os/syscall.c
index 1cc5aeb..f54ed86 100644
--- a/os/syscall.c
+++ b/os/syscall.c
@@ -4,6 +4,7 @@
 #include "syscall_ids.h"
 #include "timer.h"
 #include "trap.h"
+#include "proc.h"
 
 uint64 sys_write(int fd, char *str, uint len)
 {
@@ -31,14 +32,46 @@ uint64 sys_sched_yield()
 uint64 sys_gettimeofday(TimeVal *val, int _tz)
 {
     uint64 cycle = get_cycle();
-    val->sec     = cycle / CPU_FREQ;
-    val->usec    = (cycle % CPU_FREQ) * MICROSECONDS_PER_SECOND / CPU_FREQ;
+    tracef("sys_gettimeofday cycle = %d", cycle);
+    val->sec  = cycle / CPU_FREQ;
+    val->msec = (cycle % CPU_FREQ) * MILLISECONDS_PER_SECOND / CPU_FREQ;
+    val->usec = (cycle % CPU_FREQ) * MICROSECONDS_PER_SECOND / CPU_FREQ;
     return 0;
 }
 
-/*
-* LAB1: you may need to define sys_task_info here
-*/
+/** 
+ * LAB1:此处定义 sys_task_info 函数
+ * 查询当前正在执行的任务信息,任务信息包括任务控制块相关信息(任务状态)、任务使用的系统调用次数、任务总运行时长。 
+ */
+int sys_task_info(TaskInfo *ti)
+{
+    struct proc *proc = curr_proc();
+    // TODO: proc 检查为空
+    for (int i = 0; i < MAX_SYSCALL_NUM; i++) {
+        ti->syscall_times[i] = proc->syscall_times[i];
+    }
+    uint64 cycle = get_cycle();
+    uint64 current_time =
+        (cycle % CPU_FREQ) * MILLISECONDS_PER_SECOND / CPU_FREQ;
+    infof("sys_task_info current_time = %d", current_time);
+    infof("proc->start_time = %d", proc->start_time);
+    infof("ti->time = %d", current_time - proc->start_time);
+
+    if (proc->state == RUNNING) {
+        ti->status = Running;
+    } else if (proc->state == RUNNABLE) {
+        ti->status = Ready;
+    } else if (proc->state == SLEEPING) {
+        ti->status = Ready;
+    } else if (proc->state == ZOMBIE) {
+        ti->status = Exited;
+    } else if (proc->state == UNUSED) {
+        ti->status = UnInit;
+    }
+
+    ti->time = current_time - proc->start_time;
+    return 0;
+}
 
 extern char trap_page[];
 
@@ -51,8 +84,9 @@ void syscall()
     tracef("syscall %d args = [%x, %x, %x, %x, %x, %x]", id, args[0], args[1],
            args[2], args[3], args[4], args[5]);
     /*
-	* LAB1: you may need to update syscall counter for task info here
+	* LAB1: 更新系统调用次数
 	*/
+    curr_proc()->syscall_times[id]++;
     switch (id) {
     case SYS_write:
         ret = sys_write(args[0], (char *)args[1], args[2]);
@@ -67,8 +101,15 @@ void syscall()
         ret = sys_gettimeofday((TimeVal *)args[0], args[1]);
         break;
     /*
-	* LAB1: you may need to add SYS_taskinfo case here
+	* LAB1: 在此处添加 SYS_task_info 的系统调用处理情况
 	*/
+    case SYS_task_info:
+        ret = sys_task_info((TaskInfo *)args[0]);
+        break;
+    case SYS_getpid:
+        infof("SYS_getpid %d", SYS_getpid);
+        ret = curr_proc()->pid;
+        break;
     default:
         ret = -1;
         errorf("unknown syscall %d", id);
diff --git a/os/syscall_ids.h b/os/syscall_ids.h
index 05a6cb9..3c1a5a9 100644
--- a/os/syscall_ids.h
+++ b/os/syscall_ids.h
@@ -277,9 +277,8 @@
 #define SYS_io_pgetevents          292
 #define SYS_rseq                   293
 #define SYS_kexec_file_load        294
-/*
-* LAB1: you may need to define SYS_task_info here
-*/
+// LAB1:添加 SYS_task_info 的系统调用号
+#define SYS_task_info          410
 #define SYS_pidfd_send_signal  424
 #define SYS_io_uring_setup     425
 #define SYS_io_uring_enter     426
diff --git a/os/timer.h b/os/timer.h
index c6ebd14..63ab45c 100644
--- a/os/timer.h
+++ b/os/timer.h
@@ -6,6 +6,7 @@
 #define TICKS_PER_SEC (100)
 // QEMU
 #define CPU_FREQ                (12500000)
+#define MILLISECONDS_PER_SECOND (1000)
 #define MICROSECONDS_PER_SECOND (1000000)
 
 uint64 get_cycle();
@@ -14,6 +15,7 @@ void   set_next_timer();
 
 typedef struct {
     uint64 sec;  // 自 Unix 纪元起的秒数
+    uint64 msec; // 毫秒数
     uint64 usec; // 微秒数
 } TimeVal;
 
-- 
2.34.1

问答作业

问题一

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

测试前三个测试用例指的是uCore-Tutorial-Code-2023S/user/src/ 目录下的三个bad测试用例,查看user项目的 Makefile 可以发现在编译时修改CHAPTER参数值为2_bad即可编译运行这些测试用例。

[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..0x87000ef2
[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)
[TRACE 0]load app 0 at 0x0000000080400000
[TRACE 0]load app 1 at 0x0000000080420000
[TRACE 0]load app 2 at 0x0000000080440000
[INFO 0]start scheduler!
[ERROR 1]unknown trap: 0x0000000000000007, stval = 0x0000000000000000

[INFO 1]进程 1 以代码 -1 退出
IllegalInstruction in application, core dumped.
[INFO 2]进程 2 以代码 -3 退出
IllegalInstruction in application, core dumped.
[INFO 3]进程 3 以代码 -3 退出
[PANIC 3] os/loader.c:15: all apps over

第一个进程测试用例如下:

int main()
{
	int *p = (int *)0;
	*p = 0;
	return 0;
}

在您提供的代码中,将空指针分配给指针变量*p 后,试图对其进行解引用并将值 0 赋给该指针。由于用户模式下禁止直接访问物理内存,操作系统会检测到这个非法操作并触发异常。因此,该程序 IllegalInstruction in application, core dumped.

在 RISC-V 架构中,U 模式是最低的用户模式,用户程序无法直接访问物理内存或其他特权级别资源。这种限制是为了确保操作系统的安全性和稳定性。

第二个进程测试用例如下:

int main()
{
	asm volatile("sret");
	return 0;
}

试图使用汇编语言执行 sret 指令,该指令用于从中断或异常处理程序返回。由于用户模式下禁止直接访问特权级别寄存器,操作系统会检测到这个非法操作并触发异常。因此,该程序 IllegalInstruction in application, core dumped。

第三个进程测试用例如下:

int main()
{
	uint64 x;
	asm volatile("csrr %0, sstatus" : "=r"(x));
	return 0;
}

原因同上,试图使用汇编语言执行 csrr 指令,该指令用于从特权级别寄存器中读取值。由于用户模式下禁止直接访问特权级别寄存器,操作系统会检测到这个非法操作并触发异常。因此,该程序 IllegalInstruction in application, core dumped。

在操作系统代码中,触发异常后会进入void usertrap() 函数,该函数会根据 scause 寄存器的值判断异常类型,用例中的结果进入了case IllegalInstruction,其中 IllegalInstruction = 2。我们查阅手册 riscv-privileged.pdf ,可以查到 IllegalInstruction 的值为 2,与预期相符。

问题二

请结合用例理解 trampoline.S 中两个函数 userretuservec 的作用,并回答如下几个问题:

L79: 刚进入 userret 时,a0a1 分别代表了什么值。

在进入userret函数时,a0a1分别代表以下值:

  • a0: TRAPFRAME 的地址,指向当前进程的陷阱帧(trapframe)结构体。
  • a1: 用户页表的地址,即进程的页表(pagetable)。这个地址会被写入到satp寄存器中,用于切换到用户模式的页表。

L87-L88: sfence 指令有何作用?为什么要执行该指令,当前章节中,删掉该指令会导致错误吗?

csrw satp, a1
sfence.vma zero, zero

sfence指令(Store Fence)的作用是确保之前的存储操作完成,并且对其他处理器上的核心可见。

执行sfence指令的主要目的是为了保证内存访问的顺序性和一致性。在多核处理器系统中,不同的核心可能会有自己的缓存,当一个核心修改了共享内存中的数据时,为了保证其他核心能够看到这个修改,需要使用sfence指令来刷新缓存并将修改写回共享内存。

在代码中,sfence指令被用于确保对用户页表的修改对其他处理器上的核心可见。因为目前我只使用了单核处理器,所以不会出现多核处理器的情况,因此sfence指令的作用是确保对用户页表的修改对当前核心可见。

因此,当前章节中,删掉该指令不会导致错误

L96-L125: 为何注释中说要除去 a0?哪一个地址代表 a0?现在 a0 的值存在何处?

# restore all but a0 from TRAPFRAME
ld ra, 40(a0)
ld sp, 48(a0)
ld t5, 272(a0)
ld t6, 280(a0)

a0保存在 sscratch 寄存器中的,首先,该代码通过 ld 指令从 TRAPFRAME 中加载各个寄存器的值。然后,这些值被存储在相应的寄存器中,以便在恢复用户上下文时使用。

接下来,代码使用 csrrw 指令将 sscratch 寄存器的值与 a0(即 TRAPFRAME)进行交换。这样做是为了将用户的 a0TRAPFRAME)保存在 sscratch 寄存器中,以便后续步骤可以正确地恢复用户上下文。

最后,通过 sret 指令返回到用户模式,并将控制权交给用户代码。在执行 sret 指令后,处理器将根据用户上下文中的 sepc 寄存器的值跳转到用户代码的指令地址。返回的同时,处理器还会自动恢复 sstatus 寄存器的值,以确保正确的特权级别和中断状态。

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

userret函数中,发生状态切换的指令是sret指令。

sret指令用于从内核模式切换到用户模式,并将控制权交给用户代码。执行sret指令后,处理器会根据用户上下文中的sepc寄存器的值跳转到用户代码的指令地址。

执行sret指令之后进入用户态的原因是,该指令会自动恢复sstatus寄存器的值,以确保正确的特权级别和中断状态。当sret指令执行后,处理器将从内核态切换回用户态,程序将继续执行用户代码。这意味着userret函数成功完成了从内核切换到用户模式的过程。

L29:执行之后,a0 和 sscratch 中各是什么值,为什么?

csrrw a0, sscratch, a0     

在执行指令后,a0sscratch中的值发生了互换。

假设原始a0寄存器中的值为 X,而sscratch寄存器中的值为 Y。执行csrrw a0, sscratch, a0指令后,a0寄存器中的值变为 Y,而sscratch寄存器中的值变为 X。

这是因为csrrw指令是一个特权指令,用于将某个 CSR(Control and Status Register)的值读取到目标寄存器,然后将目标寄存器的值写回到该 CSR 中。在这里,csrrw a0, sscratch, a0指令将sscratch寄存器的值读取到a0寄存器中,同时将a0寄存器中的值写回到sscratch寄存器中,从而实现了两者之间的数据交换。

L32-L61: 从 trapframe 第几项开始保存?为什么?是否从该项开始保存了所有的值,如果不是,为什么?


sd ra, 40(a0)
sd sp, 48(a0)
...
sd t5, 272(a0)
sd t6, 280(a0)

进入 S 态是哪一条指令发生的?

L75-L76: ld t0, 16(a0) 执行之后,t0中的值是什么,解释该值的由来?

ld t0, 16(a0)
jr t0

ld t0, 16(a0)就是从 trapframe 中恢复 t0寄存器值,t0保存了kernel_trap的入口地址。使用 jr t0,就跳转到了我们早先设定在 trapframe->kernel_trap 中的地址,也就是 trap.c 之中的 usertrap 函数。这个函数在 main 的初始化之中已经调用了。