[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,表示不使用线程局部存储。
相关推荐
中云DDoS CC防护蔡蔡1 小时前
微信小程序被攻击怎么选择高防产品
服务器·网络安全·微信小程序·小程序·ddos
HPC_fac130520678162 小时前
以科学计算为切入点:剖析英伟达服务器过热难题
服务器·人工智能·深度学习·机器学习·计算机视觉·数据挖掘·gpu算力
yaoxin5211233 小时前
第二十七章 TCP 客户端 服务器通信 - 连接管理
服务器·网络·tcp/ip
内核程序员kevin3 小时前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
sinat_384241097 小时前
使用 npm 安装 Electron 作为开发依赖
服务器
朝九晚五ฺ7 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream7 小时前
Linux的桌面
linux
xiaozhiwise8 小时前
Makefile 之 自动化变量
linux
Kkooe8 小时前
GitLab|数据迁移
运维·服务器·git