4、调度策略:基于优先级的抢占式调度、时间片轮转、协作式调度

调度策略,说白了就是操作系统决定「下一个该谁跑」的规则。Zephyr 在这方面做得挺灵活,它支持三种调度方式:基于优先级的抢占式调度、时间片轮转、以及协作式调度。这三种可以混着用,也可以单独用,完全看你的应用场景。

我个人习惯把调度策略比作「会议室的使用规则」。抢占式调度就像有个大老板,优先级高的人一来,优先级低的人就得让座。时间片轮转呢,就像大家轮流发言,每人说 10 分钟,时间到了换下一位。协作式调度最佛系——每个人自觉,说完了才让下一位上。

嗯,咱们一个一个来看。

4.1 基于优先级的抢占式调度

这是 Zephyr 默认的调度方式。每个任务都有一个优先级,数值越小优先级越高。Zephyr 支持 0 到 31 共 32 个优先级,其中 0 最高,31 最低。

抢占式调度的核心规则很简单:高优先级任务就绪时,立即打断低优先级任务的执行

我在项目中遇到过一个问题:一个数据采集任务优先级设得太高,结果把显示任务饿死了。屏幕半天不刷新,用户还以为死机了。后来我把采集任务优先级降了两级,显示任务优先级提了一级,问题就解决了。

关键点:抢占式调度保证了高优先级任务的实时性,但要注意优先级反转和任务饿死的问题。

Zephyr 中配置抢占式调度很简单,默认就是开启的。你只需要在创建线程时设置好优先级即可:

// 创建高优先级任务,优先级为 5
k_thread_create(&high_prio_thread, high_prio_stack,
                K_THREAD_STACK_SIZEOF(high_prio_stack),
                high_prio_entry, NULL, NULL, NULL,
                5, 0, K_NO_WAIT);

// 创建低优先级任务,优先级为 10
k_thread_create(&low_prio_thread, low_prio_stack,
                K_THREAD_STACK_SIZEOF(low_prio_stack),
                low_prio_entry, NULL, NULL, NULL,
                10, 0, K_NO_WAIT);

当高优先级任务就绪时,低优先级任务会被自动挂起。你想想看,这就像急诊病人来了,普通门诊就得先停一停。

避坑指南:我曾经在项目中把两个关键任务设成了相同优先级,结果发现它们互相抢 CPU,导致系统抖动。后来我才意识到,同优先级任务默认是不抢占的,需要配合时间片轮转才能公平分配。

4.2 时间片轮转

时间片轮转解决的是「同优先级任务怎么分 CPU」的问题。说白了,就是大家轮流来,每人分一段固定时间。

Zephyr 的时间片轮转是按优先级分组生效的。什么意思呢?就是只有处于同一优先级的任务之间才会轮转。不同优先级的任务之间,还是按抢占规则来。

配置时间片轮转需要两步:

  1. 在 prj.conf 中开启配置:CONFIG_TIMESLICING=y
  2. 在代码中调用 k_sched_time_slice_set() 设置时间片长度

举个例子:

// 设置时间片为 100 毫秒,仅对优先级 10 及以上的任务生效
k_sched_time_slice_set(100, 10);

这段代码的意思是:优先级 10 及以上的任务(数值越小优先级越高,所以这里指的是优先级 0-10 的任务),每个任务最多连续运行 100 毫秒。时间到了,就换下一个同优先级的任务运行。

我记得有一次做物联网网关项目,三个传感器数据采集任务优先级相同。没开时间片轮转时,第一个任务一直占着 CPU,后面两个任务的数据迟迟采集不到。开了时间片轮转后,三个任务轮流跑,数据采集就均匀了。

配置项 说明 默认值
CONFIG_TIMESLICING 开启时间片轮转功能 n
CONFIG_TIMESLICE_SIZE 默认时间片大小(毫秒) 0
CONFIG_TIMESLICE_PRIORITY 时间片生效的最高优先级 0

小技巧:时间片不要设得太短,否则频繁切换上下文会浪费 CPU。我一般设 50-200 毫秒,具体看任务的实时性要求。

4.3 协作式调度

协作式调度,也叫非抢占式调度。它的规则很简单:任务主动让出 CPU,调度器才会切换任务

在 Zephyr 中,协作式调度通过设置线程的 K_COOPERATIVE 标志来启用。或者更简单,把线程优先级设为负数(-1 到 -31),这些优先级默认就是协作式的。

协作式调度的好处是什么?没有抢占,就没有优先级反转的问题。任务可以安全地访问共享资源,不需要加锁。坏处呢?如果一个任务死循环或者忘记让出 CPU,整个系统就卡死了。

我曾经在一个工业控制项目中用过协作式调度。当时有个任务需要连续采集 1000 个数据点,中间不能被中断。用抢占式调度的话,每次采集都要加锁保护,太麻烦。改成协作式调度后,任务自己控制节奏,采集完主动调用 k_yield() 让出 CPU,代码清爽多了。

// 创建协作式任务,优先级设为 -2(负数表示协作式)
k_thread_create(&coop_thread, coop_stack,
                K_THREAD_STACK_SIZEOF(coop_stack),
                coop_entry, NULL, NULL, NULL,
                -2, 0, K_NO_WAIT);

// 任务内部主动让出 CPU
void coop_entry(void *arg1, void *arg2, void *arg3)
{
    while (1) {
        // 执行关键操作
        do_critical_work();
        
        // 主动让出 CPU
        k_yield();
        
        // 或者等待某个事件
        // k_sleep(K_MSEC(100));
    }
}

核心区别:抢占式调度由调度器决定何时切换,协作式调度由任务自己决定何时切换。

4.4 三种策略如何选择

你可能会问:实际项目中到底用哪种?我的建议是:

  • 大部分场景用抢占式调度:Zephyr 默认就是抢占式,适合大多数嵌入式应用。实时性要求高的任务设高优先级,普通任务设低优先级。
  • 同优先级任务多时加时间片轮转:比如多个传感器采集任务、多个通信处理任务,优先级相同,用时间片轮转保证公平。
  • 特定场景用协作式调度:比如需要原子操作、共享资源访问频繁、或者任务执行时间很短且确定。

其实这三种策略可以混合使用。Zephyr 允许你同时存在抢占式任务和协作式任务。抢占式任务优先级高,协作式任务优先级低。协作式任务运行时,高优先级的抢占式任务可以打断它。反过来,抢占式任务运行时,协作式任务只能等它主动让出 CPU。

嗯,这里要注意:协作式任务如果优先级设得比抢占式任务还高,那抢占式任务就永远没机会运行了。我见过有人踩过这个坑,排查了半天才发现是优先级设反了。

实战建议:刚开始做多任务设计时,先用默认的抢占式调度。等系统跑起来了,再根据实际需求调整。不要一开始就搞复杂,容易把自己绕进去。

好了,调度策略就讲到这里。下一章咱们聊聊 Zephyr 的同步机制——信号量、互斥锁和消息队列。这些东西在多任务编程中天天要用,学会了能省不少事。