input子系统剖析

时间:2022-07-24
本文章向大家介绍input子系统剖析,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

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 classn");
  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循环执行一次输入事件上报,然后再次休眠。从而实现输入事件实时上报的功能。