C04 · 第 1 章 · 进程与调度

进程与系统调用

介绍进程状态、用户态与内核态、创建和替换进程、文件描述符以及管道通信,分析执行顺序和资源关闭条件。

预备知识程序、类型、控制流与函数

本章目标

  1. 说明就绪、运行和阻塞状态之间的事件关系。
  2. 区分程序创建、映像替换和退出状态收集。
  3. 计算父子进程数量及共享文件位置的变化。
  4. 根据管道端点和调用返回值判断等待原因。
页面阅读位置0%
章节未开始
本册完成进度0/6 章 · 0%
本页目录

进程的运行状态

同一份可执行文件启动两次,通常会产生两个进程。每个进程有自己的虚拟地址空间、寄存器状态和内核管理信息;打开的文件、通信端点和权限也由内核记录。程序文件描述要执行的指令,进程记录这些指令当前执行到了哪里。两个进程即使运行相同代码,输入、执行位置和退出时间也可能不同。

处理器执行指令时,程序计数器指出下一条指令的位置,寄存器保存操作数和中间结果,栈保存函数调用所需的信息。切换到另一个进程之前,内核保存恢复执行所需的上下文。以后重新调度这个进程时,内核恢复上下文,执行才会从正确位置继续。上下文切换本身也占用处理器时间,频繁切换还可能影响缓存命中率。

进程状态

就绪进程已经具备执行条件,等待处理器分配;运行进程正在处理器上执行;阻塞进程正在等待输入、锁或其他事件。事件完成后,阻塞进程通常先进入就绪状态,调度器随后决定它何时运行。

单个逻辑处理器在任意时刻至多执行一个线程,但多个进程的执行区间能够交错。分析执行顺序时,我们需要同时记录进程状态和触发状态变化的事件。例如,磁盘读取仍在进行时,继续给同一个等待者分配时间没有帮助;调度其他就绪进程能够利用这段等待时间。多处理器系统还允许不同进程同时执行,单个逻辑处理器上的交错分析只是其中一种情况。

用户态与系统调用

普通应用在用户态执行,受到地址空间和特权级的限制。访问受保护硬件、修改地址转换配置或管理其他进程,需要进入内核提供的接口。系统调用指令触发受控的特权转换,内核根据调用编号分派处理函数,检查参数和权限,再把结果返回调用者。

调用中的指针仍然指向用户地址空间。内核必须检查地址范围、读写权限和长度,在适当时机复制数据,防止错误输入破坏内核状态。检查一个地址是否属于用户空间,还不能保证整个缓冲区都有效;长度相加可能溢出,缓冲区可能跨越未映射页面,其他线程也可能改变映射。因此系统调用实现需要按明确的内存访问规则处理完整范围。

库函数与系统调用处在不同层次。格式化输出可能先把文本放入用户空间缓冲区,随后合并为一次写入;某些时间查询又可能由内核提供的共享映射完成。我们分析开销时,应记录实际进入内核的次数,避免仅凭源代码中的函数调用数量估计成本。MIT xv6 教材的第 2、4 章说明特权级、陷入入口与系统调用参数传递,适合结合源码查看这段执行路径。

创建与替换进程

在 Unix 风格接口中,成功的 fork 创建子进程。父进程收到子进程标识,子进程收到零,两者从调用后的语句继续。它们随后对普通私有内存的修改相互独立;实现可使用写时复制延迟物理复制,但接口提供的私有内存语义保持一致。调用失败时不会产生子进程,父进程必须处理错误。

exec 系列接口把当前进程的用户程序映像替换为指定程序,成功后执行新程序的入口。进程标识通常保留,打开文件描述符是否保留取决于相应标志。父进程通过 wait 或 waitpid 收集子进程退出状态。已经退出、尚未被父进程收集状态的子进程会暂时保留退出信息,长期忽略收集可能耗尽相关系统资源。

连续创建的进程数量

设初始只有一个进程,所有创建调用均成功,每个进程依次执行两次无条件 fork,且中途没有退出。第一次调用后共有两个进程;两个进程都执行第二次调用,各产生一个子进程,因此最终有四个进程。若有 kk 次这样的创建阶段,令 NjN_j 为第 jj 次结束时的进程数,则

N0=1,Nj+1=2Nj,Nk=2k.N_0=1,\qquad N_{j+1}=2N_j,\qquad N_k=2^k.

我们核对创建次数:两阶段一共发生 1+2=31+2=3 次成功创建,增加三个进程,与最终四个一致。如果第二次调用只在最初的父进程分支执行,则只再增加一个进程,最终数量变成三个。计数依赖控制流,不能只数源文件中出现几次调用名。

文件描述符与共享位置

文件描述符是进程内的一项整数索引。描述符表项引用内核中的打开文件对象,对象保存访问模式、当前位置等状态。同一个进程复制描述符,或子进程继承描述符时,多个表项可能引用同一个打开文件对象。因此,私有变量与文件偏移具有不同的共享关系,需要分别分析。

继承描述符后的读取位置

一个普通文件含有十个字节 ABCDEFGHIJ。父进程先打开文件,初始偏移为零,再创建子进程。假定两者继承的描述符引用同一个打开文件对象,并通过同步保证父进程先成功读取三个字节、子进程再成功读取两个字节。

父进程得到 ABC,共享偏移由零增加到三。子进程随后从偏移三开始得到 DE,偏移最终为五。我们按成功返回的字节数累加,得到 0+3+2=50+3+2=5。若父子各自重新打开文件并从各自描述符读取,两个打开文件对象独立,子进程会从 A 开始。若缺少同步,具体由谁先读取得决于执行顺序,程序不能依赖这里指定的结果。

读取接口可能返回少于请求长度的字节数。顺序读取时,应按实际返回长度推进缓冲区位置;返回零通常表示文件结束,错误返回则需要根据错误原因处理。写入也要检查实际写入长度。把一次调用等同于完成整个业务消息,会在管道、终端和网络输入上产生边界错误。

管道与进程协作

管道提供字节流通信,程序用一端写入、另一端读取。构造两个进程组成的流水线时,每个进程应关闭自己不用的端点,父进程也要关闭多余副本。当所有写端均关闭且已有数据读完后,读者才能观察到文件结束。如果某个无关进程仍然持有写端,即使它永远不写数据,读者也可能持续等待。

管道容量有限。写者遇到满管道时可能阻塞,读者遇到空管道且仍有写端时也可能阻塞。这种背压限制生产者领先消费者的程度,却不能自动纠正进程之间的错误等待关系。例如,父进程先等待子进程退出,子进程又因管道已满等待父进程读取,就形成了相互等待。解决办法是安排并发读取,或先完成数据传递再收集退出状态。

实际排查时,我们先区分计算慢、等待事件和反复失败重试,再查看相关进程的状态、文件描述符与返回值。仅观察处理器占用率无法判断程序是否正常:等待输入的服务可能长期接近零占用,错误的忙等程序却可能持续满负载。记录事件条件与资源关系,才能解释进程停留在某个状态的原因。

进程接口练习

练习

初始一个进程,执行三轮无条件进程创建,每轮所有现存进程各创建一个子进程。计算最终数量与成功创建次数,并说明假设。

查看解答

每个已有进程都创建一个子进程,因此三阶段数量依次为 2、4、8。成功创建调用总数为 1+2+4=71+2+4=7。假设要求所有调用成功,失败时数量需要按实际返回值重算。

练习

进程在私有内存中保存整数四,创建子进程后,子进程把自己的变量改为九。父进程随后读取自己的变量,结果是什么?

查看解答

创建时父子各有值为 4 的私有变量。子进程赋值为 9 只改变自己的变量;父进程仍为 4。若改用显式共享内存,则必须重新分析共享映射与同步规则。

练习

程序请求读取一千零二十四字节,成功返回六百。需要完整记录的程序下一步如何更新缓冲区位置与剩余请求长度?

查看解答

本次只取得 600 字节,尚差 424 字节。后续从缓冲区偏移 600 继续请求;若遇文件结束或错误,按实际情况终止或处理,不能直接把未写入区域作为有效输入。

练习

子进程已关闭管道写端并退出,父进程读取完已有数据后仍然等待。父进程自己保留了一个不用的写端,这与等待有什么关系?

查看解答

读端判断结束需要所有写端关闭。父进程持有的写端仍有效,读者会继续等待。父进程应关闭不使用的写端,并让其他进程也关闭多余副本。

进程管理参考资料

Operating Systems: Three Easy Pieces

Remzi H. Arpaci-Dusseau, Andrea C. Arpaci-Dusseau · 2023 · Arpaci-Dusseau Books

打开官方来源

OSTEP 的 Process API 一章讨论创建、程序替换与等待接口,其中关于执行顺序的讨论有助于检查父子进程分支。阅读示例时应同时记录每个进程拥有的私有状态和继承资源,再对照实际返回值分析错误分支。

xv6: a simple, Unix-like teaching operating system

Russ Cox, Frans Kaashoek, Robert Morris · 2025 · Massachusetts Institute of Technology

打开官方来源

MIT xv6 教材第 1 章给出文件描述符、管道与 shell 的组合,第 4 章说明陷入和系统调用。xv6 是教学内核,其接口集合和错误处理细节与完整 POSIX 系统存在差别;实现实验时应使用所运行系统的接口说明。