应用视角的操作系统:程序到底是什么?------从状态机、syscall 到 strace 看懂应用如何运行
目录
- [应用视角的操作系统:程序到底是什么?------从状态机、syscall 到 strace 看懂应用如何运行](#应用视角的操作系统:程序到底是什么?——从状态机、syscall 到 strace 看懂应用如何运行)
-
- 一、前言:这节课换了一个视角看操作系统
- 二、操作系统上的程序:我们到底在运行什么?
- [三、程序 = 状态机:这节课的第一条主线](#三、程序 = 状态机:这节课的第一条主线)
-
- [3.1 状态机到底是什么?](#3.1 状态机到底是什么?)
- [3.2 程序为什么也是状态机?](#3.2 程序为什么也是状态机?)
- [四、C 程序为什么可以被"解释执行"?](#四、C 程序为什么可以被“解释执行”?)
- [五、函数调用:C 里的函数不是数学函数](#五、函数调用:C 里的函数不是数学函数)
-
- [5.1 数学函数更像"映射"](#5.1 数学函数更像“映射”)
- [5.2 C 函数是会执行动作的](#5.2 C 函数是会执行动作的)
- 六、编译器:把一种状态机翻译成另一种状态机
-
- [6.1 什么叫行为等价?](#6.1 什么叫行为等价?)
- [6.2 为什么编译器能优化?](#6.2 为什么编译器能优化?)
- [七、最小程序:从 _start 开始拿到控制权](#七、最小程序:从 _start 开始拿到控制权)
-
- [7.1 最小程序想解决什么问题?](#7.1 最小程序想解决什么问题?)
- 八、ELF:可执行文件是"初始状态的描述"
- 九、程序自己不能"停下来":退出也要靠操作系统
- 十、syscall:应用程序主动交出控制权
-
- [10.1 为什么 syscall 特殊?](#10.1 为什么 syscall 特殊?)
- [10.2 为什么说像全身麻醉?](#10.2 为什么说像全身麻醉?)
- [十一、应用程序世界:我们感知到的其实不是 OS,而是进程](#十一、应用程序世界:我们感知到的其实不是 OS,而是进程)
-
- [11.1 看得见的应用程序](#11.1 看得见的应用程序)
- [11.2 看不见的后台程序](#11.2 看不见的后台程序)
- 十二、一切应用程序,本质上都是一样的
- 十三、strace:用正确的工具"打开"程序
-
- [13.1 strace 是干什么的?](#13.1 strace 是干什么的?)
- [13.2 为什么 strace 这么重要?](#13.2 为什么 strace 这么重要?)
- [十四、从 strace 看"任何程序"的共同结构](#十四、从 strace 看“任何程序”的共同结构)
- 十五、图形界面程序也一样吗?
- [十六、杀毒软件、病毒、任务管理器,本质上也都是 API 组合](#十六、杀毒软件、病毒、任务管理器,本质上也都是 API 组合)
-
- [16.1 任务管理器](#16.1 任务管理器)
- [16.2 杀毒软件](#16.2 杀毒软件)
- [16.3 病毒](#16.3 病毒)
- [十七、应用程序 = 计算 + 操作系统 API](#十七、应用程序 = 计算 + 操作系统 API)
- 十八、这一讲和上一篇有什么关系?
- 十九、全文总结
-
- [19.1 程序不是静态代码,而是动态状态机](#19.1 程序不是静态代码,而是动态状态机)
- [19.2 函数调用不是数学函数](#19.2 函数调用不是数学函数)
- [19.3 编译器是在翻译状态机](#19.3 编译器是在翻译状态机)
- [19.4 syscall 是应用和 OS 的边界](#19.4 syscall 是应用和 OS 的边界)
- [19.5 strace 是观察程序行为的显微镜](#19.5 strace 是观察程序行为的显微镜)
- 二十、结语:所谓应用程序,没有想象中那么神秘
- 参考资料
往期回顾
《指针合集》《c语言基础》《数据结构》《机器学习导论》《前端基础》
声明
这部分内容主要是笔者根据NJU的蒋炎岩老师的OS课程整理而成,大家如果感兴趣的话可以上课程的网站看看.
课程网站
https://jyywiki.cn
一、前言:这节课换了一个视角看操作系统
上一篇我们从"AI 时代为什么还要学操作系统"开始,看到了操作系统的来路:
从 ENIAC、Fortran、CTSS 到 Unix,操作系统不是人拍脑袋发明出来的,而是复杂度一步步"逼出来"的。
而这次我们将:
不要先站在内核视角看操作系统,而是先站在应用程序视角看操作系统。
也就是说,我们先不急着学:
- 内核怎么调度?
- 页表怎么映射?
- 文件系统怎么实现?
- 中断怎么切换?
而是先问一个更贴近我们的问题:
我写的程序,到底是怎么在操作系统上跑起来的?
其实可以用一句话概括:
程序 = 状态机;应用程序 = 计算 + 操作系统 API。
这句话看起来很短,但它其实把 C 程序、编译器、ELF 可执行文件、系统调用、strace、图形界面程序、后台服务,全都串起来了。
二、操作系统上的程序:我们到底在运行什么?
我们平时说"运行程序",听起来很自然。
比如当我们双击一个图标:
text
Chrome
VS Code
QQ
但问题是:
这些程序本质上到底是什么?
那我们可以说:
任何程序,本质上都是一个状态机。
无论你看到的是:
- 一个最小的 Hello World
- 一个命令行工具
- 一个编译器 gcc
- 一个图形界面编辑器
- 一个浏览器
- 一个游戏
- 一个杀毒软件
- 一个 AI Agent 工具
它们在操作系统眼里,都可以先粗暴地看成:
text
初始状态 + 一条条指令 + 系统调用
换句话说,应用程序没有那么神秘。
它不是"魔法",而是一个不断执行、不断改变状态的机器。
三、程序 = 状态机:这节课的第一条主线
上一讲里老师已经埋下了一个非常关键的观点:
Everything is a state machine.
这一讲把它真正落到了程序上。
3.1 状态机到底是什么?
状态机最简单的形式是:
text
当前状态 + 输入/动作 -> 下一状态
比如地铁闸机:
text
刷卡
LOCKED ─────▶ UNLOCKED
▲ │
│ │ 通过
└──────────────┘
- 当前是锁住状态,刷卡后变成打开状态
- 当前是打开状态,人通过后变回锁住状态
3.2 程序为什么也是状态机?
对一个 C 程序来说,状态大概包括:
text
栈帧 StackFrame
全局变量
局部变量
PC:当前执行到哪一句
程序状态也就是:
text
[StackFrame, StackFrame, ...] + 全局变量
也就是说,一个程序运行时,不只是"代码在那里",而是有一堆正在变化的状态。
比如:
c
int x = 1;
x = x + 1;
printf("%d\n", x);
程序执行过程可以理解成:
text
状态 0:x 未初始化
│ 执行 int x = 1;
▼
状态 1:x = 1
│ 执行 x = x + 1;
▼
状态 2:x = 2
│ 执行 printf
▼
状态 3:屏幕上出现输出
所以程序运行不是"代码静静躺在那里",而是:
状态在一步一步迁移。
四、C 程序为什么可以被"解释执行"?
这一讲一开始讲了一个很有意思的问题:
C 程序能不能像 Python 那样一行一行解释执行?
直觉上很多人会觉得:
C 不是编译型语言吗?怎么解释执行?
那我们有一个这样的思路:
如果我们把 C 程序改写成一种"每一行只做一小件事"的形式,那么它当然可以被单步解释。
可以想象一个解释器循环:
c
while (1) {
stmt = fetch_statement();
decode_and_execute(stmt);
}
也就是说:
- 取一条语句
- 分析它
- 执行它
- 改变程序状态
- 再取下一条
图解如下:
text
┌──────────────┐
│ 取下一条语句 │
└──────┬───────┘
▼
┌──────────────┐
│ 解释这条语句 │
└──────┬───────┘
▼
┌──────────────┐
│ 更新程序状态 │
└──────┬───────┘
│
└── 回到取下一条语句
从这个角度看,解释执行并不神秘。
所谓解释器,就是一个帮助程序状态不断迁移的程序。
五、函数调用:C 里的函数不是数学函数
很多同学第一次学递归时都会有一种感觉:
好像懂了,但又没完全懂。
尤其是汉诺塔这类程序:
c
void hanoi(int n, char from, char to, char via) {
if (n == 1) {
printf("%c -> %c\n", from, to);
return;
}
hanoi(n - 1, from, via, to);
printf("%c -> %c\n", from, to);
hanoi(n - 1, via, to, from);
}
它和数学函数很不一样。
5.1 数学函数更像"映射"
数学里:
text
f(x) = x + 1
你关心的是输入和输出。
text
输入 x -> 输出 x + 1
至于计算过程很多时候不重要。
5.2 C 函数是会执行动作的
但 C 里的函数不是纯粹的数学映射。
它会:
- 创建栈帧
- 修改局部变量
- 调用其他函数
- 打印输出
- 读写文件
- 甚至改变全局状态
所以 C 里的函数调用更像:
text
当前栈帧
│ call f()
▼
压入新的栈帧
│ 执行 f 的代码
▼
返回调用点
│
▼
继续执行
所以程序状态里最重要的一部分,就是栈帧。
所以说:
函数调用不是玄学,它只是状态机的一种机械状态迁移。
六、编译器:把一种状态机翻译成另一种状态机
接下来进入第二条主线:编译器。
很多教材会说:
编译器是把高级语言翻译成低级语言的程序。
这句话没错,但还是太粗。
从这一讲的视角看,编译器真正要保证的是:
翻译前后的两个程序,在相同输入下,有相同的行为。
也就是说:
text
C 程序状态机
│ 编译
▼
机器指令状态机
它们看起来形式不同,但行为要等价。
6.1 什么叫行为等价?
比如原程序:
c
printf("hello\n");
编译后变成了一堆机器指令。
但只要最终效果一样:
text
向标准输出写入 hello\n
我们就认为它们在这个行为上是等价的。
更准确一点:
text
相同输入下
C 程序的 API 调用序列
应该等价于
机器代码的 API 调用序列
6.2 为什么编译器能优化?
因为只要行为等价,编译器就有很大操作空间。
比如:
c
int x = 1 + 2;
printf("%d\n", x);
编译器完全可以提前算好:
c
printf("%d\n", 3);
因为最终行为没有变。
因此就有一个很有意思的推论:
如果一个程序从未调用外部 API,并且最终结果也没有被观察到,那么很多代码都可能是"死代码"。
这就引出了编译器优化里的一个重要概念:
text
DCE:Dead Code Elimination,死代码消除
也就是说,编译器不是"照着翻译"这么简单,它还会问:
这些计算最终有没有被外部世界观察到?
如果没有,可能就直接删掉。
七、最小程序:从 _start 开始拿到控制权
平时我们写 C 程序,总是从 main 开始:
c
int main() {
return 0;
}
但从系统角度看,事情没有这么简单。
程序真正被加载时,入口并不天然就是 main。
更底层的入口通常是:
c
void _start() {
// ...
}
main 更像是运行时库帮我们包装出来的入口。
7.1 最小程序想解决什么问题?
这里设想我们要构造一个"最小"的程序:
从一开始就取得程序的控制权。
也就是说,绕过那些我们平时看不到的启动代码,直接从 _start 开始。
这背后的问题是:
text
操作系统加载一个可执行文件后,
到底从哪里开始执行?
答案是:
text
从可执行文件里记录的入口地址开始执行。
在 Linux 上,这通常和 ELF 文件格式有关。
八、ELF:可执行文件是"初始状态的描述"
这一讲里面老师提到过:
可执行文件就是状态机初始状态的描述。
这句话非常漂亮。
我们平时看到的可执行文件,比如:
bash
./a.out
从文件管理器里打开,可能是一堆乱码。
但在操作系统眼里,它不是乱码,而是一份说明书:
text
这个程序的入口在哪里?
代码段在哪里?
数据段在哪里?
需要加载到内存的哪些位置?
初始权限是什么?
在 Linux 中,常见的可执行文件格式是 ELF。
可以把 ELF 粗略理解成:
text
┌──────────────────────┐
│ ELF Header │ 程序总体信息
├──────────────────────┤
│ Program Header Table │ 怎么加载到内存
├──────────────────────┤
│ .text 代码段 │ 指令序列
├──────────────────────┤
│ .data 数据段 │ 已初始化数据
├──────────────────────┤
│ .bss │ 未初始化数据描述
└──────────────────────┘
所以程序运行不是凭空开始的,而是:
text
ELF 文件
│ execve 加载
▼
进程初始状态
│ 从入口地址执行
▼
状态机开始运行
这就解释了为什么说:
系统里的程序和 minimal.S 本质上没有区别。
浏览器也好,游戏也好,杀毒软件也好,最后都得被操作系统加载成一个状态机。
九、程序自己不能"停下来":退出也要靠操作系统
这时候要说一个反直觉的事:
程序自己是不能真正"停下来"的。
你可能会说:
c
return 0;
不是退出了吗?
但从机器指令角度看,CPU 只会不断执行指令。
指令集里并没有一条通用指令叫:
text
请把我这个进程从操作系统里优雅删除
所以退出程序也必须靠操作系统。
在 x86-64 Linux 上,程序可以通过类似这样的方式请求退出:
asm
movq $SYS_exit, %rax
movq $1, %rdi
syscall
意思大致是:
text
我要调用 exit 这个系统调用,
退出状态码是 1,
请操作系统接管。
这说明一个关键事实:
只要程序想做超出"纯计算"之外的事情,就必须请求操作系统。
退出是这样,打印也是这样,读文件也是这样,联网也是这样。
十、syscall:应用程序主动交出控制权
这一讲最重要的内容,讲的是 syscall。
syscall 像全身麻醉。
10.1 为什么 syscall 特殊?
普通指令只能在应用程序自己的世界里折腾:
text
改寄存器
算加法
跳转
读写自己允许访问的内存
但 syscall 不一样。
它是一条特殊指令,作用是:
text
把控制权交给操作系统
图解如下:
text
应用程序状态机
│
│ syscall
▼
操作系统接管
│
├── 可能帮你写文件
├── 可能帮你创建进程
├── 可能帮你分配内存
├── 可能直接终止你
▼
返回应用程序 / 不再返回
10.2 为什么说像全身麻醉?
因为执行 syscall 后:
- 你主动交出控制权
- 权限更高的操作系统接管
- 操作系统可以改变你的状态
- 操作系统可以让你继续执行
- 也可以直接让你结束
就像前面那个非常生动的比喻:
text
可能醒来没事,
可能肾没了,
也可能再也醒不过来了。
这听起来夸张,但对程序来说是真的。
调用 syscall 之后,操作系统真的可以:
- 修改返回值
- 修改内存映射
- 挂起进程
- 唤醒其他进程
- 终止当前进程
所以 syscall 是应用程序和操作系统之间最重要的边界。
十一、应用程序世界:我们感知到的其实不是 OS,而是进程
我们天天说"操作系统",但日常使用电脑时,其实很少直接感知操作系统本身。
我们真正感知到的是:
text
运行在操作系统上的程序
比如:
11.1 看得见的应用程序
开发类:
- VS Code
- Cursor
- gcc
- clang
- nodejs
- gdb
- vim
- tmux
- htop
日用类:
- 浏览器
- 办公软件
- 图像处理软件
- 视频播放器
- 录屏软件
11.2 看不见的后台程序
后台还有很多 daemon,也就是守护进程:
- systemd
- sshd
- cron
- dbus
- firewalld
- pipewire
- pulseaudio
它们平时不一定出现在你眼前,但它们一直在帮系统提供服务。
图解如下:
text
用户能看到:
Chrome / VS Code / 终端 / 游戏
│
用户不太看到:
systemd / sshd / dbus / cron / pipewire
│
▼
操作系统内核
│
▼
硬件
所以应用视角的操作系统,就是要先看清楚:
操作系统上运行着一个巨大的应用世界。
十二、一切应用程序,本质上都是一样的
这一讲里有一个很有意思的问题:
复杂应用程序和 minimal.S 有任何本质区别吗?
答案很干脆:
没有。
当然,浏览器比 minimal.S 大太多了,游戏也复杂太多了。
但从操作系统视角看,它们都是:
text
可执行文件
│
▼
被加载成进程
│
▼
执行普通指令 + 系统调用
所以:
text
任何程序 = minimal.S = 状态机
它们差别只是:
- 状态更复杂
- 指令更多
- 调用的 API 更多
- 依赖的库更多
- 和外部世界交互更多
但本质结构没有变。
十三、strace:用正确的工具"打开"程序
这一讲非常强调工具思维。
老师说:
操作系统课会帮助大家建立正确的"工具体系"思维。
其中一个特别重要的工具就是:
bash
strace
13.1 strace 是干什么的?
strace 可以追踪一个程序执行过程中发起的系统调用。
比如:
bash
strace ./hello
你可能会看到类似输出:
text
execve("./hello", ["./hello"], ...) = 0
write(1, "Hello\n", 6) = 6
exit_group(0) = ?
这就像给程序装了一个"系统调用摄像头"。
程序平时在干什么,我们看不到。
但它一旦请求操作系统服务,strace 就能帮我们抓出来。
13.2 为什么 strace 这么重要?
因为它让我们确认了这一讲的核心观点:
真实应用程序确实就是计算 + 系统调用。
比如一个 Hello World:
text
普通计算:准备字符串
系统调用:write 输出到屏幕
系统调用:exit_group 退出
再比如 gcc:
bash
strace -f gcc a.c
你会发现 gcc 不是一个"单体魔法程序",它会启动其他进程,调用各种工具,读写大量文件。
这也再次体现了 Unix Philosophy:
text
一个复杂任务
│
▼
由多个小工具协作完成
十四、从 strace 看"任何程序"的共同结构
这一讲最后把应用程序的共同结构总结得非常清楚。
任何程序总是:
text
被操作系统加载
│
▼
经历状态机执行
│
├── 普通计算
├── 进程管理 API
├── 文件 / 设备 API
├── 存储管理 API
▼
最终退出
更具体一点:
text
进程管理:
fork, execve, exit
文件 / 设备管理:
open, close, read, write
存储管理:
mmap, brk
图解如下:
text
ELF 可执行文件
│ execve
▼
进程初始状态
│
▼
普通指令执行
│
├── read / write
├── open / close
├── mmap / brk
├── fork / execve
└── exit / exit_group
│
▼
进程结束
这张图非常重要。
它说明不管程序表面多复杂,本质都离不开这条主线。
十五、图形界面程序也一样吗?
很多同学可能会问:
命令行程序是这样,那图形界面程序呢?浏览器呢?游戏呢?
答案仍然是:
一样。
只不过图形界面程序调用的 API 更多,交互对象更复杂。
比如一个窗口管理器:
- 需要管理屏幕设备
- 需要绘制像素
- 需要接收键盘鼠标事件
- 需要和其他进程通信
但本质还是:
text
计算 + 操作系统 API
图解如下:
text
图形界面程序
│
├── 计算窗口布局
├── 处理用户输入
├── 读写配置文件
├── 和显示服务器通信
├── mmap 显存 / 缓冲区
└── send / recv 进程通信
所以别被"图形界面"吓住。
它只是比 Hello World 多调了一大堆系统服务。
十六、杀毒软件、病毒、任务管理器,本质上也都是 API 组合
这一讲特别有意思的地方,是它把很多"童年梦想"里的程序也拉进来了。
比如:
16.1 任务管理器
任务管理器能看到进程,是因为操作系统提供了进程对象的信息。
它可能会做:
text
读取进程目录
读取进程状态
显示 CPU / 内存占用
在类 Unix 系统中,这类信息常常可以通过 /proc 这样的接口暴露出来。
16.2 杀毒软件
杀毒软件也不是魔法。
它可能做两类事:
text
静态扫描:
read 文件内容,分析是否可疑
主动防御:
监控进程行为,观察异常系统调用
16.3 病毒
病毒也不是魔法。
它同样要靠系统 API 做事:
text
读文件
写文件
创建进程
修改启动项
联网通信
注入其他进程
这就回到了这一讲最核心的观点:
应用程序的能力边界,本质上由操作系统提供的对象和 API 决定。
十七、应用程序 = 计算 + 操作系统 API
现在我们可以给这一讲一个总公式:
text
应用程序 = 纯计算 + 操作系统 API
纯计算包括:
text
加减乘除
字符串处理
数据结构
算法逻辑
状态更新
操作系统 API 包括:
text
进程管理
文件读写
设备访问
网络通信
内存映射
进程间通信
权限控制
这就是应用视角下的操作系统。
十八、这一讲和上一篇有什么关系?
上一篇我们讲:
操作系统是复杂度逼出来的抽象层。
这一篇进一步讲:
应用程序通过系统调用使用这个抽象层。
两篇连起来就是:
text
第一讲:
为什么需要 OS?
因为复杂系统需要抽象。
第二讲:
应用怎么使用 OS?
通过系统调用和 OS API。
也就是说,第一讲是在讲"操作系统为什么存在",第二讲是在讲"程序如何站在操作系统上运行"。
十九、全文总结
如果你现在刚学 C、刚学一点数据结构,看到 syscall、ELF、strace 这些词可能会有点懵。
但你先抓住下面 5 件事就够了。
19.1 程序不是静态代码,而是动态状态机
代码只是文本。
真正运行起来后,程序有栈、有变量、有 PC、有状态迁移。
19.2 函数调用不是数学函数
C 函数会创建栈帧,会产生副作用,会改变程序状态。
19.3 编译器是在翻译状态机
编译器不是简单替换文本,而是把高级语言程序翻译成行为等价的机器程序。
19.4 syscall 是应用和 OS 的边界
普通指令还在程序自己的世界里。
一旦 syscall,控制权就交给操作系统。
19.5 strace 是观察程序行为的显微镜
看不懂程序怎么和 OS 交互?
跑一下:
bash
strace ./program
你会看到程序真实发起了哪些系统调用。
二十、结语:所谓应用程序,没有想象中那么神秘
浏览器、游戏、编辑器、杀毒软件、病毒、AI Agent,看起来千差万别。
但从操作系统视角看,它们都可以被还原成:
text
状态机 + 系统调用
或者更直白一点:
text
计算 + OS API
让我们第一次真正站在"应用视角"看操作系统:
- 程序如何开始?
- 程序如何退出?
- 程序如何请求 OS 服务?
- 程序如何访问文件、设备、网络?
- 程序如何被工具观察?
而这恰好也是学习操作系统最好的入口。
因为我们最终要理解的,不只是内核里发生了什么,更是:
我们写下的每一个程序,是如何真正活在操作系统之上的。
参考资料
-
南京大学 2026 春《操作系统》第二讲:《应用视角的操作系统》
https://jyywiki.cn/OS/2026/lect2.md -
OSTEP: Operating Systems: Three Easy Pieces
https://pages.cs.wisc.edu/~remzi/OSTEP/ -
Linux man-pages: syscalls(2)
https://man7.org/linux/man-pages/man2/syscalls.2.html -
Linux man-pages: strace(1)
https://man7.org/linux/man-pages/man1/strace.1.html -
Linux man-pages: elf(5)
https://man7.org/linux/man-pages/man5/elf.5.html