LV050-框架说明
这一节我们来了解一下内核模块编写的框架。
一、模块源码
这里就用前面准备的 hello_world_demo.c 源码(03_module_basic/01_hello_world_demo/hello_world_demo.c):
#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 文件夹。
#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 文件夹,我们到该文件夹下,查看这两个头文件都有什么内容。
/* 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 头文件主要包含了内核模块用到的一些宏定义,因此,只要我们涉及内核模块的编程,就需要加上该头文件。
#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_init 和 module_exit 函数声明在 include/linux/module.h 文件中,旧版本的内核这两个函数声明在 include/linux/init.h 文件中。
2. 模块加载和卸载函数
2.1 module_init()
2.1.1 一般格式
static int __init func_init(void)
{
}
module_init(func_init);当模块初始化成功,的时候会在/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:
#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:
#define module_init(x) __initcall(x);宏定义 module_init 用于通知内核初始化模块的时候, 要使用哪个函数进行初始化。它会将函数地址加入到相应的节区 section 中, 这样的话,开机的时候就可以自动加载模块了。
2.2 module_exit()
2.2.1 一般格式
与内核加载函数相反,内核模块卸载函数 func_exit 主要是用于释放初始化阶段分配的内存, 分配的设备号等,是初始化过程的逆过程。
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:
#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
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)内核模块许可证有 “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 中:
/*
* 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:
/* 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:
/* For userspace: you can also call me... */
#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)模块信息中“alias”信息来自于宏定义 MODULE_ALIAS。该宏定义用于给内核模块起别名。 注意,在使用该模块的别名时,需要将该模块复制到/lib/modules/(uname -r)/下, 使用命令 depmod 更新模块的依赖关系,否则的话,Linux 内核不知道这个模块还有另一个名字。