DreamCity

愿你有一天,能与你最重要的人重逢

TL;DR

重新编译 WSL 2 使用的 Linux 内核,在其中加入 USB 存储设备支持即可。


最近有个关系蛮不错的同学说想借走我的树莓派打电赛用,想了想反正树莓派放在自己这儿也是吃灰,就干脆借出去了。

当然,借出去之前我肯定是要把 SD 卡上的东西先自己备份一遍的。不过我懒得手动整理文件再逐个打包备份,就想直接用 Linux 的 dd 工具直接把整张 SD 卡读出来,到时候恢复起来也省事,直接整个镜像写进去就完事,系统都不用重新刷。然而 Windows 上没有 dd(其实是有的移植版的,我到后来才发现…),用虚拟机又嫌太麻烦,就干脆用了 WSL。

从 WSL 2 开始,WSL 的实现方式从 WSL 1 的 API 转译换成了基于 Hyper-V 的虚拟机。这能带来更接近原生 Linux 的用户体验,比如可以用 NVIDIA 显卡的 CUDA 核心来训练人工智能模型、可以运行 Docker(Windows 上的 Docker 的本质还是在虚拟机里跑 Linux 再跑容器…)、可以实现图形化界面(WSLg),但也带来了 WSL 1 所没有的一些问题,比如硬件透传:虚拟机的硬件环境都是宿主机虚拟化出来的,一般情况下宿主机上的硬件在虚拟机中是看不到的。

个人观点:WSL 1 还是 WSL 2?

对我来说,WSL 的意义在于 Linux 环境和 Windows 环境的互操作性。本质上,我使用 WSL,只是希望我能够在享受 Windows 生态的便利性的同时,也能拥有我熟悉的在 Linux 环境下做开发的那一套的便利性(大人我全都要 XD)。所以对我来说,WSL 1 和 WSL 2 的区别并不大,如果没有什么特殊需求的话,这两个我都是能用下去的。但是,既然 WSL 2 能提供更完整的 Linux 体验,我为什么要闲着蛋疼去用一个转译来的兼容层呢?

我的电脑内建的读卡器是走 USB 的。因为 WSL 2 的本质是虚拟机,我的读卡器在 WSL 2 内是不可见的。虽然微软官方在 《比较 WSL 1 和 WSL 2》 这篇文档和 WSL 的 FAQ 中提到,WSL 2 目前还不支持 USB 设备,但在 《连接 USB 设备》 这篇文档中,微软官方也提到了,可以使用 usbipd-win 这个项目来连接 USB 设备到 WSL。(其实这篇文档在我开始尝试在 WSL 2 中连接 USB 存储设备的半个月前才发布,所以…我其实算是第一批吃螃蟹的几个人之一?)

usbipd-win 是 Windows 上的一个对 USB/IP 协议的实现,而 USB/IP 是用来将 USB 封装成 TCP/IP,然后通过网络传输的一个项目,理论上有网的地方就能用。有很多人尝试过使用树莓派等设备配合 USB/IP 将有线键鼠转为无线键鼠这种操作(不过这么做的操作延迟不会很大吗…),也有大佬尝试过通过 USB/IP 将 USB 设备透传入 WSL 2 的(链接放在这篇博文底部的「参考链接」中了),不过彼时微软尚未在 WSL 使用的 Linux 内核中加入 USB/IP 所需的 VHCI 控制器支持,想这么做还得手动编译内核才行。然而,微软已经在 e445d061c3989a6990180873efc74520b385ca52 这个提交中加入了 VHCI 控制器的支持,文档也发出来了,也就意味着我们可以直接使用 USB/IP 在 WSL 2 中连接 USB 设备了。

但是,就在我兴冲冲地根据微软官方的文档将读卡器透传入 WSL 2 后,我却在 /dev 找不到读卡器的设备文件(device file),而 lsusbdmesg 打印出的内容又显示 WSL 认到了我的读卡器,这属实令我迷惑。我询问了几位日常使用 Linux 的朋友,都表示没有头绪。我甚至怀疑是不是微软没有在 WSL 2 的 Linux 内核中加入 USB 存储设备的驱动,没想到这居然让我蒙对了——搜寻相关资料的时候,看到了前文提到的手动编译内核以支持 USB/IP 的大佬的博文,其中提到了 USB 存储设备的支持是可以在内核配置文件中启用和禁用的。果然,在 WSL 2 的 Linux 内核配置文件 中,我看到了:

1
# CONFIG_USB_STORAGE is not set

好嘛!不愧是你,巨硬,还是这种「什么都能做但又什么都做不好」的风格。

这样我们的解决方案也就很明确了:再手动编译一个 Linux 内核,启用 USB 存储设备的支持。(怎么觉得比我直接用虚拟机还麻烦…不过既然已经到了这步了,这条路就继续走下去吧)

确保 WSL 中安装好了 build-essentialflexbisonlibssl-devlibelf-devdwarves 这几个包,然后从 WSL 2 的 Linux 内核的仓库 clone 一份最新的内核源码,执行:

1
2
3
4
# 注意要指定 KCONFIG_CONFIG 参数
# 内核配置文件的默认位置是源码目录下的 .config,而 WSL 2 的内核配置文件在 Microsoft/config-wsl,如果不手动指定的话会载入默认配置文件
# 当然你也可以手动复制一份内核配置文件出来,或者进 menuconfig 之后再手动加载
make menuconfig KCONFIG_CONFIG=Microsoft/config-wsl

来用 menuconfig 这种在 CLI 里画 GUI 的工具来编辑内核配置文件(头铁的技术力比较高的大佬也可以手动编辑位于 Microsoft/config-wsl 的内核配置文件),进入 Device Drivers -> USB support -> Support for Host-side USB ,选中 USB Mass Storage support* 号是直接编译进内核,M 是编译为内核模块,内核模块需要手动加载),把下面弹出来的一堆驱动都选上;其它有需要支持的内容也可以一并选上(我这里是读卡器读的 SD 卡,所以下面再选一个 MMC/SD 卡支持),如果你有这个兴致的话,还可以在 General setup -> Local version 中自定义你的内核版本号的后缀。保存退出,然后开始编译内核:

1
2
3
4
5
6
# 编译并生成压缩后的内核,我比较懒,就直接把这些驱动全编译进内核了
make -j$(nproc) bzImage KCONFIG_CONFIG=Microsoft/config-wsl
# 作为内核模组编译的话,把模组编译出来,然后安装模组
make -j$(nproc) modules KCONFIG_CONFIG=Microsoft/config-wsl
make -j$(nproc) modules_install KCONFIG_CONFIG=Microsoft/config-wsl
# 关于如何加载模组请自行搜索

把编译好的内核复制出来,放在 Windows 的任意路径下,然后在 Windows 的用户目录(默认是 C:\Users\{username})下创建一个名为 .wslconfig 的文件,内容根据 微软官方文档 来:

1
2
[wsl2]
kernel=path\\to\\kernel

请注意这里的内核路径里的反斜杠要转义(双反斜杠)!!!否则 WSL 2 是读不到指定的内核的,而且在这种情况下 WSL 2 非但不会报错,还会直接以默认内核启动!!!这个问题坑了我好久,我一度认为是我编译出来的内核有问题呜呜呜

在 Windows 的命令行中执行 wsl --shutdown 关闭 WSL 2 虚拟机,然后再启动 WSL。如果你有修改内核版本号的后缀的话,这时执行 uname -r,应该能看到打印出来的内核版本号有变化。这时再通过 usbipd-win 连接读卡器,成了!

(假装这里有图)


后记:我终究还是没有成功备份我的 SD 卡

不知道为什么,我的 SD 卡在 dd 读取数据时,每读取大约 400MiB 的数据就会报一次 I/O Error,再插入树莓派中好像也没法开机。本来以为是 SD 卡坏了,但是还是不死心,想着反正里面也没什么重要数据,干脆死马当活马医,直接重新刷写系统算了。没想到写入的过程很顺利,没有报错,在重新刷入系统之后再使用 dd 读取,连续读了 20G 也没什么问题,就很迷惑…

参考链接

连接 USB 设备 | Microsoft 文档

Hyper-V 和 WSL 2 的 USB 直通 - Yadomin的博客 -

How to get USB devices working under Linux

鸟哥的 Linux 私房菜——第 24 章:LInux 核心编译与管理

Accessing USB storage devices in WSL 2? · Issues #7770 · microsoft/WSL

This article was last updated on days ago, and the information described in the article may have changed.