不知道大家的童年记忆里面有没有过金庸武侠?不知道你们还记不记得倚天屠龙记中张三丰临时调教徒孙张无忌太极拳和太极剑的片段?当张三丰演示给张无忌拳法后询问他是否学会了?还记得张无忌说了什么吗?他的回答那时的我就觉得高深莫测,深不见底,我一叶障目不能窥视武当绝学的全貌。可是,你想过他为什么说全忘记了吗?
时至今日,以我目前看世界的方式,我还是不能得出一个答案,只不过将我的理解通过我今天无脑的提问相信能够映射出来。
自学了一段时间freeRTOS系统后,我就开始了裸机和操作系统内容界定不清楚的问题。感觉很明白,又不明白,最后全都不会,然后又全都明白。
无脑问题:
裸机中的任务为什么不能做到同时呢?这两者的区别是什么呢?难道操作系统中代码不是一步一步执行的吗?
答:
先明确概念,什么是同步?真的是同步进行的吗?
1. 什么是"同时"执行?
- 真正的同时:需要多核处理器,多个任务分别运行在不同的CPU核上,是真正的物理并行。
- 看似同时 :在单核处理器上,操作系统通过快速切换任务(即时间分片 )模拟出"同时执行"的效果,称为逻辑并行。
在单核裸机编程中,由于没有调度机制,无法实现逻辑并行,只能严格按照代码的顺序运行。
2. 裸机和操作系统的区别
3. 为什么裸机中不能做到"同时"?
裸机编程中代码按照顺序执行,具体原因如下:
-
缺乏调度器:
- 裸机程序没有调度器的概念,无法在不同任务间切换。
- 例如,在控制
LED1
和LED2
的闪烁时,如果程序在执行LED1
的逻辑,LED2
的逻辑必须等待。
-
单核串行执行:
- 单核处理器在任一时刻只能执行一个指令,裸机程序中没有机制在任务间快速切换。
-
无任务优先级或时间片:
- 裸机程序中,所有任务都以固定的顺序执行,没有优先级或时间片机制来模拟并发。
-
延时阻塞整个程序:
- 在裸机中使用延时(如简单的
for
循环延时或定时器延时),整个程序会被阻塞,无法继续执行其他逻辑。
- 在裸机中使用延时(如简单的
4. 操作系统如何做到"同时"?
操作系统利用任务调度和时间片的机制实现"看似同时"的执行:
-
任务切换:
- 操作系统会在特定条件下保存当前任务的状态(如寄存器、栈),然后加载另一个任务的状态,切换到新任务运行。
- 这种快速切换给人一种"同时运行"的错觉。
-
时间片:
- 操作系统将CPU时间划分为小片段,每个任务轮流获得一定的CPU时间。
- 例如:
LED1_Task
运行10ms。- 切换到
LED2_Task
运行10ms。 - 循环往复。
-
优先级:
- 操作系统可以根据任务的优先级决定执行顺序。例如,紧急任务可以抢占低优先级任务的CPU时间。
-
中断支持:
- 操作系统会处理中断,允许任务之间的灵活切换,而不需要程序员手动管理。
5. 示例对比:裸机与操作系统的LED控制
裸机代码
cpp
void main(void)
{
while (1)
{
LED1_ON;
Delay_ms(500); // 简单延时
LED1_OFF;
Delay_ms(500);
LED2_ON;
Delay_ms(500);
LED2_OFF;
Delay_ms(500);
}
}
现象:
LED1
先闪烁,然后LED2
闪烁。- 没有办法让
LED1
和LED2
同时闪烁,因为Delay
会阻塞整个程序。
操作系统代码
cpp
void LED1_Task(void* param)
{
while (1)
{
LED1_ON;
vTaskDelay(500); // 非阻塞延时
LED1_OFF;
vTaskDelay(500);
}
}
void LED2_Task(void* param)
{
while (1)
{
LED2_ON;
vTaskDelay(500); // 非阻塞延时
LED2_OFF;
vTaskDelay(500);
}
}
void main(void)
{
xTaskCreate(LED1_Task, "LED1", 128, NULL, 1, NULL);
xTaskCreate(LED2_Task, "LED2", 128, NULL, 1, NULL);
vTaskStartScheduler();
}
现象:
LED1
和LED2
以相同频率同时闪烁,因为操作系统通过调度实现了任务切换和并发。
6. 关键总结
- 裸机程序是单线程模型,所有逻辑是严格顺序执行的,无法做到逻辑并发。
- 操作系统引入了调度器,通过时间片和任务切换模拟并发,使多个任务可以"同时运行"。
- 操作系统的本质 :
- 本质上仍是一行行执行(单核情况下)。
- 通过快速任务切换,给人一种"同时执行"的感觉。
- 多核情况下,多个任务可真正并行。
如果需要多个任务看似同时运行,操作系统是必不可少的工具。
无脑问题:
操作系统如何通过调度实现了任务切换和并发?
操作系统通过任务调度 实现了任务切换和并发 ,主要依赖以下机制:任务状态管理、上下文切换以及调度策略。这些机制协同工作,使得多个任务能够看似"同时"运行,即逻辑并发。
1. 任务的生命周期和状态
在操作系统中,每个任务(或线程)都有一个生命周期和对应的状态。常见的任务状态包括:
任务的生命周期通常是以下过程:
- 创建任务 → 初始为 就绪态 → 被调度器选中后进入 运行态。
- 在执行过程中,可能因调用阻塞操作(如
vTaskDelay
)进入 阻塞态。 - 当阻塞条件解除后(如延时到期、信号量被释放),重新进入 就绪态。
2. 任务调度的核心:调度器
调度器是操作系统的核心组件,负责在多个任务中选择一个任务运行。调度器通过以下步骤实现任务切换和并发:
-
任务状态检查:
- 定期(通过系统时钟中断或其他触发事件)检查所有任务的状态。
- 找出当前处于就绪态的任务。
-
任务选择:
- 根据调度策略,选择一个合适的任务进入运行态。
- 调度策略(见下文)决定了任务选择的优先级。
-
上下文切换:
- 在切换到新任务前,保存当前任务的上下文信息(如寄存器、程序计数器、栈指针)。
- 恢复新任务的上下文信息,使其从上次暂停的地方继续执行。
通过频繁的任务切换,调度器在多个任务间快速切换,给人一种"同时运行"的假象。
3. 任务切换的关键:上下文切换
上下文切换是任务切换的核心技术。以下是上下文切换的主要步骤:
上下文内容
- CPU寄存器的值(如通用寄存器、程序计数器、栈指针)。
- 当前任务的状态信息(任务控制块 TCB)。
- 堆栈内容(保存任务执行时的局部变量和返回地址)。
切换过程
-
保存当前任务上下文:
- 将当前任务的CPU寄存器值保存到任务的堆栈或控制块中。
-
更新调度器信息:
- 调度器选择一个新任务,将新任务的状态从就绪态 改为运行态。
- 将当前任务的状态更新为就绪态 或阻塞态。
-
恢复新任务上下文:
- 从新任务的控制块或堆栈中恢复寄存器值和程序计数器。
- CPU跳转到新任务的代码位置,开始执行。
上下文切换是快速且透明的,用户通常感知不到切换过程。
4. 调度策略:决定谁先执行
调度策略决定了哪个任务优先获得CPU资源。常见的调度策略包括:
抢占式调度
- 高优先级任务可以抢占低优先级任务的CPU时间。
- 适用于实时系统,优先保证关键任务的及时性。
时间片轮转
- 每个任务按时间片(例如10ms)轮流执行。
- 时间片到期后,调度器切换到下一个任务。
优先级调度
- 任务被赋予不同的优先级。
- 高优先级任务优先获得CPU。
多级队列调度
- 任务根据优先级、类型等分类到不同队列中。
- 调度器先调度高优先级队列中的任务。
事件触发调度
- 某些任务因外部事件(如中断或信号)触发进入就绪态,调度器立即运行这些任务。
5. 时间片和抢占实现并发
操作系统通过定期触发时间片中断实现任务切换:
- 每隔固定时间(如1ms),系统时钟中断触发调度器运行。
- 调度器检查是否有高优先级任务需要运行。
- 如果当前任务用完时间片,调度器切换到其他就绪任务。
抢占式调度确保关键任务能及时运行,例如:
- 如果一个高优先级任务进入就绪态,调度器立即停止当前任务,切换到高优先级任务。
6. 示例:FreeRTOS任务切换过程
以下是FreeRTOS中任务切换的简化流程:
- 任务运行 :
- 当前任务(
Task_A
)正在运行。
- 当前任务(
- 时间片到期 :
- 系统时钟中断触发。
- FreeRTOS调用调度器函数(
vTaskSwitchContext
)。
- 保存上下文 :
- 保存
Task_A
的寄存器和堆栈信息。
- 保存
- 选择新任务 :
- 调度器根据策略选择
Task_B
。
- 调度器根据策略选择
- 恢复上下文 :
- 恢复
Task_B
的寄存器和堆栈信息。
- 恢复
- 继续执行 :
- CPU跳转到
Task_B
,任务切换完成。
- CPU跳转到
7. 操作系统的并发能力来源
-
快速任务切换:
- 单核处理器无法真正同时运行多个任务,但通过快速切换任务(如每1ms),任务看似同时运行。
- 例如,两个LED任务以不同时间片交替运行时,LED会看似同时闪烁。
-
任务阻塞与解阻:
- 如果一个任务因等待I/O或信号量而阻塞,调度器自动运行其他任务,最大化利用CPU时间。
-
抢占式优先级:
- 关键任务可以抢占低优先级任务的CPU时间,确保高优先级任务的实时性。
8. 总结
操作系统通过以下关键技术实现了任务切换和并发:
- 任务状态管理:跟踪每个任务的运行状态。
- 上下文切换:保存和恢复任务的执行环境。
- 调度策略:根据优先级、时间片等策略选择任务。
- 时间片中断:周期性触发任务切换,模拟并发。
尽管单核CPU在任意时刻只能运行一个任务,但操作系统的调度机制让多个任务看似同时运行,从而实现逻辑上的并发。