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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <linux/kernel.h>
#include <linux/init.h> /* module_init module_exit */
#include <linux/module.h> /* MODULE_LICENSE */


// 模块入口函数
static int __init hello_world_demo_init(void)
{
printk("hello_world_demo module is running!\n");
return 0;
}

// 模块出口函数
static void __exit hello_world_demo_exit(void)
{
printk("hello_world_demo will exit\n");
}

// 将__init定义的函数指定为驱动的入口函数
module_init(hello_world_demo_init);


// 将__exit定义的函数指定为驱动的出口函数
module_exit(hello_world_demo_exit);

/* 模块信息(通过 modinfo hello_world_demo 查看) */
MODULE_LICENSE("GPL"); /* 源码的许可证协议 */
MODULE_AUTHOR("sumu"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

二、代码框架分析

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
2
3
#include <linux/module.h> // 包含了内核加载module_init()/卸载module_exit()函数和内核模块信息相关函数的声明
#include <linux/init.h> // 包含一些内核模块相关节区的宏定义
#include <linux/kernel.h> // 包含内核提供的各种函数,如printk

编写内核模块中经常要使用到的头文件有以下两个:<linux/init.h>和<linux/module.h>。 我们可以看到在头文件前面也带有一个文件夹的名字linux,对应了include下的linux文件夹,我们到该文件夹下,查看这两个头文件都有什么内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_INIT_H
#define _LINUX_INIT_H

#include <linux/compiler.h>
#include <linux/types.h>

// ......
/* These are for everybody (although not all archs will actually
discard it in modules) */
#define __init __section(.init.text) __cold __latent_entropy __noinitretpoline
#define __initdata __section(.init.data)
#define __initconst __section(.init.rodata)
#define __exitdata __section(.exit.data)
#define __exit_call __used __section(.exitcall.exit)

// ......

#endif /* _LINUX_INIT_H */

init.h头文件主要包含了内核模块用到的一些宏定义,因此,只要我们涉及内核模块的编程,就需要加上该头文件。

1
2
3
4
5
6
7
8
#define module_init(x)  __initcall(x);
#define module_exit(x) __exitcall(x);

/* Generic info of form tag = "info" */
#define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)
#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)
#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)

以上代码中,包含了内核模块的加载、卸载函数的声明,还列举了module.h文件中的部分宏定义,这部分宏定义, 有的是可有可无的,但是MODULE_LICENSE这个是指定该内核模块的许可证,是必须要有的。

注意: 在本这里使用的4.19.71版本内核中, module_initmodule_exit 函数声明在 include/linux/module.h 文件中,旧版本的内核这两个函数声明在 include/linux/init.h 文件中。

2. 模块加载和卸载函数

2.1 module_init()

2.1.1 一般格式

1
2
3
4
static int __init func_init(void)
{
}
module_init(func_init);

当模块初始化成功,的时候会在/sys/module下新建一个以模块名为名的目录。例如我们之前插入hello_world_demo.ko后,会有如下目录产生:

image-20241118230823024

在C语言中,static关键字的作用如下:

(1)static修饰的静态局部变量直到程序运行结束以后才释放,延长了局部变量的生命周期;

(2)static的修饰全局变量只能在本文件中访问,不能在其它文件中访问;

(3)static修饰的函数只能在本文件中调用,不能被其他文件调用。

内核模块的代码,实际上是内核代码的一部分, 假如内核模块定义的函数和内核源代码中的某个函数重复了, 编译器就会报错,导致编译失败,因此我们给内核模块的代码加上static修饰符的话, 那么就可以避免这种错误。

2.1.2 __init

这个宏定义在init.h - include/linux/init.h

1
2
#define __init      __section(.init.text) __cold  __latent_entropy __noinitretpoline
#define __initdata __section(.init.data)

以上代码 __init、__initdata宏定义(位于内核源码/linux/init.h)中的__init用于修饰函数,__initdata用于修饰变量。 带有__init的修饰符,表示将该函数放到可执行文件的__init节区中,该节区的内容只能用于模块的初始化阶段, 初始化阶段执行完毕之后,这部分的内容就会被释放掉。

2.1.3 module_init

module_init也是一个宏,它定义在module.h - include/linux/module.h

1
#define module_init(x) __initcall(x);

宏定义module_init用于通知内核初始化模块的时候, 要使用哪个函数进行初始化。它会将函数地址加入到相应的节区section中, 这样的话,开机的时候就可以自动加载模块了。

2.2 module_exit()

2.2.1 一般格式

与内核加载函数相反,内核模块卸载函数func_exit主要是用于释放初始化阶段分配的内存, 分配的设备号等,是初始化过程的逆过程。

1
2
3
4
static void __exit func_exit(void)
{
}
module_exit(func_exit);

与函数func_init区别在于,该函数的返回值是void类型,且修饰符也不一样, 这里使用的使用__exit,表示将该函数放在可执行文件的__exit节区, 当执行完模块卸载阶段之后,就会自动释放该区域的空间。

2.2.2 __exit

__exit宏定义在init.h - include/linux/init.h

1
2
#define __exit          __section(.exit.text) __exitused __cold notrace
#define __exitdata __section(.exit.data)

__exit用于修饰函数,__exitdata用于修饰变量。 宏定义module_exit用于告诉内核,当卸载模块时,需要调用哪个函数。

3. 许可证

Linux是一款免费的操作系统,采用了GPL协议,允许用户可以任意修改其源代码。 GPL协议的主要内容是软件产品中即使使用了某个GPL协议产品提供的库, 衍生出一个新产品,该软件产品都必须采用GPL协议,即必须是开源和免费使用的, 可见GPL协议具有传染性。因此,我们可以在Linux使用各种各样的免费软件。 在以后学习Linux的过程中,可能会发现我们安装任何一款软件,从来没有30天试用期或者是要求输入激活码的。这个宏定义在module.h - include/linux/module.h

1
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)

内核模块许可证有 “GPL”,“GPL v2”,“GPL and additional rights”,“Dual SD/GPL”,“Dual MPL/GPL”,“Proprietary”。这里要是不标明的话可能会出现这种问题:

image-20241120220437718

4. 相关信息声明

最后这部分内容只是为了给使用该模块的读者一本“说明书”,属于可有可无的部分, 有则锦上添花,若没有也无所谓。例如:

4.1 作者信息

MODULE_AUTHOR这个宏定义的事作者信息,它定义在module.h - include/linux/module.h中:

1
2
3
4
5
/*
* Author(s), use "Name <email>" or just "Name", for multiple
* authors use multiple MODULE_AUTHOR() statements/lines.
*/
#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)

我们可以使用modinfo中打印出的模块信息,其中“author”信息便是来自于宏定义MODULE_AUTHOR。 该宏定义用于声明该模块的作者。

4.2 模块描述信息

MODULE_DESCRIPTION这个宏定义的是模块的描述信息,它定义在module.h - include/linux/module.h

1
2
/* What your module does. */
#define MODULE_DESCRIPTION(_description) MODULE_INFO(description, _description)

模块信息中“description”信息就是来自宏MODULE_DESCRIPTION,该宏用于描述该模块的功能作用。

4.3 模块别名

MODULE_ALIAS用于定义模块的别名,这个宏定义在module.h - include/linux/module.h

1
2
/* For userspace: you can also call me... */
#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)

模块信息中“alias”信息来自于宏定义MODULE_ALIAS。该宏定义用于给内核模块起别名。 注意,在使用该模块的别名时,需要将该模块复制到/lib/modules/(uname -r)/下, 使用命令depmod更新模块的依赖关系,否则的话,Linux内核不知道这个模块还有另一个名字。