Tags

, ,

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 中实现一个系统调用。(提示:需要添加系统调用号,系统调用函数,用户接口等等)。

  1. syscall.h中添加对应的系统调用号 #define SYS_setrlimit 22
  2. syscall.c中添加对应的处理程序的调用接口
    extern int sys_setrlimit(void);
        static int (*syscalls[])(void) = {
        ...
        [SYS_setrlimit]   sys_setrlimit,
        };
  3. sysproc.c中添加系统调用函数int sys_setrlimit(void),具体实现对于进程资源使用限制的设置