4.2.3 任务调度
4.2.3 任务调度
📚 本节导读
学习时长: 约 25 分钟
难度级别: ⭐⭐⭐⭐☆
前置知识: 任务状态机、优先级概念、Cortex-M PendSV 机制
🎯 学习目标
- 理解 OneOS 的优先级抢占式调度策略
- 理解同优先级任务的时间片轮转调度
- 掌握
os_task_yield()主动让出的用法 - 了解 PendSV 上下文切换机制
- 掌握获取当前任务和任务总数的 API
一、调度策略概述
OneOS-Lite 采用基于优先级的抢占式调度策略,辅以同优先级时间片轮转调度。调度器始终选择就绪队列中优先级最高的任务运行。
调度策略 = 优先级抢占 + 同优先级时间片轮转1.1 优先级抢占
当一个新的高优先级任务就绪时,调度器会立即抢占当前正在运行的低优先级任务,暂停低优先级任务,先运行高优先级任务:
时间轴(横向为时间流逝):
──────────────────────────────────────────────────────►
低优先级任务 A ━━━━━━━━━━━━━━━━━┃ (暂停)
┃ 高优先级任务 B 就绪,立即抢占
高优先级任务 B ━━━━━━━━━━━━━━━━━━ (插队运行)
┃
▼
A 被挂起(保存现场),B 开始运行关键理解:
- "抢占"(Preemption) = 高优先级任务"插队",强制打断低优先级任务的运行。
- 被打断的低优先级任务 A 不是被销毁,而是被暂停(保存现场),等 B 运行完或阻塞后,A 再继续执行。
- 抢占的触发前提:新就绪的任务优先级 > 当前运行任务优先级。
1.2 时间片轮转
当多个同优先级任务就绪时,调度器采用时间片轮转(Round-Robin)方式:每个任务运行一个时间片后,切换到下一个同优先级任务:
时间轴:
────────────────────────────────────────────────────────────►
任务 A (优先级 5) ████████████┃ ┃ ┃
任务 B (优先级 5) ████████████┃ ┃
任务 C (优先级 5) ████████████┃
│ │ │
A 时间片用完 B 时间片用完 C 时间片用完
│ │ │
切换到 B 切换到 C 切换到 A(循环)关键理解:
- 时间片轮转只发生在同优先级任务之间。不同优先级之间靠抢占(1.1)而非轮转。
- 每个任务运行
time_slice个 tick 后,调度器把它移到就绪链表尾部,让下一个同优先级任务运行。 - 一个 tick(时钟节拍)中断时会检查当前任务的时间片是否用完(见四、调度触发时机)。
1.3 两种调度策略的关系
| 维度 | 优先级抢占 | 时间片轮转 |
|---|---|---|
| 作用对象 | 不同优先级任务 | 同优先级任务 |
| 触发方式 | 高优先级任务就绪时强制打断 | 时间片用完时主动轮换 |
| 调度依据 | 优先级高低 | 时间片是否用完 |
1.4 常见疑问:低优先级任务会不会"饿死"?
疑问:如果有两个任务,优先级分别是 0 和 8,那么优先级 8 的任务是不是永远不会运行?
回答:是的,在优先级 0 的任务始终就绪(不阻塞、不睡眠、不让出)的情况下,优先级 8 的任务永远不会被调度运行。原因如下:
- 调度器永远运行最高优先级任务:只要优先级 0 的任务就绪,
g_os_high_task就是它,调度器总是选择它。 - 时间片轮转只发生在同优先级之间:优先级 0 的任务轮转,只会轮到另一个优先级 0 的任务;优先级 8 的任务不在优先级 0 的轮转链表中,不会被轮转到。
那么优先级 8 的任务什么时候才能运行?
只有优先级 0 的任务让出 CPU 时才可能运行,包括:
| 让出方式 | 说明 |
|---|---|
| 被阻塞 | 等待信号量/互斥锁/消息队列等 IPC 资源 |
| 睡眠/延时 | 调用 os_task_msleep() / os_task_tsleep() |
| 被销毁 | 优先级 0 的任务被 os_task_destroy() 销毁 |
| 主动让出 | 调用 os_task_yield()(且此时无其他优先级 0 任务) |
⚠️ 这是嵌入式 RTOS 设计中必须警惕的"优先级饥饿(Priority Starvation)"问题:高优先级任务若长期占满 CPU,低优先级任务将长时间得不到运行。实际开发中应:
- 合理规划任务优先级,避免高优先级任务长期占用 CPU。
- 在高优先级任务的循环里加入延时或阻塞,给低优先级任务让出运行机会。
- 这正是 IPC(信号量、消息队列、互斥锁)存在的意义之一——通过阻塞让低优先级任务有机会运行。
二、调度器核心数据结构
2.1 就绪队列(Ready Queue)
调度器使用位图(bitmap)算法管理就绪队列,按优先级组织任务:
/* 优先级数 ≤ 32 时,使用单个 uint32_t 位图 */
struct readyq_bitmap
{
uint32_t priority_bmap; /* 优先级位图 */
os_list_node_t priority_list_array[OS_TASK_PRIORITY_MAX]; /* 每个优先级一个双向链表 */
};
/* 优先级数 > 32 时,使用两级位图 */
struct readyq_bitmap
{
uint32_t priority_group_bmap; /* 组位图 */
uint8_t priority_bmap[(OS_TASK_PRIORITY_MAX + 7) / 8]; /* 组内位图 */
os_list_node_t priority_list_array[OS_TASK_PRIORITY_MAX];
};位图算法为什么高效?—— 具体例子
就绪队列的核心难点是:如何快速找到优先级最高的就绪任务。位图(bitmap)算法用一个"优先级位图"来解决:
- 位图的每个 bit 代表一个优先级。bit 为 1 表示"该优先级下至少有一个就绪任务",bit 为 0 表示"该优先级下没有就绪任务"。
- 假设
OS_TASK_PRIORITY_MAX = 32,优先级 0(最高)对应 bit 0,优先级 31(最低)对应 bit 31。
例如当前就绪队列中,优先级 3、10、15 下都有任务就绪:
bit 位置: 31 30 ... 15 14 ... 10 ... 3 2 1 0
│ │ │ │ │ │ │ │ │
位图值: 0 0 ... 1 0 ... 1 ... 1 0 0 0
└───────┬───────┘
有 1 的位依次是 bit15、bit10、bit3os_ffs()(Find First Set)如何快速找到最高优先级?
os_ffs(value) 会返回最低位为 1 的 bit 位置。因为优先级越高数值越小(bit 位越低),所以找到最低的 1,就找到了最高优先级:
位图值 = 0x0000_0808
= 0000 0000 0000 1000 0000 1000 (bit15、bit10、bit3 为 1)
os_ffs(0x00000808) = 3 (返回最低位为 1 的位置,即优先级 3)查找复杂度为什么是 O(1)?
- 传统方法需要从最高优先级到最低优先级逐一扫描链表,找到第一个非空链表,最坏情况要扫描
OS_TASK_PRIORITY_MAX次。 - 位图算法把"逐级扫描"变成了一条
os_ffs()指令,无论多少个优先级,都只需要一次运算,所以复杂度是 O(1)。 - 找到最高优先级后,直接从
priority_list_array[优先级]取出链表头部的任务即可。
小结:位图算法的核心思想是"用一个 bit 代表一个优先级是否存在就绪任务",再用
os_ffs()一条指令快速定位,从而把"找最高优先级任务"从O(N)优化到O(1)。
2.2 关键全局变量
单核模式:
os_task_t *g_os_current_task = OS_NULL; /* 当前运行任务 */
os_task_t *g_os_next_task = OS_NULL; /* 下一个要运行的任务 */
os_task_t *g_os_high_task = OS_NULL; /* 最高优先级就绪任务 */
int16_t g_os_sched_lock_cnt = 0; /* 调度锁定计数 */SMP 模式:
os_task_t *g_os_current_task[OS_SMP_MAX_CPUS]; /* 每个 CPU 的当前任务 */
os_task_t *g_os_next_task[OS_SMP_MAX_CPUS]; /* 每个 CPU 的下一个任务 */
static uint32_t g_os_need_sched_bit = 0; /* 需要调度的 CPU 位图 */三、os_task_yield() —— 主动让出 CPU
3.1 函数签名
os_err_t os_task_yield(void);3.2 功能说明
os_task_yield() 使当前任务主动让出 CPU 使用权,将其移动到同优先级就绪链表的末尾,然后触发调度让同优先级的下一个任务运行。
3.3 使用限制
- 不允许在中断上下文中调用
- 不允许在中断禁用时调用
- 不允许在调度器锁定时调用
这些限制在
os_task_yield()内部通过OS_ASSERT断言检查强制保证(见下方源码)。因为让出 CPU 需要上下文切换,而中断上下文、关中断、调度锁定等场景都无法安全地进行任务切换。
3.4 完整源码
来源: kernel/source/os_task.c
os_err_t os_task_yield(void)
{
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_KERNEL_ENTER();
current_task = _k_task_self();
OS_ASSERT((current_task != OS_NULL));
if (OS_TRUE == k_readyq_move_tail(current_task))
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
return OS_SUCCESS;
}3.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(OS_FALSE == os_is_irq_active()) | 不能在中断上下文中让出 CPU |
OS_ASSERT(OS_FALSE == os_is_irq_disabled()) | 不能在中断禁用时让出 CPU |
OS_ASSERT(OS_FALSE == os_is_schedule_locked()) | 不能在调度器锁定时让出 CPU |
关键点:让出 CPU 的本质是触发一次上下文切换,而上述三种场景下都无法安全地完成任务切换,所以用断言提前拦截。
第二步:获取当前任务
OS_KERNEL_ENTER();
current_task = _k_task_self();
OS_ASSERT((current_task != OS_NULL));详细解释:
OS_KERNEL_ENTER():进入内核临界区,禁止调度,保证后续操作原子性。_k_task_self():获取当前正在运行的任务。在单核模式下它就是g_os_current_task(见 2.2 节),在 SMP 模式下是g_os_current_task[os_cpu_id_get()]。OS_ASSERT(current_task != OS_NULL):当前任务不能为空。
第三步:尝试将当前任务移到就绪链表尾部
if (OS_TRUE == k_readyq_move_tail(current_task))
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}详细解释:
| 代码 | 作用 |
|---|---|
k_readyq_move_tail(current_task) | 把当前任务从就绪链表头部移到同优先级就绪链表的尾部 |
返回值 OS_TRUE | 移动成功,说明存在同优先级的其他就绪任务 |
返回值 OS_FALSE | 移动失败,说明当前优先级就绪链表中只有它自己 |
为什么要把自己移到链表尾部?
就绪队列中每个优先级是一个链表,调度器总是从链表头部取任务运行。当前任务让出 CPU 时:
移动前: [优先级 5 链表] A(当前) → B → C (A 正在运行,在链表头部)
移动后: [优先级 5 链表] B → C → A (A 被移到链表尾部)- 移动后,调度器会从链表头部取出 B 运行,从而实现"把 CPU 让给同优先级的下一个任务"。
- 若没有同优先级任务(链表里只有 A 自己),
k_readyq_move_tail返回OS_FALSE,此时不触发调度,当前任务继续运行。
两个分支的动作:
| 返回值 | 动作 | 说明 |
|---|---|---|
OS_TRUE | OS_KERNEL_EXIT_SCHED() | 退出临界区并触发调度,切换到同优先级的下一个任务 |
OS_FALSE | OS_KERNEL_EXIT() | 仅退出临界区,不触发调度,当前任务继续运行 |
最后返回 OS_SUCCESS:os_task_yield() 无论是否真正切换,都返回成功。
3.6 使用示例
void task_a(void *arg)
{
while (1)
{
/* 处理一部分数据 */
process_data();
/* 主动让出 CPU 给同优先级任务 */
os_task_yield();
}
}典型应用场景:当多个同优先级任务需要相互协作、轮流处理时,可以在每个任务处理完一段数据后调用 os_task_yield(),让其他同优先级任务也有机会运行,而不必依赖时间片轮转强制切换。
四、调度触发时机
调度器不是"一直运转"的,而是在特定时机被"请求"调度。调度的本质是:就绪队列的最高优先级任务发生了变化(可能有更高优先级的任务就绪了,或当前任务被移出了就绪队列),此时需要重新选择"下一个该运行的任务"。
调度器在以下时机被触发:
| 触发时机 | 说明 |
|---|---|
| 任务创建启动 | os_task_startup() 将新任务加入就绪队列后 |
| 中断退出 | 中断处理完成后,检查是否需要调度 |
| 任务主动让出 | os_task_yield() |
| 任务延时 | os_task_tsleep() / os_task_msleep() 让任务睡眠 |
| IPC 操作 | 信号量释放、互斥锁释放等可能唤醒高优先级任务 |
| 调度器解锁 | os_schedule_unlock() 在解锁时检查是否积累的调度请求 |
| 系统时钟节拍 | 每个 tick 中断检查时间片是否用完 |
为什么这些时机都能改变"最高优先级就绪任务"?
| 触发时机 | 发生了什么变化 |
|---|---|
| 任务启动 | 有新的任务进入就绪队列,可能比当前任务优先级更高 |
| 任务让出/延时/阻塞 | 当前任务离开就绪队列(或被移到尾部),需要选下一个 |
| IPC 操作 | 可能有被阻塞的高优先级任务被唤醒进入就绪队列 |
| 时钟节拍 | 高优先级任务的延时到期重新就绪,或时间片用完需要轮换 |
调度的通用流程(各触发时机都遵循):
① 就绪队列发生变化(有新任务就绪 / 当前任务让出)
↓
② 用位图 os_ffs() 找到最高优先级就绪任务(O(1))
↓
③ 该任务 ≠ 当前运行任务?
├─ 是 → 设置 PendSV,请求切换(见五)
└─ 否 → 继续运行当前任务,无需切换五、PendSV 上下文切换机制
5.1 为什么使用 PendSV?
在 ARM Cortex-M 架构中,OneOS 使用 PendSV(Pendable Service Call)异常来实现任务上下文切换。PendSV 是一种"可挂起的服务请求"异常,它的关键特性是可以被延后执行。
先理解"上下文切换"是什么:
任务切换的本质是"换人上场"——把当前任务的寄存器(现场)保存到它的任务栈,再从下一个任务的任务栈恢复它的现场。这就像切换两个人工作时,先记下甲的工作进度(保存现场),再调出乙的进度继续干(恢复现场)。
为什么需要 PendSV 这个"特殊中介"?
切换任务的工作很敏感,如果在"不对的时机"(比如正在处理一个中断)就直接切换,会出问题。PendSV 的作用是把切换工作推迟到安全的时机再做:
| PendSV 特性 | 通俗解释 | 好处 |
|---|---|---|
| 延迟执行 | PendSV 优先级设为最低,只有所有中断处理完才轮到它 | 确保在 ISR 全部执行完毕后才切换任务,不会打断中断处理 |
| 避免中断嵌套 | 不在 ISR 内部直接切换任务 | 避免在中断上下文中操作复杂数据结构,降低出错风险 |
| 原子性保障 | PendSV 异常处理本身不会被打断 | 切换过程"一气呵成",不会被其他操作干扰 |
打个比方:想象一个乐队演出中,需要换演奏者。直接"中途冲上去换人"会打断正在演奏的乐手(相当于在 ISR 中直接切换,很危险)。PendSV 就像"等这段演奏彻底结束、幕间休息时再换人",既安全又不会出错。
5.2 切换流程
┌──────────────┐
│ 1. 触发调度 │ (设置 PendSV 挂起位)
└──────┬───────┘
▼
┌──────────────┐
│ 2. 当前 ISR │ (如果有中断正在处理)
│ 执行完毕 │
└──────┬───────┘
▼
┌──────────────┐
│ 3. PendSV │ (最低优先级,所有 ISR 完成后执行)
│ 异常进入 │
└──────┬───────┘
▼
┌──────────────┐
│ 4. 保存上下文 │ 保存当前任务寄存器(R4-R11, SP, LR, PC)
│ 到任务栈 │
└──────┬───────┘
▼
┌──────────────┐
│ 5. 选择下一 │ 从就绪队列获取最高优先级任务
│ 个任务 │
└──────┬───────┘
▼
┌──────────────┐
│ 6. 恢复上下文 │ 从新任务栈恢复寄存器
│ 到寄存器 │
└──────┬───────┘
▼
┌──────────────┐
│ 7. 异常返回 │ 跳转到新任务的 PC 继续执行
└──────────────┘流程中的关键概念:
- 保存上下文到任务栈(第 4 步):把当前任务"运行到哪了"记录下来——包括它的通用寄存器(R4~R11)、栈指针(SP)、返回地址(PC)、链接寄存器(LR)等。这些信息保存在当前任务自己的栈里。
- 选择下一个任务(第 5 步):用位图
os_ffs()从就绪队列中找到最高优先级任务。 - 恢复上下文到寄存器(第 6 步):从新任务的任务栈里,把之前保存的寄存器值"装回去",让新任务从它上次停下的地方继续运行。
- 异常返回(第 7 步):通过异常返回机制,跳转到新任务记录的程序计数器(PC),新任务正式开始执行。
参考:关于 PendSV 的详细实现,请参阅第 3 章"中断管理与上下文切换"相关内容。
六、任务信息查询 API
6.1 os_get_current_task() —— 获取当前任务
os_task_id os_get_current_task(void);返回当前正在 CPU 上运行的任务 ID。
完整源码(kernel/source/os_task.c):
os_task_id os_get_current_task(void)
{
#ifdef OS_USING_SMP
os_ubase_t irq_save;
const os_task_t *task;
irq_save = os_irq_lock();
task = g_os_current_task[os_cpu_id_get()];
os_irq_unlock(irq_save);
return OS_TYPE_CONVERT(os_task_id, task);
#else
return OS_TYPE_CONVERT(os_task_id, g_os_current_task);
#endif
}详细解释:
| 模式 | 逻辑 |
|---|---|
| 单核 | 直接返回全局变量 g_os_current_task(当前运行任务) |
| SMP | 加锁后返回 g_os_current_task[os_cpu_id_get()],即当前 CPU 核心上运行的任务 |
使用示例:
os_task_id current = os_get_current_task();
os_kprintf("Current task: %s\r\n", os_task_get_name(current));6.2 os_task_get_total_count() —— 获取任务总数
uint32_t os_task_get_total_count(void);返回系统中当前存在的任务总数(包括空闲任务、回收任务等系统任务)。
完整源码(kernel/source/os_task.c):
uint32_t os_task_get_total_count(void)
{
uint32_t task_count;
os_spin_lock(&gs_os_task_resource_list_lock);
task_count = os_list_len(&gs_os_task_resource_list_head);
OS_ASSERT((0U != task_count));
os_spin_unlock(&gs_os_task_resource_list_lock);
return task_count;
}详细解释:
gs_os_task_resource_list_head是全局任务资源链表(任务创建时把 TCB 登记在这里,见 4.2.2 第五节)。os_list_len()统计该链表的长度,即当前存在的任务总数。- 加自旋锁保护链表,避免并发访问。
使用示例:
uint32_t count = os_task_get_total_count();
os_kprintf("Total tasks: %u\r\n", count);6.3 os_task_get_id() —— 按名称查找任务
os_task_id os_task_get_id(const char *name);通过任务名称查找对应的任务 ID。
完整源码(kernel/source/os_task.c):
os_task_id os_task_get_id(const char *name)
{
os_list_node_t *pos;
os_task_t *iter_task;
os_task_t *found_task;
OS_ASSERT(OS_NULL != name);
found_task = OS_NULL;
/*
* Because recycle task and idle task are system tasks,
* They don't want to be accessed by the application layer.
*/
if ((name[0] != '\0') && (strncmp(name, OS_RECYCLE_TASK_NAME, OS_NAME_MAX) != 0) &&
(strncmp(name, OS_IDLE_TASK_NAME, OS_NAME_MAX) != 0))
{
os_spin_lock(&gs_os_task_resource_list_lock);
os_list_for_each(pos, &gs_os_task_resource_list_head)
{
iter_task = os_list_entry(pos, os_task_t, resource_node);
if (strncmp(name, iter_task->name, OS_NAME_MAX) == 0)
{
found_task = iter_task;
break;
}
}
os_spin_unlock(&gs_os_task_resource_list_lock);
}
return OS_TYPE_CONVERT(os_task_id, found_task);
}详细解释:
- 通过遍历全局任务资源链表,逐个比较任务名称(
strncmp),找到名称匹配的任务。 - 不能查找系统任务:代码中特意排除了
OS_RECYCLE_TASK_NAME(回收任务)和OS_IDLE_TASK_NAME(空闲任务),因为它们是内核内置任务,不希望被应用层访问。 - 未找到时返回
OS_NULL。
6.4 os_task_check_exist() —— 检查任务是否存在
os_bool_t os_task_check_exist(os_task_id tid);检查指定的任务 ID 是否对应一个有效的任务。
完整源码(kernel/source/os_task.c):
os_bool_t os_task_check_exist(os_task_id tid)
{
os_task_t *iter_task;
os_list_node_t *node;
os_bool_t exist;
os_task_t *task;
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
exist = OS_FALSE;
os_spin_lock(&gs_os_task_resource_list_lock);
os_list_for_each(node, &gs_os_task_resource_list_head)
{
iter_task = os_list_entry(node, os_task_t, resource_node);
if (task == iter_task)
{
exist = OS_TRUE;
break;
}
}
os_spin_unlock(&gs_os_task_resource_list_lock);
return exist;
}详细解释:
- 遍历全局任务资源链表,判断给定的任务 ID(TCB 指针)是否存在于链表中。
- 存在则返回
OS_TRUE,否则返回OS_FALSE。 - 与
os_task_get_id()类似,都是通过遍历全局任务资源链表实现,只是比较的对象是"任务 ID 指针"而非名称。
七、调度器的启动
调度器通过 k_start() 函数启动,它在系统初始化完成后被调用,负责选中最高优先级的就绪任务并启动第一个任务,从此内核进入无限调度循环。
7.1 完整源码
来源: kernel/source/os_sched.c
void k_start(void)
{
#ifdef OS_USING_SMP
OS_KERNEL_INIT();
int32_t current_cpu_index;
current_cpu_index = os_cpu_id_get();
OS_KERNEL_ENTER();
g_os_next_task[current_cpu_index] = _k_highest_task(current_cpu_index);
g_os_next_task[current_cpu_index]->cpu_index = current_cpu_index;
k_sched_readyq_remove(g_os_next_task[current_cpu_index]);
#else
g_os_next_task = g_os_high_task;
#endif
os_first_task_start();
/* Never come back. */
return;
}7.2 分步详解
k_start() 的实现在单核(非 SMP)与多核(SMP)下有差异,下面分别说明。
单核模式(非 SMP)
g_os_next_task = g_os_high_task;
os_first_task_start();
/* Never come back. */
return;详细解释:
| 代码 | 作用 |
|---|---|
g_os_next_task = g_os_high_task | 把"下一个要运行的任务"设为当前最高优先级的就绪任务 g_os_high_task |
os_first_task_start() | 启动第一个任务(架构相关的汇编函数),它根据 g_os_next_task 恢复该任务的上下文,并跳转执行 |
return(注释 Never come back) | 永远不会返回到这里——因为 os_first_task_start() 已经把控制权交给用户任务,不再回到调度器启动代码 |
为什么用 g_os_high_task 而不是 g_os_next_task?
在系统刚启动、还没有任何任务运行时,"下一个任务"自然就是当前就绪队列中优先级最高的任务。g_os_high_task 正是调度器维护的最高优先级就绪任务指针。
SMP 模式
OS_KERNEL_INIT();
int32_t current_cpu_index;
current_cpu_index = os_cpu_id_get();
OS_KERNEL_ENTER();
g_os_next_task[current_cpu_index] = _k_highest_task(current_cpu_index);
g_os_next_task[current_cpu_index]->cpu_index = current_cpu_index;
k_sched_readyq_remove(g_os_next_task[current_cpu_index]);详细解释:
| 代码 | 作用 |
|---|---|
os_cpu_id_get() | 获取当前 CPU 核心的编号 |
_k_highest_task(current_cpu_index) | 获取该 CPU 上最高优先级的就绪任务 |
g_os_next_task[current_cpu_index] = ... | 把该 CPU 的下一个任务设为此最高优先级任务 |
->cpu_index = current_cpu_index | 记录该任务运行在哪个 CPU 上 |
k_sched_readyq_remove(...) | 把它从就绪队列移除(因为它马上要运行了) |
7.3 k_start() 在启动流程中的位置
k_start() 在系统初始化完成后、正式进入多任务调度前被调用:
系统初始化(创建 idle、recycle 等系统任务)
↓
k_start() ──► 选中最高优先级任务 ──► os_first_task_start()
↓
进入用户任务执行(调度器持续运行)说明:
os_first_task_start()是架构相关的汇编函数,负责恢复第一个任务的上下文并跳转。它的详细实现可参考第 3 章启动流程相关内容。
📝 本节小结
核心要点回顾
本节围绕 OneOS 的任务调度机制展开,核心要点如下:
1. 调度策略
- 采用基于优先级的抢占式调度,调度器始终运行就绪队列中优先级最高的任务。
- 辅以同优先级时间片轮转,让多个同优先级任务轮流运行。
2. 就绪队列的高效实现
- 使用位图(bitmap)算法管理就绪队列,每个优先级一个链表。
- 通过
os_ffs()(Find First Set)快速定位最高优先级的非空链表,查找复杂度为 O(1)。
3. os_task_yield() 主动让出
- 把当前任务移到同优先级就绪链表尾部,让出 CPU 给同优先级的下一个任务。
- 若没有同优先级任务,则不切换、继续运行。
4. PendSV 上下文切换
- 使用 PendSV 异常实现任务切换,其优先级设为最低,确保所有中断处理完毕后才切换。
- 避免在中断上下文操作复杂数据结构,保证原子性。
5. 调度器启动(k_start())
- 选中最高优先级就绪任务,通过
os_first_task_start()启动第一个任务,此后内核进入无限调度循环。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
在中断/关中断/调度锁定时调用 os_task_yield() | os_task_yield() 内部有断言拦截,只能在任务上下文调用 |
| 混淆抢占与让出 | 抢占是高优先级任务就绪时强制打断低优先级;让出是当前任务主动放弃给同优先级 |
| 忽略调度触发时机 | 调度不仅在主动让出时触发,中断退出、IPC、延时、时钟节拍等都会触发 |
用 os_task_get_id() 查找系统任务 | 空闲任务、回收任务不能被该函数查找 |
下一步
任务调度是内核最核心的机制。接下来请学习下一小节:
- 4.2.4 任务优先级:了解优先级的范围、动态修改及优先级继承机制