目录
一、什么是内核模块
二、为什么要用内核模块
三、模块和驱动的关系
四、内核模块实验
4.0 实验程序
4.1 模块程序解释
4.1.1 驱动头文件解释
4.1.2 init/exit:模块的“生命周期回调”
4.1.3 printk介绍
4.1.4 module_init/module_exit
4.1.5 MODULE_* 元信息
4.2 Makefile文件编写与模块装载卸载
4.2.1 Makefile文件详解
4.2.2 模块的装载卸载
4.2.3 模块相关的指令
参考资料
一、什么是内核模块
内核模块可以理解为“给正在运行的 Linux 内核安装的插件”。
Linux 内核本身是一套常驻内存、负责管理硬件和系统资源的核心程序。
内核模块(Kernel Module)就是一段可以在系统运行过程中,动态加载到内核里执行的代码文件(通常以 .ko 结尾)。
加载后,它就和内核代码一样运行在内核态,可以调用内核提供的接口,做驱动、文件系统、网络协议等内核级工作;不用时还可以卸载。
二、为什么要用内核模块
如果把所有驱动和功能都写死在内核里,会带来几个问题:
模块解决的就是这些问题:
- 用到什么功能就加载什么模块(按需加载)
- 驱动更新时只替换 .ko 文件(无需动整个内核)
- 开发调试可以“改完就加载试一次”(效率高)
一个直观的对比如下:
- 编译进内核(built-in):像把功能焊死在主板上,启动就存在,不能随时拆。
- 内核模块(module):像插在主板插槽里的扩展卡,用的时候插上,不用可以拔掉。
三、模块和驱动的关系
大多数 Linux 设备驱动(网卡、USB、摄像头、GPIO 等)都是以内核模块形式提供的:
- 插入 USB 网卡时,系统会加载对应的驱动模块
- 拔掉设备后,模块可以保持加载(也可以手动卸载)
所以你写驱动时,通常也是写一个模块:
- 模块被加载 → 驱动初始化、注册设备
- 模块被卸载 → 驱动注销、释放资源
四、内核模块实验
4.0 实验程序
从一个简单的模块程序开始介绍内核模块:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_EMERG "[ KERN_EMERG ] Hello Module Init\\\\n");
printk( "[ default ] Hello Module Init\\\\n");
return 0;
}
static void __exit hello_exit(void)
{
printk("[ default ] Hello Module Exit\\\\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL2");
MODULE_AUTHOR("cc ");
MODULE_DESCRIPTION("hello world module");
MODULE_ALIAS("test_module");
这个程序实现了一个最小可运行的内核模块:
- 模块被加载(insmod/modprobe)时,内核会调用 hello_init(),打印两条日志,然后返回 0 表示初始化成功。
- 模块被卸载(rmmod)时,内核会调用 hello_exit(),打印一条日志并退出。
模块的“入口/出口”由:
module_init(hello_init);
module_exit(hello_exit);
这两句注册。
4.1 模块程序解释
4.1.1 驱动头文件解释
Linux内核中常常用到如下的头文件:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
-
linux/module.h
提供模块框架的核心宏和声明:
module_init/module_exit、MODULE_LICENSE、MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_ALIAS 等。
-
linux/init.h
提供 __init、__exit 这类“生命周期标记”宏。
-
linux/kernel.h
提供常用内核宏/函数声明(包含 printk 所需的一些定义;在部分内核版本里日志相关宏也会经由这里间接引入)。
4.1.2 init/exit:模块的“生命周期回调”
__init 的意义:
static int __init hello_init(void)
__init 表示:初始化阶段用到的代码。
对于“编译进内核”的代码(built-in),内核启动完成后,这段 init 代码所在内存往往可以被释放/回收,以节省内存。
对于“可加载模块”(.ko),它依然表示“初始化代码段”,但具体是否回收以及何种方式回收,取决于内核实现;你可以把它理解为一种“告诉内核和工具链:这段代码只在 init 阶段需要”。
init 返回值决定模块是否“算加载成功”
return 0;
- 返回 0:模块加载成功,随后你会在 lsmod 里看到它。
- 返回 非 0:模块加载失败,内核会回滚已做的部分工作,模块不会留在系统里。驱动开发中常用这个机制处理“硬件不存在/资源申请失败”等情况。
__exit 的意义:
static void __exit hello_exit(void)
- __exit 表示:卸载阶段才需要的代码。
- 如果某段代码是 built-in(无法卸载),__exit 标记通常会让编译器/链接阶段倾向于丢弃它(因为根本不会被执行)。
- 而在模块场景下,rmmod 会触发卸载流程,这段代码会被执行,用来释放资源。
在C语言中,static关键字的作用如下:
内核模块的代码,实际上是内核代码的一部分, 假如内核模块定义的函数和内核源代码中的某个函数重复了, 编译器就会报错,导致编译失败,因此我们给内核模块的代码加上static修饰符的话, 那么就可以避免这种错误。
4.1.3 printk介绍
printf是glibc实现的打印函数,工作于用户空间
printk:内核模块无法使用glibc库函数,内核自身实现的一个类printf函数,但是需要指定打印等级。
- #define KERN_EMERG “<0>” 通常是系统崩溃前的信息
- #define KERN_ALERT “<1>” 需要立即处理的消息
- #define KERN_CRIT “<2>” 严重情况
- #define KERN_ERR “<3>” 错误情况
- #define KERN_WARNING “<4>” 有问题的情况
- #define KERN_NOTICE “<5>” 注意信息
- #define KERN_INFO “<6>” 普通消息
- #define KERN_DEBUG “<7>” 调试信息
查看当前系统printk打印等级:cat /proc/sys/kernel/printk, 从左到右依次对应当前控制台日志级别、默认消息日志级别、 最小的控制台级别、默认控制台日志级别。

打印内核所有打印信息:dmesg,注意内核log缓冲区大小有限制,缓冲区数据可能被覆盖掉。
4.1.4 module_init/module_exit
module_init(hello_init);
module_exit(hello_exit);
这两句的核心作用是:把函数指针放到模块的“特殊段/结构”中,让内核的模块加载器在合适时机调用。
从行为上你可以理解为:
- insmod hello.ko 时:内核完成装载与符号解析后调用 hello_init()
- rmmod hello 时:如果引用计数允许卸载,内核调用 hello_exit()
补充一点常见认知:
- 模块加载卸载本质上对应内核的模块管理流程(底层会涉及 ELF 解析、重定位、符号解析、vermagic 校验等),但对写模块而言你只需要把 init/exit “挂上去”。
4.1.5 MODULE_* 元信息
在驱动模块的尾部,我们添加了一些辅助信息:
MODULE_LICENSE("GPL2");
MODULE_AUTHOR("cc ");
MODULE_DESCRIPTION("hello world module");
MODULE_ALIAS("test_module");
| MODULE_LICENSE() | 表示模块代码接受的软件许可协议,Linux内核遵循GPL V2开源协议,内核模块与linux内核保持一致即可。 |
| MODULE_AUTHOR() | 描述模块的作者信息 |
| MODULE_DESCRIPTION() | 对模块的简单介绍 |
| MODULE_ALIAS() | 给模块设置一个别名 |
4.2 Makefile文件编写与模块装载卸载
4.2.1 Makefile文件详解
根据先前的实验环境,编写Makefile文件,主要目的是把编译工作交给内核源码树里的Kbuild系统
KERNEL_DIR=../../../../kernel/
ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-
export ARCH CROSS_COMPILE
obj-m := hellomodule.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONE:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
核心命令是:
$(MAKE) -C$(KERNEL_DIR) M=$(CURDIR) modules
含义是:
- C $(KERNEL_DIR):先切到内核源码/构建目录去执行内核的 Makefile
- M=$(CURDIR):告诉内核:当前这个目录是一个“外部模块目录”,里面有 obj-m 等规则
- modules:让内核 Kbuild 以“构建外部模块”的方式去编译并最终产出 .ko
这也是官方推荐的外部模块构建方式(即利用 M= 机制)。
KERNEL_DIR=../../../kernel/
这一句定义了变量KERNEL_DIR,用来保存内核源码的目录,需要指定到内核编译输出目录下,此处使用的是相对路径,改成绝对路径也可以。
ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-
export ARCH CROSS_COMPILE
- ARCH=arm64:告诉内核 Kbuild 当前目标架构是 ARM64。
- CROSS_COMPILE=aarch64-linux-gnu-:告诉 Kbuild 交叉工具链前缀,Kbuild 会自动拼出:
- aarch64-linux-gnu-gcc
- aarch64-linux-gnu-ld
- aarch64-linux-gnu-objcopy 等
- export ARCH CROSS_COMPILE:把变量导出到子 make 进程中,否则进入 $(KERNEL_DIR) 后内核 Makefile 未必能收到这些变量。
说明:如果你从命令行传参(如 make ARCH=arm64 CROSS_COMPILE=…),也可以不 export,但 export 是一种稳妥写法。
obj-m := hellomodule.o
这是外部模块目录里最关键的一行。它告诉 Kbuild:
- 你要构建一个模块,模块名为 hellomodule
- 最终输出:hellomodule.ko
- “模块的主对象”是 hellomodule.o(由 hellomodule.c 编译而来)
直观映射关系:
- hellomodule.c → 编译 → hellomodule.o
- hellomodule.o → 链接成模块 → hellomodule.ko
补充两点工程常识:
:= 与 +=
- obj-m := hellomodule.o 表示“只构建这一个模块”(覆盖式赋值)
- obj-m += hellomodule.o 更常见,便于后续再追加其他模块
若模块由多个 .o 组成(例如 a.c、b.c、c.c),写法是:
obj-m += hellomodule.o
hellomodule-y := a.o b.o c.o
这样最终还是产出 hellomodule.ko,只是内部由多个对象文件组成。
all:
$(MAKE) -C$(KERNEL_DIR) M=$(CURDIR) modules
- all 是你在模块目录执行 make 时默认会走的目标。
- 这条命令触发 Kbuild 外部模块构建流程,典型会生成:
- hellomodule.ko
- hellomodule.mod.o
- hellomodule.mod.c
- modules.order
- Module.symvers(是否生成与内核配置/构建状态有关)
- 一堆 .cmd 依赖文件
这里的 $(CURDIR) 是 GNU make 的内置变量,表示当前目录的绝对路径(通常等价于 pwd 的结果)。用它传 M= 很合适。
clean:
$(MAKE) -C$(KERNEL_DIR) M=$(CURDIR) clean
- 同样借助内核 Kbuild 的清理规则。
- 会删掉本目录下 Kbuild 生成的中间文件与 .ko。
我们在终端执行make编译模块,可以看到生成了很多文件,其中.ko文件便是我们想要的模块。

使用如下命令查看模块信息:
file hellomodule.ko # 看看是否是 aarch64
modinfo hellomodule.ko
vermagic 应与 RK3588 板卡 uname -r 匹配,否则上板极易 invalid module format。
4.2.2 模块的装载卸载
使用scp可以将该模块拷贝到开发板上,具体的方法可以自行搜索。
在板卡上,执行以下命令可以装载卸载模块
# 板卡上装载模块
insmod hellomodule.ko
#查看内核输出信息
dmesg | tail -n 50
# 板卡上卸载模块
rmmod hellomodule
#查看内核输出信息
dmesg | tail -n 50
可以看到内核的打印信息中已经成功装载卸载模块了

4.2.3 模块相关的指令
下面按“模块开发与上板调试最常用的指令链路”把 Linux 内核模块(.ko)相关命令做一套系统详解。默认你使用的是常见的 kmod 工具集(insmod/rmmod/lsmod/modprobe/modinfo/depmod)
1) 查看模块是否已加载:lsmod / /proc/modules
lsmod
用途:列出当前已加载模块、占用大小、被谁引用。
lsmod
lsmod | grep hello

输出含义(典型):
- Module:模块名(通常不含 .ko 后缀)
- Size:模块占用大小(字节)
- Used by:引用计数及依赖该模块的其他模块列表
常见解读:
- Used by 0:没有其他模块依赖它,通常允许卸载(但仍可能因“正在使用的设备句柄”导致卸载失败)。
- Used by >0:有依赖或引用,rmmod 多半会失败(除非强制)。
/proc/modules
用途:更底层、脚本化读取已加载模块。
cat /proc/modules |head

这与 lsmod 信息本质相同;很多情况下 lsmod 就是格式化读取这里的数据。
2) 查看模块文件信息:modinfo
用途:查看 .ko 的元信息(license、author、description、alias、depends、vermagic 等),判断是否可能与当前内核匹配。
modinfo hellomodule.ko
modinfo -F vermagic hellomodule.ko
modinfo -F license hellomodule.ko
modinfo -F depends hellomodule.ko
modinfo -Falias hellomodule.ko

关键字段解读:
- filename:模块文件路径
- license:MODULE_LICENSE() 声明的许可
- description/author:模块元信息
- alias:MODULE_ALIAS() 声明的别名(影响 modprobe 自动匹配)
- depends:依赖模块(modprobe 会用到)
- vermagic:极关键,表示模块编译时绑定的内核版本/特性(不匹配常见报错:invalid module format)
3) 加载模块:insmod 与 modprobe
insmod
用途:直接把指定 .ko 插入内核,不解析依赖、不查索引。
sudo insmod hellomodule.ko
sudo insmod hellomodule.ko param1=123 debug=
特点:
- 适合开发调试、单文件模块测试。
- 不自动加载依赖:依赖缺失时常见 Unknown symbol …。
常见错误定位:
dmesg |tail -n 100
modprobe
用途:按模块名加载,会自动处理依赖(基于 modules.dep / modules.alias 等索引)。
sudo modprobe hellomodule
sudo modprobe -v hellomodule# 显示执行细节
sudo modprobe -n -v hellomodule# dry-run:只显示将要做什么,不执行
特点:
- 推荐在工程化场景使用(依赖自动拉起、别名匹配、黑名单可控)。
- 加载对象来自标准目录 /lib/modules/$(uname -r)/ 下的索引;因此你若只是把 .ko 放在某个临时目录,modprobe 可能找不到。
4) 卸载模块:rmmod 与 modprobe -r
rmmod
用途:卸载指定模块(不处理依赖关系)。
sudo rmmod hellomodule
sudo rmmod -f hellomodule# 强制卸载(需要内核配置允许;不建议常用)
常见报错:
- Module is in use:模块被引用(lsmod 的 Used by 不为 0)或设备节点仍被占用(例如有进程打开了设备文件)。
排查“谁在用”常用:
lsmod | grep hello
lsof | grep /dev/xxx# 字符设备场景
modprobe -r
用途:按依赖顺序卸载(更“聪明”)。
sudo modprobe -r hellomodule
sudo modprobe -r -v hellomodule
5) 建立模块索引/依赖:depmod
用途:扫描 /lib/modules/$(uname -r)/ 下所有模块,生成依赖索引文件,如 modules.dep、modules.alias 等,供 modprobe 使用。
典型场景:你把自己编译的 hellomodule.ko 放到了标准目录下,需要让系统“认识它”。
modprobe是怎么知道一个给定模块所依赖的其他的模块呢?在这个过程中,depmod起到了决定性作用,当执行modprobe时, 它会在模块的安装目录下搜索module.dep文件,这是depmod创建的模块依赖关系的文件。
在Linux系统中,/lib/modules目录通常包含内核相关的模块和配置文件,该文件夹包含了与内核版本号相关文件夹,用来存放的模块和配置信息。

以上配置文件或者目录说明如下:
| build | 指向当前正在运行的内核源代码的符号链接 |
| kernel | 包含编译后的内核模块文件(.ko) |
| modules.alias | 定义模块别名的文件 |
| modules.alias.bin | 模块别名文件的二进制缓存版本 |
| modules.builtin | 列出了由内核构建的模块(静态连接在内核中) |
| modules.builtin.bin | 由内核构建的模块列表的二进制缓存版本 |
| modules.dep | 列出了模块之间的依赖关系 |
| modules.dep.bin | 模块依赖关系文件的二进制缓存版本 |
| modules.devname | 包含了每个模块设备的名称 |
| modules.order | 定义模块加载顺序的文件 |
| modules.symbols | 保存导出的符号信息 |
| modules.symbols.bin | 导出的符号信息的二进制缓存版本 |
| modules.softdep | 包含模块软依赖关系的文件 |
我们最关心的配置文件是modules.dep,该文件列出了模块之间的依赖关系,当我们执行depmod -a建立模块之间的依赖关系时,就会把依赖关系写入到modules.dep当中。
6) 查看内核日志/模块打印:dmesg / journalctl -k
dmesg
用途:读取内核环形缓冲区日志(模块 printk/pr_info 输出在这里非常常见)。 journalctl -k(systemd 系统) 用途:从 systemd journal 查看内核日志(部分发行版会比 dmesg 更完整或更易检索)。
至此,详细介绍了内核驱动模块的详细内容,后续,笔者会进一步深入Linux内核驱动开发,欢迎关注。
参考资料
- 野火Linux驱动资料:https://doc.embedfire.com/linux/rk356x/driver/zh/latest/linux_driver/base_first_module.html
- LINUX设备驱动程序(第三版)





