大家好,我是王鸽,这篇文章是介绍Linux 输入子系统(Input Subsystem),从核心作用、架构组成、工作流程、驱动开发模板四个维度讲解。一、输入子系统核心作用(先搞懂 “它是干嘛的”)
Linux 输入子系统是内核中专门管理输入设备的框架,核心目标是:
- 统一各类输入设备的驱动开发接口(键盘、鼠标、触摸屏、按键、摇杆、传感器等);
- 向上层(用户空间)提供标准化的访问方式(
/dev/input/eventX节点); - 解耦 “硬件驱动” 和 “输入事件处理”,驱动开发者不用关心上层如何处理事件,只需按框架上报事件即可。
简单类比:输入子系统就像一个 “输入设备翻译官”—— 不管是键盘按了 A 键、触摸屏点了 (100,200)、按键按了电源键,驱动只需告诉翻译官 “发生了什么事件”,翻译官会把事件转换成统一格式,交给上层(比如应用程序、GUI)处理。二、输入子系统核心架构(三层架构,从下到上)
输入子系统分为驱动层、核心层、事件处理层,三层分工明确,驱动开发主要关注硬件层:
如上图,最底层的具体设备,比如触摸屏、 按键、 USB 键盘/鼠标等,中间部分属于Linux 内核空间,分为驱动层、核心层和事件层,最上面是用户空间,所有的输入设备以文件的形式供用户应用程序使用。
鼠标移动、键盘按键按下等输入事件都需要通过设备驱动层→核心层→事件处理层→用户空间,层层上报,直到应用程序;
事件处理层和核心层是内核维护人员提供的,作为嵌入式开发工程师是不需要修改,只需要理解和学会使用相关接口,去编写设备驱动层;
| | | |
|---|
| 1. 初始化硬件(如配置 GPIO 为输入、注册中断);2. 读取硬件状态(如按键是否按下);3. 调用核心层接口上报事件 | struct input_dev | |
| 1. 提供驱动注册 / 注销、事件上报的 API;2. 管理输入设备和事件处理层的匹配 | struct input_handler | |
| 1. 注册到核心层,接收事件;2. 创建 /dev/input/eventX 设备节点;3. 将内核事件转发到用户空间 | struct input_event | |
其实输入子系统把各种硬件(键盘、触摸、鼠标、手柄…)的原始数据标准化为事件(EV_KEY/EV_ABS/EV_REL/...),通过 handler(如 evdev) 暴露为 /dev/input/eventX 给用户程序。
三个层次之间的关系
1.核心层负责管理事件处理层和设备驱动层;2.设备驱动层的设备可以匹配上多个事件处理层的类别;3.一个事件处理层的类别可以管理有多个设备驱动层的设备;
三、输入子系统核心概念(必须掌握)
1.核心数据结构
相关联的代码位于这些文件kernel/drivers/input/input.c和kernel/include/linux/input.h中
主要是几个核心数据结构体struct input_dev,struct input_handle,struct input_handler, strcut input_event。
struct input_dev(输入设备对象)输入设备的核心结构体,驱动需创建并初始化它,向核心层注册;内核中代表一个物理输入设备的结构体,每个输入设备(键盘 / 鼠标)都会被驱动程序创建一个 input_dev 实例。核心行为:通过 input_event() 函数向子系统上报输入事件。
struct input_dev {const char *name; //设备名称const char *phys; //设备在系统中的物理路径const char *uniq; //设备唯一识别符struct input_id id; //设备工D,包含总线ID(PCI 、 USB)、厂商工D,与 input handler 匹配的时会用到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)]; //设备支持的具体的混杂事件unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; //设备支持的具体的LED指示灯事件unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; //户设备支持的具体的音效事件unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; //设备支持的具体的力反馈事件unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; //设备支持的具体的开关事件unsigned int keycodemax; //键盘码表的大小unsigned int keycodesize; //键盘码表中的元素个数void *keycode; //设备的键盘码表//下面两个是可选方法,用于配置和获取键盘码表int (*setkeycode)(struct input_dev *dev, unsigned int scancode, unsigned int keycode);int (*getkeycode)(struct input_dev *dev, unsigned int scancode, unsigned int *keycode);struct ff_device *ff; //如果设备支持力反馈,则该成员将指向力反馈设备描述结构unsigned int repeat_key; //保存上一个键值,用于实现软件自动重复按键(用户按住某个键不放)struct timer_list timer; //用于软件自动重复按键的定时器int sync; //在上次同步事件(EV_SYNC)发生后没有新事件产生,则被设置为 1 int abs[ABS_CNT]; //用于上报的绝对坐标当前值int rep[REP_MAX + 1]; //记录自动重复按键参数的当前值unsigned long key[BITS_TO_LONGS(KEY_CNT)]; //舍反映设备按键、 按钮的当前状态unsigned long led[BITS_TO_LONGS(LED_CNT)]; //反映设备LED指示灯的当前状态时unsigned long snd[BITS_TO_LONGS(SND_CNT)]; //反映设备音效的当前状态会unsigned long sw[BITS_TO_LONGS(SW_CNT)]; //反映设备开关的当前状态int absmax[ABS_CNT]; //绝对坐标的最大值int absmin[ABS_CNT]; //绝对坐标的最小值int absfuzz[ABS_CNT]; //绝对坐标的噪音值,变化小于该值的一半可忽略该事件int absflat[ABS_CNT]; //摇杆中心位置大小int absres[ABS_CNT];//提供以下4个设备驱动层的操作接口,根据具体的设备需求实现它们int (*open)(struct input_dev *dev);void (*close)(struct input_dev *dev);int (*flush)(struct input_dev *dev, struct file *file);//用于处理送到设备驱动层来的事件,很多事件在事件处理层被处理,但有的事件仍需送到设备驱动中.//如LED指示灯事件和音效事件,因为这些操作通常需要设备驱动执行(如点亮某个键盘指示灯) int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);//指向独占该设备的输入句柄( input handle ),通常设备驱动上报的事件会被分发到与设备//关联的所有事件处理程序( input handler )中处理,但如果通过ioctl 的EVIOCGRAB命令//设置了独占句柄,则上报事件只能被所设置的输入句柄对应的事件处理程序处理struct input_handle *grab;spinlock_t event_lock; //调用 event () 时需要使用该自旋锁来实现互斥struct mutex mutex; //用于串行化的访问 open()、 close()和flush()等设备方法//记录输入事件处理程序(input handlers)调用设备open()方法的次数.保证设备open()方法是在//第一次调用 input_open_device()中被调用,设备close()方法在最后一次调用 input_close_device()中被调用unsigned int users;bool going_away;struct device dev; //内嵌device结构struct list_head h_list; //与该设备相关的输入句柄列表(struct input handle)struct list_head node; //挂接到input_dev_list链表上};#define to_input_dev(d) container_of(d, struct input_dev, dev)
struct input_handler(输入事件处理者)抽象的表示一个事件处理驱动,在内核启动过程中会向核心层注册handler;定义 connect/disconnect/event 回调与匹配表 id_table。核心行为:当找到匹配的 input_dev 时,通过 connect() 建立关联,并重写 event() 处理设备上报的事件。struct input_handler {void *private; //由具体的事件处理程序指定的私有数据//用于处理送到事件处理层的事件,该方法由核心层调用,调用时已经禁止了中断,//并获得dev->event lock ,因此不能喔珉void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);bool (*filter)(struct input_handle *handle, unsigned int type, unsigned int code, int value);bool (*match)(struct input_handler *handler, struct input_dev *dev);//在事件处理程序关联到一个输入设备时调用int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);//在事件处理程序脱离与之关联的输入设备时调void (*disconnect)(struct input_handle *handle);//开启对给定句柄的处理程序。 该函数由核心层在connect ()方法被调用之后,//或者在独占句柄释放它占有的输入设备时调void (*start)(struct input_handle *handle);const struct file_operations *fops; //该事件处理驱动的文件操作集合int minor; //该驱动能够提供的32个连续次设备号中的第一个const char *name; //该处理程序的名称.可 以在/ proc/bus/input/handlers 中看到//输入设备 ID衰,事件处理驱动通过这个成员来匹配它能处理的设备const struct input_device_id *id_table;//输入设备ID黑名单.即使在id_table 匹配的设备.只要出现在这个黑名单中,也应该被忽略const struct input_device_id *blacklist;struct list_head h_list; //该事件处理程序关联的输入句柄列struct list_head node; //所有有事件处理驱动都会通过该成员连接到 input_handler_list链表上};
struct input_handle(输入句柄)用于记录匹配上的设备和事件处理驱动,在设备上报输入事件时才知道该往哪些事件处理驱动上报;
struct input_handle {void *private; //由事件处理程序指定的私有数据int open; //记录句柄打开的次数 const char *name; //处理程序创建该句柄时,指定的句柄名称*struct input_dev *dev; //该句柄关联的输入设备struct input_handler *handler; //句柄关联的事件处理程序struct list_head d_node; //通过它将该句柄放到与之关联的设备的输入句柄列表中struct list_head h_node; //通过它将该句柄放到与之关联的处理程序的输入句柄列表中};
struct input_handle 连接 input_dev 和 input_handler 的 “中间枢纽”,是两者的双向引用载体。
核心作用:
input_dev,input_handler,input_handle 三者的关系:
- input_dev是输入事件的 “生产者”,对应物理输入设备;input_handler 是事件的 “消费者”,对应事件处理逻辑;
- input_handle是连接两者的 “桥梁”,通过双向指针和链表,让生产者和消费者建立唯一关联;
- 三者的核心交互逻辑是:
input_dev 产生事件 → 通过 input_handle 找到匹配的 input_handler → input_handler 处理事件并传递到用户层。
言简意赅点:input_dev 卖家 ,input_handle 快递小哥, 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. 输入事件类型(统一的事件格式)
所有输入设备的事件都被抽象为以下核心类型(定义在linux/input.h):
每个事件包含:类型(type) + 编码(code) + 值(value),比如:
- 按键按下:
type=EV_KEY, code=KEY_0, value=1; - 按键松开:
type=EV_KEY, code=KEY_0, value=0; - 触摸屏点击:
type=EV_ABS, code=ABS_X, value=100(X 坐标) + type=EV_ABS, code=ABS_Y, value=200(Y 坐标) + type=EV_SYN, code=SYN_REPORT, value=0(同步)。
| | |
|---|
input_allocate_device() | | |
set_bit(EV_KEY, dev->evbit) | | |
set_bit(KEY_0, dev->keybit) | | |
input_register_device() | | |
input_event(dev, type, code, value) | | type=EV_KEY/ABS 等,value=1(按下)/0(松开) |
input_sync(dev) | | |
input_unregister_device() | | |
input_free_device() | | |
四、举一个例子
一个输入事件(如按键按下)的典型旅程如下:
2. 驱动上报:设备驱动层的中断处理函数读取硬件状态,并调用 "input_report_key()" 等函数生成事件。3. 核心处理:核心层接收事件,进行必要的过滤和验证。4. 事件分发:核心层将事件分发给所有已注册的、匹配的事件处理器(Handler)。5. 用户空间读取:用户空间应用程序(如GUI系统)通过读取 "/dev/input/eventX" 获取事件数据。
分配input_dev → 配置事件类型/按键码 → 注册input_dev → 上报事件(input_event+input_sync) → 注销+释放;(1)设备驱动层核心代码(以简单按键为例)
步骤大概如下:
调用 input_allocate_device() 分配 input_dev 内存;
设置 input_dev 的核心属性:
注册设备:调用 "input_register_device(dev)" 将设备注册到子系统。
上报事件:在适当的时候(如中断处理函数中)上报事件:input_report_key(dev, KEY_A, 1); // 报告KEY_A按下,input_sync(dev); // 标记事件报告完成
#include<linux/input.h>#include<linux/interrupt.h>#include<linux/module.h>// 定义按键GPIO和中断号#define KEY_GPIO GPIOA(0)#define KEY_IRQ gpio_to_irq(KEY_GPIO)// 全局变量:input_dev结构体指针,保存分配的设备描述符static struct input_dev *key_input_dev;// 中断处理函数:按键触发时上报事件staticirqreturn_tkey_irq_handler(int irq, void *dev_id){ int key_val = gpio_get_value(KEY_GPIO); // 读取按键电平(0/1) // 上报按键事件:KEY_0为按键码,!key_val表示按下/释放(低电平按下) input_report_key(key_input_dev, KEY_0, !key_val); input_sync(key_input_dev); // 同步事件(表示一组事件结束) return IRQ_HANDLED;}// 驱动初始化函数staticint __init key_input_init(void){ int ret; // 1. 分配input_dev key_input_dev = input_allocate_device(); if (!key_input_dev) { return -ENOMEM; } // 2. 配置input_dev:声明支持的事件类型和按键 __set_bit(EV_KEY, key_input_dev->evbit); // 支持按键事件 __set_bit(KEY_0, key_input_dev->keybit); // 支持KEY_0按键 // 可选配置:设备名称、物理路径(便于系统识别) key_input_dev->name = "simulate_key_device"; // 设备名 key_input_dev->phys = "simulate_key/input0"; // 物理路径 // 3. 注册input_dev到核心层 ret = input_register_device(key_input_dev); if (ret) { input_free_device(key_input_dev); return ret; } // 4. 申请GPIO和中断(下降沿触发) ret = request_irq(KEY_IRQ, key_irq_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, "key_input", NULL); if (ret) { input_unregister_device(key_input_dev); input_free_device(key_input_dev); return ret; } return 0;}// 驱动退出函数staticvoid __exit key_input_exit(void){ free_irq(KEY_IRQ, NULL); input_unregister_device(key_input_dev); input_free_device(key_input_dev);}module_init(key_input_init);module_exit(key_input_exit);MODULE_LICENSE("GPL");
编译驱动(Makefile)
obj-m += key_input.oKERNELDIR ?= /lib/modules/$(shell uname -r)/buildPWD := $(shell pwd)all: make -C $(KERNELDIR) M=$(PWD) modulesclean: make -C $(KERNELDIR) M=$(PWD) clean
加载驱动
insmod key_input.ko # 加载驱动ls /dev/input/ # 查看输入设备(新增的eventX即为驱动创建的设备)cat /proc/bus/input/devices # 查看设备详情(能看到simulate_key_device)
用户空间读取事件示例
#include<stdio.h>#include<fcntl.h>#include<unistd.h>#include<linux/input.h>intmain(){ int fd = open("/dev/input/event0", O_RDONLY); if (fd < 0) { perror("open failed"); return -1; } struct input_event ev; while (1) { // 读取事件(阻塞) ssize_t len = read(fd, &ev, sizeof(ev)); if (len != sizeof(ev)) continue; // 解析事件:EV_KEY表示按键事件 if (ev.type == EV_KEY && ev.code == KEY_0) { printf("KEY_0 %s (time: %ld.%06ld)\n", ev.value ? "pressed" : "released", ev.time.tv_sec, ev.time.tv_usec); } } close(fd); return 0;}
关键要点
事件上报是核心动作,必须遵循 “上报具体事件 + 同步事件” 的规范,硬件触发优先使用中断方式(效率更高)。
代码调用图

总结
Linux 输入子系统分为设备驱动层(硬件适配)、核心层(事件中转)、事件处理层(用户交互),三层通过input_dev/input_handler/input_handle关联,解耦硬件和应用。驱动注册input_dev → 核心层匹配事件处理器 → 硬件触发中断后驱动上报事件 → 事件处理层通过/dev/input/eventX向用户空间暴露事件。input_allocate_device()/input_register_device()/input_report_key();用户层通过读取struct input_event 解析事件。