☰
一次屏幕点击的内核之旅:Linux Input 子系统源码走读(Linux 6.18.7)
2026/10/8 6:46:11 网站建设 项目流程

一次屏幕点击的内核之旅: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=eventXinput.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_probeprobe 阶段资源获取失败(电源/GPIO/中断号)goodix.c:975-999
Error reading N bytes from 0xXXXXI2C 读坐标寄存器失败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 无从下手:

  1. 过滤是静默的。input_event()发现事件类型不在evbit菜单里就直接返回,不打印任何日志(input.c:394)。“没日志"不等于"没走到这”,必须用 getevent 在③④层分界。

  2. 自动去重。坐标值和上次一样的事件不会往下送(EV_KEY 见input.c:244,EV_ABS 见input.c:193)。手指按住不动时没有 MOVE 事件是正常的。

  3. fuzz 去抖。坐标变化小于fuzz阈值会被压平(input.c:191的input_defuzz_abs_event),这是防抖动特性,不是丢数据。

  4. SYN_DROPPED = 缓冲区溢出。应用读得太慢、环形缓冲区写满时,evdev 丢弃旧事件并插入一个SYN_DROPPED(evdev.c:220-233)。官方文档明确规定客户端收到它后的行为:丢弃直到下一个 SYN_REPORT 的所有事件,并用EVIOCG*ioctl 重新查询设备当前状态(Documentation/input/event-codes.rst:113-118)。getevent 流里看到它,说明消费者太慢,而不是驱动有问题。

  5. 设备可以被"禁用"(inhibited)。dev->inhibited置位后所有事件在核心层入口就被丢弃(input.c:215-216), sysfs 里有对应的inhibited属性——排查"设备突然哑了"时别忘了它。

  6. EVIOCGRAB 独占。某个进程调用EVIOCGRAB后,其他客户端都收不到事件(evdev.c:1084,分发逻辑见input.c:120-124的 grab 判断)。"别的进程收不到事件"先查谁 grab 了设备。

  7. 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最原始的二进制流,排除工具干扰⑤
dmesgprobe / 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/

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询