4.2.6 任务挂起与恢复
4.2.6 任务挂起与恢复
📚 本节导读
学习时长: 约 15 分钟
难度级别: ⭐⭐☆☆☆
前置知识: 任务状态机、SUSPEND 状态概念
🎯 学习目标
- 理解挂起(SUSPEND)状态的含义和作用
- 掌握
os_task_suspend()和os_task_resume()的用法 - 了解挂起状态对调度的影响
- 理解挂起与其他状态的叠加行为
一、挂起状态概述
挂起(SUSPEND)是 OneOS 任务状态中一个特殊的状态,它表示任务被手动暂停执行。与 SLEEP 和 BLOCK 不同,挂起不是由任务自身行为(延时或等待资源)触发的,而是由外部显式调用 os_task_suspend() 触发。
1.1 挂起状态的特点
| 特性 | 说明 |
|---|---|
| 触发方式 | 通过 os_task_suspend() 手动触发 |
| 可叠加 | 可与 SLEEP、BLOCK 状态叠加 |
| 解除方式 | 必须通过 os_task_resume() 显式解除 |
| 调度影响 | 挂起后任务从就绪队列中移除,不再参与调度 |
| 自己挂自己 | 允许,但会立即触发调度切换到其他任务 |
1.2 挂起与其他状态的区别
| 状态 | 触发方式 | 恢复方式 | 是否可叠加挂起 |
|---|---|---|---|
| SLEEP | 调用延时函数 | 延时到期自动恢复 | ✅ 是 |
| BLOCK | 等待 IPC 资源 | 资源可用自动恢复 | ✅ 是 |
| SUSPEND | 调用 os_task_suspend() | 调用 os_task_resume() | N/A |
| READY | 任务就绪 | 被调度选中 | ✅ 是 |
二、os_task_suspend() —— 挂起任务
2.1 函数签名
os_err_t os_task_suspend(os_task_id tid);2.2 参数与返回值
| 参数 | 说明 |
|---|---|
tid | 要挂起的目标任务的任务 ID |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 挂起成功 |
OS_FAILURE | 挂起失败(任务状态不正确) |
2.3 完整源码
来源: kernel/source/os_task.c
os_err_t os_task_suspend(os_task_id tid)
{
os_err_t ret;
os_task_t *task;
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
OS_ASSERT(OS_KOBJ_INITED == task->object_inited);
OS_ASSERT((OS_FALSE == os_is_irq_disabled()) || (task != k_task_self()));
OS_ASSERT((OS_FALSE == os_is_schedule_locked()) || (task != k_task_self()));
ret = OS_SUCCESS;
OS_KERNEL_ENTER();
if (task->state & ~(OS_TASK_STATE_READY | OS_TASK_STATE_SLEEP | OS_TASK_STATE_BLOCK))
{
OS_KERNEL_EXIT();
OS_KERN_LOG(KERN_ERROR,
TASK_TAG,
"Incorrect task(%s) state(0x%04X), not allow to suspend.",
task->name,
task->state);
ret = OS_FAILURE;
}
else
{
/* The task is at ready queue */
if (task->state & OS_TASK_STATE_READY)
{
k_readyq_remove(task);
task->state &= ~OS_TASK_STATE_READY;
}
task->state |= OS_TASK_STATE_SUSPEND;
#ifdef OS_USING_SMP
if (task->cpu_index != -1)
{
OS_KERNEL_EXIT_SCHED();
}
#else
if (task == _k_task_self())
{
OS_KERNEL_EXIT_SCHED();
}
#endif
else
{
OS_KERNEL_EXIT();
}
}
return ret;
}2.4 分步详解
第一步:准备与参数校验
os_err_t ret;
os_task_t *task;
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
OS_ASSERT(OS_KOBJ_INITED == task->object_inited);
OS_ASSERT((OS_FALSE == os_is_irq_disabled()) || (task != k_task_self()));
OS_ASSERT((OS_FALSE == os_is_schedule_locked()) || (task != k_task_self()));详细解释:
| 断言/语句 | 作用 |
|---|---|
OS_KERNEL_INIT() | 确保内核已初始化 |
OS_TYPE_CONVERT(os_task_t *, tid) | 把任务 ID 转换为 os_task_t * |
OS_ASSERT(OS_NULL != task) | 任务指针非空 |
OS_ASSERT(OS_KOBJ_INITED == task->object_inited) | 任务必须处于已初始化状态 |
OS_ASSERT((OS_FALSE == os_is_irq_disabled()) || (task != k_task_self())) | 不能在中断禁用且挂起自己时调用 |
OS_ASSERT((OS_FALSE == os_is_schedule_locked()) || (task != k_task_self())) | 不能在调度锁定且挂起自己时调用 |
关键点:后两条断言体现了"挂起自己"的特殊性——挂起自己会触发调度,所以不允许在中断禁用或调度锁定(这两种情况都无法安全切换上下文)时挂起自己。但挂起其他任务不涉及切换,所以允许。
第二步:检查任务状态
if (task->state & ~(OS_TASK_STATE_READY | OS_TASK_STATE_SLEEP | OS_TASK_STATE_BLOCK))
{
OS_KERNEL_EXIT();
OS_KERN_LOG(KERN_ERROR, TASK_TAG,
"Incorrect task(%s) state(0x%04X), not allow to suspend.",
task->name, task->state);
ret = OS_FAILURE;
}
else
{ ... }详细解释:
- 条件
task->state & ~(READY | SLEEP | BLOCK):检查任务状态中是否含有除 READY、SLEEP、BLOCK 之外的位。 - 只有当任务处于 READY、SLEEP 或 BLOCK 状态时,这个条件为假,才能挂起。
- 如果任务处于 INIT、CLOSE 等状态(不在可挂起集合内),则:
- 退出临界区。
- 打印错误日志(含任务名和状态值,便于定位)。
- 返回
OS_FAILURE。
第三步:从就绪队列移除(若任务在就绪队列)
if (task->state & OS_TASK_STATE_READY)
{
k_readyq_remove(task);
task->state &= ~OS_TASK_STATE_READY;
}详细解释:
- 如果任务在就绪队列中(
state & READY),先把它从就绪队列移除,并清除 READY 状态位。 - 为什么必须移除:挂起后任务不再参与调度,若不移除,调度器仍可能选中它,违背挂起的意义。
第四步:置位 SUSPEND 状态
task->state |= OS_TASK_STATE_SUSPEND;详细解释:设置任务状态为 SUSPEND。此时任务的状态可能是:
- 纯
SUSPEND(原本就绪,已被移除就绪队列) SLEEP | SUSPEND(原本在睡眠)BLOCK | SUSPEND(原本在阻塞)
这就是挂起状态的叠加性——SUSPEND 与原有状态位并存。
第五步:是否触发调度
#ifdef OS_USING_SMP
if (task->cpu_index != -1)
{
OS_KERNEL_EXIT_SCHED();
}
#else
if (task == _k_task_self())
{
OS_KERNEL_EXIT_SCHED();
}
#endif
else
{
OS_KERNEL_EXIT();
}详细解释:
- 单核:
task == _k_task_self()表示挂起的是当前正在运行的任务(挂起自己)。此时需要OS_KERNEL_EXIT_SCHED()退出临界区并触发调度,切换到其他任务。 - SMP:
task->cpu_index != -1表示任务正运行在某个 CPU 上,同样需要触发调度。 - 否则(挂起的是其他非运行任务):
OS_KERNEL_EXIT()仅退出临界区,不触发调度。
关键点:只有挂起"正在运行的任务"(即挂起自己)才需要触发调度,因为该任务放弃 CPU 后必须让其他任务顶上;挂起其他空闲任务则不需要。
2.5 使用限制
- 不允许在中断上下文、中断禁用或调度锁定时挂起自己(这三条由断言保证)。
- 任务必须处于 READY、SLEEP 或 BLOCK 状态才能被挂起。
- 不能挂起处于 INIT 或 CLOSE 状态的任务(返回
OS_FAILURE)。
2.6 使用示例
/* 挂起另一个任务 */
os_task_id tid = os_task_get_id("worker_task");
if (tid != OS_NULL)
{
os_err_t ret = os_task_suspend(tid);
if (ret == OS_SUCCESS)
{
os_kprintf("Task suspended\r\n");
}
}
/* 任务挂起自己 */
void self_suspend_task(void *arg)
{
while (1)
{
/* 做一些工作 */
do_work();
/* 挂起自己,等待被其他任务恢复 */
os_task_suspend(os_get_current_task());
/* 被恢复后继续执行 */
os_kprintf("Task resumed!\r\n");
}
}三、os_task_resume() —— 恢复任务
3.1 函数签名
os_err_t os_task_resume(os_task_id tid);3.2 参数与返回值
| 参数 | 说明 |
|---|---|
tid | 要恢复的目标任务的任务 ID |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 恢复成功 |
OS_FAILURE | 恢复失败(任务不在 SUSPEND 状态) |
3.3 完整源码
来源: kernel/source/os_task.c
os_err_t os_task_resume(os_task_id tid)
{
os_err_t ret;
os_task_t *task;
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
OS_ASSERT(OS_KOBJ_INITED == task->object_inited);
ret = OS_SUCCESS;
OS_KERNEL_ENTER();
if ((task->state & OS_TASK_STATE_SUSPEND) == 0)
{
OS_KERNEL_EXIT();
OS_KERN_LOG(KERN_ERROR, TASK_TAG, "Task(%s) state is not suspend, not allow to resume.", task->name);
ret = OS_FAILURE;
}
else
{
task->state &= ~OS_TASK_STATE_SUSPEND;
if ((task->state & (OS_TASK_STATE_SLEEP | OS_TASK_STATE_BLOCK)) != 0)
{
/*
* When the task sleep or block, only clear the suspend state.
* In this case, the resume is also considered successful.
*/
OS_KERNEL_EXIT();
}
else
{
task->state |= OS_TASK_STATE_READY;
k_readyq_put(task);
OS_KERNEL_EXIT_SCHED();
}
}
return ret;
}3.4 分步详解
第一步:准备与参数校验
os_err_t ret;
os_task_t *task;
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
OS_ASSERT(OS_KOBJ_INITED == task->object_inited);详细解释:
| 断言/语句 | 作用 |
|---|---|
OS_KERNEL_INIT() | 确保内核已初始化 |
OS_TYPE_CONVERT(os_task_t *, tid) | 把任务 ID 转换为 os_task_t * |
OS_ASSERT(OS_NULL != task) | 任务指针非空 |
OS_ASSERT(OS_KOBJ_INITED == task->object_inited) | 任务必须处于已初始化状态 |
注意:
os_task_resume()比os_task_suspend()少了两条关于"中断/调度锁定 + 挂起自己"的断言。因为恢复任务不会触发切换当前任务上下文(os_task_resume()只能恢复其他任务,被恢复的任务不一定立即运行),所以没有这些限制。
第二步:判断任务是否处于 SUSPEND 状态(if/else 整体)
if ((task->state & OS_TASK_STATE_SUSPEND) == 0)
{
OS_KERNEL_EXIT();
OS_KERN_LOG(KERN_ERROR, TASK_TAG, "Task(%s) state is not suspend, not allow to resume.", task->name);
ret = OS_FAILURE;
}
else
{
/* ③ 清除 SUSPEND 状态 */
task->state &= ~OS_TASK_STATE_SUSPEND;
/* ④ 根据剩余状态决定是否就绪 */
if ((task->state & (OS_TASK_STATE_SLEEP | OS_TASK_STATE_BLOCK)) != 0)
{
...
}
else
{
...
}
}详细解释:本步骤是一个 if / else 结构,根据任务是否处于 SUSPEND 状态分成两个分支:
if分支(不在 SUSPEND 状态):条件(task->state & OS_TASK_STATE_SUSPEND) == 0为真,说明任务未被挂起,不能恢复。此时:- 退出临界区。
- 打印错误日志(含任务名)。
- 返回
OS_FAILURE。
else分支(处于 SUSPEND 状态):任务确实被挂起了,进入真正的恢复处理,其中又包含两个子步骤 ③④(见下)。
第三步:清除 SUSPEND 状态(else 分支的第 1 个子步骤)
task->state &= ~OS_TASK_STATE_SUSPEND;详细解释:
- 本行位于
else分支内,即只有任务确实处于 SUSPEND 状态时才会执行。 - 作用:清除任务状态的 SUSPEND 位,解除挂起。
- 注意:这一行是
else分支的第一个动作,紧随"检查是否处于 SUSPEND"之后——先确认挂起,再解除挂起。
第四步:根据剩余状态决定是否就绪(else 分支的第 2 个子步骤)
if ((task->state & (OS_TASK_STATE_SLEEP | OS_TASK_STATE_BLOCK)) != 0)
{
/* When the task sleep or block, only clear the suspend state. */
OS_KERNEL_EXIT();
}
else
{
task->state |= OS_TASK_STATE_READY;
k_readyq_put(task);
OS_KERNEL_EXIT_SCHED();
}详细解释:清除 SUSPEND 后,任务可能还残留 SLEEP/BLOCK 状态,需要根据残留状态分两种情况处理。
情况一:任务仍在 SLEEP/BLOCK 状态(SLEEP | SUSPEND 或 BLOCK | SUSPEND)
- 说明任务除了被挂起,原本还在睡眠/阻塞中。
- 此时只清除 SUSPEND 标志,不做就绪处理(源码注释明确:
When the task sleep or block, only clear the suspend state.)。 - 任务保持 SLEEP/BLOCK 状态,等延时到期或资源可用时再自动转为 READY。
- 只退出临界区,不触发调度。
情况二:任务仅处于 SUSPEND(无其他状态)
- 说明任务恢复后可以直接运行。
- 设置 READY 状态,
k_readyq_put()加入就绪队列。 OS_KERNEL_EXIT_SCHED():退出临界区并触发调度(若被恢复的任务优先级更高,会立即抢占运行)。
3.5 恢复后的行为总结
| 挂起前的状态 | 恢复后的行为 |
|---|---|
| 仅 SUSPEND | 清除 SUSPEND → 转为 READY → 加入就绪队列 → 触发调度 |
| SLEEP + SUSPEND | 清除 SUSPEND → 保留 SLEEP → 延时到期后自动 READY |
| BLOCK + SUSPEND | 清除 SUSPEND → 保留 BLOCK → 资源可用后自动 READY |
关键点:恢复任务的本质是"解除挂起",但是否立即就绪取决于任务原本还处于什么状态。只有"纯挂起"的任务才会被直接置为 READY;还处于睡眠/阻塞的任务需要等相应条件满足后才就绪。
3.6 使用示例
/* 控制任务:挂起和恢复 worker 任务 */
os_task_id worker_tid;
void controller_task(void *arg)
{
worker_tid = os_task_get_id("worker_task");
while (1)
{
/* 暂停 worker 任务 */
os_task_suspend(worker_tid);
os_kprintf("Worker paused\r\n");
/* 等待一段时间 */
os_task_msleep(5000);
/* 恢复 worker 任务 */
os_task_resume(worker_tid);
os_kprintf("Worker resumed\r\n");
os_task_msleep(5000);
}
}四、典型应用场景
4.1 任务间同步控制
使用挂起/恢复实现简单的任务间同步:
/* 生产者-消费者模式中的流控 */
os_task_id consumer_tid;
void producer_task(void *arg)
{
while (1)
{
produce_data();
/* 唤醒消费者处理数据 */
os_task_resume(consumer_tid);
}
}
void consumer_task(void *arg)
{
consumer_tid = os_get_current_task();
while (1)
{
consume_data();
/* 挂起自己,等待生产者唤醒 */
os_task_suspend(os_get_current_task());
}
}注意:对于更复杂的任务间同步,建议使用信号量或事件标志组,它们提供了更丰富的同步语义。
4.2 调试与诊断
在调试时挂起可疑任务,便于分析系统状态:
/* 调试命令:挂起指定任务 */
void debug_suspend_task(const char *name)
{
os_task_id tid = os_task_get_id(name);
if (tid != OS_NULL)
{
os_task_suspend(tid);
os_kprintf("Task '%s' suspended for debugging\r\n", name);
}
}4.3 电源管理
在低功耗场景下挂起非关键任务:
void power_save_mode(void)
{
/* 挂起所有非关键任务 */
os_task_suspend(log_task);
os_task_suspend(monitor_task);
os_task_suspend(display_task);
/* 进入低功耗模式 */
enter_low_power();
/* 唤醒后恢复任务 */
os_task_resume(log_task);
os_task_resume(monitor_task);
os_task_resume(display_task);
}五、注意事项
不要滥用挂起:挂起/恢复是一种粗暴的控制方式,可能导致数据丢失或状态不一致。对于同步需求,优先使用信号量、事件标志组等 IPC 机制。
挂起持有资源的任务:如果挂起的任务持有互斥锁或其他共享资源,可能导致其他任务阻塞甚至死锁。挂起前应确保任务已释放所有资源。
多次挂起不支持:OneOS 不支持嵌套挂起。如果对一个已经挂起的任务再次调用
os_task_suspend(),会返回OS_FAILURE。恢复未挂起的任务:如果任务不在 SUSPEND 状态,调用
os_task_resume()会返回OS_FAILURE。挂起自己:任务可以挂起自己,但挂起后立即触发调度,后续代码在任务被恢复后才继续执行。
📝 本节小结
核心要点回顾
本节围绕任务的挂起与恢复展开,核心要点如下:
1. 挂起(SUSPEND)状态
- 挂起是通过
os_task_suspend()手动暂停任务执行的机制,与 SLEEP/BLOCK(由任务自身行为触发)不同。 - SUSPEND 状态可叠加:可与 READY、SLEEP、BLOCK 状态并存。
2. os_task_suspend() 挂起任务
- 仅 READY、SLEEP、BLOCK 状态的任务可挂起;INIT/CLOSE 状态返回
OS_FAILURE。 - 挂起就绪任务时会先将其从就绪队列移除。
- 挂起"自己"会触发调度切换,因此不能在中断禁用/调度锁定状态下挂起自己。
3. os_task_resume() 恢复任务
- 仅 SUSPEND 状态的任务可恢复;否则返回
OS_FAILURE。 - 清除 SUSPEND 后,若任务仅处于 SUSPEND 则直接转 READY;若仍在 SLEEP/BLOCK 则等待条件满足。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
对非 SUSPEND 任务调用 os_task_resume() | 返回 OS_FAILURE,应确保任务确实被挂起过 |
对 INIT/CLOSE 任务调用 os_task_suspend() | 返回 OS_FAILURE,只能挂起 READY/SLEEP/BLOCK 状态任务 |
| 在中断禁用/调度锁定时挂起自己 | 触发断言,不允许(挂起自己会触发调度) |
| 认为恢复后任务一定立即运行 | 仅"纯挂起"任务恢复后直接就绪;睡眠/阻塞中的任务要等条件满足 |
| 挂起持有资源的任务 | 可能导致其他任务死锁,挂起前应确保任务已释放资源 |
| 滥用挂起做同步 | 复杂同步优先用信号量、事件标志组等 IPC 机制 |
下一步
任务挂起与恢复是一种手动控制手段。接下来请学习下一小节:
- 4.2.7 任务延时:了解任务如何通过睡眠/延时主动让出 CPU