06、内核模块符号的导出
在 Linux 内核开发中,驱动程序通常以模块的形式存在。这些模块是以 .ko
(Kernel Object)文件的形式编译生成的,并且在正常情况下,它们是相互独立的。也就是说,一个模块中的变量或函数默认无法被其他模块直接访问。然而,在实际开发中,复杂的驱动程序往往需要分层设计,某些功能可能需要在多个模块之间共享。为了实现这种跨模块的功能调用和数据共享,Linux 提供了内核符号导出机制。
一、内核符号导出的作用
内核符号导出的作用是把一个模块里的某些功能或数据记录到一个共享列表里。这样其他模块就能直接找到并使用这些内容。这就像把工具放进公共工具箱,大家都能取用,既方便合作又不互相干扰。例如,底层模块可以公开一些基础功能,让上层模块直接调用,这样系统就能像乐高积木一样分层搭建,结构更清晰。
二、符号导出的实现方式
在Linux系统中,有两个宏(EXPORT_SYMBOL
和EXPORT_SYMBOL_GPL
)用来让内核模块共享功能。它们的定义在内核源代码的include/linux/export.h
文件里,但开发者不需要单独引入这个文件,因为module.h
已经自动包含了它。
这两个宏的区别是:
- EXPORT_SYMBOL(sym) 这个宏会把某个符号(比如函数或变量)公开出去,所有内核模块都能使用它,无论这些模块是否遵守GPL开源协议。
- EXPORT_SYMBOL_GPL(sym) 这个宏同样会公开符号,但只允许遵循GPL协议的模块来调用。这样设计是为了保护开源项目:如果别人要用这个功能,就必须把自己的代码也开源,确保技术共享的精神不被破坏。
简单来说:
- 普通导出(
EXPORT_SYMBOL
):对所有人开放 - GPL导出(
EXPORT_SYMBOL_GPL
):仅限开源项目使用
三、实验代码
以下实验展示了如何通过符号导出实现模块间的资源共享。
3.1、代码
模块一:(module 1)
#include <linux/init.h>
#include <linux/module.h>
int num = 10; // 定义全局变量 num
EXPORT_SYMBOL(num); // 导出变量 num
int add(int a, int b) // 定义加法函数 add()
{
return a + b;
}
EXPORT_SYMBOL(add); // 导出函数 add()
static int __init math_init(void) // 驱动入口函数
{
printk("math_module init\n");
return 0;
}
static void __exit math_exit(void) // 驱动出口函数
{
printk("math_module exit\n");
}
module_init(math_init); // 注册入口函数
module_exit(math_exit); // 注册出口函数
MODULE_LICENSE("GPL v2"); //同意 GPL 开源协议
MODULE_VERSION("1.0"); //驱动的版本
MODULE_DESCRIPTION("module1"); //lsmod
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
在这个模块中,我们定义了一个全局变量 num
和一个函数 add()
,并通过 EXPORT_SYMBOL
宏将它们导出。这意味着其他模块可以通过公共符号表访问这些资源。
模块二:(module 2)
#include <linux/init.h>
#include <linux/module.h>
extern int num; // 声明外部变量 num
extern int add(int a, int b); // 声明外部函数 add()
static int __init hello_init(void) // 驱动入口函数
{
static int sum;
printk("num = %d\n", num); // 打印 num 的值
sum = add(3, 4); // 调用 add 函数计算 3 + 4
printk("sum = %d\n", sum); // 打印计算结果
return 0;
}
static void __exit hello_exit(void) // 驱动出口函数
{
printk("Goodbye hello module\n");
}
module_init(hello_init); // 注册入口函数
module_exit(hello_exit); // 注册出口函数
MODULE_LICENSE("GPL v2"); //同意 GPL 开源协议
MODULE_VERSION("1.0"); //驱动的版本
MODULE_DESCRIPTION("module2"); //lsmod
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
配套Makefile:
export ARCH=arm64
export CROSS_COMPILE=/home/book/rk/tspi/prebuilts/gcc/linux-x86/aarch64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-
obj-m +=module1.o #此处要和你的驱动源文件同名
obj-m +=module2.o #此处要和你的驱动源文件同名
KDIR := /home/book/rk/tspi/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make#操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean操作
2
3
4
5
6
7
8
9
10
3.2、加载与运行
- 编译并加载
module1
:
insmod module1.ko
此时,num
和 add()
被注册到公共符号表中。
- 编译并加载
module2
:
insmod module2.ko
module2
成功访问了 moudule2
中的 num和add()
上述输出表明,hello_module
成功访问了 math_module
导出的全局变量 num
和函数 add()
,并正确执行了相关操作。
- 卸载模块时,需注意顺序。由于
hello_module
依赖于math_module
提供的符号,因此必须先卸载hello_module
,再卸载math_module
:内核日志中将显示如下信息:
如果尝试在 math_module
未加载的情况下加载 hello_module
,系统会报错,提示无法解析符号 num
和 add()
。这是因为 hello_module
在加载时需要从公共符号表中查找这些符号,而此时它们尚未被注册。