Linux输入子系统简析

news2024/11/13 9:59:08

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->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\*

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/181250.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Cloud_Ribbon负载均衡服务调用

目录一、概述1.是什么2.官网资料3.能干嘛二、Ribbon负载均衡演示1.架构说明2.POM三、Ribbon核心组件Irule1.IRule2.如何替换3.新建package&#xff08;注意&#xff1a;包的位置&#xff09;4.上面包下新建MySelfRule规则类5.主启动类添加RibbonClient6.测试四、Ribbon负载均衡…

Python将JSON格式文件导入 redis,多种方法

在导入前需要先确定你已经安装 Redis&#xff0c;并且可以启动相关服务。 windows 上启动 redis 的命令是 redis-server.exe redis.windows.conf&#xff0c;效果图如下&#xff1a; 文章目录使用 Python 连接 redis安装 redis 与 导入使用代码连接 Redis写入键值操作 JSON 文…

第26章 分布式缓存数据库配置的定义实现

1 Core.Configuration.CacheConfig namespace Core.Configuration { /// <summary> /// 【缓存配置--类】 /// <remarks> /// 摘要&#xff1a; /// 通过该类中的属性成员实例对“appsettings.json”文件中的1个指定缓存项(键/值对)在内存或指定分布式软件中…

mysql-installer-community-8.0.22.0安装教程

1. 下载 mysql-installer-community-8.0.22.0安装包 首先去官网&#xff1a;https://dev.mysql.com/downloads/installer/ 下载MySQL。 2. 默认Next 3. 点击Execute 4. 同意安装 5. 点击Next后点Yes 6. 点击Execute 这里出现10个选项是正确的&#xff0c;如果不是&#xff0c;…

C++ vector 容器介绍

C vector 容器介绍 C的vector是标准库中常见的一种容器&#xff0c;使用起来非常方便&#xff0c;可以用来代替c原本的数组。vector是种容器&#xff0c;类似数组一样&#xff0c;但它的size可以动态改变。vector的元素在内存中连续排列&#xff0c;这一点跟数组一样。由于vect…

队列同步器AQS的实现与分析——独占锁模式

AQS独占锁模式源码分析1、tryAcquire()、acquire()方法2、addWaiter()方法3、acquireQueued()方法4、shouldParkAfterFailedAcquire()方法5、tryRelease()、release()方法1、tryAcquire()、acquire()方法 protected boolean tryAcquire(int arg) {throw new UnsupportedOperat…

glassfish任意文件读取漏洞

glassfish任意文件读取漏洞1.简介1.1.漏洞类型1.2.漏洞成因1.3.语法搜索1.4.影响版本2.漏洞复现2.1.POC2.2.访问地址2.3.GlassFish的敏感目录2.3.1.获取数据库密码2.3.2.获取GlassFish的后台密码2.4.POC脚本1.简介 GlassFish是一款强健的商业兼容应用服务器&#xff0c;达到产品…

MyEclipse提示过期,MyEclipse Subscription Expired激活方案

一、错误描述 紧接上文&#xff0c;虽然解决了MyEclipse提示过期问题&#xff0c;但是你会发现出现一行红色提示如下&#xff1a; 1.错误日志 Product activation must be completed within 5 days. 2.错误说明 产品激活必须在5天内完成。 二、解决方案 从错误日志很明显的可…

C++语法复习笔记-2. c++基础句法

文章目录1. 图灵机与三种基本结构1. 顺序结构2. 分支结构自定义结构-枚举结构体与联合体结构体数据对齐问题3. 循环结构三种循环结构反汇编查看三种结构效率实例&#xff1a;输出所有形如aabb的四位数的完全平方数方案1: 构造aabb数&#xff0c;再判断方案2&#xff1a;反向操作…

《网络编程实战》学习笔记 Day10

系列文章目录 这是本周期内系列打卡文章的所有文章的目录 《Go 并发数据结构和算法实践》学习笔记 Day 1《Go 并发数据结构和算法实践》学习笔记 Day 2《说透芯片》学习笔记 Day 3《深入浅出计算机组成原理》学习笔记 Day 4《编程高手必学的内存知识》学习笔记 Day 5NUMA内存知…

自制DAPLink 基于ARM官方源码以及STM32F103C8T6

【本文发布于https://blog.csdn.net/Stack_/article/details/128771308&#xff0c;未经许可禁止转载&#xff0c;转载须注明出处】 一、安装工具并配置环境变量 1、python3 【官网】 【网盘】 链接&#xff1a;https://pan.baidu.com/s/1zW_H_eQlkzX3FkXuClFnTA 提取码&#…

python 操作 json 文件的种种知识点

本篇博客将带你全方位了解 Python 操作 json 文件的技术点 让你碰到 json 格式文件不在发愁 文章目录json 模块读取 JSON写入 JSON读取与写入基本用法如下json 模块进阶用法控制输出格式在 JSON 中存储 Python 特殊类型对数据进行验证和清洗第三方模块json 模块 Python 提供了…

CE自动汇编之AOB注入

目录 一、什么是AOB注入&#xff1f; 二、什么时候使用AOB注入&#xff1f; 三、代码注入 四、全部注入 五、“全部注入”和“AOB注入”的分别 六、代码注入与AOB注入的区别 CE自动汇编的模板中&#xff0c;有三种注入代码的方式&#xff1a; 第一种是代码注入&#xff…

Qt使用数据库模型中的删除详解

以下使用 QSqlTableModel 模型&#xff0c;使用tableView显示内容 以下为界面&#xff1a; 这里主要介绍删除操作&#xff1a; 删除一行为&#xff1a; int rowui->tableView->currentIndex().row();//获取行号model->revertRow(row);//删除该行model->submitAll(…

git 关于分支和仓库的理解

何时需要initgit init//初始化本地仓库.git目录如果初始化就会在当前文件夹中出现.git的目录&#xff0c;该目录默认是隐藏的&#xff0c;需要关闭显示隐藏文件才能看到。执行完git init命令后&#xff0c;当前目录就成为了工作区&#xff08;工作区可以理解为操作本地仓库的车…

MyBatis-Plus知识快速入门

文章目录1.MyBatis-Plus简介2.入门案例2.1开发环境2.2创建测试数据库和表2.3创建SpringBoot工程2.4创建实体类以及lombok的使用2.5添加mapper2.6加入日志功能3.基本的CRUD3.1BaseMapper3.2插入3.3删除3.4修改3.5查询4.通用Service4.1创建Service接口和实现类5.常用注解5.1Table…

“华为杯”研究生数学建模竞赛2005年-【华为杯】A题:城市出租车交通规划综合模型(附获奖论文和matlab代码)

赛题描述 A: Highway Traveling time Estimate and Optimal Routing Ⅰ Highway traveling time estimate is crucial to travelers. Hence, detectors are mounted on some of the US highways. For instance, detectors are mounted on every two-way six-lane highways o…

springboot 分布式全局唯一id的生成-雪花算法snowflake

一 背景描述 1.1 问题产生 在分布式系统中&#xff0c;怎么使用全局唯一id&#xff1f; 在分布式是&#xff0c;微服务的架构中&#xff0c;或者大数据分库分表中&#xff0c;多个不同节点怎么保持每台机器生成的主键id不重复&#xff0c;具有唯一性&#xff1f; 方案1&…

【算法基础】归并排序(原理、过程、例题、代码)

一、归并排序原理 1. 算法介绍 归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有…

力扣 2309. 兼具大小写的最好英文字母

题目 给你一个由英文字母组成的字符串 s &#xff0c;请你找出并返回 s 中的 最好 英文字母。返回的字母必须为大写形式。如果不存在满足条件的字母&#xff0c;则返回一个空字符串。 最好 英文字母的大写和小写形式必须 都 在 s 中出现。 英文字母 b 比另一个英文字母 a 更…