Firecracker 快照恢复缺页处理机制详解:内核缺页加载与 Userfaultfd 用户态页故障服务
【免费下载链接】firecrackerSecure and fast microVMs for serverless computing.项目地址: https://gitcode.com/GitHub_Trending/fi/firecracker
Firecracker 在从快照恢复(snapshot resume)时,需要把快照中 file-backed 的 Guest 内存按需搬入 RAM。如何搬、由谁来搬,直接影响冷启动延迟、内存装载吞吐与故障隔离能力。本文以 docs/snapshotting/handling-page-faults-on-snapshot-resume.md 为骨架,结合快照恢复 API、persist.rs恢复实现与src/firecracker/examples/uffd下的参考 handler,讲清"内核逐页缺页加载"与"Userfaultfd 用户态缺页服务"两条路径的完整原理、交互协议、与 balloon 等设备的配合约束及工程化避坑指南。读完你将掌握/snapshot/load中mem_backend两种后端的选择依据,以及如何编写、加固一个可投入生产的用户态页故障处理进程。
快照恢复时 Guest 内存是如何被加载的
Firecracker 的快照通常由三部分组成:Guest 内存文件(memory file)、微虚拟机状态文件(microVM state file)以及由用户自行管理的磁盘文件。在加载快照时,Firecracker并不会在恢复瞬间把整份内存文件全部读入物理内存,而是对 memory file 建立一个MAP_PRIVATE映射,实现运行时的按需内存页加载;后续 Guest 的写入则落到 copy-on-write 的匿名内存映射中。这也是"从快照恢复"能做得非常快、但要求 memory file 在恢复后的微虚拟机整个生命周期内始终存在的原因。
这种按需加载的天然代价在于缺页处理:每次 Guest 触碰到尚未驻留在 Firecracker 进程内存中的页,就会产生一次缺页(page fault)。默认情况下,缺页由宿主内核负责兜底,其处理过程是一次上下文切换加一次 IO 操作。如果 Guest 内存很大、冷页很多,逐页进行"陷入内核 → IO → 返回用户态"的处理会非常耗时。
因此,Firecracker 把快照加载方式抽象为两种内存后端(对应/snapshot/load请求中的mem_backend.backend_type字段):
File:依赖宿主内核处理缺页,把 memory file 内容按需搬入内存,backend_path指向快照的内存文件;Uffd:把 Guest 内存区域的缺页处理权交给一个专用的用户态进程,backend_path指向该进程监听的一个 Unix domain socket。
后者的实现基础就是 Linux 的Userfaultfd机制。宿主内核是否支持、UFFD 对象如何创建,因内核版本而异(详见下文"创建 UFFD 对象")。
Userfaultfd:把缺页事件从内核空间移交到用户空间
Userfaultfd 是一种把缺页事件处理责任从内核空间移交到用户空间的机制。用户态需要先取得一个 userfault 文件描述符对象(UFFD),再把自己关心的内存地址范围注册进去,之后该范围内的缺页事件(含触发缺页的地址)就会被投递到用户态,由用户态决定用哪种UFFDIO_*操作去解决这次缺页。
在 Firecracker 的Uffd后端场景下,涉及两个用户态进程协同工作:
- Firecracker 进程(
src/vmm):负责创建 UFFD、匿名映射 Guest 内存并注册区域、把 UFFD fd 与内存布局通过 Unix socket 交给缺页处理进程; - 页故障处理进程(由用户负责编写):监听 Userfaultfd 事件,把内存文件内容搬运到对应 Guest 页。
用户必须自行实现这个处理进程来监视并处理 userfaultfd 事件——仓库提供的只是可参考示例(见下文"官方示例 handler 解剖")。
创建 UFFD 对象:内核版本差异
- 宿主内核 5.10:UFFD 对象通过直接调用
userfaultfd系统调用来创建。 - 宿主内核 6.1:UFFD 通过
/dev/userfaultfd字符设备创建,对该设备的访问受文件系统权限管控,因此 Firecracker 进程需要具备相应权限。从 jailer 源码 可以看到,jailer 会解析主机上该设备的主次设备号并尝试在 jail 内准备该节点:只要宿主机存在/dev/userfaultfd,jailer 就会把它放入 jail 中,使 Firecracker 进程无需额外配置即可使用;若设备缺失,jailer 会报告userfaultfd device not loaded一类的错误。
如果用户不借助 jailer 运行 Firecracker,则需要自行管理对/dev/userfaultfd的权限。例如在依赖访问控制列表(ACL)的系统中,可执行:
sudo setfacl -m u:${USER}:rw /dev/userfaultfd注册内存区域与事件处理
取得 UFFD 之后,下一步是把内存地址范围注册到 userfault 文件描述符上,使 userfault 对象能监视这些地址上发生的缺页。注册完成后,用户态进程就能通过 userfault fd 读取并服务事件;事件中会携带触发缺页的地址。处理线程可选择用内核提供的UFFDIO_*系列操作(例如UFFDIO_COPY、UFFDIO_ZEROPAGE等)来解决这些缺页。
需要注意的是,Firecracker 侧的 UFFD 依赖EVENT_REMOVE特性。在 persist.rs 的guest_memory_from_uffd实现中可以看到:
uffd_builder.require_features(FeatureFlags::EVENT_REMOVE);即恢复时创建的 UFFD 强制要求内核支持非协作式 userfaultfd 的UFFD_EVENT_REMOVE事件,这是后续与 balloon 设备协同工作的前提。
Firecracker 与页故障处理进程的完整交互流程
整条交互链路以 Unix domain socket 为纽带,由页故障处理进程(用户设计)发起监听。下面是完整时序:
- 页故障处理进程绑定并监听一个 Unix domain socket,以便与 Firecracker 进程通信(下图)。
- 用户向 Firecracker 的 API 线程发起
PUT /snapshot/load请求,请求体封装了"页故障处理进程监听的那个 Unix domain socket 的路径"。 - Firecracker 进程创建 userfault 对象,取得 userfault 文件描述符。
- 页故障处理进程把 Guest 内存文件的内容以私有方式
mmap到自己的地址空间。 - Firecracker 根据微虚拟机状态文件中的内存描述,以匿名映射方式建立内存,并把这些内存区域注册到 userfault 对象上,使 userfaultfd 能够感知这些地址上的缺页事件;随后 Firecracker 连接页故障处理进程先前打开的 socket。
- Firecracker 通过 socket 把 userfault 文件描述符与 Guest 内存布局(如各内存区域尺寸、以及以 KiB 为单位的页面大小)传给页故障处理进程。
- 完成信息交接后,Firecracker 继续执行常规的快照恢复流程:从微虚拟机状态文件读取相关序列化组件并载入内存。
- 此后 Firecracker 触碰 Guest 内存所引发的缺页,全部交由页故障处理进程通过之前收到的 userfault fd 读取事件并服务:收到缺页事件后,处理进程执行
UFFDIO_COPY,把先前 mmap 的内存文件内容拷入对应内存区域。
需要特别强调的两个边界:
- jail 边界:如果使用了 Jailer,则页故障处理进程、Unix domain socket 与内存文件都必须位于 jail 内部;UDS 只应对 Firecracker 与页故障处理进程可见。
- 通信只发生一次:Firecracker 发出载荷(内存映射与文件描述符)之后,UDS 上(或其它通道)不会再有任何 Firecracker 与页故障处理进程之间的通信。
源码级验证:UFFD 交接如何实现
src/vmm/src/persist.rs是上述流程的核心实现。guest_memory_from_uffd完成了 UFFD 创建、逐区域注册与握手:
- 由
backend_mappings为每个内存区域构造GuestRegionUffdMapping(字段包括宿主虚拟地址基址base_host_virt_addr、区域大小size、在文件后端中的偏移offset与配置的页大小page_size),该结构体即 persist.rs 中定义、并与示例 handler 共享约定的内存布局描述; - 对每个内存区域调用
uffd.register(...)注册进 userfault 对象; - 通过
send_uffd_handshake把序列化后的Vec<GuestRegionUffdMapping>与 UFFD 文件描述符经由 Unix socket 发送出去——底层使用 SCM_RIGHTS(sendmsg附带 fd)跨进程传递文件描述符,示例侧对应的接收逻辑是uffd_utils.rs中的recv_with_fd; - 得到的
Uffd通过set_uffd存入 Vm 结构(见 vstate/vm.rs),供后续恢复期使用。
因此,UFFD 交接的实质是一次"带外"握手:握手消息是序列化 JSON 格式的内存区域清单 + 一个通过 socket 附带发送的 fd。示例 handler 中对该 JSON 反序列化失败或未收到 fd 时会重试数次(见uffd_utils.rs的get_mappings_and_file,默认重试 5 次、每次间隔 100ms),随后根据assert_eq!(memsize, size)校验内存文件尺寸与区域总大小一致。
API 侧完整入参:backend_type、backend_path 与 huge_pages
在 docs/snapshotting/snapshot-support.md 中,/snapshot/load请求对 Uffd 后端给出了完整字段语义:
backend_type: "Uffd"表示用专用用户态进程处理 Guest 内存范围的缺页;- 此时
backend_path不再指向内存文件,而是指向Firecracker 与用户态页故障处理进程之间通信用 Unix domain socket 的路径; huge_pages字段用于选择恢复后微虚拟机的主机页配置,取值Snapshot(默认,复用快照中记录的值)、None(使用宿主默认内存映射行为)、Transparent与2M;其中显式指定2Mhugetlbfs 页必须搭配Uffd后端,若与File组合会直接报错;采用Uffd时透明大页(THP)的实际效果可能受限。
一个典型的使用 Uffd 后端的加载请求如下:
curl --unix-socket /tmp/firecracker.socket -i \ -X PUT 'http://localhost/snapshot/load' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "snapshot_path": "./snapshot_file", "mem_backend": { "backend_path": "/path/to/uffd.sock", "backend_type": "Uffd" }, "track_dirty_pages": true, "resume_vm": false }'注意事项:
LoadSnapshot只允许在微虚拟机启动(boot)之前发起,且在加载前仅可先配置 logger 与 metrics 系统;- 加载成功后微虚拟机处于
Paused状态,需再执行PATCH /vm将其Resumed(或直接令resume_vm: true); - 旧字段
mem_file_path已进入废弃流程,且与mem_backend互斥,两者同时出现会返回错误; - 恢复成功后,充当只读数据源的内存文件(
File后端场景)必须视为不可变——宿主机侧对该文件的任何外部修改都会破坏 Guest 内存并导致未定义行为; track_dirty_pages配置不会随快照保存,若希望基于 dirty page tracking 生成 diff 快照,需在加载请求中显式再次置true。
对应的集成测试位于 tests/integration_tests/functional/test_uffd.py:test_bad_socket_path验证 socket 路径不存在时报Failed to connect to UDS Unix stream;test_unbinded_socket验证 handler 未就绪时的错误路径;test_valid_handler用on_demandhandler 完成一次成功的 Uffd 恢复并 resume;test_malicious_handler则验证 handler 崩溃后的失败行为。
与 balloon 设备的协同:UFFD_EVENT_REMOVE 处理
Firecracker 的 balloon 设备允许宿主从微虚拟机回收内存(详见 docs/ballooning.md)。当 balloon 设备请求移除某段内存范围时,Firecracker 会以MADV_DONTNEED标志调用madvise,让内核知道可以释放该区域的物理内存;在 userfaultfd 的语义下,这个系统调用会触发向用户态发送UFFD_EVENT_REMOVE事件。
这意味着实现页故障处理进程时,用户必须识别UFFD_EVENT_REMOVE事件,并对被移除的内存范围执行清零处理:内存虽被移除,但该区域仍处于 userfaultfd 监视之下。经过一次 balloon 膨胀(inflation)与收缩(deflation)循环之后,之前被 balloon 移除(并被 handler 清零)的内存范围仍可能再次触发缺页;此时处理进程必须把缺页页清零(zero out)而不是从文件取回内容——这正是内核 userfaultfd 文档对非协作式 userfaultfd 场景的建议。
除语义上的要求外,还有两类现实中的复杂情况(源码注释在 on_demand_handler.rs 中有详细记录):
remove事件排队期间 ioctl 全部返回 EAGAIN:只要 UFFD 队列中还挂着一个remove事件未处理,所有 UFFD ioctl 都会返回EAGAIN。因此不能再简单逐条处理事件,而必须把remove事件之前的所有事件一次性预取出来,解除 UFFD 阻塞后再回头处理。- 事件可能乱序到达:例如 Guest 内核先响应 balloon 膨胀释放内存并通知 Firecracker,Firecracker 对这段内存
madvise(MADV_DONTNEED)从而产生remove事件;紧接着 Guest 内核又(比如因 OOM 策略)把同一页触回缺页产生pagefault事件。缺页是在 vCPU 线程内由 KVM 触发,而 balloon 设备在 VMM 线程处理,所以 handler可能先收到pagefault再收到其因果前驱remove。若简单"贪心"地一次性预取全部事件,就可能先按pagefault从快照文件取页,而正确行为应当是取回零页。示例 handler 刻意忽略这一问题(并注明理由:Guest 内核反正会清零新 fault in 的页),但生产级 handler 通常需要保证同一范围内的remove事件永远先于pagefault事件被处理。
示例 handler 还针对乱序问题引入deferred_events队列:本轮处理不了的pagefault先挂起,若期间收到remove则调用unregister_range注销该范围(对应uffd_utils.rs中基于Uffd::unregister的实现),下一轮循环再回头处理积压事件,直到没有滞留事件为止。
另外,如果 balloon 驱动被攻陷,处理进程可能被海量UFFD_EVENT_REMOVE淹没。Firecracker 文档建议启用 jailer 内置的cgroup 功能作为纵深防御,以限制 Firecracker 进程的资源占用,防止恶意事件洪泛拖垮宿主资源。
必须知道的 Caveats:崩溃、信号与超时
把缺页服务外包给独立进程,等于把可用性责任也一并外包了,Firecracker 文档明确列出三类风险:
- handler 崩溃会导致 Firecracker 永久挂起。若 handler 进程在 Firecracker 恢复快照期间崩溃,那么一旦发生缺页,Firecracker 会永远等待该页被提供——因为 Firecracker 被设计为"等待所请求的页变为可用",且协议上握手后双方再无通信,它无法感知 handler 已消失。用户应当自行监视页故障处理进程的状态、采集 Firecracker 进程挂起指标,并在必要时实现进程回收(recycle)机制。
- 错误处理与退出通知是 handler 的责任。handler 在自身崩溃/退出时需要向 Firecracker 进程发送信号告知异常。获取 Firecracker PID 的推荐方式是对已连接的 socket 执行
getsockopt(SO_PEERCRED)——返回的对端凭据包含PID、GID 与 UID。示例 handler 正是这样做的:uffd_utils.rs中的peer_process_credentials用SO_PEERCRED拿对端凭据,install_panic_hook安装 panic hook,一旦 handler panic 就向对端 Firecracker 进程发送SIGKILL,避免 Firecracker 因无人服务缺页而永久挂死。 - 通信方可能中途消失,handler 应自带超时。建议 handler 在"等待 Firecracker 连接 UDS"和"等待 Firecracker 发送信息"两处都设置超时,以应对 Firecracker 在连接/发送数据之前就崩溃的意外场景。
官方示例 handler 解剖:一个最小可运行的参考实现
仓库在src/firecracker/examples/uffd下提供了多个可直接对照实现的小型 handler(相关二进制在 firecracker/Cargo.toml 中以uffd_on_demand_handler、uffd_fault_all_handler、uffd_malicious_handler三个 bin 目标注册),它们共用uffd_utils.rs骨架:
- Runtime:接收 socket 连接后,以
PROT_READ | MAP_PRIVATE | MAP_POPULATE方式把内存文件整体 mmap 进自己的地址空间作为数据源;随后用poll()在一个循环里同时监听 UDS(等待新 UFFD fd)与各个 userfault fd(等待缺页事件)。代码注释说明:一旦收到新的 UFFD(socket 可读),就新建一个UffdHandler加入轮询集合,因此该骨架天然支持多个 Firecracker/多区域注册。 - UffdHandler:从 socket 上接收
GuestRegionUffdMappingJSON 清单与 UFFD fd,把 fd 包装为Uffd;对每个缺页事件先按页对齐地址,再在mem_regions中二分定位所属区域,最终调用Uffd::copy(即UFFDIO_COPY)把后端缓冲区中对应偏移的内容拷入目标页——这是各示例 handler 的公共"服务缺页"原语。 - 三种策略:
on_demand_handler.rs:文档重点推荐的按需示例,收到某个地址的缺页时把该地址所属的整个内存区域载入内存,同时正确处理remove/pagefault乱序与 EAGAIN;fault_all_handler.rs:收到首个缺页后一次性把全部内存区域 fault in,并打印耗时(供性能对比);malicious_handler.rs:一收到缺页就 panic,刻意扮演"恶意/故障" handler,用于测试崩溃路径。
注意:按文档说法,on_demand_handler"在某地址发生缺页时把该地址所属整个区域载入内存"只是一种策略示范——用户完全可以按自己的用例实现任意其它行为,例如逐页按需加载、预取(prefetch)、页压缩、RDMA 直取等。仓库中另有一个仅用于 CI 集成的fault_all_handler(把恢复延迟换算为"整机 fault in 全内存"),以及演示最坏行为(handler 主动杀死 Firecracker)的malicious_handler,配合 tests/integration_tests/functional/test_uffd.py 可在集成测试套件中验证正反向路径。
小结:如何选择与如何落地
- 什么时候该用
File后端:追求最小部署复杂度、能接受逐页陷入内核的缺页开销,且不需要2Mhugetlbfs 显式大页(File+2M是非法组合)。File是加载快照时"由宿主内核代为管理 Guest 内存换入"的传统路径。 - 什么时候该用
Uffd后端:希望自定义缺页策略(如整区预取、全量预载、异步流水线),或希望把"从文件搬数据"的 IO 负载从 Firecracker 关键路径中剥离,或需要显式2Mhugetlbfs 页面;同时接受新增一个必须自研、自运维、自监控的独立进程的复杂度。 - 工程落地 Checklist:
- 确认宿主内核版本对应的 UFFD 创建方式(5.10 用 syscall、6.1 走
/dev/userfaultfd);非 jailer 场景手动授权/dev/userfaultfd; - 把 handler、UDS、内存文件全部放进 jail 内,并收紧 UDS 访问权限;
- 在
/snapshot/load里用mem_backend.backend_type = "Uffd"、backend_path指向 UDS,不要与mem_file_path混用; - handler 必须处理
UFFD_EVENT_REMOVE(清零语义),并考虑remove/pagefault乱序与 ioctl EAGAIN 阻塞; - 为 UDS 的"连接"与"收包"两阶段设置超时;用
SO_PEERCRED感知对端 Firecracker 并实现崩溃信令与回收机制; - 借助 jailer 的 cgroup 能力限制资源使用,作为被攻陷 balloon 驱动等场景的纵深防御。
- 确认宿主内核版本对应的 UFFD 创建方式(5.10 用 syscall、6.1 走
如需更宏观的快照能力总览(暂停、创建、恢复、diff 合并、快照安全与版本管理),可继续阅读 docs/snapshotting/snapshot-support.md 与其在 docs/snapshotting 下的姊妹文档(如 docs/snapshotting/versioning.md、docs/snapshotting/network-for-clones.md、docs/snapshotting/random-for-clones.md)。
【免费下载链接】firecrackerSecure and fast microVMs for serverless computing.项目地址: https://gitcode.com/GitHub_Trending/fi/firecracker
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考