因为操作系统的很多操作会消耗系统的物理资源,例如创建一个新进程时,要做很多底层的细致工作,如分配物理内存,从父进程拷贝相关信息,拷贝设置页目录、页表等,这些操作显然不能随便让任何程序都可以做,于是就产生了特权级别的概念,与系统相关的一些特别关键性的操作必须由高级别的程序来完成,这样可以做到集中管理,减少有限资源的访问和使用冲突。Intel的X86架构的CPU提供了0到3四个特权级,而在我们Linux操作系统中则主要采用了0和3两个特权级,也就是我们通常所说的内核态和用户态。
运行于用户态的进程可以执行的操作和访问的资源都受到极大的限制,而运行于内核态的进程则可以执行任何操作并且在资源的使用上也没有限制。很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。本文主要要介绍的就是这个过程。
这里再明确一个概念,每个进程都有一个4G大小的虚拟地址空间,在这个4G大小的虚拟地址空间中,前0~3G为用户空间,每个进程的用户空间之间是相互独立的,互不相干。而3G~4G为内核空间,因为每个进程都可以从用户态切换到内核态,因此,内核空间对于所有进程来说,可以说是共享的,不过这么说有些不太严谨,应该说内核空间中大部分区域对于所有的进程来说都是共享的,这不共享的小部分区域是存储所有进程内核栈的区域,为什么这么说,因为每个进程都存在一个内核栈,而各个进程的内核栈之间一定是不共享的。关于内核空间的详细描述,参见
1、Linux内核--内核地址空间分布和进程地址空间
2、Linux用户空间与内核空间
下面正式开始进入由用户态向内核态切换的过程。
首先需要了解,什么情况下会发生从用户态向内核态切换。这里细分为3种情况。
1、发生系统调用时
这是处于用户态的进程主动请求切换到内核态的一种方式。用户态的进程通过系统调用申请使用操作系统提供的系统调用服务例程来处理任务。而系统调用的机制,其核心仍是使用了操作系统为用户特别开发的一个中断机制来实现的,即软中断。
2、产生异常时
当CPU执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行的进程切换到处理此异常的内核相关的程序中,也就是转到了内核态,如缺页异常。
3、外设产生中断时
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作的完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
可以看到上述三种由用户态切换到内核态的情况中,只有系统调用是进程主动请求发生切换的,中断和异常都是被动的。
由于系统调用、中断和异常由用户态切换到内核态的机制大同小异,所以这里仅就系统调用的切换过程进行具体说明。
如果一个用户程序需要调用底层的系统接口,如fork等诸如libc里面的系统调用函数,就牵涉到用户态与内核态的切换问题,因为系统调用处理程序都是运行在内核态下。
在系统调用时由于用户态和内核态是运行于两个独立的栈上面,即分别为内核栈和用户栈,因此,不能仅简单的传递函数指针,因为对于内核态堆栈在用户态下是不可见的,所以对于系统调用函数的处理程序对于用户态是不可见的;同时,因为内核栈和用户栈是相互独立的,所以在参数传递的过程中不能使用普通的压栈出栈的方式来进行参数传递。
二:内核栈
内核栈:Linux中每个进程有两个栈,分别用于用户态和内核态的进程执行,其中的内核栈就是用于内核态的堆栈,它和进程的task_struct结构,更具体的是thread_info结构一起放在两个连续的页框大小的空间内。
在内核源代码中使用C语言定义了一个联合结构方便地表示一个进程的thread_info和内核栈:
此结构在3.3内核版本中的定义在include/linux/sched.h文件的第2106行:
2016 union thread_union {
2017 struct thread_info thread_info;
2018 unsigned long stack[THREAD_SIZE/sizeof(long)];
2019 };
其中thread_info结构定义如下
3.3内核 /arch/x86/include/asm/thread_info.h文件第26行:
它们的结构图大致如下:
esp寄存器是CPU栈指针,存放内核栈栈顶地址。在X86体系中,栈开始于末端,并朝内存区开始的方向增长。从用户态刚切换到内核态时,进程的内核栈总是空的,此时esp指向这个栈的顶端。
在X86中调用int指令型系统调用后会把用户栈的%esp的值及相关寄存器压入内核栈中,系统调用通过iret指令返回,在返回之前会从内核栈弹出用户栈的%esp和寄存器的状态,然后进行恢复。所以在进入内核态之前要保存进程的上下文,中断结束后恢复进程上下文,那靠的就是内核栈。
这里有个细节问题,就是要想在内核栈保存用户态的esp,eip等寄存器的值,首先得知道内核栈的栈指针,那在进入内核态之前,通过什么才能获得内核栈的栈指针呢?答案是:TSS
三:TSS
X86体系结构中包括了一个特殊的段类型:任务状态段(TSS),用它来存放硬件上下文。TSS反映了CPU上的当前进程的特权级。
linux为每一个cpu提供一个tss段,并且在tr寄存器中保存该段。
在从用户态切换到内核态时,可以通过获取TSS段中的esp0来获取当前进程的内核栈 栈顶指针,从而可以保存用户态的cs,esp,eip等上下文。
注:linux中之所以为每一个cpu提供一个tss段,而不是为每个进程提供一个tss段,主要原因是tr寄存器永远指向它,在任务切换的适合不必切换tr寄存器,从而减小开销。
下面我们看下在X86体系中Linux内核对TSS的具体实现:
内核代码中TSS结构的定义:
3.3内核中:/arch/x86/include/asm/processor.h文件的第248行处:
其中主要的内容是:
硬件状态结构 : x86_hw_tss
IO权位图 : io_bitmap
备用内核栈: stack
其中硬件状态结构:其中在32位X86系统中x86_hw_tss的具体定义如下:
/arch/x86/include/asm/processor.h文件中第190行处:
linux的tss段中只使用esp0和iomap等字段,并且不用它的其他字段来保存寄存器,在一个用户进程被中断进入内核态的时候,从tss中的硬件状态结构中取出esp0(即内核栈栈顶指针),然后切到esp0,其它的寄存器则保存在esp0指的内核栈上而不保存在tss中。
每个CPU定义一个TSS段的具体实现代码:
3.3内核中/arch/x86/kernel/init_task.c第35行:
INIT_TSS的定义如下:
3.3内核中 /arch/x86/include/asm/processor.h文件的第879行:
其中init_stack是宏定义,指向内核栈:
61 #define init_stack (init_thread_union.stack)
这里可以看到分别把内核栈栈顶指针、内核代码段、内核数据段赋值给TSS中的相应项。从而进程从用户态切换到内核态时,可以从TSS段中获取内核栈栈顶指针,进而保存进程上下文到内核栈中。
总结:有了上面的一些准备,现总结在进程从用户态到内核态切换过程中,Linux主要做的事:
1:读取tr寄存器,访问TSS段
2:从TSS段中的sp0获取进程内核栈的栈顶指针
3: 由控制单元在内核栈中保存当前eflags,cs,ss,eip,esp寄存器的值。
4:由SAVE_ALL保存其寄存器的值到内核栈
5:把内核代码选择符写入CS寄存器,内核栈指针写入ESP寄存器,把内核入口点的线性地址写入EIP寄存器
此时,CPU已经切换到内核态,根据EIP中的值开始执行内核入口点的第一条指令。