4.2.4 任务优先级
4.2.4 任务优先级
📚 本节导读
学习时长: 约 15 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: 任务调度机制、优先级抢占概念
🎯 学习目标
- 理解优先级的取值范围和配置方式
- 掌握
os_task_set_priority()和os_task_get_priority()的用法 - 理解互斥锁场景下的优先级继承机制
- 了解系统任务的优先级安排
- 掌握优先级分配的最佳实践
一、优先级范围与配置
1.1 优先级范围
在 OneOS 中,优先级数值越小,优先级越高。0 是最高优先级,OS_TASK_PRIORITY_MAX - 1 是最低优先级。
1.2 配置方式
优先级最大值通过 Kconfig 配置,位于 kernel/Kconfig 的 Task 菜单中:
choice
prompt "The max level value of priority of task"
default OS_TASK_PRIORITY_32
config OS_TASK_PRIORITY_8 → bool "8"
config OS_TASK_PRIORITY_16 → bool "16"
config OS_TASK_PRIORITY_32 → bool "32" ← 默认
config OS_TASK_PRIORITY_64 → bool "64"
config OS_TASK_PRIORITY_128 → bool "128"
config OS_TASK_PRIORITY_256 → bool "256"
endchoice| 配置选项 | 优先级范围 | 适用场景 |
|---|---|---|
| 8 | 0 ~ 7 | 极简系统,任务数量极少 |
| 16 | 0 ~ 15 | 简单嵌入式系统 |
| 32 | 0 ~ 31 | 默认配置,适合大多数场景 |
| 64 | 0 ~ 63 | 中等复杂度系统 |
| 128 | 0 ~ 127 | 复杂系统,任务较多 |
| 256 | 0 ~ 255 | 大型系统,任务数量多且需要精细优先 |
注意:
OS_TASK_PRIORITY_MAX不能超过 256,调度器代码中对此有编译时检查。
1.3 调度器位图与优先级
当 OS_TASK_PRIORITY_MAX ≤ 32 时,调度器使用单个 uint32_t 位图;当 OS_TASK_PRIORITY_MAX > 32 时,使用两级位图(组位图 + 组内位图)。两者都保证了 O(1) 的最高优先级查找时间。
二、os_task_set_priority() —— 设置优先级
2.1 函数签名
os_err_t os_task_set_priority(os_task_id tid, uint8_t new_priority);2.2 参数说明
| 参数 | 说明 |
|---|---|
tid | 目标任务的任务 ID |
new_priority | 新的优先级值,必须 < OS_TASK_PRIORITY_MAX |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 设置成功 |
2.3 完整源码
来源: kernel/source/os_task.c
os_err_t os_task_set_priority(os_task_id tid, uint8_t new_priority)
{
os_bool_t need_schedule;
os_bool_t task_hold_mutex;
os_task_t *task;
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(task != OS_NULL);
OS_ASSERT(new_priority < OS_TASK_PRIORITY_MAX);
OS_ASSERT(task->object_inited == OS_KOBJ_INITED);
need_schedule = OS_FALSE;
OS_KERNEL_ENTER();
#if defined(OS_USING_MUTEX)
task_hold_mutex = !os_list_empty(&task->hold_mutex_list_head);
#else
task_hold_mutex = OS_FALSE;
#endif
task->backup_priority = new_priority;
/* Task does not hold mutex */
if (OS_FALSE == task_hold_mutex)
{
if ((task->state & OS_TASK_STATE_READY) != 0)
{
k_readyq_remove(task);
task->current_priority = new_priority;
k_readyq_put(task);
need_schedule = OS_TRUE;
}
else if ((task->state & OS_TASK_STATE_BLOCK) != 0)
{
#ifdef OS_USING_SMP
if ((OS_NULL != task->block_list_head) && (OS_TRUE == task->is_wake_prio))
#else
OS_ASSERT((OS_NULL != task->block_list_head));
if (OS_TRUE == task->is_wake_prio)
#endif
{
os_list_del(&task->task_node);
task->current_priority = new_priority;
k_blockq_insert(task->block_list_head, task);
}
else
{
task->current_priority = new_priority;
}
}
else
{
task->current_priority = new_priority;
}
}
if (OS_TRUE == need_schedule)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
return OS_SUCCESS;
}2.4 分步详解
先看整体决策流程:os_task_set_priority() 的核心是一个"是否持有互斥锁 → 任务在哪个状态"的两级判断,决定了优先级如何修改:
os_task_set_priority()
│
▼
先无条件更新 backup_priority = new_priority
│
▼
任务是否持有互斥锁(hold_mutex_list_head 非空)?
├── 是 ──► 什么都不做(current_priority 不变)
│ (优先级继承机制保持 current_priority)
└── 否 ──► 根据任务状态决定:
│
▼
任务处于什么状态?
├── READY ──► 移除→改优先级→重新入队→触发调度
├── BLOCK 且按优先级唤醒 ──► 移除→改优先级→按新优先级重新插入
├── BLOCK 且 FIFO 唤醒 ──► 仅改 current_priority
└── 其他 ──► 仅改 current_priority一句话总结:持有互斥锁时只改
backup_priority(预留目标),不持有互斥锁时才真正修改current_priority,并按任务当前状态决定是否需要重新排队。
第一步:准备与参数校验
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(task != OS_NULL);
OS_ASSERT(new_priority < OS_TASK_PRIORITY_MAX);
OS_ASSERT(task->object_inited == OS_KOBJ_INITED);
need_schedule = OS_FALSE;
OS_KERNEL_ENTER();详细解释:
| 断言/语句 | 作用 |
|---|---|
OS_KERNEL_INIT() | 确保内核已初始化 |
OS_TYPE_CONVERT(os_task_t *, tid) | 把任务 ID 转换为 os_task_t * |
OS_ASSERT(task != OS_NULL) | 任务指针非空 |
OS_ASSERT(new_priority < OS_TASK_PRIORITY_MAX) | 新优先级必须合法(在 0 ~ OS_TASK_PRIORITY_MAX-1 范围内) |
OS_ASSERT(task->object_inited == OS_KOBJ_INITED) | 任务必须处于已初始化状态 |
need_schedule = OS_FALSE | 初始默认不触发调度 |
OS_KERNEL_ENTER() | 进入临界区,保护后续对任务状态的检查和修改 |
第二步:判断任务是否持有互斥锁
#if defined(OS_USING_MUTEX)
task_hold_mutex = !os_list_empty(&task->hold_mutex_list_head);
#else
task_hold_mutex = OS_FALSE;
#endif详细解释:
task->hold_mutex_list_head是任务持有的互斥锁链表头。链表非空说明任务当前持有至少一个互斥锁。- 关键:这个判断决定了后续只更新
backup_priority还是真正修改current_priority。 - 未启用互斥锁功能时,直接置
OS_FALSE。
第三步:先更新备用优先级
task->backup_priority = new_priority;详细解释:
- 无论是否持有互斥锁,
backup_priority都会先被更新为new_priority。 backup_priority的作用:保存任务的原始优先级。当互斥锁释放后,任务优先级会恢复到这个值。
第四步:根据任务状态分情况处理
/* Task does not hold mutex */
if (OS_FALSE == task_hold_mutex)
{
if ((task->state & OS_TASK_STATE_READY) != 0)
{
k_readyq_remove(task);
task->current_priority = new_priority;
k_readyq_put(task);
need_schedule = OS_TRUE;
}
else if ((task->state & OS_TASK_STATE_BLOCK) != 0)
{
#ifdef OS_USING_SMP
if ((OS_NULL != task->block_list_head) && (OS_TRUE == task->is_wake_prio))
#else
OS_ASSERT((OS_NULL != task->block_list_head));
if (OS_TRUE == task->is_wake_prio)
#endif
{
os_list_del(&task->task_node);
task->current_priority = new_priority;
k_blockq_insert(task->block_list_head, task);
}
else
{
task->current_priority = new_priority;
}
}
else
{
task->current_priority = new_priority;
}
}详细解释:
任务持有互斥锁时(task_hold_mutex == OS_TRUE):
- 进入
if (OS_FALSE == task_hold_mutex)的else,只更新backup_priority,不修改current_priority。 - 原因:任务持有互斥锁时,
current_priority可能已被优先级继承机制提升了。若直接改成new_priority会破坏优先级继承。new_priority会被保存为backup_priority,作为锁释放后的目标优先级。
任务不持有互斥锁时(task_hold_mutex == OS_FALSE),根据状态分三种情况:
| 任务状态 | 处理方式 | 说明 |
|---|---|---|
| READY(就绪) | 从就绪队列移除 → 改 current_priority → 重新加入 → 触发调度 | 因为优先级变了,要放到新优先级对应的就绪链表,并重新调度 |
| BLOCK(阻塞)且按优先级唤醒 | 从阻塞队列移除 → 改优先级 → 按新优先级重新插入阻塞队列 | 阻塞队列按优先级排序时,改优先级要重新排队 |
| BLOCK(阻塞)但 FIFO 唤醒 | 仅改 current_priority | 阻塞队列按 FIFO 顺序,不调整位置 |
| 其他状态 | 仅改 current_priority | 不涉及队列操作 |
关于 BLOCK 分支的两个关键点:block_list_head 和 is_wake_prio
task->block_list_head:任务当前被阻塞在哪个 IPC 队列(信号量/互斥锁/消息队列的等待队列)的头指针。task->is_wake_prio:这个阻塞队列的唤醒方式——是按优先级唤醒(OS_TRUE)还是按 FIFO 顺序唤醒(OS_FALSE)。
| 唤醒方式 | 含义 | 阻塞队列如何排序 | 改优先级后 |
|---|---|---|---|
is_wake_prio == OS_TRUE | 按优先级唤醒 | 等待者按优先级高低排队,高优先级排在前面 | 优先级变了,队列顺序可能错,必须移除后按新优先级重新插入 |
is_wake_prio == OS_FALSE | 按 FIFO 顺序唤醒 | 按先来后到排队 | 队列顺序不依赖优先级,只需改优先级字段,不用调整位置 |
为什么按优先级唤醒时"必须重新排队"? 因为阻塞队列是按优先级排序的(高优先级任务等锁时排前面)。任务优先级改变后,它在队列中的"正确位置"变了,如果不重新插入,可能出现"低优先级反而排前面"的错误唤醒顺序。所以要先
os_list_del()移除,再k_blockq_insert()按新优先级插入。
READY 状态为什么必须重新入队?
就绪队列是按优先级组织的(每个优先级一个链表)。任务优先级改变后,它必须从原来的优先级链表移到新优先级链表,否则调度器会按错误的优先级调度它:
改优先级前:优先级 5 链表 → A(current_priority=5)
改优先级后:A 移到优先级 10 链表
[优先级 5 链表] (A 移出)
[优先级 10 链表] → A(current_priority=10)READY 状态修改优先级后的调度效果
os_task_set_priority() 在 READY 分支设置了 need_schedule = OS_TRUE,意味着:
修改前:A 在优先级 5,正在运行(或即将运行)
│
▼ os_task_set_priority(A, 10) 把 A 降到优先级 10
│
▼ 重新调度
├─ 若就绪队列中有优先级 1~9 的任务 → 立即切换到更高优先级任务运行
└─ 若没有更高优先级任务 → A 继续运行(但按优先级 10 参与后续调度)这就是"动态修改优先级会立即影响调度"的体现——把一个正在运行的任务优先级调低,可能立刻触发一次抢占调度。
第五步:收尾
if (OS_TRUE == need_schedule)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
return OS_SUCCESS;详细解释:
- 只有 READY 状态修改优先级时
need_schedule为OS_TRUE,因为此时任务在就绪队列中的位置变了,需要重新调度。 OS_KERNEL_EXIT_SCHED():退出临界区并触发调度;OS_KERNEL_EXIT():仅退出临界区。
2.5 使用示例
/* 修改其他任务的优先级 */
os_task_id tid = os_task_get_id("worker_task");
os_task_set_priority(tid, 3); /* 提升到优先级 3 */
/* 修改自己的优先级 */
os_task_set_priority(os_get_current_task(), 10);三、os_task_get_priority() —— 获取优先级
3.1 函数签名
os_err_t os_task_get_priority(os_task_id tid, uint8_t *priority);3.2 完整源码
来源: kernel/source/os_task.c
os_err_t os_task_get_priority(os_task_id tid, uint8_t *priority)
{
os_task_t *task;
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(task != OS_NULL);
OS_ASSERT(task->object_inited == OS_KOBJ_INITED);
OS_ASSERT(OS_NULL != priority);
*priority = task->current_priority;
return OS_SUCCESS;
}3.3 分步详解
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(task != OS_NULL);
OS_ASSERT(task->object_inited == OS_KOBJ_INITED);
OS_ASSERT(OS_NULL != priority);
*priority = task->current_priority; /* 关键:把当前优先级写入传出参数 */详细解释:
| 断言/语句 | 作用 |
|---|---|
OS_TYPE_CONVERT(os_task_t *, tid) | 把任务 ID 转换为 os_task_t * |
OS_ASSERT(task != OS_NULL) | 任务指针非空 |
OS_ASSERT(task->object_inited == OS_KOBJ_INITED) | 任务必须已初始化 |
OS_ASSERT(OS_NULL != priority) | 传出参数指针非空(防止写入非法地址) |
*priority = task->current_priority | 把任务的当前优先级写入 priority 指向的变量 |
关键点:
priority是传出参数(out 参数),函数把结果写入它指向的内存,所以指针不能为空。- 返回的是
current_priority(当前优先级)。如果任务持有互斥锁且发生了优先级继承,返回的可能是继承后的(被提升的)优先级,而不是原始优先级(backup_priority)。
3.4 使用示例
uint8_t prio;
os_task_get_priority(tid, &prio);
os_kprintf("Task priority: %u\r\n", prio);四、优先级继承机制
4.1 问题场景:优先级反转
优先级反转(Priority Inversion) 是 RTOS 中的经典问题:一个高优先级任务,因为等待一个低优先级任务持有的资源,反而被一个"无关"的中优先级任务阻塞,导致运行时间被无故拉长。
假设有三个任务:
| 任务 | 优先级(数值) | 相对关系 |
|---|---|---|
| 任务 H(High) | 1 | 最高 |
| 任务 M(Middle) | 5 | 中间 |
| 任务 L(Low) | 10 | 最低 |
再假设有一个互斥锁 Mutex,任务 L 和任务 H 都要用到它(访问同一份共享数据),而任务 M 完全不需要这个锁(它只是在做自己的事)。
优先级反转是如何一步步发生的?
| 步骤 | 发生了什么 | 任务 L | 任务 M | 任务 H |
|---|---|---|---|---|
| 1 | 任务 L 先运行,成功获取了互斥锁 Mutex | ✅ 持有锁,正在运行 | — | — |
| 2 | 任务 H 就绪,且优先级最高,抢占 L 并尝试获取 Mutex,但锁被 L 占着 → H 被阻塞 | ⏸ 被抢占,但仍持有锁 | — | ❌ 尝试获取锁失败,阻塞等待 |
| 3 | 任务 M 就绪(优先级 5),抢占 L 并开始运行。M 不需要锁,所以能一直跑 | ⏸ 等待(本来应该尽快释放锁) | ✅ 运行中(无关任务) | ❌ 还在阻塞等待 |
| 4 | 任务 M 长时间运行(如处理大量数据),L 一直得不到 CPU,无法释放锁,H 就一直等 | ⏸ 被 M 饿着 | ✅ 占用 CPU | ❌ 一直等待锁 |
问题的核心在步骤 3~4:
- 正常情况下,H 等的是 L 的锁,L 应该尽快运行、释放锁,然后 H 拿到锁继续。
- 但中途插进来了一个"无关的"中优先级任务 M。M 优先级比 L 高,所以 M 抢占 L;M 又不需要锁,所以它一直运行,把 L "饿住"了。
- L 被饿住 → 无法释放锁 → H 只能继续等。结果:高优先级的 H,被一个优先级比它低、而且和锁完全无关的 M 给阻塞了。
优先级反转示意图(时间从左到右):
时间线 │ 1 2 3 4 5
──────┼─────────────────────────────────────────►
任务H │ ───────── 阻塞等待锁 ──────── 拿到锁
任务M │ ┌── 运行 ──┐
任务L │ ── 持有锁 ── 被M抢占(持有锁但得不到CPU)─ 释放锁
↑
M 与锁无关,却让 H 白白等待!为什么说"反转"?
"反转"指的是实际生效的优先级顺序颠倒了:H 的优先级明明是 1(最高),但因为它要等 L 的锁,实际效果变成了——H 要等 M 运行完才能继续。相当于 H 被降级到了 M 的优先级之下,这违背了"高优先级先执行"的实时性保证,可能造成系统响应延迟甚至超时。
注意:优先级反转与前面 4.2.3 讨论的"优先级饥饿"不同。饥饿是"低优先级任务永远得不到 CPU";反转是"高优先级任务因无关任务被拖慢"。两者都是实时系统需要警惕的问题。
4.2 OneOS 的优先级继承
OneOS 的互斥锁实现了优先级继承机制来解决优先级反转问题。核心思想是:当一个高优先级任务 H 在等待低优先级任务 L 持有的互斥锁时,把 L 的优先级临时提升到 H 的优先级,从而让 L 能尽快运行并释放锁,缩短 H 的等待时间。
优先级继承的过程:
- 当高优先级任务 H 尝试获取已被低优先级任务 L 持有的互斥锁时
- L 的
current_priority被临时提升到 H 的优先级(即优先级 1) backup_priority保存 L 的原始优先级(即优先级 10)- 当 L 释放互斥锁后,优先级恢复为
backup_priority
优先级继承的效果:
────────────────────────────────────────────►
任务 L 获取锁 ──┬── 优先级提升到 1 ──┬── 释放锁,恢复优先级 10
│ │
任务 H └── 尝试获取锁,L 被提升 └── 获取锁成功
任务 M 无法抢占 L(因为 L 的优先级已经是 1)4.3 优先级提升的源码实现
优先级提升发生在高优先级任务尝试获取已被低优先级任务持有的互斥锁时。这个过程分两层:
- 外层:互斥锁获取(
os_mutex_lock)中判断"是否需要提升拥有者优先级",命中则调用_k_mutex_set_owner_priority()。 - 内层:
_k_mutex_set_owner_priority()真正执行优先级提升,并返回"是否需要重新调度"。
4.3.1 触发条件(互斥锁获取时的判断)
来源: kernel/source/os_mutex.c(os_mutex_lock 内部)
/* Priority inherit */
if ((OS_MUTEX_WAKE_TYPE_PRIO == mutex->wake_type) &&
(current_task->current_priority < mutex->owner->current_priority))
{
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
k_kernel_spin_lock();
#endif
(void)_k_mutex_set_owner_priority(mutex, current_task->current_priority);
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
k_kernel_spin_unlock();
#endif
}触发条件逐句解释:
| 条件 | 含义 | 通俗解释 |
|---|---|---|
OS_MUTEX_WAKE_TYPE_PRIO == mutex->wake_type | 该互斥锁是按优先级唤醒类型 | 只有这种锁才做优先级继承(FIFO 唤醒的锁不继承) |
current_task->current_priority < mutex->owner->current_priority | 当前任务(等待者)优先级比拥有者高 | 只有"高优先级等低优先级"才需要提升,反之不提升 |
两个条件同时满足才触发优先级继承。SMP 相关代码(
k_kernel_spin_lock)仅在多核非单锁模式下编译,用于保护多核并发。
用 4.1 的 H/M/L 场景举例:
沿用 4.1 的例子:任务 L 持有锁(current_priority = 10),任务 H(current_priority = 1)尝试获取锁时发现锁被占:
current_task = H(优先级 1)
mutex->owner = L(优先级 10)
判断条件:
current_task->current_priority(1) < mutex->owner->current_priority(10) → 成立!
→ 触发优先级继承:把 L 的优先级提升到 14.3.2 提升函数 _k_mutex_set_owner_priority()
来源: kernel/source/os_mutex.c(完整源码,与项目一致)
OS_INLINE os_bool_t _k_mutex_set_owner_priority(const os_mutex_t *mutex, uint8_t new_priority)
{
os_bool_t need_sched;
if ((mutex->owner->state & OS_TASK_STATE_READY) != 0)
{
k_readyq_remove(mutex->owner);
mutex->owner->current_priority = new_priority;
k_readyq_put(mutex->owner);
need_sched = OS_TRUE;
}
else
{
mutex->owner->current_priority = new_priority;
need_sched = OS_FALSE;
}
return need_sched;
}分步解释:
| 代码 | 作用 |
|---|---|
OS_INLINE | 内联函数修饰,减少函数调用开销 |
os_bool_t 返回值 | 返回"是否需要重新调度"(need_sched) |
(mutex->owner->state & OS_TASK_STATE_READY) != 0 | 判断拥有者是否在就绪队列中 |
k_readyq_remove(mutex->owner) | 把拥有者从就绪队列移除(其优先级链表要变) |
mutex->owner->current_priority = new_priority | 核心:把拥有者优先级提升到 new_priority(即等待者的优先级) |
k_readyq_put(mutex->owner) | 按新优先级重新加入就绪队列 |
need_sched = OS_TRUE / OS_FALSE | 在就绪队列中则需重新调度,否则不需要 |
关键点 —— 为什么这里要"移除再重新入队"?
拥有者在就绪队列中时,它的优先级从 10 提升到 1,就绪链表位置变了(从优先级 10 链表移到优先级 1 链表),所以必须 k_readyq_remove() 后再 k_readyq_put():
提升前:优先级 10 链表 → [L](current_priority=10)
提升后:L 移到优先级 1 链表 → [L](current_priority=1)关键点 —— 优先级提升后的效果:
这正是"优先级继承"的核心——拥有者(低优先级 L)临时继承了等待者(高优先级 H)的优先级。此时:
L 的 current_priority = 1 (被提升到 H 的优先级)
L 的 backup_priority = 10 (保存原始优先级,锁释放后恢复)因为 L 的优先级变成 1(与 H 相同),中优先级任务 M(优先级 5)就无法再抢占 L,L 能尽快运行并释放锁,从而解决了 4.1 描述的优先级反转问题。
4.4 优先级恢复的源码实现
当拥有者释放互斥锁后,内核需要恢复拥有者的优先级。恢复逻辑比提升更复杂,因为它要处理"一个任务可能同时持有多个互斥锁"的情况。
来源: kernel/source/os_mutex.c(完整源码,与项目一致)
OS_INLINE os_bool_t _k_mutex_restore_priority(const os_mutex_t *mutex)
{
const os_task_t *owner_task;
uint8_t new_priority;
os_bool_t need_sched;
const os_task_t *block_task;
const os_mutex_t *iter_mutex;
owner_task = mutex->owner;
need_sched = OS_FALSE;
if (owner_task->current_priority != mutex->original_priority)
{
new_priority = mutex->original_priority;
os_list_for_each_entry(iter_mutex, &owner_task->hold_mutex_list_head, os_mutex_t, hold_node)
{
if (!os_list_empty(&iter_mutex->task_list_head))
{
block_task = os_list_first_entry(&iter_mutex->task_list_head, os_task_t, task_node);
if (block_task->current_priority < new_priority)
{
new_priority = block_task->current_priority;
}
}
}
if (new_priority != owner_task->current_priority)
{
if ((mutex->owner->state & OS_TASK_STATE_READY) != 0)
{
k_readyq_remove(mutex->owner);
mutex->owner->current_priority = new_priority;
k_readyq_put(mutex->owner);
need_sched = OS_TRUE;
}
else
{
mutex->owner->current_priority = new_priority;
}
}
}
return need_sched;
}分步解释:
| 代码 | 作用 |
|---|---|
if (owner_task->current_priority != mutex->original_priority) | 仅当该锁导致过优先级提升时才处理 |
new_priority = mutex->original_priority | 先假设要恢复为"该锁被获取时拥有者的优先级" |
os_list_for_each_entry(...) | 遍历拥有者当前持有的所有互斥锁 |
block_task->current_priority < new_priority | 找出所有被阻塞等待者中优先级最高的一个 |
new_priority = block_task->current_priority | 若有更高优先级的等待者,则保留该继承优先级 |
关键点 —— 为什么恢复时要遍历所有持有的锁?
一个任务可能同时持有多个互斥锁,每个锁都可能触发了优先级继承。释放其中一个锁时,不能简单恢复成原始优先级,而要看它持有的其他锁是否还需要保持继承优先级:
场景:任务 L 同时持有两个锁 M1 和 M2
- 锁 M1 被高优先级任务 H1(优先级 1)等待 → L 提升到 1
- 锁 M2 被高优先级任务 H2(优先级 3)等待 → 也触发了继承
当 L 释放 M1 时:
- 不能直接恢复成原始优先级 10
- 因为 L 还持有 M2,M2 上有 H2(优先级 3)在等
- 所以遍历后 L 的优先级应恢复为 3(而不是 10)这就是 new_priority 先设为 original_priority、再遍历找最高优先级等待者的原因——确保恢复后的优先级仍然能满足所有还在等待的锁的继承需求。
关于 original_priority:它记录的是任务获取该锁时的优先级(见 os_mutex.c 中 mutex->original_priority = current_task->current_priority),是恢复时的基准,与 TCB 中的 backup_priority(备用优先级,主要用于 os_task_set_priority())作用不同。
关键点:整个恢复逻辑只在 current_priority != original_priority(即确实被提升过)时才执行,避免了不必要的调度开销。
4.5 优先级继承对 os_task_set_priority() 的影响
当任务持有互斥锁时,调用 os_task_set_priority() 只会更新 backup_priority,不会改变 current_priority。这样既保证了优先级继承机制的正常工作,也允许用户"预定"锁释放后的目标优先级。
/* 任务 L 持有互斥锁,且被提升了优先级 */
os_task_set_priority(tid_L, 12); /* 只更新 backup_priority */
/* 当 L 释放锁后,优先级自动恢复为 12,而不是原始的 10 */为什么不能修改 current_priority?
- 持有互斥锁时,
current_priority是优先级继承提升后的值(可能已被提升到 1)。 - 若直接改成
new_priority,会破坏优先级继承,可能导致其他等待锁的任务等待时间失控。 - 所以
os_task_set_priority()只把new_priority保存到backup_priority。当任务释放所有互斥锁、优先级不再需要继承时,调度器会让current_priority恢复到合理的值(具体恢复逻辑见 4.4,实际以mutex->original_priority为基准)。
五、系统任务优先级
OneOS 内核自带以下系统任务,它们的优先级安排如下:
| 系统任务 | 名称 | 默认优先级 | 说明 |
|---|---|---|---|
| 空闲任务(idle) | "idle" | 最低(OS_TASK_PRIORITY_MAX - 1) | 当没有其他任务就绪时运行,通常执行低功耗相关操作 |
| 回收任务(recycle) | "recycle" | 0(最高) | 负责任务销毁后的资源回收(清理回调、释放内存) |
| 系统任务(sys) | 系统工作队列任务 | 0(最高,可配置) | 系统工作队列任务,处理延迟工作项 |
注意:回收任务优先级为 0(最高),确保被销毁任务的资源能尽快被回收。用户任务不应使用优先级 0,以免干扰系统任务的正常运行。
六、优先级分配最佳实践
6.1 分配原则
| 原则 | 说明 |
|---|---|
| 实时性优先 | 对时间要求严格的任务赋予更高优先级 |
| 关键路径优先 | 系统关键功能(如安全检测、通信协议)赋予高优先级 |
| 短任务优先 | 执行时间短、不阻塞的任务可赋予较高优先级 |
| 避免优先级 0 | 保留给系统任务使用 |
| 分层设计 | 将任务按功能分层,每层分配一个优先级段 |
6.2 推荐优先级分配示例(32 级优先级)
优先级 0: 系统任务(recycle、sys)
优先级 1~3: 实时控制任务(传感器采样、电机控制)
优先级 4~7: 通信协议处理(网络协议栈、CAN 通信)
优先级 8~15: 业务逻辑任务(数据处理、算法计算)
优先级 16~25: 后台任务(日志、监控、状态上报)
优先级 26~30: 低优先级任务(LED 指示、按键扫描)
优先级 31: 空闲任务(idle)6.3 注意事项
- 不要频繁修改优先级:动态优先级修改会触发调度,影响系统性能
- 避免优先级竞争:不要创建过多高优先级任务,防止低优先级任务饥饿
- 互斥锁搭配优先级继承:使用互斥锁保护共享资源时,优先级继承可以避免反转问题
- 中断优先级独立:任务的优先级和中断的优先级是两个独立的概念,互不影响
📝 本节小结
核心要点回顾
本节围绕任务优先级展开,核心要点如下:
1. 优先级范围
- 优先级数值越小,优先级越高:
0最高,OS_TASK_PRIORITY_MAX - 1最低。 - 最大值通过 Kconfig 配置(8/16/32/64/128/256),默认 32。
2. 动态修改优先级
os_task_set_priority():修改任务优先级,并会根据任务状态(READY/BLOCK 等)重新排队、触发调度。os_task_get_priority():获取任务当前优先级(current_priority)。
3. 当前优先级与备用优先级
current_priority:任务当前实际生效的优先级。backup_priority:任务的原始/目标优先级。- 持有互斥锁时,
os_task_set_priority()只改backup_priority,current_priority由优先级继承控制。
4. 优先级继承
- 用于解决优先级反转问题:高优先级任务等锁时,把锁拥有者优先级临时提升。
- 锁释放后,优先级恢复为
backup_priority。 - 系统任务(recycle/sys)占用最高优先级 0,用户任务应避开。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
把优先级设成 OS_TASK_PRIORITY_MAX 或更大 | 必须 < OS_TASK_PRIORITY_MAX,否则断言失败 |
| 使用优先级 0 | 0 预留给系统任务,用户任务应避开 |
混淆 current_priority 和 backup_priority | 持锁任务修改优先级只影响备用优先级,获取到的是当前(可能被提升的)优先级 |
| 频繁动态修改优先级 | 会触发调度,影响性能,尽量在任务创建时定好优先级 |
| 认为任务优先级与中断优先级相关 | 两者是独立概念,互不影响 |
下一步
优先级是任务调度的核心依据。接下来请学习下一小节:
- 4.2.5 任务时间片:了解同优先级任务如何通过时间片轮转分配 CPU