Windows WSL2 虚拟硬盘迁移终极方案:Junction 一劳永逸,告别 C 盘焦虑

Windows WSL2 虚拟硬盘迁移终极方案:Junction 一劳永逸,告别 C 盘焦虑
适用环境: Windows 10/11 + WSL2

适用对象: 所有 WSL2 分发版(Ubuntu、Fedora、Podman machine 等)

难度: ⭐⭐(小白友好,照做即可)

最后验证: 2026年4月28日,Podman 5.8.2 + WSL2 Mirrored 网络模式

 

【打造 AI 开发环境系列】WSL 路径治理必做篇:先迁移,再开发,别让 C 盘爆满!

目录

Windows WSL2 虚拟硬盘迁移终极方案:Junction 一劳永逸,告别 C 盘焦虑

一、问题:WSL 的虚拟硬盘为什么会吃掉你的 C 盘

默认存储位置

二、迁移方案对比:哪种最靠谱

方案 A:手动移动 vhdx + 修改注册表 ❌ 不推荐

方案 B:SymbolicLink(符号链接)✅ 有条件可用

方案 C:wsl --export / wsl --import ✅ 可靠但繁琐

方案 D:Junction(目录联接)✅✅ 推荐方案

三、Junction 方案详细教程

3.1 理解 Junction 是什么

3.2 迁移 WSL 默认安装目录

前置条件

操作步骤

3.3 迁移 Podman Machine 存储目录

3.4 迁移已有分发版(保留数据)

3.5 Docker Desktop 存储目录

四、全方案适用性总结

五、踩坑记录:我是怎么一步步找到最优解的

坑 1:对子目录做 Junction,WSL 挂载 vhdx 失败

坑 2:SymbolicLink 在目标目录不存在时导致 mkdir 冲突

坑 3:SSH 端口三处不同步

坑 4:wsl --export 后未验证就 --unregister

坑 5:Podman Desktop 与命令行冲突

坑 6:WSL Mirrored 模式下的代理继承问题

坑 7:Podman machine start 超时

六、最佳实践清单

七、一键检查脚本

八、总结

一、问题:WSL 的虚拟硬盘为什么会吃掉你的 C 盘
WSL2 的每个 Linux 分发版都使用一个 ext4.vhdx 虚拟硬盘文件来存储所有数据。这个文件默认创建在 C 盘用户目录下,而且有个让人头疼的特性:它只会膨胀,不会自动缩小。

你在 WSL 里下载了 10GB 的数据再删除,vhdx 文件仍然占据 10GB 的磁盘空间。随着使用时间的增长,一个分发版的 vhdx 轻松膨胀到几十甚至上百 GB。如果你像我一样同时运行多个分发版(Ubuntu、Podman machine、HeyGem、duix 等),C 盘空间会迅速告急。

默认存储位置
不同来源安装的 WSL 分发版,vhdx 默认位置不同:

安装方式    默认 vhdx 路径
wsl --install / Microsoft Store    C:\Users\<用户名>\AppData\Local\Packages\...\LocalState\ext4.vhdx
wsl --import    你指定的路径
Podman machine init    C:\Users\<用户名>\.local\share\containers\podman\machine\wsl\wsldist\<名称>\ext4.vhdx
Docker Desktop    C:\Users\<用户名>\AppData\Local\Docker\wsl\...\ext4.vhdx
可以看到,几乎所有方式都默认落在 C 盘。

二、迁移方案对比:哪种最靠谱
我在实际操作中尝试了四种迁移方案,逐一分析它们的优缺点。

方案 A:手动移动 vhdx + 修改注册表 ❌ 不推荐
思路: 直接移动 vhdx 文件到新位置,然后修改 WSL 注册表 HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss\{GUID}\BasePath 指向新路径。

问题:

WSL 本身能跑,但依赖 WSL 的上层工具(如 Podman)有自己独立的配置文件记录了 SSH 端口、socket 路径等信息,不会跟着注册表变化自动更新,导致各种"能 ping 通但连不上"的诡异问题。
手动改注册表容易遗漏 \\?\ 长路径前缀,格式不对会引发隐蔽错误。
不可复现,每次迁移都是手工操作,容易出错。
方案 B:SymbolicLink(符号链接)✅ 有条件可用
思路: 在 C 盘原路径创建 SymbolicLink 指向新盘。

实测结果: 经过实际验证,SymbolicLink 对父目录做链接是可以成功的,podman machine init、podman machine start、podman run 全部正常,vhdx 正确落到目标盘。

但有一个前置条件: 目标目录必须已经存在。如果目标目录不存在,某些程序(如 Podman)在创建目录时,Go 语言的 os.MkdirAll 会检测到路径已被 SymbolicLink 占用但目标不可达,报 Cannot create a file when that file already exists。而 Junction 无论目标目录是否预先存在都能正常工作。

其他注意事项:

早期 Windows 10 需要管理员权限创建 SymbolicLink(Windows 10 1703+ 开启开发者模式后不需要)。
SymbolicLink 在 Windows API 层解析,极少数底层程序可能不识别。
方案 C:wsl --export / wsl --import ✅ 可靠但繁琐
思路: 导出分发版为 tar 文件,注销旧的,导入到新位置。

优点: 最"正统"的方式,WSL 自己管理注册表路径,不会有格式错误。

缺点:

每次迁移都要导出/导入,vhdx 越大越慢(100GB 的分发版可能需要十几分钟)。
操作步骤多,中间任何一步失败(如 tar 文件没生成就执行了 unregister)可能导致数据丢失。
只是一次性操作,以后重建分发版还是会落回 C 盘,需要再次迁移。
对 Podman machine 等上层工具,导入后可能需要额外修复配置。
方案 D:Junction(目录联接)✅✅ 推荐方案
思路: 对存放 vhdx 的父目录创建 Junction,指向新盘。所有后续创建的分发版自动落到新盘。

优点:

一次设置,永久生效。 以后不管是 wsl --install、podman machine init 还是任何其他方式创建的分发版,vhdx 都自动落到目标盘。
对所有程序透明。 Junction 在 NTFS 文件系统层面解析,对 WSL、Podman、Docker 等上层程序完全透明,它们感知不到路径被重定向了。
不需要管理员权限(Windows 10 1703+)。
不占 C 盘空间。 Junction 本身只是一个目录元数据,不存储任何文件内容。
可逆。 删除 Junction 再创建普通目录,就恢复原状。
注意事项:

Junction 必须对父目录做,不能对单个分发版的子目录做(否则 WSL 的 vhdx 挂载可能失败,详见后文踩坑记录)。
目标盘必须是 NTFS 格式(不支持 FAT32、exFAT)。
目标盘不能是网络驱动器或可移动存储(U盘)。
三、Junction 方案详细教程
3.1 理解 Junction 是什么
Junction(目录联接)是 NTFS 文件系统的一个特性,类似于 Linux 的 mount bind。当你在 A 路径创建一个 Junction 指向 B 路径时,任何程序访问 A 路径下的文件,实际上都会被文件系统透明地重定向 到 B 路径。

与 SymbolicLink 的区别:

特性    Junction    SymbolicLink
解析层级    NTFS 文件系统层(更底层)    Windows API 层
跨盘支持    ✅ 本地盘之间    ✅ 支持网络路径
管理员权限    不需要    早期 Windows 10 需要
程序兼容性    极好(几乎所有程序透明)    好(少数程序不识别)
目标目录不存在时    ✅ 正常工作    ❌ 某些程序 mkdir 报冲突
作用对象    仅目录    目录和文件
两者都经过实测验证可用,但 Junction 无条件可用、不需要预创建目标目录,更省心。推荐优先使用 Junction。

3.2 迁移 WSL 默认安装目录
这一步将通过 wsl --install 安装的分发版的默认存储位置重定向到其他盘。

前置条件
目标盘是 NTFS 格式的本地固定磁盘
已关闭所有 WSL 分发版
操作步骤
# ============================================================
# 步骤 0:关闭所有 WSL 实例
# ============================================================
wsl --shutdown
Start-Sleep -Seconds 5
 
# 验证
wsl -l -v
# 所有分发版应显示 Stopped
一键获取完整项目代码
# ============================================================
# 步骤 1:确认当前默认目录位置和内容
# ============================================================
$wslDefaultPath = "$env:LOCALAPPDATA\wsl"
Write-Host "默认 WSL 路径: $wslDefaultPath"
Write-Host "是否存在: $(Test-Path $wslDefaultPath)"
 
if (Test-Path $wslDefaultPath) {
    Get-ChildItem $wslDefaultPath -Recurse -Filter "*.vhdx" |
      Select-Object FullName, @{N='SizeMB';E={[math]::Round($_.Length/1MB,1)}}
}
一键获取完整项目代码
如果这个目录下有现有分发版的 vhdx 文件: 需要先用 wsl --export 导出,再用 wsl --import 导入到新位置,最后才能删除原目录创建 Junction。具体步骤见 3.4 节。

如果这个目录为空或不存在: 可以直接进行下一步。

# ============================================================
# 步骤 2:创建目标目录
# ============================================================
$targetPath = "I:\WSL\wsl-default"    # 改成你的目标路径
New-Item -ItemType Directory -Path $targetPath -Force
一键获取完整项目代码
# ============================================================
# 步骤 3:替换为 Junction
# ============================================================
# 如果原目录存在且为空,先删除
if (Test-Path $wslDefaultPath) {
    $items = Get-ChildItem $wslDefaultPath -Force
    if ($items.Count -eq 0) {
        Remove-Item $wslDefaultPath
        Write-Host "已删除空目录"
    } else {
        Write-Host "❌ 目录不为空,请先迁移里面的分发版!" -ForegroundColor Red
        return
    }
}
 
# 创建 Junction
New-Item -ItemType Junction -Path $wslDefaultPath -Target $targetPath
 
# 验证
$link = Get-Item $wslDefaultPath
Write-Host "类型: $($link.LinkType)"
Write-Host "目标: $($link.Target)"
一键获取完整项目代码
# ============================================================
# 步骤 4:验证——安装一个测试分发版
# ============================================================
wsl --install -d Debian
 
# 检查 vhdx 是否落在目标盘
Get-ChildItem $targetPath -Recurse -Filter "*.vhdx" |
  Select-Object FullName, @{N='SizeMB';E={[math]::Round($_.Length/1MB,1)}}
 
# 如果不需要 Debian,可以卸载
# wsl --unregister Debian
一键获取完整项目代码
3.3 迁移 Podman Machine 存储目录
Podman machine 使用独立的存储路径,需要单独设置 Junction。

# ============================================================
# 步骤 0:关闭并删除现有 Podman machine
# ============================================================
# ⚠️ 这会删除所有容器和镜像,确认不需要保留再执行
podman machine stop
wsl --shutdown
Start-Sleep -Seconds 5
podman machine rm podman-machine-default --force
一键获取完整项目代码
# ============================================================
# 步骤 1:确认 wsldist 目录状态
# ============================================================
$podmanWsldist = "$HOME\.local\share\containers\podman\machine\wsl\wsldist"
Write-Host "路径: $podmanWsldist"
Write-Host "是否存在: $(Test-Path $podmanWsldist)"
 
if (Test-Path $podmanWsldist) {
    $items = Get-ChildItem $podmanWsldist -Force
    Write-Host "内容数量: $($items.Count)"
    if ($items.Count -gt 0) {
        Write-Host "❌ 目录不为空,请先删除所有 Podman machine!" -ForegroundColor Red
        return
    }
}
一键获取完整项目代码
# ============================================================
# 步骤 2:创建目标目录
# ============================================================
$targetPath = "I:\WSL\podman-wsldist"    # 改成你的目标路径
New-Item -ItemType Directory -Path $targetPath -Force
一键获取完整项目代码
# ============================================================
# 步骤 3:替换为 Junction
# ============================================================
if (Test-Path $podmanWsldist) {
    Remove-Item $podmanWsldist
}
 
New-Item -ItemType Junction -Path $podmanWsldist -Target $targetPath
 
# 验证
$link = Get-Item $podmanWsldist
Write-Host "类型: $($link.LinkType)"
Write-Host "目标: $($link.Target)"
一键获取完整项目代码
# ============================================================
# 步骤 4:重建 Podman machine 并验证
# ============================================================
podman machine init --rootful --disk-size 100 --cpus 12 --memory 2048
podman machine start
 
# 验证 vhdx 在目标盘
Get-ChildItem $targetPath -Recurse -Filter "*.vhdx" |
  Select-Object FullName, @{N='SizeMB';E={[math]::Round($_.Length/1MB,1)}}
 
# 功能测试
podman run --rm hello-world
一键获取完整项目代码
3.4 迁移已有分发版(保留数据)
如果你已经有分发版在 C 盘运行,不想重建,可以先导出再导入,最后再设置 Junction 防止未来的分发版再落回 C 盘。

# ============================================================
# 以 Ubuntu 为例,迁移已有分发版
# ============================================================
 
# 步骤 1:关闭
wsl --shutdown
Start-Sleep -Seconds 5
 
# 步骤 2:导出(注意路径不要和目标目录冲突)
wsl --export Ubuntu "D:\temp\ubuntu-backup.tar"
 
# 步骤 3:确认 tar 文件存在且大小合理
$tar = Get-Item "D:\temp\ubuntu-backup.tar"
if (-not $tar -or $tar.Length -lt 1MB) {
    Write-Host "❌ 导出失败,停止操作!" -ForegroundColor Red
    return
}
Write-Host "✅ 导出成功: $([math]::Round($tar.Length/1MB,1)) MB"
 
# 步骤 4:注销旧的
wsl --unregister Ubuntu
 
# 步骤 5:导入到新位置
wsl --import Ubuntu "I:\WSL\Ubuntu" "D:\temp\ubuntu-backup.tar"
 
# 步骤 6:验证
wsl -d Ubuntu -- echo "OK"
 
# 步骤 7:清理 tar
Remove-Item "D:\temp\ubuntu-backup.tar"
一键获取完整项目代码
注意: wsl --import 导入后,默认登录用户会变成 root。如果需要恢复默认用户,在分发版内部的 /etc/wsl.conf 中添加:

[user]
default=你的用户名
一键获取完整项目代码
然后 wsl --terminate Ubuntu 重启分发版生效。

3.5 Docker  Desktop 存储目录
Docker Desktop 的原理类似,默认路径在 C:\Users\<用户名>\AppData\Local\Docker\wsl\。同样可以用 Junction 方式重定向,但需要注意:

先完全退出 Docker Desktop
wsl --shutdown 确保 docker-desktop 和 docker-desktop-data 分发版停止
对 AppData\Local\Docker\wsl 做 Junction
重启 Docker Desktop
操作流程与 Podman 类似,此处不再赘述。

四、全方案适用性总结
Junction 方案是否能涵盖所有 WSL 分发版?答案是可以,但需要对不同来源的分发版做不同父目录的 Junction。

分发版来源    需要做 Junction 的父目录    一次设置后效果
wsl --install    %LOCALAPPDATA%\wsl    以后所有 wsl --install 的分发版自动落到目标盘
Podman machine    %USERPROFILE%\.local\share\containers\podman\machine\wsl\wsldist    以后所有 podman machine init 自动落到目标盘
Docker Desktop    %LOCALAPPDATA%\Docker\wsl    以后 Docker 的 WSL 数据自动落到目标盘
wsl --import    不需要(导入时直接指定路径)    —
Microsoft Store 安装    %LOCALAPPDATA%\Packages\...\LocalState    每个分发版路径不同,不适合用 Junction,建议用 export/import
推荐策略:

对前三类(wsl --install、Podman、Docker)各设一个 Junction,一劳永逸。
Microsoft Store 安装的分发版路径分散在不同 Package 目录下,不方便统一做 Junction,建议用 wsl --export/import 单独迁移。
wsl --import 本身就能指定目标路径,不需要额外处理。
五、踩坑记录:我是怎么一步步找到最优解的
以下是我在迁移 Podman machine 过程中踩过的所有坑,记录下来供大家避坑。

坑 1:对子目录做 Junction,WSL 挂载 vhdx 失败
错误操作:

# ❌ 对单个分发版的子目录做 Junction
Move-Item "...\wsldist\podman-machine-default" "I:\WSL\podman-machine-default"
New-Item -ItemType Junction -Path "...\wsldist\podman-machine-default" -Target "I:\WSL\podman-machine-default"
一键获取完整项目代码
报错:

无法将磁盘 "...\ext4.vhdx" 附加到 WSL2: 系统找不到指定的文件。
一键获取完整项目代码
原因: WSL 的 vhdx 挂载走的是 Hyper-V 层,当挂载路径本身是一个 Junction 时,Hyper-V 可能无法正确解析。但当 Junction 在更上层的父目录时,文件系统先解析 Junction 再到达 vhdx 文件,Hyper-V 拿到的已经是实际路径了。

正确做法: Junction 要做在 父目录 wsldist 上,不要做在单个分发版的子目录上。

坑 2:SymbolicLink 在目标目录不存在时导致 mkdir 冲突
错误操作:

# ❌ 目标目录 I:\WSL\podman-wsldist 尚未创建
New-Item -ItemType SymbolicLink -Path "...\wsldist" -Target "I:\WSL\podman-wsldist"
podman machine init ...
一键获取完整项目代码
报错:

Error: could not create wsldist directory: mkdir ...\wsldist: Cannot create a file when that file already exists.
一键获取完整项目代码
原因: Podman 的 machine init 在代码中用 mkdir 创建 wsldist 目录。当 SymbolicLink 指向的目标目录不存在时,Go 语言的 os.MkdirAll 检测到路径已存在但不是有效目录,就报错了。

后续验证: 当目标目录已存在时,重新测试 SymbolicLink,podman machine init + start + run 全部成功,vhdx 正确落到目标盘。所以 SymbolicLink 本身是可用的,前提是目标目录必须预先创建。

推荐做法: 用 Junction 而不是 SymbolicLink,因为 Junction 无论目标目录是否存在都能正常工作,少一个前置条件。

# ✅ Junction:无条件可用
New-Item -ItemType Junction -Path "<原路径>" -Target "<目标路径>"
 
# ⚠️ SymbolicLink:需要目标目录已存在
New-Item -ItemType Directory -Path "<目标路径>" -Force   # 必须先创建
New-Item -ItemType SymbolicLink -Path "<原路径>" -Target "<目标路径>"
一键获取完整项目代码
坑 3:SSH 端口三处不同步
Podman machine 的 SSH 端口信息存在三个地方:

WSL 内部的 /etc/ssh/sshd_config 中的 Port 行
Windows 侧的 podman-machine-default.json 中的 SSH.Port 字段
podman system connection list 中的 URI 端口
手动迁移(方案 A)后这三个地方容易不同步。Junction 方案完全避免了这个问题,因为不需要手动干预任何配置。

坑 4:wsl --export 后未验证就 --unregister
这是最危险的操作失误。wsl --export 可能因为各种原因(磁盘空间不足、权限问题、目标路径不可写)静默失败,如果不检查 tar 文件就执行 wsl --unregister,分发版数据将永久丢失。

安全操作模板:

wsl --export <分发版名> "<目标.tar>"
 
# ⚠️ 必须验证!
$tar = Get-Item "<目标.tar>" -ErrorAction SilentlyContinue
if (-not $tar -or $tar.Length -lt 1MB) {
    Write-Host "❌ 导出失败,不要执行 unregister!" -ForegroundColor Red
    return
}
Write-Host "✅ 导出成功: $([math]::Round($tar.Length/1MB,1)) MB"
 
# 确认后才注销
wsl --unregister <分发版名>
一键获取完整项目代码
坑 5:Podman Desktop 与命令行 冲突
Podman Desktop 后台运行时会占用 named pipe,导致命令行 podman machine start 报 could not start api proxy since expected pipe is not available。

解决: 命令行操作 Podman machine 时,先关闭 Podman Desktop:

Stop-Process -Name "Podman Desktop" -Force -ErrorAction SilentlyContinue
Stop-Process -Name "win-sshproxy" -Force -ErrorAction SilentlyContinue
一键获取完整项目代码
坑 6:WSL Mirrored 模式下的代理继承问题
使用 WSL Mirrored 网络模式(.wslconfig 中 networkingMode=Mirrored)时,Windows 侧的代理环境变量会被自动传递到 WSL 内部。如果代理工具(如 *** Verge)没有运行,WSL 内部所有网络请求都会超时。

Podman machine 还会自动生成 /etc/profile.d/default-env.sh 文件写入代理设置,而且 no_proxy 的值包含未转义的分号,导致 bash 语法错误。

解决: 删除这个文件:

wsl -d podman-machine-default -- sudo rm /etc/profile.d/default-env.sh
一键获取完整项目代码
坑 7:Podman machine start 超时
WSL2 从冷启动到 systemd + sshd 完全就绪,在某些机器上需要 25 秒以上,超过了 Podman 5.8.x 的内部等待超时。表现为全新 init 后 machine start 仍然报 ssh error: machine not in running state。

Workaround: 如果第一次 machine start 超时但 WSL 分发版保持 Running 状态,等几秒再执行一次 podman machine start,通常就能成功。

六、最佳实践清单
尽早迁移。 vhdx 越小,操作越快、风险越低。新装的分发版只有不到 1GB,迁移秒完成。

Junction 优先。 对存放 vhdx 的父目录做 Junction,一次设置永久生效,不需要每次重建后再迁移。

Junction 做在父目录。 不要对单个分发版的子目录做 Junction,否则 WSL 的 vhdx 挂载可能失败。

目标盘用 NTFS。 Junction 只支持 NTFS 格式的本地固定磁盘。

导出必验证。 使用 wsl --export 时,必须在 --unregister 之前确认 tar 文件存在且大小合理。

操作前关闭一切。 wsl --shutdown、关闭 Podman Desktop、关闭 Docker Desktop——确保没有进程占用 vhdx 文件。

用 Junction 而非 SymbolicLink。 Junction 在 NTFS 层解析,兼容性远好于 SymbolicLink,不会导致 mkdir 冲突。

记录你的 Junction。 Junction 在文件管理器中看起来和普通文件夹一样,建议在目标目录下放一个 README.txt 标记说明,方便日后回忆。

七、一键检查脚本
把以下脚本保存为 check-wsl-storage.ps1,随时运行检查你的 WSL 存储状况:

Write-Host "========== WSL 存储检查 ==========" -ForegroundColor Cyan
 
# 1. 检查 WSL 默认目录
$wslDefault = "$env:LOCALAPPDATA\wsl"
$item = Get-Item $wslDefault -ErrorAction SilentlyContinue
if ($item) {
    if ($item.LinkType -eq "Junction") {
        Write-Host "[✅ Junction] $wslDefault -> $($item.Target)" -ForegroundColor Green
    } else {
        $size = (Get-ChildItem $wslDefault -Recurse -Filter "*.vhdx" -ErrorAction SilentlyContinue |
                 Measure-Object -Property Length -Sum).Sum / 1GB
        Write-Host "[⚠️  普通目录] $wslDefault (vhdx 占用: $([math]::Round($size,2)) GB)" -ForegroundColor Yellow
    }
} else {
    Write-Host "[--] $wslDefault 不存在" -ForegroundColor Gray
}
 
# 2. 检查 Podman 目录
$podmanDir = "$HOME\.local\share\containers\podman\machine\wsl\wsldist"
$item = Get-Item $podmanDir -ErrorAction SilentlyContinue
if ($item) {
    if ($item.LinkType -eq "Junction") {
        Write-Host "[✅ Junction] $podmanDir -> $($item.Target)" -ForegroundColor Green
    } else {
        $size = (Get-ChildItem $podmanDir -Recurse -Filter "*.vhdx" -ErrorAction SilentlyContinue |
                 Measure-Object -Property Length -Sum).Sum / 1GB
        Write-Host "[⚠️  普通目录] $podmanDir (vhdx 占用: $([math]::Round($size,2)) GB)" -ForegroundColor Yellow
    }
} else {
    Write-Host "[--] $podmanDir 不存在" -ForegroundColor Gray
}
 
# 3. 检查 Docker 目录
$dockerDir = "$env:LOCALAPPDATA\Docker\wsl"
$item = Get-Item $dockerDir -ErrorAction SilentlyContinue
if ($item) {
    if ($item.LinkType -eq "Junction") {
        Write-Host "[✅ Junction] $dockerDir -> $($item.Target)" -ForegroundColor Green
    } else {
        $size = (Get-ChildItem $dockerDir -Recurse -Filter "*.vhdx" -ErrorAction SilentlyContinue |
                 Measure-Object -Property Length -Sum).Sum / 1GB
        Write-Host "[⚠️  普通目录] $dockerDir (vhdx 占用: $([math]::Round($size,2)) GB)" -ForegroundColor Yellow
    }
} else {
    Write-Host "[--] $dockerDir 不存在" -ForegroundColor Gray
}
 
# 4. 列出所有分发版及其 vhdx 位置
Write-Host "`n========== 分发版列表 ==========" -ForegroundColor Cyan
$lxssKeys = Get-ChildItem "HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss" -ErrorAction SilentlyContinue
foreach ($key in $lxssKeys) {
    $name = $key.GetValue("DistributionName")
    $basePath = $key.GetValue("BasePath")
    if ($name -and $basePath) {
        $vhdx = Join-Path $basePath "ext4.vhdx"
        $sizeMB = if (Test-Path $vhdx) {
            [math]::Round((Get-Item $vhdx).Length / 1MB, 1)
        } else { "N/A" }
        
        $drive = if ($basePath -match "^\\\\?\?\\(.):") { $Matches[1] } 
                 elseif ($basePath -match "^(.):") { $Matches[1] }
                 else { "?" }
        
        $color = if ($drive -eq "C") { "Yellow" } else { "Green" }
        Write-Host "  $name -> ${drive}: 盘 ($sizeMB MB)" -ForegroundColor $color
    }
}
 
Write-Host "`n========== 检查完成 ==========" -ForegroundColor Cyan
一键获取完整项目代码
运行示例输出:

========== WSL 存储检查 ==========
[✅ Junction] C:\Users\love\AppData\Local\wsl -> I:\WSL\wsl-default
[✅ Junction] C:\Users\love\.local\share\...\wsldist -> I:\WSL\podman-wsldist
[--] C:\Users\love\AppData\Local\Docker\wsl 不存在
 
========== 分发版列表 ==========
  Ubuntu -> I: 盘 (4096.0 MB)
  podman-machine-default -> I: 盘 (972.0 MB)
  HeyGem-GenVideo -> I: 盘 (15360.0 MB)
 
========== 检查完成 ==========
一键获取完整项目代码
八、总结
WSL 虚拟硬盘迁移的最优解是 Junction 方案:对存放 vhdx 的父目录做 Junction,将其重定向到非系统盘。这个方案一次设置永久生效,对所有上层程序完全透明,既不需要修改注册表,也不需要每次重建后再迁移。

核心命令只有三行:

# 删除空的原目录
Remove-Item "<原路径>"
 
# 创建目标目录
New-Item -ItemType Directory -Path "<目标路径>" -Force
 
# 创建 Junction
New-Item -ItemType Junction -Path "<原路径>" -Target "<目标路径>"
一键获取完整项目代码
就这么简单。希望这篇文章能帮你告别 C 盘焦虑。
Windows WSL2 虚拟硬盘迁移终极方案:Junction 一劳永逸,告别 C 盘焦虑_wsl2迁移-CSDN博客

Podman Machine 虚拟硬盘迁移实战二:用 Junction 把 vhdx 从 C 盘搬到其他盘-CSDN博客

Podman Machine 虚拟硬盘迁移实战二:用 Junction 把 vhdx 从 C 盘搬到其他盘
环境: Windows 11 + Podman 5.8.2 + WSL2 (networkingMode=Mirrored)

硬件: RTX 3090, 驱动 595.02

验证日期: 2026年4月28日

最终效果: podman machine init 自动将 vhdx 创建到 I 盘,C 盘零占用,且以后重建 machine 无需再次迁移

 

一、问题
Podman 在 Windows 上通过 WSL2 运行容器。执行 podman machine init 时,它会在以下路径创建一个 ext4.vhdx 虚拟硬盘:

C:\Users\<用户名>\.local\share\containers\podman\machine\wsl\wsldist\podman-machine-default\ext4.vhdx
一键获取完整项目代码
这个文件会随着拉取镜像、创建容器不断膨胀。实测在拉取 PyTorch、Stable Diffusion 等大型镜像后,vhdx 轻松超过 100GB,严重挤占 C 盘空间。

更麻烦的是,每次 podman machine rm + podman machine init 重建后,vhdx 又会回到 C 盘,需要重复迁移。

在上一篇中,我们通过 wsl --export/import 的方式虽然也实现的无损迁移,但是总觉得似乎这种方案过于繁琐,于是我们再次探索了使用 符号链接 的方式进行快捷安全的迁移,在尝试了很多种 符号链接后,终于实践出了 针对 Podman Machine 迁移的有效方案。

 wsl --export/import 传统方案

Windows Podman Machine 虚拟硬盘迁移完整指南:从 C 盘到非系统盘

同时也再次验证了 我们之前已发布过的 方案的可行性:

【安全有效新方案】WSL 默认路径迁移实战:通过 PowerShell 符号链接实现自动重定向

Docker 目录迁移脚本【Windows Junction 类型链接】

Windows 开发环境部署指南:WSL、Docker Desktop、Podman Desktop 部署顺序与存储路径迁移指南

【笔记●避免C盘爆满】Windows 系统开发环境存储路径迁移全规划参考清单

【打造 AI 开发环境系列】WSL 路径治理必做篇:先迁移,再开发,别让 C 盘爆满!

符号链接 SymbolicLink 全面教程:跨盘迁移的多能方案

Junction 目录联接教程:系统兼容性较强的迁移方案

第3篇:软链接 mklink /D 教程:轻量缓存目录迁移利器

二、方案选择:为什么用 Junction
在找到最终方案之前,我尝试了多种方式:

方案    结果    原因
手动移动 vhdx + 改注册表    ❌    Podman 自身的 SSH 端口配置、connection list 不会跟着更新,导致连接失败
对 vhdx 子目录做 Junction    ❌    WSL 的 Hyper-V 层无法通过 Junction 挂载 vhdx,报"系统找不到指定的文件"
对父目录做 SymbolicLink(目标目录不存在)    ❌    Podman 的 mkdir 检测到 SymbolicLink 占用同名路径,报 Cannot create a file when that file already exists
对父目录做 SymbolicLink(目标目录已存在)    ✅    目标目录已存在时,mkdir 正常通过,init/start/run 全部成功
wsl --export/import    ✅ 但繁琐    能用,但每次重建 machine 后还要再迁移一次,不是一劳永逸的方案
对父目录 wsldist 做 Junction    ✅ 推荐    无条件可用,一次设置永久生效,对 Podman 和 WSL 完全透明
关键结论:

Junction 和 SymbolicLink 对父目录做链接都能成功,实测 podman machine init + start + run --rm hello-world 全部通过
推荐 Junction,因为它无条件可用——不需要预先创建目标目录。SymbolicLink 需要目标目录已存在才行,多一个前置条件
链接必须做在 wsldist 父目录上,不能做在 podman-machine-default 子目录上
原理是:当 Junction 在父目录时,Podman 的 mkdir 在 Junction 目录内部创建子目录,这个操作对 NTFS 来说完全正常。随后 WSL 导入分发版时,拿到的 vhdx 完整路径已经被 NTFS 解析为实际物理路径(I 盘),Hyper-V 挂载不会出问题。而当 Junction 直接在子目录上时,wsl --import 拿到的路径本身就是一个 Junction 入口,Hyper-V 不一定能正确解析。

三、完整操作步骤
3.1 前置条件
目标盘是 NTFS 格式的本地固定磁盘(不能是 exFAT、网络盘或 U 盘)
确认当前 Podman machine 里没有需要保留的重要容器或镜像(本方案需要删除并重建 machine)!!!重要提示!!!
如果 Podman Desktop 在运行,先关闭它
3.2 操作流程
步骤 1:关闭并删除现有 Podman machine
# 关闭 Podman Desktop(如果在运行)
Stop-Process -Name "Podman Desktop" -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
 
# 停止 machine
podman machine stop
 
# 关闭所有 WSL
wsl --shutdown
Start-Sleep -Seconds 5
 
# 删除 machine(会同时注销 WSL 分发版并删除 vhdx)
podman machine rm podman-machine-default --force
一键获取完整项目代码
验证删除成功:

podman machine list
# 预期输出:空列表,没有任何 machine
 
wsl -l -v
# 预期输出:列表中不应包含 podman-machine-default
一键获取完整项目代码
步骤 2:确认 wsldist 目录状态
$wsldist = "$HOME\.local\share\containers\podman\machine\wsl\wsldist"
 
# 检查目录是否存在
Test-Path $wsldist
 
# 如果存在,检查是否为空
if (Test-Path $wsldist) {
    $items = Get-ChildItem $wsldist -Force
    if ($items.Count -gt 0) {
        Write-Host "目录不为空,请先确认并清理内容:" -ForegroundColor Yellow
        $items | Format-Table Name, LastWriteTime
    } else {
        Write-Host "目录为空,可以继续" -ForegroundColor Green
    }
}
一键获取完整项目代码
执行 podman machine rm --force 后,这个目录通常要么为空,要么已被删除。如果目录中还有残留文件,说明之前有多个 machine 或删除不干净,需要手动确认后清理。

步骤 3:创建目标目录并建立 Junction
# 设置目标路径(按你的实际情况修改)
$targetPath = "I:\WSL\podman-wsldist"
 
# 创建目标目录
New-Item -ItemType Directory -Path $targetPath -Force
 
# 删除 C 盘的 wsldist(空目录或不存在的情况都处理)
if (Test-Path $wsldist) {
    Remove-Item $wsldist -Recurse -Force
}
 
# 创建 Junction
New-Item -ItemType Junction -Path $wsldist -Target $targetPath
一键获取完整项目代码
验证 Junction 创建成功:

$link = Get-Item $wsldist
Write-Host "路径: $($link.FullName)"
Write-Host "类型: $($link.LinkType)"
Write-Host "目标: $($link.Target)"
 
# 预期输出:
# 路径: C:\Users\love\.local\share\containers\podman\machine\wsl\wsldist
# 类型: Junction
# 目标: I:\WSL\podman-wsldist
一键获取完整项目代码
步骤 4:重建 Podman machine
podman machine init --rootful --disk-size 100 --cpus 12 --memory 2048
一键获取完整项目代码
预期输出:

Looking up Podman Machine image at quay.io/podman/machine-os:5.8 to create VM
Extracting compressed file: podman-machine-default-amd64: done
Importing operating system into WSL (this may take a few minutes on a new WSL install)...
操作成功完成。
Configuring system...
Machine init complete
To start your machine run:
        podman machine start
一键获取完整项目代码
如果这一步报错 could not create wsldist directory: mkdir ... Cannot create a file when that file already exists,说明你用了 SymbolicLink 而不是 Junction。删掉重新用 New-Item -ItemType Junction 创建。

 

步骤 5:启动并测试
podman machine start
一键获取完整项目代码
预期输出:

Starting machine "podman-machine-default"
API forwarding listening on: npipe:////./pipe/docker_engine
Docker API clients default to this address. You do not need to set DOCKER_HOST.
Machine "podman-machine-default" started successfully
一键获取完整项目代码
功能测试:

podman run --rm hello-world
一键获取完整项目代码
预期输出包含 Hello Podman World 的 ASCII 艺术图。

步骤 6:确认 vhdx 在目标盘
# vhdx 应该在 I 盘
Get-ChildItem $targetPath -Recurse -Filter "*.vhdx" |
  Select-Object FullName, @{N='SizeGB';E={[math]::Round($_.Length/1GB,2)}}
 
# C 盘应该只有 Junction,不占实际空间
Get-Item $wsldist | Select-Object FullName, LinkType, Target
一键获取完整项目代码
预期输出:

FullName                                                SizeGB
--------                                                ------
I:\WSL\podman-wsldist\podman-machine-default\ext4.vhdx    0.95
 
FullName                                                          LinkType Target
--------                                                          -------- ------
C:\Users\love\.local\share\containers\podman\machine\wsl\wsldist  Junction I:\WSL\podman-wsldist
一键获取完整项目代码
四、验证:以后重建 machine 是否自动落到 I 盘
Junction 方案的最大优势是一劳永逸。验证一下:

# 模拟以后的重建场景
podman machine stop
wsl --shutdown
Start-Sleep -Seconds 5
podman machine rm podman-machine-default --force
 
# 重新 init
podman machine init --rootful --disk-size 100 --cpus 12 --memory 2048
podman machine start
podman run --rm hello-world
 
# 确认 vhdx 仍在 I 盘
Get-ChildItem "I:\WSL\podman-wsldist" -Recurse -Filter "*.vhdx" |
  Select-Object FullName, @{N='SizeGB';E={[math]::Round($_.Length/1GB,2)}}
一键获取完整项目代码
无需任何额外操作,vhdx 自动落到 I 盘。

五、可能遇到的问题及解决
问题 1:podman machine start 报超时错误
Error: machine did not transition into running state: ssh error: machine not in running state
一键获取完整项目代码
原因: WSL2 systemd 冷启动到 sshd 就绪需要时间(某些机器上超过 20 秒),Podman 5.8.x 的等待超时可能不够。

解决: 等几秒,再执行一次 podman machine start。第二次它会检测到分发版已在运行,直接连上:

# 第一次可能超时
podman machine start
 
# 如果报错,等一会儿再试
Start-Sleep -Seconds 10
podman machine start
 
# 验证
podman run --rm hello-world
一键获取完整项目代码
问题 2:could not start api proxy since expected pipe is not available
原因: Podman Desktop 在后台运行,占用了 named pipe。

解决:

Stop-Process -Name "Podman Desktop" -Force -ErrorAction SilentlyContinue
Stop-Process -Name "win-sshproxy" -Force -ErrorAction SilentlyContinue
Stop-Process -Name "gvproxy" -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
podman machine start
一键获取完整项目代码
问题 3:WSL 内部无法联网
原因: Windows 侧的代理环境变量被 Mirrored 模式传递到 WSL 内部,但代理工具未运行。Podman machine 还可能自动生成 /etc/profile.d/default-env.sh 写入错误的代理配置。

排查:

wsl -d podman-machine-default -- env | grep -i proxy
一键获取完整项目代码
解决:

wsl -d podman-machine-default -- sudo rm /etc/profile.d/default-env.sh
一键获取完整项目代码
问题 4:wsl.conf 警告 Unknown key
wsl: Unknown key 'network.hostAddressLoopback' in /etc/wsl.conf:9
一键获取完整项目代码
原因: hostAddressLoopback 和 mirrorNetwork 是分发版内部 wsl.conf 中 WSL 不认识的键。Mirrored 模式应在 Windows 侧的 %USERPROFILE%\.wslconfig 中配置。

解决:

wsl -d podman-machine-default -- sudo sed -i "/hostAddressLoopback/d" /etc/wsl.conf
wsl -d podman-machine-default -- sudo sed -i "/mirrorNetwork/d" /etc/wsl.conf
一键获取完整项目代码
注意:PowerShell 中 sed 的多个 -i 表达式不能用分号连接(PowerShell 会将分号解释为命令分隔符),必须分两条命令执行。

六、原理深入:Junction vs SymbolicLink,以及为什么对父目录有效
Junction 和 SymbolicLink 对父目录做链接都能成功
经过实际测试验证:

链接类型    对父目录 wsldist    对子目录 podman-machine-default
Junction    ✅ 无条件成功    ❌ WSL 无法挂载 vhdx
SymbolicLink    ✅ 目标目录已存在时成功    未测试
两者对父目录做链接时,podman machine init + start + run --rm hello-world 全部通过,vhdx 正确落到目标盘。

为什么推荐 Junction 而非 SymbolicLink
虽然两者都能用,但 Junction 有一个关键优势:无条件可用。

SymbolicLink 在目标目录不存在时,Podman 的 machine init 会报错:

Error: could not create wsldist directory: mkdir ...\wsldist: Cannot create a file when that file already exists.
一键获取完整项目代码
这是因为 Go 语言的 os.MkdirAll 对 SymbolicLink 的处理方式不同——它检测到路径已存在但目标不可达,就报错了。而 Junction 在 NTFS 文件系统层面表现为一个真正的目录,os.MkdirAll 会正常通过。

简单说:

# ✅ Junction:不需要关心目标目录是否存在
New-Item -ItemType Junction -Path "<原路径>" -Target "<目标路径>"
 
# ⚠️ SymbolicLink:必须先创建目标目录
New-Item -ItemType Directory -Path "<目标路径>" -Force    # 少了这一步就会失败
New-Item -ItemType SymbolicLink -Path "<原路径>" -Target "<目标路径>"
一键获取完整项目代码
链接的工作层级
Junction 在 NTFS 文件系统层面生效。当一个路径中包含 Junction 节点时,NTFS 驱动在解析路径的过程中会透明地将 Junction 替换为目标路径,然后继续解析剩余部分。

访问路径:C:\...\wsldist\podman-machine-default\ext4.vhdx
                  ↑ Junction / SymbolicLink
NTFS 解析后:I:\WSL\podman-wsldist\podman-machine-default\ext4.vhdx
一键获取完整项目代码
上层程序(包括 Podman、WSL、Hyper-V)拿到的是解析后的实际路径,完全感知不到链接的存在。

为什么对子目录做 Junction 会失败
访问路径:C:\...\wsldist\podman-machine-default\ext4.vhdx
                              ↑ Junction
NTFS 解析后:I:\WSL\podman-machine-default\ext4.vhdx
一键获取完整项目代码
这种情况下,podman-machine-default 目录本身就是 Junction 入口。WSL 在导入分发版时,会将 BasePath 记录为 C:\...\wsldist\podman-machine-default(Junction 入口路径),然后 Hyper-V 尝试挂载这个路径下的 vhdx。但 Hyper-V 的文件操作可能不经过标准的 NTFS 路径解析,导致无法跟随 Junction。

而当链接在父目录时,podman-machine-default 子目录是一个真实的目录(在 I 盘上),WSL 记录的 BasePath 要么是解析后的实际路径,要么即使记录了 C 盘路径,经过 NTFS 解析也能正确到达。

验证方式
# 查看 WSL 注册表中记录的路径
$key = Get-ChildItem "HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss" |
  Where-Object { $_.GetValue("DistributionName") -eq "podman-machine-default" }
$key.GetValue("BasePath")
 
# 对比目标盘实际文件
Get-ChildItem "I:\WSL\podman-wsldist\podman-machine-default" | Select-Object Name, Length
一键获取完整项目代码
七、与其他方案的对比总结
对比维度    Junction 父目录    SymbolicLink 父目录    wsl --export/import    手动移动 + 改注册表
一次设置永久生效    ✅    ✅    ❌ 每次重建需重做    ❌ 每次重建需重做
对 Podman 透明    ✅    ✅    ⚠️ 可能需修复配置    ❌ 多处配置不同步
操作复杂度    低(3 条命令)    低(4 条命令,多一步建目标目录)    中(5+ 步骤)    高(手改注册表)
前置条件    无    目标目录必须已存在    无    无
数据安全风险    低(重建即可)    低(重建即可)    中(导出失败可能丢数据)    中
需要管理员权限    否    视 Windows 版本    否    是(改注册表)
支持保留现有数据    ❌ 需重建    ❌ 需重建    ✅ 可导出导入    ✅
结论: Junction 和 SymbolicLink 对父目录做链接都能成功,但 Junction 是最优解——无条件可用,不需要预创建目标目录,兼容性最好。如果必须保留现有数据,先用 wsl --export/import 迁移一次,然后再设置 Junction 防止未来重建时 vhdx 回到 C 盘。

八、完整操作速查
整个过程的核心命令(适合收藏后直接执行):

# === Podman Machine vhdx 迁移到 I 盘 ===
 
# 1. 清理
Stop-Process -Name "Podman Desktop" -Force -ErrorAction SilentlyContinue
podman machine stop
wsl --shutdown
Start-Sleep -Seconds 5
podman machine rm podman-machine-default --force
 
# 2. 创建 Junction(核心,一次设置永久生效)
$wsldist = "$HOME\.local\share\containers\podman\machine\wsl\wsldist"
$target  = "I:\WSL\podman-wsldist"              # ← 改成你的目标路径
 
New-Item -ItemType Directory -Path $target -Force
if (Test-Path $wsldist) { Remove-Item $wsldist -Recurse -Force }
New-Item -ItemType Junction -Path $wsldist -Target $target
 
# 3. 重建并验证
podman machine init --rootful --disk-size 100 --cpus 12 --memory 2048
podman machine start
podman run --rm hello-world
 
# 4. 确认 vhdx 在目标盘
Get-ChildItem $target -Recurse -Filter "*.vhdx" |
  Select-Object FullName, @{N='SizeGB';E={[math]::Round($_.Length/1GB,2)}}
一键获取完整项目代码
以后无论怎么 podman machine rm + podman machine init,vhdx 都会自动落到 I 盘。C 盘从此解放。

posted @ 2026-05-06 15:17  charygao1990  阅读(32)  评论(0)    收藏  举报