POSIX与实时操作系统

13章 POSIX和实时操作系统

13.1 可移植操作系统接口简介

POSIX是“可移植操作系统接口”(portable operating system interface)的缩写,是由电气与电子工程师协会(IEEE)开发的一种开放式操作系统(OS)接口标准,并由国际标准化组织认可。

美国国家标准协会 (ANSI)。作为一种标准软件接口,POSIX 的主要目标是促进应用程序在不同 Unix 操作系统变体之间的互操作性和可移植性。理想情况下,只要两个操作系统都支持 POSIX,为一个操作系统编写并编译的程序也可以在不修改源代码的情况下为另一个操作系统编译并执行。

1988年,IEEE发布了其首个POSIX标准——IEEE Std1003.1‐1988。后续版本分别于 1990年、1996年和2001年发布。随着新标准的发布,“POSIX”这一术语被扩展用于指代一系列相关的标准,而POSIX.1则专指IEEE Std 1003.1‐1988及其后续版本。截至目前, POSIX.1标准的最新版本是IEEE Std1003.1‐2008,也称为POSIX.1‐2008[17]。

POSIX.1‐2008主要由POSIX基础定义(如并发执行、文件访问权限、内存同步、路径名解析和头文件)、系统接口(如套接字、线程、信号、流和实时服务)以及命令和实用程序(如exec、cat和echo)组成。表13.1列出了一些POSIX.1核心服务和实时扩展。

13.1.1 POSIX进程和线程

在POSIX中,每个正在运行的程序实例称为进程。每个进程都有其独立的受保护地址空间。进程之间相互隔离,以防止它们无意中侵犯彼此。由于这种强制分离,进程之间的通信只能通过使用操作系统内核服务来实现。

POSIX线程是运行在进程内的单个执行流。当一个进程开始运行时,实际上是在运行一个线程,这个线程通常被称为“主线程”,因为C程序的入口点是main()函数。主线程可以根据程序设计创建额外的线程。因此,进程实际上是一个包含一个或多个线程的集合。

作为一个单一的执行流,进程内的每个线程都有其自己的栈和寄存器值。然而,进程中运行的线程至少共享以下内容:
- 进程的虚拟地址空间(因此在线程之间切换不涉及更改内存上下文);
- 不在栈上的变量(因此线程可以通过全局变量共享数据并相互通信);
- 信号处理程序和进程级信号控制结构。

当实时嵌入式系统基于操作系统构建时,每个任务被实现为一个进程或仅仅是一个线程。

13.1.1.1 进程创建

对于由多个程序组成的大型(嵌入式)系统,通常会有一个主程序,也称为启动程序,用于启动所有其他程序。该启动程序本身通常是shell脚本文件(命令批处理)中的最后一条命令,在系统启动并运行后自动执行。

一个进程(实际上是进程内的线程)可以调用系统调用来创建新进程。新创建的进程被称为调用进程的子进程,而调用进程则被称为新进程的父进程。子进程与其父进程独立运行,但子进程会从其父进程继承许多属性。

POSIX 提供了以下系统调用用于启动新进程:
- fork() 。此调用会创建一个新进程,该进程复制调用进程:(a)两个进程执行相同的应用程序代码;(b)它们在此调用处保持执行同步——两者都即将从fork()调用返回。fork()调用的返回值可用于区分调用进程和新创建的子进程:在新创建的子进程中,返回值为0;而在父进程中,返回值是子进程的进程ID。当新进程刚被创建时,它只包含一个线程。如果从一个多线程进程中调用fork(),则新进程仅包含调用线程的副本。
- exec()函数族 。调用进程的进程映像被覆盖为从可执行文件构建的新进程映像。调用后不会返回到原进程。
- posix_spawn() 函数族 。fork() 的实现通常依赖于内存管理单元(MMU)服务,例如内存交换或动态地址转换,这对于实时应用而言通常太慢。posix_spawn() 函数(及其变体)是一种简单、快速的实现,无需地址转换或其他内存管理单元服务。成功完成后,posix_spawn() 将子进程的进程ID返回给父进程。
- system() 。此调用接收一个命令,启动一个 shell(命令语言解释器)来执行该命令。此调用在子进程终止之前不会返回。它可以通过 posix_spawn() 调用来实现,或者通过 fork() 后跟 exec() 来实现。

13.1.1.2 线程创建

创建后,一个进程具有单个主线程或初始线程。活动线程被指示执行程序代码,该代码可能包含用于创建额外线程的系统调用。POSIX函数pthread_create()用于在进程中创建新线程。

进程内的线程之间不存在父进程与子进程的关系。线程可以自行终止。如果一个进程中的任意线程调用了 exit(),则该进程及其所有线程都将终止。

13.1.2 POSIX实时扩展

许多实时嵌入式系统资源有限,不需要文件系统或任务的独立地址空间等高级功能。对于此类系统,最好基于仅支持特定子集POSIX函数的操作系统来实现。为满足这一需求,实时配置文件标准 IEEE Std 1003.13‐1998 于1998年首次发布。它包含四个实时应用程序环境配置文件:
1. 最小配置文件:用于构建没有内存管理单元、文件系统或I/O终端的小型嵌入式系统。只允许一个进程,但允许多个线程。
2. 实时系统最小配置文件:用于构建具有文件系统和I/O终端的实时控制器。只允许一个进程,但允许多个线程。
3. 嵌入式系统有限配置文件:用于构建没有文件系统的大规模嵌入式系统。允许多个进程和线程。
4. 最大配置文件:用于构建支持所有功能的大型实时系统和嵌入式系统。

IEEE Std 1003.1‐2008,即POSIX.1标准的最新版本,还包含了一些与实时系统相关的amendments或扩展:
- IEEE Std 1003.1b‐1993 实时扩展;
- IEEE Std 1003.1c‐1995 线程;
- IEEE Std 1003.1d‐1999 附加实时扩展;以及
- IEEE Std 1003.1j‐2000 高级实时扩展。

以下,我们介绍一些在实时扩展中指定的POSIX.1服务。

13.1.2.1 基于优先级的调度

线程是可调度的实体(即承载作业的资源)。操作系统调度器在任何时刻最多选择一个线程在每个处理器上执行。

每个POSIX线程在创建时都会关联一个称为调度优先级的数值,操作系统调度器使用该数值来确定何时以及如何调度该线程。我们考虑基于优先级的调度中的三个原则。
1. 优先级原则 。当没有线程在运行,且多个线程准备被调度执行时,具有更高优先级的线程总是优先于低优先级线程被调度。
2. 抢占原则 。当一个线程正在运行,而另一个更高优先级的线程变为准备执行状态时,该更高优先级的线程将抢占当前线程的执行。
3. 公平性原则 。当多个具有相同或不同优先级的线程竞争使用处理资源时,系统的调度行为由与这些线程关联的调度策略进行调控。

优先级原则是基于优先级的调度的基础。但是,如果一个线程就绪时,一个较低优先级的线程正在运行会发生什么?让我们首先检查如图13.1所示的一种称为非抢占式调度的方法。这里每个任务都作为一个线程实现。任务 T1(即相应的线程)具有最低优先级,任务 T3具有最高优先级,而任务 T2的优先级居中。任务 T1的第一个作业 J11在时间0释放。由于 J11是唯一的作业,因此被调度执行。作业 J21和 J31分别在时间 15和时间20释放。然而,这两个作业尽管各自优先级高于 J11,,也不会抢占 J11的执行。一旦 J11在时间25完成,根据优先级原则,J31将被调度执行。当 J31在时间40完成时, J21被调度执行。请注意,此调度导致 J21的截止时间被打破。

示意图0

图13.2 展示了另一个应用抢占原则的场景。这三个任务的参数仍与抢占原则中给出的相同。当作业J21在时间15被释放时,根据抢占原则,它会抢占作业J11。类似地,在时间20,作业J31抢占作业J21,,后者在时间35当J31执行完毕后恢复执行。同样,J11在时间40当J21执行完毕后恢复执行。根据此调度,所有三个任务都能满足它们的截止时间。这种调度方式称为抢占式调度。

当任务(或线程)与调度策略相关联时,公平性原则适用。POSIX.1‐2008 标准规定了四种调度策略:SCHED_FIFO、SCHED_RR、SCHED_SPORADIC 和 SCHED_OTHER。这些策略及相关结构在表13.2中进行了描述。

每个 POSIX 进程/线程都可以与一个调度策略及其所选调度策略相关的属性相关联。具体来说:
- 在创建进程时,可以通过 posix_spawn 调用指定其调度策略和属性,或者通过 fork 调用继承父进程的策略和优先级设置。
- 通过显式调用函数 sched_setscheduler() 或 sched_setparam(),一个进程可以设置或更改自身或其他进程的调度策略和/或属性。
- 在线程创建时(通过 pthread_create 调用),可以为其指定调度策略和调度属性。
- 通过显式调用函数 pthread_setschedparam() 或 pthread_setschedprio(),调用线程可以设置/更改同一进程内某个线程的调度策略和/或属性。

进程的调度策略和属性(如果有的话)仅对单个线程的调度行为产生间接影响。创建进程时,其唯一的线程会继承该进程的调度策略以及相关的调度属性。新创建的线程也可以选择继承创建线程的调度属性,如果这些属性是可取的话。

POSIX 定义 描述
SCHED_FIFO_
SCHED RR_
SCHED SPORADIC_
SCHED OTHER_
表示先进先出策略的整数常量
一个表示轮转调度策略的整数常量
一个表示偶发服务器策略的整数常量
一个表示任何其他策略的整数常量
struct sched_param sched_priority:调度优先级的数值(更高值表示更高优先级)
sched_ss_low_priority:SCHED_SPORADIC 的低优先级
sched_ss_repl_period:SCHED_SPORADIC 的补充周期
sched_ss_init_budget:SCHED_SPORADIC 的初始预算
sched_ss_max_repl:SCHED_SPORADIC 的最大补充次数
sched_setparam
sched setscheduler_
pthread_setschedparam
pthread_setschedprio
为指定进程设置调度参数
为指定的进程设置调度策略和参数
为指定线程设置调度策略和参数
为指定线程动态设置调度优先级

策略SCHED_FIFO、SCHED_RR和SCHED_SPORADIC在第13.4节中进一步讨论。POSIX调度策略SCHED_OTHER取决于具体实现;它可能是一种实时调度策略,也可能不是。

13.1.2.2 任务同步

POSIX.1‐2008 标准定义了用于管理进程同步的信号量函数。信号量是一种操作系统内核对象,可实现对共享资源的互斥访问。POSIX 信号量无法避免一种称为“无界优先级反转”的不良问题,该问题发生在高优先级任务需要等待低优先级任务完成其受信号量保护的操作时。有关信号量的使用,请参见第18.3节。

POSIX.1‐2008 标准还定义了mutexes用于进程内的线程同步。如果配置得当,互斥锁对象也可用于进程间同步。POSIX 互斥锁具有多个重要属性:
- 所有权 。当对互斥锁对象调用 pthread_mutex_lock() 成功时,该互斥锁被调用线程锁定,此线程成为该互斥锁的所有者。一个被锁定的互斥锁只能由其所有者通过调用 pthread_mutex_unlock() 来解锁。
- 优先级上限 。这定义了由互斥锁保护的临界区执行时的最低优先级。为了在设计时避免优先级反转,互斥锁的优先级上限应设置为高于或等于所有可能锁定该互斥锁的线程中的最高优先级。
- 协议 。可以通过调用函数 pthread_mutexattr_setprotocol() 将其设置为以下三种协议之一:PTHREAD_PRIO_NONE、PTHREAD_PRIO_INHERIT 和 PTHREAD_PRIO_PROTECT。默认的协议值为 PTHREAD_PRIO_NONE。

简而言之,拥有互斥锁的线程的优先级和调度可能会受到如下影响:
- 线程的优先级和调度不受拥有具有PTHREAD_PRIO_NONE协议属性的互斥锁的影响。
- 当一个线程由于拥有一个或多个具有PTHREAD_PRIO_INHERIT协议属性的互斥锁而阻塞高优先级线程时,该线程应以其自身的优先级与所有因等待该线程所拥有的、且使用此协议初始化的互斥锁的线程中的最高优先级两者之间的较高者运行。
- 当一个线程拥有一个或多个使用PTHREAD_PRIO_PROTECT协议初始化的互斥锁时,该线程应以其自身的优先级和所有由该线程拥有并使用此协议初始化的互斥锁的优先级上限中的较高者来执行,而无论是否有其他线程阻塞在这些互斥锁上。

如果一个线程同时拥有以不同协议初始化的多个互斥锁,则该线程应以其通过这些协议获得的优先级中的最高优先级执行。

通过使用具有PTHREAD_PRIO_INHERIT或PTHREAD_PRIO_PROTECT协议的互斥锁,可以避免无界优先级反转。有关这些协议的详细信息,请参见第14.5节。有关互斥锁的使用,请参见第18.3节。

13.1.2.3 其他服务

图13.3 给出了操作系统内核中通常实现的一些对象。

POSIX 定义了一种用于异步进程间通信的信号机制:尽管任务可以指定在信号到达时执行的特定操作,但任务无法预测信号何时到达。POSIX 信号可以在接收进程处排队,从而避免事件或请求丢失。每个信号都关联一个优先级;实时信号按优先级顺序出队,使得紧急事件能够更早被处理并更快响应。信号处理的详细信息参见第21章。

POSIX.1‐2008 标准要求操作系统实现至少支持一个由常量 CLOCK_REALTIME 标识的实时钟,默认情况下,该实时钟表示自纪元以来经过的时间(以秒或纳秒为单位)。实时钟可以以有限的速率跳动,例如 1000 赫兹。时钟的分辨率(也称为其粒度)是其跳动速率的倒数。例如,跳动速率为 1000 赫兹的时钟分辨率为 1 毫秒。有关时间分辨率的更多信息,请参见第22章。

表13.3 给出了用于实时钟的一些POSIX函数。对于特定的实时钟,用户可以获取其分辨率、获取当前时间或为该时钟设置新时间。用户还可以通过调用睡眠函数来挂起线程的执行,这些睡眠函数可接受相对或绝对时间值,时间值可以以秒(低分辨率)或纳秒(高分辨率)为单位。如果相对睡眠函数被信号中断,则返回剩余时间。在这种情况下,如有必要,可使用剩余时间重新发出另一次函数调用以满足原始时间要求。如果绝对睡眠函数(即在绝对模式下调用 clock_nanosleep())被信号中断,可以使用与第一次调用相同的绝对时间再次调用。

POSIX实时时钟可用于控制实时任务的执行,以满足其时序要求。特别是,在实时系统中存在一些需要被挂起并以周期性方式多次激活的任务(线程)。例如,显示设备可能需要定期刷新,或非中断设备的状态可能需要定期轮询。这种周期性行为可以通过使用实时时钟来实现。例如,假设一个周期性任务T1=(p1,e1)在时间t0(这是一个绝对时间点)开始执行,并在时间t1= t0+e1运行完成。它需要将自身挂起,直到下一个周期开始,即时间t0+p1。这可以通过调用clock_nanosleep()函数并以绝对时间点 t0+p1作为请求的时间参数“精确”地实现。

需要注意的是,通过调用相对睡眠函数(如 sleep()、nanosleep() 或 clock_nanosleep() 的相对版本)可能无法实现“精确的周期性激活”。例如,如果任务 T1 改为使用 nanosleep(),则在调用 nanosleep() 函数之前,必须先调用 clock_gettime() 获取当前时间,然后计算当前时间与 t0+p1, 之间的差值,最后使用计算出的时间间隔调用 nanosleep()。然而,这两个函数调用之间任务可能会被其他任务抢占。在这种情况下,计算出的时间间隔将不准确,任务 T1 将比预期更晚唤醒。使用绝对版本的 clock_nanosleep() 函数则不会出现此问题,因为只需一次函数调用即可将任务挂起至期望的时间。

POSIX定时器 也可用于控制实时任务的执行,以满足其时序要求。一个进程可以创建多个定时器3来计数特定的时间间隔。由一个进程创建的定时器不能被其子进程继承。定时器依赖于信号的使用,4并且必须引用实时钟作为其计时基准。如图13.3所示,一个定时器可以指定两个时间间隔:定时器周期和到期值,前者用于定义周期性定时器,后者指定单次触发定时器的超时时间或周期性定时器的首次到期值。当指定的超时时间到达后,定时器可向创建该定时器的进程发送一个“超时”信号。对于给定的定时器,在任何时刻只能有一个超时信号被排队到该进程中。周期定时器可以生成数量不限的超时信号。有关定时器使用的更多信息,请参见第15章和 第22章。

POSIX.1‐2008 标准将消息队列 定义为一种异步进程间通信机制。连接到消息队列的进程可以向其中发送结构化消息或从中读取消息。传输和接收不是同步的,即发送方不会等待接收方实际从队列中取走消息。消息的传输和接收可以是阻塞的或非阻塞的。第19章描述了用于消息队列的POSIX函数及其使用方法。

POSIX.1标准还规定了内存映射文件和共享内存对象作为进程共享物理内存部分的方法。当实时应用需要以极小开销共享大量数据时,这一点非常有用。内存映射文件提供了一种机制,允许进程将其地址空间直接映射文件内容;由于采用直接内存操作,文件数据访问速度非常快。如果多个进程映射同一个文件,则它们共享该文件的内容。特别地,通过一个进程的地址空间写入内存对象的数据,会出现在所有映射同一内存对象区域的进程的地址空间中。有关共享内存对象的使用,请参见第18章。

13.1.3 POSIX合规性与一致性

POSIX 实现合规性 意味着操作系统实现部分支持POSIX.1标准,并且有文档说明支持哪些POSIX功能以及不支持哪些功能。

POSIX 实现一致性 意味着一个操作系统实现完全支持 POSIX.1 标准中定义的所有强制性系统接口(函数和头文件)、工具和功能,以及一些标准扩展。

根据符合POSIX.1标准的程度,可以将操作系统分为部分符合POSIX.1或完全(100 %)符合POSIX.1(一致性)。一些操作系统,如Solaris和QNX [8],是完全符合的,而其他一些,如GNU/Linux和VxWorks,则不是完全符合的。

POSIX 应用程序一致性 意味着一个应用程序严格符合POSIX.1标准,即它仅依赖于标准中描述的特性。应用程序一致性对于支持软件互操作性和可移植性至关重要。为一个符合POSIX的操作系统编写的代码通常可以移植到另一个符合POSIX的操作系统上,甚至可以以较低的成本移植到部分兼容的操作系统上。此外,熟悉一个符合 POSIX的操作系统的程序员可以直接将其技能应用于涉及其他符合POSIX的操作系统的项目中。

13.2 任务静态与动态

13.2.1 一般任务结构

图13.4 展示了表示“任务”概念静态结构的UML类图: 任务是一种可调度实体。为了实现可调度实体接口,任务具有调度策略和分配的优先级等属性。任务可能会以不同的优先级(即实际优先级)临时运行。任务还可以根据所选的调度策略指定其他相关参数。
- 任务可以具有任务参数,例如周期、释放时间、执行时间和截止时间。任务可能是周期性任务、非周期性任务或偶发性任务。在任意时刻,任务可能处于四种状态之一:就绪、运行、阻塞或睡眠。
- 任务是一个抽象概念。线程(实现)了一个任务。线程的线程ID是一个唯一标识该线程的整数。
- 线程包含在进程内。每个线程都关联一个任务例程,其中程序计数器指向该例程的当前指令。
- 线程具有一个执行栈,用于表示线程的当前执行状态。该栈由函数调用期间形成的多个栈帧组成。每个栈帧包含多个对象,每个对象保存某个寄存器的值。执行栈对于上下文切换至关重要,在上下文切换过程中,当前正在运行的任务被暂停,并与另一个任务进行交换(该替换任务成为新的运行任务)。在上下文切换期间,处理器所使用的所有寄存器的值会被保存到当前正在运行的任务的执行栈中,而替换任务的执行栈(如果非空)则用于恢复其最新的执行状态。
- 线程还具有一个控制块对象,用于保存与调度、资源访问和性能监控相关的线程级信息,例如累计运行时间、阻塞时间、阻塞信号的掩码、待处理信号队列以及持有的互斥锁列表。为了实现调度和阻塞等动态功能,线程控制块还可能包含指向其他线程控制块的指针。

进程是多个线程的容器。特别是,每个进程都有一个主线程,该主线程是一个具有执行入口点的特殊线程。
- 进程具有一个属性PID——一个唯一标识该进程的整数。一个进程可以有多个子进程,并且可能有一个父进程。
- 进程映像由可执行文件对象创建。一旦形成进程映像,它就拥有自己独立于其他进程的虚拟地址空间。
- 进程的虚拟地址空间包含其内每个线程的执行栈以及每个线程的任务例程。
- 进程还具有一个控制块对象,其中包含用于高效管理进程的进程级信息,例如被忽略信号的掩码、待处理信号队列、指向信号处理程序的指针,以及进程使用的进程级资源(如I/O设备、定时器和消息队列)。

13.2.2 任务状态转换

在任何时刻,一个任务(线程)都处于四种状态之一:就绪、运行、睡眠或阻塞。在讨论状态转换之前,我们首先介绍操作系统用于线程(任务)调度的一些数据结构:
- 有序列表是一种数据结构,包含零个、一个或多个以特定方式排序的节点。有序列表一端的节点称为头节点,另一端的节点称为尾节点。可以从列表中删除现有节点,也可以向列表中添加新节点。
- 一个按时间排序的列表是一种节点按其在列表中的停留时间进行排序的列表结构。通常,列表的头部是停留在列表中时间最长的节点,而尾部是停留在列表中时间最短的节点。
- 阻塞线程列表 是一个有序列表,其中每个节点代表一个处于阻塞状态的线程。
- 睡眠线程列表 是一个有序列表,其中每个节点表示处于睡眠状态的线程。
- 就绪线程列表 是一个按时间排序的列表,其中每个节点表示处于就绪状态(即准备执行)的线程。

由于多个任务可能以相同的优先级运行,因此从概念上讲,每个优先级级别都有一个就绪线程列表。级别 i 的就绪线程列表仅包含优先级为i的线程。一个就绪线程列表在级别i上被称为最高优先级非空就绪线程列表(HNERT),如果它至少包含一个节点,并且对于任意j>i,级别j的就绪线程列表为空。

图13.5 展示了一个UML状态图,显示了任务(线程)的动态状态转换。它具有以下转换规则:
1. 线程在创建后即准备执行,它将被放置在其优先级对应的就绪线程列表的尾部。
2. 在任意时刻,单处理器系统上只有一个运行线程。当当前没有线程在运行时,符合POSIX.1标准的操作系统会选择HNERT头部的线程,将其从HNERT中移除,并主动将执行控制权转移给该线程。
3. 当运行线程的优先级i不再最高——例如,一个优先级高于i的任务变为就绪状态时——该线程会被抢占,并成为第i级就绪线程列表的头部(此规则符合SCHED_FIFO策略)。随后选择另一个线程作为新的运行线程。
4. 当优先级为i的运行线程用尽其调度策略规定的时间片时,它将成为第i级就绪线程列表的尾部(此规则符合SCHED_RR策略)。随后选择另一个线程作为新的运行线程。
5. 当运行线程调用阻塞函数且相应的阻塞条件为真时,该线程将被阻塞。示例包括申请不可用的信号量或互斥锁、从空管道读取数据、向可用空间小于请求大小的管道写入数据、向已满的消息队列发送消息,以及从空的消息队列接收消息。被阻塞的线程将被放入阻塞线程列表中,并选择另一个就绪线程作为新的运行线程。
6. 当一个阻塞线程被解除阻塞时,要么阻塞条件已消除,要么被信号中断,该线程将从阻塞线程列表中移除,并成为其优先级对应的就绪线程列表的尾部成员。
7. 当运行线程通过调用睡眠函数主动暂停其执行时,它开始进入睡眠状态,并被放入睡眠线程列表中。另一个线程被选为新的运行线程。
8. 当睡眠线程被唤醒时,可能是请求的睡眠时间已到,或是被信号中断,该线程将从睡眠线程列表中移除,并成为其优先级对应的就绪线程列表的尾部成员。
9. 当运行线程调用sched_yield()函数时,它将成为其优先级对应的就绪线程列表的尾部成员,并选择另一个线程作为新的运行线程。
10. 当通过调用 pthread_setschedprio() 修改了运行或就绪线程的优先级时,
- 如果其优先级被提高,该线程将成为对应其新优先级的就绪线程列表的尾部;
- 如果其优先级被降低,该线程将成为对应其新优先级的就绪线程列表的头部。
11. 当运行或就绪线程的优先级或策略被 pthread_setschedprio() 以外的函数修改时,它将成为对应其新优先级的就绪线程列表的尾部。

13章 POSIX和实时操作系统

13.3 实时操作系统

通用操作系统是管理用户应用程序和计算机硬件资源的系统软件,它定义了规则和编程接口,使程序能够请求操作系统服务并与系统其余部分交互 [47, 69]。具备某些实时特性的通用操作系统可用于软实时应用程序。

然而,对于硬实时系统而言,必须使用实时操作系统(RTOS),以便以有界响应时间响应通常与硬时间约束(截止时间)相关的外部事件。例如,计算机数控(CNC)机床在底层是步进式操作的:该步进操作是一个周期性任务(例如,周期为1ms)。在每个步进过程中,计算机数控(CNC)机床必须监控切削头的当前位置,实时计算轨迹,然后驱动切削头到达下一个位置。为了精确地遵循预设的切割路径,计算机数控(CNC)机床显然需要一个实时操作系统(RTOS)来生成精确计时的脉冲序列,以控制切削头的运动。

那么,什么是实时操作系统呢?毫无疑问,实时操作系统首先是一个操作系统,由多个软件子系统组成,其中以内核为核心。根据具体实现的不同,实时操作系统可能提供许多通用操作系统中常见的服务,例如多任务、优先级、任务抢占、资源共享和任务间通信。

当然,实时操作系统(RTOS)不仅仅是通用操作系统。RTOS 采用专门的调度算法,旨在运行具有软截止时间和/或硬截止时间的实时应用。因此,RTOS 的关键特性是其对事件的响应时间应为确定性的(或可预测的)。换句话说,RTOS 应提供可靠的机制,例如实时信号、抢占式调度和非阻塞进程间通信,以实现确定性响应 [43, 57]。

硬件设备X的事件响应时间是指从中断请求被提出到设备X的服务任务完全处理该请求的时间间隔。它包括中断延迟、设备X的中断服务程序(ISR)的执行时间、调度延迟以及设备X的服务任务的执行时间。
- 中断延迟 指的是从中断请求被提出到相应ISR的第一条指令开始执行之间的时间间隔。部分中断延迟可能是由于软件禁用了中断,如图13.6所示,或正在执行更高优先级设备的ISR所致。这部分完全由系统设计者掌控,任何实时操作系统都无法弥补不良的设计。良好的做法是尽可能缩短禁用中断的代码段。此外,硬件设备的优先级应合理设计,以使关键设备的中断延迟得到合理优化。
- 中断延迟的最后一部分是由上下文切换引起的,此时需要保存当前执行上下文并将其切换为另一个任务(在本例中为中断服务例程)。上下文切换时间是实时操作系统的一个重要性能指标。
- 调度延迟 是指服务任务处于就绪状态但尚未开始执行的时间间隔。它可能由高优先级任务的执行、上下文切换以及调度开销引起。调度开销指的是内核调度器的执行时间。
- 服务任务的执行时间 取决于任务例程的长度。任务的代码(包括中断服务例程)可能会调用系统调用来请求各种操作系统服务。系统调用的响应时间是实时操作系统的一个重要性能指标。此外,任务可能与其他任务进行通信;每次任务间通信都会引入通信延迟。

总之,实时操作系统通常通过确定性的响应来实现
- 支持抢占式优先级调度:高优先级任务总是抢占低优先级任务,无论用户任务或系统任务。
- 在上下文切换时间、调度开销和系统调用方面提供有界延迟。有界延迟意味着对延迟具有可测量的保证,这意味着响应时间应该是
- 短且可预测 :上下文切换时间、调度开销以及系统调用的完成时间应较短,并处于已知延迟范围内。
- 具有低抖动 :在所有可能情况下,响应时间(针对上下文切换、调度开销以及每种系统调用)的方差应非常小。例如,一个系统调用平均可在10毫秒内响应,但可能偶尔出现高达20毫秒的峰值。最坏情况值通常用于可调度性分析。
- 提供高效的中断服务例程:默认中断服务例程(信号处理程序)的执行时间应短且有界。
- 支持确定性同步:多个任务可以在可预测的时间内进行通信。

最广泛采用的实时操作系统包括RTLinux、Windows CE、LynxOS、VxWorks和 QNX等[24]。例如,VxWorks是一种商业实时操作系统,支持具有256个优先级的抢占式优先级调度,以及信号量、消息队列和高速任务间通信。VxWorks适用于多种处理器平台,包括x86、PowerPC、ARM、MIPS、奔腾和SPARC。QNX Neutrino [8]被广泛用作许多嵌入式实时应用的基础,如汽车机电部件、医疗仪器、国防系统和核电站。QNX基于微内核架构,如图13.7所示,该架构可使系统缩小到非常小的尺寸,同时仍提供多任务、抢占式调度和快速上下文切换。QNX Neutrino可在多种现代处理器平台上运行,包括PowerPC、x86、MIPS、ARM和XScale。

我们已经知道,实时操作系统仅代表嵌入式软件的一种架构类型。对于规模较小、功能有限或仅有软定时约束的实时嵌入式系统,您不一定需要使用实时操作系统。如果在关键任务系统中使用实时操作系统是最佳选择,那么为了在市场上可用的实时操作系统中做出“明智”的选择,您可能需要考虑诸如实时特性(有界延迟)、可移植性(处理器支持)、可扩展性(操作系统内存使用)、支持(开发工具、设备驱动程序和板级支持包)以及预算限制等因素。

13.4 POSIX实时调度策略

在第13.1.2.1节中,我们讨论了优先级原则和抢占原则,这些原则适用于准备执行的任务具有不同优先级的情况。调度策略的目的是为具有相同优先级的任务定义公平规则(例如,SCHED_FIFO 和 SCHED_RR)或具有有界执行预算的任务(例如,SCHED_SPORADIC)。

13.4.1 先进先出调度策略

由于SCHED_FIFO(“先进先出”)调度策略仅适用于具有相同优先级的任务调度,因此我们在此重点关注优先级为k的就绪线程列表——即具有优先级k的任务。

假设第 k 级就绪线程列表包含以下线程:[T1, T2, …, Tn]。我们用 T0 表示具有相同优先级 k 的运行线程。根据就绪线程列表的定义,如果 1 ≤i<j ≤ n,则 Ti 在列表中的时间必须比 Tj 更长。

对于位于层级 k就绪线程列表中的任务,SCHED_FIFO 策略规定,任务 Ti绝不会被任何任务 Tj(j>i)抢占。换句话说,任务 Ti+1只能被调度执行,直到 Ti运行完成之后。

图13.8 给出了一个示例,其中系统由三个具有相同优先级的周期性任务组成。作业 J11 在时间0就绪(被释放)。由于 T1 是就绪列表中唯一的任务,因此它成为运行线程。在时间1,作业 J21 被释放并添加到就绪列表中。在时间2,J11 完成,J21 成为新的正在运行的作业。同时,作业 J31 在时间2被释放并添加到就绪列表中。在时间4,J21 完成, J31 成为新的正在运行的作业。在时间6,作业 J12 被释放并添加到就绪列表中。作为练习,您可以继续此分析。有一点是明确的:任何稍后释放的作业从未抢占过更早释放的作业。

请注意,作业 J14在时间24时其截止时间被打破。

13.4.2 时间片轮转调度策略

与SCHED_FIFO类似,SCHED_RR(轮转调度)调度策略仅适用于调度具有相同优先级的任务。对于实现SCHED_RR策略的操作系统,时间片的长度(也称为时间片)由POSIX函数sched_rr_get_interval()返回。

SCHED_RR 策略规定:(1)每个就绪线程列表上的所有任务按照其顺序依次被调度;(2)当需要选择一个新的运行线程时,如果级别 k 的就绪线程列表为 HNERT,则将头部线程从列表中移除并成为运行线程;(3)一个运行线程一旦用完其时间片,即成为其优先级对应就绪线程列表的尾部线程。

图13.9 给出了一个示例,其中系统由三个具有相同优先级的周期性任务组成。作业 J11 在时间0被释放并成为正在运行的作业。在时间1,作业J21被释放并加入就绪列表。从该时刻开始,两个作业J11和J21,将轮流执行。在时间2,作业J31被释放并同样加入轮转调度。一旦作业运行完成,即退出轮转调度。

SCHED_RR 策略实际上提供了一种时间分片机制,确保当存在其他相同优先级的线程时,一个任务不会独占处理器。这种机制可以通过使用时钟(定时器)中断来实现。此外,如果系统中存在不同优先级的任务,则就绪线程列表上的任务轮转调度可能会被更高优先级任务的执行所抢占(“中断”)。

13.4.3 偶发服务器调度策略

调度策略 SCHED_SPORADIC(偶发性服务器)允许任务在运行时更改其优先级。我们关注使用 SCHED_SPORADIC 调度策略的特定线程Ti。Ti的调度参数如下所示:
- 一个正常优先级,表示为 μ;
- 一个低优先级,用 ω 表示,其中 ω< μ;
- 一个补充周期,表示为p;以及
- 一个初始执行预算,表示为 e。

线程 Ti 在其创建时获得初始执行预算 e。当 Ti 运行时,预算值 e 将被消耗。当 e> 0 时,线程 Ti 的有效优先级为 μ,否则为 ω。根据其有效优先级,当 Ti 处于就绪状态 时,它将位于第 μ 级就绪列表或第 ω 级就绪列表中。

Ti的执行预算可以通过补充动作进行 replenishment。每次补充动作由 〈t, v〉定义,其中t为执行此补充动作的时间, v为要补充的数量。

Ti的调度行为受以下规则约束:
1. 每当Ti被放置到第μ级就绪列表的尾部时,该时间点会被 τ记录。τ称为Ti的最新激活时间。在Ti创建时,其具有 τ= 0。 τ随时间变化。
2. 当Ti以优先级μ运行时,其预算以每单位执行时间消耗1的速率被消耗。
3. 当以优先级μ运行的Ti的预算变为 0时,它将被置于第ω级就绪列表的尾部。操作系统会安排一个补充动作 〈t, v〉,其中t = τ+ p,且v等于自最新激活时间 τ以来已消耗的预算。如果t早于当前时间,则立即执行该补充动作。
4. 当以优先级μ运行的Ti变为阻塞线程时,操作系统会安排一个补充动作 〈t, v〉,其中t= τ+ p,且v等于自最新激活时间 τ以来已消耗的预算。如果t早于当前时间,则立即执行该补充动作。
5. 当以优先级μ运行的Ti被抢占时,它将成为 第μ级就绪列表的头部。在这种情况下, τ不发生变化。
6. 当Ti位于第ω级就绪列表 的头部,且该列表为HNERT时,它将成为运行线程。当Ti以优先级ω运行时,其预算保持不变。
7. 当到达执行补充动作〈t, v〉的时间(即当前时间不早于t)时,Ti的预算将被更改:e = e + v。如果Ti的有效优先级为ω,则将其更改为μ,并且Ti将被置于第μ级就绪列表的尾部。

例如,考虑一个由两个任务(线程)T1和T2组成的系统(假设使用实时操作系统时,系统任务的执行开销可以忽略不计)。这两个任务共享一个由信号量保护的互斥资源。如图13.10所示,任务T1的释放时间、周期、执行时间需求和截止时间分别为5、120、35和80;任务T2的任务参数分别为0、60、20和60。任务T2具有固定优先级50;任务T1采用SCHED_SPORADIC策略,其参数如下:
- μ= 60
- ω= 40
- p= 30;以及
- e= 10.

下面解释 图13.10 中给出的调度场景:
1. 在时间0系统启动时,线程 T2就绪并成为运行线程。在时间2.5,T2请求并成功获取了资源。我们还有 τ= 0。
2. 在时间5,线程 T1就绪。它抢占了T2的执行并成为新的运行线程。
3. 从时间5到10,T1以优先级60执行。其执行预算在时间10减少到 5。
4. 在时间10,T1请求当前被T2锁定的资源。T1被阻塞并放入阻塞线程列表。操作系统调度一个补充动作 〈t, v〉,其中t= τ+ p= 0+ 30= 30,以及 v= 5,即从时间0以来已消耗的预算。此时,T2是唯一准备执行的线程,因此它成为运行线程。
5. 从时间10到15,T2执行。
6. 在时间15,T2释放资源,导致T1从阻塞列表中移除并成为优先级60就绪列表 的尾部。现在我们有 τ= 15。此外,T1立即抢占T2并成为运行线程。
7. 从时间15到 20,T1以优先级60执行。其执行预算在时间20变为0。
8. 在时间20,由于已耗尽全部预算,T1成为优先级40就绪列表的尾部。操作系统调度一个补充动作 〈t, v〉,其中 t = τ+p = 15+ 30= 45,以及 v= 5,即从时间15以来已消耗的预算。此时,T2是 HNERT的头部,并成为运行线程。
9. 从时间20到25,T2执行。
10. 在时间25,T2 请求当前被T1锁定的资源。T2被阻塞并放入阻塞线程列表。此时,T1是唯一准备执行的线程,因此它成为运行线程。
11. 从时间25到30,T1以优先级40执行。其执行预算 保持为0。
12. 在时间30,操作系统执行补充动作 〈30, 5〉,T1的预算变为5且其优先级 恢复为60。同时我们有 τ= 30。
13. 从时间30到35,T1以优先级60执行。其执行预算在时间35变为0。在时间32.5,T1释放资源,导致T2从阻塞列表中移除并成为优先级50就绪列表的尾部。
14. 在时间35,由于已耗尽全部预算,T1成为优先级40就绪列表的尾部。操作系统调度一个补充动作 〈t, v〉,其中t = τ+p = 30+ 30= 60,以及 v= 5,即从时间30以来已消耗的预算。此时,T2是HNERT的头部,并成为运行线程。
15. 从时间35到40,T2执行。
16. 在时间40,T2释放资源并运行完成。此时, T1是唯一准备执行的线程,因此它成为运行线程。
17. 从时间40到45,T1以优先级 40执行。其执行预算保持为0。
18. 在时间45,操作系统执行补充动作 〈45, 5〉,T1的 预算变为5且其优先级恢复为60。同时我们有 τ= 45。
19. 从时间45到50,T1以优先级60执行。其执行预算在时间50变为0。
20. 在时间50,由于已耗尽全部预算,T1成 为优先级40就绪列表的尾部。操作系统调度一个补充动作 〈t, v〉,其中t = τ+p = 45+ 30= 75,以及 v= 5,即从时间45以来已消耗的预算。此时,T1是唯一的就绪线程,并成为运行线程。
21. 从时间50到55,T1以优先级 40执行。其执行预算保持 为0。
22. 在时间55,T1运行完成。
23. 在时间60,T2的第二个作业被释放并开始运行。

13.5 其他实时调度策略

13.5.1 最小松弛度优先

任务laxity定义为剩余时间(截止时间之前的时间)与完成该任务仍需时间之间的差值。更正式地,给定一个任务Ti=(pi,ri,ei,di),其松弛时间定义为
(di − t)− e′i,
其中t是自当前周期开始以来经过的时间,而e′i ≤ ei是其剩余的执行时间需求。

松弛度是衡量任务调度灵活性的指标。若一个任务的松弛度为t,则意味着即使该任务延迟t个时间单位,仍可满足其截止时间。若松弛度过零,则表示该任务必须立即开始exec,否则将有无法满足截止时间的风险。

最小松弛度优先调度策略,也称为最小松弛度优先或最少松弛时间调度,是一种动态优先级方法,其中松弛度较小的任务具有更高优先级。显然,松弛度最小的任务具有最高优先级。

为了实现该策略,操作系统调度器需要持续监控所有就绪作业的松弛度,并将其与 正在运行的作业的松弛度进行比较。每当作业的相对松弛度顺序发生变化时,调度器就会重新分配作业的优先级,允许具有相同松弛度的作业以轮转调度方式执行。

显然,由于持续计算带来的运行时开销是显著的,过多的轮转调度会带来不必要的上下文切换开销。在实际中,操作系统可能采用一种非严格方法,即仅在作业被释放或完成时才检查就绪作业的松弛度。

图13.11 给出了一个符合非严格最小松弛度优先策略的调度示例。在 图13.11底部的表格给出了相应“关键”时刻的作业松弛度。系统启动时,三个任务的第一个作业被释放,它们各自的松弛度分别为2.5、7和9。由于J11的松弛度最小,因此获得最高优先级,并在时间1.5运行完成。在时间1.5检查就绪作业的松弛度,J21的松弛度最小。因此,J21获得最高优先级并运行至时间4,此时作业J12被释放。这提供了一次重新检查就绪作业松弛度的机会。新释放的作业J12具有最小的松弛度,因此获得最高优先级,并在时间5.5运行完成。作为练习,请追踪该场景,观察作业松弛度如何变化,以及这些变化如何影响三个任务的调度。

13.5.2 最早截止时间优先

另一种常见的实现方法称为最早截止时间优先(EDF),这也是一种动态优先级调度策略。EDF策略要求截止时间较早的任务(作业)具有更高优先级。显然,截止时间最早的任务具有最高优先级。

图13.12 给出了一个基于EDF的调度示例。位于图13.12底部的表格给出了相应“关键”时间点的作业截止时间。系统启动时,两个任务的第一个作业被释放,它们各自的截止时间为2和5。由于J11的截止时间更早,因此获得更高优先级,并在时间1运行完成。在时间1,J21是唯一就绪的作业;它运行到时间2,此时作业J12被释放。由于J12的截止时间更早,被调度执行,并在时间3完成。J21从时间3恢复执行,并持续运行到时间5,此时作业J21完成,同时作业J22被释放。与此同时,作业J13在时间4被释放,截止时间为6。在时间5,J13开始运行,因为它比作业J22具有更早的截止时间。

在时间8发生了一个有趣的情况,此时作业J15和J22具有相同的截止时间。由于它们的优先级相同,操作系统以轮转调度方式调度它们。作业J15在时间10完成;然而,作业J22,未能满足其截止时间。

13.5.3 截止时间单调分配调度

截止时间单调分配方法是一种静态优先级调度策略,其中根据设计时已知的任务截止时间来为任务分配优先级:截止时间更早的任务被赋予更高优先级。与EDF方法不同,截止时间单调分配中的任务优先级是固定的。

图13.13 给出了基于截止时间单调分配的调度的一个示例。根据它们的截止时间,任务 T2具有最高优先级,任务 T3具有最低优先级,而任务 T1的优先级在中间。系统启动时,作业J11被释放并开始运行。在时间15,J21被释放,并抢占J11。作业J 31在时间20被释放,并因其优先级成为就绪列表的尾部。当J21在时间25完成时,由于J11具有比J31更高的优先级,它成为正在运行的作业。在时间35,J11完成,J31开始运行。

13.5.4 速率单调分配调度

速率单调分配方法也是一种静态优先级调度策略 [49, 65],,其中任务的优先级根据设计时已知的周期进行分配:为具有更短周期(即更高频率)的任务分配更高优先级。与截止时间单调分配类似,速率单调分配中的任务优先级是固定的。

图13.14 给出了基于速率单调分配的调度的一个示例。根据它们的周期,任务 T1具有最高优先级,任务 T3具有最低优先级,而任务 T2的优先级在中间。系统启动时,三个作业——J11, J21,和J 31——同时被释放。J11开始运行,因为它具有最高优先级。在时间7,J11执行完毕后,J21开始运行,因为它比J31具有更高优先级。在时间19,J21执行完成,J31开始运行。在时间24,J12,(任务T1,的第二个作业)变为就绪并开始运行。更多基于速率单调分配的可调度性分析原理在第16章中介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值