什么是 Linux 模块
Linux模块,也就是可加载内核模块(LKMs),允许在运行时动态加载到内核中。
这说明两点:
- 是内核模块, 也就是说是内核的一部分, 只能依赖内核的接口, 且必须遵循内核的规则.
- 运行时可加载, 这避免了重复编译内核和重启系统. 并且内核和模块是分开的, 部署更灵活. 同时也可以只在需要的时候加载模块, 节省内核的内存占用.
开发模块时常用的命令
insmod module.ko # 加载模块
rmmod module.ko # 卸载模块
modprobe module.ko # 智能加载模块, 会自动解决依赖
lsmod # 查看已加载的模块
modinfo module.ko # 查看模块信息
depmod -a # 更新模块依赖
dmesg # 查看内核日志
uname -r # 查看内核版本
如何实现一个 Linux 模块
-
编写模块代码
#include<linux/module.h> #include<linux/init.h> // 不能依赖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); // 必须有,指定 模块卸载时调用的函数
-
编写 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 模块是如何工作的
.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许可证的模块使用.