1. linux可加载内核模块机制(LKM)介绍
- linux模块机制 在不需要重新编译 和重启内核的情况下可以动态的加载或者卸载一个模块。
- 内核模块(*.ko)本质是一个 未链接的ELF可重定位文件,其代码中的内核符号引用在编译时未绑定地址。 加载时由内核模块加载器执行 动态重定位。
- 内核模块运行在内核态。运行效率和静态编译基本一致,同时又支持动态加载和卸载的特性。
2. 内核模块代码的组成 - 一个最简单的内核模块
-
一个简单的内核模块示例
c#include <linux/init.h> #include <linux/module.h> static int __init hello_init(void) { printk(KERN_EMERG "hello world!\n"); // dump_stack(); return 0; } static void __exit hello_exit(void) { printk(KERN_EMERG "goodbye world!\n"); // dump_stack(); } module_init(hello_init); /* 模块入口,加载模块时会盗用 */ module_exit(hello_exit); /* 模块出口,卸载模块时会调用 */ MODULE_LICENSE("GPL"); /* 模块许可证书,必须声明否则会产生kernel tainted警告 */ MODULE_AUTHOR("xxxxxxxxxx"); /* 作者信息 */ MODULE_DESCRIPTION("a kernel module demo"); /* 模块描述信息 */ MODULE_VERSION("V1.0"); /* 模块版本号 */ MODULE_ALIAS("module alias name: hello_demo"); /* 模块别名 */
-
Makefile
Makefile.PHONY: all clean #ifneq ($(KERNELRELEASE),) obj-m := hello.o #else EXTRA_CFLAGS += -DDEBUG KDIR := ../../linux-orangepi all: make -C $(KDIR) M=$(PWD) modules clean: make -C $(KDIR) M=$(PWD) modules clean #endif
-
上述Makefile是一个支持在内核外编译编译模块的Makefile,只需要指定内核路径即可(当前硬件运行的内核,必须与指定内核源码的路径一致,否则会造成内核污染),编译生成*.ko文件。可以通过如下命令动态的加载和卸载模块,如下是一些常用的路径。
insmod hello.ko
:安装一个模块rmmod hello
:卸载一个模块lsmod
:查看当前已安装模块modinfo hello.ko
:获得当前模块的信息
3. 向模块传递参数
我们在编写一个内核模块,如编写一个驱动程序时,通常需要向模块中传递一些参数,比如uart驱动的波特率等,又或者模块在调试阶段,可以通过参数对模块进行调试。
-
linux内核提供了如下向模块传递参数的宏
c/* * 向内核传递一个参数 * name:变量的名称 * type:变量的类型 * - bool:布尔型 * - inbool:布尔反值 * - charp:字符指针 * - short:短整型 * - ushort:无符号短整型 * - int:整型 * - uint:无符号整型 * - long:长整型 * - ulong:无符号长整型。 * perm:访问权限 * #define S_IRWXU 00700 /* 文件所有者 可读/可写/可执行 */ * #define S_IRUSR 00400 /* 文件所有者 可读 */ * #define S_IWUSR 00200 /* 文件所有者 可写 */ * #define S_IXUSR 00100 /* 文件所有者 可执行 */ * * #define S_IRWXG 00070 /* 与文件所有者同组的用户 可读/可写/可执行 */ * #define S_IRGRP 00040 /* 与文件所有者同组的用户 可读 */ * #define S_IWGRP 00020 /* 与文件所有者同组的用户 可写 */ * #define S_IXGRP 00010 /* 与文件所有者同组的用户 可执行 */ * * #define S_IRWXO 00007 /* 其他用户 可读/可写/可执行 */ * #define S_IROTH 00004 /* 其他用户 可读 */ * #define S_IWOTH 00002 /* 其他用户 可写 */ * #define S_IXOTH 00001 /* 其他用户 可执行 */ */ #define module_param(name, type, perm) /* * 向内核传递一个数组 * nump:数组长度 */ #define module_param_array(name, type, nump, perm) /* * 向内核传递一个字符串 * len:字符串长度 */ #define module_param_string(name, string, len, perm)
-
用法示例
c#include <linux/init.h> #include <linux/module.h> static int debug_level = 1; // 整型参数,默认值1 static char *device_name = "default"; // 字符串参数,默认值"default" static int ports[5]; // 存储整型数组 static int port_count; // 记录实际传入的元素数量 static char target_ip[16]; // 存储IP地址的缓冲区 module_param(debug_level, int, 0644); // 权限:用户可读写,其他用户只读 MODULE_PARM_DESC(debug_level, "Debug level (0=disable, 1=enable)"); module_param(device_name, charp, 0444); // charp表示字符指针,权限:所有用户只读 MODULE_PARM_DESC(device_name, "Target device name"); module_param_array(ports, int, &port_count, 0644); MODULE_PARM_DESC(ports, "Array of port numbers"); module_param_string(ip, target_ip, sizeof(target_ip), 0644); MODULE_PARM_DESC(ip, "Destination IP address"); static int __init hello_init(void) { printk(KERN_EMERG "hello world!\n"); printk(KERN_EMERG "Debug: %d, Device: %s\n", debug_level, device_name); for (int i = 0; i < port_count; i++) { printk(KERN_EMERG "Port[%d]: %d\n", i, ports[i]); } printk(KERN_EMERG "Target IP: %s\n", target_ip); return 0; } static void __exit hello_exit(void) { printk(KERN_EMERG "goodbye world!\n"); // dump_stack(); } module_init(hello_init); /* 模块入口,加载模块时会盗用 */ module_exit(hello_exit); /* 模块出口,卸载模块时会调用 */ MODULE_LICENSE("GPL"); /* 模块许可证书,必须声明否则会产生kernel tainted警告 */ MODULE_AUTHOR("xxxxxxxxxx"); /* 作者信息 */ MODULE_DESCRIPTION("a kernel module demo"); /* 模块描述信息 */ MODULE_VERSION("V1.0"); /* 模块版本号 */ MODULE_ALIAS("module alias name: hello_demo"); /* 模块别名 */
-
加载命令
bashinsmod my_module.ko debug_level=3 device_name="eth0" ports=80,443,8080 ip="192.168.1.1"
4. 内核模块符号的导出
-
c语言中的符号默认是全局符号,所以静态编译时全局符号默认会加入全局符号表。
-
模块是分开编译的,模块内的全局符号作用域止于模块,其他模块是不能使用。
-
linux内核提供了一种机制可以显示的导出全局符号,可供内核或者其他模块使用,使用是只需要将要导出的符号使用 如下宏声明即可:
c/linux-orangepi/include/linux/export.h /* 将符号 sym(函数或全局变量)导出到公共内核符号表 */ #define EXPORT_SYMBOL(sym) _EXPORT_SYMBOL(sym, "") /* 导出符号 sym,但仅限GPL兼容模块使用。非GPL模块加载时会因符号检查失败而拒绝链接 */ #define EXPORT_SYMBOL_GPL(sym) _EXPORT_SYMBOL(sym, "_gpl") /* 将符号 sym 导出到特定命名空间 ns,避免全局符号污染,增强模块化安全性。 */ #define EXPORT_SYMBOL_NS(sym, ns) __EXPORT_SYMBOL(sym, "", __stringify(ns)) /* 同时限制符号的命名空间和GPL许可证,提供最高级别的访问控制 */ #define EXPORT_SYMBOL_NS_GPL(sym, ns) __EXPORT_SYMBOL(sym, "_gpl", __stringify(ns))
-
模块导出的全局符号存储在
/lib/modules/6.1.43/build/Module.symvers
5. 将模块编译进内核
在编写一个内核模块时,通常会在内核外独立编译加载运行,当功能稳定后一般会考虑将其固话到内核,本节介绍将模块编译进内核的流程
-
修改Makefile
- 将编写的模块放到这个目录下
./linux-orangepi/drivers/char
(假设这个模块是一个字符驱动), - 在当前目录的Makefile下增加如下内容
obj-$(CONFIG_HELLO) += hello.o
:- 在linux内核的Makefile中,变量obj-y对应的值会静态编译,obj-n对应的值不会编译, obj-m对应的值会编译为一个模块
- CONFIG_HELLO 可在Kconfig 中增加一个配置项,用来决定当前模块 静态编译/不编译/编译为一个模块
- 将编写的模块放到这个目录下
-
修改Kconfig增加一个配置项
Makefileconfig HELLO tristate "A simplist kernel module: hello" default n help a kernel module demo: hello world
config HELLO
通过menuconfig
配置生成.config
,最终生成CONFIG_HELLO
被Makefile使用 -
编译并加载内核
-
通过menuconfig配置
-
编译内核
bashmake -j10
-
安装内核及模块
csudo make modules_install /* 安装内核模块 */ sudo make install /* 安装内核 */ sudo make dtbs_install INSTALL_DTBS_PATH=/boot/dtb/ /* 安装设备树文件到boot/btb中 */
-
重启 Linux 系统就会加载新编译的内核了
-