📖 推荐博主书籍:《Yocto项目实战教程:高效定制嵌入式Linux系统》
🎥 更多学习视频请关注 B 站:嵌入式Jerry
Linux 输入子系统(Input Subsystem)一文吃透:结构、调用链、关键 API 与实战
目标:用一篇文把 Linux 输入子系统讲清楚:它做什么、分几层、怎么绑定、哪些关键结构体与函数、如何写一个最小的输入驱动、如何从用户空间读取/注入事件、以及常见调试方法。

1. 快速总览(一句话 + 总体结构图)
一句话:输入子系统把各种硬件(键盘、触摸、鼠标、手柄…)的原始数据标准化为事件(EV_KEY/EV_ABS/EV_REL/...),通过 handler(如 evdev) 暴露为 /dev/input/eventX 给用户程序。
硬件中断/轮询
│
▼
设备驱动(Producer)—— drivers/input/*
- 创建并注册:struct input_dev
- 上报事件:input_report_*() + input_sync()
│
▼
Input Core(撮合/转发)—— drivers/input/input.c
- 维护设备/handler 列表
- 匹配:input_match_device()
- 绑定:handler->connect()
- 转发:input_event()
│
▼
事件处理层 Handler(Consumer)—— 以 evdev 为例
- /dev/input/eventX(字符设备接口)
- evdev_event()/evdev_read()/ioctl
│
▼
用户空间:Xorg/Wayland(libinput) 或 直接 read()/libevdev
2. 关键数据结构与事件类型
2.1 核心结构体(头文件:include/linux/input.h)
struct input_dev:设备驱动创建的输入设备对象;包含能力位图、回调(open/close)等。struct input_handler:事件驱动(如 evdev/mousedev/joydev)的抽象;定义connect/disconnect/event回调与匹配表id_table。struct input_handle:纽带,把一个input_dev与一个input_handler连接起来。struct input_event(UAPI:include/uapi/linux/input.h):用户空间读到的事件结构:
struct input_event {
struct timeval time; /* 时间戳 */
__u16 type; /* EV_* */
__u16 code; /* KEY_* / ABS_* / REL_* / ... */
__s32 value; /* 值 */
};
2.2 常见事件类型(速查表)
| 类型 | 含义 | 典型 code | value 例子 |
|---|---|---|---|
EV_KEY | 按键/按钮 | KEY_A, BTN_TOUCH, KEY_POWER | 0=松开, 1=按下, 2=自动重复 |
EV_ABS | 绝对轴 | ABS_X/Y, ABS_MT_* | 坐标/压力/面积等 |
EV_REL | 相对轴 | REL_X/Y, REL_WHEEL | 相对位移/滚轮 |
EV_SYN | 同步 | SYN_REPORT, SYN_MT_REPORT, SYN_DROPPED | 帧结束/MT触点/丢帧提示 |
要点:用户态通常按帧处理事件:一组
EV_*之后会跟一个EV_SYN/SYN_REPORT表示“该帧结束”。
3. 从设备树到 /dev/input/eventX 的链路
以 I²C 触摸(如 maXTouch)为例:
-
DTS 匹配:
compatible = "atmel,maxtouch"→ I²C 框架创建i2c_client并与驱动匹配。 -
probe:驱动获取电源/中断/GPIO/pinctrl 等资源。
-
注册 input_dev:
devm_input_allocate_device()- 声明能力
input_set_capability()/ 设轴input_set_abs_params()/ 多点触控input_mt_init_slots() input_register_device()
-
input core 撮合 handler:遍历已注册 handler(如 evdev),按
id_table + 能力位图匹配 → 调用handler->connect()。 -
evdev 生成节点:创建
/dev/input/eventX字符设备。 -
中断上报:IRQ 线程解析触点 →
input_report_*()+input_mt_sync_frame()+input_sync()→ 用户态可读。
4. 设备驱动(Producer)最小骨架(按键示例)
类似
drivers/input/keyboard/gpio_keys.c思路,演示 input API 最小用法。
// SPDX-License-Identifier: GPL-2.0
#include <linux/input.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/platform_device.h>
struct mykey {
struct input_dev *idev;
struct gpio_desc *gpiod;
int irq;
};
static irqreturn_t mykey_irq(int irq, void *devid)
{
struct mykey *mk = devid;
int v = gpiod_get_value_cansleep(mk->gpiod); // 1=高电平,假定按下
input_report_key(mk->idev, KEY_POWER, v);
input_sync(mk->idev); // 一帧结束(很关键)
return IRQ_HANDLED;
}
static int mykey_probe(struct platform_device *pdev)
{
struct mykey *mk = devm_kzalloc(&pdev->dev, sizeof(*mk), GFP_KERNEL);
int err;
mk->idev = devm_input_allocate_device(&pdev->dev);
if (!mk->idev) return -ENOMEM;
mk->idev->name = "mykey-power";
mk->idev->phys = "input/mykey";
input_set_capability(mk->idev, EV_KEY, KEY_POWER);
err = input_register_device(mk->idev);
if (err) return err;
mk->gpiod = devm_gpiod_get(&pdev->dev, NULL, GPIOD_IN);
if (IS_ERR(mk->gpiod)) return PTR_ERR(mk->gpiod);
mk->irq = gpiod_to_irq(mk->gpiod);
if (mk->irq < 0) return mk->irq;
err = devm_request_threaded_irq(&pdev->dev, mk->irq, NULL, mykey_irq,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
dev_name(&pdev->dev), mk);
return err;
}
static const struct of_device_id mykey_of[] = {
{ .compatible = "myvendor,mykey-power" },
{ }
};
MODULE_DEVICE_TABLE(of, mykey_of);
static struct platform_driver mykey_drv = {
.probe = mykey_probe,
.driver = {
.name = "mykey",
.of_match_table = mykey_of,
},
};
module_platform_driver(mykey_drv);
MODULE_LICENSE("GPL");
检查点:
- 有
input_register_device();- 有
input_report_key()+input_sync();- 能力声明了
EV_KEY/KEY_POWER。
5. Input Core:撮合与转发(drivers/input/input.c)
-
注册设备:
input_register_device(struct input_dev *dev)- 把
dev放入全局设备链表; - 遍历所有
input_handler,用input_match_device()按id_table + 能力位图匹配; - 成功则调用
handler->connect(handler, dev, id),生成input_handle绑定二者。
- 把
-
注册 handler:
input_register_handler(struct input_handler *handler)- 把
handler放入全局 handler 链表; - 反向遍历现有设备,逐个尝试连接(同上)。
- 把
-
事件转发:
input_event(dev, type, code, value)- 对
dev绑定的每个 handle:调用handle->handler->event(handle, ...)。
- 对
-
open/close 机制:
- 第一次有人打开某个 handler 对应的设备文件(如
/dev/input/eventX) →input_open_device()计数从 0→1 → 调dev->open()(设备驱动提供,常用于上电/使能中断)。 - 最后一个关闭 →
input_close_device()计数从 1→0 → 调dev->close()。
- 第一次有人打开某个 handler 对应的设备文件(如
6. Handler:以 evdev 为例(drivers/input/evdev.c)
- 注册 handler:
static const struct input_handler evdev_handler = {
.event = evdev_event,
.connect = evdev_connect,
.disconnect = evdev_disconnect,
.name = "evdev",
.id_table = evdev_ids, // 基本匹配所有常见输入设备
};
static int __init evdev_init(void)
{ return input_register_handler(&evdev_handler); }
-
connect():创建设备节点
/dev/input/eventX,设置file_operations evdev_fops:evdev_open()/release():打开/关闭时创建/销毁evdev_client(每个进程一个环形缓冲)。evdev_read():把struct input_event从环形缓冲拷给用户态;配合evdev_poll()支持poll/epoll。evdev_ioctl():EVIOCGNAME/EVIOCGBIT/EVIOCGABS/EVIOCGRAB/...。
-
event():写入 client 缓冲
- 每次
input_event()调到evdev_event(),把(type,code,value)写到所有打开者的环形缓冲; - 帧结束由
EV_SYN/SYN_REPORT标识; - 若缓冲溢出,向该 client 注入
SYN_DROPPED,提示用户态重同步。
- 每次
7. 多点触控(MT)最常用套路
- 初始化槽位:
input_mt_init_slots(input, slots, INPUT_MT_DIRECT); - 上报一帧(Type-B 协议):
for_each_contact(...) {
input_mt_slot(input, slot_id);
input_mt_report_slot_state(input, MT_TOOL_FINGER, true);
input_report_abs(input, ABS_MT_POSITION_X, x);
input_report_abs(input, ABS_MT_POSITION_Y, y);
}
input_report_key(input, BTN_TOUCH, contacts > 0);
input_mt_sync_frame(input);
input_sync(input);
注意:坐标范围需用
input_set_abs_params(input, ABS_MT_POSITION_X, min, max, fuzz, flat)预先声明。
8. 用户空间:读取与注入
8.1 直接读取(最小 C 示例)
#include <linux/input.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ioctl.h>
int main(int argc, char **argv) {
const char *dev = argc > 1 ? argv[1] : "/dev/input/event0";
int fd = open(dev, O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
char name[256];
ioctl(fd, EVIOCGNAME(sizeof(name)), name);
printf("device: %s\n", name);
struct input_event ev;
while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
printf("%ld.%06ld type=%04x code=%04x value=%d\n",
(long)ev.time.tv_sec, (long)ev.time.tv_usec,
ev.type, ev.code, ev.value);
}
return 0;
}
桌面环境(Wayland/Xorg)下,普通应用通常不直接读
eventX,而是通过 GUI 框架事件;无合成器(EGLFS/fbdev/DRM 自绘)时可直接读或用 libevdev/libinput。
8.2 用户空间注入:uinput(最小示例:按 A 键)
#include <linux/uinput.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
static void emit(int fd, __u16 type, __u16 code, __s32 val) {
struct input_event ie = {0};
gettimeofday(&ie.time, NULL);
ie.type=type; ie.code=code; ie.value=val;
write(fd, &ie, sizeof(ie));
}
int main(){
int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
ioctl(fd, UI_SET_EVBIT, EV_KEY);
ioctl(fd, UI_SET_KEYBIT, KEY_A);
struct uinput_setup us = { .id.bustype=BUS_USB };
strcpy(us.name, "demo-uinput");
ioctl(fd, UI_DEV_SETUP, &us);
ioctl(fd, UI_DEV_CREATE);
emit(fd, EV_KEY, KEY_A, 1); emit(fd, EV_SYN, SYN_REPORT, 0);
usleep(100000);
emit(fd, EV_KEY, KEY_A, 0); emit(fd, EV_SYN, SYN_REPORT, 0);
ioctl(fd, UI_DEV_DESTROY); close(fd); return 0;
}
需
modprobe uinput,并有权限写/dev/uinput。
9. 调试 & 排错清单
-
设备是否注册为 input:
cat /proc/bus/input/devices(看是否有你的设备名与Handlers= eventX)。 -
从 eventX 反查:
- 名称:
cat /sys/class/input/eventX/device/name - handler:
readlink -f /sys/class/input/eventX/device/driver(应为.../drivers/input/evdev) - DTS 节点:
readlink -f /sys/class/input/eventX/device/of_node
- 名称:
-
Kconfig:
CONFIG_INPUT,CONFIG_INPUT_EVDEV是否开启。 -
常见坑:
- 没声明能力(
input_set_capability/input_set_abs_params)→ 用户态看不到对应事件。 - 忘记
input_sync()→ 应用端“卡帧”。 - 多点触控未调用
input_mt_sync_frame()→ slot 同步错乱。 - 缓冲溢出 → 收到
SYN_DROPPED,需要应用侧重同步。 - 权限/独占:Wayland 下应用通常无权直接读
eventX;或被他人EVIOCGRAB抢占。
- 没声明能力(
10. 快速归属 & 源码定位技巧(5 分钟内搞定)
- 搜注册 API:
rg -n "input_register_device|led_classdev_register|video_register_device|register_netdev"(判断属于哪个功能子系统)。 - 搜标志结构体:
rg -n "struct input_dev|struct input_handler|struct evdev" drivers/。 - 看文件路径:
drivers/input/*基本实锤;drivers/leds/*是 LED 子系统(不是 input)。 - 看 bindings:
Documentation/devicetree/bindings/input/*.yaml。
11. 常见问答(FAQ)
Q: 设备驱动(device driver)与事件驱动(event driver/handler)有什么区别?如何绑定?
A: 设备驱动面向硬件,创建 input_dev 并 input_report_*;事件驱动面向 input core 事件(如 evdev),以 struct input_handler 形式注册。绑定由 input core 在 input_register_device() 与 input_register_handler() 时互相撮合,通过 input_match_device() 匹配,handler->connect() 生成 input_handle 连接。
Q: evdev 的作用?
A: 提供通用字符设备接口 /dev/input/eventX。把 input core 的事件写入每个打开者的环形缓冲(evdev_event()),用户态通过 read/poll/ioctl 读取(evdev_read())。
Q: Wayland/Xorg 存在时应用还要直接读 eventX 吗?
A: 一般不需要。桌面环境通过 libinput 统一接管输入,应用用 GUI 框架事件即可。若无合成器(EGLFS/DRM 自绘),可直接读 evdev 或用 libevdev/libinput。
Q: devm_ 前缀是什么?
A: device-managed 的缩写:资源与 struct device 生命周期绑定,probe() 失败或 remove() 时自动释放,对应释放由 devres 机制管理。
12. 关键文件与目录索引(方便 grep)
- 核心:
drivers/input/input.c,include/linux/input.h,include/uapi/linux/input.h - handler:
drivers/input/evdev.c(以及mousedev.c、joydev.c) - 多点触控:
drivers/input/input-mt.c - 示例驱动:
drivers/input/touchscreen/*、drivers/input/keyboard/* - 工具:用户空间
evtest;库:libevdev、libinput
13. 小结
- 结构:设备驱动(Producer)→ Input Core(撮合/转发)→ Handler(Consumer,evdev 等)→ 用户空间。
- 关键:
input_register_device()、input_register_handler()、input_event()、evdev_connect/event/read/ioctl。 - 实战:声明能力 → 注册
input_dev→ 中断上报 +input_sync;用户侧读eventX或交给桌面/框架。
把本文打印一页贴墙,配合
rg/grep思路去读驱动代码,很快就能在面试或项目里从容应答与定位问题。
📖 推荐博主书籍:《Yocto项目实战教程:高效定制嵌入式Linux系统》
🎥 更多学习视频请关注 B 站:嵌入式Jerry
1953

被折叠的 条评论
为什么被折叠?



