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. |