RTOS开发学习记录(5):IPC基础

成元 2026-04-22 { 教程, 笔记 } [ RTOS ]

前言

RTOS 把程序拆成了多个任务,但任务之间并不是完全独立的。它们往往需要等待同一个资源、传递数据、通知事件、保护临界区。

这些功能一般统称为 IPC,也就是 Inter-Process Communication。虽然在单片机 RTOS 中更准确地说是任务间通信,但大家一般也沿用 IPC 这个说法。

这篇文章先记录 Yuan RTOS 中 IPC 的基础层实现。后面的文章再分别记录信号量、互斥量和队列。

完整代码请访问我的Github仓库,网址为:yuan-rtos

值得注意是本文的示例代码在你阅读时可能已经进行过修改了,具体实现以仓库中代码为主。

为什么要抽象 IPC 基础层

信号量、互斥锁、队列看起来是不同的东西,但它们都有一个共同点:当条件不满足时,需要让当前任务阻塞;当条件满足时,又需要把等待任务唤醒

比如:

如果每个组件都单独维护一套阻塞链表和唤醒逻辑,代码会重复很多,而且行为也容易不一致。因此 Yuan RTOS 中抽出了 yr_ipc_base_t 作为公共基础层。

IPC 基础结构

IPC 基础对象定义如下:

typedef struct yr_ipc_t {
    yr_uint8_t is_valid;
    yr_uint32_t flag;
    yr_list_head_t blocked_list;
} yr_ipc_base_t;

其中:

目前支持两种排队策略:

typedef enum yr_ipc_flag_t {
    YR_IPC_FLAG_NONE = 0,
    YR_IPC_FLAG_FIFO,
    YR_IPC_FLAG_PRIO,
} yr_ipc_flag_t;

YR_IPC_FLAG_FIFO 表示先阻塞的任务先被唤醒,YR_IPC_FLAG_PRIO 表示优先级高的任务排在前面。

这里仍然要注意,Yuan RTOS 中任务优先级数值越小,优先级越高。

初始化 IPC 对象

IPC 基础对象初始化时,会设置有效标志、排队策略,并初始化阻塞链表:

yr_err_t yr_ipc_init( yr_ipc_base_t *ipc_base, yr_uint32_t flag)
{
    ipc_base->is_valid = YR_TRUE;
    ipc_base->flag = flag;
    yr_list_init( &ipc_base->blocked_list);

    return YR_OK;
}

信号量、互斥锁和队列内部都会调用这个函数。

例如信号量中只有一个 IPC 基础对象,而队列中有两个 IPC 基础对象:一个用于发送阻塞,一个用于接收阻塞。这一点在后面的队列文章中再展开。

阻塞任务

任务需要等待某个 IPC 对象时,会调用:

yr_err_t yr_ipc_block_task( yr_ipc_base_t *ipc_base,
                            yr_task_t *task)
{
    yr_sched_remove_task( task);
    task->status = YR_TASK_STATUS_BLOCKED;
    yr_ipc_insert_blocked_task( ipc_base, task );

    return YR_OK;
}

这个函数做了两件事情:

任务从就绪队列移除后,调度器就不会再选择它运行。之后通常会调用 yr_sched_switch() 切换到其它就绪任务。

阻塞队列的插入策略

真正把任务插入阻塞链表的是内部函数 yr_ipc_insert_blocked_task()

如果使用 FIFO 策略,就直接插入链表末尾:

yr_list_insert_before( &ipc_base->blocked_list,
                       &task->list_node);

如果使用优先级策略,就需要按照当前优先级插入:

while( plist != &ipc_base->blocked_list ) {
    yr_task_t *ptask = YR_LIST_ENTRY( plist,
                                      yr_task_t,
                                      list_node);

    if( task->current_priority < ptask->current_priority ) {
        yr_list_insert_before( &ptask->list_node,
                               &task->list_node);
        break;
    }

    plist = plist->next;
}

这样唤醒任务时,只需要取阻塞链表的第一个节点,就可以得到当前策略下最应该被唤醒的任务。

唤醒所有任务

IPC 对象删除时,通常需要唤醒所有等待它的任务。公共函数如下:

yr_err_t yr_ipc_resume_all( yr_ipc_base_t *ipc_base)
{
    while (!yr_list_isempty( list_head ))
    {
        task = YR_LIST_ENTRY( list_head->next,
                              yr_task_t,
                              list_node);

        yr_list_delete_self(&task->list_node);
        yr_timer_stop(&task->timer);
        task->status = YR_TASK_STATUS_READY;
        yr_task_set_msg( task,
                         NULL,
                         NULL,
                         YR_TASK_MR_NONE,
                         YR_TASK_MN_WAIT_IPC_DELETED);
        yr_sched_insert_task( task);
    }

    return YR_OK;
}

唤醒任务时要做几件事:

这里的等待结果很重要。任务被唤醒不一定表示拿到了资源,也可能是等待超时,或者 IPC 对象被删除。因此任务控制块中有一个 msg_info 字段,用于记录等待原因和唤醒结果。

等待结果

任务控制块中有一组和等待结果相关的字段:

typedef struct yr_task_msg_t {
    void *source;
    void *msg;
    yr_uint8_t reason;
    yr_uint16_t notify;
} yr_task_msg_t;

其中:

常见的唤醒结果有:

YR_TASK_MN_WAIT_OK
YR_TASK_MN_WAIT_TIMEOUT
YR_TASK_MN_WAIT_IPC_DELETED

因此任务被唤醒后,不能只看“我重新运行了”,还要看这次等待到底是成功、超时,还是等待对象被删除。

阻塞队列重新排序

IPC 基础层还有一个接口:

yr_err_t yr_ipc_reorder_blocked_task( yr_ipc_base_t *ipc_base,
                                      yr_task_t *task)

这个函数用于在任务当前优先级发生变化时,重新整理它在 IPC 阻塞队列中的位置。

为什么会有这种情况?主要是互斥锁的优先级继承可能会修改任务当前优先级。如果 IPC 对象采用优先级排队,那么任务优先级改变后,原来的链表顺序就可能不再正确。因此需要把任务从原位置摘出来,再按新优先级插回去。

IPC 与定时器的关系

IPC 等待和软件定时器是配合使用的。

当任务等待一个 IPC 对象时,如果设置了有限等待时间,那么任务会同时:

如果 IPC 条件先满足,就停止定时器并设置 YR_TASK_MN_WAIT_OK

如果定时器先超时,就由超时回调把任务从 IPC 阻塞链表中移除,并设置 YR_TASK_MN_WAIT_TIMEOUT

这也是为什么 IPC 唤醒任务时需要调用 yr_timer_stop(),否则任务可能已经被正常唤醒了,但定时器后续又触发一次超时回调,导致状态混乱。

总结

Yuan RTOS 的 IPC 基础层可以概括为:

有了这个基础层,后面的信号量、互斥锁和队列就可以把重点放在各自的资源语义上,而不必重复实现阻塞与唤醒逻辑。