Linux 输入子系统(Input Subsystem)一文吃透:结构、调用链、关键 API 与实战


📖 推荐博主书籍:《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 常见事件类型(速查表)

类型含义典型 codevalue 例子
EV_KEY按键/按钮KEY_A, BTN_TOUCH, KEY_POWER0=松开, 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)为例:

  1. DTS 匹配compatible = "atmel,maxtouch" → I²C 框架创建 i2c_client 并与驱动匹配。

  2. probe:驱动获取电源/中断/GPIO/pinctrl 等资源。

  3. 注册 input_dev

    • devm_input_allocate_device()
    • 声明能力 input_set_capability() / 设轴 input_set_abs_params() / 多点触控 input_mt_init_slots()
    • input_register_device()
  4. input core 撮合 handler:遍历已注册 handler(如 evdev),按 id_table + 能力位图 匹配 → 调用 handler->connect()

  5. evdev 生成节点:创建 /dev/input/eventX 字符设备。

  6. 中断上报: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 绑定二者。
  • 注册 handlerinput_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()

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. 调试 & 排错清单

  • 设备是否注册为 inputcat /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
  • KconfigCONFIG_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 分钟内搞定)

  • 搜注册 APIrg -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)。
  • 看 bindingsDocumentation/devicetree/bindings/input/*.yaml

11. 常见问答(FAQ)

Q: 设备驱动(device driver)与事件驱动(event driver/handler)有什么区别?如何绑定?
A: 设备驱动面向硬件,创建 input_devinput_report_*;事件驱动面向 input core 事件(如 evdev),以 struct input_handler 形式注册。绑定由 input coreinput_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.cinclude/linux/input.hinclude/uapi/linux/input.h
  • handler:drivers/input/evdev.c(以及 mousedev.cjoydev.c
  • 多点触控:drivers/input/input-mt.c
  • 示例驱动:drivers/input/touchscreen/*drivers/input/keyboard/*
  • 工具:用户空间 evtest;库:libevdevlibinput

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


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值