我是靠谱客的博主 能干眼神,最近开发中收集的这篇文章主要介绍linux输入子系统详解——看这一篇就够了1、输入子系统宏观介绍2、重点数据结构3、输入子系统核心层的实现4、通用事件处理驱动的实现5、输入子系统设备驱动层的实现6、设备驱动和事件处理驱动的匹配7、事件处理驱动层上报输入事件到应用层8、设备驱动层上报输入事件到事件处理驱动层,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

1、输入子系统宏观介绍

1.1、层次结构

在这里插入图片描述

(1)输入子系统分为三层,分别是事件处理层、核心层、设备驱动层;
(2)鼠标移动、键盘按键按下等输入事件都需要通过设备驱动层→核心层→事件处理层→用户空间,层层上报,直到应用程序;
(3)事件处理层和核心层是内核维护人员提供的,我们作为嵌入式开发工程师是不需要修改,只需要理解和学会使用相关接;我们只需要根据核心层提供的接口和硬件特性,去编写设备驱动层;

1.2、引入输入子系统的优点

(1)统一了物理形态各异的输入设备的处理方法。对不同的输入设备,大致分为按键类、相对坐标类、绝对坐标类等,每个类在核心层和事件处理层如何处理都是代码已经写好的,我们只需要学习接口的使用;
(2)供用于分发输入报告给用户应用程序的简单事件接口。对于应用程序来说,输入类设备都同一用struct input_event结构体来表示,屏蔽了输入设备的差异;
(3)提炼出输入驱动程序的通用部分,简化驱动程序的开发和移植工作。核心层提供了输入设备的注册、卸载、事件上报接口,我们只需要根据核心层提供的接口和硬件的特性去编写驱动代码;

1.3、事件处理层

(1)事情处理层主要是负责将输入事件上报到应用程序;对于向内核输入子系统注册的输入设备,在sysfs中创建设备节点,应用程序通过操作设备节点来获取输入事件;
(2)事件处理层将输入事件划分为几大类,比如:通用事件(event)、鼠标事件(mouse)、摇杆事件(js)等等,每个输入类设备在注册时需要指定自己属于哪个类;
(3)通用事件是能和任何输入设备匹配上的,意味着只要注册一个输入类设备就会sysfs就会创建对应的/dev/input/eventn设备节点;

1.4、核心层

(1)核心层是起到承上启下的作用,负责协调输入事件在事件处理层和设备驱动层之间的传递;
(2)核心层负责管理事件处理层和设备驱动层,核心层提供相关的接口函数,事件处理层和设备驱动层都必须先向核心层注册,然后才能工作;
(3)核心层负责设备驱动层和事件处理层的匹配问题,设备驱动根据硬件特性是各种各样的,事件处理层也是分为好几种类型,具体硬件驱动和哪一类或者哪几类事件处理类型匹配,需要核心层去做判断;

1.5、设备驱动层

(1)设备驱动层分为两大部分:硬件特性部分 + 核心层注册接口;
(2)设备驱动层的硬件特性部分是具体操作硬件的,不同的硬件差异很大,且不属于内核,这也是我们移植驱动的重点;
(3)核心层注册接口:输入子系统提供的输入设备向内核注册的接口,属于内核代码部分,我们需要理解和会使用这些接口,接口的使用都是模式化的,降低了编写驱动的难度;

1.6、三个层次之间的关系

(1)核心层负责管理事件处理层和设备驱动层;
(2)设备驱动层的设备可以匹配上多个事件处理层的类别;
(3)一个事件处理层的类别可以管理有多个设备驱动层的设备;

2、重点数据结构

2.1、input_dev结构体

2.1.1、input_dev结构体定义

struct input_dev {
	const char *name;	//设备名称
	const char *phys;	//设备在系统中的物理路径
	const char *uniq;	//设备唯一识别符
	struct input_id id;	//设备工D,包含总线ID(PCI 、 USB)、厂商工D,与 input handler 匹配的时会用到

	unsigned long evbit[BITS_TO_LONGS(EV_CNT)];	//设备支持的事件类型
	
	unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];	//设备支持的具体的按键、按钮事件
	unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; 	//户设备支持的具体的相对坐标事件
	unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];	//设备支持的具体的绝对坐标事件
	unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];	//设备支持的具体的混杂事件
	unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];	//设备支持的具体的LED指示灯事件
	unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];	//户设备支持的具体的音效事件
	unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];	//设备支持的具体的力反馈事件
	unsigned long swbit[BITS_TO_LONGS(SW_CNT)];	//设备支持的具体的开关事件

	unsigned int keycodemax;	//键盘码表的大小
	unsigned int keycodesize;	//键盘码表中的元素个数
	void *keycode;	//设备的键盘码表
	
	//下面两个是可选方法,用于配置和获取键盘码表
	int (*setkeycode)(struct input_dev *dev,
			  unsigned int scancode, unsigned int keycode);
	int (*getkeycode)(struct input_dev *dev,
			  unsigned int scancode, unsigned int *keycode);

	struct ff_device *ff;	//如果设备支持力反馈,则该成员将指向力反馈设备描述结构
	unsigned int repeat_key;	//保存上一个键值,用于实现软件自动重复按键(用户按住某个键不放)
	struct timer_list timer;	//用于软件自动重复按键的定时器

	int sync;		//在上次同步事件(EV_SYNC)发生后没有新事件产生,则被设置为 1 

	int abs[ABS_CNT];	//用于上报的绝对坐标当前值
	int rep[REP_MAX + 1];	//记录自动重复按键参数的当前值

	unsigned long key[BITS_TO_LONGS(KEY_CNT)];		//舍反映设备按键、 按钮的当前状态
	unsigned long led[BITS_TO_LONGS(LED_CNT)];	//反映设备LED指示灯的当前状态时
	unsigned long snd[BITS_TO_LONGS(SND_CNT)];	//反映设备音效的当前状态会
	unsigned long sw[BITS_TO_LONGS(SW_CNT)];	//反映设备开关的当前状态

	int absmax[ABS_CNT];	//绝对坐标的最大值
	int absmin[ABS_CNT];	//绝对坐标的最小值
	int absfuzz[ABS_CNT];	//绝对坐标的噪音值,变化小于该值的一半可忽略该事件
	int absflat[ABS_CNT];	//摇杆中心位置大小
	int absres[ABS_CNT];

	//提供以下4个设备驱动层的操作接口,根据具体的设备需求实现它们
	int (*open)(struct input_dev *dev);
	void (*close)(struct input_dev *dev);
	int (*flush)(struct input_dev *dev, struct file *file);
	//用于处理送到设备驱动层来的事件,很多事件在事件处理层被处理,但有的事件仍需送到设备驱动中.
	//如LED指示灯事件和音效事件,因为这些操作通常需要设备驱动执行(如点亮某个键盘指示灯) 
	int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);
	
	//指向独占该设备的输入句柄( input handle ),通常设备驱动上报的事件会被分发到与设备
	//关联的所有事件处理程序( input handler )中处理,但如果通过ioctl 的EVIOCGRAB命令
	//设置了独占句柄,则上报事件只能被所设置的输入句柄对应的事件处理程序处理
	struct input_handle *grab;

	
	spinlock_t event_lock;	//调用 event () 时需要使用该自旋锁来实现互斥
	struct mutex mutex;	//用于串行化的访问 open()、 close()和flush()等设备方法

	//记录输入事件处理程序(input handlers)调用设备open()方法的次数.保证设备open()方法是在
	//第一次调用 input_open_device()中被调用,设备close()方法在最后一次调用 input_close_device()中被调用
	unsigned int users;
	bool going_away;

	struct device dev;  //内嵌device结构

	struct list_head	h_list;	//与该设备相关的输入句柄列表(struct input handle)
	struct list_head	node;	//挂接到input_dev_list链表上
};

在内核输入子系统中struct input_dev结构体用于抽象的表示一个输入设备,设备驱动向核心层注册输入设备就是根据自己的硬件去构建一个合适input_dev结构体,然后通过核心层的设备注册接口去注册;

2.1.2、input_dev 结构中的 evbit 成员

宏定义事件类型
EV_SYN同步事件
EV_KEY按键事件
EV_REL相对坐标事件:比如说鼠标
EV_ABS绝对坐标事件:比如触摸屏
EV_MSC杂项事件
EV_SW开关事件
EV_LED指示灯事件
EV_SND音效事件
EV_REP重复按键事件
EV_FF力反馈事件
EV_PWR电源事件
EV_FF_STATUS力反馈状态事件

input_dev 结构中的 evbit 成员表示设备支持的事件类型,它以位图的方式存储,某位被置
位时,表示设备支持该位代表的事件类型,内核中有专门的位操作函数和宏定义,参考博客:《内核的位图和位操作接口介绍》;

2.2、input_handler结构体

struct input_handler {

	void *private; //由具体的事件处理程序指定的私有数据

	//用于处理送到事件处理层的事件,该方法由核心层调用,调用时已经禁止了中断,
	//并获得dev->event lock ,因此不能喔珉
	void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
	
	bool (*filter)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
	bool (*match)(struct input_handler *handler, struct input_dev *dev);
	
	//在事件处理程序关联到一个输入设备时调用
	int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);
	
	//在事件处理程序脱离与之关联的输入设备时调
	void (*disconnect)(struct input_handle *handle);

	//开启对给定句柄的处理程序。 该函数由核心层在connect ()方法被调用之后,
	//或者在独占句柄释放它占有的输入设备时调
	void (*start)(struct input_handle *handle);

	const struct file_operations *fops; //该事件处理驱动的文件操作集合
	int minor;	//该驱动能够提供的32个连续次设备号中的第一个
	const char *name;	//该处理程序的名称.可 以在/ proc/bus/input/handlers 中看到
	
	//输入设备 ID衰,事件处理驱动通过这个成员来匹配它能处理的设备
	const struct input_device_id *id_table;

	//输入设备ID黑名单.即使在id_table 匹配的设备.只要出现在这个黑名单中,也应该被忽略
	const struct input_device_id *blacklist;

	struct list_head	h_list;	//该事件处理程序关联的输入句柄列
	struct list_head	node;	//所有有事件处理驱动都会通过该成员连接到 input_handler_list链表上
};

在内核中struct handler结构体用于抽象的表示一个事件处理驱动,在内核启动过程中会向核心层注册handler;

2.3、input_handle结构体

struct input_handle {

	void *private; //由事件处理程序指定的私有数据

	int open;	//记录句柄打开的次数 
	const char *name;	//处理程序创建该句柄时,指定的句柄名称*

	struct input_dev *dev;	//该句柄关联的输入设备
	struct input_handler *handler; //句柄关联的事件处理程序

	struct list_head	d_node;	//通过它将该句柄放到与之关联的设备的输入句柄列表中
	struct list_head	h_node;	//通过它将该句柄放到与之关联的处理程序的输入句柄列表中
};

struct handle结构体用于记录匹配上的设备和事件处理驱动,在设备上报输入事件时才知道该往哪些事件处理驱动上报;

2.4、input_dev、input_handler、input_handle三者的关系

(1)input_handle结构体是用于记录匹配上的输入设备和事件处理驱动的,当设备驱动和事件处理驱动匹配上时就会新建一个handle并向核心层注册;
(2)input_dev结构体中有h_list链表,里面记录的是设备对应的handle结构体;
(3)input_handler结构体中有h_list链表,里面记录的是设备对应的handle结构体;
(4)handle结构体里的d_node是记录匹配上的input_dev结构体,h_node是记录的handler结构体;
(5)如果知道handle结构体,可以通过d_node链表找到对应的input_dev结构体,通过h_node链表找到对应的handler结构体;
(6)如果知道input_dev结构体,可以通过h_list链表找到handle结构体,再通过handle结构体的h_node链表找到匹配的handler结构体;
(7)如果知道handler结构体,可以通过h_list链表找到handle结构体,再通过handle结构体的d_node链表找到匹配的input_dev结构体;
总结:三个结构体通过链表互相关联,只要知道其中一个就能通过链表找到另外两个

2.5、一个设备对应多个事件处理驱动的情况

在这里插入图片描述>(1)input_dev_list是内核中管理输入设备的链表,每一个节点都是一个input_dev输入设备,input_dev输入设备在向核心层注册时,会被挂接到input_dev_list链表上;
(2)在input_dev结构体中有h_list链表,链表记录的是匹配的struct input_handle结构体;
(3)struct input_handle结构体里有d_node链表,里面记录的是匹配的input_dev设备,h_node链表记录的匹配的handler事件处理驱动;
(3)input_dev中的h_list链表中有几个struct handle结构体,就代表匹配上几个事件处理驱动;

2.6、一个事件驱动对应多个设备的情况

在这里插入图片描述

(1)intput_handler_list是内核中管理handler事件驱动的链表,事件驱动向核心层注册时就会被挂载到该链表,链表的每个节点都是一个handler事件驱动;
(2)struct handler里有h_list链表,链表记录的是匹配的struct input_handle结构体;
(3))struct input_handler结构体里有d_node链表,里面记录的是匹配的input_dev设备,h_node链表记录的匹配的handler事件处理驱动;
(4)struct input_handler中的h_list链表中有几个struct handle结构体,就代表匹配上几个设备驱动;

3、输入子系统核心层的实现

3.1、核心层的注册

//输入类设备的主设备号
#define INPUT_MAJOR		13

//输入类设备统一的设备节点open方法
static const struct file_operations input_fops = {
	.owner = THIS_MODULE,
	.open = input_open_file,
};

//这个和创建输入类设备的设备节点有关
//输入类设备的设备节点都在/dev/input目录下,默认都是直接在/dev目录下
static char *input_devnode(struct device *dev, mode_t *mode)
{
	return kasprintf(GFP_KERNEL, "input/%s", dev_name(dev));
}

struct class input_class = {
	.name		= "input",
	.devnode	= input_devnode,
};

static int __init input_init(void)
{
	int err;

	input_init_abs_bypass();

	//注册名字将input的类,将来输入设备都属于这个类
	err = class_register(&input_class);
	if (err) {
		printk(KERN_ERR "input: unable to register input_dev classn");
		return err;
	}

	//初始化proc文件系统相关,会看到/proc/bus/input目录
	err = input_proc_init();
	if (err)
		goto fail1;

	//注册主设备号是13的字符设备,名字叫做input
	err = register_chrdev(INPUT_MAJOR, "input", &input_fops);
	if (err) {
		printk(KERN_ERR "input: unable to register char major %d", INPUT_MAJOR);
		goto fail2;
	}

	return 0;

 fail2:	input_proc_exit();
 fail1:	class_unregister(&input_class);
	return err;
}

static void __exit input_exit(void)
{
	input_proc_exit();
	unregister_chrdev(INPUT_MAJOR, "input");
	class_unregister(&input_class);
}

//用subsys_initcall宏声明input_init函数,会赋予input_init函数".initcall4.init"段属性,
//保证input_init函数在内核启动过程中被自动执行
subsys_initcall(input_init); 

module_exit(input_exit);

(1)核心层的注册函数input_init会在内核启动时被自动执行,核心层实际就是安装字符设备的驱动实现的;
(2)注册函数创建了input类,这是输入设备的总类,将来创建的输入设备都是这个input类的设备,在sysfs的/sys/class/input目录下可以找到;
(3)在proc文件系统中创建了输入设备相关的文件(/proc/bus/input),里面记录了输入子系统的相关信息;
(4)向内核注册了名字是input的字符设备,主设备号是13,将来新建的输入类设备共享这个主设备号,所以输入类设备的主设备号都是13,只有次设备号不同

3.2、input_dev设备注册和管理

3.2.1、input_register_devic()函数

int input_register_device(struct input_dev *dev)
{
	//input_no是一个原子变量
	static atomic_t input_no = ATOMIC_INIT(0);
	
	struct input_handler *handler;
	const char *path;
	int error;

	/* 每个输入设备都需要支持同步事件 */
	__set_bit(EV_SYN, dev->evbit);

	/* 将KEY_RESERVED位清零 */
	__clear_bit(KEY_RESERVED, dev->keybit);

	/* 确保dev->evbit中没有设置的位都被清零 */
	input_cleanse_bitmasks(dev);

	//初始化定时器
	init_timer(&dev->timer);

	//REP_DELAY和REP_PERIOD是关于重复按键的,如果没有设置就赋值为默认值
	if (!dev->rep[REP_DELAY] && !dev->rep[REP_PERIOD]) {
		dev->timer.data = (long) dev;
		dev->timer.function = input_repeat_key;
		dev->rep[REP_DELAY] = 250;	//第一次按下多久算一次,这里是
		dev->rep[REP_PERIOD] = 33;	//如果按键没有被抬起,则每33ms算一次
	}

	//获取键的扫描码
	if (!dev->getkeycode)
		dev->getkeycode = input_default_getkeycode;

	//设置键的扫描码
	if (!dev->setkeycode)
		dev->setkeycode = input_default_setkeycode;

	//设置内嵌dev的名字
	dev_set_name(&dev->dev, "input%ld",
		     (unsigned long) atomic_inc_return(&input_no) - 1);

	//将input_dev内嵌的dev注册到sysfs
	error = device_add(&dev->dev);
	if (error)
		return error;

	path = kobject_get_path(&dev->dev.kobj, GFP_KERNEL);
	printk(KERN_INFO "input: %s as %sn",
		dev->name ? dev->name : "Unspecified device", path ? path : "N/A");
	kfree(path);
	
	//上锁
	error = mutex_lock_interruptible(&input_mutex);
	if (error) {
		device_del(&dev->dev);
		return error;
	}

	//将输入设备挂载到input_dev_list链表上
	list_add_tail(&dev->node, &input_dev_list);

	//遍历事件处理驱动,如果找到匹配的事件处理驱动就将该设备依附上去,
	//生成一个handle
	list_for_each_entry(handler, &input_handler_list, node)
		input_attach_handler(dev, handler);

	input_wakeup_procfs_readers();

	mutex_unlock(&input_mutex);

	return 0;
}

(1)input_register_devic()函数是核心层提供给设备驱动层用于注册输入设备的接口,传参就是设备驱动层构建好的struct input_dev结构体;
(2)注册函数需要将传入的struct input_dev结构体挂接到input_dev_list链表上;
(3)将传递进来的input_dev结构体和所有注册的事件驱动进行匹配,匹配上就会产生handle,并在事情驱动会产生设备节点;

3.2.2、input_dev_list链表

static LIST_HEAD(input_dev_list);

(1)input_dev_list是内核链表,用LIST_HEAD宏来定义,在SourceInsight软件中不能直接找跳转到定义处;
(2)input_dev_list是内核用于管理输入设备的,所有向核心层注册的输入设备都会被挂接到这个链表上,通过遍历这个链表就可以找到所有注册的输入设备;

3.3、handler的注册和管理

3.3.1、input_register_handler()函数

int input_register_handler(struct input_handler *handler)
{
	struct input_dev *dev;
	int retval;
	
	//上锁
	retval = mutex_lock_interruptible(&input_mutex);
	if (retval)
		return retval;

	//初始化链表
	INIT_LIST_HEAD(&handler->h_list);

	//根据handler的次设备号基数,将handler存放到input_table数组的合适位置
	//"handler->minor >> 5"就是在数组中的下标
	if (handler->fops != NULL) {
		if (input_table[handler->minor >> 5]) {
			retval = -EBUSY;
			goto out;
		}
		input_table[handler->minor >> 5] = handler;
	}

	//将handler挂接到input_handler_list链表上
	list_add_tail(&handler->node, &input_handler_list);

	//遍历已经注册的输入设备,看是否有匹配上的
	list_for_each_entry(dev, &input_dev_list, node)
		input_attach_handler(dev, handler);

	input_wakeup_procfs_readers();

 out:
	
	//解锁
	mutex_unlock(&input_mutex);
	return retval;
}

(1)input_register_handler()函数是核心层导出的事件驱动注册函数,传参只有struct input_handler结构体;
(2)注册函数会将handler保存到input_table数组中保存起来,位置和handler的次设备号基准值有关;
(3)注册的handler会被挂接到input_handler_list链表中;
(4)遍历已经注册的input_dev设备,看是否有能匹配上的;

3.3.2、input_handler_list链表

static LIST_HEAD(input_handler_list);

(1)input_handler_list是内核链表,用LIST_HEAD宏来定义,在SourceInsight软件中不能直接找跳转到定义处;
(2)input_handler_list是内核用于管理事件驱动的,所有向核心层注册的事件驱动都会被挂接到这个链表上,通过遍历这个链表就可以找到所有注册的事件驱动;

3.4、input_dev设备和handler的匹配

3.4.1、input_attach_handler()函数

static int input_attach_handler(struct input_dev *dev, struct input_handler *handler)
{
	const struct input_device_id *id;
	int error;

	//判断handler和dev是否匹配
	id = input_match_device(handler, dev);
	if (!id)
		return -ENODEV;

	//如果匹配就调用handler的connect函数
	error = handler->connect(handler, dev, id);
	if (error && error != -ENODEV)
		printk(KERN_ERR
			"input: failed to attach handler %s to device %s, "
			"error: %dn",
			handler->name, kobject_name(&dev->dev.kobj), error);

	return error;
}

(1)调用input_match_device()函数判断handler和dev是否匹配上,如果匹配上就返回一个struct input_device_id结构体指针;
(2)如果匹配上就调用handler->connect函数指针,这是当事件驱动提供的连接函数,当handler和dev匹配上时被调用,会产生一个handle;

3.4.2、input_match_device()函数

#define MATCH_BIT(bit, max) 
		for (i = 0; i < BITS_TO_LONGS(max); i++) 
			if ((id->bit[i] & dev->bit[i]) != id->bit[i]) 
				break; 
		if (i != BITS_TO_LONGS(max)) 
			continue;
static const struct input_device_id *input_match_device(struct input_handler *handler,
							struct input_dev *dev)
{
	const struct input_device_id *id;
	int i;

	//遍历handler的id_table,id_table是保存的事件驱动的匹配信息
	//每一项是否匹配要根据struct input_device_id的flag标志位
	for (id = handler->id_table; id->flags || id->driver_info; id++) {

		//匹配总线
		if (id->flags & INPUT_DEVICE_ID_MATCH_BUS)
			if (id->bustype != dev->id.bustype)
				continue;

		//匹配生产厂商
		if (id->flags & INPUT_DEVICE_ID_MATCH_VENDOR)
			if (id->vendor != dev->id.vendor)
				continue;

		//匹配产品型号
		if (id->flags & INPUT_DEVICE_ID_MATCH_PRODUCT)
			if (id->product != dev->id.product)
				continue;

		//匹配版本号
		if (id->flags & INPUT_DEVICE_ID_MATCH_VERSION)
			if (id->version != dev->id.version)
				continue;

		//判断设备驱动和事件驱动相关事件类型的bit位是否匹配
		//事情驱动设置为1的事件位,设备驱动也要设置为1才能匹配
		//事情驱动设置为0的事件位,设备驱动设什么值无所谓
		MATCH_BIT(evbit,  EV_MAX);
		MATCH_BIT(keybit, KEY_MAX);
		MATCH_BIT(relbit, REL_MAX);
		MATCH_BIT(absbit, ABS_MAX);
		MATCH_BIT(mscbit, MSC_MAX);
		MATCH_BIT(ledbit, LED_MAX);
		MATCH_BIT(sndbit, SND_MAX);
		MATCH_BIT(ffbit,  FF_MAX);
		MATCH_BIT(swbit,  SW_MAX);

		if (!handler->match || handler->match(handler, dev))
			return id;
	}
	return NULL;
}

(1)整个匹配过程事件驱动占主导,判断设备驱动是否符合事件驱动的要求,如果符合就返回匹配上的struct input_device_id结构体指针;
(2)handler->id_table指针可能指向好几个struct input_device_id结构体,只要有一个匹配上就算匹配成功;
(3)匹配中最重要的是事件类型bit位的检查,事情驱动中没有设置的bit位代表事件驱动不关心这个bit位,因为每个事件驱动对应不同类型的输入设备,所以支持的输入事件是有区别的;

3.5、输入事件的上报

3.5.1、input_event()函数

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);
		add_input_randomness(type, code, value); //利用输入值调整随机数产生器

		//上报事件
		input_handle_event(dev, type, code, value);
		spin_unlock_irqrestore(&dev->event_lock, flags);
	}
}

3.5.2、input_handle_event()函数

#define INPUT_IGNORE_EVENT	0
#define INPUT_PASS_TO_HANDLERS	1
#define INPUT_PASS_TO_DEVICE	2
#define INPUT_PASS_TO_ALL	(INPUT_PASS_TO_HANDLERS | INPUT_PASS_TO_DEVICE)

static void input_handle_event(struct input_dev *dev,
			       unsigned int type, unsigned int code, int value)
{
	int disposition = INPUT_IGNORE_EVENT;

	switch (type) {

	case EV_SYN:
		switch (code) {
		······
		case SYN_REPORT:
			if (!dev->sync) {
				// 将同步标志值1
				dev->sync = 1;
				
				//表示将该事件送往事件处理驱动处理
				disposition = INPUT_PASS_TO_HANDLERS;
			}
			break;
		}
		break;

	case EV_KEY:
		//如果设备支持该事件码且本次上报值同 dev->key中记录的上次上报值不同
		if (is_event_supported(code, dev->keybit, KEY_MAX) &&
		    !!test_bit(code, dev->key) != value) {

			if (value != 2) {
				__change_bit(code, dev->key);
				if (value)
					input_start_autorepeat(dev, code); //按键是按下的,则启动自动重复按键
				else
					input_stop_autorepeat(dev);//按键是按下的,则停止自动重复按键
			}

			//将事件的派送位置设置为送往事件处理驱动
			disposition = INPUT_PASS_TO_HANDLERS;
		}
		break;

	case EV_SW:
	······
	}
	
	//只有上报了同步事件并且事件码是SYN_REPORT,同步标志dev->sync才会置1
	if (disposition != INPUT_IGNORE_EVENT && type != EV_SYN)
		dev->sync = 0;

	//如果设置了送往设备的标志位,那么将调用设备的event()方法
	if ((disposition & INPUT_PASS_TO_DEVICE) && dev->event)
		dev->event(dev, type, code, value);

	//如果设置了表示送往事件处理驱动的标志位,那么将调用 inpilt_pass_event()方法
	if (disposition & INPUT_PASS_TO_HANDLERS)
		input_pass_event(dev, type, code, value);
}
传参含义
struct input_dev *dev输入设备结构体
unsigned int type输入事件的大类
unsigned int code输入事件的子类
int value输入事件的值

(1)截取的代码省略了除 EV_fiYN 和 EV_KEY 事件外的其他事件的处理部分;
(2)整个函数会对传入的不同的输入事件进行判断和处理,最终是调用input_pass_event()函数将输入事件上报到事件驱动层;
(3)上面传入的type、code、value对应struct input_event结构体;

3.5.3、input_pass_event()函数

static void input_pass_event(struct input_dev *dev,
			     unsigned int type, unsigned int code, int value)
{
	struct input_handler *handler;
	struct input_handle *handle;

	rcu_read_lock();

	//判断设备是否有独占式句柄
	handle = rcu_dereference(dev->grab);

	//仅有独占句柄关联的那个事件处理驱动的event方法会被调用
	if (handle)
		handle->handler->event(handle, type, code, value);
	else {
		bool filtered = false;

		//遍历与设备关联的所有句柄,并且逐个调用与这些句柄关联的事件处理驱动的event方法
		list_for_each_entry_rcu(handle, &dev->h_list, d_node)
		{
			if (!handle->open)
				continue;

			handler = handle->handler;
			if (!handler->filter) {
				if (filtered)
					break;

				handler->event(handle, type, code, value);

			} else if (handler->filter(handle, type, code, value))
				filtered = true;
		}
	}
	rcu_read_unlock();
}

(1)input_pass_event()函数功能就是遍历和设备匹配的事件驱动,调用事件驱动handler->event方法,把输入事件传到事件驱动层;
(2)注意这里又分为独占式事情驱动和非独占式事件驱动,默认都是非独占的,只有调用了input_grab_device()函数的handler才是独占的;

4、通用事件处理驱动的实现

4.1、事件处理驱动的分类

(1)事件处理驱动层分为几大类,比如通用事件处理驱动、鼠标事件处理驱动、按键事件处理驱动,在sysfs层表现为设备节点的名字不一样,比如通用事件处理驱动的设备节点名字是/dev/input/eventn,鼠标事件处理驱动的设备名字节点是/dev/input/mousen;
(2)一个设备驱动可能匹配多个事件处理驱动,也就是可能多个设备节点指向的是同一个设备;
(3)通用事件处理驱动是万能匹配的,简单理解就是无论什么类型的输入设备都能和通用事件处理驱动匹配上,也就是每个输入设备都会有一个对应的/dev/input/eventn设备节点;
(4)通用事件处理驱动统一了输入设备的差异,不管什么类型的输入设备,都是以struct input_event结构体的方式上报给应用,通用事件处理驱动是以后发展的趋势,所以事件处理驱动层这里只介绍通用处理驱动

4.2、evdev_handler变量定义

static struct input_handler evdev_handler = {
	.event		= evdev_event,		//用于处理送到事件处理层的事件,该方法由核心层调用
	.connect	= evdev_connect,	//在事件处理程序关联到一个输入设备时调用
	.disconnect	= evdev_disconnect,
	.fops		= &evdev_fops,	//通用事件驱动的设备操作方法
	.minor		= EVDEV_MINOR_BASE,	//该事件处理驱动能够提供的32个连续次设备号中的第一个
	.name		= "evdev",	//通用事件处理驱动的名字
	.id_table	= evdev_ids, //输入设备ID表,事件处理驱动通过这个成员来匹配它能处理的设备
};

struct input_handler结构体是内核层用来表示一个事件处理驱动的,每个事件处理驱动都会构建一个struct input_handler结构体,然后调用核心层提供的注册接口进行注册;

evdev_ids变量

static const struct input_device_id evdev_ids[] = {
	{ .driver_info = 1 },	/* Matches all devices */
	{ },			/* Terminating zero entry */
};

(1)struct input_device_id结构体是保存事件处理驱动对设备驱动的匹配要求的;
(2)evdev_ids是个数组,但是每个成员变量几乎都是0,说明通用事件处理驱动对设备驱动没有要求,这也是为什么通用事件处理驱动能和所有设备驱动匹配上的原因;

4.3、evdev_fops文件操作集合

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
};

这些都是将来操作通用事件处理驱动的设备节点要用到的函数,后面会逐一进行分析;

4.4、通用事件处理驱动的注册

static int __init evdev_init(void)
{
	return input_register_handler(&evdev_handler);
}

static void __exit evdev_exit(void)
{
	input_unregister_handler(&evdev_handler);
}

module_init(evdev_init);
module_exit(evdev_exit);

(1)注册代码很简单,就是调用核心层提供的input_register_handler()注册函数,把构建好的struct input_handler结构体类型的变量evdev_handler注册到核心层;
(2)从代码看,通用事件处理驱动可以单独编译成ko文件也可以编译进内核,一般都是直接编译进内核;

4.5、input_handler[ ]数组

static struct input_handler *input_table[8];

(1)input_table数组是核心层用来管理事件处理驱动的,向核心层注册的handler都保存在input_table数组里;
(2)数组只有8个成员,说明核心层最多支持8种事件处理驱动;

4.6、主次设备号划分

事件处理驱动主设备号第一个次设备号次设备数量
joydev.c13032
mousedev.c133232
evdev.c136432

(1)上面列举的是部分事件处理驱动的主次设备号,从上面可以看出各个事件处理驱动的设备主设备号都是一样的,次设备号不同;
(2)每个事件处理驱动最多可以有32个次设备号,意味着每个事件处理驱动最多支持32个输入设备;
(3)比如通用事件处理驱动所属的设备节点,主设备号都是13,次设备范围在[64,96]之间;

5、输入子系统设备驱动层的实现

5.1、输入设备的注册流程

(1)申请一个struct input_dev结构体;
(2)根据输入设备填充struct input_dev结构体,主要是输入设备支持的事件类型;
(3)调用核心层提供的设备驱动注册函数,将struct input_dev结构体注册到核心层;
(4)当输入事件发生时,调用核心层提供的事件上报函数接口;

5.2、设备的注册和注销

int input_register_device(struct input_dev *dev)void input_unregister_device(struct input_dev *dev)

5.3、申请struct input_dev结构体的内存

struct input_dev *input_allocate_device(void)
{
	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; //核心层注册的input类
		device_initialize(&dev->dev);
		mutex_init(&dev->mutex);
		spin_lock_init(&dev->event_lock);
		INIT_LIST_HEAD(&dev->h_list);
		INIT_LIST_HEAD(&dev->node);

		__module_get(THIS_MODULE);
	}

	return dev;
}

核心层提供了申请struct input_dev结构体的函数接口,里面做了一些必要的初始化,我们直接调用即可;

5.4、构建input_dev结构体

	static struct input_dev *button_dev;
	
	button_dev = input_allocate_device();
	if (!button_dev) 
	{ 
		printk(KERN_ERR "key-s5pv210.c: Not enough memoryn");
		error = -ENOMEM;
		goto err_free_irq; 
	}
	
	button_dev->evbit[0] = BIT_MASK(EV_KEY);
	button_dev->keybit[BIT_WORD(KEY_LEFT)] = BIT_MASK(KEY_LEFT);
	
	error = input_register_device(button_dev);

上面是截取按键驱动代码的一部分,可以看到输入设备在注册时主要是是在设备支持的事件类型,这在和事件处理驱动匹配和上报输入事件时会用到;

5.6、报告输入事件

static inline void input_report_key(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_KEY, code, !!value);
}

static inline void input_report_rel(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_REL, code, value);
}

static inline void input_report_abs(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_ABS, code, value);
}

static inline void input_report_ff_status(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_FF_STATUS, code, value);
}

static inline void input_report_switch(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_SW, code, !!value);
}

上报输入事件有一系列的上报函数,但是其实都是对input_event()函数的封装,只是方便设备驱动去上报输入事件;

6、设备驱动和事件处理驱动的匹配

6.1、函数调用

input_register_device()
	list_for_each_entry(handler, &input_handler_list, node)
	input_attach_handler()
			input_match_device()
				handler->connect()

(1)具体的匹配是input_match_device()函数做的,具体细节在核心层介绍过;
(2)当设备驱动和事件处理驱动匹配上时,就会调用handler的connect函数;

static int evdev_connect(struct input_handler *handler, struct input_dev *dev,
			 const struct input_device_id *id)
{
	struct evdev *evdev;
	int minor;
	int error;

	//在evdev_table[]数组中找一个空闲的变量,变量在
	//数组中的下标和设备的次设备号有关系
	//次设备号=handler的基准次设备号 + 数组下标
	for (minor = 0; minor < EVDEV_MINORS; minor++)
		if (!evdev_table[minor])
			break;

	//如果minor==EVDEV_MINORS,表示数组已经没有空闲位置,不能再注册
	if (minor == EVDEV_MINORS) {
		printk(KERN_ERR "evdev: no more free evdev devicesn");
		return -ENFILE;
	}

	evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL);
	if (!evdev)
		return -ENOMEM;

	INIT_LIST_HEAD(&evdev->client_list);
	spin_lock_init(&evdev->client_lock);
	mutex_init(&evdev->mutex);
	init_waitqueue_head(&evdev->wait);

	//设置设备的名字,将来会在/dev/input/目录下看到
	dev_set_name(&evdev->dev, "event%d", minor);
	evdev->exist = 1;
	evdev->minor = minor;

	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, EVDEV_MINOR_BASE + minor);

	//设备属于input类,将来能在/sys/class/input目录下看到
	evdev->dev.class = &input_class;
	evdev->dev.parent = &dev->dev;
	evdev->dev.release = evdev_free;
	device_initialize(&evdev->dev);

	//向核心层注册填充好的handle,将handle分别挂接到对应设备和事件处理驱动的h_list链表上
	error = input_register_handle(&evdev->handle);
	if (error)
		goto err_free_evdev;

	//将构建号的evdev存放到evdev_table[]数组中
	error = evdev_install_chrdev(evdev);
	if (error)
		goto err_unregister_handle;

	//将设备导出到sysfs中
	error = device_add(&evdev->dev);
	if (error)
		goto err_cleanup_evdev;

	return 0;

 err_cleanup_evdev:
	evdev_cleanup(evdev);
 err_unregister_handle:
	input_unregister_handle(&evdev->handle);
 err_free_evdev:
	put_device(&evdev->dev);
	return error;
}

(1)构建一个struct evdev结构体,保存到evdev_table[ ]数组中,在数组中的下标和设备的次设备有关;
(2)填充好handle,里面包含了匹配的设备驱动信息和事件处理驱动信息,调用核心层的注册接口进行注册;

7、事件处理驱动层上报输入事件到应用层

7.1、整体流程

(1)事件处理驱动层的输入事件是设备驱动层上报的,因为设备驱动层在注册时已经和事件处理驱动层进行了匹配,所以设备驱动调用核心层的事件上报接口时,会将输入事件转发到匹配上的事件处理驱动层;
(2)应用层通过read设备节点,读取到输入事件;

7.2、struct evdev client结构体

struct evdev_client {
	//存放未提交到用户空间的输入事件的环形缓冲区,可存放EEVDEV_BUFFER_SIZE(64)个事件
	struct input_event buffer[EVDEV_BUFFER_SIZE];	
	
	int head;	//形缓冲区buffer的头指针,从head开始向buffer放入事件
	int tail;	//环形缓冲区buffer的尾指针,从tail开始从buffer取出事件
	spinlock_t buffer_lock; //供对buffer 、 head和 tail 几个成员的访问保护 
	struct fasync_struct *fasync; //用来实现异步通知事件
	struct evdev *evdev;	//执行本struct evdev_client对象所属的evdev
	struct list_head node;	//将来挂接到该对象所属的evdev的client_list列表上
	struct wake_lock wake_lock;
	char name[28];
};

7.3、struct input_event结构体

struct input_event {
	struct timeval time;	//输入事件发生的事件
	__u16 type;	//事件的大类
	__u16 code;	//事件的子类
	__s32 value;	//事件的值
};

7.4、应用打开设备节点

7.4.1、函数调用

input_open_file()
	evdev_open()
		evdev_attach_client()
		evdev_open_device()

7.4.2、input_open_file()函数

static int input_open_file(struct inode *inode, struct file *file)
{
	struct input_handler *handler;
	const struct file_operations *old_fops, *new_fops = NULL;
	int err;

	err = mutex_lock_interruptible(&input_mutex);
	if (err)
		return err;

	/* 根据次设备号获取对应的handler */
	handler = input_table[iminor(inode) >> 5];
	if (handler)
		new_fops = fops_get(handler->fops); //获取handler的fops

	mutex_unlock(&input_mutex);

	/*
	 * That's _really_ odd. Usually NULL ->open means "nothing special",
	 * not "no device". Oh, well...
	 */
	if (!new_fops || !new_fops->open) {
		fops_put(new_fops);
		err = -ENODEV;
		goto out;
	}

	//替换struct file的f_op,后续再操作设备节点就是调用handler的fops
	old_fops = file->f_op;
	file->f_op = new_fops;

	//调用handler的open方法
	err = new_fops->open(inode, file);
	if (err) {
		fops_put(file->f_op);
		file->f_op = fops_get(old_fops);
	}
	fops_put(old_fops);
out:
	return err;
}

(1)因为输入类设备共享主设备号13,所以打开设备节点都是调用的input_open_file函数,具体看核心层的input_init初始化函数;
(2)核心层input_open_file函数功能就是找到handler对应的fops替换掉struct file结构体中的fops,后续应用程序再操作设备节点就是调用的事件处理驱动层的fops;

7.4.3、evdev_open()函数

static int evdev_open(struct inode *inode, struct file *file)
{
	struct evdev *evdev;
	struct evdev_client *client;

	//根据次设备号,得到对应evdev在evdev_table[]数组中下标
	int i = iminor(inode) - EVDEV_MINOR_BASE;
	int error;
	
	if (i >= EVDEV_MINORS)
		return -ENODEV;

	error = mutex_lock_interruptible(&evdev_table_mutex);
	if (error)
		return error;

	//取出对应的evdev
	evdev = evdev_table[i];
	if (evdev)
		get_device(&evdev->dev);
	mutex_unlock(&evdev_table_mutex);

	if (!evdev)
		return -ENODEV;

	//申请一个struct evdev_client
	client = kzalloc(sizeof(struct evdev_client), GFP_KERNEL);
	if (!client) {
		error = -ENOMEM;
		goto err_put_evdev;
	}

	spin_lock_init(&client->buffer_lock);

	//client的名字
	snprintf(client->name, sizeof(client->name), "%s-%d",
			dev_name(&evdev->dev), task_tgid_vnr(current));
	
	wake_lock_init(&client->wake_lock, WAKE_LOCK_SUSPEND, client->name);

	//将evdev保存到client->evdev
	client->evdev = evdev;

	//将client挂载接evdev->client_list
	evdev_attach_client(evdev, client);

	//判断设备节点是否第一次被打开,如果是第一次打开则判断对应的struct input_dev是否定义了open方法,
	//如果dev定义了open则调用
	error = evdev_open_device(evdev);
	if (error)
		goto err_free_client;

	file->private_data = client; //将client作为文件私有数据供其他操作使用
	nonseekable_open(inode, file);

	return 0;

 err_free_client:
	evdev_detach_client(evdev, client);
	kfree(client);
 err_put_evdev:
	put_device(&evdev->dev);
	return error;
}

(1)根据次设备号找到对应的evdev结构体,构建client结构体,然后将client挂载到evdev->client_list;
(2)如果设备节点是第一次被打开,则需要判断事件驱动层否定义了open方法,如果dev定义了open则调用;

7.5、应用读取输入事件

7.5.1、函数调用

evdev_read()
	evdev_fetch_next_event()	//从client的buffer中读取输入数据
	
	input_event_to_user()
		copy_to_user()	//拷贝输入事件到用户缓冲区

7.5.2、evdev_read()函数

static ssize_t evdev_read(struct file *file, char __user *buffer,
			  size_t count, loff_t *ppos)
{
	//得到对应的client结构体,这是在evdev_open的时候对file->private_data赋值的
	struct evdev_client *client = file->private_data;
	
	struct evdev *evdev = client->evdev;
	struct input_event event;
	int retval;

	//应用传入的缓冲大小不能小于sizeof(struct input_event)
	if (count < input_event_size())
		return -EINVAL;

	//当前没有输入事件,如果是非阻塞打开就直接返回
	if (client->head == client->tail && evdev->exist &&
	    (file->f_flags & O_NONBLOCK))
		return -EAGAIN;

	//一直阻塞到有输入事件产生
	retval = wait_event_interruptible(evdev->wait,
		client->head != client->tail || !evdev->exist);
	if (retval)
		return retval;

	if (!evdev->exist)
		return -ENODEV;

	//从client的buffer缓冲区中得到输入事件
	//向用户空间的缓冲区填充输入事件,不能超过缓冲区的大小
	while (retval + input_event_size() <= count &&
	       evdev_fetch_next_event(client, &event)) {

		//向用户缓冲区拷贝数据
		if (input_event_to_user(buffer + retval, &event))
			return -EFAULT;

		retval += input_event_size();
	}
	return retval;
}

(1)如果是阻塞方式读取,则在没有输入事件产生时会被阻塞,知道产生输入事件,也就是client->head != client->tail;
(2)从client的buffer缓冲区中得到输入事件,然后拷贝到用户缓冲区;当用户缓冲区满或者当前输入事件被读取完,都会返回;

7.5.3、evdev_fetch_next_event()函数

static int evdev_fetch_next_event(struct evdev_client *client,
				  struct input_event *event)
{
	int have_event;

	spin_lock_irq(&client->buffer_lock);

	//当client->head != client->tail时,表示缓冲区中有输入事件
	have_event = client->head != client->tail;

	//从buffer的client->tail位置取出一个输入事件
	if (have_event) {
		*event = client->buffer[client->tail++];
		//client->tail不能超过数组大小
		client->tail &= EVDEV_BUFFER_SIZE - 1;
		if (client->head == client->tail)
			wake_unlock(&client->wake_lock);
	}

	spin_unlock_irq(&client->buffer_lock);
	return have_event;
}

(1)函数功能就是从client->buffer中取出一个输入事件;
(2)是否有输入事件,以及从buffer缓冲区的哪个位置去取输入事件,都是client->head和client->tail觉得的,在向buffer放输入事件和取输入事件,都会操作这两个变量;

8、设备驱动层上报输入事件到事件处理驱动层

8.1、函数调用关系

input_event()
	input_handle_event()
		input_pass_event()
			handler->event()
				evdev_event()
					list_for_each_entry_rcu()
						evdev_pass_event()			

8.5、evdev_event()函数

static void evdev_event(struct input_handle *handle,
			unsigned int type, unsigned int code, int value)
{
	struct evdev *evdev = handle->private;
	struct evdev_client *client;
	struct input_event event;
	struct timespec ts;

	//给输入事件struct input_event结构体赋值
	ktime_get_ts(&ts);
	event.time.tv_sec = ts.tv_sec;
	event.time.tv_usec = ts.tv_nsec / NSEC_PER_USEC;
	event.type = type;
	event.code = code;
	event.value = value;

	rcu_read_lock();

	//判断是否有独占的独占句柄
	client = rcu_dereference(evdev->grab);
	if (client)
		evdev_pass_event(client, &event);
	else
		//变量evdev->client_list链表,把输入事件放到每个client的buffer缓冲区中
		list_for_each_entry_rcu(client, &evdev->client_list, node)
			evdev_pass_event(client, &event);

	rcu_read_unlock();
	wake_up_interruptible(&evdev->wait);
}

(1)根据传递的参数填充struct input_event结构体,这是一个完整的输入事件;
(2)把输入事件放到对应evdev上挂接的client的buffer缓冲区中;

8.6、evdev_pass_event()函数

static void evdev_pass_event(struct evdev_client *client,
			     struct input_event *event)
{
	spin_lock(&client->buffer_lock);
	wake_lock_timeout(&client->wake_lock, 5 * HZ);
	//将输入事件存放到buffer的client->head位置,client->head要加一
	client->buffer[client->head++] = *event;
	//client->head不能超过数组大小
	client->head &= EVDEV_BUFFER_SIZE - 1;
	
	spin_unlock(&client->buffer_lock);

	//如果是同步信号,则发生SIGIO信号,通知进程可以读数据
	if (event->type == EV_SYN)
		kill_fasync(&client->fasync, SIGIO, POLL_IN);
}

最后

以上就是能干眼神为你收集整理的linux输入子系统详解——看这一篇就够了1、输入子系统宏观介绍2、重点数据结构3、输入子系统核心层的实现4、通用事件处理驱动的实现5、输入子系统设备驱动层的实现6、设备驱动和事件处理驱动的匹配7、事件处理驱动层上报输入事件到应用层8、设备驱动层上报输入事件到事件处理驱动层的全部内容,希望文章能够帮你解决linux输入子系统详解——看这一篇就够了1、输入子系统宏观介绍2、重点数据结构3、输入子系统核心层的实现4、通用事件处理驱动的实现5、输入子系统设备驱动层的实现6、设备驱动和事件处理驱动的匹配7、事件处理驱动层上报输入事件到应用层8、设备驱动层上报输入事件到事件处理驱动层所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(44)

评论列表共有 0 条评论

立即
投稿
返回
顶部