在 Linux 内核中,EXPORT_SYMBOL()
用于将模块中的函数或变量导出,使得其他内核模块能够使用这些导出的符号。这对于模块之间共享功能或数据非常有用。给出的代码示例展示了如何使用 EXPORT_SYMBOL()
将变量和函数导出供其他模块使用。
/* ... */
int GLOBAL_VARIABLE = 1000;
EXPORT_SYMBOL(GLOBAL_VARIABLE);
// Function to print hello for num times.
void print_hello(int num)
{
while (num--) {
printk(KERN_INFO "Hello Friend!!!\n");
}
}
EXPORT_SYMBOL(print_hello);
// Function to add two passed number.
void add_two_numbers(int a, int b)
{
printk(KERN_INFO "Sum of the numbers %d", a + b);
}
EXPORT_SYMBOL(add_two_numbers);
static int __init my_init(void)
{
printk(KERN_INFO "Hello from Export Symbol 1 module.");
return 0;
}
static void __exit my_exit(void)
{
printk(KERN_INFO "Bye from Export Symbol 1 module.");
}
module_init(my_init);
module_exit(my_exit);
/* ... */
int GLOBAL_VARIABLE = 1000;
EXPORT_SYMBOL(GLOBAL_VARIABLE);
GLOBAL_VARIABLE
是一个全局变量,初始值为 1000。通过EXPORT_SYMBOL(GLOBAL_VARIABLE)
,该变量被导出,使得其他模块可以通过它访问或修改这个变量。- 用途: 这在多个模块需要共享同一个全局变量时非常有用。例如,如果多个模块需要共享一个状态变量,它们可以通过导出这个全局变量实现。
2. 导出函数 print_hello()
:
void print_hello(int num) {
while (num--) {
printk(KERN_INFO "Hello Friend!!!\n");
}
}
EXPORT_SYMBOL(print_hello);
print_hello()
函数用于打印指定次数的 "Hello Friend!!!" 消息。- 通过
EXPORT_SYMBOL(print_hello)
,该函数也被导出,使得其他模块可以调用print_hello()
函数。 - 用途: 这在需要其他模块执行类似任务时很有用。例如,一个通用的日志输出功能可以通过导出函数供多个模块使用。
3. 导出函数 add_two_numbers()
:
void add_two_numbers(int a, int b) {
printk(KERN_INFO "Sum of the numbers %d", a + b);
}
EXPORT_SYMBOL(add_two_numbers);
add_two_numbers()
函数用于打印传入的两个整数的和。- 通过
EXPORT_SYMBOL(add_two_numbers)
,该函数也被导出,使得其他模块可以调用它来计算两个数字的和并输出结果。 - 用途: 当多个模块需要类似的简单计算时,这样的功能可以被复用。
4. 模块的初始化和退出函数
my_init()
:这是模块加载时执行的初始化函数。它在加载时输出一条消息,表明模块已被加载。my_exit()
:这是模块卸载时执行的清理函数。它在模块卸载时输出一条消息,表明模块已被卸载。module_init()
和module_exit()
用来注册模块的初始化和退出函数。
比喻:
可以把 EXPORT_SYMBOL()
想象成把某些工具放在一个“共享工具箱”中,供其他模块(类似于工程师)使用。每个工程师(模块)都可以从工具箱里取出这些工具(导出的函数或变量)来完成任务,而不需要自己重新造轮子。
导出的 API 在另一个模块中的使用:
假设我们有另一个模块 myModule2
,这个模块想要使用 myModule1
中导出的 GLOBAL_VARIABLE
和 print_hello()
。
myModule2 示例代码:
#include <linux/init.h>
#include <linux/module.h>
extern int GLOBAL_VARIABLE; // 声明外部导出的全局变量
extern void print_hello(int num); // 声明外部导出的函数
static int __init my_module_init(void) {
printk(KERN_INFO "Hello from myModule2.\n");
// 使用导出的变量和函数
printk(KERN_INFO "GLOBAL_VARIABLE is: %d\n", GLOBAL_VARIABLE);
print_hello(5); // 打印5次 "Hello Friend!!!"
return 0;
}
static void __exit my_module_exit(void) {
printk(KERN_INFO "Bye from myModule2.\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
-
extern 关键字:
- 使用
extern
关键字来声明在myModule1
中导出的变量和函数。这告诉内核这个符号已经在其他模块中定义,可以直接引用。
- 使用
-
使用导出的变量和函数:
- 通过
GLOBAL_VARIABLE
,myModule2
可以访问myModule1
中的全局变量。 - 通过调用
print_hello(5)
,myModule2
可以调用myModule1
中的函数,并打印 5 次 "Hello Friend!!!"。
- 通过
/* ... */
extern void print_hello(int);
extern void add_two_numbers(int, int);
extern int GLOBAL_VARIABLE;
/*
* Call functions which are in other module.
*/
static int __init my_init(void)
{
printk(KERN_INFO "Hello from Hello Module");
print_hello(2);
add_two_numbers(5, 6);
printk(KERN_INFO "Value of GLOBAL_VARIABLE %d", GLOBAL_VARIABLE);
return 0;
}
static void __exit my_exit(void)
{
printk(KERN_INFO "Bye from Hello Module");
}
module_init(my_init);
module_exit(my_exit);
/* ... */
在 Linux 内核中,模块之间的加载顺序非常重要,尤其当一个模块依赖另一个模块导出的符号时。如果依赖的模块没有先加载,依赖模块将无法找到其需要的符号,导致加载错误。
为什么会出错:
在你给出的场景中:
myModule1.ko
导出了全局变量和函数(如GLOBAL_VARIABLE
、print_hello()
)。myModule2.ko
依赖myModule1.ko
中导出的符号,并通过extern
引用它们。
如果你尝试先加载 myModule2.ko
,会发生错误,因为在加载 myModule2.ko
时,内核找不到 GLOBAL_VARIABLE
和 print_hello()
等符号——这些符号还没有被 myModule1.ko
导出。
内核模块加载的过程类似于以下步骤:
- 内核会首先检查模块的依赖项,并查看该模块是否需要使用其他模块导出的符号。
- 如果依赖的符号没有找到(即所需的模块尚未加载),内核会报错,并拒绝加载该模块。
因此,必须先加载导出符号的模块(myModule1.ko
),然后再加载依赖模块(myModule2.ko
)。
如何解决:
为了解决模块加载顺序问题,确保在插入内核模块时遵循正确的依赖顺序:
-
先插入
myModule1.ko
:- 运行
insmod myModule1.ko
或者modprobe myModule1
,首先将导出符号的模块加载到内核中。 - 这样,内核会将
myModule1.ko
中的符号(GLOBAL_VARIABLE
、print_hello()
等)导出并使其在整个内核中可用。
- 运行
-
再插入
myModule2.ko
:- 在
myModule1.ko
成功加载后,再运行insmod myModule2.ko
或modprobe myModule2
,这时内核可以找到myModule1.ko
导出的符号,myModule2.ko
将能够正确加载。
- 在
错误示例:
如果你反过来加载模块,先加载 myModule2.ko
,会看到类似以下的错误:
insmod: error inserting 'myModule2.ko': -1 Unknown symbol in module
这个错误通常表示模块中有未解析的符号,原因是这些符号(GLOBAL_VARIABLE
和 print_hello()
)还没有被内核注册,因为 myModule1.ko
尚未加载。
//Insert myModule1.ko then myModule2.ko and you can see the following:
$ sudo insmod myModule1.ko
$ sudo insmod myModule2.ko
[15606.692155] Hello from Export Symbol 1 module.
[15612.175760] Hello from Hello Module
[15612.175764] Hello Friend!!!
[15612.175766] Hello Friend!!!
[15612.175780] Sum of the numbers 11
[15612.175782] Value of GLOBAL_VARIABLE 1000
5. EXPORT_SYMBOL — Linux Kernel Workbook 1.0 documentation (lkw.readthedocs.io)
Linux World: Exporting symbols from module (tuxthink.blogspot.com)
c - How to call exported kernel module functions from another module? - Stack Overflow
How to define a function in one linux kernel module and use it in another? - Stack Overflow
How to create a working thread
In our case, the working function is my_fork(), so my_fork() needs to do all the job.
How to create a working process
在 Linux 内核中,kernel_clone()
函数是用来创建一个新进程或线程的底层系统调用。这类似于用户空间的 fork()
或 clone()
,但在内核中允许更细粒度的控制。提供的代码片段展示了如何使用 kernel_clone()
来创建一个新进程,并让该进程执行指定的函数(如 hello()
)。
struct kernel_clone_args clone_args = {
.flags = SIGCHLD,
.pidfd = NULL,
.child_tid = NULL,
.parent_tid = NULL,
.exit_signal = SIGCHLD,
.stack = (unsigned long) &hello,
.stack_size = 0,
.tls = 0
};
pid_t pid = kernel_clone(&clone_args);
字段解释:
-
flags
:- 作用: 用来控制新进程或线程的行为。
- 在这个例子中,
flags = SIGCHLD
表示在子进程终止时会发送SIGCHLD
信号给父进程。这是常见的用于通知父进程子进程终止的机制。 - 其他可能的标志:
CLONE_VM
: 子进程共享父进程的地址空间。CLONE_FS
: 子进程共享父进程的文件系统信息。
-
pidfd
:- 作用: 如果不为
NULL
,则存储一个文件描述符(PID file descriptor),该文件描述符指向子进程的 PID。这在进程控制中很有用。 - 在你的例子中,
pidfd = NULL
,表示不使用 PID 文件描述符。
- 作用: 如果不为
-
parent_tid
和child_tid
:- 作用: 用于线程(而不是进程)的同步。如果不为
NULL
,则将父/子线程的 TID(线程 ID)存储到指定的地址中。 - 在你的例子中,这两个字段都被设置为
NULL
,表示不使用线程 ID。
- 作用: 用于线程(而不是进程)的同步。如果不为
-
exit_signal
:- 作用: 指定子进程终止时父进程接收到的信号。在这个例子中,
exit_signal = SIGCHLD
表示当子进程终止时,父进程会接收到SIGCHLD
信号。 - 这与
fork()
系统调用的默认行为一致。
- 作用: 指定子进程终止时父进程接收到的信号。在这个例子中,
-
stack
:- 作用: 该字段指定新进程的栈起始地址。
- 在这个例子中,
stack = (unsigned long) &hello
,即将hello
函数的地址作为栈地址传递。这意味着当新进程启动时,它将执行hello()
函数。 - 注意:在一般情况下,
stack
通常用于指定用户态进程的栈地址。而在这里,hello()
函数将被用作新进程的执行入口。
-
stack_size
:- 作用: 用于指定栈的大小。
- 在这个例子中,
stack_size = 0
,表示默认的栈大小。这意味着内核会使用系统默认的栈大小。
-
tls
:- 作用: 这是线程局部存储(Thread Local Storage)的指针,用于在多线程环境中给每个线程分配独立的存储区域。
- 在这个例子中,
tls = 0
,表示不使用线程局部存储。