在操作系统的组成方式上,一直有宏内核与微内核之间的争论,除了这两种方式,还有其他解决办法吗?Linux 给出了答案,那就是”使用模块“。模块允许内核在运行时动态地向其中插入或从中删除代码,无需重新编译整个内核并重新引导系统,可以方便地扩展内核的功能。在驱动开发中,设备驱动程序都是由一个个模块构成的,在我们开始编写驱动前,先看看内核模块是如何使用的吧。
驱动加入到内核的方式
- 将驱动编译进 Linux 内核中,当 Linux 内核启动的时就会自动运行驱动程序。
- 添加代码 :在
driver
目录下添加相应驱动代码 - Makefile :本级目录添加
Makefile
,上级目录修改Makefile
;将新添加的源码加入编译过程- 本级 Makefile
obj-$(CFG_XXX) += xxx.o xxx.objs := xxx-a.o xxx-b.o xxx-c.o
1
2
3
4
3. 上一级 Makefile
4. ```makefile
obj-$(CFG_XXX) += dir_name/
- 添加代码 :在
- 将驱动编译成模块(Linux 下模块扩展名为
.ko
),在Linux 内核启动以后使用相应命令加载驱动模块。- 内核模块是Linux内核向外部提供的一个插口
- 内核模块是具有独立功能的程序,他可以被单独编译,但不能单独运行。他在运行时被链接到内核作为内核的一部分在内核空间运行
- 内核模块便于驱动、文件系统等的二次开发
- 根据源码路径,交叉编译路径,添加
Makefile
(和上面相同),编译为.ko
文件,命令为:make -C /kernel/source/location/ SUBDIRS=$PWD modules
内核模块组成
模块入口函数
1 | module_init(xxx_init); |
- module_init 函数用来向 Linux 内核注册一个模块加载函数,
- 参数 xxx_init 就是需要注册的具体函数(理解是模块的构造函数)
- 当加载驱动的时, xxx_init 这个函数就会被调用
模块退出函数
1 | module_exit(xxx_exit); |
- module_exit函数用来向 Linux 内核注册一个模块卸载函数,
- 参数 xxx_exit 就是需要注册的具体函数(理解是模块的析构函数)
- 当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用
模块许可证明
1 | MODULE_LICENSE("GPL") //添加模块 LICENSE 信息 ,LICENSE 采用 GPL 协议 |
模块参数(可选)
模块参数是一种内核空间与用户空间的交互方式,只不过是用户空间 –> 内核空间单向的,他对应模块内部的全局变量
模块信息(可选)
1 | MODULE_AUTHOR("huashan sun <sunhuashan624@gamil.com>") //添加模块作者信息 |
模块打印 printk
printk在内核中用来记录日志信息的函数,只能在内核源码范围内使用。和printf非常相似。 printk函数主要做两件事情:①将信息记录到log中 ②调用控制台驱动来将信息输出
printk 可以根据日志级别对消息进行分类,一共有 8 个日志级别
1
2
3
4
5
6
7
8
9以下代码就是设置“gsmi: Log Shutdown Reason\n”这行消息的级别为 KERN_EMERG。
1
printk(KERN_DEBUG"gsmi: Log Shutdown Reason\n");
如果使用 printk 的时候不显式的设置消息级别,那 么printk 将会采用默认级别MESSAGE_LOGLEVEL_DEFAULT,默认为 4。
在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT,定义如下:
1
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 模块
1 |
|
先看头文件,其中 init.h
包括 __init
宏和 __exit
宏,module.h
包含 模块相关的宏,这两个头文件在驱动程序中是必不可少的。
1 |
紧接着是模块的入口和出口函数,这里我们只是进行简单的打印。
1 | static int __init hello_init(void) |
最后是模块相关的宏,其中最重要的是 module_init
和 module_exit
宏。当使用 insmod
或 modprobe
宏安装模块时,使用 module_init
注册的函数会被调用,这里调用的是 hello_init 函数,类似的,当模块被移除时,调用的是使用 module_exit
注册的函数,这里是 hello_exit。MODULE_LICENSE
指定的是模块使用的开源协议。
1 | module_init(hello_init); |
编译模块
当以模块的形式编写驱动程序时,需要在 Makefile 文件中指定内核的源码路径:
1 | KERNEL_PATH := /home/sunhuashan/workplace/wsl-linux-5.10.16.3 |
在这个简单的 Makefile 文件中,各个变量的意义如下:
KERNEL_PATH
:内核源码路径PWD
:模块所在路径,这里使用shell
中的pwd
命令获取了 Makefile 所在的目录,一般而已,这也是应该是模块源码所在的目录obj-m
:模块所依赖的目标文件,应该还源码中的源文件一一对应- 剩下的就是简单的 Makefile 语法了,值得注意的是,使用
-C
指定源码路径,M=
指定模块路径,modules
表示编译模块,clean
表示清除模块
实战
- make 编译
1 | make build_modules |
- insmod 安装
1 | sudo insmod hello.ko |
- dmesg 查看输出日志
1 | dmesg | grep hello |
可以看到,我们的 hello_init 函数被调用,并且打印了一行输出。
- rmmod 卸载
1 | sudo rmmod hello |
可以看到,我们的 hello_exit 函数被调用,并且打印了一行输出。