1. 概述


linux中input子系统与I2C子系统类似,也被主观分成三部分:输入驱动、输入设备和输入核心。

  • 输入驱动 :由linux抽象出通用的几个输入事件代码(evdev.c、keyboard.c、mousedev.c)。
  • 输入设备 :需要用户自己实现具体输入设备的代码。
  • 输入核心 :搭建输入子系统,并提供输入设备与输入驱动需要的注册API。
2. 流程


在不采用input子系统,而是自己实现的按键字符驱动中,会自己注册驱动,提供file_operations接口,并在读接口中,读取按键的电平值上传给应用。在linux系统中(linux4.9.88),构建了input子系统,所有采用input子系统的设备,在有输入事件后都会主动上报输入事件。

在输入设备中会有以下几个问题:a. 何时上报?是在输入设备输入事件中断产生时上报。b. 如何上报?输入设备在中断函数中调用input提供的input_report_key函数。c. 上报什么东西?(猜测) 由输入设备设定,再经由input.c代码实现与设定值相对应的输入事件驱动匹配。然后经由输入驱动决定上报的具体格式数据。



输入核心(driver/input/input.c):

此代码主要负责搭建linux中input子系统架构,从入口函数进行分析:

/* include/uapi/linux/major.h */#define INPUT_MAJOR  13
/* driver/input/input.c */static int __init input_init(void){ int err;
err = class_register(&input_class); if (err) { pr_err("unable to register input_dev class\n"); return err; }
err = input_proc_init(); if (err) goto fail1;
err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0), INPUT_MAX_CHAR_DEVICES, "input"); if (err) { pr_err("unable to register char major %d", INPUT_MAJOR); goto fail2; }
return 0;
fail2: input_proc_exit(); fail1: class_unregister(&input_class); return err;}


从input.c 入口函数,可以发现其实现了字符驱动函数都需要实现的通用操作。其中主设备号被固定为13,不同的输入设备主设备号相同,分配的次设备号不同。



输入设备:

以之前写的一个按键驱动代码为例,详细分析此代码如何采用input子系统的。

代码路径:https://github.com/LinuxTaoist/Linux_drivers/blob/master/KEY/input_key/1.0/input_key.c

其入口函数就不再介绍,都是platform_driver通用操作,主要关注一下其probe函数和中断服务函数:

static irqreturn_t gpio_key_handler(int irq, void *dev_id){    /* 上报输入事件 */    input_report_key(dev->inputdev, dev->irq_keydesc->value, 0);    input_sync(dev->inputdev);}
static int input_key_probe(struct platform_device *pdev){
/* 申请中断 */ for (i = 0; i < input_key_dev.count; i++) { ret = request_irq(input_key_dev.irq_keydesc[i].irqnum, gpio_key_handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, input_key_dev.irq_keydesc[i].name, &input_key_dev);
} /* 申请input分配input_dev设备 */ input_key_dev.inputdev = input_allocate_device(); input_key_dev.inputdev->name = INPUT_KEY_NAME; /* 初始化input_dev,设置产生哪些事件 */ __set_bit(EV_KEY, input_key_dev.inputdev->evbit); /* 设置产生按键事件 */ __set_bit(EV_REP, input_key_dev.inputdev->evbit); /* 重复事件,比如按下去不放开,就会一直输出信息 */
/* 初始化input_dev,设置产生哪些按键 */ __set_bit(KEY_0, input_key_dev.inputdev->keybit); /* 注册input_dev设备 */ ret = input_register_device(input_key_dev.inputdev); return 0;}


输入设备代码是需要用户自己完成的,其步骤大概包括:

  1. 获取设备树硬件属性
  2. 注册事件中断
  3. 向input子系统申请input_dev设备
  4. 设置input_dev设备相关参数
  5. 注册input_dev设备
  6. 中断服务函数上报输入事件

实现以上步骤,基本上input输入设备代码就完成了。这里分析输入设备是如何加入到input子系统的:

a. 首先需要向input.c申请一个input_dev设备,用到 input_allocate_device来申请:

/* driver/input/input.c */struct input_dev *input_allocate_device(void){ static atomic_t input_no = ATOMIC_INIT(-1); struct input_dev *dev;
dev = kzalloc(sizeof(struct input_dev), GFP_KERNEL); if (dev) { dev->dev.type = &input_dev_type; dev->dev.class = &input_class; device_initialize(&dev->dev); mutex_init(&dev->mutex); spin_lock_init(&dev->event_lock); 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));
__module_get(THIS_MODULE); }
return dev;}


可以看到input.c函数input_allocate_device内部初始化一个input_dev(包括初始化了input_dev的h_list成员),然后初始化内部其他成员,将input_dev返回给调用者。

b. 根据当前设备需要设置input_dev其他成员,并调用input_register_device注册到input子系统:

int input_register_device(struct input_dev *dev){    /* 插入链表中 */ list_add_tail(&dev->node, &input_dev_list);    /* 匹配input_dev对应的handler */ list_for_each_entry(handler, &input_handler_list, node)  input_attach_handler(dev, handler);
}


input_dev与handler匹配函数:input_attach_handler

static int input_attach_handler(struct input_dev *dev, struct input_handler *handler){    /* 匹配测试 */ id = input_match_device(handler, dev);
error = handler->connect(handler, dev, id);}


先匹配测试,当匹配成功以后会进入 handler->connect()函数进行handler(输入驱动)中的connect函数,放在驱动输入分析。



输入驱动:

在概述中介绍过,输入驱动就是对通用的输入事件进行抽象的代码。这里以evdev.c为例。

代码路径:driver/input/evdev.c

static int __init evdev_init(void){ return input_register_handler(&evdev_handler);}
int input_register_handler(struct input_handler *handler){ INIT_LIST_HEAD(&handler->h_list);
list_add_tail(&handler->node, &input_handler_list);
list_for_each_entry(dev, &input_dev_list, node) input_attach_handler(dev, handler);}


这里输入驱动注册函数input_register_handler与输入设备代码注册函数input_register_device对应,都是在注册时寻找有没有对应的handler/input_dev,然后进入匹配函数。

static int input_attach_handler(struct input_dev *dev, struct input_handler *handler){    /* 匹配测试 */ id = input_match_device(handler, dev);
error = handler->connect(handler, dev, id);}


这里匹配成功后,进入handler->connect,在输入驱动代码中,看下此指针指向:

static struct input_handler evdev_handler = { .event  = evdev_event, .events  = evdev_events, .connect = evdev_connect, .disconnect = evdev_disconnect, .legacy_minors = true, .minor  = EVDEV_MINOR_BASE, .name  = "evdev", .id_table = evdev_ids,};


就会进入evdev_connect:

static int evdev_connect(struct input_handler *handler, struct input_dev *dev,    const struct input_device_id *id){
/* 获取新的次设备号 */ minor = input_get_new_minor(EVDEV_MINOR_BASE, EVDEV_MINORS, true);
dev_set_name(&evdev->dev, "event%d", dev_no);
/* 将handler和input_dev绑定,及通过handle可以获取到inpu_dev和对应handler */ evdev->handle.dev = input_get_device(dev); evdev->handle.name = dev_name(&evdev->dev); evdev->handle.handler = handler; evdev->handle.private = evdev; /* 将handle作为字符驱动注册到内核 */ evdev->dev.devt = MKDEV(INPUT_MAJOR, minor); evdev->dev.class = &input_class; evdev->dev.parent = &dev->dev; evdev->dev.release = evdev_free; device_initialize(&evdev->dev);
/* 将新建的handle注册到input子系统 */ error = input_register_handle(&evdev->handle);
/* 增加file_operations接口 */ cdev_init(&evdev->cdev, &evdev_fops); evdev->cdev.kobj.parent = &evdev->dev.kobj; error = cdev_add(&evdev->cdev, evdev->dev.devt, 1);
error = device_add(&evdev->dev);
}


通过这里的分析,就明白了最开始input.c中为什么只调用register_chrdev_region单纯申请一下主设备号,而没有cdev_init和cdev_add来注册file_operations。

原因是只有在input_dev与handler匹配成功,才会在connect函数中注册对应的输入驱动的file_operations。而且还会生成handle结构体,其成员

    evdev->handle.dev = input_get_device(dev); evdev->handle.handler = handler;


如此便能通过handler/input_dev,找到对应的彼此。

接着分析input_register_handle:

int input_register_handle(struct input_handle *handle){ struct input_handler *handler = handle->handler; struct input_dev *dev = handle->dev;
list_add_tail_rcu(&handle->d_node, &dev->h_list);
list_add_tail_rcu(&handle->h_node, &handler->h_list);
return 0;}


发现是将handler与对应的input_dev的hlist成对插入handle->d_node、handle->h_node中,方便以后查找。



3. 触发方式

以上便是input子系统的大概流程了,一切都看似铺垫完毕了。

那么问题来了。如何读取这个按键呢,准确实时的获取键值?

在linux中是这样的,当有按键被按下时,输入设备代码中的中断就会被触发,此时我们只需要在中断中获取按键的状态并调用input子系统提供的上报函数直接上报即可。

static irqreturn_t gpio_key_handler(int irq, void *dev_id){    /* 上报输入事件 */    input_report_key(dev->inputdev, dev->irq_keydesc->value, 0);    input_sync(dev->inputdev);}
static inline void input_report_key(struct input_dev *dev, unsigned int code, int value){ input_event(dev, EV_KEY, code, !!value);}
void input_event(struct input_dev *dev,   unsigned int type, unsigned int code, int value){ unsigned long flags;
if (is_event_supported(type, dev->evbit, EV_MAX)) {
spin_lock_irqsave(&dev->event_lock, flags); input_handle_event(dev, type, code, value); spin_unlock_irqrestore(&dev->event_lock, flags); }}
static void input_handle_event(struct input_dev *dev,          unsigned int type, unsigned int code, int value){  input_pass_values(dev, dev->vals, dev->num_vals);}
static void input_pass_values(struct input_dev *dev,         struct input_value *vals, unsigned int count){
handle = rcu_dereference(dev->grab); if (handle) { count = input_to_handler(handle, vals, count); } else { list_for_each_entry_rcu(handle, &dev->h_list, d_node) if (handle->open) { count = input_to_handler(handle, vals, count); if (!count) break; } }
/* trigger auto repeat for key events */ if (test_bit(EV_REP, dev->evbit) && test_bit(EV_KEY, dev->evbit)) { for (v = vals; v != vals + count; v++) { if (v->type == EV_KEY && v->value != 2) { if (v->value) input_start_autorepeat(dev, v->code); else input_stop_autorepeat(dev); } } }}
static unsigned int input_to_handler(struct input_handle *handle,   struct input_value *vals, unsigned int count){ handler->events(handle, vals, count);}


最终会进入输入驱动的events函数,

static struct input_handler evdev_handler = { .event  = evdev_event, .events  = evdev_events, .connect = evdev_connect, .disconnect = evdev_disconnect, .legacy_minors = true, .minor  = EVDEV_MINOR_BASE, .name  = "evdev", .id_table = evdev_ids,};
static void evdev_events(struct input_handle *handle,    const struct input_value *vals, unsigned int count){
client = rcu_dereference(evdev->grab);
if (client) evdev_pass_values(client, vals, count, ev_time); else list_for_each_entry_rcu(client, &evdev->client_list, node) evdev_pass_values(client, vals, count, ev_time);}
static void evdev_pass_values(struct evdev_client *client,   const struct input_value *vals, unsigned int count,   ktime_t *ev_time){ if (wakeup)  wake_up_interruptible(&evdev->wait);}


可以看到这里是唤醒evdev->wait事件,那么就在当前文件找对应的函数wait_event_interruptible(&evdev->wait):

static ssize_t evdev_read(struct file *file, char __user *buffer,     size_t count, loff_t *ppos){
for (;;) { if (client->packet_head == client->tail && (file->f_flags & O_NONBLOCK)) return -EAGAIN;

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); } }
return read;}


分析到这里基本就清楚了!首先应用会先调用open输入驱动文件,然后调用read函数进入evdev_read。此函数的设计是在死循环中一直上报按键的状态。

然后会根据应用传入的flag是阻塞还是非阻塞决定是否休眠此函数,如果被休眠,就不会再运行输入事件上报。

最后在其他API中使用唤醒接口就能实时控制输入事件的上报。在输入驱动中的中断函数实时发生,并调用input_report_key最终会调用wait_event_interruptible实时上传输入事件给应用层。

4.总结

本次的文章主要是对input子系统进行个人的系统分析。input子系统其实就是linux针对不同的输入设备搭建的一个架构,抽象出相同的代码,然后提供用户通用的接口,按linux的标准来就能上报标准的输入数据。

其大致运行流程是:

  • 输入驱动注册进去后,系统会在/dev/input/节点下生成对应的eventN
  • APP应用程序阻塞式open("/dev/input/eventN",O_RDWR),并执行read。内核进入evdev_read中,(也可以是其他匹配的驱动read),进入死循环并在其中上报输入事件,但是会设计等待队列休眠。
  • 在设备驱动中断中调用input_report,最终会调用等待队列唤醒,从而使evdev_read循环执行一次输入事件上报,然后再次休眠。从而实现输入事件实时上报的功能。

如需技术交流,可关注公众号 “开源519”

input子系统剖析_输入设备开源519.jpg