2. Linux 内核模块基础:模块定义、module_init/module_exit宏、许可证声明与模块参数
在深入蓝牙驱动中断配置之前,必须牢固掌握 Linux 内核模块的基本骨架。内核模块是运行时动态加载到内核的代码片段,它不像用户态程序拥有 main() 入口,而是通过特定的宏和结构体向内核注册生命周期回调。本节将拆解一个最小蓝牙驱动模块的每个组成部分,并重点说明许可证声明与模块参数的实际用途。
2.1 内核模块的标准骨架
一个完整的内核模块至少包含以下四个要素:
- 头文件包含:
<linux/module.h>(模块核心 API)和<linux/init.h>>(初始化/退出宏)。 - 许可证声明:使用
MODULE_LICENSE()宏,直接影响符号导出与内核态度。 - 入口/出口函数:通过
module_init()和module_exit()宏注册。 - 模块参数(可选):通过
module_param()宏实现加载时传参。
关键区别: 用户态程序从
main() 开始执行,内核模块则从 module_init() 指定的函数开始,且不能调用标准 C 库函数(如 printf),必须使用内核提供的 printk。
2.2 module_init / module_exit 宏的工作机制
这两个宏并非函数,而是编译器属性标记。它们将初始化函数和退出函数的指针放入内核预定义的 ELF 段(.init.text 和 .exit.text)中。当用户执行 insmod 时,内核模块加载器从 .init.text 段取出函数指针并执行;执行完毕后,该段占用的内存会被释放。退出时,rmmod 触发 .exit.text 中的函数。
2.3 许可证声明:MODULE_LICENSE
许可证声明不是形式主义,它直接影响内核符号的导出行为。如果模块声明为 GPL,内核会向其开放所有 EXPORT_SYMBOL_GPL 标记的符号;否则只能访问 EXPORT_SYMBOL 导出的符号。蓝牙核心协议栈(如 BlueZ)中的许多关键函数都使用 EXPORT_SYMBOL_GPL 导出。
| 许可证字符串 | 含义 | 符号访问范围 | 蓝牙驱动场景 |
|---|---|---|---|
"GPL" |
GNU 通用公共许可证 v2 | 全部导出符号(含 GPL-only) | ✅ 推荐,可调用 BlueZ 内部 API |
"GPL v2" |
GPL v2 专用 | 全部导出符号 | ✅ 与 "GPL" 等效 |
"BSD" |
BSD 许可证 | 仅非 GPL 符号 | ⚠️ 无法使用 GPL-only 蓝牙符号 |
"Proprietary" |
专有许可证 | 仅非 GPL 符号 | ❌ 内核会标记为“污染”,不推荐 |
实战警告: 蓝牙驱动开发中,如果声明
MODULE_LICENSE("Proprietary"),内核将无法调用 hci_register_dev() 等 GPL-only 函数,导致模块加载失败或内核崩溃。始终使用 "GPL" 或 "GPL v2"。
2.4 模块参数:module_param 与动态配置
模块参数允许在 insmod 加载时传递配置值,无需重新编译。对于蓝牙驱动调试,常用参数包括:调试日志级别、HCI 设备编号、电源管理开关等。
2.4.1 参数声明语法
#include <linux/moduleparam.h>
static int debug_level = 0;
static char *hci_name = "hci0";
static bool enable_quirks = false;
module_param(debug_level, int, 0644);
module_param(hci_name, charp, 0644);
module_param(enable_quirks, bool, 0644);
MODULE_PARM_DESC(debug_level, "Debug log level (0=off, 1=basic, 2=verbose)");
MODULE_PARM_DESC(hci_name, "HCI device name");
MODULE_PARM_DESC(enable_quirks, "Enable hardware quirk workarounds");
2.4.2 参数类型与权限
| 类型标识 | C 类型 | 加载示例 | 说明 |
|---|---|---|---|
byte |
unsigned char | debug_level=2 |
单字节无符号整数 |
int |
int | irq_num=37 |
标准整数,蓝牙中断号常用 |
bool |
bool | enable_quirks=1 |
布尔开关,0/1 或 y/n |
charp |
char * | hci_name=hci1 |
字符串指针,指向内核拷贝的副本 |
权限值(如 0644)控制 /sys/module/<name>/parameters/ 下对应文件的访问权限。设为 0 则不在 sysfs 中暴露。
2.5 完整的最小蓝牙模块示例
下面是一个包含所有基础要素的蓝牙驱动模块骨架,可直接编译并加载测试:
/*
* bt_minimal.c - 最小蓝牙内核模块示例
*/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
static int bt_irq = 37; /* 默认蓝牙中断号 */
static char *bt_device = "hci0"; /* 默认 HCI 设备名 */
static bool bt_debug = false; /* 调试开关 */
module_param(bt_irq, int, 0644);
module_param(bt_device, charp, 0644);
module_param(bt_debug, bool, 0644);
MODULE_PARM_DESC(bt_irq, "Bluetooth IRQ number");
MODULE_PARM_DESC(bt_device, "Bluetooth HCI device name");
MODULE_PARM_DESC(bt_debug, "Enable debug output");
static int __init bt_minimal_init(void)
{
printk(KERN_INFO "[BT_MIN] Loading Bluetooth module\n");
printk(KERN_INFO "[BT_MIN] IRQ: %d, Device: %s, Debug: %s\n",
bt_irq, bt_device, bt_debug ? "enabled" : "disabled");
/* 此处将在后续章节添加 HCI 注册与中断申请 */
return 0;
}
static void __exit bt_minimal_exit(void)
{
printk(KERN_INFO "[BT_MIN] Unloading Bluetooth module\n");
}
module_init(bt_minimal_init);
module_exit(bt_minimal_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Bluetooth Driver Course");
MODULE_DESCRIPTION("Minimal Bluetooth driver module example");
MODULE_VERSION("1.0");
2.6 模块信息宏与辅助声明
除了核心骨架,建议每个模块都包含以下信息宏,便于内核管理和调试:
MODULE_AUTHOR():作者信息,显示在modinfo输出中。MODULE_DESCRIPTION():简短描述模块功能。MODULE_VERSION():版本号,用于模块依赖检查。MODULE_ALIAS():为模块创建别名,方便modprobe自动加载。
调试技巧: 使用
modinfo bt_minimal.ko 查看模块信息;使用 insmod bt_minimal.ko bt_irq=42 bt_debug=1 加载并传递参数;使用 dmesg | tail 查看 printk 输出。
2.7 模块加载与卸载的完整流程
2.8 常见编译错误与避坑指南
| 错误现象 | 根本原因 | 解决方案 |
|---|---|---|
modprobe: ERROR: could not insert 'bt_minimal': Unknown symbol |
使用了 GPL-only 符号但未声明 GPL 许可证 | 添加 MODULE_LICENSE("GPL") |
insmod: ERROR: could not insert module: Invalid parameters |
模块参数类型不匹配或权限错误 | 检查 module_param 类型标识与传入值类型一致 |
BUG: unable to handle kernel NULL pointer dereference |
init 函数中使用了未检查的指针 | 始终检查 kmalloc、platform_device_alloc 等返回值 |
rmmod: ERROR: Module bt_minimal is in use |
模块被其他模块或进程引用 | 使用 lsmod 查看引用计数,先卸载依赖模块 |
本节要点: 内核模块的骨架由
module_init / module_exit 宏定义生命周期,MODULE_LICENSE 决定符号访问权限,module_param 提供运行时配置能力。在后续章节中,我们将在这个骨架上添加蓝牙 HCI 设备注册、中断申请与数据传输逻辑。