Linux input 子系统

早在几年前,我第一次翻开 Linux 0.11 源代码完全注释那本书的时候,我就折服于这神奇的计算机世界,其实这里面的知识不是有多难,而是沉淀了太久,让上面的人琢磨不透,这也是封装的弊端,总得有人知道轮子是怎么造出来的。

后来涉及到驱动的架构的时候,读了挺多的 PCI 架构的一些知识,当时是用打印机把相关代码打印出来,然后用笔写一些注释,但是看懂已经很吃力了,还得发到网上分享,其实是一件非常累的事情,所以我很佩服在网络上能分享知识的人,因为都是无偿的,和出书不同。

这几天又看了一些输入系统的相关的文章,特在此处写一下关键的部分,不然像之前阅读的代码那时,很多东西其实没有记下来,有点后悔。

驱动的初始化

设备驱动的特点

这里都是说一些源代码,还有自己的一些理解,其实不同的子系统(总线,USB),都有一个显著的特点,就是有俩个全局变量,分别是一条链把所有的设备连接,另外一条就连接所有的驱动,然后设备和驱动之间如果匹配成功(所谓匹配成功,以 PCI设备和驱动为例,PCI 设备在Rom区会标识自己的ID,及生产厂商等信息,驱动加载的时候也会提供这些信息,然后不管挂载新驱动还是设备,都会遍历所有的设备(如果是挂载设备,就遍历驱动),然后比对他们的ID是否相同,如果相同就会挂钩,挂钩就是指一些指针存储着对方的信息,这时候我们就叫匹配成功),就会有相关的指针存储对方的信息。

重点: 1. 程序有办法知道目前挂载的所有设备还有驱动 2. 驱动和设备都有一个相同的id用来辨识

如果有读者看过文件系统和超级块(vfs 实现),其实也有全局变量把它们串联在一起,目的就是方便遍历

input 子系统

了解上面这个特点,现在看 input,一个全局的 List 头,用来挂载所有的 输入设备,还有一个挂载的就是驱动,上面一个宏定义了内核能支持的最大输入设备数,另外还有一个表,用来快速获取对应的驱动,等下们可以看到它的用处。( linux -2.6.39 )

#define INPUT_DEVICES    256

static LIST_HEAD(input_dev_list);
static LIST_HEAD(input_handler_list);

static struct input_handler *input_table[8];

下面来看input子系统的初始化

关键在,注册一个字符设备,它的设备号就是 13(INPUT_MAJOR),设备号分为主次设备号的原因就是分类,其实本质就是一个数字,多嘴一句,如何通过设备号找到对应的设备很简单,就是一个映射的表,最简单的办法就是创建一个数组大小就是 设备号的最大值,里面存放设备的地址,但是这样太浪费了,所以我们就用数组加链表,本质其实还是一样的。

这里还很多谜团没有解开,比如 open 函数的作用(参考块设备的open函数也不难猜到),还有input设备/驱动的注册

input设备的注册

这个函数是内核暴露出来的驱动接口,供驱动模块调用,这里省略了错误的判断,为了更直观的显示它的作用。

有一点读者肯定很好奇,为什么 minor 要除以32,得到了它的位置,也就是设备号的低5位是被忽略了,也就是说 一个输入驱动(input handler),可以处理32个设备。这里注意的 input handler 和 input handle 是俩个结构,最开始我也弄错了

所有的设计都是为了方便操作,驱动里面的这些函数大部分会被底层的设备调用,我们先以具体的一个输入设备来举例子。

evdev 事件设备

id_table 就是我们上面所说的和设备匹配的一个表,注释里说明了一个事实,就是所有的输入设备都会和这个evdev的驱动匹配

所以最后会调用,evdev的 connect 函数

也是省略了部分出错的判断,我们来看看关键的几个地方

  • 首先,自己计算一个minor,在自己管理的32个minor的基础上

  • 分配一个新的,evdev结构,初始化(包括了设备初始化)

  • 注册一个新的 handle 注意不是handler

  • 存储 evdev,就是放在一个数组中,方便索引

注释很清楚啦, input_handle 就是连接 handler 和 input device 一个媒介,这里就是添加其到相应的设备链表上。

对应关系,来源网络

换言之,只要是输入设备,一定会与 evdev 的驱动匹配,从而创建一个 evdev设备(名字 event%d(0~31)),其父设备就是 Input device,然后 evdev 的 minor 是计算出来的,只要数组里的元素没有赋值,那就是可用的

具体的 input device 例子 - usb kbd

USB 键盘下接 USB总线,这里我先忽略,上接 input 子系统,我们关注它的这一部分

usb 特有的部分都可以忽略,关键在于分配了一个 input device 并注册了它,这就是关键了,联系上面,这个设备肯定会和 evdev 驱动匹配,(intput_register_device 同样会遍历驱动的链表,逐项比对id,最终连接双方),然后建立一个新的 evdev 设备,分配一个 Input handle 结构,连接驱动和现在分配的这个设备。

然后,当我们按下按键的时候,就会触发中断,最终来到这里。这里我们还可以发现,input device 也有自己open close 它们会在某个地方被调用,先卖个关子。

有一些按键的继续按下,这个我们可以忽略,关键在于调用了一个 input_report_key

这个函数读者肯定都可以猜到,就是遍历设备里的那个监听链表,我们在注册handle的时候放在了设备和驱动那儿,而且对于有 filter 的设备还是放在头部的,就是为了快速处理

调用 handler的 event 函数,最终就会来到, evdev_event()

evdev 又会和监听它的上层挂钩,这里可以看出, evdev的存在就是监听所有的 Input device,每个设备会有自己的驱动,但是 evdev 就像是网卡的混淆模式,可以监听到所有的包,它可以监听到所有input device可传递的事件。

为什么要注册一个字符设备

最开始在子系统初始化的过程中,我们看到注册了一个主设备号是 INPUT_MAJOR 的一个字符设备,并给它提供了一个文件操作函数,到目前为止我们都没有解释它存在的意义,注意,我们之前说的注册输入设备,本质就是挂载到了那个全局的链表,如果存在和它匹配的驱动,当然 evdev 驱动必然匹配,然后还会创建一个新的 evdev 设备,但是注意一点,input device 设备并没有被打开。即在那时,它提供的 open 函数并没有被调用。

注意到,这些设备都被认为是字符设备,但是我们并没有调用 register_chardev ,读者估计已经猜到了,肯定就是为了和input初始化那里对接上,试想想,我们给设备认为是字符设备,最终内核就会根据它的 MAJOR 和 MINOR找到最合适的一个 fops即文件操作函数,这个关键就是 kobj_map 函数,其实就是一个映射表。总之,这些 input device,包括 evdev device,都是拥有相同的major,所以当用户打开这个设备的时候,就会调用到初始化的那个open函数。

当用户第一次调用 open("event0", RW) 的时候,因为它是字符设备,同时 major 又跟 最开始注册 input设备一样,所以内核认为input设备注册是 fops 就是能适用,所以调用。

上面的代码不难理解,就是调用了handler的open函数,对于 evdev,它的open即为

这里读者知道,刚才再说 usb 键盘驱动那里我们卖了一关子,usb 设备对 input 设备注册了自己 open 函数,但是没说哪里调用。

这里设备的 open 函数,以 刚才 usb 键盘为例,就会来到 usb_kbd_open 了,这里与 USB 子系统有关,就不继续分析了。

Summary

最后我们总结一下,最开始 input 设备注册的函数,只是一个分发器,为的就是调用它们的handler->open ,因为每个handler可能有自己自带的设备,注意,以 evdev 为例,它注册了一个新的设备号拥有不同的 minor, 名称为 event%d,但是它的父设备,又是与这个 handler 匹配成功的 input device,这里有点绕,但是必须理解清楚。

evdev 的handler 最终调用了 input device 的 open 函数,其实内核的代码注释里面也说了,这里直接引用吧。

"

"

这是一种懒惰的思想,只有当驱动想要使用设备,也就是上层需要读取设备信息的时候,我们才打开设备,目的就是为了节省系统资源。

这篇文章得理解一下几点

  • 为什么要注册那个字符设备

  • 为什么字符设备的open函数可以最终到达目标输入设备的open

  • 驱动的特点,即俩个链表,还有匹配的特点

  • 设备信息如何传递到监听它的 handler(利用设备的 h_list)

  • evdev 可以自由分配自己的 32 个minor

Last updated