QNX中微子RTOS提供了posix标准的线程级同步原语,其中一些在不同进程的线程之间是有用的。
同步服务至少包括以下内容:
互斥锁或互斥锁是最简单的同步服务。互斥用于确保对线程间共享的数据的独占访问。
互斥对象通常被获取(pthread_mutex_lock()或pthread_mutex_timedlock()),并在访问共享数据(通常是关键部分)的代码周围释放(pthread_mutex_unlock())。
在任何时候,只有一个线程可能锁定互斥锁。试图锁定已经锁定的互斥锁的线程将阻塞,直到拥有互斥锁的线程解锁为止。当线程解锁互斥锁时,等待锁定互斥锁的最高优先级线程将解除阻塞,成为互斥锁的新所有者。这样,线程将按优先级顺序通过一个关键区域。
在大多数处理器上,获取互斥锁并不需要进入内核以获得一个免费的互斥锁。允许这样做的是在x86处理器上使用比较和交换操作码,在大多数RISC处理器上使用load/store条件操作码。
只有当互斥锁已经被持有,线程才能进入阻塞列表时,才能进入内核;如果其他线程正在等待清除该互斥锁,则在退出时完成内核条目。这使得获取和释放一个没有争议的关键部分或资源变得非常快,只会引起操作系统的工作,从而解决争用问题。
可以使用非阻塞锁函数(pthread_mutex_trylock())来测试互斥锁当前是否被锁定。为了获得最佳性能,关键部分的执行时间应该很短,并且持续时间是有限的。如果线程可能在临界段内阻塞,则应使用条件变量。
默认情况下,如果优先级高于互斥锁所有者的线程试图锁定互斥锁,那么当前所有者的有效优先级将增加到等待互斥锁的高优先级阻塞线程的优先级。当前所有者的有效优先级在解锁互斥锁时再次被调整;它的新优先级是它自己的优先级的最大值,它仍然直接或间接阻止那些线程的优先级。
该方案不仅保证了高优先级线程在等待互斥锁的最短时间内被阻塞,而且解决了经典的优先级反转问题。
pthread_mutexattr_init()函数将协议设置为pthread_prio_inheritance,以允许这种行为;可以调用pthread_mutexattr_setprotocol()来覆盖此设置。pthread_mutex_trylock()函数不会改变线程的优先级,因为它不会阻塞线程。
还可以修改互斥对象的属性(使用pthread_mutexattr_settype()),以允许同一个线程递归锁定互斥对象。这对于允许线程调用一个可能试图锁定某个互斥锁的例程是很有用的,而这个互斥锁恰好已经锁定了。
条件变量(condvar)用于阻塞临界段内的线程,直到满足某个条件为止。这个条件可以是任意复杂的,并且是独立于条件变量的。然而,为了实现监视器,条件变量必须始终与互斥锁一起使用。
条件变量支持三种操作:
•等(pthread_cond_wait())
•信号(pthread_cond_signal())
•广播(pthread_cond_broadcast())
下面是如何使用condvar的一个典型例子:
在这个代码示例中,在测试条件之前获取互斥量。这确保只有该线程能够访问正在检查的任意条件。当条件为真时,代码示例将阻塞等待调用,直到其他线程在条件变量上执行信号或广播。
需要while循环有两个原因。首先,POSIX不能保证不会发生错误唤醒(例如,多处理器系统)。其次,当另一个线程对条件进行了修改时,我们需要重新测试以确保修改符合我们的标准。当等待的线程被阻塞以允许另一个线程进入临界区时,pthread_cond_wait()自动解锁关联的互斥锁。
执行信号的线程将解除条件变量上排队的最高优先级线程的阻塞,而广播将解除条件变量上排队的所有线程的阻塞。关联的互斥锁被最高优先级的未阻塞线程原子性地锁定;然后,线程必须在通过临界区之后解锁互斥锁。条件变量等待操作的一个版本允许指定超时(pthread_cond_timedwait())。当超时过期时,等待线程可以被解除阻塞。
阻塞是一种同步机制,它允许您“捕获”几个合作线程(例如,在一个矩阵计算中),强制它们在某个特定的点上等待,直到所有线程都完成了才能继续执行。
与pthread_join()函数不同,在pthread_join()函数中,需要等待线程终止,而在阻塞的情况下,需要等待线程在某个点会合。当指定数量的线程到达阻塞时,将解除所有线程的阻塞,以便它们能够继续运行。
首先使用pthread_barrier_init()创建一个阻塞:
这将在传递的地址创建阻塞对象(阻塞对象的指针位于阻塞中),其属性由attr指定。count成员持有必须调用thread_barrier_wait()的线程数。一旦创建了阻塞,每个线程都会调用pthread_barrier_wait()来表明它已经完成了:
当一个线程调用pthread_barrier_wait()时,它会阻塞,直到最初在pthread_barrier_init()函数中指定的线程数量调用pthread_barrier_wait()(并阻塞)为止。当正确数量的线程调用pthread_barrier_wait()时,所有这些线程将同时解除阻塞。
这里有一个例子:
主线程创建barrier对象,并初始化它,在线程继续执行之前,线程总数必须与barrier同步。在上面的示例中,使用了一个计数3:一个是主()线程,一个是thread1(),一个是thread2()。然后启动thread1()和thread2()。为了简化这个例子,我们让线程休眠以导致延迟,就好像计算发生了一样。为了同步,主线程简单地阻塞了barrier,因为它知道barrier只会在两个工作线程同时加入之后才会解除阻塞。
在这个版本中,包括以下barrier功能:
Sleepon locks和condvars非常类似,除了几个微小的不同。
与condvars一样,sleepon锁(pthread_sleepon_lock())可用于阻塞,直到条件变为true(比如内存位置更改值)。但是与condvars不同的是,sleepon锁在一个互斥锁上复用它们的功能,并动态地分配condvar,而不管检查的条件有多少。condvars的最大数量最终等于阻塞线程的最大数量。这些锁是在UNIX内核中通常使用的休眠锁之后形成模式的。
更正式的说法是“多个读取器、单个写入器锁”,当数据结构的访问模式由多个读取数据的线程和(最多)一个写入数据的线程组成时,就会使用这些锁。这些锁比互斥锁更昂贵,但是对于这种数据访问模式很有用。
这个锁通过允许所有请求读访问锁(pthread_rwlock_rdlock())的线程在请求中成功工作。但是,当希望写入的线程请求锁(pthread_rwlock_wrlock())时,请求被拒绝,直到所有当前的读线程释放它们的读锁pthread_rwlock_unlock()。
多个写线程可以排队(按照优先级顺序)等待它们有机会写受保护的数据结构,所有被阻塞的写线程将在允许再次访问电子邮件线程之前运行。没有考虑读线程的优先级。
还有一些调用(pthread_rwlock_tryrdlock()和pthread_rwlock_trywrlock()),以允许线程在不阻塞的情况下测试实现请求锁的尝试。这些调用返回一个成功的锁或一个状态,指示不能立即授予锁。reader /writer锁不是直接在内核中实现的,而是由内核提供的互斥和condvar服务构建的.
信号量是另一种常见的同步形式,它允许线程在信号量上“post”和“等待”,以控制线程何时唤醒或休眠。post(sem_post())操作增加了信号量;wait(sem_wait())操作会递减。
如果你等待一个正信号,你不会阻塞。等待非正信号量将阻塞,直到其他线程执行post。在等待之前发布一次或多次是有效的。这种使用将允许一个或多个线程在不阻塞的情况下执行等待。信号量与其他同步原语的一个显著区别是,信号量是“异步安全的”,可以由信号处理程序操作。如果期望的效果是让一个信号处理程序唤醒一个线程,信号量是正确的选择。
信号量的另一个有用特性是它们被定义为在进程之间操作。虽然我们的互斥对象在进程之间工作,但POSIX线程标准认为这是一个可选的功能,因此可能无法在系统之间移植。对于单个进程中线程之间的同步,互斥量将比信号量更有效。
作为一种有用的变体,还可以使用命名信号量服务。它允许您在由网络连接的不同机器上的进程之间使用信号量。
因为信号量,像条件变量一样,可以合法地返回一个非零值,因为错误的唤醒,正确的使用需要一个循环:
通过选择POSIX FIFO调度策略,我们可以确保在非smp系统上没有两个具有相同优先级的线程同时执行关键部分。FIFO调度策略规定,系统中具有相同优先级的所有FIFO计划线程在调度时都将运行,直到它们自愿将处理器释放到另一个线程。
当线程作为请求另一个进程的服务的一部分而阻塞时,或者当出现信号时,也会发生这种“释放”。因此,必须仔细编码和记录关键区域,以便以后的代码维护不会违反此条件。
此外,该进程(或任何其他进程)中的高优先级线程仍然可以抢占这些fifo计划的线程。因此,在临界区中可能“冲突”的所有线程都必须以相同的优先级调度fifo。执行了这个条件之后,线程就可以随意地访问这个共享内存,而不必首先进行显式同步调用。
我们的发送/接收/回复消息传递IPC服务(稍后描述)通过其阻塞特性实现了隐式同步。在许多情况下,这些IPC服务会使其他同步服务变得不必要。它们也是唯一可以跨网络使用的同步和IPC原语(不包括基于消息传递的命名信号量)
在某些情况下,可能希望执行一个简短的操作(例如递增一个变量),并保证操作将自动执行—即。,该操作不会被其他线程或ISR(中断服务例程)抢占。QNX中微子RTOS为:
•添加一个值
•减去一个值
•清理BIT位
•设置位
•切换(互补)位
这些原子操作都可以使用,包括C头文件< atomic.h >。尽管您可以在任何地方使用这些原子操作,但是您会发现它们在这两种情况下特别有用:
•在ISR和线程之间
•在两个线程(SMP或单处理器)之间
因为ISR可以在任何给定的点抢占一个线程,所以线程能够保护自己的唯一方法就是禁用中断。由于您应该避免在实时系统中禁用中断,所以我们建议您使用QNX中微子提供的原子操作。
在SMP系统上,多个线程可以并且确实可以并发地运行。同样,我们遇到了与上面的中断相同的情况——您应该在适用的情况下使用原子操作来消除禁用和重新启用中断的需要。
下表列出了各种微内核调用和从它们构造的高级POSIX调用:
时钟服务用于维护一天的时间,而内核计时器调用则使用时钟服务来实现间隔计时器。
注:QNX中微子系统的有效日期从1970年1月到2038年。time_t数据类型是一个无符号32位数字,它将这个范围扩展到2106个应用程序。内核本身使用无符号64位数字来计算自1970年1月以来的纳秒,因此可以处理到2554的日期。如果您的系统必须操作超过2554,并且无法在现场对系统进行升级或修改,那么您必须特别注意系统日期(请与我们联系以获得帮助)。
ClockTime()内核调用允许您获取或设置由ID (CLOCK_REALTIME)指定的系统时钟,该ID维护系统时间。一旦设置好,系统时间就会根据系统时钟的分辨率增加一些纳秒。可以使用 ClockPeriod()调用查询或更改此分辨率。
在系统页面(内存中的数据结构)中,有一个64位字段(nsec),它保存系统启动以来的纳秒数。nsec字段总是单调递增的,并且从不受ClockT ime()或ClockAdjust()设置一天当前时间的影响。
函数的作用是:返回一个自由运行的64位循环计数器的当前值。这在每个处理器上实现,作为一种高性能的定时短间隔机制。例如,在Intel x86处理器上,使用读取处理器时间戳计数器的操作码。在奔腾处理器上,这个计数器在每个时钟周期上递增。一个100mhz的奔腾频率的周期是1/100,000,000秒(10纳秒)。其他CPU架构也有类似的指令。在没有在硬件中实现这种指令的处理器上,内核会模拟一个指令。这将提供比在IBM pc兼容系统上提供指令(838.095345纳秒)更低的时间分辨率。在所有情况下,SYSPAGE_ENTRY(qtime)->cycles_per_sec字段在一秒内给出clockcycle()增量的数量。
ClockPeriod()函数允许线程将系统计时器设置为若干纳秒;操作系统内核将尽其所能以可用的硬件满足请求的精度。所选的间隔总是四舍五入到底层硬件定时器的精度的一个积分。当然,将其设置为极低的值可能会导致CPU性能的很大一部分被用于服务计时器中断。
为了减少功耗,内核可以以无滴答模式运行,但这有点用词不当。系统仍然有时钟滴答声,一切正常运行,除非系统空闲。只有当系统完全处于空闲状态时,内核才会关闭时钟节拍,而实际上,它所做的是减慢时钟的速度,以便下一个滴答声在下一次活动计时器触发后发生,这样计时器就会立即触发。为了启用无时间的操作,请指定- z选项。
为了便于应用时间校正,而不会让系统在时间上经历突然的“步骤”(甚至让时间向后跳转),ClockAdjust()调用提供了指定时间校正应用间隔的选项。这将在指定的时间间隔内加速或延迟时间,直到系统与指定的当前时间同步。此服务可用于实现网络上多个节点之间的网络协调时间平均。
QNX中微子RTOS直接提供了POSIX定时器的全部功能,因为这些定时器可以快速创建和操作,所以它们是内核中廉价的资源。POSIX定时器模型非常丰富,提供了使定时器过期的功能:
•一个绝对的日期
•一个相对的日期(例如:, n纳秒之后)
•周期性(即。,每一个n纳秒)
周期模式非常重要,因为最常见的计时器使用往往是事件的周期性来源,它可以“踢”一个线程到生命中进行一些处理,然后再回到休眠状态,直到下一次事件发生。如果线程必须为每个事件重新编程计时器,那么就有时间流逝的危险,除非线程正在编程一个绝对日期。更糟糕的是,如果线程不能在计时器事件上运行,因为一个高优先级的线程正在运行,那么下一个编程到计时器的日期可能已经过去了!
循环模式通过要求线程只设置一次计时器,然后简单地响应由此产生的周期性事件源,从而避免了这些问题。
由于计时器是操作系统中另一个事件的来源,所以它们也利用了它的事件传递系统。因此,应用程序可以请求在超时发生时将QNX中微子支持的任何事件交付给应用程序。
操作系统提供的一个经常需要的超时服务是指定应用程序准备等待任何给定的内核调用或请求完成的最长时间。在抢占式实时操作系统中使用通用操作系统计时器服务的一个问题是,在超时规范和服务请求之间的间隔内,高优先级进程可能已经计划运行和抢占了足够长的时间,以至于指定的超时在请求服务之前就已经过期了。然后,应用程序将以一个已经失效的超时结束请求服务。,没有超时)。这个定时窗口可能导致“挂起”进程、数据传输协议中莫名其妙的延迟以及其他问题。
我们的解决方案是服务请求本身的超时请求原子形式。一种方法可能是在每个可用的服务请求上提供一个可选的超时参数,但是这样会使传递的参数过于复杂,而这些参数通常不会被使用。
QNX中微子提供了一个计时器Timeout()内核调用,该调用允许应用程序指定一组阻塞状态,以便启动指定的超时。稍后,当应用程序对内核发出请求时,如果应用程序即将阻塞指定状态之一,内核将自动启用先前配置的超时。
由于操作系统的阻塞状态非常少,所以这种机制的工作非常简洁。在服务请求或超时结束时,计时器将被禁用,并将控制权交还给应用程序。
无论我们多么希望它是这样,计算机并不是无限快。在实时系统中,没有不必要地消耗CPU周期是绝对重要的。从外部事件的发生到线程中负责对事件作出反应的代码的实际执行的时间最小化也是非常重要的。这个时间称为延迟。我们最关心的两种延迟形式是中断延迟和调度延迟。
中断延迟是指从断言硬件中断到执行设备驱动程序中断处理程序的第一条指令为止的时间。操作系统几乎总是完全启用中断,因此中断延迟通常是无关紧要的。但是代码的某些关键部分确实要求暂时禁用中断。这种禁用时间的最大值通常定义了最坏情况下的中断延迟——在QNX中微子中,这是非常小的。下面的图表说明了硬件中断由已建立的中断处理程序处理的情况。中断处理程序要么简单地返回,要么返回并导致事件被传递。
图16:中断处理程序直接终止
上面图中的中断延迟(Til)表示最小延迟,即中断发生时中断已完全启用。最糟糕的中断延迟将是这次加上操作系统(或运行中的系统进程)禁用CPU中断的最长时间。
在某些情况下,低级硬件中断处理程序必须调度更高级别的线程来运行。在这个场景中,中断处理程序将返回并指示要交付一个事件。这引入了另一种形式的延迟—调度延迟—必须加以考虑。
调度延迟是指从用户的中断处理程序的最后一条指令到驱动线程的第一条指令执行之间的时间。这通常意味着保存当前执行线程的上下文和恢复所需驱动线程的上下文所需的时间。虽然比中断延迟更大,但在QNX中微子系统中,这个时间也是很小的。
图17:中断处理程序终止,返回事件
需要注意的是,大多数中断在不传递事件的情况下终止。在许多情况下,中断处理程序可以处理所有与硬件相关的问题。传递事件以唤醒更高级别的驱动线程只在发生重大事件时发生。例如,串行设备驱动程序的中断处理程序将在每个接收到的传输中断时向硬件提供一个字节的数据,并且仅当输出缓冲区几乎为空时才会触发(devc-ser*)中的更高级别线程。
QNX中微子RTOS完全支持嵌套中断。前面的场景描述了最简单也是最常见的情况,即只有一个中断发生。对未屏蔽中断的最坏情况下的计时考虑必须考虑当前正在处理的所有中断的时间,因为更高优先级的未屏蔽中断将抢占现有中断。
在下面的图中,线程A正在运行。中断IRQx导致中断处理程序Intx运行,它被IRQy及其处理程序Inty抢占。Inty返回导致线程B运行的事件;Intx返回一个导致线程C运行的事件。
图18:堆放中断
中断处理API包括以下内核调用:
使用此API,具有适当特权的用户级线程可以调用InterruptAttach()或InterruptAttach Event(),在中断发生时将在线程的地址空间中传递硬件中断号和函数地址。QNX中微子允许将多个isr附加到每个硬件中断号上,在运行中断处理程序的执行过程中,可以为未屏蔽中断提供服务。
下面的代码示例展示了如何将ISR附加到PC上的硬件计时器中断(操作系统也用于系统时钟)。由于内核的计时器ISR已经在处理中断源的清除,这个ISR可以简单地在线程的数据空间中增加一个计数器变量,然后返回到内核:
使用这种方法,具有适当特权的用户级线程可以在运行时动态地将(和分离)中断处理程序附加到硬件中断向量。这些线程可以使用常规源代码级调试工具进行调试;ISR本身可以通过在线程级别和源代码级别上调用它来调试,或者使用InterruptAttachEvent()调用。
当硬件中断发生时,处理器将在微内核中输入中断重定向。这段代码将当前正在运行的线程的上下文寄存器放入适当的线程表条目中,并设置处理器上下文,以便ISR能够访问包含ISR的线程的代码和数据。这允许ISR使用缓冲区和代码在用户级线程解决中断,如果需要更高级别的工作线程,将事件队列的线程ISR的一部分,它可以工作在ISR的数据放入thread-owned缓冲区。
因为它使用包含它的线程的内存映射来运行,ISR可以直接操作映射到线程地址空间的设备,或者直接执行I/O指令。因此,操作硬件的设备驱动程序不需要链接到内核中。
微内核中的中断重定向代码将调用连接到该硬件中断的每个ISR。如果返回的值表示要传递某种类型的事件,那么内核将对该事件进行排队。当最后一个ISR被调用时,内核中断处理程序将完成对中断控制硬件的操作,然后“从中断返回”。
这个中断返回不一定会进入被中断线程的上下文中。如果排队事件导致高优先级线程准备就绪,那么微内核将中断返回到现在准备好的线程的上下文。
这种方法提供了一个具有良好边界间隔发生的中断执行的第一个指令级的ISR(中断延迟),和最后一个指令的ISR的第一个指令线程已经准备好的ISR(如线程或进程调度延迟)。最坏情况下的中断延迟是有限制的,因为操作系统仅在几个关键区域对一对操作码禁用中断。那些中断被禁用的间隔具有确定性的运行时,因为它们不依赖于数据。
在调用用户的ISR之前,微内核的中断重定向只执行一些指令。因此,对硬件中断或内核调用的进程抢占也同样快速,并且基本上使用相同的代码路径。
ISR在执行时,它具有完全的硬件访问权限(因为它是特权线程的一部分),但不能发出其他内核调用。ISR旨在应对硬件中断在尽可能少的微秒,做最少的工作来满足中断(从UART读取字节等),如果有必要,导致一个线程被安排在一些指定的优先级做进一步的工作。
最坏情况下的中断延迟可以直接从内核施加的中断延迟和每个中断的最大ISR运行时计算出给定的硬件优先级。由于硬件中断优先级可以重新分配,所以系统中最重要的中断可以成为最高优先级。
还要注意,通过使用InterruptAttachEvent()调用,不会运行用户ISR。相反,在每个中断上都会生成用户指定的事件;该事件通常会导致一个等待线程被调度运行并执行工作。当事件生成时,中断被自动屏蔽,然后由在适当时间处理设备的线程显式地解除屏蔽。
因此,硬件中断生成的工作的优先级可以在os计划的优先级上执行,而不是在硬件定义的优先级上执行。由于中断源在得到服务后才会重新中断,所以中断对关键代码区域运行时的影响是可以控制的。
除了硬件中断之外,微内核中的各种“事件”也可以被用户进程和线程“钩住”。当其中一个事件发生时,内核可以向上调用用户线程中指定的函数来执行此事件的特定处理。例如,当系统中的空闲线程被调用时,用户线程可以将内核向上调用添加到线程中,这样就可以很容易地实现特定于硬件的低功耗模式。