| 编辑推荐: |
本文主要介绍了一个结构体两个指针,RT-Thread 靠它管住了所有组件相关内容。希望对您的学习有所帮助。
本文来自于微信公众号一枚嵌入式码农,由火龙果软件Alice编辑、推荐。
|
|
如果让你设计一个 RTOS,线程要管、定时器要管、IPC对象也要管——你怎么用一套统一的方式把它们全串起来?
|
RT-Thread 源码里随处可见的 rt_list_t
翻开 RT-Thread 的源码,你会发现一个出镜率极高的结构体:
struct rt_list_node { struct rt_list_node *next; struct rt_list_node *prev; }; typedef struct rt_list_node rt_list_t;
|
两个指针,一个前驱,一个后继,就这么简单。
但就是这个看起来平平无奇的东西,在 RT-Thread 内核里无处不在:线程管理、定时器链表、IPC
对象队列、设备驱动框架……几乎所有需要"把一堆同类对象串起来"的地方,都能看到它。
你可能会问:一个双向链表节点有什么好讲的?
关键不在于 rt_list_t 本身,而在于它和 结构体、函数指针 组合之后能玩出什么花样。这三样东西一旦配合起来,你就能在纯
C 环境下写出"面向对象"风格的可扩展架构。
这不是什么花拳绣腿,RT-Thread 内核就是这么干的。
把链表节点"嵌"进结构体:侵入式设计
传统教科书链表是节点包裹数据,像超市储物柜一样,东西往格子里塞。这种方式的问题上一篇文章已经聊过了——每种数据类型都得重新写一套链表操作,累死人。
RT-Thread 和 Linux 内核一样,采用的是侵入式设计:数据结构主动把链表节点包含进来。
看 RT-Thread 的线程控制块是怎么做的:
struct rt_thread { char name[RT_NAME_MAX]; rt_uint8_t type; rt_uint8_t stat;
rt_list_t tlist; /* ← 线程链表节点,嵌在结构体内部 */
void *sp; void *entry; rt_uint32_t init_tick; /* ... 其他成员 ... */ };
|
注意看 tlist 这个成员。它不是一个独立的链表,而是结构体自身的一部分。当你想把多个线程串成一条链时,只需要把各个线程结构体里的
tlist 连起来就行了。
这个思路用图来说明最清楚:
链表只连接 tlist 成员,但整个线程结构体的数据都跟着一起"挂"在链上了。
那怎么从 tlist 找回整个线程结构体?
问题来了:遍历链表的时候,你拿到的是 rt_list_t * 指针——它指向的是结构体内部的某个成员,而不是结构体本身。怎么从这个成员地址逆推出整个结构体的首地址?
RT-Thread 提供了一个宏:
#define rt_container_of(ptr, type, member) \ ((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))
|
原理其实就一道减法:结构体首地址 = 成员地址 - 成员偏移量。
配合遍历宏一起用,代码就很清爽了:
rt_list_t *node; /* 遍历就绪线程链表 */ rt_list_for_each(node, &ready_list) { struct rt_thread *thread = rt_container_of(node, struct rt_thread, tlist); rt_kprintf("thread: %s\n", thread->name); }
|
一行代码就能从链表节点反推出完整的线程对象,这就是侵入式设计的威力。
加上函数指针:让结构体拥有"行为"
到目前为止,结构体+链表解决了"把一堆对象串起来统一管理"的问题。但光串起来还不够——每个对象的行为可能不一样,你还得能让它们各干各的活。
这时候就该函数指针登场了。
RT-Thread 的设备驱动框架就是一个典型。看看 rt_device 是怎么定义的:
struct rt_device { struct rt_object parent; /* 继承自内核对象(里面就有 rt_list_t) */
enum rt_device_class_type type; rt_uint16_t flag; rt_uint16_t ref_count;
/* ↓ 一组函数指针,定义了设备的"行为接口" */ rt_err_t (*init) (rt_device_t dev); rt_err_t (*open) (rt_device_t dev, rt_uint16_t oflag); rt_size_t (*read) (rt_device_t dev, rt_off_t pos, void *buf, rt_size_t size); rt_size_t (*write) (rt_device_t dev, rt_off_t pos, const void *buf, rt_size_t size); rt_err_t (*control)(rt_device_t dev, int cmd, void *args); rt_err_t (*close) (rt_device_t dev);
void *user_data; };
|
注意看中间那一排函数指针:init、open、read、write、control、close。
这就是 RT-Thread 定义的一套设备操作接口。不管你底层是 UART、SPI、I2C 还是
CAN,上层统一用这套接口调用。具体怎么初始化、怎么读写,由每个驱动自己的函数来填充。
画张图看看整体结构:
上层应用代码完全不关心底下挂的是什么设备,统一调 rt_device_read()、rt_device_write()
就完事了。RT-Thread 内核帮你转发到对应驱动的函数指针上。
这和 C++ 的虚函数、Java 的接口,本质上是一回事——用函数指针实现运行时多态。
实战:搭一个传感器管理框架
光看内核代码可能还是觉得抽象。我们自己动手,用 rt_list_t + 结构体 + 函数指针,搭一个简单但完整的传感器管理框架。
定义传感器"基类"
/* sensor_manager.h */
#include <rtthread.h>
typedef struct sensor_device { char name[16]; /* 传感器名称 */ rt_list_t list; /* 链表节点 —— 挂到管理链表上 */
/* 函数指针 —— 定义传感器的行为 */ int (*init)(struct sensor_device *self); int (*read_data)(struct sensor_device *self, float *value); int (*deinit)(struct sensor_device *self);
} sensor_device_t;
|
三个要素全齐了:name 这些是数据,list 负责串联,函数指针负责定义行为。
管理器:注册、注销、遍历
/* sensor_manager.c */
/* 全局传感器链表头 */ static rt_list_t sensor_head = RT_LIST_OBJECT_INIT(sensor_head);
/* 注册传感器 */ void sensor_register(sensor_device_t *sensor) { rt_list_insert_after(&sensor_head, &sensor->list); rt_kprintf("[sensor] registered: %s\n", sensor->name); }
/* 注销传感器 */ void sensor_unregister(sensor_device_t *sensor) { rt_list_remove(&sensor->list); rt_kprintf("[sensor] unregistered: %s\n", sensor->name); }
/* 初始化所有已注册的传感器 */ void sensor_init_all(void) { rt_list_t *node; rt_list_for_each(node, &sensor_head) { sensor_device_t *sensor = rt_container_of(node, sensor_device_t, list); if (sensor->init) sensor->init(sensor); } }
/* 采集所有传感器数据 */ void sensor_read_all(void) { rt_list_t *node; float value = 0;
rt_list_for_each(node, &sensor_head) { sensor_device_t *sensor = rt_container_of(node, sensor_device_t, list); if (sensor->read_data && sensor->read_data(sensor, &value) == 0) { rt_kprintf("[sensor] %s: %.2f\n", sensor->name, value); } } }
|
管理器代码有个很重要的特点:它完全不知道具体有哪些传感器。它只认 sensor_device_t 这个"接口",谁来注册都行。
具体传感器的实现
/* dht11_sensor.c —— 温度传感器 */
static int dht11_init(sensor_device_t *self) { /* 初始化 DHT11 引脚、时序 */ rt_kprintf("DHT11 init OK\n"); return 0; }
static int dht11_read(sensor_device_t *self, float *value) { /* 实际场景中这里读取真实硬件 */ *value = 25.6f; return 0; }
/* 定义并初始化传感器实例 */ static sensor_device_t dht11 = { .name = "DHT11", .init = dht11_init, .read_data = dht11_read, .deinit = RT_NULL, };
/* 模块启动时自动注册 */ static int dht11_register(void) { sensor_register(&dht11); return 0; } INIT_APP_EXPORT(dht11_register);
|
/* bmp280_sensor.c —— 气压传感器 */
static int bmp280_init(sensor_device_t *self) { rt_kprintf("BMP280 init OK\n"); return 0; }
static int bmp280_read(sensor_device_t *self, float *value) { *value = 1013.25f; /* hPa */ return 0; }
static sensor_device_t bmp280 = { .name = "BMP280", .init = bmp280_init, .read_data = bmp280_read, .deinit = RT_NULL, };
static int bmp280_register(void) { sensor_register(&bmp280); return 0; } INIT_APP_EXPORT(bmp280_register);
|
现在回过头看整体架构:
想加一个新传感器?写一个 .c 文件,填好函数指针,调用 sensor_register() 注册,完事。管理器的代码一行都不用改。
这就是开放-封闭原则在嵌入式 C 里的真实落地。
这套组合拳在 RT-Thread 里到底用了多少次?
上面那个传感器框架不是我凭空编的,RT-Thread 源码里到处都是同样的套路。
观察一下这张表,你会发现一个规律:RT-Thread 内核对象几乎都继承自 rt_object,而
rt_object 里就包含了 rt_list_t。
struct rt_object { char name[RT_NAME_MAX]; rt_uint8_t type; rt_uint8_t flag;
rt_list_t list; /* ← 所有内核对象都通过这个节点挂到对应的容器链表上 */ };
|
这意味着什么?线程、定时器、信号量、互斥量、设备……它们虽然功能完全不同,但在内核看来都是"对象",都能用同一套链表操作来管理。
整个内核对象的管理体系长这样:
当你调用 rt_object_find() 查找某个内核对象时,RT-Thread 做的事情就是:根据类型找到对应的容器链表头,然后遍历链表逐个比较
name。底层逻辑非常简洁,但已经能管理整个系统的所有资源。
为什么你应该关注这些"套路"?
到这里你可能已经注意到了,我们全篇没写一行 C++ 代码,但用到的思想全都有迹可循:
• 结构体嵌入链表 → 组合模式的思路
• 函数指针接口 → 策略模式的 C 语言实现
• 注册-遍历-回调 → 观察者模式(发布-订阅)的雏形
• 统一基类 rt_object → 模板方法模式的味道
• 上层不关心具体实现 → 依赖倒置原则
这些东西在设计模式的书里都有系统的讲解,只不过用的是 Java 或 C++ 的例子。很多嵌入式工程师一看到"设计模式"四个字就觉得跟自己没关系——"那是写
Java 的人搞的花架子"。
但事实恰恰相反。你仔细看看 RT-Thread 的源码、Linux 内核的代码,再回头翻翻《设计模式》那本书,你会发现这些"经典模式"早就被内核开发者用
C 语言实现了无数遍。他们只是没叫它"策略模式"或者"观察者模式"罢了。
区别在于:内核开发者是踩了几十年坑之后自然而然写出了这些结构,而你可以通过系统学习设计模式,少走十年弯路。
掌握了设计模式,你再去读任何开源 RTOS 或内核源码,都会有一种"拨开迷雾"的感觉——那些看起来精妙复杂的架构,拆开来就是几个经典模式的排列组合。
如果你也想系统地学习嵌入式 C 语言中的设计模式,可以看我整理的这套合集,每一篇都配合实战代码,从头到尾讲透一个模式在嵌入式场景下的落地:
嵌入式C语言设计模式实战合集
写在最后:rt_list_t 本身只是一个极简的双向链表节点。但当它和结构体、函数指针组合起来,就成了 RT-Thread 管理一切的基础设施。理解这套组合拳的思想,比记住 API 的用法重要得多——因为你能用同样的方式,搭出属于自己项目的可扩展框架。
|
|