Unix/Linux编程:实时进程调度

概述

系统上一般会同时运行交互式进程和后台进程,标准的内核调度算法一般能够为这些进程提供足够的性能和响应度。但是实时应用对调度器由更加严格的要求:

  • 实时应用必须尽可能快的响应外部输入,比如交通导航系统的慢速响应会带来灾难。为了满足这种需求,内核必须要提供工具让高优先级进程能够快速的取得CPU的控制权,抢占当前运行的所有进程。

一些时间关键的应用程序可能需要采取其他措施来避免不可接受的延迟。比如为了避免由于页面错误引起的延迟,应用程序可能会使用mlock()或者mlockall()将其所有虚拟内存锁在RAM中

  • 高优先级进程应该能够保持互斥的访问CPU直到任务完成或者主动放弃CPU
  • 实时应用应该能够精确的控制其组件检测的调度顺序

SUSv3 规定的实时进程调度 API(原先在 POSIX.1b 中定义)部分满足了这些要求。这个API 提供了两个实时调度策略:SCHED_RR 和 SCHED_FIFO。使用这两种策略中任意一种策略进行调度的进程的优先级要高于标准循环时间分享策略来调度的进程。

每个实时策略允许一个优先级范围。SUSv3要求实现至少要为实时策略提供32个离散的优先级。在每个调度策略中,拥有高优先级的可运行进程在尝试访问CPU时总是优先于低优先级的进程。

  • 对于多处理器系统(包括超线程系统)来讲,高优先级总是优于低优先级进程的规则不适用。在多处理器系统中,各个CPU拥有独立的运行队列,并且每个CPU的运行队列中的优先级都局限于该队列。。如假设一个双处理器系统中运行着三个进程,进程 A 的实时优先级为 20,并且它位于 CPU 0 的等待队列中,而该 CPU 当前正在运行优先级为 30 的进程 B,即使 CPU 1 正在运行优先级为 10 的进程 C,进程 A 还是需要等待 CPU 0
  • 可以使用CPU亲和力API来解决这种问题。比如在一个四处理器系统中,所有非关键进程被分配到一个CPU中,让其他三个CPU处理实时应用。

Linux提供了99个实时优先级,其数值从 1(最低)~99(最高),并且这个取值范围同时适用于两个实时调度策略。每个策略中的优先级是等价的。这意味着如果两个进程拥有同样的优先级,一个进程采用了 SCHED_RR 的调度策略,另一个进程采用了 SCHED_FIFO 的调度策略,那么两个都符合运行的条件,至于到底运行哪个则取决于它们被调度的顺序了。实际上,每个优先级级别都维护着一个可运行的进程队列,下一个运行的进程是从优先级最高的非空队列的队头选取出来的

满足上面列出的所有要求的应用程序有时候被称为硬实时应用程序。但 POSIX 实时进程调度 API 无法满足这些要求。它仅仅提供了的软实时,即允许控制调度哪个进程的CPU。 但从版本2.6.18开始,Linux为硬实时应用程序提供了完全的原生支持。

SCHED_RR策略

优先级相同的进程以循环时间分享的方式执行。进程每次使用CPU的时间为一个固定长度的时间片。一旦被调度执行之后,使用SCHED_RR策略的进程会保持对CPU的控制知道下面条件中的一个被满足:

  • 达到时间片终点了
  • 主动放弃CPU,这可能是由于执行了一个阻塞式系统调用或者调用了shed_yield()系统调用
  • 终止了
  • 被一个优先级更高的进程抢到了

对于上面的前两个事件,这个进程将会被防止在于其优先级对应的队列的队尾。

对于上面的最后一个事件,当优先级更高的进程结束之后,被抢占的进程会继续执行直到其剩余时间片被消耗完(即被抢占的进程依然在其优先级级别对应的队列的队头)

在 SCHED_RR 和 SCHED_FIFO 两种策略中,当前运行的进程可能会因为下面某个原因而被抢占:

  • 之前被阻塞的高优先级进程解除阻塞了(如它所等待的 I/O 操作完成了)
  • 另一个进程的优先级被提到了一个级别高于当前运行的进程的优先级的优先级
  • 当前运行的进程的优先级被降低到其他可运行的进程的优先级了

SCHED_RR 策略与标准的循环时间分享调度算法(SCHED_OTHER)类似,即它与允许优先级相同的一组进程分享CPU时间

  • 它们之间最重要的差别在于SCHED_RR策略存在严格的优先级级别,高优先级的进程总是优先于低优先级的进程。而在SCHED_OTHER策略中,高优先级进程不会独占CPU,它仅仅在调度决策时为进程提供一个较大的严重,一个优先级较低的进程总是至少会用到一些CPU时间的
  • 另一个差别是SCHED_RR策略允许精确控制进程被调用的顺序

SCHED_FIFO策略

SCHED_FIFO(先入先出,first-in,first-out)策略与 SCHED_RR 策略类似,它们之间最主要的差别在于SCHED_FIFO策略中不存在时间片。一旦一个SCHED_FIFO进程获得了CPU的控制权后,它就会一直执行直到下面某个条件被满足

  • 主动放弃CPU
  • 终止了
  • 被一个优先级更高的进程抢占了

在第一种情况中,进程会被放置在与其优先级级别对应的队列的队尾。在最后一种情况中,当高优先级进程执行结束之后(被阻塞或终止了),被抢占的进程会继续执行(即被抢占的进程位于与其优先级级别对应的队列的队头)。

SCHED_BATCH和SCHED_IDLE策略

Linux 2.6 系列的内核添加了两个非标准调度策略:SCHED_BATCH 和 SCHED_IDLE。尽管这些策略是通过 POSIX 实时调度 API 来设置的,但实际上它们并不是实时策略。

  • SCHED_BATCH 策略是在版本为 2.6.16 的内核中加入的,它与默认的 SCHED_OTHER 策略类似,两个之间的差别在于 SCHED_BATCH 策略会导致频繁被唤醒的任务被调度的次数较少。这种策略用于进程的批量式执行
  • SCHED_IDLE 策略是在版本为 2.6.23 的内核中加入的,它也与 SCHED_OTHER 类似,但提供的功能等价于一个非常低的 nice 值(即低于+19)。在这个策略中,进程的 nice 值毫无意义。它用于运行低优先级的任务,这些任务在系统中没有其他任务需要使用 CPU 时才会大量使用 CPU

API

实时优先级范围

sched_get_priority_min()和 sched_get_priority_max()系统调用返回一个调度策略的优先级取值范围

NAME
       sched_get_priority_max, sched_get_priority_min  - get static priority range

SYNOPSIS
       #include <sched.h>

       int sched_get_priority_max(int policy);

       int sched_get_priority_min(int policy);

DESCRIPTION
       sched_get_priority_min()返回指定策略的最小优先级,
	   sched_get_priority_max()返回指定策略的最大优先级
       
       policy 指定了需获取哪种调度策略的信息, 其支持的策略值为 SCHED_FIFO、SCHED_RR、SCHED_OTHER 和
       SCHED_BATCH。 有关这些策略的更多详细信息,请参见 sched_setscheduler(2)。 

       优先级数值较高的进程在优先级数值较低的进程之前被调度。 因此,sched_get_priority_max() 返回的值
       将大于 sched_get_priority_min() 返回的值。 

       Linux 允许 SCHED_FIFO 和 SCHED_RR 的静态优先级值范围为 199,而 SCHED_OTHER 和SCHED_BATCH 
       的优先级值为 0。 各种策略的调度优先级范围是不可改变的。 

       调度优先级的范围在其他 POSIX 系统上可能会有所不同。。因此不能在应用程序中硬编码优先级值,而是需要根据两
       个函数的返回值来指定优先级。因此,SCHED_RR 策略中最低的优先级应该是sched_get_priority_min(SCHED_FIFO),
       比它高一级的优先级是sched_ get_ priority_min(SCHED_FIFO)+1,依此类推。

		POSIX.1-2001 要求 SCHED_FIFO 和 SCHED_RR 的最大值和最小值之间至少有 32 的差值。 

       可以使用 sched_get_priority_max()sched_get_priority_min() 的 POSIX 系统在 <unistd.h> 中
       定义了 _POSIX_PRIORITY_SCHEDULING。 

RETURN VALUE
       成功 sched_get_priority_max() and sched_get_priority_min() 返回指定调度策略的最大/最小优先级值。 
       出错时,返回 -1,并适当设置 errno。

修改和获取策略和优先级

sched_setscheduler()系统调用修改进程 ID 为 pid 的进程的调度策略和优先级。如果 pid 为0,那么将会修改调用进程的特性

NAME
       sched_setscheduler, sched_getscheduler - set and get scheduling policy/parameters

SYNOPSIS
       #include <sched.h>

       int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);

       int sched_getscheduler(pid_t pid);

       

param 参数是一个指向下面这种结构的指针。

	  struct sched_param {
           ...
           int sched_priority;
           ...
       };

SUSv3将param参数定义成一个结构以允许实现包含额外的特定于实现的字段,当实现提供了额外的调度策略时这些字段可能会变得非常有用。但与大多数UNIX 实现一样,Linux 提供了 sched_priority 字段,该字段指定了调度策略。对于 SCHED_RR 和 SCHED_FIFO 来讲,这
个字段的取值必须位于 sched_get_priority_min()和 sched_get_priority_max()规定的范围内;对于其他策略来讲,优先级必须是 0

policy 参数确定了进程的调度策略,取值如下:

策 略描 述
SCHED_FIFO实时先入先出
SCHED_RR实时循环
SCHED_OTHER标准的循环时间分享
SCHED_BATCH与 SCHED_OTHER 类似,但用于批量执行(自 Linux 2.6.16 起)
SCHED_IDLE与 SCHED_OTHER 类似,但优先级比最大的 nice 值(+19)还要低(自 Linux 2.6.23 起

成功调用sched_setscheduler()会将pid指定的进程移到与其优先级对应的队列的队尾

SUSv3 规定成功调用 sched_setscheduler()时其返回值应该是上一种调度策略。但 Linux 并没有遵循这个规则,在成功调用时该函数会返回 0。一个可移植的应用程序应该通过检查返回值是否不为−1 来判断调用是否成功。

通过 fork()创建的子进程会继承父进程的调度策略和优先级,并且在 exec()调用中会保持这些信息

sched_setparam()系统调用提供了 sched_setscheduler()函数的一个功能子集。它修改一个进程的调度策略,但不会修改其优先级

NAME
       sched_setparam, sched_getparam - set and get scheduling parameters

SYNOPSIS
       #include <sched.h>

       int sched_setparam(pid_t pid, const struct sched_param *param);

       int sched_getparam(pid_t pid, struct sched_param *param);

       struct sched_param {
           ...
           int sched_priority;
           ...
       };

sched_setparam中的pid 和 param 参数与 sched_setscheduler()中相应的参数是一样的。

成功调用 sched_setparam()会将 pid 指定的进程移到与其优先级级别对应的队列的队尾。

sched_getscheduler()和 sched_getparam()系统调用获取进程的调度策略和优先级

防止实时进程锁住系统

由于SCHED_RR和SCHED_FIFO进程会抢占所有低优先级的进程,因此在开发使用这些策略的应用程序时需要小心可能会发生失控的实时进程因一直占住CPU而导致锁住系统的情况。在程序中可以通过一些方法来避免这种情况的发生。

  • 使用setrlimit()设置一个合理的低软CPU时间组员限制(RLIMIT_CPU)。如果进程消耗了太多CPU,那么它将会收到一个SIGXCPU信号,该信号在默认情况下会杀死进程。
  • 使用alarm()设置一个警报定时器。如果进程的运行时间超出了由alarm()调用指定的秒数,那么该进程会被SIGALRM杀死
  • 创建一个拥有高实时优先级的看门狗进程。这个进程可以进行无限循环,每次循环都睡眠指定的时间间隔,然后醒来并监控其他进程的状态。这种监控可以包含对每个进程消耗的CPU时间的度量并使用sched_getscheduler()和 sched_getparam()来检查进程的调度策略和优先级。如果一个进程看起来行为异常,那么看门狗线程可以降低该进程的优先级或者向其发送合适的信号来停止或者终止该进程。
  • 从2.6.25的内核开始,Linux提供了一个非标准的资源限制RLIMIT_RTTIME用于控制一个运行在实时调度策略下的进程在单次运行中能够消耗的CPU事件。RLIMIT_RTTIME 的单位是毫秒,它限制了一个进程在不执行阻塞式系统调用时能够消耗的CPU时间。当进程执行了这样的系统调用,累积消耗的CPU时间将会被重置为0。当这个进程被一个优先级更高的进程抢占时,累积消耗的CPU时间不会被重置,当进程的时间片被消耗完成或者调用sched_yield()时进程会放弃CPU。当进程达到了CPU时间限制RLIMIT_RTTIME之后,系统会向其发送一个SIGXCPU信号,该信号默认情况下会杀死这个进程。

避免子进程进程特权调度策略

Linux 2.6.32 增加了一个 SCHED_RESET_ON_FORK,在调用 sched_setscheduler()时可以将 policy 参数的值设置为该常量。系统会将这个标记值与上表的一种一个策略OR。如果设置了这个标志,那么由这个进程使用fork()创建的子进程就不会继承特权调度策略和优先级了。其规则如下:

  • 如果调用进程拥有一个实时调度策略(SCHED_RR 或 SCHED_FIFO),那么子进程的策略会被重置为标准的循环时间分享策略 SCHED_OTHER。
  • 如果进程的 nice 值为负值(即高优先级),那么子进程的 nice 值会被重置为 0

SCHED_RESET_ON_FORK标记用于媒体回放应用程序,它允许创建单个拥有实时调度策略但不会将该策略传递给子进程的进程。使用SCHED_RESET_ON_FORK标记能够通过创建多个运行于实时调度策略下的子进程来防止创建试图超出RLIMIT_RTTIME 资源限制的子进程。

一旦进程启用了SCHED_RESET_ON_FORK 标记,那么只有特权进程(CAP_SYS_ NICE)才能够禁用该标记。当子进程被创建出来之后,它的 reset-on-fork 标记会被禁用。

释放CPU

实时进程可以通过两种方式自愿释放CPU:通过调用一个阻塞进程的系统调用(如从终端中read())或者调用sched_yield()

NAME
       sched_yield - yield the processor

SYNOPSIS
       #include <sched.h>

       int sched_yield(void);

DESCRIPTION
       sched_yield() 导致调用线程放弃 CPU。 线程被移动到队列的末尾以获取其静态优先级,并且一个新线程开始运行。 

RETURN VALUE
       成功时,sched_yield() 返回 0。错误时,返回 -1,并适当设置 errno。 

调用sched_yield()之后,如果存在于调用进程优先级相同的其他排队的可运行进程,那么调用进程会被放在队列的末尾,队列中队头的进程将会被调度使用CPU。如果在该优先级队列中不存在其他可运行进程,那么不会做任何事情,调用进程会继续使用CPU

虽然 SUSv3 允许 sched_yield()返回一个错误,但在 Linux 或很多其他 UNIX 实现上这个系统调用总会成功。可移植的应用程序应该总是检查这个系统调用是否返回错误。

非实时进程使用 sched_yield()的结果是未定义的。

SCHED_RR时间片

通过过 sched_rr_get_interval()系统调用能够找出SCHED_RR进程在每次被授权使用CPU是分配到的时间片长度:

NAME
       sched_rr_get_interval - get the SCHED_RR interval for the named process

SYNOPSIS
       #include <sched.h>

       int sched_rr_get_interval(pid_t pid, struct timespec * tp);

与其他进程调度系统调用一样,pid 标识出了需查询信息的进程,当 pid 为 0 时表示调用进程。返回的时间片是由 tp 指向的 timespec 结构。

    struct timespec {
               time_t tv_sec;    /* seconds */
               long   tv_nsec;   /* nanoseconds */
           };

总结

默认的内核调度算法采用的是循环时间分享策略。默认情况下,这一策略下的所有进程都能平等的使用CPU,但是可以将进程的nice值设置为为一个范围从−20(高优先级)~+19(低优先级)的数字来影响调度器对进程的调度,但即使给一个进程设置了一个最低的优先级,它仍然有机会用到CPU

Linux还实现了POSIX实时调度扩展。这些扩展允许应用程序精确地控制如何分配CPU给进程。运作在两个实时调度策略SCHED_RR(循环)和 SCHED_FIFO(先入先出)下的进程的优先级总是高于允许在非实时策略下的优先级。实时进程优先级的取值范围为 1(低)~99(高)。只要进程处于可运行状态,那么优先级更高的进程就会完全将优先级低的进程排除在CPU之外。运行在SCHED_FIFO策略下的进程会互斥的访问CPU直到它执行终止或者自动释放CPU或者被进入可运行状态的优先级更高的进程抢占。类似的规则同样适用于 SCHED_RR 策略,但在该策略下,如果存在多个进程运行于同样的优先级下,那么 CPU 就会以循环的方式被这些进程共享

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值