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 中的函数。

module_init / module_exit 生命周期流程 insmod rmmod module_init() module_exit() .init.text 段 执行后释放 .exit.text 段 module_init 函数执行完后,其代码段被内核回收,不可再次调用

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 模块加载与卸载的完整流程

模块加载/卸载完整流程 insmod bt_minimal.ko module_init() 注册设备/中断 返回 0 (成功) rmmod bt_minimal module_exit() 注销设备/中断 模块从内核移除 init 返回非 0 值时,模块加载失败,内核自动回滚所有注册操作

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 设备注册、中断申请与数据传输逻辑。