让 Windows 与 Ubuntu 双系统“从哪里重启,就回到哪里”
在 UEFI 双系统中,一个自然的期待是:Windows 重启后仍进入 Windows,Ubuntu 重启后仍进入 Ubuntu;只有主动点击桌面上的“切换系统”,才在下一次启动时进入另一端。
本文给出一套克制而可靠的实现。它不改写引导器文件,也不让 GRUB 长期接管默认入口,而是只使用 UEFI 已定义的两项机制:
BootOrder:长期启动顺序;BootNext:仅下一次启动有效的临时入口,用过即失效。
最终策略如下:
| 当前操作 | 下一次进入 |
|---|---|
| Windows 正常重启 | Windows |
| Ubuntu 正常重启 | Ubuntu |
| Ubuntu 桌面点击“重启到 Windows” | Windows |
| Windows 桌面点击“重启到 Ubuntu” | Ubuntu |
| Ubuntu 关机后再开机 | Windows |
这里有意把 Windows 设为固件默认项。Ubuntu 只在“从 Ubuntu 正常重启”时,通过 systemd hook 临时写入 BootNext=Ubuntu。
为什么必须明确一个默认项,而且选择 Windows
BootNext 只覆盖一次启动,而且并非始终存在。正常开机、关机后再开机、断电恢复、硬复位,乃至 hook 执行失败时,固件都需要回到稳定的长期顺序。因此,这套设计必须在 BootOrder 中明确一个默认入口;它既是日常冷启动的选择,也是所有异常路径的安全回退。
将这个入口设为 Windows,不是因为 Windows 完全做不了重启 hook。借助服务、计划任务或关机脚本,Windows 也能拼出相近机制;困难在于这些入口通常不如 systemd 的 reboot.target 直接,很难对所有路径稳定地区分“重启”与“关机”,还可能受到系统更新、快速启动和不同重启方式的影响。
反过来设计更简单:让 Windows 依靠固件默认顺序自然回到 Windows;让 Ubuntu 借助明确的 reboot.target hook,在正常重启前写入一次性的 BootNext=Ubuntu。跨系统启动则由两个桌面按钮显式完成。这样需要拦截的系统更少,失败时也只会退回 Windows,而不会落入不确定状态。
适用范围:UEFI/GPT 安装的 Windows 与 Ubuntu,Ubuntu 使用 GRUB。传统 BIOS/Legacy 模式不适用。
一、先厘清两层引导
本机的启动链并非只有一层:
UEFI firmware
├── Windows Boot Manager → Windows
└── Ubuntu shim → GRUB → Ubuntu / Windows
因此需要同时约束两处默认值:
- UEFI 的长期默认入口设为 Windows;
- 一旦 UEFI 临时选择 Ubuntu,GRUB 必须稳定选择 Ubuntu,不能沿用过去保存的菜单选择。
否则即使 BootNext 正确指向 Ubuntu,也可能进入 GRUB 后再次跳回 Windows。
二、设置 UEFI 的长期启动顺序
在 Ubuntu 中查看固件启动项:
sudo efibootmgr -v
本文机器上的关键条目为:
Boot0000* Windows Boot Manager
Boot0001* Ubuntu
将 Windows 放在第一位,同时保留其余入口:
sudo efibootmgr --bootorder 0000,0001,0002,0004
启动编号由每台机器的固件决定,不能照抄。执行前应以 efibootmgr -v 的实际输出为准。
三、让 GRUB 在被选中时固定进入 Ubuntu
编辑 /etc/default/grub:
GRUB_DEFAULT=0
GRUB_SAVEDEFAULT=false
然后重新生成配置:
sudo update-grub
GRUB_DEFAULT=0 选择第一条菜单项,即 Ubuntu;关闭 GRUB_SAVEDEFAULT,可以防止一次手工选择 Windows 改变后续行为。
四、为 Ubuntu 正常重启安装 BootNext hook
创建 /usr/local/sbin/set-ubuntu-bootnext-on-reboot:
#!/bin/bash
set -euo pipefail
skip_marker=/run/reboot-to-windows.skip-ubuntu-bootnext
if [[ -e "$skip_marker" ]]; then
/usr/bin/rm -f "$skip_marker"
echo "Dedicated Windows reboot requested; leaving UEFI BootNext unchanged."
exit 0
fi
mapfile -t ubuntu_boot_ids < <(
/usr/bin/efibootmgr |
/usr/bin/sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\* Ubuntu.*/\1/p'
)
if [[ ${#ubuntu_boot_ids[@]} -ne 1 ]]; then
echo "Expected exactly one active Ubuntu EFI entry; found ${#ubuntu_boot_ids[@]}." >&2
exit 1
fi
ubuntu_boot_id="${ubuntu_boot_ids[0]}"
if [[ ! "$ubuntu_boot_id" =~ ^[0-9A-Fa-f]{4}$ ]]; then
echo "Invalid Ubuntu EFI entry: $ubuntu_boot_id" >&2
exit 1
fi
if [[ ! -f /boot/efi/EFI/ubuntu/shimx64.efi ]]; then
echo "Ubuntu EFI loader is missing; cannot set BootNext." >&2
exit 1
fi
if [[ "${1:-}" == "--dry-run" ]]; then
echo "Would set UEFI BootNext to Ubuntu (Boot$ubuntu_boot_id)."
exit 0
fi
if [[ $# -ne 0 ]]; then
echo "Usage: $0 [--dry-run]" >&2
exit 2
fi
/usr/bin/efibootmgr --bootnext "$ubuntu_boot_id"
boot_next="$(
/usr/bin/efibootmgr |
/usr/bin/sed -n 's/^BootNext: //p'
)"
if [[ "${boot_next^^}" != "${ubuntu_boot_id^^}" ]]; then
echo "Could not verify UEFI BootNext for Ubuntu." >&2
exit 1
fi
echo "Set UEFI BootNext to Ubuntu (Boot$ubuntu_boot_id)."
赋予执行权限:
sudo chmod 0755 /usr/local/sbin/set-ubuntu-bootnext-on-reboot
再创建 /etc/systemd/system/ubuntu-bootnext-on-reboot.service:
[Unit]
Description=Keep Ubuntu selected across an Ubuntu reboot
Documentation=man:efibootmgr(8)
DefaultDependencies=no
Before=shutdown.target umount.target final.target systemd-reboot.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/set-ubuntu-bootnext-on-reboot
[Install]
WantedBy=reboot.target
加载并启用:
sudo systemctl daemon-reload
sudo systemctl enable ubuntu-bootnext-on-reboot.service
这个 unit 只挂到 reboot.target,不会在普通关机时执行。于是 Ubuntu 正常重启会写入一次性的 Ubuntu 入口,而关机后再次开机仍服从长期 BootOrder,进入 Windows。
五、Ubuntu 桌面的“重启到 Windows”
专用切换入口必须绕过上一节的 hook,否则它刚写入 BootNext=Windows,就会在关机阶段被覆盖为 Ubuntu。
解决方法是在 /run 下创建一个仅本次运行有效的标记。/run 位于临时文件系统中,重启后自然消失;hook 发现标记时,将其删除并保持 BootNext 不变。
创建 root 脚本 /usr/local/sbin/reboot-to-windows:
#!/bin/bash
set -euo pipefail
skip_marker=/run/reboot-to-windows.skip-ubuntu-bootnext
mapfile -t windows_boot_ids < <(
/usr/bin/efibootmgr |
/usr/bin/sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\* Windows Boot Manager.*/\1/p'
)
if [[ ${#windows_boot_ids[@]} -ne 1 ]]; then
echo "Expected exactly one active Windows Boot Manager entry; found ${#windows_boot_ids[@]}." >&2
exit 1
fi
windows_boot_id="${windows_boot_ids[0]}"
if [[ ! "$windows_boot_id" =~ ^[0-9A-Fa-f]{4}$ ]]; then
echo "Invalid Windows boot entry: $windows_boot_id" >&2
exit 1
fi
if [[ ! -f /boot/efi/EFI/Microsoft/Boot/bootmgfw.efi ]]; then
echo "Windows EFI loader is missing; refusing to reboot." >&2
exit 1
fi
if [[ "${1:-}" == "--dry-run" ]]; then
echo "Windows Boot Manager: Boot$windows_boot_id"
echo "EFI loader: /boot/efi/EFI/Microsoft/Boot/bootmgfw.efi"
echo "The Ubuntu reboot hook will be bypassed."
exit 0
fi
if [[ $# -ne 0 ]]; then
echo "Usage: $0 [--dry-run]" >&2
exit 2
fi
/usr/bin/efibootmgr --bootnext "$windows_boot_id"
boot_next="$(
/usr/bin/efibootmgr |
/usr/bin/sed -n 's/^BootNext: //p'
)"
if [[ "${boot_next^^}" != "${windows_boot_id^^}" ]]; then
/usr/bin/efibootmgr --delete-bootnext 2>/dev/null || true
echo "Could not verify UEFI BootNext; reboot cancelled." >&2
exit 1
fi
/usr/bin/touch "$skip_marker"
/usr/bin/chmod 0600 "$skip_marker"
if ! /usr/bin/systemctl reboot; then
/usr/bin/rm -f "$skip_marker"
/usr/bin/efibootmgr --delete-bootnext 2>/dev/null || true
echo "Could not request reboot; BootNext was cleared." >&2
exit 1
fi
赋予执行权限:
sudo chmod 0755 /usr/local/sbin/reboot-to-windows
创建用户侧包装脚本 ~/.local/bin/reboot-to-windows-gui:
#!/bin/bash
set -e
/usr/bin/zenity --question \
--title='重启到 Windows' \
--text='将设置一次性 Windows 启动项并立即重启。\n\n继续吗?' \
--ok-label='重启到 Windows' \
--cancel-label='取消'
exec /usr/bin/pkexec /usr/local/sbin/reboot-to-windows
chmod 0755 ~/.local/bin/reboot-to-windows-gui
最后创建 ~/Desktop/reboot-to-windows.desktop:
[Desktop Entry]
Type=Application
Version=1.0
Name=重启到 Windows
Comment=设置一次性 Windows Boot Manager 并立即重启
Exec=/home/YOUR_USER/.local/bin/reboot-to-windows-gui
Icon=system-reboot
Terminal=false
StartupNotify=true
Categories=System;
将 YOUR_USER 换成实际用户名,然后执行:
chmod 0755 ~/Desktop/reboot-to-windows.desktop
GNOME 首次使用时可能要求右键该图标并选择“允许运行”。pkexec 会在真正修改 UEFI 变量前请求管理员认证。
六、Windows 桌面的“重启到 Ubuntu”
Windows 端与 Ubuntu 端的思想相同,但工具不同:bcdedit /enum firmware /v 用于查看固件入口;{fwbootmgr} 代表 UEFI Firmware Boot Manager;它的 bootsequence 对应下一次固件启动顺序。
不要直接使用:
bcdedit /bootsequence ...
该写法默认操作 Windows Boot Manager 自己的启动序列,并不等价于设置 UEFI BootNext。这里应明确指定 {fwbootmgr}。
1. 获取 Ubuntu 的固件 GUID
以管理员身份打开 PowerShell:
bcdedit.exe /enum firmware /v
找到描述为 Ubuntu 的固件应用,记录它的 identifier,形如:
{12345678-1234-1234-1234-123456789abc}
UEFI 中的 Boot0001 与 Windows 展示的 BCD GUID 是两套标识,不能把 0001 直接填进 Windows 脚本。
2. 创建 PowerShell 切换脚本
创建目录 C:\ProgramData\DualBoot,并保存以下文件为 C:\ProgramData\DualBoot\Reboot-To-Ubuntu.ps1:
Add-Type -AssemblyName System.Windows.Forms
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# 替换成 bcdedit /enum firmware /v 显示的 Ubuntu 固件 GUID。
$UbuntuFirmwareId = '{12345678-1234-1234-1234-123456789abc}'
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
$isAdministrator = $principal.IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator
)
if (-not $isAdministrator) {
$arguments = @(
'-NoProfile'
'-ExecutionPolicy', 'Bypass'
'-File', ('"{0}"' -f $PSCommandPath)
)
$startParameters = @{
FilePath = 'powershell.exe'
Verb = 'RunAs'
ArgumentList = $arguments
}
Start-Process @startParameters
exit
}
if ($UbuntuFirmwareId -notmatch '^[{][0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}[}]$') {
throw 'Ubuntu firmware GUID 格式无效。'
}
$firmwareEntries = (& bcdedit.exe /enum firmware /v 2>&1) -join "`n"
if ($LASTEXITCODE -ne 0) {
throw '无法读取 UEFI 固件启动项。'
}
if ($firmwareEntries -notmatch [regex]::Escape($UbuntuFirmwareId)) {
throw "固件中不存在 Ubuntu 启动项 $UbuntuFirmwareId。"
}
$answer = [System.Windows.Forms.MessageBox]::Show(
'将设置一次性 Ubuntu 启动项并立即重启。是否继续?',
'重启到 Ubuntu',
[System.Windows.Forms.MessageBoxButtons]::YesNo,
[System.Windows.Forms.MessageBoxIcon]::Question
)
if ($answer -ne [System.Windows.Forms.DialogResult]::Yes) {
exit
}
& bcdedit.exe /set '{fwbootmgr}' bootsequence $UbuntuFirmwareId
if ($LASTEXITCODE -ne 0) {
throw '无法设置 UEFI 下一次启动项。'
}
& shutdown.exe /r /t 0
if ($LASTEXITCODE -ne 0) {
& bcdedit.exe /deletevalue '{fwbootmgr}' bootsequence | Out-Null
throw '无法请求重启;已尝试清除一次性启动项。'
}
3. 创建桌面入口
在桌面创建 重启到 Ubuntu.cmd:
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass ^
-File "C:\ProgramData\DualBoot\Reboot-To-Ubuntu.ps1"
双击后,PowerShell 会通过 UAC 请求管理员权限,显示确认框,设置一次性的 Ubuntu 固件入口,然后重启。该设置仅影响下一次启动;Ubuntu 启动后,UEFI 的长期默认顺序仍是 Windows 优先。
如果 Windows 中完全看不到 Ubuntu 固件项,应先回到 Ubuntu 检查
efibootmgr -v和\EFI\ubuntu\shimx64.efi,而不是在 Windows 中凭空创建或覆盖引导项。
七、验证
在 Ubuntu 中先做静态检查:
sudo /usr/local/sbin/set-ubuntu-bootnext-on-reboot --dry-run
sudo /usr/local/sbin/reboot-to-windows --dry-run
systemctl is-enabled ubuntu-bootnext-on-reboot.service
systemctl list-dependencies reboot.target --plain
检查长期启动顺序以及是否存在临时启动项:
sudo efibootmgr
预期长期顺序以 Windows 开头:
BootOrder: 0000,0001,...
完整验收应覆盖四条路径:
- Ubuntu 普通重启,确认仍进入 Ubuntu;
- Ubuntu 点击“重启到 Windows”,确认进入 Windows;
- Windows 普通重启,确认仍进入 Windows;
- Windows 点击“重启到 Ubuntu”,确认进入 Ubuntu。
八、边界与故障恢复
这套机制依赖正常的 systemd 重启流程。reboot -f、内核崩溃、断电和机箱复位不会执行 Ubuntu hook;下一次开机将服从长期 BootOrder,即进入 Windows。这是预期的降级行为。
如果固件升级或 Windows 更新重写了 UEFI 顺序,可在 Ubuntu 中再次执行:
sudo efibootmgr --bootorder 0000,0001,0002,0004
若要撤销 Ubuntu 端配置:
sudo systemctl disable ubuntu-bootnext-on-reboot.service
sudo rm /etc/systemd/system/ubuntu-bootnext-on-reboot.service
sudo rm /usr/local/sbin/set-ubuntu-bootnext-on-reboot
sudo systemctl daemon-reload
随后恢复 /etc/default/grub 中原来的 GRUB_DEFAULT 与 GRUB_SAVEDEFAULT,并运行:
sudo update-grub
结语
这套方案的关键,不是不断改写永久启动顺序,而是让永久配置保持稳定,只在确有需要时写入一次性的下一跳:Windows 以默认顺序维持自身,Ubuntu 在正常重启前短暂声明“下一次仍然回来”,两个桌面切换入口则明确表达跨系统意图。
长期状态越少,启动逻辑越容易理解,也越容易恢复。
