目录
一、什么是内核模块
二、为什么要用内核模块
三、模块和驱动的关系
四、内核模块实验
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修饰的静态局部变量直到程序运行结束以后才释放,延长了局部变量的生命周期。
- static的修饰全局变量只能在本文件中访问,不能在其它文件中访问。
- 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):先切到内核源码/构建目录去执行内核的 MakefileM=$(CURDIR):告诉内核:当前这个目录是一个“外部模块目录”,里面有obj-m等规则modules:让内核 Kbuild 以“构建外部模块”的方式去编译并最终产出.ko
这也是官方推荐的外部模块构建方式(即利用M=机制)。
KERNEL_DIR=../../../kernel/这一句定义了变量KERNEL_DIR,用来保存内核源码的目录,需要指定到内核编译输出目录下,此处使用的是相对路径,改成绝对路径也可以。
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- export ARCH CROSS_COMPILEARCH=arm64:告诉内核 Kbuild 当前目标架构是 ARM64。CROSS_COMPILE=aarch64-linux-gnu-:告诉 Kbuild 交叉工具链前缀,Kbuild 会自动拼出:aarch64-linux-gnu-gccaarch64-linux-gnu-ldaarch64-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.ohellomodule.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) modulesall是你在模块目录执行make时默认会走的目标。- 这条命令触发 Kbuild 外部模块构建流程,典型会生成:
hellomodule.kohellomodule.mod.ohellomodule.mod.cmodules.orderModule.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.kovermagic 应与 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 100modprobe
用途:按模块名加载,会自动处理依赖(基于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 hellomodule5) 建立模块索引/依赖: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设备驱动程序(第三版)