3
14
2017
9

我的 zsh 提示符

这是我用了多年的 zsh 提示符。

My zsh prompt

右提示符比较简单,先说。

首先,这个右提示符是 zsh 才支持的,不是 hack 左提示符来的哦。

我的右提示符显示的是(提示符打印出来时的)时间。在有后台任务时,会在左边以黄色显示出后台任务的数量,增加些许后台默默工作的进程的存在感啦。

截图中可以看到,只有最后一行才显示了右提示符(以至于我截图都得 hack 一下)。我使用了setopt transient_rprompt,这样 zsh 会清掉旧的右提示符,就不会影响复制了。以前每次复制时都带上一堆空格然后几个时间,折行之后根本没法看,后来才发现体贴的 zsh 已经有这么个选择了。

另外,在输入命令到右提示符时,右提示符会自动消失,以免和命令混淆。都说了很体贴的哦~

左边,是一个两行的提示符。之所以做成两行,是为了保持命令的起始位置不会因为提示符的长度变化而变化,每次输入新命令的时候,光标都在同一列,易读好找。我就不明白,那些坚持 bash 默认提示符的人是怎么坚持下来的,用着用着不知道自己光标去哪里了……对了,zsh 在输出提示符时,会保证它从终端最左边那一列开始输出。如果上一行不完整,zsh 会打印一个反色的「%」来表示(截图里 ^C 那里就有一个)。

蓝色「>>> 」是学 Python 的,但是使用了蓝色以免和 Python 混淆。如果是 root 用户,则显示红色的「### 」以警示。这个比较刺眼,所以就尽量不用 root 跑 shell 啦。

第一行开头是命令序号,就是历史记录里有多少条命令。每执行一条命令它就会加一,空行或者 Ctrl-C 放弃的不算。其实没什么用的样子。

然后是一个用于标识不同机器的名字。比如这里 lilywork 表示我正在我的工作机上。我家里那个系统里不会显示这个。这个信息可以通过ZSH_PS_HOST变量来设置,比如一般可以设置成$(hostname)。GitLab 之前的提示符里大概没有这个吧。

再就是最后一条命令的状态码($?)。如果命令成功就不显示,否则显示一个红色的数字,以提示上条命令出错了。所以说了嘛,我没法理解坚持使用 bash 及其默认提示符的人……

然后是缩短过的当前目录。~tmp是我的临时目录,有名字(hash -d tmp=....)的。但是它不会缩短中间路径的名字,反正我在它下边写命令,不用担心路径太长。不过我不建议深入探索 nodejs 的模块树,显示好几行的路径并不好看的。

最后一项又是可选的,git 当前分支。这个功能是我自己写的,不是 zsh 自带的那个,是异步显示的哦~忙着干活呢,不能在这种小事上浪费时间、中断思绪嘛。并且还可以通过设置来排除一些目录,比如访问特别慢的远程目录,比如已经死掉很久的 Wuala。

显示的信息不多,也一点都不华丽,但十分有用呢。

介绍完毕,提示符的定义我这里就不写啦。代码都在这里:https://github.com/lilydjwg/dotzsh

Category: shell | Tags: zsh linux
2
25
2017
17

中键的功能

鼠标中键,就是左键和右键之间的那个键啦。常见的鼠标上它在滚轮上。所以你知道了,滚轮是可以往下按的哦。如果是触摸板并且没有中键的话,可以配置双指点击来作为中键使用的(synclient ClickFinger2=3)。

中键具有以下好用的功能哦~(括号里是适用的场景)

  • 粘贴选择区,不用按复制和粘贴的快捷键了~不过选择区的寿命通常比较短,只适合快速的粘贴操作。另见 X Window 中的剪贴板一文。(Linux 桌面、macOS 终端、gpm)
  • 在后台新标签页打开链接(火狐、Google Chrome 等浏览器都支持)
  • 关闭标签页(基本上也是用于网页浏览器。我自己的 GVim 也支持)
  • 定位滚动条,可以快速地定位到开头、结尾,或者之前的位置。不需要拖来拖去的麻烦。可惜 GTK 3 里这个功能不好用的了。(GTK 2、Qt)
  • 移动画布(GIMP、Inkscape 等作图软件、GNOME 的文档查看器 Evince)

这只是比较通用的功能。我的 Awesome 还配置了使用中键关闭窗口呢(「关闭标签页」语义的扩展)。火狐的一些菜单项也支持中键点击,比如书签菜单,右键的「查看图像」菜单,比如前进/后退按钮,以及在它上边点击右键出来的历史记录项目。

总结一下中键的语义:

  • 在可以粘贴的地方,粘贴
  • 在打开对象时,打开新对象而不取代已有者
  • 在打开的对象本身上时,关闭之
  • 在可定位对象上,移动之
Category: Linux | Tags: linux X Window X window
12
22
2016
5

利用 systemd 的 watchdog 功能重启卡住的服务

我在用 offlineimap。用着用着就发现一个问题:偶尔 offlineimap 会卡在网络上不动弹了。跟 getmail 一个德性……

但是 offlineimap 又跟 getmail 有点不一样,它是持续运行着的。虽然非要把之前那个 killhung 程序拿来用不是不可以,但我还是重新弄了一个更优雅的方案:systemd watchdog。

我的 offlineimap 本来就是用 systemd 服务的方式来跑的,所以很适合这样的改造呢。只是,当我瞅了一眼源码之后,我就放弃了 patch offlineimap 的打算。很难在合适的地方添加 watchdog 相关的代码。

既然从内部着手不好做,那就从外部写一个 wrapper 好了,反正 offlineimap 跟 getmail 不一样,正常情况下一直在输出东西,就把这个作为它的「心跳」特征好了。当然这个 wrapper 还可以给其它程序用。

于是,watchoutput 程序诞生了!稍微改一下 offlineimap 的 .service 文件,像这样子就好了:

[Unit]
Description=Offlineimap Service

[Service]
Type=notify
ExecStart=.../watchoutput /usr/bin/offlineimap
TimeoutStopSec=3s
SyslogIdentifier=offlineimap
Restart=on-failure
WatchdogSec=70
LimitCORE=0

[Install]
WantedBy=default.target

加上LimitCORE=0是为了阻止重启的时候由于 SIGABRT 信号导致 coredump,浪费磁盘空间。

用了几天之后,终于观察到一次由 watchdog 触发的重启:

12月 19 12:26:53 lilywork offlineimap[21623]:  Establishing connection to imap.exmail.qq.com:993 (main-remote)
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Watchdog timeout (limit 1min 10s)!
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Killing process 21623 (python3) with signal SIGABRT.
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Killing process 21625 (offlineimap) with signal SIGABRT.
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Main process exited, code=dumped, status=6/ABRT
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Unit entered failed state.
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Failed with result 'core-dump'.
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Service hold-off time over, scheduling restart.
12月 19 12:28:03 lilywork systemd[687]: Stopped Offlineimap Service.
12月 19 12:28:03 lilywork systemd[687]: Starting Offlineimap Service...
12月 19 12:28:04 lilywork systemd[687]: Started Offlineimap Service.

没过几天,我又给这个 watchoutput 的脚本找到另外的用处:自动重启网络。

我家里的笔记本连 Wi-Fi 不知怎么,这些天经常会卡住(只发不收,一直处于 ARP 找网关的状态)。内核之前报过一次错,现在也没反应了。

于是:

[Unit]
Description=Watch for network availability

[Service]
Type=notify
ExecStart=.../watchoutput --retry-on-exit 2 --wait-before-retry 30 --ignore-stderr \
    -- ping -i 30 192.168.1.1
Restart=on-failure
WatchdogSec=70
StandardOutput=null
StandardError=journal
LimitCORE=0
SyslogIdentifier=watch-network

[Install]
WantedBy=default.target

拿 watchoutput 监控 ping 网关的输出,每30秒 ping 一次,如果70秒还没反应就重启它自己。然后我们还需要重新连接网络。在 /etc/systemd/system 下建立 netctl-auto@wlan0.service.d 目录,并在其下建立一个 watchdog.conf 文件,给 netctl-auto@wlan0.service 服务增加一项配置:

[Unit]
PartOf=watch-network.service

这样当 watch-network.service 重启的时候,netctl-auto@wlan0.service 就会自动重启了~

Category: Linux | Tags: linux systemd
11
10
2016
14

数据让 git 给吃了!

之前一直觉得 git 是很安全的,除非用户显式指定(比如 --force 啦,reset --hard 啦,checkout xxxx 啦),git 在用户会失去数据时都会停下来,让不小心的用户有机会处理被遗忘的修改。直到有一天,我们有个文件让 git 给吃了!

嗯,是「我们」,不是「我」。这是我们的代码部署服务器上出的事。这仓库不是我使用的,整个操作流程我也没有参与设计与评估。实际上我只是作为 troubleshooter 参与到这次神秘事件之中的。

要让 git 愉快地吃掉数据,只要这样就可以了:

  • 提交 A 不包含文件 f
  • 提交 B 包含文件 f
  • 当前工作区为提交 A,并且包含一份未被 git 管理的文件 f,并且 f 被 gitignore 忽略掉了

然后做如下操作,未被管理的那份 f 就会消失不见了:

  • 将工作区切换到提交 B。因为 f 被忽略,所以 git 不会报错(代码
  • 将工作区再切换回 A。因为 A 不包含 f,所以 f 被删掉了

正在吃 f 的 git:主人遗弃了的 f 就交给我好了~

要避免出现这种问题,当然是在 git 工作区会有修改的时候,不要依靠 git 来在多个版本间切换啦~btrfs 或者 zfs 的快照多好!如果文件系统不支持快照的话,那就用多个目录吧。

Category: 版本控制 | Tags: linux Git
10
21
2016
0

在 Python 里设置 stdout 的编码

有时候进程的运行环境里,locale 会被设置成只支持 ASCII 字符集的(比如 LANG=C)。这时候 Python 就会把标准输出和标准错误的编码给设置成 ascii,造成输出中文时报错。

一种解决办法是设置支持 UTF-8 的 locale,但是那需要在 Python 进程启动前设置。启动之后,初始化过了,再设置 locale 也不会重新初始化那些对象。

另一种办法是往 sys.stdout.buffer 这种地方直接写 bytes。理论上完全没问题,但是写起程序来好累……

我就去找了一下怎么优雅地弄一个新的 sys.stdout 出来。Python 3 的 I/O 不再使用 C 标准库的 I/O 函数,而是直接使用 OS 提供的接口。封装位于 io 这个模块里边,有带缓冲的,不带缓冲的,二进制的,文本的。

研究了一下文档可知,sys.stdout 是个 io.TextIOWrapper,有个 buffer 属性,里边是个 io.BufferedWriter。我们用它造一个新的 io.TextIOWrapper,指定编码为 UTF-8:

import sys
import io

def setup_io():
  sys.stdout = sys.__stdout__ = io.TextIOWrapper(
    sys.stdout.detach(), encoding='utf-8', line_buffering=True)
  sys.stderr = sys.__stderr__ = io.TextIOWrapper(
    sys.stderr.detach(), encoding='utf-8', line_buffering=True)

这里除了可以设置编码之外,也可以设置错误处理和缓冲。所以这个技巧也可以用来容忍编码错误、改变标准输出的缓冲(不需要在启动的时候加 -u 了)。

其实这样子还是不够彻底。Python 在很多地方都有用到默认编码。比如 subprocess,指定 universal_newlines=True 时 Python 会自动给标准输入、输出、错误编解码,但是呢,在 Python 3.6 之前,这里的编码是不能手动指定的。还有参数的编码,也是不能指定的(不过可以传 bytes 过去)。

所以,还是想办法去设置合适的 locale 更靠谱……

Category: python | Tags: Python 中文支持 linux
9
13
2016
7

Linux 下的 Wi-Fi 分享

首先看看你的网卡和驱动组合是否支持这样的操作。

>>> iw list | grep -A2 combinations:
        valid interface combinations:
                 * #{ managed } <= 1, #{ AP, P2P-client, P2P-GO } <= 1, #{ P2P-device } <= 1,
                   total <= 3, #channels <= 2

上边这个输出说明支持,并且频道可以不一样。

然后,添加一个用途 AP 的网络接口,并配置 IP 地址。我的无线网络接口名字是 wlan0,因为我通过创建空 /etc/udev/rules.d/80-net-setup-link.rules 文件的方式禁用了 systemd 的网络接口改名。

sudo iw dev wlan0 interface add wlan0_ap type __ap
sudo ifconfig wlan0_ap 192.168.17.1

配置 NAT:

echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
sudo iptables -w -t nat -A POSTROUTING -s 192.168.17.0/24 -j MASQUERADE

配置 DHCP。我用的是 dnsmasq。它本来是作为 DNS 缓存用的,但是也支持 DHCP,那就用它了:

interface=wlan0_ap
no-dhcp-interface=wlan0
dhcp-range=192.168.17.50,192.168.17.150,12h

注意不要在其它只提供 DNS 服务的接口上提供 DHCP 服务,以免出现冲突。

然后就可以开启热点啦。hostapd 配置如下:

interface=wlan0_ap
driver=nl80211
ssid=名字
channel=1
hw_mode=g
ieee80211d=1
country_code=cn
ieee80211n=1
ieee80211h=1
ignore_broadcast_ssid=0
auth_algs=1
wpa=2
wpa_passphrase=secret
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP

最后把它们跑起来就可以了。

为了方便使用,我创建了个 systemd 服务 wlan0_ap.service:

[Unit]
Description=Setup wlan0_ap
Before=hostapd.service
After=sys-subsystem-net-devices-wlan0.device
After=iptables.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/iw dev wlan0 interface add wlan0_ap type __ap
ExecStart=/usr/bin/ip address add dev wlan0_ap 192.168.17.1/24
ExecStart=/usr/bin/iptables -w -t nat -A POSTROUTING -s 192.168.17.0/24 -j MASQUERADE
ExecStop=-/usr/bin/iptables -w -t nat -D POSTROUTING -s 192.168.17.0/24 -j MASQUERADE
ExecStop=/usr/bin/ip address delete dev wlan0_ap 192.168.17.1/24
ExecStop=/usr/bin/iw dev wlan0_ap del

[Install]
WantedBy=hostapd.service

systemctl enable wlan0_ap 之后就可以直接 systemctl start hostapd 来启动了~当然也很容易停止服务:systemctl stop hostapd wlan0_ap。我的 dnsmasq 总是开启的,所以就不用加依赖了。还有 ipv4_forward 我也是早就写到配置文件 /etc/sysctl.d/99-sysctl.conf 里的。

Category: Linux | Tags: linux 网络 systemd
7
23
2016
0

发包太快,请勿跟踪

之前写的那个处理 DNS AAAA 的程序,后来请求量大的时候就经常报错。经过研究,是在sendto的时候返回了「Pemission Denied」错误。后来的 Rust 版本也发生了类似的问题,得到操作系统返回的代码「EPERM」。

我翻了半天 man 手册,其中只说到向广播地址发包可能会得到 EACCES 错误。Google 也没有得到结果(都是些权限不够的问题,但我的程序是 root 跑的呀,并且错误比较零星)。后来发到 shlug 邮件列表中询问,才终于得知了和我有同样问题的人,但是也没有结论,只是说关掉 iptables 就正常了。可我的程序依赖 iptables 呢……而且我要的不仅仅是解决方案(实际上这个问题并没有造成什么可感知的影响,就算有,我也有办法 migrate),我更想知道为什么。

确定是发包太快造成的问题,拿着相关关键词去搜,还真找到了一些有用的信息。比如之前看过的 CloudFlare 低延迟 UDP 实验时会让 iptables 不跟踪相关数据包,有人在使用 SIP 协议时也遇到了同样的问题,并且在内核日志的帮助下解决了。于是我照着做,让 conntrack 放过我发出的 UDP 包:

iptables -t raw -I OUTPUT -p udp -m udp --sport 53 -j NOTRACK

然后不仅那些错误都没了,而且处理速度快了一倍!(图中红虚线是发生错误的时候。)

程序统计信息

Category: 网络 | Tags: iptables linux UDP
6
17
2016
7

Linux 作业控制实践

事情的起因是这样子的。

有一个非常常用的调试工具叫 strace。输出的信息是纯文本,一大片看起来累。在 Vim 里可以给它高亮一下,就好看多了。再加上各种搜索、清理,以及非常赞的 mark.vim 插件,用起来就舒服多了!

然而我并不想每次都让 strace 写到文件里然后再拿 Vim 去读,因为还得记着清理那些文件。如果数据量不大的话,直接通过管道传给 Vim 多好。

于是有了如下 zsh 函数:

(( $+commands[strace] )) && strace () { (command strace "$@" 3>&1 1>&2 2>&3) | vim -R - }

效果是达到了,但是这样子要中断 strace 的话,得去另一个终端里去 kill。按 Ctrl-C 的话,SIGINT 也会被发给 Vim,导致 Vim 显示空白。

所以嘛,得把 Vim 放到一个单独的进程组里,这样就不会在 Ctrl-C 的时候收到 SIGINT 了。但是,Vim 还得用终端啊。

一开始,我用自制的 expect.py 模块,给 Vim 分配了一个新的终端。这样子 Ctrl-C 好用了。然后我发现 Ctrl-Z 不好用了……

Ctrl-Z 还是挺方便的功能,临时需要执行个命令,不用开新的 shell(以及 ssh),直接按一下 Ctrl-Z,完事之后再回来,多好啊!就跟 zsh 的 Alt-q 一样方便好用呢。

于是就想还是不开 pty 了。直接子进程放新组里跑。这样 Vim 在尝试向终端输出时会收到 SIGTTOU 信号,因为它不是前台进程组。找了一下,用 tcsetpgrp 就可以把指定进程组放到前台了。然后发个 SIGCONT 让可能已经停下来了的 Vim 继续。

然后,当 Vim 收到 SIGTSTP 而停止的时候,我的程序该怎么知道呢?搜了一下,原来这种情况下也会收到 SIGCHLD 的!我以前一直以为只有子进程退出才会收到 SIGCHLD 啊……然后是一个关于 SIGCHLD 的坑,之前在 pssh 里看到过的,这次没有及时想到:不给 SIGCHLD 注册信号处理器时是收不到 SIGCHLD 的!不过诡异的是,我的这个程序有时却能够收到——在我使用 strace 跟踪它的时候……

于是,当 Vim 收到 SIGTSTP 时,把我们自己设置成前台进程组,然后给自己发一个 SIGTSTP 也停下来好了。令人意外的是,后台进程在调用 tcsetpgrp 时竟然也会收到 SIGTTOU。不过没关系,忽略掉就好了。

当用户 fg 时,就再把 Vim 设置成前台进程,并给它一个 SIGCONT 让它继续就好了。

最终的成品 vimtrace 在这里我的 zsh 配置是这样子的:

if (( $+commands[vimtrace] )); then
  (( $+commands[strace] )) && alias strace='vimtrace strace'
  (( $+commands[ltrace] )) && alias ltrace='vimtrace ltrace'
else
  (( $+commands[strace] )) && strace () { (command strace "$@" 3>&1 1>&2 2>&3) | vim -R - }
  (( $+commands[ltrace] )) && ltrace () { (command ltrace "$@" 3>&1 1>&2 2>&3) | vim -R - }
fi

后记:

strace 有时候还是会改变进程的行为的。这种时候更适合用 sysdig。Arch 刚刚更新的 sysdig 版本已经修正了崩溃的问题了~不过 Vim 对 sysdig 的输出就不像 strace 那样有好看的语法着色了。

其实我当时用 systemtap 来看信号发送情况更方便一些。不过那个需要内核调试符号,几百M的东西,装起来累啊……

Category: Linux | Tags: linux 终端 Python zsh
6
12
2016
0

SIGHUP, nohup, disown 以及 expect + sudo + bash + ssh

这些东西都和终端消失的时候(比如 ssh 连接中断)有关,但是细节上又各有不同。

nohup,去 coreutils 看源码就知道,是忽略 SIGHUP 然后 exec 相应的命令。信号处理器如果被设置成忽略,那么在 exec 之后依旧是忽略(与设置成用户自定义函数的情况不一样)。man 7 signal 可以看到说明。

disown 这个,我去看 zsh 的源码了。然而只看到 zsh 把进程从它的任务列表里删掉了,根本没提 SIGHUP 的事情。后来看到这个答案才知道原来 shell 也会发 SIGHUP 信号。

当然内核也会发 SIGHUP。查阅 drivers/tty/tty_io.c 可知,内核会给 session leader 及其组发 SIGHUP 和 SIGCONT,也会给前台进程组发,但是不会给后台进程组发 SIGHUP。那个是 shell 发的,所以 disown 之后后台进程就不会被 SIGHUP 干掉了。

所以,前台进程组如果没有被信号杀掉的话,会收到两次 SIGHUP 信号,一次 SIGCONT 信号。而后台进程组只会收到一次 SIGHUP。disown 过的不会收到任何信号。当然那些没死的进程,如果去读写终端,还是会得到 EIO 错误,写的时候还会收到 SIGPIPE 信号。

strace 可以观察到这些过程。

最后一个,出了问题。通过 expect 调用 sudo,然后登录服务器。终端断开时,expect 被 SIGHUP 杀死。sudo 会把用户发的信号传给它的子进程,但是内核发的不传。而 zsh 给 sudo 发信号时会因为权限原因而发送失败。于是后边的 bash 和 ssh 都会收不到 SIGHUP 信号。但是终端消失它们是能感知到的,所以这个出问题的进程树才这么深嘛。ssh 发现终端消失了,它干了什么呢?当然是通知对端终端没啦,然后等回复。对端 sshd 收到消息之后说,「哦哦,我去把 /dev/ptmx 给关掉。」于是 sshd 关掉了三个 /dev/ptmx 中的其中一个。所以这个 sshd 下的 bash 进程并不会得到 EIO 错误,还继续跑着。于是 sshd 还继续等着它跑完。于是这边的 ssh 还在等对端的 sshd 回复。于是这棵进程树就 hang 在这里了……

结论:还是 systemd 好啊,会话关闭时直接干掉这个会话启动的所有进程。需要在会话结束之后依旧运行的,自己用 systemd-run --user --scope xxx 启动就好。留下这些 sudo、ssh、bash 的还好,占用的资源不多。supervisorctl 这种的就囧了,死循环地读 stdin 又读不到东西,浪费一颗 CPU。

Category: Linux | Tags: linux 终端
5
21
2016
13

当 SSD 坏掉之后

某天,我注意到系统日志中有如下报错:

ata2.00: exception Emask 0x0 SAct 0x80 SErr 0x0 action 0x0
ata2.00: irq_stat 0x40000008
ata2.00: failed command: READ FPDMA QUEUED
ata2.00: cmd 60/18:38:64:ad:96/00:00:00:00:00/40 tag 7 ncq 12288 in
         res 41/40:00:64:ad:96/00:00:00:00:00/00 Emask 0x409 (media error) <F>
ata2.00: status: { DRDY ERR }
ata2.00: error: { UNC }
ata2.00: configured for UDMA/133
sd 1:0:0:0: [sdb] tag#7 UNKNOWN(0x2003) Result: hostbyte=0x00 driverbyte=0x08
sd 1:0:0:0: [sdb] tag#7 Sense Key : 0x3 [current] [descriptor] 
sd 1:0:0:0: [sdb] tag#7 ASC=0x11 ASCQ=0x4 
sd 1:0:0:0: [sdb] tag#7 CDB: opcode=0x28 28 00 00 96 ad 64 00 00 18 00
blk_update_request: I/O error, dev sdb, sector 9874788
bcache: bch_count_io_errors() sdb1: IO error on reading dirty data from cache, recovering
ata2: EH complete

啊,看来我的 SSD 要坏掉了么?!拿 bcache-status 看了一下状态,已经500多个报错了,但是我并没有遇到什么问题。看了一下 SSD 的 SMART 信息,也是500多个报错。这种报错出现已经有几天了,但是似乎影响并不大?SMART 报告也说「状况正常」。那就等有时间再处理好了,先把 bcache 缓存策略从「writeback」改成「writethrough」好了。我当时是这么想的。

出事的 SSD 是笔记本自带的,16G,被我用作缓存了。使用的是 bcache,一年之前我写文章讲过的。没想到刚刚一年,就开始出问题了。

过了一两天之后。我正上班中,同步 MediaWiki 数据时发现自己的机器又连不上了。我起初以为是网络问题,因为我记得很清楚我的机器是开着的。但是过了一会儿它还是不能访问。我 ping 了一下,却发现是通的!登陆到我作为路由器的树莓派上却发现,网络是正常的。能 ping 通笔记本,但是从树莓派上也 ssh 不了,访问 nginx 也没回应。所以笔记本还在线,但是系统出问题了。奇怪的是,访问另一端口上我用 Tornado 写的 Web 程序时却是正常的。

终于等到了下班。回到家里之后就听见笔记本风扇呼呼地响着。赶紧打开查看。htop 显示 journald 正在使用 CPU。于是去看 journald 日志,心里顿时凉了半截:日志只记录到上午,但满是上边那样的 SSD 报错。进一步的检查发现,/ 分区已经被挂载为只读,syslog-ng 日志比 journald 日志记录了更多的报错。火狐已经不知道什么时候挂掉了,并且启动不了。sudo 还是好的,但是运行 zsh 时会段错误。运行 bash 的话,会报一点错,可以看到有些文件已经变成无意义的数据了。bash 能启动,但是很奇怪的是,输入不了字符 e……想起还装了 dash,于是用这个连最基本的补全都没有的 shell,配合还没有挂掉的连着 VPS 的 mosh 上的 IRC 上网友们的帮助,尝试禁用 SSD 缓存。

结果是失败了。关于 bcache 有些资料与我看到的情况并不一致。重启会报一些错,然后我进入了 initramfs 里的 shell,尝试禁用 SSD,依旧失败中,脏数据写不回去,所以禁用不了缓存……不过看到了缓存中脏数据只有几百K。当然单位是什么我就不知道了……

放弃。又一天,我改从救援系统进入。记得有个工具能把普通分区转成 bcache。手机上网找到了,blocks。但是它并不能把 bcache 转回来。好在是开源项目,我读了一下代码,结合别人的说法,结论是:把普通分区转为 bcache,只要把分区缩小,在起始处空出一点空间,然后写入 bcache 的超级块就可以了。于是反过来,只要跳过 bcache 的超级块来读,就可以读到原来的分区数据了。

但是,bcache 超级块有多大呢?不知道。不过之前看到过一个扫数据的脚本,用类似的办法找一下分区数据的位置好了:

for i in {1..20}; do dd if=/dev/sda2 skip=$i | file -; done

file 显示在第8块的地方发现了「LUKS encrypted file」。这就是我要找的文件系统了!

为保险起见,先读读看。使用 losetup 把 /dev/sda2 映射成只读的 loop 设备,但是跳过开头的 8192 字节(dd 的块是 512 字节一个)。然后用 cryptsetup 解密。解密成功了。只读挂载解密之后的 ext4 文件系统。挂载成功了。看了一下内核日志,没有报错,没有警告。chroot 进去,正常,没有段错误,配置文件没有变成无意义的乱码。一切进行得好顺利!损坏程度比我以为的要轻不少呢!

为了确认损坏情况,我进行了一次模拟备份,也就是 rsync 的「--dry-run」选项。这样与最近的一次备份作比较,看看差异。结果很乐观,rsync 只报了十来个 I/O 错误。不过这也说明文件系统还是有问题的。

又一天。开始恢复系统了。还是从救援系统进入。进入 gdisk,删除原来的 sda2 分区,新建一个小了 8192 字节的分区。经验表明这样子删掉再重建分区表是没有问题的。也确实没出什么问题。不带 bcache 的文件系统出来了。解密之,然后使用 fsck 修复。修复过程中,fsck 问了我一大堆是不是要修复的问题。修复完毕之后,更改内核选项以便适应新的分区表,重启。

一切正常。不过根据之前 rsync 和修复过程中提到的文件名,去看了一下那些文件。都是最近更新系统时的一些 Python 文件。有一些文件消失了,另一些文件的权限和类型变得很奇怪。删掉,重装安装对应的软件包。完事了~不过没有了 SSD 缓存,能够感觉到有些操作会慢不少。

这 SSD 也真容易坏啊。刚刚一年呢。而且坏的过程很快,预警期只有几天。现在 SMART 报告已经出现过1000多个错误了,但整体评估还是「磁盘状况正常」……

好在我用的都是自由软件,虽然可能找遍整个互联网都没有个说法,但是还有实现它们的源码可以阅读,可以自己去思考解决方案。当然啦,就算这个 bcache 完全坏掉了,我也不需要太惊慌,因为我有备份嘛,更麻烦,但最多只损失几天的数据,不会突然之间什么都没有了。

Category: Linux | Tags: linux 数据恢复 bcache

部分静态文件存储由又拍云存储提供。 | Theme: Aeros 2.0 by TheBuckmaker.com