13 工程扩展:自动注册与 Linux 内核中的对象组织

C 语言面向对象系列

  1. 总纲:C 语言面向对象设计概览
  2. 封装:结构体与多实例接口
  3. 信息隐藏:公共接口与内部实现
  4. 模块组织:命名约定与对象生命周期
  5. 数据组织:实例状态、模块状态与常量
  6. HAL 实例:STM32 外设接口中的封装设计
  7. 结构体嵌套:公共数据与行为的复用
  8. 函数指针:可替换行为与回调
  9. 操作表:使用 ops 组织对象接口
  10. 多态:统一接口、行为分发与类型转换
  11. 接口约定:必选操作、可选操作与默认实现
  12. 框架整合:应用、驱动与平台的分层
  13. 工程扩展:自动注册与 Linux 内核中的对象组织(当前篇)

前面的 LED 框架已经能够通过统一接口管理不同设备。最后一篇讨论模块和设备数量增加后的组织方式:如何安排初始化、注册对象,以及如何对照这些概念阅读 Linux 内核代码。

模块增多后的初始化管理

简单项目可以在 main() 中依次调用初始化函数:

1
2
3
4
5
6
7
8
9
10
11
int main(void)
{
gpio_driver_init();
pwm_driver_init();
i2c_driver_init();
led_driver_init();
sensor_driver_init();

app_run();
return 0;
}

模块较少时,这种方式清楚直接。模块增多后,新增模块需要修改这份调用列表,初始化顺序和不同子系统之间的关系也需要集中维护。

一种扩展方式是让各模块声明自己的初始化入口,再由系统统一调用。

收集并执行初始化函数

先定义初始化函数指针类型。假设这些指针已被收集到一段连续区域,系统可以遍历调用:

1
2
3
4
5
6
7
8
9
10
11
typedef int (*InitCall_t)(void);

extern InitCall_t __initcall_start[];
extern InitCall_t __initcall_end[];

void do_initcalls(void)
{
for (InitCall_t *fn = __initcall_start; fn < __initcall_end; fn++) {
(*fn)();
}
}

__initcall_start 和 __initcall_end 分别表示这段区域的开始和结束。循环每次取出一个函数指针,再执行对应初始化函数。

下面用编译器的 section 属性,将每个模块的函数指针放入指定段:

1
2
3
4
5
6
7
8
9
10
11
static int led_driver_init(void)
{
/* 注册 LED 驱动 */
return 0;
}

#define module_init(fn) \
static InitCall_t __initcall_##fn \
__attribute__((section(".initcall"))) = fn

module_init(led_driver_init);

这里的 section 可以先理解为目标文件中按名称组织的一段内容。宏将指针放入 .initcall 段,链接脚本再负责收集、保留这些内容,并提供起止位置。

这是一个简化模型,编译器支持、链接脚本、初始化顺序和失败处理还需要配套设计。单独使用这段宏,并不会自动解决模块依赖或调用顺序。

Linux 的 module_init() 也用于声明初始化入口:编入内核的代码在启动阶段调用,可加载模块在装入时调用。上面的同名宏只演示收集入口的思路,实际行为见 Linux 内核的驱动初始化文档。

从 LED 操作表理解文件操作接口

前面的 LED 操作表包含一组函数指针:

1
2
3
4
5
typedef struct {
int (*on)(LedBase_t *me);
int (*off)(LedBase_t *me);
int (*set_brightness)(LedBase_t *me, uint8_t brightness);
} LedOps_t;

文件操作也可以采用类似组织方式。下面定义的是教学模型,用来说明 file_operations 一类接口的结构:

1
2
3
4
5
6
typedef struct {
int (*open)(void *file);
int (*read)(void *file, char *buf, int len);
int (*write)(void *file, const char *buf, int len);
int (*release)(void *file);
} FileOperations_t;

文件对象保存操作表和私有数据:

1
2
3
4
typedef struct {
const FileOperations_t *fops;
void *private_data;
} File_t;

统一入口先检查指针,再调用操作表中的读取函数:

1
2
3
4
5
6
7
8
int vfs_read(File_t *file, char *buf, int len)
{
if (file == NULL || file->fops == NULL || file->fops->read == NULL) {
return -1;
}

return file->fops->read(file, buf, len);
}

这个过程与 led_on(led) → led->ops->on(led) 相同:入口负责公共处理,操作表提供具体实现。这里的类型和函数签名经过简化,阅读 Linux 源码时应以实际定义为准。

在内核代码中辨认相似结构

可以用前面掌握的概念理解以下结构:

内核中的名称 可以关注的设计内容
file_operations 文件操作对应的一组函数指针
I2C driver 驱动描述及 probe/remove 等回调
gpio_chip GPIO 控制器数据和操作回调
net_device_ops 网络设备的操作表
container_of 从成员指针取得所属结构体
module_init 模块初始化入口的声明

阅读时先区分结构体保存的数据、回调的参数,以及初始化和注册入口。掌握这些关系后,再继续看各个子系统的具体约定。

从公共成员访问具体对象

回调参数经常只提供公共部分,具体实现则需要访问完整对象。第 10 篇使用的简化宏如下:

1
2
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))

例如,PWM 实现需要从 LedBase_t * 取得 PwmLed_t *,再读取通道:

1
2
3
4
5
6
static int pwm_led_on(LedBase_t *base)
{
PwmLed_t *me = container_of(base, PwmLed_t, base);
pwm_set_duty(me->pwm_channel, me->base.brightness);
return 0;
}

宏根据成员偏移计算对象地址,因此传入指针必须确实指向对应类型的成员。实际内核提供了自己的宏实现,类型限定等细节应查对应版本;这里保留简化形式用于理解地址关系。相关接口见 Linux 内核的 container_of 文档。

将设备对象注册到框架

初始化入口负责安排模块启动,设备注册接口则负责把具体对象加入框架。可以先定义设备描述:

1
2
3
4
5
6
typedef struct {
const char *name;
LedBase_t *led;
} LedDevice_t;

int led_register_device(const LedDevice_t *dev);

框架内部保存基础指针,之后统一调用 LED 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static LedBase_t *g_leds[16];
static int g_led_count;

int led_register_device(const LedDevice_t *dev)
{
if (dev == NULL || dev->led == NULL) {
return -1;
}

g_leds[g_led_count++] = dev->led;
return 0;
}

void led_turn_all_off(void)
{
for (int i = 0; i < g_led_count; i++) {
led_off(g_leds[i]);
}
}

这里的注册过程只保存了对象指针,没有复制完整对象。注册后,对象需要在框架使用期间保持有效。

示例数组只能保存 16 个指针,代码省略了容量检查。用于实际工程时,应在添加前检查 g_led_count,并处理容量已满的情况。

设备注册与第 10 篇的管理器类似,都让框架通过公共类型使用设备。对象如何创建和配置,则由具体驱动或板级代码负责。

回顾对象与框架的分工

整个系列中的主要组成可以对应为:

本系列中的内容 职责
Led_t 保存某个设备实例的数据
LedBase_t 保存不同实现共用的部分
LedOps_t 描述一组可调用操作
led_on() 提供统一调用入口
gpio_led_on() 完成具体硬件操作
container_of 从公共成员取得具体对象
board_init() 创建、初始化并组织板上的设备
module_init() 声明模块初始化入口

Linux 内核中的相似结构还会配合各子系统的规则使用。理解这些基本职责,有助于沿着对象、接口、回调和注册关系阅读代码。

按项目需要选择组织方式

可以根据项目规模和变化需求逐步增加机制:

项目情况 可以采用的做法
小项目、实现较固定 struct + init/deinit,先明确数据和调用顺序
中等项目、同类设备有多种实现 base + ops + board_init
需要支持多种芯片或仿真平台 增加 platform 分层
需要接入多种驱动或模块 使用注册表,按需要安排自动注册
接近内核或大型框架的组织规模 结合子系统、操作表和成员到对象的转换

已有模块能够清楚表达数据、接口和生命周期时,可以先保持简单。需要统一管理更多实现时,再增加操作表、注册或分层,并把相应的调用条件写清楚。