You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

14 KiB

第三章实验2系统调用的实现

3.1 实验环境搭建

实验2需要在实验1的基础之上完成所以首先你需要切换到lab2_small的branch然后commit你的代码。

首先查看本地拥有的branch输入如下命令

$ git branch

如果有如下输出:

lab1_small
lab2_small
lab3_small
lab4_small
lab5_small

则你本地就有实验二的branch那么可以直接切换分支到lab2输入如下命令

$ git checkout lab2_small

当然如果本地没有lab2的相关代码也可以直接从远端拉取

$ git checkout -b lab2_small origin/ lab2_small

然后merge实验一中你的代码

$ git merge -m "merge lab1" lab1_small

完成一切后,我们就可以正式进入实验二了!

3.2 实验内容

实验要求:了解系统调用的执行过程,并实现一个自定义的系统调用。

3.2.1 练习一在app中使用系统调用

系统调用的英文名字是System Call用户通过系统调用来执行一些需要特权的任务那么我们具体在app中是如何使用内核所提供的系统调用的呢

RISC-V中提供了ecall指令用于向运行时环境发出请求我们可以使用内联汇编使用该指令进行系统调用代码如下

 1     #define ecall() ({\
 2      asm volatile(\
 3         "li x17,81\n"\
 4       "ecall");\
 5     })
 6
 7     int main(void){
 8         //调用自定义的81号系统调用
 9         ecall();
 10         return 0;
 11     }

例3.1 ecall

以上代码中我们将系统调用号81通过x17寄存器传给内核再通过ecall指令进行系统调用当然目前代理内核中并没有81号系统调用的实现这需要我们在后面的实验中完成。

3.2.2 练习二:系统调用过程跟踪

在我们执行了ecall指令后代理内核中又是如何执行的呢

在第一章的表1.7中我们可以看到Environment call from U-mode是exception(trap)的一种其对应的code是8。我们在实验二中已经讨论过中断入口函数位置的设置现在继续跟踪中断入口函数找出系统调用的执行过程。

3.2.3 练习三:自定义系统调用(需要编程)

阅读pk目录system.c文件增加一个系统调用sys_get_memsize()系统调用返回spike设置的内存空间大小, 系统调用号为81。

提升在pk目录下的mmap.c文件中函数pk_vm_init中定义了代理内核的内存空间大小。

spike 通过-m来指定分配的物理内存单位是MiB默认为2048。如

$ spike obj/pke app/elf/app2_1

得到的输出如下:

PKE IS RUNNING
physical mem_size = 0x80000000

可以看到默认情况下spike的物理内存是2GB

你可以修改-m选项运行app3观察输出。

$ spike -m1024 obj/pke app/elf/app2_1

预计得到输出格式:

PKE IS RUNNING
physical mem_size = 0x40000000

如果你的app可以正确输出的话那么运行检查的python脚本

$ ./pke-lab2

若得到如下输出,那么恭喜你,你已经成功完成了实验二!!!

build pk : OK
running app3 m2048 : OK
test3_m2048 : OK
running app3 m1024 : OK
test3_m1024 : OK
Score: 20/20

3.3 基础知识

3.3.1 系统调用

​首先,我们要知道什么是系统调用。

例如读文件(read)、写文件(write)等,其实我们已经接触过形形色色的系统调用。系统调用和函数调用的外形相似,但他们有着本质的不同。

系统调用的英文名字是System Call。由于用户进程只能在操作系统给它圈定好的“用户环境”中执行但“用户环境”限制了用户进程能够执行的指令即用户进程只能执行一般的指令无法执行特权指令。如果用户进程想执行一些需要特权指令的任务比如通过网卡发网络包等只能让操作系统来代劳了。系统调用就是用户模式下请求操作系统执行某些特权指令的任务的机制。

相交于函数调用在普通的用户模式下运行,系统调用则运行在内核模式中。

见下图:

fig3_1

图3.1系统调用过程

系统调用属于一种中断,当用户申请系统调用时,系统会从用户态陷入到内核态,完成相应的服务后,再回到原来的用户态上下文中。

3.3.2 代理内核与应用程序的加载

阅读pke.lds文件可以看到整个PK程序的入口为reset_vector函数

3   OUTPUT_ARCH( "riscv" )
4
5   ENTRY( reset_vector )

我们在machine/mentry.S中找的这个符号。

36   reset_vector:
37  j do_reset

首先初始化x0~x31共32个寄存器其中x10a0寄存器与x11a1寄存器存储着从之前boot loader中传来的参数而不复位。

223     do_reset:
224     li x1, 0
  .....
255     li x31, 0

将mscratch寄存器置0

256     csrw mscratch, x0

将trap_vector的地址写入t0寄存器trap_vector是mechine模式下异常处理的入口地址。再将t0的值写入mtvec寄存器中。然后读取mtvec寄存器中的地址到t1寄存器。比较t0于t1。

259     la t0, trap_vector
260     mtvec, t0
261   	rr t1, mtvec
262     1:bne t0, t1, 1b

正常情况下t1自然是的等于t0的于是程序顺序执行将栈地址写入sp寄存器中

264     la sp, stacks + RISCV_PGSIZE - MENTRY_FRAME_SIZE

读取mhartid到a3寄存器调整sp

266     csrr a3, mhartid
267     slli a2, a3, RISCV_PGSHIFT
268   add sp, sp, a2

当a3不等于0时跳转到 init_first_hart

270      # Boot on the first hart
271     beqz a3, init_first_hart

此时进入"machine/minit.c"文件在init_first_hart中对外设进行初始化

154      void init_first_hart(uintptr_t hartid, uintptr_t dtb)
155     {
         …… //初始化外设
180          boot_loader(dtb);
181     }

在init_first_hart的最后一行调用boot_loader函数

160 void boot_loader(uintptr_t dtb)
161 {
     …….   //CSR寄存器设置
169      enter_supervisor_mode(rest_of_boot_loader, pk_vm_init(), 0);
170  }

在boot_loader中经历设置中断入口地址清零sscratch寄存器关中断等一系列操作后。最后会调用enter_supervisor_mode函数正式切换至Supervisor模式。

204     void enter_supervisor_mode(void (*fn)(uintptr_t), uintptr_t arg0, uintptr_t arg1)
205   {
206         uintptr_t mstatus = read_csr(mstatus);
207      mstatus = INSERT_FIELD(mstatus, MSTATUS_MPP, PRV_S);
208      mstatus = INSERT_FIELD(mstatus, MSTATUS_MPIE, 0);
209         write_csr(mstatus, mstatus);
210      write_csr(mscratch, MACHINE_STACK_TOP() - MENTRY_FRAME_SIZE);
211       #ifndef __riscv_flen
212      uintptr_t *p_fcsr = MACHINE_STACK_TOP() - MENTRY_FRAME_SIZE; // the x0's save slot
213      *p_fcsr = 0;
214       #endif
215      write_csr(mepc, fn);
216
217         register uintptr_t a0 asm ("a0") = arg0;
218         register uintptr_t a1 asm ("a1") = arg1;
219       asm volatile ("mret" : : "r" (a0), "r" (a1));
220      __builtin_unreachable();
221     }

在enter_supervisor_mode函数中将 mstatus的MPP域设置为1表示中断发生之前的模式是Superior将mstatus的MPIE域设置为0表示中段发生前MIE的值为0。随机将机器模式的内核栈顶写入mscratch寄存器中设置mepc为rest_of_boot_loader的地址并将kernel_stack_top与0作为参数存入a0和a1。

最后执行mret指令该指令执行时程序从机器模式的异常返回将程序计数器pc设置为mepc即rest_of_boot_loader的地址将特权级设置为mstatus寄存器的MPP域即方才所设置的代表Superior的1MPP设置为0将mstatus寄存器的MIE域设置为MPIE即方才所设置的表示中断关闭的0MPIE设置为1。

于是当mret指令执行完毕程序将从rest_of_boot_loader继续执行。

144     static void rest_of_boot_loader(uintptr_t kstack_top)
145     {
146      arg_buf args;
147      size_t argc = parse_args(&args);
148      if (!argc)
149       panic("tell me what ELF to load!");
150
151      // load program named by argv[0]
152      long phdrs[128];
153      current.phdr = (uintptr_t)phdrs;
154      current.phdr_size = sizeof(phdrs);
155      load_elf(args.argv[0], &current);
156
157      run_loaded_program(argc, args.argv, kstack_top);
158     }

这个函数中我们对应用程序的ELF文件进行解析并且最终运行应用程序。

3.3.3 中断的处理过程

考虑一下当程序执行到中断之前程序是有自己的运行状态的例如寄存器里保持的上下文数据。当中断发生硬件在自动设置完中断原因和中断地址后就会调转到中断处理程序而中断处理程序同样会使用寄存器于是当程序从中断处理程序返回时需要保存需要被调用者保存的寄存器我们称之为callee-saved寄存器。

在PK的machine/minit.c中间中便通过delegate_traps()将部分中断及同步异常委托给S模式。同学们可以查看具体是哪些中断及同步异常

 43     // send S-mode interrupts and most exceptions straight to S-mode
 44     static void delegate_traps()
 45     {
 46      if (!supports_extension('S'))
 47       return;
 48
 49      uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP;
 50      uintptr_t exceptions =
 51       (1U << CAUSE_MISALIGNED_FETCH) |
 52       (1U << CAUSE_FETCH_PAGE_FAULT) |
 53       (1U << CAUSE_BREAKPOINT) |
 54       (1U << CAUSE_LOAD_PAGE_FAULT) |
 55       (1U << CAUSE_STORE_PAGE_FAULT) |
 56       (1U << CAUSE_USER_ECALL);
 57
 58      write_csr(mideleg, interrupts);
 59      write_csr(medeleg, exceptions);
 60      assert(read_csr(mideleg) == interrupts);
 61      assert(read_csr(medeleg) == exceptions);
 62     }

这里介绍一下RISCV的中断委托机制在默认的情况下所有的异常都会被交由机器模式处理。但正如我们知道的那样大部分的系统调用都是在S模式下处理的因此RISCV提供了这一委托机制可以选择性的将中断交由S模式处理从而完全绕过M模式。

接下我们继续看S模式下的中断处理。在pk目录下的pk.c文件中的boot_loader函数中将&trap_entry写入了stvec寄存器中stvec保存着发生异常时处理器需要跳转到的地址也就是说当中断发生我们将跳转至trap_entry现在我们继续跟踪trap_entry。trap_entry在pk目录下的entry.S中其代码如下

 60     trap_entry:
 61      csrrw sp, sscratch, sp
 62      bnez sp, 1f
 63      csrr sp, sscratch
 64     1:addi sp,sp,-320
 65      save_tf
 66      move a0,sp
 67      jal handle_trap

在61行交换了sp与sscratch的值这里是为了根据sscratch的值判断该中断是来源于U模式还是S模式。

如果sp也就是传入的sscratch值不为零则跳转至64行若sscratch的值为零则恢复原sp中的值。这是因为当中断来源于S模式是sscratch的值为0sp中存储的就是内核的堆栈地址。而当中断来源于U模式时sp中存储的是用户的堆栈地址sscratch中存储的则是内核的堆栈地址需要交换二者是sp指向内核的堆栈地址。

接着在64,65行保存上下文最后跳转至67行处理trap。handle_trap在pk目录下的handlers.c文件中代码如下

112     void handle_trap(trapframe_t* tf)
113     {
114      if ((intptr_t)tf->cause < 0)
115       return handle_interrupt(tf);
116
117      typedef void (*trap_handler)(trapframe_t*);
118
119      const static trap_handler trap_handlers[] = {
120       [CAUSE_MISALIGNED_FETCH] = handle_misaligned_fetch,
121       [CAUSE_FETCH_ACCESS] = handle_instruction_access_fault,
122       [CAUSE_LOAD_ACCESS] = handle_load_access_fault,
123       [CAUSE_STORE_ACCESS] = handle_store_access_fault,
124       [CAUSE_FETCH_PAGE_FAULT] = handle_fault_fetch,
125       [CAUSE_ILLEGAL_INSTRUCTION] = handle_illegal_instruction,
126       [CAUSE_USER_ECALL] = handle_syscall,
127       [CAUSE_BREAKPOINT] = handle_breakpoint,
128       [CAUSE_MISALIGNED_LOAD] = handle_misaligned_load,
129       [CAUSE_MISALIGNED_STORE] = handle_misaligned_store,
130       [CAUSE_LOAD_PAGE_FAULT] = handle_fault_load,
131       [CAUSE_STORE_PAGE_FAULT] = handle_fault_store,
132      };

handle_trap函数中实现了S模式下各类中断的处理。可以看到代码的126行就对应着系统调用的处理handle_syscall的实现如下

100     static void handle_syscall(trapframe_t* tf)
101     {
102      tf->gpr[10] = do_syscall(tf->gpr[10], tf->gpr[11], tf->gpr[12], tf->gpr[13],
103                  tf->gpr[14], tf->gpr[15], tf->gpr[17]);
104      tf->epc += 4;
105     }

还记得我们在例3.1中是将中断号写入x17寄存器嘛其对应的就是这里do_syscall的最后一个参数我们跟踪进入do_syscall函数其代码如下

313 long do_syscall(long a0, long a1, long a2, long a3, long a4, long a5, unsigned long n)
314     {
315      const static void* syscall_table[] = {
316      // your code here:
317      // add get_init_memsize syscall
318       [SYS_init_memsize ] = sys_get_init_memsize,
319       [SYS_exit] = sys_exit,
320       [SYS_exit_group] = sys_exit,
321       [SYS_read] = sys_read,
322       [SYS_pread] = sys_pread,
323       [SYS_write] = sys_write,
324       [SYS_openat] = sys_openat,
325       [SYS_close] = sys_close,
326       [SYS_fstat] = sys_fstat,
327       [SYS_lseek] = sys_lseek,
328       [SYS_renameat] = sys_renameat,
329       [SYS_mkdirat] = sys_mkdirat,
330       [SYS_getcwd] = sys_getcwd,
331       [SYS_brk] = sys_brk,
332       [SYS_uname] = sys_uname,
333       [SYS_prlimit64] = sys_stub_nosys,
334       [SYS_rt_sigaction] = sys_rt_sigaction,
335       [SYS_times] = sys_times,
336       [SYS_writev] = sys_writev,
337       [SYS_readlinkat] = sys_stub_nosys,
338       [SYS_rt_sigprocmask] = sys_stub_success,
339       [SYS_ioctl] = sys_stub_nosys,
340       [SYS_getrusage] = sys_stub_nosys,
341       [SYS_getrlimit] = sys_stub_nosys,
342       [SYS_setrlimit] = sys_stub_nosys,
343       [SYS_set_tid_address] = sys_stub_nosys,
344       [SYS_set_robust_list] = sys_stub_nosys,
345      };
346
347      syscall_t f = 0;
348
349      if (n < ARRAY_SIZE(syscall_table))
350       f = syscall_table[n];
351      if (!f)
352       panic("bad syscall #%ld!",n);
353
354      return f(a0, a1, a2, a3, a4, a5, n);
355     }

do_syscall中通过传入的系统调用号n查询syscall_table得到对应的函数并最终执行系统调用。