2016 年,WSL 1 用一套精巧的 DrvFs 方案让 Linux 进程几乎"零距离"触碰 NTFS——/mnt/c 下的读写延迟极低,体验接近原生。2019 年 WSL 2 换上完整 Linux 内核跑在 Hyper-V 虚拟机里,Linux 生态兼容性飞跃,但代价是:从 VM 内访问 Windows 分区,数据要穿越 VirtIO 设备链路、9P 协议栈、宿主侧服务进程,层层转发后才能抵达 NTFS。跨系统文件操作的速度骤然成为痛点。
微软没有回避这个问题,而是持续迭代优化这条路径。理解这段技术演进,对日常在 WSL 2 下开发的人有直接的操作价值。
DrvFs 为什么快,9P 为什么慢
WSL 1 的 DrvFs 是一个直接挂载在 NT 内核上的自定义文件系统驱动。Linux 进程发起的系统调用,被 WSL 1 的"Pico"进程层翻译成 NT 内核调用,文件请求几乎直通 NTFS,中间没有网络协议、没有跨进程通信、没有虚拟设备模拟。路径短,延迟自然低。
WSL 2 的架构变了:Linux 内核跑在独立 VM 里,它不再能直接调用 NT 内核。访问 /mnt/c 需要走这条链路:
Linux VFS → 9P client (内核模块) → VirtIO transport →
宿主侧 9P server (WSL 服务进程) → Win32 API → NTFS
9P 协议本身是 Plan 9 遗留下来的简单文件协议,每个操作都是一次完整的请求-响应往返。再加上 VirtIO 的虚拟设备队列、宿主侧服务进程的上下文切换,一次 stat 或 read 的延迟远超 WSL 1 时代。
实际体感:在 /mnt/c 下做 git status 或 npm install,WSL 2 可能比 WSL 1 慢 3-5 倍甚至更多,I/O 密集操作差距更明显。
微软的优化路线
微软的优化不是一步到位的,而是分多个阶段逐步收窄瓶颈:
协议层优化:早期 9P 实现每次操作都走完整往返,后来引入了批量操作和缓存机制,减少不必要的协议往返次数。比如目录遍历从逐条 readdir 改为批量返回。
VirtIO 传输层优化:调整 VirtIO 队列深度和批处理策略,让多个 9P 请求可以合并到一次 VM exit/entry 中完成,降低虚拟化切换开销。
宿主侧服务优化:9P server 进程的线程模型和 I/O 调度被重新设计,减少 Win32 API 调用路径上的阻塞和冗余转换。
缓存与元数据预取:对频繁访问的目录和文件元数据做缓存,避免反复跨 VM 边界查询 stat 信息——这对 git 类工具的加速效果显著。
这些优化在近几个 WSL 版本中逐步落地,用户端不需要额外配置,升级 WSL 即可受益。
实测:你的环境还慢吗
优化在推进,但具体到你的机器,速度取决于 WSL 版本、Windows 版本、磁盘类型等多重因素。跑一组简单基准测试,比凭体感判断靠谱。
下面这个脚本可以直接在 WSL 2 内运行,对比 Linux 本地文件与 Windows 跨系统文件的读写速度差距:
#!/usr/bin/env bash
# wsl-io-bench.sh — 在 WSL 2 内对比本地与跨系统文件 I/O
# 用法: bash wsl-io-bench.sh
# 依赖: dd, stat, time (bash 内建)
set -euo pipefail
LINUX_DIR="$HOME/bench_tmp"
WIN_DIR="/mnt/c/bench_tmp"
TEST_FILE="test_50mb.bin"
SIZE_MB=50
cleanup() {
rm -rf "$LINUX_DIR" 2>/dev/null || true
rm -rf "$WIN_DIR" 2>/dev/null || true
}
trap cleanup EXIT
mkdir -p "$LINUX_DIR" "$WIN_DIR"
echo "=== 生成 ${SIZE_MB}MB 测试文件 ==="
dd if=/dev/urandom of="$LINUX_DIR/$TEST_FILE" bs=1M count=$SIZE_MB status=none
echo ""
echo "=== Linux 本地文件系统 (ext4 on /) ==="
echo "--- 写入 ---"
dd if="$LINUX_DIR/$TEST_FILE" of="$LINUX_DIR/copy_linux.bin" bs=1M status=progress
echo "--- 读取 ---"
dd if="$LINUX_DIR/$TEST_FILE" of=/dev/null bs=1M status=progress
echo "--- stat (1000 次) ---"
time for i in $(seq 1 1000); do stat "$LINUX_DIR/$TEST_FILE" >/dev/null; done
echo ""
echo "=== Windows 跨系统文件 (/mnt/c, 9P + VirtIO) ==="
echo "--- 写入 ---"
dd if="$LINUX_DIR/$TEST_FILE" of="$WIN_DIR/copy_win.bin" bs=1M status=progress
echo "--- 读取 ---"
dd if="$WIN_DIR/copy_win.bin" of=/dev/null bs=1M status=progress
echo "--- stat (1000 次) ---"
time for i in $(seq 1 1000); do stat "$WIN_DIR/$TEST_FILE" >/dev/null; done
echo ""
echo "=== 清理完成 ==="
运行方式:
# 确保你在 WSL 2 环境
wsl.exe --list --verbose # 先确认版本是 2
# 在 WSL 内执行
bash wsl-io-bench.sh
关注几个关键数字:
- 写入速度:
/mnt/c通常比本地慢不少,但近版本差距在缩小 - stat 1000 次:这个最敏感,WSL 2 早期可能要数秒,优化后应降到 1 秒左右
- 读取速度:大块连续读取受影响较小,小块随机读取差距更明显
如果你的 stat 测试仍然很慢,说明你可能还在旧版本上——升级方法见下一节。
开发者的实操策略
优化在持续,但眼下如果你在 WSL 2 做日常开发,有几条原则能立刻改善体验:
项目放 Linux 本地文件系统。这是最直接的一条。把代码仓库 clone 到 $HOME 下而不是 /mnt/c,git 操作、node_modules 安装、编译构建的速度差异是肉眼可见的。用 Windows 侧的 IDE(VS Code Remote-WSL)远程编辑 Linux 侧文件,比反过来快得多。
# 推荐:项目放在 Linux 侧
cd ~
git clone https://github.com/your-org/your-project.git
# VS Code 会自动通过 Remote-WSL 连接到 Linux 侧
# 从 Windows 侧启动:
code .
大文件跨系统传输用 wslpath + cp,别用 9P 挂载点做流式处理。如果需要把 Linux 侧生成的大文件搬到 Windows 侧,先写到本地再一次性 copy 过去,比在 /mnt/c 上边写边处理快。
# 在 Linux 侧完成所有处理,最后一次性拷贝到 Windows
tar czf ~/archive.tar.gz ~/project/
cp ~/archive.tar.gz /mnt/c/Users/you/Desktop/
保持 WSL 和 Windows 更新。9P 和 VirtIO 的优化随 Windows 更新推送,旧版本不会自动获得。确认你跑在较新版本上:
# 在 PowerShell 中检查并升级
wsl --update
wsl --list --verbose # 确认 DISTRO 版本号为 2
# 查看 WSL 内核版本(优化与内核版本关联)
wsl -- cat /proc/version
必要时回退 WSL 1。如果你的工作场景重度依赖 /mnt/c 的 I/O 性能(比如必须直接操作 Windows 侧文件),且当前优化版本仍不够快,可以针对特定发行版回退到 WSL 1:
# 仅对特定发行版回退,不影响其他
wsl --set-version Ubuntu-22.04 1
WSL 1 的 DrvFs 在跨系统访问上仍然更快,代价是失去完整 Linux 内核兼容性(Docker、systemd 等不可用)。按需取舍。
眼下的取舍与未来的方向
WSL 2 的跨系统文件访问优化是一个典型的"架构换血后逐步补性能"的故事。换 VM 架构换来的是 Linux 内核完整性——Docker 能跑了,systemd 能跑了,eBPF 能跑了——但文件访问这条高频路径被拉长,需要逐层压延迟。微软已经在协议、传输、服务、缓存四个层面做了实质优化,近版本体感改善明显。
对开发者来说,务实的做法是:项目放 Linux 本地,大文件批量搬运,保持版本更新。这三条不需要等任何未来优化,今天就能执行。同时用基准脚本定期验证环境状态,避免在旧版本上白白忍受已经修复的慢。