一次屏幕点击的内核之旅:Linux Input 子系统源码走读(Linux 6.18.7)
本文参考微信公众号文章《以手机触摸屏为例讲透 Linux Input 子系统》,
但所有代码都替换成了本地 Linux 6.18.7 真实源码(linux-6.18.7-ref/drivers/input/),
每一段结论都能跳到对应文件和行号验证。
一、30 秒看懂整体框架
手机上输入设备五花八门:触摸屏、电源键、指纹……如果每种设备都给应用单独定制接口,谁也维护不起。内核的办法是把"谁生产数据"和"谁消费数据"彻底解耦,中间由 Input Core 居中协调,分三层:
触摸 IC (I2C) │ 中断 ▼ ┌─────────────────┐ │ 设备驱动层 │ goodix.c:读寄存器,翻译成标准事件 ├─────────────────┤ │ Input Core │ input.c:注册、匹配、过滤、分发 ├─────────────────┤ │ 事件处理层 │ evdev.c:把事件写进 /dev/input/eventX └─────────────────┘ │ read() ▼ Android InputReader → InputDispatcher → App 的 onTouchEvent()快递比喻:驱动是寄件人,Input Core 是分拣中心,evdev 是快递公司,/dev/input/eventX就是你家门口的快递柜。应用只盯快递柜,不关心货是谁寄的。
二、三个结构体:设备、处理器、牵手凭证
理解整个子系统只需抓住三个角色:
| 结构体 | 角色 | 在哪定义 |
|---|---|---|
struct input_dev | 输入设备(触摸屏) | include/linux/input.h |
struct input_handler | 事件处理器(evdev 等) | include/linux/input.h |
struct input_handle | 两者的绑定关系 | include/linux/input.h |
evdev 这个 handler 在内核里长这样(drivers/input/evdev.c:1421):
staticstructinput_handlerevdev_handler={.events=evdev_events,.connect=evdev_connect,.disconnect=evdev_disconnect,.name="evdev",.id_table=evdev_ids,// 匹配规则};设备注册时,核心层把它挂上全局链表input_dev_list,然后遍历所有 handler 逐个"相亲"(drivers/input/input.c:2375):
list_add_tail(&dev->node,&input_dev_list);list_for_each_entry(handler,&input_handler_list,node)input_attach_handler(dev,handler);input_attach_handler(input.c:985)先用id_table匹配,成功就调用handler->connect()创建一个input_handle把双方绑定。关系是多对多的:一个触摸屏可以同时喂多个处理器,一个 evdev 也能服务多个设备。
三、驱动层:TP 驱动只干三件事
触摸 IC(如汇顶 Goodix)以 100~200Hz 扫描电容矩阵,手指落下时拉低 INT 引脚通知 SoC,驱动再经 I2C 读出坐标。对应到代码就是三件事:管 I2C、接中断、报坐标。看真实的drivers/input/touchscreen/goodix.c:
① probe 里注册设备——分配input_dev,用input_set_capability声明"我会报什么"(相当于填菜单),最后注册(goodix.c:1139,1167,1215,1235):
ts->input_dev=devm_input_allocate_device(&ts->client->dev);...input_set_capability(ts->input_dev,EV_ABS,ABS_MT_POSITION_X);input_set_capability(ts->input_dev,EV_ABS,ABS_MT_POSITION_Y);...input_mt_init_slots(ts->input_dev,ts->max_touch_num,...);...error=input_register_device(ts->input_dev);② 接中断(goodix.c:549):
devm_request_threaded_irq(&ts->client->dev,ts->client->irq,NULL,goodix_ts_irq_handler,...);③ 中断里读坐标、报事件(goodix.c:516 → 468),灵魂就是这几个上报函数:
staticirqreturn_tgoodix_ts_irq_handler(intirq,void*dev_id){goodix_process_events(ts);// I2C 读坐标,逐触点上报...}// goodix_process_events 内部(goodix.c:489-499):for(i=0;i<touch_num;i++)goodix_ts_report_touch_8b(ts,...);// 内部调用:// input_mt_slot(dev, id); // 第 i 号触点// input_report_abs(dev, ABS_MT_POSITION_X, x); // 报坐标input_mt_sync_frame(ts->input_dev);input_sync(ts->input_dev);// SYN_REPORT:一帧结束经典坑:
input_set_capability菜单填漏了,坐标会被核心层直接丢弃——
报上去了但应用永远收不到。原因就在下一节的过滤逻辑里。
四、核心层:一个事件的内核流水线
input_report_abs()其实只是个内联包装(include/linux/input.h:447),真正入口是input_event()。一次上报在 Input Core 里走四步:
input_report_abs() include/linux/input.h:447 └─ input_event() drivers/input/input.c:391 ① 查菜单:事件类型不在 evbit 里?直接丢弃 └─ input_handle_event() input.c:358 ② input_get_disposition() input.c:208 - 没声明的 code?丢 - 值和上次一样?丢(去重) - 坐标抖动?fuzz 滤波(input.c:191) ③ input_event_dispose() input.c:318 攒进 dev->vals[] 数组,SYN_REPORT 时整帧刷出 ④ input_pass_values() input.c:111 遍历设备的 handle 链表,逐个调 handler->handle_events()第①步就是"菜单填漏导致坐标消失"的源码铁证(input.c:391):
voidinput_event(structinput_dev*dev,unsignedinttype,unsignedintcode,intvalue){if(is_event_supported(type,dev->evbit,EV_MAX)){guard(spinlock_irqsave)(&dev->event_lock);input_handle_event(dev,type,code,value);}}第②步里还有个容易忽略的细节:核心层会自动去重——坐标没变化的事件不会往下送(input.c:244的 EV_KEY 状态比较、input.c:193的 EV_ABS 比较),这减轻了用户空间的负担。
五、evdev:写进环形缓冲区,叫醒读者
事件最终落到 evdev。它的events回调把事件打上时间戳、写入每个打开者各自的环形缓冲区,收到 SYN_REPORT 就唤醒等待的进程(drivers/input/evdev.c:244-286):
staticvoidevdev_pass_values(structevdev_client*client,conststructinput_value*vals,...){...for(v=vals;v!=vals+count;v++){...__pass_event(client,&event);// 写环形缓冲区if(SYN_REPORT)wakeup=true;}...if(wakeup)wake_up_interruptible_poll(&client->wait,EPOLLIN|...);}而阻塞在read()上的用户进程(比如 Android InputReader),此时被wait_event_interruptible唤醒,读出一个个struct input_event(evdev.c:556,596):
structinput_event{// include/uapi/linux/input.h:28structtimevaltime;// 时间戳__u16 type;// EV_ABS / EV_KEY / EV_SYN__u16 code;// ABS_MT_POSITION_X / BTN_TOUCH ...__s32 value;// 值};六、亲眼看一看:getevent
在 Android 设备上adb shell getevent -lt /dev/input/eventX,单指点击能看到:
EV_ABS ABS_MT_SLOT 00000000 # 0 号槽位 EV_ABS ABS_MT_TRACKING_ID 000003e7 # 触点获得 ID EV_ABS ABS_MT_POSITION_X 00000182 # x = 386 EV_ABS ABS_MT_POSITION_Y 000004b4 # y = 1204 EV_KEY BTN_TOUCH DOWN # 按下 EV_SYN SYN_REPORT 00000000 # 一帧结束 ... # 抬起时 TRACKING_ID = -1每一行都对应第四节流水线的产物。
七、调试速查
| 症状 | 典型原因 |
|---|---|
| 点击完全无反应 | 中断没触发(极性配错)、I2C 失败、probe 失败 |
| 坐标颠倒/镜像 | 触摸 IC 坐标方向与屏幕装配不一致 |
| 只有第一下有效 | 忘了input_sync() |
| 报了事件但应用收不到 | 能力位没声明,被input_event()过滤(第四节①) |
| 多指混乱 | slot 协议 A/B 配置错误 |
常用命令:cat /proc/bus/input/devices(看注册了什么)、getevent -lt(看原始事件)、dmesg | grep goodix(看驱动日志)。
八、遇到问题怎么办:分层定位与日志分析
这一节结合内核官方文档(Documentation/input/)和 6.18.7 源码,给出系统化的排查方法。
8.1 总思路:自底向上,逐层"分界"
Input 问题最大的难点是链路长、环节多。正确姿势不是乱猜,而是每一层找一个"观测点",先确定问题卡在哪一层,再深入:
| 层 | 观测手段 | 判断依据 | 源码依据 |
|---|---|---|---|
| ① 驱动注册 | dmesg | 有无 probe 报错、input: xxx as /devices/...注册日志 | input.c:2367(pr_info)、goodix.c:975-999(dev_err_probe) |
| ② 设备就位 | cat /proc/bus/input/devices | 能看到设备名和H: Handlers=eventX | input.c:1089-1102(procfs 输出) |
| ③ 驱动上报 | getevent -lt/evtest | 摸屏时有事件→ 驱动以上都正常 | — |
| ④ Core 分发 | 对比 dmesg 与 getevent | 驱动报了但 getevent 没有 → 被核心层过滤 | input.c:394(能力位)、input.c:244(去重) |
| ⑤ evdev 送达 | getevent 流里找SYN_DROPPED | 缓冲区溢出丢帧 | evdev.c:220-233 |
| ⑥ 用户空间 | Android:dumpsys input、logcat | 内核有事件但 App 无响应 → InputReader/Dispatcher 或权限问题 | — |
举例:“点击完全无反应”——先cat /proc/bus/input/devices,设备不在 → 问题在①②层(probe/中断/I2C);设备在但getevent无输出 → ③层(中断没触发或 I2C 读失败);getevent 有输出但 App 不动 → ⑥层。一次二分就把范围缩小到一层。
8.2 日志怎么看
驱动日志(dmesg)——以 goodix 为例,每个失败点都有明确报错:
dmesg|grep-iE"goodix|input"| 日志 | 含义 | 源码位置 |
|---|---|---|
Failed to get AVDD28 regulator等 dev_err_probe | probe 阶段资源获取失败(电源/GPIO/中断号) | goodix.c:975-999 |
Error reading N bytes from 0xXXXX | I2C 读坐标寄存器失败 | goodix.c:192 |
Controller reset failed | 触摸 IC 复位失败(唤醒后失灵常见) | goodix.c:802 |
Controller irq sync failed | 中断引脚同步失败 | goodix.c:766 |
input: Goodix Capacitive TouchScreen as /devices/... | 注册成功 | input.c:2367-2370 |
failed to attach handler evdev to device ... | handler 匹配/绑定失败 | input.c:996 |
/proc/bus/input/devices解读——这段输出由input.c:1089-1102生成:
N: Name="Goodix Capacitive TouchScreen" ← 设备名 P: Phys=i2c-GT911/... ← 物理路径(挂在哪条总线) S: Sysfs=/devices/.../input/input3 ← sysfs 位置 H: Handlers=event2 ← 关键!绑定到 /dev/input/event2 B: EV=b / B: ABS=... ← 能力位(菜单)的十六进制位图H:行告诉你该getevent哪个节点;B: EV/ABS/KEY就是第三节说的"菜单",怀疑能力位问题时直接对照这里。
8.3 源码里藏着的"反直觉"行为(排障高频坑)
这些行为都有源码铁证,不理解它们就会把正常现象当 bug,或对着真 bug 无从下手:
过滤是静默的。
input_event()发现事件类型不在evbit菜单里就直接返回,不打印任何日志(input.c:394)。“没日志"不等于"没走到这”,必须用 getevent 在③④层分界。自动去重。坐标值和上次一样的事件不会往下送(EV_KEY 见
input.c:244,EV_ABS 见input.c:193)。手指按住不动时没有 MOVE 事件是正常的。fuzz 去抖。坐标变化小于
fuzz阈值会被压平(input.c:191的input_defuzz_abs_event),这是防抖动特性,不是丢数据。SYN_DROPPED = 缓冲区溢出。应用读得太慢、环形缓冲区写满时,evdev 丢弃旧事件并插入一个
SYN_DROPPED(evdev.c:220-233)。官方文档明确规定客户端收到它后的行为:丢弃直到下一个 SYN_REPORT 的所有事件,并用EVIOCG*ioctl 重新查询设备当前状态(Documentation/input/event-codes.rst:113-118)。getevent 流里看到它,说明消费者太慢,而不是驱动有问题。设备可以被"禁用"(inhibited)。
dev->inhibited置位后所有事件在核心层入口就被丢弃(input.c:215-216), sysfs 里有对应的inhibited属性——排查"设备突然哑了"时别忘了它。EVIOCGRAB 独占。某个进程调用
EVIOCGRAB后,其他客户端都收不到事件(evdev.c:1084,分发逻辑见input.c:120-124的 grab 判断)。"别的进程收不到事件"先查谁 grab 了设备。read 以帧为单位。evdev 只在收到 SYN_REPORT 后才推进
packet_head、唤醒读者(evdev.c:238-241),所以驱动忘了input_sync()时,事件其实已经在缓冲区里,只是永远凑不成一帧——这就是"只有第一下有效"的机理。
8.4 多指问题:先分清协议 A 还是 B
官方文档Documentation/input/multi-touch-protocol.rst是权威依据:
- Type A(匿名触点):硬件不区分手指身份,驱动按顺序报所有触点,触点之间用
SYN_MT_REPORT分隔(文档 :20, :60)。适合无跟踪能力的旧屏。 - Type B(可跟踪触点):每个触点一个 slot,用
input_mt_slot()开头,ABS_MT_TRACKING_ID标识身份(文档 :44, :67)。TRACKING_ID 为 -1 表示该手指抬起(文档 :152)。
排障方法:getevent -lt抓一段多指操作,对照文档 :135-162 的官方范例序列逐帧比对——TRACKING_ID 是否单调递增、抬起时是否报 -1、slot 是否与触点一一对应。哪一帧对不上,bug 就在哪。"多指混乱、手指漂移"几乎都是 slot 映射或 TRACKING_ID 生命周期管理出错。
8.5 休眠唤醒后失灵
这类问题横跨两层,两边都要看:
- 驱动层:
goodix_suspend/goodix_resume(goodix.c:1427, :1474),resume 里会调用goodix_reset()重新复位触摸 IC(goodix.c:1507)。唤醒后没复位 IC、中断引脚电平没恢复,是此类问题的头号原因——dmesg 里找Controller reset failed。 - 系统层:整机 suspend/resume 流程在
kernel/power/suspend.c,唤醒源(wakeup source)的注册与统计在drivers/base/power/wakeup.c——怀疑 TP 中断把系统唤醒异常、或唤醒后中断丢失时,可以结合/sys/kernel/debug/wakeup_sources一起看。
8.6 工具箱
| 工具 | 用途 | 作用于哪层 |
|---|---|---|
cat /proc/bus/input/devices | 看注册了哪些设备、绑到哪个 eventX、能力位 | ①② |
getevent -lt(Android)/evtest(Linux) | 实时观察原始事件流,分界③④层的主力 | ③④⑤ |
hexdump /dev/input/eventX | 最原始的二进制流,排除工具干扰 | ⑤ |
dmesg | probe / I2C / 复位 / 中断错误日志 | ① |
uinput | 反向注入假事件测试用户空间(官方文档uinput.rst) | ⑥ |
Android:dumpsys input、logcatInputReader/InputDispatcher | 用户空间两角色的内部状态 | ⑥ |
evtest --grab | 独占设备做实验,排除其他消费者干扰 | ⑤ |
8.7 升级版速查表(症状 → 层 → 验证)
| 症状 | 最可能卡在 | 第一步验证 |
|---|---|---|
| 点击完全无反应 | ① 或 ③ | cat /proc/bus/input/devices有无设备;有则getevent看有无输出 |
| 报了事件但应用收不到 | ④ 或 ⑥ | 对照/proc/bus/input/devices的能力位;查 SELinux 权限 |
| 只有第一下有效 | ③ | 检查驱动是否每帧都调input_sync() |
| 多指混乱/漂移 | ③ | 按 8.4 用 getevent 对照协议 B 范例序列 |
| 事件流里出现 SYN_DROPPED | ⑤ | 消费者读得太慢,查用户空间进程调度 |
| 休眠唤醒后失灵 | ①(resume 路径) | dmesg 找Controller reset failed/ irq sync 报错 |
| 别的进程收不到事件 | ⑤ | 查是否有进程对节点做了EVIOCGRAB |
九、一句话总结
触摸 IC把手指变成寄存器里的数字;TP 驱动在中断里读出数字,用input_mt_slot/input_report_abs/input_sync翻译成标准事件;Input Core对照"菜单"过滤、去重、分发给匹配好的evdev;事件进入/dev/input/eventX唤醒InputReader,一路翻译到 App 的onTouchEvent()。
数据一路向上"翻译",控制一路向下"分层"——驱动只管报数,应用只管消费,中间的一切交给统一框架。这就是 Input 子系统设计的优雅之处。
本文涉及的源码与官方文档(Linux 6.18.7)
linux-6.18.7-ref/drivers/input/input.c— Input Core:注册匹配(:2375)、事件过滤分发(:208, :318, :111, :391)、procfs 输出(:1089)linux-6.18.7-ref/drivers/input/evdev.c— evdev 处理器:缓冲区与唤醒(:244)、SYN_DROPPED(:220)、阻塞读(:556)、handler 定义(:1421)linux-6.18.7-ref/drivers/input/touchscreen/goodix.c— 真实 TP 驱动:probe(:1139)、中断上报(:468, :516)、错误日志点(:192, :802)、suspend/resume(:1427, :1474)linux-6.18.7-ref/include/linux/input.h— 上报 API 内联包装(:437, :447, :462)linux-6.18.7-ref/include/uapi/linux/input.h—struct input_event(:28)linux-6.18.7-ref/Documentation/input/event-codes.rst— 事件编码与 SYN_DROPPED 语义(:113)linux-6.18.7-ref/Documentation/input/multi-touch-protocol.rst— 多点触控协议 A/B 官方规范linux-6.18.7-ref/Documentation/input/uinput.rst— 用户空间事件注入接口kernel/power/suspend.c、drivers/base/power/wakeup.c— 休眠/唤醒源(排休眠类问题用)
参考文章:https://mp.weixin.qq.com/s/dYGEWzJi8qqtLY3F3XX4iw
内核官方文档在线版:https://docs.kernel.org/input/