【进程管理】生命周期与调度算法

上一篇 我们搞清了"进程是什么"(身份证、地址空间、状态机),这一篇我们聚焦 "进程如何被管理、如何被分配CPU时间" ------生命周期流转的完整过程,以及调度器选谁运行的底层逻辑。

前言:调度解决什么问题?

假设系统中有 10 个进程同时处于就绪态,但 CPU 只有 1 个(或有限个)核心。

调度器的任务就是:决定下一个运行谁,以及运行多久。

这是一个"资源分配"问题,核心矛盾在于:

  • 公平性:谁都不能被饿死
  • 效率:CPU 要尽可能满负荷运转
  • 响应时间:交互式应用(如键盘输入)不能卡顿
  • 吞吐量:批处理任务要尽可能多完成

一、进程的完整生命周期(五态模型)

上一篇我们讲了"三态模型"(就绪/运行/阻塞),但真实的进程生命周期比这更丰富。

(一) 五态模型

状态 含义 典型触发条件
创建态(New) 进程正在被创建,内核已分配PCB但资源尚未就绪 fork() 调用后、exec() 之前
就绪态(Ready) 万事俱备,只等CPU 创建完成 / 阻塞解除 / 时间片用完
运行态(Running) 正在CPU上执行指令 被调度器选中
阻塞态(Blocked) 因等待某事件而主动放弃CPU read()wait()、申请锁失败
终止态(Terminated) 进程已结束执行,但PCB尚未被回收 exit() 调用 / 收到终止信号

(二) 补充概念:挂起态(Suspend)

在内存资源紧张时,操作系统可以将某个进程的整个地址空间换出到磁盘 ,该进程进入挂起态

  • 就绪挂起:进程在外存,但随时可以调入内存执行
  • 阻塞挂起:进程在外存,且在等待某个事件

引入挂起态的核心原因:内存不够用了,把暂时不跑的进程"冷冻"到磁盘上腾出空间。

(三) 状态转换的完整规则

转换 触发条件 谁触发
创建 → 就绪 资源分配完成,进程初始化完毕 内核
就绪 → 运行 调度器选中该进程 调度器
运行 → 就绪 时间片用完 / 被高优先级进程抢占 时钟中断 / 调度器
运行 → 阻塞 请求I/O、等待事件、申请锁 进程自身(系统调用)
阻塞 → 就绪 I/O完成、事件发生、锁可用 硬件中断 / 其他进程
运行 → 终止 exit()、致命错误、被信号杀死 进程自身 / 内核
终止 → 消亡 父进程调用wait()回收资源 父进程

二、 调度算法

第一阶段:批处理时代的"公平排队"

① 先来先服务(FCFS, First Come First Served)

按到达顺序排队,非抢占。

  • 优点:简单、公平、无饥饿
  • 缺点:护航效应------一个长作业堵住后面所有短作业,平均等待时间极差

② 短作业优先(SJF, Shortest Job First)

预估运行时间,最短的先执行。

  • 优点:平均等待时间最优(数学上已证明)
  • 缺点:
    • 需要预知运行时间(不可能精确获知)
    • 长作业饥饿------短作业源源不断到来,长作业永远得不到执行

③ 高响应比优先(HRRN)

引入动态优先级优先级 = (等待时间 + 服务时间) / 服务时间

  • 短作业优先,但长作业等待久了优先级也会提升
  • 妥协方案,兼顾公平与效率

第二阶段:交互式时代的"分时轮转"

批处理时代的算法都是非抢占的,一个进程必须运行完毕才能让出CPU。这在分时系统(如Unix、Windows)中是不可接受的------用户点一下鼠标不能等几秒才响应。

④ 时间片轮转(RR, Round Robin)

每个进程分配一个固定时间片(典型值 10ms~100ms),用完即排队到队尾。

  • 优点:响应快,交互体验好
  • 缺点:
    • 时间片过大 → 退化为 FCFS,交互响应变差
    • 时间片过小 → 上下文切换开销占比飙升(系统把大部分时间花在"切换"而非"执行"上)

时间片大小选择的黄金法则:上下文切换开销 < 时间片的 1%。

第三阶段:兼顾公平与响应的"多级反馈队列"

⑤ 多级反馈队列(MFQ, Multilevel Feedback Queue)

这是 "兼顾响应性、公平性、效率" 的经典设计,广泛应用于现代操作系统的基础框架中。

核心机制:

markdown 复制代码
┌─────────────────────────────────────────────────────────────┐
│  就绪队列 0(优先级最高)→ 时间片 8ms  →  先来先服务       │
│  就绪队列 1(优先级中)  → 时间片 16ms →  先来先服务       │
│  就绪队列 2(优先级最低)→ 时间片 32ms →  轮转调度         │
└─────────────────────────────────────────────────────────────┘

规则:
1. 新进程入队 0(最高优先级)
2. 队列 0 中按 FCFS 执行,时间片内完成则退出;未完成则降级到队列 1
3. 队列 0 为空时,才执行队列 1;队列 1 为空时,才执行队列 2
4. 只要队列 0 中有新进程到达,立即抢占低优先级队列正在运行的进程

为什么 MFQ 是经典之作?

  • 交互式进程(如编辑器) :时间片内就能完成,始终停留在高优先级队列 → 响应极快
  • CPU密集型长进程 :逐渐降级到低优先级队列,获得更长的时间片 → 减少上下文切换开销
  • 公平性 :低优先级队列中的进程最终也能得到执行,不会饥饿
  • 无需预知运行时间:内核自动根据行为调整 → 这就是"反馈"的含义

Linux 的 CFS 虽然放弃了"多级队列"的结构,但 MFQ 的设计思想(区分I/O型与CPU型进程、动态调整优先级)深刻影响了 CFS。

三、Linux CFS(完全公平调度器):现代调度器的标杆

(一) 为什么需要 CFS?

传统 Unix 调度器(O(1)调度器)依赖复杂的优先级计算和多个运行队列,代码晦涩,且对"交互式进程"和"CPU密集型进程"的区分不够精准。

CFS(Completely Fair Scheduler,完全公平调度器)在 Linux 2.6.23 引入,核心思想简单到令人惊讶:

"给每个进程分配一个比例,确保它们获得"公平"的CPU时间份额。"

(二) 核心概念:vruntime(虚拟运行时间)

这是 CFS 的精髓。

  • vruntime 记录了一个进程"已经消耗了多少CPU时间",单位是纳秒
  • 但 vruntime 的增长速度是按优先级加权的:
    • 普通进程(nice=0):vruntime ≈ 实际运行时间
    • 高优先级(nice=-20):vruntime 增长更慢(同样的物理时间,vruntime加得更少)
    • 低优先级(nice=+19):vruntime 增长更快

CFS 的调度决策极其简单:永远选择 vruntime 最小的进程运行。

这就像一群人在排队领饭,每个人领完后会在"我已经吃了多少"的计数器上加一笔。CFS 总是让吃得最少的人先去吃------这样长期来看,所有人都吃得一样多,但"权重高"的人每口饭加得少,相当于能多吃几口。

(三) 数据结构:红黑树(Red-Black Tree)

CFS 如何快速找到 vruntime 最小的进程

答案:用 红黑树 组织所有就绪进程,以 vruntime 为键值。

  • 查找最小节点:O(log N),即使系统有 10000 个进程也能飞快完成
  • 插入/删除:O(log N),进程状态变化时高效更新
  • 自平衡:树的高度始终可控,不会退化
scss 复制代码
         [进程C vruntime=50]
          /              \
   [进程A vruntime=30]   [进程D vruntime=80]
        /      \
 [进程E vruntime=10]  [进程B vruntime=40]
                        (← 最左节点,vruntime最小,下一个运行)

(四) nice 值的含义

nice 值 是用户控制进程优先级的接口,范围 -20 ~ +19

  • nice 值越低 → 优先级越高 → 获得更多CPU时间
  • nice 值越高 → 优先级越低 → 获得更少CPU时间
  • 默认 nice = 0(普通优先级)

只有 root 可以调低 nice 值(提高优先级),普通用户只能调高 nice 值(降低优先级)

nice 值 优先级 适用场景
-20 ~ -10 极高 系统关键进程(不建议普通程序使用)
-5 ~ -1 较高 对延迟敏感的服务(如数据库)
0 普通 大多数应用程序
5 ~ 10 较低 后台批处理任务(如日志压缩、备份)
15 ~ 19 极低 非紧急的后台任务,保证不影响交互体验

在 CFS 中,nice 值与 vruntime 增长速率的关系:

约每降低一个 nice 值,CPU 时间份额增加约 10%。

即:nice=0 的进程获得 1 份时间;nice=-1 获得约 1.1 份;nice=+1 获得约 0.9 份。

两个 nice 值相差 1 的进程,长期 CPU 分配比例约为 1.1 : 1

(五) CFS 的抢占时机

CFS 不是"时间片"驱动的,而是通过 sched_latency_ns(调度延迟)决定每个进程能运行多久:

  • 目标调度延迟默认约 6ms(可配置)
  • 如果有 N 个就绪进程,每个进程一次最多运行 目标延迟 / N
  • 当一个进程运行时间超过它的"应得份额"时,CFS 会触发抢占

效果 :进程数少时,每个进程分到更长时间片(减少切换开销);进程数多时,每个进程分到更短时间片(保证响应性)。自适应,无需人工配置。

四、总结:

调度算法 核心思想 适用场景 核心问题
FCFS 先来先服务 批处理系统 护航效应
SJF 最短先执行 理想化模型 需预知时间、长作业饥饿
时间片轮转 固定时间片轮流 分时交互系统 时间片大小难调优
多级反馈队列 多级优先级 + 降级 + 抢占 通用操作系统基础框架 参数复杂
CFS vruntime最小者优先 + 红黑树 Linux(通用现代系统) 几乎无短板
  1. 调度的本质不是"选谁",而是"如何量化公平" ------CFS 用 vruntime 给出了一个优雅的数学答案
  2. 多级反馈队列是"自适应"的先驱------进程的行为(I/O密集型 vs CPU密集型)决定了它在哪一级队列,无需管理员手动配置
  3. nice 值不是"绝对优先级",而是"权重比例" ------CFS 中的 nice 控制的是 CPU 时间份额的相对比例,而非谁先谁后
  4. "抢占"与"切换"是两码事 ------调度器决定哪个进程该获得CPU,但真正让CPU放弃当前进程的,是时钟中断 或其他硬件中断

本文是「进程管理」系列的第二篇,欢迎关注,持续更新。如有任何疑问或指正,请在评论区留言讨论。

相关推荐
free351 小时前
【进程管理】Linux下的进程管理
操作系统
charlie1145141913 天前
Cinux · 第一次跳进 Ring 3:用户态与特权隔离
开发语言·c++·操作系统·开源项目
程序猿乐锅3 天前
【操作系统 | 第一章】从资源管理到虚拟机
操作系统
CoovallyAIHub5 天前
一个集装箱从船到火车的两个小时,AI 搭档 Coco在中间做了什么
操作系统·agent·资讯
驱动探索者5 天前
RISC-V 指令集深度解析:从 ISA 到 Zephyr
网络·计算机·操作系统·os
码农小韩6 天前
Linux操作系统(六)——软件包管理
linux·操作系统·linux驱动·嵌入式软件开发·linux应用
纵有疾風起6 天前
I/O 软件层次结构:从用户态到硬件中断的完整旅程
操作系统·408·中断·lut·驱动程序·设备独立性
TechEdu2026066 天前
[操作系统]操作系统文件系统与输入输出:架构、行为与调优
操作系统·计算机科学·os
charlie1145141916 天前
Cinux是如何管理进程的 —— 上下文与调度
开发语言·c++·操作系统·开源项目