SCHED_FIFO 和 SCHED_RR 同时存在时,如何调度

1 问题背景

SCHED_FIFO 和 SCHED_RR 同为 linux rt 调度类中的两个调度策略。

SCHED_FIFO 是先入先出的调度顺序,如下图所示,调度队列中依次入队了 P1, P2, P3, P4 4 个线程,调度策略均是 FIFO,并且假设优先级相同,4 个线程都绑定到了一个 cpu 核上来运行。之所以做这两个假设,是因为如果优先级不同,那么体现不出 FIFO 的特点,因为高优先级可以抢占低优先级;如果不绑定到一个核上运行,而是可以在多个核上运行,那么也体现不出 FIFO 的特点,多个线程可以在多个核上并发运行。

如下图所示,线程的执行顺序是 P1 --> P2 --> P3 --> P4。如果 P1 没有运行完,那么 P2 永远没有运行的机会,以此类推;只有 P1 运行完的时候,P2 才有机会运行。假如 P1 里边有一个 while(1) 循环,并且循环中什么都没有做,只是一个死循环,那么 P1 就永远运行不完,P2 永远没有运行的机会;假如 P1 里边调用了 sleep() 进行睡眠,或者进行磁盘 io 或者网络 io 的时候发生了阻塞,或者 P1 调用了 api 主动让出了 cpu,那么这个时候 P1 就会让出 cpu,P2 获得运行的机会。

SCHED_RR 中的 RR 全称是 round robin,轮询,SCHED_RR 是有时间片的调度策略,一个线程将时间片用完的时候会触发调度。如果有下边 4 个线程,调度策略均是 SCHED_RR,优先级相同,并且绑定到了同一个 cpu 核上,那么 4 个线程是轮流运行的,即使 P1 中有一个 while(1) 死循环,一直不主动让出 cpu,那么后边的 P2, P3, P4 也会有运行的机会。

当 FIFO 的线程和 RR 的线程同时存在的时候,怎么调度呢 ? 如下图所示,有 4 个线程,调度策略依次是 FIFO, RR, RR, FIFO,那么如果 P1 不主动让出 cpu,后边的 P2 和 P3 会得到调度吗 ?如果 P2 和 P3 不主动让出 cpu,后边的 P4 能得到调度吗 ?

本文通过实验的方式来观察一下调度规律。

2 实验验证

实验代码:

如下代码,创建了 4 个线程,分别是 t1,t2,t3,t4,4 个线程的入口函数分别是 thread_entry1,thread_entry2,thread_entry3,thread_entry4。4 个线程间隔 1s 进行创建,保证了线程进入运行态的顺序就是 t1, t2, t3, t4 的顺序。

调度策略设置:

函数 set_rr(int prio) 可以将线程的调度策略设置为SCHED_RR,形参是优先级;set_fifo(int prio) 可以将线程的调度策略设置为 SCHED_FIFO,形参是优先级。

cpu 亲和性设置:

set_affinity() 可以设置线程的亲和性,代码中把所有线程都绑定到 2 核上。

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <stdio.h>
#include <linux/types.h>
#include <sched.h>
#include <string.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

#define BIND_CPU_CORE 2

int set_rr(int prio) {
  struct sched_param sp = {.sched_priority = prio};
  int policy = SCHED_RR;
  return sched_setscheduler(0, policy, &sp);
}

int set_fifo(int prio) {
  struct sched_param sp = {.sched_priority = prio};
  int policy = SCHED_FIFO;
  return sched_setscheduler(0, policy, &sp);
}

int32_t set_affinity() {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(BIND_CPU_CORE, &cpuset);

    if (sched_setaffinity(0, sizeof(cpuset), &cpuset) != 0) {
       printf("bind cpu error
");
       return -1;
    }
    return 0;
}

void *thread_entry1(void *data) {
  set_fifo(90);
  set_affinity();
  printf("thread 1
");
  while(1);
}

void *thread_entry2(void *data) {
  set_fifo(90);
  set_affinity();
  printf("thread 2
");
  while(1);
}

void *thread_entry3(void *data) {
  set_fifo(90);
  set_affinity();
  printf("thread 3
");
  while(1);
}

void *thread_entry4(void *data) {
  set_fifo(90);
  set_affinity();
  printf("thread 4
");
  while(1);
}


int main() {
  pthread_t t1;
  pthread_t t2;
  pthread_t t3;
  pthread_t t4;

  pthread_create(&t1, NULL, thread_entry1, NULL);
  sleep(1);

  pthread_create(&t2, NULL, thread_entry2, NULL);
  sleep(1);

  pthread_create(&t3, NULL, thread_entry3, NULL);
  sleep(1);

  pthread_create(&t4, NULL, thread_entry4, NULL);
  sleep(1);

  sleep(100);
  return 0;
}

2.1 全是 FIFO

 4 个线程的优先级均是 90,上述代码,只有 t1 在运行,t2,t3,t4 得不到运行的机会。

如果将 t2, t3,t4 其中的优先级设置比 90 高,比如将 t4 的优先级设置为 91, 那么 t4 可以抢占 t1 运行,并且 t4 运行之后,其它线程没有机会运行。

2.2 全是 RR

将上述代码中的 set_fifo() 改成 set_rr(),那么 4 个线程就会被设置成 SCHED_RR 调度策略。

上述代码编译运行之后,4 个线程均会得到运行,线程中的打印都能打印出来。

4 个线程的调度策略均为 SCHED_RR,并且优先级相等,这种情况下,4 个线程都能得到调度的机会,在一个 cpu 核上,分别占用 25% 左右的 cpu。

如果将其中一个线程的优先级设置的比其它线程大,比如将 t2 的优先级设置为 91,那么 t1 会打印日志,t2 会打印日志,但是 t3 和 t4 中的日志不会打印出来。SCHED_RR 虽然是有时间片的调度策略,但这是在优先级相等的前提下,如果这个线程比其它 SCHED_RR 的线程优先级高,那么这个线程就会一直运行,不会在多个 RR 的线程之间轮询。

linux 中为了防止实时调度策略占满所有 cpu,有一个配置可以限制实时调度策略所能占用的 cpu 的最大值,如下图所示,默认是 95%。如我们上边做的实验,即使有一个高优先级的实时调度策略一直运行,默认情况下也只能消耗 95% 的 cpu,并不是 100%,线程在每一秒还会被调度一次。

当然,如果把如下配置修改成 1000000,那么实时调度策略就能占用 100% 的 cpu,不会被调度。

2.3 FIFO 和 RR 混合

(1)将 t1 和 t4 设置成 SCHED_FIFO 调度策略,t2 和 t3 设置成 SCHED_RR 调度策略,优先级都设置为 90

t1 会打印,t2,t3, t4 不会打印。

(2)将 t1 和 t4 设置为 SCHED_RR 调度策略,t2 和 t3 设置为 SCHED_FIFO 调度策略,优先级都设置为 90

t1 个 t2 会打印,t3 和 t4 不会打印。

2.4 总结

(1)从上边的实验现象可以看出来,实验现象符合 SCHED_FIFO 先入先出,SCHED_RR 时间轮询的调度原理。

(2)在实时调度策略中,高优先级具有绝对的优先调度的的特权,即使 SCHED_RR 这种时间片轮询的调度算法,也是说的多个 SCHED_RR 的优先级相等的前提下才会轮询,如果优先级不相等,那么只会调度优先级最高的那个线程。