内核模块入门:从用户空间到内核空间的第一步¶
适用平台:i.MX6ULL (ARM Cortex-A7) 内核版本:Linux 6.12.49 (linux-imx) 目标读者:准备入门驱动开发的嵌入式工程师 前置知识:C语言基础、Linux基本操作
为什么要写这篇文章¶
如果说Linux内核是一座巨大的城堡,那内核模块就是你进入这座城堡的特制通行证。
我知道,很多刚开始学驱动开发的同学都有这种感觉:内核太复杂了,不知道从哪下手。你想写一个简单的GPIO驱动,但网上教程一上来就是一堆陌生的API,什么request_irq、cdev_init、file_operations...看得你头晕眼花。
其实,所有这些复杂的东西都建立在一个基础之上:内核模块。内核模块是Linux驱动开发的起点,也是理解内核工作原理的最好切入点。
这篇文章的目标非常明确:带你彻底搞清楚什么是内核模块,它和普通程序有什么区别,内核是如何加载和卸载模块的,以及你该如何开始编写自己的第一个模块。
在正式开始之前,我要说明一下:本文所有关于内核源码的引用都基于 third_party/linux-imx 目录下的实际代码,这是NXP维护的i.MX系列处理器内核,版本是6.12.49。我们引用代码时会明确标注文件路径,方便你去查看源码。
一、什么是内核模块,为什么需要它¶
1.1 单内核与可加载模块¶
Linux采用的是单内核(Monolithic Kernel)架构。这意味着整个操作系统核心运行在一个单一的地址空间里,所有内核代码共享同一个内存空间。与之相对的是微内核(如Minix),微内核把很多服务放到用户空间作为独立进程运行。
单内核的优势是性能好——函数调用就是普通的函数调用,不需要IPC(进程间通信)的额外开销。但单内核有个大问题:一旦内核编译完成,要添加新功能就得重新编译整个内核,这在实际使用中很不方便。
内核模块(Loadable Kernel Module,LKM)就是Linux为了解决这个问题而引入的机制。它允许你在不重新编译内核的情况下,动态地向运行中的内核添加或删除代码。
你可以把内核模块理解为"内核的插件":
- 编译时:模块是独立的.ko(Kernel Object)文件
- 加载时:模块的代码被加载到内核地址空间,成为内核的一部分
- 卸载时:模块的代码从内核移除,释放占用的资源
1.2 内核模块的典型应用场景¶
在嵌入式Linux开发中,内核模块主要用于:
- 设备驱动:这是最常见的用途。GPIO、I2C、SPI、UART等外设驱动通常以模块形式存在
- 文件系统:支持新的文件系统类型
- 网络协议:添加新的网络协议或过滤规则
- 调试和监控:如ftrace、perf等工具的核心模块
- 安全模块:如SELinux、AppArmor的Linux Security Module(LSM)
对于i.MX6ULL开发来说,你会频繁打交道的模块包括:
- imx-sdma.ko - SDMA固件驱动
- imx-thermal.ko - 温度传感器驱动
- gpio_keys.ko - GPIO按键驱动
- 各种你自定义的设备驱动
1.3 模块 vs 静态编译¶
| 特性 | 模块(.ko) | 静态编译(vmlinuz) |
|---|---|---|
| 加载方式 | 运行时动态加载 | 启动时自动加载 |
| 内存占用 | 按需占用 | 始终占用 |
| 开发效率 | 高(无需重启) | 低(需重新编译内核) |
| 适用场景 | 开发调试、可选驱动 | 核心功能、启动必需驱动 |
二、用户空间 vs 内核空间:你必须理解的核心概念¶
在深入内核模块之前,有一个概念你必须彻底理解:用户空间和内核空间的区别。这是理解内核模块工作原理的基础,也是新手最容易踩坑的地方。
2.1 虚拟地址空间的划分¶
现代操作系统使用虚拟内存,每个进程都有自己独立的虚拟地址空间。在Linux中(以32位ARM为例),这个4GB的虚拟地址空间被划分为两部分(参见 Documentation/mm/highmem.rst):
+----------------------+ 0xFFFFFFFF
| |
| 内核空间 (1GB) | ← 所有进程共享
| |
+----------------------+ 0xC0000000 (内核空间边界)
| |
| |
| 用户空间 (3GB) | ← 进程私有空间
| |
| |
+----------------------+ 0x00000000
关键点: - 用户空间:0x00000000 ~ 0xBFFFFFFF(3GB) - 内核空间:0xC0000000 ~ 0xFFFFFFFF(1GB) - 每个进程有独立的用户空间,但所有进程共享同一个内核空间
对于64位系统(如i.MX8系列),这个划分会有所不同,但原理是一样的:虚拟地址空间被划分为用户空间和内核空间两部分。
2.2 用户空间的特权限制¶
用户空间程序运行在非特权模式(User Mode,ARMv7称为PL0),有以下限制:
- 不能直接访问硬件:不能直接读写寄存器
- 受限的内存访问:只能访问自己的虚拟地址空间
- 不能执行特权指令:如修改页表、开关中断等
- 需要系统调用进入内核:任何需要内核服务的操作都必须通过系统调用
2.3 内核空间的特权¶
内核代码运行在特权模式(Kernel Mode,ARMv7称为PL1),拥有:
- 完整的硬件访问:可以直接访问所有外设寄存器
- 完整的内存访问:可以访问任何物理内存
- 特权指令:可以修改页表、控制中断等
- 无限制:理论上可以做任何事情(因此内核bug可能导致系统崩溃)
2.4 用户空间与内核空间的通信¶
既然用户空间和内核空间是隔离的,它们如何通信?
+----------------+ +----------------+
| | 系统调用接口 | |
| 用户空间 | <---------------------> | 内核空间 |
| 应用程序 | ioctl / mmap / proc | 内核模块 |
| | | |
+----------------+ +----------------+
主要的通信方式包括:
| 方式 | 用途 | 典型应用 |
|---|---|---|
| 系统调用(syscall) | 基本服务 | open、read、write |
| ioctl | 设备特定操作 | 配置寄存器、控制设备 |
| mmap | 内存映射 | 将设备寄存器映射到用户空间 |
| procfs/sysfs | 信息暴露 | /proc、/sys 下的文件 |
| netlink | 网络相关 | 路由、防火墙规则 |
2.5 为什么内核模块需要特别小心¶
正因为内核运行在特权模式,内核模块的代码bug可能导致:
- 系统崩溃:Oops、panic
- 数据损坏:误写内核数据结构
- 安全漏洞:权限提升、信息泄露
- 硬件损坏:错误配置外设寄存器(罕见但可能)
这也是为什么内核开发有严格的代码规范要求(参见 Documentation/process/coding-style.rst)。
三、内核模块的基本架构¶
现在让我们看看内核模块到底长什么样。一个完整的内核模块包含以下几个部分:
3.1 模块的基本结构¶
/* 头文件包含 */
#include <linux/module.h> // 模块核心API
#include <linux/init.h> // __init、__exit宏
#include <linux/kernel.h> // printk等核心函数
/* 模块许可证声明(必须!) */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <your.email@example.com>");
MODULE_DESCRIPTION("A simple kernel module for i.MX6ULL");
MODULE_VERSION("1.0");
/* 模块初始化函数 - 加载时执行 */
static int __init my_module_init(void)
{
printk(KERN_INFO "my_module: Hello, kernel space!\n");
/* 初始化代码放在这里 */
return 0; // 返回0表示成功,非0表示失败
}
/* 模块清理函数 - 卸载时执行 */
static void __exit my_module_exit(void)
{
printk(KERN_INFO "my_module: Goodbye, kernel space!\n");
/* 清理代码放在这里 */
}
/* 注册模块的加载和卸载函数 */
module_init(my_module_init);
module_exit(my_module_exit);
3.2 关键宏说明¶
| 宏 | 用途 | 必需性 |
|---|---|---|
MODULE_LICENSE |
声明代码许可证 | 必需 |
MODULE_AUTHOR |
作者信息 | 推荐 |
MODULE_DESCRIPTION |
模块描述 | 推荐 |
MODULE_VERSION |
模块版本 | 可选 |
module_init |
指定初始化函数 | 必需 |
module_exit |
指定清理函数 | 必需 |
3.3 __init 和 __exit 标记¶
你可能会注意到代码中的 __init 和 __exit 标记:
static int __init my_module_init(void) // __init标记
{
// ...
}
static void __exit my_module_exit(void) // __exit标记
{
// ...
}
这两个标记的作用(定义在 include/linux/init.h):
__init:标记的代码会被放在.init.text段,模块加载成功后这些内存会被释放__exit:标记的代码会被放在.exit.text段,只有当模块可卸载时才链接
这是一个内存优化技术:内核的初始化代码只需要执行一次,执行完就可以把内存释放掉。
3.4 模块许可证的重要性¶
MODULE_LICENSE 不仅仅是法律声明,它直接影响模块的功能:
/* 来自 include/linux/module.h */
MODULE_LICENSE("GPL"); // 可以访问GPL符号
MODULE_LICENSE("GPL v2"); // 可以访问GPL符号
MODULE_LICENSE("Dual BSD/GPL"); // 可以访问GPL符号
MODULE_LICENSE("Dual MIT/GPL"); // 可以访问GPL符号
MODULE_LICENSE("Proprietary"); // 不能访问GPL符号!
如果许可证不是GPL兼容的,模块将无法使用 EXPORT_SYMBOL_GPL 导出的符号。内核中很多重要API都是用 EXPORT_SYMBOL_GPL 导出的,所以非GPL许可证的模块功能会受限。
四、模块加载流程:从 insmod 到内核函数调用¶
当你在命令行执行 insmod my_module.ko 时,内核到底做了什么?让我们追踪这个过程的源码实现。
4.1 系统调用入口¶
insmod 命令最终会调用 init_module 系统调用。这个系统调用的定义在 kernel/module/main.c:
/* third_party/linux-imx/kernel/module/main.c:3066 */
SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
{
// ...
err = copy_module_from_user(umod, len, &info);
if (err) {
mod_stat_inc(&failed_kreads);
return err;
}
return load_module(&info, uargs, 0);
}
4.2 模块加载的主流程¶
load_module() 函数是模块加载的核心,定义在 kernel/module/main.c:2857:
/* third_party/linux-imx/kernel/module/main.c:2857 */
static int load_module(struct load_info *info, const char __user *uargs,
int flags)
{
struct module *mod;
long err = 0;
/* 1. 签名验证 */
err = module_sig_check(info, flags);
if (err)
goto free_copy;
/* 2. ELF格式验证和拷贝 */
err = elf_validity_cache_copy(info, flags);
if (err)
goto free_copy;
/* 3. 早期模块检查 */
err = early_mod_check(info, flags);
if (err)
goto free_copy;
/* 4. 布局和内存分配 */
mod = layout_and_allocate(info, flags);
if (IS_ERR(mod)) {
err = PTR_ERR(mod);
goto free_copy;
}
/* 5. 添加到模块列表 */
err = add_unformed_module(mod);
if (err)
goto free_module;
/* 6. 更新内核taint标志 */
module_augment_kernel_taints(mod, info);
/* 7. Per-CPU数据分配 */
err = percpu_modalloc(mod, info);
if (err)
goto unlink_mod;
/* 8. 模块卸载初始化 */
err = module_unload_init(mod);
if (err)
goto unlink_mod;
/* 9. 查找各个段 */
err = find_module_sections(mod, info);
if (err)
goto free_unload;
/* 10. 检查符号版本 */
err = check_export_symbol_versions(mod);
if (err)
goto free_unload;
/* 11. 设置模块信息 */
setup_modinfo(mod, info);
/* 12. 符号解析 */
err = simplify_symbols(mod, info);
if (err < 0)
goto free_modinfo;
/* 13. 重定位 */
err = apply_relocations(mod, info);
if (err < 0)
goto free_modinfo;
/* 14. 重定位后处理 */
err = post_relocation(mod, info);
if (err < 0)
goto free_modinfo;
/* 15. 刷新指令缓存 */
flush_module_icache(mod);
/* 16. 复制参数 */
mod->args = strndup_user(uargs, ~0UL >> 1);
if (IS_ERR(mod->args)) {
err = PTR_ERR(mod->args);
goto free_arch_cleanup;
}
/* 17. 初始化build ID */
init_build_id(mod, info);
/* 18. Ftrace初始化 */
ftrace_module_init(mod);
/* 19. 最后的初始化 - 调用你的init函数! */
/* ... */
return do_init_module(mod);
}
4.3 执行模块的初始化函数¶
do_init_module() 函数最终会调用你在模块中定义的初始化函数:
/* third_party/linux-imx/kernel/module/main.c:2516 */
static noinline int do_init_module(struct module *mod)
{
int ret = 0;
struct mod_initfree *freeinit;
/* 分配init内存 */
freeinit = kmalloc(sizeof(*freeinit), GFP_KERNEL);
if (!freeinit) {
ret = -ENOMEM;
goto fail;
}
/* 执行构造函数 */
do_mod_ctors(mod);
/* 这里!调用模块的init函数 */
if (mod->init != NULL)
ret = do_one_initcall(mod->init);
if (ret < 0) {
goto fail_free_freeinit;
}
/* 模块状态变为LIVE */
mod->state = MODULE_STATE_LIVE;
blocking_notifier_call_chain(&module_notify_list,
MODULE_STATE_LIVE, mod);
/* 发送uevent */
kobject_uevent(&mod->mkobj.kobj, KOBJ_ADD);
/* ... */
/* 释放init段内存 */
if (llist_add(&freeinit->node, &init_free_list))
schedule_work(&init_free_wq);
return 0;
fail:
/* 失败处理 */
mod->state = MODULE_STATE_GOING;
/* ... */
return ret;
}
4.4 模块状态机¶
内核用 enum module_state 来表示模块的状态(定义在 include/linux/module.h):
/* third_party/linux-imx/include/linux/module.h:320 */
enum module_state {
MODULE_STATE_LIVE, /* 正常运行状态 */
MODULE_STATE_COMING, /* 正在初始化 */
MODULE_STATE_GOING, /* 正在卸载 */
MODULE_STATE_UNFORMED, /* 还在设置中 */
};
模块加载时的状态转换:
4.5 模块卸载流程¶
当你执行 rmmod my_module 时,内核会调用 delete_module 系统调用:
/* third_party/linux-imx/kernel/module/main.c:700 */
SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
unsigned int, flags)
{
struct module *mod;
char name[MODULE_NAME_LEN];
int ret;
/* 1. 权限检查 */
if (!capable(CAP_SYS_MODULE) || modules_disabled)
return -EPERM;
/* 2. 查找模块 */
mod = find_module(name);
if (!mod) {
ret = -ENOENT;
goto out;
}
/* 3. 检查依赖 */
if (!list_empty(&mod->source_list)) {
/* 有其他模块依赖我们 */
ret = -EWOULDBLOCK;
goto out;
}
/* 4. 检查状态 */
if (mod->state != MODULE_STATE_LIVE) {
ret = -EBUSY;
goto out;
}
/* 5. 检查是否有exit函数 */
if (mod->init && !mod->exit) {
/* 有init但没有exit,不能卸载(除非force) */
ret = -EBUSY;
goto out;
}
/* 6. 停止模块 */
ret = try_stop_module(mod, flags, &forced);
if (ret != 0)
goto out;
/* 7. 调用模块的exit函数 */
if (mod->exit != NULL)
mod->exit();
/* 8. 清理模块 */
blocking_notifier_call_chain(&module_notify_list,
MODULE_STATE_GOING, mod);
free_module(mod);
return 0;
}
五、内核版本与模块版本兼容性¶
这是新手最容易遇到的问题:为什么在开发板上编译的模块,换个内核就加载不了了?
5.1 vermagic 机制¶
内核模块在编译时会嵌入一个"版本魔法字符串"(vermagic),加载时内核会检查这个字符串是否匹配。vermagic的定义在 include/linux/vermagic.h:
/* third_party/linux-imx/include/linux/vermagic.h:42 */
#define VERMAGIC_STRING \
UTS_RELEASE " " \
MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT \
MODULE_VERMAGIC_MODULE_UNLOAD MODULE_VERMAGIC_MODVERSIONS \
MODULE_ARCH_VERMAGIC \
MODULE_RANDSTRUCT
这个字符串包含:
- UTS_RELEASE:内核版本号(如 "6.12.49")
- MODULE_VERMAGIC_SMP:是否启用SMP
- MODULE_VERMAGIC_PREEMPT:抢占模式
- MODULE_VERMAGIC_MODULE_UNLOAD:是否支持模块卸载
- MODULE_VERMAGIC_MODVERSIONS:是否启用模块版本化
- MODULE_ARCH_VERMAGIC:架构相关配置
5.2 版本检查的实现¶
版本检查的实现在 kernel/module/main.c 的 check_modinfo 函数:
/* third_party/linux-imx/kernel/module/main.c:2096 */
static int check_modinfo(struct module *mod, struct load_info *info, int flags)
{
const char *modmagic = get_modinfo(info, "vermagic");
int err;
if (flags & MODULE_INIT_IGNORE_VERMAGIC)
modmagic = NULL;
/* 没有vermagic?可以用--force强制 */
if (!modmagic) {
err = try_to_force_load(mod, "bad vermagic");
if (err)
return err;
} else if (!same_magic(modmagic, vermagic, info->index.vers)) {
pr_err("%s: version magic '%s' should be '%s'\n",
info->name, modmagic, vermagic);
return -ENOEXEC;
}
return 0;
}
same_magic 函数在 kernel/module/version.c 中实现:
/* third_party/linux-imx/kernel/module/version.c:80 */
int same_magic(const char *amagic, const char *bmagic,
bool has_crcs)
{
if (has_crcs) {
/* 如果有CRC校验,跳过内核版本部分 */
amagic += strcspn(amagic, " ");
bmagic += strcspn(bmagic, " ");
}
return strcmp(amagic, bmagic) == 0;
}
5.3 常见版本不匹配错误¶
当你看到这样的错误时:
my_module: version magic '6.12.49 preempt mod_unload '
should be '6.12.49 preempt_unipi mod_unload '
说明模块是在不同的内核配置下编译的。
解决方案:
1. 正确做法:在目标内核源码目录下编译模块
2. 强制加载(不推荐):insmod -f my_module.ko 或 modprobe --force
3. 禁止版本检查(非常危险):insmod --modversion-ignore
5.4 MODVERSIONS 机制¶
即使启用了 CONFIG_MODVERSIONS,内核还会对每个导出的符号计算CRC值:
/* third_party/linux-imx/kernel/module/version.c:13 */
int check_version(const struct load_info *info,
const char *symname,
struct module *mod,
const s32 *crc)
{
/* ... */
for (i = 0; i < num_versions; i++) {
u32 crcval;
if (strcmp(versions[i].name, symname) != 0)
continue;
crcval = *crc;
if (versions[i].crc == crcval)
return 1; /* 版本匹配 */
goto bad_version;
}
/* ... */
}
这意味着,即使内核版本号相同,如果导出符号的签名改变了,模块也无法加载。
5.5 Module.symvers 文件¶
当你编译内核或模块时,会生成 Module.symvers 文件,记录所有导出符号及其CRC值:
/* Module.symvers 格式 */
0x12345678 printk vmlinux EXPORT_SYMBOL_GPL
0x9abcdef0 kmalloc vmlinux EXPORT_SYMBOL
编译外部模块时,构建系统会读取内核源码目录下的 Module.symvers 来验证符号版本。
六、完整可编译代码示例¶
现在让我们写一个完整的、可以直接在i.MX6ULL上编译运行的模块示例。
6.1 模块源码(hello_imx6ull.c)¶
// SPDX-License-Identifier: GPL-2.0
/*
* hello_imx6ull.c - 一个简单的内核模块示例
* 适用于 i.MX6ULL + Linux 6.12.49
*
* 编译方法:
* make -C <内核源码路径> M=$(pwd) modules
*
* 加载/卸载:
* insmod hello_imx6ull.ko
* rmmod hello_imx6ull
*
* 查看输出:
* dmesg | tail
*/
#include <linux/module.h> /* 模块核心API,MODULE_* 宏 */
#include <linux/init.h> /* __init、__exit 宏 */
#include <linux/kernel.h> /* printk、pr_* 系列函数 */
#include <linux/printk.h> /* printk 相关定义 */
/*
* 模块元数据
*
* MODULE_LICENSE 是必须的,没有它模块无法使用GPL符号
* 其他元数据有助于识别模块信息
*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("IMX-Forge Tutorial <tutorial@example.com>");
MODULE_DESCRIPTION("A simple hello world module for i.MX6ULL");
MODULE_VERSION("1.0");
/*
* 模块参数示例
*
* 可以在加载时通过命令行传入:insmod hello_imx6ull.ko count=5
*/
static int count = 1;
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "Number of times to print hello message");
static char *name = "i.MX6ULL";
module_param(name, charp, 0644);
MODULE_PARM_DESC(name, "Name to greet");
/*
* hello_init - 模块初始化函数
*
* 这个函数在模块加载时被调用。所有的初始化工作应该在这里完成:
* - 注册设备
* - 分配内存
* - 注册中断
* - 创建proc/sysfs文件
*
* 返回值:0表示成功,负数表示错误码
*/
static int __init hello_init(void)
{
int i;
/*
* printk 是内核版本的 printf
* KERN_INFO 是日志级别,等价于 printk("<6>...")
* 也可以使用 pr_info() 等简写形式
*/
pr_info("=== hello_imx6ull module loading ===\n");
pr_info("Hello, %s!\n", name);
pr_info("This module was compiled for Linux %s\n", UTS_RELEASE);
pr_info("Running on %s architecture\n", UTS_MACHINE);
/* 打印多次,演示参数的使用 */
for (i = 0; i < count; i++) {
pr_info("[%d] Greeting from kernel space!\n", i + 1);
}
/*
* 打印模块信息
* THIS_MODULE 是当前模块的 struct module 指针
*/
pr_info("Module name: %s\n", THIS_MODULE->name);
pr_info("Module state: %d\n", THIS_MODULE->state);
/*
* 返回0表示成功
* 如果返回非0,内核认为模块初始化失败,不会加载它
*/
return 0;
}
/*
* hello_exit - 模块清理函数
*
* 这个函数在模块卸载时被调用。所有在init中分配的资源应该在这里释放:
* - 注销设备
* - 释放内存
* - 删除proc/sysfs文件
* - 禁用中断
*
* 注意:这个函数只在模块可以卸载时才会被调用。
* 如果模块的init函数返回失败,exit不会被调用。
*/
static void __exit hello_exit(void)
{
/*
* 打印退出消息
* 注意:在exit函数中要确保已经释放了所有资源
*/
pr_info("=== hello_imx6ull module unloading ===\n");
pr_info("Goodbye, %s!\n", name);
pr_info("Thank you for using this module.\n");
}
/*
* 注册模块的加载和卸载函数
*
* module_init 指定模块加载时调用的函数
* module_exit 指定模块卸载时调用的函数
*/
module_init(hello_init);
module_exit(hello_exit);
6.2 Makefile¶
# hello_imx6ull 模块的 Makefile
#
# 使用方法:
# make - 编译模块
# make clean - 清理编译产物
# make install - 安装模块到系统
# 内核源码路径 - 修改为你的实际路径
KERNEL_DIR := /home/charliechen/imx-forge/third_party/linux-imx
# 当前目录
PWD := $(shell pwd)
# 模块名称
obj-m := hello_imx6ull.o
# 编译目标
all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
# 清理目标
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
rm -f Module.symvers modules.order
# 安装目标
install:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules_install
.PHONY: all clean install
6.3 编译和测试¶
# 1. 进入模块目录
cd /path/to/module/directory
# 2. 编译模块
make
# 编译成功后会生成:
# hello_imx6ull.ko - 内核模块文件
# hello_imx6ull.o - 目标文件
# hello_imx6ull.mod.o - 包含模块元信息的目标文件
# Module.symvers - 导出的符号列表
# modules.order - 模块顺序
# 3. 查看模块信息
modinfo hello_imx6ull.ko
# 4. 加载模块(使用默认参数)
sudo insmod hello_imx6ull.ko
# 5. 查看内核日志
dmesg | tail
# 6. 检查模块是否加载成功
lsmod | grep hello
# 7. 查看模块参数
cat /sys/module/hello_imx6ull/parameters/*
# 或者
systool -v -m hello_imx6ull
# 8. 卸载模块
sudo rmmod hello_imx6ull
# 9. 使用自定义参数加载
sudo insmod hello_imx6ull.ko count=3 name="Linux Learner"
七、常见错误、调试方法与内核报错解读¶
7.1 常见编译错误¶
错误1:找不到内核头文件¶
原因:KERNEL_DIR 路径不正确,或者内核源码没有配置。
解决:
# 确认路径是否正确
ls $(KERNEL_DIR)/include/linux/module.h
# 如果内核源码没有配置,运行
cd $(KERNEL_DIR)
make imx_v6_defconfig # 或其他合适的defconfig
make modules_prepare
错误2:缺少 Module.symvers¶
原因:内核没有完整编译过。
解决:
7.2 常见加载错误¶
错误1:版本不匹配¶
hello_imx6ull: version magic '6.12.49 preempt mod_unload '
should be '6.12.0 preempt mod_unload '
insmod: ERROR: could not insert module
原因:模块编译时使用的内核版本与运行时不同。
解决:
错误2:许可证问题¶
hello_imx6ull: module license 'Proprietary' taints kernel.
hello_imx6ull: module uses symbols from GPL-only module, refusing to load.
原因:模块许可证不是GPL兼容的,但使用了GPL符号。
解决:将 MODULE_LICENSE("Proprietary") 改为 MODULE_LICENSE("GPL")。
错误3:未知符号¶
原因:模块使用了一个不存在的内核符号。
解决:检查函数名是否拼写正确,或者该符号是否在当前内核配置下被导出。
7.3 调试技巧¶
技巧1:使用 dmesg 查看输出¶
技巧2:使用 ftrace 跟踪函数¶
# 启用 ftrace
echo function > /sys/kernel/debug/tracing/current_tracer
# 只跟踪特定函数
echo 'do_init_module' > /sys/kernel/debug/tracing/set_ftrace_filter
# 查看跟踪结果
cat /sys/kernel/debug/tracing/trace
技巧3:使用 crash 分析 Oops¶
如果模块导致内核崩溃,生成的 Oops 信息可以用来分析问题:
[ 123.456789] BUG: unable to handle page fault for address: 0xdeadbeef
[ 123.456790] Internal error: Oops: 86000007 [#1] PREEMPT SMP ARM
[ 123.456791] Modules linked in: hello_imx6ull(O+) ...
[ 123.456792] CPU: 0 PID: 1234 Comm: insmod Tainted: G O 6.12.49 ...
[ 123.456793] Hardware name: Freescale i.MX6 Ultralite (Device Tree)
[ 123.456794] PC is at hello_init+0x10/0x20 [hello_imx6ull]
[ 123.456795] LR is at do_one_initcall+0x48/0x1b0
使用 addr2line 可以将地址转换为源码行号:
7.4 内核 OOPS/panic 解读¶
当内核检测到严重错误时,会打印 Oops 或 panic 信息。理解这些信息对于调试非常重要:
| 类型 | 含义 | 系统状态 |
|---|---|---|
| BUG() | 内核断言失败 | 继续运行(但可能已损坏) |
| Oops | 页面错误或非法访问 | 继续运行(但可能已损坏) |
| panic | 致命错误,无法继续 | 系统停止运行 |
常见的 Oops 原因: 1. 空指针解引用:访问 NULL 指针 2. 用户空间指针:在内核空间访问用户空间地址 3. 栈溢出:递归过深或局部变量太大 4. 非法指令:CPU执行了无效指令
八、练习题与实战代码查看¶
练习题 1:带参数的模块¶
修改 hello_imx6ull 模块,添加以下功能:
- 添加一个
bool类型的参数verbose,控制是否输出详细日志 - 添加一个
int类型的参数delay,表示初始化时的延迟秒数 - 当
verbose=true时,打印更多调试信息
参考答案:
static bool verbose = false;
module_param(verbose, bool, 0644);
MODULE_PARM_DESC(verbose, "Enable verbose output");
static int delay = 0;
module_param(delay, int, 0644);
MODULE_PARM_DESC(delay, "Delay in seconds before initialization");
static int __init hello_init(void)
{
if (verbose) {
pr_info("Verbose mode enabled\n");
pr_info("Parameters: verbose=%d, delay=%d\n", verbose, delay);
pr_info("Compile time: %s %s\n", __DATE__, __TIME__);
}
if (delay > 0) {
pr_info("Sleeping for %d seconds...\n", delay);
msleep(delay * 1000); /* 注意:不能用 sleep()! */
}
pr_info("Hello, %s!\n", name);
return 0;
}
练习题 2:查看内核源码中的模块实现¶
查找并阅读以下源码文件,理解它们是如何实现模块功能的:
third_party/linux-imx/kernel/module/main.c- 模块加载/卸载的核心逻辑third_party/linux-imx/include/linux/module.h- 模块相关的数据结构和宏定义third_party/linux-imx/kernel/module/version.c- 版本检查机制
任务:
- 在 main.c 中找到 SYSCALL_DEFINE3(finit_module, ...) 函数,理解它是如何被用户空间调用的
- 在 module.h 中找到 struct module 的完整定义,理解内核如何描述一个模块
- 在 version.c 中找到 check_version() 函数,理解CRC校验是如何工作的
练习题 3:查看系统中已加载的模块¶
在i.MX6ULL开发板上执行以下命令,理解输出内容:
# 1. 查看所有已加载的模块
lsmod
# 2. 查看特定模块的信息
modinfo gpio_keys
# 3. 查看模块的依赖关系
lsmod | grep -E "^Module|^gpio"
modprobe --show-depends gpio_keys
# 4. 查看 /sys/module 下的信息
ls /sys/module/gpio_keys/
cat /sys/module/gpio_keys/refcnt
cat /sys/module/gpio_keys/coresize
cat /sys/module/gpio_keys/taint
# 5. 查看 /proc/modules
cat /proc/modules | head
练习题 4:分析一个实际的驱动模块¶
选择一个简单的驱动模块(如 drivers/char/misc.c 或 drivers/gpio/gpiolib-sysfs.c),阅读它的源码:
- 找到
module_init和module_exit宏 - 理解模块初始化时做了什么
- 理解模块清理时做了什么
- 找到模块导出的符号(
EXPORT_SYMBOL或EXPORT_SYMBOL_GPL)
练习题 5:创建一个统计模块¶
编写一个内核模块,在 /proc 下创建一个文件,显示以下统计信息:
- 模块加载后的运行时间(秒)
- 模块被访问的次数(每次读取
/proc文件时计数) - 当前系统负载(从内核获取)
提示:
- 使用 proc_create() 创建 /proc 文件
- 使用 jiffies 或 ktime_get() 获取时间
- 使用 module_put() 和 try_module_get() 管理引用计数
九、下一步学习建议¶
恭喜你!如果你读到了这里,说明你已经对内核模块有了基本的理解。下一步,你可以:
- 学习设备驱动基础:理解字符设备、块设备、网络设备的概念
- 学习并发控制:理解 spinlock、mutex、semaphore 等同步机制
- 学习内存管理:理解 kmalloc、vmalloc、slab 分配器的区别
- 实践项目:为你的i.MX6ULL板子编写一个实际的设备驱动
推荐继续阅读本教程系列: - 下一章:字符设备驱动入门 - 设备树基础与应用 - GPIO驱动实战 - I2C/SPI驱动开发
参考资料与延伸阅读¶
内核文档¶
Documentation/kbuild/modules.rst- 外部模块编译指南Documentation/process/coding-style.rst- 内核编码风格Documentation/core-api/kobject.rst- kobject 和 sysfsDocumentation/driver-api/- 驱动开发API文档
推荐书籍¶
- "Linux Device Drivers, 3rd Edition" - Jonathan Corbet 等
- "Understanding the Linux Kernel, 3rd Edition" - Daniel P. Bovet
- "Linux Kernel Development, 3rd Edition" - Robert Love
在线资源¶
- Linux Kernel Newbies - 内核新手社区
- The Linux Kernel Module Programming Guide - 模块编程指南
- Linux Cross Reference - lxr.linux.no - 在线源码浏览
作者: IMX-Forge 项目组 最后更新: 2026年3月 内核版本: 6.12.49 (linux-imx)