夜雨聆风学习资料网

ARTICLE · 1006167

FreeRTOS 任务优先级机制源码剖析

FreeRTOS 任务优先级机制源码剖析
平台:STM32F103RET6(Cortex-M3)内核:FreeRTOS V11.1.0(RVDS/ARM_CM3 移植层,SMP-capable 新版 tasks.c,本工程按单核编译)本文所有代码均取自本工程实际源码

一、优先级是什么:一个反直觉的事实

FreeRTOS 任务优先级数值越大,优先级越高——和 STM32 的 NVIC 中断优先级正好相反(NVIC 数值越小越优先)。写过裸机中断的人第一次用 FreeRTOS,十个里有五个在这里栽跟头。

/* 本工程 FreeRTOSConfig.h */#define configMAX_PRIORITIES    5      /* 合法范围 0 ~ 4 */
  • 优先级 0 保留给空闲任务(tskIDLE_PRIORITY),应用任务别用 0;
  • 本工程 configMAX_PRIORITIES=5,传 5 及以上的优先级:本工程开了 assert,会先触发 configASSERT 死机(创建路径与 vTaskPrioritySet 的断言都在钳位之前,见第四节要点 4);
  • 软件定时器服务任务占 configMAX_PRIORITIES-1 = 4(本工程配置),意味着它比一切应用任务都高——定时器回调里干重活会饿死你的应用。

任务优先级与中断优先级是两套独立体系:中断永远抢占任务,再高的任务优先级也压不过 NVIC 数值更小的中断。任务优先级只决定"CPU 在任务之间给谁"。

1.1 configMAX_PRIORITIES 可以填多大?

硬上限 32。这个宏本质是就绪链表数组的大小:

PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

上限的根源在 configUSE_PORT_OPTIMISED_TASK_SELECTION=1 的优化实现:CM3 移植层用一个 32 位变量当位图,portGET_HIGHEST_PRIORITY 靠 CLZ(前导零计数)指令选任务——位图只有 32 位,所以 FreeRTOS 头文件里有静态断言 configMAX_PRIORITIES ≤ 32。

本工程 configUSE_PORT_OPTIMISED_TASK_SELECTION=0,连位图都不用,理论上填多大都行,代价只是 RAM(每个 List_t 在 32 位平台约 20 字节):

configMAX_PRIORITIES
就绪链表数组开销
说明
5(当前)
~100 B
本工程实际够用(1~3 + 空闲 0 + 定时器 4)
15
~300 B
多花 200 B,F103RET6 的 64KB RAM 毫无压力
32
~640 B
上限

uxTopReadyPriority(UBaseType_t,32 位)放得下任何 ≤32 的值,xEventListItem 里存的补数 configMAX_PRIORITIES - priority 是 TickType_t(32 位)也不会溢出。改成 32 以内任意值只需重编译,不用动其他代码。但要注意三点:

  1. 定时器任务优先级跟着变
    ——configTIMER_TASK_PRIORITY = configMAX_PRIORITIES - 1,从 4 变 14 后依然是最高,相对关系不变,但排查问题时要记得这个数变了;
  2. 优先级是"序"不是"档"
    ——建议留空档(如 1/3/5/7),将来插中间优先级的任务不用全盘重排;
  3. 填满 ≠ 用满
    ——15 个任务挤 15 个不同优先级意味着完全无同级轮转(本工程时间片还关着),任何阻塞时机变化都会放大调度顺序的敏感度。

二、优先级存储在哪里

2.1 TCB 里的三个字段

// task.c#if ( configUSE_MUTEXES == 1 )    UBaseType_t uxBasePriority;  /* 创建/设置时的"本名"优先级——继承机制的基准 */    UBaseType_t uxMutexesHeld;   /* 当前持有的互斥量计数——决定何时归还继承的优先级 */#endifvolatile UBaseType_t uxPriority; /* 当前生效的优先级——调度器实际看这个 */

为什么要有两个优先级?优先级继承:低任务持有互斥量时会被临时抬到高任务的优先级,但"抬完还得还"——uxBasePriority 就是还的依据,uxPriority 是实时值。两者相等 = 没有继承发生。

2.2 就绪链表数组:pxReadyTasksLists[]

// task.c/* 每个优先级一条双向链表,同优先级任务挂在同一条上(尾插) */PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];/* "当前最高就绪优先级"缓存——调度的起点 */PRIVILEGED_DATA static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;

存储布局:优先级是数组下标,同优先级任务在一条链表上轮转。查找最高优先级任务 = 从 uxTopReadyPriority 往下找第一条非空链表,O(1) 起步、最坏 O(configMAX_PRIORITIES)。

2.3 事件链表项里存的"补数"

任务阻塞在队列/信号量上时,xEventListItem 的排序值存的不是优先级本身,而是补数:

// task.c/* 值越小 = 优先级越高 → 链表升序插入后,高优先级任务排在前面 */listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ),                         configMAX_PRIORITIES - uxPriority );

这样事件到来时 vTaskRemoveFromUnorderedEventList 按序值升序唤醒,优先级高的阻塞任务先被救活。

三、调度如何用优先级:三段核心代码

3.1 入队时记录最高优先级:taskRECORD_READY_PRIORITY

本工程 configUSE_PORT_OPTIMISED_TASK_SELECTION=0:

// task.c#define taskRECORD_READY_PRIORITY( uxPriority ) \do {                                            \    if( ( uxPriority ) > uxTopReadyPriority )    \    {                                           \        uxTopReadyPriority = ( uxPriority );     \    }                                           \} while( 0 )

只升不降——所以它是个"缓存上界",真实最高就绪优先级可能比它低(任务被删/阻塞后不清零),查找时靠向下扫描兜底。

3.2 挂入就绪链表:prvAddTaskToReadyList

// task.c#define prvAddTaskToReadyList( pxTCB )                                                                     \do {                                                                                                       \    traceMOVED_TASK_TO_READY_STATE( pxTCB );                                                                \    taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );                                                     \    listINSERT_END( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) );      \} while ( 0 )

尾插(listINSERT_END)保证同优先级任务先来先轮转——这就是时间片轮询的链表基础。

3.3 选出该跑的任务:taskSELECT_HIGHEST_PRIORITY_TASK

// task.c#define taskSELECT_HIGHEST_PRIORITY_TASK()                                       \do {                                                                             \    UBaseType_t uxTopPriority = uxTopReadyPriority;                              \    /* 从缓存点向下扫,找到第一条非空就绪链表 */                                  \    while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) != pdFALSE ) \    {                                                                            \        configASSERT( uxTopPriority );                                          \        --uxTopPriority;                                                        \    }                                                                            \    /* 同优先级轮转取下一个(游标式遍历链表) */                                    \    listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB,                                  \                                 &( pxReadyTasksLists[ uxTopPriority ] ) );      \    uxTopReadyPriority = uxTopPriority;   /* 回写缓存,下次从这里起步 */             \} while ( 0 )

listGET_OWNER_OF_NEXT_ENTRY 每次取"游标指向的下一个"并移动游标——同优先级任务由此均分 CPU。这段宏在 PendSV 的 vTaskSwitchContext 里执行,即每次上下文切换的心脏。

四、vTaskPrioritySet:运行时改优先级(tasks.c)

// task.cvoidvTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority ){    TCB_t * pxTCB;    UBaseType_t uxCurrentBasePriority, uxPriorityUsedOnEntry;    BaseType_t xYieldRequired = pdFALSE;    configASSERT( uxNewPriority < configMAX_PRIORITIES );    /* 软钳位:越界值压回最高合法优先级 */    if( uxNewPriority >= ( UBaseType_t ) configMAX_PRIORITIES )    {        uxNewPriority = ( UBaseType_t ) configMAX_PRIORITIES - 1U;    }    taskENTER_CRITICAL();    {        pxTCB = prvGetTCBFromHandle( xTask );      /* NULL = 改自己 */        #if ( configUSE_MUTEXES == 1 )            uxCurrentBasePriority = pxTCB->uxBasePriority;        #endif        if( uxCurrentBasePriority != uxNewPriority )        {            if( uxNewPriority > uxCurrentBasePriority )            {                /* 抬别的任务且高过当前运行任务 → 需要让位 */                if( pxTCB != pxCurrentTCB &&                    uxNewPriority > pxCurrentTCB->uxPriority )                {                    xYieldRequired = pdTRUE;                }                /* 抬自己:本来就是最高,不用 yield */            }            else if( taskTASK_IS_RUNNING( pxTCB ) == pdTRUE )            {                /* 压低正在运行的自己 → 可能要让给别人 */                xYieldRequired = pdTRUE;            }            uxPriorityUsedOnEntry = pxTCB->uxPriority;            #if ( configUSE_MUTEXES == 1 )            {                /* 关键:正在持有互斥量(继承着别人的高优先级)时,                   只有新优先级更高才动 uxPriority;本名优先级无条件更新 */                if( ( pxTCB->uxBasePriority == pxTCB->uxPriority ) ||                    ( uxNewPriority > pxTCB->uxPriority ) )                {                    pxTCB->uxPriority = uxNewPriority;                }                pxTCB->uxBasePriority = uxNewPriority;            }            #endif            /* 事件链表排序值同步更新(补数) */            listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ),                ( TickType_t ) configMAX_PRIORITIES - uxNewPriority );            /* 就绪态 → 摘下来挂到新优先级的链表 */            if( listIS_CONTAINED_WITHIN( &( pxReadyTasksLists[ uxPriorityUsedOnEntry ] ),                                         &( pxTCB->xStateListItem ) ) != pdFALSE )            {                uxListRemove( &( pxTCB->xStateListItem ) );                prvAddTaskToReadyList( pxTCB );   /* taskRECORD 更新缓存 */            }            /* 阻塞/挂起态 → 只改字段,醒来自然进新链表 */        }    }    taskEXIT_CRITICAL();    if( xYieldRequired != pdFALSE )    {        taskYIELD_TASK_CORE_IF_USING_PREEMPTION( pxTCB );  /* PendSV 立即切换 */    }}

四个要点:

  1. 整个操作在临界区内
    ——改优先级就是改三处存储(TCB 字段、链表、缓存),必须原子;
  2. 升别人 / 降自己才 yield
    :升自己不用(已经是最高);降别人不用(反正压不过我);
  3. 互斥量保护
    :继承期间改本名优先级,只更新 uxBasePriority,不动 uxPriority——防止把继承来的高优先级意外弄丢,否则持有者提前被压下去,优先级反转又会回来;
  4. 注意 configASSERT( uxNewPriority < configMAX_PRIORITIES ) 在钳位之前——本工程开了 assert,直接传越界值会断言死机,而不是被静默钳位(钳位是给关闭 assert 的工程的保险)。

五、优先级继承与返还:互斥量的灵魂

这正对应本工程 main.c 的优先级反转对照实验(时序图见 8.2,实验代码见 9.2)。

5.1 继承:xTaskPriorityInherit(tasks.c)

高任务 Take 互斥量失败、把持锁的低任务抬上来:

// task.cBaseType_t xTaskPriorityInherit( TaskHandle_t const pxMutexHolder ){    TCB_t * const pxMutexHolderTCB = pxMutexHolder;    BaseType_t xReturn = pdFALSE;    /* 中断里不会持有互斥量,持有者必为任务 */    if( pxMutexHolder != NULL )    {        /* 持有者比我低 → 它临时继承我的优先级 */        if( pxMutexHolderTCB->uxPriority < pxCurrentTCB->uxPriority )        {            /* 就绪态:先从旧优先级链表摘除 */            if( listIS_CONTAINED_WITHIN(                    &( pxReadyTasksLists[ pxMutexHolderTCB->uxPriority ] ),                    &( pxMutexHolderTCB->xStateListItem ) ) != pdFALSE )            {                uxListRemove( &( pxMutexHolderTCB->xStateListItem ) );                /* 先继承,再挂新链表——顺序不能反 */                pxMutexHolderTCB->uxPriority = pxCurrentTCB->uxPriority;                prvAddTaskToReadyList( pxMutexHolderTCB );            }            else            {                /* 阻塞态:只改字段即可 */                pxMutexHolderTCB->uxPriority = pxCurrentTCB->uxPriority;            }            xReturn = pdTRUE;   /* 发生了继承 */        }    }    return xReturn;}

注意:只动 uxPriority,绝不动 uxBasePriority——本名留着还。(节选省略了事件链表排序值同步与 trace 钩子,完整逻辑见源码。)

5.1.1 内核怎么知道"继承谁的优先级":两个身份的场景

一个常被问到的问题:继承发生时,内核怎么知道要把谁抬起来、又抬到哪个优先级(即继承谁的)?答案是调用点就在现场,两个身份都直接从当前上下文里读,不需要"查"也不需要"遍历"。

触发继承的调用点在 queue.c,是 xQueueSemaphoreTake() 里"互斥量已经被别人持有,当前任务即将进入阻塞态"之前的一刹那:

/* queue.c —— xQueueSemaphoreTake() 内部 */if( prvIsQueueEmpty( pxQueue ) != pdFALSE )   /* 量空 = 互斥量被人拿着 */{    #if ( configUSE_MUTEXES == 1 )    {        if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )        {            taskENTER_CRITICAL();            {                /* 传参:持有者身份(来自互斥量结构体,Give/Take 成功时写入)*/                xInheritanceOccurred = xTaskPriorityInherit(                    pxQueue->u.xSemaphore.xMutexHolder                );            }            taskEXIT_CRITICAL();        }    }    #endif    /* 之后才是真正把当前任务挂到等待队列的 vTaskPlaceOnEventList */}

两个身份的来源:

身份
存储位置
含义
为什么此刻一定准确
被抬者(互斥量持有者 TCB)
pxQueue->u.xSemaphore.xMutexHolder
(队列结构体字段)
当前谁拿着这把锁
同一函数内上一行刚判断了 prvIsQueueEmpty → 锁不为空 → 此字段必非空(除非中断里拿了互斥量但那是非法路径直接判 NULL)
优先级来源(抬到谁的优先级)
pxCurrentTCB
(全局运行中任务 TCB)
就是此刻在执行 xQueueSemaphoreTake、拿不到锁、即将被挂起的这个任务自己
这是任务级 API(非 FromISR 版),能跑到这里的上下文就是运行中的任务,pxCurrentTCB 不可能错

所以继承的真正含义,用一句话说就是:当前运行的高任务要拿锁、发现被别人占了 → 以互斥量里登记的持有者 TCB 为目标,把它的 uxPriority 改成当前任务(=请求者)的 uxPriority。真正的"继承动作"就是这一行赋值(tasks.c 继承函数里):

/* 持有者就绪态路径 */pxMutexHolderTCB->uxPriority = pxCurrentTCB->uxPriority;

三个边界行为要一起理解,才不会搞错"抬到多少":

  1. 严格小于才抬
    :pxMutexHolderTCB->uxPriority<pxCurrentTCB->uxPriority。等号不触发。
  • 已经被别的高任务抬过(如低任务 1 → 已被任务 3 抬到 3),再来一个优先级 3 的请求者要锁 → 3 vs 3 不抬。
  • 反向:请求者比持有者优先级低 → 路径上不会走到这里,低优先级任务根本抢不过正在运行的高优先级持有者,只有高任务醒来/解除阻塞后 Take 失败才会触发继承。
  1. 多层继承不需要栈,直接覆盖
    :低任务 L(1) 持锁 → M(3) 来要 → 抬到 3 → 更高的 H(4) 也来要 → 再次执行 uxPriority = 4,直接覆盖上一次继承值,不需要记录继承层。返还时只看 uxBasePriority(本名 1),一步归位。
  2. 持有者当前是阻塞态也照样继承
    :else 分支(tasks.c:6641)只改字段不挂链表,等它从阻塞里醒来的那一刻,挂就绪链表用的就是已经抬高的优先级。
所以 5.2 节里 uxMutexesHeld==0 才降级的判断就是这个机制的对称面:不管中间被继承抬了几层、也不管同时拿了几把互斥量,只有最后一把锁还完才回本名优先级——否则中途一降级,另一把锁的等待者立刻回到反转状态。

5.2 返还:xTaskPriorityDisinherit(tasks.c)

持有者 Give 互斥量时,把偷来的优先级还回去:

// task.cBaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder ){    TCB_t * const pxTCB = pxMutexHolder;    configASSERT( pxTCB == pxCurrentTCB );  /* 还锁的一定是运行中的自己 */    ( pxTCB->uxMutexesHeld )--;    /* 当前优先级 ≠ 本名优先级 → 说明继承过 */    if( pxTCB->uxPriority != pxTCB->uxBasePriority )    {        /* 持有的互斥量全还完了才降级(还持有别的锁就不动) */        if( pxTCB->uxMutexesHeld == ( UBaseType_t ) 0 )        {            /* 从高优先级链表摘除 */            uxListRemove( &( pxTCB->xStateListItem ) );            /* 归还本名优先级 */            pxTCB->uxPriority = pxTCB->uxBasePriority;            /* 事件链表排序值同步刷新 */            listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ),                ( TickType_t ) configMAX_PRIORITIES - pxTCB->uxPriority );            prvAddTaskToReadyList( pxTCB );            xReturn = pdTRUE;   /* 可能需要切换(比如刚放跑了一个更高优先级的等锁者) */        }    }    return xReturn;}

两个防御性设计值得看两遍:

  • configASSERT( pxTCB == pxCurrentTCB )
    :互斥量禁止在 ISR 里 Give(Give 的必须是持有者本人),这也是"互斥量不能用于中断"这一硬约束的源码根源;
  • uxMutexesHeld
     计数:嵌套/多锁场景下,最后一锁还完才降级——中途降级会让另一把锁的等待者重新暴露在反转风险里。

六、时间片:本工程关掉了它

6.1 tick 里的同优先级轮转(tasks.c)

/* Tasks of equal priority to the currently running task will shareprocessing time (time slice) if preemption is on... */#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ){    if( listCURRENT_LIST_LENGTH(            &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > 1U )    {        xSwitchRequired = pdTRUE;   /* 同优先级还有别人 → 切 */    }}#endif

逻辑很直白:当前优先级的就绪链表长度 > 1 就切换。配合 listGET_OWNER_OF_NEXT_ENTRY 的游标轮转,同优先级任务在每个 tick 边界轮流坐庄。

6.2 本工程的特殊配置

/* FreeRTOSConfig.h */#define configUSE_PREEMPTION       1#define configUSE_TIME_SLICING     0    /* ← 关闭时间片! */#define configIDLE_SHOULD_YIELD    1

时间片关闭意味着:同优先级任务之间,运行中的那个不主动让出(阻塞/延时)就永远独占 CPU,tick 到了也不切。好处是省去无谓切换、时间行为更确定;代价是同优先级任务必须自觉让出。另外 configIDLE_SHOULD_YIELD=1 是独立开关:空闲任务每轮循环发现还有优先级 0 的应用任务就绪时主动让位——注意它让的只是同优先级的,压不过优先级 1 及以上的任务。

七、注意事项(踩坑清单)

坑
机制根源
优先级数值方向和 NVIC 相反
任务优先级数值升序,NVIC 降序,两套体系互不相干
传 ≥configMAX_PRIORITIES 的值
本工程开 assert → configASSERT 死机;关 assert 才走钳位
用优先级 0 建应用任务
和空闲任务同级,configIDLE_SHOULD_YIELD 开着还能被带一带,关了就饿死
中断里调 vTaskPrioritySet
没有 FromISR 版本
——它要进任务级临界区并在任务上下文 yield,ISR 里调用直接破坏内核数据
二值信号量当互斥量用
无优先级继承 → 反转(本工程实验验证,见 8.2)
中断里调 xSemaphoreGive(互斥量)
configASSERT( pxTCB == pxCurrentTCB )
 失败死机(见 5.2)
同优先级任务"卡死"不轮转
本工程 configUSE_TIME_SLICING=0,同优先级不让出就独占
定时器回调里干重活
定时器任务优先级 = configMAX_PRIORITIES-1 = 本工程最高(4),回调慢会拖垮所有应用任务
改优先级覆盖了继承中的优先级
vTaskPrioritySet
 的 mutex 保护分支:继承中只改 uxBasePriority(见第四节)
uxTaskPriorityGet
 返回值误当本名
它读的是实时 uxPriority——继承期间读到的是抬高的值,本名在 uxBasePriority(无 API 可读)

任务优先级 vs 中断优先级再强调一次:本工程 configKERNEL_INTERRUPT_PRIORITY=15<<4(SysTick/PendSV 为最低),configMAX_SYSCALL_INTERRUPT_PRIORITY=5<<4。数值 0~4 的中断永远不被内核屏蔽、不能调任何 FreeRTOS API;只有 5~15 的中断才能用 FromISR 系列。这套数值方向和任务优先级完全相反,混着记必出事。

八、时序图

8.1 vTaskPrioritySet 全流程

8.2 优先级反转 vs 继承(本工程 main.c 实验)

8.3 同优先级轮转(时间片,本工程已关闭)

九、用例(可直接编译验证)

9.1 运行时改优先级 + 查询

// main.c/* 观察:就绪态任务被抬高优先级 → 摘旧链表挂新链表 → 立即抢占运行中的任务 *//* worker:忙循环占着 CPU(优先级1,就绪态) */voidworker_task( void *pvParameters ){    (void)pvParameters;    volatile uint32_t ulCount = 0;    while( uxTaskPriorityGet( NULL ) == 1 )   /* 优先级还是1就一直忙转 */    {        ulCount++;    }    /* 被抬到4的瞬间抢上台,落到这里 */    printf( "worker: promoted to %u!\r\n",            ( unsigned ) uxTaskPriorityGet( NULL ) );    vTaskDelay( pdMS_TO_TICKS( 100 ) );       /* 一阻塞,controller 立刻回来 */    vTaskDelete( NULL );}voidcontroller_task( void *pvParameters ){    (void)pvParameters;    TaskHandle_t xWorker = NULL;    xTaskCreate( worker_task, "worker", 128, NULL, 1, &xWorker );    vTaskDelay( pdMS_TO_TICKS( 1000 ) );      /* 期间 worker(1) 独占 CPU 忙转 */    printf( "worker priority = %u\r\n",            ( unsigned ) uxTaskPriorityGet( xWorker ) );  /* 读实时值:1 */    vTaskPrioritySet( xWorker, 4 );  /* worker 就绪态:摘1号链表挂4号链表,                                        且4 > 本任务2 → 立即抢占 */    /* ——worker(4) 立刻上台打印;它一睡,下面这行才执行—— */    printf( "back in controller\r\n" );    vTaskPrioritySet( NULL, 0 );    /* 把自己压到0:低于几乎所有任务,小心用 */    vTaskDelete( NULL );            /* 没有更高优先级任务就绪时才轮到这行 */}

9.2 优先级反转对照实验(本工程 main.c 实际代码)

// main.c#define HIGH_PRIORITY_TASK_PRIORITY   ( tskIDLE_PRIORITY + 3 )   /* = 3 */#define MEDIUM_PRIORITY_TASK_PRIORITY ( tskIDLE_PRIORITY + 2 )   /* = 2 */#define LOW_PRIORITY_TASK_PRIORITY    ( tskIDLE_PRIORITY + 1 )   /* = 1 *//* 高优先级任务:t=300ms 醒来要锁,记录等了多久 */voidhigh_priority_task( void *pvParameters ){    (void)pvParameters;    vTaskDelay( pdMS_TO_TICKS( 300 ) );            /* 等低任务把锁拿到手 */    printf( "high: 要锁...\n" );    TickType_t t0 = xTaskGetTickCount();    xSemaphoreTake( xSemaphore, portMAX_DELAY );   /* 阻塞:锁在低任务手里 */    printf( "high: 拿到锁! 等了 %u ms\n",            (unsigned)( xTaskGetTickCount() - t0 ) * portTICK_PERIOD_MS );    xSemaphoreGive( xSemaphore );    vTaskDelete( NULL );}/* 中优先级任务:t=500ms 开始忙等 3s——反转的帮凶 */voidmedium_priority_task( void *pvParameters ){    (void)pvParameters;    vTaskDelay( pdMS_TO_TICKS( 500 ) );            /* 等高任务已经开始等锁 */    printf( "medium: 开始忙等 3s 捣乱\n" );    TickType_t xStart = xTaskGetTickCount();    while ( xTaskGetTickCount() - xStart < pdMS_TO_TICKS( 3000 ) )    {    }    vTaskDelete( NULL );}/* 低优先级任务:开局拿锁,边睡边磨蹭约 2s */voidlow_priority_task( void *pvParameters ){    (void)pvParameters;    xSemaphoreTake( xSemaphore, portMAX_DELAY );   /* 开局就拿到锁 */    printf( "low: 拿到锁,慢慢干\n" );    for ( int i = 0; i < 10; i++ )    {        printf( "low %d\n", i );        vTaskDelay( pdMS_TO_TICKS( 200 ) );        /* 持锁磨蹭约 2s */    }    xSemaphoreGive( xSemaphore );    vTaskDelete( NULL );}intmain( void ){    NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );   /* FreeRTOS 推荐 */    USART1_Init( 115200 );    /* 对照实验开关:换回 xSemaphoreCreateBinary() 即复现反转 */    xSemaphore = xSemaphoreCreateMutex();    xSemaphoreGive( xSemaphore );    xTaskCreate( high_priority_task,   "high",   256, NULL, HIGH_PRIORITY_TASK_PRIORITY,   NULL );    xTaskCreate( medium_priority_task, "medium", 256, NULL, MEDIUM_PRIORITY_TASK_PRIORITY, NULL );    xTaskCreate( low_priority_task,    "low",    256, NULL, LOW_PRIORITY_TASK_PRIORITY,    NULL );    vTaskStartScheduler();    for( ;; );}

实测结论(串口打印的 high: 拿到锁! 等了 xxx ms):

- 二值信号量:高任务等锁时间 = 中任务忙等剩余时间 + 低任务磨完剩余循环——被无关的中任务拖长(反转);- 互斥量:高任务等锁时间 ≈ 低任务持锁的固有磨蹭时间(约 2s),不被中任务额外拖长——继承让低任务每次醒来都能压过中任务推进。

十、总结

1. 优先级数值越大越高(与 NVIC 相反),0 是空闲任务专属,本工程合法范围 0~4;2. 存储三处:TCB 双字段(实时+本名,为继承而生)、就绪链表数组(优先级作下标+同级轮转)、事件链表补数(阻塞队列按优先级降序唤醒);3. 改优先级 = 临界区内原子迁移三处存储 + 视情况 yield;互斥量的继承/返还只动 uxPriority 并以 uxBasePriority 为归还依据——这就是二值信号量会反转、互斥量不会的源码根源。

十一、与工程踩坑记录的关联

坑
关联机制
任务优先级数值升序,与 NVIC 相反
两套独立体系(本文第一节)
中断里不能用互斥量
xTaskPriorityDisinherit
 断言 pxTCB == pxCurrentTCB(本文 5.2)
互斥量优先级继承防反转
xTaskPriorityInherit
 临时抬持有者(本文第五节,main.c 实验验证)
IDLE 任务被饿死
优先级 0 最低 + 同级任务不让出(本文第六节)
定时器回调拖垮系统
定时器任务占 configMAX_PRIORITIES-1(本文第一节)
ISR 中调任务级 API
vTaskPrioritySet
 无 FromISR 版本(本文第七节)

— 写在最后 —

如果这篇文章对你有帮助,欢迎点赞、在看、转发三连支持,这是我持续更新的最大动力。

关注我,更多硬核技术剖析持续更新中,下一期再见。

相关学习资料