在操作系统的组成方式上,一直有宏内核与微内核之间的争论,除了这两种方式,还有其他解决办法吗?Linux 给出了答案,那就是”使用模块“。模块允许内核在运行时动态地向其中插入或从中删除代码,无需重新编译整个内核并重新引导系统,可以方便地扩展内核的功能。在驱动开发中,设备驱动程序都是由一个个模块构成的,在我们开始编写驱动前,先看看内核模块是如何使用的吧。
驱动加入到内核的方式
将驱动编译进 Linux 内核中,当 Linux 内核启动的时就会自动运行驱动程序。
添加代码 :在driver目录下添加相应驱动代码
Makefile :本级目录添加Makefile,上级目录修改Makefile;将新添加的源码加入编译过程
本级 Makefile
obj-$(CFG_XXX) += xxx.o
xxx.objs := xxx-a.o xxx-b.o xxx-c.o
12343. 上一级 Makefile4. ```makefile obj-$(CFG_XXX) += dir_name/
将驱动编译成模块(Linux 下模块扩展名为 .ko),在Linux 内核启动以后使用相应命令加载驱动模块。
内核模块是Linux内核向外部提供的一个插口
内核模块是具有独立功能的程序,他可以被单独编译,但不能单独运行。他在运行时被链接到内核作为内核的一部分在内核空间运行
内核模块便于驱动、文件系统等的二次开发
根据源码路径,交叉编译路径,添加Makefile(和上面相同),编译为.ko文件,命令为: make -C /kernel/source/location/ SUBDIRS=$PWD modules
内核模块组成模块入口函数1module_init(xxx_init);
module_init 函数用来向 Linux 内核注册一个模块加载函数,
参数 xxx_init 就是需要注册的具体函数(理解是模块的构造函数)
当加载驱动的时, xxx_init 这个函数就会被调用
模块退出函数1module_exit(xxx_exit);
module_exit函数用来向 Linux 内核注册一个模块卸载函数,
参数 xxx_exit 就是需要注册的具体函数(理解是模块的析构函数)
当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用
模块许可证明1MODULE_LICENSE("GPL") //添加模块 LICENSE 信息 ,LICENSE 采用 GPL 协议
模块参数(可选)模块参数是一种内核空间与用户空间的交互方式,只不过是用户空间 –> 内核空间单向的,他对应模块内部的全局变量
模块信息(可选)1MODULE_AUTHOR("huashan sun
模块打印 printkprintk在内核中用来记录日志信息的函数,只能在内核源码范围内使用。和printf非常相似。 printk函数主要做两件事情:①将信息记录到log中 ②调用控制台驱动来将信息输出
printk 可以根据日志级别对消息进行分类,一共有 8 个日志级别
123456789#define KERN_SOH "\001" #define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误 */#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */#define KERN_DEBUG KERN_SOH "7" /* 调试信息 */
以下代码就是设置“gsmi: Log Shutdown Reason\n”这行消息的级别为 KERN_EMERG。
1printk(KERN_DEBUG"gsmi: Log Shutdown Reason\n");
如果使用 printk 的时候不显式的设置消息级别,那 么printk 将会采用默认级别MESSAGE_LOGLEVEL_DEFAULT,默认为 4。
在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT,定义如下:
1#define CONSOLE_LOGLEVEL_DEFAULT 7
CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上,此宏默认为 7,意味着只有优先级高于 7 的消息才能显示在控制台上。
这个就是 printk 和 printf 的最大区别,可以通过消息级别来决定哪些消息可以显示在控制台上。默认消息级别为 4,4 的级别比 7 高,所示直接使用 printk 输出的信息是可以显示在控制台上的。
模块相关的命令加载模块
insmod XXX.ko
为模块分配内核内存、将模块代码和数据装入内存、通过内核符号表解析模块中的内核引用、调用模块初始化函数(module_init)
insmod要加载的模块有依赖模块,且其依赖的模块尚未加载,那么该insmod操作将失败
modprobe XXX.ko
加载模块时会同时加载该模块所依赖的其他模块,提供了模块的依赖性分析、错误检查、错误报告
modprobe 提示无法打开“modules.dep”这个文件 ,输入 depmod 命令即可自动生成 modules.dep
卸载模块
rmmod XXX.ko
查看模块信息
lsmod
查看系统中加载的所有模块及模块间的依赖关系
modinfo (模块路径)
查看详细信息,内核模块描述信息,编译系统信息
hello world 模块实战光说不练假把式,最后,我们以一个 hello world 模块实战结束,正式进入驱动开发的世界。这个系列的实战例程参考了 《Linux Device Driver 3rd》这本书,因为没有涉及到真实硬件,所以选择在 WSL 下进行开发,感兴趣的读者可以参考 WSL2的安装、应用、内核模块编译安装 。
一个最简单的 hello 模块1234567891011121314151617#include
先看头文件,其中 init.h 包括 __init 宏和 __exit 宏,module.h 包含 模块相关的宏,这两个头文件在驱动程序中是必不可少的。
12#include
紧接着是模块的入口和出口函数,这里我们只是进行简单的打印。
12345678910static int __init hello_init(void){ printk( KERN_DEBUG "hello world init\n"); return 0;}static void __exit hello_exit(void){ printk( KERN_DEBUG "hello world exit\n");}
最后是模块相关的宏,其中最重要的是 module_init 和 module_exit 宏。当使用 insmod 或 modprobe 宏安装模块时,使用 module_init 注册的函数会被调用,这里调用的是 hello_init 函数,类似的,当模块被移除时,调用的是使用 module_exit 注册的函数,这里是 hello_exit。MODULE_LICENSE 指定的是模块使用的开源协议。
123module_init(hello_init);module_exit(hello_exit);MODULE_LICENSE("GPL");
编译模块当以模块的形式编写驱动程序时,需要在 Makefile 文件中指定内核的源码路径:
123456789KERNEL_PATH := /home/sunhuashan/workplace/wsl-linux-5.10.16.3PWD := $(shell pwd)obj-m := hello.obuild_modules: $(MAKE) -C $(KERNEL_PATH) M=$(PWD) modulesclean: $(MAKE) -C $(KERNEL_PATH) M=$(PWD) clean
在这个简单的 Makefile 文件中,各个变量的意义如下:
KERNEL_PATH :内核源码路径
PWD:模块所在路径,这里使用 shell 中的 pwd 命令获取了 Makefile 所在的目录,一般而已,这也是应该是模块源码所在的目录
obj-m:模块所依赖的目标文件,应该还源码中的源文件一一对应
剩下的就是简单的 Makefile 语法了,值得注意的是,使用 -C 指定源码路径,M= 指定模块路径, modules 表示编译模块, clean 表示清除模块
实战
make 编译
12345678$ make build_modules make -C /home/sunhuashan/workplace/wsl-linux-5.10.16.3 M=/home/sunhuashan/workplace/ldd3/ours/misc_modules modulesmake[1]: Entering directory '/home/sunhuashan/workplace/wsl-linux-5.10.16.3' CC [M] /home/sunhuashan/workplace/ldd3/ours/misc_modules/hello.o MODPOST /home/sunhuashan/workplace/ldd3/ours/misc_modules/Module.symvers CC [M] /home/sunhuashan/workplace/ldd3/ours/misc_modules/hello.mod.o LD [M] /home/sunhuashan/workplace/ldd3/ours/misc_modules/hello.komake[1]: Leaving directory '/home/sunhuashan/workplace/wsl-linux-5.10.16.3'
insmod 安装
1$ sudo insmod hello.ko
dmesg 查看输出日志
12$ dmesg | grep hello[52394.010385] hello world init
可以看到,我们的 hello_init 函数被调用,并且打印了一行输出。
rmmod 卸载
1234$ sudo rmmod hello$ dmesg | grep hello[52394.010385] hello world init[52601.531399] hello world exit
可以看到,我们的 hello_exit 函数被调用,并且打印了一行输出。