[toc]
在这里插入图片描述

drivers/tty/sysrq.c 内核魔法键(Magic SysRq Key) 系统的终极应急与调试工具

历史与背景

这项技术是为了解决什么特定问题而诞生的?

Magic SysRq Key(魔法系统请求键)是为了解决一个系统管理员和内核开发者面临的终极难题:当系统完全冻结,无法通过网络(SSH)或本地终端响应时,如何进行诊断或安全地重启?

在系统严重死锁或负载极高的情况下,用户空间的程序、Shell、甚至内核的调度器都可能停止工作。然而,内核的底层部分,如键盘中断处理,可能仍然在运行。SysRq机制就是利用这个“一线生机”,提供了一个直接与内核底层进行通信的“后门”。它允许操作员通过一个特殊的键盘组合键,绕过所有上层软件栈,直接向内核发送预定义的命令,从而:

  • 获取调试信息:在系统崩溃的瞬间,抓取任务状态、内存使用情况等快照。
  • 执行应急操作:如同步磁盘、以只读方式重新挂载文件系统。
  • 安全地重启系统:在别无选择时,执行一个比直接按电源键(硬重启)更安全的重启序列,最大限度地减少数据损坏的风险。

它的发展经历了哪些重要的里程碑或版本迭代?

  1. 源自SGI IRIX:这个概念并非Linux原创,而是从SGI的IRIX操作系统中借鉴而来,并在Linux 2.1.43内核中首次被引入。
  2. 命令集的不断扩充:最初,SysRq只支持少数几个核心命令(如重启、同步)。随着内核的发展,它的功能被极大地丰富了,加入了大量用于调试的命令,例如显示任务状态(t)、显示内存信息(m)、手动触发OOM Killer(f)等。
  3. /proc/sysrq-trigger接口的引入(关键里程碑):这是一个巨大的进步。除了物理键盘,内核还提供了一个/proc/sysrq-trigger文件接口。向这个文件写入一个命令字符,其效果等同于在物理键盘上按下对应的组合键。这使得SysRq的功能可以被远程触发(例如,通过带外管理控制台)和脚本化,极大地增强了其在数据中心环境下的实用性。

目前该技术的社区活跃度和主流应用情况如何?

SysRq是一个非常成熟和稳定的内核特性。它被广泛认为是Linux系统管理员和内核开发者的必备应急工具

  • 服务器环境:在生产服务器上,它通常被默认启用,作为系统无响应时的最后救援手段。
  • 桌面环境:出于安全考虑(物理接触键盘的人可以轻易地使系统重启),许多桌面发行版可能默认限制或禁用了SysRq的某些功能。
  • 内核开发:在调试复杂的内核死锁或性能问题时,SysRq提供的快照功能是不可或缺的。

核心原理与设计

它的核心工作原理是什么?

drivers/tty/sysrq.c 的核心是一个特殊的键盘输入过滤器命令分发器

  1. 低层键盘事件拦截:当你在键盘上按下Alt + SysRq + <command_key>时,键盘驱动程序会识别出这个特殊的组合。sysrq.c中的代码会作为一个过滤器(sysrq_filter),在键盘事件被发送到正常的终端处理程序之前将其拦截。
  2. 命令查找与分发sysrq.c内部维护着一个静态的命令表(sysrq_key_table。这个表是一个简单的数组,将命令字符(如'b'代表reboot)映射到一个函数指针(如sysrq_handle_reboot)。
  3. 直接、同步执行:一旦找到匹配的命令,对应的处理函数就会被立即、直接地在中断上下文中或专用的内核线程中执行。这是其“魔法”的关键所在:它绕过了内核的调度器、进程管理和VFS等所有可能已经死锁或无响应的上层系统。它是一种对内核的低级别、特权调用。
  4. /proc接口:当向/proc/sysrq-trigger写入时,内核会直接调用与键盘路径相同的命令分发逻辑,从而实现了两种方式的功能统一。

它的主要优势体现在哪些方面?

  • 终极可靠性:只要内核的中断处理仍在工作,SysRq就有可能成功执行,是名副其实的“最后手段”。
  • 绕过系统状态:不依赖于用户空间、调度器或大多数内核子系统的正常运行。
  • 丰富的功能集:提供了从安全重启到深度调试快照的多种强大功能。
  • 远程可控:通过/proc接口,完美适配了现代数据中心的远程管理需求。

它存在哪些已知的劣劣势、局限性或在特定场景下的不适用性?

  • 极其危险:这是其最大的“劣势”。SysRq命令是粗暴且立即执行的,它不会给应用程序任何优雅退出的机会。例如,reboot (b) 命令的效果几乎等同于按物理重置按钮,不会同步磁盘或卸载文件系统。
  • 安全风险:如果系统启用了SysRq,任何能够物理接触键盘的人都可以无视系统权限,直接重启或关闭系统。同样,任何获得了root权限的进程都可以通过/proc接口造成拒绝服务(DoS)。
  • 非万能:如果内核已经完全崩溃(例如,发生三重故障或中断处理本身被破坏),SysRq也将无能为力。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

SysRq是处理系统完全无响应场景的唯一标准解决方案。

  • 安全重启冻结的服务器:这是最经典的使用场景。当服务器SSH不通、控制台无响应时,管理员会依次按下著名的REISUB序列:
    • Raw (解除键盘独占), End (向所有进程发送SIGTERM), Ignore (向所有进程发送SIGKILL), Sync (同步所有文件系统), Unmount (以只读方式重新挂载), Boot (立即重启)。这个序列最大限度地保证了在重启前的数据安全。
  • 诊断内核死锁:系统卡死,怀疑是内核锁争用。可以通过SysRq触发:
    • t: 显示所有任务的状态及其调用栈。
    • w: 显示所有处于不可中断睡眠状态(通常是等待I/O或锁)的任务。
    • m: 显示内存使用信息。
      将这些信息(通常需要通过串行控制台或netconsole捕获)用于事后分析。
  • 在测试环境中模拟系统故障:开发者可以使用SysRq手动触发OOM Killer (f) 或内核崩溃 (c),以测试其应用程序或监控系统的鲁棒性。

是否有不推荐使用该技术的场景?为什么?

绝对不应该在任何可以正常操作的情况下使用SysRq来重启或关闭系统!

  • 替代shutdown/reboot命令shutdownrebootsystemctl poweroff等命令会执行一个优雅的关机流程:通知所有正在运行的服务,允许它们保存数据并干净地退出;同步并卸载文件系统;最后才关闭电源或重启。SysRq完全跳过了这个过程,是非优雅的暴力的

对比分析

请将其 与 其他相似技术 进行详细对比。

特性 Magic SysRq 优雅关机/重启 (shutdown, reboot) 看门狗定时器 (Watchdog Timer)
功能概述 手动的低级别的内核命令接口,用于应急和调试。 软件控制的高级别的有序的系统关闭流程。 自动的硬件或软件的系统复位机制。
触发方式 人工通过键盘组合键或写入/proc文件。 用户或脚本通过执行命令,经由systemdinit 内核或守护进程未能在一个设定的时间窗口内“喂狗”(ping),导致硬件自动触发复位。
执行过程 立即、粗暴。直接在内核底层执行,绕过大部分系统服务。 优雅、有序。通知服务,运行关机脚本,同步磁盘,卸载文件系统。 最粗暴。通常等同于硬件复位,是完全自动的最后保障。
主要目标 诊断和手动恢复 正常的、计划内的系统维护 自动从完全死锁中恢复
适用场景 系统完全无响应,但内核中断尚存。 所有正常的关机和重启操作。 无人值守的、要求高可用性的嵌入式系统或服务器。

Magic SysRq 配置接口:init_sysrq_sysctl 与 sysrq_sysctl_handler

本代码片段展示了 Linux 内核中用于在运行时动态配置“魔术 SysRq”功能sysctl 接口的实现。其核心功能是:在 /proc/sys/kernel/ 目录下创建一个名为 sysrq 的文件,允许系统管理员通过读写该文件来查询和设置一个全局的掩码(sysrq_enabled),从而控制 SysRq 功能的开关以及具体哪些 SysRq 命令是允许执行的。它通过一个状态转换逻辑,确保只有在 SysRq 功能从“禁用”变为“启用”(或反之)时,才会去注册或注销底层的输入处理句柄,以实现高效的资源管理。

实现原理分析

此机制是内核 sysctl 框架与 SysRq 子系统之间的桥梁,其设计体现了安全配置和动态资源管理的原则。

  1. 自定义 Sysctl 处理 (sysrq_sysctl_handler):

    • 与上文分析的 sysctl_max_threads 类似,此接口也采用了一个自定义处理函数。sysrq_sysctl_table.data 成员为 NULL,所有操作都委托给 sysrq_sysctl_handler
    • 它同样遵循了**“本地副本与验证后更新”的安全模式:
      a. 在函数开始时,它调用 sysrq_mask() 获取当前的 SysRq 掩码,并存入一个
      本地变量 tmp**。
      b. 它将一个临时的 ctl_table 结构 t.data 指针指向这个本地变量 tmp
      c. 然后,它调用标准的 proc_dointvec_minmax 函数来处理与用户空间的交互。读操作会返回 tmp 的值;写操作会将用户输入的值解析并存入 tmp
      d. 只有在写操作成功后,它才会调用 sysrq_toggle_support(tmp),将经过验证的、存储在本地变量 tmp 中的新掩码应用到系统中。
  2. 动态句柄管理 (sysrq_toggle_support):

    • 这是此机制的核心逻辑,负责将掩码的改变转化为实际的行为。
    • 状态转换检测: 它首先通过 was_enabled = sysrq_on() 记录下在应用新掩码之前 SysRq 功能是否处于激活状态(sysrq_on() 通常检查掩码是否非零)。然后,它更新全局的 sysrq_enabled 掩码。最后,它再次调用 sysrq_on() 来获取之后的状态,并进行比较 (was_enabled != sysrq_on())。
    • 按需注册/注销:
      a. 只有当状态发生了**从“禁用”到“启用”的变化时,它才会调用 sysrq_register_handler()。此函数负责向输入子系统(如键盘驱动)注册一个钩子,以拦截 SysRq 组合键。
      b. 只有当状态发生了
      从“启用”到“禁用”**的变化时,它才会调用 sysrq_unregister_handler() 来移除这个钩子。
    • 优化: 这个设计是一个重要的效率优化。如果 SysRq 功能本身已经是启用的,而用户只是修改了允许的命令子集(即改变了掩码中的某些比特位,但掩码仍非零),那么 was_enabledsysrq_on() 的结果将同为 true,状态没有发生转换,也就不会发生不必要的句柄注销和重新注册。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* @brief sysrq_toggle_support - 根据掩码启用或禁用 SysRq 支持。
* @param enable_mask: 新的 SysRq 功能掩码。
* @return int: 始终返回0。
*/
int sysrq_toggle_support(int enable_mask)
{
// 记录在应用新掩码之前的 SysRq 启用状态。
bool was_enabled = sysrq_on();

// 应用新的功能掩码。
sysrq_enabled = enable_mask;

// 比较应用前后的状态是否发生了变化 (从启用->禁用 或 禁用->启用)。
if (was_enabled != sysrq_on()) {
if (sysrq_on())
// 如果状态从 禁用 变为 启用,则注册底层的 SysRq 处理句柄。
sysrq_register_handler();
else
// 如果状态从 启用 变为 禁用,则注销处理句柄。
sysrq_unregister_handler();
}

return 0;
}
EXPORT_SYMBOL_GPL(sysrq_toggle_support);

/**
* @brief sysrq_sysctl_handler - /proc/sys/kernel/sysrq 的自定义处理函数。
* @param table: 指向 ctl_table 结构的指针。
* @param write: 标志位,非0表示写操作。
* @param buffer: 指向用户空间数据的缓冲区。
* @param lenp: 指向数据长度的指针。
* @param ppos: 指向文件偏移量的指针。
* @return int: 成功返回0,否则返回错误码。
*/
static int sysrq_sysctl_handler(const struct ctl_table *table, int write,
void *buffer, size_t *lenp, loff_t *ppos)
{
int tmp, ret;
// 创建一个临时的 ctl_table 副本。
struct ctl_table t = *table;

// 获取当前的 SysRq 掩码值,存入本地变量 tmp。
tmp = sysrq_mask();
// 将临时表的 .data 指针指向本地变量 tmp。
t.data = &tmp;

/*
* 行为类似于 proc_dointvec,因为 t 表没有设置 min 或 max。
*/
// 调用标准的整数读写函数,任何读写都将作用于本地变量 tmp。
ret = proc_dointvec_minmax(&t, write, buffer, lenp, ppos);
if (ret)
return ret;

// 如果是一次成功的写操作...
if (write)
// ...则调用核心逻辑函数,应用新的掩码值。
sysrq_toggle_support(tmp);

return 0;
}

// 定义 sysctl 表。
static const struct ctl_table sysrq_sysctl_table[] = {
{
.procname = "sysrq", // 文件名
.data = NULL, // .data 为 NULL,表示使用自定义处理函数
.maxlen = sizeof(int), // 最大长度
.mode = 0644, // 文件权限
.proc_handler = sysrq_sysctl_handler, // 指定自定义处理函数
},
};

/**
* @brief init_sysrq_sysctl - 初始化 SysRq 的 sysctl 接口。
* @return int: 始终返回0。
*/
static int __init init_sysrq_sysctl(void)
{
// 在 "kernel" (即 /proc/sys/kernel/) 目录下注册上述定义的 sysctl 表。
register_sysctl_init("kernel", sysrq_sysctl_table);
return 0;
}

// 使用 subsys_initcall 宏,确保该初始化函数在内核子系统初始化阶段被调用。
subsys_initcall(init_sysrq_sysctl);

SysRq 输入事件处理器:sysrq_handler 定义与注册

本代码片段展示了“魔术 SysRq”功能与 Linux 内核输入子系统(Input Subsystem)进行对接的核心机制。其主要功能是:定义一个 input_handlersysrq_handler),它像一个“插件”,专门用于监听和过滤来自键盘等输入设备的事件。它通过一个匹配规则(sysrq_ids)来自动绑定到所有可能产生 SysRq 序列的设备(即所有带 Alt 键的键盘),并通过 sysrq_register_handlersysrq_unregister_handler 这两个函数,将这个“插件”动态地插入或拔出输入子系统的事件处理流水线。

实现原理分析

此代码是 SysRq 功能得以工作的物理基础,它将一个抽象的内核调试功能,与具体的硬件输入事件关联起来。其实现原理基于输入子系统的分层和回调模型。

  1. 输入子系统分层模型:

    • Linux 的输入子系统是一个分层的架构。底层是设备驱动(如键盘驱动、串口驱动),它们将硬件信号转换为标准的输入事件(input_event)。上层是事件处理器(input_handler),它们消费这些事件。
    • sysrq_handler 就是一个事件处理器。当它被注册后,输入子系统核心会成为一个“中间人”,将来自匹配设备的所有事件,都先传递给 sysrq_handler.filter 回调函数。
  2. 设备匹配规则 (sysrq_ids):

    • sysrq_ids 是一个 input_device_id 数组,它定义了 sysrq_handler 感兴趣的设备类型。
    • 匹配逻辑: 它要求设备必须同时满足两个条件:
      a. INPUT_DEVICE_ID_MATCH_EVBIT: 设备的事件类型位掩码 evbit 必须包含 EV_KEY,意味着这是一个能产生按键事件的设备。
      b. INPUT_DEVICE_ID_MATCH_KEYBIT: 设备的按键码位掩码 keybit 必须包含 KEY_LEFTALT,意味着这个设备至少有一个左 Alt
    • 为何匹配 Alt: 注释中解释了一个关键的设计决策。不直接匹配 KEY_SYSRQ 键,是因为很多键盘(尤其是一些嵌入式或服务器设备)没有物理的 SysRq (PrintScreen) 键。而 Alt 键是几乎所有标准键盘的标配。SysRq 组合键总是以 Alt + SysRq + <command_key> 的形式触发,因此,只要能确保 Alt 键存在,就为触发 SysRq 提供了前提。
  3. 动态注册与生命周期管理:

    • sysrq_register_handler 是一个封装函数,由上一节分析的 sysrq_toggle_support 在 SysRq 功能被启用时调用。它调用 input_register_handler(&sysrq_handler),将 sysrq_handler 插入到输入子系统的全局处理器列表中。此后,输入核心会自动为所有已存在和未来新插入的、符合 sysrq_ids 规则的设备调用 sysrq_handler.connect 回调。
    • sysrq_unregister_handler 则对称地调用 input_unregister_handler,将处理器从系统中移除,输入核心会为所有已连接的设备调用其 .disconnect 回调,从而彻底断开 SysRq 功能与硬件输入的联系。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
* @var sysrq_ids
* @brief SysRq 输入处理器的设备匹配规则表。
* @note 我们匹配 KEY_LEFTALT 而不是 KEY_SYSRQ,因为并非所有键盘都预定义了 SysRq 键,
* 用户可能稍后将其添加到键盘映射中,但我们期望所有这类键盘都有左 Alt 键。
*/
static const struct input_device_id sysrq_ids[] = {
{
// 匹配标志:要求同时匹配事件类型位掩码和按键码位掩码。
.flags = INPUT_DEVICE_ID_MATCH_EVBIT |
INPUT_DEVICE_ID_MATCH_KEYBIT,
// 事件类型要求:必须支持 EV_KEY (按键事件)。
.evbit = { [BIT_WORD(EV_KEY)] = BIT_MASK(EV_KEY) },
// 按键码要求:必须支持 KEY_LEFTALT (左 Alt 键)。
.keybit = { [BIT_WORD(KEY_LEFTALT)] = BIT_MASK(KEY_LEFTALT) },
},
{ }, // 数组结束的空条目
};

/**
* @var sysrq_handler
* @brief SysRq 的输入事件处理器结构体。
*
* 这个结构体定义了 SysRq 功能如何作为一个“插件”接入到 Linux 输入子系统。
*/
static struct input_handler sysrq_handler = {
.filter = sysrq_filter, // 事件过滤函数:对来自匹配设备的每个事件调用
.connect = sysrq_connect, // 连接函数:当一个匹配的设备被发现时调用
.disconnect = sysrq_disconnect, // 断开函数:当匹配的设备被移除时调用
.name = "sysrq", // 处理器的名称
.id_table = sysrq_ids, // 指向设备匹配规则表
};

static void sysrq_of_get_keyreset_config(void)
{
u32 key;
struct device_node *np;

np = of_find_node_by_path("/chosen/linux,sysrq-reset-seq");
if (!np) {
pr_debug("No sysrq node found");
return;
}

/* Reset in case a __weak definition was present */
sysrq_reset_seq_len = 0;

of_property_for_each_u32(np, "keyset", key) {
if (key == KEY_RESERVED || key > KEY_MAX ||
sysrq_reset_seq_len == SYSRQ_KEY_RESET_MAX)
break;

sysrq_reset_seq[sysrq_reset_seq_len++] = (unsigned short)key;
}

/* Get reset timeout if any. */
of_property_read_u32(np, "timeout-ms", &sysrq_reset_downtime_ms);

of_node_put(np);
}

/**
* @brief sysrq_register_handler - 注册 SysRq 输入处理器。
* @note 这是一个内联函数,用于封装注册逻辑。
*/
static inline void sysrq_register_handler(void)
{
int error;

// 从设备树 (Open Firmware) 获取 SysRq 的按键复位配置。
sysrq_of_get_keyreset_config();

// 向输入子系统核心注册 sysrq_handler。
error = input_register_handler(&sysrq_handler);
if (error)
pr_err("Failed to register input handler, error %d", error);
}

/**
* @brief sysrq_unregister_handler - 注销 SysRq 输入处理器。
* @note 这是一个内联函数,用于封装注销逻辑。
*/
static inline void sysrq_unregister_handler(void)
{
// 从输入子系统核心注销 sysrq_handler。
input_unregister_handler(&sysrq_handler);
}

SysRq 事件处理回调:sysrq_connect, sysrq_disconnect, 与 sysrq_filter

本代码片段展示了 sysrq_handler 的核心实现,即当一个匹配的输入设备(如键盘)被接入或移除,以及当该设备产生输入事件时,内核所执行的具体操作。其核心功能是:

  1. sysrq_connect: 为每一个新接入的键盘设备,动态分配一个独立的状态机struct sysrq_state),并建立一个 input_handle 将设备、处理器和状态机三者牢固地绑定在一起。
  2. sysrq_filter: 作为事件处理的**“热路径”,它拦截来自该键盘的每一个输入事件,将其送入一个状态机(sysrq_handle_keypress)进行分析,并决定是否要“消费”**这个事件(即确认为 SysRq 序列的一部分并阻止其被系统其他部分看到)。
  3. sysrq_disconnect: 在设备被拔出时,执行一个干净、同步的拆除,确保所有与该设备相关的资源(状态机内存、定时器、工作队列)都被完全释放。

实现原理分析

此机制是 Linux 输入子系统事件驱动模型的具体应用。它通过为每个设备实例维护一个私有状态,实现了对复杂组合键序列的精确跟踪。

  1. 每个设备一个状态机 (sysrq_connect):

    • sysrq_connect 函数体现了“实例管理”的思想。当一个符合 sysrq_ids 规则的键盘被接入时,它不会使用一个全局的状态变量,而是 kzalloc 一个 sysrq_state 结构。这至关重要,因为它允许多个键盘同时连接到系统,而每个键盘的 SysRq 状态(例如,Alt 键是否被按下)都能够被独立、无冲突地跟踪。
    • 它初始化了一个工作队列 (reinject_work) 和一个定时器 (keyreset_timer),这些都是 sysrq_state 的一部分。
    • 关键绑定: 通过 input_register_handleinput_open_device,它建立了一个从硬件设备到这个新创建的 sysrq_state 的事件流通道。handle->private = sysrq; 这一行是实现状态绑定的核心。
  2. 事件过滤与状态机驱动 (sysrq_filter):

    • sysrq_filter 是一个过滤器,而不是一个传统的事件处理器。它的返回值 (bool suppress) 决定了事件的命运。
    • 返回 true: 意味着“此事件已被我处理,应到此为止”。这用于“吃掉”组成 SysRq 序列的按键(如 Alt, SysRq, 和命令键),防止它们被发送到用户空间的应用程序或终端。
    • 返回 false: 意味着“此事件与我无关,请继续传递”。
    • 状态机调用: sysrq_handle_keypress(sysrq, code, value) 是实际的状态机逻辑(未在此代码段中显示)。sysrq_filter 负责将按键事件 (EV_KEY) 喂给这个状态机,并根据其输出来决定是否要抑制事件。
    • 重注入保护: if (sysrq->reinjecting) 检查是一个防止无限循环的关键保护。SysRq 有时需要将它“吃掉”的 Alt+SysRq 键重新注入回输入系统,以兼容某些图形环境。这个检查确保了过滤器不会拦截自己重新注入的事件。
  3. 同步拆除 (sysrq_disconnect):

    • 当键盘被拔出时,sysrq_disconnect 被调用。它必须执行一个非常干净的逆向操作。
    • cancel_work_sync(&sysrq->reinject_work)timer_shutdown_sync(&sysrq->keyreset_timer) 是至关重要的。_sync 后缀表示这些函数会阻塞等待,直到任何正在运行的工作队列或定时器回调函数完全执行完毕。这彻底杜绝了在 kfree(sysrq) 之后,仍有回调函数尝试访问已被释放的 sysrq_state 内存的可能性,是防止 use-after-free 错误的关键。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/**
* @brief sysrq_filter - SysRq 的输入事件过滤器回调。
* @param handle: 指向 input_handle 的指针,代表设备与处理器的连接。
* @param type: 输入事件的类型 (如 EV_KEY, EV_SYN)。
* @param code: 事件代码 (如 KEY_A, KEY_B)。
* @param value: 事件值 (如 1 for press, 0 for release)。
* @return bool: 如果事件应被抑制 (suppress),则返回 true;否则返回 false。
*/
static bool sysrq_filter(struct input_handle *handle,
unsigned int type, unsigned int code, int value)
{
// 从 handle 中获取为此设备分配的私有状态数据。
struct sysrq_state *sysrq = handle->private;
bool suppress;

/*
* 如果我们正在重新注入 Alt+SysRq 组合键,则不过滤任何东西。
*/
if (sysrq->reinjecting)
return false;

switch (type) {

case EV_SYN:
// 同步事件,通常不抑制。
suppress = false;
break;

case EV_KEY:
// 按键事件,交由状态机处理,并根据其结果决定是否抑制。
suppress = sysrq_handle_keypress(sysrq, code, value);
break;

default:
// 其他类型的事件 (如鼠标移动),如果 SysRq 序列已激活,则抑制。
suppress = sysrq->active;
break;
}

return suppress;
}

/**
* @brief sysrq_connect - 当一个匹配的输入设备连接时调用的回调。
* @param handler: 指向 sysrq_handler 的指针。
* @param dev: 指向新连接的 input_dev 的指针。
* @param id: 指向匹配的 input_device_id 的指针。
* @return int: 成功返回0,失败返回错误码。
*/
static int sysrq_connect(struct input_handler *handler,
struct input_dev *dev,
const struct input_device_id *id)
{
struct sysrq_state *sysrq;
int error;

// 为这个新设备分配一个私有的、零初始化的状态结构。
sysrq = kzalloc(sizeof(struct sysrq_state), GFP_KERNEL);
if (!sysrq)
return -ENOMEM;

// 初始化用于重新注入按键的工作队列。
INIT_WORK(&sysrq->reinject_work, sysrq_reinject_alt_sysrq);

// 填充 input_handle 结构,建立设备、处理器和私有状态之间的连接。
sysrq->handle.dev = dev;
sysrq->handle.handler = handler;
sysrq->handle.name = "sysrq";
sysrq->handle.private = sysrq;
// 初始化用于按键复位的定时器。
timer_setup(&sysrq->keyreset_timer, sysrq_do_reset, 0);

// 向输入核心注册这个 handle。
error = input_register_handle(&sysrq->handle);
if (error) {
pr_err("Failed to register input sysrq handler, error %d\n",
error);
goto err_free;
}

// "打开"设备,表示我们希望开始接收来自它的事件。
error = input_open_device(&sysrq->handle);
if (error) {
pr_err("Failed to open input device, error %d\n", error);
goto err_unregister;
}

return 0;

// 错误处理 goto 链,按逆序清理已分配的资源。
err_unregister:
input_unregister_handle(&sysrq->handle);
err_free:
kfree(sysrq);
return error;
}

/**
* @brief sysrq_disconnect - 当匹配的输入设备断开时调用的回调。
* @param handle: 指向要断开的 input_handle 的指针。
*/
static void sysrq_disconnect(struct input_handle *handle)
{
struct sysrq_state *sysrq = handle->private;

// "关闭"设备,停止接收事件。
input_close_device(handle);
// 同步取消任何挂起的重新注入工作。
cancel_work_sync(&sysrq->reinject_work);
// 同步关闭并清理定时器。
timer_shutdown_sync(&sysrq->keyreset_timer);
// 从输入核心注销这个 handle。
input_unregister_handle(handle);
// 释放为这个设备分配的私有状态结构。
kfree(sysrq);
}

SysRq Keypress State Machine: sysrq_handle_keypress

本代码片段是“魔术 SysRq”功能的大脑——一个有限状态机 (Finite State Machine, FSM)。其核心功能是精确地跟踪键盘上 Alt, Shift, 和 SysRq 键的状态,识别出合法的 Alt + SysRq + <Command> 组合键序列,并将最终的命令字符分发给 __handle_sysrq 执行器。它通过“抑制”(suppress)输入事件,实现了对用户透明的组合键拦截,同时还包含了复杂的逻辑来处理按键的重新注入(re-injection)和系统复位序列的检测。

实现原理分析

此函数是 SysRq 机制的核心算法实现,它通过管理一个 sysrq_state 结构体来驱动状态转换。

  1. 核心状态:sysrq->active:

    • 这是状态机的核心标志位。false 表示“非激活”或“监听”状态;true 表示“激活”状态。
    • 进入激活状态: 当 SysRq 键被按下,并且一个 Alt 键当前正被按住时,状态机从“非激活”转换到“激活”。这是识别 SysRq 序列开始的关键。
    • 退出激活状态: 当启动了该序列的那个 Alt 键被释放时,状态机从“激活”转换回“非激活”。
  2. 状态变量的精确捕获:

    • alt, shift: 这两个变量实时跟踪当前 AltShift 键的按下状态。
    • alt_use, shift_use: 这两个变量是状态机的一个精妙之处。它们在状态机进入激活状态的那一刻,“快照”了当时的 altshift 状态。这意味着,即使在 SysRq 序列激活后用户按下了另一个 Alt 键或改变了 Shift 键的状态,也不会影响后续的命令处理。命令的大小写 (toupper) 只取决于进入激活状态时的 Shift 状态。
  3. 事件抑制与传递 (suppress):

    • 函数的返回值 suppress 几乎总是等于 sysrq->active
    • 激活时: suppresstruesysrq_filter 会“吃掉”所有按键事件,防止它们被发送到应用程序。
    • 非激活时: suppressfalse,所有按键事件都被正常传递。
    • 例外: 有一个特殊情况 if (value == 0 && test_and_clear_bit(code, sysrq->key_down))。它处理的是:如果一个键在进入 SysRq 模式之前就被按下了,那么当它在 SysRq 模式激活期间被释放时,这个“释放”事件应该被传递下去,而不是抑制。这防止了按键状态的混乱(例如,防止系统认为某个键一直被按住)。
  4. 按键重新注入 (need_reinjectreinject_work):

    • Alt+SysRq 被按下(进入激活状态)时,need_reinject 被设为 true
    • 如果此时用户按下了任何一个命令键need_reinject 会被清为 false
    • 当用户释放 Alt 键(退出激活状态)时,if (was_active) 条件满足,会调度 reinject_work 工作队列。这个工作队列的执行函数会检查 need_reinject 标志。如果它仍然是 true(意味着用户只按了 Alt+SysRq 而没有按命令键),工作队列就会手动地重新生成一个 Alt+SysRq 的按键事件并注入回输入系统。
    • 目的: 这确保了如果用户只是想使用 Alt+SysRq (PrintScreen) 的原始功能,这个按键组合仍然能够正常工作。SysRq 机制只在有命令键跟随的情况下,才“偷走”这个组合键。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
* @brief sysrq_handle_keypress - SysRq 的核心状态机,处理单个按键事件。
* @param sysrq: 指向当前设备的 sysrq_state 结构体。
* @param code: 按键码。
* @param value: 按键值 (1=按下, 0=释放, 2=重复)。
* @return bool: 如果此事件应被抑制,则返回 true。
*/
static bool sysrq_handle_keypress(struct sysrq_state *sysrq,
unsigned int code, int value)
{
bool was_active = sysrq->active; // 记录进入函数时的激活状态。
bool suppress;

switch (code) {

case KEY_LEFTALT:
case KEY_RIGHTALT:
if (!value) { // Alt 键被释放
// 如果 SysRq 正处于激活状态,并且被释放的正是启动序列的那个 Alt 键...
if (sysrq->active && code == sysrq->alt_use)
sysrq->active = false; // ...则退出激活状态。

sysrq->alt = KEY_RESERVED; // 标记 Alt 键已抬起。

} else if (value != 2) { // Alt 键被按下 (非重复)
sysrq->alt = code; // 记录哪个 Alt 键被按下。
sysrq->need_reinject = false; // 重置重注入标志。
}
break;

case KEY_LEFTSHIFT:
case KEY_RIGHTSHIFT:
if (!value)
sysrq->shift = KEY_RESERVED; // Shift 键被释放
else if (value != 2)
sysrq->shift = code; // Shift 键被按下
if (sysrq->active) // 如果已激活,立即更新捕获的 shift 状态
sysrq->shift_use = sysrq->shift;
break;

case KEY_SYSRQ:
// 如果 SysRq 键被按下,并且之前已经有 Alt 键被按下了...
if (value == 1 && sysrq->alt != KEY_RESERVED) {
sysrq->active = true; // ...则进入激活状态!
sysrq->alt_use = sysrq->alt; // 捕获是哪个 Alt 键启动了序列。
sysrq->shift_use = sysrq->shift; // 捕获按下 SysRq 时的 Shift 状态。
/*
* 如果没有其他键被按下,我们将需要重新注入 Alt-SysRq 键击。
*/
sysrq->need_reinject = true;
}

/*
* 假装 SysRq 键从未被按下过。这对于正确处理 KGDB 是必需的,
* 它在退出调试器后会尝试释放所有按键。
*/
if (sysrq->active)
clear_bit(KEY_SYSRQ, sysrq->handle.dev->key);

break;

default: // 其他任意按键
// 如果 SysRq 已激活,并且这是一个“按下”事件...
if (sysrq->active && value && value != 2) {
// 从转换表中查找按键码对应的命令字符。
unsigned char c = sysrq_xlate[code];

sysrq->need_reinject = false; // 已收到命令键,不再需要重注入。
// 根据进入激活状态时捕获的 Shift 状态,决定命令是否为大写。
if (sysrq->shift_use != KEY_RESERVED)
c = toupper(c);
// 执行 SysRq 命令!
__handle_sysrq(c, true);
}
break;
}

// 抑制标志通常与激活状态一致。
suppress = sysrq->active;

// 如果当前未激活...
if (!sysrq->active) {

/*
* 检查复位序列自上次以来是否已更改。
*/
if (sysrq->reset_seq_version != sysrq_reset_seq_version)
sysrq_parse_reset_sequence(sysrq);

/*
* 如果我们不抑制按键,则跟踪键盘状态,以便我们可以释放在
* 进入 SysRq 模式之前已按下的键。
*/
if (value)
set_bit(code, sysrq->key_down);
else
clear_bit(code, sysrq->key_down);

// 如果是刚刚从激活状态退出...
if (was_active)
// ...则调度工作队列来处理可能的按键重注入。
schedule_work(&sysrq->reinject_work);

/* 检查复位序列 */
sysrq_detect_reset_sequence(sysrq, code, value);

} else if (value == 0 && test_and_clear_bit(code, sysrq->key_down)) {
/*
* 对于在进入 SysRq 模式之前已按下的键,传递其“释放”事件。
*/
suppress = false;
}

return suppress;
}

SysRq Work Handlers: 密钥重新注入与系统重置

本代码片段展示了“魔术 SysRq”功能中两个异步执行的回调函数。其核心功能是:

  1. sysrq_reinject_alt_sysrq: 作为一个工作队列(work queue)处理函数,它负责处理一种特殊情况——当用户按下了 Alt+SysRq没有紧跟任何命令键时,此函数会以编程方式重新生成一个 Alt+SysRq 的按键事件,并将其注入回输入系统,从而保留该组合键的原始功能(通常是 PrintScreen)。
  2. sysrq_do_reset: 作为一个定时器(timer)回调函数,它是在一个特定的“复位序列”被成功检测到后,由定时器触发的最终执行动作,其唯一职责是调用 orderly_reboot()安全地重启系统

实现原理分析

这两个函数展示了内核中如何使用工作队列和定时器,将耗时或延迟的操作从中断的“快路径”中解耦出来。

  1. 按键重新注入 (sysrq_reinject_alt_sysrq):

    • 执行上下文: 这是一个 work_struct 的处理函数,意味着它在**内核线程(进程上下文)**中执行。这允许它安全地调用 input_inject_event 等可能涉及复杂锁或分配内存的函数。
    • 触发条件: sysrq_handle_keypress 状态机在检测到 Alt 键被释放(即 SysRq 序列结束)时,会调度这个工作。但是,此函数内部有一个关键的 if (sysrq->need_reinject) 检查。这个标志只有在 Alt+SysRq 被按下后、且没有任何命令键跟随的情况下,才会保持为 true
    • 核心机制 (input_inject_event): 此函数使用 input_inject_event 来模拟一个完整的 Alt+SysRq 按键的按下和释放过程。它精确地按顺序注入“Alt 按下”、“SysRq 按下”、“同步事件”、“SysRq 释放”、“Alt 释放”、“同步事件”,这对于上层(如图形界面)来说,与真实的物理按键操作完全无法区分。
    • 防无限循环 (reinjecting 标志与内存屏障):
      a. 在注入事件之前,它设置 sysrq->reinjecting = true;
      b. 在 sysrq_filter 函数中,第一个检查就是 if (sysrq->reinjecting) return false;
      c. 这个组合构成了一个临时的“旁路”,它告诉 sysrq_filter:“接下来的一系列事件是我自己生成的,请不要过滤它们”,从而完美地避免了过滤器捕获自己注入的事件而导致的无限循环。
      d. mb()(内存屏障)确保了对 reinjecting 标志的修改对其他 CPU(或被编译器重排)是立即可见的,保证了在注入事件之前标志已设置,在注入完成之后标志才被清除。
  2. 延迟复位 (sysrq_do_reset):

    • 执行上下文: 这是一个 timer_list 的处理函数,它在软中断(softirq)上下文中执行。
    • 触发条件: 它由 sysrq_handle_keypress 内部的 sysrq_detect_reset_sequence 逻辑来触发。该逻辑通常会启动一个短时间的定时器。如果用户在定时器到期前按下了复位序列中的下一个键,定时器会被重置;如果用户完成了整个序列,就会设置一个调用此 sysrq_do_reset 的最终定时器。
    • 核心机制 (orderly_reboot): 此函数的核心是调用 orderly_reboot()。这是一个内核提供的标准接口,它会尝试执行一个干净的重启流程(同步文件系统、关闭设备等),比直接触发硬件复位要安全得多。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* @brief sysrq_reinject_alt_sysrq - 重新注入 Alt+SysRq 按键事件的工作队列处理函数。
* @param work: 指向 work_struct 的指针。
*/
static void sysrq_reinject_alt_sysrq(struct work_struct *work)
{
// 从 work_struct 指针反向推导出其所属的 sysrq_state 结构。
struct sysrq_state *sysrq =
container_of(work, struct sysrq_state, reinject_work);
struct input_handle *handle = &sysrq->handle;
unsigned int alt_code = sysrq->alt_use; // 获取启动序列时使用的 Alt 键码。

// 仅在需要重注入时(即只按了 Alt+SysRq 而没有命令键)才执行。
if (sysrq->need_reinject) {
// 设置“正在重注入”标志,防止 sysrq_filter 拦截自己生成的事件。
sysrq->reinjecting = true;
// 内存屏障,确保对 reinjecting 的写入在注入事件之前对所有观察者可见。
mb();

/* 模拟按下和释放 Alt + SysRq */
input_inject_event(handle, EV_KEY, alt_code, 1); // Alt 按下
input_inject_event(handle, EV_KEY, KEY_SYSRQ, 1); // SysRq 按下
input_inject_event(handle, EV_SYN, SYN_REPORT, 1); // 同步事件,提交“按下”状态

input_inject_event(handle, EV_KEY, KEY_SYSRQ, 0); // SysRq 释放
input_inject_event(handle, EV_KEY, alt_code, 0); // Alt 释放
input_inject_event(handle, EV_SYN, SYN_REPORT, 1); // 同步事件,提交“释放”状态

// 内存屏障,确保在清除标志之前,所有注入事件的操作都已完成。
mb();
sysrq->reinjecting = false;
}
}

/**
* @brief sysrq_do_reset - 由定时器触发的、执行系统复位的回调函数。
* @param t: 指向触发此回调的 timer_list 的指针。
*/
static void sysrq_do_reset(struct timer_list *t)
{
// 从 timer_list 指针安全地推导出其所属的 sysrq_state 结构。
struct sysrq_state *state = timer_container_of(state, t,
keyreset_timer);

// 设置一个标志,表示复位请求已被确认(主要用于调试或状态查询)。
state->reset_requested = true;

// 调用内核的有序重启函数。
orderly_reboot();
}

SysRq 命令操作:全面定义集

本代码片段是“魔术 SysRq”功能的**“动作库”。其核心功能是为每一个 SysRq 命令字符(如 ‘b’ for reboot, ‘c’ for crash)定义一个对应的操作**。它通过一系列静态定义的 sysrq_key_op 结构体来实现,每个结构体都将一个命令的处理函数 (handler)、帮助信息 (help_msg) 和授权掩码 (enable_mask) 绑定在一起。最终,这些操作被组织到一个名为 sysrq_key_table 的**分发表(dispatch table)**中,供 __handle_sysrq 函数快速查找和调用。

实现原理分析

此代码是 SysRq 框架中“策略”与“机制”分离的典范。__handle_sysrq 提供了分发和授权的“机制”,而本代码片段则定义了所有具体的“策略”(即每个命令实际做什么)。

  1. struct sysrq_key_op:原子操作的封装:

    • 这个结构体是 SysRq 命令的基本单元。它将一个命令的所有相关信息打包在一起:
      • .handler: 一个函数指针,指向真正执行该命令的代码。这是最重要的成员。
      • .help_msg: 一段简短的文本,用于在用户请求帮助时(例如,输入一个未定义的 SysRq 键)显示。
      • .action_msg: 一段在命令即将执行时打印到控制台的消息,为用户提供明确的反馈。
      • .enable_mask: 一个标志位,用于将此命令与 /proc/sys/kernel/sysrq 中的特定控制位相关联。__handle_sysrq 会用这个掩码来检查命令是否被授权。
  2. 具体的操作实现 (sysrq_handle_*):

    • 代码为每个命令都定义了一个小型的、职责单一的处理函数。这些函数是 SysRq 强大功能的最终体现。例如:
      • sysrq_handle_reboot: 调用 emergency_restart() 来立即重启系统。它还特意关闭了锁依赖检查 (lockdep_off) 并开启了中断,以增加在系统严重不稳定时重启成功的概率。
      • sysrq_handle_crash: 非常直接,它调用 panic() 来主动触发内核崩溃(kernel panic)。这对于获取一份完整的 vmcore (内存转储) 以进行事后调试非常有用。
      • sysrq_handle_sync: 调用 emergency_sync(),强制将所有文件系统的脏数据同步到磁盘。
      • sysrq_handle_showstate: 调用 show_state(),打印出所有任务的当前状态和堆栈回溯,是调试死锁或系统挂起的首选工具。
      • sysrq_handle_term / kill: 遍历系统中的所有非内核线程的用户进程,并向它们发送 SIGTERMSIGKILL 信号。
      • sysrq_handle_moom: 通过工作队列异步触发一次“内存溢出杀手”(Out-Of-Memory Killer)。
  3. 分发表 (sysrq_key_table):

    • sysrq_key_table 是一个 sysrq_key_op 指针数组,它扮演着一个哈希表或分发表的角色。数组的索引与命令字符的 ASCII 值(或其偏移)相对应。
    • __handle_sysrq 接收到命令字符 key 时,它会通过一个转换函数(__sysrq_get_key_op,未在此展示)将 key 映射到这个数组的一个索引上,从而以 O(1) 的时间复杂度快速找到对应的操作结构。
    • 数组中的 NULL 条目表示该键当前没有分配任何 SysRq 操作,其中一些被保留给第三方模块(如内核调试器)动态注册。
  4. 条件编译的模块化:

    • 代码广泛使用了 #ifdef#else#endif 结构。这使得 SysRq 的功能集可以根据内核的编译配置进行裁剪。
    • 例如,只有在 CONFIG_SMP (多核支持) 被启用时,sysrq_showallcpus_op (显示所有 CPU 的回溯) 相关的代码才会被编译进去。同样,sysrq_showlocks_op 依赖于 CONFIG_LOCKDEP (锁依赖检查器)。这种设计确保了在不需要某些功能的配置下,相关的代码和数据结构不会占用任何内存。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/**
* @brief sysrq_handle_loglevel - SysRq handler for changing console loglevel.
* @param key: 命令字符 ('0'-'9').
*/
static void sysrq_handle_loglevel(u8 key)
{
u8 loglevel = key - '0';

// 临时将日志级别设为默认,以确保 pr_info 能打印出来。
console_loglevel = CONSOLE_LOGLEVEL_DEFAULT;
pr_info("Loglevel set to %u\n", loglevel);
// 设置新的日志级别。
console_loglevel = loglevel;
}
// '0'-'9'键的操作定义
static const struct sysrq_key_op sysrq_loglevel_op = {
.handler = sysrq_handle_loglevel,
.help_msg = "loglevel(0-9)",
.action_msg = "Changing Loglevel",
.enable_mask = SYSRQ_ENABLE_LOG,
};

// ... (其他 sysrq_handle_* 函数的实现,每个都对应一个具体的操作) ...

/**
* @brief sysrq_handle_reboot - SysRq handler for rebooting the system.
* @param key: 命令字符 'b'.
*/
static void sysrq_handle_reboot(u8 key)
{
lockdep_off(); // 关闭锁依赖检查,以增加在锁混乱状态下重启的成功率。
local_irq_enable(); // 确保中断是开启的,某些重启路径可能需要。
emergency_restart(); // 调用紧急重启函数。
}
// 'b'键的操作定义
static const struct sysrq_key_op sysrq_reboot_op = {
.handler = sysrq_handle_reboot,
.help_msg = "reboot(b)",
.action_msg = "Resetting",
.enable_mask = SYSRQ_ENABLE_BOOT,
};

// ... (sysrq_handle_sync, sysrq_handle_crash 等函数的实现) ...

/**
* @brief sysrq_handle_showstate - SysRq handler for showing task states.
* @param key: 命令字符 't'.
*/
static void sysrq_handle_showstate(u8 key)
{
show_state(); // 打印所有任务的状态和堆栈回溯。
show_all_workqueues(); // 打印所有工作队列的状态。
}
// 't'键的操作定义
static const struct sysrq_key_op sysrq_showstate_op = {
.handler = sysrq_handle_showstate,
.help_msg = "show-task-states(t)",
.action_msg = "Show State",
.enable_mask = SYSRQ_ENABLE_DUMP,
};

// ... (其他操作的定义) ...

/* Key Operations table and lock */
static DEFINE_SPINLOCK(sysrq_key_table_lock); // 用于保护动态注册/注销操作的锁

/**
* @var sysrq_key_table
* @brief SysRq 命令的分发表。
* 这是一个指针数组,将命令字符映射到对应的 sysrq_key_op 结构。
*/
static const struct sysrq_key_op *sysrq_key_table[62] = {
/* '0' to '9' */
&sysrq_loglevel_op, &sysrq_loglevel_op, &sysrq_loglevel_op,
&sysrq_loglevel_op, &sysrq_loglevel_op, &sysrq_loglevel_op,
&sysrq_loglevel_op, &sysrq_loglevel_op, &sysrq_loglevel_op,
&sysrq_loglevel_op,

/* 'a' to 'z' */
NULL, /* a */
&sysrq_reboot_op, /* b */
&sysrq_crash_op, /* c */
&sysrq_showlocks_op, /* d */
&sysrq_term_op, /* e */
&sysrq_moom_op, /* f */
NULL, /* g */
NULL, /* h (help) */
&sysrq_kill_op, /* i */
&sysrq_thaw_op, /* j */
&sysrq_SAK_op, /* k */
&sysrq_showallcpus_op, /* l */
&sysrq_showmem_op, /* m */
&sysrq_unrt_op, /* n */
NULL, /* o (off) */
&sysrq_showregs_op, /* p */
&sysrq_show_timers_op, /* q */
&sysrq_unraw_op, /* r */
&sysrq_sync_op, /* s */
&sysrq_showstate_op, /* t */
&sysrq_mountro_op, /* u */
NULL, /* v */
&sysrq_showstate_blocked_op, /* w */
NULL, /* x */
NULL, /* y */
&sysrq_ftrace_dump_op, /* z */

/* ... (大写字母的部分定义) ... */
};

SysRq 命令执行引擎: __handle_sysrq

本代码片段是“魔术 SysRq”功能的最终执行引擎。其核心功能是:接收一个命令字符(key),在一个受 RCU 保护的上下文中,查找与该字符对应的操作。如果找到,它会验证该操作是否被当前系统的 SysRq 掩码所允许(除非被告知跳过检查),然后打印一条信息性消息,并调用该操作的最终处理函数handler)。如果未找到对应的操作,它会打印一份包含所有可用命令的帮助信息。

实现原理分析

__handle_sysrq 是 SysRq 序列被识别后的终点,是所有魔术命令的“分发中心”。其实现体现了内核在执行潜在危险操作时的多重安全和同步保障。

  1. 授权检查 (check_mask):

    • __handle_sysrqcheck_mask 参数是一个关键的设计。它区分了两种调用来源:
      a. 物理键盘/控制台: 当用户通过 Alt+SysRq+Key 触发时,sysrq_handle_keypress 会调用 __handle_sysrq(c, true)check_mask = true 强制函数检查 sysrq_on_mask(op_p->enable_mask),即该命令是否被 /proc/sys/kernel/sysrq 的掩码所允许。
      b. /proc/sysrq-trigger: 当用户通过向 /proc/sysrq-trigger 写入一个字符来触发命令时,内核会调用 __handle_sysrq(c, false)check_mask = false 会绕过 sysrq_on_mask 检查,使得 root 用户总能通过这个接口执行任何 SysRq 命令,即使它对物理键盘是禁用的。这为远程管理提供了一个强大的后门。
  2. 强制控制台输出:

    • suppress_printk 是一个全局变量,可以静默内核消息。__handle_sysrq 首先保存其当前值,然后强制设为 0,并在函数结束时恢复。
    • printk_force_console_enter() / exit() 是一对更强的原语。它确保了在 __handle_sysrq 内部的所有 printk 调用都会绕过日志缓冲区,直接、同步地写入到物理控制台驱动。
    • 目的: 这两项措施确保了无论系统处于何种状态(即使 printk 被禁用或日志系统卡死),用户都能在控制台上立即看到 SysRq 命令的反馈,这在系统挂起时至关重要。
  3. RCU 安全的操作表访问:

    • 整个核心逻辑被 rcu_sysrq_start() / end()rcu_read_lock() / unlock() 包围。
    • __sysrq_get_key_op(key) 会在一个全局的 sysrq_key_table 数组中查找与 key 对应的 sysrq_key_op 结构。
    • 目的: 使用 RCU 保护对这个表的访问,是为了在理论上支持动态加载/卸载 SysRq 操作模块的场景。RCU 保证了在 rcu_read_lock 临界区内,op_p 指针所指向的 sysrq_key_op 结构是稳定有效的,不会被并发地释放。
  4. 命令分发与帮助信息:

    • 如果 __sysrq_get_key_op 成功找到了操作 op_p
      • 在通过授权检查后,它会先打印 op_p->action_msg(例如 “Rebooting”),然后调用核心的 op_p->handler(key) 函数指针,这才是真正执行重启、转储任务等操作的地方。
    • 如果未找到操作 op_p
      • 它会进入帮助模式,打印 “HELP : “,然后遍历整个 sysrq_key_table,将所有已注册操作的 help_msg 打印出来。
      • 内部的 j 循环是一个去重机制,确保对于有别名的命令(多个键指向同一个操作),其帮助信息只被打印一次。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* @brief __handle_sysrq - SysRq 命令的中央处理和分发函数。
* @param key: 要执行的命令字符。
* @param check_mask: 如果为 true,则检查命令是否被 sysrq 掩码允许。
*/
void __handle_sysrq(u8 key, bool check_mask)
{
const struct sysrq_key_op *op_p; /// < 指向查找到的 SysRq 操作的指针。
int orig_suppress_printk; /// < 用于保存原始的 printk 抑制状态。
int i;

// 保存并重置 printk 抑制状态,确保 SysRq 输出总是可见。
orig_suppress_printk = suppress_printk;
suppress_printk = 0;

// 进入 SysRq 特有的 RCU 读侧临界区。
rcu_sysrq_start();
rcu_read_lock();
/*
* 进入强制控制台输出模式,以便 SysRq 头部信息能显示出来,
* 为用户提供明确的反馈。
*/
printk_force_console_enter();

// 根据命令字符查找对应的操作。
op_p = __sysrq_get_key_op(key);
if (op_p) { // 如果找到了有效的操作...
/*
* 是否应该检查启用的操作 (来自 /proc/sysrq-trigger 的调用不应检查),
* 以及被调用的操作是否已启用?
*/
if (!check_mask || sysrq_on_mask(op_p->enable_mask)) {
// 如果不需要检查掩码,或者命令已被掩码允许...
pr_info("%s\n", op_p->action_msg); // 打印将要执行的动作信息。
printk_force_console_exit(); // 退出强制控制台模式,handler可能会耗时。
op_p->handler(key); // 调用真正的处理函数!
} else {
// 如果命令被掩码禁用...
pr_info("This sysrq operation is disabled.\n");
printk_force_console_exit();
}
} else { // 如果没有找到与 key 对应的操作...
pr_info("HELP : "); // ...则打印帮助信息。
// 遍历整个 SysRq 操作表。
for (i = 0; i < ARRAY_SIZE(sysrq_key_table); i++) {
if (sysrq_key_table[i]) {
int j;

// 这是一个去重循环:确保每个唯一的 handler 的帮助只被打印一次。
for (j = 0; sysrq_key_table[i] !=
sysrq_key_table[j]; j++)
;
if (j != i)
continue;
// 使用 pr_cont 在同一行继续打印。
pr_cont("%s ", sysrq_key_table[i]->help_msg);
}
}
pr_cont("\n");
printk_force_console_exit();
}
rcu_read_unlock();
rcu_sysrq_end();

// 恢复原始的 printk 抑制状态。
suppress_printk = orig_suppress_printk;
}

SysRq 重置序列检测器: sysrq_detect_reset_sequence

本代码片段是“魔术 SysRq”复位功能的核心状态机和事件检测器。其主要功能是:在每次按键事件发生时被调用,通过精确地跟踪一个预定义按键序列(例如 Ctrl+Alt+Delete)的按下和释放状态,来判断用户是否正在尝试执行一个硬件无关的系统复位。它实现了一个严格的“所有键必须同时按住”的逻辑,并包含了防止意外触发的安全机制。

实现原理分析

此函数是一个典型的、在事件驱动的“热路径”中运行的有限状态机(FSM)。它的状态由 state->reset_seq_cnt(当前有多少个序列键被按住)和 state->reset_canceled(序列检测是否被临时禁用)共同定义。

  1. “闯入者”按键导致序列失效:

    • if (!test_bit(code, state->reset_keybit)): 这是函数的第一道检查。它利用 sysrq_parse_reset_sequence 预先生成的位图,以 O(1) 的效率判断当前按下的键是否属于预定义的复位序列。
    • 如果按键属于序列,并且当前已经有部分序列键被按下(state->reset_seq_cnt > 0),那么这个“闯入者”按键会立即触发两个动作:
      a. state->reset_canceled = true;: 将序列设置为“已取消”状态。
      b. timer_delete(&state->keyreset_timer);: 如果一个最终的复位倒计时已经启动,立即将其取消。
    • 原理: 这实现了一个非常严格的安全策略:一旦开始输入复位序列,任何不相关的按键都会立即中止该序列。
  2. “序列键释放”导致倒计时中止:

    • else if (value == 0): 这个分支处理属于复位序列的某个键被释放的事件。
    • timer_delete(&state->keyreset_timer);: 释放任何一个序列键都会立即取消复位倒计时。这强制要求用户必须同时按住所有序列键达到指定时长才能触发复位。
    • if (--state->reset_seq_cnt == 0) state->reset_canceled = false;: 它递减当前按下的序列键计数。如果计数归零(意味着所有序列键都已被释放),它会清除 reset_canceled 标志,从而重新使能对下一次复位序列的检测。
  3. “序列键集齐”触发最终动作:

    • else if (value == 1): 这个分支处理属于复位序列的某个键被按下(非重复)的事件。
    • if (++state->reset_seq_cnt == state->reset_seq_len && !state->reset_canceled): 这是成功的触发条件,必须同时满足:
      a. 递增计数器后,其值恰好等于序列的总长度(state->reset_seq_len)。
      b. 并且,序列当前未处于“已取消”状态。
    • sysrq_handle_reset_request(state);: 如果条件满足,则调用一个辅助函数(未在此处展示)。这个函数通常会启动一个短暂的定时器,如果在这个定时器到期前没有任何键被释放,定时器的回调函数 sysrq_do_reset 就会被执行,从而触发系统重启。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* @brief sysrq_detect_reset_sequence - 检测用户是否正在输入系统复位按键序列。
* @param state: 指向当前设备的 sysrq_state 结构体。
* @param code: 按键码。
* @param value: 按键值 (1=按下, 0=释放, 2=重复)。
*/
static void sysrq_detect_reset_sequence(struct sysrq_state *state,
unsigned int code, int value)
{
// 检查当前按键是否属于预定义的复位序列。
if (!test_bit(code, state->reset_keybit)) {
/*
* 按下任何不在复位序列中的键都会取消该序列。
* 同时,也取消定时器,以防在请求复位后又按下了其他键。
*/
// 如果是一个“按下”事件,并且我们已在序列中(计数器>0)...
if (value && state->reset_seq_cnt) {
// ...则将序列标记为已取消,并删除可能已启动的复位定时器。
state->reset_canceled = true;
timer_delete(&state->keyreset_timer);
}
} else if (value == 0) {
/*
* 按键释放 - 复位序列中的所有键都需要被持续按住,
* 复位倒计时才会生效。
*/
// 释放任何一个序列键,都会立即取消复位定时器。
timer_delete(&state->keyreset_timer);

// 将按下的序列键计数减一。如果计数归零...
if (--state->reset_seq_cnt == 0)
// ...则清除“已取消”标志,为下一次序列检测做好准备。
state->reset_canceled = false;
} else if (value == 1) {
/* 按键按下,非自动重复 */
// 将按下的序列键计数加一。如果计数等于序列总长度,并且序列未被取消...
if (++state->reset_seq_cnt == state->reset_seq_len &&
!state->reset_canceled) {
// ...则触发处理复位请求的下一步(通常是启动最终的倒计时)。
sysrq_handle_reset_request(state);
}
}
}

static void sysrq_handle_reset_request(struct sysrq_state *state)
{
if (state->reset_requested)
__handle_sysrq(sysrq_xlate[KEY_B], false);

if (sysrq_reset_downtime_ms)
mod_timer(&state->keyreset_timer,
jiffies + msecs_to_jiffies(sysrq_reset_downtime_ms));
else
sysrq_do_reset(&state->keyreset_timer);
}

SysRq 重置序列解析器: sysrq_parse_reset_sequence

本代码片段展示了“魔术 SysRq”功能中一个关键的预处理和状态同步函数。其核心功能是:当 SysRq 的全局复位序列配置发生变化时,此函数被调用来解析这个全局配置,并将其缓存到一个 per-device 的 sysrq_state 结构体中。在这个过程中,它会将序列从一个 keycode 数组转换成一个更高效的位图(bitmap),并执行一项重要的安全检查,以防止在用户已经按下部分复位键的情况下意外触发系统重启。

实现原理分析

此函数是 SysRq 复位功能的一个性能和安全优化。它避免了在每次按键时都去遍历全局配置数组,而是将配置“编译”成一种更适合实时检测的格式。

  1. 版本驱动的懒惰更新(Lazy Update):

    • 此函数并在每次按键时都被调用。在 sysrq_handle_keypress 中,有一个 if (state->reset_seq_version != sysrq_reset_seq_version) 的检查。
    • sysrq_reset_seq_version 是一个全局版本号,只有当系统管理员通过 sysctl 等方式修改了全局 sysrq_reset_seq 数组时,这个版本号才会增加。
    • state->reset_seq_version 存储了当前 sysrq_state 上一次同步配置时的版本号。
    • 这个机制确保了 sysrq_parse_reset_sequence 这个相对耗时(因为它有一个循环)的函数,仅在配置真正发生变化时才被执行一次,是一种高效的“懒惰更新”或“缓存失效”策略。
  2. 数据结构优化(数组到 Bitmap):

    • 函数的核心是一个 for 循环,它遍历全局的 sysrq_reset_seq 数组(一个 keycode 数组)。
    • 对于序列中的每一个有效 key,它调用 __set_bit(key, state->reset_keybit)
    • state->reset_keybit 是一个位图。这个转换的意义在于,后续在“热路径”中(即 sysrq_detect_reset_sequence 函数),检查一个按下的键是否属于复位序列,就可以从一个 O(N) 的数组搜索,变成一个 O(1) 的 test_bit 操作,极大地提升了性能。
  3. 关键的安全机制 (reset_canceled):

    • 在构建位图的同时,函数还执行了 if (test_bit(key, state->key_down)) state->reset_seq_cnt++;
    • state->key_down 是一个实时记录当前设备上所有被按下的按键的位图。
    • 这个检查的含义是:“在同步这个新的复位序列配置的时刻,序列中的某个键是否已经被用户按下了?”
    • state->reset_canceled = state->reset_seq_cnt != 0; 这一行是最终的安全策略。如果 reset_seq_cnt 不为零,意味着存在“预先按下的键”,此时 reset_canceled 标志被设置。
    • sysrq_detect_reset_sequence 函数会检查这个标志,如果它被设置,则完全禁用复位序列的检测,直到所有预先按下的键都被释放。这可以有效地防止用户在无意中(例如,正在按 Ctrl+Alt+F1 切换终端时)意外地触发一个以 Ctrl+Alt 开头的复位序列。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @brief sysrq_parse_reset_sequence - 解析全局复位序列并更新 per-device 状态。
* @param state: 指向要更新的 sysrq_state 结构体。
*/
static void sysrq_parse_reset_sequence(struct sysrq_state *state)
{
int i;
unsigned short key;

// 重置当前已按下的复位序列键的计数器。
state->reset_seq_cnt = 0;

// 遍历全局定义的复位序列数组。
for (i = 0; i < sysrq_reset_seq_len; i++) {
key = sysrq_reset_seq[i];

// 对 keycode 进行有效性检查。
if (key == KEY_RESERVED || key > KEY_MAX)
break;

// 将该键添加到 per-device 的复位键位图中,用于快速查找。
__set_bit(key, state->reset_keybit);
// 更新 per-device 的序列长度。
state->reset_seq_len++;

// 检查这个属于复位序列的键,当前是否已经被按下了。
if (test_bit(key, state->key_down))
// 如果是,则增加计数器。
state->reset_seq_cnt++;
}

/*
* 在旧的按键被释放之前,禁用复位功能。
* 这是一个安全措施,防止意外触发。
*/
state->reset_canceled = state->reset_seq_cnt != 0;

// 更新 per-device 的版本号,表示已同步到最新的全局配置。
state->reset_seq_version = sysrq_reset_seq_version;
}

SysRq Procfs 触发接口:/proc/sysrq-trigger

本代码片段展示了“魔术 SysRq”功能的程序化触发接口。其核心功能是:在 /proc 文件系统中创建一个名为 sysrq-trigger只写文件。通过向这个文件写入一个命令字符,root 用户可以以编程方式触发任何 SysRq 命令,并且这个操作会绕过 /proc/sys/kernel/sysrq 的权限掩码检查。它还支持一个特殊的“批量模式”,允许一次性触发多个命令。

实现原理分析

此机制为系统管理员和自动化脚本提供了一个比物理键盘更强大、更灵活的 SysRq 触发方式。其实现原理基于 procfs 的自定义文件操作。

  1. Procfs 文件创建 (sysrq_init_procfs):

    • sysrq_init_procfs 函数通过 proc_create/proc 根目录下创建 sysrq-trigger 文件。
    • 权限: 权限被设置为 S_IWUSR (0200),意味着只有文件的所有者(即 root 用户)有权写入。这个文件是不可读的。
    • 绑定操作: proc_create 将这个文件与 sysrq_trigger_proc_ops 结构关联起来,该结构指定了 write_sysrq_trigger 作为其写操作的处理函数。
  2. 自定义写操作 (write_sysrq_trigger):

    • root 用户执行 echo 'b' > /proc/sysrq-trigger 时,write_sysrq_trigger 函数被调用。
    • 安全的用户空间拷贝: 它通过一个循环和 get_user(c, buf + i) 来逐个字符地、安全地从用户空间缓冲区拷贝数据。这是防止内核被恶意用户空间指针欺骗的关键安全措施。
    • 默认行为(单命令): 默认情况下,bulk 标志为 false。函数在处理完第一个非下划线的字符后,就会 break 退出循环。这意味着 echo 'bcs'echo 'b' 的效果是完全相同的,都只触发了 ‘b’ 命令。
    • 批量模式(Bulk Mode):
      a. 如果写入的第一个字符是下划线 _bulk 标志被设为 true
      b. 下划线本身不触发任何命令。
      c. 在此之后,if (!bulk) break; 条件将不再满足,循环会继续处理后续的所有字符,并依次为每个字符调用 __handle_sysrq。例如,echo '_sub' > /proc/sysrq-trigger 会依次触发 Sync, Unmount, 和 Reboot。
    • 特权调用: 最关键的一步是 __handle_sysrq(c, false)。它调用了 SysRq 的中央执行引擎,并将 check_mask 参数设为 false。如前文分析,这会完全绕过 /proc/sys/kernel/sysrq 中设置的权限掩码。这意味着,即使用户通过 sysctl 完全禁用了物理键盘的 SysRq 功能(sysrq=0),root 用户仍然可以通过 sysrq-trigger 接口执行任何命令。
  3. 初始化流程 (sysrq_init):

    • 此函数通过 device_initcall 注册,在设备驱动初始化阶段被调用。
    • 它首先调用 sysrq_init_procfs() 来创建 /proc/sysrq-trigger 文件。
    • 然后,它检查 sysrq_on()。这个检查的目的是,如果内核通过启动参数(如 sysrq_always_on=1)被配置为默认开启 SysRq,那么它会立即调用 sysrq_register_handler() 来注册物理键盘的输入句柄。这确保了两种触发方式都能在系统启动后按预期工作。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#ifdef CONFIG_PROC_FS
/*
* 向 /proc/sysrq-trigger 写入 'C' 就像按下 sysrq-C。
* 通常,只有写入的第一个字符会被处理。
* 但是,如果第一个字符是下划线,则所有字符都会被处理。
*/
/**
* @brief write_sysrq_trigger - /proc/sysrq-trigger 的写操作处理函数。
* @param file: 文件对象指针。
* @param buf: 指向用户空间数据缓冲区的指针。
* @param count: 要写入的字节数。
* @param ppos: 文件偏移量指针 (未使用)。
* @return ssize_t: 成功则返回消耗的字节数,失败返回错误码。
*/
static ssize_t write_sysrq_trigger(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
bool bulk = false; // 批量模式标志。
size_t i;

// 遍历用户写入的所有字符。
for (i = 0; i < count; i++) {
char c;

// 安全地从用户空间获取一个字符。
if (get_user(c, buf + i))
return -EFAULT;

if (c == '_')
// 如果字符是下划线,则启用批量模式。
bulk = true;
else
// 否则,调用 SysRq 的中央执行引擎。
// check_mask 设为 false,以绕过 sysctl 的权限掩码检查。
__handle_sysrq(c, false);

// 如果不是批量模式,则处理完第一个字符后立即退出。
if (!bulk)
break;
}

// 告知上层,所有写入的字节都已被“消耗”。
return count;
}

// 定义 /proc/sysrq-trigger 的 proc 文件操作集。
static const struct proc_ops sysrq_trigger_proc_ops = {
.proc_write = write_sysrq_trigger, // 只定义了写操作。
.proc_lseek = noop_llseek, // lseek 是一个空操作。
};

/**
* @brief sysrq_init_procfs - 初始化 /proc/sysrq-trigger 文件。
*/
static void sysrq_init_procfs(void)
{
// 创建 /proc/sysrq-trigger 文件,权限为 0200 (仅属主可写)。
if (!proc_create("sysrq-trigger", S_IWUSR, NULL,
&sysrq_trigger_proc_ops))
pr_err("Failed to register proc interface\n");
}

#else // 如果 CONFIG_PROC_FS 未定义

// 提供一个空的内联函数,以避免编译错误。
static inline void sysrq_init_procfs(void)
{
}

#endif /* CONFIG_PROC_FS */

/**
* @brief sysrq_init - SysRq 子系统的主要初始化函数之一。
* @return int: 始终返回0。
*/
static int __init sysrq_init(void)
{
// 初始化 procfs 接口。
sysrq_init_procfs();

// 如果 SysRq 功能在启动时默认是开启的...
if (sysrq_on())
// ...则立即注册物理键盘的输入处理句柄。
sysrq_register_handler();

return 0;
}
// 使用 device_initcall 宏,确保该初始化函数在设备驱动初始化阶段被调用。
device_initcall(sysrq_init);