RT-Thread的内核对象管理,设计比你想的巧妙
你有没有想过一个嵌入式实时操作系统怎么管理它手里那几十上百个线程、信号量、消息队列、定时器FreeRTOS的做法是每个内核对象一个结构体开发者自己维护句柄。RT-Thread走了一条不太一样的路——它把所有内核对象都放进了同一个对象管理体系里。先看一下这段代码rt_thread_t tid rt_thread_create(worker, thread_entry, NULL, 2048, 25, 10); if (tid ! RT_NULL) { rt_thread_startup(tid); }rt_thread_create返回的是什么一个rt_thread_t。它本质上是一个指针指向一个rt_thread结构体。但有意思的是这个结构体的第一个成员不是线程栈指针也不是优先级而是一个rt_objectstruct rt_thread { struct rt_object parent; /* 继承自内核对象基类 */ rt_uint8_t *stack_addr; /* 线程栈地址 */ rt_uint32_t stack_size; /* 线程栈大小 */ rt_list_t tlist; /* 线程链表节点 */ rt_uint8_t current_priority; /* 当前优先级 */ rt_uint8_t init_priority; /* 初始优先级 */ /* ... 省略其余成员 */ };每个线程、信号量、互斥量、事件、邮箱、消息队列、内存池、设备它们的结构体第一个成员都是一个rt_object。这就是RT-Thread内核对象管理的核心思路C语言模拟面向对象的继承。对象容器——RT-Thread的花名册所有内核对象被分门别类地放在不同的容器里。每个容器就是一个rt_object_containerstruct rt_object_container { rt_list_t object_list; /* 该类型所有对象的链表 */ rt_size_t object_size; /* 该类型对象的结构体大小 */ const char *name; /* 容器名字用于调试 */ };一个rt_list_t链表把所有同类型的对象串起来。比如你创建了三个信号量它们就挂在信号量容器的object_list上。RT-Thread内部维护了一个静态数组static struct rt_object_container _container[RT_Object_Class_Unknown];每个枚举值RT_Object_Class_Thread、RT_Object_Class_Semaphore……对应一个容器。需要遍历所有线程时直接去_container[RT_Object_Class_Thread]的链表上走一遍就行。这个设计好在哪里调试的时候你可以从一个入口拿到所有内核对象的快照——线程、信号量、互斥量……任何你在系统中创建的内核对象都在某个容器里登记在册。FinSH控制台敲个list_thread能直接遍历容器链表把每个线程的名字、优先级、状态、栈使用情况打出来。对象ID分配——从名称到指针RT-Thread提供了rt_object_find_by_name这个APIrt_object_t rt_object_find_by_name(const char *name, enum rt_object_class_type type);实现逻辑很简单在对应容器里遍历链表用rt_strncmp比对每个对象的name字段。找到返回对象指针找不到返回空。这种设计意味着什么东西你可以在运行时通过名字动态定位任意内核对象。比如一个模块A创建了一个叫serial_lock的互斥量模块B不需要提前知道这个互斥量的句柄只需要在初始化时调用rt_mutex_find(serial_lock)就能拿到引用。对于组件化、可插拔的应用场景这个能力很实用。自动初始化——RT-Thread藏着的小心思RT-Thread还有一个设计上的巧思初始化宏。很多RTOS的组件初始化靠的是显式调用函数或者靠链接器段的构造/析构函数。RT-Thread定义了一组宏INIT_BOARD_EXPORT(fn) INIT_PREV_EXPORT(fn) INIT_DEVICE_EXPORT(fn) INIT_COMPONENT_EXPORT(fn) INIT_ENV_EXPORT(fn) INIT_APP_EXPORT(fn)这些宏把函数指针塞到了ELF文件的特定section里。以INIT_COMPONENT_EXPORT为例#define INIT_COMPONENT_EXPORT(fn) \ rt_used const init_fn_t __rt_init_##fn \ rt_section(.rti_fn. #fn) fn链接的时候所有INIT_xxx_EXPORT注册的函数指针被连续放置在.rti_fn开头的段里。系统启动时rt_components_init函数遍历这个段按优先级依次调用INIT_BOARD_EXPORT— 板级初始化最早执行INIT_PREV_EXPORT— 预初始化INIT_DEVICE_EXPORT— 设备驱动初始化INIT_COMPONENT_EXPORT— 组件初始化文件系统、网络协议栈等INIT_ENV_EXPORT— 环境初始化INIT_APP_EXPORT— 应用初始化最晚执行开发者写好驱动只需在函数定义后面加一行INIT_DEVICE_EXPORT(drv_hw_init)这个函数就会在系统启动的合适阶段被自动调用。不需要手动在main函数里写一堆初始化调用链——事实上RT-Thread的main函数通常干干净净。这种做法在Linux内核里很常见它的module_init系列宏也是类似的原理但在小资源MCU的RTOS里RT-Thread把这个机制做得很完整。对于中等规模以上的嵌入式项目这能省掉不少维护初始化顺序的心力。对象管理对资源受限设备的影响当然凡事有两面。这套统一的对象管理体系有两个代价第一每个内核对象结构体头部多了rt_object的几个字段name、type、flag、list_node。对于一个只有几十KB RAM的MCU来说这几个字节的额外开销乘以几十个对象也算不上大问题但在极端资源受限场景比如2KB RAM下FreeRTOS那种零开销的裸结构体确实更省。第二rt_object_find_by_name是O(n)的线性查找。如果系统里挂了上百个同类型对象查找可能会慢一点。好在大多数嵌入式场景下每个类型的对象数量不超过两位数这个开销可以忽略。RT-Thread在实时性要求高、资源不算特别受限、同时需要丰富中间件支持的场景里这套对象管理体系的收益远大于成本。特别是当你需要动态创建/销毁内核对象、或者在运行时通过名字做服务发现时它的设计会更顺手一些。今天先聊这么多。如果你之前只用过FreeRTOS的裸结构体方式不妨试试RT-Thread这套统一对象模型感受一下不同的设计哲学——也许会对OS内核设计多一层理解。