跳转至

Queue 队列与互斥量继承

1. Queue_t 控制块底层内存结构

在 FreeRTOS 中,信号量(Semaphore)和互斥锁(Mutex)在底层完全基于 Queue_t 结构体实现。理解了 Queue_t,就理解了 FreeRTOS 整个 IPC 体系的基石。

typedef struct QueueDefinition
{
    int8_t *pcHead;                 /* 指向队列物理存储区首地址 (RAM 起点) */
    int8_t *pcWriteTo;              /* 指向下一个写入位置 (环形写指针) */

    union
    {
        QueuePointers_t xQueue;     /* 用于队列模式: pcTail 指向环形存储末端, pcReadFrom 指向读位置 */
        SemaphoreData_t xSemaphore; /* 用于互斥量模式: 记录锁占有者 TCB (xMutexHolder) 及递归计数 */
    } u;

    List_t xTasksWaitingToSend;     /* 等待发送的任务链表 (当队列满时,生产者在此按优先级降序排队) */
    List_t xTasksWaitingToReceive;  /* 等待接收的任务链表 (当队列空时,消费者在此按优先级降序排队) */

    volatile UBaseType_t uxMessagesWaiting; /* 当前队列中有效消息个数 */
    UBaseType_t uxLength;           /* 队列最大容量 (消息槽数) */
    UBaseType_t uxItemSize;         /* 单个消息的字节大小 (若为 0 则为信号量) */

    volatile int8_t cRxLock;        /* 队列锁状态: 记录在临界区内 ISR 移出消息的计数 */
    volatile int8_t cTxLock;        /* 队列锁状态: 记录在临界区内 ISR 写入消息的计数 */
} xQUEUE;
classDiagram
    class QueueDefinition {
        +int8_t* pcHead
        +int8_t* pcWriteTo
        +List_t xTasksWaitingToSend
        +List_t xTasksWaitingToReceive
        +UBaseType_t uxMessagesWaiting
        +UBaseType_t uxLength
        +UBaseType_t uxItemSize
    }
    class MutexExtension {
        +TaskHandle_t xMutexHolder
        +UBaseType_t uxRecursiveCallCount
    }
    QueueDefinition <|-- MutexExtension : 复用 Union

2. 队列阻塞与唤醒状态机

当调用 xQueueReceive(xQueue, pvBuffer, xTicksToWait) 时:

flowchart TD
    Start["调用 xQueueReceive()"] --> CheckMsg{"uxMessagesWaiting > 0 ?"}

    CheckMsg -->|"是: 有消息"| Copy["从 pcReadFrom 拷贝 uxItemSize 字节到 pvBuffer"]
    Copy --> DecCount["uxMessagesWaiting 计数减 1"]
    DecCount --> WakeSender{"xTasksWaitingToSend 有排队任务?"}
    WakeSender -->|"是"| UnblockSend["唤醒优先级最高的阻塞发送者移入就绪态"]
    WakeSender -->|"否"| FinishOK["返回 pdPASS"]
    UnblockSend --> CheckYield["若唤醒任务优先级 > 当前任务,触发调度"]

    CheckMsg -->|"否: 队列空"| CheckTimeout{"xTicksToWait > 0 ?"}
    CheckTimeout -->|"超时时间为 0"| RetFail["立即返回 errQUEUE_EMPTY"]
    CheckTimeout -->|"超时时间 > 0"| Block["1. 将当前任务从就绪列表移出<br/>2. 插入 xTasksWaitingToReceive 链表<br/>3. 插入延时列表 pxDelayedTaskList"]
    Block --> Yield["触发 portYIELD_WITHIN_API() 进行上下文切换"]

3. 互斥量(Mutex)如何实现优先级继承?

在 FreeRTOS 中,互斥量实质上是一个容量 uxLength = 1 且消息大小 uxItemSize = 0 的特殊队列。其 u.xSemaphore.xMutexHolder 保存了当前持有该互斥量的任务 TCB 指针。

3.1 继承发生点(xQueueGenericReceive

当任务 \(A\)(优先级 10)尝试获取互斥锁,发现锁已被任务 \(B\)(优先级 2)占有:

/* 简化内核核心源码逻辑 */
if( ( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ) )
{
    taskENTER_CRITICAL();
    {
        /* 检查持有者优先级是否低于申请者 */
        if( pxQueue->u.xSemaphore.xMutexHolder->uxPriority < pxCurrentTCB->uxPriority )
        {
            /* 动态提升持有者任务优先级 */
            vTaskPriorityInherit( ( TaskHandle_t ) pxQueue->u.xSemaphore.xMutexHolder );
        }
    }
    taskEXIT_CRITICAL();
}

3.2 恢复发生点(xQueueGenericSend / xSemaphoreGive)与 xTaskPriorityDisinherit

当任务 \(B\) 退出临界区并调用 xSemaphoreGive() 释放锁时,内核最终调用 xTaskPriorityDisinherit()(参考官方 FreeRTOS V10.5.1 tasks.c):

/* 据 FreeRTOS V10.5.1 节选;省略部分断言、注释和覆盖率分支,非独立编译单元。 */
BaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder )
{
    TCB_t * const pxTCB = pxMutexHolder;
    BaseType_t xReturn = pdFALSE;

    if( pxMutexHolder != NULL )
    {
        /* 1. 持有锁计数递减 */
        ( pxTCB->uxMutexesHeld )--;

        /* 2. 检查当前是否处于优先级被继承提升的状态 */
        if( pxTCB->uxPriority != pxTCB->uxBasePriority )
        {
            /* 3. 【关键前提】仅当该任务持有的所有互斥量全部释放完毕时,才降回基础优先级! */
            if( pxTCB->uxMutexesHeld == ( UBaseType_t ) 0 )
            {
                if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
                {
                    portRESET_READY_PRIORITY( pxTCB->uxPriority, uxTopReadyPriority );
                }
                /* 降回原始基础优先级 */
                traceTASK_PRIORITY_DISINHERIT( pxTCB, pxTCB->uxBasePriority );
                pxTCB->uxPriority = pxTCB->uxBasePriority;

                listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ),
                    ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) pxTCB->uxPriority );
                prvAddTaskToReadyList( pxTCB );

                xReturn = pdTRUE;
            }
            else
            {
                /* 依然持有其他互斥锁: 不降级,维持当前继承的高优先级! */
                mtCOVERAGE_TEST_MARKER();
            }
        }
    }
    return xReturn;
}

Warning

多锁嵌套反例与工程等待时延影响(Nested Mutex Caveat)

假设任务 \(L\)(基础优先级 2)依次获取了 Mutex 1Mutex 2: 1. 高优先级任务 \(H\)(优先级 10)请求 Mutex 1 发生阻塞,\(L\) 的优先级被继承提升至 10; 2. 任务 \(L\) 执行完相关逻辑,调用 xSemaphoreGive(Mutex 1) 释放了 Mutex 1; 3. 内核关键行为:此时由于任务 \(L\) 还持有 Mutex 2(uxMutexesHeld = 1),FreeRTOS 并不会将 \(L\) 立即降回优先级 2,而是继续保持在优先级 10! 4. 设计取舍机理:该版本采用简化的优先级继承机制;在这条正常释放路径上,不会逐锁重新计算仍需继承的优先级。代价是任务 \(L\) 在持有 Mutex 2 期间会继续以优先级 10 执行,从而可能在短时间内延迟其他中间优先级任务的执行。在此正常释放路径中,只有当 \(L\) 将所持有的最后一把互斥量释放(uxMutexesHeld == 0后,才会真正恢复为原始的 uxBasePriority

以上讨论限定于正常释放路径;等待超时还涉及 vTaskPriorityDisinheritAfterTimeout(),不能将“释放最后一把锁”概括成所有降级场景。


4. 队列锁:cRxLock/cTxLock 与延迟事件处理

这是 queue.c 源码中最常被教程跳过、却最能体现设计功力的机制。第 1 节结构体里那两个不起眼的 int8_t 字段,是任务级操作与 ISR 操作安全交错的关键。

4.1 问题:ISR 撞上"半执行"的队列操作

任务级的 xQueueReceive 不是全程处在关中断临界区里(那会拖垮中断延迟)。它的骨架是:

xQueueReceive():
    短临界区内检查队列;有数据则拷贝并返回
    仅在无数据且允许等待时进入阻塞准备分支:
    vTaskSuspendAll()          ← 挂起调度器 (uxSchedulerSuspended++)
    prvLockQueue(pxQueue)      ← 队列上锁 (cRxLock/cTxLock = 0)
    重新检查超时和队列是否仍为空
    [若需阻塞] 把自己挂上 xTasksWaitingToReceive + 延时链
    prvUnlockQueue()           ← 调度器仍挂起时处理延后唤醒
    xTaskResumeAll()           ← 恢复调度器,处理暂存就绪任务

危险窗口在 上锁之后、彻底阻塞之前:此刻任务正在事件链上排队但状态尚未冻结,若此刻 ISR 打进来并直接调用 xTaskRemoveFromEventList() 唤醒它——就绪链更新发生在调度器挂起期间,与主流程的状态推进竞态,内核链表可能被写坏。

4.2 解法:数据照搬,唤醒记账

FromISR 路径遇到已上锁的队列时:环形缓冲的数据拷贝立即执行(短临界区内安全),而唤醒动作被记为欠账——给对应计数器 +1:

/* ISR 接收路径的控制流示意,省略返回值与跟踪字段 */
UBaseType_t saved = portSET_INTERRUPT_MASK_FROM_ISR();
{
    if( pxQueue->uxMessagesWaiting > ( UBaseType_t ) 0 )
    {
        prvCopyDataFromQueue( pxQueue, pvBuffer );            /* 数据照常搬走 */
        pxQueue->uxMessagesWaiting--;

        if( pxQueue->cRxLock == queueUNLOCKED )     /* 未锁: 立即唤醒等待的发送者 */
        {
            if( listLIST_IS_EMPTY( &pxQueue->xTasksWaitingToSend ) == pdFALSE )
            {
                if( xTaskRemoveFromEventList( &pxQueue->xTasksWaitingToSend ) != pdFALSE )
                {
                    *pxHigherPriorityTaskWoken = pdTRUE; /* 示例假定指针非 NULL;由 ISR 调用方请求切换 */
                }
            }
        }
        else
        {
            prvIncrementQueueRxLock( pxQueue, pxQueue->cRxLock );                     /* 已锁: 只记账, 欠一次"唤醒发送者" */
        }
    }
    /* 省略空队列处理 */
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( saved );

计数器三态语义:

cTxLock/cRxLock 队列状态 行为
queueUNLOCKED(-1) 自由 任务级/ISR 级操作均即时完成数据+事件处理
queueLOCKED_UNMODIFIED(0) 已锁、无欠账 prvLockQueue 刚上锁的初始态
\(> 0\)(N) 已锁、欠 N 次事件 ISR 每做一次数据操作 N++,唤醒推迟

队列函数在调度器仍挂起时调用 prvUnlockQueue(),随后才调用 xTaskResumeAll();前者 把欠账清偿——按计数逐次唤醒等待者,最后把计数复位为 queueUNLOCKED

sequenceDiagram
    autonumber
    participant T as 任务 A (xQueueReceive)
    participant Q as 队列 (已上锁)
    participant ISR as UART ISR (xQueueSendFromISR)

    T->>Q: 挂起调度器+上锁, 数据为空 → 挂上等待链
    Note over T: 正在走向阻塞的"半执行"窗口
    ISR->>Q: 数据拷入环形缓冲 (立即)
    ISR->>Q: 发现已锁 → cTxLock++ (记账, 不唤醒!)
    Note over ISR: ISR 退出, 全程未碰等待链
    T->>Q: prvUnlockQueue() 处理延后唤醒
    T->>T: xTaskResumeAll() 恢复调度并处理暂存就绪任务

Note

设计收益:ISR 数据通路完全不需要知道任务侧正在做什么——最坏情况只是唤醒被推迟到解锁之后(微秒级),正确性依赖中断优先级与 API 使用约束。配合调度器挂起期的 xPendingReadyList 暂存机制(见就绪列表页第 5.2 节),整个内核实现了"长操作不关中断、短临界区不记账"的分层并发控制。


5. xQueueGenericSend 成功路径走读

队列为空且接收者在等待时的一次发送(以队列模式为例):

/* 据 V10.5.1 queue.c xQueueGenericSend 简化 (省略超时/队列集分支) */
taskENTER_CRITICAL();
{
    if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
    {
        prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );  /* ① */

        if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
        {
            if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
            {
                portYIELD_WITHIN_API();        /* ② 唤醒者优先级更高 → 立即切换 */
            }
        }
        xReturn = pdPASS;
    }
    else { /* 满: 走 4.1 的挂起-上锁-阻塞骨架 */ }
}
taskEXIT_CRITICAL();

prvCopyDataToQueue() 内部按队列类型分派,是"一个 queue.c 通吃全家"的落地现场:

队列类型 拷贝动作 附带动作
普通队列 memcpypcWriteTo 后写指针步进(环形回卷)
queueOVERWRITE(长度 1 队列) 覆盖已有值或写入空队列 断言长度为 1;覆盖已有值不增加消息数
计数信号量 不拷贝数据 uxMessagesWaiting++("写入"的是计数值)
互斥量(give 路径) 不拷贝数据 xTaskPriorityDisinherit()(第 3.2 节)

6. 信号量家族总表:选型与禁区

原语 创建 API 几何参数 优先级继承 ISR 可用性 典型用途
二值信号量 xSemaphoreCreateBinary() \(1 \times 0\) give ✓ / take ✓ ISR→任务的事件通知
计数信号量 xSemaphoreCreateCounting(max, init) \(N \times 0\) 资源池配额、事件累计
互斥量 xSemaphoreCreateMutex() \(1 \times 0\) 双向禁止 共享资源互斥
递归互斥量 xSemaphoreCreateRecursiveMutex() \(1 \times 0\) 双向禁止 可重入代码路径加锁
队列 xQueueCreate(len, size) \(len \times size\) 定长消息传递

Warning

互斥量为什么彻底禁入 ISR? 优先级继承的操纵对象是"持有者任务的 TCB"(提升/恢复其 uxPriority 并做就绪链更新)。ISR 没有任务身份:take 会阻塞(ISR 不可阻塞);give 则找不到持有者可 disinherite,且 give 时若持有者是任务,语义上 ISR 根本不可能是资源占有者——它只是"事件到达"的信使。"ISR 里通知事件用二值信号量,任务间保护共享资源用互斥量",两者不可互换。


7. 任务通知:绕开队列的第三条路

xTaskNotifyGive() / ulTaskNotifyTake() 不创建任何 Queue_t——直接读写目标 TCB 内嵌的 ulNotifiedValue 与通知状态位,路径上没有环形缓冲拷贝、没有队列锁记账、没有事件链遍历。官方文档将其列为轻量级事件通知首选,通常可减少路径长度和独立对象内存。代价是可多发送者、单目标接收(定向投递给单个任务,不能广播、不能多消费者竞争)。完整机制见任务通知、事件组与软件定时器


8. 现场排查:IPC 行为异常

症状 疑似根因 验证手段
发送任务死等在 xQueueSend,接收任务"在跑但收不到" 接收端取走消息后处理过长,消费速率 < 生产速率(容量设计失配);或接收任务实际阻塞在别处 uxQueueMessagesWaiting() 定期采样看是否恒满;任务状态快照定位接收任务真实阻塞点
ISR 里操作队列偶发死机/链表断言 ISR 误用非 FromISR 版本(普通版本在临界区内部会尝试调度);或该中断优先级数值低于 syscall 阈值仍调 API 审查 ISR 代码的 API 后缀;核对 NVIC 优先级数值与 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
中断唤醒任务"偶尔"迟一拍 FromISR 之后 portYIELD_FROM_ISR() 被遗漏或参数恒为 pdFALSE 逻辑分析仪量 ISR 退出到任务首指令延迟
xSemaphoreGive 返回成功但对方 take 不到 用二值信号量做互斥,多次 give 导致计数漂移;或队列集路由吞掉事件 打印 give/take 次数配平;互斥需求改用 Mutex 并检查 uxMessagesWaiting
互斥量保护的临界区偶发被第三方闯入 混用"互斥量+手动关中断",或中断 handler 里直接读写了共享结构 审查共享资源的全部访问点;中断侧只允许"取数据入队,处理留任务"
优先级继承开启仍出现长时间阻塞 多锁嵌套下释放非最后一把锁不降级(见第 3 节警告);或持有者在临界区内长跑 tracealyzer/SystemView 看持有者被提升后干了什么

参考