用户程序启动的流程

user.lds用户程序的链接脚本:加载 .text, .data , .bss 至 0x00400000,设定用户程序的入口为 _start

entry.S: user\lib,用户程序main函数运行时需要两个参数,跳转至libmain

libmain: libos.c,设置 env ,使得用户可以访问到当前进程的进程控制块;跳转至 main 主函数;exit 系统调用结束当前进程的运行。

系统调用 - 8号异常

系统调用过程

1. 用户程序使用 syscall_* 接口

  1. 用户程序调用 syscall_* 函数接口。
  2. syscall_*将实现系统调用所需的参数作为 msyscall 的传入参数,在调用 msyscall 时,会自动将参数(第一个参数为系统调用号)存入寄存器和用户栈(内核后续可以访问这些参数)。此处的自动理解为存储过程编译器会自动编译为汇编代码,我们只需要在内核中显示地从保存的用户上下文中获取函数的参数
  3. msyscall 中 执行 syscall 指令使 CPU 陷入内核态完成系统调用;在返回用户态继续运行此函数时,执行 jr ra 指令返回到 syscall_* 函数。
LEAF(msyscall)
// 系统由此陷入内核态
syscall
jr ra
END(msyscall)

2. 异常入口与分发

用户态下执行 syscall 指令会触发异常,CPU 陷入内核态,从 0x80000080 (即 .exc_gen_entry所在位置) 开始取指令。

.section .text.exc_gen_entry
exc_gen_entry:
// 将用户进程的上下文运行环境保存在内核栈中
SAVE_ALL
// 取出 CP0_CAUSE 中的异常码
mfc0 t0, CP0_CAUSE
andi t0, 0x7c
// 以异常码8为索引在 exception_handlers 数组中倒找对应的异常处理函数 handle_sys
lw t0, exception_handlers(t0)
// 跳转至相应异常处理函数处理用户的系统调用请求
jr t0

3. 内核处理系统调用请求

handle_sys核心逻辑位于 do_syscall 函数。当内核处理完系统调用请求后,调用 ret_from_exception 从内核态返回用户程序。

kern/genex.S 中用 BUILD_HANDLER 宏封装了 handle_sys 函数。

.macro BUILD_HANDLER exception handler
NESTED(handle_\exception, TF_SIZE + 8, zero)
move a0, sp // 作为 do_syscall 的传入参数
addiu sp, sp, -8
jal \handler
addiu sp, sp, 8
j ret_from_exception
END(handle_\exception)
.endm

.text

FEXPORT(ret_from_exception)
RESTORE_SOME
lw k0, TF_EPC(sp)
lw sp, TF_REG29(sp) /* Deallocate stack */
.set noreorder
jr k0
rfe /*在上一条jr指令的延迟槽中执行,这样可以保证原子性*/
.set reorder

NESTED(handle_int, TF_SIZE, zero)
mfc0 t0, CP0_CAUSE
mfc0 t2, CP0_STATUS
and t0, t2
andi t1, t0, STATUS_IM4
bnez t1, timer_irq
// TODO: handle other irqs
timer_irq:
sw zero, (KSEG1 | DEV_RTC_ADDRESS | DEV_RTC_INTERRUPT_ACK)
li a0, 0
j schedule
END(handle_int)

BUILD_HANDLER tlb do_tlb_refill

#if !defined(LAB) || LAB >= 4
BUILD_HANDLER mod do_tlb_mod
BUILD_HANDLER sys do_syscall
#endif

BUILD_HANDLER reserved do_reserved

C预处理器中的宏替换语法

#define macro_name(parameter_list) replacement_text

在这个代码片段中,BUILD_HANDLER 宏具有两个参数,exceptionhandler。它会将 \handler 替换为传递给宏的第二个参数,即对应的异常处理程序的名称。在代码中使用宏时,只需在宏名称后面加上括号,并在括号中传递参数即可。\ 被用作宏替换文本中的转义符。当宏替换文本中需要使用 \ 字符时,需要在它前面加上 \ 转义符,以确保在宏替换时 \ 能够正确地被处理。

内核获取用户传递的参数

do_syscall 函数在处理系统调用请求时需要获得由用户进程传递的参数。

用户进程在调用 msyscall 时,已将这些参数存入 a0 , a1 , a2 , a3 寄存器和用户栈中。内核处理系统调用请求时,CPU处于内核态,通用寄存器等现场环境发生了变化,堆栈指针也已指向了内核栈。借助内核栈中保存的用户进程上下文环境。观察 SAVE_ALL ,在保护用户态现场时 sp 减去了一个 Trapframe 结构体的空间大小,此时我们将用户进程现场保存在内核栈中范围为 [sp, sp + sizeof(TrapFrame)) 的这一空间范围内。

void do_syscall(struct Trapframe *tf) {
// tf:内核栈保存的用户进程现场
int (*func)(u_int, u_int, u_int, u_int, u_int); // 系统调用内核函数指针
int sysno = tf->regs[4]; // a0 系统调用号
if (sysno < 0 || sysno >= MAX_SYSNO) {
tf->regs[2] = -E_NO_SYS;
return;
}
// 内核完成系统调用返回用户态时,将从内核栈保存到 `Trapframe` 结构体中恢复进程现场。
// epc 修改前存放 msyscall 函数中 syscall 指令地址,修改后指向 msyscall 函数中 jr ra 指令
tf->cp0_epc += 4;
// 系统调用函数在 syscall_table 数组中以系统调用号作为索引存储
func = syscall_table[sysno];
u_int arg1 = tf->regs[5];
u_int arg2 = tf->regs[6];
u_int arg3 = tf->regs[7];
u_int arg4, arg5;
arg4 = *(u_int *)(tf->regs[29] + 16);
arg5 = *(u_int *)(tf->regs[29] + 20);
tf->regs[2] = func(arg1, arg2, arg3, arg4, arg5); // 函数返回值写入 v0
}

在所有内核的syscall实现中,如果想修改用户进程从 syscall 返回后的用户现场,都需要修改内核栈中的 Trapframe 实现,而不是直接修改进程控制块的tf。

image-20230423092019674

系统调用功能实现

在内核处理进程发起的系统调用时,我们并没有切换地址空间(页目录地址),也不需要将进程上下文(Trapframe)保存到进程控制块中,只是切换到内核态下,执行了一些内核代码。

envid2env 函数

int envid2env(u_int envid, struct Env **penv, int checkperm) {
struct Env *e;
if (!envid) {
*penv = curenv;
return 0;
}
else
e = &envs[ENVX(envid)];
if (e->env_status == ENV_FREE || e->env_id != envid)
{
return -E_BAD_ENV;
}
// 当 checkperm 为 1 时,e 必须满足 e 是当前进程 或 是当前进程的子进程。
if (checkperm && e != curenv && e->env_parent_id != curenv->env_id) {
return -E_BAD_ENV;
}
*penv = e;
return 0;
}

envid2env 函数中,如果 checkperm 不为零,那么函数需要检查调用者是否有足够的权限来操作指定的进程(e)。具体来说,如果 e 不是当前进程(curenv)或当前进程的直接子进程,那么就认为权限不足,返回 -E_BAD_ENV 错误。

进程间通信(IPC)

由于进程的地址空间之间是相互独立的,要想传递数据,我们就需要想办法把一个地址空间中的东西传给 另一个地址空间。因为所有的进程共享内核空间(主要为 kseg0),我们可以借助内核空间来实现不同用户地址空间的数据交换。发送方进程使用系统调用将传递的数据存放在进程控制块中,接收方进程同样使用 系统调用在进程控制块中找到对应的数据,读取并返回。

进程创建FORK

image-20230423111257874

写时复制机制

在 fork 时,我们只需将地址空间中的所有可写页标记为写时复制页面(PTE_COW = 1 && PTE_D = 0),使得在父进程或子进程对写时复制页面进行写入时,会触发 TLB Mod 页写入异常。操作系统在异常处理时,为当前进程试图写入的虚拟地址分配新的物理页面,并复制原页面的内容,最后再返回用户程序,对新分配的物理页面进行写入。

页写入异常流程:

当用户程序写入一个在 TLB 中被标记为不可写入(无 PTE_D)的页面时,MIPS 会陷入页写入异常(TLB Mod)。

  1. 用户进程触发页写入异常,陷入到内核中的 handle_mod,再跳转到 do_tlb_mod 函数。
  2. do_tlb_mod 函数负责将当前现场保存在异常处理栈中,并设置 a0 和 EPC 寄存器的值,使得从异常恢复后能够以异常处理栈中保存的现场(Trapframe)为参数,跳转到env_user_tlb_mod_entry 域存储的用户异常处理函数的地址。
  3. 从异常恢复到用户态,跳转到用户异常处理函数 cow_entry 中,由用户程序完成写时复制等自定义处理。
void do_tlb_mod(struct Trapframe *tf) {
struct Trapframe tmp_tf = *tf;
// 将栈指针指向异常处理栈
if (tf->regs[29] < USTACKTOP || tf->regs[29] >= UXSTACKTOP) {
tf->regs[29] = UXSTACKTOP; // UXSTACKTOP:异常处理栈
}
// 将当前现场保存在异常处理栈中
tf->regs[29] -= sizeof(struct Trapframe);
*(struct Trapframe *)tf->regs[29] = tmp_tf;

if (curenv->env_user_tlb_mod_entry) {
tf->regs[4] = tf->regs[29];
tf->regs[29] -= sizeof(tf->regs[4]);
// 从异常恢复到用户态,跳转到用户异常处理函数中,由用户程序完成写时复制等自定义处理。
tf->cp0_epc = curenv->env_user_tlb_mod_entry;
}
else {
panic("TLB Mod but no user handler registered");
}
}
// 用户态下
static void __attribute__((noreturn)) cow_entry(struct Trapframe *tf) {
// 获取触发 TLB Mod 的页面虚拟地址
u_int va = tf->cp0_badvaddr;
u_int perm;
Pte pte = vpt[VPN(va)];
perm = pte & 0xfff;
// 检查 va 对应的页是否为写时复制页面
if (!(perm & PTE_COW)) {
user_panic("cow_entry: 'va' is not a copy-on-write page.");
}
perm = ((perm & ~PTE_COW) | PTE_D);
struct Page *pp;
// 分配一页物理页,暂时映射到虚拟地址 UCOW,更新权限
panic_on(syscall_mem_alloc(0, (void *)UCOW, perm));
// 复制页面内容到新页面
memcpy((void *)UCOW, (void *)ROUNDDOWN(va, BY2PG), BY2PG);
// 将新页面映射到虚拟地址 va
panic_on(syscall_mem_map(0, (void *)UCOW, 0, va, perm));
// 把虚拟地址 UCOW 的临时映射取消
panic_on(syscall_mem_unmap(0, (void *)UCOW));
//还原当前进程的上下文,恢复 TLB Mod 异常前的状态
int r = syscall_set_trapframe(0, tf);
user_panic("syscall_set_trapframe returned %d", r);
}
int fork(void) {
u_int child;
u_int i;
extern volatile struct Env *env;
// 父进程为自己注册 TLB Mod 异常处理函数
if (env->env_user_tlb_mod_entry != (u_int)cow_entry) {
try(syscall_set_tlb_mod_entry(0, cow_entry));
}
// 创建子进程(暂时为 ENV_NOT_RUNNABLE 状态)
// 父进程记录下子进程的 envid;子进程为自己的 env 变量赋值(libos.c)并返回 0
child = syscall_exofork();
if (child == 0) {
// 子进程,更新当前进程标识符并返回
env = envs + ENVX(syscall_getenvid());
return 0;
}
// 父进程使用 duppage 将 USTACKTOP 以下的页面映射给子进程
for (i = 0; i < USTACKTOP; i += BY2PG) {
if ((vpd[i>>PDSHIFT] & PTE_V) && (vpt[i>>PGSHIFT] & PTE_V))
{
duppage(child, VPN(i));
}
}
// 父进程为子进程注册 TLB Mod 异常处理函数 cow_entry(syscall_set_tlb_mod_entry)
try(syscall_set_tlb_mod_entry(child, cow_entry));
// 父进程将子进程的状态设置为 ENV_RUNNABLE,使子进程可以被调度开始运行(syscall_set_env_status)
try(syscall_set_env_status(child, ENV_RUNNABLE));
// 父进程返回子进程的 envid
return child;
}
int sys_set_trapframe(u_int envid, struct Trapframe *tf) {
// 检查 tf 是否处在合法的用户地址空间
if (is_illegal_va_range((u_long)tf, sizeof *tf)) {
return -E_INVAL;
}
// 获取目标进程的进程控制块
struct Env *env;
try(envid2env(envid, &env, 1));

// 修改当前正在运行的进程的 Trapframe,这通常发生在系统调用的处理函数中,例如在 do_syscall函数中,会调用 sys_set_trapframe 修改当前进程的 Trapframe,以便在系统调用返回时使用正确的上下文。
if (env == curenv) {
// 使用tf指向的Trapframe替换系统调用时的Trapframe
*((struct Trapframe *)KSTACKTOP - 1) = *tf;
// 返回 tf->regs[2] 而不是返回 0,因为 do_syscall 中我们会用返回值覆盖掉现场中的 regs[2]
return tf->regs[2];
}
// 修改另一个进程的 Trapframe,这通常发生在进程切换的时候,例如在 sched_yield 函数中,会调用 sys_set_trapframe 修改目标进程的 Trapframe,以便在切换回该进程时能够正确地恢复该进程的上下文。
/*此处存疑!!!*/
else {
// 使用 tf 指向的 Trapframe 替换该进程控制块中存储的 Trapframe
env->env_tf = *tf;
return 0;
}
}
int sys_set_tlb_mod_entry(u_int envid, u_int func) {
struct Env *env;
// 通过进程的 envid 获取对应的进程控制块
try(envid2env(envid, &env, 1));
// 在进程控制块中设置 TLB Mod 异常处理函数为 func
env->env_user_tlb_mod_entry = func;
return 0;
}
int sys_exofork(void) {
struct Env *e;
// 创建子进程,并在新创建的进程控制块 e 中记录父进程的 envid
panic_on(env_alloc(&e, curenv->env_id));
// 设置子进程的 Trapframe 为父进程系统调用时的 Trapframe
// 父进程系统调用时的上下文存储在内核栈中,内核栈此时只有父进程的 Trapframe
e->env_tf = *((struct Trapframe *)KSTACKTOP - 1);
// 将子进程的 Trapframe 中的 v0 寄存器值设为 0(如同子进程返回 0)
e->env_tf.regs[2] = 0;
// 设置子进程为不可运行状态(ENV_NOT_RUNNABLE)
e->env_status = ENV_NOT_RUNNABLE;
// 设置子进程的优先级与当前进程相同
e->env_pri = curenv->env_pri;
return e->env_id;
}
static void duppage(u_int envid, u_int vpn) {
int r;
u_int addr;
u_int perm;
// 通过 vpn 获取对应虚拟页的虚拟地址 addr
addr = vpn << PGSHIFT;
// 获取父进程对应虚拟页的权限
Pte pte = vpt[vpn];
perm = pte & 0xfff; // 取出低12位
// 根据父进程对应虚拟页的权限,进行不同的映射操作
// 若页面不可写、或为共享页面时,不需要进行写时复制保护,保持原有权限映射给子进程
// 若页面已经为写时复制页面,保持原有权限映射给子进程
// 其它情况下,需要对页面加上写时复制保护(PTE_COW)、去掉可写权限(PTE_D)并映射给子进程
if ((perm & PTE_D) && !(perm & PTE_LIBRARY) && !(perm & PTE_COW)) {
panic_on(syscall_mem_map(0, addr, envid, addr, (perm & ~PTE_D) | PTE_COW));
panic_on(syscall_mem_map(0, addr, 0, addr, (perm & ~PTE_D) | PTE_COW));
} else {
panic_on(syscall_mem_map(0, addr, envid, addr, perm));
}
}
int sys_set_env_status(u_int envid, u_int status) {
struct Env *env;
// 若 status 不为 ENV_RUNNABLE 或 ENV_NOT_RUNNABLE 则返回 -E_INVAL
if (status != ENV_RUNNABLE && status != ENV_NOT_RUNNABLE) {
return - E_INVAL;
}
// 使用 envid 获取目标的进程的进程控制块
try(envid2env(envid, &env, 1));
// 根据进程状态的切换,维护 env_sched_list
// 若进程从 RUNNABLE 变为 NOT_RUNNABLE 则需要从 env_sched_list 移除
// 反之则需要添加进 env_sched_list 的尾部
if (env->env_status == ENV_RUNNABLE && status == ENV_NOT_RUNNABLE) {
TAILQ_REMOVE(&env_sched_list, env, env_sched_link);
} else if (env->env_status == ENV_NOT_RUNNABLE && status == ENV_RUNNABLE) {
TAILQ_INSERT_HEAD(&env_sched_list, env, env_sched_link);
}
env->env_status = status;
return 0;
}

思考题

1. 系统调用的实现

  1. 内核在保存现场的时候是如何避免破坏通用寄存器的?

    • stackframe.h 定义了 SAVE_ALLRESTORE_SOME 宏用于保存和恢复CPU寄存器。在 SAVE_ALL 宏中,首先用 k0 暂存栈指针(k0,k1仅内核态下使用,允许异常处理程序使用k0且无需保存或恢复其值),然后对 sp 操作确定保存用户上下文环境的内核栈空间。先将之前暂存的 k0 存入,再依次将 CP0 寄存器、HI/LO寄存器和通用寄存器存入。
  2. 系统陷入内核调用后可以直接从当时的 a0-a3 参数寄存器中得到用户调用 msyscall留下的信息吗?

    • 不可以,系统陷入内核态后,进行上下文环境切换,上述值应该从内核栈中获取。
  3. 我们是怎么做到让 sys 开头的函数“认为”我们提供了和用户调用 msyscall 时同样的参数的?

    • 根据 MIPS 调用规范,用户进程再调用 msyscall 时,已将这些参数存入 a0 - a3 寄存器和用户栈中。由于内核处理系统调用请求时,CPU处于内核态,需要从内核栈中保存的用户进程上下文环境获取这些参数。
  4. 内核处理系统调用的过程对 Trapframe 做了哪些更改?这种修改对应的用户态的变化是什么?

    • cp0_epc + 4, epc 修改前存放 msyscall 函数中 syscall 指令地址,修改后指向 msyscall 函数中 jr ra 指令。将func函数返回值写入 v0 寄存器,使得在恢复用户进程上下文环境后,用户程序可以从该寄存器读取 msyscall 的返回值。

2. envid2env 的实现

思考 envid2env 函数: 为什么 envid2env 中需要判断 e->env_id != envid的情况?如果没有这步判断会发生什么情况?

观察生成新进程的 env_id 的函数:

#define LOG2NENV 10
#define NENV (1 << LOG2NENV)
#define ENVX(envid) ((envid) & (NENV - 1))
u_int mkenvid(struct Env *e) {
static u_int i = 0;
return ((++i) << (1 + LOG2NENV)) | (e - envs);
}

envid != 0 时,通过 e = &envs[ENVX(envid)] 获取当前进程,ENVX 宏取 envid 的低10位,没有对高位进行判断。当查询的 envid 为错误的进程 id,比如 envid = 0xc00,而 envs[0].env_id = 0x400,如果没有这个判断,*penv = &envs[0] ,这显然是不正确的。

3. mkenvid 函数细节

请回顾 kern/env.c 文件中 mkenvid() 函数的实现,该函数不会返回 0,请结合系统调用和 IPC 部分的实现与envid2env() 函数的行为进行解释。

u_int mkenvid(struct Env *e) {
static u_int i = 0;
return ((++i) << (1 + LOG2NENV)) | (e - envs);
}

在系统调用和 IPC部分的实现中,常常需要使用 envid2env 函数来查找进程标识符,而在 envid2env 函数中我们将 envid==0 的含义定义为返回当前进程。观察函数即可发现其通过 ((++i) << (1 + LOG2NENV)) 保证该函数不会返回0,避免冲突。

4. fork的返回结果

关于 fork 函数的两个返回值,下面说法正确的是:

A、fork 在父进程中被调用两次,产生两个返回值

B、fork 在两个进程中分别被调用一次,产生两个不同的返回值

C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值

D、fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值

C

5. 用户空间的保护

我们并不应该对所有的用户空间页都使用 duppage 进行映射。那么究竟哪些用户空间页应该映射,哪些不应该呢?请结合 kern/env.c 中 env_init 函数进行的页面映射、include/mmu.h 里的内存布局图以及本章的后续描述进行思考。

不同权限位的页使用情况处理:

  • 只读页面:对于不具有 PTE_D 权限位的页面。按照相同权限(只读)映射给子进程即可。
  • 写时复制页面:即具有 PTE_COW 权限位的页面。这类页面是之前的 fork 时 duppage 的结果,且在本次 fork 前必然未被写入过。按照相同权限(写时复制)映射给子进程即可。
  • 共享页面:即具有 PTE_LIBRARY 权限位的页面。这类页面需要保持共享可写的状态,即在父子进程中映射到相同的物理页,使对其进行修改的结果相互可见。按照相同权限(共享)映射给子进程即可。
  • 可写页面:即具有 PTE_D 权限位,且不符合以上特殊情况的页面。这类页面需要在父进程和子进程的页表项中都使用 PTE_COW 权限位进行保护。

6. vpt 和 vpd 的使用

  1. vpt 和 vpd 的作用是什么?怎样使用它们?

    #define vpt ((volatile Pte *)UVPT) // 指向当前进程页表的指针
    #define vpd ((volatile Pde *)(UVPT + (PDX(UVPT) << PGSHIFT))) // 指向当前进程页目录表的指针
    //volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据
    Pde pde = vpd[va >> PDSHIFT];
    Pte pte = vpt[va >> PGSHIFT];
    // 通过上述操作访问相应的页目录项和页表项
  2. 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?

    它们是如何体现自映射设计的?

    观察 env.c 中的 env_setup_vm 函数:

    static int env_setup_vm(struct Env *e) {
    struct Page *p;
    try(page_alloc(&p)); // 页目录
    p->pp_ref ++;
    e->env_pgdir = (Pde *)page2kva(p);
    // 将 base_pgdir页目录表中从UTOP 到 UVPT 的页目录项拷贝到进程页目录表中的相同位置,那么用户程序就可以读取到内核的 pages 和 envs 数据。
    memcpy(e->env_pgdir + PDX(UTOP), base_pgdir + PDX(UTOP),
    sizeof(Pde) * (PDX(UVPT) - PDX(UTOP)));
    // 将进程页目录表中 UVPT 所对应的页目录项设置成一个自映射的页表,将自身映射到UVPT所在的虚拟地址上。(其实我还是不理解这个自映射)
    e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V;
    return 0;
    }
  3. 进程能够通过这种方式来修改自己的页表项吗?

​ 不可以。观察 env.c/env_setup_vm 用户空间页表页发现只有可读权限。并且实践也说明不行。

7. 页写入异常 - 内核处理

在 do_tlb_mod 函数中,你可能注意到了一个向异常处理栈复制 Trapframe运行现场的过程,请思考并回答这几个问题:

• 这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重入”?

• 内核为什么需要将异常的现场 Trapframe 复制到用户空间?

  • 异常重入是指一个异常在处理过程中,又触发了新的异常。此处,在处理写时复制的页写入异常时,又发生了缺页,就需要异常重入。
  • 因为页写入异常的处理在用户态下进行。用户进程需要读取 Trapframe 的值获得哪一条指令触发了异常,并且需要 Trapframe 恢复现场。

8. 页写入异常 - 内核处理1

在用户态处理页写入异常,相比于在内核态处理有什么优势?

  • 提高系统的可靠性和稳定性。在用户态处理页写入异常时,如果出现问题,只会导致当前进程崩溃;而如果交给内核态处理,出现问题会导致整个系统崩溃,另外内核态能访问到关键数据,容易产生安全问题。
  • 提高系统的性能。由于内核态的系统调用涉及上下文切换,会消耗一定的时间和资源。

9. 页写入异常 - 内核处理2

为什么需要将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前?

我认为放在 syscall_exofork 可以,不会有影响。

如果放置在写时复制保护机制完成之后会有怎样的效果?

duppage 函数会调用 syscall_mem_map 实现系统调用,其中调用的 msyscall 会将参数存入寄存器和用户栈。而用户栈可能也会被设置 PTE_COW 位,此时若未设置页写入异常处理函数,会无法处理页写入异常。

debug

make objdump
# 通过 epc 寻找问题发生的地方
# cause 和 badva 观察异常类型
make dbg
trace # 打开函数调用关系的输出
c
make dbg > temp # 若输出过长,然后在 temp内操作
# 用户态调试类似

课上

lab4-2 的 extra 没通过,感觉用惯了vscode,在vim上写有点慢,在vim上也不方便直接复制,下次想直接在gitlab修改,感觉会更方便,lab4-2的码量还是有的,再加上不断切换文件,速度有点慢,然后有点急,就容易读题不仔细,其中一个bug就是 sem_init 返回分配的 id,一开始没看到。另外当前进程使用某个信号量是否合法,需要判断该进程是否为创建该信号量的进程或其后代进程,由于父进程可能会丢失,直接dfs会有问题,建议并查集做。