Linux 输入子系统:input_dev 结构体、事件上报流程、input_handler 匹配机制
好,我们直接进入正题。上一章我们聊了多点触摸的硬件基础,这一章要啃的是 Linux 输入子系统的核心。说白了,就是搞清楚你的触摸屏驱动怎么跟内核“对话”,数据怎么从硬件一路跑到用户空间。
我刚开始接触输入子系统时,觉得它就是个黑盒子。后来被一个 bug 折磨了两天,才老老实实把源码啃了一遍。嗯,今天我把这些经验拆成三块讲:input_dev 结构体、事件上报流程、input_handler 匹配机制。这三块搞懂了,你写触摸驱动时心里就有底了。
2.1 input_dev 结构体:驱动的“身份证”
每个输入设备在内核里都对应一个 input_dev 结构体。你可以把它理解成设备的“身份证”——内核靠它知道你是谁、你能干什么。
先看一眼核心字段:
struct input_dev {
const char *name; // 设备名,比如 "Goodix Touchscreen"
const char *phys; // 物理路径,比如 "i2c-0/0-005d/input0"
struct input_id id; // 总线类型、厂商ID、产品ID、版本号
unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; // 支持的事件类型位图
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; // 支持的按键码位图
unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; // 支持的相对坐标位图
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; // 支持的绝对坐标位图
unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; // 支持的其他事件位图
struct input_absinfo absinfo[ABS_CNT]; // 绝对轴信息(分辨率、最大最小值)
struct list_head h_list; // 挂载到该设备的 handler 链表
struct list_head node; // 全局 input_dev_list 链表节点
int (*open)(struct input_dev *dev);
void (*close)(struct input_dev *dev);
int (*flush)(struct input_dev *dev, struct file *file);
bool (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);
};
重点提醒:位图字段(evbit、keybit、absbit 等)是内核判断设备类型的依据。你设错了,evdev 可能根本不认你的设备。
我个人习惯在 probe 函数里这样初始化:
static int touch_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct input_dev *input_dev;
int ret;
input_dev = devm_input_allocate_device(&client->dev);
if (!input_dev)
return -ENOMEM;
input_dev->name = "My Touchscreen";
input_dev->phys = "i2c-0/0-005d/input0";
input_dev->id.bustype = BUS_I2C;
input_dev->id.vendor = 0x1234;
input_dev->id.product = 0x5678;
input_dev->id.version = 0x0100;
// 设置支持的事件类型
__set_bit(EV_ABS, input_dev->evbit); // 支持绝对坐标
__set_bit(EV_KEY, input_dev->evbit); // 支持按键事件
__set_bit(BTN_TOUCH, input_dev->keybit); // 触摸按键
// 设置触摸点坐标范围
input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0, 1024, 0, 0);
input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0, 768, 0, 0);
input_set_abs_params(input_dev, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
// 注册设备
ret = input_register_device(input_dev);
if (ret) {
dev_err(&client->dev, "Failed to register input device\n");
return ret;
}
return 0;
}
小技巧:用 devm_input_allocate_device 代替 input_allocate_device,资源自动释放,省得你写 remove 函数时忘了注销。
2.2 事件上报流程:从硬件到用户空间
设备注册好了,接下来就是上报数据。触摸屏驱动上报事件的流程其实很固定,我总结成三步:
- 填充事件:调用
input_event()或封装好的input_report_abs()、input_report_key() - 同步提交:调用
input_sync()告诉内核这一批事件发完了 - 内核分发:
input_event()内部通过input_pass_event()分发给所有匹配的 handler
看一个实际的多点触摸上报例子:
static void touch_report(struct touch_device *touch, int id, int x, int y, int pressure)
{
struct input_dev *input = touch->input;
// 第一步:上报触摸点信息
input_report_abs(input, ABS_MT_SLOT, id); // 指定槽位
input_report_abs(input, ABS_MT_TRACKING_ID, id); // 跟踪ID
input_report_abs(input, ABS_MT_POSITION_X, x);
input_report_abs(input, ABS_MT_POSITION_Y, y);
input_report_abs(input, ABS_MT_TOUCH_MAJOR, pressure);
// 第二步:同步提交
input_sync(input);
}
这里有个坑,我曾经踩过:每次上报完触摸点必须调 input_sync()。内核靠 SYN_REPORT 事件来切分数据包。你不调 sync,用户空间收不到完整的一帧数据,触摸点会卡住不动。
事件在内核里的流转路径是这样的:
驱动调用 input_event()
└─> input_handle_event() // 事件过滤、去重
└─> input_pass_event() // 遍历 dev->h_list,分发给 handler
└─> handler->event() // 比如 evdev 的 evdev_event()
└─> 写入 evdev 客户端缓冲区
└─> 唤醒等待的 read() 进程
注意:input_handle_event() 里有个去重逻辑——如果上报的值跟上次一样,内核会直接丢弃。所以触摸驱动里不要重复上报相同的坐标值,否则用户空间可能收不到事件。
我建议你在调试时打开 CONFIG_INPUT_EVBUG 内核选项,它会把所有输入事件打印到内核日志里。不过生产环境千万别开,日志会被刷爆。
2.3 input_handler 匹配机制:谁来处理你的事件?
设备注册了,事件上报了,那谁来消费这些事件?答案是 input_handler。
内核里预置了好几个 handler:
| Handler 名称 | 功能 | 典型设备节点 |
|---|---|---|
| evdev | 通用事件处理,用户空间通过 /dev/input/eventX 读取 | /dev/input/event0 |
| joydev | 游戏杆专用 | /dev/input/js0 |
| mousedev | 模拟鼠标(把绝对坐标转成相对移动) | /dev/input/mouse0 |
| keyboard | 键盘专用 | /dev/tty 等 |
匹配过程其实不复杂。每个 handler 都有一个 id_table 或 match() 回调。当 input_register_device() 被调用时,内核会遍历所有已注册的 handler,逐个调用 handler->match() 或检查 handler->id_table。
拿 evdev 举例,它的匹配逻辑很简单:
static const struct input_device_id evdev_ids[] = {
{ .driver_info = 1 }, // 匹配所有设备
{ }, // 终止符
};
static int evdev_connect(struct input_handler *handler, struct input_dev *dev,
const struct input_device_id *id)
{
// 为每个匹配的设备创建一个 evdev 客户端
// 分配次设备号,创建 /dev/input/eventX 节点
}
说白了,evdev 来者不拒——只要是个输入设备,它就给你创建个 eventX 节点。但有些 handler 会挑设备,比如 mousedev 只匹配设置了 EV_REL 或 EV_ABS 且没有 EV_KEY 的设备。
关键点:你的触摸屏驱动要想被 evdev 识别,evbit 里必须设置 EV_ABS。如果你只设了 EV_KEY,内核会把它当成按键设备,不会创建触摸相关的 event 节点。
匹配完成后,handler 会调用 connect() 回调,创建一个 input_handle 结构体,把它挂到 dev->h_list 上。这样后面上报事件时,input_pass_event() 就能找到对应的 handler 去分发。
我画了一张图,把整个流程串起来:
2.4 避坑指南:我踩过的三个坑
最后分享几个实战中容易翻车的地方:
- 位图没设全:只设了
EV_ABS忘了设EV_KEY,结果BTN_TOUCH事件发不出去。用户空间收不到触摸按下/抬起状态。 - ABS_MT_SLOT 顺序搞错:必须先设 SLOT,再设其他 ABS_MT_* 值。顺序反了,内核会把坐标值关联到错误的槽位。
- 忘记调 input_sync():这个我犯过好几次。调试时发现触摸点不动了,一查日志,SYN_REPORT 没发出去。
严重警告:不要在中断上下文里调用 input_register_device()!这个函数会睡眠,而中断上下文不允许睡眠。我曾经在客户现场排查一个随机死机问题,最后发现是驱动在中断里注册设备导致的。