Newlib
Newlib 是一個用于嵌入式系統的開放來源碼的C語言程式程式庫。特點是輕型級,速度快,可攜式到很多CPU架構上。
Newlib程式庫是一個複雜的標准C程式庫,包括字串支援,浮點運算,記憶體配置(malloc),和I/O流函數(printf,fprinf(),等等)。Newlib有兩個套件搆成:libc提供了主要的c語言程式庫的實現,而libm提供了浮點運算支援。
為什么要為Newlib費心呢?如果你正在寫一個非常小的嵌入式系統,你可能有自己的C標准程式庫的子集。但是如果你要求很多的基本函數,那么一個現存的程式庫能夠幫你略過大量的工作而使你集中力量在真正的工作上。這就是Newlib的價值,你可以從Newlib中的得到100,000多行的預先寫好的經過測試的程式庫程式碼。
儘管Newlib 提供了複雜的函數集,你不用擔心它脹滿你的ROM。程式碼是非常模組化的,所以你的目標程式碼連線器將在需要的時候從程式庫中呼叫相應的功能。
發布與授權許可
Newlib程式庫的發布是來源碼級的,你在嵌入式應用中使用它前需要交叉編譯生成二進位程式庫。由于主要使用GNU GCC交叉編譯器工具鏈,所以使用GCC配置和生成二進位程式庫是一個簡單的處理過程。你也可以使用非GCC工具,但需要更多的手工配置。
Newlib 包括了從各種資源中收集來的程式碼,多個免費軟體授權的配置也反映了其起源的多樣性。基本上它有一個嵌入式系統開發者所喜愛的像BSD一樣的非限制性授權。允許你不需要公開你的來源碼來使用該程式庫。你可以從newlib home page 得到更詳細的資訊。
系統呼叫例程
Newlib依賴于少量的系統呼叫例程,你必須對依賴于系統的工作提供處理,特別是I/O支援。例如,你呼叫了printf()函數,Newlib程式庫負責建立一個格式化的字串,但是不知道在那裡或如何察看它。因此它需要呼叫_write系統例程來執行最后匯出。
int _write(int handle, char *ptr, size_t len);
當呼叫printf()的時候,”handle”將被保留其中”1”意味着stdout標准匯出,”ptr” 指向包含格式化字串的緩衝區,”len”將是格式化字串的長度。你的_write()系統呼叫例程的實現是通過一個串列除錯通訊埠傳送緩衝區內容到一個遠端的除錯終端機上察看出來。
下面是Newlib可能需要的系統呼叫例程的清單。不要被這個清單嚇住:很少需要你來實現所有這些呼叫。而且經常是傳回-1來指示一個錯誤或者一個合適的虛擬結果。
_sbrk: 改變堆的配置(對 malloc而言)
_open: 開啟檔案(基于控點的)
_close: 關閉檔案
_write: 寫檔案
_read: 讀檔案
_lseek: 重新定向檔案中的位置
_fcntl: 執行一個檔案說明符的作業
_fstat: 得到檔案狀態的控點
_stat: 按名稱得到檔案狀態
_link: 生成一個檔案鏈結(檔案命名)
_unlink: 移除目錄項
_times: 讀取時間資訊
_gettimeofday: 得到時間日期
_execve: 執行一個檔案
_kill: 殺死一個程式
_getpid: 得到程式識別
_fork: 建立一個新的程式
_wait: 等待子程式的終止
實現系統呼叫例程的最簡單方法是按照需要來實現。直接使用Newlib程式庫,直到目標程式碼連線器警告一個程式庫函數缺少了一個系統呼叫函數再去實現它。通過這樣的方法,你就不會為那些你的系統不需要的系統呼叫浪費時間了。
可重入性的實現
C執行程式庫的可重入性問題主要是程式庫中的全域變數在多工作環境下的可重入性問題,Newlib解決這個問題的方法是,定義一個struct _reent類型的架構,將執行程式庫所有會引起可重入性問題的全域變數都放到該架構中。而這些全域變數則被重新定義為若干個巨集,以errno為例,名為“errno”的巨集參照指向struct _reent架構類型的一個全域指標,這個指標叫做_impure_ptr。
對于用戶,這一切都被errno巨集隱藏了,需要檢查錯誤時,用戶只需要像其他ANSI C環境下所做的一樣,檢查errno“變數”就可以了。實際上,用戶對errno巨集的存取是傳回_impure_ptr->errno的值,而不是一個全域變數的值。
Newlib定義了_reent架構類型的一個靜態實例,并在系統起始化時用全域指標_impure_ptr指向它。如果系統中只有一個工作,那么系統將正常執行,不需要做其餘的工作;如果希望newlib執行在多工作環境下,必須完成下面的兩個步驟:
1) 每個工作提供一個_reent架構的實例并起始化;
2) 工作內文切換的時刻重新設定_impure_ptr指標,使它指向即將投入執行工作的_reent架構實例。
這樣就可以保障大多數程式庫函數(尤其是stdio程式庫函數)的可重入性。如果需要可重入的malloc,還必須設法實現__malloc_lock()和__malloc_unlock()函數,它們在記憶體配置過程中保障堆(heap)在多工作環境下的安全。
Newlib的移植
Newlib的所有程式庫函數都建立在20個樁函數的基礎上[2],這20個樁函數完成一些newlib無法實現的功能:
1) 級I/O和檔案系統存取(open、close、read、write、lseek、stat、fstat、fcntl、link、unlink、rename);
2) 擴大記憶體堆的需求(sbrk);
3) 獲得目前系統的日期和時間(gettimeofday、times);
4) 各種類型的工作管理函數(execve、fork、getpid、kill、wait、_exit);
這20個樁函數在語義、語法上與POSIX標准下對應的20個同名系統呼叫是完全相容的。成功移植newlib的關鍵是在目標系統環境下,找到能夠與這些樁函數銜接的功能函數并實現這些樁函數。
Newlib為每個樁函數提供了可重入的和不可重入的兩種版本。兩種版本的區別在于,如果不可重入版樁函數的名字是xxx,則對應的可重入版樁函數的名字是_xxx_r,如close和_close_r,open和_open_r,等等。此外,可重入的樁函數在參數表中含有一個_reent架構指標,這個指標使得系統的實現者能在程式庫和目標作業環境之間傳輸內文相關的資訊,尤其是發生錯誤時,能夠便捷的傳輸errno的值到適當的工作中。
所謂最小實現是指,假定將要移植的目標系統中沒有檔案系統,也沒有符合POSIX標准的工作管理機制和應用程式設計介面(Application Programming Interface, API),僅僅實現newlib的一個最小移植。在newlib的移植過程中全功能實現的樁函數只有open、close、read、write和sbrk五個,其他樁函數僅僅實現一個傳回錯誤的空函數。
工作管理的execve、fork、getpid、kill、wait和_exit六個樁函數,僅僅實現一個傳回-1的空函數,傳回之前將errno設定為ENOTSUP,表示系統不支援該函數。
與檔案相關的link和unlink樁函數也僅僅實現一個傳回-1的空函數,將errno設定為EMLINK表示連線過多;lseek函數則不需要傳回任何錯誤,直接傳回0,表示作業成功。
fstat和stat樁函數在newlib中主要用于判斷流的類型(常規檔案、字元裝置、目錄),將其實現為不論匯入參數如何,都傳回字元裝置類型的空函數。
times樁函數傳回目前程式中的各種時間資訊,如果目標系統中的工作無法提供類似的時間資訊,僅僅實現一個傳回-1的空函數,將errno設定為ENOTSUP。
由于newlib認為在目標系統中fcntl、rename和gettimeofday三個樁函數預設是不提供的,所以也不提供這三個樁函數的實現。
I/O樁函數的實現
Newlib在使用open、close、read和write樁函數時嚴格遵守POSIX標准,為了使實現的樁函數完全符合POSIX,就必須在內部機制上實現裝置名表、檔案說明符表和驅動位址表3個表的相關作業。
三個表的架構、作用及相關作業
1) 裝置名表記錄系統中所有裝置的名字及其裝置號。系統起始化時必須將所有的裝置名及其裝置號填入表中備查。
對于裝置名表應該實現以下兩個作業:
(1) 裝置名/裝置號註冊函數NameRegister;
(2) 從裝置名到裝置號的轉換函數NameLookup;
2) 檔案說明符表記錄系統中目前開啟的裝置的裝置號。每個表項代表一個處于開啟狀態的裝置。每個表項的索引值就是需要傳回給用戶的檔案說明符。
對檔案說明符表需要實現以下3個作業:
(1) 檔案說明符配置函數FdAllocate;
(2) 檔案說明符釋放函數FdFree;
(3) 從檔案說明符到裝置號的轉換函數Fd2DevCode;
3) 驅動位址表記錄系統中每個驅動程式的進入點位址。每個表項代表一個驅動程式,對每個驅動程式都應該實現五個具有統一介面的操組函數:init、open、close、read、write。每個表項在表中的索引值就是該裝置的裝置號。需要注意是每個驅動程式都必須提供init作業。
對驅動位址表需要實現以下作業:
起始化驅動表中的所有驅動函數InitAllDrivers;
該作業對表中的每一個驅動程式呼叫init作業,完成表中所有驅動程式的起始化作業。
在系統起始化的時間,應該呼叫InitAllDrivers()作業,完成系統中所有驅動程式的起始化作業。在每個驅動程式的init作業中,應該呼叫NameRegister()作業,完成驅動程式對應的裝置註冊,以COM1驅動程式的com1_init()作業為例,它的實現如下:
void com1_init(int devCode)
{
/*首先註冊裝置名和裝置號到裝置名表中*/
NameRegister(“COM1”, devCode);
/*然后完成其他的裝置起始化作業*/
}
只要所有的裝置驅動程式都遵守這個約定,在系統起始化完成之后,系統中所有的驅動程式就得到了起始化,并且系統中所有的裝置都註冊到了裝置名表中。后續的I/O樁函數的實現就非常容易了。
裝置名表、檔案說明符表和驅動位址表3個表的架構及相關作業如圖1所示。
open 樁函數的實現
open樁函數的實現流程如下:
1) 用NameLookup()作業在裝置名表中搜索符合的裝置名,并獲得對應的裝置號;
2) 用FdAllocate()作業從檔案說明符表中配置一個空的表項,填入裝置號,并獲得對應的索引號即fd;
3) 通過裝置號直接呼叫驅動位址表中對應驅動程式的open作業;
4) 傳回fd。
read、write和close樁函數的實現
read和write樁函數的實現方法完全相同,流程如下:
1) 呼叫Fd2DevCode()作業獲得與匯入參數fd對應的裝置號devCode;
2) 通過裝置號直接呼叫驅動位址表中對應驅動的read或write作業;
3) 傳回實際交換的資料量。
close樁函數的實現與read、write几乎完全相同,唯一不同之處在于最后呼叫FdFree()作業,釋放fd而不是傳回實際交換的資料量,流程如下:
1) 呼叫Fd2DevCode()作業獲得與匯入參數fd對應的裝置號devCode;
2) 通過裝置號直接呼叫驅動位址表中對應驅動的close作業;
3) 呼叫FdFree()作業釋放fd。
至此,與裝置I/O相關的四個樁函數open、close、read和write的實現就全部完成了。
本文沒有介紹驅動程式的實現方法,并不是驅動程式不重要,恰恰相反,驅動程式中必須完成可靠高效的裝置作業,保證驅動程式的各項作業在語義上與上面4個樁函數完全一致,并且實質性的作業都在驅動程式中完成。因此,在驅動程式的實現上必須仔細斟酌。由于篇幅的原因,不再贅述。
關于malloc
大多數嵌入式作業系統都實現了自己的動態記憶體配置機制,并且提供了多工作環境下對記憶體配置機制的保護措施,如果移植newlib到這樣的系統時,可以放棄newlib自帶的malloc函數。儘管newlib自帶的malloc非常高效,但是几乎所有的用戶都習慣使用malloc來作為動態記憶體配置器。在這種情況下,最好對系統自帶的動態記憶體配置API進行封裝,使它不論在風格、外觀上,還是在語義上都與malloc完全相同,這對于提高應用程式的可攜式性大有好處。
對于那些沒有實現動態記憶體配置機制的嵌入式系統環境來說,newlib的malloc是一個非常好的選取,只需實現sbrk樁函數,malloc就可以非常好地工作起來。與之同名的POSIX系統呼叫的作用是從系統中獲得一塊記憶體,每當malloc需要更多的記憶體時,都會呼叫sbrk函數。
在單工作環境下,只需實現sbrk樁函數,malloc就可以正常執行;但在多工作環境下,還需實現__malloc_lock()和__malloc_unlock()函數,newlib用這兩個函數來保護記憶體堆免受沖擊。用戶可利用目標環境中的互斥訊號量機制來實現這兩個函數,在__malloc_lock()函數中申請互斥訊號量,而在__malloc_unlock()函數中釋放同一個互斥訊號量。
http://www.builder.com.cn/2003/0616/89614.shtml
Newlib包含了完整的C程式庫與數學程式庫。數學程式庫的程式碼在/newlib/libm下面。
數學程式庫的根目錄下包含很多子目錄。其中,mathfp是為有浮點運算器的CPU准備的,沒有浮點運算器的CPU不需要這些程式碼。
test目錄下包含了一些測試程式
common目錄下面包含了一些通用的程式碼,例如亂數的實現。
math目錄下包含了數學函數的實現,如sin等。
machine目錄下包含了特定處理器對數學程式庫的支援,1.15.0版本只有對386的支援。
newlib關于C程式庫支援的程式碼存放在/newlib/libc下面,該目錄也包含了許多子目錄,實現不同的功能。
argz實現的功能不詳,但卻是必須的。
ctype實現ctype.h的功能,例如判斷一個asc碼的大小寫之類的函數。
errno實現errno的功能,關于它實現重入的說明已經很多了。
iconv實現編碼轉換的功能
include目錄比對關鍵,C程式庫的整個標頭檔案包含在這個目錄,但這個目錄內部的檔案需要進行調整,下面有說明。
local目錄存放與C程式庫本土化相關的內容,例如小數點象徵式的使用。雖然不常使用,這部分內容也是ieee定義的規格之一。
machine目錄包含了針對不同處理器的實現程式碼,需要用本文夾的標頭檔案交替include目錄下面的同路徑內容。
misc目錄包含了不好歸類的功能實現。
posix目錄包含了posix介面的實現程式碼,例如readdir作業。
reent包含了IO重導的程式碼實現,如果系統包含了重導的實現,則需要在編譯選項中添加 REENTRANT_SYSCALLS_PROVIDED巨集定義
search目錄包含了一些尋找算法的實現
signa目錄包含了訊號的實現
stdio目錄與stdio64包含了系統IO的實現,stdio64一般用不到
sys目錄包含了針對不同系統的實現,需要用本目錄下的檔案交替include目錄下的同名檔案。如果用ARM的話會發現sys目錄與machine目錄下都有arm這個檔案夾,但他們完成的功能不同:sys目錄下的ARM目錄使用軟中斷(半主程式庫)的方式實現IO。
syscall目錄包含了系統呼叫的實現
time目錄包含時間程式庫的實現
unix目錄是針對unix系統的一些實現。
留言列表