Linux中input子系统
一、Input子系统输入设备管理
Input子系统的核心目标是屏蔽硬件差异,提供统一的软件接口——对驱动开发者,只需按子系统规范注册设备;对应用开发者,只需从固定节点(如/dev/input/event0
)读取输入事件,无需关心底层硬件是键盘还是触摸屏。
其核心价值体现在:
- 兼容性:支持所有类型的输入设备(按键、触摸、鼠标、摇杆等),新设备只需适配子系统规范即可接入。
- 简化开发:驱动开发者无需关注字符设备注册、数据分发等通用逻辑,只需专注硬件操作(如中断处理、数据读取)。
- 统一接口:应用层通过标准的文件IO接口(
open
/read
/poll
)与所有输入设备交互,无需针对不同设备编写适配代码。
二、输入设备的驱动框架
Input子系统采用分层架构,将驱动分为“硬件适配层”和“事件处理层”,中间通过核心层(Input Core)衔接,结构如下:
各层功能拆解
层级 | 功能描述 | 核心角色 |
---|---|---|
硬件适配层 | 与具体硬件交互,负责初始化硬件、处理中断、读取硬件数据,并上报给核心层。 | input_dev (输入设备结构体) |
核心层(Input Core) | 管理所有输入设备和处理程序,负责设备与处理程序的匹配、事件分发。 | 内核模块input.c |
事件处理层 | 接收核心层分发的事件,为应用层提供访问接口(如字符设备节点)。 | input_handler (处理程序) |
分层优势:硬件适配层与事件处理层解耦——同一类处理程序(如evdev
)可适配所有符合规范的输入设备,同一设备也可被多个处理程序匹配(如触摸屏既支持evdev
也支持tslib
)。
三、Input子系统核心结构体
Input子系统的核心是三个结构体:input_dev
(设备)、input_handler
(处理程序)、input_handle
(匹配关系),三者协同实现设备管理与事件分发。
1. 核心结构体定义与功能
(1)input_dev
:描述输入设备
input_dev
是硬件适配层的核心,代表一个具体的输入设备,需由驱动开发者初始化并注册到内核。
struct input_dev {const char *name; // 设备名称(如"GPIO Key")const char *phys; // 物理路径(如"/dev/input/event0")const char *uniq; // 唯一标识(可选)struct input_id id; // 设备ID(用于与handler匹配)unsigned long evbit[NBITS(EV_MAX)]; // 支持的事件类型(如EV_KEY、EV_SYN)unsigned long keybit[NBITS(KEY_MAX)];// 支持的按键(如KEY_ENTER、KEY_VOLUMEUP)unsigned long relbit[NBITS(REL_MAX)];// 支持的相对坐标(如鼠标REL_X、REL_Y)unsigned long absbit[NBITS(ABS_MAX)];// 支持的绝对坐标(如触摸屏ABS_X、ABS_Y)struct list_head h_list; // 关联的input_handle链表(设备匹配的handler)struct list_head node; // 用于挂载到内核的input_dev_list链表// ... 其他成员(如中断号、私有数据等)
};
关键字段说明:
evbit
:必须初始化,指定设备支持的事件类型(如EV_KEY
表示按键事件,EV_SYN
表示同步事件)。keybit/relbit/absbit
:根据设备类型初始化(如按键设备需设置keybit
,鼠标需设置relbit
)。id
:包含bustype
(总线类型,如BUS_HOST
)、vendor
(厂商ID)、product
(产品ID),用于与input_handler
的id_table
匹配。
(2)input_handler
:描述事件处理程序
input_handler
是事件处理层的核心,负责接收设备上报的事件,并为应用层提供访问接口(如创建/dev/input/eventX
节点)。内核已实现多个通用input_handler
,如:
evdev
:通用事件处理程序,支持所有输入设备,生成/dev/input/eventX
节点,是最常用的handler。kbd
:专门处理键盘事件,用于控制台键盘。mousedev
:专门处理鼠标事件。
struct input_handler {void (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);// 批量处理事件(可选,优先级高于event)void (*events)(struct input_dev *dev, const struct input_event *ev, unsigned int count);// 事件过滤(可选,可修改或丢弃事件)int (*filter)(struct input_dev *dev, 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);const struct input_device_id *id_table; // 支持的设备ID列表(用于匹配input_dev)struct list_head h_list; // 关联的input_handle链表(handler匹配的设备)struct list_head node; // 用于挂载到内核的input_handler_list链表// ... 其他成员(如名称、私有数据等)
};
关键字段说明:
id_table
:静态匹配规则,列出handler支持的input_dev
的id
(bustype
/vendor
/product
),是最常用的匹配方式。connect
:匹配成功后调用,核心工作是创建input_handle
并注册字符设备(如evdev
在connect
中创建/dev/input/eventX
)。event/events
:接收input_dev
上报的事件,最终分发给应用层。
(3)input_handle
:关联设备与处理程序
input_handle
是input_dev
与input_handler
的“桥梁”,当两者匹配成功后,内核会创建input_handle
并关联到两者的h_list
链表中。
struct input_handle {struct input_dev *dev; // 关联的输入设备struct input_handler *handler; // 关联的处理程序struct list_head d_node; // 挂载到input_dev->h_list的节点struct list_head h_node; // 挂载到input_handler->h_list的节点// ... 其他成员(如私有数据)
};
作用:通过input_handle
,内核可快速找到设备对应的处理程序(或处理程序对应的设备),实现事件的精准分发。
2. 三者关系图解
input_dev
和input_handler
通过input_handle
建立多对多关系:一个设备可被多个handler匹配(如触摸屏同时被evdev
和tslib
处理),一个handler也可匹配多个设备(如evdev
处理所有按键设备)。
内核维护两个全局链表:
input_dev_list
:所有已注册的input_dev
。input_handler_list
:所有已注册的input_handler
。
四、Input设备驱动注册流程:从硬件到内核
Input设备驱动的开发核心是初始化input_dev
并注册到内核,同时完成硬件配置(如中断申请)。以下以“基于设备树的Platform驱动”为例,详细拆解注册流程。
1. 注册流程总览
- 设备树配置:定义输入设备节点,指定硬件资源(如中断号、GPIO、设备ID等)。
- Platform驱动编写:实现
probe
函数(驱动与设备匹配时执行),完成硬件初始化、input_dev
创建与注册。 - 内核匹配与连接:
input_dev
注册后,内核自动与input_handler
匹配,调用connect
函数建立关联并创建字符设备节点。
2. 步骤1:设备树节点配置
在设备树中添加输入设备节点(以GPIO按键为例),指定中断、设备ID等信息,供驱动解析:
/* 设备树节点:GPIO按键设备 */
gpio_key: gpio-key@0 {compatible = "my-gpio-key"; // 与Platform驱动的of_match_table匹配interrupt-parent = <&gpio1>; // 中断父控制器interrupts = <10 IRQ_TYPE_EDGE_FALLING>; // 中断引脚(GPIO1_10)、下降沿触发key-code = <KEY_ENTER>; // 按键对应的键值(回车)linux,code = <EV_KEY>; // 事件类型(按键事件)status = "okay";
};
关键属性:
compatible
:驱动匹配的“标识符”,需与Platform驱动的of_match_table
一致。interrupts
:中断资源,驱动需通过platform_get_irq
获取。key-code
:自定义属性,指定按键对应的Linux键值(如KEY_ENTER
对应回车键)。
3. 步骤2:Platform驱动实现(核心代码)
Platform驱动是Linux中最常用的设备驱动框架,通过probe
函数完成设备初始化。以下是GPIO按键的Input驱动核心代码:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/interrupt.h>
#include <linux/of.h>
#include <linux/of_gpio.h>// 驱动私有数据:存储设备相关信息
struct gpio_key_data {struct input_dev *input_dev; // 输入设备结构体int irq; // 中断号unsigned int key_code; // 键值(如KEY_ENTER)
};// 中断服务程序:按键触发中断时执行,读取硬件状态并上报事件
static irqreturn_t gpio_key_irq_handler(int irq, void *dev_id) {struct gpio_key_data *data = dev_id;// 1. 上报按键事件(EV_KEY:事件类型,key_code:键值,1:按下,0:释放)input_event(data->input_dev, EV_KEY, data->key_code, 1);// 2. 上报同步事件(告知应用层:本轮事件已结束)input_sync(data->input_dev);// (可选)模拟按键释放(实际硬件需根据GPIO电平判断,此处简化)msleep(20); // 消抖input_event(data->input_dev, EV_KEY, data->key_code, 0);input_sync(data->input_dev);return IRQ_HANDLED;
}// probe函数:驱动与设备匹配时执行,初始化硬件和input_dev
static int gpio_key_probe(struct platform_device *pdev) {struct gpio_key_data *data;struct device_node *node = pdev->dev.of_node;int ret;// 1. 分配私有数据内存data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);if (!data) return -ENOMEM;platform_set_drvdata(pdev, data); // 绑定私有数据到设备// 2. 从设备树解析硬件资源// 2.1 获取键值ret = of_property_read_u32(node, "key-code", &data->key_code);if (ret) {dev_err(&pdev->dev, "Failed to read key-code\n");return ret;}// 2.2 获取中断号data->irq = platform_get_irq(pdev, 0);if (data->irq < 0) {dev_err(&pdev->dev, "Failed to get irq\n");return data->irq;}// 3. 分配并初始化input_devdata->input_dev = devm_input_allocate_device(&pdev->dev);if (!data->input_dev) return -ENOMEM;// 3.1 设置input_dev基本信息data->input_dev->name = "my-gpio-key"; // 设备名称data->input_dev->phys = "gpio-key/input0"; // 物理路径data->input_dev->id.bustype = BUS_HOST; // 总线类型data->input_dev->id.vendor = 0x1234; // 厂商ID(自定义)data->input_dev->id.product = 0x5678; // 产品ID(自定义)// 3.2 设置设备支持的事件类型和键值__set_bit(EV_KEY, data->input_dev->evbit); // 支持按键事件__set_bit(data->key_code, data->input_dev->keybit); // 支持的键值// 4. 注册input_dev到内核ret = input_register_device(data->input_dev);if (ret) {dev_err(&pdev->dev, "Failed to register input dev\n");return ret;}// 5. 申请中断(绑定中断服务程序)ret = devm_request_irq(&pdev->dev, data->irq, gpio_key_irq_handler,IRQF_TRIGGER_FALLING, "gpio-key-irq", data);if (ret) {dev_err(&pdev->dev, "Failed to request irq\n");return ret;}dev_info(&pdev->dev, "GPIO key driver probed successfully\n");return 0;
}// remove函数:设备移除时执行(可选,devm_xxx函数会自动释放资源)
static int gpio_key_remove(struct platform_device *pdev) {dev_info(&pdev->dev, "GPIO key driver removed\n");return 0;
}// 设备树匹配表:驱动通过compatible匹配设备
static const struct of_device_id gpio_key_of_match[] = {{ .compatible = "my-gpio-key" },{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, gpio_key_of_match);// Platform驱动结构体
static struct platform_driver gpio_key_driver = {.probe = gpio_key_probe,.remove = gpio_key_remove,.driver = {.name = "gpio-key-driver",.of_match_table = gpio_key_of_match, // 设备树匹配},
};// 模块加载与卸载
module_platform_driver(gpio_key_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("GPIO Key Input Driver");
MODULE_AUTHOR("Your Name");
代码关键步骤:
- 私有数据:
gpio_key_data
存储input_dev
、中断号等信息,避免全局变量。 - 设备树解析:通过
of_property_read_u32
、platform_get_irq
获取硬件资源。 - input_dev初始化:
devm_input_allocate_device
:使用devm_
前缀,自动释放内存,避免内存泄漏。__set_bit
:设置支持的事件类型(EV_KEY
)和键值(如KEY_ENTER
)。
- 中断申请:
devm_request_irq
绑定中断服务程序,按键触发时上报事件。 - input_dev注册:
input_register_device
将设备加入内核input_dev_list
链表,等待与input_handler
匹配。
4. 步骤3:input_dev与input_handler的匹配逻辑
当input_dev
或input_handler
注册时,内核会触发匹配流程,具体如下:
(1)注册input_dev:input_register_device
int input_register_device(struct input_dev *dev) {// ... 省略其他初始化 ...// 1. 将input_dev加入全局链表input_dev_listlist_add_tail(&dev->node, &input_dev_list);// 2. 遍历所有已注册的input_handler,尝试匹配list_for_each_entry(handler, &input_handler_list, node) {input_attach_handler(dev, handler); // 执行匹配}// ... 省略其他逻辑 ...
}
(2)注册input_handler:input_register_handler
int input_register_handler(struct input_handler *handler) {// ... 省略其他初始化 ...// 1. 将input_handler加入全局链表input_handler_listlist_add_tail(&handler->node, &input_handler_list);// 2. 遍历所有已注册的input_dev,尝试匹配list_for_each_entry(dev, &input_dev_list, node) {input_attach_handler(dev, handler); // 执行匹配}// ... 省略其他逻辑 ...
}
(3)匹配核心函数:input_attach_handler
该函数实现input_dev
与input_handler
的匹配判断,流程如下:
- 检查id_table匹配:判断
input_dev->id
是否在input_handler->id_table
中(通过input_match_device
函数)。 - 检查match函数(可选):若
input_handler
定义了match
函数,需额外调用该函数进行复杂匹配(如根据设备特殊属性判断)。 - 匹配成功则调用connect:若上述两步均通过,调用
input_handler->connect
函数建立关联。
static int input_attach_handler(struct input_dev *dev, struct input_handler *handler) {const struct input_device_id *id;int error;// 1. 第一步:通过id_table匹配(必须满足)id = input_match_device(handler, dev);if (!id) return -ENODEV;// 2. 第二步:若有match函数,执行额外匹配(可选)if (handler->match && !handler->match(handler, dev))return -ENODEV;// 3. 匹配成功,调用connect函数error = handler->connect(handler, dev, id);if (error && error != -ENODEV)dev_err(&dev->dev, "Failed to connect handler %s\n", handler->name);return error;
}
(4)匹配规则总结
匹配场景 | 判断逻辑 |
---|---|
无match 函数 | 仅需input_dev->id 在input_handler->id_table 中即可匹配 |
有match 函数 | 先满足id_table 匹配,再通过match 函数的自定义逻辑(如设备树属性检查) |
示例(evdev handler) | evdev 的id_table 为EVDEV_ID_TABLE ,支持所有bustype ,因此几乎所有input_dev 都能匹配 |
5. 步骤4:匹配成功后的connect
函数执行
以最常用的evdev
(通用事件处理程序)为例,其connect
函数(evdev_connect
)的核心工作的是:
- 创建
input_handle
:关联input_dev
与input_handler
,并加入两者的h_list
链表。 - 注册字符设备:创建
/dev/input/eventX
节点(X为设备编号),供应用层访问。
static int evdev_connect(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id) {struct evdev *evdev; // evdev私有数据结构int error, minor;// 1. 分配evdev内存(包含input_handle和cdev)evdev = kzalloc(sizeof(*evdev), GFP_KERNEL);if (!evdev) return -ENOMEM;// 2. 初始化input_handle,关联dev和handlerevdev->handle.dev = input_get_device(dev); // 获取input_devevdev->handle.handler = handler; // 关联input_handlerevdev->handle.private = evdev; // 绑定私有数据INIT_LIST_HEAD(&evdev->client_list); // 初始化客户端链表(多APP访问)init_waitqueue_head(&evdev->wait); // 初始化等待队列(用于read/poll)// 3. 注册input_handle到内核error = input_register_handle(&evdev->handle);if (error) goto err_free_evdev;// 4. 分配字符设备次设备号(minor)minor = input_get_new_minor(EVDEV_MINOR_BASE, EVDEV_MINORS, true);if (minor < 0) {error = minor;goto err_unregister_handle;}evdev->minor = minor;// 5. 初始化并注册字符设备cdev_init(&evdev->cdev, &evdev_fops); // 绑定文件操作结构体evdev_fopsevdev->cdev.owner = handler->owner;error = cdev_device_add(&evdev->cdev, &evdev->dev); // 注册字符设备if (error) goto err_release_minor;// 6. 创建/dev/input/eventX节点(由udev自动完成,内核提供设备信息)dev_info(&dev->dev, "evdev: initialized device as %s (minor %d)\n",dev_name(&evdev->dev), minor);return 0;// 错误处理(省略)
err_release_minor:input_release_minor(minor);
err_unregister_handle:input_unregister_handle(&evdev->handle);
err_free_evdev:kfree(evdev);return error;
}
关键结果:connect
执行完成后,应用层可通过/dev/input/eventX
(如/dev/input/event0
)访问该输入设备,后续即可通过read
/poll
读取事件。
五、输入事件读取流程:从硬件中断到应用层
当输入设备产生动作(如按键按下、鼠标移动)时,事件会通过“中断→内核上报→应用读取”的流程传递,具体如下:
1. 流程总览
- 硬件中断触发:设备动作(如按键按下)触发硬件中断,执行中断服务程序(ISR)。
- 内核事件上报:ISR中读取硬件数据,调用
input_event
上报事件到核心层。 - 事件分发到handler:核心层通过
input_handle
找到匹配的input_handler
(如evdev
),调用其event
函数。 - 应用层读取:应用通过
read
函数从/dev/input/eventX
读取事件数据。
2. 步骤1:硬件中断与事件上报(ISR)
以GPIO按键为例,按键按下时触发中断,执行之前注册的gpio_key_irq_handler
:
static irqreturn_t gpio_key_irq_handler(int irq, void *dev_id) {struct gpio_key_data *data = dev_id;int gpio_val;// 1. (可选)读取GPIO电平,判断按键状态(按下/释放)gpio_val = gpio_get_value(data->gpio); int key_pressed = (gpio_val == 0) ? 1 : 0; // 假设低电平为按下// 2. 上报按键事件:type=EV_KEY,code=键值,value=1(按下)/0(释放)input_event(data->input_dev, EV_KEY, data->key_code, key_pressed);// 3. 上报同步事件:告知应用层“本轮事件已完整,可读取”input_sync(data->input_dev);return IRQ_HANDLED;
}
核心函数:
input_event
:上报单个输入事件,是Input子系统的核心上报接口。input_sync
:本质是调用input_event(data->input_dev, EV_SYN, SYN_REPORT, 0)
,用于同步事件,避免应用层读取到不完整的事件序列。
3. 步骤2:input_event
的事件分发逻辑
input_event
函数负责将事件从input_dev
分发到匹配的input_handler
,核心代码如下:
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value) {struct input_handle *handle;// ... 省略事件合法性检查(如设备是否支持该type/code) ...// 遍历input_dev关联的所有input_handle(即所有匹配的handler)list_for_each_entry(handle, &dev->h_list, d_node) {// 1. 优先调用handler的filter函数(可选,用于过滤/修改事件)if (handle->handler->filter) {if (handle->handler->filter(dev, handle, type, code, value))continue; // 若filter返回1,跳过该handler}// 2. 分发事件到handler:优先调用events(批量处理),无则调用event(单个处理)if (handle->handler->events) {handle->handler->events(dev, &event, 1); // 批量处理(count=1)} else if (handle->handler->event) {handle->handler->event(dev, type, code, value); // 单个处理}}
}
关键逻辑:
- 事件会分发给
input_dev
的所有匹配handler
(如触摸屏同时分发给evdev
和tslib
)。 filter
函数可拦截事件(如屏蔽特定按键),events
函数用于高效处理批量事件(如鼠标连续移动)。
4. 步骤3:evdev
的事件存储与应用读取
以evdev
为例,其event
函数(evdev_event
)会将事件存储到环形缓冲区,供应用层通过read
读取:
(1)evdev_event
:存储事件到缓冲区
static void evdev_event(struct input_dev *dev, unsigned int type, unsigned int code, int value) {struct evdev *evdev = handle->private; // 从handle获取evdev私有数据struct evdev_client *client; // 对应一个应用层客户端(open一次创建一个)struct input_event event; // 事件结构体// 1. 构造input_event(包含时间戳、type、code、value)event.time = ktime_get_real_ts64();event.type = type;event.code = code;event.value = value;// 2. 遍历所有客户端(多个APP同时打开同一设备),将事件写入各自缓冲区rcu_read_lock();list_for_each_entry_rcu(client, &evdev->client_list, node) {evdev_feed_event(client, &event); // 写入客户端缓冲区}rcu_read_unlock();// 3. 唤醒等待队列(若有APP调用poll/read等待事件)wake_up_interruptible(&evdev->wait);
}
(2)应用层read
调用:evdev_read
当应用调用read(/dev/input/event0, buf, sizeof(struct input_event) * N)
时,会触发evdev
的read
函数(evdev_read
):
static ssize_t evdev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {struct evdev_client *client = file->private_data; // 客户端私有数据struct evdev *evdev = client->evdev;struct input_event event;size_t read = 0;// 1. 若缓冲区无数据且非非阻塞模式,休眠等待事件if (client->head == client->tail && evdev->exist && !(file->f_flags & O_NONBLOCK)) {if (wait_event_interruptible(evdev->wait, client->head != client->tail || !evdev->exist))return -ERESTARTSYS; // 被信号中断}// 2. 若设备已移除,返回0if (!evdev->exist)return 0;// 3. 从缓冲区读取事件,复制到用户空间while (read + sizeof(event) <= count && client->head != client->tail) {event = client->buffer[client->tail]; // 从环形缓冲区取事件client->tail = (client->tail + 1) % EVDEV_BUFFER_SIZE; // 移动尾指针if (copy_to_user(buf + read, &event, sizeof(event)))return -EFAULT; // 复制失败read += sizeof(event);}return read; // 返回读取的字节数
}
应用层示例代码:读取/dev/input/event0
的按键事件
#include <stdio.h>
#include <fcntl.h>
#include <linux/input.h>int main() {int fd;struct input_event ev;// 打开输入设备节点fd = open("/dev/input/event0", O_RDONLY);if (fd < 0) {perror("open /dev/input/event0 failed");return -1;}// 循环读取事件while (1) {read(fd, &ev, sizeof(ev)); // 阻塞读取// 打印事件信息printf("type: %d, code: %d, value: %d\n", ev.type, ev.code, ev.value);// 若为按键释放事件且是回车键,退出循环if (ev.type == EV_KEY && ev.code == KEY_ENTER && ev.value == 0)break;}close(fd);return 0;
}
六、QEMU调试Input设备:模拟与验证
在开发Input驱动时,通常需要通过QEMU模拟嵌入式环境进行调试,以下是关键配置与操作步骤:
1. QEMU Input设备模拟原理
QEMU支持模拟多种输入设备(如键盘、鼠标、触摸屏),并通过tslib
(触摸屏库)处理触摸事件。其核心逻辑是:
- QEMU将宿主机的输入事件(如鼠标点击、键盘按键)转发给虚拟机内的Input子系统。
- 虚拟机内的Input驱动(如
evdev
)接收事件,生成/dev/input/eventX
节点,供应用层(如Qt、TSLIB应用)访问。
2. QEMU启动参数配置(以ARM架构为例)
启动QEMU时,需指定模拟的输入设备(如触摸屏),关键参数如下:
qemu-system-arm \-M vexpress-a9 \ # 模拟ARM vexpress-a9开发板-kernel zImage \ # 内核镜像-dtb vexpress-v2p-ca9.dtb \ # 设备树-rootfs rootfs.ext4 \ # 根文件系统-append "root=/dev/mmcblk0 rw console=ttyAMA0,115200" \ # 内核启动参数-serial stdio \ # 串口重定向到宿主机终端-usb \ # 启用USB控制器-device usb-mouse \ # 模拟USB鼠标-device usb-kbd \ # 模拟USB键盘-device virtio-touchscreen-pci \ # 模拟PCI触摸屏(需内核支持virtio驱动)
参数说明:
-device usb-mouse/usb-kbd
:模拟USB鼠标/键盘,内核会自动加载usbhid
驱动,生成/dev/input/eventX
节点。-device virtio-touchscreen-pci
:模拟触摸屏,需内核启用CONFIG_VIRTIO_INPUT
配置,配合tslib
使用。
3. QEMU内验证Input设备
(1)查看已注册的Input设备
通过/proc/bus/input/devices
文件可查看所有已注册的Input设备信息:
cat /proc/bus/input/devices
示例输出(USB键盘):
I: Bus=0003 Vendor=0627 Product=0001 Version=0110
N: Name="QEMU USB Keyboard"
P: Phys=usb-0000:00:04.0-1/input0
S: Sysfs=/devices/pci0000:00/0000:00:04.0/usb1/1-1/1-1:1.0/0003:0627:0001.0001/input/input0
U: Uniq=
H: Handlers=sysrq kbd event0
B: PROP=0
B: EV=120013
B: KEY=10000 0 0 0 1007b f902f bf0047f dfffe 1cfffff ffffffff fffffffe
B: MSC=10
B: LED=7
Handlers=event0
:表示该设备对应/dev/input/event0
节点。EV=120013
:表示支持的事件类型(EV_KEY
、EV_MSC
、EV_LED
等)。
(2)使用hexdump
读取事件
通过hexdump
工具可直接读取/dev/input/eventX
的事件数据,验证设备是否正常工作:
hexdump /dev/input
六、QEMU调试Input设备:模拟与验证
(2)使用hexdump
读取事件
通过hexdump
工具可直接读取/dev/input/eventX
的事件数据,验证设备是否正常工作:
hexdump /dev/input/event0 # event0对应上述USB键盘
按下键盘上的“Enter”键,会输出类似以下数据:
0000000 0a4b 5c8f 0000 0000 0001 001c 0001 0000
0000010 0a4b 5c8f 0000 0000 0000 0000 0000 0000
0000020 0a4b 5c90 0000 0000 0001 001c 0000 0000
0000030 0a4b 5c90 0000 0000 0000 0000 0000 0000
数据解析(对应struct input_event
结构):
- 前8字节:时间戳(
tv_sec
和tv_usec
),如0a4b 5c8f
表示秒,0000 0000
表示微秒。 - 第9-12字节:事件类型(
type
),0001
对应EV_KEY
(按键事件)。 - 第13-16字节:事件编码(
code
),001c
对应KEY_ENTER
(回车键)。 - 第17-20字节:事件值(
value
),0001
表示按键按下,0000
表示按键释放,0002
表示按键重复(长按)。 - 后续8字节:同步事件(
type=0000
,code=0000
),表示本轮事件结束。
(3)使用tslib
调试触摸屏
若QEMU模拟了触摸屏(如通过-device virtio-touchscreen-pci
参数),需通过tslib
工具验证触摸功能。tslib
是开源的触摸屏校准与事件处理库,可屏蔽不同触摸屏的硬件差异,提供统一的触摸接口。
步骤1:安装tslib
在根文件系统中预装tslib
(可通过Buildroot或Yocto编译),安装后包含以下工具:
ts_calibrate
:触摸屏校准工具。ts_test
:触摸屏测试工具(显示触摸坐标、滑动轨迹)。ts_print
:打印触摸事件原始数据。
步骤2:配置tslib
环境变量
在QEMU终端中设置tslib
的输入设备节点(通常为/dev/input/event1
,需根据实际设备调整):
export TSLIB_TSDEVICE=/dev/input/event1 # 触摸设备节点
export TSLIB_CONFFILE=/etc/ts.conf # tslib配置文件路径
export TSLIB_PLUGINDIR=/usr/lib/ts # tslib插件目录
export TSLIB_CALIBFILE=/etc/pointercal # 校准数据存储文件
步骤3:校准与测试触摸屏
-
执行校准:
ts_calibrate
按照提示点击屏幕上的5个校准点,校准完成后会生成
/etc/pointercal
文件,后续触摸事件会自动应用校准参数。 -
测试触摸功能:
ts_test
屏幕会显示触摸坐标(
X
/Y
)和压力值(Pressure
),滑动手指可看到轨迹,验证触摸是否正常响应。
4. QEMU退出方法
在QEMU运行过程中,若需退出虚拟机,需执行以下操作:
- 同时按住键盘上的
Ctrl + A
组合键(无需松开Ctrl
)。 - 松开
Ctrl + A
后,单独按下x
键。 - QEMU会立即关闭,并返回宿主机终端。
注意:若直接关闭QEMU窗口,可能导致根文件系统损坏(尤其是采用rw
(可写)模式挂载时),建议通过上述命令正常退出。
七、uinput:应用层模拟输入设备
uinput
是Linux内核提供的一个特殊驱动模块,允许应用层程序创建虚拟输入设备(如虚拟键盘、虚拟鼠标),并模拟输入事件(如自动按键、模拟鼠标移动)。其核心价值在于无需编写内核驱动,即可通过用户空间代码扩展输入功能(如自动化测试、远程控制)。
1. uinput工作原理
uinput
的本质是一个“用户空间输入设备驱动”,其工作流程如下:
- 应用层通过
open("/dev/uinput")
或open("/dev/input/uinput")
打开uinput设备节点(需内核启用CONFIG_UINPUT
配置)。 - 应用层通过
ioctl
命令配置虚拟设备的属性(如支持的事件类型、键值、坐标范围)。 - 应用层通过
write
函数向uinput写入struct input_event
格式的事件数据。 - uinput驱动将接收到的事件转发给内核Input子系统,与物理设备的事件处理流程完全一致(即分发给
input_handler
,供其他应用读取)。
2. uinput核心操作步骤
(1)内核配置要求
使用uinput前,需确保内核已启用uinput
模块:
- 编译内核时,开启配置项:
Device Drivers → Input device support → User level driver support → <M> User input device driver
(可编译为模块uinput.ko
,或直接编译进内核)。 - 若为模块,需手动加载:
modprobe uinput
,加载后会生成/dev/uinput
或/dev/input/uinput
节点。
(2)应用层操作流程(C语言示例)
以下以“创建虚拟键盘并模拟按下‘Enter’键”为例,演示uinput的核心用法:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/uinput.h>
#include <linux/input.h>#define UINPUT_DEV "/dev/uinput" // uinput设备节点// 函数:创建虚拟键盘设备
int create_virtual_keyboard(int fd) {struct uinput_setup setup;int ret;// 1. 启用键盘支持的事件类型(EV_KEY:按键事件,EV_SYN:同步事件)ret = ioctl(fd, UI_SET_EVBIT, EV_KEY);if (ret < 0) { perror("UI_SET_EVBIT EV_KEY failed"); return ret; }ret = ioctl(fd, UI_SET_EVBIT, EV_SYN);if (ret < 0) { perror("UI_SET_EVBIT EV_SYN failed"); return ret; }// 2. 启用需要模拟的键值(此处为KEY_ENTER:回车键)ret = ioctl(fd, UI_SET_KEYBIT, KEY_ENTER);if (ret < 0) { perror("UI_SET_KEYBIT KEY_ENTER failed"); return ret; }// 3. 配置虚拟设备信息(名称、总线类型、厂商ID、产品ID)memset(&setup, 0, sizeof(setup));setup.id.bustype = BUS_USB; // 总线类型(模拟USB设备)setup.id.vendor = 0x1234; // 厂商ID(自定义)setup.id.product = 0x5678; // 产品ID(自定义)snprintf(setup.name, UINPUT_MAX_NAME_SIZE, "Virtual Keyboard"); // 设备名称ret = ioctl(fd, UI_DEV_SETUP, &setup);if (ret < 0) { perror("UI_DEV_SETUP failed"); return ret; }// 4. 注册虚拟设备(此时内核会为其分配event节点,如/dev/input/event2)ret = ioctl(fd, UI_DEV_CREATE);if (ret < 0) { perror("UI_DEV_CREATE failed"); return ret; }printf("Virtual keyboard created successfully (name: %s)\n", setup.name);return 0;
}// 函数:模拟按键事件(按下/释放)
void simulate_key_event(int fd, int key_code, int value) {struct input_event ev;// 1. 构造按键事件memset(&ev, 0, sizeof(ev));ev.type = EV_KEY; // 事件类型:按键ev.code = key_code; // 键值(如KEY_ENTER)ev.value = value; // 0:释放,1:按下,2:重复gettimeofday(&ev.time, NULL); // 设置时间戳(可选,内核会自动补全)write(fd, &ev, sizeof(ev)); // 写入事件// 2. 构造同步事件(告知内核:本轮事件结束)memset(&ev, 0, sizeof(ev));ev.type = EV_SYN; // 事件类型:同步ev.code = SYN_REPORT; // 同步类型:报告事件ev.value = 0;gettimeofday(&ev.time, NULL);write(fd, &ev, sizeof(ev));
}int main() {int fd;// 1. 打开uinput设备节点(O_RDWR:读写模式)fd = open(UINPUT_DEV, O_RDWR);if (fd < 0) {perror("open " UINPUT_DEV " failed");exit(EXIT_FAILURE);}// 2. 创建虚拟键盘if (create_virtual_keyboard(fd) != 0) {close(fd);exit(EXIT_FAILURE);}// 3. 模拟按键:按下Enter键(1秒后释放)printf("Simulating Enter key press...\n");simulate_key_event(fd, KEY_ENTER, 1); // 按下sleep(1); // 保持按下1秒simulate_key_event(fd, KEY_ENTER, 0); // 释放printf("Enter key released\n");// 4. 销毁虚拟设备(可选,关闭fd也会自动销毁)ioctl(fd, UI_DEV_DESTROY);close(fd);return 0;
}
3. 代码关键解析
(1)核心ioctl
命令
uinput
通过ioctl
命令配置虚拟设备,常用命令如下:
命令 | 功能描述 |
---|---|
UI_SET_EVBIT | 启用虚拟设备支持的事件类型(如EV_KEY 、EV_REL (相对坐标)、EV_ABS (绝对坐标))。 |
UI_SET_KEYBIT | 启用虚拟设备支持的键值(如KEY_ENTER 、KEY_A ,需先启用EV_KEY )。 |
UI_SET_RELBIT | 启用虚拟设备支持的相对坐标(如REL_X 、REL_Y ,用于模拟鼠标)。 |
UI_SET_ABSBIT | 启用虚拟设备支持的绝对坐标(如ABS_X 、ABS_Y ,用于模拟触摸屏)。 |
UI_DEV_SETUP | 配置虚拟设备的基本信息(struct uinput_setup ,包含id 、name )。 |
UI_DEV_CREATE | 注册虚拟设备到内核Input子系统,此时会生成/dev/input/eventX 节点。 |
UI_DEV_DESTROY | 从内核中移除虚拟设备(关闭fd 前建议调用,避免资源泄漏)。 |
(2)事件写入格式
应用层通过write
函数向uinput
写入的事件,必须符合struct input_event
格式(与内核Input子系统的事件格式一致):
struct input_event {struct timeval time; // 事件时间戳(可选,内核可自动补全)__u16 type; // 事件类型(如EV_KEY、EV_SYN)__u16 code; // 事件编码(如KEY_ENTER、REL_X)__s32 value; // 事件值(如1=按下、0=释放;坐标值等)
};
- 每发送一个功能事件(如按键、鼠标移动),必须跟随一个
EV_SYN
同步事件,否则内核无法识别事件边界。
4. uinput应用场景
- 自动化测试:模拟用户输入(如自动点击按钮、输入文本),用于测试GUI应用(如Qt、Android应用)。
- 远程控制:接收远程端的输入指令(如网络消息),通过uinput转换为本地输入事件,实现远程操控设备。
- 辅助工具:开发自定义输入设备(如通过语音识别生成键盘事件、通过手柄模拟鼠标)。
- 调试Input子系统:无需硬件设备,即可验证
input_handler
(如evdev
)的事件处理逻辑。