深度解析:基于 kexec 的在线内核升级与资源保留框架)
Linux 内核 Live Update OrchestratorLUO深度解析基于 kexec 的在线内核升级与资源保留框架【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux导读Live Update OrchestratorLUO是 Linux 内核中一套基于 kexec 的在线升级机制它允许运行中的内核从某一版本无缝切换到新版本同时保留选定的资源状态如文件描述符、共享全局对象并让指定的硬件设备在切换期间持续工作。本文以 Documentation/core-api/liveupdate.rst 为主线结合 kernel/liveupdate/ 下的源码实现系统讲解 LUO 的会话Session、文件描述符保留File Preservation、FLB 全局数据、ABI 规范与用户态 ioctl 接口并给出 memfd 与 systemd 的真实集成案例。读完本文你将掌握 LUO 的完整工作流程、内核侧回调 API 的设计动机以及如何让一个服务在内核热升级后无缝接回自己的资源。LUO 概览用 kexec 完成整机内核升级LUOLive Update Orchestrator本质上是一种专门化的、基于 kexec 的重启过程它允许内核从一个版本更新到另一个版本同时保留选定资源的状态并让指定的硬件设备在内核切换期间保持可用。对于这些设备DMA 活动可以跨越内核切换持续进行——这是它区别于普通 kexec 重启的核心能力。核心定义位于 kernel/liveupdate/luo_core.c 顶部的DOC:注释块中。驱动 LUO 设计的首要场景是云环境中的虚拟机监控hypervisor希望在 VM 不中断、设备不复位的前提下快速升级宿主内核。但 LUO 框架本身与负载无关任何系统都可以使用。例如一个运行 memcached 等内存缓存缓存数据达数 GB的非虚拟化系统用户态服务把缓存放进 memfd由 LUO 保留其状态kexec 之后立即恢复。无论是虚拟机、容器、高性能数据库还是网络服务LUO 的核心目标都是通过保留关键用户态状态、让关键设备持续运行来完成一次完整的内核升级。LUO 的核心机制包含两部分进度跟踪机制跟踪一次 live update 从创建会话到新内核接管的完整进度回调 API允许 kvm、iommu、中断、vfio、参与的文件系统、内存管理等内核子系统挂接进 live update 流程。在底层LUO 使用Kexec HandoverKHO将内存状态从当前内核传递给下一个内核相关实现见 kernel/liveupdate/kexec_handover.c 与 kernel/liveupdate/kho_block.c。如何启用 LUO启用 LUO 需要内核配置与启动参数两级开关内核配置在 kernel/liveupdate/Kconfig 中CONFIG_LIVEUPDATE依赖KEXEC_HANDOVER而KEXEC_HANDOVER又依赖ARCH_SUPPORTS_KEXEC_HANDOVER与ARCH_SUPPORTS_KEXEC_FILE。CONFIG_LIVEUPDATE_MEMFD则额外提供 memfd 保留能力。启动参数启用 LUO 需要在启动时向内核传递liveupdateon命令行参数。该参数由early_param(liveupdate, ...)解析见 kernel/liveupdate/luo_core.c将全局luo_global.enabled置为 true。liveupdate_enabled()在代码中随处可见是各子系统判断 LUO 是否生效的统一入口。从 kernel/liveupdate/luo_core.c 可以看到新内核在早期启动时会通过kho_retrieve_subtree(LUO, ...)从 KHO 中取回上一代内核保存的 LUO 状态校验 ABI 兼容字符串luo-v5然后依次设置 incoming 的会话与 FLB 数据而 outgoing 状态的初始化则放在late_initcall阶段因为只有用户态开始使用/dev/liveupdate时才需要它。LUO 会话Sessions命名容器与生命周期会话是 LUO 对struct file *实例进行分组管理的基础机制。每个会话是一个由用户命名的容器用户态代理agent通过它管理对工作负载至关重要的资源生命周期。会话的核心概念定义在 kernel/liveupdate/luo_session.c 的DOC:注释中要点如下命名容器会话由用户提供的唯一名字标识这个名字既用于当前内核的创建也用于下一个内核中的找回名字最大长度由LIVEUPDATE_SESSION_NAME_LENGTH定义为 64 字节含结尾的\0见 include/uapi/linux/liveupdate.h。用户态接口会话管理完全由用户态通过/dev/liveupdate上的 ioctl 驱动。序列化会话元数据借助 KHO 框架保留。当通过 kexec 触发 live update 时会话元数据被序列化为一串链接块linked-blocks放入保留内存区第一个块头的物理地址被存进集中的struct luo_ser结构。会话生命周期四步曲根据源码注释与实现kernel/liveupdate/luo_session.c会话的完整生命周期如下创建Creation用户态代理调用luo_session_create()创建一个新的空会话并获得一个会话文件描述符。对应 ioctl 为LIVEUPDATE_IOCTL_CREATE_SESSION。创建时会校验名字长度空名或超过 63 字节返回-EINVAL并检查重名返回-EEXIST。序列化Serialization当调用reboot(LINUX_REBOOT_CMD_KEXEC)系统调用时luo_session_serialize()被触发遍历所有活动会话并把元数据写入 KHO 保留的内存区域。每个会话的元数据项struct luo_session_ser包含名字和该会话全部文件的序列化信息struct luo_file_set_ser。反序列化Deserializationkexec 之后新内核中当/dev/liveupdate第一次被打开时luo_session_deserialize()运行见 kernel/liveupdate/luo_core.c 的luo_open读取序列化数据并重建struct luo_session对象列表。注意如果反序列化失败luo_open一律向用户返回-EIO且失败后的资源按泄漏处理——源码注释明确说明预期恢复路径是让用户态检测到失败后触发一次重启以可靠复位设备并回收内存。找回Retrieval新内核中的用户态代理用会话名调用luo_session_retrieve()获得新文件描述符以访问被保留的状态。对应的 ioctl 为LIVEUPDATE_IOCTL_RETRIEVE_SESSION如果找不到同名会话返回-ENOENT。一个 incoming 会话只能被 retrieve 一次重复 retrieve 返回-EINVAL。三层锁层次串行化与并发安全会话子系统采用三层锁层级来保证并发安全、避免死锁详见 kernel/liveupdate/luo_session.cluo_session_serialize_rwsem全局读写锁保护会话的变更创建、找回、释放、ioctl与 reboot 期间的序列化过程互斥。读者由所有访问/修改会话状态的路径持有如luo_session_create()、luo_session_retrieve()、luo_session_release()、luo_session_ioctl()写者由序列化过程luo_session_serialize()在 reboot 时持有——成功后写锁被无限期持有以冻结子系统失败则释放以允许恢复。luo_session_header-rwsem每列表读写锁同步 incoming/outgoing 两个会话链表层面的操作。写者在链表增删时持有读者在遍历如按名查找时持有。luo_session-mutex每会话互斥锁保护单个会话的内部状态与文件集合在逐会话操作preserve、retrieve、freeze 文件期间获取。三层锁的获取顺序严格固定luo_session_serialize_rwsem→luo_session_header-rwsem→luo_session-mutex这一层级关系保证了并发下不会出现锁序反转。文件描述符保留回调驱动的生命周期管理LUO 提供基础设施让特定的、有状态的stateful文件描述符跨越 kexec 式 live update 被保留下来。典型工作负载包括使用 vfio、memfd 或 iommufd 的虚拟机——它们需要不间断地访问自己的核心资源。这套机制建立在回调式 handler 模型和每个被保留文件的明确定义生命周期之上见 kernel/liveupdate/luo_file.c 的DOC:注释。Handler 注册与回调集合负责某一类文件的内核模块如 memfd、vfio会注册一个struct liveupdate_file_handler完整定义见 include/linux/liveupdate.h其核心是struct liveupdate_file_ops回调集合回调必需性作用can_preserve()必需轻量检查判断该 handler 是否兼容给定的struct filepreserve()必需重量级操作保存文件状态并返回一个不透明的 u64 句柄。通常在负载仍活跃时执行以把实际切换期间的停机时间压到最低unpreserve()必需清理preserve()分配的资源在 reboot 前中止会话关闭时被调用freeze()可选reboot 前的最后准备机会。此时已进入 reboot 系统调用用户态不能再改动文件unfreeze()可选撤销freeze()的动作用于 freeze 阶段之后 live update 被中止的场景retrieve()必需在新内核中根据保留的句柄重建struct filecan_finish()可选检查该 FD 是否满足完成finish的全部前置条件finish()必需在新内核中做最终检查与清理。成功 finish 后LUO 交出对该文件的拥有权get_id()可选返回文件的唯一标识符默认回退为(unsigned long)file此外每个 handler 携带一个compatible 字符串如memfd-v1、vfiofd-v1长度上限LIVEUPDATE_HNDL_COMPAT_LENGTH即 48 字节唯一标识其支持的文件类型并在保留/恢复时与具体struct file实例关联的 compatible 字符串匹配。注册入口为liveupdate_register_file_handler()kernel/liveupdate/luo_file.c它会校验必需回调是否齐全缺一返回-EINVAL、compatible 字符串是否重复重复返回-EEXISTLUO 未启用时返回-EOPNOTSUPP。handler 之间通过全局luo_file_handler_list链表组织并用luo_register_rwlock读写锁保护。文件保留生命周期正常路径Happy PathPreserve正常运行期用户态代理通过 ioctl 逐个保留文件。对每个文件luo_preserve_file()会校验 token 未被占用-EEXIST→ 确保序列化内存已分配 → 遍历已注册 handler 用can_preserve()寻找兼容者找不到返回-ENOENT→ 调用preserve()保存状态 → 创建内部struct luo_file跟踪活动状态kernel/liveupdate/luo_file.c。成功时 LUO 通过fget()成为文件的共同拥有者这份引用会一直持有到文件被 unpreserve 或在新内核中 finish防止文件被提前销毁。此函数其他可能的错误码还包括-EBUSYFD 已被其他会话保留、-EBADFFD 无效、-ENOSPCfile_set 已满、-ENOMEM内存分配失败。Freezereboot 前kexec 前一刻调用luo_file_freeze()kernel/liveupdate/luo_file.c。它按 FIFO保留顺序遍历所有被保留文件先调用各 handler 的freeze()让设备静默、准备状态成功后把最终元数据——handler 的 compatible 字符串、用户 token、最终数据句柄——拷贝进为 KHO 预分配的连续内存块。Deserialize新内核kexec 后当会话被反序列化即/dev/liveupdate首次被打开时luo_file_deserialize()读取 KHO 内存区中的序列化数据按 compatible 字符串找到对应 handler重建struct luo_file内存链表此时file指针为 NULL尚未找回。Retrieve新内核用户态就绪用户态代理通过 token 找回文件描述符。luo_retrieve_file()kernel/liveupdate/luo_file.c按 token 匹配调用 handler 的retrieve()重建struct file并返回新 FD。文件可以按任意顺序找回不要求与保留顺序一致。该操作具备幂等性已成功找回的文件再次被请求时直接返回同一文件的引用若某次 retrieve 失败错误码会被记入retrieve_status并在此后重复返回失败不会被重试。Finish新内核清理会话找回完成后调用luo_file_finish()kernel/liveupdate/luo_file.c。它先对每个文件调用can_finish()检查前置条件任一失败则整体中止并返回-EBUSY随后逐个执行finish()清理、通过fput()释放 LUO 的拥有引用、回收内部结构内存。文件保留生命周期异常路径Unhappy PathsReboot 前中止如果用户态代理在调用 reboot 之前中止了 live update例如关闭会话文件描述符会话的 release 处理会调用luo_file_unpreserve_files()kernel/liveupdate/luo_file.c对所有被保留文件调用unpreserve()释放所有已分配资源让系统回到干净状态。Freeze 失败在reboot()系统调用期间如果某个 handler 的freeze()失败__luo_file_unfreeze()会对所有此前成功freeze 的文件调用unfreeze()回滚状态然后reboot()向用户态返回错误取消本次 live update。Finish 失败在新内核中如果某个 handler 的finish()失败luo_file_finish()中止LUO 继续持有该会话内的全部文件包括尚未处理的。用户态代理可以稍后重试 finish若问题无法解决这些资源会一直被 LUO 持有直到下一次 live update 周期被丢弃。FLB文件生命周期绑定的全局数据File-Lifecycle-Bound Global Data许多文件类型依赖跨文件共享的全局状态。以 VFIO 为例所有 VFIO fd 都需要 IOMMU 核心状态但该状态不应随第一个文件被保留就重复保存、也不应随每个文件结束就被反复清理。FLB 机制正是为此设计见 kernel/liveupdate/luo_flb.c 的DOC:注释生命周期与文件绑定FLB 状态的保留由第一个依赖它的文件被 preserve 时触发清理unpreserve 或 finish由最后一个依赖它的文件被 unpreserve/finish 时触发。这种引用计数方式确保共享状态恰好保存一次、恰好恢复一次且生命周期正确跨越 kexec 切换。依赖声明文件 handler 通过liveupdate_register_flb()声明对若干 FLB 的依赖kernel/liveupdate/luo_flb.c。注册时校验回调齐全、检查与当前 handler 是否重复-EEXIST、全局唯一性以及全局 FLB 数量上限LUO_FLB_MAX超出返回-ENOSPC。回调模型每个 FLB 由struct liveupdate_flb_ops定义include/linux/liveupdate.hLUO 在关键节点调用.preserve()首个依赖文件被保留时调用保存全局状态通过argp-data返回单一自包含的 u64 句柄、argp-obj返回活动对象.unpreserve()最后一个依赖文件被 unpreservereboot 前中止时调用.retrieve()新内核中按需调用——第一次有组件请求访问该共享对象时才执行接收保留句柄并重建活动对象.finish()新内核中最后一个依赖文件 finish 时调用做最终清理。FLB 的引用计数保存在struct luo_flb_private_state含refcount_t count、数据句柄、活动对象、互斥锁、finished/retrieve_status 标志中并区分outgoingreboot 前 preserve/unpreserve 生命周期与incomingreboot 后 retrieve/finish 生命周期两套状态include/linux/liveupdate.h。新内核组件通过liveupdate_flb_get_incoming()获取共享对象——首次调用会触发retrieve()重建后续调用返回缓存对象liveupdate_flb_put_incoming()则在引用归零时触发finish()。LUO ABI新旧内核之间的稳定契约LUO 使用Kexec HandOverKHO框架定义了一套稳定的应用二进制接口ABI把状态从升级前的内核传递给升级后的内核定义见 include/linux/kho/abi/luo.h。这套接口是一份契约任何对结构体字段、compatible 字符串或__packed序列化结构布局的修改都构成破坏性变更必须递增相关_COMPATIBLE字符串中的版本号防止新内核误解旧内核的数据。只有支持相同 ABI 版本的内核之间才保证向后/向前兼容。整体上全部 LUO 状态被封装在单个 KHO 条目中条目名为LUOLUO_KHO_ENTRY_NAME该条目包含根结构struct luo_ser其当前兼容字符串为luo-v5LUO_ABI_COMPATIBLE。序列化结构体系如下struct luo_ser中央 ABI 结构含兼容字符串、liveupdate_num成功 live update 次数计数器、sessions_pa首个会话块头的物理地址、flbs_paFLB 头的物理地址。它是一切被保留 LUO 状态的根。struct luo_session_ser单个会话的元数据含会话名LIVEUPDATE_SESSION_NAME_LENGTH字节和指向该会话全部文件的第一个struct kho_block_header_ser的物理指针多个块通过 header 中的next字段链接。struct luo_file_ser单个被保留文件的元数据含用于在新内核查找 handler 的compatible字符串、用户提供的token标识、以及供 handler 使用的不透明data句柄。struct luo_flb_header_serFLB 数组的头部含保留内存块的总页数pgcnt和紧随其后的struct luo_flb_ser条目数count。struct luo_flb_ser单个被保留全局对象的元数据含name兼容字符串、不透明data句柄、依赖它的文件数count。这些结构在序列化时通过virt_to_phys()/phys_to_virt()在保留内存与虚拟地址间转换整个生命周期由luo_core.c中的luo_state_setup()outgoing 侧与luo_early_startup()incoming 侧管理kernel/liveupdate/luo_core.c。用户态接口/dev/liveupdate 与 ioctlLUO 向用户态暴露一个字符设备通常位于/dev/liveupdate由 misc device 框架注册设备名liveupdate见 kernel/liveupdate/luo_core.c。该设备允许用户态代理管理 LUO 状态机及其关联资源如可保留的文件描述符。访问是排他的为保证状态机由单一实体控制任意时刻只允许一个进程打开/dev/liveupdate——后续 open 会返回-EBUSY直到首个进程关闭其文件描述符luo_open中用atomic_cmpxchg实现。另外若新内核反序列化失败luo_open一律返回-EIO。ioctl 通用格式与错误码语义所有 ioctl 遵循统一的、可扩展的通用格式定义见 include/uapi/linux/liveupdate.h 的DOC:注释每个 ioctl 都以结构体指针为参数结构体的第一个 u32 存放结构大小内核会检查结构体超出其理解范围的部分是否全为 0从而允许用户态在沿用向后兼容部分的同时稳定地使用更新的、更大的结构体。ioctl 号类型为0xBALIVEUPDATE_IOCTL_TYPE。通用错误码语义ENOTTYioctl 号完全不支持E2BIGioctl 号受支持但提供的结构体中内核不理解的区域非零EOPNOTSUPPioctl 号与结构体均可识别但某个已知字段含内核不支持的值EINVALioctl 一切可识别但某字段不正确ENOENT提供的 token 不存在ENOMEM内存不足EOVERFLOW数学运算溢出。设备级 ioctl/dev/liveupdate命令编号功能LIVEUPDATE_IOCTL_CREATE_SESSION0x00创建新 live update 会话输出fd带O_CLOEXEC与输入name≤63 字节LIVEUPDATE_IOCTL_RETRIEVE_SESSION0x01按名字找回保留的会话输出新fd仅在新内核中可用找不到返回-ENOENT会话级 ioctl会话 FD命令编号适用方向功能LIVEUPDATE_SESSION_PRESERVE_FD0x40outgoing输入用户fd与不透明唯一token校验并启动对该 FD 的保留LIVEUPDATE_SESSION_RETRIEVE_FD0x41incoming输入保留时的token输出新fd引用恢复后的资源LIVEUPDATE_SESSION_FINISH0x42incoming标记会话恢复完成内核释放对保留资源的拥有权reserved必须为 0LIVEUPDATE_SESSION_GET_NAME0x43双向输出会话完整名字因/proc中的名字可能被截断用户态典型流程是升级前打开/dev/liveupdate→CREATE_SESSION创建会话 → 逐个SESSION_PRESERVE_FD把 FD 与 token 登记进会话 →reboot(LINUX_REBOOT_CMD_KEXEC)触发升级新内核启动后打开/dev/liveupdate→RETRIEVE_SESSION按名字找回会话 →SESSION_RETRIEVE_FD按 token 恢复每个 FD → 全部就绪后SESSION_FINISH完成交接。若某个finish失败资源仍保留在内存中用户态可再次调用 finish否则资源会在下一次 live update 周期被重置。实战案例一memfd 内存保留memfd 是 LUO 文件保留机制的首个落地实现mm/memfd_luo.c。用户态可以把自己的内存内容放进 memfd交给 LUO 保留kexec 后原样取回——这正是前面 memcached 场景的技术底座。需要明确的是这种保留不是透明的只有部分文件属性会被保留其余全部重置为默认值。当前版本保留的属性包括文件内容全部数据文件大小文件的空洞会在保留过程中分配页面填充文件位置保留当前读写位置应用可无缝继续读写文件状态标志memfd 总是以O_RDWRO_LARGEFILE打开此属性保持不变文件封印seals保留并在恢复时重新应用仅允许本 LUO 版本已知的 sealsMEMFD_LUO_ALL_SEALS否则保留失败并返回-EOPNOTSUPP。不保留的属性必须假定被重置为默认FD_CLOEXEC标志创建时用MFD_CLOEXEC设置的标志不会保留恢复后需通过fcntl()重新设置。另外目前不支持 Hugetlb 后备的 memfd以MFD_HUGETLB创建的 memfd 会被拒绝。从实现上看memfd 的 handlercompatible 为memfd_luo_handler展示了 LUO 回调的完整用法mm/memfd_luo.ccan_preserve()要求是 shmem 文件且i_nlink 0匿名文件preserve()加 inode 锁并shmem_freeze(inode, true)用memfd_pin_folios()固定全部 folio将脏页/未 up-to-date 页统一处理未使用过的空洞页会被清零并标记再通过kho_preserve_folio()把每个 folio 交给 KHO 保留最终把序列化结构的物理地址放进serialized_datafreeze()由于f_pos在 preserve 后可能变化此时重新快照文件位置retrieve()用memfd_alloc_file()重建文件、重新应用 seals、恢复位置与大小再经kho_restore_folio()把 folio 重新挂回 page cachefinish()若 retrieve 未被调用则负责丢弃那些从未被取回的保留 folio。这一实现同时说明了 LUO 回调分阶段设计的意义preserve()在负载活跃时完成最重的数据快照停机时间不计入freeze()只做轻量收尾从而把 kexec 切换窗口压缩到最短。实战案例二systemd 文件描述符存储的在线保留用户态层面systemdv261 起已使用 LUO 在 kexec 式 live update 期间保留其按服务划分的文件描述符存储per-service file descriptor store详见 Documentation/userspace-api/liveupdate.rst服务通过在 unit 中设置FileDescriptorStoreMax和FileDescriptorStorePreserve选择加入通过sd_pid_notify_with_fds(..., FDSTORE1\nFDNAMEfoo)把带名字的 FD 推进存储服务也可以自行通过/dev/liveupdate创建 LUO 会话并把生成的会话 FD 像普通 FD 一样推进自己的文件描述符存储。systemd 能识别这类会话 FD 并妥善处理kexec 后通过既有的 FD 存储服务接口把重新找回的会话 FD 交还给服务。该文档同时给出一个重要提示Live Update 功能仍在开发中、可能发生变化因此在不同内核版本之间 kexec 重启时并不保证兼容这一状况预期在未来版本中改善并稳定。与此对应LUO 的 ABI 契约compatible 字符串版本化机制正是为这一稳定性目标服务的。源码阅读路线图如果想深入理解 LUO建议按以下顺序阅读文档入口Documentation/core-api/liveupdate.rst本文主体、Documentation/userspace-api/liveupdate.rst用户态接口、Documentation/mm/memfd_preservation.rstmemfd 保留核心实现kernel/liveupdate/luo_core.c调度与 ioctl 设备、kernel/liveupdate/luo_session.c会话、kernel/liveupdate/luo_file.c文件保留、kernel/liveupdate/luo_flb.cFLB 全局数据接口定义include/linux/liveupdate.h内核公共 API、include/linux/kho/abi/luo.hABI 结构、include/uapi/linux/liveupdate.h用户态结构配置与依赖kernel/liveupdate/KconfigCONFIG_LIVEUPDATE及其对KEXEC_HANDOVER的依赖、kernel/liveupdate/kexec_handover.cKHO 底层首个落地实现mm/memfd_luo.cmemfd handler 的完整回调示例。综合来看LUO 通过会话分组 回调驱动 ABI 版本化 KHO 内存移交四层设计把在线更换内核这一复杂命题拆解为清晰的状态机用户态用 ioctl 管理会话与 FD内核子系统用回调参与各阶段新旧内核用版本化的序列化结构交接状态。它是理解 Linux 内核热升级、设备热迁移以及 kexec 生态的重要入口。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考