NixOS 系统切换内幕:深入解析 `nixos-rebuild switch` 与 `switch-to-configuration` 执行流程

发布时间:2026/9/21 16:07:39
NixOS 系统切换内幕:深入解析 `nixos-rebuild switch` 与 `switch-to-configuration` 执行流程
NixOS 系统切换内幕深入解析nixos-rebuild switch与switch-to-configuration执行流程【免费下载链接】nixpkgsNix Packages collection NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgsnixos-rebuild switch是 NixOS 下最常用的操作之一但它在底层究竟做了什么本指南以官方手册章节 what-happens-during-a-system-switch.chapter.md 为骨架结合当前仓库中 switch-to-configuration-ng 的 Rust 源码 与相关 NixOS 模块逐层拆解一次系统切换从命令发起到服务重启完毕的完整过程。读完你将掌握switch、test、boot、dry-activate四种动作的差异、系统差异计算的两大数据源、动作执行的固定顺序以及面向模块开发者的单元行为微调手段和排障环境变量。从nixos-rebuild到switch-to-configuration一次切换的入口与许多部署方案一样nixos-rebuild最终会调用位于新系统 store 路径下的$out/bin/switch-to-configuration。这个脚本在当代实现中是一个 Rust 二进制以要执行的动作作为第一个参数被调用支持的动作包括switch应用新配置并让系统下次重启时也默认引导它test立即应用新配置但不修改引导项重启后失效boot只更新引导项让新配置在下次重启时生效本次不应用dry-activate不真正执行任何动作而是模拟打印以test方式切换时会发生什么。其中dry-activate是一个非常有用的预演手段它可以用来检查如果切换配置哪些服务状态会被改变适合在改动关键单元前做安全评估。需要特别指出的是旧版 Perl 实现的switch-to-configuration已被新的 Rust 实现switch-to-configuration-ng取代。在 switchable-system.nix 中system.switch.enableNg选项已被标记为移除mkRemovedOptionModule并说明新的 switch-to-configuration-ng 现在是唯一的 switch-to-configuration 实现。构建系统配置时会把该二进制软链接到系统的$out/bin/switch-to-configuration见 switchable-system.nix 第 54 行并通过wrapProgram注入OUT、TOPLEVEL、DISTRO_ID、INSTALL_BOOTLOADER、PRE_SWITCH_CHECK、SYSTEMD等运行所需的环境变量。在 main.rs 的入口 中可以看到程序解析动作参数后若以 root 身份运行则进入do_system_switch否则直接报错退出——该程序必须以 root 用户运行。它还会检查系统是否为 NixOS存在/etc/NIXOS文件或/etc/os-release的ID与DISTRO_ID匹配否则输出 This is not a NixOS installation! 并退出。动作预处理引导器更新与 store 同步如果动作是switch或boot程序会先更新引导器确保该配置是下次开机默认引导的配置。对应源码位于 main.rs 第 1843-1846 行调用的正是INSTALL_BOOTLOADER环境变量指向的程序。该变量由 activation-script.nix 中的system.build.installBootLoader提供——它是一个把引导器安装脚本写入第一个命令行参数指定路径的程序且同一时刻只允许启用一个引导器例如 GRUB 与 systemd-boot 不能同时启用。随后除非设置了NIXOS_NO_SYNC1/nix/store会被同步syncfs到磁盘参见 main.rs 第 1848-1857 行。注释解释了原因以防新配置导致系统挂死先确保 store 内容已落盘。如果动作是boot完成上述两步后程序就直接退出main.rs 第 1859-1861 行不进行任何实际切换。还有一个容易被忽略的检查程序会比较/run/current-system/init-interface-version与新配置的init-interface-version。若两者不一致说明新旧配置的 init 接口不兼容会打印警告并以退出码 100 结束提示新配置在重启前不会生效main.rs 第 1863-1879 行。计算差异两大数据源当动作是switch或test时程序会检查当前运行的系统并计算出切换到新系统所需的动作清单。这个过程以两个数据源为依据/etc/fstab和当前 systemd 状态。数据源一/etc/fstab中的挂载与交换分区程序分别解析当前/etc/fstab与新配置中的toplevel/etc/fstabmain.rs 第 1958-1963 行逐项对比生成动作。源码注释强调新文件系统会自动通过启动local-fs.target挂载因此这里只处理发生变化的部分挂载的文件系统类型或设备变化若挂载点不是/或/nix则重新挂载先卸载再挂载即restart对应的.mount单元若是/或/nix则绝不卸载——仅当挂载选项也变化时执行重挂载reload否则跳过main.rs 第 1970-1992 行仅挂载选项变化重挂载该.mount单元reload文件系统条目在新 fstab 中消失停止对应的.automount或.mount单元main.rs 第 1993-2001 行交换分区swap消失直接调用swapoff关闭对应交换设备。源码注释说明这里不能使用systemctl stop因为 systemd 存在大量别名单元会阻止 stop 真正调用swapoffmain.rs 第 2004-2022 行。数据源二systemd 当前状态与目标状态的差值程序通过 D-Bus 查询当前活跃单元集合然后逐个对比当前单元文件/etc/systemd/system下的符号链接与新配置的单元文件toplevel/etc/systemd/system计算出需要 stop / start / reload / restart 的单元见 main.rs 第 1937-1951 行的collect_unit_changes调用。对比逻辑的细节包括单元内容不相等且需要重启时走UnequalNeedsRestart分支交由handle_modified_unit决定是 stop、restart 还是仅跳过例如设置了RefuseManualStart的单元单元内容不相等但仅需重载时进入UnequalNeedsReload分支加入 reload 列表target 单元有特殊处理所有处于活跃状态的 target 会被加入启动列表以重启依赖链suspend.target、hibernate.target、hybrid-sleep.target以及设置了RefuseManualStart/X-OnlyManualStart的 target 除外main.rs 第 1247-1274 行设置了X-StopOnReconfiguration的 target 会被停止以保证依赖顺序正确main.rs 第 1276-1291 行单元文件被删除时依据X-StopOnRemoval默认 true决定是否停止该单元main.rs 第 1242-1246 行。与手册提到的有很多可以由单元控制的细微差别相呼应switch-to-configuration-ng的源码注释main.rs 第 73-87 行进一步说明激活脚本可通过写入列表文件请求额外重启/重载单元模块中的restartIfChanged false、reloadIfChanged true会被程序尊重stopIfChanged true则被忽略notSocketActivated true可让单元不被当作socket-activated处理。健壮性设计锁与清单文件在执行任何动作前程序会创建/run/nixos目录并对其中的switch-to-configuration.lock获取非阻塞独占锁flock防止并发切换main.rs 第 1804-1823 行。同时为了在切换中途被中断时能恢复程序会把需要 start / restart / reload 的单元持久化到/run/nixos/start-list、/run/nixos/restart-list、/run/nixos/reload-list三个清单文件并在每次启动时重新读取从而接着上次中断的地方继续main.rs 第 64-86 行。动作执行固定不变的顺序差异计算完成后动作按固定顺序执行。手册给出的顺序与源码中的实现一一对应停止单元systemctl stop对units_to_stop中的每个单元调用systemd.stop_unit并等待所有 stop job 完成main.rs 第 2192-2210 行运行激活脚本$out/activate这是更新/etc、创建用户账户等工作的关键步骤main.rs 第 2229-2244 行。激活脚本失败时程序以退出码 2 结束。仓库中 activation-script.nix 明确定义了system.activationScripts每次运行nixos-rebuild都会执行因此激活脚本必须幂等且快速检查激活脚本是否请求了更多需要重启的单元激活脚本可以通过/run/nixos/activation-restart-list与/run/nixos/activation-reload-list请求额外重启/重载单元main.rs 第 2246-2327 行。源码注释指出该机制已被废弃计划在 NixOS 26.11 移除并建议改用模块中的 restart/reload 触发器按需重启 systemdsystemd daemon-reexec当 pid 1 的路径/proc/1/exe或/etc/systemd/system.conf发生变化时需要重启 systemd且使用当前版本的 systemd 执行 reexec以防新版本无法与运行中的 pid 1 通信。等待 reexec 完成有 180 秒超时main.rs 第 2024-2050、2329-2351 行清除失败状态systemctl reset-failedsystemd.reset_failed()main.rs 第 2353-2356 行重载 systemdsystemctl daemon-reload让 systemd 读取新单元文件同样有 180 秒超时main.rs 第 2358-2379 行重载 systemd 用户实例systemctl --user daemon-reload程序通过 logind 枚举登录用户对每个存在/run/user/uid即用户管理器活跃的用户以对应用户的 uid/gid 重新执行自身二进制通过__NIXOS_SWITCH_TO_CONFIGURATION_PARENT_EXE环境变量进入do_user_switch从而完成用户级单元的 daemon-reloadmain.rs 第 2381-2441 行重新激活 sysinitsystemctl restart sysinit-reactivation.target见下文专题重载单元systemctl reload对 reload 列表中的单元执行 reload。特别地若某单元在 reload 前已处于非活跃状态则改为启动它main.rs 第 2464-2521 行重启单元systemctl restart对 restart 列表中的单元执行 restartmain.rs 第 2523-2549 行启动单元systemctl start启动所有活跃 target 及之前被停止的需要重新激活的单元——后者中有些可能不是 target 的依赖例如手动启动的单元因此必须显式启动main.rs 第 2551-2581 行检查变化并汇报程序等待 systemd 事件沉淀最长 90 秒随后对比切换前后的活跃单元打印新启动的单元与失败的单元失败单元还会自动附加systemctl status --no-pager --full输出并以退出码 4 结束main.rs 第 2583-2684 行。关键机制详解激活脚本$out/activate的作用激活脚本是整个切换的核心副作用执行器更新/etc下的配置、创建用户与组、生成各类缓存如桌面程序菜单缓存等。其内容由system.activationScripts拼装而成见 activation-script.nix。模块开发者需要理解激活脚本在每次切换时都会运行因此必须保证幂等且它在停止单元之后、重载/重启单元之前运行这一时序决定了激活脚本产生的文件变化如何被随后的单元动作消费。为什么需要sysinit-reactivation.target第 8 步专门重启sysinit-reactivation.target其背后有重要的设计原因。源码注释main.rs 第 2443-2448 行解释这个 target 存在的唯一目的是重启排在sysinit.target之前启动的服务。之所以不能直接对sysinit.target使用X-StopOnReconfiguration是因为所有常规服务都默认依赖sysinit.target——重启它会导致整个系统的服务全部重启。而sysinit-reactivation.target只影响排序在 sysinit 之前的服务且尊重这些服务之间的依赖顺序。仓库中systemd-tmpfiles-setup与systemd-sysusers相关单元就通过requiredBy [ sysinit-reactivation.target ]与before挂接在该 target 上见 tmpfiles.nix 第 258-265 行 与 sysusers.nix 第 142-143 行。dry-activate预演完整打印将要发生的动作dry-activate分支main.rs 第 2044-2188 行不会执行任何真正的切换而是依次输出would stop the following units: ...跳过被过滤的 target 单元would NOT stop the following changed units: ...例如设备变化但绝不能卸载的/、/nix挂载would activate the configuration...随后执行新配置中的$out/dry-activate脚本would restart systemd若需要 daemon-reexecwould reload the following units: ...would restart the following units: ...would start the following units: ...这意味着在修改关键服务定义后可以先运行nixos-rebuild dry-activate或直接执行sudo /run/current-system/bin/switch-to-configuration dry-activate来预演影响范围。这种用法也出现在 specialisation.nix 的文档示例 中sudo /run/current-system/specialisation/fewJobsManyCores/bin/switch-to-configuration test展示了如何针对特定 specialisation 直接调用切换程序。面向模块开发者的单元行为控制手册指出有很多可以由单元控制的细微差别结合switch-to-configuration-ng源码注释与实现这些控制手段包括单元属性 / 模块选项作用源码依据X-StopOnRemoval单元文件被删除时是否停止该单元默认truemain.rs 第 1244 行X-StopOnReconfigurationtarget 单元在重新配置时是否被停止用于保持依赖顺序main.rs 第 1284-1291 行RefuseManualStart/X-OnlyManualStart排除 target 被自动重启main.rs 第 1256-1267 行restartIfChanged false单元内容变化时不重启main.rs 第 73-87 行注释reloadIfChanged true单元内容变化时仅重载同上notSocketActivated true不把单元当作 socket-activated 处理同上调试与输出控制环境变量STC_DEBUG1将日志级别从默认的Info提升为Debug输出更详细的内部日志main.rs 第 1775-1779 行。日志通过 syslog 的LOG_USERfacility 记录tag 为nixosSTC_DISPLAY_ALL_UNITS1默认情况下总会自动启动的 target 单元会被过滤掉避免输出过于啰嗦设置该变量可在开发或测试时禁用此过滤显示全部单元main.rs 第 1271-1273 行NIXOS_NO_SYNC1跳过/nix/store的syncfs落盘main.rs 第 1849-1857 行NIXOS_NO_CHECK1跳过前置切换检查见下文用于强制切换main.rs 第 1829-1836 行。前置检查与切换抑制器在进入正式切换前程序会执行前置切换检查do_pre_switch_check。其内容由 pre-switch-check.nix 定义system.preSwitchChecks是一组 shell 片段任何片段失败都会导致整个切换提前失败退出。片段脚本会收到两个位置参数$1为新配置路径$2为传给switch-to-configuration的动作动词。仓库还内置了一个重要的抑制机制——切换抑制器switch inhibitors。在 switchable-system.nix 中system.switch.inhibitors接受一个字符串属性集当其中某个键的值在切换时发生变化程序会阻止直接切换到新配置并建议改用nixos-rebuild boot重启系统switchable-system.nix 第 93-143 行。其判定逻辑为对当前系统的/run/current-system/switch-inhibitors与新配置中的switch-inhibitors做 JSON 对比输出所有发生变化的键值对对boot与dry-activate动作则跳过检查。若确实需要强制切换可设置NIXOS_NO_CHECK1sudo 配置中已通过env_keepNIXOS_NO_CHECK保留了该变量。此外system.switch.enable默认true控制是否在系统中包含切换能力switchable-system.nix 第 19-32 行关闭它会使系统无法通过nixos-rebuild重新配置。这适用于镜像式设备appliance——更新在镜像之外处理关闭该能力可以让镜像更轻量、更安全。相关背景可参见手册中的 non-switchable-systems.section.md。测试与验证switch-to-configuration-ng的单元测试覆盖了两个核心解析逻辑main.rs 测试模块parse_fstab验证 fstab 解析对空输入、非法行的容错以及典型 NixOS 生成的 fstab含 btrfs 根挂载、x-systemd.automount的/boot、erofs 只读挂载与交换分区注释的解析结果filter_units验证输出过滤逻辑在空集合与混合集合下的行为。在 NixOS 层面仓库还提供了系统级集成测试 nixos/tests/switch-test.nix 来验证实际切换行为。对开发者而言若要在本地构建并调试该程序可参考其 READMEcd ./pkgs/by-name/sw/switch-to-configuration-ng nix-shell ../../../.. -A switch-to-configuration-ng cargo build结语一次看似简单的nixos-rebuild switch背后是一条严谨且顺序固定的执行流水线先更新引导器与同步 store再以 fstab 和 systemd 状态为输入计算差异随后依序执行停止单元、运行激活脚本、重启 systemd、重载单元、重启单元、启动单元最后汇总失败与新增单元。理解了这条流水线模块开发者就能为自己的单元选择正确的行为控制属性管理员也能借助dry-activate、STC_DEBUG与STC_DISPLAY_ALL_UNITS在切换前精准预判、在异常时快速定位。【免费下载链接】nixpkgsNix Packages collection NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考