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一樣的記憶體 Clerk之外,還提供另一個可選的。普通Linux中預設的記憶體 Clerk是使用“2的冪”的分配方法,這樣可以快速找到符合要求的記憶體地區。不幸的是,在uClinux下這種方法可能會帶來令人痛苦的結果。
為了理解這一問題帶來的結果,尤其是大的記憶體配置,我們舉例說明。試想一個應用程式要求33KB的記憶體空間進行裝載。如果使用“2的冪”的分配方法,就必須分配64KB(2的6次方)記憶體空間,多餘的31KB記憶體空間不能被利用上。在uClinux中,這種浪費是不能接受的。為瞭解決這個問題,專門為uClinux核心設計了可選的記憶體 Clerk。不同的核心版本,這個可選的記憶體 Clerk不同,一般是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下工作時常遇到陷阱和誤解。