构造和运行模块
在尝试运行模块之前,需要使用合适的系统(通常是封闭的)实现内核原代码的相应实验
Hello world模块
- 模块构造/析构: 使用module_init/module_exit宏装饰相应函数,实现内核模块的装载/移除
- 许可证: 使用MODULE_LICENSE(" ")实现对许可证的装载;
- 模块的装载与移除: 装载insmod;移除rmmod
核心模块与应用程序的对比
- 应用程序: 执行单个任务;可以将资源释放扔给系统;可以使用广泛的库函数;可以使用调试器处理段错误。
- 模块: 注册以便服务请求-事件驱动;退出时需要撤销初始化函数所实现的一切;只能使用内核所实现的函数;内核错误会影响整个系统。
- 链接模块到内核过程(insmod): 其本质是一个modutiles模块应用程序,执行init_module函数
- 计算模块代码、模块名、module对象的内存大小
- 用户空间分配内存区,拷贝相应代码;init域指向入口函数分配地址;exit域指向出口函数分配地址;
- 调用init_module(linux/kernel/module.c的SYSCALL_DEFINE3),传递用户区内存区地址;
- 声明内核应用模块,并呼叫初始化函数
- 释放用户态内存
用户空间和内空间
- 操作系统的核心作用: 提供对计算机硬件的一致视图;并保证程序的独立操作和阻止资源的非法访问
- 模块化代码在内核空间运行,其作为系统调用的一部分
内核中的并发
- 核心思想: 在同一时刻,会有许多事情正在发生;
- 并发问题的来源: Linux系统的进程并发性;中断程序的异步性;软件抽象的异步性;SMP系统;可抢占的进程调度策略;
- 并发问题带来的要求: Linux内核代码的可重入性(处理并发问题并避免竞态);
- 并发问题的注意事项: 不能假定在给定代码段中对处理器的独占性。
当前进程
- 当前进程可以使用current获得:<asm.current.h>和<linux/sched.h> 或<linux/sched.h>
- 打印当前进程信息:
printk(KERN_INFO "The process is \" &s\" (pid &i)\n" current->comm, current->pid);
其他细节
- 栈的分配: 内核栈较小,因此如果需要大结构,应该动态分配;
- 内核API命名: __开头函数为内核底层组件,谨慎使用;
- 浮点运算: 内核代码通常不支持浮点运算;其是使用软浮点实现的(通过编译器)或在在编译时候打开浮点运算单元的支持,并在Makefile添加相应编译浮点指令;
编译和装载
编译模块
-
参考文件 Documentation/kbuild, 工具版本:Documentation/Changes
-
添加makefile: obj - m := module.o 与 module-objs := file1.o file2.o
-
对文件的编译依赖于内核的makefile:
参考代码:
ifeq ($(DEBUG),y) DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines else DEBFLAGS = -O2 endif EXTRA_CFLAGS += $(DEBFLAGS) EXTRA_CFLAGS += -I$(LDDINC) ifneq ($(KERNELRELEASE),) # call from kernel build system scull-objs := main.o pipe.o access.o obj-m := scull.o else KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) modules: $(MAKE) -C $(KERNELDIR) M=$(PWD) LDDINC=$(PWD)/../include modules endif
装载和卸载模块
- insmod工具: 参照上文的链接模块到内核过程(insmod)
- modprobe工具: 相较于insmod,其会检查所装载模块的依赖性,并在当前路径搜索并一并加载
- rmmod注意事项: 使用状态/配置为禁止移除模块无法移除
- 模块调试:读取/proc/modules获得模块信息,读取/sys/module获得当前已装载模块信息;
版本依赖
- 内核兼容性检查: 通过链接ermagic.o,实现在装载时内核模块版本匹配的检查,并查看/ver/log中相应的log文件查看失败原因。
- 模块兼容性: 使用 #ifdef 对代码进行构造/编译,可以使用linux/version.h的定义,其被linux/module.h自动包含
- 常用宏:UTS_RELEASE(内核版本字符串)、LINUX_VERSION_CODE(内核版本二进制表示)、KERNEL_VERSUON(major, minor, release)(将已知版本字符串转化为二进制表示)
- 封装:将特定版本的底层实现隐藏封装在特定头文件中,并通过低层宏/函数实现
平台依赖
- 内核开发对平台的依赖: 内核可根据不同需求指定寄存器
- 平台依赖的检查: 链接ermagic.o,可以实现对处理器相关部分配置的选项检查
- 一般性的发布: 考虑到不同平台的兼容性(GPL)
内核符号表
- 内核符号表作用: insmod使用内核符号表对模块进行解析,通常来说,模块的符号是不用导出的,但是如果与其他模块存在依赖关系时,其符号需要导出(模块层叠技术)。
- 模块层叠技术的应用: fat-> msdos、usbcore + input -> USB、并口子系统(Ip、parport、parport_pc、内核API);其实就是设计模式中的装饰与组合
- 处理层叠模块: modprobe可以从标准的已安装模块目录实现装入模块;
- 全局宏导出符号: EXPORT_SYMBOL(name)或EXPORT_SYMBOL_GPL(name),其将全局变量保存在ELF段。其原理为于<linux/module.h>中
预备知识
- 代码要求:
- 模块头文件:
#include<linux/module.h> #include<linux/init.h> #include<linux/moduleparam.h>
- 许可证等MODULE:
MODULE_LICENSE("GPL"); MODULE_AUTHOR(); MODULE_DESCPTION(); MODULE_VERSION(); MODULE_ALIAS(); MODULE_DEVICE_TABLE();
初始化和关闭:
- 初始化:
static int __init initialisztion_fun(void) { } module_init(initialisztion_fun);
- 模块的注册:使用相应的内核函数实现注册-软件抽象;层叠功能:
EXPORT—_SYMBOL
或者register
; - 清除:
static void __exit cleanup_fun(void) { } module_exit(cleanup_fun);
- 初始化错误处理: 检查返回值,确保操作成功;初始化错误处理应该继续向前并尽可能提供功能;如无法装载模块,应撤销之前工作;
- 常用语句: 使用goto语句处理错误,一旦出错,调用goto + 清除函数(检查状态);
- 错误代码: 尽量使用<linux/errno.h>中的符号值,使其存在意义;
- 竞态: 在支持某个设备的内部初始化完成之前,不要注册任何设施;初始化失败,但是内核已经使用注册设备,需等待操作完成;
模块参数:
- 模块参数赋值: 装载模块(insmode/modprobe),配置文件(modprobe)(etc/modprobe.conf)
- 模块参数加载
module_param(name, type[,num], perm)
[]中的为数组可选;注意参数一定为static并给定默认值- type类型:bool、invbool、charp、int、long、short、uint、ulong、ushort
- 可使用hook定义其他类型,见moduleparam.h
- parm为访问许可值,其定义位于<linux/stat.h>, 如果其为0,则没有其对应的sysfs项目,对于该参数,其尽量设置为只读(S_IRUGO)因为内核不会通知模块
用户空间编写驱动程序
- 其表现为一个服务器进程,其多用于新的,不常见硬件的编程开发
- 注意: 无法使用中断/只能用mmap映射访问内存/只能用ioperm或iopl访问端口/响应时间慢/使用mlock防止换出/不能处理重要设备
快速参考(P44-P45)
参考文献
Insmod模块加载过程分析 https://www.cnblogs.com/edver/p/8419897.html
The Linux Kernel https://linux-kernel-labs.github.io/refs/heads/master/labs/kernel_modules.html#objdump