Linux 驱动简述

设备

老生常谈,驱动设备分为:字符设备,块设备,网络设备.

其中字符设备最为常见,传输以字符为单位提供连续的数据流,应用程序可以顺序读取,

如: 键盘, 鼠标.

块设备多为存储设备: 硬盘.

网络设备是射频相关的硬件:wifi, BT等

Linux 驱动简述_第1张图片

驱动模型

Kobject和kset

Kobject:

个人理解中,kobject就像java中的object类(android中的context)一样拥有所有模块的基础特性.是内核运行过程中的基础.kobject结构图构成如下:

Linux 驱动简述_第2张图片

  1. name:该内核对象名称.加入后会出现在内核文件系统下.
  2. Entry:又来将一系列内核对象构成链表.
  3. Parent:父对象,用来构建层级关系.
  4. Kset:当前内核对象所属的kset集合.kset由一系列相同类型的kobject对象组成.
  5. Kref:该内核对象被引用的次数,由于最终生命周期.
  6. Ktype:该对象拥有的sysfs的相关属性.
  7. Sd:该对象在文件系统中对应的实例.
  8. State_initialized:标记该对象是否被初始化.
  9. State_in_sysfs:表示该对象在sysfs系统中是否有入口点.
  10. State_suppressed:用来表示时候向用户空间发送消息.

如下是字符设备的结构,可以看到kobject在其中:

Linux 驱动简述_第3张图片

Kset:

Kset是kobject的集合,由于本身也是内核对象,所以也要内嵌一个kobject.

Linux 驱动简述_第4张图片

总线

Linux 驱动简述_第5张图片

以上是总线结构体的主体部分,下面说明一下构成总线结构的参数意义.

  1. name总线名称.
  2. Match 函数指针,用来对挂载到总线上的设备进行匹配.相关的函数指针有(remove, probe, shutdown, suspend...).
  3. Pm电源管理相关的操作集(resume,suspend等)
  4. *p用来管理总线上设备和驱动的数据结构.

1)subsys表示该总线所在的子系统.

2)drivers_kset该总线上的驱动集合

3)devices_kset该总线上挂载的设备集合

4)Klist_devices,klist_drivers表示该总线上的设备驱动链表.

总线的属性:

总线属性代表着总线特有的属性,对应数据结构如下:

Linux 驱动简述_第6张图片

其中show和store分别用来显示和更改总线属性. 应当于cat操作相关

设备与驱动

Linux中设备和驱动通过总线绑定在一起,而总线的实现帮助忽略了复杂的底层细节进而提供了更友好的接口给驱动工程师.

通过device_register可以将设备注册到总线,并且会自动匹配相应的驱动程序.

通过driver_register可以将驱动程序注册到总线,并且会自动匹配相应的设备.

设备:

在设备的结构体中,包含了指向parent的device,绑定的总线bus,对应的driver(如果没有帮顶驱动为NULL),作为内核对象必不可少的kobject,name等.

在设备被加入系统时会经过如下过程:

Linux 驱动简述_第7张图片

将名为demodev设备号为251,从设备号为0并且dev->class为NULL的设备加入系统.下图中阴影部分为后加入设备的新增目录和文件.

Linux 驱动简述_第8张图片

驱动:

驱动的结构体如下所示

Linux 驱动简述_第9张图片

关于结构体内的属性,重点说一下probe和remove两个函数指针.

  1. probe为驱动实现的探测函数,在与device绑定的时候内核会尝试调用bus实现的probe函数,如果bus没有实现对应的函数进而调用驱动自身的probe函数.
  2. 同上,当内核试图卸载一个驱动时,会按照以上的流程调用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文件形式储存的,目标文件分为如下格式:

  1. 可重定位目标文件:二进制代码和数据,由各个数据节(section)构成,从地址0开始.
  2. 可执行目标文件:可运行的二进制代码和数据.
  3. 共享目标文件:一种特殊类型的可重定位目标文件,动态加载链接.

结构

Linux 驱动简述_第10张图片

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将用户空间对应的模块空间拷贝至内核中新申请的内存中.如下:

Linux 驱动简述_第11张图片

2.字符串表是ELF文件中的一个section,代表了各个section的符号名.这样为将来使用相关信息留下了接口,如下:

Linux 驱动简述_第12张图片

3.改写HDR,遍历section header table中每个entry,将s_addr指向对应的section所在地址.

4.Struct module初始化,通过find_sec函数查找“gnu.linkonce.this_module”的section,初始化mod指针.

Linux 驱动简述_第13张图片

驱动入口和出口函数,不过经过函数名称转换后,似乎不那么容易被找到这中间的过程.(这种转换保证了驱动程序的统一性,只要知道是这个样子就好)

在初始化之后,内核会遍历HDR中的每个section,将带有SHF_ALLOC(表示模块在运行过程中需要占用空间)的section分为两类:CORE和INIT(section name以INIT开头的为INIT).

在对内核模块划分之后,会调用vmalloc方法为它们重新分配空间,并且更新对应section的位置上,并将mod->module_core,mod->module_init记录基地址

之所以再次对部分section进行迁移,是因为在模块初始化结束时会释放掉HDR所在区域.并且初始化结束后INIT部分的资源也会被释放掉.最后存在的只是CORE部分的资源.

Linux 驱动简述_第14张图片

5. 重定位主要用于解决静态链接时的符号引用和动态链接时的符号地址不一致的情况.

使用EXPORT_SYMBOL的模块会单独生成一个特殊的section”rel_ksymtab”专门用于”ksymtab“的重定位.

6. 模块参数.可以在模块加载过程中传递参数如:insmod ***.ko dolphin=10 bobcat =5(不能在参数名和传入参数值之间不能存在空格).代码如下:

Linux 驱动简述_第15张图片

7. 版本控制,内核和模块之间通过一个4位的CRC校验码确认双方接口,如果校验码不匹配则为不同接口.内核和模块都需要在编译阶段启用CONFIG_MODVERSIONS宏来支持CRC功能.

8. Modinfo dev.ko可以查看模块信息.

Linux 驱动简述_第16张图片

9.Vermagic,内核加载时会检测模块中的vermagic和内核定义的是否一致(Linux 对可装载模块采取了两层验证:模块的 CRC 值校验和 vermagic 的检查.其中模块 CRC 值校验针对模块(内核)导出符号,是一种简单的 ABI(即 Application Binary Interface)一致性检查.而模块 vermagic(即Version Magic String)则保存了模块编译时的内核版本以及SMP等配置信息,当模块vermagic与主机信息不相符时亦将终止模块的加载).

10.Licence限制,为了避免污染内核,声明的licence需要本限制为如下所示:

Linux 驱动简述_第17张图片

sys_init_module

在上述过程之后,接下来加载驱动的init函数,模块状态更新为MODULE_STATE_GOING.

Linux 驱动简述_第18张图片

像前文所说,释放INIT section和HDR的空间.

呼叫模块通知链:内核通过单链表的形式,将关注同一事件的模块放入链表中.当该通知链关注的事件发生后回调相关函数.(这一实现很像android framework中brodcast receiver的实现机制).

Linux 驱动简述_第19张图片

卸载

  1. 模块的卸载过程比较简单,rmmod 模块名.
  2. 利用name找到要卸载的模块mod结构.
  3. 验证模块的关联关系(被别的模块依赖),有关联关系的模块不能被卸载.
  4. free_module.

字符设备

字符设备驱动由下方这个结构体承载,平时针对内核模块调用的read, write,open等操作最终会调用到下方结构体中的函数指针中。

Linux 驱动简述_第20张图片

内核中的抽象

Linux 驱动简述_第21张图片

现实中字符设备可能会更加复杂,进而内核提供的cdev无法满足要求.所以cdev往往会作为内嵌的结构体存在.

驱动程序进入init函数后,需要执行如下必要操作:

  1. 为cdev申请空间.
  2. 将cdev加入驱动设备链表中.
  3. 将cdev初始化,将kobj和ops进行填充.

Cdev的初始化显得比较直白:

Linux 驱动简述_第22张图片

注册

Cdev_add将设备加入probe结点中,该结点被放入cdev_map实现的哈希表中(主设备号作为键值).(Linux用一个32为的无符号整型表示设备号)

该数据结构模型如下所示:

Linux 驱动简述_第23张图片

对应的,删除结点的方法为cdev_del.将管理的设备从系统中移除.

打开

在用户空间调用open函数打开对应设备就可得到对应的文件描述符,但是上层的open函数和驱动中对应的.open函数指针之间存在一定的差异.下图展示了大体上的调用流程:

Linux 驱动简述_第24张图片

对于给定的文件描述符,在内核中对应一个file结构体.fd只是作为一个索引存在.

f_op是对应内核中实现的struct file_operation结构体对象.

f_flag用于记录文件打开方式.

f_count为使用计数.

private_data用于记录设备驱动程序自身定义的数据.

根据file结构体提供的信息,以后使用read,write,ioctl就可以直接调用到相应

调用open时,首先根据设备ID找到对应的设备inode.找到之后,会将inode中的ops指针赋值给f_op指针.如下图:

Linux 驱动简述_第25张图片

范例:

Linux 驱动简述_第26张图片

 

你可能感兴趣的:(Linux 驱动简述)