CRIU 工作原理与使用场景
1. 核心思想
CRIU(Checkpoint/RestoreInUserspace)要解决的问题是:
如何把一个运行中的进程"拍快照"存到磁盘,之后再原样恢复。
它的突破点在于,一个进程的"状态"几乎全都能通过 Linux 内核已有的接口从用户态读出来和写回去,所以不需要在内核里写一个庞大的 checkpoint 子系统。
一个进程的完整状态包括:
| 状态类别 | 具体内容 |
|---|---|
| 内存 | 代码段、数据段、堆、栈、mmap 区域 |
| CPU 上下文 | 寄存器、FPU 状态、信号掩码 |
| 打开的资源 | 文件 fd、socket、pipe、eventfd、timerfd |
| 进程关系 | PID、PPID、线程、会话、进程组 |
| 其他 | 挂载命名空间、cgroup、TCP 连接状态等 |
CRIU 本身是一个用户态项目/工具(criu.org),不是内核的一部分,但依赖一系列专门的内核接口才能工作。它是Linux 平台专属的机制。
2. Checkpoint(转储)流程
关键技术点:
- 冻结进程:用
ptrace(PTRACE_SEIZE)把目标进程及其所有子进程停住。 - 收集信息:大量读取
/proc/<pid>/下的虚拟文件(maps、fd、stat、status 等)。 - 寄生代码(parasite code):CRIU 把一小段代码注入到目标进程内部运行,因为有些信息(如进程内部的内存内容、寄存器)只有"在进程里面"才能拿到。dump 完成后再把这段代码清理掉。
- 序列化:所有状态写成一组
.img镜像文件(Protocol Buffers 格式)。
3. Restore(恢复)流程
恢复的精妙之处:
- 复用 PID:通过
/proc/sys/kernel/ns_last_pid或 clone 带指定 PID 的能力,让恢复的进程拿到和原来一样的 PID(很多程序依赖自己的 PID)。 - “变形”(restorer blob):CRIU 进程本身最后会把自己的地址空间替换成目标进程的,然后跳转到断点继续执行——就像"借壳还魂"。
4. 依赖的内核能力
CRIU 不是内核的一部分,但依赖内核提供这些接口:
/proc/<pid>/丰富的进程信息ptrace(冻结、注入)kcmp()系统调用(判断两个 fd 是否指向同一对象)CAP_CHECKPOINT_RESTORE权限(内核 5.9+,专为 C/R 设计)- TCP repair 模式(恢复 TCP 连接而不断链)
- 各种驱动的 C/R 插件接口(如 AMDKFDGPU 插件)
5. 典型使用场景
1. 容器热迁移(最主要用途)
把一个运行中的容器从机器 A不中断地迁移到机器 B。Podman、LXC/LXD、OpenVZ、Kubernetes(Forensic Container Checkpointing)都集成了 CRIU。
# 机器 Acriu dump-t<pid>-D/checkpoint --shell-job# 拷贝 /checkpoint 到机器 B# 机器 Bcriu restore-D/checkpoint --shell-job2. 长任务的保存/恢复
科学计算、HPC、AI 训练跑几天,担心机器重启或崩溃。做周期性 checkpoint,崩溃后从最近快照恢复,而不是从头再来。这正是 GPU CRIU 的核心价值——ROCm 训练任务可以连 GPU 状态一起保存恢复。
3. 加速启动 / "慢启动"服务
有些程序启动要做大量初始化(加载模型、预热缓存)。可以启动一次 → checkpoint → 之后直接从快照恢复,秒级冷启动。Java/JVM 的 CRaC、FaaS 冷启动优化都用到这个思路。
4. 进程级调试 / 快照回放
把出问题的进程状态 dump 下来,带到别处反复分析、复现 bug。
5. 负载均衡 / 节能
把进程从繁忙节点迁移到空闲节点,或迁移后关闭空闲机器省电。
6. 局限性
- 仅限 Linux,且对内核版本有要求
- 跨机器迁移要求两端CPU 架构、内核、库版本基本兼容
- 某些资源难以恢复:依赖特定硬件的设备 fd、外部网络对端状态、GUI/X11 连接等(这类资源通常需要专门的驱动插件支持)
7. 案例:CRIU 遇到设备 fd 时怎么办
普通进程的资源(内存、普通文件、socket)CRIU 都能自己处理,但当进程打开了一个设备文件 fd(如/dev/kfd、render node、各种加速器节点)时,CRIU 就进入了盲区——它只知道"有一个指向某设备的 fd",完全不了解这个 fd 背后在内核驱动和硬件里关联了哪些状态(显存、队列、映射等)。
下面以 AMD GPU(amdgpu/KFD)为例,说明处理这类设备 fd 的通用模式。
7.1 为什么 CRIU 自己做不了
一个设备 fd 背后的状态往往满足以下任意一点,导致 CRIU 无法独立处理:
| 特点 | 举例(GPU) |
|---|---|
| 状态不在系统内存里 | 显存缓冲区(VRAM BO) |
| 状态由硬件/固件维护 | 正在被硬件调度的队列、波前上下文 |
| 存在驱动内部的句柄/地址映射 | BO handle、GPU 虚拟地址、doorbell |
| 需要特定时序地暂停/恢复硬件 | 队列 evict(冻结)与 restore(解冻) |
这些只有驱动自己知道怎么安全地导出和重建。
7.2 通用解决模式:用户态插件 + 内核 ioctl
CRIU 为此提供插件(plugin)机制,驱动厂商按两层实现:
- 用户态插件:注册到 criu 的一个
.so,声明"这类设备 fd 由我负责",在 dump/restore 时被 criu 回调。 - 内核态接口:驱动提供一个专用 ioctl,真正完成设备内部状态的导出与重建。
7.3 分阶段协议(以 KFD 为例)
设备状态的 C/R 通常不是一次性完成,而是拆成多个阶段,配合 criu 主流程的节奏:
| 阶段 | 作用 |
|---|---|
PROCESS_INFO | dump 前:暂停(evict)设备队列,统计需要保存的对象数量和大小,告诉用户态要分配多大缓冲区 |
CHECKPOINT | 把设备内部状态(显存、队列、事件、映射)导出到用户缓冲区 |
UNPAUSE | dump 结束后恢复队列运行(若选择让原进程继续) |
RESTORE | 在新进程里按快照重建设备对象,并还原原有的句柄 / 地址 |
RESUME | 最后一步:解除对内存通知的阻塞,让进程与设备真正恢复运行 |
7.4 恢复时的关键约束:一致性
设备 fd 恢复后,应用里保存的各种"指针"必须依旧有效,所以驱动在 restore 时要保证:
- 句柄不变:缓冲区/对象的 handle 与 checkpoint 前一致
- 地址不变:GPU 虚拟地址、doorbell 偏移等保持原值
- 映射关系还原:同一块显存在多设备间的映射要重新建立
这也是为什么设备 C/R 必须由驱动主导——只有它掌握这些内部映射的完整信息。
7.5 小结
CRIU 处理设备 fd 的通用思路是:把自己搞不定的部分"外包"给驱动。
criu 负责整体进程的冻结/恢复与调度,驱动插件负责设备侧状态的导出/重建,两者通过"用户态插件 + 内核 ioctl + 分阶段协议"协作。amdgpu/KFD 的 CRIU 支持正是这一模式的典型实现。
8. 显存数据的存储开销与优化
设备 C/R 必须把显存 BO 里的实际数据(而不仅是元数据)保存下来,否则恢复后 GPU 显存就是一片垃圾值,任务无法正确续跑。显存里装的是真正的计算内容:模型权重、优化器状态、激活值、中间张量等,一个字节都不能少。
8.1 开销有多大
checkpoint 大小基本等于进程占用的显存量:
| 场景 | 显存占用 ≈ checkpoint 大小 |
|---|---|
| 小模型推理 | 几百 MB ~ 几 GB |
| 单卡 LLM 训练 | 20 ~ 80 GB |
| 多卡大模型 | 每卡几十 GB,总量可达数百 GB ~ TB |
对大模型训练,一次 checkpoint 几十上百 GB 是正常现象——这是 GPU C/R 的固有成本,不是实现缺陷。
8.2 常见优化手段
| 手段 | 说明 |
|---|---|
| 压缩 | CRIU 支持镜像压缩;显存数据(尤其含大量零/稀疏)常有可观压缩比 |
| 增量 checkpoint | 只保存相比上次快照变化的页(dirty pages),而非每次全量 |
| 快速存储 | 落到 NVMe / 本地高速盘,缩短冻结窗口 |
| 降低频率 | 只在迁移、维护窗口、崩溃容错点触发,而非常态化高频保存 |
| 应用层替代 | 框架(PyTorch/TF)自带的保存权重机制,只存必要张量,通常远小于全显存 |
8.3 为什么仍然值得做
- 对无法改代码的闭源/第三方 GPU 程序,CRIU 是唯一能"冻结续跑"的办法。
- 对热迁移场景,必须连完整显存状态一起搬,应用层 save/load 做不到无缝。
- 磁盘空间相对 GPU 算力时间是便宜的——存几百 GB 换回几天的训练进度,很划算。
一句话:显存数据必须全量保存,checkpoint 大小 ≈ 显存占用;业界用压缩、增量、快速存储、降低频率等手段把开销压到可接受范围,用"磁盘空间"换"不中断 / 可恢复"的价值。