1 實時操作系統概述
1.1 實時操作系統
在計算的早期開發的操作系統的最原始的結構形式是一個統一的實體(monolithic)。在這樣的系統中,提供的不同功能的模塊,如處理器管理、內存管理、輸入輸出等,通常是獨立的。他們在執行過程中並不考慮其他正在使用中的模塊,各個模塊都以相同的時間粒度運行,即嚴格按時間片分時方式運行。由於現代實時環境需要許多不同的功能,以及在這樣的環境中存在的並發活動所引起的異步性和非確定性,操作系統變得更加復雜。所以早期操作系統的統一結構的組織已經被更加精確的內部結構所淘汰。
操作系統的最好的內部結構模型是一個層次性的結構,最低層是內核。這些層次可以看成為一個倒置的金字塔,每一層都建立在較低層的功能之上。 內核僅包含一個操作系統執行的最重要的低層功能。正像一個統一結構的操作系統,內核提供了在高層軟件與下層硬件之間的抽象層。然而,內核僅提供了構造操作系統其他部分所需的最小操作集。
一個實時操作系統內核需滿足許多特定的實時環境所提出的基本要求,這些包括:
- 多任務:由於真實世界的事件的異步性,能夠運行許多並發進程或任務是很重要的。多任務提供了一個較好的對真實世界的匹配,因為它允許對應於許多外部事件的多線程執行。系統內核分配CPU給這些任務來獲得並發性。
- 搶占調度:真實世界的事件具有繼承的優先級,在分配CPU的時候要注意到這些優先級。基於優先級的搶占調度,任務都被指定了優先級, 在能夠執行的任務(沒有被掛起或正在等待資源)中,優先級最高的任務被分配CPU資源。換句話說,當一個高優先級的任務變為可執行態,它會立即搶占當前正在運行的較低優先級的任務。
- 快速靈活的任務間的通信與同步:在一個實時系統中,可能有許多任務作為一個應用的一部分執行。系統必須提供這些任務間的快速且功能強大的通信機制。內核也要提供為了有效地共享不可搶占的資源或臨界區所需的同步、互斥機制。
- 方便的任務與中斷之間的通信:盡管真實世界的事件通常作為中斷方式到來,但為了提供有效的排隊、優先化和減少中斷延時,我們通常希望在任務級處理相應的工作。所以需要雜任務級和中斷級之間存在通信。
- 性能邊界:一個實時內核必須提供最壞情況的性能優化,而非針對吞吐量的性能優化。我們更期望一個系統能夠始終以50微妙執行一個函數,而不期望系統平均以10微妙執行該函數,但偶爾會以75微妙執行它。
特殊考慮:由於對實時內核的要求的增加,必須考慮對內核支持不斷增加的復雜功能的要求。這包括多進程處理,Ada和對更新的、功能更強的處理器結構如RISC的支持。許多商用化的內核支持的功能遠強於上面所列的要求。在這方面,他們不是真正的內核,而更象一個小的統一結構的操作系統。因為他們包含簡單的內存分配、時鍾管理、甚至一些輸入輸出系統調用的功能。
1.2 基本概念
- 任務(TASK):任務是由計算機所執行的一項活動,它包括一個程序和於這個程序有關的數據、程序狀態及計算機資源等。
- 進程(PROCESS):在UNIX操作系統里,任務被稱作進程。任務和進程都可以作為調度單位被內核調度執行,但在一些既有任務又有進程的系統里任務和進程是有區別的,主要表現在:任務可以直接對內層進行尋址,而進程不能,進程只能從其父進程里繼承一些特定的屬性,而任務卻能和父任務在幾乎相同的環境下運行。
- 任務的狀態:包括運行、就緒、掛起、休眠等在內的任務所處的狀態。
- 任務的調度:負責控制各個任務在各個任務狀態之間的轉換。
- 任務的控制塊(TCB):任務控制塊用來描述一個任務,每一個任務都與一個TCB相關聯。TCB包括了任務的當前狀態、優先級、要等待的事件或資源、任務程序碼的起始地址、初始堆棧指針等信息。此外,TCB還被用來存放任務的“上下文”(CONTEXT)。
- 任務的同步:對單個任務而言,同步就是使它能在指定的時間執行,對兩個或兩個以上的任務,同步是任務間需要協調執行的情況。
- 消息:消息機制使用一個被各有關進程共享的消息隊列,進程之間經由這個消息隊列發送和接收消息,並用來協調(同步)對共享資源的使用的方法。
- 管道(PIPE):是一個類似文件的結構,它用於同步讀/寫操作,使用系統調用PIPE可以產生一個管道並返回兩個文件指針,一個用於讀管道,一個用於寫管道。
- 中斷(Interrupt)和中斷處理程序(Interrupt Service Routine):中斷一般是指硬件中斷,它用於通知操作系統特定外部事件的發生,中斷響應速度對於實時系統是至關重要的。可以使用intConnect()函數把特定的ISR與某一中斷聯系起來。
- 信號(SIGNAL):它用來通知一個任務特定事情的發生,並立即調用相應的信號處理函數進行處理,它異步的改變任務的控制流程。信號與中斷類似,每個信號通常需要和特定的信號處理程序綁定起來(調用sigaction()或signal())。信號通常用於錯誤或異常處理,用於通知一些硬件中斷錯誤,如:總線錯誤、非法指令、浮點異常、被零除等。它也可以用於進程間通訊。
- 信號量(semaphore):很多方面類似一個全局變量,不同的是,對信號量的操作(Creat、Delete、Take、Give)都是特定的原子操作,是不可中斷的。信號量主要用於進程間的同步和對共享資源的互斥訪問。
2 VxWorks的系統綜述
2.1 VxWorks
VxWorks 是美國 Wind River System 公司( 以下簡稱風河 公司 ,即 WRS 公司)推出的一個實時操作系統。Tornado 是WRS 公司推出的一套實時操作系統開發環境,類似Microsoft Visual C,但是提供了更豐富的調試、防真環境和工具。(我這里用的是Workbench)
VxWorks操作系統有以下部件組成:
l 內核(wind):
- 多任務調度(采用基於優先級搶占方式,同時支持同優先級任務間的分時間片調度)
- 任務間的同步
- 進程間通信機制
- 中斷處理
- 定時器和內存管理機制
l I/O 系統
VxWorks 提供了一個快速靈活的與 ANSI C 兼容的 I/O 系統,包括 UNIX 標准的Basic I/O(creat(), remove(), open(),close(), read(), write(), and ioctl().),Buffer I/O (fopen(), fclose(), fread(), fwrite(), getc(), putc()) 以及POSIX 標准的異步 I/O。VxWorks 包括以下驅動程序:網絡驅動、管道驅動、RAM盤驅動、SCSI驅動、鍵盤驅動、顯示驅動、磁盤驅動、並口驅動等
l 文件系統
支持四種文件系統: dosFs,rt11Fs,rawFs 和 tapeFs
支持在一個單獨的 VxWorks 系統上同時並存幾個不同的文件系統。
l 板級支持包 BSP(Board Support Package)
板級支持包向VxWorks操作系統提供了對各種板子的硬件功能操作的統一的軟件接口,它是保證VxWorks操作系統可移植性的關鍵,它包括硬件初始化、中斷的產生和處理、硬件時鍾和計時器管理、局域和總線內存地址映射、內存分配等等。 每個板級支持包括一個 ROM 啟動(Boot ROM)或其它啟動機制。
l 網絡支持:
它提供了對其它VxWorks系統和TCP/IP 網絡系統的"透明"訪問,包括與BSD套接字兼容的編程接口,遠程過程調用(RPC),SNMP(可選項),遠程文件訪問(包括客戶端和服務端的NFS機制以及使用RSH,FTP 或 TFTP的非NFS機制)以及BOOTP 和代理ARP、DHCP、DNS、OSPF、RIP。無論是松耦合的串行線路、標准的以太網連接還是緊耦合的利用共享內存的背板總線,所有的 VxWorks 網絡機制都遵循標准的 Internet 協議。
l 系列網絡產品:
- WindNet SNMP
- WindNet STREAMS
- WindNet 第三方產品,包括 OSI、SS7、ATM、Frame Relay、CORBA、ISDN、X.25、
- CMIP/GDMO、分布式網絡管理等。
這些產品擴展了VxWorks的網絡特性,並增強了嵌入式處理器的網絡特性
l 虛擬內存( VxVMI)與共享內存(VxMP)
- VxVMI 為帶有 MMU 的目標板提供了虛擬內存機制。
- VxMP 提供了共享信號量,消息隊列和在不同處理器之間的共享內存區域。
l 目標代理(Target Agent)
目標代理遵循 WBD(Wind Debug)協議,允許目標機與主機上的 Tornado 開發工具相連。在目標代理的缺省設置中,目標代理是以 VxWorks 的一個任務tWdbTask 的形式運行的。
Tornado 目標服務器(Target Server)向目標代理發送調試請求。調試請求通常決定目標代理對系統中其它任務的控制和處理。缺省狀態下,目標服務器與目標代理通過網絡進行通信,但是用戶也可以改變通信方式。
l 實用庫
VxWorks 提供了一個實用例程的擴展集,包括中斷處理、看門狗定時器、消息登錄、 內存分配、字符掃描、線緩沖和環緩沖管理、鏈表管理和 ANSI C 標准。
l 基於目標機的工具
在 Tornado 開發系統中,開發工具是駐留在主機上的。但是也可以根據需要將基於目標機的Shell 和裝載卸載模塊加入 VxWorks。
總之,VxWorks的系統結構是一個相當小的微內核的層次結構。內核僅提供多任務環境、進程間通信和同步功能。這些功能模塊足夠支持VxWorks在較高層次所提供的豐富的性能的要求。VxWorks與各部件關系如圖所示:
2.2 Tornado
Tornado組成:
- Tornado一整套交叉開發工具
- VxWorks 實時操作系統
- 連接目標機與宿主機的通信選項
Tornado所提供的工具:
- WindConfig: 建立適合用戶特性的 VxWorks 操作系統。
- Loader:具有動態連接的裝載器。
- CrossWind:源程序(C 或 C++ 以及匯編程序等)的調試工具。CrossWind結合了圖形方式和命令行方式的最大特點。最普通的調試方式,例如斷點設置和程序執行控制可以通過便捷的點擊方式實現。同樣,程序顯示框和數據偵察窗也提供了一個直接的可視窗口來觀察應用程序中最關鍵的一部分。
- Browser:可觀察系統對象(任務、消息隊列、信號量等)和存儲器使用情況;Browser匯總了應用進程,內存消耗和一個目標內存的映射。通過 Browser,可觀察信號量、 消息隊列、內存分配、看門狗計時器、堆棧使用情況、 目標CPU使用率、對象模塊結構和符號表以及每個任務的詳細信息。
- WindSh:提供從宿主機到目標機之間的一個命令shell,它可以解釋執行內層目標中的幾乎所有C語言函數或指令(it can interpret and execute almost all C-language expressions)。
- WindView:系統可視診斷和分析工具。可觀察各任務,中斷程序之間的相互作用。它是在嵌入式系統應用開發期間的可視工具。
- VxSim:快速原型仿真器。可在硬件設備未完成之前,在宿主機上對應用程序進行仿真分析。他可以在宿主機上模擬目標機的VxWorks操作系統環境,在許多方面他與目標機上的VxWorks系統能力是一樣的,他們的區別主要在於:在VxSim里,內存Image是作為宿主機(Windows or UNIX)的一個進程執行,沒有與目標硬件的交互,所以不適合於設備驅動的仿真。
- StethoScope:實時數據圖形監控器。收集數據,並將數據以圖形方法顯示出來。
- ObjectCenter:C++ 開發環境。選用的是 GNU C++ 編譯器,I/O 流庫支 持 C++ 中的格式化 I/O。
Tornado調試模式:
- 系統模式: 對整個應用系統進行調試,可在系統中設置斷點等。調試中應用系統必須停下 來
- 任務模式(動態調試): 調試是針對系統中某一任務模塊進行的,整個系統仍可保留在工作狀態。
本文中重點介紹了VxWorks操作系統的多任務內核、任務調度、任務間的通信和中斷處理機制等核心內容。
3 VxWorks操作系統基礎
3.1 VxWorks的任務
在VxWorks中,一般的應用被組織成相互獨立又相互協作的程序,每一個程序在運行時稱作一個任務。在VxWorks中,任務不僅可以共享和快速訪問大部分的系統資源,還保留着充分獨立的上下文以實現單獨的線程控制。
3.1.1 多任務
多任務為應用程序對多個離散的現實事件的控制和反應提供了基本的機制。VxWorks的實時內核wind提供了一個基本的多任務環境。內核按照一種調度算法交替運行各個任務,造成一種多個任務並行運行的假象,每一個任務都有自己的任務上下文。任務上下文是任務每次被調度運行時所能看到的CPU環境和系統資源。在一次上下文切換中,一個任務的上下文被存在任務控制塊(TCB)中。一個任務的上下文包括:
- 一個用於執行的線程,即任務的程序計數器
- CPU的寄存器和可選擇的浮點寄存器
- 用於動態變量和函數調用的堆棧
- 對標准的輸入、輸出、出錯的I/O口的分配
- 延時(休眠)時鍾
- 時間片時鍾
- 內核的控制結構
- 信號句柄
- 調試和性能監視參數
VxWorks的調度策略進行任務調度時,就是讓正在使用CPU執行的任務停下來,然后把CPU分配給另一個任務來執行。這個過程就叫做上下文切換。可以看到,上下文切換就是對兩個任務Context的讀寫操作。為了保證實時性,這個切換過程必須准確、快速。
任務是執行單元,組成有TCB和棧。多任務並發執行,上下文快速切換。
在VxWorks中,一個非常重要但並不是任務上下文的一項資源就是存儲器地址空間:所有代碼(任務)都在一個單一的公有的地址空間運行,這一點是區別於許多非實時操作系統的(UNIX, Windows)。
3.1.2 任務的狀態遷移
內核維護系統中的每個任務的當前狀態。狀態遷移發生在應用程序調用內核功能服務的時候。任務被創建以后進入掛起態,需要通過特定的操作使被創建的任務進入就緒態,這一操作執行速度很快,使應用程序能夠提前創建任務,並以一種快捷的方式激活該任務。在VxWorks中,任務有以下幾種狀態,並在這幾種狀態中切換。
- 就緒態(ready) ----一個任務當前除了CPU不等待任何資源
- 阻塞態(pend) ----一個任務由於某些資源不可獲得而被阻塞
- 延遲態(delay) ----一個任務睡眠一段時間
- 掛起態(suspend)----主要用於調試的一個輔助狀態,掛起只是禁止任務的執行, 但沒有禁止狀態遷移。因此處於pended-suspended狀態的任務仍然可以解阻塞(unblocked),處於delayed-suspended狀態的任務仍然可以蘇醒(awaken)。
另外還有其他一些輔助狀態:
- DELAY+S 任務被delayed+suspended
- PEND+S 任務被pended+suspended
- PEND+T 任務被pended with a timeout value
- PEND+S+T 任務被pended with a timeout value and suspended
- State+I 這個任務的狀態取決於當前的狀態(所列所有的狀態)再加一個繼承的優先級
還有一種特殊的狀態Stop,以及多種組合狀態:
- Stop 任務被Debugger或ED&R機制或SIGSTOP信號停下
- Stop+P 任務阻塞並被停下
- Stop+T 任務延時並被停下
- Stop+S 任務停下並被掛起
- STOP+P+T 任務阻塞並延時並被Debugger停下
- STOP+T+S 任務被掛起並延時並被Debugger停下
- ST+P+S+T 任務阻塞並延時並被掛起並被Debugger停下
狀態的轉換:
- ready pended semTake( )/msgQReceive( )
- ready delayed taskDelay( )
- ready suspended taskSuspended()
- pended ready semGive( )/msgQSend( )
- pended suspended taskSuspend( )
- delayed ready expied delay
- delayed suspended taskSuspend( )
- suspended ready taskResume( )/taskActive( )
- suspended pended taskResume( )
- suspended delayed taskResume( )
3.1.3 Wind內核的任務調度
那么當有了空閑的CPU時,到底是分配給哪個Ready的Task呢?這就涉及到系統的Scheduling(調度策略)了。多任務需要一個調度算法分配CPU給就緒的任務。在VxWorks中默認的調度算法是基於優先級的搶占調度,但應用程序也可以選擇使用時間片輪轉調度。不管哪種策略,都基於一個很重要的概念,即Task的Priority(優先級)。Task的優先級是在創建時指定的,而且后期也可以動態調整,不過不建議在應用中頻繁修改Task的優先級,因為可能帶來了調度的不確定性。優先級的取值范圍是0-255,其中0的優先級最高,255的優先級最低。具體每個Task的優先級取值是沒有限制的,只是(VxWorks567)建議應用Task的優先級不小於100,驅動Task的不大於99。任務的調度控制程序,調度控制函數:
- kernelTimeSlice( ) 控制輪轉調度
- taskPrioritySet( ) 改變一個任務的優先級
- taskLock( ) 禁止任務的重調度
- taskUnlock( ) 允許任務的重調度
優先級式的搶先調度
在Vxworks Image Project中對應的component是INCLUDE_VX_TRADITIONAL_SCHEDULER。基於優先級的搶占調度,每個任務被指定一個優先級,內核分配CPU給處於就緒態的優先級最高的任務。調度采用搶占的方式,是因為當一個優先級高於當前任務的任務變為就緒態時,內核將立即保存當前任務的上文,並切換到高優先級任務的上下文。在創建的時候任務被指定一個優先級,在任務運行的過程中可以動態地修改優先級以便跟蹤真實世界的事件優先級。外部中斷被指定優先於任何任務的優先級,這樣能夠在任何時候搶占一個任務。函數taskRotate()可以把一個Task從其List的頭部移到尾部。例如taskRotate(100)就是把優先級100的List的頭部的Task移到該List的尾部。
Kernel為每種狀態的Task都維護着一個隊列,其中Ready隊列又有多個List,相同優先級的Task處於同一個List。當某一優先級的Task可以執行時,都是該List最前端的Task來執行。Task在隊列中的位置可能發生變化,情景如下
- Task被其它高優先級的Task搶占后,還保持在其List的頭部
- Task退出Ready隊列(例如進入Pended、Delayed、Suspended等)后,又返回Ready隊列的,排在其原List的尾部
- Task的優先級被taskPrioritySet()修改后,排在新List的尾部
- Task的優先級被互斥信號量的繼承策略臨時提高后,又恢復原有優先級的,排在其原List的尾部
時間片輪轉式調度
基於優先級搶占調度可以擴充時間片輪轉調度。時間片輪轉調度允許在相同優先級的處於就緒態的任務公平地共享CPU。沒有時間片輪轉調度,當有多個任務在同一優先級共享處理器時,一個任務可能獨占CPU,不會被阻塞直到被一個更高優先級的任務搶占,而不給同一優先級的其他任務運行的機會。如果時間片輪轉被使能,執行任務的時間計數器在每個時鍾滴答遞增。當指定的時間片耗盡,計數器會被清零,該任務被放在同一優先級任務隊列的隊尾。加入特定優先級組的新任務被放在該組任務的隊尾,並將運行計數器初始化為零。要想打開時間片輪轉策略,只需要調用函數kernelTimeSlice(),其參數為0就表示關閉該策略,參數大於零時就表示打開策略並設置時間片的長度。
搶占鎖定
Wind的調度器可以顯式通過tasklock( )和taskUnlock( )對一個任務鎖定或允許搶占調度。當一個任務通過調用taskLock( )來鎖定搶占調度,在任務的運行期間就避免了高優先級的任務的搶占。如果打開了時間片輪轉,那么它的時間片計數也不會增加。但如果該任務阻塞或掛起,內核仍然可以調度其他任務運行,一旦該任務重新運行,它又變成不可搶占的。
注意禁止搶先阻止了任務上下文的切換,但不會鎖住中斷的處理程序。必要的話,可以加上intLock()/intUnlock()的組合,把Interrupt的搶占也禁掉。搶占鎖會影響實時操作系統的性能,所以必須保證鎖定時間盡量短。
3.1.4 任務控制
在VxWorks中的taskLib可以得到有關任務控制的基本例程。這些程序提供了任務的創建、控制和信息。
任務的創建和激活
- TaskSpawn( ) 創建並激活一個新的任務
- TaskInit( ) 初始化一個新的任務
- TaskActivate( ) 激活一個已經初始化的任務
taskSpawn()的參數包括新任務的名稱(ASCII碼串),優先級,選項字,堆棧大小,主程序地址(任務的執行函數地址),和10個可以傳遞給主程序的起始參數。 taskSpawn()的第一步就是為Stack和TCB分配內存,然后初始化它們,最后將這個任務放入Ready隊列。taskSpawn()創建新任務的上下文,包括分配堆棧、設定具有指定參數的調用主例程的任務環境,新任務從給定的主例程程序入口處開始執行。
id=taskSpawn(name,priority,option,stacksize,mainFunc,arg1,…arg10);
taskSpawn()例程屏蔽了低層次的資源分配、初始化、激活步驟。初始化、激活功能分別由taskInit()和taskActivate()函數提供,然而,建議只有當需要在分配或激活上進一步控制的時候才使用他們。當項目的實時性需求非常高時,可以考慮使用taskCreate()和taskActivate()的組合。
任務的名字和IDs
當一個任務建立后,你可以規定一個任意長度的ASCII碼字符串作為任務的名稱,VxWorks將返回一個任務的ID,這個ID是一個指向任務數據結構的4字節的句柄。通常,ID=0表示當前任務。
任務名字之間不能沖突,同時為了更好使用Tornado開發環境,任務名也不能和全局變量和全局函數沖突。為了避免命名的沖突,VxWorks約定使用命名前綴:應用於目標機的任務前綴為“t”,應用於宿主機的任務前綴為“u”。
如果用戶不指定任務的名字(調用taskSpawn()時,name=NULL),系統自動賦予任務一個唯一的名字----tN(N是一個可自動累加的十進制整數)。任務名字和ID的例程:
- TaskName ( ) 通過任務的ID來獲得任務的名字
- TaskNameTold ( ) 通過任務的名字查找任務的ID
- TaskIdSelf ( ) 獲得當前任務的ID
- TaskIdVerify ( ) 核實一個特定任務的存在
- taskIdListGet()獲取當前的任務列表
priority
任務的優先級,VxWorks調度任務時就是基於它。優先級的取值范圍是0-255,可以動態改變或查詢:
- TaskPrioritySet()
- TaskPriorityGet()
那在我們自己的應用程序里,優先級設置為多少合適呢?很多人習慣於將內核任務設置為100,用戶態任務的稍低一些,150或200,這個並沒有什么限制,只要平衡好多個應用任務之間的關系即可。不過建議應用任務的優先級不要高於系統任務的。例如,在《Task之常見系統任務》里,我們提到過WDB任務的優先級默認為3,所以我們應用任務盡量不要高於4,否則可能會影響到調試了。
任務選項(Task Options)
當創建一個任務時,通過運行一個邏輯或操作運算來確定選擇的任務選項,就是taskSpawn ( )函數的第三個參數。可供設置的選項如下表所列,注意如果任務需要處理浮點運算,那么必須設置VX_FP_TASK選項。Task Options:
- VX_FP_TASK 0x8 在浮點協處理器上運行
- VX_NO_STACK_FILL 0x100 不用0xEE填充堆棧
- VX_PRIVATE_ENV 0x80 在私有環境中執行任務
- VX_UNBREAKABLE 0x2 本任務禁止斷點
建立一個包含浮點操作的任務采用以下調用格式:
tid = taskSpawn (“tMyTask”,90,VX_FP_TASK,200000,myFunc,2387,0,0,0,0,0,0,0,0,0);
任務創建之后可以通過taskOptionsGet ( )(檢查任務參數)和taskoptionsSet ( )(設置任務參數)對任務的參數進行檢查和修改,目前只可修改VX_UNBREAKABLE這一項。
stackSize
任務的棧,單位是Byte,從系統內存池分配。一經分配,尺寸就固定不變了。如果分配的數值過大,就會增加一點點初始化時間,並浪費部分內存空間,影響倒是不大;而分配的過小,就有棧溢出的風險,這可是致命的了。因此,開發過程中,要評估一下具體的數值。那如何才能知道分配的是否合適呢?可以在Shell里使用checkStack()來檢查。
狀態轉換圖
VxWorks的內核維護着當前每個任務的狀態,將它們放入不同的隊列。而處於Ready隊列最前端的任務就是正在使用CPU執行的任務,因此把這個任務處於的狀態又可以叫做Executing(執行態)。
從上圖可以看到,在不同隊列里排隊的,其實就是每個任務的TCB。
任務信息
下表所列的函數用於得到任務的信息,這些函數在被調用時通過對任務的上下文拍攝快照的方式得到調用時刻的任務信息,因為任務狀態本身是動態的,所以你所得到的信息可能並不是當前任務真實的信息,除非你知道任務本身處於休眠狀態。
- TaskIdListGet ( ) 填充所有激活任務的ID隊列
- TaskInfoGet ( ) 獲得一個任務的信息
- TaskPriorityGet ( ) 檢查任務的優先級
- TaskRegsGet ( ) 檢查任務的寄存器
- TaskregsSet ( ) 設置一個任務的寄存器
- TaskIsSuspended ( ) 檢查任務是否處於懸掛狀態
- TaskIsReady ( ) 檢查任務是否就緒
- TaskIsPended ( )
- TaskTcb ( ) 獲得任務控制塊的指針
任務刪除和刪除保護
一個任務可以被動態地刪除,任何任務都可以隱式或顯式的調用exit ( )函數結束自己,一個任務也可以通過調用taskDelete ( )刪除其他任務。結束任務時,內核會自動釋放任務堆棧和任務控制塊,用戶申請的內存不會自動釋放,必須由任務自己負責釋放。
wind內核提供防止任務被意外刪除的機制。通常,一個執行在臨界區或訪問臨界資源的任務要被特別保護。我們設想下面的情況:一個任務獲得一些數據結構的互斥訪問權,當它正在臨界區內執行時被另一個任務刪除。由於任務無法完成對臨界區的操作,該數據結構可能還處於被破壞或不一致的狀態。而且,假想任務沒有機會釋放該資源,那麽現在其他任何任務現在就不能獲得該資源,資源被凍結了。下表列出了任務刪除函數:
- Exit( ) 刪除自身
- TaskDelete() 刪除別的任務
- TaskSafe() 保護自己不被其他任務刪除
- TaskUnsafe() 解除刪除保護
- taskExit()會終止當前任務,並刪除任務的Stack與TCB。
執行完最后一條語句時,就會自動調用taskExit()。也就是說一般情況下,代碼中沒必要顯式的調用taskExit()。exit()在Kernel模式下,與taskExit()的作用相同。不過在用戶模式下,exit()要強大一些,它會把當前的進程整個刪除,並釋放進程的內存空間。事實上,RTP的main()函數在執行完最后一條語句時,就會自動調用exit ()。同樣,代碼中很少顯式的調用exit()。taskDelete()的參數為0時就是刪除任務本身,相當於taskExit(0)。換句話說,taskDelete()不同的地方是,它可以刪除其它任務。
可以看到,不管任務是主動退出,還是被刪除,都只是刪除Stack和TCB(因為任務就是由它倆組成的),這就存在一個很大的風險,即任務在工作時占用的其它系統資源沒有被釋放。這些系統資源主要有三類:動態申請的內存,IO資源,和互斥信號量。所以任務退出或刪除時,務必對其資源進行回收(RTP里好一些,動態內存和私有信號量會隨着RTP的退出而自動回收)。任務可以使用taskSafe ( ) 和taskUnsafe ( )來實現刪除保護,如果一個任務調用了taskSafe ( ),任何任何試圖刪除該任務的任務將被阻塞,只到該任務調用taskUnsafe ( )解除刪除保護為止。該機制同時支持嵌套調用,這樣需要保存一個計數器用來跟蹤taskSafe ( )調用次數,只有當該計數器為零時任務才能被刪除。下面是使用以上函數來臨界區的例子:
taskSafe (); semTake (semId, WAIT_FOREVER); /* Block until semaphore available / . . critical region . semGive (semId); / Release semaphore */ taskUnsafe ();
正如上面所展示的,任務刪除保護通常伴有互斥操作,為了方便性和效率,互斥信號量包含了刪除保護選項(參見"互斥信號量")。
臨界資源與臨界區
臨界資源是指每次僅允許一個進程訪問的資源。屬於臨界資源的硬件有打印機、磁帶機等,軟件有進程之間共享的消息緩沖隊列、變量、數組、緩沖區等。諸進程間應采取互斥方式,實現對這種資源的共享。
每個進程中訪問臨界資源的那段代碼稱為臨界區。顯然,若能保證諸進程互斥地進入自己的臨界區,便可實現諸進程對臨界資源的互斥訪問。為此,每個進程在進入臨界區之前,應先對欲訪問的臨界資源進行檢查,看它是否正被訪問。如果此刻該臨界資源未被訪問,進程便可進入臨界區對該資源進行訪問,並設置它正被訪問的標志;如果此刻該臨界資源正被某進程訪問,則本進程不能進入臨界區。
// 為了保證臨界資源的正確使用,可以將臨界資源的訪問分為4個部分
do {
entry section; // 進入區(檢查進程是否可以進入臨界區) critical section; // 臨界區(訪問臨界資源) exit section; // 退出區(清除正在訪問臨界區的標志) remainder section;// 剩余區(剩余代碼) } while(true)
任務控制
Vxworks的調試系統需要調用能使任務掛起或恢復執行的例程從而可凍結任務的狀態以便檢查。任務在對嚴重錯誤作出反應時也需要在運行期間重啟,taskRestart()使用初始參數重新創建任務。TaskDelay()延時操作則可提供簡單的機制使任務持續一個固定的時間段的睡眠狀態。sysClkRateGet()的作用就是返回系統時鍾每秒鍾的tick數。sysClkRateGet()*2就是2s,可以通過sysClkRateSet()修改時鍾頻率。要注意的是,每個tick到來時,都會觸發系統時鍾中斷。作為延時操作的一個附加結果,taskDelay( ) 操作把調用任務放到相同優先級的就緒隊列的末尾。所以,你可以使用零延時達到讓出CPU給同優先級其他任務的目的。零延時只適用於taskDelay( ) ,nanosleep()認為非法。任務控制函數:
- taskSuspend() 掛起一個任務,將任務放到Suspend隊列,taskSuspend(0)就是掛起任務本身
- taskResume() 恢復執行一個任務,將任務放到Ready隊列,這兩個API通常只是在調試時才會使用。
- taskRestart() 重啟一個任務,使用的是原有的屬性,其中優先級和選項可能被動態改變過,那就使用最新的值。
- taskDelay() 延時一個任務,延時的時間單位以ticks計
- nanosleep() 延時一個任務,延時時間單位以納秒計
3.1.5 任務擴展性
支持不可預見的內核擴展的能力與已有功能的可配置性是同樣重要的。簡單的內核接口和互斥方法使內核級功能擴展相當容易;在某些情況下,應用可以僅利用內核鈎子函數來實現特定的擴展。內部鈎子函數:為了不修改內核而能夠向系統增加額外的任務相關的功能,VxWorks提供了任務創建、切換和刪除的鈎子函數。這些允許在任務被創建、 上下文切換和任務被刪除的時候,額外的例程被調用執行。這些鈎子函數可以利用任務上下文中的空閑區創建wind內核的任務特性擴展。
- taskCreateHookAdd() 在每一次任務創建時增加一個被調用的程序。注冊的Hook數量默認為40
- TaskCreateHookDelete() 刪除在任務創建時增加的程序
- taskSwitchHookAdd() 在每一次任務切換時增加一個被調用的程序
- TaskSwitchHookDelete() 刪除在任務切換時增加的程序
- taskDeleteHookAdd() 在每一次任務刪除時增加一個被調用的程序
- TaskDeleteHookDelete() 刪除在任務刪除時增加的程序
- taskStopHookAdd() 每次有Task在Stop其它Task時,都會自動調用函數
- taskStopHookAddDelete()
- taskContHookAdd() 每次有Task在Continue其它Task時,都會自動調用
- taskContHookAddDelete()
- taskSwitchHookAdd() 每次有Task切換執行時,都會調用函數switchHook()。例如Task B搶占了正在執行的Task A,就會調用switchHook()。Task A再恢復執行時,就再次調用switchHook()。
- taskSwapHookAdd()
- taskSwapHookAddDelete()
- taskSwapHookAttach()
- taskSwapHookDetach()
與Switch比較像,都是在Task切換時調用相應的Hook。
區別是:Switch是每次Task切換都會調用相應的Hook;而Swap注冊Hook后,只對taskSwapHookAttach()掛接的任務有效。而且taskSwapHookAttach()掛接時,還可以指定是切入有效,還是切出有效。例如taskSwapHookAttach((FUNCPTR)swapHook,TaskA,1,0)表示TaskA被CPU執行時才會調用swapHook(),即切入有效;而TaskA被其它Task搶占了,是不會觸發swapHook()的,即切出無效。
另外,Switch與Swap注冊的Hook是在Kernel的上下文里執行,因此並不是所有的系統函數都可以在它們的Hook里調用。可以被調用的函數如下
用戶安裝的任務切換(switch)鈎子函數是在內核上下問中被調用的,所以切換鈎子函數不能訪問所有的VxWorks資源。有些函數不能被切換鈎子函數所調用,具體使用時需要注意。
可以注冊多Hook,用於調試或監測。
3.1.6 任務錯誤狀態:errno
按照慣例,當函數遇到一個錯誤時,C庫函數設置一個全局整數變量errno成一個適當的錯誤號碼。這個約定被定義成ANSI C標准的一部分。
在VxWorks中,errno 同時用兩種方法定義。一種是符合ANSI C標准,有命名為errno的全局變量。另外,errno 同時是在errno.h中定義的一個宏。這個定義除了一個函數外,對所有的VxWorks都是可見的。這個宏的定義是調用一個__errno()的函數,得到全局變量errno的地址。這是一個很有用的特性。我們可以在調試時在函數中設下斷點,來看看到底產生了什么錯誤。不過,由於errno宏的結果是errno全局變量的地址,在C程序中允許這樣的標准賦值方法:
errno = someErrorNumber;
請注意在編程時不要定義和errno一樣的本地變量,以免混淆。
每個任務有自己的errno值
在VxWorks中,全局變量errno是一個單個的預定義全局變量。它可以在和VxWorks系統鏈接時被應用程序代碼直接引用。(不論是在主機靜態鏈接時或者是在動態加載時)。顯而易見,errno在多任務環境中是十分有用的,但是每一個任務都必需觀察它自己的errno變量,所以error值必須采用某種切換機制。這就是說,errno變量是任務進行切換時,系統內核保存和恢復的任務上下文的一部份。類似地,中斷服務程序(ISR)也有自己的errno變量存儲。
內核自動提供中斷的入口和出口代碼中,完成存儲和恢復中斷棧中的errno值。也就是說,不管VxWorks的上下文如何,一個錯誤碼都將被存儲或者做為參考直接寫到全局變量errno中去。
錯誤返回碼的約定
幾乎所有的VxWorks函數都遵從這樣的約定:函數操作通過返回實際值來簡單地指示成功或者失敗。許多函數只返回狀態值OK(0)或ERROR(-1)。一些函數通常返回非負數(如open( )函數返回文件句柄),同時也使用ERROR表示錯誤。函數返回指針時常常用NULL指示錯誤。在多數情況下,一個函數返回的錯誤指示同時也和在errno中寫入特定的錯誤碼。
VxWorks函數從來不會清除errno全局變量。這意味着,這個錯誤值一直指示着最后一個發生的錯誤狀態設置。當一個VxWorks的子程序調用另一個程序而得到錯誤指示時,它通常只是返回自己的錯誤指示而並不修改errno。即errno的值只是低層函數才使用,它指示的是低層函數的錯誤返回值。我們可以通過errno的值,來查看Task/Interrupt最后的一次錯誤情況。不過Kernel函數已經對errno做了處理(但很可能不清除它),只要Task的狀態沒有問題,我們App就不用額外擔心它們了。
比如:VxWorks的函數intConnect ( ),它把中斷處理程序和硬件中斷聯系起來,其中需要調用內存分配函數malloc ( )分配內存,如果分配失敗,malloc( )將會設立errno值,標記內存不足,同時返回一個NULL指示失敗。而intConnect()函數收到malloc()返回的NULL后,將返回一個自己的錯誤指示ERROR。但它並不更改errno的值。Errno中仍然標記着“內存不足”的錯誤碼。
我們推薦你最好在自己的程序中使用自己的錯誤碼機制,而系統提供的errno只做調試時檢測之用。如:
if ((pNew = malloc (CHUNK_SIZE)) == NULL)
return (ERROR);
錯誤狀態值的分配
VxWorks的errno值表明了一個模塊的錯誤類型,高位兩字節的代表模塊號,低位兩個字節來表示單獨的錯誤碼。所有VxWorks的errno的模塊號從1-500,如果模塊號為0,則表示用於資源的兼容性問題,其他模塊號可以由應用程序使用。
應用程序可以使用大於501<<16的正數以及所有負數。錯誤碼請參見errnoLib參考。那怎么查看errno呢?不建議直接訪問變量errno或TCB成員errorStatus。而是調用函數errnoGet(), 或者包含組件INCLUDE_STAT_SYM_TBL然后在Shell里調用printErrno()
3.1.7 任務異常處理
程序代碼和數據中的錯誤可能會導致硬件異常情況的發生,比如出現非法指令,總線和地址錯誤,被零除等等。VxWorks的異常處理軟件包能處理好所有的此類異常。默認的異常處理方法是將產生此異常的任務掛起,並將任務在發生異常點的狀態保存下來。內核和其他任務繼續運行而不間斷。我們可以使用Tornado檢查被掛起任務的狀態。
任務也可以通過信號機制把自己的處理程序掛在已有的硬件異常上。如果一個任務為一個異常提供了信號處理程序,默認的異常處理方法將被替代而不起作用。信號同時也可以用於報告軟件異常,使用方法和在硬件異常中一樣。
3.1.8 共享代碼和代碼重入
在VxWorks中,通常會有許多不同的任務調用一個相同的程序或子程序庫。被多個任務執行的同一套代碼被稱為共享代碼。共享代碼必須具有可重入性。當一個例程可以被幾個任務的上下文同時調用而不發生沖突的時候,這個子程序就具有可重入性。由於程序的數據和代碼都是同一拷貝,當子程序修改了全局變量或靜態變量時就會往往產生沖突。一個程序引用這樣的變量的會造成不同任務上下文的重疊和調用干擾。
VxWorks的大多數程序都具有可重入性,但所有具有一個name_r()形式函數的對應函數name()都被認為不具有可重入性。VxWorks的I/O和驅動程序都具有可重入性,但需要慎重地設計應用程序。對於I/O緩存,我們推薦每個任務使用一組文件指針。在驅動級,因為VxWorks使用的是全局文件指針描述表,可能會有不同的任務以流的形式寫緩存區。這種做法是否合適,取決於應用程序本身。比如:一個包驅動可以將來自不同任務的數據流合成到一起,因為每個包的頭上都定義了目標地址。大多數的VxWorks的程序都采用如下的可重入技術:
- 動態堆棧變量(局部變量)
- 由信號量機制保護的全局變量和靜態變量
- 任務變量
動態堆棧變量
許多子程序都是純代碼,除了動態堆棧變量外沒有自己的數據。程序處理的數據都是調用者以參數的形式來提供的。如鏈接表庫lstLib就是一個很好的例子。它的函數只對調用者調用時提供的列表和節點進行操作。這一類函數天然具有可重入的特性。多個任務可以同時使用此函數而互不干擾,因為每個任務都有自己的的堆棧,所以多個任務可以同時調用程序而不產生沖突。
被保護全局變量和靜態變量
一些封裝庫可以訪問公共數據,例如內存分配庫,menLib就管理着由多個任務共用的內存池。這個庫說明並使用自己的靜態數據變量來跟蹤內存池的分配。這種庫在使用時需要當心,因為它的函數不是天然可以重入的。多個任務同時調用庫中的程序會干擾相互對公共變量的訪問。所以這類庫必須提供互斥機制來禁止任務同時訪問臨界代碼。最常用的互斥機制是互斥信號量,互斥機制的實現后面再詳細介紹。
任務變量
一些被多任務同時調用的程序需要全局或靜態變量,而對每一個調用的任務又要求這些變量都具有不同的值。為了適應這種需求,VxWorks提供了一種叫“任務變量”的機制。它允許將一個四字節的變量加到任務的上下文中。這樣每次進行任務切換操作時,變量的值都會發生切換。
典型的例子中,如果多個任務申明了一個相同的變量(4字節)作為任務變量。每一個任務都可以把這個變量作為自己的私有地址空間。這個功能由taskVarLib提供的taskVarAdd(),taskVarDelete(),taskVarSet(),taskVarGet()一組函數實現。頻繁使用這種機制可能會增加任務上下文切換時間,解決的方法是可以把一個模塊中所有需要使用任務變量的變量定義在一個單獨結構中,對這些變量的訪問都可以通過一個指向該結構的指針進行,這樣只需要把這個指針定義為任務變量就可以了。
在VxWorks中,允許使用同一套代碼創建多個任務。每一個新任務都有自己的堆棧和上下文。每次產生新任務時都可以傳遞不同的參數。此時,可以使用任務變量的方法實現重入規則。當同一個函數使用不同的參數設置在系統中同時運行時,任務變量是十分有用的。比如,一個特殊設備監控程序可能要派生出多道任務來監控好幾個這樣的設備。這是可以使用特定設備的編號來作為任務的輸入參量。
3.1.9 VxWorks的系統任務
VxWorks包括以下幾個系統任務:
- 根任務:tUsrRoot
根任務tUsrRoot是由內核執行的第一個任務。根任務的入口函數是installDir/target/config/all/usrConfig.c下的usrRoot()函數,此函數初始化多數的VxWorks功能。它產生其他任務,諸如,注冊任務,異常處理任務,網絡功能任務,以及tRlogind守護進程。一般情況下,當所有的初始化完成后,根任務將被被刪除。你可以隨意在根任務中添加需要的初始化過程。優先級:0
- 注冊任務:tLogTask
注冊任務tLogTask,VxWorks模塊使用注冊任務可以實現不通過當前任務的上下文進行I/O操作而記錄系統消息。比如printf將發送要打印的系統消息,可以直接將系統消息掛到LogTask的隊列中。再由Log任務發送此消息,而調用printf的任務並不直接對I/O端口操作。本任務在logLib庫中。觸發條件:中斷或任務里調用logMsg()。組件:INCLUDE_LOGGING。優先級:0
- 異常處理任務:tExcTask
異常處理任務tExcTask支持捕獲那些不會引起中斷的VxWorks異常。這個任務必需是系統中任務優先級最高的任務。而且不允許掛起,刪除和改變優先級。本任務在excLib庫中。觸發條件:中斷里調用excJobAdd()。組件:INCLUDE_EXC_TASK。優先級:0
- 網絡任務:tNetTask
tNetTask守護任務處理VxWorks網絡功能中所需的任務級函數。
- 目標代理任務:tWdbTask
如果任務模式中設置了目標代理,系統將創建一個tWdbTask任務。它負責響應Tornado的目標服務器。主要用於觀察調試。優先級:3
- 任務名稱:tJobTask
優先級:啟動時為0,根據執行的工作而動態調整。棧尺寸:JOB_TASK_STACK_SIZE,默認8000。觸發條件:其它任務提交工作。組件:INCLUDE_JOB_TASK。描述:這個任務用於執行其它任務提交的工作。它使用優先級0等待工作,在執行工作時,動態調整為提交工作的任務的優先級。主要作用之一是處理任務的自我刪除。
- 任務名稱:tIsrN
優先級:0。棧尺寸:8192。觸發條件:設備中斷調用isrDeferJobAdd()。組件:INCLUDE_ISR_DEFER。描述:這個(組)任務用於執行設備驅動通過isrDeferJobAdd()提交的工作。名稱中的字母N表示這個任務所使用的CPU的序號,在單核環境里,那就只有tIsr0了。這組任務創建時,每個都綁定到相應序號的CPU上。多核模式的設備驅動將需要推遲的工作綁定到當前的CPU上,用於避免跨CPU調度。
- 任務名稱:tNet0
優先級:NET_TASK_PRIORITY,默認值50。棧尺寸:NET_TASK_STACKSIZE,默認值10000。觸發條件:數據包到達,傳輸完成,網絡協議里的定時器到時,socket應用的請求,等等。組件:INCLUDE_NET_DEAMON。描述:這是網絡驅動和網絡協議的守護線程。
- 任務名稱:tShellN
優先級:SHELL_TASK_PRIORITY,默認值1。棧尺寸:SHELL_STACK_SIZE,默認值0x10000。觸發條件:系統啟動。組件:INCLUDE_SHELL。描述:kernel shell以任務形式存在的,可以同時啟動多個,不同的shell使用不同的序號N為名稱后綴,名稱”tShell”是通過SHELL_TASK_NAME_BASE定義的。在shell里再調用的函數會使用這個shell的上下文。
- 任務名稱:ipcom_telnetd。
優先級:50。棧尺寸:6144。觸發條件:新的Telnet連接。組件:INCLUDE_IPTELNETS。描述:這個守護線程允許遠程用戶通過Telnet登陸VxWorks的kernel shell。它會為每個Telnet連接啟動一組任務,包括ipcom_telnetspawn,tStdioProxyhexNum,tLoginhexNum,tShellRemdecNum
- TRlogind
如果配置了目標shell和rlogin特性,當一個遠程用戶注冊到VxWorks主機上,在連接的兩端都產生tRlogInTask和tRlogOutTask作為tty終端提供給用戶。
- tTelnetd
如果配置了目標shell和telnet特性,守護進程遠程用戶通過telnet連接在VxWorks上。它可以接受遠程用戶注冊在VxWorks或主機系統上,並產生tTelnetInTask和tTelnetOutTask輸入輸出任務。提供一個tty的模擬終端給用戶使用。
- TPortmapd
如果你配置了RPC特性,這個守護任務將成為同一個機器中的所有的RPC服務的注冊中心。PRC的客戶都必須通過這個tPortmapd守護任務才能訪問服務。
3.2 任務間通信
VxWorks提供了幾種任務間通訊的方法。
- Shared Data Structures,用於簡單的數據共享;
- Semaphores,用於基本的互斥和同步;
- Message Queues & Pipes,實現同一CPU內任務間的消息傳遞;
- Sockets & RPC,實現網絡透明的任務間通信;
- Signals,用於異常處理。
3.2.1 共享數據結構
同一內存空間里(都在內核,或在同一RTP里)的任務之間,最顯而易見的的通信方式就是訪問共享的數據結構,因為它們使用單一線性地址空間
不同的任務可以直接訪問全局變量、線性緩沖區、環形緩沖區、鏈表,以及指針。不過這些共享的數據結構,需要使用互斥機制(例如互斥信號量)來保護
3.2.2 互斥
wind內核的任務間通信機制的基礎是所有任務所在的共享地址空間。通過共享地址空間,任務能夠使用共享數據結構的指針自由地通信。管道不需要映射一塊內存區到兩個互相通信任務的尋址空間。不幸的是,共享地址空間具有上述優點的同時,帶來了未被保護內存的重入訪問的危險。UNIX操作系統通過隔離進程提供這樣的保護,但同時帶來了對於實時操作系統來說巨大的性能損失。
當一個共享地址空間簡化了數據交換,通過互斥訪問避免資源競爭就變為必要的了。用來獲得一個資源的互斥訪問的許多機制僅在這些互斥所作用的范圍上存在差別。實現互斥的方法包括禁止中斷、禁止任務搶占和通過信號量進行資源鎖定。
中斷禁止和中斷延時
最強的互斥方法是屏蔽中斷。這樣的鎖定保證了對CPU的互斥訪問。這種方法當然能夠解決互斥的問題,但它對於實時是不恰當的,因為它在鎖定期間阻止系統響應外部事件。長的中斷延時對於要求有確定的響應時間的應用來說是不可接受的。
funcA() { int lock=intLock(); … /critical region that cannot be interrupted/ … intUnlock(lock); }
當任務去訪問ISR也會訪問的數據結構時,就使用intLock()來阻止ISR的搶占。中斷鎖定對於包含ISR(中斷服務程序)的互斥有時是必需的,但在任何情況下,要盡量使中斷鎖定的時間短。警告:使用中斷鎖時,再調用系統函數的話,可以導致中斷不定期的重新使能,例如調用的函數發生阻塞,或者激活高優先級任務
搶占禁止和延時
禁止搶占提供了強制性較弱的互斥方式。 當前任務運行的過程中不允許其他任務搶占,而中斷服務程序可以執行。這也可能引起較差的實時響應,就象被禁止中斷一樣,被阻塞的任務會有相當長時間的搶占延時,就緒態的高優先級的任務可能會在能夠執行前被強制等待一段不可接受的時間。為避免這種情況,在可能的情況下盡量使用信號量實現互斥。
funcA() { taskLock(); … /critical region that cannot be interrupted/ … taskUnlock(lock); }
3.2.3 信號量Semaphores
在VxWorks中,Semaphores是一個高度優化的、提供最快任務間通信的機制。主要定位於兩類需求:
互斥
在互斥方面,信號量是用於鎖定共享資源訪問的基本方式。不象禁止中斷或搶占,信號量限制了互斥操作僅作用於相關的資源。一個信號量被創建來保護資源。VxWorks的信號量遵循Dijkstra的P()和V()操作模式。
當一個任務請求信號量,P(), 根據在發出調用時信號量的置位或清零的狀態,會發生兩種情況。如果信號量處於置位態,信號量會被清零,並且任務立即繼續執行。如果信號量處於清零態,任務會被阻塞來等待信號量。
當一個任務釋放信號量,V(),會發生幾種情況。如果信號量已經處於置位態,釋放信號量不會產生任何影響。如果信號量處於清零態且沒有任務等待該信號量,信號量只是被簡單地置位。如果信號量處於清零態且有一個或多個任務等待該信號量,最高優先級的任務被解阻塞,信號量仍為清零態。
通過將一些資源與信號量關聯,能夠實現互斥操作。當一個任務要操作資源,它必須首先獲得信號量。只要任務擁有信號量,所有其他的任務由於請求該信號量而被阻塞。當一個任務使用完該資源,它釋放信號量,允許等待該信號量的另一個任務訪問該資源。
任務同步
信號量另一種通常的用法是用於任務間的同步機制。在這種情況下,信號量代表一個任務所等待的條件或事件。最初,信號量是在清零態。一個任務或中斷通過置位該信號量來指示一個事件的發生。等待該信號量的任務將被阻塞直到事件發生、該信號量被置位。一旦被解阻塞,任務就執行恰當的事件處理程序。信號量在任務同步中的應用對於將中斷服務程序從冗長的事件處理中解放出來以縮短中斷響應時間是很有用的。
同步:
同步就是指一個進程在執行某個請求的時候,若該請求需要一段時間才能返回信息,那么這個進程將會一直等待下去,直到收到返回信息才繼續執行下去。
可以理解為同步需要進行等待,在上一步跑完后下一步才能繼續執行。
一般用於流程性比較強的程序,比如用戶登錄,需要對用戶驗證完成后才能登錄系統。 特點:
- 同步是阻塞模式;
- 同步是按順序執行,執行完一個再執行下一個,需要等待,協調運行;
異步
異步是指進程不需要一直等下去,而是繼續執行下面的操作,不管其他進程的狀態。當有消息返回時系統會通知進程進行處理,這樣可以提高執行的效率。
異步不需要進行等待,不管其他進程狀態,直接就可以執行。比如加載頁面。特點:
- 異步是非阻塞模式,無需等待;
- 異步是彼此獨立,在等待某事件的過程中,繼續做自己的事,不需要等待這一事件完成后再工作。線程是異步實現的一個方式。
優缺點:
- 同步可以避免出現死鎖,讀臟數據的發生。一般共享某一資源的時候,如果每個人都有修改權限,同時修改一個文件,有可能使一個讀取另一個人已經刪除了內容,就會出錯,同步就不會出錯。但,同步需要等待資源訪問結束,浪費時間,效率低。
- 異步可以提高效率,但是安全性較低。
原文鏈接:https://blog.csdn.net/weixin_42898315/article/details/110855741
VxWorks提供三種信號量:
- 二值信號量(Binary),最快、最通用的semaphore,被優化用於同步和互斥。
- 互斥信號量(mutual exclusion),一種特殊的二值信號量,對互斥所固有的問題提供了優化解決方案:優先級繼承,刪除保護,遞歸。
- 計數信號量(Counting),類似二值信號量, 但是可以跟蹤信號量被給於的次數,用於一種資源的多個實例的保護。
VxWorks信號量的控制
VxWorks並不是為每一種信號量提供了一整套的控制機制,相反他對三種信號量提供了統一的操作接口,只是信號量的創建函數針對不同的信號量是不同的。信號量控制函數:
- semBCreate() 創建並初始化二值信號量
- semMCreate() 創建並初始化互斥信號量
- semCCreate() 創建並初始化計數信號量
- semDelete() 刪除並釋放一個信號量
- semTake() 獲取信號量
- semGive() 歸還信號量
- semFlush() 喚醒所有等待一個信號量的任務(不能用於互斥信號量)
函數semBCreate() semMCreate()semCCreate()返回一個信號量ID,他可以被其他信號量控制函數作為句柄使用。信號量創建時需要指定信號量任務隊列的隊列類型,主要兩種隊列方式:優先級方式(SEM_Q_PRIORITY)和先進先出方式(SEM_Q_FIFO)。它決定了多個任務將以何種方式排隊等待一個信號量。
3.2.3.1 二值信號量
一個二值信號量被認為是一個具有“可獲得”(滿)和“不可獲得”(空)兩種狀態的標志量,SEM_Full (資源可用) SEM_Empty (資源不可用).。一個任務能否通過semTake()獲得一個二值信號量取決於在調用的時候信號量是滿還是空。如果信號量是滿,在任務取之后變成空,任務可繼續執行;如果信號量是空,且超時參數不等於NO_WAIT,則任務排到阻塞隊列中進入阻塞狀態;如果信號量是空,且超時參數等於NO_WAIT,任務繼續運行,未獲取信號量。ISR(中斷服務程序)不能調用semTake()操作!
同樣一個任務通過semGive()還回信號量時,如果信號量是滿,則不起任何作用,如果是空且沒有等待該信號量的任務,則將其置為滿。如果信號量是空,且已經有任務在等待該信號量,那么該信號量的阻塞任務隊列中的第一個任務被解阻塞繼續運行,信號量仍為空。
二值信號量用於互斥
二值信號量有效地鎖定了對共享資源的訪問,互斥操作時初始狀態設為(SEM_FULL)可用。並在同一個Task中成對、順序調用semTake()、semGive()。當一個任務希望訪問資源,它必須先得到信號量,只要任務持有信號量,其他訪問同一臨界資源的任務就會阻塞。當任務結束對資源的使用就會歸還信號量,從而允許其他任務使用資源。
semTake(semMutex,WAIT_FOREVER); … /critical region ,only accessible by a single task at a time/ … semGive(semMutex);
二值信號量用於同步
當用於任務間的同步時,信號量則代表一個任務正在等待的事件或條件。同步操作時初始狀態設為(SEM_EMPTY)不可用。一個任務或中斷服務程序通過給信號量semGive()來告之事件的發生,而另一個任務通過semTake()取信號量來知道特定事件的發生,這個任務在獲得信號量之前一直處於阻塞狀態。
/* This example shows the use of semaphores for task synchronization. */ #include “vxWorks.h” #include “semLib.h” #include “arch/arch/ivarch.h” // replace arch with architecture type SEM_ID syncSem; // ID of sync semaphore init (int someIntNum) { // connect interrupt service routine intConnect (INUM_TO_IVEC (someIntNum), eventInterruptSvcRout, 0); // create semaphore syncSem = semBCreate (SEM_Q_FIFO, SEM_EMPTY); // spawn task used for synchronization. taskSpawn (“sample”, 100, 0, 20000, task1, 0,0,0,0,0,0,0,0,0,0); } task1 (void) { … semTake (syncSem, WAIT_FOREVER); // 等待事件的發生 printf (“task 1 got the semaphore\n”); … // process event } eventInterruptSvcRout (void) { … semGive (syncSem); // 通知事件的發生 … }
semFlush( ):解鎖所有等待信號量的任務
3.2.3.2 互斥信號量
互斥信號量是一種特殊的雙態信號量,它用於解決某些互斥中的一些特有問題,包括優先級倒置、刪除保護和對資源的遞歸調用。互斥信號量是一種特殊的二值信號量,與二值信號量基本是相同的,不同之處有:
- 它只用於互斥
- 它只能被取走它的任務歸還
- 它不能在中斷服務程序中執行歸還操作
- semFlush()操作對它是非法的
優先級逆轉
優先級逆轉發生在一個高優先級的任務被強制等待一段不確定的時間以便一個較低優先級的任務完成執行。考慮下面的假設:T1,T2和T3分別是高、中、低優先級的任務。T3通過擁有信號量而獲得相關的資源。當T1搶占T3,為競爭使用該資源而請求相同的信號量的時候,它被阻塞。如果我們假設T1僅被阻塞到T3使用完該資源為止,情況並不是很糟。畢竟資源是不可被搶占的。然而,低優先級的任務並不能避免被中優先級的任務搶占,一個搶占的任務如T2將阻止T3完成對資源的操作。這種情況可能會持續阻塞T1等待一段不可確定的時間。這種情況成為優先級逆轉,因為盡管系統是基於優先級的調度,但卻使一個高優先級的任務等待一個低優先級的任務完成執行。
互斥信號量有一個選項SEM_INVERSION_SAFE,允許實現優先級繼承的算法。優先級繼承通過在T1被阻塞期間提升T3的優先級到T1解決了優先級逆轉引起的問題。這防止了T3,間接地防止T1,被T2搶占。通俗地說,優先級繼承協議使一個擁有資源的任務以等待該資源的任務中優先級最高的任務的優先級執行。當執行完成,任務釋放該資源並返回到它正常的或標准的優先級。因此,繼承優先級的任務避免了被任何中間優先級的任務搶占。
優先級繼承使用方法如下,注意SEM_INVERSION_SAFE選項必須和信號量的優先級隊列選項一起使用。
semId = semMCreate (SEM_Q_PRIORITY | SEM_INVERSION_SAFE);
刪除保護
互斥引起的一個問題會涉及到任務刪除。在由信號量保護的臨界區中,需要防止執行任務被意外地刪除。刪除一個在臨界區執行的任務是災難性的。資源會被破壞,保護資源的信號量會變為不可獲得,從而該資源不可被訪問。通常刪除保護是與互斥操作共同提供的。由於這個原因,互斥信號量通常提供選項來隱含地提供前面提到的任務刪除保護的機制。
前面講到,原語taskSafe()和taskUnsafe()對任務的刪除提供了一條解決的途徑。互斥信號量提供另一種更方便易用的保護方法,互斥信號量提供了一個SEM_DELETE_SAFE的參數使得每個semTake()都含有taskSafe(),且每個semGive()都含有taskUnsafe()。通過這個方法,一個持有信號量的任務就可以得到刪除保護,而且代碼的效率也更高。
semId = semMCreate (SEM_Q_FIFO | SEM_DELETE_SAFE);
遞歸資源訪問
互斥信號量可以被遞歸地獲取。這意味着占有信號量的任務在釋放前可以不止一次地取得信號量。遞歸可以實現當一組程序需要相互調用又要互斥訪問同一個資源時的情況。
在任務釋放時,互斥信號量的TAKE的次數必須等於GIVE的次數。互斥型信號量必須是同一個任務申請,同一個任務釋放,其他任務釋放無效。同一個任務能遞歸申請。
InitFun() { sem_ID = semMCreate(…); } funB() { semTake(sem_ID, SEM_FOREVER); /*訪問臨界資源*/ semGive(sem_ID); } funA() { semTake(sem_ID, SEM_FOREVER); /*訪問臨界資源*/ funB(); //遞歸訪問, 而不會死鎖 semGive(sem_ID); }
3.2.3.3 計數信號量
計數信號量與二值信號量的唯一區別在於它跟蹤信號量獲取和歸還的次數。每次信號量被獲取,計數器減一,每當信號量被歸還,計數器加一。當計數器為零時,需要獲取信號量的任務則需進入阻塞隊列等待。和二值信號量一樣,如果歸還信號量時,有任務在等待該信號量,則該任務被解阻塞。和二值信號量不同的是,如果歸還信號量時,沒有任務在等待該信號量,則信號量遞增。也就是說如果信號量被歸還3次,那么他就可以被獲取3次。
計數信號量用於保護具有多個實例的資源。例如對一個具有3個磁帶驅動器的應用則需對計數信號量初始化為3,下面顯示了計數信號量的使用:
信號量函數調用 調用后信號量值 調用結果
semCCreate() 3 信號量創建,初始值為3
semTake() 2 信號量被獲取
semTake() 1 信號量被獲取
semTake() 0 信號量被獲取
semTake() 0 信號量為空,任務阻塞
semGive() 0 阻塞的任務得到信號量
semGive() 1 沒有任務等待信號量,信號量值遞增
這段代碼很簡單,大致的意思就是每成功申請一次信號量,就打印一句話。啟動一個任務(t1)來調用這個函數:
可以看到t1阻塞到信號量semId上了。直接給它釋放一次信號量
任務(t1)打印了一句話,說明收到了一次信號量。接下來試試釋放兩次信號量,可以用Shell的命令repeat()
任務(t1)也打印了兩句話,說明收到了兩次信號量。接下來試試多次的
可以看到, repeat的次數大於2之后,任務(t1)都是只能收到兩次信號量。我們看看semGive()的操作流程
從上圖可以看到,repeat()第一次釋放信號量時,它會將阻塞狀態的t1置為就緒狀態。第二次釋放時,沒有任務阻塞了,於是將信號量置為有效(0->1),之后再釋放時,都是將信號量置有效(1->1)。直到repeat()執行完畢,就緒狀態的t1開始執行后續操作,出現第一次打印。然后又可以成功申請一次信號量(1->0),就有了第二次打印。這之后,信號量就又是無效的了,t1再次進入了阻塞狀態。這就是二進制信號量的特點,它是用來表示事件是否發生了,而不能表示事件發生的次數。如果需要記錄事件發生的次數呢?可以試試提高t1的優先級。不過VxWorks專門提供了用於計數的信號量: 計數信號量。
semCCreate()用來動態創建計數信號量,semCInit()用來初始化靜態分配的信號量。initCount表示計數信號量的初值,因為是有符號整型值,其取值范圍是0-2147483647(0x0-0x7fffffff)。
semTake()用來申請信號量,信號量無效時,引起阻塞,因此不能在ISR中使用
semGive()用來釋放信號量,在任務或ISR中都可以調用
Anyway,實際應用中,count的值不太可能那么大的。還是回到開始位置,把testSemB那個例子改了看看吧
這時候再多次釋放信號量,任務(t1)就可以收到多次了
同時,計數信號量也支持semFlush()操作,即它也是可以用於多任務同步的。
3.2.3.3 讀寫信號量
如果多個任務操作同一個資源,最好使用互斥信號量進行保護。那如果這些任務僅僅是執行讀操作呢?就沒必要把它們都串行起來了,尤其是在多核盛行的今天。因此,風河從Vx6開始引入一種新的機制 - 讀寫信號量 (read/write semaphore),只不過到了Vx69,這種機制才算完整了
參數options的取值與互斥信號量基本一致。因此讀寫信號量的特性與互斥信號量非常像,例如只能由持有者釋放,不能在ISR中操作,不支持semFlush()。
參數maxReaders表示最多可以有多少個讀者同時持有該信號量,取值是1至SEM_RW_MAX_CONCURRENT_READERS。這就是它的主要區別:申請模式分類讀和寫兩種
當信號量有效時,讀者或寫者都可以成功申請;
-
當信號量被讀者占用時,新讀者不超過maxReaders的話,可以成功申請;超過maxReaders的讀者,或者寫者,就會阻塞;
-
當信號量被寫者占用時,新的申請者都會阻塞;
-
釋放信號量時,優先解除寫者的阻塞狀態;
-
當信號量被讀者占用,但有寫者阻塞時,新的讀者也會阻塞,不管是否超過maxReaders;
比之前的信號量確實復雜了一些,寫個例子,就知道是怎么回事了
可以看到,當訪問同一資源的多個任務,多數都是寫操作時,用傳統的互斥信號量就可以;而多數僅是讀操作時,使用讀寫信號量就更適合了
3.2.4 消息隊列
在實時系統中,應用被構造成一組相互獨立又協調工作的任務。信號量提供了互斥和任務之間的同步。通常,需要更高級的機制來允許任務之間相互通信,在VxWorks中,單個CPU中任務之間的通信(數據傳遞)可由消息隊列完成。
消息隊列:可以存放不定數量的消息,每條消息長度可以也不一樣,消息按一定機制排隊。任何任務或是中斷處理程序都可以向一個消息隊列中發送消息。任何任務都可以從消息隊列取消息。VxWorks消息隊列控制:
- MsgQCreate() 創建並初始化一個消息隊列
- MsgQDelete() 刪除並釋放一個消息隊列
- MsgQSend() 向一個消息隊列中發送消息
- MsgQReceive() 從一個消息隊列中接收消息
使用msgQCreate()創建消息隊列時,需要指明消息隊列的最大消息數,以及消息的最大長度(單位:BYTE),這樣內核才能確定分配的內存空間。
一個任務或者中斷處理程序可以使用msgQSend()向消息隊列中發送消息,如果沒有任務在消息隊列上等待消息,消息被加入隊列,如果已經有任務等待消息,那么消息直接交給等待的第一個任務。
一個任務可以使用msgQReceive()從消息隊列中取消息,如果消息隊列中已經有消息,那么第一條消息被取出隊列並返回給調用者。如果隊列為空,則該任務被阻塞,加入到消息隊列的等隊任務隊列。
超時TimeOuts
在消息隊列的相關收發操作中,都牽扯到一個參數:超時(TimeOut)。在發送的時候,如果沒有空間來對消息進行排隊的話,TimeOut指定可以等待直到得到緩存的Tick數。在接收的時候,TimeOut指定在消息到來之前可以等待的Tick數。TimeOut可以選擇:
- NO_WAIT(0),意味着立即返回,不管是否發送/接收成功;
- WAIT_FOREVER(-1),意味着永遠等待,只到操作成功;
- 非負整數,表示等待的Tick數。
緊急消息Urgent Messages
函數msgQSend()允許規定消息的優先級:
- 正常消息 MSG_PRI_NORMAL
- 緊急消息 MSG_PRI_URGENT
正常級別的消息被放到消息隊列的末尾,而緊急消息放到隊列頭上。
#include "vxWorks.h" #include "msgQLib.h" #define MAX_MSGS (10) #define MAX_MSG_LEN (100) MSG_Q_ID myMsgQId; task2 (void) { char msgBuf[MAX_MSG_LEN]; if (msgQReceive(myMsgQId, msgBuf, MAX_MSG_LEN, WAIT_FOREVER) == ERROR) return (ERROR); printf ("Message from task 1:/n%s/n", msgBuf); } #define MESSAGE "Greetings from Task 1" task1 (void) { if ((myMsgQId = msgQCreate (MAX_MSGS, MAX_MSG_LEN, MSG_Q_PRIORITY)) == NULL) return (ERROR); if (msgQSend (myMsgQId, MESSAGE, sizeof (MESSAGE), WAIT_FOREVER,MSG_PRI_NORMAL) == ERROR) return (ERROR); }
3.2.5 管道
管道(Pipes)提供了和消息隊列類似的另外一種進程間通訊方式。管道是一種虛擬的I/O設備,是由pipeDrv驅動管理的。創建一個管道:
status = pipeDevCreate ("/pipe/name", max_msgs, max_length);
pipeDevCreate將創建一個管道設備,以及與其相關聯的消息隊列。其參數分別分別指定了管道的名字,最大消息數,以及消息的最大長度。
管道,就是在消息隊列上面又封裝了一層,以IO設備的形式來提供服務。因此,使用管道之前,需要先創建這個虛擬的IO設備;用完之后,可以刪除這個設備;而具體的數據操作,就是調用基本IO的幾個函數。Pipes被當作一個標准的I/O設備,可以被open、close、read、write以及ioctrl等函數操作。Pipes提供Message Queue的消息排隊和阻塞機制,並能提供一個獨特的能力:與select()函數一起使用。通過select(),Pipes允許Task等待接收來自一個I/O設備集合的數據。由於select()也可以用於其他一些異步設備,因此,使用select(),Task可以等待接收多個Pipes、Sockets、Serial Ports設備集合的數據。需要注意的是不能對消息隊列使用select()操作。
那管道與消息隊列有什么區別呢?
消息隊列
- 收發時都可以指定超時
- 消息有兩種優先級
- 由內核提供,效率更高
- 直接使用show()命令就可以查看
管道
- 基於消息隊列封裝,效率略差一點點
- 使用基本IO就可以操作
- 可用於IO重定向 –ioTaskStdSet()
- 可用於select()
3.2.6 跨網絡的任務間通信
套接字sockets
VxWorks中,網絡上任務間的通信可以通過套接字接口。一個套接字就是任務間通信的端點,數據從一個套接字傳到另一個套接字上,當一個套接字創建后,就已經規定了網絡通信中數據傳輸的協議。VxWorks支持Internet中的TCP和UDP兩種協議。TCP通常作為有連接的可靠傳輸協議,而UDP則是用於無連接的數據報協議。關於socket本文不做詳細描述,具體參見“VxWorks Network Programmer’s Guide”。
遠程進程調用
RPC (Remote Procedure Call)允許在一個機器上的進程可以調用在同一個機器或遠端機器上的進程的函數(過程)。可以實現跨進程或跨CPU的遠程函數調用。RPC采用套接字作為通信機制的。
對於前面討論過的消息隊列和管道,許多實時系統都是由用戶-服務器的任務模式所組成。用戶任務向服務器任務提出服務請求后便等待回答。RPC規范了這種模式並為傳送請求和回應提供了一套標准的協議。
3.2.7 信號
VxWorks提供一個軟信號機制來異步地改變任務的執行流程。任何任務或ISR都可以向一個特定任務發送信號,接收到信號后任務當前的執行線程被掛起,執行任務指定的(或缺省的)信號處理函數,信號可以被理解為一個軟中斷,不過與ISR不同的是:信號處理函數是在任務下一次調度運行的時候執行,而ISR可以隨時打斷Task的執行。
Signals比其他任務間通信機制更適合錯誤和異常的處理。信號處理函數的處理類似ISR,因此其中不能使用可能造成阻塞的函數。由於Signals是異步的,很難確定哪個資源會不可靠,因此Signals盡量使用在ISR中可以安全使用的函數。Wind支持兩種類型的信號操作接口:UNIX-BSD方式和POSIX兼容方式,POSIX方式提供了更豐富的控制功能:
Wind所支持的兩種信號量編程接口
POSIX 1003.1b 函數 UNIX BSD函數 描述
Signal() signal() 指定特定信號的處理函數
kill() kill() 向任務發送一個信號
Raise() N/A 給自己發送信號
Sigaction() sigvec() 檢查或設置特定信號的處理函數
Sigsuspend() pause() 掛起一個任務只到一個信號發送
Sigpending() N/A 得到阻塞的信號集
Sigemptyset()Sigfillset()Sigaddset()Sigdelset()Sigismember() sigsetmask() 控制、更改信號屏蔽字
Sigprocmask() Sigsetmask() 設置一個阻塞信號的信號屏蔽字
Sigprocmask() Sigblock() 向阻塞信號集中增加信號
注:信號屏蔽字規定了當前阻塞而不能傳遞給該進程的信號集。
Signal是一種處理異常或異步改變執行流程的機制,類似於軟中斷。與POSIX兼容,VxWorks也定義了63種Signal(0為NULL Signal)。任務或ISR可以發送Signal到任務本身或其它任務;而對方可以根據Signal Mask選擇接收還是忽略該Signal。如果要接收的話,需要注冊Signal的處理函數。發送Signal時,使用的是發送者的上下文。而處理函數在處理接收到的Signal時,使用的是接收者的上下文。即使接收者阻塞或者掛起了,這個處理還是會執行的。Signal的處理函數中可以調用的系統函數有這些:
庫 |
函數 |
bLib |
All routines |
errnoLib |
errnoGet(), errnoSet() |
eventLib |
eventSend() |
fppArchLib |
fppSave(), fppRestore() |
intLib |
intContext(), intCount(), intVecSet(), intVecGet() |
intArchLib |
intLock(), intUnlock() |
logLib |
logMsg() |
lstLib |
All routines except lstFree() |
mathALib |
All routines, if fppSave()/fppRestore() are used |
msgQLib |
msgQSend() |
rngLib |
All routines except rngCreate() and rngDelete() |
semLib |
semGive() except mutual-exclusion semaphores, semFlush() |
sigLib |
kill() |
taskLib |
taskSuspend(), taskResume(), taskPrioritySet(), taskPriorityGet(), taskIdVerify(), taskIdDefault(), taskIsReady(), taskIsSuspended(), taskIsPended(), taskIsDelayed(), taskTcb() |
tickLib |
tickAnnounce(), tickSet(), tickGet() |
tyLib |
tyIRd(), tyITx() |
vxLib |
vxTas(), vxMemProbe() |
wdLib |
wdStart(), wdCancel() |
spinLockLib |
spinLockIsrTake() and spinLockIsrGive() |
vxAtomicLib |
All routines. |
vxCpuLib |
vxCpuIndexGet(), vxCpuIdGet(), vxCpuPhysIndexGet(), vxCpuIdToPhysIndex(), vxCpuPhysIndexToId(), vxCpuReservedGet(), CPU_LOGICAL_TO_PHYS(), and CPU_PHYS_TO_LOGICAL() |
fioLib |
vsprintf() and vsnprintf() |
fioBaseLib |
sprintf() and snprintf() |
ansiString |
All routines. |
Kernel里,每個Task都有針對Signal的掩碼(Mask)。掩碼值為1表示攔截該Signal,即不處理Signal;掩碼值為0表示會處理該Signal。而且默認情況下每個Task都會處理發給自己的Signal,只不過默認的處理方案是SIG_IGN(丟棄/忽略)。因此,要對Signal有所反應,就需要手動掛接Signal的處理機制了。今天看看Mask相關的操作
#include <stdio.h> //printf() #include <signal.h> // sigaction() #include <unistd.h> // pause() #include <taskLib.h>// taskName() static void myHandler( int sigNum, siginfo_t *pInfo, void *pContext) { printf ("\n從%d接收到Signal(#%d), 並附帶數值%d\n",pInfo->si_code, sigNum, pInfo->si_value.sival_int); printf("任務%s的掩碼是0x%016llx\n", taskName(0),((struct sigcontext *)pContext)->sc_mask); } void testSig() { struct sigaction newAction; sigset_t newSet; sigset_t oldSet; taskDelay(10); /* 注冊Signal處理函數到SIGUSR1 */ newAction.sa_sigaction = myHandler; newAction.sa_mask = 0; newAction.sa_flags = SA_SIGINFO; sigaction(SIGUSR1, &newAction, NULL); /* 攔截SIGUSR2 */ sigemptyset(&newSet); sigaddset(&newSet, SIGUSR2); sigprocmask(SIG_BLOCK, &newSet, &oldSet); printf("\n原掩碼是0x%016llx\n", oldSet); /* 查看當前掩碼 */ sigprocmask(0, NULL, &newSet); printf("當前掩碼是0x%016llx\n", newSet); pause(); printf("任務%s被Signal激活\n", taskName(0)); } /* 發送SIGUSR1到testSig */ void giveSig(int tId) { union sigval value; value.sival_int = 100; printf("發送Signal(#%d)到任務%s, 並附帶數值%d\n", SIGUSR1, taskName(tId), value.sival_int); sigqueue(tId, SIGUSR1, value); }
3.3 中斷服務代碼
硬件中斷處理在實時系統中具有重要的意義,因為通常都是通過中斷來通知系統外部事件的發生。為盡可能快速對中斷進行反應,VxWorks的中斷服務程序(ISRs)運行在任務上下文之外的一個特殊的上下文中。因此,中斷處理不包括任務的上下文切換。使用命令行命令isrShow,可以查看已安裝的所有ISR。Tag默認表示的是中斷向量vector,而這個vector是中斷號通過宏INUM_TO_IVEC()轉換得到的。庫intLib和intArchlIB提供的中斷程序如下所示:
- intConnect() 連接一個C程序到中斷矢量上
- intContext() 如果被中斷級調用則返回真
- intCount() 獲得當前中斷嵌套深度
- intLevelSet() 設置處理器中斷屏蔽級別
- intLock() 禁止中斷
- intUnlock() 恢復中斷
- intVecBaseSet() 設置矢量的基地址
- intVecBaseGet() 得到矢量的基地址
- intVecSet() 設置一個異常矢量
- intVecGet() 得到一個異常矢量
3.3.1 應用代碼與中斷連接
VxWorks提供例程intConnect()允許將C函數與任何中斷相連,程序的參數是連接的中斷矢量的字節偏移量,被連接的C函數的地址和傳遞到該函數的一個參數。當一個中斷發生並通過這種方法建立了一個矢量,連接的C函數在中斷級上使用先前指定的參數被調用,當中斷處理結束后,被連接的函數返回。以這種方式連接到中斷上的程序稱為中斷服務程序(ISR)。
中斷事實上不是直接指向一個C函數,而是通過intConnect()建立部分代碼用於保存必需的寄存器,根據傳遞的參數建立一個堆棧入口(既可在一個特定的中斷堆棧也可在一個當前任務的堆棧上),並且調用連接的函數。在函數返回時,它恢復寄存器和堆棧並離開中斷。
3.3.2 中斷堆棧
有些構架允許所有的ISRs使用相同的中斷堆棧,在系統啟動時系統根據配置的參數而分配和初始化堆棧,它必須足夠大到能處理最壞連接情況的嵌套中斷。在大多數架構里,所有中斷使用同一個棧。這個中斷棧是在系統啟動時初始化,其尺寸由宏ISR_STACK_SIZE定義。有些構架不允許使用一個獨立的中斷堆棧,這樣ISRs就使用中斷任務的堆棧。如果你使用這種構架,你在創建任務時必須分配足夠大的堆棧空間,以處理最壞情況的中斷嵌套和調用嵌套。請查看BSP資料看你所使用的硬件構架是否支持獨立的中斷堆棧。
中斷棧里默認進行了填充,采用checkStack()可以在開發期間查看中你的斷服務程序或任務是否已經用完了全部的堆棧空間。為了更好的性能,也可以通過VX_GLOBAL_NO_STACK_FILL取消填充。另外,還可以通過INCLUDE_PROTECT_INTERRUPT_STACK,打開中斷棧的溢出保護。
3.3.3 ISRs的一些特殊限制
許多VxWorks的函數或資源都允許ISRs訪問,但有一些重要的限制。所以最基本的一個限制就是它不准調用有可能使其阻塞的程序。首先,中斷服務程序中哪些不可以做:
1. 不能使用printf,可用logMsg代替。可以調用VxWorks的一些機制來將消息打印到系統控制台:logMsg()、kprintf()和kputs()。
2. 不能使用free,malloc(也就意味着也不能是用C++的new,delete操作符),它們都包含信號量操作,有可能使中斷阻塞
3. 不能調用任何阻塞的處理,比如semTake,taskDelay等
4. 不能進行請求處理任務上下文的操作,不能使用0作為任務id,比如調用taskSuspend(0)
5. 不能使用互斥信號量,因為其中使用了taskSafe和taskUnsafe
6. 不能使用系統的IO調用,包括寫操作。大多數的I/O設備驅動都需要任務上下文,因為他們可能阻塞調用任務以等待特定的設備。
7. 盡量不進行浮點運算處理,如果要進行浮點運算,必須顯式的保存或回復FP寄存器(fppArchLib)。
8. ISR不應該直接訪問共享數據區(shared data region)。ISR繼承它搶占的任務的內存上下文,如果該任務沒有映射該共享數據區,則它無法訪問該內存,並導致異常。為了可靠地訪問共享數據區,ISR可以將相關操作交給映射了該共享數據區的任務。
那么,中斷服務程序中哪些可以或者應該去做呢。
1. 對二值或者計數信號量,可以調用semGive或semFlush
2. 可以對內存進行讀或寫操作,包括對映射內存的訪問
3. 可以對非阻塞操作的管道或者消息隊列進行寫操作
4. 可以使用taskSuspend,taskResume等
5. 處理盡量短小,最理想的處理是只包含semGive處理,把其他處理放在中斷服務任務中處理
那么ISR中可以調用哪些機制或函數呢:
bLib |
所有函數 |
errnoLib |
errnoGet(), errnoSet() |
eventLib |
eventSend() |
fppArchLib |
fppSave(), fppRestore() |
intLib |
intContext(), intCount(), intVecSet(), intVecGet() |
intArchLib |
intLock(), intUnlock() |
logLib |
logMsg() |
lstLib |
除lstFree(),所有函數 |
mathALib |
使用fppSave()/fppRestore()時,所有函數 |
msgQLib |
msgQSend() |
rngLib |
除rngCreate()/rngDelete(),所有函數 |
pipeDrv |
write() |
selectLib |
selWakeup(), selWakeupAll() |
semLib |
semFlush(),非互斥信號量的semGive() |
semPxLib |
sem_post() |
sigLib |
kill() |
taskLib |
taskSuspend(), taskResume(), taskPrioritySet(), taskPriorityGet(), taskIdVerify(), taskIdDefault(), taskIsReady(), taskIsSuspended(), taskIsPended(), taskIsDelayed(), taskTcb() |
tickLib |
tickAnnounce(), tickSet(), tickGet() |
tyLib |
tyIRd(), tyITx() |
vxLib |
vxTas(), vxMemProbe() |
wdLib |
wdStart(), wdCancel() |
3.3.4 中斷級別的異常
當一個任務致使硬件發生異常如非法指令或總線錯誤,任務就被掛起而系統其他任務繼續運行。然而,如果一個ISR導致了這個異常,系統則沒有安全的資源來處理異常。ISR沒有上下文可以掛起。而VxWorks則把異常的描述存儲在低速存儲器的一個特定的地方並執行系統的重啟。
VxWorks的boot ROMs在低速存儲器中檢測到異常描述的出現並在系統控制台上顯示。例如:WorkQPanic:kernel work queue overflow.這個異常的出現通常是在內核從中斷級別產生非常高速的調用時發生。
3.3.5 保持高中斷級別
在某些情況下,事件需要一些低級控制例如關鍵的動作控制或系統故障反應,這需要保持最高的中斷級別來對事件實現零延時的反應。為此,VxWorks提供了一個intLockLevelSet()方法,可以將系統范圍內的中斷鎖定級別設置為一個特定的級別。
3.3.6 對高中斷級別的ISRs的一些附加限制
對於與沒有被鎖定中斷級別連接的ISR(既包括中斷級別高於intLockLevelSet()所設定的,也包括硬件規定的中斷級別是非屏蔽的)有如下限制:
- ISR只能通過intVecSet()來連接;
- ISR不能使用依靠中斷鎖定實現正確操作的任何VxWorks的操作系統的功能。
3.3.7 中斷與任務間的通信
VxWorks支持中斷處理程序與一般任務間的通訊,以下通訊方式可以用於ISR與任務級代碼間的通訊:
- 共享存儲器和環形緩沖器:ISRs可和任務級代碼共享變量、緩沖器和環形緩沖器;
- 信號量:ISRs可以歸還信號量而任務可以獲取或等待相應的信號量;
- 消息隊列:ISRs能夠發送消息到消息隊列,任務則可以取消息,若隊列已滿。則消息被丟棄,因為ISR不能阻塞。
- 管道:ISRs可以向管道寫消息讓任務讀。
- 信號(SIGNALS):ISRs可以“指示”任務導致任務的信號句柄的異步調度
- VxWorks Event。ISR可以向任務發送VxWorks Event
3.4 事件
從VxWorks 5.5開始,提供了新的任務間同步通信的機制----事件,事件可用於任務和中斷服務程序ISR之間、任務和任務之間、任務和VxWorks資源之間進行通信。 任務用函數eventReceive()來接收它關心的事件,用eventSend()來向另一個任務發送事件。(是否看似簡單?)
先來看第一個例子:
void eventRoute1(void* param) { int task_id = (int)param; eventSend(task_id , VXEV01); } void tryEvent() { int id = taskIdSelf(); taskSpawn("vxEvent1", 150, VX_FP_TASK, 30000, (FUNCPTR)eventRoute1, id,0,0,0,0,0,0,0,0,0 ); eventReceive(VXEV01, EVENTS_WAIT_ALL, WAIT_FOREVER, 0); }
在 tryEvent中啟動一個任務,然后調用 eventReceive 通過設置 EVENTS_WAIT_ALL來 等待 VXEV01 事件的 發生,子任務中調用 eventSend向 初始任務 發送 事件 VXEV01 ,從而觸發初始任務繼續執行。
從這個例子,很多人也許會覺得事件跟信號量功能一樣,沒有太多意義。其實上面體現的功能只是事件的冰山一角,讓我們慢慢來一窺全貌吧。
那么,事件有哪些特點呢?
第一,從上面的例子已經可以看出,使用事件時不需要創建任何對象,所以它是一種更輕量級的同步通信機制。因為每一個任務都有一個32位事件寄存器,其中高8位由VxWorks系統保留,開發者可以使用低24位(VXEV01~VXEV24),每一位表示一種事件。
第二,使用事件可以等待多個事件的發生,這里的多個事件發生的關系,可以是“與”也可以是“或”,在eventReceive方法中可以指定EVENTS_WAIT_ANY或EVENTS_WAIT_ALL,比如等待VXEV01或VXEV02之一的發生:。個人認為這是非常有用的一點,畢竟如果要自己實現多事件結合並可控制超時時間的話,可以說並非那么簡單,而且性能也會有問題。
eventReceive(VXEV01 | VXEV02, EVENTS_WAIT_ANY, WAIT_FOREVER, 0);
第三,除了上述例子中eventReceive和eventSend的使用外,事件還可用於任務跟VxWorks資源之間進行通信,這里所謂的資源有信號量,消息隊列等。
比如再看第二個例子(假設已創建了sem_ev1和sem_ev2兩個信號量):
void eventRoute1(void* param) { semGive(sem_ev1); } void eventRoute2(void* param) { semGive(sem_ev2); } void tryEvent() { semEvStart(sem_ev1, VXEV01, EVENTS_SEND_ONCE); semEvStart(sem_ev2, VXEV02, EVENTS_SEND_ONCE); taskSpawn("vxEvent1", 150, VX_FP_TASK, 30000, (FUNCPTR)eventRoute1, 0,0,0,0,0,0,0,0,0,0 ); taskSpawn("vxEvent2", 150, VX_FP_TASK, 30000, (FUNCPTR)eventRoute2, 0,0,0,0,0,0,0,0,0,0 ); eventReceive(VXEV01 | VXEV02, EVENTS_WAIT_ALL, WAIT_FOREVER, 0); semEvStop(sem_ev1); semEvStop(sem_ev2); }
跟資源通信,需要把對應資源注冊到特定事件,如上例中是通過 semEvStart 把信號量 sem_ev1和sem_ev2分別 注冊到事件 VXEV01和VXEV02,然后semGive信號量被釋放且可用時,對應注冊的事件會被觸發。Event是一種高效的任務間同步機制,其實它還可以與某些資源同步(信號量與消息隊列)。使用xxEvStart()可以將當前任務注冊到相應的資源上,當這個資源變為有效,且沒有其它任務阻塞在這個資源上,系統就會給這個注冊的任務發送Event。再結合Event的基本操作,就可以實現任務同時與多種資源的同步操作。
/* Copyright 2020 VxWorks567 */ #include <stdio.h> /* printf() */ #include <eventLib.h> /* eventReceive() */ #include <semEvLib.h> /* semEvStart() */ #include <msgQEvLib.h>/* msgQEvStart() */ #define MSGS_NUM 5 #define MSG_LEN 5 static SEM_ID semId; static MSG_Q_ID msgQId; void testEvent() { _Vx_event_t eventsReceived; /* 用二進制信號量表示一種資源 */ semId = semBCreate(SEM_Q_FIFO, SEM_EMPTY); /* 將當前任務注冊到該信號量 */ semEvStart(semId, VXEV01, EVENTS_OPTIONS_NONE); /* 用消息隊列表示一種資源 */ msgQId = msgQCreate(MSGS_NUM, MSG_LEN, MSG_Q_FIFO); /* 將當前任務注冊到該消息隊列 */ msgQEvStart(msgQId, VXEV02, EVENTS_OPTIONS_NONE); while(1) { /* 任一資源有效時,即進行處理 */ eventReceive(VXEV01|VXEV02, EVENTS_WAIT_ANY, WAIT_FOREVER, &eventsReceived); if(eventsReceived&VXEV01) { /* 信號量有效,且無其它任務申請信號量 */ printf("\n\tsemphore is free\n"); semTake(semId, WAIT_FOREVER); } else if(eventsReceived&VXEV02) { /* 新消息達到,且無其它任務讀取消息 */ printf("\n\tmsgQ is free\n"); msgQReceive(msgQId, NULL, 0, WAIT_FOREVER); } } } void freeSem() { /* 釋放信號量,表示對應資源有效 */ semGive(semId); } void freeMsgQ() { /* 發送消息,表示對應資源有效 */ msgQSend(msgQId, 0, 0, 0, 0); }
第四,前面說到semGive信號量被釋放且可用時,對應注冊的事件會被觸發,這里所謂可用的含義是,semGive釋放信號量同時沒有其他任務正在鎖定該資源,也就是該時間點該資源可用時,才會觸發事件。但不能保證該資源在事件被觸發后還可用,因為后續有可能又被其他任務鎖定。
第五,如果相關被注冊到事件的資源被刪除時,通過eventReceive等待該事件的任務會自動被觸發,也就是說semDelete和msgQDelete會觸發該資源注冊過事件。但是需要注意的是,當調用semDelete時,任務執行於semEvStart和eventReceive之間的話,則后續eventReceive會被阻塞。
3.5 看門狗計時器Watchdog Timers
VxWorks里有一個看門狗計時器的功能允許任何一個C函數和一段特定延時相關。看門狗計時器是ISR系統時鍾的一部分。通常看門狗計時器調用函數的運行是作為中斷級別系統時鍾的中斷服務代碼。而且,如果內核無法立即執行中斷,函數就會被放到tExcTask的工作隊列里。
一個看門狗計時器的創建通過調用wdCreate(),然后調用wdStart()啟動計時器,其參數包括:延時的ticks數,一個C函數func()以及傳給這個函數的一個參數para。當指定的ticks數到達時,上面的func()函數會被調用,這個C函數使用para作為其參數。定時器可以在到達前,隨時用函數wdCancel()取消。關於以上兩個函數的使用方法,請參照以下例程:
#define SECONDS (3) WDOG_ID myWatchDogId; task (void) { /* Create watchdog */ if ((myWatchDogId = wdCreate( )) == NULL) return (ERROR); // Set timer to go off in SECONDS - printing a message to stdout if (wdStart (myWatchDogId, sysClkRateGet( ) * SECONDS, logMsg,“Watchdog timer just expired\n”) == ERROR) return (ERROR); }
- wdCreate() 分配並初始化一個看門狗計時器
- wdDelete() 終止並釋放一個看門狗計時器
- wdStart() 啟動計時器
- wdCacel() 取消正在計時的計時器
4 VxWorks中的函數庫
4.1 TaskLib
taskLib - task management library
taskSpawn( ) - spawn a task
taskInit( ) - initialize a task with a stack at a specified address
taskActivate( ) - activate a task that has been initialized
exit( ) - exit a task (ANSI)
taskDelete( ) - delete a task
taskDeleteForce( ) - delete a task without restriction
taskSuspend( ) - suspend a task
taskResume( ) - resume a task
taskRestart( ) - restart a task
taskPrioritySet( ) - change the priority of a task
taskPriorityGet( ) - examine the priority of a task
taskLock( ) - disable task rescheduling
taskUnlock( ) - enable task rescheduling
taskSafe( ) - make the calling task safe from deletion
taskUnsafe( ) - make the calling task unsafe from deletion
taskDelay( ) - delay a task from executing
taskIdSelf( ) - get the task ID of a running task
taskIdVerify( ) - verify the existence of a task
taskTcb( ) - get the task control block for a task ID
4.2 Errno
LiberrnoLib - error status library
errnoGet( ) - get the error status value of the calling task
errnoOfTaskGet( ) - get the error status value of a specified task
errnoSet( ) - set the error status value of the calling task
errnoOfTaskSet( ) - set the error status value of a specified task
4.3 SigLib
sigLib - software signal facility library
sigInit( ) - initialize the signal facilities
sigqueueInit( ) - initialize the queued signal facilities
sigemptyset( ) - initialize a signal set with no signals included (POSIX)
sigfillset( ) - initialize a signal set with all signals included (POSIX)
sigaddset( ) - add a signal to a signal set (POSIX)
sigdelset( ) - delete a signal from a signal set (POSIX)
sigismember( ) - test to see if a signal is in a signal set (POSIX)
signal( ) - specify the handler associated with a signal
sigaction( ) - examine and/or specify the action associated with a signal (POSIX)
sigprocmask( ) - examine and/or change the signal mask (POSIX)
sigpending( ) - retrieve the set of pending signals blocked from delivery (POSIX)
sigsuspend( ) - suspend the task until delivery of a signal (POSIX)
pause( ) - suspend the task until delivery of a signal (POSIX)
sigtimedwait( ) - wait for a signal
sigwaitinfo( ) - wait for real-time signals
sigvec( ) - install a signal handler
sigsetmask( ) - set the signal mask
sigblock( ) - add to a set of blocked signals
raise( ) - send a signal to the caller’s task
kill( ) - send a signal to a task (POSIX)
sigqueue( ) - send a queued signal to a task
4.4 LstLib
lstLib - doubly linked list subroutine library
lstInit( ) - initialize a list descriptor
lstAdd( ) - add a node to the end of a list
lstConcat( ) - concatenate two lists
lstCount( ) - report the number of nodes in a list
lstDelete( ) - delete a specified node from a list
lstExtract( ) - extract a sublist from a list
lstFirst( ) - find first node in list
lstGet( ) - delete and return the first node from a list
lstInsert( ) - insert a node in a list after a specified node
lstLast( ) - find the last node in a list
lstNext( ) - find the next node in a list
lstNth( ) - find the Nth node in a list
lstPrevious( ) - find the previous node in a list
lstNStep( ) - find a list node nStep steps away from a specified node
lstFind( ) - find a node in a list
lstFree( ) - free up a list
4.5 MemLib
memLib - full-featured memory partition manager
memPartOptionsSet( ) - set the debug options for a memory partition
memalign( ) - allocate aligned memory
valloc( ) - allocate memory on a page boundary
memPartRealloc( ) - reallocate a block of memory in a specified partition
memPartFindMax( ) - find the size of the largest available free block
memOptionsSet( ) - set the debug options for the system memory partition
calloc( ) - allocate space for an array (ANSI)
realloc( ) - reallocate a block of memory (ANSI)
cfree( ) - free a block of memory
memFindMax( ) - find the largest free block in the system memory partition
版權聲明:本文為CSDN博主「種瓜大爺」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/czg13548930186/article/details/88327281