4.2.8 调度器锁定
4.2.8 调度器锁定
📚 本节导读
学习时长: 约 15 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: 任务调度机制、临界区概念
🎯 学习目标
- 理解调度器锁定的作用和适用场景
- 掌握
os_schedule_lock()/os_schedule_unlock()的用法 - 理解调度器锁定与关中断的区别
- 了解调度器锁定期间的注意事项
一、调度器锁定概述
1.1 什么是调度器锁定?
调度器锁定(Schedule Lock)是一种轻量级的临界区保护机制。当调度器被锁定后,内核不会进行任务切换,当前任务可以独占 CPU 直到调度器被解锁。
1.2 调度器锁定的特点
| 特性 | 说明 |
|---|---|
| 可嵌套 | 支持嵌套锁定,使用计数 g_os_sched_lock_cnt 管理 |
| 中断仍可响应 | 锁定调度器不会关闭中断,ISR 仍可正常执行 |
| 不可在锁定期间阻塞 | 锁定期间不能调用会导致任务阻塞的 API |
| 不可在锁定期间延时 | 锁定期间不能调用 os_task_tsleep() / os_task_msleep() |
| SMP 支持 | 每个 CPU 核心独立维护锁定计数 |
1.3 锁定计数机制
调度器使用一个计数器来跟踪锁定状态:
/* 单核模式 */
int16_t g_os_sched_lock_cnt = 0; /* 0 = 未锁定,> 0 = 已锁定 */
/* SMP 模式 */
static int16_t gs_os_sched_lock_cnt[OS_SMP_MAX_CPUS] = {0}; /* 每个 CPU 独立计数 */每次调用 os_schedule_lock() 计数器加 1,每次调用 os_schedule_unlock() 计数器减 1。只有当计数器回到 0 时,调度器才真正解锁。
二、API 函数
2.1 os_schedule_lock() —— 锁定调度器
void os_schedule_lock(void);锁定后,调度器不会进行任务切换。即使有更高优先级的任务就绪,当前任务也会继续运行。
内部实现:
void os_schedule_lock(void)
{
OS_KERNEL_ENTER(); /* 关中断,进入临界区 */
g_os_sched_lock_cnt++; /* 锁定计数 +1 */
OS_KERNEL_EXIT(); /* 开中断,退出临界区 */
}2.2 os_schedule_unlock() —— 解锁调度器
void os_schedule_unlock(void);解锁调度器。如果锁定计数降为 0,并且有更高优先级的任务就绪,会立即触发任务切换。
内部实现:
void os_schedule_unlock(void)
{
OS_KERNEL_ENTER();
sched_lock_cnt = --g_os_sched_lock_cnt;
if (sched_lock_cnt == 0)
{
/* 计数归零,触发调度(如有更高优先级任务就绪) */
OS_KERNEL_EXIT_SCHED();
}
else if (sched_lock_cnt > 0)
{
/* 仍有嵌套锁定,仅退出临界区 */
OS_KERNEL_EXIT();
}
else
{
/* 锁定计数为负,使用错误 */
OS_ASSERT_EX(OS_FALSE, "Task use schedule lock correctly");
}
}2.3 os_is_schedule_locked() —— 检查锁定状态
os_bool_t os_is_schedule_locked(void);| 返回值 | 说明 |
|---|---|
OS_TRUE | 调度器已锁定 |
OS_FALSE | 调度器未锁定 |
三、使用示例
3.1 基本用法
void critical_operation(void)
{
/* 锁定调度器,确保原子操作 */
os_schedule_lock();
/* 以下代码不会被其他任务打断 */
shared_var1 = new_value1;
shared_var2 = new_value2;
/* 两个变量作为一个整体被更新 */
os_schedule_unlock();
}3.2 嵌套锁定
void outer_function(void)
{
os_schedule_lock(); /* 锁定计数 = 1 */
/* 临界区代码 A */
inner_function();
/* 临界区代码 B */
os_schedule_unlock(); /* 锁定计数 = 0,允许调度 */
}
void inner_function(void)
{
os_schedule_lock(); /* 锁定计数 = 2 */
/* 嵌套临界区代码 */
os_schedule_unlock(); /* 锁定计数 = 1 */
}3.3 保护共享资源
static int g_counter = 0;
void increment_counter(void)
{
os_schedule_lock(); /* 防止任务并发访问 */
int temp = g_counter;
temp++;
g_counter = temp; /* 原子操作 */
os_schedule_unlock();
}注意:上述示例假设只有任务会访问
g_counter。如果 ISR 也会访问,则需要使用关中断或自旋锁来保护。
四、调度器锁定 vs 关中断
| 维度 | 调度器锁定 | 关中断 |
|---|---|---|
| 保护范围 | 仅防止任务切换 | 防止所有中断和任务切换 |
| 中断响应 | ✅ 中断仍可响应 | ❌ 中断被屏蔽 |
| 实时性影响 | 较小(仅影响任务调度) | 较大(影响中断延迟) |
| 可嵌套 | ✅ 支持 | ✅ 支持 |
| 适用场景 | 任务间临界区保护 | 任务与 ISR 共享数据的保护 |
| 性能开销 | 较低 | 较低 |
选择指南
是否需要保护数据免受 ISR 访问?
│
├── 是 → 使用关中断(os_irq_lock / os_irq_unlock)
│
└── 否 → 仅需防止任务间竞争?
│
├── 是 → 使用调度器锁定
│
└── 否 → 使用互斥锁 / 信号量等 IPC 机制五、注意事项
5.1 不要在锁定期间阻塞
调度器锁定期间,绝对不能调用任何可能导致任务阻塞的 API,包括但不限于:
os_task_tsleep()/os_task_msleep()—— 会导致断言失败os_sem_wait()—— 会阻塞当前任务,但调度器已锁定无法切换os_mutex_lock()—— 同上- 任何带有
OS_WAIT_FOREVER超时的 IPC 操作
/* 错误示例:锁定期间延时 */
os_schedule_lock();
os_task_msleep(100); /* ❌ 断言失败!调度器已锁定 */
os_schedule_unlock();5.2 锁定时间尽量短
调度器锁定期间,更高优先级的任务无法运行,这会影响系统的实时性。因此锁定时间应尽可能短:
/* 推荐:锁定时间短 */
os_schedule_lock();
g_flags |= FLAG_READY; /* 简单的标志位操作 */
os_schedule_unlock();
/* 不推荐:锁定时间长 */
os_schedule_lock();
compute_heavy(); /* 复杂计算,耗时较长 */
send_data(); /* 发送大量数据 */
os_schedule_unlock();5.3 确保配对使用
每次 os_schedule_lock() 必须有对应的 os_schedule_unlock()。不配对会导致调度器永久锁定或锁定计数异常。
/* 推荐:使用 RAII 风格(如果支持)或在同一函数中配对 */
void safe_function(void)
{
os_schedule_lock();
/* 临界区代码 */
os_schedule_unlock();
}
/* 不推荐:在不同分支中锁定但未解锁 */
void unsafe_function(void)
{
os_schedule_lock();
if (error_condition)
{
return; /* ❌ 忘记解锁! */
}
os_schedule_unlock();
}5.4 与互斥锁的区别
| 维度 | 调度器锁定 | 互斥锁 |
|---|---|---|
| 粒度 | 全局(所有任务) | 指定资源 |
| 保护对象 | 任务间临界区 | 特定共享资源 |
| 优先级继承 | 不支持 | 支持 |
| 阻塞行为 | 不可阻塞 | 获取失败时阻塞 |
| 死锁风险 | 低(不可阻塞) | 存在 |
对于共享资源的保护,优先使用互斥锁;调度器锁定适用于需要确保多个操作原子性、且操作时间极短的场景。
📝 本节小结
调度器锁定是 OneOS 提供的一种轻量级临界区保护机制,通过 os_schedule_lock() 和 os_schedule_unlock() 控制。锁定期间任务不会切换,但中断仍可响应。与关中断相比,调度器锁定对实时性影响更小,但无法保护任务与 ISR 之间的共享数据。使用时应确保锁定时间短、配对使用,且绝对不要在锁定期间调用可能导致阻塞的 API。