单核跑几百进程是障眼法——任务间切片切换,切得够快就"同时"。从MCU裸机到FreeRTOS抢占再到Linux CFS,调度器决定了谁拿CPU。
一、并发本质---时间片轮转
并发不是并行。并行要多个核同时干活,并发是一个核在多个任务间切片切换。一个收银员给三个人排队结账是并发,三个收银员同时开工才是并行。
单核板上音乐在放、串口在收数据、按键要响应——靠的就是时间片切片。
切换不是免费的。时间片太短,CPU全耗在切换上;太长,按键100ms才响应,用户摔鼠标。好的调度器目标不是让一个任务最快,而是让所有任务都觉得"不卡"。
本节要点:并发是单核分时复用,调度器的核心矛盾是切换开销与响应延迟的权衡。
二、调度策略演进---从协作到公平
演进逻辑一句话:协作式一个死循环全系统卡死 → 抢占式OS强制切换但低优先级可能饿死 → 公平调度按比例分配谁都饿不死,但最紧急的事可能多等一会。
本节要点:调度策略从协作到抢占再到公平,核心矛盾从"卡死"演进到"饿死"再演进到"响应延迟"。
三、SuperLoop裸机 没有调度器的世界
每个嵌入式工程师都写过这个while(1):
int main(void){ init_all();while (1) { read_sensor_task(); // 读传感器,2ms process_data_task(); // 处理数据,5ms upload_data_task(); // 上传数据,50ms ← 卡在这 check_button_task(); // 按键,0.1ms }}
upload_data_task()阻塞50ms,按键50ms后才响应——这就是SuperLoop的死穴。裸机"并发"全靠中断硬扛,而中断的坑最难查:共享变量没加volatile被编译器优化、中断里调printf丢嵌套。
救SuperLoop的唯一办法是状态机,把长任务拆成每次几十微秒的小片段:
typedef enum { UPLOAD_IDLE, UPLOAD_SENDING, UPLOAD_WAIT_ACK } upload_state_t;void upload_data_task(void){static upload_state_t state = UPLOAD_IDLE;switch (state) {case UPLOAD_IDLE:if (data_ready) { start_uart_send(data); state = UPLOAD_SENDING; }break;case UPLOAD_SENDING:if (uart_tx_complete()) state = UPLOAD_WAIT_ACK;break; // 每个case执行<100μs,快速返回case UPLOAD_WAIT_ACK:if (ack_received) state = UPLOAD_IDLE;break; }}
状态机能解决卡顿,但本质是你在手写调度逻辑。复杂到一定程度,你就在手搓一个RTOS——这时候该上FreeRTOS了。
本节要点:SuperLoop用状态机拆长任务换取响应性,但手写状态机等于手写调度器,复杂度有上限。
四、FreeRTOS抢占 优先级调度实战
调度规则:永远让最高优先级的就绪任务运行。中断唤醒了高优先级任务,立刻抢占当前任务;高优先级阻塞了,回到低优先级继续。
怎么O(1)找到最高优先级?FreeRTOS用优先级位图+CLZ指令:
// 就绪位图 uxTopReadyPriority:每一位代表一个优先级是否就绪// bit0=优先级0就绪, bit3=优先级3就绪, ... bit31=优先级31就绪// 找最高优先级:一条CLZ指令,O(1)最高优先级 = 31 - __CLZ(uxTopReadyPriority)
三种配置模式:
抢占式的代价是优先级反转:高优先级等低优先级的锁时,被中等优先级插队反卡死——靠优先级继承补救,治本则别让不同优先级抢同一把锁。
本节要点:FreeRTOS用位图+CLZ实现O(1)调度,核心代价是优先级反转,需用优先级继承或锁隔离来规避。
五、Linux CFS vruntime公平调度
FreeRTOS那套在Linux上行不通——一个系统跑200+进程,严格优先级会让SSH饿死,系统直接废。
CFS的核心规则就一条:给每个进程一个vruntime计数器,谁vruntime最小就选谁跑。
vruntime += 实际运行时间 × (NICE_0_LOAD / 进程权重)nice值越低 → 权重越高 → vruntime长得慢 → 更容易被选中
你只调nice(-20~+19)表达"谁更重要",CFS自动把"重要"翻译成"多拿CPU"。nice每差1级,CPU份额约差10%。
时间片按权重动态算,进程多了每片保底0.75ms,避免切换开销吞噬CPU。红黑树存就绪队列,找最小vruntime从链表O(n)降到O(1)取最左(缓存leftmost指针)。
实时策略补充:CFS只管普通进程(SCHED_OTHER),实时任务用SCHED_FIFO(跑到阻塞)或SCHED_RR(同优先级轮转),优先级1-99,永远抢占CFS。
切换代价:Linux切换比FreeRTOS慢几个数量级(数μs vs 1.4μs),因为要保存MMU上下文、浮点寄存器、刷新TLB。
本节要点:CFS用vruntime实现按权重公平分配CPU,红黑树O(1)选任务,实时进程用SCHED_FIFO/RR补充。
六、调度配置详解 从编译期到运行期
FreeRTOS的配置在FreeRTOSConfig.h,运行期通过任务API调:
// FreeRTOSConfig.h 编译期宏#define configUSE_PREEMPTION 1 // 1=抢占式, 0=协作式#define configUSE_TIME_SLICING 1 // 同优先级时间片轮转#define configMAX_PRIORITIES 7 // 优先级上限#define configTICK_RATE_HZ 1000 // tick频率// 任务创建指定优先级,运行期可动态调整xTaskCreate(task_func, "name", 256, NULL, 5, &handle);vTaskPrioritySet(handle, 5); // 改优先级vTaskSuspend(handle); // 挂起 / vTaskResume() 恢复
Linux不用重新编译,运行期随时改:
# 命令行nice -n 10 ./program # 启动时指定nicerenice -n 5 -p 1234 # 运行期改nicechrt -f 80 ./program # SCHED_FIFO 优先级80chrt -r 50 ./program # SCHED_RR 优先级50
// 代码接口setpriority(PRIO_PROCESS, 0, 5); // 调nicesched_setscheduler(0, SCHED_FIFO, ¶m); // 改策略
# /proc调CFS参数 + cgroup限带宽/proc/sys/kernel/sched_latency_ns # target_latency 默认6ms/proc/sys/kernel/sched_min_granularity_ns # min_granularity 默认0.75msecho 512 > cpu.weight # cgroup权重 1-10000echo "50000 100000" > cpu.max # 50% CPU配额
控制权和通用性不可兼得。SuperLoop全控制但没调度器;CFS超通用但你几乎控制不了细节;FreeRTOS在中间——有调度器,但你说了算。
注意事项
| | |
|---|
| | |
| | |
| | |
| | SCHED_FIFO |
| | 打PREEMPT_RT补丁或用sched_setscheduler |
本节要点:FreeRTOS配置编译期定策略、运行期调优先级;Linux运行期随时改策略和nice,cgroup圈带宽——控制粒度与通用性互斥。
- 单核并发本质是时间片切片,调度器核心矛盾是切换开销与响应延迟的权衡
- SuperLoop用状态机拆长任务,FreeRTOS用位图+CLZ做O(1)抢占调度,CFS用vruntime+红黑树做公平分配
- FreeRTOS优先级反转靠优先级继承,Linux实时进程需cgroup限带宽防饿死
- 控制权和通用性不可兼得——从裸机到Linux,你交出控制权换回通用性,调度器替你做选择