这期内容原本应该顺着 Linux 安装的话题继续往下写,但最近收到的实操问题实在太多,从 ZIP 解压乱码到 WSL 磁盘爆满,再到 Python 版本管理,几乎每一个都让我重新翻了一遍文档。所以我把第 10 期的上半部分直接做成一个“问题复盘集”,每一条都给出排查路径和可直接复制的命令,适合已经装了 Linux、正在被日常小问题折磨的人。下面这四个问题看似不相关,实际上都指向同一件事:系统某个默认机制在背后做了你没注意到的假设,把机制摸清了,问题就解决了一半。
1. 解压乱码的根因不在二进制,而在文件名编码
1.1 为什么同一个 ZIP,在 Windows 下正常、到 Linux 就乱码
很多人第一次遇到解压乱码时,会以为这个压缩包损坏了,或者怀疑系统缺少中文字体支持。其实根源比想象中简单:ZIP 格式规范从来没有强制要求文件名字段的编码方式。Windows 自带的压缩工具和国内一些压缩软件,默认使用本地代码页,中文环境下就是 GBK/CP936;而 Linux 桌面环境默认期望 UTF-8。问题在于,ZIP 文件里没有一个像 HTTP header 那样明确的“charset”标记字段,解压工具只能靠猜、靠当前 locale 去解释文件名。两边默认编码不一致,猜错就变成了乱码。
更要命的是,这个乱码通常只发生在文件名上,文件内容反而是完整的。如果你用cat打开一个乱码压缩包里的文本文件,内容显示正常,那就基本可以确定是文件名编码出了问题,这时候去换字体、改系统语言都是白费力气。
1.2 先分清是内容乱码还是文件名乱码
动手之前,我建议先花一分钟做两步检查,否则很容易把处理方向搞反。
第一步,看解压出来的文件内容能不能读。如果内容里中文全部变成“锟斤拷”这类符号,说明文本文件本身是 GBK 编码,需要用iconv做内容编码转换:
iconv -f GBK -t UTF-8 原文件.txt > 转换后.txt第二步,看是只有文件名乱码,还是内容和文件名都乱。如果文件名乱、内容正常,问题就出在 zip 的文件名字段解析上,下面这些工具和方法才是对症的。
1.3 可复制的处理流程
我的个人经验是:如果手里只有一个压缩包要处理,最省事的方式是安装unar。它会自动检测各种常见编码,中文 ZIP 基本能一把梭。
sudo apt install unar p7zip-full convmv unar 中文异常的压缩包.zipunar对 macOS 和 Linux 上常见压缩格式的支持都比较完善,尤其是对编码混乱的 ZIP 做了不少特殊处理。如果你不想额外装工具,也可以试试 Info-ZIP 自带的-O参数,明确告诉它文件名按 GBK 解码:
unzip -O GBK 中文异常的压缩包.zip需要注意一个坑:有些发行版编译 unzip 的时候没有启用字符集支持,执行unzip -O GBK会直接报invalid option,这不是命令写错,而是二进制根本不带这个功能。碰到这种情况就别纠结了,直接换unar。
如果压缩包已经解压过、乱码文件名已经落在磁盘上,还可以用convmv批量修正文件名编码。比如当前文件名实际是 GBK 字节,但系统按 UTF-8 解析成了乱码,就执行:
convmv -f GBK -t UTF-8 --notest -r 目标目录/如果你的场景正好相反,是从 UTF-8 环境拷到 GBK 环境,就把-f和-t的参数调换一下。这个命令的本质只是重命名,不会动文件内容,但批量操作前建议先在一个子目录里试跑一遍,确认输出的文件名是你预期的样子,再铺开执行。我自己吃过一次亏,当时一着急把参数传反了,几百个文件的中文名全部变成另一种乱码,最后只能靠备份恢复。
2. 配置 DNS 之后解析依旧失败:从 stub 到上游的完整排查链
2.1 先看 resolv.conf 到底是谁生成的
DNS 排错最容易被忽略的一步,是搞清楚/etc/resolv.conf里面那几行配置到底是谁写的。很多教程会让你直接编辑这个文件,但在现代 Linux 发行版上,它往往不是一个普通配置文件,而是符号链接,指向 systemd-resolved 生成的 stub 文件。
你执行下面的命令看到的如果是下面这种输出:
lrwxrwxrwx 1 root root 39 ... /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf那就说明你系统上真正工作的解析器是systemd-resolved,/etc/resolv.conf里的127.0.0.53只是本地 stub,所有 DNS 查询会先进入 systemd-resolved,再由它根据网络接口的配置选择上游 DNS。这时候你手动改/etc/resolv.conf,多半会在重启网络或者重新拨号后被覆盖,因为那根本不是它该看的地方。
2.2 定位“真正生效”的 DNS
我排障时会同时开三个窗口,分别执行下面的命令,对比输出结果:
dig @223.5.5.5 www.example.com dig www.example.com resolvectl status第一种是用指定的公共 DNS 直接查,绕开系统解析器,用来确认“网络本身通不通、上游能不能给出答案”。第二种走系统默认解析链路,看到底卡在哪一步。第三种则查看 systemd-resolved 当前给每个网卡分配了哪些 DNS 服务器。
如果dig @223.5.5.5正常返回,而普通dig超时,问题基本就锁定在系统 DNS 配置文件上。还可以再用resolvectl query验证 systemd-resolved 自己的解析链路:
resolvectl query www.example.com这一层和glibc的getent hosts又是两套东西,所以有时候你会看到ping能通但curl报解析失败,或者反过来。别慌,这是正常的,先把每一层结果都记下来,再去对照网卡配置。
2.3 三个最容易掉进去的坑
我在实际环境里见过最多的问题,基本可以归纳成三类。
第一类是 DHCP 覆盖。虚拟机或者路由器分配的 DHCP 自带一套 DNS,每次重启都会把你在/etc/resolv.conf里手动写的 nameserver 冲掉。这种情况最稳妥的做法,是在网络管理器层面配置。NetworkManager 系的发行版可以这样操作:
nmcli con mod "Wired connection 1" ipv4.dns "223.5.5.5 114.114.114.114" nmcli con mod "Wired connection 1" ipv4.ignore-auto-dns yes nmcli con up "Wired connection 1"Ubuntu 如果走 netplan,就在对应的 yaml 文件里加上nameservers地址,然后执行sudo netplan apply。总之一句话:不要直接改/etc/resolv.conf,要去配置真正生成它的上层服务。
第二类是多网卡优先级。系统里同时有有线网卡、虚拟机网卡、或者连接了外部网络时,systemd-resolved 会把不同的 DNS 分配在不同接口上。如果你想让某个查询走指定网卡的上游,可以用resolvectl临时指定:
sudo resolvectl dns eth0 223.5.5.5 114.114.114.114第三类是上游 DNS 本身只能解析内网域名。比如公司内网 DNS 能正确解析.internal域名,但转发外网查询时超时。表现就是ping内网服务器秒回、打开外网超时。排查方法很简单:用dig手动指定不同的上游 DNS,对比内网和外网解析结果,再决定是否需要把 DNS 拆分配置。
2.4 不要漏掉 hosts、路由和防火墙
如果上面三层都检查过了还不行,再回头确认两件事。第一,/etc/hosts里是不是有旧记录;第二,出方向的 UDP 53 端口是否被防火墙拦截。某些最小化安装的服务器会把防火墙策略写得很严,偶尔也会出现配置了正确的 DNS 但查询包根本没发出去的情况。
可以用一个简单的方式测试端口通不通:
timeout 1 bash -c 'echo > /dev/udp/223.5.5.5/53' && echo ok这个技巧利用 bash 的虚拟设备特性,比nc更轻量,适合没有预装网络工具的机器上快速验证。
3. WSL 磁盘空间只涨不缩:ext4.vhdx 的回收与预防
3.1 为什么 df 显示正常,Windows 磁盘却变小了
WSL2 的发行版数据全部存储在 Windows 侧的一个虚拟磁盘文件里,通常是ext4.vhdx。它就像一个鱼缸,你在鱼缸里清掉了沙子,鱼缸本身不会自动变小。文件系统里删除文件,只是把对应块标记为“可重用”,并没有主动告诉虚拟磁盘“这些块可以还给宿主机”。所以你在 WSL 里执行df -h看到空间明明已经空出来了,Windows 的磁盘可用空间却没有变化,甚至会越来越小。
这几年我见过不少开发机,因为反复编译、下载依赖、删掉重拉,最后ext4.vhdx膨胀到 100GB 以上,实际使用量却不到一半。这个问题不解决,再大的固态硬盘也顶不住长期折腾。
3.2 标准回收流程:TRIM + shutdown + diskpart compact
回收空间的标准动作分三步走,顺序不能乱。
第一步,在 WSL 里先确认实际使用量,并且主动告诉文件系统哪些块可以回收:
df -h / du -sh /home sudo fstrim -v /fstrim的作用是把已删除文件对应的空闲块标记为可回收,这一步不做的话,后面的压缩效果会大打折扣。
第二步,完全退出 WSL,确保没有进程还在占用虚拟磁盘。在 Windows 的 PowerShell 或 CMD 里执行:
wsl --shutdown注意,这一步不是关掉终端窗口,而是让整个 WSL 虚拟机结束运行,否则ext4.vhdx处于挂载状态,后面没法安全压缩。
第三步,用 Windows 自带的 diskpart 打开并压缩 VHDX 文件。以管理员身份打开 PowerShell,执行:
diskpart select vdisk file="C:\Users\你的用户名\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu22.04LTS_xxx\LocalState\ext4.vhdx" attach vdisk readonly compact vdisk detach vdisk exitext4.vhdx的具体路径,以实际安装的发行版用户目录里为准,记不清的话可以在 PowerShell 里搜索:
Get-ChildItem "$env:LOCALAPPDATA\Packages\*Ubuntu*\LocalState\ext4.vhdx" | Select-Object FullName我自己实际操作过一次,原来膨胀到 128GB 的 vhdx,执行完fstrim、shutdown、compact之后,直接降到 81GB,Windows 侧立刻多出 47GB 可用空间。整个过程不需要第三方工具,但压缩前最好备份一份 vhdx 或者确认重要代码已经推送到远端,毕竟任何磁盘操作都有风险。
3.3 新版 WSL 的稀疏文件选项
如果你的 WSL 版本比较新,还可以用一条命令把 vhdx 转换成“稀疏文件”,让虚拟磁盘不再预占全部空间,而是按实际写入增长:
wsl --manage Ubuntu22.04 --set-sparse true这个功能现在很实用,相当于从源头避免“删了文件但空间不释放”的尴尬。但老版本 WSL 的wsl.exe未必支持这个参数,执行前建议先跑一下wsl --version确认版本,不符合要求就老老实实走 diskpart 流程。新版 WSL 还有自动回收或者定期压缩的选项,但不同 Windows 版本的表现不完全一样,别把自动化配置当成所有机器上的通用方案。
4. 装了多个 Python 版本,系统却越来越脆弱:先分清楚谁不该被替换
4.1 apt 与 Python 的耦合关系
搜索词里“linux系统安装python”热度很高,但大家对“系统自带的 Python”和“自己安装的 Python”似乎没有太明显的边界意识。在 Ubuntu/Debian 这类系统上,/usr/bin/python3不只是给开发者用的解释器,它还是大量系统管理工具的运行环境,apt、gnome-terminal、软件更新器背后都依赖它。
如果你直接用 pip 给系统 Python 装了一堆包,或者把默认python3软链接到另一个版本,轻则出现各种奇怪的ModuleNotFoundError,重则让包管理器整个崩溃。最典型的就是执行apt时因为缺少某些 Python 模块直接退出,那感觉就像你只是换了双鞋,结果整栋楼的承重墙塌了。
4.2 独立版本的安全安装方式
最稳妥的思路,是让业务 Python 和系统 Python 物理隔离。我自己会习惯把新版本编译到独立的目录下,而不是塞进/usr/bin或者直接替换系统软链接。
以 Python 3.12 为例,完整步骤如下。先安装编译需要的依赖:
sudo apt install build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev然后下载源码、编译、安装:
cd /tmp wget https://www.python.org/ftp/python/3.12.10/Python-3.12.10.tgz tar xf Python-3.12.10.tgz cd Python-3.12.10 ./configure --prefix=/usr/local/python-3.12 --enable-optimizations make -j$(nproc) sudo make altinstall这里最关键的是make altinstall,不是make install。altinstall只会生成python3.12这样的独立命令,不会创建不带头尾数字的python可执行文件,也就不会覆盖系统里的python3软链接。
安装完成后,把新版本加进当前用户的 PATH 就行,写在~/.bashrc或者~/.profile里:
export PATH=/usr/local/python-3.12/bin:$PATH这样新版本只在当前用户环境下生效,系统脚本继续用它们原来的解释器,互不干扰。
4.3 用 update-alternatives 管理默认 python3 的风险
网上很多教程会介绍用update-alternatives把默认python3指向新版本,这个思路本身没错,但它更适合管理同一系列的系统版本,而不是直接把业务版本顶上去。如果你确实需要让命令行的python3指向新版本,可以把它作为低优先级选项注册进去:
sudo update-alternatives --install /usr/bin/python3 python3 /usr/local/python-3.12/bin/python3.12 100但我要提醒的是,这个操作之后,系统里凡是调用#!/usr/bin/env python3的脚本,都会跑到新版本解释器上。新版本不一定有系统脚本依赖的某些模块,即使有,模块版本也可能对不上。所以我的原则是:默认python3保持不动,需要新版本时显式敲python3.12,或者用虚拟环境隔离。多打几个字符,换来的是系统环境稳定,这笔账很划算。
4.4 日常开发推荐:虚拟环境和用户级版本管理工具
如果你经常要在不同项目之间切换 Python 版本,每次源码编译就太笨重了。我更推荐用pyenv或者uv这类工具,在用户目录下管理多个 Python 版本。以uv为例,安装后可以一行命令装好指定版本:
uv python install 3.12 uv venv uv pip install requests这样连源码编译都省了,版本切换以项目目录为单位,不会再出现“全局 Python 被某个项目改坏”的惨剧。
不管用什么方式安装,最终落到项目里,都要养成进虚拟环境再装包的习惯:
cd ~/myproject python3.12 -m venv .venv source .venv/bin/activate pip install -U pip另外再次提醒,sudo pip3 install xxx这种操作能避免就尽量避免。pip 本身不会主动知道哪些包是系统需要保留的,一旦它把某个关键包升级或者回退到不兼容版本,排查起来比直接重装系统还痛苦。真要装全局工具,优先考虑pipx,它会把每个工具隔离在自己的虚拟环境里,命令行直接可用,不会污染系统解释器。
我这一路复盘下来,最大的体会是:很多人不是不会敲命令,而是少了一条把“症状”和“机制”连起来的思路。解压乱码要先去想 ZIP 的元数据编码,DNS 要先去想系统解析器和上游的关系,WSL 磁盘要先去想虚拟磁盘的文件格式,Python 则要先去想系统管理脚本对解释器的依赖。只要思路理顺,再复杂的现象最后都能落到四五条命令上。第 10 期上半部分就先整理到这里,下半部分我准备把 systemd 服务和日志排错相关的坑单独拿出来写,如果你在同样的场景里踩过更离谱的坑,欢迎把现象和排查过程留下来,大家一起对照,比一个人翻文档快得多。