這一篇,我們將深入討論內存管理,所謂的內存管理其實就是動態內存的管理,只有動態內存才會有生命周期,又開始有結束,他才會有需要管理的問題,例如我們人類,假如是靜態的內存,從被定義開始,他就是永恒存在的,不需要管理。
在C語言中,經常使用malloc()和free()函數來動態的申請和釋放內存,這種方式是基于堆來管理內存的方式,這種方式在嵌入式實時系統中,會存在很多問題隱患:
1.頻繁的使用這種方式進行內存的分配和釋放,會把堆搞得支離破碎,并因為不能再分配更多的內存而導致應用程序崩潰。
2.基于堆得內存管理是浪費的,所有的堆管理算法必須為每個被分配的塊維護某些頭部信息,造成內存額外的開銷。
3.malloc()和free()函數在實際申請和釋放內存的時候,執行的時間是不可確定的,這意味著他們潛在的可能需要一段長的時間,這與我們的實時原則是矛盾的。
然而,堆的問題并不僅僅限于以上這些,在多線程環境中使用堆時,會引入新的問題,首先堆變成了一個共享資源,這引入了并發的問題:
1. malloc()和free()函數是不可重入的,也就是說,他們不能從多個線程被安全的調用。當然可以通過互斥體來彌補API無法重入的問題,但是涉及到互斥體,你又該考慮線程阻塞的問題以及由于互斥體的加入,是否會造成優先級反轉的問題,問題似乎變得更復雜了。
2.malloc()和free()是必須成雙入對,當你需要使用時,申請了一塊內存,當你使用完該內存時,一定要主動地去釋放他,不然它會一直占用該內存,變成了靜態內存的存在,相當于在動態內存里面開了一塊靜態的內存,當你只開一塊或者幾塊的時候,也沒問題,怕就怕,某種情況下觸發了頻繁這樣的去申請內存,而在使用完時,又沒有釋放,積少成多,你的堆就被消耗干凈了。
3.與第二種方式相反,當你申請了一塊內存以后,在應用程序還沒有使用完的時候,被意外釋放了,那么將會造成dangling指針,當應用程序再次使用該指針時你的應用程序可能崩潰。
4.堆相關的問題是出名的難以測試。
講了這么多堆的缺點,堆有個最大的優點,可以申請可變大小的內存塊,這是我們接下里介紹的內存池的方式無法提供的,凡是總是有利有弊,引入內存池是為了解決以上由于使用堆而帶來的那些問題。
為了更簡單,更高性能和更安全的使用動態內存,一個通用的方案是,定義一塊尺寸固定的堆,也被稱為內存池,對于QF框架來說,在管理動態事件時,使用內存池可能是一個更好的選擇。
先來看看使用內存池的缺點,首先他只能以一種固定尺寸的塊,但是事件的尺寸并不是統一的,所以為了支持所有的事件,塊的尺寸必須被定義為能夠支持的最大尺寸的事件,這樣會造成內存的浪費,折中的方案是定義多個不同尺寸的塊的內存池,用于支持不同尺寸的事件,QF最大支持三種不同尺寸的內存池,你可以定義小中大三種。
接下里說說他的優點:
1.內尺寸不會因為頻繁的申請和釋放變得碎片化。
2.申請的速度遠快于堆申請速度,僅需臨界區操作,避免的阻塞的情況。
3.動態的事件管理完全由QF框架掌控,他自身就有垃圾回收機制,類似java,所以只管申請,不用擔心釋放問題。
事件的管理完全交給了QF框架,那么QF需要知道以下幾件事:
1.這個事件需不需要我管理(動態還是靜態事件)。
2.這個事件來自于哪個內存池。
3.這個事件在什么情況下被釋放(釋放條件)。 以上這些內容在事件被定義時就被確認了,也就是由事件本身來維護,如圖:
關于內存池的使用有兩種情況,一種是當QF框架和其它RTOS一起使用時,他可以借用RTOS現有的消息隊列,這種情況后面講到QP+RTOS方案時,我們具體再聊,這里主要介紹一種原生的QF事件隊列,所謂原生就是由QF給出的解決方案。 首先我們來看一下QF是如何管理事件池的,其控制方案由一個控制塊+內存池的方案組成:
接下來我們看一下如何實際定義一個內存池及如何從內存池中申請內存:
1.先定義內存池。
2.初始化內存池。
3.為事件申請內存
動態事件申請內存不需要釋放,QF框架會根據實際情況來回收事件內存。