Linux下输出自定义扫描码的键盘中断处理器失效问题排查
嘿,我帮你梳理下这两个问题的核心原因,还有对应的修复办法:
一、为什么读取到的扫描码始终是0x7a(释放状态)?
你的中断处理函数犯了一个致命错误:完全没读取键盘数据寄存器(0x60)里的内容。键盘发送中断后,必须读取数据寄存器的数值来清除中断请求,不然键盘会一直重复触发中断,而且缓冲区里的旧数据会一直滞留在那里。0x7a大概率是你替换中断前最后一个按键的释放扫描码,因为没被读取,所以每次中断都会读到这个旧值。
另外,你直接操作irq_desc结构体来获取原中断处理函数并调用free_irq的方式非常不规范,属于内核内部结构的非法操作。原键盘驱动的中断逻辑可能还在运行,这种粗暴的卸载会导致系统状态混乱,进一步加重异常情况。
二、为什么自定义扫描码无法被输出?
这个问题更直接:你的outb调用参数写反了!Linux内核里outb的用法是outb(要写入的数据, 目标端口),你写的outb(KBD_DATA_REG, scancode)相当于把端口号(0x60)写到了scancode对应的内存地址,而不是把自定义扫描码发送到键盘数据寄存器。这就导致你根本没把想要的扫描码传给键盘控制器,终端自然收不到预期的输入。
除此之外,你还忽略了键盘控制器的ACK应答机制,直接发送扫描码可能会被控制器忽略;而且终端获取键盘输入是依赖内核输入子系统的,直接往数据寄存器写扫描码其实不是注入输入的标准方式(不过这是进阶问题,先解决眼前的错误再说)。
修复后的代码示例
下面是修正了核心错误的代码,同时采用了更安全的方式替换和恢复键盘中断处理(注:这种方式在现代内核中可能存在兼容性问题,因为键盘驱动架构已经更复杂,但足以验证你的核心逻辑):
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/interrupt.h> #include <linux/irq.h> #include <asm/io.h> MODULE_LICENSE("GPL"); #define KBD_IRQ 1 /* 键盘对应的IRQ号 */ #define KBD_DATA_REG 0x60 /* 键盘数据I/O端口 */ #define KBD_STATUS_REG 0x64 /* 键盘状态I/O端口 */ #define KBD_SCANCODE_MASK 0x7f /* 用于清除扫描码的释放位 */ static int current_index = 0; static int text_len = 2; static unsigned char scancodes[2] = {0x15, 0x1e}; /* 'Q'和'A'的按下扫描码 */ static struct irqaction *original_kbd_action; /* 保存原键盘中断处理函数 */ static irqreturn_t keyboard_intr_handler(int irq, void *dev_id) { unsigned char original_scancode; unsigned char custom_scancode; // 1. 必须先读取原始扫描码,清除键盘的中断请求 original_scancode = inb(KBD_DATA_REG); printk(KERN_INFO "[KBD MOD] 原始扫描码: 0x%x\n", original_scancode); // 2. 选择要输出的自定义扫描码,循环切换 custom_scancode = scancodes[current_index]; // 如果原始是释放扫描码,我们也生成对应的自定义释放码(最高位设为1) if (original_scancode & 0x80) { custom_scancode |= 0x80; } printk(KERN_INFO "[KBD MOD] 自定义扫描码: 0x%x\n", custom_scancode); // 3. 正确写入自定义扫描码到键盘数据寄存器 outb(custom_scancode, KBD_DATA_REG); // 4. 等待键盘控制器处理完成(可选,但能提升稳定性) while (inb(KBD_STATUS_REG) & 0x02); current_index = (current_index + 1) % text_len; // 如果需要让原处理函数继续处理,可以调用original_kbd_action->handler(irq, dev_id) // 这里我们完全替换输入,所以返回IRQ_HANDLED return IRQ_HANDLED; } static int __init my_register_interrupt(void) { struct irq_desc *desc = irq_to_desc(KBD_IRQ); int ret; if (!desc) { printk(KERN_ERR "[KBD MOD] 无法获取IRQ %d的描述符\n", KBD_IRQ); return -EINVAL; } // 保存原键盘中断处理(仅作演示,实际环境建议用更安全的方式) original_kbd_action = desc->action; if (!original_kbd_action) { printk(KERN_ERR "[KBD MOD] 未找到原IRQ处理函数\n"); return -EINVAL; } // 卸载原键盘中断处理 free_irq(KBD_IRQ, original_kbd_action->dev_id); // 注册自定义中断处理(注意:键盘中断通常不支持共享,所以去掉IRQF_SHARED) ret = request_irq(KBD_IRQ, keyboard_intr_handler, 0, "custom_kbd_handler", NULL); if (ret) { printk(KERN_ERR "[KBD MOD] 注册IRQ %d失败: %d\n", KBD_IRQ, ret); // 失败时恢复原中断处理,避免键盘失效 request_irq(KBD_IRQ, original_kbd_action->handler, original_kbd_action->flags, original_kbd_action->name, original_kbd_action->dev_id); return ret; } printk(KERN_INFO "[KBD MOD] 自定义键盘中断处理已注册\n"); return 0; } static void __exit my_unregister_interrupt(void) { // 卸载自定义中断处理 free_irq(KBD_IRQ, NULL); // 恢复原键盘中断处理,保证系统正常使用键盘 if (original_kbd_action) { request_irq(KBD_IRQ, original_kbd_action->handler, original_kbd_action->flags, original_kbd_action->name, original_kbd_action->dev_id); printk(KERN_INFO "[KBD MOD] 已恢复原键盘中断处理\n"); } } module_init(my_register_interrupt); module_exit(my_unregister_interrupt);
关键修复点说明
- 修正
outb参数顺序:把错误的outb(KBD_DATA_REG, scancode)改成outb(scancode, KBD_DATA_REG),确保自定义扫描码被正确发送到键盘数据寄存器。 - 强制读取原始扫描码:每次中断必须读取
KBD_DATA_REG,否则键盘会持续触发中断,导致重复读取旧数据。 - 处理释放扫描码:同步处理按下和释放状态,保证输入逻辑的完整性,避免出现按键“卡住”的假象。
- 安全恢复原中断:模块退出时恢复原键盘中断处理,防止系统失去键盘功能。
- 移除错误的
IRQF_SHARED标志:键盘中断通常不支持共享,注册时添加这个标志会导致注册失败或异常。
内容的提问来源于stack exchange,提问作者Gili Jacobi




