
RTOS内核开发实战(1):侵入式双向链表,先把内核“挂链”这件事做对
做 RTOS,最先落地的往往不是调度算法,而是数据结构。
任务要进 ready list,延时任务要进 sleep list,后续做信号量、消息队列时还会有等待链表。如果底层链表设计得不稳,后面的调度、阻塞、超时唤醒都会跟着变脆。
这版实现没有选择“链表节点单独分配”的做法,而是直接把 list_node_t 嵌进任务控制块或内核对象里,走典型的 intrusive list 路线。
为什么先做链表
这样做的核心原因有三个:
- RTOS 内核尽量避免动态内存依赖,节点跟对象生命周期一致,最省事。
- 节点就在对象内部,插入和摘链都能做到 O(1),不用额外查表。
- 同一套链表原语可以服务于 ready、sleep、event wait 等多种场景,内核的数据结构风格会更统一。
先看链表节点和链表头的定义:
typedef struct list_node {
struct list_node *prev;
struct list_node *next;
list_t *owner;
} list_node_t;
struct list {
list_node_t *head;
list_node_t *tail;
uint32_t item_count;
};
#define LIST_CONTAINER_OF(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))
owner 字段为什么关键
这里最值得注意的是 owner。
很多初学版 RTOS 链表只保存 prev/next,结果节点重复插入、错误摘链时很难第一时间发现。当前实现把“节点属于哪条链”直接记录在 owner 里,于是很多非法路径都可以在链表层被提前拦住,而不是等到链表结构被破坏之后再调试。
例如头插和尾插都先检查 node->owner:
uint8_t list_insert_tail(list_t *list, list_node_t *node)
{
if ((list == NULL) || (node == NULL))
{
return 0U;
}
if (node->owner != NULL)
{
return 0U;
}
node->prev = list->tail;
node->next = NULL;
node->owner = list;
if (list->tail != NULL)
{
list->tail->next = node;
}
else
{
list->head = node;
}
list->tail = node;
list->item_count++;
return 1U;
}
插入操作守住什么边界
这段代码背后的设计意图很明确:链表层不负责“帮上层纠错”,而是要求调用者遵守约束。
一个节点只有在成功从原链表摘下来之后,才允许重新挂到别的链表。这个约束看起来严格,但正是这种严格,才能把 ready queue、阻塞队列、延时队列之间的状态切换边界切清楚。
摘链逻辑也是一样的思路:
uint8_t list_remove(list_t *list, list_node_t *node)
{
if ((list == NULL) || (node == NULL))
{
return 0U;
}
if (node->owner != list)
{
return 0U;
}
if (node->prev != NULL)
{
node->prev->next = node->next;
}
else
{
list->head = node->next;
}
if (node->next != NULL)
{
node->next->prev = node->prev;
}
else
{
list->tail = node->prev;
}
node->prev = NULL;
node->next = NULL;
node->owner = NULL;
if (list->item_count > 0U)
{
list->item_count--;
}
return 1U;
}
摘链操作真正保护了什么
这里有两个细节值得强调。
第一,node->owner != list 时直接失败。也就是说,调用方不能“猜一个链表”就去摘节点,必须知道自己到底在操作哪条链。这会迫使上层在任务状态迁移时保持语义一致。
第二,摘链成功后会把 prev/next/owner 全部清零。这一步不是为了美观,而是为了把节点重新放回“未挂链”状态,避免旧指针残留影响下一次插入。
为什么需要 LIST_CONTAINER_OF
LIST_CONTAINER_OF 也是这一层的关键。
链表本身只认识 list_node_t,但调度器真正关心的是 tcb_t。因此在拿到链表节点后,内核可以反推外层对象:
if (LIST_CONTAINER_OF(node, tcb_t, sched_node) == task)
{
return 1U;
}
这让链表成为真正的“基础设施层”。
它不关心节点属于任务、定时器还是消息队列等待项;谁把节点嵌进去,谁就在上层恢复出自己的对象。
小结
从当前进度看,这套链表实现还很朴素,没有做排序插入,也没有做遍历宏封装,但它已经把 RTOS 最核心的两个约束固定下来了:
- 节点生命周期跟对象生命周期绑定。
- 一次只能属于一条链表,且链表归属可验证。
对于一个正在成长中的 RTOS,这比“功能很多”更重要。因为只要挂链语义是干净的,后面的 ready queue、事件等待、超时唤醒都能建立在同一套纪律上继续往前扩。
下一篇会往上走一层,讨论任务控制块 tcb_t 和 ready_queue_t 的组织方式,看看这个内核是怎么把“一个任务”真正变成“一个可调度对象”的。