-
Q1: 操作系统启动后到底做了什么?
-
Q2: 操作系统如何管理程序 (进程)?
-
主要内容
- 虚拟化:操作系统上的进程
- 进程管理 API
从系统启动到第一个进程
回顾 thread-os.c 的加载过程
- CPU Reset → Firmware → Boot loader → Kernel
_start()
操作系统会加载 “第一个程序”
init=,按照 “默认列表” 尝试一遍
-
- 从此以后,Linux Kernel 就进入后台,成为 “中断/异常处理程序”
程序:状态机
- C 代码视角:语句
- 汇编/机器代码视角:指令
- 与操作系统交互的方式:syscall
定制最小的 Linux
没有存储设备,只有包含两个文件的 “initramfs”
$ tree .
.
├── bin
│ └── busybox (可以在我们的Linux里直接执行)
└── init
加上 vmlinuz (内核镜像) 就可以在 QEMU 里启动了
小结:应用程序视角的操作系统
Linux 操作系统启动流程
- CPU Reset → Firmware → Loader → Kernel
_start() → 第一个程序
/bin/init → 程序 (状态机) 执行 + 系统调用
操作系统为 (所有) 程序提供 API(三大系统调用)
- 进程 (状态机) 管理
-
- fork, execve, exit - 状态机的创建/改变/删除 ← 今天的主题
- 存储 (地址空间) 管理
-
- mmap - 虚拟地址空间管理
- 文件 (数据对象) 管理
-
- open, close, read, write - 文件访问管理
- mkdir, link, unlink - 目录管理
fork()
执行流通过fork()分叉
操作系统:状态机的管理者
C 程序 = 状态机
- 初始状态:
main(argc, argv)
- 程序可以直接在处理器上执行
虚拟化:操作系统在物理内存中保存多个状态机
通过fork()来复制一份一样的状态机,除了fork()的返回值不一样
操作系统这时候就变成了并发程序
- 通过虚拟内存实现每次 “拿出来一个执行”
- 中断后进入操作系统代码,“换一个执行”
状态机管理:创建状态机
如果要创建状态机,我们应该提供什么样的 API?
UNIX 的答案: fork
- 做一份状态机完整的复制 (内存、寄存器现场)
int fork();
- 立即复制状态机 (完整的内存)
- 新创建进程返回 0
- 执行 fork 的进程返回子进程的进程号
记住 Fork 了!
因为状态机是复制的,因此总能找到 “父子关系”
- 因此有了进程树 (pstree)
systemd-+-accounts-daemon---2*[{accounts-daemon}]
|-agetty
|-atd
|-automount---2*[{automount}]
|-avahi-daemon---avahi-daemon
|-cron
|-dbus-daemon
|-irqbalance---{irqbalance}
|-lxcfs---7*[{lxcfs}] ...
fork()无情的复制机器
execve()
状态机管理:替换状态机
光有 fork 还不够,怎么 “执行别的程序”?
UNIX 的答案: execve
- 将当前运行的状态机重置成成另一个程序的初始状态
int execve(const char *filename, char * const argv, char * const envp);
- 执行名为
filename 的程序
- 允许对新状态机设置参数
argv (v) 和环境变量
envp (e)
- 刚好对应了
main() 的参数!
环境变量
“应用程序执行的环境”
- 使用
env 命令查看
-
- PATH: 可执行文件搜索路径
- PWD: 当前路径
- HOME: home 目录
- DISPLAY: 图形输出
- PS1: shell 的提示符
- export: 告诉 shell 在创建子进程时设置环境变量
_exit()
状态机管理:终止状态机
有了 fork, execve 我们就能自由执行任何程序了,最后只缺一个销毁状态机的函数!
UNIX 的答案: _exit
- 立即摧毁状态机
void _exit(int status)
- 销毁当前状态机,并允许有一个返回值
- 子进程终止会通知父进程 (后续课程解释)
这个简单……
- 但问题来了:多线程程序怎么办?
结束程序执行的三种方法
exit 的几种写法 (它们是不同)
- exit(0) - stdlib.h 中声明的 libc 函数
-
- 会调用 atexit
- _exit(0) - glibc 的 syscall wrapper
-
- 执行 “exit_group” 系统调用终止整个进程 (所有线程)
-
- 细心的同学已经在 strace 中发现了
- 不会调用 atexit
- syscall(SYS_exit, 0)
-
- 执行 “exit” 系统调用终止当前线程
- 不会调用 atexit
不妨试一试
结束当前进程执行的四种方式
总结
- Q1: 操作系统启动后到底做了什么?
- Q2: 操作系统如何管理程序 (进程)?
Take-away messages
- 对 “操作系统” 的完整理解
-
- CPU Reset → Firmware → Loader → Kernel
_start() → 执行第一个程序
/bin/init → 中断/异常处理程序
-
- 一个最小的 Linux 系统的例子
- 进程管理 API
-
- fork, execve, exit: 状态机的复制、重置、销毁
- 理论上就可以实现 “各种功能” 了!