用户态和内核态

经常在各种原理解析场景看到用户态和内核态的切换术语,但是自己一直没有了解过,今天准备认真了解一下其中的区别

1. 概念

用户态和内核态的概念之间说明会产生很多问题,所以先给出几个基本概念

1.1 特权级

在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。所以,CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾难的指令。

特权级显然是非常有效的管理和控制程序执行的手段,因此在硬件上对特权级做了很 多支持,就Intel x86架构的CPU来说一共有0~3四个特权级,0级最高,3级最低,硬件上在执行每条指令时都会对指令所具有的特权级做相应的检查,相关的概念有 CPL、DPL和RPL,这里不再过多阐述。对于 Unix/Linux来说,只使用了0级特权级3级特权级。也就是说在Unix/Linux系统中,一条工作在0级特权级的指令具有了CPU能提供的最 高权力,而一条工作在3级特权级的指令具有CPU提供的最低或者说最基本权力。

1.2 用户态和内核态

现在我们从特权级的调度来理解用户态和内核态就比较好理解了,当程序运行在3级特权级上时,就可以称之为运行在用户态,因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;反之,当程序运行在0级特权级上时,就可以称之为运行在内核态。

虽然用户态下和内核态下工作的程序有很多差别,但最重要的差别就在于特权级的不同,即权力的不同。熟悉Unix/Linux系统的人都知道,fork的工作实际上是以系统调用的方式完成相应功能的,具体的工作是由sys_fork负责实施。运行在用户态下的程序不能直接访问操作系统内核数据结构和程序,比如我们的代码就不能直接调用sys_fork(),因为前者是工作在用户态,属于用户态程序,而sys_fork()是工作在内核态,属于内核态程序。

当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系 统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态。

2. 用户态和内核态的转换

2.1 用户态切换到内核态的3种方式

a. 系统调用

这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使 用操作系统提供的服务程序完成工作。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int80h中断。

b. 异常

当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

c. 外围设备的中断

当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。

2.2 具体的切换操作

从触发方式上看,可以认为存在前述3种不同的类型,但是从最终实际完成由用户态 到内核态的切换操作上来说,涉及的关键步骤是完全一致的,没有任何区别,都相当于执行了一个中断响应的过程,因为系统调用实际上最终是中断机制实现的,而异常和中断的处理机制基本上也是一致的,关于它们的具体区别这里不再赘述。关于中断处理机制的细节和步骤这里也不做过多分析,涉及到由用户态切换到内核态的步骤主要包括:

  • 从当前进程的描述符中提取其内核栈的ss0及esp0信息。
  • 使用ss0和esp0指向的内核栈将当前进程的cs,eip,eflags,ss,esp信息保存起来,这个过程也完成了由用户栈到内核栈的切换过程,同时保存了被暂停执行的程序的下一条指令。
  • 将先前由中断向量检索得到的中断处理程序的cs,eip信息装入相应的寄存器,开始执行中断处理程序,这时就转到了内核态的程序执行了。

3. 解惑

首先内核态与用户态是操作系统的两种运行级别,跟intel cpu没有必然的联系,如上所提到的intel cpu提供Ring0-Ring3四种级别的运行模式,Ring0级别最高,Ring3最低。Linux只是使用了Ring3级别运行用户态,Ring0作为内核态,却没有没有使用Ring1和Ring2。

此外内核态和用户态的切换其实在大多数的情况下只存在于你的代码中,所以是同一个线程或者进程内,当从用户态切换到内核态时只是改变了cpu的权限等级,从进程和线程的概念来讲,并没有发生进程或者线程的切换。参考如下:

示例中有两个并发的进程Shell和Hello,在A处“向量分解”,我们发现它即发生了模式切换又发生了进程切换(上下文切换);在B处,我们发现它只发生了模式切换。

“操作系统的进程切换和CPU的模式切换并没有什么关系,发生模式切换可以不改变正处于运行态的进程状态,这种情况下,保存上下文环境和以后恢复上下文环境只需要很少的开销。但是,如果改变正处于运行态的进程状态到另一个状态(就绪、阻塞等),则操作系统必须使其环境产生实质性的变化”——《操作系统:精髓与设计原理》

4. 参考

https://www.zhihu.com/question/40147261

https://www.jianshu.com/p/85e931636f27

https://www.cnblogs.com/think-in-java/p/5924183.html