为什么需要操作系统
一台计算机由硬件和软件组成,硬件包括处理器、内存、磁盘、键盘、鼠标、显示器、网卡等。可问题是:程序员不可能亲手去操作这些硬件,也不可能掌握所有硬件的细节。于是硬件之上盖了一层软件——操作系统,它替用户程序扛下所有脏活累活。
操作系统扮演两种角色:
- 扩展机器(Extended Machine):把丑陋的硬件抽象成一种”更清晰、更好用的计算机模型”,比如用”文件”来代表磁盘、用”进程”来代表 CPU 的占用。
- 资源管理器(Resource Manager):管理 CPU、内存、I/O 这些稀缺资源,决定谁先用、用多少,并防止坏程序把系统搞垮。
平时看到的操作系统还有一层”皮”:带图形界面的叫 GUI(Windows、macOS),基于文本命令行的叫 Shell。它们本质都是用户接口程序,往下才是真正干活的内核。
一、程序的一生:从源码到运行
1.1 信息是什么:context + byte
从大家最熟的 hello world 入手,看一个程序的完整生命周期。
1 |
|
在计算机里所有信息都是二进制——一句源码、一张图片、一段视频,底层都是 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"]
- 预处理(
gcc -E):把以#开头的命令(如#include)展开,把引用的头文件内容全部插入源文件对应位置,得到完整的hello.i。 - 编译(
gcc -S):编译器把hello.i翻译成汇编程序hello.s。这一步让”语言”和”机器”解耦——不同语言用不同编译器,最后都汇成同一套通用汇编语言。 - 汇编(
gcc -c):汇编器把.s翻译成机器语言指令,打包成可重定位目标文件.o。这是二进制文件,人直接打开只能看到乱码。 - 链接(
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 | $ ./hello |
shell 的四大作用:
- 命令解释器:解析输入的命令,内置命令直接执行,否则查找并运行对应的可执行文件;
- 脚本语言:把一串 shell 命令写成文本文件,实现自动化;
- 环境配置:管理环境变量(默认编辑器、路径等);
- 批处理:按顺序执行一系列命令。
shell 执行外部命令时自己并不跑程序,而是靠
fork + execve创建子进程去执行
真正运行那段是硬件在工作,所以操作系统和硬件不能分开看。要搞懂程序运行时发生了什么,必须先了解硬件组织。
二、操作系统所处的位置
先看一张操作系统的分层示意图:
1 | ┌─────────────────────────────────────────┐ |
- 最底下是硬件,所有计算和存储都在这里发生。
- 硬件之上是软件。计算机有两种运行模式:内核态(又称管态、核心态)和用户态。操作系统内核运行在内核态,能直接管硬件。
- 用户接口程序(shell 或 GUI)处于用户态,且位于用户态最低层,允许用户运行其他程序(浏览器、播放器)。
- 越靠近用户态的应用程序越容易编写:你不喜欢某个邮件阅读器,可以自己重写一个;但你不能自己写一个操作系统或中断处理程序——这部分由硬件保护,防外部修改。
操作系统位于硬件与用户之间,起管理和协调作用。用户态应用无法直接访问硬件,必须通过内核提供的服务间接访问。这样既提高了安全性(坏程序/恶意软件碰不到硬件),也让应用程序更容易编写维护(不用管底层硬件细节)。
三、硬件速览:OS 眼中的机器
操作系统和运行它的硬件关系极密切——它要扩展指令集、管理资源,就必须足够了解硬件。这里把一台个人电脑抽象成一个模型:CPU、内存、I/O 设备都挂在总线上,通过总线通信。
1 | 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 |
|
调用过程: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)。
怎样把驱动程序装进操作系统(在内核态运行)? 三种途径:
- 把内核和驱动程序重新连接,然后重启(UNIX 常用);
- 在一个 OS 文件里设置入口,重启时 OS 寻找并装载(Windows 常用);
- OS 运行时动态装载,无需重启(热插拔设备如 USB、IEEE 1394 需要)。
每个设备控制器都有些寄存器(比如指定磁盘地址、内存地址、扇区计数)。驱动程序从 OS 取一条指令,翻译成值写进设备寄存器,所有设备寄存器的集合就是 I/O 端口空间。设备寄存器有的被映射到可用地址空间(像内存一样读写);有的放在专门 I/O 端口空间(用 IN/OUT 指令读写,需内核态)。
实现输入的三种方式:
- 忙等待(busy waiting):用户程序发起系统调用 → 内核转成驱动程序调用 → 驱动启动 I/O 后循环检查设备是否完成。缺点是一直占着 CPU,一直轮询直到完成。
- 中断(interrupt):驱动启动设备后立刻返回,OS 安排其他工作;设备完成时发一个中断通知。中断流程:驱动写寄存器告诉控制器做什么 → 控制器启动设备 → 完成后向中断控制器发信号 → 中断控制器在 CPU 引脚上声明,并把设备编号放到总线上 → CPU 响应,把 PC 和 PSW 压栈并切到内核态 → 用设备编号查中断向量找到处理程序 → 处理完成后恢复用户程序继续执行。注意:中断可以在不合宜的时间发生,CPU 可暂时禁中断,但多个中断同时到来时由中断控制器按优先级裁决。
- 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
11while (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 | ┌────────────────────────────────────────────────────┐ |
注意这张图:逻辑上分了三层,物理上还是一个大型二进制程序、全部运行在内核态——分层只是”为了管理复杂性而划分”(理解、维护方便),并没有真正把哪层独立出来。
为了增强模块性和可扩展性,现代系统引入共享库(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 | 用户态 │ ┌──────────────────────────────────────────┐ |
有意思的是:文件系统、进程管理器这些”操作系统核心业务”,在微内核里是作为用户进程跑的——它们和你的 shell 平级,谁也压不过谁;一旦某个服务器崩溃,只有它自己消失,系统照样运行。
- 最底层:设备驱动程序(运行在用户态,无法直接访问 I/O 端口,需构造结构 + 系统调用间接交互);
- 之上一层:服务器,包括文件服务器(文件系统)、进程管理器(创建/销毁/管理进程),还有一个特殊的重生服务器——检测并修复服务器和驱动程序的故障,无需用户干预自动恢复。
- 微内核体现机制与策略分离:内核只实现”选择最高优先级进程运行”这个机制,而”如何分配优先级”这个策略由用户态进程管理器决定。内核精简、易维护、易扩展。
6.4 客户-服务器模式(Client-Server)
微内核思想把进程分成两类:服务器(提供服务)和客户端(使用服务),这就是客户-服务器模式。它有两种载体:一是在一台计算机上既当客户端又当服务器(OS 会优化);普遍情况是客户端和服务器在不同机器上,通过局域网/广域网连接。
客户端不知道消息在本地还是远程处理——两种情形对客户端一样:发请求、收回应。现代 Web 就是典型:PC 向某服务器请求网页,服务器返回。越来越多系统(包括家用 PC)成为客户端,某地运行的大型机器成为服务器。
七、计算机是如何启动的
主板(母板)上有 BIOS(基本输入输出系统),内含底层 I/O 软件(读键盘、写屏幕、磁盘 I/O 等),如今存在闪存里(非易失但可更新)。
启动流程:
- BIOS 自检:检查装了多大 RAM、键盘等基础设备是否正常响应。
- 扫描总线:扫描 PCIe/PCI 总线,找出所有设备,记录即插即用设备;若设备和上次不同则重新配置。
- 从 CMOS 清单找启动设备:CMOS 是主板上可读写的 RAM 芯片,用来保存 BIOS 设置后硬件参数(有时把 BIOS 设置叫 CMOS 设置)。用户可进 BIOS 配置程序修改设备清单,也可判定能否从 CD-ROM/USB 启动。
- 从硬盘启动:启动失败则从硬盘启动,把启动设备第一个扇区读入内存执行。它通常检查分区表确定哪个分区是活动的,再从该分区读入引导加载程序,这个加载器从活动分区读取操作系统并启动它。
- 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 解释器)。