Linux驱动(基础):10---内核模块程序结构(加载、卸载、参数、许可声明、导出符号、声明信息)

一个Linux内核模块主要由如下几个部分组成:

  • ①模块加载函数:
    • 当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作
  • ②模块卸载函数:
    • 当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功能
  • ③模块许可证声明:
    • 许可证(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到内核被污染(Kernel Tainted)的警告
    • 在Linux内核模块领域,可接受的LICENSE包括“GPL” 、 “GPL v2” 、 “GPL and additional rights” 、 “Dual BSD/GPL” 、 “Dual MPL/GPL”和“Proprietary”(关于模块是否可以采用非GPL许可权,如“Proprietary” ,这个在学术界和法律界都有争议)
    • 大多数情况下,内核模块应遵循GPL兼容许可权。Linux内核模块最常见的是以MODULE_LICENSE(“GPL v2”)语句声明模块采用GPL v2
  • ④模块参数(可选):
    • 模块参数是模块被加载的时候可以传递给它的值,它本身对应模块内部的全局变量
  • ⑤模块导出符号(可选):
    • 内核模块可以导出的符号(symbol,对应于函数或变量),若导出,其他模块则可以使用本模块中的变量或函数
  • ⑥模块作者等信息声明(可选)

必备头文件

#include 
#include 
  • module.h:包含有装载模块需要的大量符号和函数的定义
  • init.h:用来指定初始化和清除函数

一、模块的加载(module_init宏、__initdata变量)

  • 当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作

module_init宏

  • 用来指定一个加载初始化函数
  • 参数:为初始化函数的名称

初始化函数格式

static int __init 函数名();
  • 参数可选
  • 返回值:
    • 成功:返回0
    • 失败:返回错误编码
  • static(可选参数)为了防止与系统中的其他函数名称冲突,使用static将函数限制于在本.c文件内有效
  • __init(可选参数):用来暗示内核,表名该函数仅在初始化期间使用。在内核被装载之后,模块装载器会将初始化函数扔掉,并且将函数中占用的内存释放出来。__init标记的函数会被编译器放在特殊的ELF段

init区段

  • 在Linux,所有标识为__init的函数如果直接编译进入内核(成为内核镜像的一部分),在连接的时候都会放在.init.text这个区段内。下面是__init的宏定义:
#define __init __attribute__ ((__section__ (".init.text")))
  • 所有的__init函数在区段.initcall.init中还保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些__init函数,并在初始化完成后,释放init区段(包括.init.text、.initcall.init等)的内存

__initdata变量

  • 什么是初始化阶段的变量:对于只是初始化阶段需要的数据,内核在初始化完后,会释放它们占用的内存
  • 格式:带上__initdata关键字,__initdata与__init的原理一样,暗示内核此变量为模块初始化时使用
  • 与__init初始化函数一样,该变量会被放入特殊的ELF段
  • 例如:下面的代码将hello_data定义为__initdata
static int hello_data __initdata = 1;
static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, world %d\n",hello_data);
    return 0;
}
module_init(hello_init);

初始化过程中的错误处理

初始化过程中错误处理的两种情景:

  • ①如果注册设施时遇到任何错误,首先要判断模块是否可以继续初始化。通常,在某个注册失败后可以通过降低功能来继续运转。因此,只要可能,模块应该继续向前并尽可能提供其功能
  • ②如果发生了某个特定类型的错误之后无法继续装载模块,则要将出错之前的任何注册工作撤销掉。Linux么有记录每个模块都注册了哪些设施,因此,当模块的初始化出现错误之后,模块必须自行撤销已注册的设施(如果没有撤销,则内核会处于一种不稳定的状态,这是因为内核中包含了一些指向并不存在的代码的内部指针,在这种情况下,唯一有效的解决办法是重新引导系统)

撤销工作的两种处理方式

  • ①使用goo语句:在错误处理时,goto语句处理错误是非常有用的,可以避免大量复杂的、高度缩进的“结构化”逻辑。例如下面的代码准备注册三个(虚构的)设置,在出错的时候使用goo语句只撤销出错时刻以前所成功注册的设施

Linux驱动(基础):10---内核模块程序结构(加载、卸载、参数、许可声明、导出符号、声明信息)_第1张图片

  • ②调用模块的清除函数:另一个观点是不建议使用goto语句,而是记录任何成功注册的设施,然后在出错时调用模块的清除函数,清除函数仅回滚已成功完成的步骤,清除函数需要撤销初始化函数所注册的所有设施,并且习惯上(不是必须的)以相反于注册的顺序撤销设施(然而,这种方法需要更多的代码和CPU时间,因此在追求效率的代码中使用goto语句更好)

Linux驱动(基础):10---内核模块程序结构(加载、卸载、参数、许可声明、导出符号、声明信息)_第2张图片

  • ③从初始化函数中调用清除函数:每当发生错误时从初始化函数中调用清除函数,这种方法将减少代码的重复并且使代码更清晰(因为清除函数被非退出代码调用,因此不能将清除函数标记为__exit)

Linux驱动(基础):10---内核模块程序结构(加载、卸载、参数、许可声明、导出符号、声明信息)_第3张图片

模块装载竞争

  • 此处我们简答的介绍模块装载中的一个重要方面:“竞态”。会在后面文章中进一步讨论竞态问题
  • 要铭记:
    • 在注册完成之后,内核的某些部分可能会立即使用我们刚刚注册的任何设施。换句话说,在初始化函数还在运行的时候,内核就完全可能会调用我们的模块。因此,在首次注册完成之后,代码就应该准备好被内核的其他部分调用;再用来支持某个设施的所有内部初始化完成之前,不要注册任何设施
    • 还要考虑,当初始化失败而内核的某些部分已经使用了模块所注册的某个设施时应如何处理。如果这种情况可能发生在模块上,则根本不应该初始化失败的情况,毕竟模块已经成功导出了可用的功能及符号。如果初始化一定要失败,则应该仔细处理内核其他部分正在进行的操作,并且要等待这些操作的完成

二、模块内加载其他内核模块(request_module宏)

request_module宏

  • 在Linux内核中,可以使用request_module(const char*fmt,…)函数加载内核模块,从而灵活地加载其他内核模块
  • 参数:加载其他内核模块的命令参数

三、模块的卸载(module_exit宏、__exitdata变量)

  • 当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功 能

module_exit宏

  • 用来指定一个卸载清除函数,向系统中返回所有的资源
  • 参数:为卸载清除函数的名称

卸载函数格式

static void __exit 函数名();
  • 无返回值参数可选
  • static(可选参数)为了防止与系统中的其他函数名称冲突,使用static将函数限制于在本.c文件内有效
  • __exit(可选参数):用来暗示内核该函数用来模块的卸载,__exit标记的函数会被编译器会将该函数放在特殊的ELF段中

卸载函数的注意事项

  • ①如果模块是被直接编译内嵌到内核中(即built-in),或者内核的配置不允许卸载模块,则标记为__exit的函数将被丢弃(则该函数不会被链接进最后的镜像),因此我们的卸载函数就不会被执行(既然不会被执行,我们的卸载函数也就可以省略了)
  • ②接上,我们的__exit函数只能在模块被允许卸载或者系统关闭时才会被调用
  • ③如果一个模块未定义清除函数,则内核不允许卸载该模块

__exitdata变量

  • 只是退出阶段采用的数据也可以用__exitdata来形容
  • 与_exit退出函数一样,该变量会被放入特殊的ELF段
  • 例如下面的代码:
static int hello_data __exitdata = 1;
static void _ _exit cleanup_function(void)
{
    printk(KERN_INFO "exit: %d\n",hello_data);
}
module_exit(cleanup_function);

四、模块参数(module_param宏、module_param_array宏)

模块参数介绍

  • 有一些内核模块需要使用参数来控制驱动程序的操作方式,那么在装载内核模块时,用户可以向模块传递参数,这些参数对应于内核代码中的变量
  • 参数的值可以在运行insmod或modprobe命令时指定,modprobe命令还可以从它的配置目录/ec/modprob.conf的文件中读取参数值

module_param宏

  • 当参数的类型不是数组时使用这个函数进行传参
  • 这个宏不能定义在函数中,通常定义在源文件的头部
  • 格式如下:
#include 
module_param(参数名,参数类型,参数读/写权限)
  • 参数1:参数的名称,对应于代码内的变量
  • 参数2:参数的类型
    • 参数类型可以是byte、short、ushort、int、uint、long、ulong、charp(字符指针)、bool或invbool(布尔的反)
    • 在模块被编译时会将module_param中声明的类型与变量定义的类型进行比较,判断是否一致
  • 参数3:sysfs入口项的访问许可值掩码,用来控制谁能访问sysfs中对模块参数的表述,取值如下
    • 如果设置为0:就不会有对应的sysfs入口项
    • 如果为S_IRUGO:则任何人均可读取该参数,但不能修改
    • 如果为S_IRUGO|S_IWUSR:允许root用户修改该参数
    • 注意事项:如果一个参数通过sysfs而被修改,则如同模块修改了这个参数的值一样,但是内核不会以任何方式通知模块。大多数情况下,我们不应该让模块参数是可写的,除非我们打算检测这种修改并作出相应的动作

module_param_array宏

  • 当参数的类型是数组时使用这个函数进行传参
  • 这个宏不能定义在函数中,通常定义在源文件的头部
  • 格式如下:
#include 
module_param_array(数组名,数组类型,数组长,参数读/写权限)
  • 参数1:数组名
  • 参数2:数组类型
    • 在模块被编译时会将module_param_array中声明的类型与数组定义的类型进行比较,判断是否一致
  • 参数3:数组长
  • 参数4:与module_param宏相同,见上

参数传递方式

  • ①insmode(或modprobe)   参数名1=参数值  参数名2=参数值参数名.....
  • ②如果模块被内置了,就无法insmod了,但是bootloader可以通过在bootargs里设置“模块名.参数名=值”的形式给该内置的模块传递参数
  • ③如果不传递参数,参数将使用模块内定义的缺省值

/sys/module/<模块名>/parameters/目录

  • 我们知道当我们的模块被加载到内核中之后,会在/sys/module目录下建立一个与该模块名相同的目录
  • 如果我们的模块可以传递参数,那么就会在/sys/module/<模块名>/parameters/目录下建立与模块参数名相同的文件,我们的参数值就会保存在这些文件中
  • 例如:下面演示案例中我们的hello.ko内核模块中有两个参数变量(book_name和book_num),那么在/sys/module/<模块名>/parameters/目录下就存在这两个参数的文件

  • 我们可以查看该文件从而获取到内核在加载时参数所使用的值

演示案例

//hello.c

#include 
#include 

static char *book_name = "dissecting Linux Device Driver";
module_param(book_name,charp,S_IRUGO);

static int book_num = 4000;
module_param(book_num,int,S_IRUGO);

static int __init book_init(void)
{
    printk(KERN_INFO "book name:%s\n", book_name);
    printk(KERN_INFO "book num:%d\n", book_num);
    return 0;
}
module_init(book_init);

static void __exit book_exit(void)
{
    printk(KERN_INFO "book module exit\n ");
}
module_exit(book_exit);

MODULE_AUTHOR("CSDN-dongshao");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("A simple Module for testing module params");
MODULE_VERSION("V1.0");
  • 如果加载模块时不传递任何参数,则参数使用代码内的默认值。接着运行dmesg命令查看内核的输出信息

  • 我们卸载上面的hello.ko模块。接着加载带有参数的hello.ko模块,则代码内的参数变量会使用参数传递的值,并且dmesg命令可以看到打印到了我们传递的参数值(备注:最上面那条是上次模块退出打印的缓冲信息)

五、导出符号(EXPORT_SYMBOL、EXPORT_SYMBOL_GPL)

内核符号表介绍

  • 公共内核符号表包含了所有的全局内核项(即函数和变量)的地址,这是实现模块化驱动程序所必需的
  • 模块导出的任何符号都会变成内核符号表的一部分,这样其他的模块就可以使用我们模块导出的符号。例如:msdos文件熊依赖于由fat模块导出的符号;每个USB输入设备模块层叠在usbcore和input模块之上
  • Linux的“/proc/kallsyms”文件对应着内核符号表,它记录了符号以及符号所在的内存地址
  • 模块层叠技术:如果以设备驱动程序的形式实现一个新的软件抽象,则可以为硬件相关的实现提供一个“插头”。例如:video-for-linux驱动程序组划分出一个通用模块,它导出的符号表可供下层与具体硬件相关的驱动程序使用。根据安装的硬件的不同,我们加载通用的video模块以及与具体硬件相关的特定模块。另外,并口支持以及大量可插拔设备的处理(比如USB内核子系统)都使用了类似的层叠方法。通过层叠技术,可以将模块划分为多个层,通过简化每个层可缩短开发时间。下图给出了并口子系统中的层叠方式,箭头显示了模块之间以及和内核编程接口之间的通信情况

Linux驱动(基础):10---内核模块程序结构(加载、卸载、参数、许可声明、导出符号、声明信息)_第4张图片

导出符号宏

  • 模块可以使用如下两个宏导出符号到内核符号表中
  • EXPORT_SYMBOL_GPL宏:只适用于包含GPL许可权的模块
  • 这两个宏不能在函数中使用,必须在全局部分使用:这是因为这两个宏将被扩展为一个特殊变量的声明,而该变量必须是全局的。该变量将在模块可执行文件的特殊部分(“ELF”段)中保存。在装载时,内核通过这个段来寻找模块导出的变量(可以查阅获得更详细的信息)
  • GPL版本使得要导出的模块只能被GPL许可证下的模块所使用,附加文章见:https://blog.csdn.net/qq_41453285/article/details/102789822
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);

演示案例

  • 下面代码给出了一个导出整数加、减运算函数符号的内核模块的例子
#include                                 
#include                                 
MODULE_LICENSE("GPL v2");                                
                                
int add_integar(int a,int b)                                
{                                
	return a+b;                             
} 
    
int sub_integar(int a,int b)                                
{                                
	return a-b;                             
}                            

EXPORT_SYMBOL_GPL(add_integar);
EXPORT_SYMBOL_GPL(sub_integar);
  • 从“/proc/kallsyms”文件中找出add_integar、sub_integar的相关信息:

Linux驱动(基础):10---内核模块程序结构(加载、卸载、参数、许可声明、导出符号、声明信息)_第5张图片

六、模块的许可声明(MODULE_LICENSE宏)

MODULE_LICENSE(license);
  • 该声明不能出现在函数内,一般放置在文件的最后

  • 内核能够识别的许可证有:“GPL(任一版本的GNU通用公共许可证)”、“GPL v2(GPL版本2)”、“GPL and addittional rights(GPL及附加权利)”、“Dual BSD/GPL(BSD/GPL双重许可证)”、“Dual MPL/GPL(MPL/GPL双重许可证)”、“Proprietary(专有)”

  • Linux 是以 GNU 通用公共版权( GPL )的版本2作为许可的,它来自自由软件基金的GNU项目。如果你想读这个许可证,你能够在你的系统中几个地方发现它,包括你的内核源码树的目录中的 COPYING 文件(见下图)

Linux驱动(基础):10---内核模块程序结构(加载、卸载、参数、许可声明、导出符号、声明信息)_第6张图片

  • 为了符合Linux模块的编写,我们通常使用MODULE_LICENSE为内核模块声明 “GPL v2” ,见下图:
MODULE_LICENSE("GPL v2");

七、模块的描述

  • 我们可以使用下面的宏声明一些模块的信息:
    • MODULE_AUTHOR:模块的作者
    • MODULE_DESCRIPTION:模块的简短描述
    • MODULE_ALIAS:模块的别名
    • MODULE_VERSION:代码修订号/版本;有关版本字符串的创建惯例,参考中的注释
    • MODULE_DEVICE_TABLE:用来告诉用户空间模块所支持的设备(对于USB、PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE,以表明该驱动模块所支持 的设备)
  • 这些声明不能出现在函数内,一般放置在文件的最后
MODULE_AUTHOR(author);

MODULE_DESCRIPTION(description);

MODULE_VERSION(version_string);

MODULE_DEVICE_TABLE(table_info);

MODULE_ALIAS(alternate_name);

演示案例

  • 例如下面的代码列出驱动所支持的设备列表
/* table of devices that work with this driver */
static struct usb_device_id skel_table [] = {
{    
    USB_DEVICE(USB_SKEL_VENDOR_ID,
    USB_SKEL_PRODUCT_ID) },
    { } /* terminating enttry */
};

MODULE_DEVICE_TABLE (usb,skel_table);

 

你可能感兴趣的:(Linux驱动(基础))