1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 背景
本文基于 Linux 4.14
内核源码进行分析。
3. 简介
Linux 内核输入子系统
,负责对系统中的输入设备进行管理。
一方面,它向内核空间的输入设备驱动提供数据处理的公共代码逻辑,屏蔽输入设备硬件的实现细节;另一方面,它向用户空间提供输入设备数据访问接口,用户空间应用程序可通过内核提供的访问接口,获取输入设备数据。
4. 代码实现分析
4.1 输入子系统代码目录结构
/* 输入子系统公共代码 */
drivers/input/input.c: 输入设备、事件处理对象注册、注销,输入事件数据上报等输入子系统核心代码
drivers/input/input-mt.c: 【多点】输入设备、事件处理对象注册、注销,输入事件数据上报等输入子系统核心代码
drivers/input/evdev.c: 输入事件数据处理通用 input_handler
drivers/input/joydev.c: joystick 类设备输入事件数据处理 input_handler
drivers/input/mousedev.c: mouse 类设备输入事件数据处理 input_handler
drivers/input 目录下的其它 .c,.h: 输入子系统其它核心代码
/* 各类输入设备驱动目录 */
drivers/input/gameport: gameport 类输入设备驱动
drivers/input/joystick: joystick 类输入设备驱动
drivers/input/keyboard: keyboard 类输入设备驱动
drivers/input/mouse: mouse 类输入设备驱动
drivers/input/touchscreen: touchscreen 类输入设备驱动
...
drivers/input/misc: 其它杂项类输入设备驱动
4.2 输入子系统初始化
4.2.1 输入数据的处理对象注册
内核输入子系统提供接口 input_register_handler()
来注册输入数据处理对象 input_handler
,这些 input_handler
最终通过输入数据处理对象句柄 input_handle
间接地绑定到输入设备 input_dev
。
/* 所有输入类设备公共的数据处理接口对象注册 */
evdev_init()
input_register_handler(&evdev_handler)
/* 所有输入类设备公共的数据处理调试信息接口对象注册 */
evbug_init()
input_register_handler(&evbug_handler)
/* 输入类设备 LED 灯处理接口对象注册 */
input_leds_init()
input_register_handler(&input_leds_handler)
/* RF 类设备事件数据处理接口对象注册 */
rfkill_init()
rfkill_handler_init()
input_register_handler(&rfkill_handler)
/* joystick 类设备事件数据处理接口对象注册 */
joydev_init()
input_register_handler(&joydev_handler)
/* keyboard 类设备事件数据处理接口对象注册 */
kbd_init()
/* 初始化键盘状态、各 lock 键的状态数据 */
for (i = 0; i < MAX_NR_CONSOLES; i++) {
kbd_table[i].ledflagstate = kbd_defleds();
kbd_table[i].default_ledflagstate = kbd_defleds();
kbd_table[i].ledmode = LED_SHOW_FLAGS;
kbd_table[i].lockstate = KBD_DEFLOCK;
kbd_table[i].slockstate = 0;
kbd_table[i].modeflags = KBD_DEFMODE;
kbd_table[i].kbdmode = default_utf8 ? VC_UNICODE : VC_XLATE;
}
/* 键盘 LED 灯控制初始化 */
kbd_init_leds()
input_register_handler(&kbd_handler)
/* 更新键盘 LED 灯的 tasklet 初始化 */
tasklet_enable(&keyboard_tasklet);
tasklet_schedule(&keyboard_tasklet);
继续看输入设备数据处理对象 input_handler
的注册流程:
input_register_handler()
INIT_LIST_HEAD(&handler->h_list);
/* 添加到 输入事件处理对象 到全局列表 @input_handler_list */
list_add_tail(&handler->node, &input_handler_list);
/* 绑定 输入数据处理对象 到 输入设备的 【场景1】 */
list_for_each_entry(dev, &input_dev_list, node)
input_attach_handler(dev, handler) /* 细节参考后续分析 */
4.2.2 输入设备的创建和注册
4.2.2.1 输入设备的创建
内核输入子系统提供接口 input_allocate_device()
创建输入设备 input_dev
,具体流程如下:
input_allocate_device()
static atomic_t input_no = ATOMIC_INIT(-1);
struct input_dev *dev;
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
dev->dev.type = &input_dev_type;
dev->dev.class = &input_class;
...
init_timer(&dev->timer);
INIT_LIST_HEAD(&dev->h_list);
INIT_LIST_HEAD(&dev->node);
dev_set_name(&dev->dev, "input%lu",
(unsigned long)atomic_inc_return(&input_no));
...
return dev;
4.2.2.2 输入设备的配置
驱动在调用 input_allocate_device()
创建输入设备 input_dev
后,通常对设备支持的特性进行配置,以键盘类设备举例,代码片段如下:
input_dev->name = "ttp229-keypad";
input_dev->dev.parent = &ttp229->pdev->dev;
/* 配置设备支持的按键 */
for(i = 0; i < ARRAY_SIZE(key_hash_tb); i++)
input_set_capability(input_dev, EV_KEY, key_hash_tb[i].code);
__set_bit(EV_REP, input_dev->evbit); /* 启用设备按键自动 repeat 支持 */
ttp229->input_dev = input_dev;
完整的代码驱动可参考博文 linux input: TTP229触摸键盘驱动 获取。
4.2.2.3 输入设备的注册
创建、配置输入设备 input_dev
后,接下来是将输入设备注册到系统。内核输入子系统提供接口 input_register_device()
注册输入设备,具体流程如下:
input_register_device()
...
/* Every input device generates EV_SYN/SYN_REPORT events. */
__set_bit(EV_SYN, dev->evbit);
...
/* 预估输入数据帧的大小 */
packet_size = input_estimate_events_per_packet(dev);
if (dev->hint_events_per_packet < packet_size)
dev->hint_events_per_packet = packet_size;
/* 为数据帧预分配空间。设备事件产生时,将事件数据填入其中。 */
dev->max_vals = dev->hint_events_per_packet + 2;
dev->vals = kcalloc(dev->max_vals, sizeof(*dev->vals), GFP_KERNEL);
/* 按键自动 repeat 的默认延时和周期设置 */
if (!dev->rep[REP_DELAY] && !dev->rep[REP_PERIOD])
input_enable_softrepeat(dev, 250, 33);
error = device_add(&dev->dev); /* 添加设备对象到设备驱动模型 */
/*
* 在内核日志中打印输入设备对象,在设备驱动对象架构中的完整路径信息。
* 如:input: r_gpio_keys as /devices/platform/r_gpio_keys/input/input0
*/
path = kobject_get_path(&dev->dev.kobj, GFP_KERNEL);
pr_info("%s as %s\n",
dev->name ? dev->name : "Unspecified device",
path ? path : "N/A");
kfree(path);
/* 添加到输入设备对象 input_dev 的全局列表 @input_dev_list */
list_add_tail(&dev->node, &input_dev_list);
/* 绑定 输入数据处理对象 到 输入设备的 【场景2】 */
list_for_each_entry(handler, &input_handler_list, node)
input_attach_handler(dev, handler); /* 细节参考后续分析 */
前面没有分析输入设备 input_dev
和 input_handler
的绑定流程,在这里分析一下:
input_attach_handler(dev, handler)
/* 看输入设备 @dev 和 输入数据处理对象 @handler 是否匹配? */
id = input_match_device(handler, dev);
if (!id) /* 彼此不匹配 */
return -ENODEV;
/*
* 绑定输入设备对象 @dev 和 输入事件处理对象 @handler:
* evdev_connect()
* evbug_connect()
* input_leds_connect()
* kbd_connect()
* joydev_connect()
* kgdboc_reset_connect()
* mousedev_connect()
* sysrq_connect()
* ...
* 这里只看 evdev_connect() 的实现细节,其它类设备的代码,感兴趣的读者可自行阅读。
*/
handler->connect(handler, dev, id) = evdev_connect()
struct evdev *evdev;
...
/* 输入事件字符设备 /dev/input/eventX 次设备号分配 */
minor = input_get_new_minor(EVDEV_MINOR_BASE, EVDEV_MINORS, true);
/* 创输入事件处理对象 */
evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL);
...
init_waitqueue_head(&evdev->wait); /* 输入事件字符设备的进程等待队列初始化 */
evdev->exist = true;
dev_no = minor;
dev_set_name(&evdev->dev, "event%d", dev_no);
evdev->handle.dev = input_get_device(dev);
evdev->handle.name = dev_name(&evdev->dev);
evdev->handle.handler = handler;
evdev->handle.private = evdev;
evdev->dev.devt = MKDEV(INPUT_MAJOR, minor); /* 设置事件字符设备的主次设备号 */
evdev->dev.class = &input_class;
...
/* 通过 input_handle 将 input_handler 绑定到 input_dev */
input_register_handle(&evdev->handle)
struct input_handler *handler = handle->handler;
struct input_dev *dev = handle->dev;
...
/* 将 input_handler 通过 input_handle 绑定到 input_dev */
if (handler->filter)
list_add_rcu(&handle->d_node, &dev->h_list);
else
list_add_tail_rcu(&handle->d_node, &dev->h_list);
/* 将 input_handle 关联到 input_handler */
list_add_tail_rcu(&handle->h_node, &handler->h_list);
/* 启动 input_handler */
if (handler->start)
handler->start(handle); /* 如 kbd_start(), rfkill_start(), ... */
/* 事件字符设备的初始化和注册 */
cdev_init(&evdev->cdev, &evdev_fops);
cdev_device_add(&evdev->cdev, &evdev->dev);
用一张图来总结一下 input_dev, input_handle, input_handler
和 ev_dev, evdev_client
之间的关系,如下:
4.3 输入事件的上报
内核输入子系统提供通用接口 input_event()
上报输入事件数据。为方便各类型的输入设备驱动,对 input_event()
的进行封装,又提供了下列接口上报输入事件数据:
input_report_key()
input_report_rel()
input_report_abs()
...
input_sync()
input_mt_sync()
我们还是以键盘类设备为例,看一下输入事件上报的流程:
ttp229_key_report()
if (ttp229->state == new_state) /* long tap not support now!!! */
return;
input_report_key(ttp229->input_dev,
ttp229_key_hash(new_state == 0xFFFF ? ttp229->state : new_state),
new_state == 0xFFFF ? 0 : 1)
input_event(dev, EV_KEY, code, !!value)
input_handle_event(dev, type, code, value)
struct input_value *v;
v = &dev->vals[dev->num_vals++];
v->type = type;
v->code = code;
v->value = value;
...
input_pass_values(dev, dev->vals, dev->num_vals)
/* 将事件数据传递给挂接在输入设备 input_dev 上 input_handler 处理 */
list_for_each_entry_rcu(handle, &dev->h_list, d_node)
if (handle->open) {
count = input_to_handler(handle, vals, count)
/*
* 将通用事件数据传递给具体的 input_handler 处理。
* evdev_event(), kbd_event(), joydev_event(),...
*/
handler->event(handle, v->type, v->code, v->value)
evdev_event()
struct input_value vals[] = { { type, code, value } };
evdev_events(handle, vals, 1)
/* 传递设备事件数据到 open() 打开的客户端 (evdev_client) */
list_for_each_entry_rcu(client, &evdev->client_list, node)
evdev_pass_values(client, vals, count, ev_time)
for (v = vals; v != vals + count; v++) {
event.type = v->type;
event.code = v->code;
event.value = v->value;
/* 将事件数据传递给用户侧 */
__pass_event(client, &event)
client->buffer[client->tail].time = event->time;
client->buffer[client->tail].type = EV_SYN;
client->buffer[client->tail].code = SYN_DROPPED;
client->buffer[client->tail].value = 0;
client->packet_head = client->tail;
...
}
}
input_sync(ttp229->input_dev);
ttp229->state = new_state;
我们来简单总结一下输入设备事件数据上报的流程:
首先是驱动间接或直接通过 input_event()
将事件数据传递给输入子系统,然后输入输入子系统将数据传递给具体类型的事件处理接口,如 evdev_event(), kbd_event(), joydev_event()
等;然后这些接口将通用事件数据格式转换为具体类型的事件数据格式,然后放入用户空间事件数据查询客户端 evdev_client
的数据缓冲,供用户空间应用程序读取。
4.4 输入事件的读取
输入子系统通过字符设备接口,让用户空间访问输入设备的事件数据。看一下用来读取按键数据的用户空间代码:
#define KEYBOARD_EVENT_DEVICE "/dev/input/event2" // 随意写的设备名,要根据具体情况设定
int fd;
struct input_event event;
fd = open(KEYBOARD_EVENT_DEVICE, O_RDONLY);
read(fd, &event, sizeof(event));
if (event.type == EV_KEY) { // 按键事件
// 按键码
switch (event.code) {
case KEY_1: ... break;
...
}
// 按键状态:0 松开,1 按下,2 按住
if (event.value == 0) {
// 按键松开处理
} else if (event.value == 1) {
// 按键按下处理
} else if (event.value == 2) {
// 按键按住处理
}
}
close(fd);
上述是用户空间读取按键事件数据的逻辑,内核空间的处理流程如下:
sys_read()
...
evdev_read()
size_t read = 0;
...
for (;;) {
...
while (read + input_event_size() <= count &&
evdev_fetch_next_event(client, &event)) {
/* 将读取的时间数据传递到用户空间 */
if (input_event_to_user(buffer + read, &event))
return -EFAULT;
read += input_event_size();
}
if (read) /* 读取到需要的数据,返回用户空间 */
break;
/* 当前没有数据,同时以阻塞模式读取,进程将进入睡眠状态 */
if (!(file->f_flags & O_NONBLOCK)) {
error = wait_event_interruptible(evdev->wait,
client->packet_head != client->tail ||
!evdev->exist || client->revoked);
if (error)
return error;
}
}
/* 返回读取的数据量 */
return read;
4.5 输入系统调试信息
/proc/bus/input/devices # 导出系统中的输入设备 input_dev
/proc/bus/input/handlers # 导出系统中的输入事件处理对象 input_handler
/sys/class/input/* # 导出系统中输入设备的 device 信息
# 其它
/sys/bus/serio/*
/sys/bus/gameport/*
...
5. 典型的输入设备驱动框架
// 驱动入口
int xxx_probe(...)
{
struct input_dev *input_dev;
// 1. 创建输入设备
input_dev = input_allocate_device();
// 2. 配置输入设备特性
input_set_capability(input_dev, EV_KEY, KEY_1);
...
__set_bit(EV_REP, input_dev->evbit);
// 3. 注册输入设备
input_register_device(input_dev);
// 4. 配置输入事件采集接口
// . timer 轮询采集
// . work 轮询采集
// . 中断采集
...
}
// 驱动输入事件上报接口
void xxx_report(...)
{
...
input_report_key(input_dev, KEY_xxx, value);
input_sync(input_dev);
...
}
6. 输入子系统小结
7. 后记
限于篇幅,本文的内容远不不足以覆盖 Linux内核输入子系统
的方方面面,如果能够起到一个导读作用,本篇的目的已经达到。
8. 参考资料
内核文档:Documentation\input\*