一次 WSL SSH 导致远程 Linux TUI 显示异常的排查记录

一次 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-clientncursesutil-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 残留进程

因为之前多次执行过 toptcpdump,所以开第二个 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 工具损坏

由于 topps 都来自 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.comsecurity.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 本地组件后,问题解决。