0x00.前言
About
本节原始内容来自于 https://arttnba3.cn/2021/02/21/OS-0X00-LINUX-KERNEL-PART-I/ ,主要是给大家对于 Linux kernel 的大致框架和关键内容有一个总览,不会深入分析各个子系统的细节。
I.操作系统究竟是什么?
在开始之前,笔者想让大家重新来思考这个问题:
- 操作系统究竟是一个什么东西?
可能有的人会像笔者当年那样照本宣科地说——操作系统(Operation System)本质上也是一种软件,可以看作是普通应用程式与硬件之间的一层中间层,其主要作用便是调度系统资源、控制IO设备、操作网络与文件系统等,并为上层应用提供便捷、抽象的应用接口
那么在抛开掉这些词藻的背后,操作系统又是什么样的一个东西呢?这里笔者给出笔者自己所认为的定义:
- 操作系统实际上是我们抽象出来的一个概念,本质上与用户进程一般无二,都是位于物理内存中的代码+数据,不同之处在于当CPU执行操作系统内核代码时通常运行在高权限环境,拥有着完全的硬件访问能力,而 CPU在执行用户态代码时通常运行在低权限环境,只拥有部分/缺失硬件访问能力
这两种不同权限的运行状态实际上是通过硬件来实现的,这里我们开始引入新的一个概念——
II.分级保护域
分级保护域(hierarchical protection domains)又被称作保护环,简称 Rings ,是一种将计算机不同的资源划分至不同权限的模型
在一些硬件或者微代码级别上提供不同特权态模式的 CPU 架构上,保护环通常都是硬件强制的。Rings是从最高特权级(通常被叫作0级)到最低特权级(通常对应最大的数字)排列的
Ring0 拥有最高特权,并且可以和最多的硬件直接交互(比如CPU,内存),因此操作系统内核代码通常运行在 ring0 下,即 CPU 在执行操作系统内核代码时处在 ring0 下
Intel的CPU将权限分为四个等级:Ring0、Ring1、Ring2、Ring3,权限等级依次降低,现代操作系统模型中我们通常只会使用 ring0 和 ring3,对应操作系统内核与用户进程,即 CPU 在执行用户进程代码时处在 ring3 下

那么我们现在可以给【用户态】与【内核态】这两个概念下定义了:
- 用户态:CPU 运行在 ring3 + 用户进程运行环境上下文
- 内核态:CPU 运行在 ring0 + 内核代码运行环境上下文
III.运行状态切换
那么操作系统如何使得 CPU 在不同的特权级间进行切换以进行调度呢?主要有两个途径:
- 中断(interrupt):当 CPU 收到一个外部中断时,会切换到 ring0,并根据中断描述符表索引对应的中断处理代码以执行
例如当笔者敲下这一行字时,便发生了许多次的键盘中断(笑)
- 特权级相关指令:当 CPU 运行这些指令时会发生运行状态的改变,例如 iret 指令(ring0->ring3)或是 sysenter 指令(ring3->ring0)
基于这些特权级切换的方式,现代操作系统的开发者包装出了一种名为系统调用(syscall)的东西,作为由”用户态“切换到”内核态“的入口,从而执行内核代码来完成用户进程所需的一些功能;当用户进程想要请求更高权限的服务时,便需要通过由系统提供的应用接口,使用系统调用以陷入内核态,再由操作系统完成请求
IV.进程 = 进程运行环境上下文 + 内核数据结构
进程本质上是我们抽象出来的一个概念,CPU 是不知道也不可能知道进程是什么东西的,他只知道当前核心的寄存器的数据是这样,运行状态是这样,内存里的数据是那样,但我们可以人为地进行划分——将当前的运行环境上下文称之为一个进程实体
接下来我们引入一个新的概念——页表(page table)。这通常是一个多级结构,用以对应表示一个虚拟地址空间中存在的每张内存页所对应的物理内存,下图是一张经典的二级页表结构:

当开启分页模式之后,我们对内存的访问都需要通过页表进行,利用多级页表将对应的虚拟地址转化为物理地址后才能进行内存访问,因此当前页表集所表示的物理地址空间为我们当前所能访问的物理地址空间
我们想要让我们的计算机真正变成一个多任务系统,让其运行多个不同的进程,则我们需要实现不同进程间的虚拟地址空间的隔离,因此:
- 每个进程都有自己独立的一组页表集
那么现在我们可以对抽象出来的进程的概念下一个定义了:进程 = 寄存器上下文 + 虚拟地址空间,这便是进程最核心的本质
但是有了这些还不够,因为需要运行的进程数量往往大于 CPU 核心数量,因此我们不能够让某几个进程占据所有的 CPU 时间,而应当对进程进行调度,让每个进程都有机会在 CPU 上运行——进程调度(schedule)的概念就出来了
那么我们如何知道什么时候该调度一个进程?这里我们就需要借助到硬件的辅助了——时钟中断用来帮助我们实现进程的调度
什么是时钟中断?这里笔者不深入介绍,你可以理解为在 CPU 上有个定时器,每隔一段时间就会向 CPU 发送一个中断,这个中断便是我们所说的时钟中断,而前面我们讲到:当 CPU 收到一个外部中断时,会切换到 ring0,并根据中断描述符表索引对应的中断处理代码以执行,因为时钟中断是会不断定时触发的,我们可以在时钟中断对应的处理代码中加入属于进程调度的部分,由此我们得知:
- 操作系统利用时钟中断完成进程调度
那么我们怎么知道当前运行的这个进程是否需要被调度呢?我们又如何切换到另一个进程呢?为了解决这个问题,对于每一个进程,我们在内核地址空间中都会对应创建一个新的数据结构实体,在其中保存着该进程上次运行的环境上下文、运行的总时间等信息——我们称之为进程控制块(process control block,PCB)
在需要进行进程调度时,我们便能将当前进程的上下文保存到其在内核空间中对应的 PCB 中,记录其运行时间,并取出要运行的下一个进程对应的 PCB,从其中恢复待运行进程的环境上下文,因此:
- 每个进程在内核空间当中都有着一个对应的 PCB 保存其相关信息
当然, PCB 还可以记录诸如进程权限、父子关系等信息,不过这里仅介绍对于 PCB 而言最基础也是最必须要的组件:进程上下文以及进程运行时间
进程运行时间其实也可以不用记录,直接默认进程时间片为一个时钟周期即可,不过现代操作系统往往会使用更加复杂的调度系统(例如 Linux 现在使用的 CFS 调度算法)
最后,有了以上这些概念之后,我们来为进程(process)下一个定义:
- 进程 = 进程运行环境上下文 + 内核数据结构
好了,现在我们可以重新从头开始看 Linux kernel 了