2、第一个字符设备驱动:模块初始化与退出、module_init/module_exit宏、LICENSE和作者信息
2.1 从零开始:驱动模块的骨架
说实话,我第一次写字符设备驱动时,面对的就是一个空荡荡的.c文件。当时我就在想——这玩意儿到底长什么样?
一个最简单的驱动模块,其实就三个要素:入口函数、出口函数、还有许可证声明。你想想看,内核加载你的模块时总得知道从哪开始执行吧?卸载时也得知道怎么清理吧?这就是 module_init 和 module_exit 存在的意义。
我个人习惯,写任何驱动前先把这三样东西摆好,就像盖房子先搭脚手架一样。
2.2 module_init 和 module_exit 宏
这两个宏是内核提供的“注册机制”。说白了,它们告诉内核:
- module_init(func):加载模块时,调用 func 函数
- module_exit(func):卸载模块时,调用 func 函数
来看一个最基础的例子:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_driver_init(void)
{
printk(KERN_INFO "Hello, my driver loaded!\n");
return 0;
}
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "Goodbye, my driver unloaded!\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <your@email.com>");
MODULE_DESCRIPTION("A simple character device driver example");
嗯,这里要注意几个细节:
__init和__exit是内核的“提示标记”。__init告诉内核:这个函数只在初始化时用一次,用完后可以释放内存。__exit则告诉内核:如果模块被编译进内核(而非动态加载),这个函数可以省略。printk是内核版的 printf。我在项目中遇到过,调试时经常用它打印信息,但正式发布前记得清理掉,否则 dmesg 里全是垃圾。- 返回值:初始化成功返回 0,失败返回负数错误码。
2.3 为什么需要 MODULE_LICENSE?
你可能觉得奇怪——一个许可证声明而已,不写行不行?
我曾经试过不写 MODULE_LICENSE,结果 insmod 时内核直接报错:module license 'unspecified' taints kernel。内核会认为你的模块是“脏”的,污染了内核。说白了,这是内核社区对 GPL 协议的一种保护机制。
常见的许可证声明有:
| 宏定义 | 含义 |
|---|---|
MODULE_LICENSE("GPL") |
GNU General Public License v2 |
MODULE_LICENSE("GPL v2") |
GPL v2 版本 |
MODULE_LICENSE("Dual BSD/GPL") |
双许可证:BSD 或 GPL |
MODULE_LICENSE("Proprietary") |
专有许可证(会污染内核) |
我个人建议,除非你有特殊法律需求,否则一律用 "GPL"。这样你的模块可以和内核“和平共处”,也能调用大部分内核导出的符号。
2.4 作者信息与模块描述
除了许可证,还有几个常用的模块信息宏:
MODULE_AUTHOR("name <email>"):作者信息MODULE_DESCRIPTION("description"):模块功能描述MODULE_VERSION("1.0"):版本号MODULE_ALIAS("alias_name"):别名,方便 modprobe 自动加载
这些信息可以通过 modinfo 命令查看:
$ modinfo my_driver.ko
filename: my_driver.ko
license: GPL
author: Your Name <your@email.com>
description: A simple character device driver example
version: 1.0
depends:
name: my_driver
vermagic: 5.15.0-generic SMP mod_unload modversions
你看,这些信息对模块的管理和分发很有帮助。我在团队协作时,经常靠 modinfo 快速确认模块版本和作者,省去了翻代码的麻烦。
2.5 编译与测试
写好了代码,怎么跑起来?你需要一个 Makefile:
obj-m := my_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 my_driver.ko # 加载模块
$ sudo rmmod my_driver # 卸载模块
$ dmesg | tail -2 # 查看内核日志
你应该能看到:
[ 1234.5678] Hello, my driver loaded!
[ 1235.6789] Goodbye, my driver unloaded!
我曾经犯过一个低级错误——忘记用 sudo,结果 insmod 报权限不足。嗯,这种小坑踩过一次就记住了。
2.6 核心知识体系
为了让你更直观地理解本章内容,我画了一张图:
核心要点总结:
- 每个驱动模块必须有
module_init和module_exit MODULE_LICENSE是强制要求,不写会污染内核- 作者信息和描述虽然不是强制,但强烈建议写上
__init和__exit是优化标记,能节省内存
小技巧: 调试时可以用 printk(KERN_DEBUG "...") 打印调试信息,配合 dmesg -w 实时查看日志。我习惯在关键路径上打几个日志点,定位问题快很多。
注意: 千万不要在生产环境里用 printk 打印大量信息,会影响系统性能。我曾经见过有人循环里打印日志,结果系统直接卡死——教训深刻啊。
好了,第一个字符设备驱动的骨架你已经掌握了。接下来就是往这个骨架里填充真正的设备操作——open、read、write 这些函数。不过那是下一章的内容了,先把基础打牢再说。