ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

WSL2虚拟磁盘爆满?fstrim+diskpart压缩ext4.vhdx释放C盘空间实战

WSL2虚拟磁盘爆满?fstrim+diskpart压缩ext4.vhdx释放C盘空间实战

兄弟们,如果你也是一个日常靠 WSL2 跑开发的人,那 Windows 的 C 盘体积告急这件事,迟早会撞到你头上。罪魁祸首通常是一个不断膨胀的虚拟硬盘文件:ext4.vhdx。你在 WSL2 里删了多少项目、清了多少缓存,Windows 这边看到的 vhdx 文件体积却纹丝不动,C 盘可用空间继续见底。这种“只涨不缩”的坑,Windows 自带工具就能解决,核心就一句话:先让 VHD 知道哪些块没用了,再用 diskpart 的 compact vdisk 把那些空洞挤掉,把空间真正还给 Windows。

这篇内容是完整的实操记录加避坑指南,覆盖原理、压缩前预清理、diskpart 具体步骤、fstrim 与零填充备选方案、发行版迁移和自动脚本。适合被 C 盘空间不足逼到崩溃的 WSL2 用户,也适合第一次听说“VHD 要手动压缩”的新手,照着做基本能一次性搞定。

1. 为什么 WSL2 会把你的 C 盘悄悄塞满

1.1 VHD 的“只涨不缩”机制

WSL2 本质上是一个轻量级虚拟机,你的整个 Linux 文件系统都存在一个虚拟硬盘文件里,Windows 侧默认就是这个ext4.vhdx。它属于动态扩展型 VHD,创建的时候体积很小,随着你在 WSL 里安装软件、拉取仓库、编译代码、跑容器,文件会逐渐长大,默认上限一般是 256GB 这个量级。

要理解它为什么“只涨不缩”,得看一眼 VHD 的内部结构。VHD 内部维护了一张块分配表,记录哪些块被写过、哪些块是空白的。当你在 Linux 里创建一个文件,ext4 文件系统会占据一些盘块,VHD 层就把这些块标记为“已使用”,文件大小随之增长。当你删除 Linux 里的文件时,ext4 只是把元数据中的记录抹掉,告诉系统“这块区域以后可以复用了”,但 VHD 层根本感知不到这个动作,它只知道这些块曾经被写入过,于是继续保留它们。

这里用个生活类比可能更好理解:VHD 就像个玻璃罐,你往里倒过一堆沙子,移动它的时候必须按曾经装满沙子的重量来抬;后来你把沙子倒掉了,罐子确实变轻了,但它的物理体积还停留在曾经装满沙子的刻度上。要在 Windows 端把这个罐子 “缩回原来的形状”,就得手动压一下。

所以,你在 WSL2 里删掉几十 GB 数据后,C 盘可用空间不变,并不是清理没用,而是清理产生的“空洞”还留在 VHD 内部,没有真正释放给宿主机磁盘。

1.2 那个让你以为“清理了”却没变大的真相

我经常被问到类似问题:我在 WSL 里跑了一遍docker system prune,又清了apt缓存,为什么回到 Windows 一看,C 盘可用空间还是老样子?

原因就是上一节说的:清理动作发生在 Linux 文件系统层,而 VHD 文件是 Windows 层的一个文件,它记录的“最大曾用体积”不会因为内部数据减少而自动变小。VHD 站在文件系统的外面,看不到 ext4 内部哪些块已经空了,只知道这些块曾经是有效数据。

所以在 WSL2 的语境里,“压缩 VHD”并不是双击一个压缩软件那么简单,而是需要两步配合:

  • 第一步,让 VHD 内部没有数据的那些块变成“全零”或打上“可回收”标记,这一步通常靠fstrim或“零填充”完成。
  • 第二步,用diskpart的compact vdisk命令,让 VHD 物理文件真正截断这些无用的块,从而缩小文件体积。

如果你跳过第一步直接执行 compact,虽然也能回收一小部分空间,但回收率会很低。因为那些被删除数据占据过的区域,在 VHD 眼里仍然像“有效数据”,它不敢随便释放。这也是为什么网上很多人压缩 VHD 后发现效果不明显,多半是少了 fstrim 这一步。

1.3 关键概念:稀疏文件与空间回收

VHD 文件本质上是一个稀疏文件,学术一点的叫法是 Sparse File。它的特点是:从逻辑上看文件很大,但物理上只给真正写入过数据的区域分配空间。你在任务管理器或磁盘属性里看到的ext4.vhdx文件大小,通常代表它的逻辑形态,而磁盘实际占用可能略小一些,但两者都不会自动缩减。

compact 操作到底做了什么事?它会扫描 VHD 内部被标记为“全零”的区块,然后把这些全零区块从文件的物理布局中移除,让 VHDX 的逻辑结构缩水,最终磁盘上的文件大小也跟着变小。这恰恰解释了为什么压缩前要做 fstrim 或零填充:它们本质上都是让那些没用的块回到“全零”状态,给 compact 创造回收条件。

这套机制理解到位之后,后面操作里的每个命令就不再是死背步骤,而是有明确目的的执行动作。接下来就可以动手了。

2. 动手压缩前,先把准备工作做扎实

2.1 先看一眼你的 VHD 到底多大

压缩前,最好先确认自己的 VHD 文件到底有多大、放在哪个目录。WSL2 的发行版数据默认在用户目录的AppData\Local\Packages下,不同发行版的文件夹名不同,但里面都会有一个LocalState\ext4.vhdx。在 PowerShell 里执行以下命令,可以一次性列出所有 WSL2 发行版的 VHD 文件:

Get-ChildItem "$env:LOCALAPPDATA\Packages\*\LocalState\ext4.vhdx" | Select-Object FullName, @{Name='SizeGB'; Expression={[math]::Round($_.Length / 1GB, 2)}}, LastWriteTime

FullName 是完整路径,SizeGB 是文件占用的逻辑大小,LastWriteTime 能帮你认出哪个发行版最近在用。如果你装了多个发行版,你会看到多个 VHD 文件,压缩时要逐个确认,别搞混了目标。

如果你还想知道每个发行版对应的数据目录到底在哪里,可以再深入一步,从注册表读取 WSL 的配置信息:

Get-ChildItem "HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss" | ForEach-Object { $p = Get-ItemProperty $_.PSPath [PSCustomObject]@{ Name = $p.DistributionName; BasePath = $p.BasePath } } | Format-Table -AutoSize

输出结果里的 BasePath 就是这个发行版的数据目录,对应路径下的ext4.vhdx就是它的虚拟磁盘。这个方法在管理多个发行版时尤其好用,能精准定位。

2.2 在 WSL 内部先做一次有效清理

压缩的收益上限,取决于你能在 Linux 文件系统里“腾出”多少空间。所以执行压缩之前,强烈建议先进入 WSL 做一次彻底瘦身。这一步不是必须的,但效果差异非常大。拿我自己的例子说,光是在 WSL 里清理完 Docker 构建缓存和 apt 缓存,就空出了将近 20GB。

清理优先瞄准几个空间大户:

  • 包管理器缓存。Debian 系发行版用sudo apt clean清掉/var/cache/apt/archives下的 .deb 文件,Red Hat 系则用sudo dnf clean all。
  • Docker 相关缓存。用docker system prune -a -f删除所有未被容器使用的镜像、网络、构建缓存。docker builder prune -f可以单独清掉 BuildKit 构建缓存,通常也能释放几个 GB。
  • 日志文件。系统日志目录/var/log积累久了很可观,用sudo journalctl --vacuum-time=7d只保留最近 7 天日志。
  • 用户缓存目录。~/.cache/pip、~/.npm、~/.cache这些经常塞满一堆临时文件,可以按需删除。
  • 各类构建产物。node_modules、target、build、dist这种目录,通常重新拉取/编译就能恢复,不要犹豫,删。
  • 不再使用的依赖。sudo apt autoremove --purge可以把卸载应用后残留的依赖包一并清走。

清理完以后,别急着做下一步,先退到 Windows 侧,记录一下当前 C 盘的可用空间,这样压缩完成后才能准确对比。

2.3 备份这一步真的不能省

任何涉及磁盘压缩的操作都有一定风险,diskpart 的 compact 虽然是很成熟的功能,但如果 VHD 文件本身已经处于不健康状态,或者你的 WSL 发行版用了什么特殊配置,压缩时可能会出现意外。所以我每次真正动手前,一定会先给 WSL2 做一次完整导出。

导出命令是wsl --export,导出产物是一个完整的 tar 归档,之后可以用wsl --import原样恢复整个 Linux 文件系统,包括已安装的软件和配置。使用方法:

wsl --export Ubuntu D:\wsl-backup-2025.tar

把Ubuntu替换成你的发行版名称,后面的路径改成你存放备份的位置。tar 文件的大小差不多等于 WSL 内部实际数据量,如果 WSL 里装了大量数据,导出会花一些时间,这很正常。

有些人图省事,会直接复制ext4.vhdx文件来做备份,但我不建议这么干。第一,WSL 若处于运行状态,VHD 文件经常被锁定,复制会失败;第二,就算关闭 WSL 后复制成功,如果 VHD 本身有问题,备份的也是一个带病快照。tar 导出至少能保证文件系统层的一致性,出问题时恢复起来更可靠。

备份文件可能占不少空间,压缩成功且后续运行稳定后,就可以删掉或转移到移动硬盘了。

2.4 找一个不怕重启的时间窗口

压缩过程中,WSL2 必须完全关闭。注意,不只是你当前在用的那个发行版,是所有发行版都会停掉,因为wsl --shutdown是全局关闭命令。这意味着你的开发容器、后台任务、长编译任务都会中断。有未提交代码或者有任务在跑的时候,千万别手痒直接上。

此外,压缩时 diskpart 会把 VHD 挂载为只读设备,虽然安全,但整个过程还是建议选在一段不着急用电脑的时间。压缩几十 GB 的 VHD 少则几分钟,多则二十分钟,期间最好不要同时开大量大型软件,免得资源不够导致操作卡顿或失败。

万一中途断电或者被强制关闭窗口,虽然概率低,但确实有可能让 VHD 进入不一致状态。这也就是为什么我特别强调先备份,没有退路的情况下动手,等于赌运气。

3. 核心实操:diskpart 压缩 VHD 全流程

3.1 推荐先做 fstrim,让空间可回收

前面说过,compact 只能回收“全零块”,所以压缩前先得让空闲块变成可回收状态。WSL2 里最直接的方式是执行 fstrim:

sudo fstrim -v /

fstrim 会向底层块设备发送 TRIM 指令,告诉虚拟磁盘“这些块已经不再使用了”。Windows 侧的 VHD 驱动收到这个信息之后,相关区域就能在 compact 阶段被回收。现代 WSL2 的内核和 virtio 驱动基本都支持这个操作,执行完毕后会看到类似输出:

/: 14.2 GiB (152xxxxxx bytes) trimmed

这意味着系统成功回收了 14.2GB 的空闲块。

如果命令报错,提示fstrim: /: the discard operation is not supported,说明当前环境的磁盘驱动不支持 TRIM,那就得换到第 4.1 节的零填充方案。我实测下来,较新的 WSL 版本基本都支持 fstrim,但某些自编译内核或者旧 Windows 版本上确实会遇到不支持的情况。

还有一点想提醒:有些文章建议在/etc/fstab里给根分区加discard参数,让删除文件时自动 TRIM。这个思路在 WSL2 里不一定有效,而且 TRIM 本身也有性能开销。更划算的做法是平时不开启自动 discard,只在压缩前手动执行一次 fstrim,干净利落。

3.2 关闭 WSL 并定位 ext4.vhdx

fstrim 执行完成后,回到 Windows 的 PowerShell 里,把 WSL 完全关闭。不要用wsl --terminate <发行版名>去单独关闭某一个发行版,因为那不能彻底释放 VHD 文件的锁定。要使用全局关闭:

wsl --shutdown

执行后稍微等几秒,可以用wsl -l -v确认所有发行版都已经是 Stopped 状态。然后回到第 2.1 节查到的 BasePath,确认 target 的ext4.vhdx路径。我在实际操作中会把路径记录到一个文本文件里,避免待会儿在 diskpart 里手打时输错。

假设路径是类似这样的:

C:\Users\<用户名>\AppData\Local\Packages\xxxxx\LocalState\ext4.vhdx

记下来,下一步要用。

3.3 挂载只读并执行 compact vdisk

在开始之前,务必以管理员身份打开 PowerShell。在有权限的窗口输入diskpart,进入交互模式后,逐条执行以下命令:

select vdisk file="C:\Users\<用户名>\AppData\Local\Packages\xxxxx\LocalState\ext4.vhdx" attach vdisk readonly compact vdisk detach vdisk exit

逐条解释一下:

  • select vdisk:让 diskpart 定位到需要操作的 VHD 文件。路径必须用双引号包起来,如果路径里带空格,漏了引号会直接报错。
  • attach vdisk readonly:以只读方式把 VHD 挂载为 Windows 里的一个磁盘设备。readonly参数是核心,它告诉 Windows 我们只做内部整理,不往盘里写数据。如果不加只读参数,Windows 有可能向 VHD 写入一些附加元数据,反而弄脏文件系统。
  • compact vdisk:真正的压缩命令。diskpart 会开始扫描 VHD 内部的全零块并移除它们。执行时屏幕上可能长时间没有输出,看起来像卡死,其实它正在干活,一定要等。
  • detach vdisk:压缩完成后卸载虚拟磁盘,让 VHD 恢复常规状态。
  • exit:退出 diskpart。

如果一切顺利,最后能看到类似DiskPart successfully compacted the virtual disk file的提示。如果提示需要提升权限,说明 PowerShell 不是管理员模式;如果提示找不到虚拟磁盘提供程序,多半是路径写错了。

3.4 验证压缩效果

压缩完成后,先别急着启动 WSL,回到普通 PowerShell 看一眼 VHD 体积:

Get-Item "C:\Users\<用户名>\AppData\Local\Packages\xxxxx\LocalState\ext4.vhdx" | Select-Object @{Name='SizeGB'; Expression={[math]::Round($_.Length / 1GB, 2)}}

对比压缩前的数据,你会看到明显的缩减。我印象最深的一次,95GB 左右的 VHD 在 fstrim 加 compact 之后直接掉到 41GB,相当于 C 盘多出 54GB 可用空间。回收多少取决于你在 2.2 节清理了多少数据,以及 fstrim 是否执行成功。

确认文件变小后,再启动 WSL:

wsl -d Ubuntu

进去之后执行:

df -h /

你会看到 Linux 侧的文件系统大小、已用空间和压缩前保持一致,数据没有丢,C 盘可用空间却实打实变大了。到这里,一次完整的压缩就结束了。

4. 当 fstrim 不生效或者想更彻底时的备选方案

4.1 零填充法:老派但直观

如果你的环境不支持 fstrim,或者压缩后回收率不太理想,那就用经典的“零填充法”。原理很粗暴:在 WSL 里创建一个内容全为零的大文件,把文件系统所有空闲块都填一遍,再删除该文件。这样,即使 fstrim 做不到的“全零块”标记,也被我们用最笨的方法搞定了。

操作是这样的,进入 WSL 并执行:

cd / sudo dd if=/dev/zero of=/zero.tmp bs=1M status=progress

这条命令会不停往根目录写全零数据,直到磁盘空间耗尽,出现No space left on device的报错。这时按下 Ctrl+C 中断,然后删除临时文件:

sudo rm -f /zero.tmp

不过我不太推荐真的把磁盘填到 100%,因为 WSL 系统本身还要写运行日志、临时文件,磁盘在完全填满的瞬间可能出现异常。我实际操作时,会在根目录还剩 2~3GB 空间时就手动中断 dd,再删掉文件。虽然会有一小部分空间没被零覆盖,但对最终压缩结果影响很小。

零填充的缺点是速度慢。填充几十 GB 空间,就要写入等量的零数据,在 SSD 上尚可接受,机械硬盘上会让你怀疑人生。它的优点是兼容性极强,不需要任何系统组件支持,只要 WSL2 能启动就能用。填充完成后,记得先wsl --shutdown,再走一遍 diskpart compact 流程。

4.2 zerofree 工具的正确打开方式

如果你对“填满磁盘”这种操作有点心理负担,还有另一个小工具叫 zerofree,专门处理 ext2/ext3/ext4 文件系统的空闲块置零。它在普通 Linux 环境里很好用,但在 WSL2 里比较尴尬,因为它要求根文件系统不能处于挂载状态,否则会有写冲突。

标准的做法是:关闭 WSL,从另一个 Linux 环境启动,手动挂载 ext4.vhdx 中的文件系统,再对对应设备执行sudo zerofree /dev/sdb1之类的命令。操作链条长,而且对设备号判断要求很高,一旦弄错目标,轻则压缩失败,重则数据受损。如果你不是对 Linux 文件系统有足够把握的人,我更推荐继续用 fstrim 或零填充法,成本低得多。

想知道自己的根文件系统设备名,可以在 WSL 里执行lsblk或df -h。多数情况下 WSL2 的虚拟磁盘对应/dev/sdb或/dev/sdb1,但这个信息仅供参考,具体以实际输出为准。

4.3 永久迁移 WSL 到其他磁盘释放 C 盘

压缩 VHD 是一时的空间回收,如果 C 盘本身空间紧张,或者 WSL 数据量已经非常大,那更彻底的办法是把整个发行版迁到其他磁盘,以后 VHD 再膨胀也不会挤压系统盘。

迁移思路还是用的wsl --export和wsl --import组合拳。完整流程如下:

wsl --export Ubuntu D:\backup\ubuntu.tar wsl --import Ubuntu2 D:\WSL\Ubuntu D:\backup\ubuntu.tar wsl --unregister Ubuntu

导入时建议使用一个新名字,比如上面的Ubuntu2。确认迁移后的系统一切正常,再执行wsl --unregister Ubuntu删除旧发行版,这样就算迁移中新系统有问题,旧系统也还在。

有一点要提醒:wsl --import导入的发行版默认用户会变成 root。需要手动设置默认用户,不同发行版命令略有不同,常见类似:

ubuntu2.exe config --default-user <用户名>

设置完后重新打开 WSL,就能恢复到熟悉的开发环境。这个方案的本质是把 VHD 放到其他盘,彻底摆脱 C 盘容量焦虑。如果你被空间问题困扰已久,我非常建议在压缩完这次之后认真考虑做一次整体迁移。

5. 我踩过的坑和要提醒你的事

5.1 磁盘被占用的排查

diskpart 在select vdisk或attach vdisk readonly的时候,偶尔会报错提示“虚拟磁盘被占用”或“无法访问”。最常见的根因就是没有执行wsl --shutdown,或者某些基于 WSL2 的后端进程还在访问 VHD。

排查思路很直接:

  • 回到 PowerShell,执行wsl -l -v,确认所有发行版都处于 Stopped 状态;
  • 如果显示仍在运行,再执行一次wsl --shutdown并等待几秒;
  • 如果还不行,打开任务管理器,检查Vmmem、vmwp.exe这类与 WSL 相关的进程是否已经退出。

特别说一下 Docker Desktop。它默认基于 WSL2,即使你执行了wsl --shutdown,Docker Desktop 可能很快又把某个发行版拉起来。所以压缩前最好先退出 Docker Desktop 和其他依赖 WSL2 的应用,再执行 shutdown,否则 VHD 会被重新占用,diskpart 自然操作不了。

5.2 压缩过程意外中断怎么办

我经历过一次压缩过程中不小心关掉窗口的情况,当时 VHD 文件尺寸没变化,但 WSL 启动后一直卡在某个加载阶段。处理办法是:再次执行wsl --shutdown,然后用 diskpart 重新执行attach vdisk readonly和detach vdisk,把残留的挂载状态清理干净,再启动 WSL 就恢复正常了。

如果这样还救不回来,就进入备份恢复流程。先把出问题的发行版删掉:

wsl --unregister Ubuntu

再用备份重新导入:

wsl --import Ubuntu D:\WSL\Ubuntu D:\wsl-backup-2025.tar

这套流程虽然折腾,但至少数据不丢。所以我每一次都强调:压缩之前,先导出 tar 备份,给自己留条后路。

5.3 压缩后速度会变慢吗

压缩 VHD 会对 WSL2 的读写性能产生什么影响?这个问题经常被人问起。理论上,compact 只是把 VHD 文件截短,文件系统层面的布局没有变,虚拟机运行时的随机读写路径也没有变,所以不会明显变慢。

相反,压缩前做 fstrim 还有一点额外好处:空闲块被标记为可回收后,后续 WSL2 写入新数据时,VHD 可以直接复用这些块,而不需要不断扩展文件大小。这意味着压缩操作不仅帮你回收了 Windows 空间,还可能让 WSL2 在后续频繁写入时的表现更稳定。

当然,这不代表一劳永逸。VHD 文件在压缩后,等你再次往 WSL 里装大量数据,它又会慢慢变大。这是虚拟磁盘的正常特性,不用每次心疼,定期压缩一次就行。

5.4 想自动化:定期压缩脚本怎么写

如果你的开发机常年在跑大量项目,WSL2 数据增长很快,几个月不管 VHD 就能膨胀到几十 GB。写一个自动压缩脚本,放到 Windows 计划任务里,以后就不用手动干预了。

脚本核心逻辑就是三步:进入 WSL 执行 fstrim,关闭所有发行版,再用 diskpart 执行压缩。fstrim 环节可以直接通过 wsl 命令调用:

wsl -d Ubuntu -- sudo fstrim -v /

然后在 PowerShell 里继续:

wsl --shutdown $vhdx = "C:\Users\<用户名>\AppData\Local\Packages\xxxxx\LocalState\ext4.vhdx" $script = @" select vdisk file="$vhdx" attach vdisk readonly compact vdisk detach vdisk "@ $script | diskpart

因为 diskpart 不直接支持 PowerShell 变量,这里用 here-string 把命令文本拼出来,再通过管道喂给 diskpart,效果和逐条输入完全一样。跑完后 WSL 会处于关闭状态,如果你希望脚本执行完自动把开发环境拉起来,可以再补一句wsl -d Ubuntu或者直接执行对应的发行版启动器。

计划任务建议设置为每月一次,放在深夜或午休时间。如果上次压缩后 VHD 变化不大,这个过程会非常快;如果 VHD 又膨胀了不少,脚本会自动帮你处理。这套“定期压缩”配合第 4.3 节的“迁移到其他磁盘”,基本可以让 C 盘空间问题彻底进入自动化管理。


我自己第一次把 VHD 从 95GB 压到 41GB 的时候,心里想的是:为什么没人早点把这件事讲清楚。C 盘空间告急是很多 WSL2 用户的共同痛点,可大家的应对方式往往是重装系统、疯狂卸载软件、给 C 盘扩容,很少有人意识到问题藏在虚拟硬盘的“历史高水位”里。其实只要掌握了 fstrim 加 diskpart compact 这套组合拳,后续再遇到空间紧张,我都是几分钟搞定。文章里的步骤、坑和自动化脚本,都是实际验证过的,希望帮你把存进 VHD 的冤枉空间全拿回来。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进