Skip to main content
 首页 » 操作系统

Linux RT调度学习笔记

2022年07月19日28over140

一、RT进程简介

1. 什么是RT进程

进程优先级为0--99为实时进程,判断实时进程的方法就是判断进程的优先级是否是小于100。

#define MAX_USER_RT_PRIO    100    //实时进程小于100 
#define MAX_RT_PRIO         100 
#define MAX_PRIO            140    //进程最大优先级不超过140 
#define DEFAULT_PRIO        120    //普通进程的默认优先级为120 
 
/* Convert user-nice values [ -20 ... 0 ... 19 ] */ 
#define NICE_TO_PRIO(nice)    (nice+120) 
#define PRIO_TO_NICE(prio)    (prio-120) 
#define USER_PRIO(p)        (p-100) //p-100 
#define MAX_USER_PRIO        40 
 
/* 判断是否是RT进程的函数 */ 
static inline int rt_prio(int prio) //include/linux/sched/rt.h 
{ 
    if (unlikely(prio < MAX_RT_PRIO)) 
        return 1; 
    return 0; 
}

2. RT进程的 sched_class 比fair的优先级高,只要RT进程就绪就优先调度,除非RT进程被throttled.

3. RT调度器有一个优先级链表,即为每一个优先级的进程都维护一个链表,满足条件的进程(不是正在运行&CPU亲和性大于1)还会被放到pushable_tasks链表中
以便推到其它CPU上去运行。

4. linux内核中提供了两种实时调度策略:SCHED_FIFO和SCHED_RR,其中RR是带有时间片的FIFO。这两种调度算法实现的都是静态优先级。内核不为实时进程计算动态优先级。这能保证给定优先级别的实时进程总能抢占优先级比他低得进程。
(1) SCHED_FIFO 先进先出队列调度,高优先级任务一直运行,直到任务阻塞,或者主动退出,或者被更高优先级任务抢占。
(2) SCHED_RR 时间片轮转调度,每个实时任务划分一个时间片,时间片用完会切换到其他任务执行。

5. linux的实时调度算法提供了一种软实时工作方式。实时优先级范围从0到MAX_RT_PRIO减一(即是[0--99])。SCHED_NORMAL级的非实时进程的nice值共享了这个取值空间,它的取值范围是从MAX_RT_PRIO到MAX_RT_PRIO+40(即[100--140))。也就是说,在默认情况下,nice值从-20到19直接对应的是从100到139的优先级范围,这就是普通进程的静态优先级范围。

6. 在实时调度策略下。schedule()函数的运行会关联到实时调度类rt_sched_class。

7. CFS调度器是通过红黑树来组织调度实体,而RT调度器使用的是优先级队列rt_prio_array来组织实时调度实体;

二、RT调度相关结构体

1. struct rt_rq(rt的runqueue)

struct rt_rq { 
    /* 优先级链表,所有入队的rt task都按照优先级顺序挂入到active->queue链表中. 
    即每个优先级就有一个对应的链表,为active->queue[prio]. */ 
    struct rt_prio_array    active; 
    /* FIFO/RR类型task数量 */ 
    unsigned int        rt_nr_running; 
    /* RR类型task的数量 */ 
    unsigned int        rr_nr_running; 
#if defined CONFIG_SMP || defined CONFIG_RT_GROUP_SCHED 
    /* curr表示当前队列里面最高优先级的task的优先级(一般为当前正在运行的task), 
       next表示 pushable_tasks 链表最高优先级的数值,也是下次最可能运行的task的优先级。*/ 
    struct { 
        int        curr; /* highest queued rt task prio */ 
#ifdef CONFIG_SMP 
        /* 下一个要运行的RT任务的优先级,如果两个任务都有最高优先级, 
           则curr == next */ 
        int        next; /* next highest */ 
#endif 
    } highest_prio; 
#endif 
#ifdef CONFIG_SMP 
    /* 可以被迁移(cpu affinity num > 1)的rt task的数量,也可以用于确定有没有绑到哪个核上*/ 
    unsigned long        rt_nr_migratory; 
    /* rt_nr_total是入队的数量,也用于overload检查 */ 
    unsigned long        rt_nr_total; 
    /* rt_nr_total>1,rt_nr_migratory !=0,则在update_rt_migration()中设置overloaded=1*/ 
    int                    overloaded; 
    /* 每个task也有一个pushable_tasks链表节点,所有入队的rt task全部挂入(有条件) 
       到pushable_tasks链表头中,当task出队时,将其从此链表中清除。 
       pushable_tasks是一个按照优先级大小有序的链表,用的地方: 当前运行的task优先级变小了, 
       看看pushable_tasks链表里面是否有更高优先级的task,有的话可能会选择(看是否符合运行的条件) 
 
       优先级列表,用于推送过载任务*/ 
    struct plist_head    pushable_tasks; 
 
#endif /* CONFIG_SMP */ 
    /* 表示rt_rq是否在rq上,在执行过程中可能dequeue掉rt_rq */ 
    int            rt_queued; 
    /* rt_rq是否throttled,用于限流操作*/ 
    int            rt_throttled; 
    /* rt task 已经累加的运行时间,超出了本地rt_runtime时,则进行限制 */ 
    u64            rt_time; 
    /* 系统分配给本地的运行时间配额,配额使用完毕,要么借用,要么被throttled */ 
    u64            rt_runtime; 
    /* Nests inside the rq lock: */ 
    raw_spinlock_t        rt_runtime_lock; 
 
#ifdef CONFIG_RT_GROUP_SCHED 
    /* 用于优先级翻转问题解决 */ 
    unsigned long        rt_nr_boosted; 
    /* 指向运行队列 */ 
    struct rq            *rq; 
    /* 指向任务组 */ 
    struct task_group    *tg; 
#endif 
};

2.struct sched_rt_entity(rt task的调度实体)

struct sched_rt_entity { 
    /* 此调度实体挂入到队列中.因为rt task都是按照优先级入队的.优先级高的放在队头, 
       否则放置在队尾,其实就是挂在优先级链表数组中. */ 
    struct list_head        run_list; 
    /* 设置的时间超时 */ 
    unsigned long            timeout; 
    /* 用于记录jiffies值 */ 
    unsigned long            watchdog_stamp; 
    /* RR task类型分配的时间片,在__sched_fork()中默认初始化为(100 * HZ / 1000). */ 
    unsigned int            time_slice; 
    /* 入队成功置为1 */ 
    unsigned short            on_rq; 
    /* 通过run_list挂接到优先级数组链表中,置为1 */ 
    unsigned short            on_list; 
    /* rt进程的组调度有关,通过其back域组成单链表 */ 
    struct sched_rt_entity        *back; 
#ifdef CONFIG_RT_GROUP_SCHED /* 组调度相关的信息 */ 
    struct sched_rt_entity        *parent; 
    /* rq on which this entity is (to be) queued: */ 
    struct rt_rq            *rt_rq; 
    /* rq "owned" by this entity/group: */ 
    /* 使能rt组调度时,调度组内部的rt_rq,没有使能的话,此值必须为NULL。 
       调度组也是使用sched_rt_entity表示的,若my_q=NULL表示的是调度组,而不是进程。 
    */ 
    struct rt_rq            *my_q; 
#endif 
} __randomize_layout;

注:__randomize_layout会使结构体成员随机存放,增加黑客通过偏移访问成员变量的难度。

3.struct task_struct 涉及到rt调度的成员变量

struct task_struct {   
    ... 
    /* task是否在rq上 */ 
    int                             on_rq;   
    /* task的优先级 */ 
    int                             prio;   
    /* task的静态优先级,一般与prio是一致的 */ 
    int                             static_prio;   
    /* 归一化的优先级 */ 
    int                             normal_prio;   
    unsigned int                    rt_priority;   
    const struct sched_class        *sched_class;   
    /* cfs task的调度实体 */ 
    struct sched_entity             se;   
    /* rt task的调度实体 */ 
    struct sched_rt_entity          rt;   
    ...   
    /* task的cpu affinity的数量 */ 
    int                             nr_cpus_allowed;   
    /* 上层通过调度参数设置新的cpu affinity mask,会update这个mask. */   
    cpumask_t                       cpus_allowed;   
    ...   
    /*符合入队条件的task,按照优先级将此plist node插入到rt_rq的plist_head中.*/ 
    struct plist_node               pushable_tasks; 
    /* 调度策略,如SCHED_RR */ 
    unsigned int            policy; 
    /* TASK_RUNNING、TASK_STOPPED等进程状态描述*/ 
    volatile long            state; 
}  

4. struct sched_class rt_sched_class

const struct sched_class rt_sched_class = { 
    .next            = &fair_sched_class, 
    .enqueue_task        = enqueue_task_rt, 
    .dequeue_task        = dequeue_task_rt, 
    .yield_task        = yield_task_rt, 
 
    .check_preempt_curr    = check_preempt_curr_rt, 
 
    .pick_next_task        = pick_next_task_rt, 
    .put_prev_task        = put_prev_task_rt, 
 
#ifdef CONFIG_SMP 
    .select_task_rq        = select_task_rq_rt, 
 
    .set_cpus_allowed       = set_cpus_allowed_common, 
    .rq_online              = rq_online_rt, 
    .rq_offline             = rq_offline_rt, 
    .task_woken        = task_woken_rt, 
    .switched_from        = switched_from_rt, 
#endif 
 
    .set_curr_task          = set_curr_task_rt, 
    .task_tick        = task_tick_rt, 
 
    .get_rr_interval    = get_rr_interval_rt, 
 
    .prio_changed        = prio_changed_rt, 
    .switched_to        = switched_to_rt, 
 
    .update_curr        = update_curr_rt, 
};

5. struct rt_bandwidth RT进程带宽限制结构

struct rt_bandwidth { 
    /* nests inside the rq lock: */ 
    raw_spinlock_t        rt_runtime_lock; 
    /* 时间周期 */ 
    ktime_t            rt_period; 
    /* 一个时间周期内的运行时间,超过则限流,默认值为0.95ms */ 
    u64            rt_runtime; 
    /* 时间周期定时器 */ 
    struct hrtimer        rt_period_timer; 
    unsigned int        rt_period_active; 
};

 

三、RT调度实现原理

1. 实时进程的优先级队列rt_prio_array

rt_prio_array里面包含的是一组链表,每个优先级对应一个链表,由queue[MAX_RT_PRIO]表示。还维护一个由101 bit组成的bitmap,其中实时进程优先级为0-99,
占100 bit,再加1 bit的定界符。当某个优先级别上有进程被插入到优先级队列时,相应的比特位就被置位。 通常用 sched_find_first_bit()函数查询该bitmap,它返回当前被置位的最高优先级的数组下标。由于使用位图,查找一个任务来执行所需要的时间并不依赖于活动任务的个数,而是依赖于优先级的数量。可见实时调度是一个O(1)调度策略。

RT调度器的核心是优先级队列,调度程序从最高优先级队列头部选择一个task运行,调度结束会将任务放到对应的优先级队列尾部。bitmap标示优先级,某个bit置1标示该优先级队列不为空。

//kernel/sched/sched.h 
struct rt_prio_array { 
    DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */ 
    struct list_head queue[MAX_RT_PRIO]; /*每个优先级都有一个链表*/ 
};

2. pick_next_task_rt() 与负载均衡

(1) pick_next_task_rt函数是调度器用于选择下一个执行任务。流程如下:

a. 与CFS调度器不同,RT调度器会在多个CPU组成的domain中,对任务进行pull/push处理,也就是说,如果当前CPU的运行队列中任务优先级都不高,那么会考虑去其他CPU运行队列中找一个更高优先级的任务来执行,以确保按照优先级处理,此外,当前CPU也会把任务推送到其他更低优先级的CPU运行队列上。

b. _pick_next_task_rt的处理逻辑比较简单,如果实时调度实体是task,则直接查找优先级队列的位图中,找到优先级最高的任务,而如果实时调度实体是task_group,则还需要继续往下进行遍历查找;

(2) 关于任务的pull/push,linux提供了struct plist,基于优先级的双链表,其中任务的组织关系如下图:

pull_rt_task的大概示意图如下:

a. 当前CPU上的优先级任务不高,从另一个CPU的pushable_tasks链表中找优先级更高的任务来执行;

3. enqueue_task_rt/dequeue_task_rt

当RT任务进行出队入队时,通过enqueue_task_rt/dequeue_task_rt两个接口来完成,调用流程如下:

a. enqueue_task_rt和dequeue_task_rt都会调用dequeue_rt_stack接口,当请求的rt_se对应的是任务组时,会从顶部到请求的rt_se将调度实体出列;

b. 任务添加到rt运行队列时,如果存在多个任务可以分配给多个CPU,设置overload,用于任务的迁移;

4. update_curr_rt

运行时的统计数据更新,是在update_curr_rt函数中完成的:

update_curr_rt函数功能,主要包括两部分:
a. 运行时间的统计更新处理;
b. 如果运行时间超出了分配时间,进行时间均衡处理,并且判断是否需要进行限流,进行了限流则需要将RT队列出队,并重新进行调度;

为了更直观的理解,下边还是来两张图片说明一下:

sched_rt_avg_update更新示意如下:

rq->age_stamp:在CPU启动后运行队列首次运行时设置起始时间,后续周期性进行更新;

rt_avg:累计的RT平均运行时间,每0.5秒减半处理,用于计算CFS负载减去RT在CFS负载平衡中使用的时间百分比;

四、RT进程组调度

RT调度器与CFS调度器的组调度基本类似,看一下RT调度器组调度的组织关系图吧:

a. 系统为每个CPU都分配了RT运行队列,以及RT调度实体,任务组通过它包含的RT调度实体来参与调度;
b. 任务组task_group的RT队列,用于存放归属于该组的任务或子任务组,从而形成级联的结构;

看一下实际的组织示意图:

五、带宽限制

RT调度器在带宽控制中,调度时间周期设置的为1s,运行时间设置为0.95s:

/* period over which we measure -rt task CPU usage in us. default: 1s */ 
unsigned int sysctl_sched_rt_period = 1000000; 
 
/* part of the period that we allow rt tasks to run in us. default: 0.95s */ 
int sysctl_sched_rt_runtime = 950000;

这两个值可以在用户态通过/sys/fs/cgroup/cpu/rt_runtime_us和/sys/fs/cgroup/cpu/rt_period_us来进行设置。

看看函数调用流程:

a. init_rt_bandwidth函数在创建分配RT任务组的时候调用,完成的工作是将rt_bandwidth结构体的相关字段进行初始化:设置好时间周期rt_period和运行时间限制rt_runtime,都设置成默认值;

b. 可以从用户态通过操作/sys/fs/cgroup/cpu下对应的节点进行设置rt_period和rt_runtime,最终调用的函数是tg_set_rt_bandwidth,在该函数中会从下往上的遍历任务组进行设置时间周期和限制的运行时间;

c. 在enqueue_rt_entity将RT调度实体入列时,最终触发start_rt_bandwidth函数执行,当高精度定时器到期时调用do_sched_rt_period_timer函数;

d. do_sched_rt_period_timer函数,会去判断该RT运行队列的累计运行时间rt_time与设置的限制运行时间rt_runtime之间的大小关系,以确定是否限流的操作。在这个函数中,如果已经进行了限流操作,会调用balance_time来在多个CPU之间进行时间均衡处理,简单点说,就是从其他CPU的rt_rq队列中匀出一部分时间增加到当前CPU的rt_rq队列中,也就是将当前rt_rt运行队列的限制运行时间rt_runtime增加一部分,其他CPU的rt_rq运行队列限制运行时间减少一部分。

来一张效果示意图:

 

六、如何选择目标CPU

1. CPU prioriy概念:

CPU priority的使用场合在rt调度类中,因为在rt调度类中,只要抢占开启,高优先级的rt task会立即抢占低优先级task,并在其CPU上运行。所以就有了rt task priority转化为CPU priority,即task选择CPU时决定优先运行在哪些CPU上。CPU priority的思想是 rt task优先级越高,CPU priority越低,rt task优先级越低,CPU priority越高,越容易被将要运行的rt task选择作为target cpu。

实现文件:kernel/sched/cpupri.c

 

七、实例补充

1. trace某个rt线程调度情况

# cd /sys/kernel/tracing/events
# echo
1 > sched/sched_switch/enable # echo "next_comm == xxx" > sched/sched_switch/filter (filter 格式可以cat format 同目录下获得) TODO: 测试! # echo 1 > sched/sched_wakeup/enable # echo "next_comm == xxx" > sched/sched_wakeup/filter # echo 1 > sched/sched_migrate/enable # echo "next_comm == xxx" > sched/sched_migrate/filter # echo 2048 > buffer_size_kb # echo 1 > tracing_on

以上的trace event得到的结果可以观察xxx线程时候被调度到、sleep后什么时候被wakeup、以及再smp上的迁移状况。

注:
TRACE_EVENT(sched_switch, ......) /* include/trace/events/sched.h */
__schedule() --> trace_sched_switch() /* kernel/sched/core.c */

2. Linux运行时调优和调试选项

Linux引入了重要的sysctls来在运行时对调度程序进行调优(以ns结尾的名称以纳秒为单位):
sched_child_runs_first:child在fork之后进行调度;此为默认设置。如果设置为0,那么先调度parent。
sched_min_granularity_ns:针对CPU密集型任务执行最低级别抢占粒度。
sched_latency_ns:针对CPU密集型任务进行目标抢占延迟(Targeted preemption latency)。
sched_wakeup_granularity_ns:针对SCHED_OTHER的唤醒粒度。
sched_batch_wakeup_granularity_ns:针对SCHED_BATCH的唤醒(Wake-up)粒度。
sched_features:包含各种与调试相关的特性的信息。
sched_stat_granularity_ns:收集调度程序统计信息的粒度。
sched_compat_yield:由于CFS进行了改动,严重依赖sched_yield()的行为的应用程序可以要求不同的性能,因此推荐启用sysctls。

通过下面命令可以看到运行时参数的值:
# sudo sysctl -A | grep "sched" | grep -v "domain"

3. Linux还提供了运行时统计信息,分别在 kernel/sched_debug.c 和 kernel/sched_stats.h 中实现。要提供调度程序的运行时信息和调试信息,
需要将一些文件添加到proc文件系统:
/proc/sched_debug:显示运行时调度程序可调优选项的当前值、CFS 统计信息和所有可用 CPU 的运行队列信息。sched_debug_show()//sched_debug.c。
/proc/schedstat:为所有相关的CPU显示特定于运行队列的统计信息以及SMP系统中特定于域的统计信息。show_schedstat()//kernel/sched_stats.h
/proc/[PID]/sched:显示与相关调度实体有关的信息。proc_sched_show_task()//kernel/sched_debug.c

参考:

(六)Linux进程调度-实时调度器 https://blog.csdn.net/LoyenWang/article/details/106774829


本文参考链接:https://www.cnblogs.com/hellokitty2/p/14199741.html