我们希望 “模拟” 一个操作系统:crazy-os.c 能加载 argv 指定的 binary (p1, p2, …),基于 mini-rv32ima.h 模拟单步执行 (RISC-V 指令),并能够处理 syscall。
操作系统为每个进程都维护了二进制文件之外 “操作系统” 意义上的状态。实现一个 C 语言工具 (假设 Linux 系统),不调用外部工具,尽可能地获取并展示出进程自身不直接可见的操作系统状态。
创建一棵 5 层的进程树,并随机退出其中的一些进程——我们可以观察进程退出前后父子进程的关系。
如果我们使用深度优先搜索,总是需要维护当前的 “搜索状态”。通常这是通过将状态作为参数传递实现的 (当然,也可以用维护全局状态的方式实现)。借助 fork(),我们可以在每个搜索分支创建一个当前状态的快照,实现并行搜索。
fork() 会完整复制状态机;新创建的状态机返回值为 0,执行 fork() 的进程会返回子进程的进程号。同时,操作系统中的进程是并行执行的。程序的精确行为并不显然——model checker 可以帮助我们理解它。在这个例子中,我们还发现执行 ./a.out 打印的行数和 ./a.out | wc -l 得到的行数不同。根据 “机器永远是对的” 的原则,我们可以通过提出假设 (libc 缓冲区影响) 求证、对比 strace 系统调用序列等方式,最终理解背后的原因。标准输入输出的缓冲控制可以通过 setbuf(3) 和 stdbuf(1) 实现。
execve 有三个参数:path, argv, envp,分别是可执行文件的路径、传递给 main 函数的参数和环境变量。execve 是一个 “底层” 的系统调用,而 POISX 额外提供了 execl 等库函数便于我们使用。请搜索互联网或询问人工智能理解它们的区别。
除了 libc 为我们提供的 exit 函数之外,Linux 提供了两个系统调用:exit 和 exit_group,它们可以在 syscalls(2) 的手册中看到。同时,你也可以在这份手册中看到 Linux 肩负的 “历史包袱”。strace 可以查看应用程序是如何 “退出” 的。
操作系统通过进程抽象为应用程序提供了独立的执行环境。进程是操作系统中最基本的资源分配单位,它包含程序本身的状态和操作系统内部的状态。操作系统提供了一系列系统调用 (如 Linux 中的 fork、exec、wait 和 exit,Windows 中的 CreateProcess 和TerminateProcess) 来创建、管理和终止进程。
课程网站 (首页上的信息、课程概述、参考书与参考资料、生存指南);教科书 Operating Systems: Three Easy Pieces: