LK 博客
RTOS内核开发实战(1):侵入式双向链表,先把内核“挂链”这件事做对
嵌入式
约 1 分钟阅读 0 赞 0 条评论 鸿蒙黑体

RTOS内核开发实战(1):侵入式双向链表,先把内核“挂链”这件事做对

Yukikaze
Yukikaze @Yukikaze
累计点赞 0 登录后每个账号只能点一次
内容长度 0 正文词元数
正文
目录会跟随阅读位置移动。
阅读进度

做 RTOS,最先落地的往往不是调度算法,而是数据结构。

任务要进 ready list,延时任务要进 sleep list,后续做信号量、消息队列时还会有等待链表。如果底层链表设计得不稳,后面的调度、阻塞、超时唤醒都会跟着变脆。

这版实现没有选择“链表节点单独分配”的做法,而是直接把 list_node_t 嵌进任务控制块或内核对象里,走典型的 intrusive list 路线。

为什么先做链表

这样做的核心原因有三个:

  1. RTOS 内核尽量避免动态内存依赖,节点跟对象生命周期一致,最省事。
  2. 节点就在对象内部,插入和摘链都能做到 O(1),不用额外查表。
  3. 同一套链表原语可以服务于 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 最核心的两个约束固定下来了:

  1. 节点生命周期跟对象生命周期绑定。
  2. 一次只能属于一条链表,且链表归属可验证。

对于一个正在成长中的 RTOS,这比“功能很多”更重要。因为只要挂链语义是干净的,后面的 ready queue、事件等待、超时唤醒都能建立在同一套纪律上继续往前扩。

下一篇会往上走一层,讨论任务控制块 tcb_tready_queue_t 的组织方式,看看这个内核是怎么把“一个任务”真正变成“一个可调度对象”的。

作者名片

Yukikaze
Yukikaze
@Yukikaze

私にできることなら何でもするから

评论区
文章作者和管理员都可以管理这里的评论。
0 条评论
登录后即可参与评论。 去登录
还没有评论,欢迎留下第一条交流内容。