14. 操作系统上的进程
第一个进程
回顾:进程 = 状态机
编译器:C 状态机 → 汇编状态机的翻译器
- 特殊的指令/函数:syscall
- 我们同样可以建模 (模拟) 进程的执行
1 | def StateMachine(): |
操作系统的启动
Firmware 阶段
- CPU Reset 后,Firmware 代码开始执行
- 加载操作系统
操作系统初始化阶段
- 操作系统扫描系统中的硬件、初始化数据结构……
- 加载第一个进程 (状态机)
操作系统执行阶段
- 状态机在 CPU 上执行
- 允许执行
syscall
进入操作系统代码
进程的创建
我们能控制这个行为吗?
- 计算机系统没有魔法
- 你能想到的事就能实现
人工智能就能帮你实现
- 我希望用 QEMU 在给定的 Linux 内核完成初始化后,直接执行我自己编写的一个 hello 二进制文件。我应该怎么做?
- 在这个过程中,发散出很多概念 → 知识体系的快速建立
恰恰是 UNIX “干净” 的设计 (完成初始化后将控制权移交给第一个进程) 使得 Linus 可以在可控的工程代价下实现 (相当完善的) POSIX 兼容,从而掀起一场操作系统的革命。时至今日,实现接口级的兼容已经是一件极为困难的工程问题,典型的例子是微软的工程师最终抛弃了 API 行为兼容的 Windows Subsystem for Linux 1.0,进而转向了虚拟机上运行的 Linux 内核。
整个 Linux 的 “世界” 都是从这个进程开始,并通过一系列实现进程管理的操作系统 API 创建的。
创建新进程
创建状态机
1 | pid_t fork(void); |
现在我们已经有 “一个状态机” 了
- 只需要 “创建状态机” 的 API 即可
- UNIX 的答案: fork
- 做一份状态机完整的复制 (内存、寄存器现场)
fork() 的行为
立即复制状态机
- 包括所有信息的完整拷贝
- 每一个字节的内存
- 打开的文件 (共享)
- ……
- 复制失败返回 -1
- errno 会返回错误原因 (man fork)
如何区分两个状态机?
- 新创建进程返回 0
- 执行 fork 的进程返回子进程的进程号
Fork Bomb
新建状态机需要资源
- 只要不停地创建进程,系统还是会挂掉的
代码解析: Fork Bomb
1 | :(){:|:&};: # 刚才的一行版本 |
- 类比原子弹:一个重原子核 (U-235/Pu-239) 被中子击中后分裂成两个较轻的原子核,同时释放出能量和更多的中子
- “自我增殖”
进程总有 “被创建” 的关系
- 因此总能找到 “父子关系”
- 因此有了进程树 (pstree)
1 | systemd-+-ModemManager---2*[{ModemManager}] |
运行可执行文件
如何运行可执行程序?
1 | int execve(const char *filename, |
UNIX 的答案: execve (重置状态机)
- 将当前进程重置成一个可执行文件描述状态机的初始状态
execve 行为
- 执行名为 filename 的程序
- 允许对新状态机设置参数 argv (v) 和环境变量 envp (e)
- 刚好对应了 main() 的参数!
- execve 是唯一能够 “执行程序” 的系统调用
- 因此也是一切进程 strace 的第一个系统调用
fork() + execve()
UNIX 中实现 “创建新状态机” 的方式
- Spawn = fork + execve
1 | int pid = fork(); |
环境变量
“应用程序执行的环境”
- 使用 env 命令查看
- PATH: 可执行文件搜索路径
- PWD: 当前路径
- HOME: home 目录
- DISPLAY: 图形输出
- PS1: shell 的提示符
- export: 告诉 shell 在创建子进程时设置环境变量
- 小技巧:export ARCH=x86_64-qemu 或 export ARCH=native
环境变量:PATH
可执行文件搜索路径
- 还记得 gcc 的 strace 结果吗?
1 | [pid 28369] execve("/usr/local/sbin/as", ["as", "--64", ... |
- 这个搜索顺序恰好是 PATH 里指定的顺序
1 | $ PATH="" /usr/bin/gcc a.c |
计算机系统里没有魔法。机器永远是对的。
退出程序
状态机管理:销毁状态机
1 | void _exit(int status); |
fork + exec = 自由执行任何程序
- 还差一个销毁状态机的函数就完整了
- UNIX 的答案: _exit
- 立即摧毁状态机,允许有一个返回值
- 子进程终止会通知父进程 (后续课程解释)
这个简单……
- 但问题来了:多线程程序怎么办?
结束程序执行的三种方法
exit 的几种写法 (它们是不同)
- exit(0)
- 会调用 atexit
- _exit(0)
- 执行 “exit_group” 系统调用终止整个进程 (所有线程)
- 会调用 atexit 吗?
- syscall(SYS_exit, 0)
- 执行 “exit” 系统调用终止当前线程
- 会调用 atexit 吗?
因为 “程序 = 状态机”,操作系统上进程 (运行的程序) 管理的 API 很自然地就是状态机的管理。在 UNIX/Linux 世界中,以下三个系统调用创建了整个 “进程世界”,不论是我们常用的 IDE 和浏览器,还是编译时在后台调用的 gcc。其中,fork 对当前状态机状态进行完整复制,execve 将当前状态机状态重置为某个可执行文件描述的状态机,exit: 销毁当前状态机。在对这个概念有了绝对正确且绝对严谨的理解后,操作系统也就显得不那么神秘了。