QNX Neutrino微內核procnto
實現了嵌入式實時系統中常用的核心POSIX功能,並提供了基本的 QNX Neutrino 消息傳遞服務。但是未實現POSIX的功能(例如filesystem I/O and device I/O)則可以通過可選的進程和共享庫來提供。
Tips ->
procnto
系統進程包含:微內核,進程管理,內存管理和路徑名管理。procnto* 用法
微內核包含了一些基本對象以及操作這些對象的例程,這些對象定義得很具體,而且高度可重用,整個操作系統在此之上構建的。
圖 1:微內核
系統服務
QNX Neutrino微內核提供了一系列系統調用來支持以下服務:
- 線程(threads)
- 消息傳遞(message passing)
- 信號(signals)
- 時鐘(clocks)
- 定時器(timers)
- 中斷處理(interrupt handlers)
- 信號量(semaphores)
- 互斥鎖 mutual exclusion locks(mutexes)
- 條件變量 condition variables (condvars)
- 屏障 (barriers)
整個系統都是基於這些系統調用來構建的,QNX完全可搶佔,甚至在消息傳遞的過程時也能被搶佔,並在搶佔完成後恢復之前的消息傳遞狀態。
整個系統都是基於這些系統調用來構建的,QNX完全可搶佔,甚至在消息傳遞的過程時也能被搶佔,並在搶佔完成後恢復之前的消息傳遞狀態。微內核實現越簡單,越有利於減少不可搶佔區間的長度,同時,代碼量少,讓解決複雜的多處理器問題也變得簡單。將系統服務包含進內核的前提是,系統服務只有一個短的執行路徑長度。需要執行很多工作的操作,可以交給外部的進程或線程去做。嚴格按照上邊的規則來劃分內核和外部進程功能的話,微內核的運行時負載不見得就高於單內核。簡單內核的上下文切換的時間非常快,相比於在進程間通過消息傳遞來服務請求的時間,上下文的切換開銷微不足道。
下圖演示了在非對稱多處理器內核(X86實現)搶佔的細節,其中,中斷禁用或禁止搶佔的時間非常短,通常爲幾百納秒。
線程與進程
在開發應用程序時(實時、嵌入式、圖形等),通常會用到POSIX線程模型來實現多個算法同時執行。線程是微內核中最小的執行和調度單元,進程可以認爲是線程的“容器”,定義了線程將在其中執行的“地址空間”,進程會包含一個或多個線程。應用程序中的線程有可能相互獨立,也可能緊密的聯繫,QNX Neutrino提供了豐富的IPC和同步服務。
其中不涉及微內核線程調用的POSIX 線程庫接口pthread_*有如下:
- pthread_attr_destroy()
- pthread_attr_getdetachstate()
- pthread_attr_getinheritsched()
- pthread_attr_getschedparam()
- pthread_attr_getschedpolicy()
- pthread_attr_getscope()
- pthread_attr_getstackaddr()
- pthread_attr_getstacksize()
- pthread_attr_init()
- pthread_attr_setdetachstate()
- pthread_attr_setinheritsched()
- pthread_attr_setschedparam()
- pthread_attr_setschedpolicy()
- pthread_attr_setscope()
- pthread_attr_setstackaddr()
- pthread_attr_setstacksize()
- pthread_cleanup_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 |
建議:鑑於代碼可移植性和擴展性使用POSIX 接口編程。
可以將 OS 配置爲提供線程和進程的混合(由 POSIX 定義)。每個進程都相互受到MMU 保護,每個進程可能包含一個或多個共享進程地址空間的線程。
線程屬性
儘管進程中的線程共享進程地址空間中的所有內容,但每個線程仍然有一些“私有”數據,在某些情況下,這些私有數據在內核中受到保護,比如線程ID/進程ID;而其他的可能不受保護,比如線程的堆棧。 值得注意的私有數據有:
- tid,線程ID,每個線程在進程中都有唯一的ID,從1開始;
- priority,每個線程都有一個調度的優先級,線程的初始優先級是繼承而來,並且可以根據調度策略進行改變,進程沒有優先級;
- name,線程名字,可以通過
pthread_getname_np()
和pthread_setname_np()
來獲取和設置;- Register set,每個線程都有IP、SP以及處理器相關的寄存器上下文;
- Stack,線程在自己的堆棧上執行,存儲在其進程的地址空間中;
- Signal mask,信號掩碼;
- Thread local storage,線程本地存儲TLS,用於存儲線程私有的數據,用戶不需要直接訪問TLS,線程可以通過線程特定的key與用戶自定義數據進行綁定,用到的接口有
pthread_key_create()
,pthread_key_delete()
,pthread_setspecific()
,pthread_getspecfic()
.其中線程對應的key和線程ID是通過稀疏矩陣來映射的。- Cancellation handlers,線程終止時執行的回調函數;
線程生命週期
線程是動態創建的,線程創建(pthread_create())時涉及到資源分配和初始化,線程銷燬(pthread_exit(), pthread_cancel())時涉及到資源回收,當線程執行時,它的狀態通常描述爲“就緒”或“阻塞”,具體來說有以下狀態:
CONDVAR,阻塞在條件變量上,比如調用
pthread_cond_wait()
;DEAD,線程終止了,等待被其他線程join;
INTERRUPT,阻塞在等待中斷上,比如調用
InterruptWait()
;JOIN,線程阻塞在join另一個線程,比如調用
pthread_join()
;MUTEX,線程阻塞在互斥鎖上,比如調用
pthread_mutex_lock()
;NANOSLEEP,線程休眠很短的時間,比如調用
nanosleep()
;NET_REPLY,線程正在等待通過網絡傳遞迴復,比如調用
MsgReply*()
;NET_SEND,線程正在等待通過網絡發送脈衝或信號,比如調用
MsgSendPulse()
,MsgDeliverEvent(),
SignalKill()`等;READY,線程等待執行,此時處理器可能正在執行同級或更高優先級的線程;
RECEIVE,線程阻塞在接收消息上,比如調用
MsgReceive()
;REPLY,線程阻塞在消息回覆上,比如調用
MsgSend()
;RUNNING,線程正在執行,內核會使用一個數組(每個CPU上有一個入口)來跟蹤記錄所有運行的線程;
SEM,線程正在等待信號量的釋放,比如調用
SyncSemWait()
;SEND,線程阻塞在信息發送上,比如調用
MsgSend()
,但服務器還沒收到消息;SIGSUSPEND,線程阻塞在等待一個信號上,比如調用
sigsuspend()
;SIGWAITINFO,線程阻塞在等待一個信號之上,比如調用
sigwaitinfo()
;STACK,線程正在等待虛擬堆棧地址空間分配,父進程調用
ThreadCreate()
;STOPPED,線程阻塞在等待
SIGCONT
信號;WAITCTX,線程在等待非整數上下文變得可用,比如浮點運算;
WAITPAGE,線程等待爲虛擬地址分配物理地址;
WAITTHREAD,線程等待子線程完成自我創建,比如調用
ThreadCreate()
;
線程調度
當執行內核調用、異常、硬件中斷時,當前的執行線程會被掛起,每當任何線程的執行狀態發生改變時,都會做出調度決策。通常被掛起的線程將會被恢復,這時線程調度器將進行一次上下文切換。 有三種情況會發生上下文切換:
阻塞(blocks),當線程需要等待某些事件的發生時(比如響應IPC請求、等待互斥鎖等),就會阻塞等待。線程被阻塞時,會從運行隊列中移除,解阻塞時會移動到同優先級就緒隊列的尾部中。
搶佔(is preempted),高優先級線程會搶佔低優先線程;
主動讓出CPU(yields),比如調用
sched_yield()
等;
調度優先級 (Scheduling priority)
每個線程都被分配了一個優先級。 線程調度器通過查看分配給每個就緒線程(即能夠使用 CPU)的優先級來選擇下一個要運行的線程。QNX Neutrino支持256級優先級,non-root線程可以將優先級設置爲1-63,與調度策略無關,root線程(有效uid爲0),能將優先級設置爲63之上。特殊空閒(Idle)線程(在進程管理器中)優先級爲 0 且始終爲 0 準備好了。默認情況下,線程繼承其父線程的優先級。通常會採用優先級繼承來應對優先級反轉的問題。
下圖中描述了一個就緒隊列中,B-F是就緒,G-Z是阻塞,A正在運行:
您可以使用 procnto -P
選項更改非特權進程的允許優先級範圍:
procnto -P priority
procnto-smp-instr -P priority
調度策略(Scheduling policies)
爲了滿足各種應用場景需求,QNX Neutrino提供了三種調度策略:
- FIFO調度(FIFO scheduling)
- 循環調度(round-robin scheduling)
- 零星調度(sporadic scheduling)
請記住,FIFO 和循環調度策略僅在共享相同優先級的兩個或多個線程爲 READY 時才適用(即,線程直接相互競爭)。然而,零星方法對線程的執行採用“預算”。在所有情況下,如果優先級較高的線程變爲READY,它會立即搶佔所有優先級較低的線程。
在下圖中,三個具有相同優先級的線程是 READY。如果線程 A 阻塞,則線程 B 將運行。
雖然線程從其父進程繼承其調度策略,但線程可以請求更改內核應用的算法。
1.FIFO調度
在FIFO調度下,線程會在兩種情況下放棄執行:1)主動放棄CPU;2)高優先級線程搶佔;
2.循環調度
在Round-Robin調度下,線程會在三種情況下放棄執行:1)主動放棄CPU;2)高優先級線程搶佔;3)時間片消耗完畢;
如下圖所示,線程 A 一直運行直到它消耗了它的時間片;現在運行下一個 READY 線程(線程 B):
時間片爲4倍時鐘週期。與FIFO調度不同的是多了一個時間片的控制。
3.零星調度
Sporadic調度策略通常用於在給定時間段內提供線程執行時間的上限,Sporadic調度會爲線程執行提供“預算”。與FIFO調度一樣,在阻塞或被搶佔的情況下會放棄執行。Sporadic調度會自動降低線程的優先級,可以更精確的控制線程的行爲。 Sporadic調度時,線程優先級會在前臺正常優先級N和後臺低優先級L之間動態調整,通過使用下列參數控制調度條件:
Initial budget(C),線程從正常優先級調整到低優先級前,允許的執行時間;
Low priority(L),線程降到的優先級,線程在後臺以L優先級運行,在前臺以N優先級運行;
Replenishment period(T),允許線程消耗執行預算的時間段,對於Replenishment操作,POSIX實現時採用這個值作爲線程變爲Ready狀態的時間段。
Max number of pending replenishment,Replenishment最大次數,決定了Sporadic調度策略的最大系統負載上限。
下圖所示,Sporadic調度策略建立了線程的初始化執行budget(預算),線程執行時會消耗這個budget,但這個值會週期性重複填滿。
在正常優先級N時,線程會執行budget時間C,當時間耗盡後,線程的優先級會調整至L。當Replenishment發生後又將恢復到原來的優先級,在一個T的時間週期內,線程將會有機會最大去執行C的運行時間,也能保證一個線程在N優先級的情況下只消耗C/T比例的系統資源。假設在一個系統中,線程不會被阻塞或搶佔,運行情況如下圖所示:
關於優先級和調度策略的設置,有以下接口來實現:
sched_getparam()/SchedGet()
sched_setparam()/SchedSet()
sched_getscheduler()/SchedGet()
sched_setscheduler()/SchedSet()
同步服務
QNX Neutrino提供POSIX標準線程級別的同步原語:
Synchronization service | Supported between processes | Supported across a QNX Neutrino LAN |
---|---|---|
Mutexes | Yes(a) | No |
Condvars | Yes | No |
Barriers | Yes(a) | No |
Sleepon locks | No | No |
Reader/writer locks | Yes(a) | No |
Semaphores | Yes | Yes (named only) |
FIFO scheduling | Yes | No |
Send/Receive/Reply | Yes | Yes |
Atomic operations | Yes | No |
上述同步機制中,大部分都是由內核直接實現,除了以下幾種:
- 屏障(barriers)、睡眠鎖(sleepon locks)、讀寫鎖(sleepon locks),這些是基於條件變量和互斥鎖實現的;
- 原子操作(atomic operations),由處理器提供,或者在內核中模擬實現;
1. Mutex
互斥鎖是最簡單的同步服務,用於對臨界區的互斥訪問,通常會用phtread_mutext_lock()
/pthread_mutex_timedlock()
來獲取鎖,使用phread_mutext_unlock()
來釋放鎖,當獲取不到鎖的時候線程會阻塞等待,也可以使用非阻塞函數pthread_mutex_trylock()
來測試Mutex
是否已經被鎖。
2. Condvars
條件變量用於在臨界區來阻塞線程,直到滿足某些條件,這些條件可以是任意複雜的,並且與Condvar
無關。Condvar
必須始終與Mutex
一起使用。 條件變量支持三種操作:
- 等待,
pthread_cond_wait()
- 發出信號,
pthread_cond_signal()
- 廣播,
pthread_cond_broadcast()
3. Barriers
屏障是一種同步機制,可以用於將多個協作線程阻塞在某個點等待,直到所有線程都完成後纔可以繼續。pthread_join()
函數是用於等待線程終止,而屏障是用於等待多個線程在某個點“集合”,當線程都達到後,就可以取消阻塞所有線程並繼續運行了。有以下接口:
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 |
4. Sleepon locks
Sleepon locks
與Condvar
很像,也都是等待條件的滿足,不同的是Condvars
必須爲每個檢查的condition
分配Mutex
,而Sleepon locks
可以複用一個Mutex
。
5. Reader/writer locks
讀寫鎖通常用於“多個讀取者,單個寫入者”場景的同步,它的開銷遠大於Mutex
,但是在這種數據訪問模式下很有用。通常使用pthread_rwlock_rdlock()
/pthread_rwlock_wrlock()
/pthread_rwlock_unlock()
接口。
6. Semaphores
信號量是常用的同步形式,允許線程在一個信號量上post
和wait
來控制線程何時喚醒和休眠。信號量與其他同步原語的一個顯著區別是信號量是異步安全的,可以由信號處理程序操作。如果想讓一個信號處理程序喚醒一個線程,信號量是正確的選擇。 對於單個進程中的線程之間同步,互斥鎖比信號量更有效。
7. 通過調度策略來同步
可以使用FIFO調度策略來保證同一優先級的線程不會在非SMP系統中併發運行。
8. 通過消息傳遞來同步
Send/Receive/Reply
IPC消息傳遞天然就是一種同步機制,在很多情況下讓其他同步機制變得不必要。它們也是唯一可以跨網絡使用的同步和IPC原語。
9. 通過原子操作來同步
QNX Neutrino提供以下原子操作:
adding a value
subtracting a value
clearing bits
setting bits
toggling (complementing) bits 可以在任何地方使用原子操作,但原子操作在以下兩種情況下非常適用:
ISR和線程之間,ISR可以在任何時間點搶佔線程,線程保護自己不被ISR打擾的唯一方法就是禁用中斷,在實時系統中不建議禁用中斷,推薦使用QNX提供的原子操作;
兩個線程之間,在SMP系統中,線程能做到真正併發,使用原子操作來進行保護;
下表列出了各種微內核調用以及從它們構造的更高級別的 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 |
時鐘和定時器服務
時鐘服務用於維護系統時間,同時內核也會使用時間服務來完成定時器操作。 時鐘相關的接口如下:
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() | Get a clock ID for a process or thread CPU-time clock |
QNX提供了POSIX定時器所有功能函數集,定時器模型很豐富,有以下幾種timer類型:
- 絕對日期
- 相對日期
- 週期性的
週期模式非常重要,因爲定時器常用來作爲事件週期性來源,觸發某些線程進行處理,處理完後睡眠,直到下一個事件觸發。定時器是OS中的另外一個事件源,所有定時器都能用作時間分發系統。應用請求在定時器超時後,系統發送QNX支持的任意事件。 定時器有以下接口:
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 |
中斷處理
在實時系統中,減少不必要的CPU cycles是至關重要的,需要關注兩個latency:中斷latency,調度latency。
中斷latency
中斷latency指的是從硬件中斷觸發到執行驅動程序中中斷處理函數的第一條指令之間的時間間隔。在QNX中,一直維持中斷使能,但在某些特殊的代碼中需要關閉中斷,可能會造成大的延遲,在QNX中這個時間很短。
調度latency
調度latency指的是從中斷處理函數中返回後到驅動線程第一條指令執行的時間間隔,通常包括保存當前執行上下文,加載驅動線程上下文。儘管這個時間大於中斷latency,但是QNX中調度延遲仍然很小。
中斷嵌套
QNX支持中斷嵌套,中斷嵌套時序比較複雜,考慮以下這種情況:進程A在運行,中斷IRQx觸發Intx運行,在處理時又被IRQy搶佔觸發Inty運行,Inty返回一個事件導致線程B運行,Intx返回一個事件導致線程C運行,如下圖:
中斷調用接口
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. |