[OS] EXPORT_SYMBOL()

在 Linux 内核中,EXPORT_SYMBOL() 用于将模块中的函数或变量导出,使得其他内核模块能够使用这些导出的符号。这对于模块之间共享功能或数据非常有用。给出的代码示例展示了如何使用 EXPORT_SYMBOL() 将变量和函数导出供其他模块使用。

cpp 复制代码
/* ... */
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);
/* ... */
cpp 复制代码
int GLOBAL_VARIABLE = 1000;
EXPORT_SYMBOL(GLOBAL_VARIABLE);
  • GLOBAL_VARIABLE 是一个全局变量,初始值为 1000。通过 EXPORT_SYMBOL(GLOBAL_VARIABLE),该变量被导出,使得其他模块可以通过它访问或修改这个变量。
  • 用途: 这在多个模块需要共享同一个全局变量时非常有用。例如,如果多个模块需要共享一个状态变量,它们可以通过导出这个全局变量实现。
cpp 复制代码
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()
cpp 复制代码
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_VARIABLEprint_hello()

myModule2 示例代码:
cpp 复制代码
#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_VARIABLEmyModule2 可以访问 myModule1 中的全局变量。
    • 通过调用 print_hello(5)myModule2 可以调用 myModule1 中的函数,并打印 5 次 "Hello Friend!!!"。
cpp 复制代码
/* ... */
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_VARIABLEprint_hello())。
  • myModule2.ko 依赖 myModule1.ko 中导出的符号,并通过 extern 引用它们。

如果你尝试先加载 myModule2.ko,会发生错误,因为在加载 myModule2.ko 时,内核找不到 GLOBAL_VARIABLEprint_hello() 等符号------这些符号还没有被 myModule1.ko 导出。

内核模块加载的过程类似于以下步骤:

  1. 内核会首先检查模块的依赖项,并查看该模块是否需要使用其他模块导出的符号。
  2. 如果依赖的符号没有找到(即所需的模块尚未加载),内核会报错,并拒绝加载该模块。

因此,必须先加载导出符号的模块(myModule1.ko ,然后再加载依赖模块(myModule2.ko)。

如何解决:

为了解决模块加载顺序问题,确保在插入内核模块时遵循正确的依赖顺序:

  1. 先插入 myModule1.ko

    • 运行 insmod myModule1.ko 或者 modprobe myModule1,首先将导出符号的模块加载到内核中。
    • 这样,内核会将 myModule1.ko 中的符号(GLOBAL_VARIABLEprint_hello() 等)导出并使其在整个内核中可用。
  2. 再插入 myModule2.ko

    • myModule1.ko 成功加载后,再运行 insmod myModule2.komodprobe myModule2,这时内核可以找到 myModule1.ko 导出的符号,myModule2.ko 将能够正确加载。

错误示例:

如果你反过来加载模块,先加载 myModule2.ko,会看到类似以下的错误:

cpp 复制代码
insmod: error inserting 'myModule2.ko': -1 Unknown symbol in module

这个错误通常表示模块中有未解析的符号,原因是这些符号(GLOBAL_VARIABLEprint_hello())还没有被内核注册,因为 myModule1.ko 尚未加载。

cpp 复制代码
//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 prevent "error: 'symbol' undeclared here" despite EXPORT_SYMBOL in a Linux kernel module? - Stack Overflow

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())。

cpp 复制代码
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);

字段解释:

  1. flags:

    • 作用: 用来控制新进程或线程的行为。
    • 在这个例子中,flags = SIGCHLD 表示在子进程终止时会发送 SIGCHLD 信号给父进程。这是常见的用于通知父进程子进程终止的机制。
    • 其他可能的标志
      • CLONE_VM: 子进程共享父进程的地址空间。
      • CLONE_FS: 子进程共享父进程的文件系统信息。
  2. pidfd:

    • 作用 : 如果不为 NULL,则存储一个文件描述符(PID file descriptor),该文件描述符指向子进程的 PID。这在进程控制中很有用。
    • 在你的例子中,pidfd = NULL,表示不使用 PID 文件描述符。
  3. parent_tidchild_tid:

    • 作用 : 用于线程(而不是进程)的同步。如果不为 NULL,则将父/子线程的 TID(线程 ID)存储到指定的地址中。
    • 在你的例子中,这两个字段都被设置为 NULL,表示不使用线程 ID。
  4. exit_signal:

    • 作用 : 指定子进程终止时父进程接收到的信号。在这个例子中,exit_signal = SIGCHLD 表示当子进程终止时,父进程会接收到 SIGCHLD 信号。
    • 这与 fork() 系统调用的默认行为一致。
  5. stack:

    • 作用: 该字段指定新进程的栈起始地址。
    • 在这个例子中,stack = (unsigned long) &hello,即将 hello 函数的地址作为栈地址传递。这意味着当新进程启动时,它将执行 hello() 函数。
    • 注意 :在一般情况下,stack 通常用于指定用户态进程的栈地址。而在这里,hello() 函数将被用作新进程的执行入口。
  6. stack_size:

    • 作用: 用于指定栈的大小。
    • 在这个例子中,stack_size = 0,表示默认的栈大小。这意味着内核会使用系统默认的栈大小。
  7. tls:

    • 作用: 这是线程局部存储(Thread Local Storage)的指针,用于在多线程环境中给每个线程分配独立的存储区域。
    • 在这个例子中,tls = 0,表示不使用线程局部存储。
相关推荐
sszdzq5 分钟前
Docker
运维·docker·容器
book01218 分钟前
MySql数据库运维学习笔记
运维·数据库·mysql
leoufung12 分钟前
VIM FZF 安裝和使用
linux·编辑器·vim
bugtraq20211 小时前
XiaoMi Mi5(gemini) 刷入Ubuntu Touch 16.04——安卓手机刷入Linux
linux·运维·ubuntu
xmweisi1 小时前
【华为】报文统计的技术NetStream
运维·服务器·网络·华为认证
VVVVWeiYee1 小时前
BGP配置华为——路径优选验证
运维·网络·华为·信息与通信
陆鳐LuLu2 小时前
日志管理利器:基于 ELK 的日志收集、存储与可视化实战
运维·elk·jenkins
CodeWithMe2 小时前
[ Vim ] 常用命令 and 配置
linux·编辑器·vim
DC_BLOG2 小时前
Linux-GlusterFS进阶分布式卷
linux·运维·服务器·分布式
yourkin6662 小时前
TCP...
服务器·网络·tcp/ip