资源
正文
2. 应用视角的操作系统
2.1 操作系统上的程序
Operating System: A body of software, in fact, that is responsible for making it easy to run programs (even allowing you to seemingly run many at the same time), allowing programs to share memory, enabling programs to interact with devices, and other fun stuff like that. (OSTEP)
操作系统:实际上是一套软件,负责让运行程序变得简单(甚至让你看起来可以同时运行多个程序),允许程序共享内存,使程序能够与设备交互,以及其他类似的有趣功能。(摘自 OSTEP)
UNIX 哲学是一种软件设计理念,核心思想是:
- 做一件事,并做好它 —— 每个程序只专注一个功能。
- 小程序可组合 —— 通过管道或接口把小工具组合成复杂功能。
- 用文本作为接口 —— 便于调试、重定向和程序间通信。
- 简单优先 —— 避免复杂设计,程序易理解、易维护。
- 透明可调试 —— 输出清晰,便于排查问题。
总的来说,就是 小而专、可组合、简单、用文本沟通。
理解计算机程序
要想理解 “操作系统”,就要理解什么是 “程序”
- (这门课也一直从应用的视角讲操作系统)
1 |
|
优化类型 | 说明 |
---|---|
常量折叠(Constant Folding) | a + b 在编译期计算为 2 ,可能直接生成 printf("1 + 1 = 2\n") |
死代码消除(Dead Code Elimination) | 未使用的变量或计算会被去掉 |
寄存器优化 | a, b, c 可能直接存寄存器,不占用内存 |
函数内联 / 参数优化 | 对 printf 的参数计算尽量优化,减少运行时开销 |
完全优化 | 在高优化等级下,可能直接输出结果,不做实际加法运算 |
计算机:无情的执行指令的机器
- 机器永远是对的
- 如果编译器没有优化,“我们写什么,机器就执行什么”
- (编译器会对这个代码做什么优化呢?)
程序就是一个状态机。
Everything is a state machine.
- 任何程序都在计算机上运行
- 计算机是状态机
- 程序的执行就是状态的变化
C 语言的状态机模型
- PicoC: a very small C interpreter for scripting; 和你的 NEMU 一样 “单步执行”:
1 |
|
场景示例
- 解释器:不断读取用户输入的命令并执行。
- 操作系统内核:类似 事件循环/调度循环,不停地处理任务或系统调用。
- 游戏循环:不断获取用户输入、更新状态、渲染画面。
能不能把刚才的想法真的实现出来(画出 C 语言程序的流程图)?
-
AI 时代:
只要你想到,就能做到
-
曾经为 gdb(GNU Debugger)编程是非常繁琐的
GDB(GNU Debugger)是一个程序调试工具,用于 单步执行程序、设置断点、查看和修改变量、检查调用栈,帮助程序员定位和修复程序错误。
-
但现在繁琐的事已经不需要人类来干了
- 未来任何事都不需要人类了
-
使用提示词:
给我一个 Python 脚本,使得它能单步执行 gdb,并且输出一个 plot.md,嵌入 main 执行的状态迁移图,状态是局部变量 (只保留我程序中定义的变量),每一次 step 是一次迁移,执行的语句去掉行首空格画在 step 上。
把任意程序改写成非递归
C 代码总是可以改写成等价的 “SimpleC”
SimpleC 通常指的是一种简化版 C 语言或者是教学用 C 语言子集,它的特点是去掉了 C 语言中一些复杂或高级的特性,保留最基本的语法和语义,用于教学、编译原理实验或者嵌入式系统学习。
Everything (C 程序) = 状态机
堆(Heap)
- 用于动态分配内存(
malloc
、new
等)。
- 程序运行时大小可变,由程序员/语言运行时控制。
栈(Stack)
用于函数调用管理:局部变量、返回地址、函数参数等。
自动分配/释放,后进先出。
代码段 固定在内存中的代码段区域,不是堆,也不是栈。
堆用于动态数据,栈用于函数调用,代码段只存指令。
- 状态 = 变量数值 + 栈
- 初始状态 = main 的第一条语句
- 状态迁移 = 执行一条语句中的一小步
- 为什么你觉得还写不出汉诺塔?
“状态机” 是拥有严格数学定义的对象。这意味着你可以把定义写出来,并且用数学严格的方法理解它 —— 形式化方法
状态
[StackFrame, StackFrame, ...]
+ 全局变量
程序运行时的“状态”由两部分组成:
调用栈(Call Stack):
- 是一个栈结构,里面保存着多个
StackFrame
(栈帧)。- 每个
StackFrame
表示一个正在执行的函数(包括main
)。- 最上面的帧(
frames[-1]
)是当前正在执行的函数。全局变量(Global Variables):
- 所有函数之外定义的变量,它们在整个程序执行过程中都存在。
初始状态
- 仅有一个
StackFrame(main, argc, argv, PC=0)
- 全局变量全部为初始值
程序刚开始时,只有一个函数
main
在执行;这个函数的栈帧包含:
- 函数名:
main
- 参数:
argc
,argv
- 程序计数器
PC = 0
(表示main
还没执行任何语句)全局变量都处于定义时的初始值(例如全为 0 或指定值)。
状态迁移
- 执行
frames[-1].PC
处的简单语句
查看当前最上层栈帧(
frames[-1]
)的程序计数器PC
;
PC
指向当前要执行的语句;执行那条语句后:
- 程序的状态(变量、栈帧等)可能会改变;
- 然后
PC
增加,指向下一条语句。
可以用这个语义实现任何纯粹的计算
- 从简单到复杂: strlen, strstr, memcpy, sprintf, …
但还有些东西实现不了
- 有些标准库的行为超出了 “纯粹的计算”
- 例子: putchar, exit
- 观察:“纯粹的计算” 只能改变程序内的状态
- 但这些 API 涉及到 “程序外的状态”
- 这就是《操作系统》课的内容了
程序通过操作系统的系统调用以改变程序外的状态。
2.2 操作系统上的最小程序
什么是程序?
答案在 NEMU 中。
1 |
|
字段 | 含义 | 说明 |
---|---|---|
regs[32] |
通用寄存器 | 保存 CPU 当前的 32 个寄存器值 |
csrs[CSR_COUNT] |
控制/状态寄存器 | 保存 CPU 的状态(例如 PC、模式、中断状态) |
mem |
内存指针 | 指向模拟的物理内存 |
mem_offset |
内存起始地址 | 内存基地址(逻辑) |
mem_size |
内存大小 | 这块内存的字节数 |
这个结构体相当于模拟器中“整个 CPU 的快照(snapshot)”,
它包含了执行时所需的所有关键信息:寄存器状态 + 控制状态 + 内存内容。
处理器:无情的、执行指令的状态机
- 从 取出一条指令
- 执行它
- 循环往复
程序自己是不能 “停下来” 的
- 指令集里没有一条关闭计算机的指令,那么操作系统是如何在关闭所有软件后,切断计算机的电源的?
只能借助操作系统
1 |
|
- 把 “系统调用” 的参数放到寄存器中
- 执行 syscall,操作系统接管程序
- 操作系统可以任意改变程序状态 (甚至终止程序)
(二进制)程序=状态机
状态
- gdb 内可见的内存和寄存器
初始状态
-
由 ABI 规定 (例如有一个合法的 %rsp)
程序在刚开始运行时(即进入
main
之前),CPU 的某些寄存器和内存状态必须满足一定的约定,这些约定由 ABI(Application Binary Interface,应用二进制接口) 规定。
状态迁移
- 执行一条指令
- 我们花了一整个《计算机系统基础》解释这件事
- gdb 可以单步观察状态机的执行
- syscall 指令: 将状态机 “完全交给” 操作系统(上帝 & 祈祷)
2.3 操作系统上的应用程序
我们感受到的”操作系统“
作为用户,我们是感受不到操作系统的
- 我们只能感受到操作系统上运行的程序(进程)
能直接看到的程序:Applications
开发
- 集成开发环境 Vscode, Cursor, …
- 编程工具 gcc, clang, nodejs, gdb, …
- 终端工具 tmux, vim, htop, …
日用
- 办公 LibreOffice, GIMP, …
- 浏览器 Chrome, Firefox, …
- 媒体 OBS, VLC, …
能直接看到的(幕后)程序
Core Utilities (coreutils)(核心工具集)
-
Standard programs for text and file manipulation
Coreutils(GNU Core Utilities) 一组最基础、最常用的命令行工具,是所有 Unix/Linux 系统的核心组成部分。
-
系统中默认安装的是 GNU Coreutils
名称 | 特点 |
---|---|
busybox | 把几十个命令合并成一个可执行文件,体积小(常用于嵌入式系统) |
toybox | 现代、简洁的替代品(被 Android 系统广泛使用) |
系统/工具程序:无所不能
- Shell, binutils, …
- 包管理 apt, dpkg, …
- 网络 ip, ssh, curl, …
- 多媒体 ffmpeg (真神 1), gstreamer (真神 2), …
类别 | 示例 | 作用 |
---|---|---|
Shell | bash , zsh , fish |
命令行解释器,是人与系统交互的核心 |
Binutils | as , ld , objdump , nm |
GNU 的二进制工具集,用于汇编、链接、反汇编、查看 ELF |
包管理工具 | apt , dpkg , yum , pacman |
安装、更新、卸载软件包 |
网络工具 | ip , ssh , curl , ping , netcat |
网络配置、远程登录、数据传输 |
多媒体工具 | ffmpeg , gstreamer |
音视频编解码、流处理(作者称之为“真神”) |
不能直接看到的:各种后台程序
“守护进程” daemon
- 万物归宗 systemd
- systemd-network, systemd-logind, systemd-udevd, …
- 系统管理 cron, udiskd, unattended-upgrade (讨厌), …
- 各类服务 httpd, sshd, …
- 安全组件 auditd, firewalld, …
- 用户服务 pulseaudio, dbus-daemon, …
图形与媒体
- Wayland compositor; xfce4, lxde, …
- Pulseaudio, pipewire, video4linux, …
“不能直接看到的后台程序”指的是所有守护进程 (daemon)。 它们不直接与用户交互,而是在后台执行系统任务或提供服务。 在现代 Linux 中:
- systemd 是核心管理者
- 其他 daemons 是各领域的工人(网络、安全、音频、图形等)
所以所有这一切的程序…
和 minimal.S 有任何区别吗?
- 简短的答案:没有
- 任何程序 = minimal.S = 状态机
minimal.S
是一个用汇编写的最小启动代码(startup code), 负责在程序执行前建立基本环境(比如栈、寄存器、跳到 C 语言入口函数)。
可执行文件是操作系统中的对象
- 与 minimal 的二进制文件没有本质区别
- 让我们在命令行里看看上面那些 “程序” 都是什么
- 如果你是第一次接触命令行……也不用怕
- 问问大语言模型吧:我有一个 a.out 文件,我如何探索它里面有什么?
目的 | 命令 |
---|---|
查看类型 | file a.out |
查看 ELF 头 | readelf -h a.out |
查看段信息 | readelf -S a.out |
查看符号 | nm a.out |
反汇编 | objdump -d a.out |
提取字符串 | strings a.out |
调试分析 | gdb a.out |
操作系统中的”任何程序“
任何程序 = minimal.S = 状态机
- 总是从被操作系统加载开始
- 通过另一个进程执行 execve 设置为初始状态
- 经历状态机执行 (计算 + syscalls)
- 进程管理:
fork
,execve
,exit
, … - 文件/设备管理:
open
,close
,read
,write
, … - 存储管理:
mmap
,brk
, …
- 进程管理:
- 最终调用
_exit (exit_group)
退出
想象”一切应用程序“的实现
应用程序 = 计算 + 操作系统 API
- 窗口管理器
- 能直接管理屏幕设备 (
read
/write
/mmap
)- 能画一个点,理论上就能画任何东西
- 能够和其他进程通信 (
send
,recv
)
- 能直接管理屏幕设备 (
- 任务管理器
- 能访问操作系统提供的进程对象 (
readdir
)
- 能访问操作系统提供的进程对象 (
- 杀毒软件
- 文件静态扫描 (
read
)、主动防御 (ptrace
)
- 文件静态扫描 (
操作系统的职责:提供令应用程序舒适的抽象 (对象 + API)