1.什么是用户态和内核态?两者有何区别?什么是中断和系统调用?两者有何区别?计算机在运行时,是如何确定当前处于用户态还是内核态的?
用户态是指用户程序运行时的状态,又称为目态、普通态;内核态是指操作系统管理程序运行时的状态,又称为特权态、管态、系统态。对于x86而言有四个特权级,从 0(特权最高)编号到 3(特权最低)。在实际使用中,大多数的操作系统使用两个特权级,0 和 3,即为内核模式和用户模式。
两者的却别体现在运行程序对于资源和机器指令的使用权限不同,内核态能运行的指令特权更高。中断是指CPU对系统发生的某个事件作出的一种反应,该事件改变处理器执行指令的顺序;系统调用是指供用户调用操作系统的功能的特殊指令。
中断可能是由硬件、其他进程产生的,是随机发生的,而系统调用是由当前进程产生的,是可预料的。
计算机在运行时,当前执行指令的特权级存在于 %cs
寄存器中的 CPL 域中。通过 %cs
寄存器可以判断当前是用户态还是内核态。
2. 计算机开始运行阶段就有中断吗? 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中断: 输入输出设备正常结束或发生错误时引发,如读取磁盘完成
- 硬件故障中断: 机器发生故障时引发,如电源故障
- 访管中断: 对操作系统提出请求时引发,如读写文件
3. 什么是中断描述符,中断描述符表?在 XV6 里是用什么数据结构表示的?
中断描述符代表并描述一个特定的中断。中断描述符表(IDT)是一个表,将每个异常或中断向量分别与它们的处理过程联系起来,其中的表项是中断描述符。XV6中的中断描述符由mmu.h
中的gatedesc
结构体表示,保存了类型、对应处理程序、Offset等信息,中断描述表由struct gatedesc idt[256]
表示。
4. 请以某一个中断(如除零,页错误等)为例,详细描述 XV6 一次中断的处理过程。包括:涉及哪些文件的代码?如何跳转?内核态,用户态如何变化?涉及哪些数据结构等等。
以除零错误中断为例,CPU根据当前是用户态保存相应寄存器,然后访问IDT 表,表项指向的中断处理函数入口捕获到中断,将对应的中断号压栈,并调用alltraps
:
.globl vector0 vector0: pushl $0 pushl $0 jmp alltraps
alltraps
继续保存处理器的寄存器,设置数据和CPU段,然后压入 %esp,调用trap
,到此时已完成用户态到内核态的转变:
... movw $(SEG_KDATA<<3), %ax movw %ax, %ds ... pushl %esp call trap
trap
会根据%esp指向对应的tf
,首先根据trapno
判断该中断是否是系统调用,之后判断硬件中断,由于除零不是以上两种,于是判断为代码错误中断,并且是发生在用户空间的。接着处理程序将该进程标记为killed,并退出,继续下一个进程的调度。
switch(tf->trapno){ ... //PAGEBREAK: 13 default: if(proc == 0 || (tf->cs&3) == 0){ // In kernel, it must be our mistake. ... } // In user space, assume process misbehaved. ... proc->killed = 1; } ... // Check if the process has been killed since we yielded if(proc && proc->killed && (tf->cs&3) == DPL_USER) exit();
涉及到的主要数据结构:
extern uint vectors[]; // in vectors.S: array of 256 entry pointers struct gatedesc idt[256]; // hardware and by trapasm.S, and passed to trap(). struct trapframe { // registers as pushed by pusha uint edi; ... };
另外,如果是执行的是系统调用,则会通过syscall.c
中的syscall()
调用对应的处理程序,之后控制流返回trapasm.S
,还会恢复被压入栈的寄存器,执行iret
跳回到用户空间,完成内核态到用户态的转变。
5. 请以系统调用 setrlimit(该系统调用的作用是设置资源使用限制)为例,叙述如何在 XV6 中实现一个系统调用。(提示:需要添加系统调用号,系统调用函数,用户接口等等)。
- 在
syscall.h
中添加对应的系统调用号#define SYS_setrlimit 22
- 在
syscall.c
中添加对应的处理程序的调用接口extern int sys_setrlimit(void); static int (*syscalls[])(void) = { ... [SYS_setrlimit] sys_setrlimit, };
- 在
sysproc.c
中添加系统调用函数int sys_setrlimit(void)
,具体实现对于进程资源使用限制的设置