os操作系统——第4讲:不要独占CPU
目录
第4讲:不要独占CPU——阻塞机制与空闲任务的诞生
上一讲我们学会了用关中断保护临界区,避免了数据错乱。但还有一个让人头疼的问题:
如果任务A只是想让LED闪烁,每隔1秒翻转一次,但它在“等待”期间却死死霸占着CPU,让其他任务无法运行。
即使没有其他任务需要执行,CPU也在空转、发热、耗电。这一讲,我们将让任务学会“主动睡觉”——进入阻塞状态,把CPU让给别的任务;当无事可做时,CPU会跑一个特殊的“空闲任务”,甚至可以进入低功耗模式。
1. 忙等待:CPU 的“空转灾难”
回忆第一讲的例子,我们当时这样实现延时:
void delay_ms(int ms) {
for (int i = 0; i < ms * 1000; i++) {
// 空循环,浪费时间
asm volatile("nop");
}
}
这种“忙等待”(Busy Waiting)在裸机编程里很常见,但在多任务系统中是大忌:
- CPU 100% 占用:即使所有任务都在延时,CPU 依然在空转,电量白白浪费。
- 低优先级任务饿死:如果高优先级任务一直在忙等待,低优先级任务永远得不到运行。
- 实时性差:无法精确处理多个不同周期的任务。
我们需要的是一种机制:任务自愿放弃CPU,直到某个条件满足(例如时间到,或者等待的信号量可用)。 这就是 阻塞(Blocking)。
2. 阻塞状态:任务进入“休眠室”
我们在第2讲已经定义了任务的四种状态:RUNNING、READY、BLOCKED、SUSPENDED。其中阻塞态(BLOCKED) 就是专门用来处理“等待事件”的状态。
阻塞 vs 就绪的本质区别:
- 就绪态:任务还在就绪队列里,调度器随时可能选中它。
- 阻塞态:任务不在就绪队列中,调度器根本看不见它,直到等待的事件发生,它才会被重新放入就绪队列。
这就好比餐厅里的服务员:
- 就绪态 = 站在取餐口等着接单。
- 阻塞态 = 去后厨催菜了,但菜还没好,他就在后厨门口等着(不在前厅服务)。
3. 实现延时阻塞:os_sleep
我们来实现最常用的阻塞原语:让当前任务睡眠若干个系统时钟滴答(Tick)。
3.1 扩展 TCB
在 TCB 中增加一个字段 sleep_ticks,表示还需要阻塞多少个 Tick。
typedef struct tcb {
// ... 前面已有的 sp, next, priority, state ...
unsigned int sleep_ticks; // 0 表示不阻塞,>0 表示还需阻塞的 tick 数
} tcb_t;
3.2 os_sleep 函数
void os_sleep(int ticks) {
if (ticks <= 0) return;
enter_critical(); // 关中断,保护 TCB 链表操作
current_task->sleep_ticks = ticks;
current_task->state = TASK_BLOCKED;
// 从就绪队列中移除当前任务(需要知道它在哪个优先级链表里,这里简化)
remove_from_ready_list(current_task);
// 将当前任务加入阻塞队列(按唤醒时间排序,这里简单插入头部)
current_task->next = blocked_list;
blocked_list = current_task;
exit_critical();
// 主动触发调度,让出 CPU
schedule();
}
注意:
schedule()内部会寻找下一个可运行的任务。由于当前任务已被移出就绪队列,调度器不会选它,直到它被唤醒。
3.3 Tick 中断处理:唤醒阻塞任务
在每个时钟节拍中断中,除了进行时间片计数,还要遍历阻塞队列,将 sleep_ticks 减到 0 的任务唤醒。
void SysTick_Handler(void) {
// 保存当前任务上下文(由硬件或汇编完成)
// 处理阻塞队列
tcb_t *prev = NULL;
tcb_t *t = blocked_list;
while (t) {
if (t->sleep_ticks > 0) {
t->sleep_ticks--;
}
if (t->sleep_ticks == 0 && t->state == TASK_BLOCKED) {
// 唤醒任务:从阻塞队列中摘除
if (prev) prev->next = t->next;
else blocked_list = t->next;
// 改变状态并插入就绪队列
t->state = TASK_READY;
insert_into_ready_list(t); // 根据优先级插入
} else {
prev = t;
}
t = t->next;
}
// 最后调度(可能切换到更高优先级的就绪任务)
schedule();
}
关键点:
- 每次 Tick 都扫描整个阻塞队列(效率 O(n)),对于教学足够;实际 RTOS 会用“延时列表 + 时间轮”优化。
- 唤醒后任务重新加入就绪队列,调度器会在合适的时候让它恢复运行。
4. 空闲任务:当所有任务都睡了
如果系统中所有任务都进入了阻塞状态(比如都在等待外部事件或延时),就绪队列为空,此时 CPU 该做什么?
答案是:运行一个特殊的“空闲任务”(Idle Task)。
空闲任务的优先级通常最低(比如 255),它的唯一职责就是:什么都不做,或者让 CPU 进入低功耗模式。
void idle_task(void *arg) {
while (1) {
// 进入低功耗模式(ARM 的 WFI 指令)
asm volatile("wfi");
// 或者简单的一个空循环
// asm volatile("nop");
}
}
4.1 调度器如何处理空闲任务?
我们修改 get_next_task(),确保永远有一个“兜底”的任务:
tcb_t *get_next_task(void) {
// 从高优先级到低优先级搜索就绪队列
for (int prio = 0; prio < MAX_PRIORITY; prio++) {
if (ready_lists[prio] != NULL) {
return ready_lists[prio];
}
}
// 如果所有就绪队列都为空,返回空闲任务
return idle_task_tcb;
}
空闲任务是一个普通的 TCB,在系统初始化时创建,并且永远不会被阻塞。当它运行时,CPU 会执行 wfi 指令(等待中断),直到下一个 Tick 中断到来,它又被抢占,重新检查是否有任务就绪。
效果:当系统无事可做时,CPU 几乎不耗电(或处于极低功耗状态),这才是真正的“节能”。
5. 完整示例:三个任务轮流运行
假设我们有三个任务:
- Task1:高优先级,每隔 100ms 执行一次传感器采集(阻塞 100ms)。
- Task2:中优先级,每隔 200ms 执行一次数据处理(阻塞 200ms)。
- Task3:低优先级,空闲时才运行的后台任务(主动
os_sleep(0)或一直就绪)。
调度器的表现:
| 时间(ms) | 运行任务 | 说明 |
|---|---|---|
| 0-1 | task1 | 采集传感器,很快结束 |
| 1-100 | idle | task1 阻塞,task2 也阻塞,CPU 空闲 |
| 100 | task1 | 定时器唤醒 task1,抢占执行 |
| 101-200 | idle | 再次空闲 |
| 200 | task2 | 唤醒 task2(以及 task1),task2 运行 |
| … | … | 如此循环 |
可以看到,CPU 在绝大多数时间处于空闲状态,功耗大幅降低。
6. 阻塞的泛化:不只是延时
os_sleep 只是阻塞的一种特例。更通用的阻塞发生在等待信号量、消息队列、互斥锁时。它们的共同模式是:
- 检查资源是否可用。
- 如果不可用,将任务标记为
BLOCKED,并挂载到该资源的等待队列上。 - 调用
schedule()让出 CPU。 - 当资源可用时,由另一个任务或中断将该任务重新加入就绪队列。
我们将在第5讲实现这些通用的同步原语。
7. 注意事项:阻塞与临界区的交互
在阻塞期间,中断是开启的(否则 Tick 中断无法唤醒任务)。因此,阻塞函数内部不能长时间关中断。
一个常见的错误是在临界区内调用 os_sleep,比如:
enter_critical();
if (buffer_full) {
os_sleep(10); // 危险!当前中断已关闭,Tick 中断进不来,永远无法唤醒
}
exit_critical();
正确做法:在阻塞前退出临界区(或使用更高级的同步原语)。许多 RTOS 会提供 xxx_take() 函数,它们在内部会短暂关中断、检查资源、若不可用则加入等待队列,然后在调度前开中断。
8. 本讲小结 & 下集预告
今天我们添加了两个核心机制:
- 阻塞延时(
os_sleep):任务主动进入BLOCKED状态,释放 CPU,让其他任务运行。 - 空闲任务:当所有任务都阻塞时,运行最低优先级的空闲任务,它通常执行低功耗指令(如
wfi),极大降低系统功耗。
有了阻塞机制,我们的操作系统才能真正做到“无任务时不空转”,为后续更复杂的同步和通信原语打下基础。
下一讲:任务间的鹊桥——队列、信号量与互斥量
我们将学习如何在任务之间传递数据、同步动作,并解决经典的“优先级反转”问题,让任务间协作更加优雅。
✍️ 思考与练习
- 比较两种延时:在支持阻塞的 RTOS 中,分别用
os_sleep(100)和delay_ms(100)实现 LED 闪烁,画出 CPU 占用率波形图,说明差异。 - 低功耗陷阱:如果空闲任务只执行
wfi,而系统中有多个任务频繁唤醒(比如每 1ms 唤醒一次),CPU 的实际功耗还会很低吗?为什么? - 代码改进:当前阻塞队列扫描是 O(n)。如果任务数量很多,每次 Tick 都遍历会很耗时。请设计一个“分级时间轮”方案来优化唤醒效率。
- 思考:假设一个高优先级任务调用了
os_sleep,那么在它睡眠期间,中/低优先级任务是否可能运行?请用你熟悉的 RTOS 验证。
欢迎在评论区分享你对阻塞机制的理解或实验截图!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)