双系统的终极方案 - WSL2

双系统的终极方案 - WSL2

作为一个开发者主力系统使用 Linux 系统的好处是显而易见的,大部分的技术的安装、演示,都是以 Linux 系统为基础的,而且很多技术或者工具压根儿就不兼容 Windows 系统,比如 Docker。主力系统使用 Linux,开发体验将有很大提升。

主要是我这段时间主要从事运维相关的工作,对 Linux 的使用也很有心得,我感觉有这个能力了可以用好 Linux

其实从很早的时候想将主力操作系统切换为 Linux,希望能在一个跟服务器环境相同的环境中开发,这样的话,我们就可以使用 Nginx、Docker 这种非常方便的工具了,对我们的开发和日常工作都将带来极大的便利,但是经过一定的调研之后(主要调研 Ubuntu+GNOME),发现 Ubuntu 到目前为止依然有一些无法解决的问题,有这些问题的存在,让我认为现在将主力操作系统切换到 Linux 并不明智:

安装

在线安装

官方文档:安装 WSL | Microsoft Learn。官方文档写的很好,没有必要看别人的博客

首先打开 powersehll,运行以下命令行查看当前支持哪些 Linux 发行版

wsl --list --online

然后选择一个版本进行安装

wsl --install -d <DistroName> 

比如当前(2024 年 2 月 28 日)支持 Ubuntu-22.04

wsl --list --online
以下是可安装的有效分发的列表。
使用 'wsl.exe --install <Distro>' 安装。

NAME                                   FRIENDLY NAME
Ubuntu                                 Ubuntu
Debian                                 Debian GNU/Linux
kali-linux                             Kali Linux Rolling
Ubuntu-18.04                           Ubuntu 18.04 LTS
Ubuntu-20.04                           Ubuntu 20.04 LTS
Ubuntu-22.04                           Ubuntu 22.04 LTS
OracleLinux_7_9                        Oracle Linux 7.9
OracleLinux_8_7                        Oracle Linux 8.7
OracleLinux_9_1                        Oracle Linux 9.1
openSUSE-Leap-15.5                     openSUSE Leap 15.5
SUSE-Linux-Enterprise-Server-15-SP4    SUSE Linux Enterprise Server 15 SP4
SUSE-Linux-Enterprise-15-SP5           SUSE Linux Enterprise 15 SP5
openSUSE-Tumbleweed                    openSUSE Tumbleweed

我们可以直接通过以下命令安装,注意,Windows11 中默认使用的是 WSL2,而不是 WSL1

wsl --install -d Ubuntu-22.04

实际上,Ubuntu-22.04 是通过微软的应用商店下载的

安装完成之后提示重启操作系统,重启之后会自动打开 PowerShell,并让用户设置 Ubuntu-22.04 系统的用户名密码即可登录 Ubuntu-22.04
后续,如果我们想进入 Ubuntu-22.04,可以在 Windows Terminal 中进入,安装 Ubuntu-22.04 的时候,已经自动在 Windows Terminal 中添加了相关链接。选择 Ubuntu 22.04.03 LTS 登录我们刚刚安装的 Ubuntu-22.04

然后,我们可以通过以下命令,查看所有安装的发行版,

wsl --list --verbose

简化版为 wsl -l -v

在结果中,可以看到 WSL 安装的所有的发行版的版本,其中 VERSION 列表示的是 WSL 当前的版本,Windows11 中默认使用的是 WSL2。

wsl -l -v
  NAME            STATE           VERSION
* Ubuntu-22.04    Running         2

自己准备发行版 ISO 安装

官方教程:导入要与 WSL 一起使用的任何 Linux 发行版 | Microsoft Learn

过程很简单,到 Docker Hub 上下载 CentOS 的镜像,打成 tar 包,然后导入到 WSL 中即可。以后有时间再弄吧,TODO

日常使用

命令

WSL 管理 Linux 发行版(Distro)/ 子系统 的方式,有点类似 Docker 管理镜像的方式:WSL 就是一个类似于 docker 的管理程序,wsl 可以安装多个发行版,然后可以单独控制每一个发行版的启动和关闭。
wsl 命令的格式如下

wsl.exe [参数] [选项...][命令行]

其中我们一般省略 .exe
直接执行 wsl,会进入默认的子系统,如果当前没有子系统启动,则会先启动默认的子系统,然后进入默认的子系统。

后面我们会学习到 wsl --set-default <Distro> 用于设置默认的子系统,其中 --set-default 可简写为 -s。在当前博客中,<Distro> 就是 Ubuntu-22.04
通过 wsl -l --all -v 查看所有子系统的时候,默认子系统前面有星号

参数:运行 Linux 二进制文件(一般我们称之为命令 command)的参数,如果未指定 shell(例如 /bin/bash),wsl.exe 将启动默认的 shell。

每一个命令都有其对应的二进制文件,比如 cpmv 这类的,在 /bin 都有其对应的文件。

选项:

隐藏桌面图标

参考博客:Reddit - Dive into anythingBrainByteZ » [Windows 11] Hide/Remove WSL Linux icon from Desktop
具体的原理是:Registry Tweak - Add or Remove System Icons from the Desktop
安装完 Ubuntu 22.04 之后,桌面出现了图标:

这个图标无法删除,想要隐藏的话,没有找到官方提供的方法,只找到一个国外大神想到的办法。
具体做法是,到 [HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel] 这个目录下,创建一个名称为 {B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6} 类型为 DWORD (32-Bit) 的十进制的值,值为 1,然后重启,即可隐藏图标

配置用户

WSL 修改默认用户_wsl 设置默认用户-CSDN 博客

首先在 Linux 子系统中设置 root 用户密码,我这里设置的是 root/root

sudo  passwd root

然后配置以后都用 root 用户登录,修改 Linux 子系统的默认登录用户,命令为:

<DistributionName> config --default-user <Username>

当前安装的发行版是 Ubuntu-22.04,其在微软应用商店下下载,下载的软件执行器的名字为 ubuntu2204.exe,因此当前执行的就是

ubuntu2204 config --default-user root

再次启动 wsl,默认登录的就是 root 用户了。
手动的安装发行版不能这样设置,因为这些发行版没有可执行启动器。 可以改为使用 /etc/wsl.conf 文件来更改导入的发行版的默认用户。请参阅 高级设置配置 文档中的“自动装载”选项。

但是现在都 .wslconfig 了啊,不用 wsl.conf 了啊

可以用 wsl --user <Username> 或者 wsl --distribution <Distribution Name> --user <User Name> 来启动。

配置子系统

配置文件:WSL 中的高级设置配置 | Microsoft Learn

我们在 VMware 虚拟机中创建虚拟机的时候,可以配置虚拟机使用的 CPU 核心数,占用的内存,WSL 中也可以配置这些信息。

key value default 说明
内核 (kernel) path Microsoft 内置内核提供的收件箱 自定义 Linux 内核的绝对 Windows 路径。
内存 大小 Windows 上总内存的 50% 或 8GB,以较小者为准;在 20175 之前的版本上:Windows 上总内存的 80% 要分配给 WSL 2 VM 的内存量。
处理器 number Windows 上相同数量的逻辑处理器 要分配给 WSL 2 VM 的逻辑处理器数量。
localhostForwarding boolean true 一个布尔值,用于指定绑定到 WSL 2 VM 中的通配符或 localhost 的端口是否应可通过 localhost:port 从主机连接。
kernelCommandLine string 空白 其他内核命令行参数。
safeMode boolean false 在“安全模式”中运行 WSL,这会禁用许多功能,应用于恢复处于错误状态的发行版。仅适用于 Windows 11 和 WSL 版本 0.66.2+。
swap 大小 Windows 上 25% 的内存大小四舍五入到最接近的 GB 要向 WSL 2 VM 添加的交换空间量,0 表示无交换文件。交换存储是当内存需求超过硬件设备上的限制时使用的基于磁盘的 RAM。
swapFile path %USERPROFILE%\AppData\Local\Temp\swap.vhdx 交换虚拟硬盘的绝对 Windows 路径。
pageReporting boolean true 默认的 true 设置使 Windows 能够回收分配给 WSL 2 虚拟机的未使用内存。
guiApplications 布尔 * true 一个布尔值,用于在 WSL 中打开或关闭对 GUI 应用程序 (WSLg) 的支持。仅适用于 Windows 11。
debugConsole 布尔 * false 一个布尔值,用于在 WSL 2 发行版实例启动时打开显示 dmesg 内容的输出控制台窗口。仅适用于 Windows 11。
nestedVirtualization 布尔 * true 用于打开或关闭嵌套虚拟化的布尔值,使其他嵌套 VM 能够在 WSL 2 中运行。仅适用于 Windows 11。
vmIdleTimeout 数量 * 60000 VM 在关闭之前处于空闲状态的毫秒数。仅适用于 Windows 11。
dnsProxy bool true 仅适用于 networkingMode = NAT。布尔值,通知 WSL 将 Linux 中的 DNS 服务器配置为主机上的 NAT。设置为 false 会将 DNS 服务器从 Windows 镜像到 Linux。
networkingMode** string NAT 如果值为 mirrored,则会启用镜像网络模式。默认或无法识别的字符串会生成 NAT 网络。
firewall** bool 如果设置为 true,则 Windows 防火墙规则以及特定于 Hyper-V 流量的规则可以筛选 WSL 网络流量。
dnsTunneling** bool false 更改将 DNS 请求从 WSL 代理到 Windows 的方式
autoProxy* bool false 强制 WSL 使用 Windows 的 HTTP 代理信息
其中
具有 path 值的条目必须是带有转义反斜杠的 Windows 路径,例如:C:\\Temp\\myCustomKernel
具有 size 值的条目后面必须跟上大小的单位,例如 8GB 或 512MB
值类型后带有 * 的条目仅在 Windows 11 中可用。
值类型后带有 ** 的条目需要 Windows 11 版本 22H2 或更高版本。
这些设置是试验性功能的选择加入预览,我们的目标是将来将其设为默认设置。
.wslconfig 节标签:[experimental]
设置名称 默认值 说明
autoMemoryReclaim string disabled 检测空闲 CPU 使用率后,自动释放缓存的内存。设置为 gradual 以慢速释放,设置为 dropcache 以立即释放缓存的内存。
sparseVhd bool false 如果设置为 true,则任何新创建的 VHD 将自动设置为稀疏。
useWindowsDnsCache** bool false 仅当 wsl2.dnsTunneling 设置为 true 时才适用。如果此选项设置为 false,则从 Linux 隧道传输的 DNS 请求将绕过 Windows 中的缓存名称,以始终将请求放在网络上。
bestEffortDnsParsing** bool false 仅当 wsl2.dnsTunneling 设置为 true 时才适用。如果设置为 true,Windows 将从 DNS 请求中提取问题并尝试解决该问题,从而忽略未知记录。
initialAutoProxyTimeout* string 1000 仅当 wsl2.autoProxy 设置为 true 时才适用。配置启动 WSL 容器时,WSL 等待检索 HTTP 代理信息的时长(以毫秒为单位)。如果代理设置在此时间之后解析,则必须重启 WSL 实例才能使用检索到的代理设置。
ignoredPorts** string Null 仅当 wsl2.networkingMode 设置为 mirrored 时才适用。指定 Linux 应用程序可以绑定到哪些端口(即使该端口已在 Windows 中使用)。通过此设置,应用程序能够仅侦听 Linux 中的流量端口,因此即使该端口在 Windows 上用于其他用途,这些应用程序也不会被阻止。例如,WSL 将允许绑定到 Linux for Docker Desktop 中的端口 53,因为它只侦听来自 Linux 容器中的请求。应在逗号分隔列表中设置格式,例如:3000,9000,9090
hostAddressLoopback** bool false 仅当 wsl2.networkingMode 设置为 mirrored 时才适用。如果设置为 True,将会允许容器通过分配给主机的 IP 地址连接到主机,或允许主机通过此方式连接到容器。始终可以使用 127.0.0.1 环回地址,此选项也允许使用所有额外分配的本地 IP 地址。仅支持分配给主机的 IPv4 地址。
值类型后带有 * 的条目仅在 Windows 11 中可用。
值类型后显示 ** 的条目需要 Windows 版本 22H2 或更高版本。
我们可以自定义
[wsl2]
networkingMode=mirrored
dnsTunneling=true
autoProxy=true
firewall=true

[experimental]
autoMemoryReclaim=gradual  # gradual  | dropcache | disabled

移动子系统

我发现,子系统实际上安装到了 C:\Users\<username>\AppData\Local\Packages\Canonical...\LocalState\ext4.vhdx,也就是说,保存在 C 盘,

参考博客:windows subsystem for linux - Where is WSL located on my computer? - Ask Ubuntu

到目前为止,WSL 没有提供自定义安装路径的配置。比较好的办法是装好子系统之后再移动子系统,也就是先通过在线安装安装好子系统,之后再导出成一个文件放到你想要的位置,然后再导入到 WSL 中。

另一个办法是手动去下发行版文件(tar 文件),放到目标目录,然后解压再安装,不过这样就缺失了很多功能。

操作步骤是:
首先,关闭所有子系统

wsl --shutdown

查看所有分发版本

wsl -l --all -v

导出分发版为 tar 文件到 G 盘

wsl --export Ubuntu-22.04 G:\WSL\ubuntu22.04.tar

注销当前分发版

wsl --unregister Ubuntu-22.04

重新导入并安装分发版在 G:\WSL\ubuntu22.04

wsl --import Ubuntu-22.04 G:\WSL\ubuntu22.04 G:\WSL\ubuntu22.04.tar --version 2

此外,我还发现了一个工具 LxRunOffline,但是对 WSL2 的支持好像很差,有 bug,例如:Issue with WSL2 Ubuntu 22 trying to use LxRunOffline - Microsoft Q&A,反正官方的指令又不长,就用官方指令算了。

自定义 wsl 安装位置以及多 wsl 共存 | 骏马金龙

子系统桌面

子系统可配置其使用桌面,比如可以装 GNOME 或者 KDE,我找过了,有教程,但是没有必要,就用 Windows 桌面来访问子系统算了,反正文件系统是互通的。

设置 wsl 开机自启

方法如下:

  1. WIN+R 运行 shell:startup 打开启动目录,其实就是 C:\Users\<user_name>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
  2. 在此目录中创建文件 wsl-start.vbs
  3. wsl-start.vbs 中填充如下内容,Ubuntu-22.0 需替换为你使用的发行版名称。
    Set ws = CreateObject("Wscript.Shell")
    ws.run "wsl -d Ubuntu-22.04", vbhide
    
  4. 想要停止 WSL 开机自启,直接删除掉 wsl-start.vbs 即可。
    wsl 开机自启之后,我们可以在 wsl 中设置一些开机自启的服务,这样就可以做到 Windows 启动的时候 wsl 中的一些服务也自启。
    为了不让 wsl 的开机自启影响到 Windows 的性能,推荐设置一下 wsl 的内存占用,CPU 占用的限制就不必了,一般用不到

开发相关

Python

Ubuntu22.04 自带 python3.10。

WSL 的 DNS

WSL 的 DNS 是自己写死的,不会使用宿主机的 DNS。在文件 /etc/resolv.conf 中可查看,也可以配置宿主机的配置文件 wsl.confgenerateResolvConf = false 不让此文件自动生成。

网络设置 - 重点

如果你进入子系统的时候看到这个错误,说明子系统的网络没有配置好。

检测到 localhost 代理配置,但未镜像到 WSL。NAT 模式下的 WSL 不支持 localhost 代理。

别人也遇到了同样的问题:Github Issue

这个问题和下面这个问题其实是一个问题:如何实现 WSL 使用宿主机的代理,比如 Clash?

参考博客:在 WSL2 中使用 Clash for Windows 代理连接 - East Monster 个人博客

在 WSL 2.0.5 版本 后,一些特性得以稳定,比如 镜像模式网络。启用镜像模式网络会将 WSL 更改为全新的网络体系结构,其目标是将 Windows 上的网络接口“镜像”到 Linux 中,以添加新的网络功能并提高兼容性。
以下是启用此模式的当前优势:

[wsl2]
networkingMode=mirrored

经过这样设置之后,WSL 管理的子系统的 IP 跟主机一样了,也就是说,两个机器共用一套 IP 和端口,在子系统中部署的服务,在 Windows 中直接可以通过 localhost 来访问,比如我在 wsl 中部署了一个 nginx 服务,代理了 80 端口,那么在 Windows 下的浏览器中,直接输入 localhost:8000 就能访问,再加上 WSL 管理的子系统和 Windows 的文件本来就是互通的,修改和调试 Linux 下的应用就更加简单了。

docker run -d --name nginx  -p 8000:80  -v /opt/nginx/config/nginx.conf:/etc/nginx/nginx.conf  -v /opt/nginx/logs:/var/log/nginx -v /opt/nginx/www:/usr/share/nginx/html  nginx

我宣布,Windows11 是最好的 Linux 管理软件
注意,这个时候,子系统中的服务,Windows 中想要访问只能通过 localhost 来访问,因此此时只是开启了 Hyper-V 的回环地址访问(WSL 基于 Hyper-V,localhost 基于回环地址),在其他机器上通过 Windows 子系统的具体 ip 访问子系统中的服务是访问不了的,必须在 wsl 的虚拟机的防火墙上开端口。也就是配置 WSL 虚拟机的入站规则。

配置 WSL 虚拟机的入站规则

官方文档:
使用 WSL 访问网络应用程序 | Microsoft Learn
Hyper-V 防火墙 - Windows Security | Microsoft Learn

首先,我们要知道 WSL 基于 Hyper-V,而 Hyper-V 的防火墙规则按 VMCreatorId 启用。若要获取 WSL 虚拟机的 VMCreatorId,则可以这样查询:

Get-NetFirewallHyperVVMCreator

VMCreatorId  : {40E0AC32-46A5-438A-A0B2-2B479E8F2E90}
FriendlyName : WSL

此时我们查看 WSL 虚拟机的配置,我们会发现入站是被阻断(Block)的,也就是说,通过 IP 从 WSL 虚拟机的外部,是无法 ping 通 WSL 虚拟机中的任何端口的

Get-NetFirewallHyperVVMSetting -PolicyStore ActiveStore -Name '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}'

Name                  : {40E0AC32-46A5-438A-A0B2-2B479E8F2E90}
Enabled               : True
DefaultInboundAction  : Block
DefaultOutboundAction : Allow
LoopbackEnabled       : True
AllowHostPolicyMerge  : True

我们可以开启这个入站规则,开启之后,虚拟机外部访问虚拟机内部的所有端口,都会放行。此命令需要以管理员权限运行终端

Set-NetFirewallHyperVVMSetting -Name '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}' -DefaultInboundAction Allow

再次查看

Get-NetFirewallHyperVVMSetting -PolicyStore ActiveStore -Name '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}'

Name                  : {40E0AC32-46A5-438A-A0B2-2B479E8F2E90}
Enabled               : True
DefaultInboundAction  : Allow
DefaultOutboundAction : Allow
LoopbackEnabled       : True
AllowHostPolicyMerge  : True

那我们如何关闭 WSL 虚拟机入站的命令呢?

Set-NetFirewallHyperVVMSetting -Name '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}' -DefaultInboundAction Block

此外,你也可以不开启所有端口,也可以只开启特定端口,方法是创建一个入站规则

New-NetFirewallHyperVRule -Name MyWebServer -DisplayName “My Web Server” -Direction Inbound -VMCreatorId “{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}” -Protocol TCP -LocalPorts 80

然后通过以下命令查看手动添加的入站规则,

应该会有两个默认的 PolicyStoreSourceType : Local 的规则。

Get-NetFirewallHyperVRule -VMCreatorId '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}'

删除此特定的入站规则

Remove-NetFirewallHyperVRule -Name 'MyWebServer'

相关 API 页面:New-NetFirewallHyperVRule (NetSecurity) | Microsoft LearnRemove-NetFirewallHyperVRule (NetSecurity) | Microsoft Learn

不需要再手动配置子系统的代理

注意,当我们已经配置了虚拟机的网络模式为镜像(networkingMode=mirrored)的时候,我们实际上不需要再手动在子系统中配置 http_proxy 或者 https_proxyall_proxy 这些环境变量,系统已经为我们配置好了,通过 echo $http_proxy 等可以会直接输出我们在 Windows 上配置的系统代理,还挺方便的。

为 Linux 配置代理的方法请参考 博客

测试是否通过了代理:
因为 ping 是 ICMP 报文,代理不转发所以测试时不要使用 ping 来测试,而是用 curl 来进行测试。

curl ip.gs  # 推荐,启用后返回代理 IP,否则是本地 IP(但是返回结果只有 IP)
curl cip.cc # 好像有点问题,能 curl 通 Google 但是它还是显示本地 IP(返回结果会显示地址)
curl https://www.google.com  # 最直接的方法,能返回 HTML 就没问题,否则会报错

然后我们再到 Windows 下的 Clash 中查看日志,日志级别调到 ALL,可以看到从子系统中发出的请求的日志

装 Docker

在 Ubuntu 中安装 Docker,参考 官方文档
最后执行 sudo docker run hello-world 的时候发现提示超时:

root@LAPTOP-LK:/mnt/c/Users/wwwli# sudo docker run hello-world                                                                                
Unable to find image 'hello-world:latest' locally                                                                                             
docker: Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Ti
meout exceeded while awaiting headers).

后面查资料之后才发现原因:

  {
    "proxies": {
      "http-proxy": "http://127.0.0.1:1080",
      "https-proxy": "http://127.0.0.1:1080",
      "no-proxy": "*.test.example.com,.example.org"
    }
  }

然后重启 docker 服务

systemctl restart docker

然后我们在 Clash 中为域名 registry-1.docker.io 添加映射规则
再执行 login -u <username> 输入密码,如果输出 Login Succeeded 则表示登录成功。
然后就可以正常拉取镜像了
注意:
宿主机的 Clash 添加了新的规则之后,最好重启 wsl,这样 wsl 才能刷新新的规则。

wsl 子系统网络问题定位

当我发现子系统中 docker login 访问 registry-1.docker.io 老是失败,但是在子系统中通过 curl 访问此域名可以成功的时候(而且也可以在 Windows 的 Clash 中看到相应日志),我大概就可以判断出,这个时候基本确定就是因为 docker login 没有走代理