一、用户态与内核态
-
Linux 把内存主要分为 4 个段,分别是内核代码段、内核数据段、用户代码段、用户数据段。
-
内核两个段特权级都为最高级 0,用户两个段特权级都为最低级 3。内核代码段可以访问内核数据段,但不能访问用户数据段和用户代码段,同样地,用户代码段可以访问用户数据段,但不能访问内核数据段或内核代码段。
-
当前进程运行的代码若属于内核代码段,则称当前进程处于内核态,若属于用户代码段,则称当前进程处于用户态。用户代码段和内核代码段的代码分别运行在用户栈上和内核栈上。
-
处于用户态的进程若想要切换到内核态,便要依靠中断。
二、中断
- CPU 每次执行完一条指令后,总会先检查有无中断请求,无则执行下一条指令,有则在总线上读取中断的标识码即中断向量。
- 内存中存放着一个中断描述符表(Interrupt Descriptor Table,简称 IDT,也可以叫做中断向量表),表里有许多个中断描述符,所以要在中断描述符表中找到特定一项中断描述符需要一个下标,这个下标就是上文提及的中断向量,就像访问数组中的特定元素一样。每个中断描述符的内容中有 3 项信息比较重要——该描述符的特权级,中断服务函数的入口地址,中断服务函数所在段的特权级。在 Linux 中,所有的中断服务函数都放在内核代码段,故中断服务函数所在段的特权级都为 0。
- 那 CPU 通过什么手段 IDT 在内存中的地址呢?答案是寄存器。CPU 有个寄存器叫 IDTR,用于存放 IDT 的首地址和长度,就像 C++
string
类的实现一样,有一个char*
指针指向字符串首地址和一个整形变量记录字符串长度。 - 设当前进程的代码段特权级为 ,通过中断向量找到的中断描述符的特权级为 ,中断服务函数所在段的特权级为 ,中断的特权级检查有两道关卡,只有通过了这两道检查才能进入中断的重头戏——执行中断服务函数。第一关是 和 的比较,若 ,则进入下一道检查,否则触发异常;第二关是 和 的比较,若前者小于等于后者 ,则检查通过,否则触发异常。需要特别说明的是,第一道检查基本上都能通过,上文提到,Linux 的中断服务函数所在段皆为内核代码段,故 为0,且特权级数值只能是 0 到 3 的整数;其次,中断描述符的特权级大多数为 0,只有 能让不等式 成立,也就是说大多数情况下能通过第二道检查的只有内核代码段,那用户代码段是不是不用指望触发中断了呢?并不是,Linux 中有少数中断描述符的特权级为 3,这类中断描述符有 4 个在 IDT 中的下标即中断向量分别为 3、4、5、128,对应的 4 个中断服务函数分别是断点、溢出、边界检查、系统调用。
三、任务状态段
-
任务状态段(Task State Segment),简称 TSS,用于保存处理器中各种与寄存器有关的重要信息。
-
只有内核态代码段可以访问 TSS,用户态代码段并没有这个权力,故操作系统必须用硬件机制拦阻用户态代码段非法访问 TSS——将 TSS 的特权级设置为最高级 0,特权级为最低级 3 的用户态代码段若要访问 TSS 便会被保护模式的特权级比较机制半路拦截。
-
任务状态段存放着当前进程用户栈的栈顶地址和内核栈的栈顶地址,这两项信息是用户态切换到内核态的关键之一。
四、Linux 进程从用户态切换到内核态的过程
-
第一步,当前进程在用户态用汇编指令
int <中断向量号>
发出中断请求,设中断向量号为 , 。 -
第二步,CPU 在总线上捕捉到中断向量 ,通过寄存器 IDTR 找到 IDT,再找到下标为 的中断描述符,进行两道特权级检查。
-
第三步,用户栈切换到内核栈,切换的关键在于栈地址的切换,而栈地址由某些特定寄存器的值共同决定,故关键在于改变这些特定寄存器的值。如何改变?依靠 TSS——由于 TSS 存放着内核栈的栈地址,把 TSS 里的内核栈栈地址赋给这些特定寄存器,便能完成从用户栈到内核栈的切换,即完成了用户态到内核态的切换。
-
补充一点,栈切换成功后,还有许多操作,在此强调两处操作,一是将用户栈栈地址保存在内核栈上,以便从内核栈切换回用户栈,二是取出第二步访问过的中断描述符的中断服务函数的入口地址将其保存在内核栈上,并跳转到中断服务函数开始执行。