从0开始的操作系统学习之路(2)

应用视角的操作系统:程序到底是什么?------从状态机、syscall 到 strace 看懂应用如何运行

目录

  • [应用视角的操作系统:程序到底是什么?------从状态机、syscall 到 strace 看懂应用如何运行](#应用视角的操作系统:程序到底是什么?——从状态机、syscall 到 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 服务?
  • 程序如何访问文件、设备、网络?
  • 程序如何被工具观察?

而这恰好也是学习操作系统最好的入口。

因为我们最终要理解的,不只是内核里发生了什么,更是:

我们写下的每一个程序,是如何真正活在操作系统之上的。


参考资料

  1. 南京大学 2026 春《操作系统》第二讲:《应用视角的操作系统》
    https://jyywiki.cn/OS/2026/lect2.md

  2. OSTEP: Operating Systems: Three Easy Pieces
    https://pages.cs.wisc.edu/~remzi/OSTEP/

  3. Linux man-pages: syscalls(2)
    https://man7.org/linux/man-pages/man2/syscalls.2.html

  4. Linux man-pages: strace(1)
    https://man7.org/linux/man-pages/man1/strace.1.html

  5. Linux man-pages: elf(5)
    https://man7.org/linux/man-pages/man5/elf.5.html

相关推荐
daanpdf2 小时前
高中数学必修一必修二课本习题答案人教版(A版+B版)
笔记
九成宫2 小时前
WSL2 网络优化配置:提升Git克隆与包下载速度
windows·笔记·代理模式·pip·wsl
羊群智妍2 小时前
2026 GEO实战:AI搜索优化监测工具选型与用法
笔记
爱喝水的鱼丶2 小时前
SAP-ABAP:数据类型与数据对象 第二篇:底层逻辑篇——数据类型的分类体系与底层存储原理
运维·开发语言·学习·sap·abap
夹芯饼干2 小时前
CentOS 7 虚拟机联网与 yum 源配置笔记
linux·笔记·centos
wuxinyan1232 小时前
工业级大模型学习之路017:RAG零基础入门教程(第十三篇):文本分块技术全解析
人工智能·python·学习·rag
小+不通文墨2 小时前
树莓派接温湿度传感器显示温度湿度
经验分享·笔记·单片机·嵌入式硬件·学习
@杰克成3 小时前
Java学习28
java·python·学习
孬甭_3 小时前
单链表详解
c语言·数据结构