GoDm@'s Blog

我的驱动模块项目文件结构

版权信息

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 / exitprobe/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 内核编程中,是应该避免的。原因如下:

  1. 可读性差。阅读代码的人完全不知到你的.c文件依赖哪些内核头文件。
  2. 编译速度。任何 .c 文件都必须处理所有不必要的头文件内容,导致编译时间大幅增加。
  3. 命名空间污染。引入了大量的符号和宏定义,增加了符号冲突的风险,也使代码更难阅读和维护。

一个有趣的说法是,你已经把整个项目拆分成了许多功能子系统的实现,但是如果你像上面那样做,就等于又把它们杂糅在一起了,这与我们的初衷背道而驰。

应该怎么做?

  1. 按需包含 (Include What You Use):只包含你当前文件需要使用的定义。
  2. 依赖分离:将依赖关系限制在真正需要它们的源文件和头文件内。
  3. 保持最小依赖:只引入当前文件所需的最小集合的符号。

内部头文件的内容

只放:

  1. 所有 .c 都需要的头文件
  2. 跨文件函数声明
  3. 核心结构体定义
  4. 前置声明
  5. 核心结构体成员所需要的头文件

其中第4、5点我们需要视情况灵活处理,详见下文。

定义结构体成员时用不用指针?

当我们编写内核模块,在定义结构体成员时,特别是这个成员也是一个结构体,到底用指针还是直接内嵌呢?

首先我们了解一下这两种方式的特点:

内嵌

🟢适用场景:

🔴缺点:

WHAT ABOUT 指针?

当我们定义一个指针时,其内存大小是确定的——4字节(32位系统)或8字节(64位系统)。

🟢适用场景:

🔴缺点:

最终判断原则

对于我来说,在编写模块时,我会这样判断:

  1. 这个成员是不是来自其他子系统的句柄?我只是“借用”或“观察”一个由别处创建和管理的对象。如果是,则使用指针。
    例如: GPIO 描述符是由内核 GPIO 子系统管理的,你的驱动只是申请了一个“使用权”。

  2. 我会不会用这个结构体成员反向寻回其父结构体(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 则是按键设备下的一个按键。

  1. struct key_member 中,其包含了 struct timer_list,且使用了结构体本身 (struct timer_list debounce_timer),而不是指针。因此,必须包含定义它的头文件 <linux/timer.h>

  2. struct gpio_desc 使用的是指针,但是他是用于特定功能的结构体成员,也就是说它只会在使用GPIO子系统的.c文件中操作,其他文件只是“持有这个字段”。这种情况适合使用前置声明,在使用这个字段的.c文件中来包含它的依赖。

  3. 该项目需要 cdev 和 debounce_timer 寻回其父结构体,因此使用的是内嵌。

  4. 对于像 struct cdevstruct device 这样的核心结构,几乎贯穿整个项目,所以包含他们的头文件(cdev.h 和 device.h)是正确的。

  5. types.h 用于 dev_t 。虽然很多头文件会间接包含它,但明确包含是好习惯。


  1. 由于指针的内存大小是确定的,所以编译器不用知道该数据类型的具体定义以判断其所占内存,我们只需要告诉编译器这个数据类型有定义,这就是前置声明,它可以降低编译依赖。 ↩︎


共计约1.9k字。于2025/12/16首次发布,最后更新于2025/12/16。

本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

  1. 前言
  2. 文件组织形式及原则
  3. 文件的编写原则
    1. 最小化依赖头文件
    2. 内部头文件的内容
    3. 定义结构体成员时用不用指针?
  4. 一个头文件的例子