参考:《Linux内核驱动开发入门与实战》
1 驱动模块的组成:
一个驱动模块主要由如下部分组成,如图下图所示。这是一个规范的驱动模块应该包含的结构。这些结构在图中的顺序也是在源文件中的顺序。
头文件(可选) |
模块参数(可选) |
模块功能函数(可选) |
其他(可选) |
模块加载函数(必须) |
模块卸载函数(必须) |
模块许可声明(必须) |
1.1 头文件:
驱动模块会使用内核中的许多函数,所以要包含必要的头文件。有两个头文件是所有驱动模块都必须包含的。这两个头文件是:
#include <linux/module.h>
#include <linux/init.h>
module.h文件包含了加载模块时需要使用的大量符号和函数定义。
Init.h文件包含了模块加载函数和模块释放函数的宏定义。
1.2 模块参数
模块参数是驱动模块加载时,需要传递给驱动模块的参数。如果一个驱动模块需要完成两种功能,那么就可以通过模块参数选择使用哪一种功能。
模块参数定义:
static long a = 1;
module_param(a, long, S_IRUGO);
使用:insmod xxx.ko a = 1;
1.3 模块加载函数
模块加载函数是模块加载时,需要执行的函数。
module_init(test_init);
1.4 模块卸载函数
模块卸载函数是模块卸载时,需要执行的函数。
module_exit(test_exit);
1.5 模块许可声明
模块许可声明表示模块受内核支持的程度。有许可权的模块会更受开发人员的重视。需要使用MODULE_LICENSE表示模块的许可权限。内核可以识别的许可权限如下:
如果一个模块没有包含许可权,那么就会认为是不符合规范的。这时,内核加载这种模块时,会收到内核加载了一个非标准模块的警告。
2 模块参数和模块之间的通信
为了增加模块的灵活性,可以给模块添加参数。模块参数可以控制模块的内部逻辑,从而使模块可以在不同的情况下完成不同的功能。
2.1 模块参数
2.1.1 用户空间的应用程序可以接受用户的参数,设备驱动程序有时候也需要接受参数。例如一个模块可以实现两种相似的功能,这是可以传递一个参数到驱动模块,以决定其使用哪一种功能。参数需要在加载模块时指定,例如insmod xxx.ko param=1。
2.1.2可以用“mdule_param(参数名,参数数据类型,参数读写权限)”来为模块定义参数。例如下列代码定义了一个长整型和整形参数:
static long a = 1;
static int b = 1;
module_param(a, long, S_IRUGO);
module_param(b, int, S_IRUGO);
参数的数据类型可以是byte、short、ushort、int、uint、long、ulong、bool、charp(字符指针类型)。模块参数的类型中没有浮点类型。这是因为内核并不完美支持浮点数操作。在内核中使用浮点数时,除了要人工保存和恢复浮点寄存器外还有一些琐碎的事情要做。printk()函数也不支持浮点类型。
3 模块的文件格式ELF
了解模块以何种格式存储在硬盘中,对于理解模块间怎样通信是非常有必要的。使用flie命令可以知道hello.ko模块使用的是ELF文件格式,命令如下:
ELF文件的基本结构:
ELF Header头位于文件的最前部。其包含了描述整个文件的基本属性,例如ELF文件版本、目标机器型号、程序入口地址等。
.text表示代码段,存放文件的代码部分。
.data表示数据段,存放已经初始化的数据等。
.Section Table表描述了ELF文件包含的所有段的信息,例如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。
.symtab表示符号表。符号表是一种映射函数到真实内存地址的数据结构。其就像一个字典,记录了在编译阶段,无法确定地址的函数。该符号表将在模块文件加载阶段,由系统赋予真实的内存地址。
4 模块之间的通信
模块是为了完成某种特定任务而设计的。其功能比较单一,为了丰富系统的功能,模块之间常常进行通信。他们之间可以共享变量,数据结构,也可以调用对方提供的功能函数。
下图描述了模块1是怎样调用模块2的功能函数的。
模块2的加载过程如下所示:
1)使用insmod命令加载模块2.ko。
2)内核为为模块2分配空间,然后将模块的代码和数据装入分配的内存中。
3)内核发现符号表中有函数1,函数2可以导出,于是将其内存地址记录在内核符号表中。
模块1在加载进内核时,系统会执行以下操作:
1)insmod命令为内核分配空间,然后将模块的代码和数据装入内存中。
2)内核在模块1的符号表(symtab)中发现一些未解析的函数。这些未解析的函数是“函数1”,“函数2”,这些函数位于模块2的代码中。所以模块1会通过内核符号表,查到相应的函数,并将函数地址填到模块1的符号表中。
通过模块1加载的过程后,模块1可以使用模块2提供的“函数1”和“函数2”了。
下一节《模块之间的通信实例》