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 盘从此解放。

浙公网安备 33010602011771号