您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center 汽车系统工程   模型库  
会员   
   
OCSMP认证课程:OCSMP-MU
4月9-10日 线上
基于模型的数据治理与数据中台
5月19-20日 北京+线上
网络安全原理与实践
5月21-22日 北京+线上
     
   
 订阅
一个结构体两个指针,RT-Thread 靠它管住了所有组件
 
作者:一枚嵌入式码农
 
  17   次浏览      6 次
 2026-03-30
 
编辑推荐:
本文主要介绍了一个结构体两个指针,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 的用法重要得多——因为你能用同样的方式,搭出属于自己项目的可扩展框架。

   
17   次浏览       6 次
 
相关文章

CMM之后对CMMI的思考
对软件研发项目管理的深入探讨
软件过程改进
软件过程改进的实现
 
相关文档

软件过程改进框架
软件过程改进的CMM-TSP-PSP模型
过程塑造(小型软件团队过程改进)
软件过程改进:经验和教训
 
相关课程

以"我"为中心的过程改进(iProcess )
iProcess过程改进实践
CMMI体系与实践
基于CMMI标准的软件质量保证

最新活动计划
嵌入式软件测试方法&实践 3-20[在线]
MBSE理论方法到工作实践 3-28[北京]
需求分析与管理 4-21[在线]
基于LLM的Agent应用开发 4-18[北京]
SysML和EA系统设计建模 4-23[北京]
基于本体的体系架构设计 4-24[北京]
认证课:OCSMP-MU 周末班[在线]
 
 
最新文章
iPerson的过程观:要 过程 or 结果
基于模型的需求管理方法与工具
敏捷产品管理之 Story
敏捷开发需求管理(产品backlog)
Kanban看板管理实践精要
最新课程
基于iProcess的敏捷过程
软件开发过程中的项目管理
持续集成与敏捷开发
敏捷过程实践
敏捷测试-简单而可行
更多...   
成功案例
英特尔 SCRUM-敏捷开发实战
某著名汽车 敏捷开发过程与管理实践
北京 敏捷开发过程与项目管理
东方证券 基于看板的敏捷方法实践
亚信 工作量估算
更多...