计算机中的CPU往往被硬件所包裹,这些硬件通常以I/O接口的形式存在。xv6所基于的硬件是由qemu的“-machine virt”模拟的,包括RAM、ROM(有boot code)、与用户键盘和屏幕的串行连接、存储磁盘。
RISC-V有三种模式:machine mode、supervisor mode、user mode。machine mode常用于配置计算机,开机后先进入machine mode,然后xv6执行该模式下的一些语句后进入supervisor mode。
在supervisor mode下,CPU可执行特权指令,如开关中断、读写页表基址寄存器等。应用程序只能运行用户指令,且只能运行在用户空间;而supervisor mode下的软件可运行特权指令,且可运行在内核空间。
CPU提供一种特殊指令,可将CPU从user mode转到supervisor mode,并进入内核空间。接着内核检查系统调用的参数,决定该应用程序是否被允许执行其请求的操作,然后拒绝或执行
两种内核,monolithic kernel宏内核、microkernel微内核,区别见下表3、4行:
从本书角度,不管是宏内核还是微内核,都实现了系统调用、使用页表、处理中断、支持进程、用锁来实现并发控制、实现文件系统等。本书也只关注这些核心理念。
模块间的接口定义在kernel/defs.h
中
内核实现彼此隔离的进程的机制有:user/supervisor模式标志、地址空间、线程时间片。
xv6为每个进程维护一张页表,将虚拟地址转换成物理地址。
xv6操作系统的初始化进程的汇编代码
# Initial process that execs /init.
# This code runs in user space.
#include "syscall.h"
# exec(init, argv)
.globl start
start:
la a0, init // 将程序路径(即程序名)存放在a0寄存器
la a1, argv // 将程序参数存放在a1ji存起
li a7, SYS_exec //将系统调用号存放在a7寄存器
ecall // ecall指令负责陷入内核并执行系统调用
# for(;;) exit();
exit:
li a7, SYS_exit
ecall
jal exit
# char init[] = "/init\0";
init:
.string "/init\0"
# char *argv[] = { init, 0 };
.p2align 2
argv:
.long init
.long 0
xv6内核中的系统调用处理函数(当用户程序执行上面的ecall
指令时,会触发中断,内核会跳转到中断处理函数,即下面的syscall
函数:
void
syscall(void)
{
int num;
struct proc *p = myproc(); // myproc()获取当前进程的PCB
num = p->trapframe->a7; // 表示从寄存器a7中得到系统调用号。trapframe是PCB的一个字段,是进程在陷入内核时用于保存CPU寄存器状态的数据结构。该结构体包含了一系列CPU寄存器的值
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) { // NELEM(syscalls)表示系统调用数组`syscalls`的长度;`syscalls[num]`判断系统调用号对应的函数是否存在
p->trapframe->a0 = syscalls[num](); // syscalls[num]是一个函数指针,调用该函数,并将返回值存放在陷阱帧的a0寄存器中,最终会在用户空间中返回
} else { // 若系统调用号非法,则输出错误信息,并将返回值设为-1
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
陷入代码将用户寄存器中的参数保存到进程的trapframe中,以保证内核代码可以找到。
有些系统调用传递指针作为参数,内核需要通过该指针来读写用户空间。这会导致两个问题:
内核需要保证数据能够安全地传向用户地址或从用户地址传进内核。例如像exec
的文件系统调用使用fetchstr
来从用户空间获取string类型的文件名参数,而从代码可知fetchstr
函数调用copyinstr
函数来完成一些hark work:
int
fetchstr(uint64 addr, char *buf, int max)
{
struct proc *p = myproc();
if(copyinstr(p->pagetable, buf, addr, max) < 0) // copyinstr将从addr地址开始的字符串复制到buf指向的内核缓冲区
return -1;
return strlen(buf);
}
// Copy a null-terminated string from user to kernel.
// Copy bytes to dst from virtual address srcva in a given page table,
// until a '\0', or max.
// Return 0 on success, -1 on error.
int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max) // 从给定的虚拟地址srcva开始,沿着页表(由pagetable中的项目指示)映射的物理内存复制字符到dst。
{
uint64 n, va0, pa0; // 用于存储字节数、虚拟地址、物理地址
int got_null = 0; // 用于标记是否在复制过程中遇到了空字符'\0'
while(got_null == 0 && max > 0){ // 只要没有遇到空字符且还有剩余复制数量
va0 = PGROUNDDOWN(srcva); // 将虚拟地址srcva向下舍入到页的边界,得到va0
pa0 = walkaddr(pagetable, va0); // 通过页表pagetable和虚拟地址va0找到对应的物理地址pa0。walkaddr会自动检测虚拟地址是否在该进程的用户地址空间中
if(pa0 == 0) // 若找不到对应的物理地址
return -1;
n = PGSIZE - (srcva - va0); // 计算当前页中剩余的字节数
if(n > max) // 若剩余字节数大于最大复制数量max
n = max;
char *p = (char *) (pa0 + (srcva - va0)); // 从当前页的物理地址开始的字符串
while(n > 0){ // 逐字节本页复制字符
if(*p == '\0'){
*dst = '\0';
got_null = 1;
break;
} else {
*dst = *p;
}
--n;
--max;
p++;
dst++;
}
srcva = va0 + PGSIZE; // 更新srcva,使其指向下一个页的开始地址
}
if(got_null){
return 0;
} else {
return -1;
}
}