[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,表示不使用线程局部存储。
相关推荐
cocologin19 分钟前
RIP 技术深度解析
运维·网络·网络协议
cv高级工程师YKY29 分钟前
SRE - - PV、UV、VV、IP详解及区别
大数据·服务器·uv
庸子36 分钟前
基于Jenkins和Kubernetes构建DevOps自动化运维管理平台
运维·kubernetes·jenkins
眠修1 小时前
Kuberrnetes 服务发布
linux·运维·服务器
好奇的菜鸟2 小时前
Docker 配置项详解与示例
运维·docker·容器
xcs194052 小时前
集运维 麒麟桌面版v10 sp1 2403 aarch64 离线java开发环境自动化安装
运维·自动化
BAOYUCompany2 小时前
暴雨服务器成功中标华中科技大学集成电路学院服务器采购项目
运维·服务器
超龄超能程序猿3 小时前
Bitvisse SSH Client 安装配置文档
运维·ssh·github
奈斯ing3 小时前
【Redis篇】数据库架构演进中Redis缓存的技术必然性—高并发场景下穿透、击穿、雪崩的体系化解决方案
运维·redis·缓存·数据库架构
鳄鱼皮坡4 小时前
仿muduo库One Thread One Loop式主从Reactor模型实现高并发服务器
运维·服务器