QNX system architecture 2 - the QNX Neutrino Microkernel

microkernel实现了嵌入式实时系统使用的POSIX核心功能,以及QNX的消息传递服务。

有些POSIX功能(如file, device I/O)不是在procnto microkernel中实现的,这些功能是通过可选进程和共享库实现的。

想查看你使用系统的kernel版本号,可以使用uname -a命令。更多信息,可参考Utilities Reference

后续的QNX microkernel已经减少了实现系统调用的代码。kernel code的底层对象定义也变得更加特定,这样使得代码复用程度更高(比如抽象POSIX型号,实时信号和QNX pulses到一个通用的数据结构和结构的通用处理函数)。

在最底层,microkernel包含了一些基本对象以及高度抽象的处理函数。OS就构建在这个基础之上

Figure 5: The microkernel

有些开发者认为我们的微内核是用汇编代码实现的,以减小尺寸,提高性能。实际上,QNX主要是用C实现的;通过持续优化算法和数据结构进行性能是内核尺寸优化,而不是通过汇编级代码做优化。


The implementation of the QNX Neutrino RTOS

QNX发展史,QNX软件操作刺痛的应用压力已经从内存有限的嵌入式系统,扩展到高端的SMP机器。

相应的,QNX的设计目标就是适应这些系统。为了达到这些目标,就需要QNX实现其他操作系统未达到的设计目标。

Posix realtime and thread extensions

因为QNX RTOS在微内核中实现了实时和线程服务,这些服务无需加载其他OS模块即可使用。

此外,某些POSIX profiles建议这些服务无需以进程模型存在。为了适应POSIX建议,QNX直接支持线程,但是依赖QNX进程管理服务来扩展包含多线程的进程。

注意,一些实时执行单位和内核仅提供无内存保护的线程模型,没有包含进程模型和内存保护模型。没有进程模型,就不是完全兼容POSIX。


System Services

微内核支持如下内核调用

  • Threads
  • message passing
  • signals
  • clocks
  • timers
  • interrupt handlers
  • semaphores
  • mutual exclusion locks
  • condition variables(condvars)
  • barriers

整个内核都是构建在这些调用之上。OS是完全可剥夺的,甚至在消息传递过程中都可剥夺;当再次调度回来,系统会继续传送剩余的消息。

内核越简单,则进程剥夺所要等待的内核最大代码路径越小,当然代码尺寸小使得复杂多进程环境下定位问题困难。服务执行路径短,是选择服务包含进微内核中的基本原则。如果操作需要执行很多工作(比如进程负载),那么交给外部进程和线程执行,此时切换线程代价和进程处理请求相比微不足道。

严格的使用上面的规则划分内核和外部进程功能,打破了微内核负载高于大内核的神话。对于给定的上下文切换工作,简单内核使得上下文切换非常快,上下文切换消耗的时间淹没在请求花费的时间海洋中。

下图演示了non-SMP内核(x86)的preemption实现细节

中断禁用或者剥夺暂停的时间非常短,通常仅为几百纳秒。


Threads and processes

当构造一个应用(实时的,嵌入式的,图形化或者其他的)开发者可能需要考虑应用中几个算法并行执行。并行执行通过POSIX线程模型达到,也就是一个进程中包含了一个或者多个执行线程。

一个线程可以认为是一个最小执行单位,是微内核中调度和执行单位。一个进程,可以看做是线程的容器,定义了线程执行的地址空间。一个进程包含至少一个线程。

依赖于应用的性质:线程可以独立的执行,或者线程之间需要紧密的合作,进行数据通信以及线程同步。为了达到线程通信和同步,QNX提供了丰富的IPC和同步服务。

下列pthread_*(POSIX Threads)库函数没有响应的微内核实现

  • pthread_attr_destroy()
  • pthread_attr_getdetachstate()
  • pthread_attr_getinheritsched()
  • pthread_attr_getschedparam()
  • pthread_attr_getschedpolicy()
  • pthread_attr_getscope()
  • pthread_attr_getstatckaddr()
  • pthread_attr_getstacksize()
  • pthread_attr_init()
  • pthread_attr_setdetachstate()
  • pthread_attr_setinheritsched()
  • pthread_attr_setchedparam()
  • pthread_attr_setschedpolicy()
  • pthread_attr_setscope()
  • pthread_attr_setstackaddr()
  • pthread_attr_setstacksize()
  • pthread_cleadup_pop()
  • pthread_cleanup_push()
  • pthread_equal()
  • pthread_getspecific()
  • pthread_setspecific()
  • pthread_key_create()
  • pthread_key_delete()
  • pthread_self()

下表列出了POSIX线程调用相应的微内核线程调用,

POSIX call Microkernel call Description
pthread_create() ThreadCreate() Create a new thread of execution
pthread_exit() ThreadDestroy() Destroy a thread
pthread_detach() ThreadDetach() Detach a thread so it doesn't need to be joined
pthread_join() ThreadJoin() Join a thread waiting for its exit status
pthread_cancel() ThreadCancel() Cancel a thread at the next cancellation point
N/A ThreadCtl() Change a thread's QNX Neutrino-specific thread characteristics
pthread_mutex_init() SyncTypeCreate() Create a mutex
pthread_mutex_destroy() SyncDestroy() Destroy a mutex
pthread_mutex_lock() SyncMutexLock() Lock a mutex
pthread_mutex_trylock() SyncMutexLock() Conditionally lock a mutex
pthread_mutex_unlock() SyncMutexUnlock() Unlock a mutex
pthread_cond_init() SyncTypeCreate() Create a condition variable
pthread_cond_destroy() SyncDestroy() Destroy a condition variable
pthread_cond_wait() SyncCondvarWait() Wait on a condition variable
pthread_cond_signal() SyncCondvarSignal() Signal a condition variable
pthread_cond_broadcast() SyncCondvarSignal() Broadcast a condition variable
pthread_getschedparam() SchedGet() Get the scheduling parameters and policy of a thread
pthread_setschedparam(),pthread_setschedprio() SchedSet() Set the scheduling parameters and policy of a thread
pthread_sigmask() SignalProcmask() Examine or set a thread's signal mask
pthread_kill() SignalKill() Send a signal to a specific thread

配置OS,提供线程和进程支持。每一个进程通过MMU保护地址空间,防止其他进程访问。一个进程则可以包含多个线程共享同一个地址空间。

因此用户的选择不仅仅影响到应用程序的并发,而且也决定了IPC和同步服务的使用。

关于进程和线程的编程角度信息,可参考<<Get Programming with the QNX Neutrino RTOS>>中的Processes and Threads, 以及<<QNX Neutrino Programmer's Guide>>中的The Programming OVerview and Precesses

Thread attributes

尽管进程中的线程分享进程所有地址空间,每一个线程还是有一些私有数据。有些私有数据(比如tid或thread ID)被kernel保护起来,其他线程无法访问,而有些私有数据则驻留在未保护的进程地址空间内(线程栈空间),以下是比较重要的线程私有数据:

  • tid

每一个线程都有一个整形值表示的线程ID,从1开始,线程tid在所属的进程中是唯一的。

  • Priority

线程优先级用来帮助决定线程何时执行,线程从他的父亲继承初始优先级,优先级可以改变,比如显示的修改线程优先级,发送消息线程也会改变线程优先级。

在QNX Neutrino TROS中,进程没有优先级属性,只有线程有。

  • Name

从QNX Neutrino Core OS 6.3.2开始,线程有了名字属性;QNX Neutrino C 库参考中的pthread_getname_np和pthread_setname_np可以用来设置和获取线程名字。工具dumper和pidin支持线程名,线程名是QNX Neutrino扩展。

  • Register set

每个线程有自己的指令指针IP,栈指针SP,以及其他处理器特定的寄存器上下文。

  • Stack

每个线程都有专有的栈空间,存放在所属进程的地址空间中。

  • Signal mask

每个线程有专有的signal mask

  • Thread local storage

线程有一个系统定义的数据区,称为线程本地存储TLS,TLS用来存放per-thread信息,比如tid, pid, stack base, errno,以及线程特定的键值对。一个线程可以把用户定义数据关联到线程特定的data key

  • Cancellation handlers

线程结束时,执行的回调函数。

pthread库实现了线程特定数据,存储在TLS中。关联一个进程的全局key和线程特定数据。为了使用线程特定数据,首先创建一个key然后绑定一个数据值到这个key。数据值可以是一个整数或者指向动态分配数据结构的指针。通过key就可以访问这个绑定的数据。

thread特定数据的一个典型应用场景:一个线程安全函数需要为每一个调用线程维护一个上下文。

使用如下函数创建和维护线程特定数据

Function Description
pthread_key_create() Create a data key with destructor function
pthread_key_delete() Destroy a data key
pthread_setspecific() Bind a data value to a data key
pthread_getspecific() Return the data value bound to a data key


Thread life cycle

一个进程内的线程数目可能随时变化,因为线程是动态创建和销毁的。

线程创建(pthread_create())涉及到进程地址空间内的资源分配和资源初始化,然后启动线程的执行。

线程销毁(pthread_exit(), pthread_cancel())涉及到线程停止,以及线程资源回收。当一个线程执行时,它的状态通常可以描述为ready或者blocked状态。确切的说,它可以是以下状态的一个。


Figure 8: 可能的线程状态。注意,除了以上列出的变迁,线程可以从任何状态转换为READY状态。

  • CONDVAR

线程阻塞在一个条件变量(比如调用了pthread_cond_wait())

  • DEAD

线程中止并且等待其他线程join

  • INTERRUPT

线程正在等待一个中断

  • JOIN

线程阻塞等待join另外一个线程。

  • MUTEX

线程阻塞在一个互斥锁上,比如调用了pthread_mutex_lock()

  • NANOSLEEP

线程正在睡眠一个短时间间隔,比如调用了nanosleep()

  • NET_REPLY

线程正在等待reply发送到网络上,比如调用了MsgReply*()

  • NET_SEND

线程等待pulse或者signal的发送,比如调用了MsgSendPulse(), MsgDeliverEvent(), 或者SignalKill()

  • READY

线程已经准备就绪,处理器正在执行其他高优先级线程。

  • RECEIVE

线程阻塞在消息接收上,比如调用了MsgReceive

  • REPLY

线程阻塞在消息reply上,比如调用了MsgSend()

  • RUNNING

线程正在被处理器执行。内核使用一个队列(每处理器对应一个)跟踪正在运行的线程

  • SEM

线程正在等待一个信号量被释放,比如调用了SyncSemWait()

  • SEND

线程被阻塞在了消息发送,比如调用了MsgSend(),但是服务器还没有收到这个消息

  • SIGSUSPEND

线程阻塞等待一个信号,比如调用了sigsuspend

  • SIGWAITINFO

线程阻塞等待一个信号,比如调用了sigwaitinfo()

  • STACK

线程等待分配分配线程堆栈地址空间,通常是父进程调用了ThreadCreate()

  • STOPPED

线程阻塞等待SIGCONT信号

  • WAITCTX

线程正在等待一个非整数上下文变得可用

  • WAITPAGE

线程正在等待为一个虚拟地址分配物理内存

  • WAITTHREAD

线程正在等待一个子线程创建完成,比如调用了ThreadCreate()

Thread scheduling

kernel的部分工作就是决定哪个进程运行,以及何时运行。

首先,让我们看下kernel何时进行调度决定。

当微内核发生系统调用,异常或者硬件中断,正在执行的线程临时挂起,此时会进行调度决策。而不需考虑线程运行在哪个处理器上。线程调度是全局的,发生所有处理器上。

正常情况下,挂起线程会被resume,但是在一下情况下,线程调度器会执行上下文切换,从一个线程切换到另外一个线程。

  • 线程被blocked
  • 线程被preempted
  • yields

When is a thread blocked

当运行线程等待某些事件的发生时(IPC请求的响应,等待一个mutex等等)会被阻塞。阻塞的线程被从运行队列移除,并选中等待队列中优先级最高的线程执行。当被阻塞线程解除阻塞后,线程被放到该优先级等待队列的最后。

When is a thread preempted

当一个高优先级的线程被放到ready队列中,运行线程被剥夺执行,被剥夺的线程放在对应优先级等待队列的队首,然后高优先级线程获得执行。

When is a thread yielded?

正在运行的进程自愿放弃处理器(调用sched_yield()),进程被放到等待队列的末尾。然后调用最高优先级的进程执行(注意,有可能仍然是该进程被调度到)。

Scheduling priority

每一个线程都对应一个优先级。线程调度器通过查找所有READY线程的优先级,选择优先级最高的线程运行。

下图显示了五个线程(B-F)的等待队列。线程A是当前运行进程。所有其他进程(G-Z)为BLOCKED状态。线程A,B以及C在最高优先级。


Figure 9: 等待队列

OS支持256个调度优先级。一个非特权线程可以设置它的优先级为1~63(63为最高优先级)。root线程和哪些具有PROCMGR_AID_PRIORITY能力的线程允许设置优先级大于63。特殊进程idle优先级为0,随时准备运行。线程缺省情况下继承父线程的优先级。

你可以使用如下命令改变非特权进程允许的优先级范围

procnto -P priority

QNX Neutrino 6.6及后续版本,可以使用s和S选项,对于超范围的优先级请求,选择使用最大允许优先级而不是返回一个错误。

注意为了防止优先级反转,kernel可以临时提升一个线程的优先级。更多信息,参考本章和Interprocess Communication(IPC)章中的"Priority inheritance and mutexes"小节。内核线程的初始优先级是255,不过在他们阻塞在MsgReceive()后,这些内核线程优先级变成了发送消息线程的优先级。

Ready队列上的线程按照优先级排序。Ready队列实现了256个队列,每个队列对应一个优先级,相称调度时,选择最高优先级队列中的第一个线程执行。

大部分情况下,线程是FIFO方式加入到相应优先级队列中,存在如下特殊情况:

  • 一个server线程收到了来自client的消息,离开RECEIVE-blocked状态,被插到所在优先级队列的头部,此时可以认为是LIFO。
  • 如果一个线程发送了一个nc(non-cancellation point)变种消息,那么么当server回复后,线程被放到等待队列头部,而不是尾部。如果调度策略是round-robin,线程的时间片没有重添;例如,如果线程在发送之前已经使用了一半的时间片,那么在可以优雅的剥夺该线程之前,它还有一半的时间片。

Scheduling Policies

为了复合各种应用需求,QNX Neutrino RTOS提供了如下算法

  • FIFO调度
  • round-robin 调度
  • sporadic调度

系统中的线程理论上可以用上述任意调度方法运行。调度方法是每线程,而不是一个全局线程和进程调度方法。

记住FIFO和round-robin调度策略仅当两个或以上线程共享相同优先级。而sporadic调度策略,使用budget来控制线程执行。在以上所有调度策略中,如果一个高优先级线程变得READY,那么会立刻剥夺所有的低优先级进程。

下图,有三个相同优先级线程状态为READY。如果线程A blocks,线程B会执行。


FIFO 10: Thread A blocks; Thread B runs.

尽管一个线程从父进程哪里继承了调度策略,线程可以请求内核改变调度算法。

FIFO scheduling

在FIFO调度策略,线程被选择继续运行,直到

  • 自愿放弃控制
  • 被高优先级进程剥夺

Round-robin scheduling

对于round-robin调度,线程被选择继续运行,直到

  • 自愿放弃控制
  • 被高优先级进程剥夺
  • 时间片结束

如下图所示,进程A持续运行,直到消耗完时间片,下一个READY thread变成运行进程


Figure 12: Round-robin scheduling

时间片是系统赋给每个线程的时间单位。一旦消耗完时间片,线程被剥夺,相同优先级READY队列中的下一个线程被选取执行。一个时间片是4x时钟周期。

Sporadic scheduling

sporadic调度策略,通常用来对进程在某个给定时间段内,提供一个执行时间上限。

该策略对于系统中运行的周期性或者非周期性RMA非常有用。该算法可以保证执行非周期性服务线程不会影响到系统内线程和进程的实时响应上限。

当使用sporadic调度时,线程优先级动态的在前台正常优先级和后台低优先级间调整震荡。使用下面的参数,你可以控制sporadic调度的条件。

  • Initial budget

线程在从正常优先级变成低优先级前,允许执行的时间总数。

  • Low priority

线程要降到的优先级。线程作为后台进程时,在这个低优先级运行。

  • Replenishment period

允许线程消耗执行budget的时间段。对于replenishment操作,POSIX实现使用这个值做为进程变为READY状态的时间段。

  • Max number of pending replenishment

这个值限制replenishment操作的上限,因而也就决定了sporadic调度策略的系统负载上限。

如下图所示,sporadic调度策略建立了线程的初始执行budget,线程执行时会消耗这个budget,但是这个值会周期性重新充满。当一个线程被阻塞后,那么在某个特定时间后,执行budget会被重新充满。

Figure 13: A thread's budget is replenished periodically

在正常优先级N, 线程可执行时间定义为初始执行时间C,一旦这个时间被消耗完,线程优先级被调整低优先级L,直到replenishment操作发生。

下图显示了另外一种情况,线程从来没有发生阻塞或者被剥夺。


Figure14: A thread drops in priority until its budget is replenished.

在这里,线程掉到了低优先级,在低优先级线程可能会执行也可能不会执行。一旦replenishment发生,线程优先级恢复到原始级别。这样每周期T,线程都会可以在高优先级N执行最大C时间。这就确保系统线程在N优先级,仅能占用C/T系统资源。

实际上,一个线程可能会被阻塞多次,因此在优先级N线程并不会真正执行C时间,C仅仅是上限。

Manipulating priority and scheduling policies

一个线程优先级可以在执行时发生变化,线程本身直接修改,或者当线程从高优先级线程接收消息时kernel调整线程优先级。

除了优先级,用户可以选择线程的调度策略。尽管QNX库提供了许多不同的方法获取和设置调度参数,最好使用pthread_getschedparam(), pthread_setschedparam()和pthread_setschedprio()。更详细的信息,参见<<Neutrino Programmer's Guide>>中Programming Overview 一章。

IPC issues

因为进程中的所有线程都可以不受阻碍的访问共享数据空间,看起来这个执行模型可以解决所有的IPC问题?是否我们可以通过共享数据机制通信而抛弃掉其他的IPC通信机制?

如果事情真像这么简单就好了!

第一个问题是线程访问共享数据需要同步操作。一个线程读取到不一致数据因为另外一个线程正在修改这部分数据,这回导致灾难性后果。对于critical section,必须使用某种同步机制,保证对critical section的串行访问。

Muxtexes, semaphores, 以及condvars是解决该问题的方法。

尽管同步服务可以用来协调线程对共享内存的访问,共享内存仍然无法解决某些IPC通信问题。比如,线程加同步只能做为单进程内IPC通信机制,如果我们的应用需要对一个数据库server通信,我们需要传递请求细节给database server,但是要通信的线程存在于database server进程内,它的地址空间是无法寻址的。

network-distributed IPC机制通过在本地和远程网络间传送消息,因此可以用来访问所有的OS服务。因为消息是有尺寸的,并且消息一般来说都比较小,远小于共享内存传输的数据。

Thread complexity issues

尽管线程非常适合某些系统设计,但是一定要注意使用线程导致的潘多拉魔盒。

在某种意义上,MMU保护的多任务已经变得很普通了,计算机领域已经普遍采用在未保护地址空间实现多线程。这不仅使得调试变得困难,而且也让创建稳定代码更困难。

线程最初在UNIX操作系统中引入,作为一个轻量级并发机制解决重量级的进程上下文切换。尽管这是一个非常有意义的尝试,但是我们不仅要问:为什么最初进程上下文切换如此耗时?

事实上,QNX的线程和进程上下文切换性能几乎相同。QNX Neutrino RTOS进程切换时间远快于UNIX线程切换时间。因此,QNX线程无需作为解决IPCL性能问题的手段;而是应用和server进程中获取更大并发性能的方法。

无需求助于线程,QNX系统快速的进程间上下文切换,使得用一组共享显示分配共享内存的合作进程,来构建应用变得非常合理。应用程序因此仅仅会受到共享内存区域引入的bug。而它的私有内存空间则不会受到其他进程破坏。在纯线程模式中,所有线程的私有数据(包括栈空间)都可被其他线程访问,很容易被野指针影响。

尽管如此,线程仍然提供了纯进程模型所不具备的并发优点。比如,一个文件系统服务进程执行来自客户端的请求,那么显然会受益于多线程执行。如果一个client进程需要一个磁盘块,而其他的client则请求一个已经在cache中的磁盘块,文件系统进程可以利用一个线程池并发的服务客户端请求,而不是傻等第一个请求完成。

随着请求的到达,每一个线程能够直接使用buffer cache响应请求或者等待disk I/O完成,等待disk I/O过程并不会增加其他client进程的响应延迟。文件系统server可以预先创建一组线程,准备响应到来的客户端请求。尽管这种实现方式使得filesystem manager的实现方式更复杂,但是获得的并发性是客观的。

Synchronization services

QNX Neutrino RTOS提供了POSIX标准的线程级别同步原语,这些同步方法甚至可以用在不同进程的线程之间。

同步服务至少包括如下机制

Synchronization service Supported between processes Supported across aQNX Neutrino LAN
Mutexes Yes No
Condvars Yes No
Barriers No No
Sleepon locks No No
Reader/writer locks Yes No
Semaphores Yes Yes (named only)
FIFO scheduling Yes No
Send/Receive/Reply Yes Yes
Atomic operations Yes No

以上同步原语大部分是实现在kernel中,除了:

  • barriers, sleepon locks, 以及reader/writer locks
  • atomic operations,可以实现由处理器实现,或者由kernel模拟

Mutexes: mutual exclusion locks

互斥锁,或者说mutexes是最简单的同步服务。mutex被用来互斥访问线程间共享数据。

mutex获取pthread_mutex_lock()或者pthread_mutex_timedlock(),释放pthread_mutex_unlock(),在访问共享数据附近,通常是临界区。

在某个时间点,仅仅一个线程可以获得mutex锁。线程如果企图lock已经上锁的互斥锁,将会阻塞线程直到mutex被解锁。当线程unlocks互斥锁,互斥锁等待队列中最高优先级的线程被唤醒并获得mutex。以这种方式,mutex等待线程按照优先级高低顺序访问临界区。

在大部分处理器上,互斥锁获取并不需要内核项来实现空闲mutex。在x86机器上使用compare-and-swap操作码;在大部分RISC处理器上可以使用load/store条件操作码。

仅当请求已被其他进程获取的mutex时,才会创建内核项,以便把当前进程加到blocked list上;当释放mutex时,内核项被销毁。这使得申请和释放无竞争的临界区和临界资源变得非常快,仅当需要解决竞争问题时,才会引入额外的OS工作。

非阻塞函数pthread_mutex_trylock()可以用来测试mutex是否可用。为了获得更好的性能,临界区的执行时间应该很小。需要使用condvar来实现线程在临界区的阻塞。

Priority inheritance and mutexes

缺省情况下,如果申请mutext线程优先级高于当前mutex owner的进程优先级,那么当前mutex owner的有效优先级增加到等待mutex进程的优先级。当mutex owner释放mutex后,有效优先级调整回原优先级;mutext owner的有效优先级应该是它所阻塞线程的最高优先级,无论是直接阻塞还是间接阻塞。

这个模式不仅确保阻塞在mutex的高优先级线程等待时间尽可能短,而且也解决了经典的优先级反转问题。

调用ptread_mutexattr_init()函数时通过设置PTHREAD_PRIO_INHERIT,支持优先级继承;还可以调用pthread_mutexattr_setprotocol()覆盖这个初始设置。pthread_mutex_trylock()函数不会改变线程优先级,因为改函数并不会阻塞。

可以使用pthread_mutexattr_settype()修改mutex属性,允许mutex被同一个线程递归locked。这样该线程可以调用会申请mutex的进程,而这个线程已经获得了这个locked。

Condvar: condition variables

条件变量condvar,用来在临界区阻塞一个线程直到某些条件满足。条件可以是任意复杂的不依赖于条件变量本身。条件变量应该和mutex一起使用以便实现monitor。

条件变量支持三种操作:

  • wait, pthread_cond_wait()
  • signal, pthread_cond_signal()
  • broadcast, pthread_cond_broadcase()

下面代码是使用条件变量的例子

pthread_mutex_lock( &m );
. . .
while (!arbitrary_condition) {
    pthread_cond_wait( &cv, &m );
    }
. . .
pthread_mutex_unlock( &m );

在这个代码例子中,在测试condition之前获取mutex。确保只有这个线程访问arbitrary condition。当条件为帧,示例代码阻塞等待直到其他进程通过信号和广播设置condvar。

while循环存在有两个原因。第一,POSIX不能保证错误唤醒发生。第二,当另外一个线程修改了condition,我们需要测试修改是否满足我们的标准。mutex m被pthread_cond_wait自动unlocked,这样允许其他线程进入临界区。

一个线程执行信号unlock condvar等待队列上最高优先级线程,而broadcase则unblock等待队列上的所有线程。线程必须在访问临界区后unlock mutex。

pthread_cond_timedwait允许condvar指定一个timeout, 等待线程在timeout超时后会unblockd

Barriers

barrier是一种同步机制,相当于把一组合作线程阻塞,直到指定数目的线程都到达该阻塞点后,才会unblock这些线程。

和pthread_join不同,pthread_join是等待线程结束;而barrier则类似兽栏一样,把线程圈在一个地方,达到一定数目后,才把这些牲畜放走。

使用pthread_barrier_init()创建一个barrier

#include <pthread.h>
int
pthread_barrier_init (pthread_barrier_t *barrier,
const pthread_barrierattr_t *attr,
unsigned int count);

@barrier是传入的barrier对象

@count 是pthread_barrier_wait()要阻塞的线程数目。

一旦创建了barrier,每个线程通过调用pthread_wait_wait()指示线程已经完成,等待barrier放行。

#include <pthread.h>
int pthread_barrier_wait (pthread_barrier_t *barrier);

当一个线程调用了pthread_barrier_wait(),该线程阻塞在改函数,直到pthread_barrier_init()函数中指定数目的线程调用了pthread_barrier_wait(),姿势所有的线程都会解除阻塞,挂到READY队列上。

如下,是barrier使用的例子

<pre class="pre codeblock">/*
 *  barrier1.c
 */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
#include <sys/neutrino.h>

pthread_barrier_t   barrier; // barrier synchronization object

void *
thread1 (void *not_used)
{
    time_t  now;

    time (&now);
    printf ("thread1 starting at %s", ctime (&now));

    // do the computation
    // let's just do a sleep here...
    sleep (20);
    pthread_barrier_wait (&barrier);
    // after this point, all three threads have completed.
    time (&now);
    printf ("barrier in thread1() done at %s", ctime (&now));
}

void *
thread2 (void *not_used)
{
    time_t  now;

    time (&now);
    printf ("thread2 starting at %s", ctime (&now));

    // do the computation
    // let's just do a sleep here...
    sleep (40);
    pthread_barrier_wait (&barrier);
    // after this point, all three threads have completed.
    time (&now);
    printf ("barrier in thread2() done at %s", ctime (&now));
}

int main () // ignore arguments
{
    time_t  now;

    // create a barrier object with a count of 3
    pthread_barrier_init (&barrier, NULL, 3);

    // start up two threads, thread1 and thread2
    pthread_create (NULL, NULL, thread1, NULL);
    pthread_create (NULL, NULL, thread2, NULL);

    // at this point, thread1 and thread2 are running

    // now wait for completion
    time (&now);
    printf ("main() waiting for barrier at %s", ctime (&now));
    pthread_barrier_wait (&barrier);

    // after this point, all three threads have completed.
    time (&now);
    printf ("barrier in main() done at %s", ctime (&now));
    pthread_exit( NULL );
    return (EXIT_SUCCESS);
}


主线程创建了barrier对象,并且初始化count为3,在该barrier对象上调用pthread_barrier_wait的线程,会阻塞到这个调用上,当阻塞的线程数达到3时,线程继续执行。

在本次release中,包含如下barrier函数

Function Description
pthread_barrierattr_getpshared() Get the value of a barrier's process-shared attribute
pthread_barrierattr_destroy() Destroy a barrier's attributes object
pthread_barrierattr_init() Initialize a barrier's attributes object
pthread_barrierattr_setpshared() Set the value of a barrier's process-shared attribute
pthread_barrier_destroy() Destroy a barrier
pthread_barrier_init() Initialize a barrier
pthread_barrier_wait() Synchronize participating threads at the barrier

Sleepon locks

Sleepon locks和condvars非常类似,除了几个微小的不同。

和condvars类似,sleepon locks用来阻塞当前线程,直到某个条件变为真。但是不像condvars必须为每个检查的condition必须分配;sleepon locks复用一个mutex,而不管被检查的条件数目。

Reader/write locks

更正式的说法是 - 多个读者,单个写者锁。这些锁被用来实现多个线程读数据结构,单个线程写数据结构。读写锁的代价高于mutexes,但是在某些访问模式下是有用的。

读写锁允许多个线程同时申请读请求锁pthread_rwlock_rdlock(),但是如果一个线程调用了写请求锁pthread_rwlock_wrlock(),这个读锁请求会被拒绝,直到当前所有的读进程释放了读锁pthread_rwlock_unlock()

多个写进程可以排队等待写操作,所有被阻塞的写线程运行结束前,读进程不会再允许访问。读线程的优先级不会被考虑的。

pthread_rwlock_tryrdlock()和pthread_rwlock_trywrlock()允许线程尝试获取请求的锁,而不会阻塞当前线程。这些调用或者成功获取锁,或者返回一个状态指明锁无法立刻获得。

读写锁不是在kernel中实现的,而是通过kernel提供的mutex和condvar构建的。

semaphores

信号量是另外一种同步方式,允许线程通过post和wait操作控制线程唤醒和休眠。

sem_pose操作增加信号量值,sem_wait()则减少信号量值。

如果sem_wait在一个正值的信号量上,线程不会被阻塞。sem_wait在负值信号量上,会导致当前进程阻塞,直到某个进程执行了sem_post。如果在sem_post之前执行了多次sem_post,那么多个线程执行sem_wait操作就不会阻塞。

信号量和其他同步原语的主要区别是信号量是异步安全的,可以被signal handlers控制,如果需要的效果是通过信号唤醒线程,那么信号量是正确选择。

信号量另外一个有用的属性是支持进程间同步。尽管mutex也可以在进程间工作,POSIX线程标准认为这是可选功能,该功能不是可移植的。如果在单进程内线程间同步,Mutexes比信号量更有效。

信号量一个变种,命名为信号量的服务。使用它可以实现网络上不同主机上进程间同步。

Synchronization via scheduling policy

通过选择POSIX FIFO调度策略,我们可以保证在non-SMP系统上,两个具有相同优先级的进程不会访问临界区。

FIFO调度策略控保证具有相同优先级的线程持续运行,直到他们自愿放弃处理器给其他线程。

这个放弃包括线程向其他进程请求服务引起的阻塞,或者一个信号发生。临界区内必须仔细编码和注释,确保后面的维护者不破坏这个原则。

此外高优先级线程仍然有可能剥夺这些FIFO调度线程。所以临界区碰撞只能是具有相同优先级的FIFO调度线程。通过施加这个条件,线程可以放心的访问这个共享内存而无需考虑显示的同步操作。

Synchronization via message passing

 Send/Receive/Reply IPC消息天然就是一种同步机制。在很多场景下,使用IPC消息后就无需在使用其他同步机制。

Synchronization via atomic operations

在某些情况下,你可能想执行一个端操作,并且保证短操作是原子的,换句话说就是不允许其他线程或者ISR打断操作。

QNX提供了如下原子操作

  • Add a value
  • subtracting a value
  • clearing bits
  • setting bits
  • toggling bits

尽管可以在任何地方使用原子操作,但是有两种情况非常适合使用原子操作:

  • ISR和线程之间
  • 两个线程之间

ISR可以在任意时间点剥夺一个线程,线程保护自己不被ISR打扰的唯一方法就是禁用中断。在实时系统中不建议禁用中断,推荐使用QNX提供的原子操作。

在一个SMP系统中,多个线程可以同时执行。此外,上面提到的ISR仍然存在,使用QNX的原子操作解决以上问题。

Synchronization services implementation

下表列出了各种微内核调用,以及构造在这些微内核调用之上的POSIX调用。

Microkernel call POSIX call Description
SyncTypeCreate() pthread_mutex_init(),pthread_cond_init(), sem_init() Create object for mutex, condvars, and semaphore
SyncDestroy() pthread_mutex_destroy(),pthread_cond_destroy(), sem_destroy() Destroy synchronization object
SyncCondvarWait() pthread_cond_wait(),pthread_cond_timedwait() Block on a condvar
SyncCondvarSignal() pthread_cond_broadcast(),pthread_cond_signal() Wake up condvar-blocked threads
SyncMutexLock() pthread_mutex_lock(),pthread_mutex_trylock() Lock a mutex
SyncMutexUnlock() pthread_mutex_unlock() Unlock a mutex
SyncSemPost() sem_post() Post a semaphore
SyncSemWait() sem_wait(),sem_trywait() Wait on a semaphore

Clock and timer services

时钟服务用来维护时间,相应的被kernel定时器调用用来实现间隔定时器。

ClockTime()内核调用CLOCK_REALTIME用来获取系统时钟,也就是系统时间。一旦设置,系统时间基于时钟精度增加一定的纳秒。时间精度可以通过系统调用ClockPeriod()查询和设置。

在系统内存中的64bit数据结构用来保存从系统启动开始的nanoseconds。数据结构的nsec成员总是单调增加,不会受到ClockTime()和ClockAdjust()设置当前时间的影响。

ClockCycles()函数返回64bit循环计数器的当前值。这是处理器实现短时间间隔的高性能机制。例如在x86处理器上,可以通过机器码获取时间戳计数器。在Prntium处理器上,这个计数器每个时钟周期加1。一个100MHz的Pentium的时钟周期为1/100,000,000秒(10纳秒)。其他的CPU架构有类似的指令。

对于没有实现该指令的处理器架构,内核通过模拟方式,提供一个低精度实现。

在所有的情况下,SYSPAGE_ENTRY(qtime)->cycles_per_sec成员给出了每秒ClockCycles()增量数。

ClockPeriod()函数允许线程设置系统timer为纳秒的倍数。OS内核根据硬件尽量满足请求的精度。

选择的间隔最后会换算为潜在硬件的精度的整数值。当然,设置成一个非常低的值,会导致CPU性能消耗在时钟中断上。

Microkernel call POSIX call Description
ClockTime() clock_gettime(),clock_settime() Get or set the time of day (using a 64-bit value in nanoseconds ranging from 1970 to 2554).
ClockAdjust() N/A Apply small time adjustments to synchronize clocks.
ClockCycles() N/A Read a 64-bit free-running high-precision counter.
ClockPeriod() clock_getres() Get or set the period of the clock.
ClockId() clock_getcpuclockid(),pthread_getcpuclockid() Return an integer that's passed to ClockTime() as a clockid_t.

内核可以运行在无滴答模式以便减少电量消耗,但这有点误导,实际上系统仍然存在时钟滴答。仅仅当系统完全idle后,kernel才关闭时钟滴答。使能无滴答操作,可以在执行startup-*时增加-Z选项。

Time correction

为了应用时间矫正并且系统无需经历时间跳跃。ClockAdjust调用提供了一个选项,设置时间矫正的时间间隔。这回导致时间加速或者倒退直到系统同步到指定的当前时间。

Timers

QNX提供了POSIX timer功能全集。因为这些timer的创建和维护是快速的,所以timer是内核中不昂贵资源。

POSIX时钟模型是非常丰富的,提供了如下timer类型:

  • 绝对日期
  • 相对日期
  • 周期性的

周期性模式是最重要的,因为timer最常用的是作为一个周期性的源,启动某个进程处理些工作,然后继续休眠直到协议个事件。如果线程在每个事件中重新设置timer,那么就有可能错过时间除非使用绝对时间设置。但是,更坏的情况是,如果t高优先级线程导致timer事件上的线程无法及时运行,就会造成写入的绝对时间已经变成过去时。

周期模式绕开了这些问题,只设置一次,然后简单的响应周期性事件即可。

因为timer是OS中的另外一个事件源,所有timer可以用做时间分发系统。应用请求可以在timer超时后,系统发送QNX支持的任意事件。

OS提供的timeout服务可以用来指定应用等待kernel调用和请求完成的最大等待时间。使用常用OS timer服务的问题:是在可剥夺的实时操作系统下,在标识timout值到请求服务的这段时间间隔内,可能会有一个高优先级进程被调度运行,并且剥夺的时间足够长,这就导致设定的 timout在请求服务时已经过期了。这样应使用一个过期的timout请求服务(不会再超时),这可能导致挂起的进程,协议传输时莫名其妙的延迟等问题。

QNX提供了TimerTimeout()内核方法允许应用设定一系列的阻塞状态的timeout。之后,当应用向内核发送一个请求时,内核自动使能之前配置的timeout作为应用阻塞在给定状态的超时时间。

Microkernel call POSIX call Description
TimerAlarm() alarm() Set a process alarm
TimerCreate() timer_create() Create an interval timer
TimerDestroy() timer_delete() Destroy an interval timer
TimerInfo() timer_gettime() Get the time remaining on an interval timer
TimerInfo() timer_getoverrun() Get the number of overruns on an interval timer
TimerSettime() timer_settime() Start an interval timer
TimerTimeout() sleep(),nanosleep(), sigtimedwait(), pthread_cond_timedwait(), pthread_mutex_trylock() Arm a kernel timeout for any blocking state

Interrupt handling

不论我们多么期望,计算你并不能无限快。在一个实时系统中,CPU时钟不被浪费是绝对重要的,最小化外部事件到相应线程代码执行这个时间间隔也是关键的,这个时间间隔称为延迟。

我们最关心的两种延迟,是中断延迟和调度延迟。

interrupt latency

中断延迟硬件中断发生,到执行驱动处理函数第一条指令之间的时间间隔。

OS在大部分情况下都会维持中断使能,所以中断延迟不那么重要,但是某些临界区代码确实需要临时禁用中断。最大禁中断时间通常决定了最坏中断延迟,在QNX系统中这个时间是非常小的。

下图演示了硬件中断的中断处理函数流程。中断处理函数可以简单的返回,或者分发一个事件再返回。

Figure 16: Interrupt handler simply terminates

Scheduling latency

在某些情况下,低级硬件中断处理函数必须调度一个高级线程运行。在这个场景下,中断处理函数分发一个事件并返回。这就引出了第二种形式的延迟- 调度延迟。

调度延迟是从中断处理函数的最后一条指令开始,到驱动线程的第一条指令开始执行的时间间隔。这通常包括保存当前执行上下文,加载驱动线程上下文。尽管这个时间大于中断延迟,但是QNX中调度延迟仍然非常小。

Figure 17: Interrupt handler terminates, returning an event.

需要注意的是,大部分(或者部分)中断不需要发送一个事件。在大部分情况下,中断处理函数可以处理硬件相关的问题。仅当中断需要很多额外处理时,才会唤醒高级别的驱动线程。例如,串行设备驱动的中断处理函数每次收到传输中断时会向硬件填充一个数据,仅当输出buffer几乎为空时才会触发串行设备驱动的线程。

Nested interrupts

QNX完全支持nested中断。

前面的场景描述的只有一个中断发生,这是最简单的,最常见的情况。考虑最坏的时序下,当前处理中断的时间需要考虑未屏蔽的中断,因为高优先级未屏蔽中断将剥夺一个正在处理的中断。

在下图中,进程A正在运行,中断IRQx触发Intx运行,在处理过程中被IRQy的Inty剥夺,Inty返回一个事件导致线程B隐形。Intx返回一个时间导致线程C运行。

Interrupt calls

中断处理API包括如下内核调用

Function Description
InterruptAttach() Attach a local function (an Interrupt Service Routine or ISR) to an interrupt vector.
InterruptAttachEvent() Generate an event on an interrupt, which will ready a thread. No user interrupt handler runs. This is the preferred call.
InterruptDetach() Detach from an interrupt using the ID returned by InterruptAttach() or InterruptAttachEvent().
InterruptWait() Wait for an interrupt.
InterruptEnable() Enable hardware interrupts.
InterruptDisable() Disable hardware interrupts.
InterruptMask() Mask a hardware interrupt.
InterruptUnmask() Unmask a hardware interrupt.
InterruptLock() Guard a critical section of code between an interrupt handler and a thread. A spinlock is used to make this code SMP-safe. This function is a superset of InterruptDisable() and should be used in its place.
InterruptUnlock() Remove an SMP-safe lock on a critical section of code.

使用API,具有相应特权的用户线程可以调用InterruptAttach()或者InterruptAttachEvent(),绑定中断号和某个线程内地址空间内的函数地址。QNX允许多个ISRs绑定到一个硬件中断号上,在运行中断处理函数时,未屏蔽中断仍然接收服务。

  • 在系统初始化阶段,启动代码确保所有的中断源是屏蔽的。当首次调用InterruptAttach()和InterruptAttachEvent()时,内核会取消该中断的屏蔽。类似的,调用最后一个中断向量InterruptDetach(),内核则屏蔽该中断。
  • 在中断处理函数中使用浮点操作是不安全的。

下面代码演示了如何绑定ISR到PC的硬件时钟中断上。因为内核时钟ISR已经负责清理中断源,所以这个ISR只是简单的增加线程数据空间一个计数变量,然后返回到kernel。

#include <stdio.h>
#include <sys/neutrino.h>
#include <sys/syspage.h>

struct sigevent event;
volatile unsigned counter;

const struct sigevent *handler( void *area, int id ) {
    // Wake up the thread every 100th interrupt
    if ( ++counter == 100 ) {
        counter = 0;
        return( &event );
        }
    else
        return( NULL );
    }

int main() {
    int i;
    int id;

    // Request I/O privileges
    ThreadCtl( _NTO_TCTL_IO, 0 );

    // Initialize event structure
    event.sigev_notify = SIGEV_INTR;

    // Attach ISR vector
    id=InterruptAttach( SYSPAGE_ENTRY(qtime)->intr, &handler,
                        NULL, 0, 0 );

    for( i = 0; i < 10; ++i ) {
        // Wait for ISR to wake us up
        InterruptWait( 0, NULL );
        printf( "100 events\n" );
        }

    // Disconnect the ISR handler
    InterruptDetach(id);
    return 0;
    }

使用这个方法,特权用户线程可以动态的添加中断处理函数到硬件中断。这些线程可以使用源码级调试工具;当使用interuptAttachEvent()调用时,ISR本身也是可以在源代码级别调试。

当硬件中断发生时,处理器将进入内核的中断重定向。这段代码保存当前运行进程上下文到线程表项,并且设置处理器上下文为ISR代码和数据所在的线程。这允许ISR使用用户态线程buffer和代码处理中断,如果需要线程执行更高级别的工作。把ISR所在线程加入事件队列,稍后该线程即可使用ISR已经放入到线程buffers中的数据进一步处理。

因为ISR可以访问所在线程的内存映射空间,所以ISR可以直接操作映射到线程地址空间的设备,或者直接执行I/O指令。因此,控制硬件的设备驱动不去要连接到内核中。

内核中的中断重定向代码将调用绑定到硬件中断上的每一个ISR。如果返回值指示有事件需要处理,那么kernel把事件加入队列。当这个中断向量的最后一个ISR已经调用完成,内核中断处理函数完成了硬件中断的控制,并从中断返回。

中断返回并不意味着进入被中断线程的上下文,如果入队的事件使得一个高优先级线程变成READY,微内核将返回到这个高优先级线程。

这个方法使得中断发生到第一条ISR指令执行间隔,以及最后一条ISR指令到ISR触发线程第一条指令执行间隔范围良好。

最坏情况下的中断延迟是良有界的,因为OS仅仅在代码很少的临界区禁用中断。禁用中断的时间间隔是固定的,因为没有数据依赖。

微内核中断重定向在调用ISR前仅执行很少的指令。因此硬件或者内核调用的进程剥夺是非常块的,并且代码路径相同。

当ISR正在执行时,它对硬件有完全的访问权限,但是不能调用其他内核调用。ISR主要目的是在尽可能短时间内响应硬件中断,做尽可能少的工作来满足中断,如果必要,则触发一个线程调度做进一步的工作。

最坏的中断延迟计算方法:内核导致的中断延迟,加上所有大于当前中断的最大ISR运行时间。因为中断优先级是可以重新赋值的,所有系统内最重要的中断使用最高的优先级。

注意对于InterruptAttachEvent()调用,没有ISR运行。而是为每个中断生成一个用户特定事件。当时间生成后中断被自动屏蔽,在设备驱动处理线程中需要显示的重新使能中断。

因为硬件中断生成的工作优先级可以在OS调度优先级执行,而不是硬件定义的优先级。因此中断源不会在被处理前不会重入中断,

除了硬件中断,各种微内核事件也可以被绑定到用户线程和进程。当这种事件发生时,内核可以调用到用户线程中的代码,执行这个事件的特定处理。例如,当系统idle线程被调用时,一个用户线程可以被内核回调到线程内,这样硬件特定的低功耗模式可以很容易实现。

Microkernel call Description
InterruptHookIdle2() When the kernel has no active thread to schedule, it runs the idle thread, which can call a user handler. This handler can perform hardware-specific power-management operations.
InterruptHookTrace() This function attaches a pseudo interrupt handler that can receive trace events from the instrumented kernel.



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章