什么是 Linux 模块
Linux模块,也就是可加载内核模块(LKMs),允许在运行时动态加载到内核中。
这说明两点:
是内核模块, 也就是说是内核的一部分, 只能依赖内核的接口, 且必须遵循内核的规则.
运行时可加载, 这避免了重复编译内核和重启系统. 并且内核和模块是分开的, 部署更灵活. 同时也可以只在需要的时候加载模块, 节省内核的内存占用.
开发模块时常用的命令
insmod module.ko # 加载模块
rmmod module.ko # 卸载模块
modprobe module.ko # 智能加载模块, 会自动解决依赖
lsmod # 查看已加载的模块
modinfo module.ko # 查看模块信息
depmod -a # 更新模块依赖
dmesg # 查看内核日志
uname -r # 查看内核版本
如何实现一个 Linux 模块
编写模块代码
#include
#include
// 不能依赖c库, 只能使用内核提供的接口
MODULE_LICENSE("Dual BSD/GPL"); // 必须有,否则报错. 模块许可证,许可可选
static int __init myinit(void) // 必须有,在模块加载时调用
{
printk("Hi module!\n");
return 0;
}
static void __exit myexit(void) // 必须有,在模块卸载时调用
{
printk("Bye module!\n");
}
module_init(myinit); // 必须有,指定 这是模块加载时调用的函数
module_exit(myexit); // 必须有,指定 模块卸载时调用的函数
模块参数
在模块代码中加入如下代码:
#include
int param = 0;
// module_param(变量名, 类型, 权限)
// 类型可以是byte, hexint, short, ushort, int, uint, long, ulong, charp, bool, invbool
// module参数会在/sys/module/模块名/parameters/下生成一个文件, 权限就是设置这个文件的权限, 管理谁可以读取修改这个参数.
module_param(param, int, S_IRUGO);
int arrParam[3];
int arrNum = 3; // 有两作用, 1. 初始值用来限制用户传入的数组个数 2. 会被更改为实际传入的数组个数
module_param_array(arrParam, int, &arrSize, S_IRUGO); // 传入数组
在加载模块时可以传入参数, 例如insmod module.ko param=1 arrParam=1,2,3
权限相关的宏
说明
S_IRUSR
所有者可读
S_IRGRP
组可读
S_IROTH
其他可读
S_IWUSR
owner可写
S_IRWXUGO
所有者,组,其他可读写执行
...
格式就是S_I[RWX][UGO]
编写 Makefile
obj-m := module.o // 表明有一个模块要从module.o建立
module-objs := file1.o file2.o // 表明module.o的依赖, file1.o会自动根据名字找到对应的源文件file1.c(其他后缀的情况我不清楚)生成. 如果一个模块只由一个源文件生成, 可以直接写成obj-m := file1.o
#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
#complie object
all:
make -C $(LINUXKERNELPATH) M=$(CURRENT_PATH) modules
# -C 改变目录到指定内核目录, M= 指定模块所在目录
#clean
clean:
make -C $(LINUXKERNELPATH) M=$(CURRENT_PATH) clean
编译模块有几个前提:
编译模块实际使用的是内核的Makefile, 所以必须要先下载对应的内核代码并编译成功.
保证你有版本足够新的编译器, 模块工具, 以及其他必要工具. 在内核Documentation/Changes 列出了需要的工具版本
加载/卸载模块
insmod module.ko
rmmod module.ko
modprobe module.ko # 与insmod的区别是如果要加载的模块引用了内核中为定义的符号,modprobe会在当前模块搜索路径中寻找其他模块
Linux 模块是如何工作的
https://www.cnblogs.com/fanzhidongyzby/p/3730131.html
.ko文件是什么
本质是一个ELF文件, 和.o文件差不多,都是REL(relocatable file), 只是多了一些元数据,比如模块许可证, 然后把多个.o打包成一个.ko, 还多了导出的符号表.
模块加载的过程
模块加载时会使用一个系统调用sys_init_module, 在内核代码中使用SYSCALL_DEFINE3(init_module,...)来定义这个系统调用.
主要代码是
err = copy_module_from_user(umod, len, &info);
...
return load_module(&info, uargs, 0);
copy_module_from_user是把用户空间的模块数据拷贝到内核空间, 这部分内容需要理解内存管理机制.最终会把模块数据放到info中.
load_module涉及到几个部分, ELF解析,内存管理等, 暂时不深入.
怎么把用户空间数据copy到内核空间
补充: 内核模块和用户程序的区别
只连接到内核, 能够调用的唯一的函数是内核输出的那些.
错误处理
用户空间和内核空间的不同
内核的并发, 所以内核代码必须是可重入的--能在多个上下文中同时运行
应用程序存在于虚拟内存中, 有一个非常大的堆栈区. 内核, 相反, 有一
个非常小的堆栈,并和所有内核空间调用链共享;
小心使用__开始的函数, 如果你调用这个函数, 确信你知道你在做什么.
内核代码不能做浮点运算, 使能浮点将要求内核在每次进出内核空间的时候保存
和恢复浮点处理器的状态, 增加了不必要的开销.
linux 内核头文件提供了方便来管理你的符号的可见性, 如果需要输出符号给其他模块使用需要使用EXPORT_SYMBOL()/EXPORT_SYMBOL_GPL()宏._GPL表示只给GPL许可证的模块使用.