輕鬆控制 uClinux 嵌入式開發過程

       uClinux是目前比較普及的嵌入式Linux版本之一,它的功能很多,並且隨着低成本、可運行uClinux的32位CPU的激增,以及uClinux首次成爲Linux 2.6內核的一部分,uClinux將更加流行(如圖1)。下面討論一下開發者使用uClinux時如何控制開發過程,以及將會遇到的與普通Linux的不同之處。

  圖1 uClinux運行在Palm上

  應用無內存管理

  uClinux與普通Linux系統的主要區別就是它沒有內存管理。在普通Linux下,通過使用虛擬內存(VM)來實現內存管理。虛擬內存一般是通過內存管理單元(Memory Management Unit,簡稱MMU)來實現,而在uClinux的世界裏,經常可以看到“NOMMU”這個詞。

  在有虛擬內存的情況下,所有的進程都在相同的地址空間運行,由虛擬內存系統處理虛擬內存到物理內存的映射。因此,即使進程看到的虛擬內存是連續的,它所佔的物理內存也可能是分散的,有的甚至被交換到了硬盤。因爲物理內存能映射到進程地址空間的任何位置,所以這種環境下能夠向正在運行的進程添加內存。

  在沒有虛擬內存的情況下,每個進程必須被分配到固定的內存位置。由於一個進程的上、下(內存位置)都可能有別的進程在運行,所以它通常不能動態擴展內存。這就是說,在uClinux下運行的進程不能在運行過程中動態增加可用內存,這與傳統Linux下的情況有所不同。

  對於uClinux開發者來說,分配內存是一個棘手的問題,並且由於沒有任何形式的內存保護,任何應用程序或內核都可能破壞系統。更爲糟糕的是,無意識的誤操作不會引人注意,造成要跟蹤隨機的、進程間的破壞非常困難。但是這些缺陷對於uClinux來說幾乎不算問題,這是因爲使用uClinux的系統一般沒有硬盤驅動器和足夠的內存,完全沒有必要做複雜的管理和交換。

 

做足內存映射

  對於內核開發者,uClinux與普通Linux區別很小。惟一真正會遇到的問題是uClinux內核開發者不能利用MMU提供的分頁支持,比如,依賴虛擬內存的tmpfs文件系統在uClinux下就不起作用。類似的,普通Linux下的標準可執行文件格式uClinux都不支持,因爲它們都要利用虛擬內存的特性。uClinux需要一種新的格式——Flat,它是一種壓縮的可執行文件格式,只保存可執行的代碼和數據,以及將可執行程序裝載到內存時所需要的重定位信息。

  理解uClinux內核中內存映射的實現方式也是很有必要的,因爲有些方式在uClinux系統上行不通,理解內存映射的實現後可以避免使用這些方式。uClinux要求內存映射能夠直接在文件系統中指到文件,從而保證它是順序的和連續的,否則就必須事先爲文件分配好內存,並把數據拷貝到分配給它的內存塊上。

  因此,uClinux下有效內存映射的用法要素非常明確:首先,當前惟一能夠保證文件連續存儲的文件系統是ROM文件系統(Romfs),所以必須使用Romfs來避免傳統內存分配;其次,只有只讀的內存映射能夠被共享,也就是說,爲了避免傳統內存分配,映射必須是隻讀的。由於這些原因,uClinux下的開發者不能利用“Copy-on-Write”特性。

  要將設備驅動程序移植到uClinux環境,需要做一些修改,這並不是因爲內核上的區別,而是由於與硬件細節相關部分有所不同造成的。比如,普通Linux下,SMC網絡驅動程序可以支持ISA SMC卡。該驅動程序是16位的,並且一般都分配到0x3ff以下的I/O地址空間。

  但是用來支持SMC卡的非ISA嵌入式版本,驅動程序要求運行在8位、16位或32位模式下都是可能的,並且在滿32位的I/O地址中,中斷號一般要高於ISA的最大值16。所以,與硬件細節相關的部分可能還是要做一些移植工作。

 

恰當的內存分配

  uClinux除了提供跟普通Linux一樣的內存分配器之外,還提供另一個可選的。普通Linux中缺省的內存分配器是使用“2的冪”的分配方法,這樣可以快速找到符合要求的內存區域。不幸的是,在uClinux下這種方法可能會帶來令人痛苦的結果。

  爲了理解這一問題帶來的結果,尤其是大的內存分配,我們舉例說明。試想一個應用程序要求33KB的內存空間進行裝載。如果使用“2的冪”的分配方法,就必須分配64KB(2的6次方)內存空間,多餘的31KB內存空間不能被利用上。在uClinux中,這種浪費是不能接受的。爲了解決這個問題,專門爲uClinux內核設計了可選的內存分配器。不同的內核版本,這個可選的內存分配器不同,一般是page_alloc2和kmalloc2。

  page_alloc2能解決缺省的分配方法造成的浪費問題。雖然它也是使用“2的冪”的分配方法,但它是按頁(每頁4096字節,即4KB)分配的,分配的內存大小如果已經滿足了要求,則只是將當前的一頁分配出去,其它的就不再分配。在前面的例子中,如果使用這種方法,就只是分配36KB(≥33KB,且爲整頁)即可,這樣就能節省28KB的空間。

  page_alloc2還採取了一些避免內存碎片的方法。它將所有的兩頁(8KB)或更少的內存需求從空閒內存開始部分向上分配,所有大的內存需求從剩餘內存的末尾部分開始向下分配。這樣防止了網絡緩存等的臨時分配,避免了內存碎片的出現。

  一旦開發者理解了內核內存分配的區別,應用程序中就會出現變化。

  1.沒有動態棧的問題

  在使用虛擬內存的Linux上,當一個應用程序試圖沖銷棧頂單元時,會被標記異常,同時系統會映射新的內存到棧頂以便讓棧增長。在uClinux下,由於必須在編譯階段給棧分配好內存,所以不會有這樣的增長。當出現莫名其妙的崩潰或者新移植的應用程序出現怪異行爲時,開發者首先應該考慮到的是給棧分配的內存大小問題。缺省情況下,uClinux爲棧分配4KB的內存空間,開發者可以用下面提到的方法之一來增加棧的空間。

◆ 應用程序build之前

  應用程序build之前,可以在Makefile文件中增加以下兩行代碼:

  FLTFLAGS = -s

  export FLTFLAGS

  ◆ 應用程序build之後

  應用程序build之後,可以運行以下命令:

  flthdr -s executable

  其中,stacksize 就是爲棧增加的內存空間。

  2.沒有動態堆的問題

  堆是C語言中malloc及相關函數分配內存的區域。在有虛擬內存的Linux上,應用程序可能通過動態堆在運行過程中改變進程的大小。這個功能是通過在底層使用sbrk()和brk()系統調用來實現的。sbrk()是在進程的末尾增加內存空間,所以調用sbrk()能夠使應用程序獲得額外的內存。

  brk()可以把任意位置設置爲進程空間的末尾,因此,可以通過調用brk()減少或增加內存空間的佔用。由於uClinux不能實現brk()和sbrk(),它採用了一個全局的內存池,就是內核的空閒內存池。使用全局內存池的方法有一些優點。

  首先,此方法只會給進程分配使用時真正需要的內存。其次,內存用完後就會被歸還給全局內存池,而且可以利用已經存在的內核中的分配器來分配內存,這樣可以減少應用程序的代碼量。但這個方法是有缺陷的,比如,一個失控的進程可以用完系統全部的可用內存。

  新手普遍會遇到丟失內存的問題。系統會顯示大量的可用內存,但是應用程序卻不能得到。這正是由於內存碎片的存在,uClinux幾乎不可能完全利用內存,現有的解決方法中都存在這個問題。這個問題可用一個例子很好地說明。

  假設一個系統有500KB的空閒內存,爲了裝載一個應用程序需要分配100KB的空間。大家可能覺得這個需要肯定能得到滿足,然而,應該知道,必須有100KB連續的內存空間才能滿足這個需要。如果有500KB的空閒空間,但是最大的連續內存塊的大小隻有80KB,這樣是沒有辦法分配給這個應用程序的。造成這種情況有很多原因。上面講到的page_alloc2內核分配器有一個配置選項可以用來識別這個問題,在內核源代碼page_alloc2.c文件中可以獲得更多的信息。

 

     經常有人會問爲什麼不能進行內存的碎片整理,以便實現剛纔的例子中的要求?原因是uClinux沒有虛擬內存,所以不能移動程序正在使用的內存。在使用虛擬內存的情況下,只要重新定位就能實現內存的移動,從而實現內存碎片的整理。

  在沒有虛擬內存的情況下,由於程序經常會引用已經分配給它的內存區域,這樣,如果移動程序的內存,程序就會崩潰。在uClinux下,現在還沒有解決這個問題的辦法。開發者需要自己注意這個問題,如果有可能的話,儘量使用小的內存塊。

  掌控進程和應用程序

  1.進程

  有虛擬內存的Linux和uClinux的另一個區別在於後者沒有fork()系統調用。這就要求開發者在移植時對使用了fork()的應用程序做一些工作。uClinux下惟一的選擇是使用vfork()。儘管vfork()與fork()有很多共同點,但是它們之間的區別影響很大。

  對於不熟悉fork()和vfork()的人來說,這兩個系統調用都是允許將一個進程分裂成一個父進程和一個子進程。當一個進程調用fork()時,子進程是父進程的一個完全拷貝,但是它不共享父進程的任何東西,並且能夠單獨執行,就和父進程一樣。vfork()調用就不同了,首先,父進程被掛起直到子進程調用exec(),或者子進程退出才能繼續。

  由此可見,這個系統調用是用來啓動一個新的應用程序。其次,子進程在vfork()返回後直接運行在父進程的棧空間,並使用父進程的內存和數據。這意味着子進程可能破壞父進程的數據結構或棧,造成失敗。

  爲了避免這些問題,需要確保一旦調用vfork(),子進程就不從當前的棧框架中返回,並且如果子進程改變了父進程的數據結構就不能調用exit函數。子進程還必須避免改變全局數據結構或全局變量中的任何信息,因爲這些改變都有可能使父進程不能繼續。

 

通常,如果應用程序不是在fork()之後立即調用exec(),就有必要在fork()被替換成vfork()之前做仔細的檢查。

  2.應用程序

  儘管uClinux的Flat可執行格式並不會直接影響應用程序和它們的執行,但是它允許許多普通Linux下的ELF可執行格式所不允許的選項。比如,Flat可執行格式帶來兩個衍生系統—完全重定位和位置無關代碼(Position-Independent Code,簡稱PIC)的變體。完全重定位系統將對應用程序的代碼和數據進行重定位,而PIC系統通常只需要對數據進行部分重定位。

  對嵌入式開發者最有用的特性就是運行時空間大小不變(Execute-In-Place,簡稱XIP)。這樣應用程序可以直接從閃存(Flash)或ROM中運行,因爲只需要應用程序所需佔用的內存即可。不是所有的uClinux平臺都實現了XIP,因爲它需要編譯器的支持以及Flat可執行格式的PIC形式。

  uClinux下的Romfs是惟一支持XIP的文件系統。要實現XIP,應用程序就必須被連續地裝載到文件系統。 Flat格式還在它的頭部定義了應用程序的棧大小。要增加分配給應用程序的棧,只需要簡單地修改該部分,可以使用flthdr命令實現,格式如下:

  flthdr -s flat-executable

  Flat格式還允許整個可執行文件被壓縮,以儘量縮小佔用ROM的空間。它還有一個次要的作用就是使應用程序完全地裝載到一個連續的RAM塊中。既想節省ROM空間,又想使用XIP的時候,還可以選擇Data-Segment-Only壓縮形式。

  生成一個完全壓縮的可執行文件:

  flthdr -z flat-executable

  只是生成壓縮數據段:

  flthdr -d flat-executable

  特別小心共享庫

  uClinux下的共享庫各有不同。目前可用的解決方法需要修改編譯器,並需要開發者特別小心。其實,當前的uClinux發行版本中提供了uC-libc和uClibc庫,最好的方法是以這兩個庫爲例子來創建自己的共享庫。

 

  另外,uClinux下的共享庫必須是Flat格式的可執行文件,並且要真正實現共享,必須實現XIP。如果不實現XIP,共享庫就會爲每個使用它的應用程序創建一份拷貝,這還不如使用靜態鏈接應用程序。

  小結

  uClinux趨向於更深入的嵌入式系統,它需要更少的內存,並可直接在ROM上運行。如果初次在uClinux下開發的人遇到沒有硬件驅動、有嚴格的資源限制,以及沒有內存保護等一系列的情況,最好的入手方法就是使用uClinux仿真器(見圖2)。

  圖2 uClinux仿真器Xcopilot

  強調以上這些問題有助於開發者提前做好準備,避免在uClinux下工作時常遇到陷阱和誤解。

     

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章