17.用户态与内核态
操作系统在运行时存在着两种运行状态:用户态和内核态。 我们可以认为操作系统在执行我们自己所写的代码时的所处的状态叫做用户态,在执行系统调用时所处的状态叫做内核态。
想要谈清什么是用户态什么是内核态,首先需要我们知道操作系统是怎么运行的。
一、操作系统的运行原理
1.1 硬件中断
CPU执行任务时不可能时轮询检测外部设备(效率太低),因此外部设备是与中断控制器直接相连的。当外部设备想要执行时就会向中断控制器发送中断,接下来中断控制器通知CPU有中断请求,CPU就能读取到中断号知道是哪一个设备发送的中断请求。
操作系统在编写的时候为我们提供了中断向量表(IDT),即给每一个设备准备好了触发中断时所要执行的方法。
硬件上有了中断号,软件上有了中断向量表,而当前的CPU可能是繁忙的,故而中断的固定处理历程要先把CPU上的寄存器做现场保护(保存在中断上下文里),接下来CPU根据中断号去中断向量表中查找处理方法并调用,最后还原现场,继续CPU的执行。
结论:
• 中断向量表就是操作系统的⼀部分,启动就加载到内存中了
• 通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询
• 由外部设备触发的,中断系统运行的流程就叫做硬件中断
1.2 时钟中断
进程可以在操作系统的指挥下被调度被执行,那么操作系统自己被谁指挥被谁推动执行的呢?外部设备可以触发硬件中断,但是这个是需要用户触发,有没有自己可以定期触发的设备?
在计算机硬件上存在着时钟源,他会以一定的频率向CPU发送时钟中断让操作系统执行对应的中断方法,使得操作系统一直在执行中断方法一直被调度。当代的CPU已经将时钟源集成到CPU内部了,对应的指数也就是主频。
我们可以认为操作系统是基于中断向量表进行工作的。
什么是时间片呢?
时间片的本质就是一个计数器,时钟中断是以固定的频率发过来的,当调度器调度时就会让当前进程的时间片减减,减到零就进行进程切换。struct task_struct { //…… long counter; //…… };
void do_timer(long cpl) { //…… // 如果进程运行时间还没完,则退出。否则置当前任务计数值为0.并且若发生时钟中断 // 正在内核代码中运行则返回,否则调用执行调度函数。 if ((--current->counter)>0) return; current->counter=0; //…… }
1.3 软中断
上面提到的外部的硬件中断,那么有没有可能因为软件内部的原因也触发上面的条件呢?
当然有!为了让操作系统支持系统调用,CPU也设计了对应的汇编指令(int 或者system call)可以让CPU内部触发中断。
当我们进行系统调用时就会触发软中断,接下来CPU就会call系统调用的地址(int 0x80)并让操作系统根据系统调用号调用系统调用。
那么我们也会有这样的问题:
• 用户层怎么把系统调用号给操作系统?
寄存器(比如eax)
• 操作系统怎么把返回值给用户?
寄存器或者用户传入的缓冲区地址
Linux内核提供的系统调用接口根本就不是C语言函数,而是系统调用号+约定的要传递的参数,系统调用号和返回值的寄存器以及int 0x80这样的触发软中断的机制。我们所使用的系统调用是GNU glibc对系统调用进行的封装!
1.4 源码认识
下图是linux中操作系统的运行实现:
1.5 整体理解
缺页中断,内存碎片处理,除零野指针错误这些问题全部都会被转换成为CPU内部的软中断,然后走中断处理例程完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来处理内存碎片的,有的是用来给目标进程发送信号杀掉进程等等。
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error);// 设置除操作出错的中断向量值。以下雷同。
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
// 下⾯将int17-48 的陷阱⻔先均设置为reserved,以后每个硬件初始化时会重新设置⾃⼰的陷阱⻔。
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13);// 设置协处理器的陷阱⻔。
outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯⽚的IRQ2 中断请求。
outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯⽚的IRQ13 中断请求。
set_trap_gate(39,¶llel_interrupt);// 设置并⾏⼝的陷阱⻔。
}
所以操作系统就是躺在中断处理例程上的代码块!
• CPU内部的软中断,比如int 0x80或者syscall,我们叫做陷阱。
• CPU内部的软中断,比如除零/野指针等,我们叫做异常。
二、用户态与内核态
进程地址空间的0~3GB的空间属于用户区,3~4GB的空间属于内核区。用户页表每个进程都有一份,但是内核页表整个程序只有一份。故任何进程无论怎么调度都能找到同一个操作系统!
操作系统中的中断向量表、系统调研表、各种异常处理方法等甚至包括操作系统本身整体都会通过内核页表映射到内核区中。我们目前最关心的就是系统调用,我们此时使用系统调用需要关心它的物理地址吗?不关心,我们只需要将系统调用号交给CPU调用即可!
我们不论调用什么函数(库、系统调用)都是在我们自己的进程地址空间中调用的。不管通过哪个进程的地址空间进入内核都是通过软中断进入操作的!
我们如何确定当前的进程处于那种哪种状态呢?
CPU内部有CS段寄存器,它有一位CPL表示当前权限的级别,0表示内核态,3表示用户态。
怎么从用户态进入内核态呢?
a.时钟信号 b.异常 c.陷阱