暗色模式

Linux SysRq 魔术键:从 /proc/sysrq-trigger 到卡死进程取证

技术教程
2026-09-23
7
0
本文要点
  • SysRq 有两条触发路径:控制台键盘上的 Alt+SysRq+<字母>,以及文件 /proc/sysrq-trigger(root 写一个字母进去即可)。后者不需要键盘、不需要控制台,是脚本和本文演示用的方式
  • /proc/sys/kernel/sysrq 的值是位掩码,不是开关:本文这台机器是 176 = 16(sync) + 32(remount-ro) + 128(reboot),正好是救援三连,调试转储那一类(bit 8)是关掉的
  • ⚠️ 反直觉实测:掩码不含 8,但 echo m > /proc/sysrq-trigger 照样打出了 27 行内存转储。位掩码约束的是键盘那条路径,/proc/sysrq-trigger 不受它约束——只要谁能写这个文件,就能触发全部命令
  • echo h 会打印完整的命令表(20 条),这是不查文档就能想起有哪些命令的办法
  • 一次写入只有第一个字符生效:实测 echo mt 只执行了 m,返回码仍是 0
  • echo w(Show Blocked State)专门抓 D 状态(不可中断睡眠)的进程。本文机器上没有 D 状态任务,输出就只有一行标题——空输出本身就是结论:没有进程卡在 I/O 上
  • echo t 是全量任务快照,实测一次往环形缓冲区里灌了 3157 行,每个任务带内核调用栈。用两个故意制造「等锁」的进程实测,栈里能直接看到失败者停在 __x64_sys_flock 上——这才是定位「进程卡住了」的主武器
  • ⚠️ b(reboot)、c(crash)、e/i(杀进程)、o(poweroff) 在真机上按下去没有二次确认。想安全重启请记救援序列 s → u → b(同步 → 只读重挂 → 重启)
  • dmesg_restrict=1 时 SysRq 的输出只有 root 能读,普通用户连 dmesg 都打不开——它本质上是给 root 用的工具

Linux SysRq 魔术键:从 /proc/sysrq-trigger 到卡死进程取证

服务器彻底卡住的时候,最常见的三种反应都没用:SSH 连不上,kill -9 杀不掉,重启按钮在机房里。这时候还有一条路留着——内核自己留的后门,SysRq。

它最广为人知的名字是「魔术键」(Magic SysRq key):按住 AltSysRq(就是 Print Screen),再敲一个字母,内核会立刻执行对应的动作,完全不理会当前跑着什么程序。但它其实是内核的一个通用接口,键盘只是其中一条触发路径;另一条是写文件,而这一条在云服务器上更有用——毕竟没人能按到 VPS 的键盘。

本文在一台 Ubuntu 22.04(内核 5.15.0-30-generic、1 核、1.9G)上把它完整跑了一遍,包括一个和直觉相反的安全结论。

两条触发路径

路径怎么用谁能用
键盘Alt + SysRq + 字母物理接触控制台/串口的人
文件echo <字母> > /proc/sysrq-trigger能写这个文件的用户(默认只有 root)

第二条路径是本文的主角,因为它可脚本化、可远程、可复现。先看看这台机器上的现状:

cat /proc/sys/kernel/sysrq
ls -l /proc/sysrq-trigger
cat /proc/sys/kernel/dmesg_restrict
cat /etc/sysctl.d/10-magic-sysrq.conf

SysRq 现状:sysrq 值为 176、trigger 文件是 --w------- 只有 root 可写、dmesg_restrict 为 1,以及 Ubuntu 自带的位掩码含义表

三行输出值得逐条读:

  • 176:这不是「开/关」,而是位掩码,含义见下面那张表。
  • --w------- 1 root root 0 /proc/sysrq-trigger:注意权限位里没有 r,这是一个只能写、不能读、且只有 root 能碰的文件。往里写一个字母,内核就执行一次。
  • dmesg_restrict = 1:内核环形缓冲区只允许 root 读取。也就是说 SysRq 的输出(它写进 dmesg)普通用户看不到,SysRq 从头到尾都是 root 工具

最后 cat 出来的是 Ubuntu 自带的 /etc/sysctl.d/10-magic-sysrq.conf,注释里就是官方给出的位含义表:

含义含义
0完全关闭16允许 sync
1全部功能32允许只读重挂(remount-ro)
2控制台日志级别64允许给进程发信号(term/kill/oom-kill)
4键盘控制(SAK、unraw)128允许 reboot/poweroff
8调试转储(进程、内存等)256允许调整 RT 任务优先级

按这张表拆 176 = 16 + 32 + 128,也就是:只允许 sync、只读重挂、重启,其余全部关掉。这几乎可以确定是服务商刻意的设置——正好保留 s → u → b 这条救援链,同时把「能 dump 内存和杀进程」的调试位(8)和信号位(64)关掉。

实测:位掩码管不住 /proc/sysrq-trigger

如果掩码是硬约束,那在 176 下写一个 m(show-memory-usage,属于 bit 8)应该什么都不发生。实测结果不是这样:

echo "当前 kernel.sysrq = $(cat /proc/sys/kernel/sysrq)"
dmesg -C; echo m > /proc/sysrq-trigger
echo "掩码不含 8 时写 m:rc=$?,dmesg 行数 = $(dmesg | wc -l)"; dmesg | head -2
dmesg -C; echo h > /proc/sysrq-trigger
echo "[h 的命令表,fold 折行仅为阅读]"
dmesg | grep "sysrq: HELP" | fold -s -w 100
dmesg -C; echo mt > /proc/sysrq-trigger
echo "一次写两个字符 mt,dmesg 只有:"; dmesg | head -2

位掩码实测:176 下写 m 依然产出 27 行内存转储;echo h 打出 20 条命令的完整命令表;echo mt 只执行了第一个字符

三个结论:

① 掩码管的是键盘,不是这个文件。 sysrq=176 的机器上,echo m 返回码 0,dmesg 里实打实多出 27 行 sysrq: Show Memory。也就是说:/proc/sys/kernel/sysrq 的位掩码只约束控制台键盘那条路径;能写 /proc/sysrq-trigger 的人不受它限制。这台机器上 /proc/sysrq-trigger--w------- root root,所以约束这个能力的是文件权限,不是掩码。

这一点对安全判断很重要:把 kernel.sysrq 设成 176 并不能「关掉危险命令」,它只是让站在控制台前的人按不出调试功能。已经拿到 root 的人本来也不需要 SysRq 提权——他可以直接 cat /proc/kcorekill -9。掩码真正的用武之地是防「物理接触键盘的人」和「意外按键」。

echo h 是最快的命令手册。 20 条命令连同对应的字母一次列全,不用记也不用查:

sysrq: HELP : loglevel(0-9) reboot(b) crash(c) terminate-all-tasks(e)
memory-full-oom-kill(f) kill-all-tasks(i) thaw-filesystems(j) sak(k)
show-backtrace-all-active-cpus(l) show-memory-usage(m) nice-all-RT-tasks(n) poweroff(o)
show-registers(p) show-all-timers(q) unraw(r) sync(s) show-task-states(t) unmount(u)
show-blocked-tasks(w) dump-ftrace-buffer(z)

③ 一次只认第一个字符。 实测 echo mt 返回码 0,但 dmesg 里只有 m 的效果(sysrq: Show Memory),t 没有被执行。想连做两个动作必须写两次。

另外注意上面每段演示前的 dmesg -C——它会读走并清空内核环形缓冲区。演示里用它把上一段输出清掉,好让截图只显示当次的结果;生产环境排查时别随手敲,那是把现场证据清掉了。

用 t / w / l 给卡死进程取证

命令表里真正用来「看现场」的是这三条:t(show-task-states)、w(show-blocked-tasks)、l(show-backtrace-all-active-cpus)。为了有个能看的样本,先造两个进程:一个拿到文件锁后睡 40 秒,另一个去抢同一把锁——第二个会卡在锁上

cat > /tmp/lockdemo.py <<'PY'
import fcntl, os, time
f = open("/tmp/lockdemo.lock", "w")
fcntl.flock(f, fcntl.LOCK_EX)
print("pid=%d 拿到锁,持有 40 秒" % os.getpid(), flush=True)
time.sleep(40)
PY
python3 /tmp/lockdemo.py & P1=$!; sleep 1
python3 /tmp/lockdemo.py >/dev/null 2>&1 & P2=$!; sleep 2
ps -o pid,stat,comm -C python3
dmesg -C; echo l > /proc/sysrq-trigger; echo "[l 的输出]"; dmesg | head -6
dmesg -C; echo w > /proc/sysrq-trigger; echo "[w 的全部输出]"; dmesg
dmesg -C; echo t > /proc/sysrq-trigger; echo "t 输出行数 = $(dmesg | wc -l)"
dmesg | grep -A 12 "task:python3" | grep -E "task:|flock|nanosleep|__x64_sys" | head -8
kill -9 $P1 $P2 2>/dev/null; rm -f /tmp/lockdemo.py /tmp/lockdemo.lock

SysRq 取证实测:l 打印 CPU 回溯并指出触发者是 bash;w 只有一行标题(没有 D 状态进程);t 灌了 3157 行,栈里能看到等锁进程停在 __x64_sys_flock

先看 ps:两个进程都是 S

ps 那一栏给出前提:持锁的 174942 和等锁的 174944,状态都是 S(可中断睡眠)。这很关键——后面 w 为什么空,答案就在这里。

Linux 进程的睡眠分两种,运维必须分清楚:

  • S(可中断睡眠):在等一个事件,收到信号会醒。kill 能杀掉它,Ctrl-C 能中断它。等锁、sleep、读网络都属于这一类。
  • D(不可中断睡眠):通常卡在内核里等 I/O 完成,kill -9 都杀不掉(信号要等它回到可中断状态才处理)。NFS 硬挂载的对端掉线、磁盘/驱动出问题、某些内核 bug 会造成这种进程。

D 状态进程是「服务器负载 20 但 CPU 空闲」这类诡异现象的常见元凶,也是唯一一种你只能等它自己好的进程。

w:空输出就是结论

echo w 的输出只有一行标题 sysrq: Show Blocked State一条任务都没有。这条命令专门罗列处于 D 状态的任务,所以空输出恰恰是好消息:此刻系统里没有任何进程卡在不可中断的 I/O 上

反过来,如果哪天 w 列出一串进程,那就是定位到了病灶——它给出的进程名、PID 和内核栈直接告诉你谁在等什么。平时没卡的时候跑一次 echo w 记住它「应该是空的」,真出事时才有对照。

t:3157 行的全量快照

echo t 是这三条里信息量最大的:它把每个任务的名字、PID、状态和内核调用栈全部打到 dmesg 里。实测这一次就产生了 3157 行——在还能响应 SysRq 但已经不太正常的机器上,这往往是唯一能拿到的现场。

它的输出长这样(grep 过滤出两个 python3 任务的关键帧):

task:python3         state:S stack:    0 pid:174942 ppid:174940 flags:0x00000002
task:python3         state:S stack:    0 pid:174944 ppid:174940 flags:0x00000002
 __x64_sys_flock+0x12b/0x140
 ? __x64_sys_lseek+0x18/0x20

174944 的栈里出现了 __x64_sys_flock——它就是那个卡在文件锁上的进程,证据确凿。而 174942 的栈落在 schedule_hrtimeout_range_clock 一带,说明它在 nanosleep 里正常睡觉。同样是 S 状态的两个进程,靠调用栈一眼就能区分「在正常等待」和「被锁卡住」。

echo t 在排查中的典型用法就是配合 dmesg 做过滤,比如找所有 D 状态任务(state:D)、找某个进程名、或者看谁卡在同一个函数上。在机器还能响应键盘/SysRq、但 SSH 已经连不上时,这是唯一能拿到全部进程栈的手段。

l:谁在触发 SysRq

echo l 打印所有活动 CPU 的回溯。单核机器上它输出很短,但第一行信息量很大:

NMI backtrace for cpu 0
CPU: 0 PID: 174940 Comm: bash Not tainted 5.15.0-30-generic #31-Ubuntu

它明确告诉你触发这个动作的是哪个 PID、哪个进程(本例是执行 echo l 的那个 bash)。多核机器上这一条更有用:它会把每个 CPU 正在跑什么全部抓出来,判断是不是有 CPU 卡在自旋锁上空转。

救援序列:s → u → b

真到了要靠 SysRq 的地步,通常是想安全重启。直接按 b(reboot)等于拔电源——内存里没落盘的脏页全丢,文件系统可能损坏。正确顺序是三连:

顺序命令作用
1echo ssync:把所有脏页刷到磁盘
2echo uunmount:把所有文件系统重新挂成只读(umount 的缩写)
3echo breboot:立刻重启,此时已无数据可丢

这套序列的妙处在于每一步都幂等s 可以按两次,u 之后再 s 也无害。如果前一步没成功,后一步的后果会更严重,所以顺序不能颠倒。这台机器的 176 恰好就开放了这三个位。

⚠️ 下面这些命令在本文的演示机器上一次都没有执行,列出来是为了让你知道该避开什么:

命令动作风险
ccrash:故意触发内核崩溃立刻宕机,可能丢数据
b / oreboot / poweroff未经 sync,等于拔电
e / i给所有进程发 SIGTERM / SIGKILL除了 init 全部被杀,服务全停
f触发 OOM killer 杀进程随机杀掉一个进程
kSAK,杀掉当前控制台上的所有程序会踢掉控制台上的会话

这些命令都没有二次确认,敲下去立刻生效。

什么时候该把它关小

SysRq 的能力太大,默认配置需要按场景取舍:

  • 云服务器(没有物理控制台,只能靠 SSH):/proc/sysrq-trigger 只有 root 可写,键盘路径也没人能碰到,风险主要来自「root 误操作」。本文这台机器的 176 是个合理的折中——保留救援三连,关掉调试位。
  • 有物理控制台或串口的机器:陌生人碰到键盘就能 dump 内存、杀掉锁屏程序,Ubuntu 注释里担心的正是这个。可以设成 0(全关)或只留 16(sync)。
  • 要永久生效,改 /etc/sysctl.d/10-magic-sysrq.conf 里的 kernel.sysrq,然后 sysctl --system;临时改直接 sysctl -w kernel.sysrq=176 即可(本文演示中间改过,结束时已还原成 176)。

顺带说明:dmesg_restrict=1 时,SysRq 的输出只有 root 能看,这反而抬高了门槛——普通用户连 echo h 打了什么都读不到。

小结

  • SysRq 是内核级的「无条件执行」通道,键盘和 /proc/sysrq-trigger 是两条独立入口,后者是云服务器上唯一可用的那条。
  • /proc/sys/kernel/sysrq 是位掩码:本文机器 176 = sync + remount-ro + reboot,调试位是关的。
  • 掩码只约束键盘路径:实测 176 下写 m 照样拿到 27 行内存转储。想真正限制能力,靠的是 /proc/sysrq-trigger 的文件权限。
  • echo h 打印 20 条命令表;一次写入只认第一个字符(echo mt 只执行 m)。
  • 排查卡死用 t(全量任务栈,实测 3157 行)+ w(专抓 D 状态,空输出说明没有 I/O 卡死)+ l(各 CPU 回溯)。
  • 安全重启走 s → u → bc/b/e/i/f/o 这些没有二次确认,别在真机上试。

SysRq 抓出来的内核栈如果看不懂,可以接着用 strace 系统调用追踪 从用户态那一侧对一遍;如果只是想看日志而不是内核现场,journalctl 系统日志Linux 信号与后台任务 是更日常的工具。

参考资料:

发表评论

暂无评论,快来抢沙发吧!