跳转至

进程与线程

1. 进程(Process)

1.1 定义

进程是程序的一次执行实例,是操作系统进行资源分配的基本单位。

每个进程拥有独立的:

  • 虚拟地址空间(代码段、数据段、堆、栈)
  • 文件描述符表
  • 信号处理表
  • 进程 ID(PID)

1.2 进程控制块(PCB)

操作系统用 PCB(Process Control Block) 维护每个进程的元数据:

字段 内容
PID 进程标识符
进程状态 Running / Ready / Waiting / ...
PC(程序计数器) 下一条待执行指令地址
CPU 寄存器 通用寄存器、栈指针等快照
调度信息 优先级、调度队列指针
内存管理信息 页表基址、段表
I/O 状态 打开的文件列表、I/O 请求

1.3 进程状态转换

stateDiagram-v2
    [*] --> New: 创建
    New --> Ready: 允许进入就绪队列
    Ready --> Running: 调度器选中
    Running --> Ready: 时间片用完 / 被抢占
    Running --> Waiting: 等待 I/O 或事件
    Waiting --> Ready: I/O 完成 / 事件发生
    Running --> Terminated: 执行结束 / 异常退出
    Terminated --> [*]

1.4 进程创建与终止

UNIX/Linux 模型

  • fork():创建子进程(复制父进程地址空间,COW 优化)
  • exec():替换进程映像为新程序
  • wait() / waitpid():父进程等待子进程终止
  • exit():进程终止

进程树:所有进程形成以 init/systemd(PID 1)为根的树。

2. 线程(Thread)

2.1 定义

线程是 CPU 调度的基本单位,同一进程的线程共享地址空间和资源。

属性 进程 线程
地址空间 独立 共享(同进程)
创建开销 大(需复制地址空间) 小(仅需栈和寄存器)
上下文切换 慢(需切换页表、TLB flush) 快(共享地址空间)
通信方式 IPC(管道、消息、共享内存) 直接读写共享变量
隔离性 弱(一个线程崩溃影响全进程)

2.2 线程实现

类型 实现位置 优势 劣势
用户级线程 用户空间线程库 切换快、无需内核参与 一个线程阻塞→整个进程阻塞
内核级线程 操作系统内核 真正并行、可利用多核 切换需陷入内核,开销大
混合模型 M:N 映射 兼顾两者优势 实现复杂

现代实践

  • Linux:1:1 模型(NPTL),每个用户线程对应一个内核线程
  • Go:M:N 模型(goroutine → OS thread),运行时调度器管理

2.3 上下文切换

上下文切换的开销:

操作 开销来源
保存/恢复寄存器 直接 CPU 时间
TLB 刷新(进程切换) 切换后 TLB miss 增加
Cache 污染 新进程/线程的数据挤出旧数据
流水线刷新 分支预测器状态失效

典型开销:进程切换 ~1-10 μs,线程切换 ~0.1-1 μs。

3. CPU 调度

3.1 调度准则

指标 含义 优化目标
CPU 利用率 CPU 忙碌时间占比 最大化
吞吐量 单位时间完成的进程数 最大化
周转时间 提交到完成的总时间 最小化
等待时间 在就绪队列中等待的总时间 最小化
响应时间 提交到首次响应的时间 最小化(交互式系统)

3.2 调度算法

FCFS(First Come First Served)

  • 先到先服务,非抢占式
  • 护航效应(Convoy Effect):长进程阻塞后面的短进程

SJF / SRTF

  • SJF(Shortest Job First):选择预计运行时间最短的进程(非抢占)
  • SRTF(Shortest Remaining Time First):SJF 的抢占版本
  • 理论最优(最小化平均等待时间),但需要预知运行时间
\[ \text{平均等待时间}_{\text{SJF}} \leq \text{平均等待时间}_{\text{其他算法}} \]

实际中使用指数加权移动平均预测下次 CPU burst:

\[ \tau_{n+1} = \alpha \cdot t_n + (1 - \alpha) \cdot \tau_n \]

Round Robin(时间片轮转)

  • 每个进程获得固定时间片(quantum) \(q\)
  • 时间片耗尽 → 回到就绪队列尾部
  • \(q\) 太大 → 退化为 FCFS;\(q\) 太小 → 上下文切换开销过大
  • 典型值:10-100 ms

MLFQ(Multi-Level Feedback Queue)

现代操作系统的基础调度框架:

规则

  1. 高优先级队列的进程优先执行
  2. 同优先级队列内使用 Round Robin
  3. 新进程进入最高优先级队列
  4. 用完时间片 → 降级到下一级队列
  5. 定期将所有进程提升到最高优先级(Boost,防饥饿)
优先级 高  [Q0] ── RR, quantum = 8ms
          [Q1] ── RR, quantum = 16ms
          [Q2] ── RR, quantum = 32ms
优先级 低  [Q3] ── FCFS
  • I/O 密集型进程:频繁让出 CPU → 保持在高优先级 → 响应快
  • CPU 密集型进程:用尽时间片 → 逐渐降级 → 不影响交互性

CFS(Completely Fair Scheduler)

Linux 默认调度器(自 2.6.23 起):

  • 目标:每个进程获得公平的 CPU 时间份额
  • 使用虚拟运行时间(vruntime)追踪每个进程已获得的CPU时间
  • 用红黑树维护就绪队列,vruntime 最小的进程优先运行
  • nice 值影响 vruntime 增长速率

4. 进程同步

4.1 竞态条件(Race Condition)

多个线程并发访问共享数据,最终结果取决于执行顺序。

临界区(Critical Section):访问共享资源的代码段,需要互斥保护。

临界区解决方案需满足:

  1. 互斥(Mutual Exclusion):同时只有一个线程在临界区
  2. 进展(Progress):不在临界区的线程不能阻止他人进入
  3. 有界等待(Bounded Waiting):等待进入临界区的时间有上界

4.2 同步原语

互斥锁(Mutex)

pthread_mutex_lock(&mutex);    // 获取锁(阻塞)
// 临界区
pthread_mutex_unlock(&mutex);  // 释放锁

实现基础:原子指令(CAS、test-and-set、LL/SC)

自旋锁(Spinlock)

while (test_and_set(&lock) == 1)
    ; // 忙等待(spin)
// 临界区
lock = 0;
  • 等待时间短时高效(无上下文切换)
  • 等待时间长时浪费 CPU
  • 适合多核环境下的短临界区

信号量(Semaphore)

Dijkstra 提出,是更通用的同步机制:

  • P(wait/down)\(S \leftarrow S - 1\),若 \(S < 0\) 则阻塞
  • V(signal/up)\(S \leftarrow S + 1\),若有等待者则唤醒一个
类型 初始值 用途
二元信号量 1 等价于互斥锁
计数信号量 \(N\) 控制资源并发访问数

条件变量(Condition Variable)

与 mutex 配合,允许线程在条件不满足时等待:

pthread_mutex_lock(&mutex);
while (!condition)
    pthread_cond_wait(&cond, &mutex);  // 原子释放锁+睡眠
// 条件满足,执行操作
pthread_mutex_unlock(&mutex);

必须用 while 而非 if

cond_wait 返回后条件可能已被其他线程改变(虚假唤醒 spurious wakeup),必须重新检查。

4.3 经典同步问题

生产者-消费者(Bounded Buffer)

信号量:
  mutex = 1          // 互斥访问缓冲区
  empty = N          // 空槽位数
  full = 0           // 满槽位数

生产者:                         消费者:
  P(empty)                       P(full)
  P(mutex)                       P(mutex)
  放入数据                        取出数据
  V(mutex)                       V(mutex)
  V(full)                        V(empty)

读者-写者问题

  • 读者优先:多个读者可同时读,写者需等所有读者完成
  • 写者优先:写者到来后,新读者等待
  • 公平版:按到达顺序处理

哲学家就餐问题

5 个哲学家围坐圆桌,每人之间一根筷子,需同时拿起左右两根才能吃饭。

解决方案

  • 最多允许 4 人同时尝试拿筷子
  • 奇数号先拿左再拿右,偶数号相反(破坏循环等待)
  • 使用一把全局锁(降低并发度)

5. 死锁(Deadlock)

5.1 必要条件(Coffman 条件)

同时满足以下四个条件才可能发生死锁:

  1. 互斥(Mutual Exclusion):资源不可共享
  2. 持有并等待(Hold and Wait):持有资源的进程可以请求新资源
  3. 不可剥夺(No Preemption):资源不能被强制回收
  4. 循环等待(Circular Wait):存在进程的循环等待链

5.2 处理策略

策略 方法 实际应用
预防 破坏四个条件之一 资源排序(破坏循环等待)
避免 动态检查是否安全 银行家算法
检测+恢复 允许发生,检测后处理 数据库(超时+回滚)
鸵鸟策略 忽略问题 大多数通用 OS

5.3 银行家算法(Banker's Algorithm)

Dijkstra 提出的死锁避免算法。系统在分配资源前检查是否存在安全序列

数据结构\(n\) 个进程,\(m\) 类资源):

  • \(\text{Available}[m]\):当前可用资源向量
  • \(\text{Max}[n][m]\):每个进程的最大需求
  • \(\text{Allocation}[n][m]\):当前已分配
  • \(\text{Need}[n][m] = \text{Max} - \text{Allocation}\)

安全性检查

  1. 初始化 \(\text{Work} = \text{Available}\)\(\text{Finish}[i] = \text{false}\)
  2. 找到进程 \(i\) 使得 \(\text{Finish}[i] = \text{false}\)\(\text{Need}[i] \leq \text{Work}\)
  3. \(\text{Work} = \text{Work} + \text{Allocation}[i]\)\(\text{Finish}[i] = \text{true}\)
  4. 重复直到所有进程 Finish 或找不到可执行的进程
  5. 若所有 \(\text{Finish}[i] = \text{true}\),则系统处于安全状态
\[ \text{时间复杂度} = O(n^2 \times m) \]

6. 进程间通信(IPC)

机制 特点 适用场景
管道(Pipe) 单向、字节流、父子进程 Shell 管道 |
命名管道(FIFO) 有文件名、无亲缘关系 进程间简单通信
消息队列 结构化消息 异步通信
共享内存 最快(无需拷贝) 大数据量传输
信号(Signal) 异步通知 进程控制(SIGKILL等)
Socket 跨网络 网络通信

导航


评论 #