2-2-18-2 QNX Neutrino 微内核(一)

阅读前言

本文以QNX系统官方的文档英文原版资料为参考,翻译和逐句校对后,对QNX操作系统的相关概念进行了深度整理,旨在帮助想要了解QNX的读者及开发者可以快速阅读,而不必查看晦涩难懂的英文原文,这些文章将会作为一个或多个系列进行发布,从遵从原文的翻译,到针对某些重要概念的穿插引入,以及再到各个重要专题的梳理,大致分为这三个层次部分,分不同的文章进行发布,依据这样的原则进行组织,读者可以更好的查找和理解。


1. QNX Neutrino 微内核(一)

微内核实现了嵌入式实时系统中使用的核心POSIX特性,以及基本的QNX Neutrino消息传递服务。在procnto微内核中没有实现的POSIX特性(例如文件和设备I/O)由可选进程和共享库提供。

黑莓QNX各版本微内核减少了实现给定内核调用所需的代码。内核代码中最底层的对象定义变得更加具体,从而允许更好的代码复用(例如将各种形式的POSIX信号、实时信号和QNX Neutrino注入到公共数据结构和代码中,以操作这些结构)。

微内核在最低限度上包含一些基本对象和用于操作它们的高度调优程序。操作系统就是在此基础上构建的。

一些开发人员认为,出于大小或性能等方面原因的考虑,我们的微内核完全是用汇编代码实现的。事实上,我们的实现主要是用C编写的;大小和性能目标是通过不断改进的算法和数据结构来实现的,而不是通过汇编级别的peep-hole优化。

1.1. QNX Neutrino RTOS的设计目标

从历史上看,黑莓QNX操作系统的“应用压力”来自于计算量范围的两端,从内存有限的嵌入式系统一直到具有千兆物理内存的高端SMP(对称多处理)机器。

相应地,QNX Neutrino的设计目标容纳了两组看似独特的功能。追求这些目标的目的是扩展系统的适用范围,远远超出其他操作系统实现所能解决的范围。

POSIX实时和线程扩展

由于QNX Neutrino RTOS直接在微内核中实现了大多数实时性和线程服务,因此即使没有额外的操作系统模块,这些服务也是可以使用的。

此外,POSIX所定义的一些概要文件(profiles)建议,对于这些服务不需要进程模型就可以提供。为了适应这一点,操作系统提供了对线程的直接支持,但依赖于其进程管理器部分,以便把此功能扩展到包含了多个线程的进程中来。

注意,许多实时执行器和内核只提供非内存保护的线程模型,根本没有进程模型和/或受保护的内存模型。如果没有进程模型,就无法实现完全的POSIX遵从性。

1.2. 系统服务

微内核有内核调用来支持以下内容:

  • 线程【threads】
  • 消息传递【message passing】
  • 信号【signals】
  • 时钟【clocks】
  • 定时器【timers】
  • 中断处理【interrupt handlers】
  • 信号量【semaphores】
  • 互斥锁【mutual exclusion locks (mutexes)】
  • 条件变量【condition variables (condvars)】
  • 屏障【barriers】

整个操作系统都是建立在这些调用之上的。操作系统是完全可抢占式的,即使是在进程之间传递消息时,也是如此;它会恢复在抢占之前停止的消息传递。

微内核的最小复杂性,有助于确定内核最长不可抢占代码路径的上限,而较小的代码大小,使得处理复杂的多处理器问题成为了一个易于处理的问题。对包含在微内核中的服务的选择,是基于具有短的执行路径进行的。需要大量工作的操作(例如,进程加载)被分配给外部进程或外部线程,这样的话,与在线程内完成的工作相比,进入线程上下文的工作将微不足道。

严格应用此规则来划分内核和外部进程之间的功能,打破了微内核操作系统必须比整体式(monolithic)内核操作系统产生更高运行时开销的神话。上下文切换之间完成的工作(隐含在消息传递中)超过了简化内核导致的非常快的上下文切换时间。因此,执行上下文切换所花费的时间会“湮没在工作的噪音中”,这些工作是为了服务于OS进程之间消息传递的通信请求的。

下图显示了非smp内核(x86实现)的抢占机制的细节:

QNX Neutrino 抢占机制的细节
QNX Neutrino 抢占机制的细节

中断被禁用,或者抢占被推迟,时间间隔非常短(通常在几百纳秒的量级)。

1.3. 线程和进程

在构建应用程序(实时、嵌入式、图形化或其他)时,开发人员可能希望应用程序中的多个算法部分并发执行。这种并发性是通过使用POSIX线程模型实现的,该模型将进程定义为包含一个或多个执行线程。

线程可以被认为是最小的“执行单元”,即微内核中的调度和执行单元。另一方面,进程则可以被认为是线程的“容器”,它定义了线程将在其中执行的“地址空间”。一个进程总是至少包含一个线程。

根据应用程序的性质,线程可能会独立执行而不需要在不同算法部分之间通信(不太可能),或者它们可能需要紧密耦合,具有高带宽通信和紧密同步。为此,QNX Neutrino RTOS提供了许多IPC和同步服务。

下面的 pthread_*(POSIX线程)库调用不涉及任何微内核线程调用:

下表列出了具有相应微内核线程调用的POSIX线程调用。pthread_*调用可能具有更多在C库中实现的功能,而内核调用没有提供这些功能。除非有特殊情况,请使用以下pthread_*调用来代替内核调用:

POSIX call

Microkernel call

Description

pthread_create()

ThreadCreate()

创建一个新的执行线程

pthread_exit()

ThreadDestroy()

销毁线程

pthread_detach()

ThreadDetach()

分离一个线程,这样它就不需要Join了

pthread_join()

ThreadJoin()

加入(Join)一个等待退出状态的线程

pthread_cancel()

ThreadCancel()

在下一个取消点取消线程

N/A

ThreadCtl()

更改特定于QNX neutrino的线程特征

pthread_mutex_init()

SyncTypeCreate()

创建互斥锁(mutex)

pthread_mutex_destroy()

SyncDestroy()

销毁互斥锁(mutex)

pthread_mutex_lock()

SyncMutexLock()

锁定互斥锁

pthread_mutex_trylock()

SyncMutexLock()

有条件地锁定互斥锁

pthread_mutex_unlock()

SyncMutexUnlock()

解锁一个互斤锁

pthread_cond_init()

SyncTypeCreate()

创建一个条件变量

pthread_cond_destroy()

SyncDestroy()

销毁一个条件变量

pthread_cond_wait()

SyncCondvarWait()

等待一个条件变量

pthread_cond_signal()

SyncCondvarSignal()

通知一个条件变量

pthread_cond_broadcast()

SyncCondvarSignal()

广播一个条件变量

pthread_getschedparam()

SchedGet()

获取线程的调度参数和策略

pthread_setschedparam()pthread_setschedprio()

SchedSet()

设置线程的调度参数和策略

pthread_sigmask()

SignalProcmask()

检查或设置线程的信号掩码

pthread_kill()

SignalKill()

发送一个信号到一个特定的线程

由POSIX定义,操作系统可以配置为提供线程和进程的混合类型。每个进程都受mmu保护,不受其他进程的影响,每个进程可能包含一个或多个共享进程地址空间的线程。

你选择的环境不仅会影响应用程序的并发能力,还会影响应用程序可能使用的IPC和同步服务。


尽管常见的术语“IPC”指的是进程间的通信,但我们在这里使用它来描述线程之间的通信,无论它们是在同一进程中还是在单独的进程中。


从编程的角度了解进程和线程的信息,请参阅QNX Neutrino入门的 Processes and Threads 章节,以及QNX Neutrino程序员指南的Programming OverviewProcesses 章节。

1.3.1. 线程的属性

尽管进程中的线程共享进程地址空间中的所有内容,但每个线程仍然有一些“私有”数据。其中一些私有数据在内核中受到保护(例如,tid或线程ID),而其他私有数据不受保护地驻留在进程的地址空间中(例如,每个线程都有一个供自己使用的堆栈)。

有一些更值得注意的线程私有资源如下:

  • tid,每个线程由一个整数线程ID标识,从1开始。tid在线程所在进程中是唯一的。
  • Priority,优先级,每个线程都有一个优先级,这有助于确定它何时运行。线程从父线程继承其初始优先级,但优先级可以更改,这取决于调度策略、线程所做的显式更改或发送给线程的消息。

在QNX Neutrino RTOS中,进程没有优先级,但是进程的线程有优先级;

可以查看后续的 “Thread scheduling ” 章节获取更多信息;

  • Name,你可以给线程指定一个名字;

参见C库参考中的pthread_getname_np()pthread_setname_np()条目。

诸如dumperpidin之类的 Utility 支持使用线程名称。线程的 name 是由 QNX Neutrino 进行的扩展。

  • Register set,每个线程都有自己的指令指针(IP)、堆栈指针(SP)和其他特定于处理器的寄存器上下文。
  • Stack,每个线程在自己的堆栈上执行,堆栈存储在其进程的地址空间中。
  • Signal mask,每个线程都有自己的信号掩码。
  • Thread local storage,线程有一个系统定义的数据区,称为 “Thread local storage”(TLS)。TLS用于存储 “每个线程” 的信息(如tid、pid、堆栈基地址、errno 和 线程特定的 key/data 绑定)。TLS不需要由用户应用程序直接访问。线程可以拥有与线程特定 data key 相关联的用户定义数据。
  • Cancellation handlers,线程可以具有回调函数,这些回调函数在线程终止时执行。

特定于线程的数据在pthread库中实现并存储在TLS中,它提供了一种将进程全局整数 key 与 唯一的 per-thread data value 相关联的机制。要使用特定于线程的数据,首先创建一个新的 key,然后将唯一的数据值绑定到该 key(每个线程都有)。例如,数据值可以是一个整数或指向动态分配的数据结构的指针。随后,key可以返回每个线程绑定的数据值。

特定于线程的数据的典型应用是用于需要为每个调用线程维护上下文的线程安全函数。

稀疏矩阵 (tid,key) 到 data value 的映射
稀疏矩阵 (tid,key) 到 data value 的映射

你可以使用以下函数来创建和操作这些数据:

Function

Description

pthread_key_create()

用析构函数创建一个data key

pthread_key_delete()

销毁一个data key

pthread_setspecific()

绑定—个data value到—个data key

pthread_getspecific()

返回绑定到data key的data value

1.3.2. 线程的生命周期

进程中的线程数量变化很大,线程是动态创建和销毁的。

线程的创建(pthread_create())涉及分配和初始化进程地址空间内必要的资源(例如,线程堆栈),并在地址空间中的某些函数处开始线程的执行。

线程的终止(pthread_exit(), pthread_cancel())涉及停止线程并回收线程的资源。当线程执行时,其状态通常可以描述为“就绪(ready)”或“阻塞(blocked)”。更具体地说,它可以是以下其中一种:

可能的线程状态。此处省略了从任何状态(DEAD除外)移动到READY状态的描述
可能的线程状态。此处省略了从任何状态(DEAD除外)移动到READY状态的描述
  • STATE_CONDVAR,线程被一个条件变量阻塞(例如,线程调用了pthread_cond_wait())。
  • STATE_DEAD,线程已经终止,正在等待另一个线程的连接。
  • STATE_INTR,线程被阻塞等待一个中断(即,线程调用了 InterruptWait())。
  • STATE_JOIN,线程在等待加入另一个线程时被阻塞(即,线程调用了pthread_join())。
  • STATE_MUTEX,线程因互斥锁被阻塞(即,线程调用了pthread_mutex_lock())。
  • STATE_NANOSLEEP,线程正在进行间隔很短的Sleep(即,线程调用了nanosleep())。
  • STATE_NET_REPLY,线程正在等待通过网络传递的回复(即,线程调用了MsgReply*())。
  • STATE_NET_SEND,线程正在等待通过网络传递一个脉冲或信号(即,线程调用了 MsgSendPulse(), MsgDeliverEvent(), or SignalKill())。
  • STATE_READY,该线程正在等待执行,而处理器正在执行另一个具有相同或更高优先级的线程。
  • STATE_RECEIVE,线程在接收消息时被阻塞(即,线程调用了MsgReceive())。
  • STATE_REPLY,线程在进行消息回复时被阻塞(即,线程调用了 MsgSend(),并且 server 接收到了该消息)。
  • STATE_RUNNING,线程正在被处理器执行。内核使用一个数组(系统中每个处理器有一个entry)来跟踪正在运行的线程。
  • STATE_SEM,线程正在等待一个信号量被发布(即,线程调用了SyncSemWait())。
  • STATE_SEND,线程在发送消息时被阻塞(例如,线程调用了 MsgSend(),但是 server 还没有收到消息)。
  • STATE_SIGSUSPEND,线程被阻塞,等待一个信号(例如,线程调用了 sigsuspend())。
  • STATE_SIGWAITINFO,线程被阻塞,等待一个信号(例如,线程调用了 sigwaitinfo())。
  • STATE_STACK,线程正在等待分配给自己线程堆栈的虚拟地址空间(父线程调用了ThreadCreate())。
  • STATE_STOPPED,线程被阻塞,等待SIGCONT信号。
  • STATE_WAITCTX,线程正在等待一个非整数(例如,浮点数)上下文可用。
  • STATE_WAITPAGE,线程正在等待为虚拟地址分配物理内存。
  • STATE_WAITTHREAD,线程正在等待子线程完成对自身的创建(即,当前线程调用了ThreadCreate(),当前线程为父线程)。

1.4. 线程调度

何时以及如何进行调度决策?

每当内核调用、异常或硬件中断导致微内核进入时,微内核都会做出调度决策。只要任何线程的执行状态发生变化,就会做出调度决策——线程可能驻留在哪个进程中并不重要。线程是跨所有进程进行全局调度的。

正常情况下,正在运行的线程会继续运行,但是发生以下情况时,线程调度器会执行上下文切换,从一个线程切换到另一个线程:

  • blocks(阻塞时)

线程何时阻塞?

当一个正在运行的线程必须等待某个事件发生时(对IPC请求的响应,等待互斥锁等),它就会阻塞。阻塞的线程从正在运行的数组中移除,然后运行优先级最高的就绪线程。当阻塞的线程随后被解除阻塞时,它通常被放在该优先级级别的就绪队列的末尾。

  • is preempted(被抢占时)

线程何时被抢占?

当一个更高优先级的线程被放到就绪队列中时,正在运行的线程就会被抢占(由于它的阻塞条件被解决,它就会变成ready)。被抢占的线程被放在该优先级的就绪队列的开头,高优先级的线程运行。

  • yields(让步时)

线程何时进行让步?

一个正在运行的线程自愿放弃对处理器使用(例如,通过sched_yield()),将被放置在该优先级的就绪队列的末尾。然后运行优先级最高的线程(也有可能仍然还是刚才做出让步的那个线程)。

1.4.1. 调度优先级

每个线程都被分配了一个优先级。线程调度器通过查看分配给READY(即,能够使用CPU)的每个线程的优先级来选择下一个要运行的线程。

  • 在单核系统上,选择具有最高优先级的READY线程来运行。
  • 在多核(SMP)系统上,调度器在一个可用内核上运行优先级最高的READY线程。额外的内核运行系统中的其他线程,尽管不一定是下一个或下一个最高优先级的线程;调度器有一定的灵活性,这样它就可以尝试优化缓存使用并最小化线程迁移。下一次运行低优先级线程的处理器做出调度决策时,它将选择高优先级线程。调度还受到处理器关联的影响,线程可以使用处理器关联来指定它们可以在哪个处理器上运行。(有关更多信息,请参见 Multicore Processing 章节。)

下图显示了具有五个ready线程(B-F)的单核系统的就绪队列。线程A当前正在运行。所有其他线程(G-Z)都被阻塞。线程A、B和C的优先级最高,因此它们将根据正在运行的线程的调度策略共享处理器。

单核系统中的就绪队列
单核系统中的就绪队列

操作系统支持256个调度优先级。无特权线程可以将其优先级设置为1到63(最高无特权优先级)之间的级别,与调度策略无关。只有启用 PROCMGR_AID_PRIORITY 能力的线程(请参阅C库参考中的 procmgr_ability())才允许设置高于63的优先级。特殊空闲线程(在进程管理器中)的优先级为 0,并且随时准备运行。在默认情况下,线程继承其父线程的优先级。

你可以使用 procnto 的-P选项更改允许的非特权进程的优先级范围:

procnto-smp-instr -P priority

在QNX Neutrino 6.6或更高版本中,如果你希望使用超出范围的优先级请求时默认使用允许的最大优先级(达到“最大饱和点”)而不是导致错误,则可以在此选项中添加s或S。当你设置优先级时,你可以将其包装在一个(非POSIX)宏中,以指定如何处理超出范围的优先级请求:

  • SCHED_PRIO_LIMIT_ERROR(priority):显示为错误
  • SCHED_PRIO_LIMIT_SATURATE(priority):使用允许的最大优先级

下表为优先级范围的摘要:

Priority level

Owner

0

Idle thread【空闲线程】

到 priority−1

Unprivileged or privileged【非特权或特权】

priority 到 255

Privileged【特权】

请注意,为了防止优先级反转,内核可能会暂时提高线程的优先级。要了解更多信息,请参见本章后面的“Priority inheritance and mutexes”,以及进程间通信(IPC)中的 “Priority inheritance and messages” 章节。内核线程的初始优先级是255,但是它们做的第一件事是阻塞 MsgReceive(),因此在此之后,它们按照向它们发送消息的线程的优先级进行操作。

就绪队列上的线程按优先级排序。就绪队列实际上被实现为256个单独的队列,每个队列对应一个优先级。选择最高优先级队列中的第一个线程来运行。

大多数情况下,线程在其优先级队列中以FIFO顺序排队,但也有一些例外:

  • 从 RECEIVE-blocked 阻塞状态出来的 非FIFO server 线程使用 MsgSend*() 的 “nc(非取消点)” 变体发送的消息,会被插入到该优先级队列的头部,也就是说,顺序是后进先出,而不是FIFO。
  • 如果线程发送带有 MsgSend*() 的 “nc(非取消点)” 变体的消息,那么当 server 响应时,线程将被放置在就绪队列的前面,而不是最后。如果调度策略是循环的,线程的时间片不会被补满;例如,如果线程在发送之前已经使用了一半的时间片,那么在有资格获得抢占之前,它仍然只剩下一半的时间片。

1.4.2. 调度策略

为了满足各种应用的需求,QNX Neutrino RTOS提供了以下调度算法:

  • FIFO scheduling(先进先出调度)
  • round-robin scheduling(循环调度)
  • sporadic scheduling(零星调度,也称为偶发调度)

系统中的每个线程都可以使用任何方法运行。这些方法在每个线程的基础上有效,而不是在节点上的所有线程和进程的全局基础上有效。(原文:Each thread in the system may run using any method. The methods are effective on a per-thread basis, not on a global basis for all threads and processes on a node.)

请记住,FIFO和RR调度策略,仅适用于两个或多个具有相同优先级的线程就绪(即,线程之间直接相互竞争)。然而,偶发调度/零星调度方式,为线程的执行使用了一个“预算”。在所有情况下,如果高优先级线程变为READY,它会立即抢占所有低优先级线程。

@>> 阅读总结:

不同优先级的线程还是遵循抢占式的调度策略,这是大前提!

在下面的图中,三个具有相同优先级的线程是READY。如果线程A阻塞,线程B将运行。

线程A阻塞,线程B运行
线程A阻塞,线程B运行

虽然线程从其父进程继承调度策略,但线程可以请求更改内核应用的算法。

1.4.2.1. FIFO scheduling(先进先出调度)

同一优先级的线程会按照它们进入就绪状态(READY)的顺序执行,直到它们完成或遇到更高优先级的线程需要执行。这意味着一个线程可以持续执行,直到它完成或被更高优先级的线程抢占。

放弃CPU执行的条件:

  1. 自愿放弃CPU控制(例如,等待资源或让步);
  2. 被更高优先级线程抢占;

1.4.2.2. round-robin scheduling(循环调度)

首先会考虑线程的优先级。对于同一优先级的多个线程,系统会采用轮询的方式轮流调度它们,确保不会有一个线程长时间独占CPU资源。

时间片是分配给每个线程的时间单位。一旦它消耗了它的时间片,一个线程就会被抢占,下一个具有相同优先级的READY线程就会获得控制权。时间片是时钟周期的4倍。

放弃CPU执行的条件:

  1. 自愿放弃CPU控制(例如,等待资源或让步);
  2. 被更高优先级线程抢占;
  3. 其时间片被消耗完;

如下图所示,线程A一直运行,直到消耗掉它的时间片;下一个就绪线程(线程B)现在运行:

时间片是分配给每个线程的时间单位。一旦它消耗了它的时间片,一个线程就会被抢占,下一个具有相同优先级的READY线程就会获得控制权。时间片是时钟周期的 4 倍。(有关更多信息,请参阅C库参考中的 ClockPeriod()条目。)

除了时间切片之外,循环调度与FIFO调度相同。

1.4.2.3. sporadic scheduling(零星调度)

在SCHED_SPORADIC调度策略中,QNX采用了三个新的概念:Initial budget(初始预算)、Low priority(低优先级)和replenishment period(补充周期)。当一个线程开始执行时,它会被分配一个初始的时间预算(Initial budget)。如果线程在这个预算时间内没有完成,它的优先级会被降低到Low priority,直到下一个replenishment period(补充周期)到来,此时它会获得一个新的Initial budget,并有机会再次执行。

零星调度策略,通常用于在给定时间段内对线程的执行时间提供上限时间限制。

对于同时服务于周期性事件和非周期性服务事件的系统,当在该系统上执行Rate Monotonic Analysis (RMA)时,这种行为是必不可少的。从本质上讲,该算法允许线程为非周期性事件提供服务,而不会危及系统中其他线程或进程的硬性要求的deadline时间。

在FIFO调度中,使用零星调度的线程继续执行,直到它被阻塞或被更高优先级的线程抢占。与自适应调度(Adaptive Scheduling)一样,使用零星调度的线程的优先级会下降,但是使用零星调度,你可以更精确地控制线程的行为。

在零星调度下,线程的优先级可以在前台优先级(正常优先级)与后台(低优先级)之间动态切换。使用以下参数,可以控制这种“零星”切换的条件:

  • Initial budget (C),初步预算

该参数表示允许线程以正常优先级(N)执行的时间,当该时间消耗完时,将降低到低优先级(L)。

  • Low priority (L)低优先级

该参数表示线程将下降后的优先级级别。线程在后台以低优先级(L)执行,在前台以正常优先级(N)运行。

  • Replenishment period (T),补货周期

该参数表示一个时间段,在该时间段内允许线程消耗其执行预算(以优先级N运行,消耗其执行预算,当执行预算消耗殆尽,其优先级降低为L,指导当前补货周期结束才能再次补充填满执行预算)。为了调度补货操作,POSIX 实现也使用这个值作为线程变为READY时的偏移量。

@>> 阅读扩展:

POSIX规定的零星调度(Sporadic Scheduling)是一种用于处理非周期性事件和零星事件的调度策略。零星事件调度基本上有两种方法:基于空闲时间和基于服务器:

  • 基于空闲时间的零星调度

这种方法利用系统的空闲时间来执行零星任务。当系统没有其他高优先级的任务需要处理时,会执行等待中的零星任务。这种方法比较简单,但可能无法确保零星任务在特定的时间限制内完成。

  • 基于服务器的零星调度

基于服务器的零星调度使用一个专门的服务器(通常是一个周期任务)来处理零星事件。服务器和其他周期任务一起按照系统应用的调度策略被调度。零星任务被提交给服务器,服务器在其执行周期内尽可能多地处理这些任务。如果当前周期内零星服务器的执行预算已经耗尽,系统则将其优先级降为最低,以确保系统中的重要任务能够满足其截止期限要求,从而避免“过载”。

  • Max number of pending replenishments,等待补货的最大数量

该值限制了可以发生的补货操作的数量,从而限制了零星调度策略所消耗的系统开销。

在配置不佳的系统中,线程的执行预算可能会因为阻塞过多而受到侵蚀——也就是说,它无法获得足够的补充。


如下图所示,零星调度策略建立了线程的初始执行预算(C),该预算在线程运行时消耗,并进行周期性补货(补货周期为T)。当有线程阻塞时,在线程首次就绪运行的时候,已消耗的执行预算(R)被安排在稍后的某个时间进行补充填满(例如,在40毫秒后)。

在补货周期补充执行预算
在补货周期补充执行预算

在其正常优先级N下,线程将执行初始执行预算(C)所定义的时间长度。一旦这个时间被耗尽,线程的优先级将会下降到低优先级(L),直到补货操作发生。

此时线程将下降到低优先级(后台)级别,根据系统中其他线程的优先级,它可能有机会运行,也可能没有机会运行。

一旦完成“补货”,线程的优先级就会提升到原来的级别。这保证了在一个正确配置的系统中,线程将有机会在每个周期T中运行最大执行时间C。这确保了如果存在其他优先级高于L优先级的就绪线程,那么以N优先级运行的线程将只消耗系统资源的C/T的比例。

由于计算的原因,QNX Neutrino实现的零星调度器与POSIX零星调度器的不同之处在于,该算法允许每个零星服务器线程最多有一个待处理的“补货”。在每次补货时,零星服务器线程的可用预算被提升到初始预算。

1.4.2.4. 操纵优先级和调度策略

线程的优先级在执行过程中可能会发生变化,可能是由线程本身直接操作,也可能是内核在从更高优先级的线程接收消息时调整线程的优先级。

除了优先级,你还可以选择内核将为线程使用的调度算法。尽管我们的库提供了许多不同的方法来获取和设置调度参数,但你最好的选择是 pthread_getschedparam()pthread_setschedparam()pthread_setschedprio()。有关其他选择的信息,请参阅QNX Neutrino Programmer's Guide的Programming Overview章节中的“Scheduling policies”。

1.4.3. IPC问题

由于进程中的所有线程都可以不受阻碍地访问共享的数据空间,这种执行模型难道不会“轻而易举”地解决我们所有的IPC问题吗?难道我们不能仅仅通过共享内存而不需要任何其他的执行模型和IPC机制来通信数据吗?

要是有那么简单就好了!

一个问题是,各个线程对公共数据的访问必须是同步的。让线程读取到不一致的数据(因为另一个线程正在修改数据),这是一种灾难。例如,如果一个线程正在更新链表,那么在首个线程完成之前,不允许其他线程遍历或修改链表。必须以这种方式“串行”执行的代码段(即一次只由一个线程执行)称为“临界段”。除非有某种同步机制确保串行访问,否则程序将会失败(间歇性地,取决于“碰撞”发生的频率),导致链路不可修复地损坏。

互斥锁(Mutex)、信号量(Semaphore)和条件变量(Condvar)都是可以用来解决这个问题的同步工具的例子。这些工具将在本节后面介绍。

虽然可以使用同步服务来允许线程进行协作,但是共享内存本身不能解决许多IPC问题。例如,虽然线程可以通过公共数据空间进行通信,但这只有在所有通信的线程都在单个进程内时才有效。如果我们的应用程序需要与数据库服务器通信查询,该怎么办?我们需要将查询的详细信息传递给数据库服务器,但是我们需要与之通信的线程位于数据库服务器进程中,并且该服务器的地址空间对我们来说是不可寻址的。

操作系统负责网络分布式IPC问题,因为有一个接口(消息传递)在本地和网络远程情况下都可以操作,并且可以用于访问所有操作系统服务。由于可以精确地确定消息的大小,并且由于大多数消息往往非常小(例如,写请求上的错误状态,或很小的读请求),因此通过消息传递在网络上移动的数据可能比使用网络分布式共享内存要少得多,后者往往会复制4K的page页。

1.4.4. 线程复杂性问题

尽管线程非常适合某些系统设计,但重要的是要尊重使用它们所表现出的复杂性。

在某种程度上,具有讽刺意味的是,虽然受mmu保护的多任务处理已经变得很普遍,但“计算时尚”却流行起了在未受保护的地址空间中使用多线程。这不仅使调试变得困难,而且还妨碍了可靠代码的生成。

线程最初是作为一种“轻量级”并发机制引入UNIX系统的,用于解决“重量级”进程之间上下文切换缓慢的问题。尽管这是一个值得实现的目标,但一个明显的问题出现了:为什么进程到进程的上下文切换首先就是很慢?

在体系结构上,操作系统首先解决上下文切换性能问题。实际上,线程和进程提供了几乎相同的上下文切换性能数字指标。QNX Neutrino RTOS的进程切换时间比UNIX线程切换时间都要快。因此,不需要使用QNX Neutrino线程来解决IPC性能问题;相反,它们是在应用程序和 Server 进程中实现更高并发性的工具。

在不使用线程的情况下,快速的进程到进程的上下文切换,使得把应用程序构建为一组协作进程是合理的,协作进程间能够共享“显式分配共享内存区域 (explicitly allocated shared-memory region)“。因此,应用程序只有在这些漏洞(bugs)会对共享内存区域的内容产生影响时,才会将自己暴露给协作进程中的漏洞(bugs)。进程的私有内存仍然不受其他进程的影响。在纯线程模型中,所有线程(包括它们的堆栈)的私有数据都是可以公开访问的,在进程中的任何线程中都容易出现指针错误。

然而,线程还可以提供纯进程模型无法解决的并发性优势。例如,代表许多客户端(client)执行请求的文件系统 Server 进程(其中每个请求需要花费大量时间才能完成)肯定会从多线程的执行中获益。如果一个客户端(client)进程从磁盘请求一个块,而另一个客户端(client)请求已经在缓存中的块,那么文件系统进程可以利用线程池并发地为这些请求提供服务,而不是保持“忙碌(busy)”状态,直到第一个请求读取磁盘块完成。

当请求到达时,每个线程可以直接从缓冲区进行响应,也可以阻塞并等待磁盘I/O,而不会增加其他客户端进程(clientthread)可感知到的响应延迟。文件系统 Server 可以“预先创建”一组线程,以便在客户端(Client)请求到达时依次进行响应。尽管这会使文件系统管理器的体系结构复杂化,但并发性方面的收益是显著的。


未完,请继续看下一篇:《 2-2-18-2 QNX Neutrino 微内核(二)》。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星原飞火

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值