Linux内核模块是一段单独编译的内核代码,它在Linux内核空间运行,在需要时被加入内核,在不需要时也可从内核中卸载
一个内核模块通常包括以下几个部分
模块加载函数在模块加载阶段运行,它通常用于初始化内核模块中所需的资源,其原型为“int __init init_module(void)”(其中”__init“表示它只在初始化阶段运行一次,编译器在处理时会将其链接到特定的代码段中,在模块加载完成后释放此内存),此函数返回0表示执行成功,它通常需要采用宏定义“module_init”进行导出。
”module_init“宏用于导出内核模块的加载函数,因为内核模块由两种编译方式,分别是编译为独立模块和与内核核心代码编译到一起
当编译为一个独立的模块时”module_init“宏展开如下:
#define module_init(initfn) \
/* 用于校验模块函数类型,当函数不是int init_module(void)无法编译通过 */ \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
/* 给入口函数取别名为init_module模块加载时通过别名调用加载函数 */ \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
当与内核编译到一起时”module_init“宏展开如下:
#define module_init(x) __initcall(x);
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall(fn, 6)
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
/* 对上面宏进行展开 */
#define module_init(x) \
/* 在”.initcall6.init“代码段定义一个”__initcall_##x6“的变量,用于存储加载函数地址 */ \
/* 系统初始化时遍历此代码段,找到模块加载函数并执行 */ \
static initcall_t __initcall_##x6 __used \
__attribute__((__section__(".initcall6.init"))) = x
模块卸载函数在模块被卸载的时候执行,它通常用于清理模块初始化时创建或注册的资源,其原型为”void __exit cleanup_module(void)“(”__exit“表示此函数只在模块卸载时执行一次,当与内核编译到一起时,编译器不会对此函数进行链接),它通常需要采用宏定义“module_exit”进行导出。
当编译为一个独立的模块时”module_exit“宏展开如下:
#define module_exit(exitfn) \
/* 检查函数类型 */ \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
/* 为模块卸载函数取别名,模块卸载时通过别名调用 */ \
void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));
当与内核编译到一起时,因为无需对内核模块进行卸载,所以”module_exit“宏无实际意义。
内核模块必须通过”MODULE_LICENSE“声明开源协议类型,通常采用”MODULE_LICENSE(“GPL”)“,表示执行GPL开源协议,如果不正确声明开源协议类型则无法正常访问内核中通过”EXPORT_SYMBOL_GPL“导出的函数或者变量
常用的模块信息声明还有:
insmod 加载内核模块,必须先加载此模块所依赖的其他模块
modprobe 自动加载内核模块,可以自动处理内核模块将的依赖关系,在使用此命令前需要对模块进行安装操作
rmmod 卸载模块,必须要先卸载依赖于此模块的其他模块
modinfo 查看模块的详细信息
depmod 查看模块的依赖关系
//包含必要头文件
#include
#include
#include
//模块加载函数,在模块加载时会被调用一次
//__init 表示此函数只在模块加载时被调用一次
//返回0,表示成功,失败通常返回一个负值
static int __init driver_init(void)
{
printk("module test\r\n");
return 0;
}
//模块卸载函数,在模块卸载时会被调用一次
//__exit 表示此函数只在模块卸载时被调用一次
static void __exit driver_exit(void)
{
printk("module exit\r\n");
}
//导出模块加载函数
module_init(driver_init);
//导出模块卸载函数
module_exit(driver_exit);
//表示遵循GPL开源协议
MODULE_LICENSE("GPL");
//模块作者信息
MODULE_AUTHOR("csdn");
//模块描述
MODULE_DESCRIPTION("module test");
//模块别名
MODULE_ALIAS("module");
以下是一个在内核源码树外编译内核模块的Makefile
ifeq ($(KERNELRELEASE),)
#第一次执行时 KERNELRELEASE 为空,执行此分支
#内核源码目录,需要先编译内核源码,在编译源码外的内核模块
#必须使用编译内核模块时编译出来的内核,否则可能会出现内核模块加载失败
KERNELDIR ?= /home/lf/workspace/source/my_source/linux/linux-5.4.31
#NFS跟文件系统目录
ROOTFS ?= /home/lf/workspace/rootfs
#当前目录
PWD := $(shell pwd)
#$(MAKE) 相当于 make
#-C $(KERNELDIR) 执行 KERNELDIR 目录的Makefile
#M=$(PWD) 内核源码树之外的一个目录
#modules 只编译模块
module:
$(MAKE) -C $(KERNELDIR) ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- M=$(PWD) modules
install:
# 因为采用insmod加载模块,所以拷贝到NFS跟文件系统的root目录即可
# cp *.ko $(ROOTFS)/root/
# 若采用modprobe加载模块,则需要进行安装操作
$(MAKE) -C $(KERNELDIR) ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
rm -rf *.o .*.cmd *.mod.* *.mod modules.order Module.symvers .tmp_versions
else
#当从源码目录的 Makefile 再次进入时 KERNELRELEASE 已经赋值,执行此分支
#obj-m 表示编译成模块
obj-m := module_test.o
endif
在内核源码树中编译一遍Linux内核
从”https://gitcode.net/lf282481431/linux_driver_demo/-/tree/master/01-kernel_module/01-module“下载源码
修改makefile文件中的”KERNELDIR“和”ROOTFS“,”KERNELDIR“为内核源码树跟目录,”ROOTFS“为NFS跟文件系统跟目录
根据需求执行”make copy“或“make install”,”make copy“用于将编译出来的模块拷贝到NFS跟文件系统的root目录,以方便采用”imsmod“进行加载,“make install”则是将内核模块安装到跟文件系统中,以方便采用”modprobe“对模块进行加载
通过NFS挂在启动开发板
通过root用户进入开发板系统,并切换到root目录
执行”insmod test_module.ko“即可完成模块加载,加载过程中”driver_init“函数被执行,控制台输出”module init“字符串
执行”rmmod test_module.ko“命令,模块被卸载,卸载过程中”driver_exit“函数被执行,控制台输出”module exit“字符串