作为一个 Linux 驱动开发初学者,我将会通过写博客来总结和巩固我学到的知识,本篇文章作为整个系列的开篇,我将会从最简单的内核模块入手,这篇文章会完整记录我的学习过程,代码分析,以及板上部署并运行,也会记录一些我遇到的问题和解决方法,给我自己长个记性的同时也希望帮助到和我一样的 Linux 驱动开发初学者。
1. 前置知识
Linux 驱动本质上就是运行在内核态的程序,而内核模块作为驱动开发的最小单元,它可以动态的加载到内核中,无需重新编译整个内核。
野火的官方手册上有这样一句话,总结性很强,我把它摘抄过来:
- 模块本身不被编译入内核映像,这控制了内核的大小。
- 模块一旦被加载,它就和内核中其他部分完全一样。
大家在学习 Linux 驱动开发之前或多或少应该都是写过代码的。请大家想想是不是这么个道理:我们以前写的应用程序几乎都是从头到尾执行任务。
而模块不一样,我们在 加载 模块时似乎只是预先注册了一个东西,然后初始化函数就结束了,这之后我们就可以调用模块的函数使用它,不用了就可以注销。举个形象的例子,我们可以把加载模块看作你注册一个银行账户,初始化函数就是对这个账户进行一些初始化操作,而这个操作正是为了你以后存钱做准备的,在卸载模块之前,也就是注销账户之前,要把钱全部转走,对应于内核模块的引用计数为0,这时才能进行卸载。
这些内容初看可能有点难以理解,尤其是上面总结性很强的两句话。但是在学习一段时间之后,即使你没有看过写内容,仔细想想也会发现,确实是这么个道理。
2. 完整代码及分析
2.1 完整代码
我先把完整的代码放出来,大家可以先观察一下,看看哪些和以前学过的 C 语言有相同之处,哪些又不同。
代码如下,我们将包含这段代码的文件命名为hello.c:
c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
static int __init my_module_init(void)
{
printk(KERN_EMERG "The first Linux module\n");
return 0;
}
static void __exit my_module_exit(void)
{
printk(KERN_EMERG "First module exiting\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
这段代码总体看上去:结构和往常写的 C 程序还是挺相似的,代码中的 printk 大家应该不难猜出它是干什么的,但是头文件没见过,__init 和 __exit 标识符没见过,module_init 和module_exit,还有 MODULE_LICENSE 都没见过。
下面,我会详细解释这段代码。
2.2 代码分析
先来看头文件:
<linux/module.h>:这是所有内核模块都必须包含的,定义了module_init和module_exit等宏。<linux/init.h>:定义__init和__exit等初始化也退出相关的宏。<linux/kernel.h>:包含printk声明,以及日志级别宏。
再来看两个标识符:
__init:被标记的函数仅在初始化时使用,初始化完成后就会释放内存。__exit:被标记的函数仅在模块卸载是使用。
指定模块入口和出口的函数:
module_init:在加载模块时会调用这里指定的函数。module_exit:卸载模块时会调用这里指定的函数。
模块许可证:
MODULE_LICENSE:必须要有,这里我用的GPL许可证,符合内核开源协议。
到这里,除了 printk 之外的所有内容我们都了解过了。
2.3 printk详解
printk是内核中使用的打印函数,它和我们以前用过的 printf 有些不同之处。正如上面代码中表现出来的一样,在字符串前面有一个宏,这正是日志级别,也可以理解为消息的优先级,通过这些宏可以对消息的严重程度进行分类。
在头文件 <linux/kernel.h> 中定义了 8 种宏,按照优先级从高到低排列如下:
KERN_EMERG、KERN_ALERT、KERN_CRIT、KERN_ERR、KERN_WARNING、KERN_NOTICE、KERN_INFO、KERN_DEBUG。
printk会把消息打印到内核日志中,我们可以在终端使用dmesg命令查看内核日志。
此外,printk还会根据使用的日志级别选择是否将当前消息打印到终端。可以看到,我们代码中采用的是最高的日志级别,因此到时候我们可以在终端上面看到信息,也可以在内核日志中看到信息。
2.4 编写Makefile
因为我们现在在学习驱动开发,我想不论当前有没有用到硬件,尽管我们目前的代码并不涉及硬件。我还是觉得将代码交叉编译并拷贝到板子上运行更符合驱动开发的流程。
makefile
#源码目录路径,每个人情况不一样,要注意了
KERNEL_DIR := /home/xlp/workspace/kernel
# -m表示编译为模块,hello是模块名,对应hello.c
obj-m := hello.o
#交叉编译
all:
make -C $(KERNEL_DIR) M=$(PWD) ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules #注意modules前面有空格
clean:
make -C $(KERNEL_DIR) M=$(PWD) clean
3. 编译并运行
3.1 编译
在 hello.c 和 Makefile 所在目录下执行make,可以看到,多出来了许多文件:

其中有一个名为 hello.ko 的文件,就是我们需要的内核模块文件。
3.2 运行
将文件从虚拟机拷贝到板子上有很多种方法,我就不一一介绍了,大家选择自己喜欢的就好,我采用的 NFS 网络文件系统。
如图,进入板子的对应目录下,我们已经看到模块文件静静躺那了:

然后我们执行下面命令加载内核模块,注意这里需要root权限:
bash
sudo insmod hello.ko

可以看到我们printk中的字符串打印出来了。
再执行下面命令可以查看内核日志:
bash
sudo dmesg | tail

最后卸载模块再观察一下:
bash
sudo rmmod hello.ko


和我们的预期结果是完全一样的。
4. 总结
到这里,相信大家对于最简单的内核模块也有了一些认识了。
如果有兴趣的话,可以去尝试一下其他优先级的内核日志宏,看看从哪个优先级开始,消息不在终端显示了。
这是我 Linux 驱动开发的第一步,虽然只是一个简单的内核模块,但从环境准备到编译测试,也踩了不少坑。后续我会 持续更新 驱动开发的学习笔记,从基础模块到硬件驱动,一步步深入。如果这篇文章帮到了你,欢迎点赞收藏~