Understanding uC/OS-II (6)
-- Memory Management
我們知道,在 ANSI C 中可以用 malloc() 和 free() 兩個函式動態地分配記憶體和釋放記憶體。但是,在嵌入式即時操作系統中,多次這樣做會把原來 很大的一塊連續記憶體區域,逐漸地分割成許多非常小而且彼此又不相鄰的記憶體區域,也就是記憶體破碎。由於這些碎片的大量存在,使得程式到後來連非常小的記憶體也分配不到。我們曾在前面的 Task Stack 提過,用 malloc() 函式來分配堆疊時,曾經討論過記憶體破碎的問題。另外,由於記憶體管理演算法的原因,malloc() 和 free() 函式執行時間是不確定的。
在 uC/OS-II 中,kernel 把連續的大塊記憶體按分區來管理。每個分區中包含有整數個大小相同的記憶體塊,如同圖 F7.1。利用這種機制,uC/OS-II 對 malloc() 和 free() 函式進行了改變,使得它們可以分配和釋放固定大小的記憶體塊。這樣一來,malloc() 和 free() 函式的執行時間也是固定的了。
Figure 7.1 Memory partition
如圖 F7.2,在一個系統中可以有多個記憶體分割區。這樣,使用者的應用程式就可以從不同的記憶體分割區中得到不同大小的記憶體塊。但是,特定的記憶體塊在釋放時必須重新放回它以前所屬於的記憶體分割區。顯然,採用這樣的記憶體管理演算法,上面的記憶體破碎問題就得到了解決。
Figure 7.2 Multiple memory partitions
Memory Control Blocks
為了便於記憶體的管理,在 uC/OS-II 中使用記憶體控制塊 (Memory Control Blocks, MCB) 的資料結構來跟蹤每一個記憶體分割區,系統中的每個記憶體分割區都有它自己的 MCB。程式清單 L7.1 是 memory control blocks (MCB) 的定義。
程式清單 L7.1 Memory Control Block (MCB) data structure |
typedef struct { |
.OSMemAddr 是指向記憶體分割區起始位址的指標。它在建立記憶體分割區 [見 OSMemCreate()] 時被初始化,在此之後就不能更改了。
.OSMemFreeList 是指向下一個未使用的記憶體控制塊或者下一個未使用的的記憶體塊的指標,具體含義要根據該記憶體分割區是否已經建立來決定。
.OSMemBlkSize 是記憶體分割區中記憶體塊的大小,是使用者建立該記憶體分割區時指定的。
.OSMemNBlks 是記憶體分割區中總的記憶體塊數量,也是使用者建立該記憶體分割區時指定的。
.OSMemNFree 是記憶體分割區中當前可以得未使用的記憶體塊數量。
如果要在 uC/OS-II 中使用記憶體管理,需要在 OS_CFG.H 檔案中將常數定義 OS_MEM_EN 設為 1。這樣 uC/OS-II 在啟動時就會對 memory manager 進行初始化 [由 OSInit() 呼叫 OSMemInit() 來實現]。該初始化主要建立一個如圖 F7.3 所示的 MCB link list,其中的常數定義 OS_MAX_MEM_PART (見檔案 OS_CFG.H) 定義了最大的記憶體分割區數,該常數值至少要為 2。
Figure 7.3 List of free memory control blocks
Creating a partition, OSMemCreate()
在使用一個記憶體分割區之前,必須先建立該記憶體分割區。這個操作可以透過呼叫 OSMemCreate() 函式來完成。程式清單 L7.2 說明了如何建立一個含有 100 個記憶體塊、每個記憶體塊 32 位元組的記憶體分割區。
程式清單 L7.2 Creating a memory partition |
OS_MEM *CommTxBuf; |
程式清單 L7.3 是 OSMemCreate() 函式的程式碼。該函式共有 4 個參數:記憶體分割區的起始位址、分區內的記憶體塊總塊數、每個記憶體塊的位元組數和一個指向錯誤資訊程式的指標。如果 OSMemCreate() 操作失敗,它將返回一個 NULL 指標。否則,它將返回一個指向記憶體控制塊的指標。對記憶體管理的其他操作,像 OSMemGet() ,OSMemPut(),OSMemQuery() 函式等,都需要透過該指標進行。
每個記憶體分割區必須含有至少兩個記憶體塊 [L7.3(1)],每個記憶體塊至少為一個指標的大小,因為同一分區中的所有未使用的記憶體塊是由指標串聯起來的 [L7.3(2)]。接著,OSMemCreate() 從系統中的未使用的 MCB list 中取得一個 MCB [L7.3(3)],該 MCB 包含相對應記憶體分割區的程式執行期的資訊。 OSMemCreate() 必須在有未使用的 MCB 可用的情況下才能建立一個記憶體分割區 [L7.3(4)]。在上述條件均得到滿足時,所要建立的記憶體分割區內的所有記憶體塊被鏈結成一個單向 link list [L7.3(5)]。然後,在相對應的 MCB 中填寫相對應的資訊 [L7.3(6)]。完成上述各動作後,OSMemCreate() 返回指向該記憶體塊的指標。使該指標在以後對記憶體塊的操作中使用[L7.3(6)] 。
程式清單 L7.3 OSMemCreate() |
OS_MEM *OSMemCreate (void *addr, INT32U nblks, INT32U blksize, INT8U *err) |
圖 F7.4 是 OSMemCreate() 函式執行完成後,MCB 及對應的記憶體分割區和分割區內的記憶體塊之間的關係。在程式執行期間,經過多次的記憶體分配和釋放後,同一分區內的各記憶體塊之間的鏈結順序會發生很大的變化。
Figure 7.4 OSMemCreate()
Obtaining a memory block, OSMemGet()
應用程式可以呼叫 OSMemGet() 函式從已經建立的記憶體分割區中申請一個記憶體塊。該函式的唯一參數是指向特定記憶體分割區的指標,該指標是在建立記憶體分割區時,由 OSMemCreate() 函式所返回。顯然的,應用程式必須知道記憶體塊的大小,並且在使用時不能超過該容量。例如,如果一個記憶體分割區內的記憶體塊為 32 位元組,那麼,應用程式最多只能使用該記憶體塊中的 32 位元組。當應用程式不再使用這個記憶體塊後,必須及時把它釋放,重新放回相對應的記憶體分割區中 [見 OSMemPut()]。
程式清單 L7.4 是 OSMemGet() 函式的程式碼。參數中的指標pmem 指向使用者希望從其中分配記憶體塊的記憶體分割區[L7.4(1)] 。OSMemGet() 首先檢查記憶體分割區中是否有未使用的的記憶體塊[L7.4(2)] 。如果有,從未使用的記憶體塊鏈表中刪除第一個記憶體塊[L7.4(3)] ,並對未使用的記憶體塊鏈表作相應的修改 [L7.4(4)] 。這包括將鏈表頭指標後移一個元素和未使用的記憶體塊數減1[L7.4(5)] 。最後,返回指向被分配記憶體塊的指標[L7.4(6)] 。
程式清單 L7.4 OSMemGet() |
void *OSMemGet (OS_MEM *pmem, INT8U *err) (1) |
值得注意的是,使用者可以在 ISR 中呼叫 OSMemGet(),因為在暫時沒有記憶體塊可用的情況下,OSMemGet() 不會等待,而是馬上返回 NULL 指標。
Returning a memory block, OSMemPut()
當使用者應用程式不再使用一個記憶體塊時,必須及時地把它釋放並放回到原本的記憶體分割區中。這個操作由 OSMemPut() 函式完成。必須注意的是,OSMemPut() 並不知道一個記憶體塊是屬於哪個記憶體分割區的。例如,使用者的 task 從一個包含 32 位元組記憶體塊的分區中分配了一個記憶體塊,用完後,把它返還給了一個包含 120 位元組記憶體塊的記憶體分割區。當使用者應用程式下一次申請 120 位元組分區中的一個記憶體塊時,它會只得到 32 位元組的可用空間,其他 88 位元組屬於其他的 task,這就有可能使系統崩潰。
程式清單 L7.5 是 OSMemPut() 函式的程式碼。它的第一個參數 pmem 是指向 MCB 的指標,也即記憶體塊屬於的記憶體分割區 [L7.5(1)]。OSMemPut() 首先檢查記憶體分割區是否已滿 [L7.5(2)]。如果已滿,說明系統在分配和釋放記憶體時出現了錯誤。如果未滿,要釋放的記憶體塊被插入到該分區的未使用的 MCB link list 中 [L7.5(3)]。最後,將分區中未使用的記憶體塊總數加 1 [L7.5(4)]。
程式清單 L7.5 OSMemPut() |
INT8U OSMemPut (OS_MEM *pmem, void *pblk) (1) |
Obtaining status about memory partition, OSMemQuery()
在 uC/OS-II 中,可以使用 OSMemQuery() 函式來查詢一個特定記憶體分割區的有關消息。透過該函式可以知道特定記憶體分割區中記憶體塊的大小、可用記憶體塊數和正在使用的記憶體塊數等資訊。所有這些資訊都放在一個叫 OS_MEM_DATA 的資料結構中,如程式清單 L7.6。
程式清單 L7.6 Data structure used to obtain status from a partition |
typedef struct { |
程式清單 L7.7 是 OSMemQuery() 函式的程式碼,它將指定記憶體分割區的資訊複製到 OS_MEM_DATA 定義的相對應變數中。在此之前,程式首先禁止了外部中斷,防止複製過程中某些變數值被修改 [L7.7(1)]。由於正在使用的記憶體塊數是由 OS_MEM_DATA 中的局部變數計算得到的,所以,可以放在 critical section 的外面。
程式清單 L7.7 OSMemQuery() |
INT8U OSMemQuery (OS_MEM *pmem, OS_MEM_DATA *pdata) |
Using memory partitions
圖 F7.5 是一個演示如何使用 uC/OS-II 中的動態分配記憶體功能,以及利用它進行消息傳遞的例子。程式清單 L7.8 是這個例子中兩個 task 的 pseudo-code,其中一些重要程式的標號和圖 F7.5 中括弧內用數位標識的動作是相對應的。
第一個 task 讀取並檢查類比輸入量的值 (如氣壓、溫度、電壓等) ,如果其超過了一定的臨界值,就向第二個 task 發送一個消息。該消息中含有時間資訊、出錯的通道號和錯誤程式等可以想像的任何可能的資訊。
錯誤處理程式是該例子的中心。任何 task 、 ISR 都可以向該 task 發送出錯消息。錯誤處理程式則負責在顯示設備上顯示出錯資訊,在磁片上登記出錯記錄,或者啟動另一個 task 對錯誤進行糾正等。
Figure 7.5 Using dynamic memory allocation
程式清單 L7.8 Scanning analog inputs and reporting errors |
AnalogInputTask() |
Waiting for memory blocks from a partition
有的時候,在記憶體分割區暫時沒有可用的未使用的記憶體塊的情況下,讓一個申請記憶體塊的任務等待也是有用的。但是,uC/OS-II 本身在記憶體管理上並不支援這項功能。如果確實需要,則可以透過為特定記憶體分割區增加 semaphore 的方法,實現這種功能 (見 semaphore) 。應用程式為了申請分配記憶體塊,首先要得到一個相對應的 semaphore,然後才能呼叫 OSMemGet() 函式。整個過程見程式清單 L7.9。
程式碼首先定義了程式中使用到的各個變數 [L7.9(1)]。該例中,直接使用立即值定義了各個變數的大小,實際應用中,建議將這函式位定義成常數。在系統重置後,uC/OS-II 呼叫 OSInit() 進行系統初始化[L7.9(2)],然後用記憶體分割區中總的記憶體塊數來初始化一個 semaphore [L7.9(3)],緊接著建立記憶體分割區 [L7.9(4)] 和相應的要存取該分區的 task [L7.9(5)]。當然,到此為止,我們對如何增加其他的 task 也已經很清楚了。顯然,如果系統中只有一個 task 使用動態記憶體塊,就沒有必要使用 semaphore 了。這種情況不需要保證記憶體資源的互斥。事實上,除非我們要實現多工共用記憶體,否則連記憶體分割區都不需要。多工執行從 OSStart() 開始 [L7.9(6)]。當一個 task 執行期時,只有在 semaphore 有效時 [L7.9(7)],才有可能得到記憶體塊 [L7.9(8)]。一旦 semaphore 有效了,就可以申請記憶體塊並使用它,而沒有必要對 OSSemPend() 返回的錯誤碼進行檢查。因為在這裏,只有當一個記憶體塊被其他 task 釋放並放回到記憶體分割區後,uC/OS-II 才會返回到該 task 去執行。同理,對 OSMemGet() 返回的錯誤碼也無需做進一步的檢查 (一個 task 能得以繼續執行,則記憶體分割區中至少有一個記憶體塊是可用的)。當一個 task 不再使用某記憶體塊時,只需簡單地將它釋放並還回到記憶體分割區[L7.9(9)],並發送該 semaphore [L7.9(10)]。
程式清單 L7.9 Waiting for memory blocks from a partition |
OS_EVENT *SemaphorePtr; (1) |
留言列表