13、线程调度策略:SCHED_OTHER、SCHED_FIFO、SCHED_RR在SMP下的行为

各位同学,今天我们来聊聊调度策略。这个话题,说实话,在单核时代挺简单的。但一到了多核(SMP)环境下,事情就变得有意思了。

我记得刚接触RT-Thread SMP时,第一个让我头疼的问题就是:同一个调度策略,在单核和双核上,表现竟然完全不同。你想想看,一个线程在单核上跑得好好的,换到双核上突然就“不听话”了。这不是bug,而是SMP调度机制本身带来的变化。

RT-Thread支持三种调度策略:SCHED_OTHERSCHED_FIFOSCHED_RR。咱们一个一个来看,它们在SMP下到底怎么工作的。

13.1 SCHED_OTHER:时间片轮转的“平民”策略

SCHED_OTHER是默认策略。说白了,就是大家轮流用CPU,每人分一个时间片。在单核上,这很简单——一个就绪队列,线程排队,时间片到了就切换。

但在SMP下呢?

RT-Thread SMP的实现里,每个CPU核心都有自己的就绪队列。当一个SCHED_OTHER线程被创建时,调度器会把它分配到某个核心的就绪队列中。然后,这个线程就只会在那个核心上被调度。

核心要点:SCHED_OTHER线程在SMP下是“绑定”到某个核心的,不会随意迁移。

为什么会这样?我个人习惯的理解是:为了减少缓存抖动。如果一个线程在Core0上跑了一会儿,突然被迁移到Core1上,那Core0的L1/L2缓存里的数据就全废了。这在实时系统中是不可接受的。

举个例子:

/* 创建两个SCHED_OTHER线程 */
rt_thread_t thread1 = rt_thread_create("t1", entry1, NULL, 1024, 20, 10);
rt_thread_t thread2 = rt_thread_create("t2", entry2, NULL, 1024, 20, 10);

rt_thread_startup(thread1);
rt_thread_startup(thread2);

在双核系统上,这两个线程大概率会被分配到不同的核心上。每个核心各自轮转自己的时间片。你想想看,这其实相当于每个核心都在独立运行一个单核调度器。

我的经验:我曾经在一个项目中,把8个同等优先级的SCHED_OTHER线程丢到4核系统上。结果发现,有些核心忙死,有些核心闲死。后来才意识到,RT-Thread SMP的负载均衡是“懒惰”的——它不会主动迁移线程来平衡负载。所以,如果你有多个同等优先级的线程,最好手动指定它们的CPU亲和性。

13.2 SCHED_FIFO:先来先服务的“霸道”策略

SCHED_FIFO是实时策略。它的规则很简单:只要线程就绪,就一直运行,直到它主动让出CPU(比如调用rt_thread_delay或等待信号量)。

在单核上,这很好理解。但在SMP下呢?

嗯,这里要注意:SCHED_FIFO在SMP下也是“绑定”到某个核心的。但它的行为比SCHED_OTHER更“霸道”。

假设Core0上有一个SCHED_FIFO线程在运行,优先级是30。Core1上有一个SCHED_OTHER线程在运行。这时候,Core0上的SCHED_FIFO线程如果调用了rt_sem_take等待信号量,它会被挂起。Core0会从自己的就绪队列中选下一个线程运行。

但问题来了:如果Core0上还有一个优先级更高的SCHED_FIFO线程就绪了,它会立即抢占Core0上当前运行的线程。这就是SCHED_FIFO的“霸道”之处——它不关心其他核心在干什么,只关心自己所在核心的调度。

避坑指南:我曾经犯过一个错误——在双核系统上,把两个高优先级的SCHED_FIFO线程分别绑定到Core0和Core1上。结果这两个线程互相等待对方释放资源,形成了死锁。因为SCHED_FIFO线程不会主动让出CPU,除非它自己阻塞。所以,在SMP下使用SCHED_FIFO,一定要小心资源竞争和死锁

13.3 SCHED_RR:带时间片的“绅士”策略

SCHED_RRSCHED_FIFO很像,唯一的区别是:SCHED_RR有时间片限制。如果一个SCHED_RR线程用完了它的时间片,它会被放到就绪队列的末尾,让同优先级的其他线程运行。

在SMP下,SCHED_RR的行为和SCHED_FIFO基本一致,只是多了时间片轮转的机制。

举个例子:

/* 创建两个SCHED_RR线程,优先级相同 */
rt_thread_t thread_a = rt_thread_create("ta", entry_a, NULL, 1024, 25, 10);
rt_thread_t thread_b = rt_thread_create("tb", entry_b, NULL, 1024, 25, 10);

/* 设置调度策略为SCHED_RR */
rt_thread_control(thread_a, RT_THREAD_CTRL_SET_SCHED, (void*)SCHED_RR);
rt_thread_control(thread_b, RT_THREAD_CTRL_SET_SCHED, (void*)SCHED_RR);

rt_thread_startup(thread_a);
rt_thread_startup(thread_b);

在双核系统上,这两个线程大概率会被分配到不同的核心。每个核心上,SCHED_RR线程会和其他同优先级的线程轮转时间片。但如果某个核心上只有一个SCHED_RR线程,那它就会一直运行,直到被更高优先级的线程抢占。

关键区别:SCHED_FIFO和SCHED_RR在SMP下的核心行为是一样的——都是绑定到某个核心,都是实时策略。唯一的区别是SCHED_RR多了时间片轮转,防止某个线程“饿死”同优先级的其他线程。

13.4 三种策略在SMP下的对比

咱们用一张表来总结一下:

策略 是否实时 是否绑定核心 时间片 典型场景
SCHED_OTHER 是(默认绑定) 普通任务、后台任务
SCHED_FIFO 是(默认绑定) 高实时性、短任务
SCHED_RR 是(默认绑定) 实时任务、需要公平轮转

你可能会问:为什么在SMP下,这三种策略都默认绑定核心?

嗯,这个问题问得好。我个人习惯的理解是:RT-Thread SMP的设计哲学是“简单可靠”。如果允许线程自由迁移,那调度器的复杂度会急剧上升——需要处理缓存一致性、迁移开销、优先级反转等问题。而绑定核心,虽然牺牲了一定的负载均衡能力,但换来了调度行为的可预测性。

我的建议:在实际项目中,如果你需要线程在多个核心间迁移,可以手动设置CPU亲和性。比如,用rt_thread_controlRT_THREAD_CTRL_SET_AFFINITY命令,把线程的亲和性设置为所有核心。但要注意,这会带来额外的迁移开销。

13.5 一个实际案例

我记得在一个工业控制项目中,我们需要在双核系统上同时处理高速数据采集和用户界面响应。

数据采集任务要求极高的实时性,我用SCHED_FIFO策略,绑定到Core0。用户界面任务对实时性要求不高,但需要流畅响应,我用SCHED_OTHER策略,绑定到Core1。

结果呢?Core0上的数据采集任务稳定运行,没有一次超时。Core1上的用户界面任务虽然偶尔被其他后台任务打断,但整体响应很流畅。

这个案例说明:在SMP下,合理分配调度策略和CPU亲和性,可以充分发挥多核的优势

13.6 总结

好了,咱们来捋一捋:

  • SCHED_OTHER:默认策略,有时间片,绑定核心,适合普通任务。
  • SCHED_FIFO:实时策略,无时间片,绑定核心,适合高实时性任务。
  • SCHED_RR:实时策略,有时间片,绑定核心,适合需要公平轮转的实时任务。

在SMP下,这三种策略的核心行为都是“绑定到某个核心”。区别在于时间片和抢占行为。你想想看,这其实和单核调度很像,只是每个核心独立运行自己的调度器。

下一章,我们会聊聊线程优先级反转在SMP下的表现。嗯,那又是一个让人头疼的话题。到时候见!