0%

操作系统总览

为什么需要操作系统

一台计算机由硬件和软件组成,硬件包括处理器、内存、磁盘、键盘、鼠标、显示器、网卡等。可问题是:程序员不可能亲手去操作这些硬件,也不可能掌握所有硬件的细节。于是硬件之上盖了一层软件——操作系统,它替用户程序扛下所有脏活累活。

操作系统扮演两种角色:

  • 扩展机器(Extended Machine):把丑陋的硬件抽象成一种”更清晰、更好用的计算机模型”,比如用”文件”来代表磁盘、用”进程”来代表 CPU 的占用。
  • 资源管理器(Resource Manager):管理 CPU、内存、I/O 这些稀缺资源,决定谁先用、用多少,并防止坏程序把系统搞垮。

平时看到的操作系统还有一层”皮”:带图形界面的叫 GUI(Windows、macOS),基于文本命令行的叫 Shell。它们本质都是用户接口程序,往下才是真正干活的内核。


一、程序的一生:从源码到运行

1.1 信息是什么:context + byte

从大家最熟的 hello world 入手,看一个程序的完整生命周期。

1
2
3
4
5
6
#include <stdio.h>
int main()
{
printf("hello world\n");
return 0;
}

在计算机里所有信息都是二进制——一句源码、一张图片、一段视频,底层都是 0 和 1 组成的位序列。8 个位合成一个字节,每个字节代表某个字符。

问题来了:同样是一串字节,它到底是整数、字符、浮点数,还是机器指令?唯一的判别依据就是”环境”(context / 语境)。

举个语文题的例子:一句”林冲大叫一声’啊也!’”,单独摘出来,你不懂它是什么情感;放到上下文里,才知道是惊吓还是愤怒。程序也一样——一个函数、一个变量、一个地址,必须放进它所在的环境(编译时或运行时的上下文)才能理解它的含义。

子程序之于程序、进程之于操作系统、App 的一屏之于 App,都是一个道理:执行到某个子程序时,它要用到之前的一些结果(外部变量、对象等);App 点进新界面,也要保存”你是从哪个屏幕跳来的”,返回时才能跳回去。这就是 context——**可以通俗理解成”环境”**。

1.2 编译系统四步

操作系统其实看不懂你写的高级语言(C、Java、C++)。要让程序跑起来,每一条语句(以分号结束)都必须先被翻译成操作系统能识别、能执行的机器语言指令,再打包成可执行目标文件(二进制磁盘文件)。这个过程由编译器完成。

1
gcc -o hello hello.c    # -o 指定输出文件名;不加 -o 默认生成 a.out

这里用 gcc 演示,整个流程分四步:

flowchart LR
    A["📄 源文件
hello.c"] -->|"预处理 gcc -E"| B["📄 预处理后
hello.i"] B -->|"编译 gcc -S"| C["📄 汇编
hello.s"] C -->|"汇编 gcc -c"| D["📦 目标文件
hello.o"] D -->|"链接 ld"| E["🚀 可执行文件
hello"]
  1. 预处理(gcc -E):把以 # 开头的命令(如 #include)展开,把引用的头文件内容全部插入源文件对应位置,得到完整的 hello.i。
  2. 编译(gcc -S):编译器把 hello.i 翻译成汇编程序 hello.s。这一步让”语言”和”机器”解耦——不同语言用不同编译器,最后都汇成同一套通用汇编语言。
  3. 汇编(gcc -c):汇编器把 .s 翻译成机器语言指令,打包成可重定位目标文件 .o。这是二进制文件,人直接打开只能看到乱码。
  4. 链接(gcc hello.o -o hello):把多个目标文件合并。比如 printf 其实是预编译好的 printf.o(在标准库里),链接器把它和你的 hello.o 拼成一个可执行文件。

详细展开(预处理、链接、静态库/动态库、为什么会出现 undefined reference)我在《从源代码到可执行文件——初步理解编译、链接与软件构造体系》那篇写过。这里只讲它在操作系统里的位置。

为什么还要懂编译系统? 三个原因:

  • 优化性能:switch 一定比 if-else 快吗?while 和 for 谁更高效?指针和数组索引谁更快?这些选择的”代价”藏在编译器的实现里。
  • 理解链接错误:最让人头疼的往往不是逻辑 bug,而是链接错误——比如”无法解析一个引用”。为什么会在运行时报错、有些编译时就报?这跟编译、链接机制有关。
  • 安全问题:栈溢出、缓冲区溢出是大多数网络服务器漏洞的主因。理解数据和控制信息怎么在堆栈里存放,才能明白漏洞从哪来。

1.3 运行:shell 与可执行文件

编译好的可执行文件躺在磁盘上,怎么让操作系统跑它?把文件名敲进 shell。

先解释一下shell是什么:

  • 它是用户和操作系统之间的接口(桥梁):Linux/Unix 的关键组件,敲的命令第一步落在它身上,由它解析并调用相应的操作系统服务去执行;
  • 本质是一个用 C 语言编写的应用程序,运行在用户态——和 GUI 一样,是内核外面裹着的一层”皮”,往下才是真正干活的内核;
  • 既是一种命令语言,又是一种程序设计语言:逐条敲命令,它解释执行;把一串命令写成文本文件就是脚本,可以自动化。
flowchart LR
    U["👤 用户"] -->|"输入命令行"| S["🐚 shell 应用程序(用户态)"]
    S -->|"内置命令(cd / pwd 等)"| E["🔧 直接执行"]
    S -->|"外部命令(./hello)"| K["🧠 调用内核服务"]
    K -->|"指挥"| H["⚙️ 硬件真正干活"]

shell 从哪来? 以UNIX系统为例,用户登录 UNIX 系统时,系统会启动一个 shell 进程,并把用户的终端设置为它的标准输入/输出设备——所以 shell 一启动就显示提示符($ 或 #),等着敲命令。UNIX 里的 shell 还有多种实现:sh(Bourne Shell,先驱)、csh、ksh、bash 等,现在最常见的是 bash;不同 shell 语法略有差异,但角色都一样——那道”接口”。

shell 不是 UNIX 独有的——“用户和操作系统之间的接口”这个角色,任何系统都得找人扮演:

系统 代表 shell
Windows cmd.exe(命令提示符,承自 DOS 的 COMMAND.COM)+ PowerShell(现代主推,还能跨平台跑在 Linux/macOS 上)
macOS zsh(早期版本曾默认 bash,2019 年后改为 zsh)
Linux bash(主流),常见的还有 zsh、fish 等

它们的命令语法各不相同( Windows 里拷文件是 copy,Linux 里是 cp),但做的事完全相同:解析用户的输入,帮用户运行程序。

注意一个容易混淆的点:shell 不一定是命令行。Windows 语境里说 “Windows shell”,通常指的是 explorer.exe(资源管理器);macOS 的 Finder、Linux 的 GNOME/KDE 桌面环境同理。所以 shell 分两种:

  • 文本型 shell(CLI):cmd、PowerShell、bash……可以打字的那种;
  • 图形型 shell(GUI):Explorer、Finder、桌面环境——同样包在内核外面,只是长成一幅图。

无论是哪种,它们都是 1.3 开头说的那层”皮”:站在内核和用户之间,把命令交给内核。

shell 的工作流程:显示提示符($ 或 #),等用户输入命令行,然后分两类处理:

  • 内置命令:shell 自带的命令(如 cd、pwd),直接执行;
  • 外部命令:shell 把它当成一个可执行文件的名字去查找并运行——这就是为什么我们写 ./hello(./ 表示当前目录;直接敲 hello 的话,得靠 PATH 环境变量里配置的路径才能找到它)。
1
2
$ ./hello
hello world

shell 的四大作用:

  • 命令解释器:解析输入的命令,内置命令直接执行,否则查找并运行对应的可执行文件;
  • 脚本语言:把一串 shell 命令写成文本文件,实现自动化;
  • 环境配置:管理环境变量(默认编辑器、路径等);
  • 批处理:按顺序执行一系列命令。

shell 执行外部命令时自己并不跑程序,而是靠 fork + execve 创建子进程去执行

真正运行那段是硬件在工作,所以操作系统和硬件不能分开看。要搞懂程序运行时发生了什么,必须先了解硬件组织。


二、操作系统所处的位置

先看一张操作系统的分层示意图:

1
2
3
4
5
6
7
8
9
┌─────────────────────────────────────────┐
│ 用户态 │ 用户接口程序(shell / GUI) │
│ │ 应用程序(浏览器、播放器、邮件…) │
├─────────┼─────────────────────────────────┤
│ 内核态 │ 操作系统内核 │
│ │ (内存分配、进程调度、设备驱动…) │
├─────────┼─────────────────────────────────┤
│ 硬件 │ 处理器 / 内存 / 磁盘 / 网卡 / I/O │
└─────────┴─────────────────────────────────┘
  • 最底下是硬件,所有计算和存储都在这里发生。
  • 硬件之上是软件。计算机有两种运行模式:内核态(又称管态、核心态)和用户态。操作系统内核运行在内核态,能直接管硬件。
  • 用户接口程序(shell 或 GUI)处于用户态,且位于用户态最低层,允许用户运行其他程序(浏览器、播放器)。
  • 越靠近用户态的应用程序越容易编写:你不喜欢某个邮件阅读器,可以自己重写一个;但你不能自己写一个操作系统或中断处理程序——这部分由硬件保护,防外部修改。

操作系统位于硬件与用户之间,起管理和协调作用。用户态应用无法直接访问硬件,必须通过内核提供的服务间接访问。这样既提高了安全性(坏程序/恶意软件碰不到硬件),也让应用程序更容易编写维护(不用管底层硬件细节)。


三、硬件速览:OS 眼中的机器

操作系统和运行它的硬件关系极密切——它要扩展指令集、管理资源,就必须足够了解硬件。这里把一台个人电脑抽象成一个模型:CPU、内存、I/O 设备都挂在总线上,通过总线通信。

1
2
3
4
CPU           内存           I/O设备
│ │ │
└──────────────┼──────────────┘
中央总线

3.1 CPU

CPU 的作用就是处理和执行指令。它是”大脑”,但大脑下命令也要知道命令在哪——它通过程序计数器(PC)这个寄存器,始终指向主存中下一条要执行的指令地址。

处理器执行一条指令的流程:从 PC 取出指令地址 → 取指令 → 用算术/逻辑单元(ALU)算出更新后的地址和值 → 更新 PC → 继续下一条。如此循环。

CPU 还遵循一套指令集架构(ISA)。现代处理器又把 ISA 和微体系结构分开:

  • ISA 描述每条机器代码的效果(行为规范);
  • 微体系结构 描述处理器具体怎么实现。同一 ISA 可以有多种微体系结构,比如 Intel 和 AMD 都生产 x86 处理器,但内部实现差别很大。这样软件开发只管 ISA,硬件设计师可以自由优化实现。

所以,不同架构的 CPU 之间不能直接执行彼此的程序:x86 能跑的,ARM 装不上,因为指令集和寄存器布局都不同。

CPU 内有一些通用寄存器存放关键变量和临时结果(因为从内存取数比执行指令慢得多,寄存器提供了高速暂存)。指令通常有三类:

  • 加载指令:内存 → 寄存器;
  • 存储指令:寄存器 → 内存;
  • 运算指令:对操作数做运算(如 add 把两个操作数相加)。

特殊的、对程序员可见的寄存器:

  • 程序计数器(PC):下一条要取的指令地址,取完自动更新,保证顺序执行。
  • 堆栈指针(SP):指向当前栈顶,管理函数调用时的参数、局部变量、返回地址。
  • 程序状态字(PSW):记录当前系统状态(如零标志 ZF、进位标志 CF、溢出标志 OF)。用户程序能读整个 PSW,但通常只能改其中某些字段。

函数调用怎么用堆栈?

假设:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(5, 7);
printf("The sum is: %d\n", result);
return 0;
}

调用过程:main 先把参数 5、7 压栈并保存返回地址 → 调 add;add 进入后保存当前栈指针(push ebp / mov ebp, esp),从栈弹出参数执行加法和;返回时通过 ret 回到 main,栈指针恢复到调用前的状态。

操作系统必须”认识”所有这些寄存器。时间多路复用(多个程序在单个 CPU 上交替执行,让每个程序都像独占 CPU)时,OS 每次暂停一个程序,都要保存全部寄存器的值,之后才能正确恢复。

流水线与超标量

早期 CPU 一次只取一条指令、解码一条、执行一条。现代 CPU 能同时处理多条:执行第 N 条时,同时解码第 N+1 条、读取第 N+2 条——这叫流水线(pipeline)。

更进一步的超标量(superscalar)有多个执行单元(整数、浮点、布尔各一个),多条指令可同时取、解码、放入缓冲区,只要哪个执行单元空闲就取走执行。这种设计下,指令经常是乱序执行的,但硬件会保证结果和顺序执行时一致。

内核态与用户态

除了嵌入式里特别简单的 CPU,大多数 CPU 有两种操作模式:内核态和用户态,通常靠 PSW 里一个二进制位来控制。

  • 内核态:可执行指令集里任何指令,能访问硬件全部功能。
  • 用户态:只能执行部分指令、访问部分硬件,无法执行 I/O 和内存保护相关指令,也无法改 PSW 的模式位进入内核态。

为什么嵌入式系统也要把一部分代码放内核态、一部分放用户态? 综合考量:

  • 安全性:用户态应用受限,防止坏程序破坏系统、篡改数据;内核态代码才能碰硬件,隔离了系统资源。
  • 资源管理:内核态负责分配资源(内存、文件、网络),用户态通过系统调用请求服务;用户态权限受限,防滥用。
  • 灵活性:用户态程序可单独更新替换,不用重编整个系统;也更容易调试维护。
  • 性能:实时控制等性能敏感任务放内核态保证低延迟,非实时任务放用户态减轻内核负担。
  • 兼容性:借鉴桌面/服务器设计,保持一致性;用户态可用现成工具链和开源库,开发更简便。

用户程序怎么进入内核? 靠系统调用(system call),它把执行模式从用户态切到内核态。执行 TRAP 指令触发切换;完成后,系统调用后面的指令把控制权交还给用户程序。系统调用过程中还可能遇到 trap(陷阱)——硬件检测到的异常(比如除以零、浮点下溢),由操作系统接管处理。

多线程、多核、GPU

  • 多线程(超线程):允许 CPU 几乎同时维持两个线程状态,能在纳秒级切换。当一个线程等内存取数(要几个时钟周期)时,切换去跑另一个线程。注意:多线程不是真并行,任一时刻仍只有一个线程在执行,只是靠快速切换提高利用率。对 OS 来说,每个线程就像一台独立 CPU——两个物理核各跑两线程,OS 会看成四个 CPU。
  • 多核:一块芯片上集成多个完整处理器核,每个核有独立 CPU,能真正并行处理多任务。
  • GPU:由成千上万个微核组成的处理器,擅长大量并行的简单计算,核心数量远超普通 CPU。

并发与并行

上文一会儿说”多线程不是真并行”,一会儿说”多核真正并行”——这里涉及到一对经常出现的概念:

  • 并发(concurrency):多个任务在重叠的时间段里交替或同时进行,重点在”逻辑上同时“——任意时刻物理上可能只有一个任务在跑,但多个任务都处于”进行中”的状态,看起来像同时。单核 CPU 也能并发:时间片轮转,多个进程轮流上 CPU。
  • 并行(parallelism):多个任务在同一精确时刻真正同时执行,重点在”物理上同时“——多个核心各干各的活。只有多核/多处理器才谈得上并行。

并发让系统能同时处理更多任务(提到吞吐量、响应性)——靠调度维持”多个活动同时存在”并有序推进;并行让系统跑得更快——利用多核硬件。第四章进程里的”时间片轮转”、第五章系统调用里”OS 暂停当前进程、调度其他就绪进程”,都是并发的日常;本节前面”多线程靠快速切换提高利用率”,说的也是并发。

3.2 内存层次结构

内存是计算机第二大组件。理想的存储要快、大、便宜,但技术无法同时满足,于是做成层次结构:

flowchart TB
    A["寄存器
最快 · 最小 · 最贵"] --> B["高速缓存 L1/L2
硬件控制 · 快"] B --> C["主存 RAM
断电丢失 · 容量大"] C --> D["磁盘 / SSD
最慢 · 容量最大 · 最便宜"]
  • 寄存器:顶部,速度与 CPU 相同,程序须在软件里自行管理。
  • 高速缓存:寄存器之下,主要由硬件控制。主存被分成高速缓存行(通常 64 字节)。CPU 要读数据时,缓存硬件先查对应行在不在——命中(cache hit)只需约 2 个时钟周期;不命中就必须去主存取,慢得多。缓存行数量受成本限制,有的机器有两三级缓存(每级更大但略慢)。
  • 主存(RAM):缓存不命中的访问最终都到主存。RAM 可读可写,断电丢失。
  • 磁盘:比 RAM 每比特成本低两个数量级、容量高两个数量级,但随机访问慢约三个数量级。

为什么会有缓存? 硬件速度发展到瓶颈,处理器和内存的速度差距越来越大。与其砸钱提速内存,不如在处理器端引入高速缓存,利用程序的局部性原理,让大多数内存操作在缓存里就能完成,减少访问主存次数。

局部性原理:CPU 访问存储器(无论是取指令还是存取数据),所访问的存储单元总聚集在一个较小连续区域里。分为两种:

  • 时间局部性:某个内存位置被重复引用。
  • 空间局部性:某个位置被引用后,它附近的位置很快也被引用。

局部性原理可以用程序验证:遍历一个 [10000][10000] 的二维数组,dataArray[i][j](按行)和 dataArray[j][i](按列)的耗时差异,就是空间局部性好坏的直观体现。

缓存不只在 CPU,OS 也在用:比如把常用文件驻留在主存,避免反复读磁盘;把路径名到磁盘地址的转换结果缓存;把 URL 到 IP 的转换结果缓存。

任何缓存系统都要回答四个问题:何时把新内容放进来?放哪一行?没空位时移除谁?被移除的内容放到大存储器的哪里?——不同场景答案不同。

现代 CPU 通常有两级缓存:L1 cache 在 CPU 内部(存放已解码指令和常用关键字,约 16KB,无延迟);L2 cache 更大(兆字节级),访问有 1–2 个时钟周期延迟。

时钟周期:CPU 速度由振荡器两个脉冲之间的时间决定,每秒脉冲数越高越快,单位是 Hz(兆赫/千兆赫)。例如 4GHz 处理器每秒执行 40 亿个时钟周期。现代处理器每个周期可执行多条指令。

主存之外的非易失性存储:

  • ROM(只读存储器):内容一旦写入就不变,速度快成本低。启动用的引导程序(bootstrap)通常存这里。
  • EEPROM / 闪存:可擦写重写,能修正程序错误;闪存常用于移动设备,介于 RAM 和磁盘之间,但有擦写寿命限制。
  • CMOS:断电会丢数据(易失性),但常用来保存当前时间和日期。

磁盘:机械装置,有一个或多个金属盘片以 5400/7200/10800rpm 旋转,机械臂悬在盘面上。信息写在同心圆上,一个臂位置的所有磁道合起来是柱面。每个磁道分成若干扇区(512 字节)。固态硬盘(SSD)不是磁盘——它没有可动部分,用闪存存储,只是同样断电不丢数据。现代计算机支持虚拟内存机制:把主存当成一部分缓存,最常用的程序放主存,其余放磁盘;地址映射由 CPU 里的存储器管理单元(MMU)完成。

缓存和 MMU 对性能影响很大。在多道程序系统里,从一个程序切到另一个程序的机制叫上下文切换(context switch),必要时还要把缓存里修改过的数据写回磁盘。

3.3 I/O 设备

CPU 和存储器只是 OS 管理范围的冰山一角,I/O 设备同样重要。I/O 设备通常分两部分:

  • 设备控制器:一块或一组芯片,直接控制物理设备,接受并执行 OS 的指令。它设计了一个简化接口给 OS 用,屏蔽底层物理细节(类似加一层代理简化复杂)。
  • 设备本身:接口相对简单且已标准化。比如标准化的 SATA 磁盘控制器能适配任意 SATA 磁盘。

ATA 是 IBM 第二代 PC 的高级技术成果,SATA 是串行 ATA,现在是很多计算机的标准硬盘接口。OS 看到的是对”控制器”的接口,和”设备”接口差别很大。

每种设备控制器不同,需要不同软件控制。专门和控制器通信、发命令、收响应的软件叫设备驱动程序(device driver)。

怎样把驱动程序装进操作系统(在内核态运行)? 三种途径:

  1. 把内核和驱动程序重新连接,然后重启(UNIX 常用);
  2. 在一个 OS 文件里设置入口,重启时 OS 寻找并装载(Windows 常用);
  3. OS 运行时动态装载,无需重启(热插拔设备如 USB、IEEE 1394 需要)。

每个设备控制器都有些寄存器(比如指定磁盘地址、内存地址、扇区计数)。驱动程序从 OS 取一条指令,翻译成值写进设备寄存器,所有设备寄存器的集合就是 I/O 端口空间。设备寄存器有的被映射到可用地址空间(像内存一样读写);有的放在专门 I/O 端口空间(用 IN/OUT 指令读写,需内核态)。

实现输入的三种方式:

  1. 忙等待(busy waiting):用户程序发起系统调用 → 内核转成驱动程序调用 → 驱动启动 I/O 后循环检查设备是否完成。缺点是一直占着 CPU,一直轮询直到完成。
  2. 中断(interrupt):驱动启动设备后立刻返回,OS 安排其他工作;设备完成时发一个中断通知。中断流程:驱动写寄存器告诉控制器做什么 → 控制器启动设备 → 完成后向中断控制器发信号 → 中断控制器在 CPU 引脚上声明,并把设备编号放到总线上 → CPU 响应,把 PC 和 PSW 压栈并切到内核态 → 用设备编号查中断向量找到处理程序 → 处理完成后恢复用户程序继续执行。注意:中断可以在不合宜的时间发生,CPU 可暂时禁中断,但多个中断同时到来时由中断控制器按优先级裁决。
  3. DMA(直接存储器访问):专门的 DMA 芯片控制内存和控制器之间的位流,无需 CPU 干预。CPU 只需设置要传送的字节数、设备/内存地址和方向,DMA 完成后发中断。

3.4 总线

总线的英文 bus 直译”公共汽车”,比喻很贴切:总线是数据通道,沿线连接的设备像公交站,数据包像乘客,知道起点终点、沿途停靠。总线有容量限制,通常按固定长度”字”传输(当前多为 4 字节/32 位或 8 字节/64 位),还需要一个”控制器”协调各设备访问顺序,避免冲突。

早期总线多是共享总线架构(多条线路并行传同一数据字,多设备共用一组线),需要仲裁机制。PCIe 则彻底改用端到端链路(点对点串行通道,每个设备与目标设备独立连接)。对比:

维度 共享总线架构 端到端链路(PCIe)
共享性 多设备共享同一物理总线 每个设备有独立连接
带宽 被所有设备共享,易成瓶颈 每条链路独立带宽,互不影响
传输方式 并行 串行(可用多条 Lane 并行)
扩展性 设备越多性能越差 新设备加链路即可,扩展性好
管理复杂度 需复杂仲裁机制 无共享资源问题,管理简单

现代 x86 系统里总线很多:高速缓存、内存(DDR3)、PCIe、PCI、USB、SATA、DMI,各司其职。比如 CPU 通过 DDR3 与内存交互、通过 PCIe 连 GPU、通过 DMI 连集成控制中心,再通过 SATA 接硬盘和 DVD、通过串行总线接 USB 设备。USB 从 1.0(12Mb/s)→ 2.0(480Mb/s)→ 3.0(≥5Gb/s),实现即插即用;SCSI 速度可达 640MB/s,多在服务器/工作站。


四、操作系统的三大核心抽象

📄 文件 → 屏蔽 I/O 设备细节;🧠 虚拟内存 → 屏蔽主存+磁盘;⚙️ 进程 → 屏蔽处理器+主存+I/O。

4.1 进程

每个进程都带一个独立的地址空间(从 0 到某个最大值的一段连续存储区域),里面放可执行代码、数据、以及函数调用和局部变量用的栈。除了地址空间,进程还关联一串资源:寄存器(PC、SP)、打开的文件列表、待处理信号、相关进程列表等。简而言之,进程是一个装了运行所需所有信息的容器。

多任务怎么”同时”跑? 靠上下文切换。假设你开了视频编辑器、Web 浏览器、后台邮件检查三个进程,其实同一时刻 CPU 只跑一个;OS 通过时间片轮转等调度策略,周期性地挂起一个启动另一个,让所有进程公平分到 CPU。当进程被挂起时,必须精确保存状态(地址空间内容 + 寄存器值 + 文件位置等上下文信息),存在进程表里,之后才能无缝恢复。

进程的父子关系:进程可以创建子进程,子进程再创建子进程,形成一棵进程树。比如 shell 收到一条编译命令,就创建一个子进程去编译,编译完自毁。理论上进程可以无限增长,但实际 OS 会限制最大进程数。

进程之间协作要交换信息,机制叫进程间通信(IPC)。OS 还提供一批系统调用管理进程:申请/释放内存、等待子进程完成并收集结果、用新程序替换当前进程映像等。

进程有时需要接收外部信息(比如网络发消息后,设一个定时器,超时没收到确认就重传)。定时器到期后 OS 会给进程发警告信号,中断当前执行、保存寄存器、触发信号处理例程。这些信号其实是软件模拟的硬件中断,发射原因很多:定时器到期、非法指令、访问无效内存等。

身份标识:每个进程分配唯一 UID(标识运行者),新进程继承父进程 UID。用户可以属于一个或多个组,每个组有唯一 GID。UNIX 类系统还有个特殊 超级用户(Windows 里叫管理员),权限更高,能执行违反常规保护规则的操作。

4.2 线程

进程和线程都是对 CPU 工作时间段的抽象,只是颗粒度不同:进程描述更大的执行环境,线程是更细的执行单元。

假设程序 A 要依次执行 a、b、c 三个模块。CPU 拿到程序 A 的控制权、加载它的上下文后,依次跑 a、b、c。注意:a、b、c 共享的是程序 A(进程)的上下文环境,所以在这些段之间切换,是线程级上下文切换,不是更重的进程级切换——开销小得多,CPU 利用率更高。

注意:编程语言里说的”进程/线程”是语言层的具体实现,和 OS 层的概念是”具体化”的关系,别混为一谈。

4.3 地址空间与虚拟内存

地址空间:早期简单 OS 一次只跑一个程序,要开第二个就得把第一个整个移出内存,多任务能力极差。后来允许多个程序同时加载到内存并行执行,靠硬件+OS 实现隔离保护。每个进程被分配一个专属地址集合,通常从 0 连续编号到某个最大值。

理论上,32 位架构的虚拟地址空间可达 (2^{32}) 字节,64 位可达 (2^{64}) 字节,远超物理内存容量。正是虚拟内存技术解决了”地址空间比物理内存大”的问题。

虚拟内存:允许直接把物理内存暴露给程序是危险且低效的——安全风险增加(可破坏系统稳定性、数据泄露)、资源利用率低(碎片化)、内存管理复杂。虚拟内存作为物理内存和进程间的抽象层:给每个进程一个独立的、看似无限大的内存空间。每个进程都以为独占整个内存,实际只是在访问 OS 精心管理的虚拟地址空间。

flowchart TB
    subgraph VS["进程的虚拟地址空间(高地址在上,低地址在下)"]
        A["内核内存区
(OS 专用,用户不可见)"] B["堆栈区 Stack
(参数、局部变量、返回地址)从高地址往低地址增长"] C["共享库区
(printf 等库代码)"] D["堆区 Heap
(malloc 动态分配)从低地址往高地址增长"] E["进程代码与数据区
只读段 + 可读写段"] end

这张图从低地址(下)到高地址(上)排列,内核区在最上方(高地址),用户进程看不见它。从下往上看:

  • 进程代码与数据区:最顶部承载可执行文件核心内容,分只读段(机器指令、只读数据如字符串常量)和可读写段(全局变量、静态变量,运行时允许修改)。
  • 堆数据区(heap):C 程序动态内存分配区,malloc/calloc/realloc 申请、free 释放,从低地址向高地址扩展。
  • 共享库区:存放系统/第三方库代码(如 printf),多个进程共享同一份库代码,节省内存。
  • 堆栈区(stack):管理函数调用和局部变量,每个函数调用建一个栈帧(存参数、局部变量、返回地址),从高地址向低地址扩展。
  • 内核内存区:最高地址处,内核专用的代码、数据和系统资源,用户进程不可见、无法访问,保证 OS 安全。

4.4 文件

几乎所有 OS 都支持文件系统。OS 的一个核心功能就是屏蔽磁盘和其他 I/O 设备细节,给程序员一个与设备无关的抽象文件模型。创建、删除、读写文件都要通过系统调用。读文件前先”打开”(定位并验证权限),读完”关闭”。

现代 OS 引入了目录概念,作为文件系统基本构成单元,允许用户按逻辑分组文件,形成树状结构(目录本身也是特殊文件,可嵌套包含文件或其他目录)。系统调用包括创建/删除目录、移动文件等。

进程树和文件系统树都像树,但差异明显:进程树层次浅(很少超三层)、生命周期短(几分钟);文件系统树更深(可达四五层)、持久存在。权限管理也不同:进程有明确父子关系,父进程能控制子进程;文件访问权限更灵活,不只限所有者。

路径:每个文件用唯一路径名定位,从根目录向下,各层目录用斜杠分隔。如 /Faculty/Prof.Brown/Courses/CS101,首个斜杠表示根目录。Windows 用反斜杠 \,Linux/Unix 用正斜杠 /。每个进程还拥有一个工作目录,作为相对路径的基准。

打开文件后分配一个文件描述符(非负整数),用于后续读写;拒绝访问则返回错误代码。

特殊文件:为了让 I/O 设备像普通文件一样读写。有两种特殊文件类型:

  • 块特殊文件:可随机访问的块设备(如磁盘),打开后可直接访问第 N 块。
  • 字符特殊文件:处理字符流的设备(如打印机、调制解调器)。
    按惯例放在 /dev 目录,如 /dev/lp0 表示打印机。

特殊文件是 UNIX/Linux 家族(POSIX)特有的设计——背后的哲学是 UNIX 的”一切皆文件“:设备、磁盘、终端统统统一成文件接口,开发者只要会读写文件,就会操作一切外设。Windows 没有这个机制:设备是系统里的”对象”,走 Win32 API 访问,只保留了 CON、NUL、PRN 等几个从 DOS 继承的设备名。现代 Linux 还把这一思想推进了一大步:/proc、/sys 等虚拟文件系统把内核信息、硬件状态也映射成了文件——cat /proc/cpuinfo 就能”读”到 CPU 信息,如同读一个普通文件。

管道(pipe):一种与进程、文件都相关的重要特性,本质是存在内存里的一段空间,专门用于两个进程之间传输数据。注意”虚拟文件”的含义:它不是存储在磁盘上的实体,却能用文件那套操作来使用。

两个进程用管道通信的流程:先创建一个管道,然后——

  • 进程 A 把数据”写”进管道,就像写一个普通的输出文件;
  • 进程 B 从管道”读”数据,就像读一个普通的输入文件;
  • 数据并不落到磁盘上,而是实时地直接送到另一端:A 写进去的字节,B 马上就能读到。

管道价值的本质:开发人员可以用熟悉的文件操作接口来实现进程间通信——两个进程无需直接接触、互不干扰,数据就传输完毕了。

1
$ cat file1 file2 file3 | sort > /dev/lp

| 就是管道操作符:左边命令的标准输出(stdout),直接变成右边命令的标准输入(stdin)。上面这句把三个文件用 cat 合并 → 通过 | 传给 sort 排序 → 最后的 > 是重定向(< 反向,把文件内容喂给命令),把排序结果写进 /dev/lp—本小节前面讲的字符特殊文件(打印机)。shell 把这些符号串起来,一条命令就完成了”合并 → 排序 → 打印”三连。

4.5 保护:rwx 权限

以 UNIX 为例,每个文件分配一个9 位二进制保护代码,分成三段:第一段给所有者,第二段给同组用户,第三段给其他用户。每段三位,分别控制读(r)、写(w)、执行(x),即经典的”rwx 位”。

例如 rwxr-x--x:

  • 所有者(owner):r、w、x 全有;
  • 同组用户(group):只有 r(读)和 x(执行),不能改;
  • 其他用户(others):只有 x(执行),不能读也不能写。

五、系统调用:用户态进入内核的大门

上一节里的文件、进程、权限,都是通过系统调用实现”抽象接口 + 资源管理”的——现代操作系统普遍采用系统调用,尽管各系统间细节各异,但基本原理相通。此处入门仅需了解即可。

  • 任何单 CPU 计算机,在任意时刻都只能执行一条指令。 所以进程在用户态执行用户程序时(比如读文件数据),如果想把控制权转移给操作系统,就必须执行一条特定指令——异常指令或系统调用指令,光靠普通 C 代码是”够不着”内核的。
  • 系统调用的触发依赖计算机底层的机制,通常需要通过汇编语言来精确表达。 C 语言里没有”把控制权交给内核”这种语句——所以 C 程序要经由 库函数 中转:库函数内部用汇编把系统调用编号放进寄存器、执行 TRAP 指令。这也是为什么平时的代码只调 read 函数,而不是直接”调用内核”。

控制权交到内核后,操作系统先执行参数检查(确认目标进程、其所需调用的服务),确认无误后执行系统调用,控制权暂时转交给内核态的相应指令序列。

简而言之,系统调用类似于一种特殊的过程调用,但它和普通过程调用有本质区别:普通过程调用只能在同一运行模式下跳来跳去,而系统调用能够跨越用户态与内核态的界限——正是这一点,才让用户程序”合法地”碰得到硬件。

这里以**read 系统调用举例**:

1
count = read(fd, buffer, nbytes);

三个参数:文件描述符(指定文件)、缓冲区指针(数据放哪)、要读的字节数。成功返回实际读取字节数(可能小于请求值);失败返回 -1,并在全局变量 errno 里记录错误代码。所以程序必须检查返回值。

read 调用历经 11 个步骤(完整流程见下图,①~⑪ 与下面文字一一对应):

flowchart TB
    subgraph U["🎩 用户空间(用户进程)"]
        direction TB
        S1["① 压入 nbytes"] --> S2["② 压入 &buffer"]
        S2 --> S3["③ 压入 fd"]
        S3 --> S4["④ 调用 read 库函数"]
        S4 --> S5["⑤ 系统调用编号放入指定寄存器"]
        S5 --> S6["⑥ TRAP:陷入内核"]
        S9["⑨ 返回用户态的函数调用库"] --> S10["⑩ 返回给调用者
read 之后的下一条指令"] S10 --> S11["⑪ 增加 SP:清理压入的参数"] end subgraph K["🔒 内核空间(操作系统)"] direction TB S7["⑦ 解析编号,查 dispatch 表定位处理器"] --> S8["⑧ 执行系统调用处理程序"] end S6 --> S7 S8 --> S9

read 的完整执行步骤(C/C++ 编译器对函数调用按逆序压参):把参数压入堆栈 → 程序调用 read 库函数 → 底层需要更深的系统调用 → 把系统调用编号放进指定寄存器 → 执行 TRAP 指令(切到内核态,跳转到内核预定义地址)→ 内核解析编号,用 dispatch 表定位系统调用处理器 → 执行服务 → 控制权返回到用户态函数调用库 → 返回发起调用的应用。最后一步是系统调用返回后调整堆栈指针,把之前压入的参数清掉——让堆栈回到调用前的状态。若调用阻塞(如等键盘输入),OS 可能暂停当前进程、调度其他就绪进程;条件满足再唤醒。

TRAP 和普通过程调用有两点不同:它改变了执行模式;跳转目标受限于硬件/OS 定义,而非任意地址。

刚才的 read 只是系统调用世界的一个例子——一共 有100 多个系统调用。在列出之前,补充一个问题:为什么 read、fork 这些名字,在 Linux 上敲、在 macOS 上敲,全都一模一样? 答案就是 POSIX。

POSIX 是什么? 它是一门标准——IEEE 制定的”可移植操作系统接口”(Portable Operating System Interface),规定操作系统应该给应用程序提供哪些接口:系统调用名、参数、库函数、命令行约定,通通写进标准条文。它要解决的只有一个问题:可移植性。因为有了 POSIX,同样的 open/read/fork,在 Linux、macOS、FreeBSD、Solaris 上名字一模一样、参数一模一样——写一次代码,换平台重编译就能跑。

注意:POSIX 是标准条文(规定”应该长什么样”),不是某个具体的操作系统。Linux 内核 + glibc 实现了它,macOS(BSD 血统)也实现了它。打个比方:POSIX 像”同一本单词书”,Linux 和 macOS 是”两家出版社印的实体书”——程序只认标准拼写,在哪一本里都查得到,自然就跨平台了。

常用 POSIX 系统调用:

进程管理

调用 说明
pid = fork() 创建与父进程相同的子进程
pid = waitpid(pid, &statloc, options) 等待一个子进程终止
s = execve(name, argv, environp) 替换一个进程的核心映像
exit(status) 终止进程并返回状态

文件管理

调用 说明
fd = open(file, how, ...) 打开文件用于读、写
s = close(fd) 关闭文件
n = read(fd, buffer, nbytes) 从文件读数据到缓冲区
n = write(fd, buffer, nbytes) 从缓冲区写数据到文件
position = lseek(fd, offset, whence) 移动文件指针
s = stat(name, &buf) 取得文件状态信息

目录和文件系统管理

调用 说明
s = mkdir(name, mode) 创建新目录
s = rmdir(name) 删除空目录
s = link(name1, name2) 创建新目录项指向已有文件
s = unlink(name) 删除一个目录项
s = mount(special, name, flag) 安装一个文件系统
s = umount(special) 卸载一个文件系统

其他

调用 说明
s = chdir(dirname) 改变工作目录
s = chmod(name, mode) 修改文件保护位
s = kill(pid, signal) 发送信号给进程
seconds = time(&seconds) 获取从 1970-01-01 至今的时间

几个关键系统调用解读:

  • fork:POSIX 下创建进程的唯一机制。它复制当前进程(父进程)——包括文件描述符、寄存器值等;复制地址空间 = 先共享,需要写入时才真正复制(写时复制 COW)。fork 返回一个特殊值:子进程里返回 0,父进程里返回子进程 PID。通过检查返回值,程序就知道自己是在父还是子进程里。fork 后父子进程各自独立修改自己的变量。

  • **waitpid**:父进程等待一个或多个子进程结束。第一个参数为 -1 时等待任意子进程;第二个参数返回退出状态(正常/异常终止和退出码);第三个参数指定选项(如没有已退出子进程时立即返回)。

  • execve:加载并执行第一个参数指定的程序,替换当前进程映像。三个参数:文件名(字符串指针)、参数向量(以 null 结尾的字符串数组)、环境变量(name=value 形式数组)。注意程序入口是 int main(int argc, char *argv[], char *envp[])——argc 是参数个数(含程序名本身),argv 是参数字符串数组,envp 是环境变量数组。

    shell 常用 fork + execve 组合,典型伪代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    while (TRUE) {
    type_prompt(); // 显示提示符
    if (read_command(command, parameters) == -1) break;
    pid_t pid = fork();
    if (pid < 0) { perror("fork failed"); exit(1); }
    else if (pid > 0) waitpid(pid, &status, 0); // 父进程等待
    else {
    execve(command, parameters, NULL); // 子进程执行命令
    perror("execve failed"); exit(1);
    }
    }

    比如敲了 cp file1 file2,shell 先 fork 建子进程,子进程再 execve 执行 cp,传 file1、file2 作为参数。

  • **exit**:终止当前进程,返回 0–255 的状态码给父进程(通过 waitpid 的 statloc 返回)。

  • UNIX 进程的内存划分:text segment(文本区,程序代码)、data segment(数据区,变量)、stack segment(栈区)。数据向上增长、堆栈向下增长,夹在中间的是空闲区。数据段扩展通过 brk 系统调用显式进行(但 brk 不是 POSIX 标准,标准鼓励用 malloc,malloc 内部实现不适合标准化)。

文件和目录系统调用小结:open 需指定绝对/相对路径名和访问模式(O_RDONLY/O_WRONLY/O_RDWR,新建加 O_CREATE),成功返回文件描述符。read/write 最常用。lseek 改变文件读写位置指针,参数为文件描述符、偏移量、起始标志(SEEK_SET 文件开头 / SEEK_CUR 当前位置 / SEEK_END 文件末尾),成功返回新位置的绝对偏移。stat 查询文件元数据(类型、大小、修改时间),fstat 对已打开文件用。

目录相关:mkdir/rmdir 创建/删空目录。**link(name1, name2)** 让同一文件以两个或多个名称出现(多用在不同目录),它利用已有文件的 i-number(i-节点编号) 创建新目录项。UNIX 每个文件有唯一的 i-编号,是 i-节点表 的索引(记录谁拥有文件、磁盘块在哪)。目录其实是一组 (i编号, ASCII名) 映射。unlink 删一个目录项;当指向文件的目录项全部移除时,文件才被真正删除。**mount(special, name, flag)** 把两个文件系统合并,如 mount("/dev/sdb0", "/mnt", 0) 把 USB 盘挂到根文件系统;umount 卸载。

其他系统调用:chdir("/usr/ast/test") 改变工作目录(省得每次输入长路径);chmod("file", 0644) 改文件保护位(让文件对所有者之外可读);kill 发送信号(进程有信号处理程序就跑它,没准备好信号会杀掉进程);time 返回从 1970-01-01 到现在的秒数,32 位下最大 (2^32)-1 秒约 136 年,所以 2106 年 32 位 UNIX 会”发飙”。

Win32 API 对比

UNIX 里 read 系统调用和 read 库函数几乎一对一;Windows 则完全不同:库函数调用和实际系统调用几乎不对应,微软定义了一大套过程叫 Win32 应用编程接口(API),程序员通过这套标准接口实现系统调用,支持从 Win95 以来所有版本。Win32 API 数量极大(几千个),但不都在内核态运行,很多在用户态(管理视窗、几何图形、文本、字体、对话框、菜单等 GUI 功能)。

UNIX Win32 说明
fork CreateProcess 创建新进程
waitpid WaitForSingleObject 等待进程退出
execve 无 CreateProcess = fork + execve
exit ExitProcess 终止执行
open CreateFile 创建/打开文件
close CloseHandle 关闭文件
read ReadFile 从文件读数据
write WriteFile 向文件写数据
lseek SetFilePointer 移动文件指针
stat GetFileAttributesEx 获取文件属性
mkdir CreateDirectory 创建目录
rmdir RemoveDirectory 删除空目录
link / mount / umount / chmod / kill 无 Win32 不支持
unlink DeleteFile 删除文件
chdir SetCurrentDirectory 切换工作目录
time GetLocalTime 获取当前时间

关键差异:Windows 无 fork/execve,CreateProcess 把两者合成一个。Windows 没有父子进程层次——进程创建后,创建者和被创建者是平等的(UNIX 有清晰父子关系与进程树)。CreateProcess 有大量参数指定新进程性质。Win32 没有文件 link、mount、umount、chmod、kill;但也有大量 UNIX 没有的调用,尤其是 GUI 管理。


六、操作系统结构

6.1 单体内核(Monolithic Kernel)

传统很多系统用单体内核:整个操作系统作为一个大型、单一二进制程序在内核态运行,把进程管理、内存管理、文件系统、设备驱动等组件紧密集成为一个可执行文件。

  • 优点:所有组件在同一地址空间,组件间交互高效,减少上下文切换开销。
  • 缺点:系统庞大复杂,维护调试难;**”一荣俱荣,一损俱损”**——内核任何部分出问题,整个系统都可能崩溃。

开发遵循:主程序(初始化并调度)+ 服务过程(处理系统调用)+ 辅助服务程序(数据处理、资源管理,形成层次化服务):

1
2
3
4
5
6
7
8
┌────────────────────────────────────────────────────┐
│ 主程序(init) │ ← 初始化内核并调度
├────────────────────────────────────────────────────┤
│ 服务程序 A(进程管理) 服务程序 B(内存管理) │
│ 服务程序 C(文件系统) 服务程序 D(设备驱动) │ ← 处理系统调用
├────────────────────────────────────────────────────┤
│ 辅助服务程序:数据处理、资源管理等(若干小模块) │ ← 实用程序
└────────────────────────────────────────────────────┘

注意这张图:逻辑上分了三层,物理上还是一个大型二进制程序、全部运行在内核态——分层只是”为了管理复杂性而划分”(理解、维护方便),并没有真正把哪层独立出来。

为了增强模块性和可扩展性,现代系统引入共享库(UNIX)和动态链接库 DLL(Windows)动态加载扩展功能。

6.2 分层系统(Layered System)

分层系统通过层级隔离功能模块:每一层只和相邻层通信,用下层提供的服务实现自身功能,层间通信走预定义接口。

著名实例是 Dijkstra 团队开发的 THE 系统,六层结构:

层号 功能
5 操作员
4 用户程序
3 输入/输出管理
2 操作员-进程通信
1 存储器和磁鼓管理
0 处理器分配和多道程序编程
  • 第 0 层:处理器分配,中断/定时器超时时做进程切换;之上是相互协作的进程。
  • 第 1 层:内存管理,为主存进程分配空间、控制页面调入调出。
  • 第 2 层:进程与操作员控制台通信。
  • 第 3 层:管理 I/O 设备及缓冲区。
  • 第 4 层:用户程序所在层,无需关心进程/内存/控制台/I/O 细节。
  • 第 5 层:系统操作员与系统的交互界面。

分层提高了模块化、可维护性和扩展性。

6.3 微内核(Microkernel)

分层设计需要明确划分内核-用户边界。传统把所有层放内核,但实际上内核里的错误难以处理、一出错整个系统就崩。所以反过来想:尽量减少内核功能,让内核最小化。

微内核把 OS 分成小型、功能明确的模块,只有一个微内核运行在特权模式(内核态),其他模块(设备驱动、文件系统)作为普通用户进程运行——即使这些模块崩溃,也不会影响整个系统。MINIX 3 是微内核代表作,它的具体结构如下(对比上面的单体内核:这里只有最下面一条灰条在内核态):

1
2
3
4
5
6
7
8
9
10
11
12
用户态 │ ┌──────────────────────────────────────────┐
│ │ 用户程序: shell Make Other │ ← 用户进程
│ ├──────────────────────────────────────────┤
│ │ 服务器: FS Proc Reinc │ ← 文件/进程/重生
│ │ Other │
│ ├──────────────────────────────────────────┤
│ │ 设备驱动: Disk TTY New Print Other │ ← 无权直接碰 I/O
│ │ (构造结构 + 系统调用,辗转进内核) │
│ ═════════════════════════════════════════════
内核态 │ │ 微内核:中断处理 · 进程调度 · 进程通信 │ ← 唯一特权模块
│ │ (旁挂 Clock、Sys 等小服务) │
│ └──────────────────────────────────────────┘

有意思的是:文件系统、进程管理器这些”操作系统核心业务”,在微内核里是作为用户进程跑的——它们和你的 shell 平级,谁也压不过谁;一旦某个服务器崩溃,只有它自己消失,系统照样运行。

  • 最底层:设备驱动程序(运行在用户态,无法直接访问 I/O 端口,需构造结构 + 系统调用间接交互);
  • 之上一层:服务器,包括文件服务器(文件系统)、进程管理器(创建/销毁/管理进程),还有一个特殊的重生服务器——检测并修复服务器和驱动程序的故障,无需用户干预自动恢复。
  • 微内核体现机制与策略分离:内核只实现”选择最高优先级进程运行”这个机制,而”如何分配优先级”这个策略由用户态进程管理器决定。内核精简、易维护、易扩展。

6.4 客户-服务器模式(Client-Server)

微内核思想把进程分成两类:服务器(提供服务)和客户端(使用服务),这就是客户-服务器模式。它有两种载体:一是在一台计算机上既当客户端又当服务器(OS 会优化);普遍情况是客户端和服务器在不同机器上,通过局域网/广域网连接。

客户端不知道消息在本地还是远程处理——两种情形对客户端一样:发请求、收回应。现代 Web 就是典型:PC 向某服务器请求网页,服务器返回。越来越多系统(包括家用 PC)成为客户端,某地运行的大型机器成为服务器。


七、计算机是如何启动的

主板(母板)上有 BIOS(基本输入输出系统),内含底层 I/O 软件(读键盘、写屏幕、磁盘 I/O 等),如今存在闪存里(非易失但可更新)。

启动流程:

  1. BIOS 自检:检查装了多大 RAM、键盘等基础设备是否正常响应。
  2. 扫描总线:扫描 PCIe/PCI 总线,找出所有设备,记录即插即用设备;若设备和上次不同则重新配置。
  3. 从 CMOS 清单找启动设备:CMOS 是主板上可读写的 RAM 芯片,用来保存 BIOS 设置后硬件参数(有时把 BIOS 设置叫 CMOS 设置)。用户可进 BIOS 配置程序修改设备清单,也可判定能否从 CD-ROM/USB 启动。
  4. 从硬盘启动:启动失败则从硬盘启动,把启动设备第一个扇区读入内存执行。它通常检查分区表确定哪个分区是活动的,再从该分区读入引导加载程序,这个加载器从活动分区读取操作系统并启动它。
  5. OS 加载设备驱动:OS 询问 BIOS 获取配置信息,逐个设备检查是否有驱动;没有则问用户装(CD-ROM 或下载)。有驱动就加载进内核,初始化表、创建后台进程、启动登录程序或 GUI。

八、操作系统博物馆

操作系统存在大半个世纪,种类繁多。按应用场景分:

  • 大型机操作系统:高端,在大型公司数据中心。I/O 容量与 PC 天差地别,一个大型机有 1000 个磁盘、数百万 G 容量很正常。也用于高端 Web 服务器、大型电商站点。
  • 服务器操作系统:运行在服务器上(大型 PC、工作站或大型机),通过网络为多个用户服务、共享软硬件资源,可提供打印/文件/Web 服务。典型:Solaris、FreeBSD、Linux、Windows Server。
  • 多处理器操作系统:把多个 CPU 连成一个系统(并行计算机/多计算机/多处理器),需要专门 OS(配通信、连接、一致性功能)。个人电脑多核芯片出现后,Windows/Linux 也能跑多核。
  • 个人计算机操作系统:支持多道处理,启动时常有几十个程序在跑,为单个用户提供良好支持(字处理、表格、游戏、上网)。如 Linux、FreeBSD、Windows 7/8、macOS。
  • 掌上计算机操作系统:平板、智能手机、PDA,市场被 Android 和 iOS 主导。
  • 嵌入式操作系统:控制设备(微波炉、汽车、DVD 刻录机、手机、MP3)里的计算机,不允许用户装软件。所有软件跑在 ROM,应用间无保护,较简化。典型:Linux、QNX、VxWorks。
  • 传感器节点操作系统:微小传感器节点网络(可互相通信、用无线基站),用于建筑周边保护、国境界保卫、森林火灾探测、气象预测。每个节点是带 CPU、RAM、ROM 和传感器的真计算机,跑小型事件驱动 OS。
  • 实时操作系统:把时间当关键参数。硬实时应在规定时刻发生(工业控制、民航、军事);软实时偶尔违反时限还能接受、无永久损害(数字音频/多媒体、智能手机)。
  • 智能卡操作系统:最小的 OS,跑在智能卡上(带 CPU 芯片的信用卡),能耗和存储极受限。有的是单功能电子支付,有的是面向 Java(ROM 里有 JVM 解释器)。