让 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

因此需要同时约束两处默认值:

  1. UEFI 的长期默认入口设为 Windows;
  2. 一旦 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,...

完整验收应覆盖四条路径:

  1. Ubuntu 普通重启,确认仍进入 Ubuntu;
  2. Ubuntu 点击“重启到 Windows”,确认进入 Windows;
  3. Windows 普通重启,确认仍进入 Windows;
  4. 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 在正常重启前短暂声明“下一次仍然回来”,两个桌面切换入口则明确表达跨系统意图。

长期状态越少,启动逻辑越容易理解,也越容易恢复。

参考资料

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理。