背景

一言难尽的并发部分终于结束


我们学了什么?

  • (并发) 程序的状态机模型
  • 编写 (threads.h) 和阅读并发程序 (model checker)
  • 互斥 (并发数据结构) 和同步 (条件变量、信号量)

本次课内容与目标

精彩的内容就此开始!


用 “状态机” 的视角重新理解进程

  • 学习 UNIX 进程管理 API

虚拟化:进程抽象

回到 1950s (没有操作系统的时代)

假想你们对 OSLab0 (amgame) 编程

int main() {
  while (1) {
    ...
  }
}

如果想同时运行两个程序?

  • 那找两台机器吧

今天的程序能也能在 1950s 的计算机上运行吗?

  • 只要没有 “进程间通信”
  • ls, grep, cat, 简易编译器...
    • 就像 OSLab0, 它们都可以 “直接在计算机上运行”

(单线程) 程序的状态机模型

你们太熟悉了:指令驱动的状态迁移

  • 绝大部分指令只能执行 $(M, R)$ 上的确定性计算
  • 少数指令可能造成不确定性 (例如 rdrand)
  • 唯一一个和 “外界” 交互的指令:syscall

类似于 AbstractMachine (TRM + IOE)

  • IO 操作相当于 syscall

运行多个程序 = 运行多个状态机

嗷!操作系统 “模拟” 了其中所有进程的状态机!

这就是 “虚拟化”

  • 程序仿佛自己独占 CPU 执行 (进程在执行时确实独占)
  • 但还有它看不见的 “其他状态”

所有状态机都保存在共享内存里

  • 操作系统代码借助中断、虚拟存储等机制完成切换
    • “操作系统是一个中断处理程序”
      • 被动的中断:硬件 (时钟、I/O设备、NMI, ...)
      • 主动的中断:系统调用

经典操作系统中最重要的三类系统调用

进程 (状态机) 管理

  • fork, execve, exit - 状态机的创建/改变/删除

存储 (地址空间) 管理

  • mmap - 对进程虚拟地址空间的一部分进行映射
  • brk - 虚拟地址空间管理

文件 (数据对象) 管理

  • open, close - 文件访问管理
  • read, write - 数据管理
  • mkdir, link, unlink - 目录管理

(操作系统课可以在此结束,剩下部分大家完全可以自学了)

fork()

状态机管理:创建状态机

如果要创建状态机,我们应该提供什么样的 API?

  • UNIX 的答案: fork
    • 做一份状态机完整的复制 (内存、寄存器现场)

int fork();

  • 复制状态机,新创建的进程返回 0,执行 fork 的进程返回进程号

Fork Bomb

模拟状态机需要资源

  • 只要不停地创建进程,系统还是会挂掉的
  • Don't try it (or try it in docker)
    • 你们交这个到 Online Judge 是不会挂的

代码解析: Fork Bomb

:(){:|:&};:   # 刚才的一行版本

:() {         # 格式化一下
  : | : &
}; :

fork() {      # bash: 允许冒号作为标识符……
  fork | fork &
}; 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: 习题 (1)

试着拿出一张纸,写出以下程序的输出结果

  • fork-demo.c
    • 你心里可能立即想,运行一下不就完了吗?
    • 试图去理解它
pid_t pid1 = fork();
pid_t pid2 = fork();
pid_t pid3 = fork();
printf("Hello World from (%d, %d, %d)\n", pid1, pid2, pid3);

理解 fork: 习题 (2)

问以下程序的输出结果

  • 一个更好的版本: fork-printf.c
    • 用状态机的视角再试一次
    • 然后考虑一下进程持有的操作系统对象应该被如何复制?
#define n 2

int main() {
  for (int i = 0; i < n; i++) {
    fork();
    printf("Hello\n");
  }
}

计算机系统里没有魔法。机器永远是对的。

理解 fork: 习题 (3)

多线程程序的某个线程执行 fork(),应该发生什么?

  • 这是个很有趣的问题:创造 fork 时创始人并没有考虑线程

我们可能作出以下设计:

  • 仅有执行 fork 的线程被复制,其他线程 “卡死”
  • 仅有执行 fork 的线程被复制,其他线程退出
  • 所有的线程都被复制并继续执行
    • 这三种设计分别会带来什么问题?

execve()

状态机管理:替换状态机

光有 fork 还不够,我们还需要能 “执行别的程序” 啊?

  • UNIX 的答案: execve
    • 将当前运行的状态机重置成成另一个程序的初始状态

int execve(const char *filename, char * const argv, char * const envp);

  • 执行名为 filename 的程序
  • 允许对新状态机设置参数 argv (v) 和环境变量 envp (e)

环境变量

“应用程序执行的环境”

  • 使用 env 命令查看
    • PATH: 可执行文件搜索路径
    • PWD: 当前路径
    • HOME: home 目录
    • DISPLAY: 图形输出
    • PS1: shell 的提示符
  • export: 告诉 shell 在创建子进程时设置环境变量
    • 小技巧:export ARCH=x86_64-qemuexport ARCH=native
    • 上学期的 AM_HOME 终于破案了

环境变量:PATH

可执行文件搜索路径

  • 还记得 gcc 的 strace 结果吗?
[pid 28369] execve("/usr/local/sbin/as", ["as", "--64", ...
[pid 28369] execve("/usr/local/bin/as", ["as", "--64", ...
[pid 28369] execve("/usr/sbin/as", ["as", "--64", ...
[pid 28369] execve("/usr/bin/as", ["as", "--64", ...
  • 这个搜索顺序恰好是 PATH 里指定的顺序
$ PATH="" /usr/bin/gcc fork-demo.c
gcc: error trying to exec 'as': execvp: No such file or directory
$ PATH="/usr/bin/" gcc fork-demo.c

计算机系统里没有魔法。机器永远是对的。

_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

不妨试一试

结束当前进程执行的四种方式

  • return, exit, _exit, syscall
  • exit-demo.c
    • 用 strace 观察程序的执行

总结

总结

本次课内容与目标

  • 用 “状态机” 的视角重新理解进程
    • 学习 UNIX 进程管理 API

Takeaway messages

  • 进程 = 状态机
  • 进程管理 = 状态机创建、重置、删除