3、第一个字符设备驱动:模块加载与卸载、module_init/module_exit宏、LICENSE与作者信息

好,咱们正式开始写第一个字符设备驱动。

说实话,很多初学者一上来就被各种复杂的设备模型吓住了。其实没必要。字符设备驱动最核心的东西,说白了就三块:模块怎么加载、怎么卸载,以及怎么告诉内核你是谁、你写的代码是什么协议。今天我们就先把这三件事搞定。

3.1 驱动模块的“生”与“死”

一个驱动,本质上是一个内核模块。它不是一个普通的应用程序,不能双击运行。它需要被内核“请”进来,用完了再“送”出去。这个“请”和“送”的过程,就是模块的加载与卸载。

我在项目中遇到过好几次这样的情况:驱动加载成功了,但卸载时系统直接崩溃。嗯,这通常是因为卸载函数里没把资源清理干净。所以,从一开始就要养成好习惯——加载和卸载函数必须成对出现。

来看一个最简单的例子:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init my_driver_init(void)
{
    printk(KERN_INFO "Hello, my first driver loaded!\n");
    return 0;
}

static void __exit my_driver_exit(void)
{
    printk(KERN_INFO "Goodbye, driver unloaded!\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

这段代码,就是驱动模块的骨架。你想想看,它其实就两个函数:一个初始化,一个退出。内核在加载模块时会调用 my_driver_init,卸载时调用 my_driver_exit。

关键点:

  • __init 和 __exit 是内核的宏,用来标记函数。加了 __init 的函数,在模块加载完成后,内核会把它占用的内存释放掉。说白了就是“用完就扔”,省内存。
  • module_init 和 module_exit 是宏,它们告诉内核:“嘿,这个函数是入口,那个函数是出口。”
  • printk 是内核版的 printf,输出到内核日志。你可以用 dmesg 命令查看。

3.2 module_init / module_exit 宏的真相

这两个宏,看起来简单,但背后有点门道。我建议你把它理解为“注册表”。

内核维护了一张全局的模块函数表。当你调用 module_init(my_driver_init) 时,内核就把 my_driver_init 这个函数指针,登记到了这张表的“加载”那一栏。卸载同理。

为什么会这样设计?因为内核不知道你的模块叫什么名字,它只能通过固定的宏来找到入口。你想想看,如果每个驱动都自己起个名字叫 start(),内核怎么知道该调用哪个?

个人习惯: 我一般把初始化函数命名为 xxx_init,卸载函数命名为 xxx_exit。这样一看就知道是哪个模块的。别偷懒用 init 和 exit,项目大了根本分不清。

3.3 LICENSE 与作者信息——别让你的驱动变成“黑户”

内核社区对许可证非常敏感。如果你不声明 LICENSE,内核会认为你的模块是“专有”的,从而拒绝调用某些 GPL 导出的符号。说白了,就是功能受限。

我曾经在一个商业项目里,因为忘记加 MODULE_LICENSE,结果内核打印了一堆警告,而且某些内核 API 根本不能用。排查了半天才发现是这个问题。嗯,从那以后我再也不敢忘了。

标准写法如下:

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <your.email@example.com>");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_VERSION("1.0");

注意: MODULE_LICENSE 的字符串必须严格匹配内核定义的值。常见的有:

  • "GPL" —— 最常用,表示 GNU 通用公共许可证
  • "GPL v2" —— GPL 第二版
  • "Dual BSD/GPL" —— 双许可证
  • "Proprietary" —— 专有许可证(不推荐,会污染内核)

如果你写错了字符串,内核会直接忽略,按专有处理。

3.4 完整示例:第一个可用的字符设备驱动

把上面几块拼起来,就是一个完整的驱动模块了。我建议你亲手敲一遍,别复制粘贴。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init first_driver_init(void)
{
    printk(KERN_INFO "First driver: module loaded successfully.\n");
    return 0;
}

static void __exit first_driver_exit(void)
{
    printk(KERN_INFO "First driver: module unloaded.\n");
}

module_init(first_driver_init);
module_exit(first_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhang San <zhangsan@example.com>");
MODULE_DESCRIPTION("My first character device driver - demo only");
MODULE_VERSION("0.1");

3.5 编译与测试

写完了怎么跑?你需要一个 Makefile。这里给一个最简版本:

obj-m := first_driver.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

然后执行:

make
sudo insmod first_driver.ko   # 加载模块
sudo rmmod first_driver       # 卸载模块
dmesg | tail -10              # 查看内核日志

你会看到类似这样的输出:

[ 1234.5678] First driver: module loaded successfully.
[ 1235.1234] First driver: module unloaded.

避坑指南: 我曾经在 insmod 时遇到 Operation not permitted,那是因为没加 sudo。还有一次遇到 Unknown symbol,检查后发现是 MODULE_LICENSE 写成了 "GPL v2" 但内核版本只认 "GPL"。细节决定成败。

3.6 知识体系图

下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个“思维导图”来看。

第一个字符设备驱动 模块加载 module_init() 宏 __init 标记函数 模块卸载 module_exit() 宏 __exit 标记函数 模块元信息 MODULE_LICENSE MODULE_AUTHOR MODULE_DESCRIPTION

这张图里,三个分支分别对应了模块的“生”、“死”和“身份”。加载和卸载是成对出现的,元信息则是给内核和开发者看的“名片”。

3.7 小结

今天的内容,说白了就是三件事:

  • 加载与卸载:用 module_init 和 module_exit 注册入口和出口函数。
  • LICENSE:必须声明,否则内核不给你好脸色看。
  • 作者信息:方便别人找到你,也方便你自己以后维护。

嗯,这些就是字符设备驱动的“地基”。地基打牢了,后面盖楼才稳。下一章我们会在这个基础上,加上真正的设备操作——open、read、write 这些。但今天,先把模块跑起来再说。


可可精品资料库