跳转至

生命周期、调度与进程间通信

进程从创建到退出会在运行、就绪和等待等状态之间迁移。调度器选择下一个执行流,IPC 则让相互隔离的进程交换数据与同步事件。本页把三者放在同一张状态图中理解。

进程状态机

最小模型包含:

新建 -> 就绪 -> 运行 -> 退出
         ^       |
         |       v
         +---- 等待
  • 就绪:具备运行条件,只缺 CPU。
  • 运行:当前占用某个 CPU 核心。
  • 等待/阻塞:等待 I/O、锁、定时器或消息,给 CPU 也无法继续。

区分就绪与等待是诊断的关键。一个任务长期不运行,可能是 CPU 竞争,也可能是等待事件;两者需要完全不同的优化。

forkexecwaitexit

Unix 风格进程创建把“复制当前进程”和“装入新程序”拆开:

  1. fork 创建子进程,子进程从近似相同的执行现场继续。
  2. 子进程可调整文件描述符、工作目录和环境。
  3. exec 用新程序替换当前地址空间。
  4. 父进程用 wait 获取子进程退出状态并回收记录。

现代 fork 通常使用写时复制:父子先共享只读物理页,任一方写入时才复制。这使创建后立即 exec 的常见路径无需复制全部内存。

最小例子

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
    pid_t pid = fork();
    if (pid == 0) {
        execlp("printf", "printf", "child process\n", (char *)0);
        _exit(127);
    }
    if (pid > 0) {
        int status;
        waitpid(pid, &status, 0);
    }
    return pid < 0;
}

exec 成功后不会返回;失败路径用 _exit,避免重复刷新从父进程复制来的用户态缓冲。

僵尸与孤儿

子进程退出后,内核仍需保存退出码和少量统计,直到父进程调用等待接口。这段状态常称僵尸。僵尸不再执行,也不占用原地址空间,但会占用进程标识和记录。

孤儿进程是父进程先退出、子进程仍运行的情形。系统会把它交给指定的收养者处理,确保最终有人回收退出状态。僵尸和孤儿不是同一概念。

调度评价指标

设作业到达时间为 \(a\),首次运行时间为 \(s\),完成时间为 \(f\),实际 CPU 服务时间为 \(b\)

\[ T_{turnaround}=f-a, \]
\[ T_{response}=s-a, \]
\[ T_{waiting}=T_{turnaround}-b. \]
  • 周转时间关注从提交到完成。
  • 响应时间关注第一次得到反馈。
  • 等待时间关注在就绪队列中损失的时间。
  • 吞吐量关注单位时间完成多少工作。
  • 公平性和截止期限又是不同目标。

只报告平均值可能掩盖长尾。交互系统经常更关心高分位延迟,而不是平均响应时间。

经典调度策略

先来先服务

FCFS 简单、开销低,但一个长作业可让许多短作业等待,形成护航效应。它适合到达顺序具有业务意义或任务长度相近的场景,不适合强交互负载。

最短作业优先

若已知服务时间,SJF 可以最小化一组作业的平均等待时间。现实中未来运行时间通常未知,只能用历史行为估计;长作业还可能长期饥饿。

时间片轮转

RR 让就绪任务轮流运行一个时间片。时间片太长时接近 FCFS;太短时响应更快,但上下文切换比例增大。

若时间片为 \(q\),切换成本为 \(c\),忽略其他因素时,CPU 用于有效工作的比例上界近似为:

\[ U=\frac{q}{q+c}. \]

增大 \(q\) 提高该比例,却可能损害交互响应。这是典型吞吐—延迟权衡。

优先级与多级反馈队列

优先级调度允许重要任务先运行,但低优先级任务可能饥饿。老化机制会随等待时间提升优先级。

多级反馈队列尝试从行为推断任务类型:短时间主动阻塞的交互任务保留较高优先级,持续占用 CPU 的任务逐步下移。它不需要提前知道作业长度,但参数多,可能被工作负载行为影响。

公平调度

公平调度不一定按固定队列轮转,而是追踪任务获得的加权 CPU 服务,优先运行“欠服务”的任务。权重、睡眠唤醒、最小粒度和多核负载均衡都会影响实际行为。

实时调度

实时系统的“实时”指满足时间约束,不只是运行得快。

  • 硬实时:错过截止期限可能导致系统失败。
  • 软实时:错过期限会降低质量,但并非灾难。

对一组周期任务,任务 \(i\) 的执行时间为 \(C_i\)、周期为 \(T_i\),总利用率为:

\[ U=\sum_i\frac{C_i}{T_i}. \]

\(U\le 1\) 只是单核可调度的必要直觉之一,并不自动保证所有调度策略都能满足期限;阻塞、抢占成本和任务相位也要计入分析。

多核调度的新问题

  • 每核运行队列降低锁竞争,却需负载均衡。
  • 任务迁移利用空闲核心,却损害缓存与 NUMA 局部性。
  • 同时多线程共享核心资源,两个逻辑 CPU 并非两个完整物理核心。
  • 高优先级任务等待低优先级持锁者会发生优先级反转,可用优先级继承等协议缓解。

调度不是独立模块;它和锁、内存放置、功耗及热管理都有关联。

为什么需要 IPC

进程地址空间隔离后,普通指针不能跨进程直接传递。IPC 为通信建立受控通道,同时规定数据边界、缓冲、阻塞和权限。

管道

管道提供字节流,适合有亲缘关系或由 shell 连接的单向数据流:

producer stdout -> pipe buffer -> consumer stdin

它简单且自带背压:缓冲区满时写者可能阻塞。局限是消息边界通常不保留,复杂拓扑和随机访问不方便。

消息队列与套接字

消息传递保留逻辑消息或通过协议组织字节流。Unix 域套接字适合本机进程,网络套接字可跨机器。内核可实施权限与缓冲管理,但数据复制、序列化和协议处理有成本。

共享内存

共享内存把同一物理页映射到多个进程,避免为大数据反复复制。它只解决“双方都能看到字节”,不自动解决同步、一致性和对象生命周期。

共享数据区 + 互斥/原子协议 + 所有权约定

没有同步协议的共享内存只是更快地制造竞态。

信号与事件通知

信号适合异步通知少量事件,不适合承载大数据。处理器能调用的函数和可安全访问的状态受限制,复杂逻辑通常应转移到正常控制流中完成。

IPC 选择表

需求 优先考虑 主要代价
shell 风格流式连接 管道 单向、无随机访问
本机结构化请求/响应 Unix 域套接字或消息队列 序列化、复制、协议
跨主机通信 网络套接字 网络故障与部分失败
本机大块高吞吐数据 共享内存 同步与生命周期复杂
简单异步通知 信号/事件对象 信息量小、处理受限

背压与故障

任何有界通道都必须决定满时怎么办:阻塞生产者、丢弃、覆盖旧数据、扩容或向上游传播失败。无限缓冲只是把问题推迟到内存耗尽。

跨进程通信还要处理对端退出、半关闭、重复消息和超时。跨机器时故障模型更复杂:无法仅凭“没收到响应”区分请求未执行、执行中还是响应丢失。

自测

  1. 为什么就绪任务和阻塞任务的优化方向不同?
  2. fork 后立即 exec 时,写时复制节省了什么?
  3. 时间片越短为什么不一定越公平或响应越好?
  4. 共享内存为何仍需要 IPC 协议?
  5. 如何区分僵尸进程和孤儿进程?
参考思路

就绪任务需要 CPU,阻塞任务在等待事件。写时复制避免复制随后会被 exec 丢弃的地址空间。过短时间片增加切换和缓存成本。共享页不定义同步、消息边界或所有权。僵尸已经退出等待回收,孤儿仍可运行但原父进程已退出。