什么是操作系统?

操作系统 (Operating System, OS) 是一个程序,它的作用是控制其他应用程序的运行,有点像一个管家,负责管理其他程序如何使用机器。

操作系统不是什么神秘的漂浮在CPU上面的东西,反而我们需要强调,操作系统本质上就是个程序。

假如我们在电脑上运行了不少软件,例如 Chrome, Edge, 网易云, Minecraft,这些都叫做 应用程序 (Application Programs)。OS和他们只有职位和能力上的区别。

操作系统解决的问题

假设你的电脑只有一个 CPU 单核可用,但是现在有四个程序都想运行。例如网易云,VS Code,Edge和Minecraft。那么,CPU该决定此时到底应该执行谁?

CPU 不可能在同一个瞬间真的执行四条完全不同的指令流,因此OS必须决定做下面这样一件事:

让网易云跑一会 -> 暂停网易云 -> 跑一会VS Code -> 暂停 VS Code ...

如果CPU切换的足够快,我们作为用户就会感觉,这些程序都在同时运行。


从上面,我们就能看出OS的第一个主要用法了:

OS第一作用:管理有限的计算资源

其中,我们说计算资源,一般指的是 CPU时间,内存空间,存储空间,文件系统等这些进程需要执行时,吃掉的资源。

OS的一大任务就是要在 资源请求冲突的情况下,让资源的分配尽可能公平,同时最大化资源效率

这里的 “资源请求冲突” 是什么意思?假如说在同一时刻,Edge说需要一些CPU时间,Minecraft也说需要,那么现在CPU时间就是一个有限资源。同时不只是CPU时间,程序运行还需要内存呢,不同的程序需要去抢你机器上比金子还贵的内存空间。

所以简单来说,就是将有限的资源平衡高效地分配给多个需求方。


OS的第二个重要职务是:

充当其他应用程序和硬件之间的通讯桥梁

具体来说,OS会将硬件层与其他应用程序隔离开,统一管理,随后将硬件能力通过API的方式为其他程序开放。其他的程序可以向OS来请求服务和资源。

怎么理解呢?如果我们在没有OS的情况下,想要新建文件写入硬盘。那么你就需要知道你目前机子上的SSD的通讯方式:什么型号?控制器寄存器地址在哪?我应该发什么硬件命令?这些对你来说是非常痛苦的。

但今天你在python写一行 open("xxx", "w") 就能够操作OS,与硬盘交互来打开某个文件。一切都这么简单,你甚至不需要知道硬盘的型号。由于OS在中间充当通讯桥梁,应用程序直接能看到OS开放的 file, process, socket, memory等接口,调用了就能使用硬件资源,多省事。


操作系统的四大服务

所以,我们可以将操作系统做的事情总结为四大服务:

  1. 让应用程序能够正常运行
  2. 让运行中的应用程序能够使用,并共享内存
  3. 让应用程序之间能够彼此交互
  4. 让应用程序访问并共享存储空间中存储的数据。

至于具体对应的后端原理,我们会在后面的章节中展开。


我们在研究什么问题?

在这门课中,我们研究的不是 “操作系统为什么这么干”?这个答案太显而易见了,系统更加容易使用就是目标。

但是,你有没有想过,OS通过什么 机制(Mechanism)策略(Policies) 来达成这些目标?在给定同样目标情况下,我们如何做到高效?我们需要什么硬件支持?

机制在这里决定的就是 “系统能怎么做”,策略就是 “系统决定怎么做”。例如OS有能力暂停A进程去执行B,这叫机制。那么我们该什么时候暂停A,接下来放行B还是C?每个进程跑多久?这是策略决定的。

所以记住操作系统有什么功能,对学习这门课没什么太大帮助。我们想要关心的是,这些功能在底层是怎么被实现出来的。例如,

  • 怎么让多个进程共享 CPU?
  • 怎么限制程序做危险操作?
  • 怎么让很多进程共享 memory?
  • 怎么让共享数据不被同时写坏?
  • 怎么让磁盘访问更快?

这些才是正儿八经的内容。


进程与处理器调控

一个 进程 (Process),你可以理解为一个 “正在运行的程序”。

程序,例如你电脑上安装的steam.exe,静静的放在硬盘里,纯粹的静态代码和文件。这个叫做 程序 (Program)。但一旦这个程序开始双击运行,你启动了他的一个实例,他真的要吃计算资源了。这就叫一个进程。

那么OS管的是什么?当一个进程运行时,OS需要为他调配资源;当一个进程结束后,OS的任务就是把分配出去的资源收回来。否则一个程序关掉以后,它的内存、文件句柄等资源还永久霸着,那系统很快就废了。

而且一个典型系统中可能存在很多进程。有些叫做 用户进程(User Process) ,有些是系统的 系统/内核进程 (OS / Kernel Process) ,它们可能在一个或多个 CPU/core 上并行运行。这个看一看就行,后面会讲到的。


那么问题来了,操作系统怎样 管理并控制软件进程

刚才我们已经知道系统里可能有很多 process。在一个活跃系统中,你能看见:

这个 process 在运行,那个 process 在等 I/O,另一个 process 暂时没拿到 CPU,还有一个已经结束

OS 得维护它们的状态。因此OS发现,我不能够仅仅让程序去跑,还要必须 持续追踪、控制它们


那么,我们怎么确保每个进程都乖乖的,不会把整个硬件系统搞乱? 我们如何处理应用程序的命令权限

同时,才做系统还能够控制进程能够做哪些系统级别的操作。如果普通程序能够执行任何硬件操作:

Program A:
“我要直接改磁盘。”

Program B:
“我要随便访问一个物理内存地址。”

Program C:
“我要把网络设备重新配置一下。”

那整个晋西北都乱成一锅粥了。所以这里开始出现一个以后非常核心的想法:普通应用程序不能够拥有机器的全部权限

但你不给人家权限,人家也确实需要用啊?例如Edge,作为一个浏览器当然需要读文件,访问网络,申请内存,使用设备,我们该怎么办?详见一会会讲到的System Call。


第三个问题:如何让每个程序都能感觉自己在 “畅通无阻的运行”?

我们一开始就说过,只要切的够快,A,B,C都各跑一点,进程就会感觉我拥有整个CPU计算使用权的感觉。虽然说,一个CPU正在被很多进程共享,但每个进程看起来的视角就是 “我有 CPU 可以执行我的代码”。如果你学过OOP,这样的抽象思路对你来说大概会很熟悉吧。


最后, OS如何重新取得CPU的控制权?

这个问题更深入一些。因为我们之前说过,OS也是一个应用程序,CPU一视同仁,有时候OS也是需要让开,允许其他进程执行。

如果OS批准了进程A运行,按照算法分配了10ms的 CPU 时间。OS约定进程A运行10ms后,将CPU还给OS,听起来一切都很美好。

但如果A是恶意程序,里面写了个 while(true),你该怎么办?所以OS很明显,不能够依赖程序自己乖乖的将CPU交回来,否则任意进程都可以把整个系统占死。

因此,即使正在运行的进程不配合,OS 也能强制重新获得 CPU 控制权 。对于如何实现,暂且不表,不过我们要先记住这个事。


内存调控

在冯诺依曼架构的计算机下,一个正在运行的进程,它的指令和数据都一定处于内存中,CPU才能够执行它。我们说的 “内存”,在这里涵盖缓存和内存。

在之前计组我们就知道,计算机如果要执行某个操作,需要将指令从硬盘中捞到内存中,随后还要决定是否写入CPU缓存。但问题出现了:内存空间是有限的,但很多进程会同时运行。OS需要决定哪些内容需要存在内存中,哪些被踢出去。

如果A,B,C三个进程各要 4, 3, 5 GB的数据,加起来OS要分配的空间就来到了12GB。但假如我们机器只有8GB,OS就需要必须解决谁有权利留在内存里。这样做的目标,就是最优化CPU的利用率,同时也能让进程的调度在用户侧表现得尽可能快。

为什么说是优化CPU利用率?是因为我们尽量 别让 CPU 因为缺数据、缺可运行任务而闲着。用户侧的相应,代指的是:用户点了东西以后,别等半天才有反应

你也喜欢一句话总结吗?太好了!总之就是

在有限 RAM 下,OS 怎么组织运行中的程序,既保证能运行,又尽量高效、响应快。


那么,OS如何为不同的进程分配同一个物理内存池?

虽然大家都在枪同一块RAM,且虽然真实物理内存是共享的:

1
[ A ][ B ][ OS ][ C ][ free ... ]

但是A进程不应该想B在哪个物理地址,OS在哪,且我是不是要避开别人的地址?

OS的工作就是提供一种抽象,为每一个进程分配一块它的独立内存空间。在物理上这些内存共享,但是逻辑上尽量让每个进程感觉自己有独立空间


同时,我们如何保证 应用程序该如何访问内存空间?

我们当然不想让进程A有权限写入进程B声明的内存空间。如果A正在处理你的银行密码,B是一个随便下载的软件正在运行。如果B可以随意选择写入任意内存地址,那这简直胡闹了。

因此,OS不仅要为每个进程分配内存,还需要保证每个进程 只能碰你被允许碰的内存地址


那么如果 总进程所需要的内存,超过了物理内存大小怎么办

听说过虚拟内存吗?当你的RAM不够,OS会把一部分暂时不着急用的一部分内容,放到更大但写入读取更慢的二级存储,往往是你的SSD或者机械硬盘,需要时再拿回来。OS用慢但大的存储,去缓解快但小的内存不足。


最后,OS如何管理空闲内存空间?如果我们真的没内存可用了,OS该怎么办?

OS还需要管理并记住,哪些RAM已经被占用了,哪些可用,且哪些RAM可以被回收器回收。

一般来说如果要没内存了,OS会尝试从一些进程那里安全收回一些RAM。


因此,内存管理方面,OS至少需要做:

  • 内存分配 (Allocation):决定给谁内存,怎么给?
  • 内存追踪 (Tracking):追踪谁正在用哪些内存,这些内存现在用的什么情况?
  • 内存保护 (Protection):谁不能够去碰哪些内存,将进程访问内存地址的权限框起来。
  • 内存回收 (Reclamation):决定哪些内存可以被回收,以再次分配
  • 内存过量使用管理 (Oversubscription handling):如果总需求大于目前的物理内存大小,OS该如何调控?

这就是内存管理。在一个有限的物理内存池中,有很多的进程都需要用内存。OS要解决

  1. 怎么分
  2. 怎么隔离
  3. 怎么制造 private memory 的抽象
  4. RAM 不够时怎么办
  5. 怎么回收

并发

我们在用电脑的时候,不是说我们只能在一件事做完之后,才能再做下一件吧。

并发 (Concurrency) 就是这样一个描述多个执行任务在时间上重叠推进,交互的词语。实际上,OS 自己内部也不是永远“一件事情做完,再做下一件”的,这样效率就有点太难看了。

虽然我们之前讨论,内存管理决定A和B的数据应当独立管理,但是当并发进程出现时,很多时候你需要实现进程与进程之间的信息交换。这样,你就不可避免地需要让A和B同时共享某些数据。

那么问题来了,我们如何保证 它们同时访问时不会把数据弄坏

假设OS内部有一个变量,叫做 opencount。每次有人调用一个函数 open(),这个变量就+1。

如果有两个进程几乎同时调用 open(),因为系统中断可能在任意时候发生,所以对这个共享变量的更新就可能受到干扰;因此OS内核内部的进程列表、分页表等数据结构都必须非常小心地用同步基元访问。

假设 opencount = 10,A要加1。但在底层,整个流程大概分三步:

1
2
3
1. 读 opencount
2. 算 +1
3. 写回 opencount

如果A现在 read 10,还没写回,B就触发了他的流程:

1
2
3
B read 10
B computes 11
B writes 11

然后切回A:

1
2
3
A had already read 10
A computes 11
A writes 11

最后你会发现 opencount = 11,这很显然是出错了,正确值应当是12。

所以OS需要协商不同的进程在使用同一个数据的时候,该如何协商,按照什么顺序去修改数据。


文件管理

物理磁盘的底层不是分我们可见的 xxx.txt 管理的。我们熟知的文件,在底层存在不同的存储介质,不同的扇区,块和不同地址。搞半天还要自己拼。

OS中负责管理存储的部分叫做 文件系统 (File System),它负责把用户创建的数据可靠且高效地存到磁盘上。

所以我们的 xxx.txt ,就是文件系统提供的一个抽象层及。file,directory,filename都是文件系统抽象出来的概念。当我们调用 打开 report.pdf 时,文件系统就需要懂得:

它的数据实际存在哪里?
哪些 disk blocks 属于它?
怎么找到?
怎么读?


所以文件系统要做什么具体的活?

首先,它该如何管理存储空间?我们创建一个新文件,它应该找那些存储单元?我们删除一个文件,空出来的空间如何管理?

因此文件系统也需要一个类似于内存分配的机制。同样需要检测空余/占用的空间,哪个file占哪些位置?删除之后如何收回?


第二,我们该如何实现文件系统的数据结构?换句话说,给定一个文件名,我们如何完美读取这个文件的内容?

由于我们给定的是一个抽象文件,文件系统需要搞清楚,它应该去存储的哪些地方找数据?因此,文件系统本身必须在硬盘上维护元数据和数据结构。


第三关乎性能。我们该如何降低整个存储系统的读写频率,以达到更高的性能?要知道外存的性能不是很理想,所以我们能否减少实际的硬盘访问次数,让每次访问都更高效;或者单纯让整个文件系统看起来更快?


纵观进程,内存,并发和文件管理,OS现在在这四个维度上做的事情都挺统一的。

  • 进程:多个进程如何共享CPU?
  • 内存:多个进程如何共享有限物理内存?
  • 并发:多个进程如何安全共享共有数据?
  • 文件:多个进程如何可靠高效使用存储系统?

本质上都在研究 共享,抽象,控制,保护,效率 ,仅仅是要管理的目标不同。


操作系统的终极目标

就是确保四个维度最大化:

  • 易用性:系统作为一个界面,对用户和开发者,乃至应用程序来说是否易用?
  • 效率:系统如何调配资源,才能让整个系统的运行速度更快,或者在其他方面特调最佳体验?
  • 安全性与可靠性:系统如何使用各种硬件配置,各种奇技淫巧来做到系统的稳定不出错,同时做到尽可能安全?
  • 泛用性:系统能否运行在各种不同的硬件配置下,且都能最大限度发挥最强大的资源调配能力?

不过不同的目标之间甚至会互相冲突。例如更强的隔离性会让安全性更强,但某种程度上会增加检查和切换开销。更复杂的抽象会提升泛用性和易用性,但是会牺牲一部分的性能。

这就是为什么不同的操作系统架构设计会有所权衡,而不是说有一个所谓 “万金油” 架构。


操作系统架构

随着操作系统不断变复杂,它需要支持越来越多的软件、硬件和系统服务。要是所有东西都随便堆在一起,最后维护起来大概会变成屎山中的屎山。

所以,操作系统架构 (OS Architecture) 研究的,就是 操作系统内部的各种功能组件应该如何组织?它们之间应该如何通信?哪些组件应该拥有较高权限?

课件特别指出,OS architecture 一方面负责组织操作系统的各个功能组件,另一方面还要决定每个组件在什么 Privilege Level(权限级别) 下执行。

这节课主要介绍四种架构:

  • Monolithic Architecture
  • Layered Architecture
  • Microkernel Architecture
  • Modular Approach

在进入它们之前,我们需要先搞清楚两个非常重要的概念:

User Mode 和 Kernel Mode

系统内核 (Kernel) 不等于整个 OS。在一个操作系统中,并不是所有“系统相关程序”都拥有最高权限。

课件给出的结构大概可以理解成:

1
2
3
4
5
6
7
Applications / Shell

Libraries / APIs / Utilities

Kernel

Hardware

其中,Kernel才是 OS 的核心部分。它负责管理资源,并且直接和硬件交互。另一方面,像一些 工具与服务程序本身并不直接操作硬件,而是利用 kernel 提供的功能。

例如课件列出的,Windows系统下的:chkdsk,format,ps都属于 non-kernel programs。所以:Operating System 不能与 Kernel划等号。

Kernel 是 OS 中处于核心特权位置、负责资源管理以及硬件控制的部分。


CPU 至少支持两种运行模式。一个叫 User Mode,另一个叫Kernel Mode。这并不是 OS 自己随便规定的“软件规范”,而实际上是 CPU 提供的硬件权限机制。

Kernel Mode

当 CPU 正在执行 OS kernel 时,它工作的模式就在 Kernel Mode。 aka Privileged Mode /Supervisor Mode。它拥有最高的权限等级,可以执行机器能够执行的全部指令。

也就是说:Kernel 可以执行一些普通应用没有权力执行的系统级操作。

User Mode

而当CPU正在执行普通程序时,它处于User Mode。User Mode 权限更低,只允许执行机器所有命令的一个受限子集。但也不要误解成:“User Mode 很废,只能做很少的事情。”

实际上应用的大多数计算都可以直接在 User Mode 完成。例如:

1
2
3
4
5
6
7
8
9
a = b + c;

if (x > y) {
...
}

for (...) {
...
}

这种牵扯算术,比较,分支,或者程序访问自己允许访问的内存地址时都可以直接执行。

所以User Mode 限制的是危险和需要高权限的操作,而不是普通计算能力。

之所以这么设计,是为了安全性考虑。如果所有应用都能够执行任意高级指令,那么:Chrome随便控制你的硬盘,Minecraft随便改分页表,某个恶意软件随便干废你的硬盘,整个系统的 安全性就不存在了。

所以,普通程序可以自由计算,但是 涉及全局资源和危险系统状态的时候,必须交给 Kernel

系统调用

问题来了:普通 application 没有最高权限,但是它又确实需要执行打开文件,访问磁盘,I/O,使用系统资源等其他操作,难不成不给它用了?

应用可以向 OS 发出一个正式请求,请求调用相应的Kernel能力,这就是 系统调用 (System Call)

系统调用是应用向 OS 请求资源或服务的机制,同时,所有系统调用构成了应用与 OS 服务之间的接口。

执行过程大概是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
应用
User Mode

系统调用

Trap into Kernel

Kernel Mode

执行对应的 Kernel 函数

Return-from-trap

应用
User Mode

这里的 trap 可以理解为一种,CPU 从 User Mode 受控地切换进入 Kernel Mode 的一种机制。

不是说通过trap,应用就可以随便跳进 Kernel 的任意代码,而是说CPU通过特殊指令来进入预先规定好的 kernel 进入点。执行完之后,再通过 return-from-trap 回到 User Mode。


所以现在我们终于可以建立一个很重要的执行模型,也就是说,CPU 并不是一直在运行 OS。

有时 CPU → User Mode → 执行应用

有时 CPU → Kernel Mode → 执行 Kernel

应用绝大多数时间可以在User Mode里面自己计算。只有当:需要 OS 层面的内容需求时,才通过系统调用进入 kernel。

总而言之,CPU 可以直接执行应用的 user-mode 代码。当只有需要特殊服务时,才会切到 Kernel Mode。


Monolithic Architecture

我们首先来看最直接的一种设计:Monolithic Architecture(单体式架构)

单体式架构中,几乎所有主要 OS 部件都直接放在 kernel 中。整个 OS kernel 更像一个巨大的库,而且这些组件之间没有非常严格的模块边界。

这是LLM画的一个图例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Application
↓ System Call

========================
Kernel

Process Management
Memory Management
File System
Device Management
...

Component A
可以直接 call Component B

Component B
可以直接访问共享数据结构

========================

Hardware

由于全部都在同一个 kernel 中,一个部件可以很方便地通过函数调用直接和另一个部件通信。而且由于大家在同一个大程序里,数据结构也很容易共享。

单体式结构的首要优点就是快,效率高。假设文件系统需要使用硬盘驱动,由于他们都在一个Kernel中,文件系统可以直接通过函数调用的方式找到硬盘驱动,直接通讯后硬盘驱动去调用硬盘。由于大家都是邻居,没有太多额外的通信边界,因此开销就很小。这就是为什么单体式架构的OS往往具有很高的性能。

但问题就是:大家关系太亲密了。高性能是用很弱的组件边界换来的。在传统 monolithic structure 里:

  • 组件可以直接和硬件交互
  • 重要的结构性系统代码实现可能散布在 kernel 各处
  • 组件可以直接访问其他组件的数据或方法
  • 一个组件的修改可能影响其他组件
  • 一个组件的 bug 可能破坏另一个组件

因为大家都住一个屋里,性能当然不错。但某个室友发疯的时候:整个屋子一起遭殃。

因此,单体式结构的设计就是 使用较弱的隔离性,换取更直接的组件通信和更高性能。


Layered Architecture

既然 Monolithic 的问题是 “所有组件都能随便碰彼此”,一个很自然的回应就是:那我们把上下级关系规定死。这就是 多层式结构 (Layered Architecture)

它把执行相似功能、承担相似角色的组件收进同一个 module,然后一层一层叠上去,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Layer 4
Application-facing Services

Layer 3
File System

Layer 2
I/O Services

Layer 1
Hardware-related Services

Layer 0
Hardware

最关键的一条限制是:每一层只允许和它紧邻的上下层通信

所以 Layer 4 不能突然把 Layer 3,Layer 2,Layer 1 全部绕开,直接跳下去访问 hardware,它必须按规定一层一层走下去。

这样一来,系统的结构就清楚很多。因为每一层的责任都很明确,construction 和 debugging 都会简单很多。课件甚至指出,我们可以从最低层开始往上 debug:先确认 Layer 0 是对的,再加入 Layer 1 确认,再加 Layer 2 ...... 一层一层把系统搭出来。

Layered Architecture 还顺手带来了一个很重要的软件工程概念:Information Hiding

上层只需要知道 “下一层向我提供什么 interface”,而不需要知道下一层内部到底怎么实现。例如 Layer 3 只知道 lower_layer.read(),至于 lower_layer 内部到底是查缓存还是翻磁盘,它并不关心。

所以只要 interface 不变,下面从 Implementation A 换成 Implementation B,上层理论上都不需要修改。

这其实和我们在 OOP 里学到的 Abstraction,Encapsulation,Information Hiding 是同一套逻辑。


但严格分层也不是白送的。

一个 application request 可能要 Layer 5 -> Layer 4 -> Layer 3 -> Layer 2 -> Layer 1 -> Hardware 一路走完才算完成,绕的路明显变多。课件因此直接指出,Layered systems 的 efficiency 可能比 monolithic kernel 更低。

所以又是一个对照:Monolithic 更直接,更快,但内部关系更乱;Layered 结构清楚,容易维护,但可能需要绕更多路。

Virtual Machine

课件随后用 Virtual Machine 作为 Layered Approach 的一个例子。

里面有一个核心组件叫 Virtual Machine Monitor (VMM),也叫 Hypervisor。Hypervisor 干的事情,其实和我们之前学过的很多 OS 抽象很像:把一套真实硬件,包装成多个 “看起来像独立机器” 的虚拟环境。

1
2
3
4
5
6
7
8
             VM 1
OS 1

Real HW -> Hypervisor -> VM 2
OS 2

VM 3
OS 3

真实情况是 CPU 只有这一套,Memory 只有这一套,Disk 也是这一套,但是每个 Virtual Machine 里面的 OS 都会感觉 “我下面有一台自己的机器”。

所以课件用了一个特别好记的说法:Hypervisor essentially serves as an OS for OSs

仔细看你会发现,这和之前的 OS 抽象套路一模一样:

1
2
3
4
5
6
7
8
1 CPU
-> 多个 Process 感觉自己都能运行

一池 Physical Memory
-> 每个 Process 感觉自己有独立的 Memory Space

一台 Physical Machine
-> 多个 OS 感觉自己都有一台 Machine

OS 真的很喜欢制造这种 “其实没有,但是我让你感觉你有” 的东西。

Microkernel Architecture

然后我们来到了另一个非常重要的思想:Microkernel。它几乎是从 Monolithic architecture 的问题反向推出来的。

Monolithic 会把大量组件放进 kernel,privileged code 越多,高性能是拿到了,但 bug 的影响范围也巨大。于是 Microkernel 提出:我们为什么不尽量少往 kernel 里面放东西?

课件的定义是:尽可能把 functionality 从 kernel space 移到 user-space processes 中,而这些 processes 被称为 servers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
User Space

Application
File Server
Other Servers
...

========================

Kernel

bare essentials

========================

Hardware

Kernel 里只保留 bare essentials,也就是最基本、确实必须以高权限运行的东西。

这样做的最大好处就是 隔离。如果某个 File Server BOOM 了,由于它只是一个 user-space process,File Server crash 并不等于整个 kernel 一起 crash,至少从架构设计目标上,它的故障影响范围会小很多。

因此课件给出的优点包括 Extensible,Portable,Scalable,More Secure,More Reliable。其中最重要的原因其实就是一句话:less code is running in kernel mode。这非常符合我们之前一直说的原则,privilege 越高,代码越危险,所以 privileged code 越少,potential blast radius 就越小。


但是事情又回到了 trade-off。

假设 file system 被放到了 user space,它就不能再像 Monolithic 一样 File System -> direct function call -> Disk Driver 叫一声就完事,而是不同 user-space server 之间需要 Message Exchange,而这些 communication 又需要 kernel 的帮助。

于是一次请求可能变成 Application -> Kernel -> File Server -> Kernel -> Other Server ...,多绕了好几趟,这就产生额外的 communication overhead。

所以 Monolithic 和 Microkernel 几乎形成一个非常漂亮的对照:

1
2
3
4
5
6
7
8
9
10
11
12
13
Monolithic

more code in kernel
-> direct communication
-> high performance
-> larger failure impact

Microkernel

less code in kernel
-> stronger isolation
-> better reliability
-> more communication overhead

这里不存在 “哪个绝对先进?”,只有:你愿意把系统设计往哪个目标偏?

Modular Approach

最后一个架构是现实中非常重要的:Modular Approach

这个思路就非常现实主义了。Monolithic 性能很好,但是内部太乱;Layered 很整洁,但是约束有时太死;Microkernel 隔离很好,但是 communication overhead 又明显。那能不能保留 Monolithic kernel 的性能,同时把 kernel 内部组织得更模块化?

课件指出,大多数现代 OS 仍然整体采用 Monolithic architecture,但内部使用 Kernel Modules。于是整个 kernel 变成:

1
2
3
4
5
6
7
Kernel

├── Module A
├── Module B
├── Module C
├── Module D
└── ...

每个主要组件被分开实现成一个 module,但本质上整个 kernel 就是这些 modules 的集合。

它和 Layered 很像,因为大家都有明确的组件和明确的 interface。但 Layered 很严格,Layer 4 只能找 Layer 3,Layer 3 只能找 Layer 2 / 4;Modular Approach 就自由得多,课件明确说 any module can call any other module,所以 Module A -> Module C,Module C -> Module B,Module B -> Module D 都可以,只要按照 well-defined interface 来就行。因此 Modular 比 Layered 灵活。

它和 Microkernel 的区别也很清楚:Modular kernel 的 component 仍然都在 kernel 中,所以不同 module 之间 Module A -> direct kernel call -> Module B,不需要像 user-space servers 一样频繁进行 message passing。课件因此说,communication between modules is more efficient because they are all in kernel。

于是它在相当程度上同时获得了 Monolithic 的 efficiency 和 Layered / Modular design 的 clear interfaces。

Module 还有一个很爽的能力:Dynamically Loadable Modules。某些 feature 需要就 load,不需要就不加载,这样只需要把真正需要的 module 放进 memory。于是节省了 memory,组件可以独立修改,新 module 更容易加入,整体 extensibility 也更好。课件给出的例子包括 Solaris,Linux 和 traditional Unix kernels。

四种架构的本质区别

到这里,这四种架构可以压缩成一段段对照:

Monolithic 把大量 OS 组件放在同一个 kernel 里,换来高性能和直接调用,代价是隔离弱、故障影响大、难维护;Layered 让系统严格分层、只与相邻层通信,换来清晰、易 debug 和 information hiding,代价是请求可能经过很多层;Microkernel 让 kernel 只保留必要功能、其余移到 user-space server,换来隔离、安全、可靠、易扩展,代价是 message passing overhead;Modular 则是在 monolithic kernel 内部划分 kernel modules,算是性能和模块化之间的折中,但 module 仍然运行在 kernel 里,所以故障隔离要弱于 microkernel。

不过现实中的 OS 并不是在上面四个里四选一,真实系统经常会混合使用不同架构思想。课件最后展示 Windows,Linux,macOS 和 Android 的 architecture diagram,就是让我们看到真实 OS 内部通常远比理论分类复杂。最后的 Summary 也明确指出,现代 OS 往往仍然以 Monolithic approach 为基础,同时引入 Layers 和 Kernel Modules 来提升 extensibility。

所以真正应该理解的是:OS Architecture 并不是选一个名字贴上去,而是在 performance,isolation,reliability,maintainability,extensibility 之间不断权衡。

Chapter 1 小结

整个 Chapter 1 其实都围绕一个很统一的问题:我们有一堆有限、复杂、而且需要很多程序共享的 Hardware Resource,OS 到底应该怎么把它们管好?

CPU 只有这么多,所以 OS 要调度 Process;Physical Memory 只有这么大,所以 OS 要分配、隔离、回收 Memory;不同 Execution Flow 要共享数据,所以 OS 要解决 Concurrency;硬盘底层又不好用,所以 OS 给我们抽象出 File System。与此同时,OS 还不能为了方便就把整个机器的权限全部送给 Application,所以又需要 User Mode,Kernel Mode 和 System Call 来控制权限边界。


最后 Kernel 自己越来越复杂,我们又开始研究:Kernel 里面到底应该放多少东西?Component 之间到底应该靠多近?我们愿意拿多少性能去换多少隔离性?这才有了 Monolithic,Layered,Microkernel 和 Modular 这些 Architecture。

所以如果真的要用几个词概括这一章,大概就是:

Resource Management / Abstraction / Protection / Isolation / Efficiency / Trade-off

后面的 OS 内容,基本都会继续围着这几个东西打转。