Exercise1 源代码阅读
- 启动部分: bootasm.S bootmain.c 和xv6初始化模块:main.c
- bootasm.S 由16位和32位汇编混合编写成的XV6引导加载器。bootasm.S内的汇编代码会调用bootmain.c中的void bootmain(void);main.c主函数内部初始化各模块;
- 当x86 PC启动时,它执行的是一个叫BIOS的程序。BIOS存放在非易失存储器中,BIOS的作用是在启动时进行硬件的准备工作,接着把控制权交给操作系统。具体来说,BIOS会把控制权交给从磁盘第0块引导扇区(用于引导的磁盘的第一个512字节的数据区)加载的代码。引导扇区中包含引导加载器——负责内核加载到内存中。BIOS 会把引导扇区加载到内存 0x7c00 处,接着(通过设置寄存器 %ip)跳转至该地址。引导加载器开始执行后,处理器处于模拟Intel 8088处理器的模式下。而接下来的工作就是把处理器设置为现代的操作模式,并从磁盘中把 xv6内核载入到内存中,然后将控制权交给内核。
1 | # Start the first CPU: switch to 32-bit protected mode, jump into C. |
- 中断与系统调用部分: trap.c trapasm.S vectors.S & vectors.pl syscall.c sysproc.c proc.c 以及相关其他文件代码
- trap.c 陷入指令c语言处理接口,trapasm.S陷入指令的汇编逻辑;
- vector.S由vector.pl生成,中断描述符256个;
- proc.c 内部主要接口:static struct proc * allocproc(void)、void userinit(void)、int growproc(int n)、int fork(void)、void exit(void)、int wait(void)、void scheduler(void)、void yield(void);
- syscall.c 内部定义了各种类型的系统调用函数,sysproc.c内部是与进程创建、退出等相关的系统调用函数的实现。
1 | // syscall.h System call numbers |
Exercise2 带着问题阅读
- 什么是用户态和内核态,两者有何区别? 什么是中断和系统调用,两者有何区别? 计算机在运行时,是如何确定当前处于用户态还是内核态的?
- 当一个进程在执行用户自己的代码时处于用户运行态(用户态),此时特权级最低,为3级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态。Ring3状态不能访问Ring0的地址空间,包括代码和数据;当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),此时特权级最高,为0级。执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈。用户运行一个程序,该程序创建的进程开始时运行自己的代码,处于用户态。如果要执行文件操作、网络数据发送等操作必须通过write、send等系统调用,这些系统调用会调用内核的代码。进程会切换到Ring0,从而进入内核地址空间去执行内核代码来完成相应的操作。内核态的进程执行完后又会切换到Ring3,回到用户态。这样,用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用。这说的保护模式是指通过内存页表操作等机制,保证进程间的地址空间不会互相冲突,一个进程的操作不会修改另一个进程地址空间中的数据;
- 系统调用需要借助于中断机制来实现。两者都是从同一个异常处理入口开始,但是系统调用会一开始让CPU进入内核模式且使能中断,然后从系统调用表中取得相应的注册函数调用之;而中断处理则让CPU进入内核模式且disable中断。所以系统调用的真实处理(系统调用表中的注册函数执行)中可以阻塞,而中断处理的上半部不可以。所以在写驱动代码如字符设备驱动,实现读操作时是可以让其sleep的(比如没有数据时候,用户设置读模式是阻塞型的)。另一方面,如果该驱动读操作过于耗时也是不可取的,它在内核态中执行,这个时候只有中断的优先级比它高,其它的高优先级线程将不能得到及时调度执行;
- 用户态和内核态的特权级不同,因此可以通过特全级判断当前处于用户态还是内核态。
- 计算机开始运行阶段就有中断吗? XV6 的中断管理是如何初始化的? XV6 是如何实现内核态到用户态的转变的? XV6 中的硬件中断是如何开关的? 实际的计算机里,中断有哪几种?
- 计算机开始运行阶段就有BIOS支持的中断;
- 由于xv6在开始运行阶段没有初始化中断处理程序,于是xv6在bootasm.S中用cli命令禁止中断发生。xv6的终端管理初始化各部分通过main.c中的main()函数调用。picinit()和oapicinit()初始化可编程中断控制器,consoleinit()和uartinit()设置了I/O、设备端口的中断。接着,tvinit()调用trap.c中的代码初始化中断描述符表,关联vectors.S中的中断IDT表项,在调度开始前调用idtinit()设置32号时钟中断,最后在scheduler()中调用sti()开中断,完成中断管理初始化;
- xv6在proc.c中的userinit()函数中,通过设置第一个进程的tf(trap frame)中cs ds es ss处于DPL_USER(用户模式) 完成第一个用户态进程的设置,然后在scheduler中进行初始化该进程页表、切换上下文等操作,最终第一个进程调用trapret,而此时第一个进程构造的tf中保存的寄存器转移到CPU中,设置了 %cs 的低位,使得进程的用户代码运行在 CPL = 3 的情况下,完成内核态到用户态的转变;
- xv6的硬件中断由picirq.c ioapic.c timer.c中的代码对可编程中断控制器进行设置和管理,比如通过调用ioapicenable控制IOAPIC中断。处理器可以通过设置 eflags 寄存器中的 IF 位来控制自己是否想要收到中断,xv6中通过命令cli关中断,sti开中断;
- 中断的种类有:程序性中断:程序性质的错误等,如用户态下直接使用特权指令;外中断: 中央处理的外部装置引发,如时钟中断;I/O中断: 输入输出设备正常结束或发生错误时引发,如读取磁盘完成;硬件故障中断: 机器发生故障时引发,如电源故障;访管中断: 对操作系统提出请求时引发,如读写文件。
- 什么是中断描述符,中断描述符表(IDT)? 在XV6里是用什么数据结构表示的?
- 中断描述符表的每一项是一个中断描述符,在x86系统中,中断处理程序定义存储在IDT中。XV6的IDT有256个入口点,每个入口点中对应的处理程序不同,在出发trap时,只要找到对应编号的入口,就能得到对应的处理程序;
- XV6中的数据结构中中断描述符用struct gatedesc表示:
1 | // trap.c |
- alltraps继续保存处理器的寄存器,设置数据和CPU段,然后压入 %esp,调用trap,到此时已完成用户态到内核态的转变;
1 | // trapasm.S |
- trap会根据%esp指向对应的tf,首先根据trapno判断该中断是否是系统调用,之后判断硬件中断,由于除零不是以上两种,于是判断为代码错误中断,并且是发生在用户空间的。接着处理程序将该进程标记为killed,并退出,继续下一个进程的调度;
1 | // trap.c |
- 涉及到的主要数据结构:中断描述符表IDT(trap.c +12)、(vi x86.h +150)、(vi vector.S)。
1 | // trap.c |
- 请以系统调用setrlimit(该系统调用的作用是设置资源使用限制)为例,叙述如何在XV6中实现一个系统调用。(提示:需要添加系统调用号,系统调用函数,用户接口等等)。
- 在syscall.h中添加系统调用号 #define SYS_setrlimit 22;
1 | // syscall.h |
- 在syscall.c中添加对应的处理程序的调用接口
1 | // syscall.c |
- 在sysproc.c中添加系统调用函数int sys_setrlimit(void),具体实现对于进程资源使用限制的设置;
1 | // syspro.c |
- 在user.h中声明系统调用接口int setrlimit(int resource, const struct rlimit * rlim);
1 | // syspro.c |
- 在usys.S添加SYSCALL(setrlimit)。
1 | // usys.S |
参考文献
[1] xv6 idt初始化
[2] xv6中文文档
[3] xv6 alltraps
[4] xv6 trap/interrupt