本章介绍如何编写和移植要在 SunOS 中运行的应用程序。本章是为精通编写实时应用程序的程序员以及熟悉实时处理和 Solaris 系统的管理员编写的。
本章讨论以下主题:
满足特定条件时才能保证实时响应。本节介绍了这些条件以及一些比较严重的设计错误。
此处所述的大多数潜在问题都会延长系统的响应时间。其中一个潜在问题可能会冻结工作站。其他更为隐蔽的错误包括优先级倒置和系统过载。
Solaris 实时进程具有以下特征:
本章通过单线程进程介绍实时操作,但是相关说明也可适用于多线程进程。有关多线程进程的详细信息,请参见《多线程编程指南》。要保证实时调度线程,必须将此线程创建为绑定线程。此外,还必须在 RT 调度类中运行此线程的 LWP。内存锁定和之前的动态绑定对于进程中的所有线程均有效。
如果进程是优先级最高的实时进程,则此进程会在变为可运行的保证分发延迟期间内占用处理器。有关更多信息,请参见分发延迟。只要此进程仍然是优先级最高的可运行进程,它便会继续运行。
实时进程可能会因为其他系统事件而失去对处理器的控制,还可能会因为其他系统事件而无法获取对处理器的控制。这些事件包括:外部事件(如中断)、资源匮乏、等待外部事件(如同步 I/O)以及进程被优先级较高的进程抢占。
实时调度通常不适用于系统初始化服务和终止服务,如 open(2) 和 close(2)。
本节所述的问题会在不同程度上延长系统的响应时间。这种延长可能会非常严重,以至导致应用程序错过临界期限。
实时处理也会破坏运行实时应用程序的系统上其他应用程序各方面的操作。由于实时进程具有较高的优先级,因此可能会长时间阻止分时进程运行。这种现象会导致交互式活动(如显示器和键盘响应时间)明显减慢。
SunOS 中的系统响应不提供 I/O 事件的时限。这意味着不应在任何执行时间性强的程序段中包含同步 I/O 调用。即使允许具有较长时限的程序段也不能执行同步 I/O。海量存储 I/O 即是这样一个示例,它会导致在执行读写操作时将系统挂起。
常见应用程序错误是执行 I/O 以从磁盘中获取错误消息文本。应该通过独立进程或独立线程以此方式执行 I/O 操作。此类独立进程或独立线程不应该实时运行。
中断优先级与进程优先级无关。一组进程操作引起的硬件中断服务无法继承为这些进程设置的优先级。因此,高优先级实时进程所控制的设备不必具有高优先级中断处理。
通过使用动态链接的共享库,分时进程可以节省大量内存。此类型的链接通过文件映射的形式实现。动态链接库例程将导致隐式读取。
调用程序时,实时程序可以将环境变量 LD_BIND_NOW 设置为非 NULL 值。通过设置此环境变量的值,可以在避免动态绑定的同时使用共享库。此过程还会强制在开始执行程序之前绑定所有动态链接。有关更多信息,请参见《链接程序和库指南》。
分时进程可以通过占用实时进程所需的资源来阻塞实时进程。当优先级较低的进程阻塞优先级较高的进程时,便会出现优先级倒置。术语阻塞是指进程必须等待一个或多个进程放弃对资源的控制的情况。如果此阻塞时间延长,则实时进程可能会错过其最终期限。
请考虑下图所示的情况,其中高优先级进程需要共享资源。优先级较低的进程占用资源并被中优先级的进程抢占,从而阻塞高优先级进程。其中可涉及任意数量的中间进程。必须执行完所有中间进程以及优先级较低的进程的关键部分。这一系列执行操作可能需要任意长时间。
此问题及其处理方法在《多线程编程指南》中的“互斥锁属性”中介绍。
页面的锁定计数达到 65535 (0xFFFF) 时,此页面便会永久锁定到内存中。值 0xFFFF 通过实现来进行定义并可能会在将来发行版中发生更改。无法解除锁定以此方式锁定的页面。
失控的实时进程可能会导致系统停止。此类失控 (runaway) 进程还会极大减慢系统响应速度,从而使系统看似停止。
如果 SPARC 系统中具有失控 (runaway) 进程,请按 Stop-A。您可能必须按 Stop-A 多次。如果按 Stop-A 不起作用,请关闭电源,稍等片刻,然后重新打开电源。如果非 SPARC 系统中具有失控 (runaway) 进程,请关闭电源,稍等片刻,然后重新打开电源。
如果高优先级实时进程没有放弃对 CPU 的控制,则必须中断死循环才能重新获取对系统的控制。此类失控 (runaway) 进程不会对 Ctrl-C 做出响应。尝试使用优先级设置为高于失控 (runaway) 进程优先级的 shell 也不起作用。
异步 I/O 操作不会始终按照这些操作在内核中的排队顺序执行。异步操作不必按照这些操作的执行顺序返回到调用方。
如果针对一系列快速 aioread(3AIO) 调用指定单个缓冲区,则此缓冲区的状态是不确定的。从首次调用到将最后结果通知给调用方这段时间内,缓冲区的状态始终是不确定的。
一个单独的 aio_result_t 结构只能用于一个异步操作。操作可以是读操作,也可以是写操作。
SunOS 未提供任何功能来确保按照物理连续的方式分配文件。
对于常规文件,会始终缓冲 read(2) 和 write(2) 操作。应用程序可以使用 mmap(2) 和 msync(3C) 在辅助存储器与进程内存之间实现直接 I/O 传输。
实时调度约束是管理数据采集或进程控制硬件所必需的。实时环境要求进程能够在有限时间内对外部事件做出反应。旨在为一组分时进程平均分配处理资源的内核的功能中可能不具备此类约束。
本节介绍 SunOS 实时调度器及其优先级队列,以及如何使用控制调度的系统调用和实用程序。
对于实时应用程序,调度行为中最重要的元素是提供实时调度类。标准的分时调度类并不适用于实时应用程序,因为此调度类会平等对待每个进程。标准的分时调度类中的优先级概念有限。实时应用程序需要完全采用进程优先级的调度类,另外还需要进程优先级仅由显式应用程序操作更改的调度类。
术语分发延迟是指系统响应进程开始操作的请求所用的时间。使用专门根据应用程序优先级而编写的调度器,可以开发具有有限分发延迟的实时应用程序。
下图说明了应用程序响应来自外部事件的请求所用的时间。
应用程序总响应时间包括中断响应时间、分发延迟以及应用程序的响应时间。
应用程序的中断响应时间包括系统的中断延迟以及设备驱动程序自身的中断处理时间。中断延迟由系统在禁用中断时必须运行的最长间隔确定。在 SunOS 中,使用通常不需要提高处理器中断级别的同步元语可以最小化此时间。
在中断处理过程中,驱动程序的中断例程会唤醒高优先级进程并在完成时返回。系统可检测优先级高于已中断进程的进程现在是否可进行分发,然后分发此进程。将上下文从优先级较低的进程切换到优先级较高的进程所用的时间包括在分发延迟时间中。
图 10–3 说明了系统的内部分发延迟和应用程序响应时间。响应时间根据系统响应内部事件所用的时间定义。内部事件的分发延迟表示进程唤醒优先级较高的进程所需的时间。分发延迟还包括系统分发优先级较高的进程所用的时间。
应用程序响应时间是指驱动程序执行以下操作所用的时间:唤醒优先级较高的进程,释放低优先级进程中的资源,重新调度优先级较高的任务,计算响应以及分发任务。
分发延迟的时间间隔内会发生中断并可对中断进行处理。此处理会增加应用程序响应时间,但是不会影响测量分发延迟。因此,此处理不受分发延迟保证的限制。
使用实时 SunOS 提供的新调度技术,可以将系统分发延迟时间限制在指定的范围内。如下表中所示,可以通过有限进程数来改善分发延迟。
表 10–1 实时系统分发延迟
工作站 |
有限进程数 |
任意进程数 |
---|---|---|
SPARCstation 2 |
在活动进程数小于 16 的系统中,小于0.5 毫秒 |
1.0 毫秒 |
SPARCstation 5 |
小于 0.3 毫秒 |
0.3 毫秒 |
SunOS 内核按优先级分发进程。调度器或分发程序支持调度类这一概念。可以将类定义为实时类 (RT)、系统类 (SYS) 和分时类 (TS)。每个类都有一个唯一的调度策略,用于分发属于此类的进程。
内核首先分发优先级最高的进程。缺省情况下,实时进程优先于 sys 和 TS 进程。管理员可以配置系统以使 TS 进程和 RT 进程的优先级重叠。
无法由软件控制的硬件中断具有最高优先级。处理中断的例程会从中断处直接立即进行分发,而不会考虑当前进程的优先级。
实时进程具有最高的缺省软件优先级。RT 类中的进程具有优先级以及时间量程值。RT 进程将严格按照这些参数进行调度。只要 RT 进程准备运行,SYS 或 TS 进程便无法运行。通过固定优先级调度,关键进程可以按照预先确定的顺序运行,直到完成为止。这些优先级永远不会更改,除非应用程序对其进行更改。
RT 类的进程可继承父进程的时间量程(有限或无限)。具有有限时间量程的进程将一直运行到时间量程到期为止。如果具有有限时间量程的进程在等待 I/O 事件时阻塞,或者被优先级较高的可运行实时进程抢占,则该进程也会停止运行。具有无限时间量程的进程仅当终止、阻塞或被抢占时才会停止执行。
SYS 类用于调度特殊系统进程(如换页、STREAMS 和交换程序)的执行。不能将进程的类更改为 SYS 类。SYS 类的进程具有内核在进程启动时所建立的固定优先级。
分时 (TS) 进程的优先级最低。TS 类的进程以动态方式进行调度,每个时间片为数百毫秒。TS 调度器可以循环方式切换上下文,通常足以为每个进程提供平等的运行机会,具体取决于以下各项:
时间片值
进程上次休眠时所记录的进程历史记录
有关 CPU 使用率的注意事项
缺省分时策略会为优先级较低的进程提供较大的时间片。
子进程可通过 fork(2) 继承父进程的调度类和属性。通过 exec(2) 无法更改进程的调度类和属性。
每个调度类通过不同的算法分发。内核将调用与类有关的例程以做出有关 CPU 进程调度的决定。内核与类无关,并会删除队列中优先级最高的进程。每个类都负责针对属于此类的进程计算其优先级值。该值将放入此进程的分发优先级变量中。
如下图所示,每个类算法都采用不同的方法来确定将放入全局运行队列中的最高优先级进程。
每个类都具有应用于此类中的进程的一组优先级别。特定于类的映射会将这些优先级映射到一组全局优先级。不要求一组全局调度优先级映射从零开始或具有连续性。
缺省情况下,分时 (TS) 进程的全局优先级值的范围为 -20 到 +20。这些全局优先级值映射到内核 0-40,临时指定值最高为 99。实时 (RT) 进程的缺省优先级范围为 0-59,并映射到内核 100 到 159。与类无关的内核代码将运行队列中具有最高全局优先级的进程。
分发队列是指具有相同全局优先级的进程的线性链接表。每个进程都具有调用进程时附加到进程的特定于类的信息。进程将按照基于进程全局优先级的顺序从内核分发表分发。
分发进程时,会将进程的上下文连同其内存管理信息、寄存器和栈一起映射到内存。上下文映射完成之后便开始执行进程。内存管理信息采用硬件寄存器的形式,这些寄存器包含针对当前运行的进程执行虚拟内存转换所需的数据。
当可以分发优先级较高的进程时,内核便会中断其计算并强制执行上下文切换,从而抢占当前运行的进程。如果内核发现现在可以分发优先级较高的进程,则可能会随时抢占进程。
例如,假定进程 A 从外围设备执行读操作。内核会将进程 A 置于休眠状态。然后,内核发现优先级较低的进程 B 可运行。此时会分发进程 B 并开始执行此进程。最后,外围设备将中断,进程进入此设备的驱动程序。设备驱动程序使进程 A 可运行并返回。现在,内核通过处理、恢复执行已唤醒的进程 A 来抢占 B,而不是返回到已中断的进程 B。
多个进程争用内核资源时,会出现另一种值得关注的情况。高优先级的实时进程可能会等待低优先级进程所占用的资源。低优先级进程释放资源时,内核便会抢占此进程以恢复执行优先级较高的进程。
一个或多个优先级较低的进程长时间阻塞优先级较高的进程时,便会出现优先级倒置。在 SunOS 内核中使用同步元语(如互斥锁)可能会导致优先级倒置。
某进程必须等待一个或多个进程放弃资源时,此进程便已阻塞。长时间阻塞可能会导致错过最终期限,即使对于低级别使用率也是如此。
通过实现基本优先级继承策略,已经解决了 SunOS 内核互斥锁引起的优先级倒置问题。此策略指明优先级较低的进程阻塞优先级较高的进程的执行时,优先级较低的进程便会继承优先级较高的进程的优先级。此继承将设置进程可保持阻塞状态的时间上限。此策略是内核行为的一个属性,而不是程序员通过系统调用或接口执行制定的解决方案。但是,用户级别进程仍会呈现优先级倒置。
用户优先级倒置问题及其处理方法在《多线程编程指南》中的“互斥锁属性”中介绍。
以下接口调用可控制进程调度。
对活动类调度的控制是通过 priocntl(2) 实现的。通过 fork(2) 和 exec(2) 可继承类属性连同调度参数以及优先级控制所需的权限。RT 和 TS 类都采用这种继承。
priocntl(2) 是用于指定实时进程、一组进程或应用系统调用的类的接口。priocntlset(2) 还提供了更为通用的接口,用于指定应用系统调用的一整组进程。
priocntl(2) 的命令参数可以是以下各项之一:PC_GETCID、PC_GETCLINFO、PC_GETPARMS 或 PC_SETPARMS。调用过程的实际或有效 ID 必须与受影响进程的实际或有效 ID 相匹配,或者必须具有超级用户权限。
此命令采用包含可识别类名称的结构的名称字段。将返回类 ID 以及类属性数据数组。
此命令采用包含可识别类标识符的结构的 ID 字段。将返回类名称以及类属性数据数组。
此命令将返回其中一个指定进程的调度类标识符或特定于类的调度参数。即使 idtype 和 id 可能会指定许多 ID 类型和 ID,但是 PC_GETPARMS 仅返回一个进程的参数。类可选择进程。
此命令用于设置一个或多个指定进程的调度类或特定于类的调度参数。
返回指定策略的最大值。
返回指定策略的最小值。有关更多信息,请参见 sched_get_priority_max(3R) 手册页。
将指定的 timespec 结构更新到当前执行时间限制。有关更多信息,请参见 sched_get_priority_max(3RT) 手册页。
设置或获取指定进程的调度参数。
阻塞调用进程直到调用进程返回到进程列表的顶部为止。
控制进程调度的管理实用程序包括 dispadmin(1M) 和 priocntl(1)。这两个实用程序都支持包含兼容选项和可装入模块的 priocntl(2) 系统调用。这些实用程序可提供系统管理功能,用于在运行时控制实时进程调度。
priocntl(1) 命令可设置和检索进程的调度器参数。
dispadmin(1M) 实用程序通过在运行时包括 -l 命令行选项可显示所有的当前进程调度类。使用 RT 作为实时类的参数,还可以针对在 -c 选项之后指定的类更改进程调度。
以下列出了 dispadmin(1M) 的类选项:
列出当前已配置的调度器类
指定要显示或更改参数的类
获取指定类的分发参数
与 -g 一起使用,用于指定时间量程精度
指定可以在其中查找值的文件
包含分发参数的特定于类的文件也可以在运行时装入。使用此文件可以建立一组新的优先级,用于替换在引导时已确定的缺省值。此特定于类的文件必须按照 -g 选项所用的格式声明参数。RT 类的参数可在 rt_dptbl(4) 中找到,并会在示例 10–1 中列出。
要向系统中添加 RT 类文件,必须具有以下模块:
装入 rt_dptbl(4) 的类模块中的 rt_init() 例程。
rt_dptbl(4) 模块,用于提供分发参数以及将指针返回到 config_rt_dptbl 的例程。
dispadmin(1M) 可执行文件。
可以使用以下步骤安装 RT 类分发表:
使用以下命令装入特定于类的模块,其中 module_name 是特定于类的模块。
# modload /kernel/sched/module_name |
调用 dispadmin 命令。
# dispadmin -c RT -s file_name |
此文件介绍的表必须与所覆写的表具有相同的项数。
参数表 rt_dptbl(4) 和 ts_dptbl(4) 与两个调度类关联。这些表在引导时使用可装入模块配置,或者在运行时使用 dispadmin(1M) 配置。
实时核心中表用于建立 RT 调度的属性。rt_dptbl(4) 结构包含参数数组 struct rt_dpent_t。n 个优先级别中的每个级别都具有一个参数。给定优先级别的属性由此数组中的第 i 个参数结构 rt_dptbl[i] 指定。
参数结构包含以下成员,这些成员也在 /usr/include/sys/rt.h 头文件中介绍。
与此优先级别关联的全局调度优先级。rt_globpri 的值无法通过 dispadmin(1M) 进行更改。
在周期中分配给此级别进程的时间量程的长度。有关更多信息,请参见时间标记接口。时间量程值只是特定级别进程的缺省值或起始值。通过使用 priocntl(1) 命令或 priocntl(2) 系统调用可更改实时进程的时间量程。
实时管理员可以通过重新配置 config_rt_dptbl 随时更改调度器中实时部分的行为。rt_dptbl(4) 手册页的标题为“替换 rt_dptbl 可装入模块”的一节中介绍了一种方法。
在运行的系统中检查或修改实时参数表的另一种方法是通过 dispadmin(1M) 命令。通过针对实时类调用 dispadmin(1M),可以从内核的核心中表的当前 config_rt_dptbl 配置中检索 rt_quantum 值。覆写当前核心中表时,用于向 dispadmin(1M) 中输入的配置文件必须遵照 rt_dptbl(4) 手册页中所述的特定格式。
以下是具有优先级的进程示例 rtdpent_t,当此类进程可能在 config_rt_dptbl[] 中出现时,其关联的时间量程具有 config_rt_dptbl[] 值。
rtdpent_t rt_dptbl[] = { 129, 60, /* prilevel Time quantum */ 130, 40, 100, 100, 131, 40, 101, 100, 132, 40, 102, 100, 133, 40, 103, 100, 134, 40, 104, 100, 135, 40, 105, 100, 136, 40, 106, 100, 137, 40, 107, 100, 138, 40 108, 100, 139, 40, 109, 100, 140, 20, 110, 80, 141, 20, 111, 80, 142, 20, 112, 80, 143, 20, 113, 80, 144, 20, 114, 80, 145, 20, 115, 80, 146, 20, 116, 80, 147, 20, 117, 80, 148, 20, 118, 80, 149, 20, 119, 80, 150, 10, 120, 60, 151, 10, 121, 60, 152, 10, 122, 60, 153, 10, 123, 60, 154, 10, 124, 60, 155, 10, 125, 60, 156, 10, 126, 60, 157, 10, 126, 60, 158, 10, 127, 60, 159, 10, 128, 60, }
锁定内存是实时应用程序存在的最重要的问题之一。在实时环境中,进程必须能够保证可连续驻留在内存中,以缩短延迟并防止换页和交换。
本节介绍可用于 SunOS 中实时应用程序的内存锁定机制。
在 SunOS 中,进程是否驻留在内存中由其当前状态、可用物理内存总量、活动进程数以及进程的内存需求确定。此驻留适用于分时环境,但实时进程通常不可接受此驻留。在实时环境中,进程必须保证可在内存中驻留,以缩短进程的内存访问和分发延迟。
SunOS 中的实时内存锁定是由一组库例程提供的。通过这些例程,使用超级用户权限运行的进程可以将其虚拟地址空间的指定部分锁定到物理内存中。采用此方式锁定的页面可免于进行换页,直到解除锁定这些页面或进程退出为止。
操作系统会限制在系统范围内可以随时锁定的页面数。此限制是一个可调参数,其缺省值在引导时计算得出。缺省值是基于页帧数减去另一个百分比(当前设置为 10%)得出的。
调用 mlock(3C) 会要求将一个内存段锁定到系统的物理内存中。构成指定段的页面为错误页面。每个页面的锁定计数将递增。锁定计数值大于零的任何页面均可免于进行换页活动。
特定页面可以由多个进程通过不同映射多次锁定。如果两个不同的进程锁定同一页面,则此页面会保持锁定状态,直到这两个进程都解除其锁定为止。但是,在给定的映射中,页面锁定不会进行嵌套。同一进程对地址相同的锁定接口进行的多次调用可以通过单个解除锁定请求删除。
如果删除了用于执行锁定的映射,则会隐式解除锁定内存段。通过关闭或截断文件来删除页面时,也会隐式解除锁定此页面。
调用 fork(2) 之后,子进程无法继承锁定。如果具有某些锁定内存的进程派生了子进程,则此子进程必须代表自己执行内存锁定操作才能锁定自己的页面。否则,此子进程会导致写复制页面错误,这些错误通常是与派生进程关联的不利结果。
要解除锁定内存页,进程可通过调用 munlock(3C) 请求释放锁定虚拟页面段。munlock 用于递减指定物理页面的锁定计数。将页面的锁定计数递减到 0 之后,通常会进行页面交换。
超级用户进程可以通过调用 mlockall(3C) 请求锁定其地址空间内的所有映射。如果设置了标志 MCL_CURRENT,则会锁定所有的现有内存映射。如果设置了标志 MCL_FUTURE,则会将每个添加到现有映射或替换现有映射的映射锁定到内存中。
页面的锁定计数达到 65535 (0xFFFF) 时,此页面便会永久锁定到内存中。值 0xFFFF 通过实现定义。该值在将来的发行版中可能会更改。无法解除锁定通过此方式锁定的页面。请重新引导系统以进行恢复。
本节介绍实时进程的 I/O。在 SunOS 中,库提供了两组接口和调用来执行快速异步 I/O 操作。POSIX 异步 I/O 接口是最新标准。SunOS 环境还提供了文件和内存中同步操作和模式,以防止信息丢失和数据不一致。
标准 UNIX I/O 与应用程序程序员保持同步。调用 read(2) 或 write(2) 的应用程序通常会等待系统调用完成为止。
实时应用程序需要有限的异步 I/O 行为。发出异步 I/O 调用的进程可继续执行而无需等待 I/O 操作完成。I/O 操作完成后会通知调用方。
异步 I/O 可用于任何 SunOS 文件。文件会同步打开并且不需要进行特殊标记。异步 I/O 传输包含三个元素:调用、请求和操作。应用程序调用异步 I/O 接口,将对 I/O 的请求放入队列中,然后立即返回调用。有时,系统会取消请求排队并启动 I/O 操作。
可以在任何文件描述符中混合异步和标准的 I/O 请求。系统不会维护任何特殊的读写请求序列,并且会对所有暂挂读写请求进行任意重新排序。如果应用程序需要特定序列,则应用程序必须确保在完成先前的操作之后再发出相关请求。
POSIX 异步 I/O 使用 aiocb 结构来执行。aiocb 控制块可标识每个异步 I/O 请求并包含所有控制信息。一个控制块每次只能用于一个请求。控制块可以在完成其请求之后重用。
典型的 POSIX 异步 I/O 操作是通过调用 aio_read(3RT) 或 aio_write(3RT) 来启动的。可以使用轮询或信号来确定操作是否完成。如果使用信号确定操作是否完成,则可以唯一地标记每个操作。然后,在所生成信号的 si_value 组件中返回标记。请参见 siginfo(3HEAD) 手册页。
aio_read(3RT) 是通过异步 I/O 控制块调用的,用于启动读操作。
aio_write(3RT) 是通过异步 I/O 控制块调用的,用于启动写操作。
确定操作完成之后,分别调用 aio_return(3RT) 和 aio_error(3RT) 可获取返回值和错误值。
aio_cancel(3RT) 是通过异步 I/O 控制块调用的,用于取消暂挂操作。如果请求由控制块指定,则可以使用 aio_cancel 来取消特定请求。aio_cancel 还可以取消所有针对指定文件描述符暂挂的请求。
aio_fsync(3RT) 用于对指定文件的所有暂挂 I/O 操作的异步 fsync(3C) 或 fdatasync(3RT) 请求进行排队。
aio_suspend(3RT) 用于暂停调用方,如同已将一个或多个先前的异步 I/O 请求同步。
本节介绍 Solaris 操作环境中的异步 I/O 操作。
如果异步 I/O 调用成功返回,则表明 I/O 操作只是已排队并等待执行。实际操作包含一个返回值和一个潜在错误标识符。如果同步进行调用,则该返回值和潜在错误标识符会返回到调用方。完成 I/O 之后,会将返回值和错误值存储于用户在请求作为指向 aio_result_t 的指针时提供的位置。aio_result_t 的结构在 <sys/asynch.h> 中定义:
typedef struct aio_result_t { ssize_t aio_return; /* return value of read or write */ int aio_errno; /* errno generated by the IO */ } aio_result_t;
更新 aio_result_t 之后,便会将 SIGIO 信号传送到发出 I/O 请求的进程。
请注意,包含两个或多个暂挂的异步 I/O 操作的进程没有特定方法可确定 SIGIO 信号的成因。接收 SIGIO 的进程应该检查其所有可能会生成 SIGIO 信号的条件。
aioread(3AIO) 例程是 read(2) 的异步版本。除了常规读取参数之外,aioread(3AIO) 还会采用可指定文件位置和 aio_result_t 结构地址的参数。有关操作的结果信息存储在 aio_result_t 结构中。文件位置指定操作之前要在文件内执行查找操作。无论 aioread(3AIO) 调用成功还是失败,都会更新文件指针。
aiowrite(3AIO) 例程是 write(2) 的异步版本。除了常规写入参数之外,aiowrite(3AIO) 还会采用可指定文件位置和 aio_result_t 结构地址的参数。有关操作的结果信息存储在 aio_result_t 结构中。
文件位置指定操作之前要在文件内执行查找操作。如果 aiowrite(3AIO) 调用成功,则会将文件指针更新为指向可实现成功查找和写入的位置。如果写操作不再考虑后续的写入请求,则还会更新文件指针。
aiocancel(3AIO) 例程可用于尝试取消 aio_result_t 结构指定作为参数的异步请求。仅当请求仍进行排队时,aiocancel(3AIO) 调用才会成功。如果操作正在进行中,则 aiocancel(3AIO) 会失败。
调用 aiowait(3AIO) 会阻塞调用过程,直到至少有一个未完成的异步 I/O 操作完成为止。超时参数是指等待 I/O 完成的最大时间间隔。超时值为 0 指明不需要等待。aiowait(3AIO) 会针对已完成的操作返回指向 aio_result_t 结构的指针。
要以同步方式(而不是依赖于 SIGIO 中断)确定异步 I/O 事件是否完成,请使用 poll(2)。另外,还可以进行轮询来确定 SIGIO 中断的原因。
poll(2) 用于大量文件时会比较缓慢。此问题可通过 poll(7d) 进行解决。
使用 /dev/poll 可提供具有高度可伸缩性的轮询大量文件描述符的方法。这种可伸缩性是通过一组新的 API 和一个新驱动程序 /dev/poll 实现的。/dev/poll API 是 poll(2) 的备用项,而不是替换项。可以使用 poll(7d) 提供 /dev/poll API 的详细信息和示例。 如果正确使用,/dev/poll API 的扩展性将优于 poll(2)。此 API 尤其适用于满足以下条件的应用程序:
应用程序重复轮询大量文件描述符
已轮询文件描述符相对稳定,意味着这些描述符不会经常关闭然后再重新打开
相比所轮询的文件描述符的总数,实际已暂挂轮询事件的文件描述符数很少
文件通过调用 close(2) 来关闭。调用 close(2) 会取消任何可以关闭的未完成异步 I/O 请求。close(2) 将等待无法取消的操作。有关更多信息,请参见使用 aiocancel。close(2) 返回时,没有任何异步 I/O 针对文件描述符暂挂。文件关闭时,只取消排队到指定文件描述符的异步 I/O 请求,而不会取消针对其他文件描述符的任何 I/O 暂挂请求。
应用程序可能需要保证已将信息写入稳定存储器,或者保证按照特定顺序执行文件更新。为此,需要提供同步的 I/O。
在 SunOS 中,如果系统确保以后每次打开文件之后均可以读取所有写入的数据,写操作即可成功。此检查假设物理存储介质不存在任何故障。如果物理存储介质上的数据映像可用于请求进程,则可成功传输读操作的数据。如果关联数据已成功传输或者 I/O 操作已诊断为不成功,则此操作便已完成。
当出现以下情况时,I/O 操作会实现同步的 I/O 数据完整性:
对于读取,此操作已完成或在不成功时已进行诊断。仅当数据映像已成功传输到请求进程时,读取才会完成。如果在暂挂写入请求影响到数据读取时请求同步的读操作,则这些写入请求会在读取数据之前成功完成。
对于写入,此操作已完成或在不成功时已进行诊断。成功传输在写入请求中指定的数据时,写操作便会成功。此外,还必须成功传输检索数据所需的所有文件系统信息。
检索数据时不必要的文件属性在返回到调用过程之前不会进行传输。
实现同步的 I/O 文件完整性要求所有与 I/O 操作相关的文件属性都必须在返回到调用过程之前成功传输。否则,实现同步的 I/O 文件完整性会等同于实现同步的 I/O 数据完整性。
fsync(3C) 和 fdatasync(3RT) 将文件显式同步到辅助存储器中。
fsync(3C) 例程可保证在 I/O 文件完整性实现的级别同步接口。fdatasync(3RT) 可保证在 I/O 数据完整性实现的级别同步接口。
应用程序可以在每个 I/O 操作完成之前同步各操作。使用 open(2) 或 fcntl(2) 针对文件描述设置 O_DSYNC 标志,可以确保所有 I/O 写入在操作完成之前便会完成 I/O 数据。针对文件描述设置 O_SYNC 标志,可以确保所有 I/O 写入在指示操作为完成之前便已完成。针对文件描述设置 O_RSYNC 标志,可以确保所有 I/O 读取(read(2) 和 aio_read(3RT))都达到描述符设置所请求的相同完成级别。描述符设置可以为 O_DSYNC 或 O_SYNC。
本节介绍 SunOS 中作为与实时处理相关的接口的进程间通信 (interprocess communication, IPC) 接口。此外,还将介绍信号量、管道、FIFO、消息队列、共享内存、文件映射以及信号。有关有助于进程间通信的库、接口和例程的更多信息,请参见第 6 章,进程间通信。
发送者可以使用 sigqueue(3RT) 将信号连同少量信息一起发送到目标进程。
要对随后出现的暂挂信号进行排队,目标进程必须针对指定信号设置 SA_SIGINFO 位。请参见 sigaction(2) 手册页。
目标进程通常以异步方式接收信号。要同步接收信号,请阻塞信号并调用 sigwaitinfo(3RT) 或 sigtimedwait(3RT)。请参见 sigprocmask(2) 手册页。此过程会导致同步接收信号。sigqueue(3RT) 的调用方发送的值将存储在 siginfo_t 参数的 si_value 部分中。如果不阻塞信号,则会导致将信号传送到 sigaction(2) 指定的信号处理程序,并且将值显示在此处理程序的 siginfo_t 参数的 si_value 中。
指定数量的信号和关联值可以通过进程发送并保持未传送状态。首次调用 sigqueue(3RT) 时,会为 {SIGQUEUE_MAX} 信号分配存储空间。此后,调用 sigqueue(3RT) 可在目标进程中成功加入队列或是在有限时间内失败。
管道、命名管道和消息队列的行为方式与字符 I/O 设备类似。这些接口使用不同的连接方法。有关管道的更多信息,请参见进程之间的管道。有关命名管道的更多信息,请参见命名管道。有关消息队列的更多信息,请参见System V 消息和POSIX 消息。
信号量也同时以 System V 和 POSIX 风格提供。有关更多信息,请参见System V 信号量和POSIX 信号量。
请注意,使用信号量可能会导致优先级倒置,除非通过本章前面介绍的技术显式避免优先级倒置。
对于进程,最快的通信方法就是直接通过共享内存段进行通信。如果两个以上进程尝试同时对共享内存进行读写,则内存内容可能会变得不准确。这种潜在的不准确性是使用共享内存时存在的主要问题。
本节介绍针对实时应用程序使用套接字或传输级别接口 (Transport-Level Interface, TLI) 进行的异步网络通信。使用套接字的异步联网通过将类型为 SOCK_STREAM 的开放式套接字设置为异步以及非阻塞来实现。有关异步套接字的更多信息,请参见高级套接字主题。结合使用 STREAMS 异步功能以及 TLI 库例程的非阻塞模式,可支持对 TLI 事件进行异步网络处理。
有关传输级别接口的更多信息,请参见第 8 章,使用 XTI 和 TLI 编程。
套接字和传输级别接口均可提供两种服务模式:连接模式和无连接模式。
连接模式服务是面向线路的。通过此服务,可以采用可靠、有序的方式借助建立的连接传输数据。此服务还提供了标识过程,用于避免数据传输阶段地址解析和传输的开销。此服务适用于需要存在时间较长的面向数据流交互的应用程序。
无连接模式服务面向消息,并支持自包含单元(多个单元之间不需要任何逻辑关系)中的数据传输。单个服务请求可传递将数据单元从发送者传送到传输提供者所需的所有信息。此服务请求包括目标地址以及要传送的数据。无连接模式服务适用于涉及不需要保证顺序传送数据的短期交互的应用程序。无连接传输通常并不可靠。
本节介绍可用于 SunOS 中的实时应用程序的计时功能。使用这些机制的实时应用程序需要本节中所列出例程的手册页中的详细信息。
SunOS 的计时接口分为两种不同类型:时间标记和间隔计时器。时间标记接口提供已用时间的测量结果。另外,通过时间标记接口,应用程序可以测量状态的持续时间或者事件之间的时间。通过间隔计时器,可以在指定时间唤醒应用程序,并可基于所用时间计划活动。
两个接口均可提供时间标记。gettimeofday(3C) 以 timeval 结构提供当前时间,表示自格林威治标准时间 1970 年 1 月 1 日午夜以来的时间(以秒和微秒为单位)。clock_gettime(clockid 为 CLOCK_REALTIME)以 timespec 结构提供当前时间,表示 gettimeofday(3C) 所返回的相同时间间隔(以秒和纳秒为单位)。
SunOS 使用硬件定期计时器。对于某些工作站,硬件定期计时器是计时信息的唯一来源。如果硬件定期计时器是计时信息的唯一来源,则时间标记的精确度将限制为计时器的分辨率。对于其他平台,分辨率为 1 微秒的计时器寄存器表示时间标记精确到 1 微秒。
通常,实时应用程序使用间隔计时器来计划操作。间隔计时器可以分为以下两种类型:一次性类型或定期类型。
一次性计时器是已经过设置的计时器,设置为相对于当前时间或绝对时间的到期时间。计时器会到期一次然后解除设置。数据传输到存储器中或者使操作超时之后,即可使用此类型的计时器来清除缓冲区。
定期计时器设置有初始到期时间(绝对或相对)以及重复间隔。每次间隔计时器到期时,都会按照重复间隔重新装入此计时器。然后,重新设置此计时器。此计时器可帮助进行数据日志记录或伺服控制。调用间隔计时器接口时,会将小于计时器分辨率的时间值向上舍入为下一个硬件计时器间隔的倍数。此间隔通常为 10 ms。
SunOS 具有两组计时器接口。setitimer(2) 和 getitimer(2) 接口运行固定设置的计时器,此类计时器称为 BSD 计时器,使用 timeval 结构指定时间间隔。使用 timer_create(3RT) 创建的 POSIX 计时器用于运行 POSIX 时钟 CLOCK_REALTIME。POSIX 计时器操作以 timespec 结构表示。
getitimer(2) 和 setitimer(2) 函数分别用于检索和确定指定的 BSD 间隔计时器的值。可用于进程的三个 BSD 间隔计时器包括指定了 ITIMER_REAL 的实时计时器。如果 BSD 计时器已进行设置并允许到期,则系统会向设置此计时器的进程发送适当的信号。
timer_create(3RT) 例程最多可以创建 TIMER_MAX 个 POSIX 计时器。调用方可以指定计时器到期时应向进程发送的信号和关联值。timer_settime(3RT) 和 timer_gettime(3RT) 例程分别用于检索和确定指定的 POSIX 间隔计时器的值。所需的信号暂挂传送时,POSIX 计时器便会到期。此时会对计时器到期的次数进行计数,并且 timer_getoverrun(3RT) 将检索此计数。timer_delete(3RT) 用于解除分配 POSIX 计时器。
以下示例说明了如何使用 setitimer(2) 生成定期中断,以及如何控制计时器中断的到达。
#include <unistd.h> #include <signal.h> #include <sys/time.h> #define TIMERCNT 8 void timerhandler(); int timercnt; struct timeval alarmtimes[TIMERCNT]; main() { struct itimerval times; sigset_t sigset; int i, ret; struct sigaction act; siginfo_t si; /* block SIGALRM */ sigemptyset (&sigset); sigaddset (&sigset, SIGALRM); sigprocmask (SIG_BLOCK, &sigset, NULL); /* set up handler for SIGALRM */ act.sa_action = timerhandler; sigemptyset (&act.sa_mask); act.sa_flags = SA_SIGINFO; sigaction (SIGALRM, &act, NULL); /* * set up interval timer, starting in three seconds, * then every 1/3 second */ times.it_value.tv_sec = 3; times.it_value.tv_usec = 0; times.it_interval.tv_sec = 0; times.it_interval.tv_usec = 333333; ret = setitimer (ITIMER_REAL, ×, NULL); printf ("main:setitimer ret = %d\n", ret); /* now wait for the alarms */ sigemptyset (&sigset); timerhandler (0, si, NULL); while (timercnt < TIMERCNT) { ret = sigsuspend (&sigset); } printtimes(); } void timerhandler (sig, siginfo, context) int sig; siginfo_t *siginfo; void *context; { printf ("timerhandler:start\n"); gettimeofday (&alarmtimes[timercnt], NULL); timercnt++; printf ("timerhandler:timercnt = %d\n", timercnt); } printtimes () { int i; for (i = 0; i < TIMERCNT; i++) { printf("%ld.%0l6d\n", alarmtimes[i].tv_sec, alarmtimes[i].tv_usec); } }