概述
在前面关于锁的博客中,我多次提到 CPU 的状态切换,即 CPU 状态由用户态切换到内核态。本篇博客我就这两个概念简单整理一下。
分级保护域
分级保护域,经常被叫做保护环,又称环形保护、CPU环,简称 rings。这是一种用来在发生故障时保护数据和功能以及避免恶意操作的设计方式。
简单来说,根据这种设计方式,计算机资源被划分为不同访问权限。根据权限从高到低的顺序,rings 依次划分为 ring 0 到 ring n,不同的操作系统会划分不同的层级。大多数情况下我们只使用其中两种:最高权限的 ring0 和 用户程序权限的 ring3。这里的 ring0 实际上就对应内核态,ring3 对应用户态。
-
内核态:cpu 可以访问内存中的所有数据,包括外围设备,如硬盘、网卡等
-
用户态:cpu 只能访问用户进程中的数据。且不能访问外围设备
划分用户态和内核态的原因也很简单:防止用户程序直接操作内核资源,导致系统崩溃。早期的 dos 系统就是因为没有划分权限,以致于用户程序可以肆意修改内核代码,以致于各种各样的电脑病毒。
linux 体系结构
有了上面分级保护域的基础,我们看一张抽象的linux体系结构图:
上图中,应用程序和内核被系统调用划分为两部分。这里的应用程序就是指平时我们使用的软件,如QQ、微信等。内核可以看做是一个特殊的软件,它负责管理系统的进程、内存、设备驱动程序、文件和网络系统等,决定着系统的性能和稳定性。
用户态就是应用程序运行的环境,为了使应用程序可以访问到内核中的资源,如内存、CPU,I/O等,内核向上提供了一些通用的接口,这些接口就可以称为 系统调用。
系统调用
系统调用是操作系统调用的最小单位,不同版本的操作系统提供不同的系统调用数。
当我们所要实现的功能比较简单时,可以直接调用系统调用达成目标。比如申请 16k 的内存,直接调用 brk() 系统调用即可。但是大多数情况下所要实现的功能都比较复杂,需要协调大量的系统调用才行。为了减轻开发者的压力,操作系统通过一些库函数将系统调用封装起来,开发者不需要关注底层细节,直接调用库函数即可。
库函数:库函数就是屏蔽这些复杂的底层实现细节,减轻程序员的负担,从而更加关注上层的逻辑实现。库函数对系统调用进行封装,提供简单的接口给用户,增强了程序的灵活性。常见的库函数有:glibc库,posix库等。
shell
除了上文提到的库函数,还有一种更直接的调用系统调用的方式,shell。
我们可以把 shell 理解为一种应用程序,使用该应用程序可以直接和系统交互。它通过命令行的形式调用系统调用,用户可以直接在命令行使用函数执行系统调用。为了方便用户和系统交互,一般一个 shell 对应一个终端,呈现给用户交互窗口。
shell 本身也是可编程的,它含有标准的 shell 语法,符合 shell 语法的代码我们也称为 shell 脚本。在平时的开发过程中,通常会写一些 shell 脚本来提高效率。
用户态到内核态
上面说了这么多,其实总结起来也就一句话:操作系统给不同的操作分权限,有些危险的操作需要内核态权限才能执行,因此 CPU 需要从用户态切换到内核态。举个简单的例子, C语音中调用 malloc 函数申请内存,该方法最终底层还是要调用 brk 系统操作才能完成,在调用系统调用时 CPU 状态就需要从用户态切换到内核态。
常见的用户态切换到内核态有以下三种:
-
系统调用:如上面分析
-
异常:当CPU正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常
-
外设中断:当外围设备完成用户的请求操作后,会向CPU发出中断信号,此时,CPU就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换
系统调用的本质其实也是中断,相对于外围设备的硬中断,这种中断称为软中断。从效果上来看,这三种切换方式是完全一样的,都相当于是执行了一个中断响应的过程。但是从触发的对象来看,系统调用是进程主动请求切换的,而异常和硬中断则是被动的。