4.2.3 任务调度
4.2.3 任务调度
📚 本节导读
学习时长: 约 25 分钟
难度级别: ⭐⭐⭐⭐☆
前置知识: 任务状态机、优先级概念、Cortex-M PendSV 机制
🎯 学习目标
- 理解 OneOS 的优先级抢占式调度策略
- 理解同优先级任务的时间片轮转调度
- 掌握
os_task_yield()主动让出的用法 - 了解 PendSV 上下文切换机制
- 掌握获取当前任务和任务总数的 API
一、调度策略概述
OneOS-Lite 采用基于优先级的抢占式调度策略,辅以同优先级时间片轮转调度。调度器始终选择就绪队列中优先级最高的任务运行。
调度策略 = 优先级抢占 + 同优先级时间片轮转1.1 优先级抢占
当一个新的高优先级任务就绪时,调度器会立即抢占当前正在运行的低优先级任务:
时间轴:
────────────────────────────────────────────────────►
低优先级任务 A ──────────────────┐
│ 被抢占
高优先级任务 B ────────────┘
(就绪) │
▼
A 被挂起,B 开始运行1.2 时间片轮转
当多个同优先级任务就绪时,调度器采用时间片轮转(Round-Robin)方式,每个任务运行一个时间片后切换到下一个同优先级任务:
时间轴:
────────────────────────────────────────────────────►
任务 A (优先级 5) ────┬────────────┬────────────
任务 B (优先级 5) ────────────┼────────────
任务 C (优先级 5) ─────────────
│ │
时间片用完 时间片用完
切换到 B 切换到 C二、调度器核心数据结构
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];
};位图算法使得查找最高优先级任务的时间复杂度为 O(1),非常高效。os_ffs()(Find First Set)函数用于快速定位最高优先级的非空就绪链表。
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 使用限制
- 不允许在中断上下文中调用
- 不允许在中断禁用时调用
- 不允许在调度器锁定时调用
3.4 内部实现流程
os_err_t os_task_yield(void)
{
current_task = _k_task_self();
/* 尝试将当前任务移到同优先级就绪链表尾部 */
if (OS_TRUE == k_readyq_move_tail(current_task))
{
/* 有同优先级就绪任务,触发调度 */
OS_KERNEL_EXIT_SCHED();
}
else
{
/* 没有同优先级就绪任务,继续运行 */
OS_KERNEL_EXIT();
}
return OS_SUCCESS;
}3.5 使用示例
void task_a(void *arg)
{
while (1)
{
/* 处理一部分数据 */
process_data();
/* 主动让出 CPU 给同优先级任务 */
os_task_yield();
}
}四、调度触发时机
调度器在以下时机被触发:
| 触发时机 | 说明 |
|---|---|
| 任务创建启动 | os_task_startup() 将新任务加入就绪队列后 |
| 中断退出 | 中断处理完成后,检查是否需要调度 |
| 任务主动让出 | os_task_yield() |
| 任务延时 | os_task_tsleep() / os_task_msleep() 让任务睡眠 |
| IPC 操作 | 信号量释放、互斥锁释放等可能唤醒高优先级任务 |
| 调度器解锁 | os_schedule_unlock() 在解锁时检查是否积累的调度请求 |
| 系统时钟节拍 | 每个 tick 中断检查时间片是否用完 |
五、PendSV 上下文切换机制
5.1 为什么使用 PendSV?
在 ARM Cortex-M 架构中,OneOS 使用 PendSV(Pendable Service Call)异常来实现任务上下文切换。PendSV 具有以下优势:
- 延迟执行:PendSV 的优先级通常设为最低,确保在中断服务程序(ISR)全部执行完毕后才进行上下文切换
- 避免中断嵌套:不在 ISR 内部直接切换任务,避免在中断上下文中操作复杂数据结构
- 原子性保障:PendSV 中断处理函数本身是原子的,不会被同优先级中断打断
5.2 切换流程
┌──────────────┐
│ 1. 触发调度 │ (设置 PendSV 挂起位)
└──────┬───────┘
▼
┌──────────────┐
│ 2. 当前 ISR │ (如果有中断正在处理)
│ 执行完毕 │
└──────┬───────┘
▼
┌──────────────┐
│ 3. PendSV │ (最低优先级,所有 ISR 完成后执行)
│ 异常进入 │
└──────┬───────┘
▼
┌──────────────┐
│ 4. 保存上下文 │ 保存当前任务寄存器(R4-R11, SP, LR, PC)
│ 到任务栈 │
└──────┬───────┘
▼
┌──────────────┐
│ 5. 选择下一 │ 从就绪队列获取最高优先级任务
│ 个任务 │
└──────┬───────┘
▼
┌──────────────┐
│ 6. 恢复上下文 │ 从新任务栈恢复寄存器
│ 到寄存器 │
└──────┬───────┘
▼
┌──────────────┐
│ 7. 异常返回 │ 跳转到新任务的 PC 继续执行
└──────────────┘参考:关于 PendSV 的详细实现,请参阅第 3 章"中断管理与上下文切换"相关内容。
六、任务信息查询 API
6.1 os_get_current_task() —— 获取当前任务
os_task_id os_get_current_task(void);返回当前正在 CPU 上运行的任务 ID。在 SMP 模式下,返回当前 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);返回系统中当前存在的任务总数(包括空闲任务、回收任务等系统任务)。
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。注意:空闲任务("idle")和回收任务("recycle")不能被此函数查找。
6.4 os_task_check_exist() —— 检查任务是否存在
os_bool_t os_task_check_exist(os_task_id tid);检查指定的任务 ID 是否对应一个有效的任务。
七、调度器的启动
调度器通过 k_start() 函数启动:
void k_start(void)
{
/* 选中最高优先级就绪任务 */
g_os_next_task = g_os_high_task;
/* 启动第一个任务(架构相关) */
os_first_task_start();
/* 永远不会返回 */
}k_start() 在系统初始化完成后调用,选中最高优先级就绪任务并启动调度。此后内核将一直运行,任务在调度器的管理下不断切换。
📝 本节小结
OneOS 采用基于优先级的抢占式调度,结合同优先级时间片轮转。调度器使用位图算法实现 O(1) 的最高优先级任务查找。任务切换利用 PendSV 异常机制,确保中断处理完毕后才进行上下文切换。os_task_yield() 允许任务主动让出 CPU 给同优先级任务。调度器在任务创建、中断退出、IPC 操作、延时等多个时机被触发。