2、第一个字符设备驱动:模块初始化与退出、module_init/module_exit宏、LICENSE和作者信息

2.1 从零开始:驱动模块的骨架

说实话,我第一次写字符设备驱动时,面对的就是一个空荡荡的.c文件。当时我就在想——这玩意儿到底长什么样?

一个最简单的驱动模块,其实就三个要素:入口函数、出口函数、还有许可证声明。你想想看,内核加载你的模块时总得知道从哪开始执行吧?卸载时也得知道怎么清理吧?这就是 module_initmodule_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 核心知识体系

为了让你更直观地理解本章内容,我画了一张图:

字符设备驱动模块核心结构 insmod my_driver.ko module_init() my_driver_init() rmmod my_driver module_exit() my_driver_exit() 模块元信息(必须) MODULE_LICENSE("GPL") MODULE_AUTHOR("name <email>") MODULE_DESCRIPTION("...") MODULE_VERSION("1.0")

核心要点总结:

  • 每个驱动模块必须有 module_initmodule_exit
  • MODULE_LICENSE 是强制要求,不写会污染内核
  • 作者信息和描述虽然不是强制,但强烈建议写上
  • __init__exit 是优化标记,能节省内存

小技巧: 调试时可以用 printk(KERN_DEBUG "...") 打印调试信息,配合 dmesg -w 实时查看日志。我习惯在关键路径上打几个日志点,定位问题快很多。

注意: 千万不要在生产环境里用 printk 打印大量信息,会影响系统性能。我曾经见过有人循环里打印日志,结果系统直接卡死——教训深刻啊。


好了,第一个字符设备驱动的骨架你已经掌握了。接下来就是往这个骨架里填充真正的设备操作——open、read、write 这些函数。不过那是下一章的内容了,先把基础打牢再说。

专注资料整理