版权信息
warning
本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
前言
当我们编写一个linux外部模块时,如果是小demo,我们还可以把所有实现都放在一个.c文件里,但是如果你想要写一个真实、可长期维护的驱动,这样是肯定不推荐的,一定是多文件结构。
文件组织形式及原则
按职责拆分,把一个模块拆为运用多个子系统的子模块,一个子系统模块单独为一个.c文件。这些子系统模块通过内部共享头文件为main.c提供使用该子系统模块的接口。
例如,一个GPIO按键驱动的目录结构如下:
key_driver/
├── Makefile
├── Kconfig # 可选
├── key_main.c # 模块入口 / probe / remove
├── key_gpio.c # GPIO 相关
├── key_irq.c # 中断相关
├── key_fops.c # 字符设备 / file_operations
├── key_internal.h # 内部头文件(核心)
└── key_uapi.h # 可选:用户态 ioctl 定义
| 文件 | 职责 |
|---|---|
key_main.c |
module_init / exit、probe/remove |
key_gpio.c |
GPIO 获取、释放、读值 |
key_irq.c |
request_irq、中断处理 |
key_fops.c |
open/read/ioctl/poll |
key_internal.h |
模块内部共享结构和函数声明 |
key_uapi.h |
给用户态用的 ioctl 定义(可选) |
文件的编写原则
最小化依赖头文件
你可能会想,我把所有.c文件需要包含的头文件都写到key_internal.h,然后其他.c文件只引用key_internal.h就好了?这种做法在 C 语言编程,尤其是 Linux 内核编程中,是应该避免的。原因如下:
- 可读性差。阅读代码的人完全不知到你的.c文件依赖哪些内核头文件。
- 编译速度。任何
.c文件都必须处理所有不必要的头文件内容,导致编译时间大幅增加。 - 命名空间污染。引入了大量的符号和宏定义,增加了符号冲突的风险,也使代码更难阅读和维护。
一个有趣的说法是,你已经把整个项目拆分成了许多功能子系统的实现,但是如果你像上面那样做,就等于又把它们杂糅在一起了,这与我们的初衷背道而驰。
应该怎么做?
- 按需包含 (Include What You Use):只包含你当前文件需要使用的定义。
- 依赖分离:将依赖关系限制在真正需要它们的源文件和头文件内。
- 保持最小依赖:只引入当前文件所需的最小集合的符号。
内部头文件的内容
只放:
- 所有
.c都需要的头文件 - 跨文件函数声明
- 核心结构体定义
- 前置声明
- 核心结构体成员所需要的头文件
其中第4、5点我们需要视情况灵活处理,详见下文。
定义结构体成员时用不用指针?
当我们编写内核模块,在定义结构体成员时,特别是这个成员也是一个结构体,到底用指针还是直接内嵌呢?
首先我们了解一下这两种方式的特点:
内嵌
🟢适用场景:
- 组合关系(Is-part-of): 子结构体是父结构体不可分割的一部分。例如,一个按键内部“自带”一个定时器。
- 生命周期同步: 子结构体必须随父结构体一起创建和销毁。
- 性能考量:
- 减少内存碎片: 一次
kmalloc就能申请到父子所有的内存。 - 缓存友好(Cache Locality): 父子结构体在物理内存中是连续的,CPU 访问速度更快。
- 减少内存碎片: 一次
- 内核习惯用法: *
struct cdev或struct list_head通常内嵌在自定义驱动结构体中,以便通过container_of宏来找回父结构体的指针。
🔴缺点:
- 必须包含头文件: 编译器必须知道子结构体的确切大小,所以头文件里必须
#include它的定义。 - 空间固定: 即使你不使用这个成员,它也会占用内存。
WHAT ABOUT 指针?
当我们定义一个指针时,其内存大小是确定的——4字节(32位系统)或8字节(64位系统)。
🟢适用场景:
- 引用关系(Refers-to): 你只是在“借用”或“观察”一个由别处创建和管理的对象。
例如: GPIO 描述符是由内核 GPIO 子系统管理的,你的驱动只是申请了一个“使用权”。 - 生命周期不同步: 子对象的寿命可能比父对象长,或者在父对象创建后才动态生成。
- 可选性(Optional): 这个成员可能不存在。指针可以设为
NULL,而内嵌结构体永远占用空间。 - 解耦依赖: 如前所述,可以使用前置声明[1],减少头文件的相互包含。
- 共享资源: 多个不同的父对象可能需要指向同一个子对象实例。
🔴缺点:
- 内存碎片: 如果子对象也需要动态分配,会多出一次内存申请开销。
- 访问开销: 需要通过指针解引用(Dereference)访问,稍微慢一点。
最终判断原则
对于我来说,在编写模块时,我会这样判断:
-
这个成员是不是来自其他子系统的句柄?我只是“借用”或“观察”一个由别处创建和管理的对象。如果是,则使用指针。
例如: GPIO 描述符是由内核 GPIO 子系统管理的,你的驱动只是申请了一个“使用权”。 -
我会不会用这个结构体成员反向寻回其父结构体(container_of)?如果是,则直接内嵌。
我最常用的还是第二点,如果第二点不满足我会直接考虑使用指针,其余还可参考上文提到的特点综合考虑,只是我更倾向于用指针。
一个头文件的例子
#ifndef __KEY_PDEV_INTERNAL_H__
#define __KEY_PDEV_INTERNAL_H__
#include <linux/types.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/timer.h>
#define DEV_NAME "key_pdev"
#define KEY_NUM 1
struct gpio_desc;//前置声明
/* -------- 数据结构 -------- */
/* key 设备下 单个按键数据结构*/
struct key_member {
int id;
struct gpio_desc *gpio_input;
int irq_num;
struct timer_list debounce_timer;
int final_val;
};
/* key 设备数据结构 */
struct key_dev {
dev_t devno;
struct cdev cdev;
struct class *cls;
struct device *device;
struct device *pdev_dev;
struct key_member KeyMem[KEY_NUM];
};
/* -------- 跨文件函数 -------- */
#endif
在这个例子里,结构体 struct key_dev 指的是整个按键设备,而 struct key_member 则是按键设备下的一个按键。
-
在
struct key_member中,其包含了struct timer_list,且使用了结构体本身 (struct timer_list debounce_timer),而不是指针。因此,必须包含定义它的头文件<linux/timer.h>。 -
struct gpio_desc使用的是指针,但是他是用于特定功能的结构体成员,也就是说它只会在使用GPIO子系统的.c文件中操作,其他文件只是“持有这个字段”。这种情况适合使用前置声明,在使用这个字段的.c文件中来包含它的依赖。 -
该项目需要 cdev 和 debounce_timer 寻回其父结构体,因此使用的是内嵌。
-
对于像
struct cdev和struct device这样的核心结构,几乎贯穿整个项目,所以包含他们的头文件(cdev.h 和 device.h)是正确的。 -
types.h用于dev_t。虽然很多头文件会间接包含它,但明确包含是好习惯。
由于指针的内存大小是确定的,所以编译器不用知道该数据类型的具体定义以判断其所占内存,我们只需要告诉编译器这个数据类型有定义,这就是前置声明,它可以降低编译依赖。 ↩︎