老生常谈,驱动设备分为:字符设备,块设备,网络设备.
其中字符设备最为常见,传输以字符为单位提供连续的数据流,应用程序可以顺序读取,
如: 键盘, 鼠标.
块设备多为存储设备: 硬盘.
网络设备是射频相关的硬件:wifi, BT等
Kobject:
个人理解中,kobject就像java中的object类(android中的context)一样拥有所有模块的基础特性.是内核运行过程中的基础.kobject结构图构成如下:
如下是字符设备的结构,可以看到kobject在其中:
Kset:
Kset是kobject的集合,由于本身也是内核对象,所以也要内嵌一个kobject.
以上是总线结构体的主体部分,下面说明一下构成总线结构的参数意义.
1)subsys表示该总线所在的子系统.
2)drivers_kset该总线上的驱动集合
3)devices_kset该总线上挂载的设备集合
4)Klist_devices,klist_drivers表示该总线上的设备驱动链表.
总线的属性:
总线属性代表着总线特有的属性,对应数据结构如下:
其中show和store分别用来显示和更改总线属性. 应当于cat操作相关
Linux中设备和驱动通过总线绑定在一起,而总线的实现帮助忽略了复杂的底层细节进而提供了更友好的接口给驱动工程师.
通过device_register可以将设备注册到总线,并且会自动匹配相应的驱动程序.
通过driver_register可以将驱动程序注册到总线,并且会自动匹配相应的设备.
设备:
在设备的结构体中,包含了指向parent的device,绑定的总线bus,对应的driver(如果没有帮顶驱动为NULL),作为内核对象必不可少的kobject,name等.
在设备被加入系统时会经过如下过程:
将名为demodev设备号为251,从设备号为0并且dev->class为NULL的设备加入系统.下图中阴影部分为后加入设备的新增目录和文件.
驱动:
驱动的结构体如下所示
关于结构体内的属性,重点说一下probe和remove两个函数指针.
在驱动程序加入系统时会首先确认该驱动没有被注册过,然后再加入系统.如果auto_probe标志为1,则会自动调用match确认是否匹配,之后调用probe函数进行和设备的绑定.
Class:
Class是Linux中更具抽象性的概念,作为具有相同功能设备的一个容器.一个类是一个设备的高级视图, 它抽象出低级的实现细节. 驱动可以见到一个SCSI 磁盘或者一个 ATA 磁盘, 在类的级别, 它们都是磁盘. 类允许用户空间基于它们做什么来使用设备, 而不是它们如何被连接或者它们如何工作.
几乎所有的类都在 sysfs 中在 /sys/class 下出现. 因此, 例如, 所有的网络接口可在 /sys/class/net 下发现, 不管接口类型. 输入设备可在 /sys/class/input 下, 以及串行设备在 /sys/class/tty.
驱动模块***.ko是以ELF文件形式储存的,目标文件分为如下格式:
ELF header:
elf_type:表明文件类型,驱动模块为1,表明是一个可定位的文件.
e_shoff: section header table自己在文件中的偏移量.
E_shentsize: section header table每个entry的大小.
E_shnum: section header table中entry的个数.
E_shstrndx: 与section header entry中的sh_name一起指明对应的section name.
Section:
ELF文件的主体,位于文件视图的一块连续区域中,但在加载后会根据各自属性分配到新内存中.
Section header table:
位于ELF文件末尾,由section header entry组成且每个entry拥有相同的数据结构.
Sh_addr:表示entry在内存中对应的实际地址.在静态文件视图中为0,加载后会被修改未section在内存中的实际地址.
Sh_offset:对应的section在文件中的偏移量.
Sh_size:表明对应的section在文件中的大小.SHNT_NOBIT类型的section除外,不占空间.
Sh_entsize:主要用于由固定数量entry组成的表所构成的section.
驱动并不会使用这些数据,以上数据是由内核加载器在加载模块时使用.
Insmod 1.ko ->load file in memory->sys_init_module(1.ko, 文件大小, 参数地址)->load_module(1.ko, 文件大小, 参数地址)
Struct module是内核用来管理加载模块是非常重要的数据结构,代表了内核模块在现实中的抽象.
参数介绍:
enum module_state{
MODULE_STATE_LIVE, 模块成功加载入系统的状态
MODULE_STATE_COMING, 模块加载中
MODULE_STATE_GOING, 模块卸载中
MODULE_STATE_UNFORMED 模块不可用
}
Struct list_head list 内核中所有成功加载模块的链表集合
Char name[] 模块名称
Int(init *)(void) 内核模块初始化函数指针
// 用于在内核模块间建立依赖关系
Struct list_head source_list
Struct list_head target_list
load_module
ELF文件加载过程:
1.首先通过insmod加载对应模块至用户空间,后调用sys_init_module进入内核态,使用vmalloc申请一块匹配模块大小的内存,调用copy_from_user将用户空间对应的模块空间拷贝至内核中新申请的内存中.如下:
2.字符串表是ELF文件中的一个section,代表了各个section的符号名.这样为将来使用相关信息留下了接口,如下:
3.改写HDR,遍历section header table中每个entry,将s_addr指向对应的section所在地址.
4.Struct module初始化,通过find_sec函数查找“gnu.linkonce.this_module”的section,初始化mod指针.
驱动入口和出口函数,不过经过函数名称转换后,似乎不那么容易被找到这中间的过程.(这种转换保证了驱动程序的统一性,只要知道是这个样子就好)
在初始化之后,内核会遍历HDR中的每个section,将带有SHF_ALLOC(表示模块在运行过程中需要占用空间)的section分为两类:CORE和INIT(section name以INIT开头的为INIT).
在对内核模块划分之后,会调用vmalloc方法为它们重新分配空间,并且更新对应section的位置上,并将mod->module_core,mod->module_init记录基地址
之所以再次对部分section进行迁移,是因为在模块初始化结束时会释放掉HDR所在区域.并且初始化结束后INIT部分的资源也会被释放掉.最后存在的只是CORE部分的资源.
5. 重定位主要用于解决静态链接时的符号引用和动态链接时的符号地址不一致的情况.
使用EXPORT_SYMBOL的模块会单独生成一个特殊的section”rel_ksymtab”专门用于”ksymtab“的重定位.
6. 模块参数.可以在模块加载过程中传递参数如:insmod ***.ko dolphin=10 bobcat =5(不能在参数名和传入参数值之间不能存在空格).代码如下:
7. 版本控制,内核和模块之间通过一个4位的CRC校验码确认双方接口,如果校验码不匹配则为不同接口.内核和模块都需要在编译阶段启用CONFIG_MODVERSIONS宏来支持CRC功能.
8. Modinfo dev.ko可以查看模块信息.
9.Vermagic,内核加载时会检测模块中的vermagic和内核定义的是否一致(Linux 对可装载模块采取了两层验证:模块的 CRC 值校验和 vermagic 的检查.其中模块 CRC 值校验针对模块(内核)导出符号,是一种简单的 ABI(即 Application Binary Interface)一致性检查.而模块 vermagic(即Version Magic String)则保存了模块编译时的内核版本以及SMP等配置信息,当模块vermagic与主机信息不相符时亦将终止模块的加载).
10.Licence限制,为了避免污染内核,声明的licence需要本限制为如下所示:
sys_init_module
在上述过程之后,接下来加载驱动的init函数,模块状态更新为MODULE_STATE_GOING.
像前文所说,释放INIT section和HDR的空间.
呼叫模块通知链:内核通过单链表的形式,将关注同一事件的模块放入链表中.当该通知链关注的事件发生后回调相关函数.(这一实现很像android framework中brodcast receiver的实现机制).
字符设备驱动由下方这个结构体承载,平时针对内核模块调用的read, write,open等操作最终会调用到下方结构体中的函数指针中。
现实中字符设备可能会更加复杂,进而内核提供的cdev无法满足要求.所以cdev往往会作为内嵌的结构体存在.
驱动程序进入init函数后,需要执行如下必要操作:
Cdev的初始化显得比较直白:
Cdev_add将设备加入probe结点中,该结点被放入cdev_map实现的哈希表中(主设备号作为键值).(Linux用一个32为的无符号整型表示设备号)
该数据结构模型如下所示:
对应的,删除结点的方法为cdev_del.将管理的设备从系统中移除.
在用户空间调用open函数打开对应设备就可得到对应的文件描述符,但是上层的open函数和驱动中对应的.open函数指针之间存在一定的差异.下图展示了大体上的调用流程:
对于给定的文件描述符,在内核中对应一个file结构体.fd只是作为一个索引存在.
f_op是对应内核中实现的struct file_operation结构体对象.
f_flag用于记录文件打开方式.
f_count为使用计数.
private_data用于记录设备驱动程序自身定义的数据.
根据file结构体提供的信息,以后使用read,write,ioctl就可以直接调用到相应
调用open时,首先根据设备ID找到对应的设备inode.找到之后,会将inode中的ops指针赋值给f_op指针.如下图:
范例: