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

基础概念库:
- 编辑内核模块,如果在虚拟机的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!\n");return 0; // 返回 0 表示初始化成功,非0表示失败(模块加载失败)
}/* 模块退出函数:卸载模块时执行(rmmod时调用) */
static void __exit hello_exit(void)
{pr_info("Goodbye, Kernel Module!\n");
}/* 注册初始化/退出函数(内核规定的固定法写法) */
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) $@
elseobj-m:= <你的.c文件名> .o
endifclean: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.ko
- 3,modinfo(module information),打印出一个内核模块的一些基本信息,这些信息由内核模块提供。 modinfo xxx.ko
- 4,rmmod (remove module),从运行的内核中卸载一个内核模块,用法 rmmod xxx 或者rmmod xxx.ko
- 5,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