操作系统,到底是个什么东西?
操作系统,到底是个什么东西?
想象这样一个场景:你刚刚穿越回 1946 年,宾夕法尼亚大学的一间巨大房间里,几万根电子管在嗡嗡作响,空气里弥漫着灼热的铜臭味。面前这台 ENIAC 是人类历史上的第一台电子计算机,重达 30 吨,每秒能做 5000 次加法——在当时,这是神一样的存在。现在你的任务是:算一道炮弹弹道。
你不会被允许直接"编程"。事实上,"编程"这个词在那时候的意思完全不一样——它指的是用人工方式把几千根电缆插到不同的插孔里,再拨动几百个开关,让机器进入特定的状态。写一个程序需要几天时间,重新配置又要几天时间。机器真正"运行"的时间反而是少数,大部分时间都在为下一次计算做准备。
这就是"裸机"的真实面貌:程序员(或更准确地说是"操作员")必须直接面对硬件,把自己的想法翻译成电流和开关。这件事情有多痛苦?答案是:极其痛苦。痛苦到后来的人们开始想,是不是应该有某种东西站在程序员和硬件之间,把这种重复的、繁琐的、容易出错的工作接管过去。
那个"某种东西",就是操作系统。
1.1 裸机年代:当程序员直接面对硬件
ENIAC 和它的"程序"
ENIAC 的故事很能说明问题。它最初设计出来并不是"通用计算机",而是一台专门用于弹道计算的专用机器。它的"程序"不是写出来的,是插出来的——操作员要按照题目,把不同的电缆接到不同的累加器、乘法器、除法器上,再设置好初始数据。整个过程可能需要两个工程师花上整整一周。
想象一下,今天你写一段 Python 代码,运行了几秒钟就算完了;可是在 ENIAC 时代,"写代码"和"运行代码"的时间比是几十比一。机器大部分时间都在闲着,等人类帮它接线。
这个状态持续了大约二十年。从 1946 年的 ENIAC,到 1950 年代的 IBM 1401、UNIVAC I,再到 1960 年代的早期 IBM System/360,计算机的能力在飞速提升,但"使用方式"依然非常原始。程序员需要:
- 手动加载程序(最早是用纸带、穿孔卡片)
- 自己控制输入输出的时机
- 自己处理外设(打印机、磁带机)的时序差异
- 自己想办法让多个程序排队共用一台机器
每一条都是一份痛苦的工作。如果你的程序要把结果打印到打印机上,你得先查清楚打印机的接口长什么样、它一次能接收多少字节、发完指令后多久才能拿到响应。一旦搞错,机器就锁死了,必须人工重启。
批处理系统:第一次"自动化"
到了 1950 年代中期,人们开始想办法把这种重复劳动"打包"起来。思路很简单:
既然每次都要把程序从纸带加载到内存,能不能让机器自己读完纸带后自动跳转执行下一个程序?
这就是最早的"批处理系统"(Batch System)的雏形。它的工作流程是这样的:操作员把一摞作业(每个作业通常是一叠穿孔卡片)放进读卡器,机器依次执行每个作业,执行完一个就自动加载下一个。中间不再需要人工干预。
这听起来很简单,但它解决了一个核心问题:机器空转的时间被消灭了。在这之前,机器跑完一个程序,操作员要花十分钟去装下一组卡片;有了批处理系统,这十分钟被消掉了。一台机器每天能算的题目数量可以翻上好几倍。
不过批处理系统还算不上真正的"操作系统"——它只是一个自动化的加载器。一个真正的操作系统,还要解决后面这些更复杂的问题。
单道批处理的局限
批处理带来了效率,但也暴露了新的矛盾。来看一个典型的场景:
假设有一个程序需要做两件事:第一,从磁带读数据(这很慢,可能要几十毫秒);第二,把数据打印出来(这也很慢,可能也要几十毫秒)。但在等待磁带和打印机的这段时间里,CPU 其实什么都没做,它就在那儿等着。
这是巨大的浪费。在 1960 年代,一台 IBM 大型机的 CPU 价格按今天的物价换算可能要几百万美元一台。让这样的贵重资产空转几十毫秒,相当于把几千块钱扔进了垃圾桶。
于是人们想:能不能让 CPU 在等待慢设备的时候,去算别的题目?
这就是"多道程序系统"(Multiprogramming System)的思想。多道程序系统让内存里同时存在好几个程序,CPU 在某个程序等待 I/O 的时候,切换到另一个程序去执行。程序的切换由一个叫"调度器"(Scheduler)的小程序决定。调度器是"操作系统"这个词第一次变得名副其实的地方——它真的在"管理"机器上的资源了。
多道程序系统是操作系统发展史上的一道分水岭。从这之后,操作系统开始真正接管 CPU、内存、外设这些"资源",把它们分配给不同的程序使用,并在程序之间切换。操作系统不再只是一个自动加载器,而是一个资源管理者。
分时系统:让每个人都有"自己的电脑"
多道程序系统主要解决的是 CPU 利用率问题,但它有一个副产品:程序切换得非常频繁,单个程序的运行速度可能慢得让人抓狂。
到了 1960 年代,MIT、贝尔实验室的几位大牛(包括后来发明 C 语言的 Dennis Ritchie)开始琢磨另一个问题:能不能让一台机器同时"看上去"是好几台机器?
他们的方案是"分时系统"(Time-Sharing System)。原理也不复杂:把 CPU 的时间切成很小的小片(比如每个用户分到 100 毫秒),调度器以极快的速度在用户之间切换,每个人都"感觉"自己独占了一台计算机。
最著名的早期分时系统是 1969 年贝尔实验室的 Multics——虽然 Multics 本身失败了,但它启发了后来 Unix 的诞生。Unix 的设计哲学就是从这里来的:小而美、组合化、文本流。这套哲学影响了过去五十多年几乎所有的操作系统,包括我们要研究的 Linux。
到这里,操作系统的两个核心动机都已经浮现:
1. 让人不必直接面对硬件——这是"抽象" 2. 让多个程序能高效共享同一台机器——这是"资源管理"
接下来我们再聊第三个动机,它来自于一个完全不同的方向。
1.2 操作系统的三大职责:管家、翻译、保安
如果你问一个老工程师"操作系统到底是干什么的",他可能会告诉你三个词:资源管理、抽象接口、安全隔离。这三件事看起来很抽象,但它们其实是层层递进的,每一层都解决了前一层解决不了的问题。
第一层:管家——资源管理
先说"管家"。操作系统是一台计算机上所有硬件资源的总管家。这些资源包括:
- **CPU 时间**:谁先谁后,每个跑多久
- **内存**:每个程序分多少空间,谁和谁共享
- **磁盘**:文件存在哪里,谁能占用多少
- **网络**:数据包怎么排队,连接给谁
- **外设**:打印机、键盘、鼠标、显示器、U 盘……
管家的活儿看着琐碎,但每一项都至关重要。CPU 调度算法设计得不好,整个系统就会卡;内存管理做得不好,就会"内存泄漏"或者"越界崩溃";磁盘调度不好,文件读写就会慢得离谱。
一个具体的例子:CPU 调度。假设有三个程序 A、B、C 都在排队等 CPU,每个都需要跑 100 毫秒。如果按先来先服务(FCFS)的策略,A 先跑 100 毫秒,B 再跑 100 毫秒,C 最后跑 100 毫秒——C 要等 200 毫秒才能开始。但如果用时间片轮转(Round Robin),每个程序跑 20 毫秒就切换一次:A 跑 20,B 跑 20,C 跑 20,然后 A 再跑 20……三个程序"看起来"都在同时推进,虽然每个都被拖慢了 1.5 倍,但平均响应时间大大缩短。
这两种策略背后是不同的"管家哲学"。操作系统设计的第一道选择题,就是这个。
第二层:翻译——抽象接口
光有管家还不够。管家把资源管起来了,但程序员还是不知道怎么用。CPU 是一堆寄存器,磁盘是一些扇区,网卡是一些 DMA 缓冲区——这些东西怎么用?
操作系统的第二个职责是抽象。它把丑陋的硬件细节藏起来,给程序员一个干净的接口。比如:
- 磁盘被抽象成"文件"——你只需要 `open()`、`read()`、`write()`、`close()`,不用关心数据到底存在盘片上的哪个磁道哪个扇区
- 网络被抽象成"套接字"(socket)——你只需要 `connect()`、`send()`、`recv()`,不用关心 TCP 三次握手、IP 路由、ARP 协议这些细节
- 内存被抽象成"虚拟地址"——每个程序都"以为"自己独占整个 4GB 地址空间,不用关心物理内存到底有多大、自己在哪一段
这套抽象的好处是巨大的。同一个程序,换一台不同 CPU 架构的机器,只要操作系统提供了相同的接口,程序不用改一行代码就能跑。这就是"可移植性"。今天的 Linux 支持从冰箱到火星探测器到你的手机里的 ARM 芯片——核心原因就是 Linux 在硬件之上提供了一套统一的抽象接口,应用开发者不需要碰底层硬件。
第三层:保安——安全隔离
如果只有前两层,世界上就只有"好的管家"和"翻译机",还没有"保安"。但保安的角色极其重要。
想象一下,如果两个程序可以互相修改对方的内存,会发生什么?一个程序不小心(或故意)改了另一个程序的指令,下一秒整个系统就会崩溃。在多道程序时代,"程序相互干扰"是最大的噩梦之一。
所以操作系统引入了隔离机制:
- 每个程序都运行在自己的"地址空间"里,访问别人的地址会触发硬件异常(段错误就是这种东西)
- 程序不能直接操作硬件设备,必须通过操作系统提供的接口(系统调用)
- 不同用户之间也相互隔离:你的文件我看不到,我的内存你也别碰
这套隔离机制是现代操作系统的基石。没有它,互联网上的任何一个网站都可能把别人的银行账户清空——因为它们都跑在同一台服务器上。
三层叠在一起
资源管理、抽象接口、安全隔离这三件事,是操作系统存在的全部理由。它们一层叠着一层:
``` 应用层 (Application) — 用户写的东西 ↑ 系统调用接口 (Syscall) — 操作系统的大门 ↑ 内核层 (Kernel) — 资源管理 + 抽象 + 隔离 ↑ 硬件层 (Hardware) — CPU、内存、磁盘、网卡 ```
下一节我们就专门讲中间这道门——内核态和用户态的边界。
1.3 内核态与用户态:为什么需要分两层
一个真实的"权限升级"
设想一个场景:你的程序想读一个文件。它调用了 C 标准库的 `fopen()` 函数,然后 `fopen()` 在内部调用了 `open()` 系统调用。然后会发生一段非常神奇的事情。
在 CPU 这个层面,"系统调用"不是普通函数调用。它是一种特殊的指令,会让 CPU 切换到一种叫做内核态(Kernel Mode)的特权模式。在内核态下,CPU 可以做任何事情:访问任意物理内存、操作硬件寄存器、关闭中断、改变 CPU 自身的运行状态。一旦系统调用执行完毕,CPU 又切换回用户态(User Mode),程序继续正常运行,但此时它的权限已经被收窄了——它不能直接碰硬件,只能通过操作系统给的接口。
为什么要这么麻烦?为什么不让应用程序直接操作硬件?
因为如果不这么分,整个系统会在五分钟内崩溃。
举一个真实案例:在 Windows 9x 时代(95/98/ME),应用程序确实可以"几乎"直接访问硬件,因为这套系统的内核态和用户态分得没那么干净。结果呢?当时最流行的崩溃原因之一,就是某个程序不小心写错了一个指针,把内核的数据结构给改了,然后整个 Windows 蓝屏重启。那个年代流行一句话:"死机是 Windows 的正常状态。"
到了 Windows NT、Linux、macOS 这些现代操作系统里,内核态和用户态的边界变得极其严格。应用程序的代码和数据被严格隔离在内核之外,任何试图越界访问的行为都会立即被 CPU 捕获,然后操作系统会把这个程序杀掉(段错误、Segfault 就是这么来的)。
四个 Ring 和一个现实
从硬件层面看,x86 CPU 一共支持四个特权级别,叫 Ring 0、Ring 1、Ring 2、Ring 3。其中 Ring 0 是最高权限,可以做任何事;Ring 3 是最低权限,几乎什么都做不了。
但实际操作系统很少用满四个 Ring。Linux 和 Windows 都只用了两个:Ring 0 作为内核态,Ring 3 作为用户态。中间的 Ring 1 和 Ring 2 在历史上曾经被考虑过(比如让设备驱动跑在 Ring 1),但后来发现复杂度太高、收益不大,就都放弃了。
所以现在我们说"内核态"基本就等于 Ring 0,"用户态"就等于 Ring 3。这是一个简化,但这个简化是全世界几十亿台设备都在用的简化。
系统调用:跨越边界的那座桥
应用程序想进入内核态,必须通过系统调用(System Call)这条唯一的合法通道。这有点像什么呢?有点像一栋大楼的安保系统:租户(应用程序)不能随便进设备间(硬件),但如果租户需要修空调,他必须填一张"维修申请单"(系统调用),保安(CPU 的特权检查机制)审核通过后才会开门。
以 Linux 为例,最常用的几个系统调用包括:
- `read()` / `write()`:读写文件描述符
- `open()` / `close()`:打开/关闭文件
- `fork()` / `exec()`:创建新进程或执行新程序
- `mmap()`:把文件映射到内存
- `ioctl()`:对设备做特殊操作
每一个系统调用都是一个具体的"门"。程序不能跳窗户进去(直接写硬件寄存器),必须从正门进(系统调用),并且进门时还要亮明身份(参数)。
下图展示了应用程序、Shell、系统调用、内核、硬件之间的关系:
```mermaid flowchart TD A[应用程序<br/>Word、Chrome、游戏] --> B[Shell 和库函数<br/>bash、glibc] B --> C[系统调用接口<br/>read、write、open、fork] C --> D[内核<br/>进程管理、内存管理、文件系统、设备驱动] D --> E[硬件<br/>CPU、内存、磁盘、网卡] ```
这张图看上去简单,但它浓缩了过去六十年操作系统设计的精髓:一层一层的"翻译",每加一层就解决一类问题。应用层关心业务逻辑,库层关心语法糖,系统调用层关心权限交接,内核层关心资源分配,硬件层只管执行指令。
1.4 血统:Linux 从哪里来
了解了操作系统的"是什么"和"为什么",我们再看一眼它的"从哪里来"。Linux 不是从天上掉下来的,它有一个非常清晰的家族谱系:
Unix (1969) → Minix (1987) → Linux (1991)
这条线上的每一代都解决了上一代留下的问题。
Unix:一切的起点
1969 年,贝尔实验室的 Ken Thompson 和 Dennis Ritchie 在一台报废的 PDP-7 小型机上写出了第一版 Unix。那时候 Multics 项目刚刚失败,贝尔实验室撤出了 Multics 团队,Thompson 决定自己做一个"简化版"。Ritchie 后来加入,他们俩一起用汇编语言重写了 Unix,又在 1973 年用 Ritchie 自己设计的 C 语言再次重写——这是历史上第一次用高级语言写操作系统内核。
C 语言写出来的 Unix 带来了一个革命性的好处:可移植性。从此以后,操作系统不必绑死在某一种 CPU 架构上,只要给新 CPU 写一个 C 编译器,Unix 就能跑上去。
1970 年代后期,Unix 在学术界疯狂传播。伯克利大学做了一版叫 BSD(Berkeley Software Distribution),贝尔实验室自己出了 System V。这两个分支后来都成了 Unix 家族的重要源头。
但是 Unix 商业化之后变得昂贵且封闭。AT&T(贝尔实验室的母公司)开始收授权费,BSD 因为和 AT&T 的法律纠纷而多年受限。学术圈需要一个"教学用、便宜、自由"的 Unix 替代品。
Minix:教学用的迷你 Unix
1987 年,阿姆斯特丹自由大学的 Andrew Tanenbaum 教授——就是那本经典教材《操作系统:设计与实现》(Operating Systems: Design and Implementation)的作者——写了 Minix。Minix 的目标是教学:让学生能够完整地读懂一个操作系统的源码。
Minix 是一个微内核系统,整个内核只有大约 12000 行 C 代码。对于教学来说非常合适。但 Tanenbaum 教授对 Minix 有一个限制:他只允许学生用作教学,不希望它被商业化或者大规模修改。这在当时是有道理的——他想保持代码的"教学纯净性"。但这个限制也让 Minix 错过了成为主流操作系统的机会。
不过 Minix 在另一个层面上意义重大:它的源码被一个芬兰大学生读了个底朝天。
Linux:一个芬兰大学生的副业
1991 年,赫尔辛基大学的 Linus Torvalds 是个大二年级的计算机系学生。他买了一个 386 处理器的新电脑,想在上面跑 Unix,但买不起商业版的 Unix。他想到了 Minix——Tanenbaum 教授允许他在 Minix 之上写程序,但不允许改 Minix 本体。
于是 Linus 开始自己动手,在 Minix 的基础上写一个新的内核。他后来在新闻组里发了那个著名的帖子:
> *"I'm doing a (free) operating system (just a hobby, won't be big and professional like gnu) for 386(486) AT clones..."*
这句话翻译过来是:"我正在做一个(自由的)操作系统(只是爱好,不会像 gnu 那么大和专业),给 386(486) AT 兼容机用……"
这个"只是爱好"的操作系统就是 Linux。
Linus 的聪明之处不在于他写出了多么革命性的代码——Linux 内核 0.01 的代码现在看来相当粗糙。真正改变游戏规则的是他做了一个决定:把源码公开到互联网上,让全世界的人一起改。
当时 GNU 运动已经有了 GCC 编译器、Bash Shell、Emacs 编辑器等一整套工具,但缺一个真正的自由内核。Linux 出现之后,刚好填补了 GNU 计划最后那块拼图。GNU 的工具加上 Linux 的内核,组成了一个完整的自由操作系统——这就是后来被叫做"GNU/Linux"的系统。
当然,在日常使用中大家更习惯直接叫它 Linux。
为什么 Linux 能赢?
回看历史,Unix 阵营有很多分支——BSD、Solaris、AIX、HP-UX、IRIX……但最终 Linux 一统天下。原因有几