4.2.5 任务时间片
4.2.5 任务时间片
📚 本节导读
学习时长: 约 15 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: 任务调度机制、优先级概念
🎯 学习目标
- 理解时间片的概念和作用
- 掌握同优先级任务的时间片轮转调度原理
- 掌握
os_task_set_time_slice()和os_task_get_time_slice()等 API - 了解时间片的 Kconfig 配置
一、时间片概念
1.1 什么是时间片?
时间片(Time Slice)是分配给同优先级任务的一个 CPU 时间配额。当一个任务的时间片用完时,调度器会将其移到同优先级就绪链表的末尾,让下一个同优先级任务运行。
时间片的单位是系统时钟节拍(tick)。例如,如果 OS_TICK_PER_SECOND = 100,一个时钟节拍就是 10ms,那么时间片值为 10 就表示 100ms 的 CPU 时间。
1.2 系统时钟节拍(tick)中断
时间片的递减是在 tick 中断里完成的,所以要先理解 tick 中断是什么。
什么是 tick 中断?
系统时钟节拍(tick)是由硬件定时器周期性产生的中断,它是 RTOS 的"心跳"。在 ARM Cortex-M 上,通常由 SysTick 定时器产生,每 1/OS_TICK_PER_SECOND 秒触发一次。例如 OS_TICK_PER_SECOND = 100 时,每 10ms 触发一次 tick 中断。
每次 tick 中断,内核都会执行 os_tick_increase()(kernel/source/os_clock.c),它主要做三件事:
SysTick 定时器周期触发
│
▼
os_tick_increase() ──► ① gs_os_tick++(系统时钟加 1)
│
├──► ② 处理节拍队列:检查是否有睡眠任务到期,到期则唤醒
│ (同时清除睡眠/阻塞状态,重新加入就绪队列)
│
└──► ③ 处理时间片:当前任务 remaining_time_slice--,
减到 0 则触发时间片轮转(见 2.3 节)tick 中断与时间片的关系:
| 方面 | 说明 |
|---|---|
| 时间基准 | 时间片的单位是 tick,一个 tick 就是一次 tick 中断的间隔 |
| 递减时机 | remaining_time_slice 的递减发生在每个 tick 中断里(每次减 1) |
| 轮转触发 | 当递减到 0 时,在 tick 中断里触发时间片轮转 |
| 睡眠唤醒 | tick 中断还负责唤醒延时到期的睡眠任务 |
关键点:
- tick 中断是周期性、高频的中断(每毫秒到几十毫秒一次),所以它的处理函数必须尽量精简高效。
- 时间片的精度完全取决于 tick 中断的频率(
OS_TICK_PER_SECOND)——时间片轮转无法做到比一个 tick 更细。
1.3 时间片的作用
同优先级就绪队列 (优先级 5):
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 任务 A │───►│ 任务 B │───►│ 任务 C │
│ 时间片:10│ │ 时间片:5 │ │ 时间片:10│
└─────────┘ └─────────┘ └─────────┘
│ │ │
▼ ▼ ▼
运行 10 tick 运行 5 tick 运行 10 tick
│ │ │
└──────────────┴──────────────┘
时间片用完,轮转到下一个时间片机制确保了同优先级任务之间的公平性——每个任务都能获得 CPU 时间,不会出现某个任务独占 CPU 导致其他同优先级任务饥饿的情况。
二、时间片的配置
2.1 Kconfig 配置
在 kernel/Kconfig 中,时间片的默认值通过 OS_SCHEDULE_TIME_SLICE 配置:
config OS_SCHEDULE_TIME_SLICE
int "Task time slice(unit: tick)"
range 1 100
default 10| 配置项 | 默认值 | 范围 | 说明 |
|---|---|---|---|
OS_SCHEDULE_TIME_SLICE | 10 | 1 ~ 100 | 新创建任务的默认时间片(单位:tick) |
2.2 时间片在 TCB 中的存储
每个任务的 TCB 中维护两个时间片相关字段:
os_tick_t time_slice; /* 任务的总时间片(每次启动时重置为此值) */
os_tick_t remaining_time_slice; /* 当前剩余时间片(随运行递减) */任务创建时,两个字段都被初始化为 OS_SCHEDULE_TIME_SLICE。每次任务被调度运行时,remaining_time_slice 从 time_slice 开始递减。当 remaining_time_slice 减到 0 时,触发时间片轮转。
2.3 时间片轮转的完整触发过程
触发位置:时间片轮转是在系统时钟节拍(tick)中断中完成的。每来一个 tick 中断,内核就执行一次 os_tick_increase(),检查当前任务的时间片,一旦耗尽就触发轮转。
完整源码(os_tick_increase(),kernel/source/os_clock.c,与项目一致):
void os_tick_increase(void)
{
os_ubase_t bidx;
const os_list_node_t *tickq_head;
struct os_task *iter_task;
os_bool_t is_sched;
os_task_t *current_task;
OS_KERNEL_INIT();
#ifdef OS_USING_SMP
int32_t i;
#endif
current_task = k_task_self();
/*The operating system is not running TODO:*/
if (OS_NULL == current_task)
{
return;
}
is_sched = OS_FALSE;
OS_KERNEL_ENTER();
gs_os_tick++;
/* Process tick queue */
bidx = gs_os_tick & (TICK_Q_BUCKETS - 1U);
tickq_head = &gs_os_tickq_bucket[bidx];
while (1)
{
if (os_list_empty(tickq_head))
{
break;
}
iter_task = os_list_first_entry((tickq_head), os_task_t, tick_node);
/* No sleeping task need to be waked */
if ((os_base_t)(iter_task->tick_absolute - gs_os_tick) > 0)
{
break;
}
k_tickq_remove(iter_task);
iter_task->state &= ~OS_TASK_STATE_SLEEP;
if ((iter_task->state & OS_TASK_STATE_BLOCK) != 0)
{
os_list_del(&iter_task->task_node);
iter_task->state &= ~OS_TASK_STATE_BLOCK;
iter_task->block_list_head = OS_NULL;
iter_task->switch_retval = OS_TIMEOUT;
}
/* The task state may be suspend, empty or running */
if (OS_TASK_STATE_SUSPEND != iter_task->state)
{
iter_task->state |= OS_TASK_STATE_READY;
k_readyq_put(iter_task);
is_sched = OS_TRUE;
}
OS_KERNEL_EXIT();
OS_KERNEL_ENTER();
}
#ifdef OS_USING_SMP
for (i = 0; i < OS_SMP_MAX_CPUS; i++)
{
current_task = g_os_current_task[i];
/*The operating system is not running TODO:*/
if (OS_NULL == current_task)
{
continue;
}
current_task->remaining_time_slice--;
if (current_task->remaining_time_slice == 0)
{
current_task->remaining_time_slice = current_task->time_slice;
if ((current_task->state & OS_TASK_STATE_READY) != 0)
{
if (OS_TRUE == k_readyq_move_tail(current_task))
{
is_sched = OS_TRUE;
}
}
}
}
#else
/* Process time slice */
g_os_current_task->remaining_time_slice--;
if (!g_os_current_task->remaining_time_slice)
{
g_os_current_task->remaining_time_slice = g_os_current_task->time_slice;
if (g_os_current_task->state & OS_TASK_STATE_READY)
{
if (OS_TRUE == k_readyq_move_tail(current_task))
{
is_sched = OS_TRUE;
}
}
}
#endif
#ifdef OS_USING_HASH_BUCKET_TIMER
/* Is need timer handle.*/
if (k_timer_need_handle())
{
is_sched = OS_TRUE;
}
/* Move gs_os_timer_list_current forward one step. */
k_move_timer_list_one_step();
#endif
#ifdef OS_USING_SINGLE_LIST_TIMER
#ifdef OS_HARD_SINGLE_LIST_TIMER
OS_KERNEL_EXIT();
k_single_list_timer_tick_proc();
OS_KERNEL_ENTER();
#endif
#ifdef OS_SOFT_SINGLE_LIST_TIMER
if (k_single_list_timer_need_handle())
{
is_sched = OS_TRUE;
}
#endif
#endif
if (is_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
return;
}说明:os_tick_increase() 在 tick 中断里主要做三件大事(从上到下依次):
os_tick_increase()(tick 中断执行)
│
▼
① 处理节拍队列(while 循环)
└─ 唤醒睡眠/延时到期的任务,重新加入就绪队列
│
▼
② 处理时间片(时间片轮转核心)
└─ 当前任务 remaining_time_slice--
└─ 减到 0 → 重置 + k_readyq_move_tail 轮转
│
▼
③ 处理软件定时器(#ifdef 部分)
└─ 检查是否有定时器需要处理
│
▼
if (is_sched) → OS_KERNEL_EXIT_SCHED()(退出中断时触发调度)
else → OS_KERNEL_EXIT()其中 ②处理时间片 是时间片轮转的核心。下面以单核(非 SMP) 路径为准,详细讲解时间片部分从"递减"到"触发轮转"的完整过程。
触发过程的前置:谁是被递减的"当前任务"?
current_task = k_task_self(); /* 获取当前正在运行的任务 */
/*The operating system is not running TODO:*/
if (OS_NULL == current_task)
{
return; /* 内核尚未运行,直接返回 */
}详细解释:
k_task_self()返回当前正在 CPU 上运行的任务。在单核模式下它就是g_os_current_task。- 如果返回
OS_NULL(系统还没开始调度任何任务),则直接返回,不做任何处理。 - 后续时间片处理的
g_os_current_task->remaining_time_slice--就是对这个当前任务递减。
触发过程分 4 步
下面的 4 步代码片段取自 os_tick_increase() 的单核(非 SMP)时间片处理部分(#else 分支)。
第 1 步:递减剩余时间片
/* Process time slice */
g_os_current_task->remaining_time_slice--;详细解释:每个 tick 中断,把当前任务的 remaining_time_slice 减 1。例如初始为 10,第 1 个 tick 后变为 9。
第 2 步:判断是否耗尽
if (!g_os_current_task->remaining_time_slice)详细解释:!remaining_time_slice 即"剩余时间片为 0 时为真"。只有减到 0(耗尽)才进入后面的轮转处理;没耗尽则本 tick 到此结束,不做任何轮转。
第 3 步:重置剩余时间片
{
g_os_current_task->remaining_time_slice = g_os_current_task->time_slice;详细解释:确认耗尽后,先把 remaining_time_slice 重置为 time_slice(总时间片),为任务下一轮运行做准备。注意:这里重置的是"下一轮"用的值,不改变当前这次轮转。
第 4 步:触发轮转
if (g_os_current_task->state & OS_TASK_STATE_READY)
{
if (OS_TRUE == k_readyq_move_tail(current_task))
{
is_sched = OS_TRUE;
}
}
}详细解释:
- 外层
if (state & OS_TASK_STATE_READY):先确认任务还在就绪队列。如果任务已被阻塞/睡眠,则不做轮转(已不在就绪链表里,无需移动)。 k_readyq_move_tail(current_task):把当前任务移到同优先级就绪链表尾部。- 返回
OS_TRUE:存在同优先级任务,移动成功,置is_sched = OS_TRUE(触发调度)。 - 返回
OS_FALSE:就绪链表里只有它自己,无需轮转,不置is_sched。
4 步完整代码(连起来看):
/* Process time slice */
g_os_current_task->remaining_time_slice--;
if (!g_os_current_task->remaining_time_slice)
{
g_os_current_task->remaining_time_slice = g_os_current_task->time_slice;
if (g_os_current_task->state & OS_TASK_STATE_READY)
{
if (OS_TRUE == k_readyq_move_tail(current_task))
{
is_sched = OS_TRUE;
}
}
}触发后的收尾:如何真正切换任务?
if (is_sched)
{
OS_KERNEL_EXIT_SCHED(); /* 退出临界区,并在退出时触发一次调度 */
}
else
{
OS_KERNEL_EXIT(); /* 仅退出临界区,不触发调度 */
}详细解释:
is_sched是一个累计标志,只要前面的节拍队列、时间片、定时器任一环节置了OS_TRUE,说明就绪队列的最高优先级任务可能发生了变化。OS_KERNEL_EXIT_SCHED():退出临界区并触发调度。此时调度器会重新选最高优先级任务,若时间片轮转把当前任务移到了链表尾部,调度器就会切换到下一个同优先级任务。OS_KERNEL_EXIT():仅退出临界区,不触发调度(本次 tick 没有需要重新调度的事件)。
关键点:
- 步骤 ④ 用
if (g_os_current_task->state & OS_TASK_STATE_READY)先判断任务是否还在就绪队列。如果任务已被阻塞/睡眠,就不做轮转。 k_readyq_move_tail()返回OS_TRUE才置is_sched = OS_TRUE;返回OS_FALSE(就绪链表里只有它自己)则不触发。- SMP 差异:多核模式下会遍历每个 CPU 的当前任务(
for (i = 0; i < OS_SMP_MAX_CPUS; i++)),对每个 CPU 上运行的任务分别递减时间片、分别判断轮转,因此每个核上的任务时间片是独立计算的。
为什么时间片精度受限于 tick?
因为时间片的递减只在 tick 中断里发生(每 tick 减 1)。所以时间片的最小单位就是一个 tick,无法做到比一个 tick 更细的轮转粒度。时间片轮转的精度取决于 OS_TICK_PER_SECOND(每秒 tick 数)。
三、时间片 API
3.1 os_task_set_time_slice() —— 设置时间片
os_err_t os_task_set_time_slice(os_task_id tid, os_tick_t new_time_slice);| 参数 | 说明 |
|---|---|
tid | 目标任务的任务 ID |
new_time_slice | 新的时间片值,必须 > 0 且 < 100 |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 设置成功 |
完整源码(kernel/source/os_task.c,与项目一致):
os_err_t os_task_set_time_slice(os_task_id tid, os_tick_t new_time_slice)
{
os_task_t *task;
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(task != OS_NULL);
OS_ASSERT(task->object_inited == OS_KOBJ_INITED);
OS_ASSERT((new_time_slice > 0) && (new_time_slice < 100));
OS_KERNEL_ENTER();
task->time_slice = new_time_slice;
if ((task->state & OS_TASK_STATE_INIT) != 0)
{
task->remaining_time_slice = new_time_slice;
}
OS_KERNEL_EXIT();
return OS_SUCCESS;
}分步详解:
| 代码 | 作用 |
|---|---|
OS_ASSERT(task != OS_NULL) | 任务指针非空 |
OS_ASSERT(task->object_inited == OS_KOBJ_INITED) | 任务必须已初始化 |
OS_ASSERT((new_time_slice > 0) && (new_time_slice < 100)) | 时间片必须在 1~99 范围内(>0 且 <100) |
task->time_slice = new_time_slice | 核心:更新总时间片字段 |
if ((task->state & OS_TASK_STATE_INIT) != 0) | 若任务处于 INIT 状态(尚未启动) |
task->remaining_time_slice = new_time_slice | 同时更新剩余时间片 |
关键点 —— 为什么只在 INIT 状态才更新 remaining_time_slice?
remaining_time_slice表示任务本次运行时还剩下多少时间。- 如果任务已经启动运行(非 INIT),它的
remaining_time_slice正在随运行递减,此时不能强行改成新值——否则会打乱正在进行的轮转计时。 - 所以只有任务尚未启动(INIT)时,才会把
remaining_time_slice一起设置为新时间片。任务下次被调度时,会从新的time_slice开始重新计时。
/* 为任务设置 5 个 tick 的时间片 */
os_task_set_time_slice(tid, 5);3.2 os_task_get_time_slice() —— 获取时间片
os_err_t os_task_get_time_slice(os_task_id tid, os_tick_t *time_slice);获取任务的总时间片(即 TCB 中 time_slice 字段的值)。
| 参数 | 说明 |
|---|---|
tid | 目标任务的任务 ID |
time_slice | 传出参数:用于接收任务的总时间片值 |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 获取成功 |
完整源码(kernel/source/os_task.c,与项目一致):
os_err_t os_task_get_time_slice(os_task_id tid, os_tick_t *time_slice)
{
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(time_slice != OS_NULL);
*time_slice = task->time_slice;
return OS_SUCCESS;
}分步详解:
第 1 步:转换任务 ID
task = OS_TYPE_CONVERT(os_task_t *, tid);详细解释:把用户传入的 os_task_id(即 TCB 指针)转换为内核内部的 os_task_t *,便于访问 TCB 字段。
第 2 步:参数校验(三条断言)
OS_ASSERT(task != OS_NULL);
OS_ASSERT(task->object_inited == OS_KOBJ_INITED);
OS_ASSERT(time_slice != OS_NULL);详细解释:
| 断言 | 作用 |
|---|---|
OS_ASSERT(task != OS_NULL) | 任务指针非空,防止访问空指针 |
OS_ASSERT(task->object_inited == OS_KOBJ_INITED) | 任务必须处于已初始化状态,防止访问未初始化/已销毁的任务 |
OS_ASSERT(time_slice != OS_NULL) | 传出参数指针非空,防止把结果写入非法地址(这是最常见的错误之一) |
关键点:time_slice 是传出参数(out 参数),函数通过指针把结果"写回"给调用者。因此它必须是有效的可写地址,OS_ASSERT(time_slice != OS_NULL) 就是为了拦截"传入空指针"的错误。
第 3 步:写入总时间片
*time_slice = task->time_slice;
return OS_SUCCESS;详细解释:
*time_slice = task->time_slice:把任务 TCB 中的time_slice字段(总时间片)写入time_slice指向的变量。return OS_SUCCESS:返回成功。
关键点 —— 获取的是"总时间片",不是"剩余时间片":
- 本函数返回
task->time_slice(总时间片),即任务每次被调度运行时从头开始计数的值。 - 若想获取当前剩余时间片(正在递减的值),应使用 3.3 的
os_task_get_remaining_time_slice()。 - 两者对比:
| 函数 | 读取字段 | 含义 |
|---|---|---|
os_task_get_time_slice() | task->time_slice | 总时间片(固定值,轮转重置目标) |
os_task_get_remaining_time_slice() | task->remaining_time_slice | 剩余时间片(动态递减) |
使用示例:
os_tick_t slice;
os_task_get_time_slice(tid, &slice);
os_kprintf("Time slice: %lu ticks\r\n", slice);典型应用场景:
- 调试时查看任务被分配的时间片配额。
- 配合 3.3 的剩余时间片,打印"剩余/总"时间片占比,观察同优先级任务的轮转情况(见 3.4 节示例)。
3.3 os_task_get_remaining_time_slice() —— 获取剩余时间片
os_err_t os_task_get_remaining_time_slice(os_task_id tid, os_tick_t *time_slice);获取任务当前剩余的可用时间片(即 TCB 中 remaining_time_slice 字段的值)。
| 参数 | 说明 |
|---|---|
tid | 目标任务的任务 ID |
time_slice | 传出参数:用于接收任务的剩余时间片值 |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 获取成功 |
完整源码(kernel/source/os_task.c,与项目一致):
os_err_t os_task_get_remaining_time_slice(os_task_id tid, os_tick_t *time_slice)
{
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(time_slice != OS_NULL);
*time_slice = task->remaining_time_slice;
return OS_SUCCESS;
}分步详解:
第 1 步:转换任务 ID
task = OS_TYPE_CONVERT(os_task_t *, tid);详细解释:把用户传入的 os_task_id(即 TCB 指针)转换为内核内部的 os_task_t *,便于访问 TCB 字段。
第 2 步:参数校验(三条断言)
OS_ASSERT(task != OS_NULL);
OS_ASSERT(task->object_inited == OS_KOBJ_INITED);
OS_ASSERT(time_slice != OS_NULL);详细解释:
| 断言 | 作用 |
|---|---|
OS_ASSERT(task != OS_NULL) | 任务指针非空,防止访问空指针 |
OS_ASSERT(task->object_inited == OS_KOBJ_INITED) | 任务必须处于已初始化状态,防止访问未初始化/已销毁的任务 |
OS_ASSERT(time_slice != OS_NULL) | 传出参数指针非空,防止把结果写入非法地址 |
第 3 步:写入剩余时间片
*time_slice = task->remaining_time_slice;
return OS_SUCCESS;详细解释:
*time_slice = task->remaining_time_slice:把任务 TCB 中的remaining_time_slice字段(剩余时间片)写入time_slice指向的变量。return OS_SUCCESS:返回成功。
关键点 —— remaining_time_slice 是一个动态变化的字段:
与 3.2 的 time_slice(固定配额)不同,remaining_time_slice 会随任务运行不断递减:
- 任务每次被调度运行时,
remaining_time_slice从time_slice开始,每个 tick 中断减 1(见 2.3 节)。 - 递减到 0 时触发时间片轮转,并把
remaining_time_slice重置为time_slice(为下一轮做准备)。 - 因此本函数每次返回的值都不同——它反映的是任务"此刻还剩下多少时间片"。
总时间片与剩余时间片对比(呼应 3.2 节):
| 函数 | 读取字段 | 值是否变化 | 含义 |
|---|---|---|---|
os_task_get_time_slice() | task->time_slice | 固定(配额) | 任务每次运行从头计数的总时间片 |
os_task_get_remaining_time_slice() | task->remaining_time_slice | 动态递减 | 任务当前还剩多少时间片 |
使用示例:
os_tick_t remaining;
os_task_get_remaining_time_slice(tid, &remaining);
os_kprintf("Remaining time slice: %lu ticks\r\n", remaining);典型应用场景:
- 监控任务时间片的消耗情况,判断任务是否即将耗尽时间片、触发轮转。
- 配合 3.2 的总时间片,打印"剩余/总"占比,直观观察同优先级任务的轮转进度(见 3.4 节示例)。
3.4 使用示例
下面演示如何为不同任务分配不同的时间片,并监控它们的剩余时间片:
void monitor_task(void *arg)
{
os_task_id tid_a = os_task_get_id("task_a");
os_task_id tid_b = os_task_get_id("task_b");
os_tick_t slice_a, slice_b;
/* 为 task_a 分配更多 CPU 时间 */
os_task_set_time_slice(tid_a, 20); /* 20 tick(假设 tick=100Hz,即 200ms) */
/* 为 task_b 分配较少 CPU 时间 */
os_task_set_time_slice(tid_b, 5); /* 5 tick(即 50ms) */
while (1)
{
os_tick_t rem_a, rem_b;
/* 获取剩余时间片 */
os_task_get_remaining_time_slice(tid_a, &rem_a);
os_task_get_remaining_time_slice(tid_b, &rem_b);
/* 获取总时间片 */
os_task_get_time_slice(tid_a, &slice_a);
os_task_get_time_slice(tid_b, &slice_b);
os_kprintf("A: %lu/%lu B: %lu/%lu\r\n",
rem_a, slice_a, rem_b, slice_b);
os_task_msleep(1000);
}
}说明:上述示例修正了"剩余时间片/总时间片"的完整获取方式。通过同时调用
os_task_get_remaining_time_slice()和os_task_get_time_slice(),即可打印任务的"剩余/总"时间片占比,便于观察同优先级任务的轮转情况。
四、时间片与优先级的关系
4.1 优先级优先
优先级抢占的优先级(pun intended)高于时间片轮转。具体表现为:
- 只要有更高优先级的任务就绪,当前任务会被立即抢占,无论其时间片是否用完
- 时间片轮转仅在同优先级任务之间起作用
- 不同优先级的任务之间不存在时间片轮转
4.2 调度决策流程
系统时钟节拍中断
│
▼
┌─────────────┐
│ 时间片递减 │
│ remaining-- │
└──────┬──────┘
│
remaining == 0?
│
┌────┴────┐
│ 是 │ 否
▼ ▼
┌──────────┐ ┌──────────┐
│ 移到同优先级│ │ 检查是否有 │
│ 队列尾部 │ │ 更高优先级 │
│ 设置同优先级│ │ 任务就绪 │
│ 调度标记 │ └─────┬────┘
└─────┬────┘ │
│ ┌────┴────┐
│ │ 是 │ 否
│ ▼ ▼
│ ┌────────┐ ┌────────┐
│ │ 触发抢占│ │ 继续运行│
│ └────────┘ └────────┘
│
▼
┌──────────┐
│ 触发调度 │
└──────────┘说明:时间片耗尽的处理源码、分步详解与演算示例,已在 2.3 节"时间片轮转的完整触发过程" 中完整讲解(含
os_tick_increase()的时间片处理源码、4 步触发过程、收尾调度)。此处不再重复。
五、时间片配置建议
| 场景 | 建议时间片 | 说明 |
|---|---|---|
| 快速响应任务 | 1 ~ 3 tick | 需要快速轮转,如多个同优先级 IO 处理任务 |
| 一般计算任务 | 5 ~ 10 tick | 默认配置,平衡性能和响应 |
| 批量处理任务 | 10 ~ 50 tick | 减少切换开销,提高吞吐量 |
| 极长批处理 | 50 ~ 100 tick | 大块数据处理,但需注意影响同优先级任务响应 |
注意:时间片设置过小会导致频繁的任务切换,增加上下文切换开销;时间片设置过大可能导致同优先级任务响应变慢。
📝 本节小结
核心要点回顾
本节围绕任务时间片展开,核心要点如下:
1. 时间片的概念
- 时间片是分配给同优先级任务的 CPU 时间配额,单位是系统时钟节拍(tick)。
- 由 TCB 中的
time_slice(总时间片)和remaining_time_slice(剩余时间片)两个字段共同管理。
2. 时间片的轮转机制
- 每个 tick 中断,当前任务的
remaining_time_slice减 1。 - 减到 0 时,重置为
time_slice,通过k_readyq_move_tail()移到同优先级就绪链表尾部,触发调度。 - 轮转仅在同优先级任务之间生效。
3. 时间片 API
os_task_set_time_slice():设置总时间片(1~99 tick)。os_task_get_time_slice():获取总时间片。os_task_get_remaining_time_slice():获取剩余时间片。
4. 优先级优先
- 优先级抢占始终优先于时间片轮转。
- 时间片轮转不跨越优先级——不同优先级任务之间不轮转。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 时间片设为 0 或 ≥100 | 必须 > 0 且 < 100,否则断言失败 |
混淆 time_slice 和 remaining_time_slice | 前者是总配额,后者是剩余量;os_task_get_time_slice() 取总、os_task_get_remaining_time_slice() 取剩余 |
| 认为时间片作用于不同优先级任务 | 轮转只在同优先级任务之间生效 |
| 认为修改时间片能立即改变运行中任务的剩余量 | os_task_set_time_slice() 只对 INIT 状态任务同时更新剩余量,运行中任务下次调度才生效 |
| 忽略 tick 精度 | 时间片精度受限于 OS_TICK_PER_SECOND,做不到比一个 tick 更细 |
下一步
时间片是任务调度的重要组成部分。接下来请学习下一小节:
- 4.2.6 任务挂起与恢复:了解如何暂停和恢复任务的执行