4.2.7 任务延时
4.2.7 任务延时
📚 本节导读
学习时长: 约 10 分钟
难度级别: ⭐⭐☆☆☆
前置知识: 任务状态机、SLEEP 状态、系统时钟
🎯 学习目标
- 掌握
os_task_tsleep()基于节拍的延时 - 掌握
os_task_msleep()基于毫秒的延时 - 理解 SLEEP 状态下的任务行为
- 理解 SLEEP 与 BLOCK 的区别
一、延时机制概述
在 RTOS 中,任务经常需要等待一段时间后再继续执行。OneOS 提供了两种延时方式:
| API | 单位 | 精度 | 说明 |
|---|---|---|---|
os_task_tsleep(tick) | 系统时钟节拍 | 1 tick | 底层延时接口 |
os_task_msleep(ms) | 毫秒 | 1 tick(内部转换) | 便捷延时接口 |
延时期间,任务进入 SLEEP 状态,不再参与调度,让出 CPU 给其他就绪任务。延时到期后,内核自动将任务恢复到 READY 状态。
二、os_task_tsleep() —— 节拍延时
2.1 函数签名
os_err_t os_task_tsleep(os_tick_t tick);2.2 参数与返回值
| 参数 | 说明 |
|---|---|
tick | 延时时间,单位是系统时钟节拍。必须 < OS_TICK_MAX / 2 |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 延时成功 |
| 其他 | 延时失败(正常情况下由断言拦截非法调用) |
tick 与时间的换算:
延时时间(ms) = tick × (1000 / OS_TICK_PER_SECOND)例如,当 OS_TICK_PER_SECOND = 100 时:
os_task_tsleep(1)≈ 延时 10msos_task_tsleep(10)= 延时 100msos_task_tsleep(100)= 延时 1s
2.3 特殊值:tick = 0
当 tick = 0 时,os_task_tsleep() 的行为等同于 os_task_yield()——将当前任务移到同优先级就绪链表末尾,立即触发调度:
/* tick = 0,等同于 os_task_yield() */
os_task_tsleep(0);2.4 完整源码
来源: kernel/source/os_task.c
os_err_t os_task_tsleep(os_tick_t tick)
{
os_task_t *current_task;
OS_KERNEL_INIT();
OS_ASSERT(OS_FALSE == os_is_irq_active());
OS_ASSERT(OS_FALSE == os_is_irq_disabled());
OS_ASSERT(OS_FALSE == os_is_schedule_locked());
OS_ASSERT(tick < (OS_TICK_MAX / 2));
current_task = k_task_self();
OS_ASSERT((OS_NULL != current_task));
OS_KERNEL_ENTER();
if (tick > 0)
{
k_readyq_remove(current_task);
current_task->state &= ~OS_TASK_STATE_READY;
current_task->state |= OS_TASK_STATE_SLEEP;
k_tickq_put(current_task, tick);
OS_KERNEL_EXIT_SCHED();
}
else
{
if (OS_TRUE == k_readyq_move_tail(current_task))
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
}
return OS_SUCCESS;
}2.5 分步详解
第一步:准备与参数校验
os_task_t *current_task;
OS_KERNEL_INIT();
OS_ASSERT(OS_FALSE == os_is_irq_active());
OS_ASSERT(OS_FALSE == os_is_irq_disabled());
OS_ASSERT(OS_FALSE == os_is_schedule_locked());
OS_ASSERT(tick < (OS_TICK_MAX / 2));
current_task = k_task_self();
OS_ASSERT((OS_NULL != current_task));详细解释:
| 断言/语句 | 作用 |
|---|---|
OS_KERNEL_INIT() | 确保内核已初始化 |
OS_ASSERT(OS_FALSE == os_is_irq_active()) | 不能在中断上下文中延时 |
OS_ASSERT(OS_FALSE == os_is_irq_disabled()) | 不能在中断禁用时延时 |
OS_ASSERT(OS_FALSE == os_is_schedule_locked()) | 不能在调度器锁定时延时 |
OS_ASSERT(tick < (OS_TICK_MAX / 2)) | 延时 tick 值不能超过系统时钟计数范围的一半(详见下方"为什么") |
current_task = k_task_self() | 获取当前正在运行的任务 |
OS_ASSERT(OS_NULL != current_task) | 当前任务不能为空(内核必须已开始调度) |
这条断言到底是干什么的?为什么 tick 必须 < OS_TICK_MAX / 2?
要理解它,需要知道内核怎么判断"延时到期"。结合 2.5 节第二步和 tick 中断逻辑:
- 任务延时后,内核记录它的绝对到期时刻:
tick_absolute = gs_os_tick + tick(见k_tickq_put())。 - 每个 tick 中断,内核用
tick_absolute - gs_os_tick的差值判断是否到期:差值 > 0 表示还没到期,≤ 0 表示已到期(见os_tick_increase())。
问题出在"时钟回绕"(wrap around)上:
gs_os_tick 是一个有最大值的计数器(OS_TICK_MAX),它会一直递增,直到最大值后回到 0 重新开始,就像时针转一圈回到 12 点。
gs_os_tick 从 0 一直涨到 OS_TICK_MAX,然后回绕成 0,再继续涨……当它快满时,gs_os_tick + tick 可能超过最大值而溢出回绕。这时"绝对到期时刻"和"当前时刻"的差值就可能判断错误。
为什么是"一半"这个界限?
用时钟表盘来理解:一圈是 12 小时,时针走到 12 点后会回到 0 点继续走。
假设现在是 11 点,要给一个任务设置闹钟:
- 超时 3 小时:到期时刻 = 11 + 3 = 14 → 表盘上回绕成 2 点。从 11 点走到 2 点,确实过去了 3 小时,判断准确。
- 超时 8 小时(超过半圈 6 小时):到期时刻 = 11 + 8 = 19 → 表盘上回绕成 7 点。但"11 点 → 7 点"这个差值是 8(向前),还是 4(向后)?分不清了!可能是"过了 8 小时",也可能是"往回退了 4 小时"。
关键结论:表盘上的"时针位置差",只有在不超过半圈(6 小时)时,才能唯一确定真正过了多久。一旦超过半圈,就产生了歧义,无法判断是"向前走了这么久"还是"向后退了那么久"。
这个道理完全适用于 tick 计数器:tick_absolute - gs_os_tick 的差值,只有小于 OS_TICK_MAX / 2(半圈)时才含义唯一。因此延时 tick 必须 < OS_TICK_MAX / 2。
结论:OS_ASSERT(tick < (OS_TICK_MAX / 2)) 是在防止延时时间过长,导致绝对到期时刻在时钟回绕后无法被正确判断。实际开发中延时时间远小于这个上限,一般不会触发。
关键点:延时会让当前任务进入 SLEEP 并触发调度切换,因此和 os_task_yield() 一样,不允许在中断上下文、中断禁用、调度锁定时调用(这些场景无法安全切换上下文)。
第二步:tick > 0 —— 真正进入延时(SLEEP)
if (tick > 0)
{
k_readyq_remove(current_task);
current_task->state &= ~OS_TASK_STATE_READY;
current_task->state |= OS_TASK_STATE_SLEEP;
k_tickq_put(current_task, tick);
OS_KERNEL_EXIT_SCHED();
}详细解释:
| 代码 | 作用 |
|---|---|
k_readyq_remove(current_task) | 把当前任务从就绪队列移除(延时期间不再参与调度) |
current_task->state &= ~OS_TASK_STATE_READY | 清除 READY 状态位 |
current_task->state |= OS_TASK_STATE_SLEEP | 置位 SLEEP 状态 |
k_tickq_put(current_task, tick) | 把任务加入节拍(延时)队列,登记到期时间 |
OS_KERNEL_EXIT_SCHED() | 退出临界区并触发调度,切换到其他就绪任务 |
关键点:
- 任务从"READY"转为"SLEEP",并挂到节拍队列。此后它不再被调度器选中,直到延时到期。
- 延时到期后,由 tick 中断(
os_tick_increase())把任务从节拍队列取出、恢复 READY、重新加入就绪队列(见 4.2.5 的 1.2 节)。
第三步:tick = 0 —— 等同于让出 CPU
else
{
if (OS_TRUE == k_readyq_move_tail(current_task))
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
}
return OS_SUCCESS;详细解释:
tick = 0时不进入 SLEEP,而是把当前任务移到同优先级就绪链表尾部(k_readyq_move_tail),效果等同于os_task_yield()。- 返回
OS_TRUE:存在同优先级任务,触发调度,切换到下一个同优先级任务。 - 返回
OS_FALSE:就绪链表里只有当前任务自己,不触发调度,继续运行。
2.6 使用限制
- 不允许在中断上下文中调用
- 不允许在中断禁用时调用
- 不允许在调度器锁定时调用
tick必须 <OS_TICK_MAX / 2
2.7 使用示例
void blink_task(void *arg)
{
while (1)
{
led_on();
os_task_tsleep(50); /* 延时 50 tick(500ms,假设 100Hz) */
led_off();
os_task_tsleep(50); /* 延时 50 tick */
}
}三、os_task_msleep() —— 毫秒延时
3.1 函数签名
os_err_t os_task_msleep(uint32_t ms);3.2 参数与返回值
| 参数 | 说明 |
|---|---|
ms | 延时时间,单位是毫秒 |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 延时成功 |
| 其他 | 延时失败(实际由 os_task_tsleep() 的断言拦截非法调用) |
3.3 完整源码
来源: kernel/source/os_task.c
os_task_msleep() 是对 os_task_tsleep() 的封装,内部将毫秒转换为 tick:
os_err_t os_task_msleep(uint32_t ms)
{
os_tick_t tick;
os_err_t ret;
tick = os_tick_from_ms(ms);
ret = os_task_tsleep(tick);
return ret;
}3.4 分步详解
第一步:毫秒转换为 tick
tick = os_tick_from_ms(ms);详细解释:
os_tick_from_ms(ms)是毫秒到 tick 的转换函数,内部根据OS_TICK_PER_SECOND换算。- 由于毫秒不一定能被一个 tick 整除,转换结果会向上取整(见 3.5 精度说明),保证实际延时 ≥ 请求延时。
第二步:调用节拍延时接口
ret = os_task_tsleep(tick);
return ret;详细解释:
- 把转换好的
tick传给os_task_tsleep(),由它完成真正的睡眠逻辑(移除就绪队列、置 SLEEP、加入节拍队列、触发调度,见 2.5 节)。 os_task_tsleep()的断言(中断上下文、中断禁用、调度锁定等限制)在此一并生效。- 把返回结果
ret原样返回给调用者。
3.5 精度说明
os_task_msleep() 的精度受限于系统时钟节拍。例如,当 OS_TICK_PER_SECOND = 100(即 10ms/tick)时:
| 请求延时 | 实际 tick 数 | 实际延时 |
|---|---|---|
| 1ms | 1 tick | 10ms |
| 5ms | 1 tick | 10ms |
| 10ms | 1 tick | 10ms |
| 15ms | 2 tick | 20ms |
| 50ms | 5 tick | 50ms |
注意:实际延时时间始终 ≥ 请求的延时时间,最小延时单位为 1 个 tick。
3.6 使用示例
void sensor_task(void *arg)
{
while (1)
{
/* 读取传感器数据 */
read_sensor();
/* 延时 500ms */
os_task_msleep(500);
}
}四、SLEEP 状态详解
4.1 SLEEP 状态的特点
| 特性 | 说明 |
|---|---|
| 触发方式 | 调用 os_task_tsleep() 或 os_task_msleep() |
| 恢复方式 | 延时到期后自动恢复为 READY 状态 |
| 调度影响 | 任务从就绪队列移除,不参与调度 |
| 可叠加 SUSPEND | ✅ 是。延时期间可被挂起,恢复后需同时满足两个条件 |
4.2 SLEEP 期间的时间线
时间轴(tick):
────────────────────────────────────────────────────►
0 10 20 30 40
任务 A
│ 调用 tsleep(20)
│ 进入 SLEEP
▼
├──── SLEEP 状态 ────┤
│ 延时到期
│ 自动恢复 READY
▼
├── 等待调度 ──┤
│ 被调度选中
│ 继续运行
▼4.3 SLEEP + SUSPEND 叠加
如果任务在延时期间被挂起(os_task_suspend()),任务状态变为 SLEEP | SUSPEND。此时:
- 延时到期后,任务仍处于 SUSPEND 状态,不会自动回到 READY
- 只有当
os_task_resume()被调用后,任务才会变为 READY 状态
/* 场景:延时期间被挂起 */
void task_a(void *arg)
{
os_task_tsleep(100); /* 延时 100 tick */
/* 如果延时期间被挂起,这里不会执行 */
/* 直到被 resume 后才继续 */
}五、SLEEP 与 BLOCK 的区别
| 维度 | SLEEP | BLOCK |
|---|---|---|
| 触发原因 | 主动延时等待 | 等待 IPC 资源(信号量、互斥锁等) |
| 恢复条件 | 延时到期自动恢复 | 资源可用时自动恢复 |
| 超时机制 | 本身就是超时 | 通常支持超时参数(如 os_sem_wait(sem, timeout)) |
| 对应 API | os_task_tsleep() / os_task_msleep() | os_sem_wait() / os_mutex_lock() 等 |
| 队列位置 | 延时队列(按到期时间排序) | IPC 对象的阻塞队列 |
六、延时 API 的最佳实践
6.1 推荐使用 os_task_msleep()
对于大多数应用场景,推荐使用 os_task_msleep() 而非 os_task_tsleep():
- 代码可读性更好(
os_task_msleep(100)比os_task_tsleep(10)更直观) - 不依赖具体的
OS_TICK_PER_SECOND配置 - 移植性更好
6.2 避免极短延时
由于延时的最小精度受限于 tick,避免使用极短的延时值:
/* 不推荐:OS_TICK_PER_SECOND=100 时,实际延时 10ms,不是 1ms */
os_task_msleep(1);
/* 推荐:如果确实需要短延时,考虑使用硬件定时器或忙等待 */6.3 延时作为等待机制
对于简单的周期性任务,延时是合适的等待方式。但对于需要响应外部事件的任务,应使用 IPC 机制(信号量、事件标志组等)配合超时参数,而不是轮询 + 延时:
/* 不推荐:轮询方式 */
while (1)
{
if (data_ready())
{
process_data();
}
os_task_msleep(10); /* 浪费 CPU 且响应延迟大 */
}
/* 推荐:事件驱动方式 */
while (1)
{
os_sem_wait(data_sem, OS_WAIT_FOREVER); /* 阻塞等待,零延迟 */
process_data();
}📝 本节小结
核心要点回顾
本节围绕任务延时展开,核心要点如下:
1. 两种延时接口
| API | 单位 | 精度 | 说明 |
|---|---|---|---|
os_task_tsleep(tick) | 系统时钟节拍 | 1 tick | 底层延时接口 |
os_task_msleep(ms) | 毫秒 | 1 tick(内部转换) | 便捷封装,内部调用 os_task_tsleep() |
2. SLEEP 状态
- 延时期间任务进入 SLEEP 状态,从就绪队列移除,不再参与调度,让出 CPU。
- 延时到期后,由 tick 中断自动恢复为 READY。
3. 特殊值 tick = 0
os_task_tsleep(0)等同于os_task_yield():把当前任务移到同优先级就绪链表尾部,不进入 SLEEP。
4. SLEEP 与 BLOCK 的区别
- SLEEP:主动等待时间,到期自动恢复。
- BLOCK:被动等待 IPC 资源,资源可用时恢复。
5. 使用限制
- 不允许在中断上下文、中断禁用、调度锁定时调用。
tick必须 <OS_TICK_MAX / 2。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 在中断/关中断/调度锁定时调用延时 | 触发断言,不允许(延时需要切换上下文) |
| 期望毫秒级高精度延时 | 延时精度受限于 tick,OS_TICK_PER_SECOND=100 时最小 10ms |
| 用短延时实现精确时序 | 实际延时 ≥ 请求延时(向上取整),不适合精确时序 |
| 用轮询 + 延时等待外部事件 | 应使用 IPC(信号量/事件)配合超时参数,避免浪费 CPU |
混淆 tsleep 和 msleep | tsleep 是 tick 单位,msleep 是毫秒单位(内部转 tick) |
下一步
任务延时是任务让出 CPU 的方式之一。接下来请学习下一小节:
- 4.2.8 调度器锁定:了解如何临时禁止任务调度,保护临界区