ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

内容分享5小时前发布
0 0 0

0、前言:

这是一篇以问题为导向,的技术贴!练习编写内核模块,在linux虚拟机中测试编写的内核模块,掌握内核模块编写模板;一个内核模块的调试过程如下:
ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调


基础概念库:

编辑内核模块,如果在虚拟机的linux中,可以在VS中打开目录编辑代码;【在linux的vs中可以点击左上角的三道杠,选择运行,启用调试,选择c或者c++,弹出的提示选择仍要调试,取消即可,这样按住ctrl就可以进行跳转;】

一、linux内核模块:

内核是一个操作系统的核心,是硬件之上的第一层软件,提供操作系统最基本的功能;内核主要分为:微内核和宏内核;
不被包函在内核当中的设备驱动模块,可以自行修改,不会影响微内核功能;宏内核,会把微内核之外的功能模块包含在其中;
linux是宏内核,为了解决宏内核的缺点,linux引入了内核模块的机制,在linux运行时,动态加载内核模块;内核模块本质是实现了某一特定功能的内核代码,经过编译生成的二进制文件;

二、★★★★★ linux引入“内核模块”的好处:

能针对性解决宏内核 “臃肿、扩展性差、升级维护复杂” 等缺点,同时保留宏内核 “性能高、内核内组件通信高效” 的优势。【内核模块机制让 “静态的宏内核” 具备了 “动态扩展的能力”
减少内核体积,降低内存占用,宏内核的默认设计是 “将所有功能(如文件系统、驱动、网络协议栈)编译进内核镜像”,即使某些功能(如蓝牙驱动、特殊网卡驱动)用户用不到,也会占用内存并随系统启动加载。内核模块可实现 “按需加载”:只有当用户需要某功能(如插入 U 盘时加载 USB 驱动模块,连接蓝牙时加载蓝牙模块),才将对应的模块加载到内核;不用时可卸载,释放内存。无需重启系统,灵活升级 / 修复功能,内核若要升级或修复某功能(如修复网络协议栈漏洞、更新显卡驱动),传统方式需要重新编译整个内核并重启系统 —— 这对服务器、嵌入式设备(如工业控制器、路由器)等 “需 7×24 小时运行” 的场景极不友好。内核模块支持 “动态更新”:升级驱动或修复漏洞时,只需编译新的模块,卸载旧模块、加载新模块即可,全程无需重启系统。降低开发 / 调试成本,提升扩展性,内核模块的开发是 “独立隔离” 的,开发时只需编写模块代码,编译成独立的 .ko 文件,加载到内核即可测试,无需编译整个内核;适配多样硬件 / 场景,增强灵活性,内核模块可实现 “内核功能的模块化组合”:系统可根据硬件配置和使用场景,加载对应的模块(如嵌入式设备加载 “传感器驱动模块”,服务器加载 “RAID 卡驱动模块”);同一内核镜像可通过加载不同模块,适配不同硬件,避免为每种硬件编译一个专属内核,降低系统维护成本。

三、linux内核模块的编译本质:

Linux 内核模块不能像普通应用(用 gcc main.c -o main)直接编译,模块需要依赖内核源码中的头文件(如 linux/module.h)、宏定义(如MODULE_LICENSE)和编译规则;模块编译必须与内核版本、编译选项(如 CONFIG_XXX)完全一致,否则加载时会因 “版本不匹配” 失败。因此,内核模块编译的核心是 “调用内核自身的 Makefile 来编译”,而不是自己写完整编译规则,下面案例中制作的所有Makefile,都是依据这个规则。下面在案例中具体讲解模块中MakeFile的编写规则;

四、内核模块编写模板总结:

0、Linux 内核模块(Kernel Module) 的核心组成部分:

模块加载函数(必备):是模块的 “启动入口”,通常通过 module_init() 宏指定。加载时自动执行,主要完成资源申请(如内存、设备号)、初始化数据结构等工作。若初始化失败,需返回非 0 值,告知内核加载失败。模块卸载函数(必备):是模块的 “清理出口”,通常通过 module_exit() 宏指定。卸载时自动执行,主要负责释放加载函数申请的资源(如内存、设备号),避免内核资源泄漏。模块许可证声明(必备):是内核识别模块合法性的关键,必须通过 MODULE_LICENSE() 宏声明,常见值如 GPL(通用公共许可证)、MIT 等。若不声明,内核会标记为 “被污染(Tainted)”,可能影响后续内核支持和部分功能。模块参数(可选):通过 module_param() 等宏定义,允许加载模块时(如 insmod module.ko param=value)传递自定义值。方便模块灵活适配不同场景,无需修改代码即可调整模块行为。模块导出符号(可选),通过 EXPORT_SYMBOL() 或 EXPORT_SYMBOL_GPL() 导出模块内的函数 / 变量。导出的符号会加入内核符号表,供其他内核模块调用,实现模块间的功能复用。模块其他信息(可选):通过 MODULE_AUTHOR()(作者)、MODULE_DESCRIPTION()(功能描述)、MODULE_VERSION()(版本)等宏声明。主要用于文档化,方便开发者或系统管理员了解模块信息,无实际功能影响。★★★ 总结:
1、模块加载函数是在代码中通过module_init声明,通过static int __init 函数名()定义,编译好之后,通过insmod触发;2、卸载加载函数是在代码中通过module_exit声明,通过static void __exit 函数名()定义,编译好之后,通过rmmod触发;

1、编写内核模块需要的文件

核心源文件(.c):实现模块功能的代码文件(如 hello.c),包含模块初始化、退出函数及业务逻辑。单文件模块:1 个 .c 文件即可(如 hello.c)。多文件模块:多个 .c 文件(如 t1.c、sort.c),需通过 Makefile 合并编译。Makefile:编译脚本,指定内核路径、模块名称及源文件,调用内核 Makefile 完成编译(核心作用是 “告诉内核如何编译你的模块”)。可选:头文件(.h):多文件模块中,用于声明跨文件调用的函数 / 变量(如 sort.h 声明 sort_int3 供 t1.c 调用)。

2、单文件模块

源文件(hello.c)


/* 内核模块必备头文件 */
#include <linux/init.h>    // 包含模块初始化/退出宏(__init、__exit)
#include <linux/module.h>  // 包含模块基本定义(MODULE_LICENSE、module_init等)
#include <linux/kernel.h>  // 包含内核打印函数(pr_info等)

/* 模块初始化函数:加载模块时执行(insmod时调用) */
static int __init hello_init(void)
{
    // 内核打印用 pr_info(类似用户态 printf,输出到内核日志,用 dmesg 查看)
    pr_info("Hello, Kernel Module!
");
    return 0;  // 返回 0 表示初始化成功,非0表示失败(模块加载失败)
}

/* 模块退出函数:卸载模块时执行(rmmod时调用) */
static void __exit hello_exit(void)
{
    pr_info("Goodbye, Kernel Module!
");
}

/* 注册初始化/退出函数(内核规定的固定法写法) */
module_init(hello_init);  // 告诉内核:加载模块时调用 hello_init
module_exit(hello_exit);  // 告诉内核:卸载模块时调用 hello_exit

/* 模块元信息(必选,否则加载可能警告) */
MODULE_LICENSE("GPL");          // 声明许可证(必须为 GPL 及兼容协议,否则符号导出受限)
MODULE_AUTHOR("Your Name");     // 作者信息(可选)
MODULE_DESCRIPTION("A simple kernel module");  // 模块描述(可选)
MODULE_VERSION("1.0");          // 版本号(可选)

Makefile(与 hello.c 同目录)


ifeq ($(KERNELRELEASE), ) #如果$(KERNELRELEASE)没有值,就执行ifeq中的语句
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build		#定义了一个变量并赋值为linux源码路径
	PWD := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) $@
else
	obj-m:= <你的.c文件名> .o
endif

clean:
	rm -rf *.o *.mod.c *.mod.o *.ko *.symvers *.order *.a *.mod .*.*.cmd

3、多文件模块

1、多个.c 文件,可以定义一个.h文件声明下跨文件函数

2、注意编写Makefile,不能把文件名写作makefile,还有就是内部需要把用到的文件编译进去;

3、头文件必须包含:所有模块都必须包含 #include <linux/init.h> 和 #include <linux/module.h>,否则无法使用 module_init、MODULE_LICENSE 等核心宏。

MODULE_LICENSE 不可少:必须声明为 GPL 或兼容协议(如 GPLv2),否则内核会拒绝加载模块(或导出符号失败)。
函数可见性控制:

仅在当前文件使用的函数加 static ,否则加了static,就会导致没法被其他文件加载这个函数;跨文件调用的函数不能加 static,且需用 EXPORT_SYMBOL 导出(多文件模块)。

4、★★★内核模块中.c文件中的部分头文件作用介绍:

#include <linux/module.h>**: 包含内核模块信息声明的相关函数 如module_init()和 module_exit()的声明#include <linux/init.h>*:包含了init和 _exit的声明#include <linux/kernel.h> 包含内核提供的各种函数,如printk

五、内核模块的打印方法:

带日志级别的 printk(推荐):内核打印支持 日志级别(控制信息的重要程度),格式为:


printk(KERN_DEBUG "格式化字符串", 参数...);

常见日志级别(数值越小,优先级越高):

KERN_EMERG(<0>):系统紧急状态(如崩溃),必须立即处理。
KERN_ALERT(<1>):需要立即响应的警报。
KERN_CRIT(<2>):严重错误(如硬件故障)。
KERN_ERR(<3>):普通错误(如函数执行失败)。
KERN_WARNING(<4>):警告信息(如可能的问题)。
KERN_NOTICE(<5>):正常但值得注意的信息。
KERN_INFO(<6>):普通信息(如模块加载 / 卸载提示)。
KERN_DEBUG(<7>):调试信息(仅调试时使用)。

简化宏:建议在模块中使用,例如pr_emerg(),对应KERN_EMERG,其他的用到可以查;常用的就是pr_info();

六、内核模块涉及到的操作命令(掌握)

1,lsmod (list module),打印当前内核中已经加载的内核模块列表2,insmod (install module),把一个内核模块加载到内核中。用法 insmod xxx.ko3,modinfo(module information),打印出一个内核模块的一些基本信息,这些信息由内核模块提供。 modinfo xxx.ko4,rmmod (remove module),从运行的内核中卸载一个内核模块,用法 rmmod xxx 或者rmmod xxx.ko5,depmod (dependency modules),用于生成内核模块的依赖关系列表,它通过分析 /lib/modules/kernel-release目录中的内核模块,创建一个类似Makefile的依赖文件,名称为modules.dep,这些模块通常来自配置文件中指定的目录。6,modprobe (model probe),modeprobe和insmod都是加载内核模块,区别是modprobe能够处理module加载时的依赖问题。比如,要加载一个 A 模块,但是 A 模块依赖于 B 模块,如果用insmod加载 A 模块就会出现加载错误信息,如果用modprobe加载 A 模块,就能够知道要先加载 B 模块后,才能加载 A 模块。
在使用modprobe加载内核模块之前 ,先要做两件事:(1)将内核模块拷贝到 /lib/modules/<内核版本> 目录下,(2)运行depmod命令,让内核读取/lib/modules/<内核版本> 目录下的所有模块,(3)运行modprobe 加载内核模块:sudo modprobe my_mod
sudo dmesg:查看日志记录;sudo dmesg -C:清空日志信息,方便看到调试信息;

七、内核模块支持的参数传递:

1、内核模块支持参数的类型:

1、基本类型:字符型(char), 布尔型(bool),整型(int),长整型(long),短整型(short),无符号整型(unsinged),字符指针(charp 内核提供了字符串分配,即char*,bool类型的相反类型(invbool)2、数组(Array)3、字符串(string)

2、内核模块中定义参数的方法

普通参数:module_param (name, type, perm);
name : 内核模块中变量的名称,同时又是用户向内核模块传入参数时所使用的参数名称。type : 如上面所述的基本类型perm:该参数设置了内核模块参数的访问权限,内核模块中的参数可以在 sysfs 文件系统中看到,权限就是用户可以在sysfs文件系统中的访问权限。
数组参数:module_param_array ( name, type, nump, perm);
name : 内核模块程序中的数组名称,同时又是用户向内核模块传入参数时所使用的名称。type : 数组的类型,如:int型,char 型。nump:是一个指向整数的指针(通常定义为 static int num,然后传 &num)。当用户传递数组参数时,内核会自动计算实际传递的元素个数,并将该数值写入 nump 指向的内存。★★注意:在用 insmod 给模块中数组传参的时候,不能用花括号赋值,要有逗号隔开输入;
★ module_param_string 的权限参数:控制用户空间通过 /sys 文件系统访问该参数的权限,与内核模块间的符号共享无关。EXPORT_SYMBOL:控制该参数能否被其他内核模块引用,与用户空间的访问权限无关。二者是独立的机制,权限设置不影响符号导出的有效性,导出的符号也不依赖于 /sys 文件的权限。


//普通参数示例:
static int mode = 1;
module_param(mode, int, S_IRUGO);	//S_IRUGO 表示 “所有用户(所有者、组、其他)都有读权限”,对应的八进制权限为 0444(r--r--r--)。

//数组参数示例:
static int array[5]; // 最多接收5个元素;
static int array_size;  // 实际接收的元素数量
module_param_array(array, int, &array_size, 0644);

/*
S_I : 只是一个前缀
R : 可读
W : 可写
X : 可执行
U : USR,所属用户
G : GROUP, 所属用户组
O : 其他用户

另外一种设置方式:module_param(mode, int, 0644);  // 权限掩码 0644:u=rw, g=r, o=r(符合修正后需求)
用户(u):6 → rw(读 + 写);
组(g):4 → r
其他(o):4 → r(读)。

这里给参数加了static,你可能会担心无法通过module_param暴漏给用户空间:
- module_param 宏的工作原理是通过符号表和内核参数系统将变量导出到 /sys 接口,与变量是否为 static 无关:
- static 仅限制变量在编译期的可见性(文件内);
- module_param 是在运行期通过内核机制将变量暴露给用户空间(通过 /sys 文件),不受 static 影响。
- 因此,static int mode; module_param(mode, int, S_IRUGO); 完全能正常工作:变量在模块内部可访问,同时能通过 /sys 接口被用户空间读取(符合 S_IRUGO 权限)。
*/

3、内核模块参数与sys文件系统

/sys/文件夹称为sysfs文件系统。1,在具有参数的内核模块加载成功后,在/sys/module/下会生成以<内核模块>命名的子目录,同时在/sys/module/<内核模块>/parameters目录下会生成以内核模块参数名命名的文件,文件内容则为内核模块参数的值。2,当具有参数的内核模块从内核中卸载后,在/sys/module/<内核模块>子目录也会被内核清除掉。以下面的“案例三”为例子,可以查看其中的参数,下面的例子调用的时候,没有给参数赋值,所以用的是默认参数;
ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调因此可以通过sysfs文件系统修改模块参数,这就引出了 模块参数回调机制
ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

八、模块参数回调机制

ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

回调机制本质: “内核在特定事件发生时,自动调用我们预先注册的函数”。在 模块参数 场景中,“特定事件” 指**“用户修改参数”** 或 “用户读取参数”,而我们注册的 param_set_value(set 回调)和 param_get_value(get 回调)就是被自动调用的函数。

1、什么内核模块中回调机制的作用:

在 Linux 内核模块中,模块参数回调机制是指当模块参数的值被修改时(无论是加载时传递、运行时通过 /sys 文件系统修改),内核会自动触发一个预先注册的回调函数,用于对参数值进行验证、转换或执行相关逻辑。这一机制确保了参数修改的合法性和安全性,避免无效值或恶意值对模块或内核造成影响。

为什么需要回调机制?

模块参数的默认行为是 “直接赋值”,但实际场景中往往需要对参数值进行控制,例如模块参数发生修改时,要校验新输入的模块参数是否正确,还有就是模块参数发生修改之后,要更新模块内部状态,回调机制正是为了这些问题设计的。

回调函数的作用

set 回调:当用户修改参数值时(如 insmod 传参、echo 值 > /sys/…/param),内核会调用此函数。功能:验证参数合法性(如范围、格式),若合法则更新模块内的变量;若不合法则返回错误(参数值不生效)。get 回调:当用户读取参数值时(如 cat /sys/…/param),内核会调用此函数。功能:将模块内的变量值转换为字符串,写入用户提供的缓冲区(供用户空间读取)。

注意:Linux 内核在绝大多数架构/配置下 默认使用 GNU89(C89 )标准编译;

1、可能会出现的错误:

C89 只允许在“语句块开头”声明变量;


// 下面的写法是C99的写法会报错:
static int __init parameter_init(void)
{
    pr_info("choice:%d, hello, kernel module!
", choice);
    for (int i = 0; i < array_size; i++)
        printk(KERN_DEBUG "%d
", array[i]);
    return 0;
}
//c89正确写法:
static int __init parameter_init(void)
{
    int i;

    pr_info("choice:%d, hello, kernel module!
", choice);
    for (i = 0; i < array_size; i++)
        printk(KERN_DEBUG "%d
", array[i]);
    return 0;
}

具体问题解决:

案例一:编写测试单文件内核模块

说明:下面内核源码创建,以及内核编译,包括内核加载和卸载,内核运行日志打印都是在虚拟机环境下运行的;查看虚拟机上linux版本的指令:uname -r【我的linux版本:5.15.0-139-generic】

1、代码部分

在虚拟机中,新建目录hello_module;创建hello_mod.c文件


#include <linux/module.h>
#include <linux/init.h>

/* 入口函数, 当内核模块被加载到内核中时, 内核会调用内核模块中的入口函数
* 返回值: 调用成功返回0, 返回小于0表示出错
*/
int __init hello_mod_init(void)
{
    printk("%s success
", __func__);
    return 0;
}

/* 出口函数, 当内核模块从内核中卸载时, 内核会调用内核模块中的出口函数*/
void __exit hello_mod_exit(void)
{
    printk("%s bye
", __func__);
}

/*声明 hello_mod_init 函数是模块的入口函数*/
module_init(hello_mod_init);

/*声明 hello_mod_ext 函数是模块的出口函数*/
module_exit(hello_mod_exit);

/*声明此模块的版权遵循GPL协议 */
MODULE_LICENSE("GPL");
MODULE_VERSION("0.0.1");
MODULE_AUTHOR("Spark.Hao");
MODULE_DESCRIPTION("THis is a demo for module.");
MODULE_ALIAS("TestHello");

创建Makefile文件


ifeq ($(KERNELRELEASE), ) # 判断KERNELRELEASE)是否为空, 阶段1:用户执行make时,先进入这里
   # 1. 定义内核源码路径:优先用环境变量KERNELDIR,没有则自动获取当前系统内核的headers路径
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build		
	# 2. 定义当前模块所在目录(即你的t1.c、sort.c所在目录)
	PWD := $(shell pwd)
modules:# 目标:编译模块(用户执行make时,默认执行第一个目标,这里是modules)
        # 关键命令:调用内核Makefile编译
        # -C $(KERNELDIR):进入内核源码目录,先执行内核的Makefile
        # M=$(PWD):告诉内核Makefile“模块源码在PWD目录(当前目录)”
        # $@:代表目标名“modules”,即告诉内核Makefile“要执行编译模块的操作”
	$(MAKE) -C $(KERNELDIR) M=$(PWD) $@
else
	obj-m:=hello_mod.o
endif

clean:
	rm -rf *.o *.mod.c *.mod.o *.ko *.symvers *.order *.a *.mod .*.*.cmd

KERNELRELEASE 是内核 Makefile 定义的变量,存储当前内核的版本信息(如 5.15.0-139-generic)。它的关键作用是区分编译的 “两个阶段”:
阶段 1(用户空间触发):当你写好自己的内核模块代码时(.c、Makefile)在终端执行 make 时,系统先读取你的 Makefile。此时 KERNELRELEASE 未被定义(因为还没进入内核编译环境),会执行 ifeq ($(KERNELRELEASE), ) 内部的逻辑;阶段 1 干了一件事:把 “编译控制权交给内核”—— 通过 $ (MAKE) -C $ (KERNELDIR) M=$ (PWD) modules 命令,让内核的 Makefile 来负责实际编译。阶段 2是内核空间实际编译(KERNELRELEASE 已定义)
当执行 $ (MAKE) -C $ (KERNELDIR) … 后,会进入内核源码目录,此时内核 Makefile 会:定义 KERNELRELEASE;回到你的模块目录(M=$ (PWD)),再次读取你的 Makefile;此时 KERNELRELEASE 已存在,执行 else 内部的逻辑。至于hello_mod.o如何编译,不用你操心,因为此时是内核在掌管如何编译,内核会用统一的 “内核模块编译规则” 处理所有 .c 文件到 .o 文件的生成。方式如下:


# 内核 Makefile 内置的规则(你无需手动写)
%.o: %.c
    $(CC) $(KERNEL_CFLAGS) -c $< -o $@

2、编译测试部分:

用make编译编译之后现清空日志记录(为了我们的内核文件加载时的信息能被清楚看到):sudo dmesg -C # 大写 C:clear终端执行:insmod hello_test.ko,加载我们自己制作的内核模块;dmesg 查看日志记录【可以看到内核模块的入口程序】终端执行:rmmod hello_test,将加载进内核的模块卸载;dmesg 查看日志记录【可以看到内核模块的出口程序】
ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

案例二:编写测试多文件内核模块

写一个内核模块,由两个c文件构成,分别是t1.c和sort.c,在t1.c中调用sort.c,把t1.c中的一个3个元素的整形数组排序,打印;

1、文件路径:

~/test_two_file_ker/
├── t1.c
├── sort.c
└── Makefile

2、源码


// sort.c
/* 内核空间排序实现 */
#include <linux/module.h>  // 必须包含,定义了 EXPORT_SYMBOL 宏

// static void sort_int3(int *a) // 这么定义,在make时会一直报错,C 语言中,static 修饰的函数是 “文件内私有” 的,只能在当前 .c 文件中使用,其他文件无法访问,更无法通过 EXPORT_SYMBOL 导出。这就是 modpost 提示 “sort_int3 undefined” 的根本原因。
void sort_int3(int *a)
{
    int i, j, tmp;
    for (i = 0; i < 2; i++) {
        for (j = 0; j < 2 - i; j++) {
            if (a[j] > a[j + 1]) {
                tmp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = tmp;
            }
        }
    }
}
EXPORT_SYMBOL(sort_int3);   /* 导出符号,供 t1.c 使用 */

// t1.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

/* 声明外部函数 */
extern void sort_int3(int *a);

static int __init t1_init(void)
{
    int data[3] = { 42, 17, 23 };

    pr_info("t1: original  %d %d %d
", data[0], data[1], data[2]);
    sort_int3(data);
    pr_info("t1: sorted    %d %d %d
", data[0], data[1], data[2]);
    return 0;
}

static void __exit t1_exit(void)
{
    pr_info("t1: module unloaded
");
}

module_init(t1_init);
module_exit(t1_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yourname");

# Makefile,
ifeq ($(KERNELRELEASE), )                                               #如果$(KERNELRELEASE)没有值,就执行ifeq中的语句
        KERNELDIR ?= /lib/modules/$(shell uname -r)/build               #定义了一个变量并赋值为linux源码路径
        PWD := $(shell pwd)
modules:
        $(MAKE) -C $(KERNELDIR) M=$(PWD) $@
else
        obj-m := ksort.o
        ksort-objs := t1.o sort.o 
        # 内核的Makefile 中:obj-m := ksort.o → 定义最终生成的模块名为 ksort(对应 ksort.ko);因此必须用 ksort-objs 来指定该模块由哪些 .o 目标文件组成(如 t1.o、sort.o)。
        # 告诉内核 “ksort.ko 不是由单个文件编译,而是由 t1.o 和 sort.o 两个目标文件链接而成”—— 这就是解决 “sort_int3 未定义” 的关键:明确多文件合并为一个模块。
endif

clean:
        rm -rf *.o *.mod.c *.mod.o *.ko *.symvers *.order *.a *.mod .*.*.cmd

上面这个 Makefile 是多文件 Linux 内核模块编译的经典模板,核心逻辑是利用 KERNELRELEASE 变量区分 “用户空间编译触发” 和 “内核空间实际编译” 两个阶段,完美适配 Linux 内核的模块化编译机制。

3、测试

切换到Makefile所在文件夹:cd ~/ksort执行:make # 生成 ksort.ko

ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

清空内核日志:sudo dmesg -C测试结果:会用到的命令有 insmod、rmmod

ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

案例三:测试内核模块传参

1、模块文件结构

ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

2、源码

parameter_test.c文件


#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

// 普通参数:
static int choice = 1;
module_param(choice,int,0664);
// 字符串参数:
static char * p = "char string";
module_param(p,charp,0664);
// 数组参数:
static int array[5]; // 最多接收5个元素;
static int array_size=0;  // 实际接收的元素数量,一般把初始值设置为0;
module_param_array(array, int, &array_size, 0644);


static int __init parameter_init(void)
{
    int i; //变量要定义在前面
    pr_info("choice:%d,hello,kernel module!
",choice);
    for(i=0;i < array_size;i++)
    {
        printk(KERN_DEBUG "%d
",array[i]);
    }
    return 0;  // 返回0表示初始化成功
}

static void __exit parameter_exit(void)
{
    pr_info("p:%s Goodbye,kernel module!
",p);
}

module_init(parameter_init);
module_exit(parameter_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("BRUSH");
MODULE_DESCRIPTION("TEST HOW TO USE PARAMENT");
MODULE_VERSION("1.0");

Makefile


ifeq ($(KERNELRELEASE), )                   				#如果$(KERNELRELEASE)没有值,就执行ifeq中的语句
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build		#定义了一个变量并赋值为linux源码路径
	PWD := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) $@
else
	obj-m:=parameter_test.o
endif

clean:
	rm -rf *.o *.mod.c *.mod.o *.ko *.symvers *.order *.a *.mod .*.*.cmd

3、通过make编译之后,测试效果:

ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

案例四:测试回调函数

下面是一个极简的模块参数回调函数示例,实现一个带范围验证的整数参数(只能是 0-100 之间的值),包含最核心的 set 回调(验证参数合法性)和 get 回调(读取参数值):

1、模块文件结构

ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

2.0、源码

test_calback.c


#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>  // 模块参数相关宏
#include <linux/kernel.h>
#include <linux/errno.h>        // 错误码定义

// 模块内部的参数变量(默认值 50)
static int value = 50;

// set 回调:修改参数时触发,验证值是否在 0-100 之间
// const char *buf:用户输入的参数值(以字符串形式传递)。
// const struct kernel_param *kp:内核传递的参数描述结构体,里面包含了模块内部变量的地址(即 value 变量的地址)。
static int param_set_value(const char *buf, const struct kernel_param *kp)
{
		// 用户输入的是字符串(比如 "80"),需要先转换成整数才能验证范围,new_val 就是用来存转换后的结果。
    int new_val;
    // kstrtoint 是内核提供的工具函数,作用是把字符串(buf)转换成整数(new_val)。
    if (kstrtoint(buf, 0, &new_val) != 0) {
        pr_err("参数格式错误!必须是整数
");
        return -EINVAL;  // 转换失败,返回错误
    }
    // 验证范围:必须在 0-100 之间
    if (new_val < 0 || new_val > 100) {
        pr_err("参数值必须在 0-100 之间!你输入了 %d
", new_val);
        return -EINVAL;  // 范围错误,返回错误
    }
    // 验证通过,更新参数值(kp->arg 指向模块内的 value 变量)
    /*
    多参数绑定同一个set时。
    - 你注册多少个参数(比如val1、val2),内核就会为每个参数创建一个  kp  实例;
    - 每个 kp 里存着该参数的关键信息: name (参数名,如"val1")、 arg (对应变量地址,如&val1)、 ops (绑定的set/get回调);
		*/
    *((int *)kp->arg) = new_val; // 
    pr_info("参数更新成功!新值:%d
", new_val);
    return 0;  // 成功
}

// get 回调:读取参数时触发,返回当前值
static int param_get_value(char *buf, const struct kernel_param *kp)
{
    // 把当前 value 的值转换成字符串,写入 buf(供用户空间读取)
    return sprintf(buf, "%d", *((int *)kp->arg));
}

// 【修正】使用 struct kernel_param_ops 定义回调操作集
/*
struct kernel_param_ops 是内核定义的 “回调操作集” 结构体,专门用于绑定 set 和 get 函数,相当于给内核一个 “操作手册”:“当用户操作参数时,按这些函数执行”。
*/
static const struct kernel_param_ops value_ops = {
    .set = param_set_value,  // 告诉内核:修改参数时调用这个函数
    .get = param_get_value,  // 告诉内核:读取参数时调用这个函数
};

// 【修正】注册带回调的参数:第二个参数是 ops 结构体,而非 kernel_param,让内核知道回调的存在
/*
module_param_cb是回调函数的注册宏,是内核定义的宏,而非函数,其核心作用是将回调函数(set/get)与特定的模块参数绑定,并向内核注册该参数的回调行为。
1、这行代码的作用是 “向内核注册一个名为 val 的参数,并关联我们定义的回调操作集 value_ops”。
2、内核会记录:“参数 val 的修改和读取,由 value_ops 里的 set 和 get 函数处理”。
3、第四个参数 0644 是 /sys 目录下参数文件的权限(控制用户空间的访问权限)。
*/
module_param_cb(val, &value_ops, &value, 0644);

// 模块加载函数
static int __init demo_init(void)
{
    pr_info("模块加载成功!当前 val 值:%d
", value);
    return 0;
}

// 模块卸载函数
static void __exit demo_exit(void)
{
    pr_info("模块卸载成功!最后 val 值:%d
", value);
}

module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");  // 必须声明许可证

Makefile


ifeq ($(KERNELRELEASE), )                   				#如果$(KERNELRELEASE)没有值,就执行ifeq中的语句
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build		#定义了一个变量并赋值为linux源码路径
	PWD := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) $@
else
	obj-m:=test_calback.o
endif

clean:
	rm -rf *.o *.mod.c *.mod.o *.ko *.symvers *.order *.a *.mod .*.*.cmd

2.1、★★★ 模板参数回调函数小结:

**总结1:**编写模块参数回调函数的流程:
ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

**总结2:**针对每个参数,内核会单独检查是否注册了回调函数,有则触发对应的 set 或 get,无则使用默认行为,每个参数的回调函数是独立注册的,彼此互不影响。可以将多个参数的回调函数写在同一个回调函数中,通过在函数内部区分不同参数(例如通过比较 kp->arg 与参数变量的地址)来执行针对性的逻辑。(这种方式,注册的时候,就要写三个回调函数的注册宏,绑定同一个set和get)

3、通过make编译之后,测试加载时输入非法参数,验证 set 回调的验证响应

ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

4、在输入正常参数值,加载成功时,尝试在模块运行时修改参数,验证 set 回调的动态响应

ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

5、读取参数,测试get回调

ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

案例五:测试内核模块互相调用

建立两个模块 m_call 和 m_called , 在 m_call 中定义一个模块参数数组,调用模块 m_called 中的排序函数,实现排序;

add_需要用到的一种机制:symbol机制:

内核中的 symbol(符号)机制,本质是模块间共享函数 / 变量的 “桥梁”,让一个模块能安全调用另一个模块的代码或访问其数据。内核中的 “符号”(Symbol),可以理解为 函数或变量的 “身份证”,包含两层信息,【符号名:函数名、变量名】【符号地址:函数或变量在内存中的实际地址(内核加载模块时分配)】内核模块默认是 “隔离” 的 —— 一个模块不能直接访问另一个模块的函数 / 变量,除非后者主动将符号 “导出” 到全局符号表。内核会维护一个 “全局符号表”(类似通讯录),记录所有已加载模块导出的符号(函数 / 变量),供其他模块查询和使用。

ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

1、模块文件结构

ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

2、模块m_call中的源码

m_call.c


#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

// 定义数组
int array[5];
int count=0;
module_param_array(array,int,&count,0664);

// 声明外部函数
extern void array_sort(int* p,int len);
// 定义函数指针,接受外部被加载的模块中的函数
void (*sortp)(int*,int);

// 模块入口定义
static int __init call_init(void)
{
    sortp = symbol_get(array_sort);
    sortp(array,count);
    return 0;
}

// 模块出口定义
static void __exit call_exit(void)
{
    /* 
    1、由于模块入口调用了get,增加了符号array_sort在内核中的引用计数;
    2、在这个函数出口处调用put,减少array_sort的引用计数;
    3、只有如此,才能确保在卸载了这个模块之后,被调用模块也可以被正确卸载;
    */
    if(sortp)
    {
        symbol_put(array_sort);
        sortp=NULL; 
    }
    pr_info("GOODBYE!
");
}
// 模块入口和出口声明
module_init(call_init);
module_exit(call_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("BRUSH");

Makefile


ifeq ($(KERNELRELEASE), )                   				#如果$(KERNELRELEASE)没有值,就执行ifeq中的语句
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build		#定义了一个变量并赋值为linux源码路径
	PWD := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) $@
else
	obj-m:=m_call.o
endif

clean:
	rm -rf *.o *.mod.c *.mod.o *.ko *.symvers *.order *.a *.mod .*.*.cmd

3、模块m_called中的源码

m_called.c


#include <linux/module.h>
#include <linux/kernel.h>

void array_sort(int* p,int len)
{
    int i,j,f,max;
    for(i=0;i<len-1;i++)
    {
        max = p[0];
        f = 0;
        for(j=0;j<len-i;j++)
        {
            if(p[j]>max)
            {
                f = j;
                max = p[j];
            }
        }
        p[f] = p[len-1-i];
        p[len-1-i] = max;
    }
    for(i=0;i<len;i++)
    {
        printk(KERN_DEBUG "%d
",p[i]);
    }
}
EXPORT_SYMBOL(array_sort);

MODULE_LICENSE("GPL");
// 关键:添加 GPL 许可证声明,与 m_call 模块兼容
MODULE_LICENSE("GPL");
MODULE_AUTHOR("BRUSH");  // 可选:添加作者信息

Makefile


ifeq ($(KERNELRELEASE), ) #如果$(KERNELRELEASE)没有值,就执行ifeq中的语句
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build		#定义了一个变量并赋值为linux源码路径
	PWD := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) $@
else
	obj-m:=m_called.o
endif

clean:
	rm -rf *.o *.mod.c *.mod.o *.ko *.symvers *.order *.a *.mod .*.*.cmd

4、测试结果:

先用 make 分别编译 m_call 和 m_called;首先先把被调用模块加载进内核,然后才能编译调用它的模块,否则 m_call 编译时就会报 undefined symbol——这是内核模块依赖的基本规则。

ARM《9》_在linux中编写内核模块(单.c文件、多.c文件)、内核模块传参(传参、回调)、内核模块互调

© 版权声明

相关文章

暂无评论

none
暂无评论...