OSTEP: initial-xv6 lab

What sculpture is to a block of marble, education is to the soul.

开始 OSTEP 实验系列,通过向 xv6 添加一个系统调用

int getreadcount(void)

其返回计数器记录所有进程系统调用 read() 的次数。

通过下面命令调用编译与评分程序,

./test-getreadcounts.sh -s

在 Ubuntu 20.04 上实验,需要安装 expectgawk 才能顺利运行评分脚本。见 Issue

System Call Background

该实验主要是理解系统调用的细节,在 CSAPP 中统一将所有中断、陷阱、错误与中止称为异常(Exception),

Class Cause Async/sync Return behavior
Interrupt Signal from I/O device Async Always returns to next instruction
Trap Intentional exception Sync Always returns to next instruction
Fault Potentially recoverable error Sync Might return to current instruction
Abort Nonrecoverable error Sync Never returns

中断例如键盘中断、时钟中断等,陷阱是主动的系统调用,错误有例如 devide error / protection fault / page fault ,中止将会退出程序。其实可将以上都视作 Trap , 因为都是从用户态陷入内核态,但实际上实现时用的词汇可能并没有那么准确,比如实际上通过 int 指令(short for interrupt)接 exception number 进入内核态,xv6 在 traps.h 中定义 exception number 为宏等等。 CSAPP 是根据原因与行为来分类。以下为 x86-64 中的异常号与对应异常。

Exception number Description Exception class
0 Divide error Fault
13 General protection fault Fault
14 Page fault Fault
18 Machine check Abort
32–255 OS-defined exceptions Interrupt or trap

xv6 定义了宏 SYSCALL(name) 进行系统调用,

#define SYSCALL(name) \
  .globl name; \
  name: \
    movl $SYS_ ## name, %eax; \
    int $T_SYSCALL; \
    ret

File: usys.S

其中 T_SYSCALL 为 traps.h 中的宏,即对应系统调用的 Exception number, 而 %eax 寄存器保存 system call number 区分不同的系统调用。我们首先在 usys.Ssyscall.h 中增加 getreadcount 的 syscall number 和对应的汇编代码。

After Call int instruction

了解 Kernel Side 如何完成 IDT (就是Trap table 向量表,就是 Exception no 与其对应的代码)的初始化后,我们要知道当 application 使用 int 指令后, 系统与 OS 做了什么。

int 指令后,硬件接管改变当前优先级,并存部分上下文(即PC, regs, eflags, stack pointer等,定义在 trapframe 中,硬件与OS各存部分,见 x86.h 的 trapframe 注释),然后将控制交给 OS , 即根据 IDT 寻找到对应代码位置。在 xv6 中 IDT 指定的代码地址在 vectors.S (其由 vectors.pl 生成)。

.globl vector64
vector64:
  pushl $64
  jmp alltraps

File: vector.S

例如,System Call 是 64 号,对应的代码先压入 trapno (其实就是 Exception no) 进入 trapframe 然后 jmp alltraps ,而 alltraps 就是完成压入一些其他 segment registers / general registers 完成 trap frame 的建立,设置数据段(改变 segment selector) 然后调用 C trap handler

C trap handler

void
trap(struct trapframe *tf)
{
  if(tf->trapno == T_SYSCALL){
    if(myproc()->killed)
      exit();
    myproc()->tf = tf;
    syscall();
    if(myproc()->killed)
      exit();
    return;
  }
  ... // processing other trap nos
} 

File: trap.c

trap 函数即为 trap handler (这又是一个用词问题,其实这里处理的是所有 exception no 对应的异常),其本质上是一个 gateway 函数,根据 trapframe 中的 trapno 完成不同的操作。当 trapno 为 T_SYSCALL(64 号)时, trap handler 检查进程是否存活(离开 trap handler 时也要检查),为当前进程关闭中断(防止当从CPU读进程状态时,进程被重新调度)并改变当前进程的 trapframe (为了系统调用后恢复上下文),然后调用 syscall() 函数。

Which System Call ?

syscall 函数将已经保存的 trapframe 中取出 %eax (在 usys.S 保存的系统调用号) 并检查合法性,然后在 system call table syscalls[] 根据调用号寻找对应的函数代码。 syscalls 数组是一个函数指针数组,其函数指针指向 int(void) 函数,于是我们修改表的内容并增加 sys getreadcount 函数。

新增的代码逻辑就是新增一个 static 计数器变量并在 sys_read 函数中自增,在 sys getreadcount 函数中返回即可。单个CPU多进程是安全的,因为内核态进程不会被 reschedule 故无数据竞争;但考虑到多CPU时多进程,多个CPU上的进程同时进入内核态,这会带来数据竞争问题(OS内核也是程序),所以需要给这个计数器加 spinlock(其他多CPU共享的数据结构也有考虑数据竞争的问题,如 file table 等)并考虑初始化与带锁的写。代码见此

Conclusion

这个实验本质不难,主要是通过系统调用熟悉 xv6 内核代码,了解 exception control flow 的细节,并了解如何增加系统调用。

Reference

  1. Basic guidance
  2. Some issue
  3. Kernal background
  4. My solution code