一次 WSL SSH 导致远程 Linux TUI 显示异常的排查与修复记录
背景
最近我在一台 Ubuntu 22.04 LTS 服务器上遇到了一个很迷惑的问题:一些终端交互命令看起来像是"卡死"或"黑屏"。
最开始的问题出现在一次抓包之后。当时我执行了类似下面的命令:
bash
tcpdump -i any host <TARGET_IP> and port 443 -nn
执行后命令一直没有返回,我误以为服务器卡住了。后来重启服务器后,问题并没有完全消失,反而陆续出现了更多异常:
text
1. 按方向键 ↑ 时,不再显示历史命令,而是出现黑屏或异常界面。
2. 执行 top 后,终端变黑或没有正常显示。
3. timeout 5s top -b -n 1 | head -40 看起来也像卡死。
4. ps aux --sort=-%cpu | head -15 执行时无响应。
5. apt install --reinstall procps 时卡在 Scanning processes...
6. 服务器资源看起来正常,但 TUI 交互命令经常异常。
一开始我怀疑是服务器本身损坏,甚至考虑是否需要重装系统。最终排查后确认:服务器没有坏,根因在本地 WSL 环境中的终端相关组件和 Linux 版 /usr/bin/ssh 的 TUI 显示链路。
最终解决方案是:恢复 WSL 原始 Ubuntu 软件源、禁用卡住的 Docker apt 源,然后重装 WSL 本地的 openssh-client、ncurses、util-linux 等组件。
一、最初的误判:tcpdump 是不是把服务器搞坏了?
最开始执行的命令类似:
bash
tcpdump -i any host <TARGET_IP> and port 443 -nn
这个命令看起来一直没有结束。后来才确认,这其实是 tcpdump 的正常行为。
tcpdump 默认会持续抓包。如果没有指定 -c 限制包数量,也没有用 timeout 限制运行时间,它会一直监听网络流量,不会主动退出。
以后更推荐这样执行:
bash
timeout 30s tcpdump -i any 'host <TARGET_IP> and port 443' -nn
或者限制抓取包数量:
bash
tcpdump -i any 'host <TARGET_IP> and port 443' -nn -c 20
这样就不会把"持续监听"误判成"系统卡死"。
二、第一个异常:按方向键出现黑屏
服务器上之前安装过一个命令历史搜索工具。为了确认是什么工具,我执行:
bash
command -v atuin
grep -nE 'atuin|fzf|mcfly|hstr|hh' ~/.bashrc ~/.zshrc ~/.profile 2>/dev/null
返回结果类似:
text
/root/.atuin/bin/atuin
/root/.bashrc:107:. "$HOME/.atuin/bin/env"
/root/.bashrc:110:eval "$(atuin init bash)"
/root/.zshrc:2:. "$HOME/.atuin/bin/env"
/root/.zshrc:4:eval "$(atuin init zsh)"
/root/.profile:11:. "$HOME/.atuin/bin/env"
这说明服务器上确实安装了 Atuin,并且在 bash、zsh、profile 里都做了初始化。
Atuin 可能会绑定方向键或打开历史搜索 TUI。如果终端渲染不兼容,就可能出现黑屏或显示异常。
一开始尝试保留 Atuin,只关闭上方向键绑定:
bash
sed -i 's|eval "$(atuin init bash)"|eval "$(atuin init bash --disable-up-arrow)"|' ~/.bashrc
sed -i 's|eval "$(atuin init zsh)"|eval "$(atuin init zsh --disable-up-arrow)"|' ~/.zshrc
后来决定彻底卸载 Atuin:
bash
cp ~/.bashrc ~/.bashrc.bak.$(date +%F_%H%M%S)
cp ~/.zshrc ~/.zshrc.bak.$(date +%F_%H%M%S) 2>/dev/null || true
cp ~/.profile ~/.profile.bak.$(date +%F_%H%M%S)
sed -i '/atuin/d;/\.atuin/d' ~/.bashrc ~/.zshrc ~/.profile 2>/dev/null
rm -rf ~/.atuin
rm -rf ~/.config/atuin
rm -rf ~/.local/share/atuin
rm -rf ~/.cache/atuin
hash -r
确认:
bash
command -v atuin || echo "Atuin 已卸载"
grep -nE 'atuin|\.atuin' ~/.bashrc ~/.zshrc ~/.profile 2>/dev/null || echo "启动文件里没有 Atuin"
不过后续排查证明,Atuin 只是一个干扰因素,并不是最终根因。
三、第二个异常:top 看起来卡死
直接执行:
bash
top
终端出现黑屏或空白,看起来像是整个 SSH 会话卡住。
为了判断 top 是真的卡死,还是只是交互式显示异常,先尝试非交互版本:
bash
timeout 5s top -b -n 1 | head -40
这条命令在当时的终端里也看起来没有正常输出。于是改成把输出写到文件:
bash
timeout -k 2s 8s top -b -n 1 >/tmp/top.out 2>/tmp/top.err
echo "exit=$?"
ls -lh /tmp/top.out /tmp/top.err
返回结果类似:
text
exit=0
-rw-r--r-- 1 root root 0 <DATE> /tmp/top.err
-rw-r--r-- 1 root root 11K <DATE> /tmp/top.out
这个结果非常关键。
它说明:
text
1. top 命令本身执行成功。
2. 没有错误输出。
3. 输出文件确实有内容。
4. 问题更像是"终端显示层异常",不是 top 或服务器真的卡死。
继续查看输出文件行数:
bash
wc -l /tmp/top.out
返回类似:
text
132 /tmp/top.out
这说明 top 的输出确实存在,只是在某些终端场景下没有正常显示出来。
四、排查服务器资源是否异常
登录服务器时,系统提示信息已经显示基础状态:
text
System load: <LOW_LOAD>
Processes: <PROCESS_COUNT>
Usage of /: <DISK_USAGE>
Memory usage: <MEMORY_USAGE>
Swap usage: 0%
继续手动执行:
bash
uptime
free -h
df -h
示例返回:
text
up <UPTIME>, <USER_COUNT> user, load average: 0.16, 0.35, 0.33
text
total used free shared buff/cache available
Mem: 3.8Gi 638Mi 1.9Gi 1.0Mi 1.3Gi 3.0Gi
Swap: 0B 0B 0B
text
Filesystem Size Used Avail Use% Mounted on
tmpfs 392M 1.0M 391M 1% /run
/dev/vda1 49G 11G 38G 22% /
tmpfs 2.0G 0 2.0G 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
/dev/vda15 105M 6.1M 99M 6% /boot/efi
从这些结果看:
text
CPU 负载很低
内存充足
磁盘空间正常
没有 swap 压力
系统整体不像是资源耗尽
所以"服务器整体卡死"的可能性下降。
五、排查是否有 tcpdump 或 top 残留进程
因为之前多次执行过 top 和 tcpdump,所以开第二个 SSH 窗口检查进程:
bash
bash --noprofile --norc
ps -eo pid,ppid,stat,wchan:32,comm,args | egrep 'top|tcpdump' | grep -v grep
曾经看到类似结果:
text
<PID> <PPID> S+ do_select top top
这个结果说明:
text
top 进程确实存在
状态是 S+,不是 D
S+ 表示它是前台交互进程,处于睡眠/等待状态
并不是不可中断 I/O 卡死
杀掉残留进程:
bash
pkill -9 top
pkill -9 tcpdump
确认:
bash
ps -eo pid,ppid,stat,wchan:32,comm,args | egrep 'top|tcpdump' | grep -v grep || echo "没有 top/tcpdump"
返回:
text
没有 top/tcpdump
这进一步说明,不是内核层面卡死。
六、重装服务器上的 procps:排除 top/ps 工具损坏
由于 top 和 ps 都来自 procps 相关工具,所以尝试在服务器上重装:
bash
apt update
apt install --reinstall -y procps
安装过程中出现了一个新问题:
text
Setting up procps (<VERSION>) ...
Processing triggers for man-db (<VERSION>) ...
Scanning processes... [================================================ ]
它卡在:
text
Scanning processes...
这里并不是 procps 本身安装失败,而是 apt 后置阶段的 needrestart 在扫描进程时卡住或显示异常。
检查包状态:
bash
dpkg -l procps needrestart | cat
ps --version
top -v
返回类似:
text
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/Trig-pend
|/ Err?=(none)/Reinst-required
||/ Name Version Architecture Description
+++-==============-===================-============-===============================================================
ii needrestart <VERSION> all check which daemons need to be restarted after library upgrades
ii procps <VERSION> amd64 /proc file system utilities
ps from procps-ng <VERSION>
procps-ng <VERSION>
Usage:
top -hv | -bcEeHiOSs1 -d secs -n max -u|U user -p pid(s) -o field -w [cols]
说明:
text
procps 安装正常
needrestart 安装正常
ps 版本正常
top -v 正常
所以服务器上的 procps 不是根因。
为了避免 apt 后续再卡在 needrestart 的扫描阶段,临时禁用 apt hook:
bash
mkdir -p /root/disabled-apt-hooks
mv /etc/apt/apt.conf.d/*needrestart* /root/disabled-apt-hooks/ 2>/dev/null || true
ls -l /etc/apt/apt.conf.d/*needrestart* 2>/dev/null || echo "needrestart apt hook 已禁用"
七、临时恢复终端显示的方法
当终端进入异常状态时,普通恢复方式是:
bash
stty sane
export TERM=xterm-256color
clear
后来发现更有效的是这组硬重置命令:
bash
stty sane 2>/dev/null
printf '\033[0m\033[?25h\033[?1049l\033c'
export TERM=xterm-256color
clear
echo ok
这组命令的作用包括:
text
恢复 TTY 输入状态
重置颜色
显示光标
退出备用屏幕
发送终端 reset 控制序列
清屏
执行后,显示可以恢复。
但这只是临时修复终端显示状态,并没有根治问题。
八、关键排查:PowerShell SSH 正常,WSL 普通 ssh 异常
为了判断是不是服务器问题,换成 Windows PowerShell 直接 SSH:
powershell
ssh root@<SERVER_IP>
登录后执行:
bash
top
PowerShell 下 top 显示正常。
接着在 WSL 中用普通 Linux ssh:
bash
ssh root@<SERVER_IP>
登录后执行:
bash
top
现象:显示异常,空白或黑屏。
然后在 WSL 里直接调用 Windows 的 OpenSSH 客户端:
bash
/mnt/c/Windows/System32/OpenSSH/ssh.exe root@<SERVER_IP>
登录后执行:
bash
top
结果正常。
这一步说明问题不在远程服务器,而在本地 WSL 到远程服务器的 SSH/TUI 显示链路上。
当时的阶段性结论是:
text
PowerShell ssh 正常
WSL 中调用 Windows ssh.exe 正常
WSL 中使用 Linux /usr/bin/ssh 异常
所以排查方向从服务器切换到 WSL 本地环境。
九、排查 WSL 的 apt 源:真正卡住的是 Docker 源
为了修复 WSL 本地 /usr/bin/ssh,需要重装 WSL 中的 openssh-client 和终端相关包。
最开始我尝试执行:
bash
sudo apt update sudo apt install --reinstall -y openssh-client ncurses-base ncurses-bin ncurses-term util-linux hash -r
这条命令是错误写法,因为多条命令被写到了一起,导致 apt 把后面的内容当成了参数,出现错误:
text
E: Command line option --reinstall is not understood in combination with the other options
正确写法应该分开执行:
bash
sudo apt update
sudo apt install --reinstall -y openssh-client ncurses-base ncurses-bin ncurses-term util-linux
hash -r
或者使用 &&:
bash
sudo apt update && sudo apt install --reinstall -y openssh-client ncurses-base ncurses-bin ncurses-term util-linux && hash -r
后来执行 apt update 时发现卡在 Docker 源:
text
Ign: https://download.docker.com/linux/ubuntu jammy InRelease
0% [Connected to download.docker.com (...)]
而原始 Ubuntu 源本身并没有问题。于是先禁用 Docker 源:
bash
sudo mkdir -p /etc/apt/disabled-sources
sudo mv /etc/apt/sources.list.d/*docker* /etc/apt/disabled-sources/ 2>/dev/null || true
期间也尝试过把 Ubuntu 源切换到清华源,但发现清华源访问很慢,于是恢复原始源。
先列出备份:
bash
ls -lt /etc/apt/sources.list.bak.*
检查哪个是原始源:
bash
echo "===== backup 1 ====="
grep -nE 'archive.ubuntu.com|security.ubuntu.com|tuna|aliyun|docker' /etc/apt/sources.list.bak.<BACKUP_1>
echo "===== backup 2 ====="
grep -nE 'archive.ubuntu.com|security.ubuntu.com|tuna|aliyun|docker' /etc/apt/sources.list.bak.<BACKUP_2>
看到原始源内容类似:
text
deb http://archive.ubuntu.com/ubuntu/ jammy main restricted
deb http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted
deb http://archive.ubuntu.com/ubuntu/ jammy universe
deb http://archive.ubuntu.com/ubuntu/ jammy-updates universe
deb http://archive.ubuntu.com/ubuntu/ jammy multiverse
deb http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse
deb http://security.ubuntu.com/ubuntu/ jammy-security main restricted
deb http://security.ubuntu.com/ubuntu/ jammy-security universe
deb http://security.ubuntu.com/ubuntu/ jammy-security multiverse
然后恢复原始源:
bash
sudo cp /etc/apt/sources.list.bak.<ORIGINAL_BACKUP> /etc/apt/sources.list
清理临时 apt 配置和缓存:
bash
sudo rm -f /etc/apt/apt.conf.d/99-wsl-network-fix
sudo apt-get clean
sudo rm -rf /var/lib/apt/lists/partial/*
再次更新:
bash
sudo apt-get update
这次正常完成:
text
Hit: http://security.ubuntu.com/ubuntu jammy-security InRelease
Hit: http://archive.ubuntu.com/ubuntu jammy InRelease
Hit: http://archive.ubuntu.com/ubuntu jammy-updates InRelease
Hit: http://archive.ubuntu.com/ubuntu jammy-backports InRelease
Reading package lists... Done
十、最终修复:重装 WSL 本地 openssh-client 和终端库
在恢复原始 Ubuntu 源并禁用 Docker 源后,执行:
bash
sudo apt-get install --reinstall -y openssh-client ncurses-base ncurses-bin ncurses-term util-linux curl
hash -r
安装过程中可以看到:
text
The following NEW packages will be installed:
ncurses-term
0 upgraded, 1 newly installed, 5 reinstalled, 0 to remove and <N> not upgraded.
Need to get <SIZE> of archives.
关键包包括:
text
ncurses-bin
util-linux
ncurses-base
openssh-client
curl
ncurses-term
安装完成后,确认本地 ssh 和终端能力:
bash
/usr/bin/ssh -V
echo "$TERM"
stty size
infocmp xterm-256color >/dev/null && echo "xterm-256color OK"
返回类似:
text
OpenSSH_8.9p1 Ubuntu-3ubuntu0.15, OpenSSL 3.0.2 15 Mar 2022
xterm-256color
30 120
xterm-256color OK
这说明:
text
WSL 本地 openssh-client 正常
TERM 是 xterm-256color
终端尺寸正常
xterm-256color terminfo 存在
然后使用 WSL 内的 Linux 版 ssh 重新连接服务器:
bash
TERM=xterm-256color /usr/bin/ssh -tt root@<SERVER_IP>
登录后执行:
bash
top
这次 top 正常显示。
随后直接使用普通命令连接:
bash
ssh root@<SERVER_IP>
再次执行:
bash
top
也正常显示。
到这里,问题最终解决。
十一、当前最终状态
目前状态如下:
text
服务器无需重装。
服务器 top 正常。
服务器资源正常。
WSL 中 Windows ssh.exe 正常。
WSL 中 Linux /usr/bin/ssh 已恢复正常。
普通 ssh root@<SERVER_IP> 后执行 top 已正常显示。
最终有效修复动作是:
text
1. 禁用 WSL 中卡住的 Docker apt 源。
2. 恢复 WSL 原始 Ubuntu apt 源。
3. apt-get update 成功。
4. 重装 WSL 本地 openssh-client。
5. 重装 ncurses-base、ncurses-bin、ncurses-term。
6. 重装 util-linux、curl。
7. 确认 TERM、stty size、infocmp 正常。
8. 重新测试 /usr/bin/ssh 和 top,显示恢复正常。
十二、最终可复用修复命令
下面是这次问题最终真正有效的一组命令。
1. 禁用 Docker apt 源
bash
sudo mkdir -p /etc/apt/disabled-sources
sudo mv /etc/apt/sources.list.d/*docker* /etc/apt/disabled-sources/ 2>/dev/null || true
2. 恢复原始 Ubuntu 源
先找到备份:
bash
ls -lt /etc/apt/sources.list.bak.*
查看备份内容:
bash
grep -nE 'archive.ubuntu.com|security.ubuntu.com|tuna|aliyun|docker' /etc/apt/sources.list.bak.*
恢复包含 archive.ubuntu.com 和 security.ubuntu.com 的那一份:
bash
sudo cp /etc/apt/sources.list.bak.<ORIGINAL_BACKUP> /etc/apt/sources.list
3. 清理 apt 状态
bash
sudo rm -f /etc/apt/apt.conf.d/99-wsl-network-fix
sudo apt-get clean
sudo rm -rf /var/lib/apt/lists/partial/*
4. 更新 apt
bash
sudo apt-get update
5. 重装 WSL 本地 ssh 与终端相关组件
bash
sudo apt-get install --reinstall -y openssh-client ncurses-base ncurses-bin ncurses-term util-linux curl
hash -r
6. 检查终端环境
bash
/usr/bin/ssh -V
echo "$TERM"
stty size
infocmp xterm-256color >/dev/null && echo "xterm-256color OK"
期望看到:
text
OpenSSH_<VERSION>
xterm-256color
<ROWS> <COLS>
xterm-256color OK
7. 测试 SSH 和 top
bash
TERM=xterm-256color /usr/bin/ssh -tt root@<SERVER_IP>
登录服务器后:
bash
top
如果正常,再测试普通方式:
bash
ssh root@<SERVER_IP>
登录后:
bash
top
十三、备用临时方案
在问题未修好之前,曾经用过一个稳定临时方案:在 WSL 中直接调用 Windows 自带 OpenSSH。
bash
/mnt/c/Windows/System32/OpenSSH/ssh.exe root@<SERVER_IP>
如果想做成 alias:
bash
cat >> ~/.bashrc <<'EOF'
alias myserver='/mnt/c/Windows/System32/OpenSSH/ssh.exe -tt root@<SERVER_IP>'
EOF
source ~/.bashrc
不过最终 WSL 的 /usr/bin/ssh 已经修复,所以这个方案现在只是备用方案。
十四、经验总结
这次排查最重要的经验是:不要看到 top 黑屏、tcpdump 不返回,就马上判断服务器坏了。
更稳妥的排查顺序应该是:
text
1. 服务器资源是否异常?
2. 命令本身是否真的卡死?
3. 非交互模式是否正常?
4. 写文件输出是否正常?
5. 是否只是当前终端显示异常?
6. 换 SSH 客户端是否正常?
7. 换本地终端是否正常?
8. 检查 WSL 本地 openssh-client 和 ncurses。
9. 检查 apt 源是否卡住。
10. 最后才考虑系统重装。
本次最终结论:
text
服务器不用重装。
tcpdump 不是根因。
Atuin 是干扰因素,但不是最终根因。
服务器 procps/top 没坏。
needrestart 曾经卡在扫描进程阶段,但不是最终根因。
Windows PowerShell ssh 正常,说明服务器本身正常。
WSL 中 Windows ssh.exe 正常,说明 Windows Terminal 本身大体正常。
WSL 中 /usr/bin/ssh 曾经异常,最终通过重装 openssh-client、ncurses、util-linux 等组件修复。
Docker apt 源卡住影响了 WSL 本地包修复流程,禁用 Docker 源后 apt 恢复正常。
最终普通 ssh root@<SERVER_IP> 后执行 top 已正常显示。
这次问题的本质不是"Linux 服务器坏了",而是一次典型的本地终端链路问题:
text
Windows Terminal
-> WSL
-> WSL Linux /usr/bin/ssh
-> 远程 Ubuntu
-> top / ncurses TUI
其中 WSL 本地的 SSH/终端组件异常,导致远程 TUI 显示异常。修复 WSL 本地组件后,问题解决。