08、evdev 通用事件处理层
通常来说,核心代码和事件处理部分都不用自己动手写。因为Linux系统已经内置了一个通用解决方案:evdev.c程序。这个程序包含了一些现成的工具和框架,让我们能方便地处理输入设备传来的各种事件信息。
一、evdev_handler 结构体
evdev_handler 结构体定义了处理输入设备事件的函数指针, 在 Linux 内核中, evdev_handler 结构体定义在 include/linux/input.h 头文件中的 evdev.h 中, 如下所示:
static struct input_handler evdev_handler = {
.event = evdev_event, // 输入事件处理函数
.events = evdev_events, // 批量输入事件处理函数
.connect = evdev_connect, // 当 input_dev 和 input_handler 匹配成功之后执行的连接处理函数
.disconnect = evdev_disconnect, // 断开连接处理函数
.legacy_minors = true, // 设置为 true,表示支持传统次设备号, 如果设置为 false,则使用动态的次设备号分配方式
.minor = EVDEV_MINOR_BASE, // 输入设备的基础次设备号
.name = "evdev", // 输入处理器的名称
.id_table = evdev_ids, // 输入设备 ID 表
};
2
3
4
5
6
7
8
9
10
在Linux内核的evdev.c文件中,有一个名为evdev_init的内部函数。这个函数的主要作用是:在系统启动时,把evdev_handler这个输入事件处理程序注册到系统中。具体来说,它通过调用input_register_handler函数,并将evdev_handler作为参数传递进去来完成注册工作。这个函数被特别标记为在系统初始化阶段自动运行。
二、input_register_handler
input_register_handler函数的作用是把输入事件处理器添加到系统处理流程中,这样就能接收并处理来自输入设备(如键盘、鼠标等)的事件。调用该函数后,它会返回一个数值结果(通常是0表示成功,负数表示错误),这个结果会直接反馈给调用者。
static int __init evdev_init(void)
{
return input_register_handler(&evdev_handler);
}
2
3
4
注册输入事件处理器的流程如下:
首先锁定input_mutex互斥锁,确保其他线程暂时无法操作关键代码区域。
为处理器准备一个链表节点,初始化它的位置标记。
把新注册的处理器追加到处理器链表的最后面,确保它排在已有处理器后面。
遍历所有已注册的输入设备:
- 对每个设备都执行处理器绑定操作,建立设备与处理器之间的连接
通知等待读取系统信息的进程,提示它们可以继续处理请求
解锁input_mutex,允许其他线程继续操作共享资源
最终返回0表示操作成功完成
整个过程就像在系统处理器列表里登记新成员,同时更新所有设备的处理器关联关系,并通知相关进程系统状态已更新。
int input_register_handler(struct input_handler *handler)
{
struct input_dev *dev; // 输入设备指针
int error; // 错误码
error = mutex_lock_interruptible(&input_mutex); // 获取互斥锁, 以阻塞方式
if (error)
return error; // 如果获取锁失败, 则返回错误码
INIT_LIST_HEAD(&handler->h_list); // 初始化输入处理器的链表头
list_add_tail(&handler->node, &input_handler_list); // 将输入处理器添加到输入处理器链表的末尾
list_for_each_entry(dev, &input_dev_list, node)
input_attach_handler(dev, handler); // 遍历输入设备链表, 并将输入处理器附加到每个输入设备上
input_wakeup_procfs_readers(); // 唤醒正在等待读取 procfs 的读者
mutex_unlock(&input_mutex); // 释放互斥锁
return 0; // 返回成功的状态码
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在上面的函数中输入处理器附加到每个输入设备上, 也就是取出 input_dev 和新注册的handler 进行匹配, 匹配成功之后执行 evdev_connect 函数。
三、evdev 结构体
evdev结构体在evdev_connect函数中负责记录设备的基本信息和运行状态。它的主要作用是管理设备的输入事件处理,具体来说:
- 跟踪设备是否已被打开
- 记录当前正在使用设备的程序
- 确保设备被正确访问(避免多个程序同时操作)
- 存储设备相关的其他必要信息
简单来说,这个结构体就是设备的"状态记录本",用来协调设备与程序之间的交互,确保输入事件能被正确管理和处理。
struct evdev {
int open; // 记录 evdev 设备打开的状态
struct input_handle handle; // 输入事件处理器的句柄
wait_queue_head_t wait; // 等待队列头, 用于休眠和唤醒等待事件的进程
struct evdev_client __rcu *grab; // 指向当前占用 evdev 设备的客户端
struct list_head client_list; // 与 evdev 设备关联的客户端链表
spinlock_t client_lock; // 用于保护客户端链表的自旋锁
struct mutex mutex; // 用于保护对 evdev 设备的互斥访问
struct device dev; // 与 evdev 设备关联的设备结构
struct cdev cdev; // evdev 设备的字符设备结构
bool exist; // 表示 evdev 设备是否存在
};
2
3
4
5
6
7
8
9
10
11
12
- open:记录设备是否开启,0表示关闭,1表示开启。
- handle:处理输入事件的工具,包含设备信息和处理函数。
- wait:等待队列的入口。当没有输入事件时,进程会在这里等待,暂停执行。
- grab:指向当前独占设备的客户端。一旦被占用,其他客户端无法访问该设备。
- client_list:存储连接到该设备的所有客户端列表。
- client_lock:保护客户端列表的锁,确保多任务同时操作时不会出错。
- mutex:控制设备访问的开关,同一时间只允许一个任务操作设备。
- dev:设备的基本信息,比如名称、编号等。
- cdev:设备在系统中的注册信息,用于管理设备文件(如文件路径、权限等)。
- exist:标记设备是否存在的开关,存在时为"是",不存在时为"否"。
四、evdev_client 结构体
现在我们来了解evdev_client结构体。这个结构体用来记录每个evdev客户端的信息和状态。每当应用程序打开一个event设备文件时,系统就会为它创建一个这样的结构体。一个设备可以对应多个客户端,每个客户端的状态(比如是否活跃、缓冲区情况等)都会被这个结构体单独管理。在evdev.c文件中,系统会通过操作这个结构体,根据客户端当前的设置,把接收到的事件数据写入对应客户端的缓冲区,或者向客户端发送通知提醒。简单来说,它就像每个设备连接的"身份卡片",帮助系统精准管理每个连接的状态和行为。
struct evdev_client {
unsigned int head; // 缓冲区的头指针, 指向下一个可写入的位置
unsigned int tail; // 缓冲区的尾指针, 指向下一个可读取的位置
unsigned int packet_head; // [未来] 下一个数据包的第一个元素的位置
spinlock_t buffer_lock; // 用于保护对缓冲区、 头指针和尾指针的访问的自旋锁
struct fasync_struct *fasync; // 用于异步通知的结构体指针
struct evdev *evdev; // 与客户端关联的 evdev 设备指针
struct list_head node; // 与 evdev 设备关联的客户端链表节点
enum input_clock_type clk_type; // 输入时钟类型
bool revoked; // 标志, 指示客户端是否被撤销
unsigned long *evmasks[EV_CNT]; // 用于事件掩码的数组
unsigned int bufsize; // 缓冲区的大小
struct input_event buffer[]; // 输入事件缓冲区, 可变长度数组
};
2
3
4
5
6
7
8
9
10
11
12
13
14
以下是每个成员的简单说明:
- head:缓冲区的起始指针,指向当前可以写入新数据的位置。
- tail:缓冲区的结束指针,指向当前可以读取数据的位置。
- packet_head:标记下一个数据包第一个数据的位置。
- buffer_lock:一个锁机制,防止多线程同时操作缓冲区、头或尾指针,确保操作安全。
- fasync:用于异步通知的指针,当需要通知客户端时会用到它。
- evdev:指向客户端所属的输入设备(如键盘、鼠标等)的指针。
- node:用于将客户端链接到对应设备的列表中的节点。
- clk_type:记录客户端使用的时钟类型。
- revoked:标记客户端是否被禁用(例如因异常被终止)。
- evmasks:一个数组,保存不同事件类型的过滤设置(大小由EV_CNT决定)。
- bufsize:缓冲区能存储的最大事件数量。
- buffer[ ]:存储输入事件的动态数组,实际数据都存在这里。
五、evdev_connect 函数
生成 /dev/input/event1
static int evdev_connect(struct input_handler *handler, struct input_dev *dev,
const struct input_device_id *id)
{
struct evdev *evdev; // evdev 结构体指针, 表示与设备关联的 evdev 实例
int minor; // 设备的次设备号
int dev_no; // 设备号
int error; // 错误码
minor = input_get_new_minor(EVDEV_MINOR_BASE, EVDEV_MINORS, true);
if (minor < 0) {
error = minor;
pr_err("failed to reserve new minor: %d\n", error);
return error;
}
evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL); // 分配 evdev 结构体的内存空间
if (!evdev) {
error = -ENOMEM;
goto err_free_minor;
}
INIT_LIST_HEAD(&evdev->client_list); // 初始化 evdev 的客户端链表头
spin_lock_init(&evdev->client_lock); // 初始化 evdev 的客户端自旋锁
mutex_init(&evdev->mutex); // 初始化 evdev 的互斥锁
init_waitqueue_head(&evdev->wait); // 初始化 evdev 的等待队列头
evdev->exist = true;
dev_no = minor;
/* Normalize device number if it falls into legacy range */
if (dev_no < EVDEV_MINOR_BASE + EVDEV_MINORS)
dev_no -= EVDEV_MINOR_BASE;
dev_set_name(&evdev->dev, "event%d", dev_no); // 设置 evdev 的设备名称
evdev->handle.dev = input_get_device(dev);
evdev->handle.name = dev_name(&evdev->dev);
evdev->handle.handler = handler;
evdev->handle.private = evdev;
evdev->dev.devt = MKDEV(INPUT_MAJOR, minor); // 设置 evdev 的设备号
evdev->dev.class = &input_class; // 设置 evdev 的设备类
evdev->dev.parent = &dev->dev; // 设置 evdev 的父设备
evdev->dev.release = evdev_free; // 设置 evdev 的释放函数
device_initialize(&evdev->dev); // 初始化 evdev 的设备结构体
error = input_register_handle(&evdev->handle); // 注册输入事件处理器
if (error)
goto err_free_evdev;
cdev_init(&evdev->cdev, &evdev_fops); // 初始化 evdev 的字符设备结构体
error = cdev_device_add(&evdev->cdev, &evdev->dev); // 将字符设备添加到系统
if (error)
goto err_cleanup_evdev;
return 0;
err_cleanup_evdev:
evdev_cleanup(evdev); // 清理 evdev
input_unregister_handle(&evdev->handle); // 反注册输入事件处理器
err_free_evdev:
put_device(&evdev->dev); // 释放 evdev 设备
err_free_minor:
input_free_minor(minor); // 释放次设备号
return error;
}
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
首先尝试获取一个新的设备次号(minor number)。如果获取失败,直接返回错误码。
用kzalloc函数为evdev结构体分配内存空间。如果内存分配失败:
- 释放之前获取的次号
- 返回错误码
初始化evdev的数据结构:
- 设置客户端列表头为空
- 初始化自旋锁、互斥锁和等待队列
- 标记evdev存在状态为"有效"
- 根据次号计算设备号并转换为标准格式
配置设备信息:
- 设置设备名称
- 配置事件处理器参数(关联设备、名称、处理函数等)
- 设置字符设备相关参数(设备号、所属类、父设备、释放函数)
注册设备:
- 初始化设备结构
- 注册事件处理器。如果注册失败,跳转到"清理evdev"步骤
- 初始化字符设备并添加到系统。如果失败,跳转到"反注册处理器并清理"步骤
如果所有步骤都成功,返回0表示连接成功
错误处理流程:
如果字符设备添加失败:
- 反注册事件处理器
- 清理evdev结构体
- 释放设备资源
- 释放次号
- 返回错误码
如果事件处理器注册失败:
- 释放设备资源
- 释放次号
- 返回错误码
整个过程遵循"先申请资源,后配置,再注册;失败则按相反顺序释放资源"的基本原则。
::: connect 函数的主要任务是将输入设备与事件处理器关联起来, 以便在事件发生时调用相应的处理函数。 它通过注册输入处理器和设置回调函数来实现这一关联, 并确保正确的事件处理器被调用。 这种关联机制允许开发者根据需要自定义处理函数, 以便根据输入设备上报的事件进行相应的处理。 :::