资讯动态

Firecracker 快照恢复缺页处理机制详解:内核缺页加载与 Userfaultfd 用户态页故障服务

发布时间:2026/9/9 14:01:35 来源:尧图企业网站定制
Firecracker 快照恢复缺页处理机制详解内核缺页加载与 Userfaultfd 用户态页故障服务【免费下载链接】firecrackerSecure and fast microVMs for serverless computing.项目地址: https://gitcode.com/GitHub_Trending/fi/firecrackerFirecracker 在从快照恢复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.10UFFD 对象通过直接调用userfaultfd系统调用来创建。宿主内核 6.1UFFD 通过/dev/userfaultfd字符设备创建对该设备的访问受文件系统权限管控因此 Firecracker 进程需要具备相应权限。从 jailer 源码 可以看到jailer 会解析主机上该设备的主次设备号并尝试在 jail 内准备该节点只要宿主机存在/dev/userfaultfdjailer 就会把它放入 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把序列化后的VecGuestRegionUffdMapping与 UFFD 文件描述符经由 Unix socket 发送出去——底层使用 SCM_RIGHTSsendmsg附带 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.pytest_bad_socket_path验证 socket 路径不存在时报Failed to connect to UDS Unix streamtest_unbinded_socket验证 handler 未就绪时的错误路径test_valid_handler用on_demandhandler 完成一次成功的 Uffd 恢复并 resumetest_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 膨胀释放内存并通知 FirecrackerFirecracker 对这段内存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等待缺页事件。代码注释说明一旦收到新的 UFFDsocket 可读就新建一个UffdHandler加入轮询集合因此该骨架天然支持多个 Firecracker/多区域注册。UffdHandler从 socket 上接收GuestRegionUffdMappingJSON 清单与 UFFD fd把 fd 包装为Uffd对每个缺页事件先按页对齐地址再在mem_regions中二分定位所属区域最终调用Uffd::copy即UFFDIO_COPY把后端缓冲区中对应偏移的内容拷入目标页——这是各示例 handler 的公共服务缺页原语。三种策略on_demand_handler.rs文档重点推荐的按需示例收到某个地址的缺页时把该地址所属的整个内存区域载入内存同时正确处理remove/pagefault乱序与 EAGAINfault_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 显式大页File2M是非法组合。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 驱动等场景的纵深防御。如需更宏观的快照能力总览暂停、创建、恢复、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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价