LV06-02-内核模块-05-内核模块框架解析
本文主要是内核模块——内核模块框架解析的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
PC端开发环境 | Windows | Windows11 |
Ubuntu | Ubuntu20.04.2的64位版本 | |
VMware® Workstation 17 Pro | 17.6.0 build-24238078 | |
终端软件 | MobaXterm(Professional Edition v23.0 Build 5042 (license)) | |
Win32DiskImager | Win32DiskImager v1.0 | |
Linux开发板环境 | Linux开发板 | 正点原子 i.MX6ULL Linux 阿尔法开发板 |
uboot | NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04 | |
linux内核 | linux-4.19.71(NXP官方提供) |
点击查看本文参考资料
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.nxp.com.cn/ | NXP官方网站 | |
https://www.nxpic.org.cn/ | NXP 官方社区 | |
https://u-boot.readthedocs.io/en/latest/ | u-boot官网 | |
https://www.kernel.org/ | linux内核官网 |
点击查看相关文件下载
分类 | 网址 | 说明 |
NXP | https://github.com/nxp-imx | NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库 |
nxp-imx/linux-imx/releases/tag/v4.19.71 | NXP linux内核仓库tags中的v4.19.71 | |
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 | NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0 | |
I.MX6ULL | i.MX 6ULL Applications Processors for Industrial Products | I.MX6ULL 芯片手册(datasheet,可以在线查看) |
i.MX 6ULL Applications ProcessorReference Manual | I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网) | |
Source Code | https://elixir.bootlin.com/linux/latest/source | linux kernel源码 |
https://elixir.bootlin.com/u-boot/latest/source | uboot源码 |
这一节我们来了解一下内核模块编写的框架。
一、模块源码
这里就用前面准备的hello_world_demo.c源码(03_module_basic/01_hello_world_demo/hello_world_demo.c · 苏木/imx6ull-driver-demo - 码云 - 开源中国 (gitee.com)):
1 |
|
二、代码框架分析
Linux 驱动的基本框架主要由模块加载函数, 模块卸载函数, 模块许可证声明, 模块参数,模块导出符号, 模块作者信息等几部分组成, 其中模块参数, 模块导出符号, 模块作者信息是可选的部分, 也就是可要可不要。 剩余部分是必须有的。
- 模块加载函数(必须): 当通过insmod或modprobe命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。
- 模块卸载函数(必须): 当执行rmmod命令卸载模块时,模块卸载函数就会自动被内核自动执行,完成相关清理工作。
- 模块许可证声明(必须): 许可证声明描述内核模块的许可权限,如果不声明模块许可,模块被加载时,将会有内核被污染(kernel tainted)的警告。 可接受的内核模块声明许可包括“GPL”“GPL v2”等。
- 模块参数: 模块参数是模块被加载时,可以传值给模块中的参数。
- 模块导出符号: 模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。
- 模块的其他相关信息: 可以声明模块作者等信息。
上面示例的hello_world_demo.c程序只包含上面三个必要部分以及模块的其他信息声明(模块参数和导出符号将在后面的学习中出现)。
头文件包含了<linux/init.h>和<linux/module.h>,这两个头文件是写内核模块必须要包含的。 模块初始化函数hello_world_demo_init()调用了printk函数,在内核模块运行的过程中,他不能依赖于C库函数, 因此用不了printf函数,需要使用单独的打印输出函数printk。该函数的用法与printf函数类似。 完成模块初始化函数之后,还需要调用宏module_init来告诉内核,使用hello_world_demo_init()函数来进行初始化。 模块卸载函数也用printk函数打印字符串,并用宏module_exit在内核注册该模块的卸载函数。 最后,必须声明该模块使用遵循的许可证,这里我们设置为GPL协议。
三、源码分析
1. 头文件
前面我们已经接触过了Linux的应用编程,了解到Linux的头文件都存放在/usr/include中。 编写内核模块所需要的头文件,并不在上述说到的目录,而是在Linux内核源码中的include文件夹。
1 |
编写内核模块中经常要使用到的头文件有以下两个:<linux/init.h>和<linux/module.h>。 我们可以看到在头文件前面也带有一个文件夹的名字linux,对应了include下的linux文件夹,我们到该文件夹下,查看这两个头文件都有什么内容。
1 | /* SPDX-License-Identifier: GPL-2.0 */ |
init.h头文件主要包含了内核模块用到的一些宏定义,因此,只要我们涉及内核模块的编程,就需要加上该头文件。
1 |
|
以上代码中,包含了内核模块的加载、卸载函数的声明,还列举了module.h文件中的部分宏定义,这部分宏定义, 有的是可有可无的,但是MODULE_LICENSE这个是指定该内核模块的许可证,是必须要有的。
注意: 在本这里使用的4.19.71版本内核中, module_init
和 module_exit
函数声明在 include/linux/module.h
文件中,旧版本的内核这两个函数声明在 include/linux/init.h
文件中。
2. 模块加载和卸载函数
2.1 module_init()
2.1.1 一般格式
1 | static int __init func_init(void) |
当模块初始化成功,的时候会在/sys/module下新建一个以模块名为名的目录。例如我们之前插入hello_world_demo.ko后,会有如下目录产生:
在C语言中,static关键字的作用如下:
(1)static修饰的静态局部变量直到程序运行结束以后才释放,延长了局部变量的生命周期;
(2)static的修饰全局变量只能在本文件中访问,不能在其它文件中访问;
(3)static修饰的函数只能在本文件中调用,不能被其他文件调用。
内核模块的代码,实际上是内核代码的一部分, 假如内核模块定义的函数和内核源代码中的某个函数重复了, 编译器就会报错,导致编译失败,因此我们给内核模块的代码加上static修饰符的话, 那么就可以避免这种错误。
2.1.2 __init
这个宏定义在init.h - include/linux/init.h:
1 |
以上代码 __init、__initdata宏定义(位于内核源码/linux/init.h)中的__init用于修饰函数,__initdata用于修饰变量。 带有__init的修饰符,表示将该函数放到可执行文件的__init节区中,该节区的内容只能用于模块的初始化阶段, 初始化阶段执行完毕之后,这部分的内容就会被释放掉。
2.1.3 module_init
module_init也是一个宏,它定义在module.h - include/linux/module.h:
1 |
宏定义module_init用于通知内核初始化模块的时候, 要使用哪个函数进行初始化。它会将函数地址加入到相应的节区section中, 这样的话,开机的时候就可以自动加载模块了。
2.2 module_exit()
2.2.1 一般格式
与内核加载函数相反,内核模块卸载函数func_exit主要是用于释放初始化阶段分配的内存, 分配的设备号等,是初始化过程的逆过程。
1 | static void __exit func_exit(void) |
与函数func_init区别在于,该函数的返回值是void类型,且修饰符也不一样, 这里使用的使用__exit,表示将该函数放在可执行文件的__exit节区, 当执行完模块卸载阶段之后,就会自动释放该区域的空间。
2.2.2 __exit
__exit宏定义在init.h - include/linux/init.h:
1 |
__exit用于修饰函数,__exitdata用于修饰变量。 宏定义module_exit用于告诉内核,当卸载模块时,需要调用哪个函数。
3. 许可证
Linux是一款免费的操作系统,采用了GPL协议,允许用户可以任意修改其源代码。 GPL协议的主要内容是软件产品中即使使用了某个GPL协议产品提供的库, 衍生出一个新产品,该软件产品都必须采用GPL协议,即必须是开源和免费使用的, 可见GPL协议具有传染性。因此,我们可以在Linux使用各种各样的免费软件。 在以后学习Linux的过程中,可能会发现我们安装任何一款软件,从来没有30天试用期或者是要求输入激活码的。这个宏定义在module.h - include/linux/module.h
1 |
内核模块许可证有 “GPL”,“GPL v2”,“GPL and additional rights”,“Dual SD/GPL”,“Dual MPL/GPL”,“Proprietary”。这里要是不标明的话可能会出现这种问题:
4. 相关信息声明
最后这部分内容只是为了给使用该模块的读者一本“说明书”,属于可有可无的部分, 有则锦上添花,若没有也无所谓。例如:
4.1 作者信息
MODULE_AUTHOR这个宏定义的事作者信息,它定义在module.h - include/linux/module.h中:
1 | /* |
我们可以使用modinfo中打印出的模块信息,其中“author”信息便是来自于宏定义MODULE_AUTHOR。 该宏定义用于声明该模块的作者。
4.2 模块描述信息
MODULE_DESCRIPTION这个宏定义的是模块的描述信息,它定义在module.h - include/linux/module.h:
1 | /* What your module does. */ |
模块信息中“description”信息就是来自宏MODULE_DESCRIPTION,该宏用于描述该模块的功能作用。
4.3 模块别名
MODULE_ALIAS用于定义模块的别名,这个宏定义在module.h - include/linux/module.h:
1 | /* For userspace: you can also call me... */ |
模块信息中“alias”信息来自于宏定义MODULE_ALIAS。该宏定义用于给内核模块起别名。 注意,在使用该模块的别名时,需要将该模块复制到/lib/modules/(uname -r)/下, 使用命令depmod更新模块的依赖关系,否则的话,Linux内核不知道这个模块还有另一个名字。