Linux 输入子系统简析

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_devinput_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_handlerev_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->head++] = *event;
															client->head &= client->bufsize - 1;																									...
													}
						}
	/* 上报按键后,紧随着发送一个 EV_SYN 事件,唤醒因等待事件而陷入睡眠的进程 */						
	input_sync(ttp229->input_dev)
		input_event(dev, EV_SYN, SYN_REPORT, 0)
			...
			evdev_pass_values(client, vals, count, ev_time)
				for (v = vals; v != vals + count; v++) {
					if (v->type == EV_SYN && v->code == SYN_REPORT) {
						...
						wakeup = true;
					}
					event.type = v->type;
  					event.code = v->code;
		  			event.value = v->value;
		  			__pass_event(client, &event);
				}
				
				if (wakeup)
					wake_up_interruptible(&evdev->wait); /* 唤醒因等待事件而陷入睡眠的进程 */

	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_open()
	...
	evdev_open()
		unsigned int bufsize = evdev_compute_buffer_size(evdev->handle.dev);
		struct evdev_client *client;
		
		client = kzalloc(size, GFP_KERNEL | __GFP_NOWARN);
		client->bufsize = bufsize;
		client->evdev = evdev;
		evdev_attach_client(evdev, client);
			list_add_tail_rcu(&client->node, &evdev->client_list);
		
		evdev_open_device(evdev)
			if (!evdev->exist)
				retval = -ENODEV;
			else if (!evdev->open++) {
				...
			}
		...
		file->private_data = client;
		
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\*
posted @ 2025-04-08 09:12  JiMoKuangXiangQu  阅读(15)  评论(0)    收藏  举报