玩 WSL2 的朋友应该都遇到过这个场景:某天打开资源管理器,发现C盘空间突然告急,一个个文件夹排查下来,最后定位到 AppData\Local\Packages 下面某个发行版的 ext4.vhdx 动辄十几二十个GB。更气人的是,你明明在 Linux 里面删了一堆没用的文件、卸载了几个大型软件包,Windows 这边的 VHD 文件体积却纹丝不动。这不是系统出了问题,而是 WSL2 的磁盘回收机制本身就是这么设计的——虚拟磁盘只涨不跌,想让它瘦下来,必须手动压缩 VHD,把空间还给 Windows。这篇文章就完整讲清楚这件事:为什么 WSL2 的虚拟磁盘会只增不减、压缩前需要做什么准备、diskpart 压缩 VHD 的完整操作链路,以及那些我在实操里踩过的坑和事后总结的维护习惯。无论你是刚装 WSL2 的新手,还是已经被磁盘占用折磨已久的老用户,这篇都值得从头看一遍。
1. VHD 为什么会只涨不跌:先搞懂 WSL2 的磁盘账本
1.1 虚拟磁盘的"删了不等于瘦了"
很多人第一次接触 WSL2 时会有个思维惯性:觉得 WSL2 和 Windows 之间就是共享文件夹的关系,删了 Linux 里的文件,Windows 的可用空间自然就回来了。这个理解在 WSL1 时代大体成立,但到了 WSL2,架构已经完全不同。
WSL2 本质上是一台运行在 Hyper-V 虚拟化层里的轻量虚拟机,整个 Linux 文件系统都存放在一个虚拟磁盘文件里,也就是我们在 Windows 上看到的 ext4.vhdx。这个文件采用动态扩展机制:Linux 里写入数据时,VHD 文件跟着字节数增长;但反过来,Linux 里删除数据时,VHD 文件却不会自动收缩。
原因在于文件系统层和虚拟磁盘层之间隔着两道账本。Linux 那边运行的是 ext4 文件系统,删除文件后,ext4 只会把对应的数据块标记为“空闲”,并不会主动告诉虚拟磁盘驱动“这些扇区可以清零了”。VHDX 格式虽然在设计上支持 trim 操作,但 WSL2 默认情况下的虚拟磁盘并不会时刻把这些释放出来的空间映射回 Windows 文件系统。一层层剥离后,最终的结果就是:你在 Linux 里删了 10GB 文件,Windows 上的 ext4.vhdx 文件大小还是老样子,那 10GB 空间继续被这个文件占着,成了“内部空闲、外部占用”的悬空空间。
可以类比成一个带锁的仓库:仓库管理员(ext4)知道哪些货架空了,但仓库大门(VHD 文件)始终为单位租着,出租方(Windows)不会主动来盘点里面到底还有多少东西。
1.2 谁在偷偷吃掉你的 C 盘:典型的 VHD 膨胀来源
明白了原理,再来看哪些行为最容易让 VHD 快速膨胀,就很有针对性了。根据我自己的观察和帮朋友排查的经历,绝大多数 VHD 体积失控都逃不出下面这几类来源:
- Docker 镜像和容器:这是最大头。Docker 的 overlay 文件系统会不断累积镜像层,
docker build产生的中间层、运行时挂载的卷,全部落在 WSL2 的虚拟磁盘里。很多人删了容器、跑了docker system prune,但镜像层里的历史数据并不会完全清干净。 - 包管理器和编译缓存:
apt下载的 deb 包、pip的 wheel 缓存、npm的缓存,还有 C/C++ 编译产生的中间文件,日常觉得每个都很小,架不住日积月累。 - 日志文件:journald 的系统日志、各种服务的 log,尤其是跑了好几个容器的情况下,日志写得特别勤快。
- 数据库和搜索索引:直接在 WSL2 里跑 MySQL、PostgreSQL、Elasticsearch,数据文件会长期占据大量空间。
- 个人资料文件:比如把大文件下载、解压到了 Linux 家目录,用完即弃,但虚拟磁盘已经为它们“胖”了起来。
所以当你发现自己的 ext4.vhdx 明显偏大时,别急着压缩,先想清楚里面到底装了什么。压缩只是物理回收手段,如果空间是被真实占用着,压完过阵子还是会涨回去,这属于治标不治本。
1.3 自查方法:你的 VHD 到底胖不胖、值不值得压
动手之前,先量化一下当前的状况。在 Windows 终端里跑:
wsl -l -v能看到所有发行版的名称和运行状态。然后打开资源管理器,进入%LOCALAPPDATA%\Packages,找到对应发行版的目录(比如 Ubuntu 的包名通常是CanonicalGroupLimited.Ubuntu...),进入LocalState文件夹,就能看到ext4.vhdx,右键属性查看“大小”和“占用空间”。
Linux 侧同样跑一个命令对比真实数据:
df -h /看/挂载点下的Used和Avail两个值。这时候会出现典型的“对不上账”:Windows 里ext4.vhdx实际大小是 30GB,Linux 里df -h显示已用只有 15GB。这中间的差额就是悬空空间,也是你压缩 VHD 能收回来的部分。
如果两个数字非常接近,说明虚拟磁盘里的空间基本都是实打实被占用的,压缩效果会非常有限。这种情况就别指望压缩能变出多少空间,先把该清理的东西清掉再说。反之,如果差额有 5GB 以上,那这篇文章后面写的每一步都值得照做。
2. 压缩前的三件套:备份、定位、彻底关机
2.1 不要嫌麻烦:wsl --export 备份的正确姿势
VHD 压缩本质上是对虚拟磁盘文件做一次物理级的整理和收缩,虽然正常操作下不会丢数据,但谁也不能保证中途不出现断电、文件系统校验出问题、或者手滑执行错命令之类的意外。所以无论你多赶时间,我都会建议先做一次完整导出。
wsl --shutdown wsl --export Ubuntu D:\wsl-backup\ubuntu-backup.tar注意第一个wsl --shutdown是必须的,导出和后续压缩操作都要求在 WSL 完全停止的状态下进行。第二个命令会把整个发行版的文件系统打包成 tar 文件,放到你指定的位置,建议放在非系统盘,避免备份本身又占用 C 盘空间。如果你平时把发行版装在了自定义盘符,比如 E 盘,导出时同样能处理,只是导出的目标路径写清楚就行。
导出需要多长时间、生成多大的 tar,取决于你虚拟磁盘里真实占用的数据量,不是 VHD 文件的大小。我自己的例子是真实数据只有 8GB,但 VHD 膨胀到了 25GB,导出大概花了两三分钟。如果你是几十GB的大型数据,记得给备份留足时间。
2.2 找到 ext4.vhdx 的真实位置与真实体积
刚才提到默认安装路径在%LOCALAPPDATA%\Packages下,但如果你当初用了wsl --import手动安装到指定目录,或者 Windows 商店应用有移动过,路径就会不一样。更稳妥的定位方法是在 PowerShell 里查询:
(Get-ChildItem -Recurse -Filter ext4.vhdx "$env:LOCALAPPDATA\Packages" -ErrorAction SilentlyContinue | Select-Object FullName, @{Name="SizeGB"; Expression={[math]::Round($_.Length/1GB,2)}})这会递归搜索所有发行版的 vhdx 文件并显示大小,一目了然。如果你的 WSL 发行版是自定义安装的,就直接去你安装时指定的目录找。
在这个阶段,顺便记录一下 VHD 当前的原始大小,压缩完成后对比用,你就能知道自己到底找回了多少空间。
2.3 让 WSL2 完全停下来的正确姿势
压缩 VHD 之前,WSL2 的所有发行版都必须处于完全停止状态。很多人以为关掉终端窗口就等于关机了,其实 WSL2 的后台进程还会继续驻留一小段时间,或者被某些配置带起来常驻。
最彻底的手段还是:
wsl --shutdown执行完之后可以再确认一下状态:
wsl -l --running如果输出为空,说明没有运行中的发行版了。这时候去任务管理器里看一眼,只要没有名为“Vmmem”或“WSL”的进程占着内存,就说明虚拟机基础设施已经停妥。如果还有残留进程,可以稍微等十几秒,或者用管理员身份在 PowerShell 里执行一次重启服务(不推荐,但确实有效):
net.exe stop LxssManager 2>$null; net.exe start LxssManager 2>$null当然这招会中断所有 WSL 相关进程,不到万不得已不用。
3. diskpart 压缩实操:从附加到分离的完整链路
3.1 attach readonly 到底为什么必须只读
核心操作环节来了。Windows 自带的diskpart工具就内置了compact vdisk命令,不需要安装任何第三方软件。你可以把它理解为 Windows 侧的磁盘管理员入口,但它以命令行方式操作,非常适合做这类脚本化、精准化的虚拟磁盘管理。
用管理员权限打开命令提示符或 PowerShell,依次输入:
diskpart进入 diskpart 交互模式后,先选中目标 VHD 文件:
select vdisk file="C:\Users\你的用户名\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu...\LocalState\ext4.vhdx"这里有一个很关键的细节:给路径加双引号,尤其是路径里含空格或中文时,少了引号 diskpart 往往解析不了。接着附加虚拟磁盘,这步让系统把 VHD 挂载成一个虚拟磁盘,但要注意必须带readonly参数:
attach vdisk readonly为什么不直接attach vdisk?compress 操作要求虚拟磁盘在附加状态下只读,目的是防止在压缩过程中有写入操作改变文件系统状态。WSL 已经停机,VHD 是静止的,只读附加是压缩的安全前提,同时也避免后续误操作对文件系统产生不可逆影响。
3.2 compact vdisk 的原理与执行过程
附加完成以后,执行压缩:
compact vdisk这条命令的原理值得展开说两句。它做的事情分两步:首先对虚拟磁盘里的空闲扇区做 trim 和清零处理,把空洞标记为可回收;然后物理地收缩 VHDX 文件,把尾部那些“全是洞”的块真正从文件里移除,让文件大小变小。整个过程可以理解成整理房间:先把你不要的东西挑出来堆在一起,再把这堆垃圾统一搬走,房间的占地面积自然就缩小了。
执行过程中,命令窗口只会显示“正在压缩…”,没有任何进度条,这是很多人心里没底的地方。短则一两分钟,长则十几分钟,具体看要处理的数据量和磁盘速度。我在一块普通 SATA SSD 上压缩过 25GB 的 VHD,实际耗时大约 5 分钟。整个过程里不要关闭窗口、不要重启电脑、不要额外去打开那个 vhdx 文件,给工具足够的时间把活儿干完。
压缩完成后,执行分离:
detach vdisk exit退出 diskpart。到这一步为止,一次完整的压缩操作就结束了。
3.3 验证结果:怎么确认空间是真的还给 Windows 了
完成后第一时间做两件事。先到刚才的LocalState目录,刷新一下文件夹,看ext4.vhdx的“大小”字段。理想情况下,数字应该已经明显缩小。以我上面那个例子,25GB 的 VHD 压缩完变成了 9GB 左右,回收了 16GB 空间到 Windows 侧。
然后重新启动 WSL,直接在 Windows 终端里输入wsl进入发行版,跑一次:
df -h /正常情况下,Linux 里的数据完好无损,已用空间保持原样,可用空间因为文件系统本身的元数据没变化也基本不变。VHD 文件缩小了,但 Linux 内部看到的一切还和压缩前一模一样——这就是虚拟磁盘回收空间后,文件系统完全无感的效果。
如果发现 WSL 启动异常,或者进入后报文件系统错误,先别慌。你还有备份:用wsl --unregister移除坏掉的发行版后,再用wsl --import把 2.1 节导出的 tar 恢复回来。不过这种情况极其少见,正常操作基本不会触发。
4. 别急着高兴:压缩失败和高占用通常栽在这几处
4.1 diskpart 各种报错的排错思路
实操中不少人会碰到报错,我把常见的几类整理成一张表,方便对照排查。
| 报错现象 | 大概率原因 | 解决方式 |
|---|---|---|
| “找不到指定的虚拟磁盘文件” | 路径拼写错误或引号没加 | 复制完整路径,确保加双引号 |
| “指定的虚拟磁盘不支持此操作” | 选中的不是 VHDX 文件,或 WSL 尚未完全关闭 | 确认文件扩展名,再跑一次wsl --shutdown |
| “无法附加” | 权限不足或文件被占用 | 以管理员身份运行 diskpart;检查资源管理器是否打开了该目录 |
| 压缩过程卡死很久不动 | 磁盘空间不足、VHD 文件过大或磁盘老化 | 尝试增加更多空闲空间再压缩;给足耐心等待 |
最容易被忽略的是磁盘可用空间不足。compact 操作虽然看起来是原地整理,但它在 trim 阶段会产生临时写入,如果 Windows 系统盘本身剩余空间不到 VHD 大小的 10%,极有可能中途失败报错。所以压缩前先看一眼 C 盘剩余空间,低于别急着压缩,先腾挪一些空间出来再动手。
4.2 为什么说只压缩VHD是治标不治本
如果你压缩完过了一两个月,发现 VHD 又膨胀回来了,这是很正常的事。真正的解法应该是两条腿走路:一方面做源头清理,把 VHD 里确实不需要的大文件干掉;另一方面定期做物理回收。压缩是个低风险但比较“笨”的操作,它不关心你里面装了什么,只负责把空闲扇区腾出来还回 Windows。
我自己见过最极端的例子:某个开发者朋友从来不在 Linux 里清 docker 镜像,半年下来 VHD 从 20GB 涨到 90GB,里面真实数据只有 10GB。压缩一次固然能瘦下来,但如果不改变使用习惯,几个月后照样打回原形。所以在压缩前、压缩中、压缩后都不妨追问一句:这些空间是被什么吃掉的?能不能从源头省下来?
4.3 Windows 11 的 sparse vhd 选项能省掉手动压缩吗
说到源头治理,这里有一个 Windows 11 上很好用的新特性值得提一嘴。新版本的 WSL 支持通过命令把 VHD 切换到稀疏模式:
wsl --manage Ubuntu --set-sparse true开启之后,虚拟磁盘会尝试在文件系统释放空间时同步收缩,减少了手动压缩的频次。但这不是银弹:稀疏模式在某些硬件环境下可能带来轻微性能损耗,尤其是频繁读写小文件的场景。如果你的 WSL2 主要跑数据库、编译任务,对磁盘 IO 比较敏感,我建议保持默认模式,手动定时压缩反而更稳妥。
另外注意,这个命令只能用于wsl --manage,并且仅在支持新版 WSL 的 Windows 11 系统上可用。Windows 10 用户还是老老实实走 diskpart 路线。
5. 源头治理:让 VHD 长期保持苗条的实操清单
5.1 先做大扫除再压缩:性价比最高的删除项
既然只压缩不清理等于白忙活,那我就把我每次压缩前都会过一遍的清理清单列出来。这些操作全部在 Linux 侧执行,安全、见效快:
# 1. APT 缓存与无用包 sudo apt autoclean sudo apt autoremove -y # 2. Docker 残留镜像、无主卷、构建缓存 docker system prune -a --volumes docker builder prune -a # 3. journald 日志按时间截断 sudo journalctl --vacuum-time=7d # 4. pip 与 npm 缓存 pip cache purge npm cache verify # 5. 用户级缓存目录,重点看看有没有旧版本工具留下的历史数据 du -sh ~/.cache 2>/dev/null tail -n 1 ~/.bashrc > /dev/null以上五类清理做完,一般能少个 2GB 到 10GB 不等,取决于你平日的使用习惯。清理完成后跑一遍:
df -h /确认空间确实释放出来了,然后再去 Windows 侧执行 diskpart 压缩。这样压缩效率更高,回收空间更明显,也能尽量避免那种“压缩完没多少变化”的落差感。
5.2 定时巡检与自动化的偷懒方案
最后聊聊维护节奏。压缩 VHD 不需要天天做,那太折腾了,但也不需要等到 C 盘满了再临时抱佛脚。我现在的习惯是每两个月做一次例行巡检:
- 看一眼
ext4.vhdx的实际大小。 - 进 Linux 跑
df -h /对比差额。 - 如果差额超过 5GB,就执行一遍上面的清理加压缩流程。
如果想更省事一点,你们完全可以把 diskpart 那几步写成一个脚本文件,双击执行就行。核心逻辑就是select、attach、compact、detach,脚本里把路径和引号处理好即可。不过要注意,脚本必须以管理员身份运行,否则 diskpart 会因为权限不足直接拒绝执行。
另外一个偷懒技巧:尽量别把所有发行版都装默认 C 盘。如果你经常使用 WSL2,可以考虑重新导入到 D 盘或其他空间充裕的盘符,从源头避免 C 盘被 WSL2 拖垮。自己装的新发行版直接用wsl --import指定路径,之前已经装在 C 盘的,也可以导出后用wsl --import迁移到新位置,不过这是另一个话题了。
结合我自己的经验,压缩 VHD 操作本身不难,真正难的是养成定期维护的习惯。第一次做的时候花个半小时把流程跑通,之后每两个月花十几分钟例行清理压缩,C 盘基本上再也不会被 WSL2 的虚拟磁盘悄悄吃空。至少在我自己的机器上,这套流程跑了两年,C 盘空间一直稳稳当当,再也没出现过因为 ext4.vhdx 膨胀而手忙脚乱的情况。