[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下的设备节点名。 |
drivers/input/serio.c 串行输入总线抽象(Serial Input Bus Abstraction) PS/2及传统输入端口的核心
历史与背景
这项技术是为了解决什么特定问题而诞生的?
这项技术以及它所实现的serio
(Serial I/O)子系统,是为了给Linux内核提供一个标准化的、与具体硬件控制器解耦的框架,来管理传统的、基于串行字节流的输入设备。它主要解决了以下问题:
- 抽象硬件接口:在PC体系结构中,键盘和PS/2鼠标都连接到一个称为i8042(或兼容的)键盘控制器上。这个控制器本身有其复杂的、通过I/O端口访问的底层接口。
serio
框架将这种底层的、特定于控制器的交互,抽象成一个简单的、通用的串行字节流接口。 - 分离总线与设备驱动:
serio
扮演了一个“总线”的角色。它将硬件控制器(如i8042)的驱动与连接在该控制器上的具体设备(如PS/2鼠标、AT键盘)的驱动分离开来。这使得鼠标驱动(psmouse.c
)无需关心它是在和一个真实的i8042芯片通信,还是在和一个虚拟化环境模拟的控制器通信。 - 支持多种设备类型:PS/2端口不仅可以连接标准的鼠标和键盘,还可以连接触摸板、指点杆(TrackPoint)等。
serio
总线允许内核探测连接在端口上的设备类型,并为其加载正确的设备驱动。
它的发展经历了哪些重要的里程碑或版本迭代?
serio
框架是Linux支持PC硬件的 foundational 组件之一,其发展相对稳定,主要里程碑体现在其架构的成熟和角色的演变上。
- 初期实现:作为Linux支持PC AT键盘和PS/2鼠标的基础,
serio
从内核早期就已经存在。 - 架构确立:其最重要的里程碑是确立了**“端口驱动 -> serio总线 -> 设备驱动”**的三层架构。这使其成为一个真正的总线系统,而不是一个单一的驱动。
- 角色的转变:随着USB(特别是USB HID类)的普及,PS/2端口和
serio
子系统的重要性急剧下降。其角色从主流输入接口转变为纯粹的遗留系统(Legacy System)支持。 - 虚拟化中的核心作用:尽管在物理机上已不常见,但
serio
在虚拟化技术中扮演着至关重要的角色。几乎所有的虚拟机(如QEMU/KVM, VMware, VirtualBox)都为客户机(Guest OS)模拟了一个标准的i8042控制器和PS/2键盘/鼠标,因为这是最通用、无需特殊驱动就能保证操作系统正常安装和使用的输入设备。这使得serio
在现代服务器环境中依然被广泛使用。
目前该技术的社区活跃度和主流应用情况如何?
- 社区活跃度:
serio
子系统已经非常成熟和稳定,处于纯维护模式。社区不会为其添加新的功能,主要的开发活动是修复bug或进行一些细微的代码清理。 - 主流应用:
- 遗留硬件支持:在仍然使用PS/2接口的旧电脑或特定工业计算机上运行Linux。
- 虚拟化:这是其当前最主流的应用。为虚拟机提供基础的键盘和鼠标输入。
核心原理与设计
它的核心工作原理是什么?
serio
的核心是一个清晰的三层驱动模型:
底层:端口驱动(Port Driver)
- 例如
drivers/input/serio/i8042.c
。 - 这个驱动知道如何与物理硬件控制器(如i8042芯片)通信。它负责读写I/O端口、注册和处理来自控制器的硬件中断。
- 它的职责是:从硬件接收原始的字节数据,并通过
serio_interrupt()
函数将这些字节“喂”给serio
核心;同时,它也提供一个write
函数,让上层能通过它向硬件发送字节。 - 端口驱动会创建一个或多个
serio_port
对象,代表控制器上的物理端口(例如,一个用于键盘,一个用于AUX/鼠标)。
- 例如
中层:Serio核心(Bus Driver)
- 即
drivers/input/serio/serio.c
本身。 - 它定义了
struct serio
对象和serio_driver
接口,构成了一个迷你的总线系统。 - 它接收来自端口驱动的字节流,并将其分发给绑定到该
serio_port
上的设备驱动。 - 它负责管理设备驱动的注册和匹配。当一个
serio_port
被创建时,serio
核心会尝试探测连接在上面的设备,并为之寻找合适的驱动。
- 即
上层:设备驱动(Device Driver)
- 例如
drivers/input/mouse/psmouse.c
和drivers/input/keyboard/atkbd.c
。 - 这些驱动不关心底层的硬件控制器是什么,它们只与
serio_port
打交道。 - 它们注册为
serio_driver
。当serio
核心认为某个端口上的设备与某个驱动匹配时,就会调用该驱动的connect
回调函数。 - 设备驱动的核心任务是解析协议。它从
serio
核心接收字节流,并根据特定设备的协议(如PS/2鼠标的3字节数据包格式,或键盘的扫描码)来解释这些字节。 - 最后,它将解析后的信息(如按键的按下/弹起、鼠标的相对位移)转换成标准的内核输入事件(
input_event
),并通过输入核心(Input Core)上报给用户空间。
- 例如
它的主要优势体现在哪些方面?
- 出色的抽象和解耦:将硬件控制、总线管理和设备协议解析完美地分离开来,使得各层驱动可以独立开发和维护。
- 模块化和可扩展性:可以很容易地为一种新的、使用PS/2兼容协议的设备编写驱动,而无需触及底层的i8042代码。
- 健壮的遗留支持:为古老但重要的PC标准输入设备提供了稳定可靠的支持。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 技术过时:
serio
所服务的硬件本身是过时的。它不支持即插即用(Plug and Play)、高带宽和现代设备的丰富功能。 - 性能有限:串行协议本身速度慢,中断驱动,效率远低于USB。
- 功能单一:它本质上只是一个字节流传输通道,不像USB HID那样,设备可以提供详细的描述符来报告自己的功能。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
在现代系统中,serio
是处理PS/2协议输入设备的唯一且标准的解决方案。
- 虚拟机基础输入:当你在KVM/QEMU或VirtualBox中创建一个新的虚拟机并开始安装操作系统时,虚拟机提供的键盘和鼠标就是通过模拟的i8042控制器和
serio
总线来工作的。这保证了即使在没有安装任何额外驱动的裸机环境下,客户机操作系统也能获得基本的输入能力。 - 嵌入式或工业控制:在一些成本敏感或有特殊要求的嵌入式/工业系统中,可能仍会使用PS/2接口的键盘或条码扫描枪,因为其协议简单、实现成本低。在这些系统上运行Linux时,
serio
是驱动这些设备的基础。
是否有不推荐使用该技术的场景?为什么?
任何现代输入设备都不应该使用serio
。
- 原因:USB、I2C、SPI、Bluetooth等现代总线为输入设备提供了远比
serio
强大的功能,包括:- 热插拔:
serio
不支持。 - 自动识别:USB HID设备可以通过描述符精确报告自身功能,无需驱动去猜测。
- 高带宽:支持高回报率(high polling rate)的电竞鼠标或复杂的触摸板。
- 统一的驱动模型:HID(Human Interface Device)类为各种输入设备(键盘、鼠标、游戏手柄、触摸屏)提供了统一的驱动模型,远比
serio
的模式更具可扩展性。
- 热插拔:
对比分析
请将其 与 其他相似技术 进行详细对比。
对比 serio
vs. USB HID (Human Interface Device)
特性 | serio (及 psmouse /atkbd ) |
USB HID |
---|---|---|
总线类型 | 简单的、点对点的、异步串行字节流。 | 复杂的、基于数据包的、主从式、可拓扑的总线。 |
设备发现 | 启动时进行简单的探测和类型猜测。不支持热插拔。 | 完全的即插即用。设备连接后会进行枚举,主机可读取其所有信息。 |
设备描述 | 无。驱动程序必须内置对各种设备协议(如 IntelliMouse, ExplorerPS/2)的了解。 | 有(HID报告描述符)。设备会提供一个详细的报告,精确描述它发送的每个数据包的格式和含义(如“第1个字节的第0位是左键”)。 |
驱动模型 | serio_driver ,特定于serio 总线。 |
usb_driver + hid_driver 。HID类驱动是通用的,可以处理所有符合HID规范的设备,无论其物理连接是什么。 |
性能 | 低。带宽和回报率受限于PS/2协议。 | 高。从低速(1.5 Mbps)到超高速(10+ Gbps),可以满足任何输入设备的需求。 |
应用范围 | 遗留设备(PS/2键盘、鼠标、触摸板)和虚拟机。 | 所有现代输入设备的标准(USB键盘、鼠标、游戏手柄、触摸屏等)。 |
Serio驱动Sysfs属性:用户空间接口与手动绑定控制
本代码片段为serio
总线上的所有驱动程序定义并实现了一组sysfs
属性。其核心功能是向用户空间导出一个标准的、基于文件的接口,允许用户查看驱动的描述信息,以及查询和修改驱动的绑定模式(自动或手动)。这为系统管理员和开发者提供了一种在运行时动态控制驱动行为的机制,特别是实现了将设备与驱动进行手动绑定的能力,从而覆盖内核默认的自动匹配逻辑。
实现原理分析
该代码的实现完全建立在Linux设备模型的sysfs
属性框架之上。它利用了一系列的宏和回调函数来创建文件节点。
属性定义:
description
(只读): 使用DRIVER_ATTR_RO
宏定义了一个名为description
的只读属性。这个宏会自动创建一个struct driver_attribute
,并将其.show
回调指向description_show
函数。当用户cat /sys/bus/serio/drivers/<driver_name>/description
时,VFS层会调用description_show
。该函数会从驱动的私有结构serio_driver
中读取description
字符串,并将其返回给用户。bind_mode
(读写): 使用DRIVER_ATTR_RW
宏定义了一个名为bind_mode
的读写属性。这会将其.show
回调指向bind_mode_show
,.store
回调指向bind_mode_store
。bind_mode_show
: 读取serio_driver
结构中的manual_bind
布尔标志,并将其转换为字符串”manual”或”auto”返回。bind_mode_store
: 当用户向该文件写入(echo "manual" > ...
)时,此函数被调用。它会解析用户写入的字符串,并相应地设置serio_driver
结构中的manual_bind
标志为true
或false
。
属性分组 (
ATTRIBUTE_GROUPS
):serio_driver_attrs
数组将所有定义的属性(description
和bind_mode
)收集起来。ATTRIBUTE_GROUPS(serio_driver)
宏则将这个属性数组封装成一个attribute_group
。- 这个
attribute_group
会被serio
总线(在上一节中分析的serio_bus
结构体中通过.drv_groups
字段指定)自动附加到每一个注册到该总线上的驱动程序的sysfs
目录中。因此,任何serio
驱动在加载后,都会在/sys/bus/serio/drivers/<driver_name>/
下自动拥有description
和bind_mode
这两个文件。
代码分析
description
属性 (只读)
1 | // description_show: "description" sysfs属性的show回调函数。 |
bind_mode
属性 (读/写)
1 | // bind_mode_show: "bind_mode" sysfs属性的show回调函数。 |
属性分组
1 | // serio_driver_attrs: 一个数组,收集了所有serio驱动的属性。 |
Serio总线行为实现:设备与驱动的匹配、绑定与事件通知
本代码片段是serio
总线类型核心回调函数的具体实现。它定义了当设备模型核心在serio
总线上进行操作时应执行的逻辑。其核心功能包括:1) 根据设备ID匹配合适的驱动程序;2) 在匹配成功后,通过调用驱动的connect
方法来“绑定”设备与驱动;3) 在解绑时调用disconnect
;4) 在系统关机等生命周期事件中通知驱动;5) 生成uevent事件,向用户空间(如udev)广播新设备的信息,以便自动加载驱动模块。
实现原理分析
该代码是Linux设备模型中“总线-驱动-设备”范式的经典实现,它将通用逻辑与驱动的特定实现分离开来。
匹配机制 (
serio_bus_match
):- 这是设备模型进行自动绑定的第一步。当一个新
serio
设备或新serio
驱动被注册时,内核会遍历所有未绑定的另一方,并调用serio_bus_match
。 - 此函数首先检查
manual_bind
标志,允许用户通过sysfs手动绑定,从而跳过自动匹配。 - 核心匹配逻辑在
serio_match_port
中。它会遍历驱动程序提供的id_table
(一个serio_device_id
数组)。对于表中的每一项,它会将type
,proto
,extra
,id
字段与设备的相应ID字段进行比较。SERIO_ANY
作为一个通配符,可以匹配任何值。只要找到表中的一项完全匹配,函数就返回成功。
- 这是设备模型进行自动绑定的第一步。当一个新
绑定与解绑 (
serio_driver_probe
,serio_driver_remove
):- Probe: 当
serio_bus_match
返回成功后,设备模型会调用serio_driver_probe
。此函数是一个简单的“中间人”,它从device
和device_driver
结构中提取出serio
和serio_driver
指针,然后调用serio_connect_driver
。serio_connect_driver
负责获取锁,并最终调用具体驱动程序提供的.connect()
方法,将serio
端口的控制权正式移交给驱动。 - Remove: 当设备或驱动被移除时,
serio_driver_remove
被调用。它通过serio_disconnect_driver
(在此代码段中未显示,但功能与serio_connect_driver
相反)来调用驱动的.disconnect()
方法,让驱动释放所有资源并断开与端口的连接。
- Probe: 当
生命周期管理 (
serio_shutdown
):- 当系统关机时,设备模型会为每个设备调用总线的
.shutdown
方法。serio_shutdown
会进一步调用驱动的.cleanup()
方法,为设备提供一个在系统断电前执行最后清理操作的机会。
- 当系统关机时,设备模型会为每个设备调用总线的
用户空间通知 (
serio_uevent
):- 当一个
serio
设备被注册时,此函数被调用以生成一个uevent。它会将设备的ID信息(type, proto, id, extra)打包成环境变量。 - 最关键的一步是创建
MODALIAS
变量。它将设备ID编码成一个标准格式的字符串,如serio:ty01pr00id00ex00
。用户空间的udev服务会监听这些uevent,并使用MODALIAS
的值去匹配modprobe
数据库,从而找到并自动加载能够处理该设备的内核模块。
- 当一个
代码分析
设备/驱动匹配
1 | // serio_match_port: 检查一个serio端口是否与给定的ID表匹配。 |
核心绑定与生命周期回调
1 | // serio_connect_driver: 连接一个驱动到serio端口。 |
用户空间事件通知
1 | // SERIO_ADD_UEVENT_VAR: 一个宏,用于简化向uevent环境添加变量的操作。 |
Serio总线注册:串行输入设备的驱动模型骨架
本代码片段是Linux内核中serio
(Serial Input/Output)子系统的核心初始化部分。其主要功能是定义并向内核的统一设备模型注册一个名为serio
的新总线类型(bus_type
)。这个serio
总线为一类简单的、基于字节流的串行输入设备(如传统的PS/2键盘、鼠标、触摸屏,以及某些触摸板和手写板)提供了一个标准化的驱动模型。它充当了底层端口驱动(如i8042 PS/2控制器驱动)和上层设备驱动(如PS/2键盘驱动)之间的抽象层和粘合剂。
实现原理分析
该代码的实现遵循了Linux设备模型的标准范式,即通过定义和注册一个bus_type
结构来创建一个新的总线。
总线类型定义 (
serio_bus
):- 代码的核心是
serio_bus
这个bus_type
结构体的静态实例。这个结构体通过一系列函数指针(回调函数)来定义总线的行为。 .name = "serio"
: 定义了总线的名字。在bus_register
被调用后,内核会在sysfs中创建对应的目录/sys/bus/serio
。.match = serio_bus_match
: 这是最重要的回调之一。当一个新的serio
设备或serio
驱动被注册到内核时,设备模型核心会调用此函数,以确定驱动是否支持该设备。匹配逻辑通常基于设备提供的ID表。.probe = serio_driver_probe
,.remove = serio_driver_remove
: 当.match
成功后,设备模型会调用总线的.probe
函数来绑定设备和驱动。这个总线级别的probe
通常会进一步调用具体驱动提供的probe
方法。.remove
则执行相反的解绑操作。- 其他回调如
.uevent
,.shutdown
,.pm
则分别定义了总线在处理热插拔事件、系统关机和电源管理时的标准行为。
- 代码的核心是
总线注册与注销:
serio_init()
: 这是一个内核模块初始化函数,通过subsys_initcall
宏被注册,在系统启动的早期被调用。它的唯一工作就是调用bus_register(&serio_bus)
,将上面定义的serio_bus
结构体正式注册到设备模型核心。从这一刻起,Linux内核就“知道”了serio
总线的存在,其他驱动可以通过serio_register_port
和serio_register_driver
来注册设备和驱动。serio_exit()
: 这是模块卸载函数,它调用bus_unregister(&serio_bus)
来执行相反的操作,从内核中移除serio
总线及其所有相关结构。
代码分析
1 | // serio_bus: 定义了 "serio" 总线类型的核心结构体。 |