13 终章:从自动注册到Linux内核OOP全貌

C 语言 OOP 系列导航

  1. C语言你真的会封装吗?
  2. 三个LED写了三份代码?
  3. 同事改一行,LED全乱了
  4. 手搓class:前缀、init/deinit和生命周期
  5. 你的全局变量,该死了
  6. 工业库也是同一套封装套路
  7. struct嵌套消灭复制粘贴
  8. 写死的函数怎么换
  9. 把一组函数指针装进对象
  10. 一个指针管所有LED
  11. 虚函数不实现会怎样
  12. 换硬件不改应用
  13. 从自动注册到Linux内核OOP全貌 ⇐ 当前位置

到这里,C 语言层面的对象、继承、函数指针、ops 和多态都已经就位。最后一篇把视角拉到大型工程 — 你会发现 Linux 内核里的很多设计,也是这套思路,只是规模更大、机制更完整。

先看问题:每加一个模块,都要手动调 init

普通项目里,初始化可能集中写在 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;
}

模块少时没问题。模块多了以后,main() 会越来越长 — 新增模块要改中心代码,初始化顺序靠人工维护,不同子系统混在一起。

自动注册的核心思想

大型系统需要一种机制:模块声明”我要初始化”,系统统一收集并按顺序执行。简化模型如下:

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)();
}
}

每个模块通过链接器 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);

模块自己声明初始化入口,链接器把这些入口收集起来,系统启动时统一调用。具体编译器、链接脚本和平台实现会更复杂,但设计思想就是这样。

从对象到子系统

前面定义过 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;

Linux 内核里到处都是类似结构。例如文件操作:

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;

统一入口通过 fops 分发:

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 的调用链路 led_on(led) → led->ops->on(led) 是同一种结构。

Linux OOP 常见形态

内核概念 OOP 对应 C 语言实现
file_operations 文件对象行为表 一组函数指针
I2C driver 设备驱动类 probe/remove 等回调
gpio_chip GPIO 控制器对象 结构体 + ops
net_device_ops 网卡行为表 open/stop/start_xmit
container_of 从成员找回对象 地址偏移计算
module_init 自动注册 section + initcall

关键词在各种规模的 C 工程里都一样:structopscallbackcontainer_ofinitregister

container_of:从 base 找回具体对象

向下转型在大型 C 工程里很常见。简化宏:

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

回调拿到的是 LedBase_t *,需要还原派生对象时用 container_of

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;
}

注册:把对象交给框架

完整框架通常通过注册接口把对象登记到子系统:

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

int led_register_device(const LedDevice_t *dev);

框架内部只保存抽象对象:

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]);
}
}

驱动注册到框架,框架面向抽象调用。

从小 LED 到大内核

把整个系列压缩成一张表:

本系列概念 大型工程里的样子
Led_t 某类设备对象
LedBase_t 抽象基类 / 通用对象头
LedOps_t 操作表
led_on() 框架统一入口
gpio_led_on() 具体驱动实现
container_of 从通用对象回到私有对象
board_init() 板级注册
module_init() 模块自动初始化

项目规模选型

项目规模 可以采用的做法
小项目 struct + init/deinit
中项目 base + ops + board_init
多硬件平台 platform 分层
多驱动插件 注册表 / 自动注册
类内核框架 子系统 + ops + container_of

不要一开始就上最复杂的机制。但你要知道复杂系统为什么会长成那样 — 当你再打开 HAL、RTOS、Linux 内核的代码时,会看到这些关键词一直在重复。

struct 表示对象,ops 表示行为,container_of 找回具体类型,module_init 把模块接入系统。Linux 内核的 OOP 没有魔法,只是一套工程纪律被严格执行到了极致。