input子系统中读取流程解析

往期内容

本专栏往期内容:

  1. input子系统的框架和重要数据结构详解-CSDN博客
  2. input device和input handler的注册以及匹配过程解析-CSDN博客

I2C子系统专栏:

  1. 专栏地址:IIC子系统_憧憬一下的博客-CSDN博客
  2. 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
    -- 末篇,有往期内容观看顺序

总线和设备树专栏:

  1. 专栏地址:总线和设备树_憧憬一下的博客-CSDN博客
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
    -- 末篇,有往期内容观看顺序

前言

参考:

  • \Linux-4.9.88\Linux-4.9.88\drivers\input.c📎input.c -- input.c
  • \Linux-4.9.88\Linux-4.9.88\drivers\evdev.c📎evdev.c -- input handler
  • \Linux-4.9.88\drivers\input\keyboard📎gpio_keys.c -- input device

本节主要介绍input子系统中读取流程的相关实现代码的解析。

建议先看完一下input子系统的相关结构体介绍:input device和input handler的注册以及匹配过程解析-CSDN博客

1.接口

先来看一下evdev.c设备驱动程序中提供给上层的api接口:

c 复制代码
static const struct file_operations evdev_fops = {
	.owner		= THIS_MODULE,
	.read		= evdev_read,
	.write		= evdev_write,
	.poll		= evdev_poll,
	.open		= evdev_open,
	.release	= evdev_release,
	.unlocked_ioctl	= evdev_ioctl,
#ifdef CONFIG_COMPAT
	.compat_ioctl	= evdev_ioctl_compat,
#endif
	.fasync		= evdev_fasync,
	.flush		= evdev_flush,
	.llseek		= no_llseek,
};

同时,还是牢记下面这张图:

2.open

APP调用open函数打开/dev/input/event0

  • 在驱动程序evdev_open里,创建一个evdev_client,表示一个"客户"
c 复制代码
static int evdev_open(struct inode *inode, struct file *file)
{
    // 通过 inode 获取关联的 evdev 结构体指针
    struct evdev *evdev = container_of(inode->i_cdev, struct evdev, cdev);

    // 计算输入设备缓冲区大小,依赖于设备的属性
    unsigned int bufsize = evdev_compute_buffer_size(evdev->handle.dev);

    // 确定 evdev_client 结构体的总大小,包含客户端结构体和缓冲区
    unsigned int size = sizeof(struct evdev_client) +
                        bufsize * sizeof(struct input_event);

    // 用于保存新创建的 evdev_client 指针
    struct evdev_client *client;

    // 错误代码变量
    int error;

    // 尝试分配内存,用于存储 evdev 客户端和缓冲区(使用 GFP_KERNEL 分配)
    client = kzalloc(size, GFP_KERNEL | __GFP_NOWARN);

    // 如果分配失败,则尝试使用 vzalloc 分配虚拟内存
    if (!client)
        client = vzalloc(size);

    // 如果仍然分配失败,则返回内存不足错误
    if (!client)
        return -ENOMEM;

    // 设置客户端的缓冲区大小
    client->bufsize = bufsize;

    // 初始化客户端的自旋锁,保护缓冲区的访问
    spin_lock_init(&client->buffer_lock);

    // 将 evdev 设备指针保存到客户端结构体中
    client->evdev = evdev;

    // 将客户端附加到 evdev 设备上,因为一个输入设备evdev是可以被多个程序evdev_client打开的
    evdev_attach_client(evdev, client);

    // 尝试打开设备,并准备接受事件
    error = evdev_open_device(evdev);

    // 如果打开设备失败,进行错误处理
    if (error)
        goto err_free_client;

    // 将客户端结构体指针保存到文件私有数据中,以供后续使用
    file->private_data = client;

    // 设置文件为非可寻址文件类型,因为输入设备通常不支持文件定位
    nonseekable_open(inode, file);

    // 成功返回 0
    return 0;

    // 如果打开设备失败,进行清理
 err_free_client:
    // 将客户端从 evdev 设备上移除
    evdev_detach_client(evdev, client);

    // 释放分配的客户端内存
    kvfree(client);

    // 返回错误代码
    return error;
}

里面提到了evdev和evdev_client,这个是啥? 它们在 Linux 内核的输入子系统中用于实现事件设备的管理。evdev 结构体代表一个输入设备,evdev_client 结构体代表与该设备关联的客户端,通常是用户空间中的程序。通俗点就是,在input_dev下层中,用input_dev来表示一个输入设备,在input_handler上层中,用evdev来表示一个输入设备

  • evdev 结构体管理输入设备本身,维护与该设备的所有客户端的连接。
  • evdev_client 结构体代表打开设备的每个客户端,负责处理设备发送的事件并存储在其环形缓冲区中。
c 复制代码
struct evdev {
    int open;  // 设备是否已经被打开的计数器,如果大于0表示有客户端打开了设备
    struct input_handle handle;  // 用于与输入子系统交互的句柄
    wait_queue_head_t wait;  // 等待队列,用于同步客户端和设备之间的事件处理
    struct evdev_client __rcu *grab;  // 表示当前抓取设备的客户端,通常用于独占设备的客户端
    struct list_head client_list;  // 链表头,用于管理当前连接到设备的所有客户端
    spinlock_t client_lock;  // 保护 client_list 的自旋锁,确保多客户端的并发安全
    struct mutex mutex;  // 保护设备状态的互斥锁,防止设备的并发访问问题
    struct device dev;  // 设备模型中的 device 结构,表示该 evdev 设备
    struct cdev cdev;  // 字符设备结构,允许该设备通过字符设备文件与用户空间交互
    bool exist;  // 设备是否存在的标志,用于检查设备是否被移除
};

struct evdev_client {
    unsigned int head;  // 缓冲区的头部索引,指向最旧的未读取事件
    unsigned int tail;  // 缓冲区的尾部索引,指向下一个可以写入事件的位置
    unsigned int packet_head;  // [未来功能] 指向下一个完整数据包的起始位置
    spinlock_t buffer_lock;  // 自旋锁,用于保护缓冲区的并发访问
    struct fasync_struct *fasync;  // 异步通知的结构体,用于支持信号通知
    struct evdev *evdev;  // 该客户端关联的 evdev 设备指针
    struct list_head node;  // 链表节点,挂载到 evdev 的 `client_list` 上
    unsigned int clk_type;  // 客户端时钟类型
    bool revoked;  // 标志客户端是否被撤销(比如设备被移除时)
    unsigned long *evmasks[EV_CNT];  // 用于跟踪设备事件掩码的数组
    unsigned int bufsize;  // 缓冲区大小
    struct input_event buffer[];  // 环形缓冲区,用于存储输入事件
};

3.read/poll

  • APP调用read/poll读取、等待数据

    • 没有数据时休眠:wait_event_interruptible(evdev->wait, ...)
c 复制代码
static ssize_t evdev_read(struct file *file, char __user *buffer,
			  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;
	int error;

	if (count != 0 && count < input_event_size())
        //检查用户请求的读取字节数 count 是否有效。
		return -EINVAL;

	for (;;) {
		if (!evdev->exist || client->revoked)
            //该部分检查设备是否仍然存在,或者客户端是否已被撤销(例如设备被移除)。
			return -ENODEV;

		if (client->packet_head == client->tail &&
		    (file->f_flags & O_NONBLOCK))
            //如果事件缓冲区为空(即 packet_head 等于 tail),并且文件被打开时设置了 O_NONBLOCK 标志,表示调用者不希望阻塞等待事件。
            //不是阻塞等待,数据为空,直接返回
			return -EAGAIN;

		/*
		 * count == 0 is special - no IO is done but we check
		 * for error conditions (see above).
		 */
		if (count == 0)
			break;

		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();
		}
        //通过 evdev_fetch_next_event() 从缓冲区中获取下一个输入事件,将事件复制到 event 结构体中
        //并使用 input_event_to_user() 函数将事件复制到用户提供的缓冲区中。

		if (read)
			break;

		if (!(file->f_flags & O_NONBLOCK)) {
			error = wait_event_interruptible(evdev->wait,
					client->packet_head != client->tail ||
					!evdev->exist || client->revoked);
            //如果启用了阻塞模式(没有设置 O_NONBLOCK 标志),当缓冲区为空时,代码会调用 wait_event_interruptible(),等待新的事件到来或者设备状态发生变化。
			if (error)
				return error;
		}
	}

	return read;
}

4.event

  • 点击、操作输入设备,产生中断:input_dev硬件层,这里以内核自带的gpio_keys.c为例:
c 复制代码
static irqreturn_t gpio_keys_irq_isr(int irq, void *dev_id)
{
	struct gpio_button_data *bdata = dev_id;
	const struct gpio_keys_button *button = bdata->button;
	struct input_dev *input = bdata->input;
	unsigned long flags;

	BUG_ON(irq != bdata->irq);

	spin_lock_irqsave(&bdata->lock, flags);

	if (!bdata->key_pressed) {
		if (bdata->button->wakeup)
			pm_wakeup_event(bdata->input->dev.parent, 0);

		input_event(input, EV_KEY, button->code, 1);//上报中断事件
		input_sync(input);//本质也是input_event

		if (!bdata->release_delay) {
			input_event(input, EV_KEY, button->code, 0);  
			input_sync(input); 
			goto out;
		}

		bdata->key_pressed = true;
	}

	if (bdata->release_delay)
		mod_timer(&bdata->release_timer,
			jiffies + msecs_to_jiffies(bdata->release_delay));
out:
	spin_unlock_irqrestore(&bdata->lock, flags);
	return IRQ_HANDLED;
}
  • input_event(input, EV_KEY, button->code, 1);:在中断服务程序input_event

    • 从硬件读取到数据
    • 使用input_handle_event/input_sync函数上报数据
c 复制代码
\Linux-4.9.88\drivers\input\input.c:
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);//处理输入事件,并决定如何将其分发给设备和输入处理程序。
        // ----------------------(1)
		spin_unlock_irqrestore(&dev->event_lock, flags);
	}
}
  • input_event做什么?

    • 从dev->h_list中取出input_handle,从input_handle取出input_handler

    • 优先调用input_handler->filter来处理

    • 如果没有input_handler->filter或者没处理成功

      • 调用input_handler->events
      • 没有input_handler->events的话,调用input_handler->event
c 复制代码
static void input_handle_event(struct input_dev *dev,
			       unsigned int type, unsigned int code, int value)
{
    //获取事件处置方式
	int disposition = input_get_disposition(dev, type, code, &value);
    //根据设备的状态和输入事件的类型,返回事件的处理方式,称为 disposition

	if (disposition != INPUT_IGNORE_EVENT && type != EV_SYN)
		add_input_randomness(type, code, value);
    //如果事件不应被忽略(INPUT_IGNORE_EVENT)且不是同步事件(EV_SYN),则调用 add_input_randomness() 增加输入随机性。

	if ((disposition & INPUT_PASS_TO_DEVICE) && dev->event)
		dev->event(dev, type, code, value);
    //如果 disposition 标记为 INPUT_PASS_TO_DEVICE,并且设备有注册事件处理函数(dev->event),则将事件传递给设备的事件处理程序。

	if (!dev->vals)//检查设备是否存储事件
		return;

    //将事件传递给处理程序
	if (disposition & INPUT_PASS_TO_HANDLERS) {
		struct input_value *v;

		if (disposition & INPUT_SLOT) {
			v = &dev->vals[dev->num_vals++]; //和input_dev关联上,对v操作就是对dev->vals操作
			v->type = EV_ABS; //事件类型
			v->code = ABS_MT_SLOT; //事件类型下的哪一种
			v->value = dev->mt->slot; //事件的值
            //这里是用input_value来表示一个事件
		}

		v = &dev->vals[dev->num_vals++];
		v->type = type;
		v->code = code;
		v->value = value;
	}
    /*
    如果事件需要传递给输入处理程序(INPUT_PASS_TO_HANDLERS),则事件被存储到 dev->vals 中。这里通过 dev->num_vals++ 依次递增存储事件值。
        如果 disposition 包含 INPUT_SLOT,表示多点触控设备需要处理槽位信息,会首先存储槽位事件(ABS_MT_SLOT)。
        然后存储当前事件的 type、code 和 value。
    */

    //检查是否需要刷新或同步事件
	if (disposition & INPUT_FLUSH) {
		if (dev->num_vals >= 2)
			input_pass_values(dev, dev->vals, dev->num_vals);
		dev->num_vals = 0;
	} else if (dev->num_vals >= dev->max_vals - 2) {
		dev->vals[dev->num_vals++] = input_value_sync;
		input_pass_values(dev, dev->vals, dev->num_vals);  //------(1)
		dev->num_vals = 0;
	}
    //如果 disposition 包含 INPUT_FLUSH,表示应立即传递事件。此时,如果存储的事件数量(dev->num_vals)达到 2 个或以上,调用 input_pass_values() 将事件传递给所有注册的处理程序。
    //如果事件数量接近设备存储区的上限 (dev->max_vals - 2),则首先添加一个同步事件(input_value_sync),然后调用 input_pass_values() 将事件传递出去。最后重置事件计数器 dev->num_vals。
    

}

其中(1)input_pass_values(dev, dev->vals, dev->num_vals)进行传输事件:

c 复制代码
\Linux-4.9.88\drivers\input\input.c
static void input_pass_values(struct input_dev *dev,
			      struct input_value *vals, unsigned int count)
{
	struct input_handle *handle;
	struct input_value *v;

	if (!count)
		return;//没有需要处理的事件。

	rcu_read_lock();//使用 RCU(Read-Copy-Update)锁来确保数据的一致性。

    //检查设备是否有处理程序
	handle = rcu_dereference(dev->grab);
	if (handle) {
		count = input_to_handler(handle, vals, count); //-------------(2)
	} 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;
			}
	}
    /*
    首先,通过 rcu_dereference() 检查设备是否有 "grab" 的处理程序(即,独占处理事件的处理程序)
        如果有,则调用 input_to_handler() 将事件传递给它。返回的 count 表示处理程序还剩下多少未处理的事件(可能部分事件已处理)。
    如果设备没有 grab 处理程序,则遍历设备的处理程序链表(dev->h_list)。对于每个已打开的处理程序(handle->open 为真),
        调用 input_to_handler() 将事件传递过去。如果事件被处理完毕(count == 0),则终止循环。
    */

    //读取保护区退出
	rcu_read_unlock();

	//处理按键自动重复(auto-repeat)
	if (test_bit(EV_REP, dev->evbit) && test_bit(EV_KEY, dev->evbit)) {
    //首先检查设备是否支持自动重复(EV_REP)并且事件类型为按键(EV_KEY)。
		for (v = vals; v != vals + count; v++) {
            //然后遍历所有剩余未处理的事件,检查每个事件的类型是否为按键(EV_KEY)且其值不为 2(2 表示自动重复按键事件,不在这里处理)。
			if (v->type == EV_KEY && v->value != 2) {
				if (v->value)
					input_start_autorepeat(dev, v->code);
				else
					input_stop_autorepeat(dev);
                //如果按键事件值为 1(按下),则启动自动重复功能(input_start_autorepeat()),如果按键事件值为 0(松开),则停止自动重复功能(input_stop_autorepeat())。
			}
		}
	}
}

(2)其中count = input_to_handler(handle, vals, count) 将事件传递给handler处理程序,内部会承上到input_handler层,调用input_handler中的filter/events处理事件函数:

c 复制代码
static unsigned int input_to_handler(struct input_handle *handle,
			struct input_value *vals, unsigned int count)
{
	// 获取处理当前事件的 handler
	struct input_handler *handler = handle->handler;

	// 定义一个指向事件值数组的指针,初始指向 vals
	struct input_value *end = vals;
	struct input_value *v;

	// 如果处理程序 handler 定义了一个过滤函数
	if (handler->filter) {
		// 遍历所有事件
		for (v = vals; v != vals + count; v++) {
			// 通过过滤函数决定是否处理该事件
			// 如果返回非零值,则跳过该事件(继续处理下一个)
			if (handler->filter(handle, v->type, v->code, v->value))
				continue;
			// 如果事件通过过滤,将其复制到 end 指针位置
			// 这样可以过滤掉无效的事件
			if (end != v)
				*end = *v;
			// end 指针向前移动
			end++;
		}
		// 更新 count 为过滤后剩余的事件数量
		count = end - vals;
	}

	// 如果没有任何剩余事件,则返回 0
	if (!count)
		return 0;

	// 如果处理程序 handler 定义了批量处理函数 `events`
	if (handler->events)
		// 调用批量处理函数处理事件
		handler->events(handle, vals, count);
	// 如果没有批量处理函数,但定义了单事件处理函数 `event`
	else if (handler->event)
		// 遍历所有事件,调用单事件处理函数处理每个事件
		for (v = vals; v != vals + count; v++)
			handler->event(handle, v->type, v->code, v->value);

	// 返回剩余事件的数量
	return count;
}
  • 以evdev.c为例

    • 它有evdev_events:用来处理多个事件
    • 也有evdev_event:实质还是调用evdev_events
    • 唤醒"客户":wake_up_interruptible(&evdev->wait);
c 复制代码
\Linux-4.9.88\drivers\input\evdev.c
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)
{
	// 获取与该事件设备关联的 evdev 结构
	struct evdev *evdev = handle->private;

	// 定义一个指针指向当前客户端
	struct evdev_client *client;

	// 创建一个数组来存储事件发生的时间,数组大小为 3(EV_CLK_MAX)
	// 分别存储 MONO、REAL 和 BOOT 时钟时间
	ktime_t ev_time[EV_CLK_MAX];

	// 获取当前的单调(MONOTONIC)时间
	ev_time[EV_CLK_MONO] = ktime_get();

	// 将单调时间转换为实际时间(REAL),并存储在相应的数组位置
	ev_time[EV_CLK_REAL] = ktime_mono_to_real(ev_time[EV_CLK_MONO]);

	// 将单调时间转换为启动时间(BOOT),并存储在相应的位置
	ev_time[EV_CLK_BOOT] = ktime_mono_to_any(ev_time[EV_CLK_MONO],
						 TK_OFFS_BOOT);

	// 进入 RCU 读取临界区
	rcu_read_lock();

	// 尝试获取当前正在 "抓取" 设备的客户端
	client = rcu_dereference(evdev->grab);

	// 如果有客户端 "抓取" 了设备,则将事件传递给该客户端
	if (client)
		evdev_pass_values(client, vals, count, ev_time);//主要是看这里-----(3)
	else
		// 否则,将事件传递给所有连接的客户端
		list_for_each_entry_rcu(client, &evdev->client_list, node)
			evdev_pass_values(client, vals, count, ev_time); 

	// 退出 RCU 读取临界区
	rcu_read_unlock();
}

其中evdev_pass_values(client, vals, count, ev_time);

c 复制代码
static void evdev_pass_values(struct evdev_client *client,
			const struct input_value *vals, unsigned int count,
			ktime_t *ev_time)
{
	struct evdev *evdev = client->evdev;
	const struct input_value *v;
	struct input_event event;
	bool wakeup = false;

	if (client->revoked)
		return;

	event.time = ktime_to_timeval(ev_time[client->clk_type]);

	/* Interrupts are disabled, just acquire the lock. */
	spin_lock(&client->buffer_lock);

	for (v = vals; v != vals + count; v++) {
		if (__evdev_is_filtered(client, v->type, v->code))
			continue;

		if (v->type == EV_SYN && v->code == SYN_REPORT) {
			/* drop empty SYN_REPORT */
			if (client->packet_head == client->head)
				continue;

			wakeup = true;
		}

		event.type = v->type;
		event.code = v->code;
		event.value = v->value; //将传来的input_value"装进"input_event
        //input_event在上层用于表示一个事件,input_value主要是中层传递给上层用的
		__pass_event(client, &event);
	}

	spin_unlock(&client->buffer_lock);

	if (wakeup)
		wake_up_interruptible(&evdev->wait); //唤醒休眠的客户端,在read中有提到没数据读了就休眠
}

5.总结

内容涉及了 Linux 的 input 子系统,包括 input_handler、evdev、evdev_client 等结构体的实现细节,尤其是 evdev 设备驱动的接口和数据传递的工作机制。

  1. 输入子系统的整体框架和接口:

    • evdev 设备驱动程序通过 struct file_operations evdev_fops 向用户层提供接口,包括 .open.read.write 等。evdev_open 用于在 APP 调用时打开 /dev/input/event 设备,创建并关联一个 evdev_client 客户端实例,实现事件设备的多客户端支持。
  2. 事件读取和事件处理:

    • evdev_read 负责从内核读取输入事件,遇到没有事件时阻塞等待;通过 wait_event_interruptible 来等待事件,利用 input_handle_event 进行数据传输。
    • gpio_keys_irq_isr 实现了 GPIO 按键输入,依靠 input_event 产生事件并调用 input_handle_event 分发事件,经过 input_to_handler 传递给上层的 input_handler
  3. input_event 和 input_handler 层的细化调用流程:

    • input_event 负责从硬件读取事件,将其封装为 input_value 对象,并通过 input_pass_values 传输至上层 input_handler
    • input_to_handler 将事件传递给实际的事件处理程序,支持过滤(filter)、批量处理(events)和逐个处理(event)。
  4. evdev 驱动的事件批量处理函数:

    • 通过 evdev_eventsevdev_event 函数,将多个事件批量上报到用户空间的 APP。
相关推荐
憧憬一下12 小时前
PCIe_Host驱动分析_地址映射
arm开发·嵌入式硬件·嵌入式·linux驱动开发·pci/pcie
楼兰公子13 小时前
相机主要调试参数
arm开发·驱动·camera·v4l2
7yewh1 天前
嵌入式驱动RK3566 HDMI eDP MIPI 背光 屏幕选型与调试提升篇-eDP屏
linux·arm开发·驱动开发·嵌入式硬件·嵌入式linux·rk·edp
charlee441 天前
CMake构建学习笔记19-OpenSSL库的构建
ssl·cmake·c/c++·构建
丶Darling.1 天前
进程间通信博客总结目录
linux·网络编程·进程同步·系统编程·c/c++·阻塞和非阻塞
witton1 天前
将VSCode配置成Goland的视觉效果
ide·vscode·编辑器·go·字体·c/c++·goland
丶Darling.1 天前
linux高性能服务器编程读书笔记目录&&建议
linux·服务器·网络编程·目录·系统编程·c/c++·博客总结
千千道2 天前
深入理解 Linux 内核启动流程
linux·arm开发·驱动开发
徐某人..3 天前
ARM嵌入式学习--第八天(PWM)
arm开发·学习·arm