[toc]
drivers/input/input.c 输入子系统核心(Input Subsystem Core) 统一所有输入设备的事件处理框架
历史与背景
这项技术是为了解决什么特定问题而诞生的?
这项技术以及它所构建的整个输入子系统,是为了解决在Linux早期一个极其混乱的问题:缺乏一个统一的、标准化的方式来处理各种各樣的输入设备。
- 接口不统一:在输入子系统出现之前,每一种输入设备(PS/2鼠标、串口鼠标、AT键盘等)都有其自己独特的驱动程序,这些驱动会创建各自不同的设备文件(如
/dev/psaux,/dev/mouse),并使用自己私有的数据协议。 - 应用程序复杂性:这意味着用户空间的应用程序(尤其是X Window System)必须编写大量复杂的、针对特定硬件的代码,才能从这些不同的设备文件中读取和解析输入数据。更换一个鼠标,就可能需要重新配置X Window。
- 可扩展性差:每当出现一种新的输入设备,不仅需要编写内核驱动,还需要修改上层的应用程序才能支持它。
输入子系统(由Vojtech Pavlik创建)的诞生,就是为了彻底解决这个乱局。它的核心目标是:在内核中创建一个通用的事件处理层,将所有物理输入设备的硬件差异完全屏蔽掉,并向用户空间提供一个单一的、标准的、统一的事件流接口。
它的发展经历了哪些重要的里程碑或版本迭代?
- 诞生与架构确立:输入子系统的创建本身就是最重要的里程碑。它确立了延续至今的三层核心架构:设备驱动 -> 输入核心 -> 事件处理器。
evdev的普及:事件处理器(Event Handler)中的evdev(Event Device)成为了事实上的标准。它为每个输入设备创建了一个/dev/input/eventX设备文件,并提供了一个包含struct input_event的原始事件流。这使得用户空间(如X.Org, Wayland, libinput)有了一个稳定、通用的API。- 支持新设备类型:输入子系统的设计极具前瞻性。它不仅支持传统的键盘鼠标,还能轻松地扩展以支持新的设备类型,如触摸屏、触摸板、數位板、游戏手柄、加速计、旋轉編碼器等,而无需修改核心框架。
- 多点触控(Multitouch)协议:为了支持现代智能手机和平板电脑的多点触摸屏,输入子系统标准化了一套多点触控(MT)协议,使得驱动程序可以用一种标准的方式上报多个触控点的信息。
目前该技术的社区活跃度和主流应用情况如何?
输入子系统是Linux内核中最成熟、最稳定、应用最广泛的子系统之一。
- 社区活跃度:其核心代码(
input.c)非常稳定,很少有大的改动。社区的开发活动主要集中在为层出不穷的新硬件编写新的设备驱动,以及改进libinput等用户空间库。 - 主流应用:它是所有现代Linux系统的基石。
- 桌面环境:X.Org和Wayland都通过
libinput库与evdev接口交互,来获取所有输入事件。 - Android:Android的整个输入框架(InputFlinger)都构建在内核输入子系统之上。
- 嵌入式系统:任何带有按钮、触摸屏或键盘的嵌入式Linux设备,都在使用这个子系统。
- 桌面环境:X.Org和Wayland都通过
核心原理与设计
它的核心工作原理是什么?
输入子系统的核心是一个分层的、事件驱动的“集线器-分发器”模型。
底层:设备驱动 (Device Drivers)
- 这是与具体硬件打交道的一层(例如
drivers/input/mouse/psmouse.c,drivers/hid/usbhid/)。 - 驱动程序的唯一职责是:读取硬件产生的信号(如鼠标移动、按键按下),并将其翻译成标准的、与硬件无关的**
struct input_event**事件。 - 一个
input_event结构体非常简单,只包含三个部分:type(事件类型,如EV_KEY按键,EV_REL相对位移),code(事件代码,如KEY_A,REL_X), 和value(事件的值,如1表示按下,-5表示向左移动5个单位)。 - 驱动程序通过调用
input_report_*()函数(如input_report_key())将这些事件“提交”给输入核心,并通过input_sync()表示一批相关事件(如一次鼠标移动的X和Y值)已发送完毕。
- 这是与具体硬件打交道的一层(例如
中层:输入核心 (Input Core)
- 这是整个子系统的心脏,主要由
drivers/input/input.c实现。 - 它维护着一个已注册的输入设备列表(
input_dev)和一个已注册的事件处理器列表(input_handler)。 - 当一个设备驱动注册自己时,输入核心会尝试将其与所有兼容的事件处理器进行“配对”和“连接”。
- 它的主要工作是接收来自所有设备驱动的
input_event事件,然后像一个交换机一样,将这些事件分发给所有与该设备连接的事件处理器。
- 这是整个子系统的心脏,主要由
上层:事件处理器 (Event Handlers)
- 这是连接内核与用户空间的桥梁(例如
drivers/input/evdev.c,mousedev.c)。 - 最重要和最常用的是
evdev。当它与一个输入设备连接后,会创建一个对应的字符设备文件,如/dev/input/event0。 - 当
evdev从输入核心接收到事件时,它就把这些input_event结构体原封不动地写入到对应的设备文件中。 - 用户空间的程序(如
libinput)通过read()这个设备文件,就可以获得一个统一的、原始的、包含所有输入设备信息的事件流。
- 这是连接内核与用户空间的桥梁(例如
它的主要优势体现在哪些方面?
- 终极的硬件抽象:应用程序开发者无需关心输入设备是USB的、蓝牙的还是PS/2的,他们面对的是完全相同的
evdev接口。 - 高度模块化:编写一个新的硬件驱动变得非常简单,因为它只需要关注如何生成标准的
input_event,而无需关心如何与用户空间交互。 - 灵活性和可组合性:一个物理设备可以上报多种类型的事件。例如,一个带有多媒体键的键盘,可以同时上报
EV_KEY(普通按键)和EV_MSC(杂项)事件。 - “机制,而非策略”:内核只负责可靠地传递原始事件(机制),而将所有复杂的解释和处理(策略),如手势识别、指针加速、掌压误触消除等,都留给了用户空间的库(如
libinput)去实现。
它存在哪些已知的劣劣势、局限性或在特定场景下的不适用性?
- 接口过于底层:
evdev提供的是非常原始的事件流,用户空间需要做大量的工作来解释它们,才能得到有意义的结果。但这通常被认为是一个优点,因为它保持了内核的简洁。 - 不适用于非输入设备:它的设计完全是针对“人机交互”事件的。对于非交互式的传感器数据(如温度、气压),应该使用IIO(Industrial I/O)子系统。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
在Linux系统中,任何需要处理旨在与用户或应用程序进行交互的事件的硬件,都必须通过输入子系统。它是唯一且标准的解决方案。
- 图形用户界面(GUI):Wayland合成器或X Server通过
libinput从/dev/input/eventX读取事件,来移动鼠标指针、响应键盘输入、处理触摸屏手势。 - 游戏:游戏程序直接或通过SDL等库读取游戏手柄、摇杆的事件,来控制游戏角色。
- 嵌入式控制面板:一个带有物理按钮和旋钮的工业设备,其内核驱动将按钮的按下/弹起和旋钮的旋转转换为
EV_KEY和EV_REL事件,上层的控制程序读取这些事件来调整设备参数。
是否有不推荐使用该技术的场景?为什么?
- 传感器数据采集:一个温度传感器周期性地产生测量值。这种数据不是“事件”,而是一种连续的测量。它应该使用IIO(Industrial I/O)子系统,该子系统专为处理这类传感器数据而设计。
- 纯数据通信:一个用于数据传输的串口或USB端点,它传输的是通用的数据流,而不是结构化的用户输入事件。它应该使用TTY或USB核心提供的标准接口。
对比分析
请将其 与 其他相似技术 进行详细对比。
对比一:输入子系统 vs. IIO (Industrial I/O) 子系统
| 特性 | 输入子系统 (Input Subsystem) | IIO (Industrial I/O) 子系统 |
|---|---|---|
| 设计目标 | 处理异步的、由用户触发的事件,用于人机交互。 | 处理传感器测量数据,通常是物理世界的量化值。 |
| 数据模型 | 事件驱动 (type, code, value),例如“A键按下”。 |
数据通道驱动,例如“通道0的值是25.3”(摄氏度)。 |
| 交互模式 | 用户产生输入,系统响应。 | 系统读取传感器,获取关于环境的信息。 |
| 用户空间接口 | /dev/input/eventX |
/sys/bus/iio/devices/iio:deviceX/ (sysfs接口) |
| 典型设备 | 键盘、鼠标、触摸屏、游戏手柄、按钮。 | 加速计、陀螺仪、温度传感器、光线传感器、ADC。 |
| 核心问题 | “发生了什么?” | “数值是多少?” |
对比二:输入子系统 vs. “前输入子系统时代” (The Old Way)
| 特性 | 输入子系统 | “前输入子系统时代” |
|---|---|---|
| 内核驱动 | 驱动程序只需生成标准input_event,与Input Core对话。 |
驱动程序需要自己创建设备文件,并定义私有协议。 |
| 用户空间接口 | 统一的/dev/input/eventX,标准的read()接口。 |
混乱的设备文件 (/dev/psaux, /dev/ttyS0等),需要特定的库或代码来解析。 |
| 应用程序 | 只需支持evdev协议即可支持所有设备。 |
需要为不同类型的设备编写或配置不同的后端。 |
| 可维护性 | 高。清晰的分层,模块化。 | 极低。紧耦合,代码重复,难以支持新硬件。 |
Input Subsystem Procfs Interface:提供设备与处理器的诊断视图
本代码片段是Linux Input子系统procfs接口的完整实现。其核心功能是在/proc/bus/input/目录下创建devices和handlers两个虚拟文件。这两个文件为开发者和系统管理员提供了一个强大且人类可读的实时诊断工具,用于:1) 查看系统中所有已注册的输入设备(键盘、鼠标、触摸屏等)的详细属性、能力和当前状态;2) 列出所有可用的输入事件处理器(handlers),如evdev。这个接口是调试输入设备问题的关键入口点。
实现原理分析
该功能的实现完全基于内核的seq_file接口,这是一种为生成大型虚拟文件而设计的、高效且内存友好的标准机制。seq_file避免了一次性在内核中分配巨大缓冲区,而是通过迭代器的方式按需生成文件内容。
Procfs文件创建 (
input_proc_init):- 在子系统初始化时,此函数首先创建
/proc/bus/input/目录。 - 接着,它调用
proc_create两次,分别创建devices和handlers文件。关键在于,它将每个文件名与一个proc_ops结构体(input_devices_proc_ops和input_handlers_proc_ops)关联起来。
- 在子系统初始化时,此函数首先创建
文件打开与Seq_file绑定:
- 当用户空间程序(如
cat /proc/bus/input/devices)打开这个文件时,VFS会调用proc_ops中的.proc_open方法(例如input_proc_devices_open)。 - 这个
open函数的核心工作是调用seq_open_private。此函数会分配一个seq_file实例,并将其与一个seq_operations结构体(例如input_devices_seq_ops)绑定。seq_file框架从此接管了对该文件的所有读操作。
- 当用户空间程序(如
迭代式内容生成:
- 当用户
read文件时,seq_file框架会调用seq_operations中定义的一系列回调函数来生成内容:
a..start: 被首次调用。它负责准备迭代,通常是锁定保护数据链表的互斥锁(input_mutex),并返回链表中的第一个(或指定位置的)元素。
b..next: 在处理完一个元素后被调用,用于获取链表中的下一个元素。
c..show: 这是核心函数,为.start或.next返回的每一个元素生成其文本表示。例如,input_devices_seq_show会打印一个input_dev的所有属性。
d..stop: 在迭代完成或中断时被调用,负责清理工作,主要是释放互斥锁。 - 这种“按需生成”的方式,使得即使有成百上千个输入设备,读取
/proc文件也只会占用极小的内核内存。
- 当用户
数据一致性:
input_devices_seq_start和input_handlers_seq_start在开始迭代前都会获取全局的input_mutex。这个锁保护了input_dev_list和input_handler_list两个全局链表。这确保了从用户开始读取文件到读取结束的整个过程中,所看到的是一个一致的、不会被并发修改的设备和处理器快照。
代码分析
devices文件实现
1 | // input_devices_seq_start: seq_file迭代器的start回调,用于/proc/bus/input/devices。 |
handlers文件与通用逻辑实现
1 | // input_seq_stop: 通用的stop回调,用于devices和handlers文件。 |
Procfs文件注册
1 | // input_proc_devices_open: "devices"文件的open回调。 |
Input Subsystem Initialization:为键盘、鼠标和触摸屏创建驱动框架
本代码片段是Linux内核Input子系统的核心初始化模块。其主要功能是在内核启动时,注册并建立Input子系统所需的所有基础软件设施。这包括创建一个名为input的设备类(class)、在/proc文件系统中建立调试接口,以及预留一个字符设备主设备号。它本身不驱动任何具体硬件,而是构建了一个所有具体输入设备驱动(如键盘、鼠标、触摸屏驱动)都必须依赖的、统一的、总线无关的框架。
实现原理分析
input_init函数的实现遵循了标准内核子系统的初始化模式,通过向不同的内核核心服务注册自身,来构建一个完整的功能层。
设备类注册 (
class_register):- 它定义了一个
struct class实例——input_class。class_register(&input_class)是第一步也是最关键的一步。此调用会在sysfs中创建/sys/class/input/目录。 - 这个
class的作用是为所有输入设备提供一个统一的视图,无论它们连接在哪个物理总线(USB, I2C, Serio等)上。当一个具体的输入设备驱动注册一个input_dev时,设备核心会自动在/sys/class/input/下创建一个符号链接指向该设备的物理路径,并创建一个以inputX命名的设备目录。 input_class还提供了一个.devnode回调函数——input_devnode。这个函数负责为每个输入设备动态生成它在/dev目录下的名字。例如,它会返回input/event0,input/mouse1等字符串,udev等用户空间工具会根据这些名字创建实际的设备节点。
- 它定义了一个
Procfs接口创建 (
input_proc_init):- 此函数通过
proc_mkdir和proc_create在/proc文件系统中创建/proc/bus/input/目录,以及devices和handlers两个文件。 /proc/bus/input/devices: 这个文件提供了一个人类可读的列表,列出了当前系统中所有已注册的输入设备及其基本信息(ID,名称,物理路径等),是调试和系统诊断的重要工具。/proc/bus/input/handlers: 这个文件列出了所有正在处理输入事件的“处理器”(handler),如evdev(事件设备处理器)、kbd(键盘处理器)等,以及它们与具体设备的连接关系。
- 此函数通过
字符设备区域注册 (
register_chrdev_region):- 这是将输入事件传递给用户空间的核心机制。
register_chrdev_region函数以INPUT_MAJOR为主设备号,预留了INPUT_MAX_CHAR_DEVICES个次设备号,并命名为”input”。 - 这个操作确保了Input子系统拥有一个专属的字符设备号范围。当
evdev等处理器与一个输入设备连接时,它会动态地分配一个未使用的次设备号,并使用cdev_add创建一个字符设备实例。这最终体现在用户空间就是我们熟悉的设备节点,如/dev/input/event0(主设备号为INPUT_MAJOR,次设备号为0)。用户空间程序(如X Server, Wayland, libinput)通过open和read这些设备节点来接收底层的输入事件。
- 这是将输入事件传递给用户空间的核心机制。
代码分析
1 | // input_devnode: 为input类设备生成/dev下的设备节点名。 |
输入子系统处理器管理:input_register_handler 与 input_unregister_handler
本代码片段展示了 Linux 内核输入子系统(Input Subsystem)的核心注册机制。input_register_handler 允许一个新的事件处理器(如键盘驱动、鼠标驱动或 SysRq 处理器)向子系统注册自己,并自动发现并连接到所有与其兼容的输入设备。input_unregister_handler 则执行相反的操作,安全地断开所有连接并将处理器从系统中移除。这一对函数是实现输入子系统“可插拔、自动匹配”架构的基石。
实现原理分析
此机制的核心是两个全局链表(input_handler_list 和 input_dev_list)以及一个全局互斥锁(input_mutex),通过它们来协调处理器与设备之间的 N:M 匹配关系。
注册过程 (
input_register_handler):- 全局锁定: 函数的核心逻辑被
scoped_cond_guard(mutex_intr, ..., &input_mutex)包围。这是一个基于 RAII 风格的、可被信号中断的互斥锁获取。它确保了在整个注册过程中,没有其他任务可以并发地添加/删除设备或处理器,从而保证了全局链表的完整性。 - 加入链表:
list_add_tail(&handler->node, &input_handler_list)将新的处理器添加到全局处理器链表的尾部。这样,当未来有新设备注册时,输入子系统就能找到这个新的处理器。 - 初始匹配与连接: 最关键的一步是遍历现有的全局设备链表
input_dev_list。对于每一个已存在的设备dev,调用input_attach_handler(dev, handler)。这个函数会检查设备的 ID 是否与处理器的id_table匹配。如果匹配,就会调用处理器的.connect回调(如前文的sysrq_connect)来建立连接(即创建一个input_handle)。 - 通知机制:
input_wakeup_procfs_readers()用于唤醒任何正在读取/proc/bus/input/handlers等文件的进程,告知它们系统状态已改变。
- 全局锁定: 函数的核心逻辑被
注销过程 (
input_unregister_handler):- 全局锁定: 同样使用
guard(mutex)(&input_mutex)来获取全局锁,保证操作的原子性。 - 断开连接: 处理器内部维护了一个
h_list,记录了所有与其连接的input_handle。函数遍历这个链表,并对每一个 handle 调用handler->disconnect回调(如sysrq_disconnect)。这个回调负责清理特定于连接的资源(如私有数据、定时器等)。 - 健全性检查:
WARN_ON(!list_empty(&handler->h_list))是一个断言。理论上,在所有disconnect调用完成后,h_list应该为空。如果不为空,说明存在资源泄漏或逻辑错误。 - 移除链表:
list_del_init(&handler->node)将处理器从全局处理器链表中物理移除。
- 全局锁定: 同样使用
代码分析
1 | static struct proc_dir_entry *proc_bus_input_dir; |
输入设备与处理器的匹配与连接:input_attach_handler, input_match_device, input_match_device_id
本代码片段是 Linux 输入子系统“即插即用”特性的核心匹配与连接引擎。其主要功能是:当一个新的设备或处理器出现时,提供一套分层的、基于规则的机制来判断一个输入处理器(handler)是否与一个输入设备(dev)兼容。如果兼容,input_attach_handler 就会调用该处理器的 .connect 回调函数,建立一个正式的连接(input_handle),从而使事件可以从设备流向处理器。
实现原理分析
此机制的设计体现了清晰的层次化和高度的灵活性,通过将匹配逻辑分解为多个函数来实现。
最底层的原子匹配 (
input_match_device_id):- 这是最基础的匹配函数。它只负责比较一个具体的设备 (
dev) 和一条具体的匹配规则 (id)。 - 基于
flags的多维度匹配:id->flags是一个位掩码,它决定了这条规则需要检查哪些字段。这使得规则可以非常灵活,可以只匹配总线类型,也可以同时匹配厂商、产品ID等。 - 能力子集检查 (
bitmap_subset): 这是最强大的一部分。它检查id中定义的能力位图是否是dev提供的能力位图的子集。例如,sysrq_handler的id要求设备必须支持EV_KEY事件和KEY_LEFTALT按键。bitmap_subset(id->keybit, dev->keybit, ...)就会检查设备的keybit是否包含了id->keybit中设置的所有位。这确保了处理器只会被连接到那些至少提供了其所需全部功能的设备上。
- 这是最基础的匹配函数。它只负责比较一个具体的设备 (
处理器级的规则迭代 (
input_match_device):- 这个函数负责将一个处理器与其设备进行匹配。
- 遍历规则表: 它通过一个
for循环,遍历handler->id_table数组中的每一条input_device_id规则。 - 两阶段匹配:
a. 它首先调用input_match_device_id进行静态的、基于 ID 的匹配。
b. 如果静态匹配成功,它还会进行一个可选的动态匹配:!handler->match || handler->match(...)。处理器可以提供一个自定义的.match函数,用于执行更复杂的、无法用静态id结构描述的匹配逻辑。 - “首个匹配”原则: 一旦找到第一个同时满足静态和(可选的)动态匹配的规则,循环就会立即停止,并返回指向该规则的指针。
顶层的连接调度 (
input_attach_handler):- 这是由
input_register_handler(当新处理器注册时)或input_register_device(当新设备注册时)调用的顶层函数。 - 调度逻辑:
a. 它首先调用input_match_device来判断是否存在匹配。
b. 如果返回NULL(不匹配),则直接返回-ENODEV。
c. 如果找到了匹配的id,它就调用handler->connect(handler, dev, id),将控制权交给处理器,由处理器自己来完成最终的连接操作(创建input_handle等)。 - 错误处理: 它会检查
.connect的返回值。返回-ENODEV被认为是一种“优雅的失败”(例如,connect函数内部进行了更精细的检查后决定不连接),而其他错误则被认为是真正的失败并会被打印到内核日志中。
- 这是由
代码分析
1 | /** |
输入设备关闭与资源释放:input_close_device 与 __input_release_device
本代码片段展示了 Linux 内核输入子系统中用于关闭设备句柄(handle)并释放相关资源的核心函数。其主要功能是:为输入处理器(handler)提供一个标准的 input_close_device 接口,用于表示它不再希望接收来自某个设备的事件。这个函数通过精细的引用计数管理和 RCU 同步,安全地拆除设备与处理器之间的连接,并在最后一个使用者关闭设备时,触发底层物理设备的关闭(例如停止轮询或断电)。
实现原理分析
此机制是输入子系统生命周期管理的“后半段”,其设计的核心是多级引用计数和RCU 同步屏障,以确保在高度并发的事件处理环境中,资源可以被安全、无误地释放。
两级引用计数:
handle->open: 这是一个 per-handle 的引用计数。一个 handle(代表一个设备和一个处理器的连接)可能被“打开”多次(例如,evdev处理器允许用户空间多次open同一个/dev/input/eventX文件)。input_close_device每被调用一次,handle->open就减一。dev->users: 这是一个 per-device 的引用计数。它只统计那些“主动”的使用者(即handler->passive_observer为false的处理器)。当dev->users从 1 降为 0 时,意味着没有任何主动处理器再需要这个设备了。
分阶段的资源释放:
input_close_device的逻辑是分阶段的:
a. 首先,它调用__input_release_device来处理“抓取”(grab)的释放。如果当前 handle 正独占性地“抓取”设备,这个抓取会被释放,并可能通知其他等待的 handle。
b. 然后,它递减dev->users计数。如果计数降为 0,并且设备未被抑制(inhibited),它就会关闭物理设备:停止轮询 (input_dev_poller_stop) 并调用设备驱动提供的.close回调。
c. 最后,它递减handle->open计数。
RCU 同步屏障 (
synchronize_rcu):- 这是此机制中最关键的安全保障。输入事件的分发路径(
input_pass_values等)通常是在 RCU 读侧临界区(rcu_read_lock)中执行的。这意味着,即使input_close_device正在执行,也可能有其他 CPU 或被抢占的任务正在处理一个通过当前handle分发的事件。 synchronize_rcu()是一个阻塞函数,它会等待,直到在它被调用之前开始的所有 RCU 读侧临界区全部结束。- 它被用在两个地方:
a. 在释放 grab 后:确保所有事件分发代码都看到了 grab 已经被释放。
b. 在handle->open降为 0 后:确保在handle可能被释放(由上层调用者disconnect函数)之前,绝对没有任何事件处理代码还在使用这个handle。这从根本上杜绝了 use-after-free 的风险。
- 这是此机制中最关键的安全保障。输入事件的分发路径(
代码分析
1 | /** |
输入事件处理与分发流水线:input_event, input_handle_event, input_event_dispose
本代码片段展示了 Linux 内核输入子系统的核心事件处理流水线。其主要功能是:为设备驱动提供一个标准的 input_event 接口来报告硬件产生的输入事件。这一系列函数通过一个多阶段的、受锁保护的流程,对事件进行预处理、缓冲和最终分发。input_event 是外部入口,input_handle_event 是核心处理逻辑,而 input_event_dispose 则是最终的执行与分发单元。
实现原理分析
此机制是输入子系统的心脏,其设计的核心在于将来自不同驱动、可能以极高频率产生的事件,高效、安全地送达上层的事件处理器。
统一入口与锁定 (
input_event):input_event是所有输入设备驱动调用的标准 API。- 能力检查: 它的第一步是
is_event_supported(...)。这是一个关键的过滤,它会检查设备在注册时是否声明过自己能够产生这种类型的事件。这可以防止驱动发送无效或意外的事件。 - 核心锁定:
guard(spinlock_irqsave)(&dev->event_lock)获取一个 per-device 的自旋锁并禁用中断。这个锁是至关重要的,它确保了在处理一个设备的事件流水线时,不会被该设备产生的另一个中断所打断,也不会被来自其他 CPU 的调用并发访问,从而保证了事件处理的序列化和原子性。
事件预处理与决策 (
input_handle_event):- 获取处置(Disposition):
input_get_disposition(dev, type, code, &value)是一个关键的预处理步骤。它会根据事件类型和设备当前的状态(例如,对于 ABS 绝对坐标轴,它会过滤掉与上次值相同的重复事件),决定这个事件应该如何被处理。disposition是一个掩码,可能包含以下标志:INPUT_PASS_TO_HANDLERS: 事件应被传递给上层处理器。INPUT_PASS_TO_DEVICE: 事件应被传递回设备驱动自己的.event回调(用于特殊设备,如力反馈手柄)。INPUT_FLUSH: 这是一个EV_SYN事件,表示一批事件已结束,需要“刷新”(flush)。
- 贡献随机数:
add_input_randomness(...)将事件的时间戳、类型和码值作为熵源,贡献给内核的随机数生成器。
- 获取处置(Disposition):
缓冲与分发 (
input_event_dispose):- 这是事件流水线的最后一站。它不是立即将事件传递出去,而是将其缓冲在一个 per-device 的小数组
dev->vals中。 - 缓冲: 对于非
EV_SYN事件,它只是简单地将type,code,value存入dev->vals数组,并递增dev->num_vals计数器。 - 刷新(Flush): 缓冲的事件在两种情况下会被“刷新”:
a. 当收到一个disposition包含INPUT_FLUSH的EV_SYN事件时。
b. 当缓冲区dev->vals即将填满时 (dev->num_vals >= dev->max_vals - 2),它会自动在末尾添加一个EV_SYN事件,然后强制刷新。 - 最终分发: 刷新操作通过调用
input_pass_values(dev, dev->vals, dev->num_vals)来完成。这个函数(未在此处展示)会在 RCU 读锁的保护下,遍历dev->h_list(所有连接到此设备的 handle),并将缓冲的事件数组传递给每个 handle 对应的处理器的.filter或.event回调。
- 这是事件流水线的最后一站。它不是立即将事件传递出去,而是将其缓冲在一个 per-device 的小数组
代码分析
1 | /** |
输入事件分发流水线与自动重复控制:input_pass_values
本代码片段展示了 Linux 内核输入子系统事件处理流水线的**“分发”阶段**。其核心功能是:接收一个已缓冲的事件数组 (vals),并以一种 RCU 安全的方式,将其广播给所有已连接并打开的事件处理器(handler)。它实现了两个关键特性:一是**独占性“抓取”(grab)模式,允许一个处理器临时垄断所有事件;二是集成的按键自动重复(auto-repeat)**逻辑,在事件分发后,根据按键的按下和释放来启动或停止自动重复定时器。
实现原理分析
input_pass_values 是 input_event 流水线的核心,是连接事件产生和事件消费的桥梁。其实现原理精妙地结合了 RCU 无锁读取、可选的独占模式以及内置的状态机(用于自动重复)。
RCU 安全的处理器遍历:
- 执行上下文: 函数注释和
lockdep_assert_held明确指出,此函数必须在持有dev->event_lock(一个禁用了中断的自旋锁)的情况下被调用。这保证了事件缓冲区的完整性。 - 无锁读取: 整个事件分发循环被
scoped_guard(rcu)包围,并使用rcu_dereference和list_for_each_entry_rcu。这是一个关键的性能设计。它允许input_pass_values在不获取任何会与处理器连接/断开操作冲突的锁的情况下,安全地遍历设备的所有句柄(dev->h_list)。处理器(handler)的注册和注销(input_register/unregister_handler)可以在其他 CPU 上并发进行,它们会使用synchronize_rcu()来等待所有像input_pass_values这样的“读者”完成,然后才安全地释放内存。
- 执行上下文: 函数注释和
独占抓取(Grab)模式:
handle = rcu_dereference(dev->grab): 这是循环的第一步。它检查设备是否被某个 handle“抓取”了。- 如果
dev->grab非空,事件数组只会被发送给这个唯一的grabber,然后函数立即退出分发循环。这实现了一种独占访问模式,常用于虚拟终端(VT)切换或某些游戏手柄的特殊模式,以确保在特定状态下,键盘输入只被一个特定的处理器接收。
事件过滤链(Filter Chain):
count = handle->handle_events(handle, vals, count): 每个 handle 的回调函数handle_events(对于sysrq_handler,它就是sysrq_filter的一个包装)可以返回它未处理的事件数量。if (!count) break;: 如果一个处理器处理(“消费”)了所有的事件,它会返回 0,分发循环会立即中止。这实现了一个过滤链:在dev->h_list中位置靠前的处理器有优先权,可以阻止事件被后续的处理器看到。
集成的自动重复逻辑:
- 时机: 自动重复逻辑在事件分发给所有处理器之后执行。它只处理那些未被任何过滤器消费掉的
EV_KEY事件。 input_start_autorepeat: 当一个“按下”(v->value == 1)事件被检测到时,此函数被调用。它会设置一个定时器,该定时器将在dev->rep[REP_DELAY]毫秒后到期。input_stop_autorepeat: 当一个“释放”(v->value == 0)事件被检测到时,此函数被调用,它会立即删除(取消)任何正在等待的自动重复定时器。- 定时器回调: 当
REP_DELAY定时器到期时,dev->timer.function会被调用。这个回调函数(未在此处展示)会生成一个值为 2(重复)的EV_KEY事件,并再次调用input_event将其注入流水线。然后,它会重新设置一个dev->rep[REP_PERIOD]毫秒的定时器,以实现连续的按键重复。
- 时机: 自动重复逻辑在事件分发给所有处理器之后执行。它只处理那些未被任何过滤器消费掉的
代码分析
1 | /** |
input_handle_events_default / input_handle_events_filter / input_handle_events_null / input_handle_setup_event_handler / input_register_handle:输入事件分发策略选择与 handle 注册挂接
input_handle_events_default:逐个事件调用 handler->event
作用与原理:将批量事件数组 vals[0..count-1] 逐个展开为单事件回调,调用 handler->event(handle, type, code, value)。适用于 handler 只实现了单事件接口 event 的情况。
1 | /** |
input_handle_events_filter:逐个调用 handler->filter,并原地压缩数组
作用与原理:对每个事件调用 handler->filter(...);若返回“需要过滤”,则丢弃该事件;否则把事件写回到 end 指向的位置,从而在原数组中保持 未过滤事件的相对顺序 并实现原地压缩。返回值是压缩后的事件数。
1 | /** |
input_handle_events_null:空处理策略
作用与原理:不进行任何处理但返回 count,表示“该批事件被消费”。用于 handler 既没有 filter/event/events 时的兜底策略,避免上层必须对空回调做额外分支。
1 | /** |
input_handle_setup_event_handler:根据 handler 能力选择 handle->handle_events
作用与原理:按优先级把 handle->handle_events 指向合适的实现:
- 若存在
filter,优先采用 filter 压缩策略(因为过滤需要在最前面生效); - 否则若存在
event,采用默认逐事件分发; - 否则若存在
events(批处理接口),直接使用 handler 自己的批处理实现; - 都没有则使用空处理。
1 | /** |
input_register_handle:把 handle 挂到 dev 与 handler 的 RCU 链表,使事件可流经该 handle
作用与原理:
先调用
input_handle_setup_event_handler(),确保handle->handle_events已绑定为可用实现。进入
dev->mutex临界区,把handle->d_node挂到dev->h_list:- 过滤器放在链表头(
list_add_rcu),使过滤器先于普通处理器看到事件; - 普通处理器放在链表尾(
list_add_tail_rcu)。
- 过滤器放在链表头(
再把
handle->h_node挂到handler->h_list,便于按 handler 维度管理/断开。若 handler 实现了
start,在注册完成后回调handler->start(handle),用于做“开始接收事件前”的准备动作。
1 | /** |
input_open_device:启动 input_handle 的事件接收并在需要时打开底层 input_dev(含 RCU 同步防并发分发)
1 | /** |
drivers/input/input-leds.c 输入子系统到 LED 子系统的桥接
input_led_info / struct input_led / struct input_leds:静态映射表与对象布局
1 |
|
input_leds_brightness_get:从 input 设备 LED 位图读取当前亮度
作用与原理
该函数不直接访问硬件灯,而是读取 input_dev->led 位图中对应 bit。该位图由 input 核心在处理 EV_LED 事件时更新,因此它代表“输入设备认为的 LED 状态”。
1 | /** |
input_leds_brightness_set:向 input 子系统注入 EV_LED 事件以改变状态
作用与原理
LED 子系统希望“设置亮度”,这里把它翻译成 input 子系统可理解的 EV_LED 事件,通过 input_inject_event() 注入到对应输入设备,使设备驱动(或上层处理链)执行实际的 LED 控制。
1 | /** |
input_leds_event:输入层事件回调(本实现为空)
作用与原理
该回调是 input_handler 的事件入口。此处留空表示该桥接模块不需要在事件到来时做额外动作;LED 状态由 input 核心维护在 input_dev->led 位图,读取时由 brightness_get 查询即可。
1 | /** |
input_leds_get_count:统计设备上可桥接的 LED 数量
作用与原理
遍历 dev->ledbit(表示“设备支持哪些 LED code”),只对 input_led_info[code].name 非空的项计数,从而决定后续需要分配多少 struct input_led。
1 | /** |
input_leds_connect:连接 input 设备并为每个 LED_* 注册一个 led_classdev
作用与原理
这是桥接的核心:当匹配到具备 EV_LED 能力的输入设备时,
- 计算 LED 数量并分配
struct input_leds(柔性数组一次性分配) - 注册并打开 input_handle,使得后续可以注入事件
- 为每个支持的 LED code 创建
led_classdev:生成名称、配置 get/set、设置默认触发器、注册到 LED 子系统 - 任意一步失败都按 goto 路径回滚,保证不泄漏资源
1 | /** |
input_leds_disconnect:断开 input 设备并注销全部 led_classdev
作用与原理
按 connect 的反向顺序释放:先注销 LED 设备并释放名称,再关闭 input 设备并注销 handle,最后释放 struct input_leds。
1 | /** |
input_leds_ids / input_leds_handler:匹配规则与处理器回调表
作用与原理
input_leds_ids:仅匹配具备EV_LED事件类型的输入设备。input_leds_handler:声明 connect/disconnect/event 入口,使 input 核心在匹配时调用。
1 | /** @brief 匹配具备 EV_LED 能力的输入设备。 */ |
input_leds_init / input_leds_exit:注册与注销 input_handler
作用与原理
模块加载时注册 handler,使得后续 input 设备枚举/热插拔会触发匹配并调用 connect;模块卸载时注销 handler 并触发已连接设备的 disconnect。
1 | /** |









