第2章:Linux内核模块基础
大家好,我是你们的嵌入式Linux驱动开发讲师。今天我们来聊聊内核模块——这个在驱动开发中绕不开的基础话题。
说实话,我刚接触Linux驱动时,对“模块”这个概念也是一头雾水。后来做项目多了,才慢慢摸清它的脾气。今天我就把这些经验分享给你。
2.1 什么是内核模块?
内核模块,说白了就是一段可以动态加载到内核中的代码。它不像应用程序那样跑在用户空间,而是直接运行在内核空间。
为什么要用模块?你想想看,如果每次加个驱动都要重新编译整个内核,那得多麻烦。模块机制让我们可以像插U盘一样,随时加载、卸载驱动代码。
核心概念:模块是内核的“插件”。它扩展内核功能,但不需要重新编译内核。
我在项目中遇到过这样的情况:客户突然要支持一款新的WiFi芯片。如果不用模块,我得重新编译整个内核,再刷机测试,来回折腾好几天。用模块的话,编译好.ko文件,insmod一下,几分钟搞定。
2.2 模块的加载与卸载
模块操作其实就几个命令,我列出来给你看:
| 命令 | 功能 | 常用选项 |
|---|---|---|
| insmod | 加载模块 | 无特殊选项 |
| rmmod | 卸载模块 | -f 强制卸载 |
| modprobe | 智能加载/卸载 | -r 卸载,-a 加载所有依赖 |
| lsmod | 查看已加载模块 | 无 |
| modinfo | 查看模块信息 | -p 显示参数 |
嗯,这里要注意:insmod和modprobe的区别。insmod是直接加载,不处理依赖。modprobe会帮你自动解决依赖问题。我建议你平时用modprobe,省心。
每个模块都有两个关键函数:
// 模块加载时调用
static int __init my_init(void) {
printk(KERN_INFO "Hello, module loaded!\n");
return 0;
}
// 模块卸载时调用
static void __exit my_exit(void) {
printk(KERN_INFO "Goodbye, module unloaded!\n");
}
module_init(my_init);
module_exit(my_exit);
我曾经犯过一个低级错误:在init函数里申请了资源,但exit函数里忘了释放。结果模块卸载后,内存泄漏了。从那以后,我每次写模块都会先画个资源申请/释放的对照表。
2.3 模块参数
模块参数是个好东西。它让你可以在加载模块时传递配置值,不用改代码。
举个例子:
#include <linux/moduleparam.h>
static int debug_level = 0;
static char *device_name = "default";
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
加载时这样用:
insmod my_module.ko debug_level=2 device_name="mydev"
我个人习惯把调试相关的参数都做成模块参数。这样在产品阶段,如果现场出了问题,可以通过参数临时打开调试信息,不用重新编译。
小技巧:模块参数的权限位(0644)决定了这个参数是否能在sysfs中查看和修改。0644表示root可读写,其他人只读。
2.4 模块依赖
模块之间也会有依赖关系。比如你的驱动要用到另一个模块导出的函数或变量。
看个例子:
// module_a.c
EXPORT_SYMBOL(my_function);
// module_b.c
extern void my_function(void); // 依赖module_a
加载顺序必须是:先加载module_a,再加载module_b。卸载时顺序相反。
用modprobe可以自动处理这个顺序。它会读取/lib/modules/$(uname -r)/modules.dep文件,找到依赖关系。
避坑指南:我曾经在项目里遇到一个诡异的问题:模块加载成功了,但功能不正常。查了两天才发现,是依赖的模块版本不匹配。所以,模块依赖不仅要看符号,还要看版本。用modinfo可以查看模块的vermagic。
2.5 Makefile编写
写模块的Makefile,其实有固定套路。我直接给你一个模板:
# 模块名称
obj-m := my_module.o
# 如果模块由多个源文件组成
# my_module-objs := file1.o file2.o
# 内核源码路径
KDIR := /lib/modules/$(shell uname -r)/build
# 当前目录
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
这个Makefile的核心是:
- obj-m:告诉内核构建系统,我们要编译成模块
- -C $(KDIR):切换到内核源码目录
- M=$(PWD):返回当前目录编译模块
为什么这么写?因为模块编译必须依赖内核的构建系统。它需要内核源码里的头文件、Makefile规则等。
我刚开始写Makefile时,总记不住这些变量名。后来我干脆把这个模板存成了文件,每次新建项目直接复制改个名字就行。
2.6 知识体系总览
下面这张图,是我为你梳理的本章知识结构:
这张图把本章的核心内容串起来了。你看到没?所有知识点都围绕着“动态扩展内核功能”这个目标。
本章要点回顾:
- 模块是内核的插件,用.ko文件表示
- 加载用insmod/modprobe,卸载用rmmod
- 模块参数让驱动更灵活,不用改代码就能调配置
- 依赖关系要处理好,否则模块加载会失败
- Makefile有固定模板,记住obj-m和KDIR就行
好了,这一章的内容就到这里。模块是驱动开发的基础,后面的章节我们会在这个基础上一步步搭建更复杂的驱动。你先把这些命令和代码多练几遍,熟能生巧。