Linux实时调度实战:优化你的应用性能

系统 Linux
如何才能挖掘 Linux 系统的潜力,让它在实时场景中大放异彩,显著优化应用性能呢?今天,就带大家走进 Linux 实时调度的实战世界,一同探索其中的奥秘,掌握让应用性能飞跃的实用技巧 。

在如今数字化浪潮汹涌的时代,无论是工业自动化生产线的精准操控,还是多媒体应用中流畅的音视频播放体验,又或是医疗设备对生命体征的实时监测,背后都离不开一个关键因素 —— 系统的实时响应能力。而 Linux 系统,作为开源世界的中流砥柱,在众多领域广泛应用。但默认情况下,Linux 的进程调度主要侧重于公平性,对于那些对时间极为敏感的实时任务而言,原生的调度机制显得有些力不从心。

那么,如何才能挖掘 Linux 系统的潜力,让它在实时场景中大放异彩,显著优化应用性能呢?今天,就带大家走进 Linux 实时调度的实战世界,一同探索其中的奥秘,掌握让应用性能飞跃的实用技巧 。 

一、Linux调度概述

在日常生活中,我们常常会遇到排队的场景。比如在超市结账时,顾客们会在收银台前依次排队等待结账。在这个过程中,收银员会按照排队的顺序为顾客服务,这就是一种简单的调度方式。而在 Linux 操作系统中,也存在着类似的调度机制,它负责管理系统中的进程,决定哪个进程可以获得 CPU 资源并运行。

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。在单处理器系统中,同一时刻只有一个进程能够在 CPU 上运行,就像只有一个收银员为顾客服务一样。而在多处理器系统中,虽然可以有多个进程同时在不同的 CPU 上运行,但总体上仍然需要对进程进行合理的调度,以确保系统的高效运行。

Linux 系统中的进程可以分为不同的类型,其中实时进程对时间要求较高,它们需要在规定的时间内完成任务。实时进程又可以进一步分为硬实时进程和软实时进程。硬实时进程必须在绝对的时间窗口内完成任务,否则可能会导致系统失效或灾难性后果,比如航空航天控制、医疗设备等领域的任务。软实时进程虽然也追求在规定时间内完成任务,但偶尔的超时通常不会导致系统完全失效,只会影响系统的服务质量或用户体验,像多媒体处理、网络通信等场景中的任务。除了实时进程,还有普通进程,它们对时间的要求相对较低,在系统资源分配中处于相对次要的地位。

为了实现对进程的有效调度,Linux 系统采用了多种调度算法。其中,时间片轮转调度算法是一种常见的调度方式。它将 CPU 的时间划分为一个个固定长度的时间片,每个进程轮流获得一个时间片来运行。当一个进程的时间片用完后,即使它还没有完成任务,也会被暂停,然后被放入就绪队列的末尾,等待下一轮调度。这种调度方式就像是超市里的顾客们轮流在收银台结账,每个人都有机会得到服务,从而保证了系统的公平性和响应性。

实时调度器主要为了解决以下四种情况:

  • 在唤醒任务时,待唤醒的任务放置到哪个运行队列最合适(这里称为pre-balance);
  • 新唤醒任务的优先级比某个运行队列的当前任务更低时,怎么处理这个更低优先级任务;
  • 新唤醒任务的优先级比同一运行队列的某个任务更高时,并且抢占了该低优先级任务,该低优先级任务怎么处理?
  • 当某个任务降低自身优先级,导致原来更低优先级任务相比之下具有更高优先级,这种情况怎么处理。
    对于情况2和情况3,实时调度器采用push操作。push操作从根域中所有运行队列中挑选一个运行队列(一个cpu对应一个运行队列),该运行队列的优先级比待push任务的优先级更低。运行队列的优先级是指该运行队列上所有任务的最高优先级。

对于情况4,实时调度器采用pull操作。当某个运行队列上准备调度时,候选任务比当前任务的优先级更低时,实时调度器检查其他运行队列,确定是否可以pull更高优先级任务到本运行队列。还有,当某个运行队列上发生调度时,该运行队列上没有任务比当前任务优先级高,实时调度器执行pull操作,从其他运行队列中pull更高优先级任务到本运行队列。

每CPU变量运行队列rq,包含一个rt_rq数据结构。rt_rq结构体主要内容如下:

struct rt_rq {
    struct rt_prio_array  active;
    ...
    unsigned long         rt_nr_running;         // 可运行实时任务个数
    unsigned long         rt_nr_migratory;       // 该运行队列上可以迁移到其他运行队列的实时任务个数
    unsigned long         rt_nr_uninterruptible;
    int                   highest_prio;
    int                   overloaded;
};

实时任务优先级范围为0到99。这些实时任务组织成优先级索引数组active,该优先级数组的数据结构类型为rt_prio_arry。rt_prio_arry数据结构由两部分组成,一部分是位图,另一部分是数组。

struct rt_prio_arry {
    unsigned long bitmap[BITS_TO_LONGS(MAX_RT_PRIO+1)];
    struct list_head queue[MAX_RT_PRIO];
}

二、实时调度策略

Linux内核中提供了两种实时调度策略:SCHED_FIFO和SCHED_RR,其中RR是带有时间片的FIFO。这两种调度算法实现的都是静态优先级。内核不为实时进程计算动态优先级。这能保证给定优先级别的实时进程总能抢占优先级比他低得进程。linux的实时调度算法提供了一种软实时工作方式。

实时优先级范围从0到MAX_RT_PRIO减一。默认情况下,MAX_RT_PRIO为100(定义在include/linux/sched.h中),所以默认的实时优先级范围是从0到99。SCHED_NORMAL级进程的nice值共享了这个取值空间,它的取值范围是从MAX_RT_PRIO到MAX_RT_PRIO+40。也就是说,在默认情况下,nice值从-20到19直接对应的是从100到139的优先级范围,这就是普通进程的静态优先级范围。在实时调度策略下。schedule()函数的运行会关联到实时调度类rt_sched_class。

2.1SCHED_FIFO:独占 CPU 的 “霸王龙”

在 Linux 的实时调度策略中,SCHED_FIFO 就像是恐龙时代的霸王龙,霸气十足。它采用先进先出(FIFO)的调度方式,这种方式非常直接,没有复杂的时间片轮转机制。一旦一个进程被调度,只要没有更高优先级的进程出现,它就会一直霸占着 CPU 运行下去,运行时长不受任何限制。这就好比在一场比赛中,只要没有更强的对手出现,当前的冠军就可以一直保持领先,不会被替换下场。

以音频处理场景为例,在实时音频录制和播放中,就经常会用到 SCHED_FIFO 策略。在录制音频时,需要保证音频数据的连续性和及时性,不能有丝毫的延迟或中断。如果采用 SCHED_FIFO 策略,音频录制进程一旦获得 CPU 资源,就会持续运行,将麦克风采集到的音频数据及时地写入存储设备。在播放音频时,音频播放进程也会独占 CPU,按照顺序将音频数据从存储设备中读取出来,并发送到音频输出设备进行播放。这样可以确保音频的流畅播放,不会出现卡顿或杂音的情况,为用户带来高品质的音频体验。

SCHED_FIFO 策略的优点显而易见,它可以为那些对时间要求极为严格的实时进程提供稳定且可预测的执行时间,这对于一些需要精确控制时间的系统来说至关重要,比如工业控制系统、机器人控制等领域。在这些系统中,任务的执行时间必须是可预测的,否则可能会导致严重的后果。

然而,SCHED_FIFO 策略也存在明显的缺点。由于它没有时间片的概念,一旦一个低优先级的进程先获得了 CPU 资源,并且一直不主动放弃,那么其他优先级较低的进程就可能会一直处于饥饿状态,无法获得 CPU 资源来执行。这就好比一群人在排队等待服务,但是排在前面的人一直占用着服务资源不离开,后面的人就只能一直等待,这显然是不公平的。

2.2SCHED_RR:公平轮替的 “时间掌控者”

与 SCHED_FIFO 不同,SCHED_RR 像是一位公平的时间掌控者,采用时间片轮转的调度机制。在这种策略下,每个进程都会被分配一个固定的时间片。当进程运行时,时间片会逐渐减少。一旦进程用完了自己的时间片,它就会被放入就绪队列的末尾,同时释放 CPU 资源,让其他相同优先级的进程有机会执行。这就像一场接力比赛,每个选手都有固定的跑步时间,时间一到就把接力棒交给下一位选手,保证了每个选手都有公平的参与机会。

以动画渲染场景为例,在制作动画时,通常会有多个任务同时进行,比如模型渲染、材质处理、光影计算等。这些任务可能具有相同的优先级,需要合理地分配 CPU 资源。如果采用 SCHED_RR 策略,每个渲染任务都会被分配一个时间片。在自己的时间片内,任务可以充分利用 CPU 资源进行计算和处理。当时间片用完后,任务会暂停,将 CPU 资源让给其他任务。这样可以确保每个渲染任务都能得到及时的处理,不会因为某个任务长时间占用 CPU 而导致其他任务延迟,从而保证了动画渲染的高效进行。

SCHED_RR 策略在保证实时性的同时,还兼顾了公平性。它通过时间片的轮转,让每个进程都能在一定的时间内获得 CPU 资源,避免了低优先级进程长时间得不到执行的情况。这使得它在一些对响应时间要求较高,同时又需要保证公平性的实时进程中得到了广泛应用,比如交互式应用程序、游戏等。在这些应用中,用户希望能够得到及时的响应,同时也不希望某个任务独占 CPU 资源,导致其他操作变得迟缓。

三、实时调度类的数据结构详解

3.1优先级队列rt_prio_array

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

struct rt_prio_array {
 DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* 包含1 bit的定界符 */
 struct list_head queue[MAX_RT_PRIO];
};

这里用include/linux/types.h中的DECLARE_BITMAP宏来定义指定长度的位图,用include/linux/list.h中的struct list_head来为100个优先级定义各自的双链表。在实时调度中,运行进程根据优先级放到对应的队列里面,对于相同的优先级的进程后面来的进程放到同一优先级队列的队尾。对于FIFO/RR调度,各自的进程需要设置相关的属性。进程运行时,要根据task中的这些属性判断和设置,放弃cpu的时机(运行完或是时间片用完)。

3.2实时运行队列rt_rq

在kernel/sched.c中,用于组织实时调度的相关信息。

struct rt_rq {
 struct rt_prio_array active;
 unsigned long rt_nr_running;
#if defined CONFIG_SMP || defined CONFIG_RT_GROUP_SCHED
 struct {
 int curr; /* 最高优先级的实时任务 */
#ifdef CONFIG_SMP
 int next; /* 下一个最高优先级的任务 */
#endif
 } highest_prio;
#endif
#ifdef CONFIG_SMP
 unsigned long rt_nr_migratory;
 unsigned long rt_nr_total;
 int overloaded;
 struct plist_head pushable_tasks;
#endif
 int rt_throttled;
 u64 rt_time;
 u64 rt_runtime;
 /* Nests inside the rq lock: */
 spinlock_t rt_runtime_lock;

#ifdef CONFIG_RT_GROUP_SCHED
 unsigned long rt_nr_boosted;

 struct rq *rq;
 struct list_head leaf_rt_rq_list;
 struct task_group *tg;
 struct sched_rt_entity *rt_se;
#endif
};

3.3实时调度实体 sched_rt_entity

在 Linux 内核的实时调度机制中,sched_rt_entity结构体扮演着至关重要的角色,它就像是一个精心打造的 “任务名片”,记录了实时进程参与调度所需的关键信息。该结构体定义于include/linux/sched.h头文件中,其源码如下:

struct sched_rt_entity {
    struct list_head run_list;         // 用于将“实时调度实体”加入到优先级队列中的
    unsigned long timeout;            // 用于设置调度超时时间
    unsigned long watchdog_stamp;     // 用于记录jiffies的值
    unsigned int time_slice;         // 时间片
    unsigned short on_rq;
    unsigned short on_list;
    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: */
    struct rt_rq *my_q;              // 表示“实时调度实体”所拥有的“实时运行队列”,用于管理“子任务”
#endif
} __randomize_layout;

run_list字段是一个双向链表节点,它就像一根无形的线,将各个实时调度实体按照优先级串联起来,加入到优先级队列中,方便调度器快速定位和处理。当一个实时进程被创建或者状态发生变化时,它的run_list就会被插入到相应的优先级队列中,等待调度器的调度。

timeout字段用于设置调度超时时间,这就像是给任务设定了一个 “闹钟”。当任务运行时间超过这个设定的超时时间时,调度器可能会对其进行特殊处理,比如将其从 CPU 上移除,重新调度其他任务,以确保系统的实时性和稳定性。在一些对时间要求极高的实时系统中,如自动驾驶汽车的控制系统,每个任务都必须在规定的时间内完成,否则可能会导致严重的后果。timeout字段就可以保证这些任务不会因为长时间占用 CPU 而影响其他关键任务的执行。

watchdog_stamp字段用于记录jiffies的值,jiffies是 Linux 内核中的一个全局变量,表示系统启动以来的时钟滴答数。通过记录jiffies的值,watchdog_stamp可以为调度器提供时间参考,用于判断任务的运行状态和调度时机。比如,调度器可以根据watchdog_stamp和当前的jiffies值来计算任务的运行时间,从而决定是否需要对任务进行调度。

time_slice字段表示时间片,对于采用时间片轮转调度策略(如SCHED_RR)的实时进程来说,这个字段尤为重要。它规定了每个进程在被调度后可以连续运行的时间长度。当进程的时间片用完后,调度器会将其从 CPU 上移除,并将其放入就绪队列的末尾,等待下一轮调度。这就像一场接力比赛,每个选手都有固定的跑步时间,时间一到就把接力棒交给下一位选手,保证了每个选手都有公平的参与机会。在多媒体播放系统中,音频和视频的解码任务通常采用SCHED_RR策略,通过合理设置time_slice,可以确保音频和视频的流畅播放,不会出现卡顿或延迟的情况。

在支持实时组调度(CONFIG_RT_GROUP_SCHED)的情况下,parent字段指向父类 “实时调度实体”,这就像是一个家族树中的父子关系,通过这种关系,调度器可以更好地管理和调度整个任务组。rt_rq字段表示 “实时调度实体” 所属的 “实时运行队列”,而my_q字段则表示 “实时调度实体” 所拥有的 “实时运行队列”,用于管理 “子任务”。这种层次化的结构设计,使得调度器能够更加灵活地处理复杂的实时任务场景。

3.4实时就绪队列 struct rt_rq

struct rt_rq结构体是 Linux 内核实时调度的核心数据结构之一,它就像是一个高效的 “任务指挥官”,负责管理实时进程的运行队列,在核心调度器管理活动进程中发挥着举足轻重的作用。该结构体定义于kernel/sched/sched.h头文件中,其源码如下:

struct rt_rq {
    struct rt_prio_array active;        // 优先级队列
    unsigned int rt_nr_running;        // 在RT运行队列中所有活动的任务数
    unsigned int rr_nr_running;
#if defined CONFIG_SMP || defined CONFIG_RT_GROUP_SCHED
    struct {
        int curr;                       // 当前RT任务的最高优先级
#ifdef CONFIG_SMP
        int next;                       // 下一个要运行的RT任务的优先级,如果两个任务都有最高优先级,则curr == next
#endif
    } highest_prio;
#endif
#ifdef CONFIG_SMP
    unsigned long rt_nr_migratory;     // 任务没有绑定在某个CPU上时,这个值会增减,用于任务迁移
    unsigned long rt_nr_total;         // 用于overload检查
    int overloaded;                    // RT运行队列过载,则将任务推送到其他CPU
    struct plist_head pushable_tasks;  // 优先级列表,用于推送过载任务
#endif /* CONFIG_SMP */
    int rt_queued;                     // 表示RT运行队列已经加入rq队列
    int rt_throttled;                  // 用于限流操作
    u64 rt_time;                       // 累加的运行时,超出了本地rt_runtime时,则进行限制
    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
};

active字段是一个rt_prio_array类型的优先级队列,它维护了 100 个优先级的队列(链表),优先级范围从 0 到 99,从高到低排列。同时,它还定义了位图,用于快速查询。这就像是一个多层的货架,每个货架层对应一个优先级,实时进程根据其优先级被放置在相应的货架层上。调度器可以通过位图快速找到最高优先级的队列,从而选择优先级最高的进程进行调度,大大提高了调度效率。在航空航天控制系统中,各种实时任务的优先级划分非常严格,通过active优先级队列,调度器能够快速响应高优先级任务,确保系统的安全和稳定运行。

rt_nr_running字段表示在 “实时运行队列” 中所有活动的任务数,这个数字就像是一个实时监控的计数器,调度器可以根据它来了解当前实时运行队列中的任务负载情况。如果任务数过多,调度器可能会采取一些措施,如任务迁移、限流等,以保证系统的正常运行。

在支持对称多处理(CONFIG_SMP)或实时组调度(CONFIG_RT_GROUP_SCHED)的情况下,highest_prio结构体中的curr字段表示当前 RT 任务的最高优先级,next字段表示下一个要运行的 RT 任务的优先级。如果两个任务都有最高优先级,则curr和next字段值相等。这些字段就像是调度器的 “指南针”,帮助调度器在众多任务中准确地选择下一个要运行的任务。

rt_nr_migratory字段用于记录任务没有绑定在某个 CPU 上时,这个值会增减,用于任务迁移。在多处理器系统中,当某个 CPU 的负载过高时,调度器可以根据这个字段的值,将一些可迁移的任务迁移到其他 CPU 上,以实现负载均衡。rt_nr_total字段用于overload检查,当rt_nr_total超过一定阈值时,说明系统可能处于过载状态,调度器会采取相应的措施,如将任务推送到其他 CPU,以缓解系统压力。overloaded字段表示 RT 运行队列过载,当该字段为真时,调度器会将任务推送到其他 CPU,以保证系统的正常运行。pushable_tasks字段是一个优先级列表,用于推送过载任务,它就像是一个 “任务搬运工”,将过载的任务从一个 CPU 推送到其他 CPU 上。

rt_queued字段表示 RT 运行队列已经加入rq队列,rq队列是系统中所有进程的运行队列,RT 运行队列是其中的一部分。rt_throttled字段用于限流操作,当实时进程的运行时间超过一定限制时,调度器会对其进行限流,以保证系统的公平性和稳定性。rt_time字段表示累加的运行时,当超出本地rt_runtime时,则进行限制。rt_runtime字段表示分配给本地池的运行时,它就像是一个 “资源配额”,限制了实时进程在本地的运行时间。

在支持实时组调度(CONFIG_RT_GROUP_SCHED)的情况下,rt_nr_boosted字段用于优先级翻转问题解决。在实时系统中,可能会出现优先级翻转的情况,即低优先级任务持有高优先级任务所需的资源,导致高优先级任务无法执行。通过rt_nr_boosted字段,调度器可以对任务的优先级进行调整,解决优先级翻转问题。rq字段指向运行队列,tg字段指向任务组,通过这些指针,调度器可以更好地管理和调度整个任务组。

实时调度的主要操作:实时调度的操作在kernel/sched_rt.c中实现。

(1)进程插入enqueue_task_rt:更新调度信息,调用enqueue_rt_entity()-->__enqueue_rt_entity(),将调度实体插入到相应优先级队列的末尾。如下:

static void
enqueue_task_rt(struct rq *rq, struct task_struct *p, int wakeup, bool head)
{
 struct sched_rt_entity *rt_se = &p->rt;

 if (wakeup)
 rt_se->timeout = 0;

 enqueue_rt_entity(rt_se, head); /* 实际工作 */

 if (!task_current(rq, p) && p->rt.nr_cpus_allowed > 1)
 enqueue_pushable_task(rq, p); /* 添加到对应的hash表中 */
}

static void enqueue_rt_entity(struct sched_rt_entity *rt_se, bool head)
{
 dequeue_rt_stack(rt_se); /* 先从运行队列中删除 */
 for_each_sched_rt_entity(rt_se)
 __enqueue_rt_entity(rt_se, head); /* 然后添加到运行队列尾部 */
}

static void __enqueue_rt_entity(struct sched_rt_entity *rt_se, bool head)
{
 struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
 struct rt_prio_array *array = &rt_rq->active;
 struct rt_rq *group_rq = group_rt_rq(rt_se);
 struct list_head *queue = array->queue + rt_se_prio(rt_se);

 /*
 * Don't enqueue the group if its throttled, or when empty.
 * The latter is a consequence of the former when a child group
 * get throttled and the current group doesn't have any other
 * active members.
 */
 if (group_rq && (rt_rq_throttled(group_rq) || !group_rq->rt_nr_running))
 return;

 if (head)
 list_add(&rt_se->run_list, queue);
 else
 list_add_tail(&rt_se->run_list, queue);
 __set_bit(rt_se_prio(rt_se), array->bitmap);

 inc_rt_tasks(rt_se, rt_rq); /* 运行进程数增一 */
}

该函数先获取运行队列中的优先级队列,然后调用include/linux/list.h:list_add_tail()--->__list_add(),将进程插入到链表的末尾。如下:

static inline void __list_add(struct list_head *new,
      struct list_head *prev,
      struct list_head *next)
{
 next->prev = new;
 new->next = next;
 new->prev = prev;
 prev->next = new;
}

(2)进程选择pick_next_task_rt:实时调度会选择最高优先级的实时进程来运行。调用_pick_next_task_rt()--->pick_next_rt_entity()来完成获取下一个进程的工作。如下:

static struct task_struct *pick_next_task_rt(struct rq *rq)
{
 struct task_struct *p = _pick_next_task_rt(rq); /* 实际工作 */

 /* The running task is never eligible for pushing */
 if (p)
 dequeue_pushable_task(rq, p);

#ifdef CONFIG_SMP
 /*
 * We detect this state here so that we can avoid taking the RQ
 * lock again later if there is no need to push
 */
 rq->post_schedule = has_pushable_tasks(rq);
#endif

 return p;
}

static struct task_struct *_pick_next_task_rt(struct rq *rq)
{
 struct sched_rt_entity *rt_se;
 struct task_struct *p;
 struct rt_rq *rt_rq;

 rt_rq = &rq->rt;

 if (unlikely(!rt_rq->rt_nr_running))
 return NULL;

 if (rt_rq_throttled(rt_rq))
 return NULL;

 do { /* 遍历组调度中的每个进程 */
 rt_se = pick_next_rt_entity(rq, rt_rq);
 BUG_ON(!rt_se);
 rt_rq = group_rt_rq(rt_se);
 } while (rt_rq);

 p = rt_task_of(rt_se);
 /* 更新执行域 */
 p->se.exec_start = rq->clock_task;

 return p;
}

static struct sched_rt_entity *pick_next_rt_entity(struct rq *rq,
   struct rt_rq *rt_rq)
{
 struct rt_prio_array *array = &rt_rq->active;
 struct sched_rt_entity *next = NULL;
 struct list_head *queue;
 int idx;
 /* 找到第一个可用的 */
 idx = sched_find_first_bit(array->bitmap);
 BUG_ON(idx >= MAX_RT_PRIO);
 /* 从链表组中找到对应的链表 */
 queue = array->queue + idx;
 next = list_entry(queue->next, struct sched_rt_entity, run_list);
 /* 返回找到的运行实体 */
 return next;
}

该函数调用include/asm-generic/bitops/sched.h:sched_find_first_bit()返回位图中当前被置位的最高优先级,以作为这组链表的数组下标找到其优先级队列。然后调用include/linux/list.h:list_entry()--->include/linux/kernel.h:container_of(),返回该优先级队列中的第一个进程,以作为下一个要运行的实时进程。例如当前所有实时进程中最高优先级为45(换句话说,系统中没有任何实时进程的优先级小于45),则直接读取rt_prio_array中的queue[45],得到优先级为45的进程队列指针。该队列头上的第一个进程就是被选中的进程。这种算法的复杂度为O(1)。

sched_find_first_bit的实现如下。它与CPU体系结构相关,其他体系结构会实现自己的sched_find_fist_bit函数。下面的实现以最快的方式搜索100 bit的位图,它能保证100 bit中至少有一位被清除。

static inline int sched_find_first_bit(const unsigned long *b)
{
#if BITS_PER_LONG == 64
 if (b[0])
 return __ffs(b[0]);
 return __ffs(b[1]) + 64;
#elif BITS_PER_LONG == 32
 if (b[0])
 return __ffs(b[0]);
 if (b[1])
 return __ffs(b[1]) + 32;
 if (b[2])
 return __ffs(b[2]) + 64;
 return __ffs(b[3]) + 96;
#else
#error BITS_PER_LONG not defined
#endif
}

(3)进程删除dequeue_task_rt:从优先级队列中删除实时进程,并更新调度信息,然后把这个进程添加到队尾。调用链为dequeue_rt_entity()--->dequeue_rt_stack()--->__dequeue_rt_entity(),如下:

static void dequeue_task_rt(struct rq *rq, struct task_struct *p, int sleep)
{
 struct sched_rt_entity *rt_se = &p->rt;
 /* 更新调度信息 */
 update_curr_rt(rq);
 /* 实际工作,将rt_se从运行队列中删除然后 
      添加到队列尾部 */
 dequeue_rt_entity(rt_se);
 /* 从hash表中删除 */
 dequeue_pushable_task(rq, p);
}

static void update_curr_rt(struct rq *rq)
{
 struct task_struct *curr = rq->curr;
 struct sched_rt_entity *rt_se = &curr->rt;
 struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
 u64 delta_exec;

 if (!task_has_rt_policy(curr)) /* 判断是否问实时调度进程 */
 return;
 /* 执行时间 */
 delta_exec = rq->clock_task - curr->se.exec_start;
 if (unlikely((s64)delta_exec < 0))
 delta_exec = 0;

 schedstat_set(curr->se.exec_max, max(curr->se.exec_max, delta_exec));
 /* 更新当前进程的总的执行时间 */
 curr->se.sum_exec_runtime += delta_exec;
 account_group_exec_runtime(curr, delta_exec);
 /* 更新执行的开始时间 */
 curr->se.exec_start = rq->clock_task;
 cpuacct_charge(curr, delta_exec); /* 组调度相关 */

 sched_rt_avg_update(rq, delta_exec);

 if (!rt_bandwidth_enabled())
 return;

 for_each_sched_rt_entity(rt_se) {
 rt_rq = rt_rq_of_se(rt_se);

 if (sched_rt_runtime(rt_rq) != RUNTIME_INF) {
 spin_lock(&rt_rq->rt_runtime_lock);
 rt_rq->rt_time += delta_exec;
 if (sched_rt_runtime_exceeded(rt_rq))
 resched_task(curr);
 spin_unlock(&rt_rq->rt_runtime_lock);
 }
 }
}

static void dequeue_rt_entity(struct sched_rt_entity *rt_se)
{
 dequeue_rt_stack(rt_se); /* 从运行队列中删除 */

 for_each_sched_rt_entity(rt_se) {
 struct rt_rq *rt_rq = group_rt_rq(rt_se);

 if (rt_rq && rt_rq->rt_nr_running)
 __enqueue_rt_entity(rt_se, false); /* 添加到队尾 */
 }
}

static void dequeue_rt_stack(struct sched_rt_entity *rt_se)
{
 struct sched_rt_entity *back = NULL;

 for_each_sched_rt_entity(rt_se) { /* 遍历整个组调度实体 */
 rt_se->back = back; /* 可见rt_se的back实体为组调度中前一个调度实体 */
 back = rt_se;
 }
 /* 将组中的所有进程从运行队列中移除 */
 for (rt_se = back; rt_se; rt_se = rt_se->back) {
 if (on_rt_rq(rt_se))
 __dequeue_rt_entity(rt_se);
 }
}

static void __dequeue_rt_entity(struct sched_rt_entity *rt_se)
{
 struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
 struct rt_prio_array *array = &rt_rq->active;
 /* 移除进程 */
 list_del_init(&rt_se->run_list);
 /* 如果链表变为空,则将位图中对应的bit位清零 */
 if (list_empty(array->queue + rt_se_prio(rt_se)))
 __clear_bit(rt_se_prio(rt_se), array->bitmap);

 dec_rt_tasks(rt_se, rt_rq); /* 运行进程计数减一 */
}

可见更新调度信息的函数为update_curr_rt(),在dequeue_rt_entity()中将当前实时进程从运行队列中移除,并添加到队尾。完成工作函数为dequeue_rt_stack()--->__dequeue_rt_entity(),它调用include/linux/list.h:list_del_init()--->__list_del()删除进程。然后如果链表变为空,则将位图中对应优先级的bit位清零。如下:

static inline void __list_del(struct list_head * prev, struct list_head * next)
{
 next->prev = prev;
 prev->next = next;
}

从上面的介绍可以看出,对于实时调度,Linux的实现比较简单,仍然采用之前的O(1)调度策略,把所有的运行进程根据优先级放到不用的队列里面,采用位图方式进行使用记录。进队列仅仅是删除原来队列里面的本进程,然后将他挂到队列尾部;而对于“移除”操作,也仅仅是从队列里面移除后添加到运行队列尾部。

四、实时调度类在实际中的应用

4.1工业控制系统:精准控制的幕后英雄

在工业 4.0 的浪潮下,工业控制系统正朝着智能化、自动化的方向飞速发展。从汽车制造到电子设备生产,自动化生产线已经成为现代工业的核心。在这些复杂的生产线上,各种设备协同工作,每一个动作、每一次数据传输都需要精确的时间控制。Linux 实时调度类在其中扮演着至关重要的角色,它就像是一位精准的指挥官,确保每一个任务都能按时执行,从而实现整个生产线的高效、稳定运行。

以汽车制造为例,自动化生产线涉及众多复杂的工序,如车身焊接、零部件装配、喷漆等。在车身焊接环节,机械臂需要按照精确的时间顺序和位置坐标进行焊接操作。如果焊接任务不能按时完成,可能会导致车身结构不稳定,影响汽车的质量和安全性。

Linux 实时调度类通过采用 SCHED_FIFO 或 SCHED_RR 策略,为焊接任务分配高优先级,确保机械臂能够及时响应控制指令,准确地完成焊接操作。同时,在生产线的物料运输环节,AGV(自动导引车)需要根据生产进度及时将零部件运输到指定位置。Linux 实时调度类可以根据 AGV 的任务优先级和实时路况,合理地调度 AGV 的运行,避免出现交通拥堵和任务延迟的情况,保证生产线的物料供应顺畅。

据相关数据显示,采用 Linux 实时调度类的工业控制系统,生产效率能够提升 20% 以上,产品次品率降低 15% 左右。这充分证明了 Linux 实时调度类在工业控制系统中的重要性和价值,它为工业生产的精准控制提供了坚实的技术保障,是工业自动化不可或缺的关键技术之一。

4.2多媒体处理:流畅体验的保障

在当今的数字时代,多媒体应用已经深入到我们生活的方方面面,从高清视频播放到音频实时处理,从视频会议到游戏娱乐,我们对多媒体体验的要求越来越高。而 Linux 实时调度类正是实现流畅多媒体体验的幕后功臣,它对时间敏感性任务的精确调度,确保了多媒体处理的流畅性、稳定性和实时性。

以高清视频播放为例,在播放高清视频时,视频解码和音频解码任务需要在极短的时间内完成,以保证视频和音频的同步播放,避免出现卡顿和延迟的情况。Linux 实时调度类可以为视频解码和音频解码任务分配较高的优先级,并采用合适的调度策略,如 SCHED_RR 策略,为每个任务分配固定的时间片,确保它们能够及时获取 CPU 资源,快速地完成解码工作。同时,在视频渲染和音频输出环节,实时调度类也能保证任务的及时执行,将解码后的视频帧和音频数据快速地输出到显示设备和音频设备上,为用户带来流畅、清晰的视听体验。

在音频实时处理领域,如语音识别、音频编辑等应用中,Linux 实时调度类同样发挥着重要作用。在语音识别过程中,麦克风采集到的语音信号需要及时进行处理和分析,以准确识别用户的语音指令。Linux 实时调度类通过对语音处理任务的优先调度,能够快速地对语音信号进行采样、滤波、特征提取等操作,提高语音识别的准确率和实时性。在音频编辑软件中,当用户对音频进行剪辑、混音等操作时,实时调度类可以确保音频处理任务的高效执行,让用户能够实时听到处理后的音频效果,提升音频编辑的效率和体验。

五、配置与优化指南

5.1系统调用设置:掌控调度的 “魔法棒”

在 Linux 系统中,要实现对线程或进程调度策略和优先级的精确控制,pthread_setschedparam和sched_setscheduler这两个系统调用就像是掌控调度的 “魔法棒”,发挥着关键作用。

pthread_setschedparam主要用于设置线程的调度参数,其函数原型如下:

#include <pthread.h>
int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param);

thread参数表示目标线程的标识符,它就像是线程的 “身份证”,通过这个标识符,系统能够准确地定位到需要设置调度参数的线程。policy参数用于指定调度策略,可取值包括SCHED_OTHER(普通分时调度策略)、SCHED_FIFO(先进先出实时调度策略)和SCHED_RR(时间片轮转实时调度策略)等,不同的取值决定了线程在系统中的调度方式。param是一个指向struct sched_param结构体的指针,该结构体中包含了线程的优先级信息,通过设置param->sched_priority的值,可以调整线程的优先级。例如:

#include <pthread.h>
#include <sched.h>
#include <stdio.h>

void* thread_function(void* arg) {
    // 获取线程的tid
    pid_t tid = gettid();
    // 定义调度策略和优先级变量
    int policy;
    struct sched_param sched_param;
    // 获取当前线程的调度参数
    pthread_getschedparam(pthread_self(), &policy, &sched_param);
    // 打印当前的调度策略和优先级
    printf("Current thread (tid: %d) policy: %d priority: %d\n", tid, policy, sched_param.sched_priority);

    // 设置线程的调度策略为FIFO
    policy = SCHED_FIFO;
    // 设置线程的优先级为最大值
    sched_param.sched_priority = sched_get_priority_max(policy);
    // 设置线程的调度策略和优先级
    pthread_setschedparam(pthread_self(), policy, &sched_param);

    // 再次获取并打印调度参数
    pthread_getschedparam(pthread_self(), &policy, &sched_param);
    printf("New thread (tid: %d) policy: %d priority: %d\n", tid, policy, sched_param.sched_priority);

    // 线程工作代码...
    return NULL;
}

int main() {
    pthread_t thread;
    // 创建线程
    pthread_create(&thread, NULL, &thread_function, NULL);
    // 等待线程结束
    pthread_join(thread, NULL);
    return 0;
}

在上述示例中,首先获取当前线程的调度参数并打印,然后将线程的调度策略设置为SCHED_FIFO,优先级设置为该策略允许的最大值,最后再次获取并打印调度参数,以验证设置是否生效。

sched_setscheduler函数则用于设置进程的调度策略和优先级,其函数原型为:

#include <sched.h>
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);

pid参数表示要设置调度策略的进程的进程 ID,如果pid为 0,则表示当前进程。policy和param的含义与pthread_setschedparam中的类似。例如,将当前进程的调度策略设置为SCHED_RR,并设置优先级为 50,可以这样实现:

#include <sched.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    struct sched_param param;
    param.sched_priority = 50;
    int ret = sched_setscheduler(0, SCHED_RR, ¶m);
    if (ret == -1) {
        perror("Failed to set scheduler");
        exit(EXIT_FAILURE);
    }
    printf("Successfully set scheduler to SCHED_RR with priority 50\n");
    return 0;
}

通过这两个系统调用,开发者可以根据实际需求灵活地设置线程或进程的调度策略和优先级,从而优化系统的性能和实时性。在实际应用中,需要根据任务的特点和系统的要求,谨慎选择调度策略和优先级,以确保系统的稳定运行和高效执行。

5.2性能优化建议:提升效率的 “秘籍”

在 Linux 实时调度中,为了充分发挥系统的性能,提升实时任务的执行效率,我们可以从以下几个方面入手进行优化。

首先,根据任务特点选择调度策略是关键的一步。对于那些对时间要求极为严格,需要在最短时间内完成的任务,如工业控制系统中的紧急控制任务、航空航天中的关键飞行控制指令处理等,应优先考虑使用SCHED_FIFO策略。因为该策略能够确保任务一旦获得 CPU 资源,就可以一直运行,直到完成或者被更高优先级的任务抢占,从而保证了任务执行的及时性和连续性。

而对于那些需要公平分配 CPU 时间,且对响应时间有一定要求的任务,如多媒体播放中的音频和视频同步处理、交互式应用程序中的用户输入响应等,SCHED_RR策略则更为合适。它通过时间片轮转的方式,让每个任务都能在一定的时间内获得 CPU 资源,避免了某个任务长时间独占 CPU,导致其他任务无法及时执行的情况,兼顾了公平性和实时性。

合理分配优先级也是优化实时调度性能的重要手段。在一个复杂的实时系统中,通常会有多个不同类型的任务同时运行,这些任务对时间的敏感度和重要性各不相同。因此,我们需要根据任务的实际需求,为它们分配合理的优先级。高优先级应分配给那些对系统正常运行至关重要,且时间要求紧迫的任务,例如在医疗设备控制系统中,生命体征监测和紧急治疗控制任务就需要设置较高的优先级,以确保能够及时响应患者的生命体征变化,保障患者的生命安全。

而对于一些相对次要的任务,如系统日志记录、数据备份等,可以分配较低的优先级,让它们在系统资源空闲时执行,避免影响关键任务的执行。同时,要注意避免出现优先级反转的情况,即低优先级任务持有高优先级任务所需的资源,导致高优先级任务无法执行。可以通过采用优先级继承、优先级天花板等算法来解决这个问题。

优化系统资源配置也不容忽视。一方面,要合理分配 CPU 资源。在多处理器系统中,可以根据任务的特点和 CPU 的负载情况,将任务绑定到特定的 CPU 核心上执行,以提高 CPU 缓存的命中率,减少任务在不同 CPU 核心之间切换带来的开销。例如,对于一些计算密集型的实时任务,可以将它们固定分配到性能较强的 CPU 核心上,以充分发挥 CPU 的计算能力。

另一方面,要关注内存资源的管理。实时任务通常对内存的访问速度和稳定性有较高的要求,因此可以通过优化内存分配算法、减少内存碎片等方式,提高内存的使用效率和性能。此外,还可以采用内存锁定技术,将关键的实时任务所需的内存页面锁定在物理内存中,避免它们被交换到磁盘上,从而提高任务的执行速度和实时性。在一些对响应时间要求极高的金融交易系统中,就可以采用内存锁定技术,确保交易处理任务能够快速、稳定地执行。

责任编辑:武晓燕 来源: 深度Linux
相关推荐

2010-02-23 16:17:59

2015-06-25 12:41:53

实时 Node应用性能监测

2020-03-30 14:00:21

Flutter前端代码

2012-10-09 09:43:50

WLAN优化无线局域网WLAN

2024-03-04 08:00:00

Java开发

2020-07-15 07:00:00

移动应用开发者指南

2014-02-20 13:36:35

业务服务管理 应用性能优化

2017-12-13 13:09:36

NginxWeb应用

2023-09-08 15:37:29

软件开发性能

2021-08-09 16:39:52

工具JVM剖析

2014-08-04 16:38:37

移动应用

2014-07-29 13:18:04

听云基调网络

2015-07-29 15:06:21

2023-07-19 15:45:47

ReactDOM轻量级

2015-12-11 14:02:02

php应用

2015-07-24 16:12:58

应用性能管理

2010-11-29 17:32:56

网站应用性能监测高明网络

2011-10-17 09:47:53

应用性能工作负载服务器

2023-08-24 16:54:05

2013-08-09 14:18:33

点赞
收藏

51CTO技术栈公众号