土法炼钢兴趣小组的算法知识备份

【操作系统百科】什么是操作系统:从 monitor 到 unikernel 的职责清单

文章导航

分类入口
os
标签入口
#operating-system#kernel-architecture#unikernel#vmm#serverless#abstraction#resource-management

目录

你在 AWS Lambda 上跑一段 Python。请求来了,函数执行,返回。整个过程里你没装 systemd、没调 /proc、没 apt install。看起来,“操作系统”这件事好像消失了。

但你稍微往下挖一点就会发现:Lambda 把你的函数跑在 Firecracker microVM 里;Firecracker 里是一个裁剪过的 Linux 内核,启动时间 125ms;这个 Linux 上跑着一个 Python runtime,由一个极简的 init(不是 systemd,是 Firecracker 自己写的一个 /sbin/init)拉起来。你的”一行函数”下面,仍然压着一整套 OS。

这就是 2025 年的 OS 景象:向上,OS 被越来越厚的 runtime / 容器 / serverless / AI framework 遮住;向下,OS 要面对 CXL、DPU、机密计算、异构加速器等越来越多样的硬件。 它不仅没消失,反而在”被隐藏”和”变复杂”这两个相反方向上同时加速。

这篇文章不是教材式的定义,而是要回答一个工程问题:OS 到底在做什么? 我们会拆出五件它不得不做的事情,然后拿这五件事去丈量宏内核、微内核、VMM、unikernel、serverless 各种形态,看谁做了哪些、代价是什么、边界在哪里。这既是本系列的开篇,也是一把后续几乎每一章都要反复用到的尺子。

一、先破除两个常见定义

打开任何一本教科书,“操作系统”一般被定义成两件事之一:

定义 A(资源管理视角):操作系统是管理计算机硬件资源、为应用程序提供服务的系统软件。

定义 B(抽象机器视角):操作系统在裸机之上构造一台”抽象机器”,让应用程序以更方便的方式使用硬件。

这两种定义在教学里没错,但在工程里都容易误导。

1.1 “资源管理” 掩盖了隔离和安全

“管理资源”听起来像是一个纯粹的调度 / 分配问题。但 Linux 内核源码里,与”资源管理”直接相关的代码(调度器、内存分配、I/O 调度)加起来不过几十万行,而整个内核是 3000 万行量级(Linux 6.6 统计约 3400 万行)。剩下的代码在干什么?大部分在做驱动、安全、隔离、可观测、协议栈——这些都不是狭义的”资源管理”。

如果你以”资源管理”为视角,会很容易忽略一个事实:现代 OS 的绝大部分复杂度,来自于”在不信任的用户空间之间隔离资源”,而不是”分配资源”本身。分配一页内存是简单的;在多个 cgroup、多个 namespace、多个用户之间把这页内存准确地归属、计费、限流、回收,才是真正的难题。

1.2 “抽象机器”掩盖了透明度

“提供抽象机器”是一种美化。实际上,OS 提供的抽象经常在 。mmap 的性能取决于页缓存脏页比例;read() 的延迟依赖于块层的队列深度;accept() 的尾延迟来自于 softirq 的堆积;fork 的成本主要在页表复制。

一个好的 OS 不是提供”完美的抽象机器”,而是在抽象之下 留出足够多的观察孔和调节旋钮,让工程师可以在需要时看透抽象,调整行为。Linux 上散落在 /proc、/sys、tracefs 里的数万个可调参数,就是这种透明度的体现。OS 的职责之一,是管理自己的抽象漏点。

1.3 回到工程视角

如果把上面两条教科书定义合起来再加上工程补丁,我们会得到一个更贴近现实的表述:

操作系统是一段常驻的、具备特权的软件,它在硬件之上为多个互不信任的工作负载构造可以复用、可观测、可治理的执行环境。

关键词是五个:特权、多个、互不信任、复用、可治理。少掉任何一个,就不是一个完整意义上的 OS——而是某种退化形态(下文会展开)。

下面这张图把本系列反复会用的”五件事”摆出来,便于下面章节对照。

flowchart LR
    App[应用进程] --> K{OS 内核}
    K --> A1[抽象<br/>fd / 虚地址 / task]
    K --> A2[复用<br/>多路调度 / 分时]
    K --> A3[隔离<br/>ns / cgroup / MMU]
    K --> A4[策略<br/>调度器 / 回收 / 限流]
    K --> A5[可观测<br/>proc / tracefs / eBPF]
    A1 --> HW[(硬件)]
    A2 --> HW
    A3 --> HW
    A4 --> HW
    A5 --> HW

图中五个分支是下文评估所有 OS 形态的五把尺子:任何一个 OS(宏内核、微内核、unikernel、serverless)最终都要解释清楚它把这五件事各自做到了什么程度、以什么方式做。

二、OS 的五件事

我把现代 OS 必须做的工作抽象成五件事。顺序是有意义的:后一件通常依赖前一件。

2.1 资源抽象(Abstraction)

CPU 是一组寄存器与 ISA;磁盘是带坏块、带 ECC、带通道的一堆 NAND;网卡是一个带收发队列的 PCIe 设备。应用程序不应该直接面对这些东西。OS 把它们抽象成:

这些抽象之所以有用,是因为它们 稳定且可组合:无论底层是 HDD 还是 NVMe,read(fd, buf, n) 的语义不变;无论底层是 Ethernet 还是 InfiniBand,send/recv 的语义不变。稳定的 ABI 是 OS 的产品接口。这条我们会在后面章节(A-05 syscall ABI、J-86 稳定 ABI)展开。

注意:抽象不是免费午餐。read(2) 与 io_uring 对同一块磁盘 I/O 的性能差可以到 3-5 倍。抽象选得不好就会卡住上层性能。这也是为什么 io_uring、XDP、DPDK 这些”旁路 OS 抽象”的机制会出现。

2.2 资源复用(Multiplexing)

一台机器上有 100 个进程,它们共享 16 个 CPU 核、64GB 内存、2 张网卡。复用是 OS 的日常:

复用的核心挑战是 切换开销。一次完整的进程上下文切换在 x86_64 上大约 1-3μs,包含寄存器保存、TLB 刷新(部分)、调度器决策。在高频切换场景(比如每秒百万级请求的网络服务),切换开销本身可能成为瓶颈。这是为什么出现了协程 / 用户态调度、ktls、io_uring SQPOLL 等减少切换的机制。

切换开销不是 OS 设计的”浪费”,而是”复用”的税。你可以通过少让 OS 参与来降低这个税(unikernel、kernel-bypass),但你也就放弃了复用带来的好处。

2.3 资源隔离(Isolation)

如果只有一个工作负载,OS 大部分代码都不需要写。OS 的存在感 80% 来自”隔离”

隔离有多种层次:

  1. 地址空间隔离(MMU + 页表,D-30)
  2. 权限隔离(ring0/ring3、EL0-3,A-04)
  3. 资源配额隔离(cgroup v2,B-18)
  4. 命名空间隔离(namespaces,B-17)
  5. 强制访问控制(SELinux / AppArmor,L-97)
  6. 硬件辅助隔离(VMX/SVM、SEV-SNP、TDX,L-101、M-108)

每一层隔离都对应一种”攻击面”或”故障传播面”。实际部署中往往是多层叠加:Kubernetes pod 就同时依赖 cgroup、namespace、seccomp、SELinux、AppArmor 至少五种机制。隔离做不好的代价有三种:

这三种是本系列反复出现的受害者。一个好 OS 的标志,是能把”隔离失败”限定在一个有限的范围并快速恢复。

2.4 公平与策略(Policy)

隔离保证”互不干扰”,但资源总量有限。OS 必须在用户之间做取舍。调度、内存、I/O、网络四个维度都有自己的公平策略:

“公平” 不是唯一可能的策略。实时系统里更常见是 优先级预算保证(C-23 SCHED_DEADLINE)。多租户系统里是 配额。OS 的价值不在于选某一种策略,而在于把 策略机制 分开:机制是如何切换、如何计量、如何限流;策略是”谁得多少、谁优先”。这种分离(由 Mach 和 L4 这一脉微内核推向极致)让同一套机制可以服务不同的策略。

2.5 可观测性(Observability)

第五件事在教科书里常常缺席,但在工程里比前四件都重要:如果 OS 不可观测,前四件事都无法验证。你说调度是公平的?拿出数据。你说内存足够?给我 MemAvailable 的定义。你说 I/O 队列深度?打开 /sys/block//queue。

现代 Linux 把可观测性做成了几个子系统:

这些机制加起来构成了 Linux 的可观测性平面。FreeBSD 的 DTrace、Solaris 的 kstat/mdb、macOS 的 os_signpost 是同一个职责的不同实现。一个不可观测的 OS 等于一个不可调试的 OS,在生产里是不可接受的——这是为什么许多实时/嵌入式 OS 在走向产品化时都在补可观测性。

三、五件事的”组合学”

五件事之间不是并列关系,而是 依赖 + 权衡 的关系。拿一些实际的 OS 形态来丈量:

┌───────────────────────────────────────────────────────────────────────┐
│                     抽象   复用   隔离   策略   可观测                 │
├───────────────────────────────────────────────────────────────────────┤
│  裸机 monitor (1950s)     ●    ◐    ○    ○    ○                     │
│  单用户 DOS               ●    ○    ○    ○    ◐                     │
│  Unix V7                  ●    ●    ◐    ◐    ◐                     │
│  宏内核 Linux 6.x         ●    ●    ●    ●    ●                     │
│  微内核 (Mach/L4)         ●    ●    ●    ●    ◐                     │
│  VMM + Guest OS           ●    ●    ●    ●    ●                     │
│  Unikernel                ●    ○    ○    ◐    ○                     │
│  Serverless (Lambda)      ●    ●    ●(VM)●(配额)●(平台侧)           │
└───────────────────────────────────────────────────────────────────────┘
 ● 一等公民   ◐ 有限实现   ○ 基本没有

读这张表的关键在于 注意空缺

这张表的结论:OS 的五件事可以被”外包”给其他层次做(虚机、容器、平台),但它们不会凭空消失。这是为什么 unikernel / 轻量容器 / serverless 的成功,恰恰依赖一个健壮的 guest OS 或 host OS。

四、“一切皆文件”和它的暗面

Unix 的口号”一切皆文件”(everything is a file)是 OS 抽象设计的一个经典范式。进程通过 fd 操作文件、管道、socket、字符设备、甚至 Linux 上的 /proc/self/mem、signalfd、timerfd、eventfd、pidfd。

这种抽象的好处是显而易见的:

  1. 统一 API:read / write / close / poll 一套接口应对一切
  2. 可组合性:管道、重定向、fd-passing 构成了 Unix shell 生态
  3. 可观测性:lsof 一条命令看所有进程持有什么

但这个抽象 。下面列四个最大的漏点。

4.1 “文件”抽象不含生命周期信号

read(fd) 只能返回”已到的字节数”。它告诉不了你”对端主动关闭”与”对端超时”、“对端 OOM 被杀”的区别。Unix 需要一个带外的 errno、SIGPIPE、epoll EPOLLRDHUP 等补丁才能表达这些语义。这是 Unix 抽象最大的失败之一,它导致几乎所有网络服务都要手写一套”心跳、超时、重连”的上层协议。

4.2 “文件”抽象不含事务语义

write(fd, buf, n) 不保证原子性,除非 n <= PIPE_BUF(通常 4096)。对一个普通磁盘文件,write(2) 甚至可能部分写入。fsync、fdatasync、O_SYNC、O_DIRECT 四种机制叠加才勉强表达”写落到稳定存储”这个事务语义——这在存储百科 11-linux-async-io、12-data-integrity 里有完整讨论。

4.3 “文件”抽象不含类型系统

Unix 里,open(“/dev/kvm”) 和 open(“/etc/passwd”) 返回同一种 fd。类型信息隐式地附加在 ioctl 的命令号里。这种”弱类型”让 Unix 系统编程既灵活又危险——errno=ENOTTY 的根因几乎一定是这种类型混淆。Plan 9 的 9P 协议试图修正这个问题,把类型与操作绑定;NT 的 handle + object type 也走了类型化路线;但 Linux 没有采用。

4.4 “文件”抽象不含命名空间

open(“/etc/passwd”) 解析到哪个 inode,取决于进程的 mount namespace、pid namespace、user namespace、cgroup namespace、time namespace 的组合(B-17)。这在容器时代是特性,但在 shell 脚本时代是陷阱——许多古老的 sysadmin 脚本假设路径是全局的。namespace 的引入是把”一切皆文件”这个口号从哲学信仰降级到工程事实

五、内核架构谱系概览

这一节只做地图级介绍,后续 A-03 会把每一种架构详细拆开。

5.1 宏内核(Monolithic)

代表:Linux、FreeBSD、AIX、Solaris 内核本体。所有内核功能(文件系统、网络栈、驱动、调度、内存、IPC)运行在同一地址空间,同一特权级。

优点:最简单、性能最高。子系统之间直接函数调用,没有 IPC 开销。 缺点:爆炸半径最大。一个驱动 bug 可以 panic 整机。

Linux 通过内核模块、命名空间、eBPF 等机制把”宏内核”变得越来越接近”可组合的宏内核”,但本质没变。

5.2 微内核(Microkernel)

代表:Mach、L4 家族(seL4、Fiasco、OKL4)、QNX、Minix 3。内核只保留 IPC、调度、基本内存管理;文件系统、网络、驱动运行在用户态。

优点:隔离性强、可验证(seL4 是第一个代码级证明正确性的 OS 内核)。 缺点:IPC 开销 → 性能代价。L4 第一代 IPC 开销高得让微内核在 90 年代几乎出局;L4 后续家族把 IPC 优化到与系统调用同一数量级,才在嵌入式与安全市场站稳。

5.3 混合内核(Hybrid)

代表:XNU(macOS/iOS)、Windows NT。架构上保留微内核思路(Mach、NT Executive),但把高性能路径直接放在内核地址空间,逻辑上用”object + message”组织。实际是宏内核的性能 + 微内核的组织。

优点:平衡。 缺点:两种哲学的夹缝里,代码复杂度高。

5.4 Exokernel

代表:MIT Exokernel、Nemesis。内核只”保护”资源,不”抽象”资源;抽象留给用户态的 libOS。unikernel 是 Exokernel 思想的现代继承者。

5.5 Unikernel

代表:MirageOS、IncludeOS、Unikraft、OSv。一个应用 + 它需要的 OS 原语编译成单一二进制,直接跑在 VMM 上。没有多进程、没有用户态/内核态区分。

优点:启动快(< 10ms)、攻击面小、内存占用小。 缺点:失去了”多工作负载复用”的能力

5.6 VMM / Hypervisor

代表:Xen、KVM、Hyper-V、ESXi、Firecracker、Cloud Hypervisor。它不是传统 OS,但承担了 OS 的五件事中的前三件(抽象 / 复用 / 隔离),只不过把单位从”进程”换成”虚机”。

趋势:VMM + 精简 guest OS(例如 Firecracker + 轻 Linux)正在”吃掉” OS 的部分传统职责。这是 M-109 disaggregated-os 的伏笔。

六、“操作系统”的边界在哪里?

一个常被问起但没人能精确回答的问题:到底哪些代码算操作系统,哪些不算?

我的工作定义是:

在一台机器上,从加电开始到允许不受信任代码运行之前,所有常驻运行、具有特权访问物理资源能力、并且为后续工作负载提供抽象/复用/隔离/策略/可观测这五件事的软件,都是操作系统。

这个定义包含以下内容:

这个定义的好处是它强调”职责”而不是”代码归属”。按它看,Docker daemon / kubelet / CRI-O 中有一部分代码就是”OS 的延伸”——它们承担了容器运行时的抽象、复用、隔离、策略和可观测。同理,AWS Nitro Hypervisor + Firecracker + init 加起来构成了”Lambda 的 OS”。

这种视角对你的意义:当你在设计一个大型系统(比如分布式数据库、AI 训练平台)时,你会反复问自己”这五件事谁来做?“——如果都推给内核,你会被调度和文件系统拖累;如果都自己做,你会重新发明一个 OS。大型基础软件的本质就是在”复用 OS”与”绕开 OS”之间做权衡

七、OS 怎么启动自己?一个顺序回放

在正式进入后续主题前,把一台 x86_64 Linux 机器从加电到 PID 1 的过程过一遍,可以让这五件事落到具体时刻。细节在 J-81 会展开;这里只画骨架。

0ms      加电 → CPU 从 RESET 向量 0xFFFFFFF0 取指,进入 UEFI 固件
50ms     UEFI 初始化 DRAM、PCI、存储控制器;加载 Boot Manager
200ms    shim/GRUB/systemd-boot 加载 Linux kernel + initramfs(含签名校验)
300ms    decompress、start_kernel() 开始执行
         setup_arch()   —— 建立早期页表、解析 CPU 特性
         build_all_zonelists() —— 注册内存区域(Zone)
         trap_init() / init_IRQ() —— 建立 IDT、中断向量
         mm_init() / sched_init() —— 伙伴系统、SLUB、调度器就绪
         rcu_init()、hrtimer_init()、workqueue_init()
         smp_init() —— 其他 CPU 上线
         rest_init() —— 创建 kernel_init 内核线程(未来的 PID 1)
1200ms   kernel_init 跑完内核 initcall,挂载 rootfs 或 initramfs
1300ms   execve("/sbin/init", ...) —— PID 1 变成 systemd
1800ms   systemd 跑完 default.target,系统就绪,允许外部登录 / 接受请求

每一毫秒里 OS 都在积累职责:先有”抽象”(建立 CPU/内存的内核表示),再有”复用”(调度器、软中断),接着”隔离”(SELinux policy、namespace 基座),最后”可观测”(sysfs/procfs 可用)。当你看到 systemd[1]: Reached target Multi-User System. 时,OS 的五件事才真正齐备。

这个时间线会在 J-81 用更细的 trace 重新走一遍;这里提前画出来,是为了让你对”OS 初始化”这件事有一个量级感——大多数机器的前 2 秒钟都在做 OS 的自举。serverless 把这个时间砍到 < 200ms 的办法,不是”更快地启动 OS”,而是”复用已经启动好的 OS 快照”(snapshot / fork-based cold start),这是 M 系列会讨论的前沿话题。

八、六十年简史速览

本节压到最短。把 OS 六十年的演化切成四段:

8.1 1950s–1960s:批处理与 OS-360

最早的”OS”是 IBM 7090 上的 FORTRAN Monitor System。它做的只有 job queue 管理。IBM OS/360(1964)第一次把 multiprogramming、spooling、中断驱动 I/O 做进一套系统。Fred Brooks 在《人月神话》里讲的就是这个 OS 的开发教训。

8.2 1970s:Unix 与分时系统

Multics 雄心太大,1965–1969 年 MIT/Bell/GE 联合开发,最终没能产品化但留下大量思想。Unix(1970,Bell Labs)是 Multics 的精神继承者 + 极度简化版。V6(1975)公开源码后,BSD 分叉,进入高校。VAX/VMS 是同期的商业代表。

8.3 1980s–1990s:个人计算与微内核浪潮

DOS(1981)把 OS 带到每一个家庭,但它不是真正的 OS。Windows NT(1993)带来了 NT 内核,Mach 风格的混合设计。Mach(CMU,1985)点燃微内核运动;L4(Jochen Liedtke,1995)把微内核从”慢”的标签里救回来。Linux(1991)在这个时代以宏内核姿态出场,反而一路赢。

8.4 2000s–2020s:隔离的军备竞赛

虚拟化复兴(VMware ESX 2001、Xen 2003、KVM 2007)把隔离单位从进程升格到虚机。容器(LXC 2008 → Docker 2013 → runc / containerd 2015)把隔离单位压回到用户态进程 + namespace + cgroup。Firecracker / Kata / gVisor(2018–)重新拉回虚机级别以抗逃逸。机密计算(SEV 2017、TDX 2022、CCA 2022)把不信任扩展到 hypervisor 与云运营者。Rust for Linux(2021 起)试图在内核层面削减内存安全漏洞。

这条主线在 A-02 os-history 会详细展开。但提前点题:过去二十年 OS 的演化,基本都绕着”隔离”这一件事

九、给读者的读法建议

写到这里,“什么是操作系统”应该从一个模糊的概念变成一组可以丈量的职责。本系列接下来的路径:

对这本系列的使用方式:

  1. 顺读:如果你是系统工程师但没系统读过 OS,建议顺序读 A → M,每读完一个子系列做一个小实验(后续每篇末尾给实验建议)。
  2. 问题驱动读:如果你碰到具体问题(比如”为什么我的服务延迟抖动”),跳到 C-26 调度延迟 + K-88 perf + K-89 eBPF,交叉读。
  3. 源码配读:每篇都会给具体 Linux 源码位置与 commit。建议 clone 一份 Linux 6.6(或以上),用 ctags/clangd 同步读。

如何高效读内核源码:别试图通读;从一个 syscall 入口(例如 SYSCALL_DEFINE3(read, ...)fs/read_write.c)出发,沿调用链往下;每到一个子系统边界(VFS → 块层 → 驱动)就停下来读对应 Documentation/*.rst。这个方法在 A-06 内核与用户态边界中会有具体示范。

十、小结:OS 的五件事,一把尺子

我们提炼出的五件事:抽象、复用、隔离、策略、可观测。它们不是教科书定义的替代品,而是一把工程尺子。你可以拿它去丈量:

这把尺子我们会一直用到 M-110 系列结语。OS 不是一个会消失的层;它是一个在每一次架构变迁中被重塑职责分工的层。看清这五件事的流动,才能看清 OS 的未来。

从下一篇起,我们回到历史——Unix 谱系、Plan 9、Linux 的设计遗产——看这五件事是如何一步步被今天的形状构造出来的。


参考文献

论文与书籍

源码 / 工程文档

在线资源


下一篇【操作系统百科】Unix 谱系与设计遗产

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-06 · os

【操作系统百科】Unikernel

Unikernel 在云上为什么没成主流?MirageOS、IncludeOS、Unikraft、OSv 的设计取舍——库操作系统、启动时间、工具链、调试困难、POSIX 兼容。

2026-04-17 · os

【操作系统百科】宏内核 vs 微内核 vs 混合内核:Tanenbaum-Torvalds 三十年后

微内核是理论正义但工程失败?Linux 宏内核赢了只是因为先上车?三十年前的那场论战,在 seL4 形式化正确、L4 家族把 IPC 做到单 syscall、Fuchsia 真正商用的 2020s,我们应该怎么重新看?本文用四个可量化的维度——性能、可维护性、隔离性、可验证性——把四种内核架构(宏、微、混合、Exokernel/Unikernel)摆到同一张尺子上对齐。

2026-04-17 · os

操作系统百科

110 篇长文,从操作系统的基础抽象到调度、虚拟内存、文件系统、并发、安全、前沿方向。以 Linux 6.x 主线为实现参照,辅以 FreeBSD、XNU、Windows NT、实时 OS 的对照。

2026-04-27 · os

【操作系统百科】内存回收

Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。


By .