什么是Newlib
Newlib是一個面向嵌入式系統(tǒng)的C運行庫。最初是由Cygnus Solutions收集組裝的一個源代碼集合,取名為newlib,現(xiàn)在由Red Hat維護,目前的最新的版本是1.11.0[1]。
對于與GNU兼容的嵌入式C運行庫,Newlib并不是唯一的選擇,但是從成熟度來講,newlib是最優(yōu)秀的。newlib具有獨特的體系結(jié)構(gòu),使得它能夠非常好地滿足深度嵌入式系統(tǒng)的要求。newlib可移植性強,具有可重入特性、功能完備等特點,已廣泛應(yīng)用于各種嵌入式系統(tǒng)中。
可重入性的實現(xiàn)
C運行庫的可重入性問題主要是庫中的全局變量在多任務(wù)環(huán)境下的可重入性問題,Newlib解決這個問題的方法是,定義一個struct _reent類型的結(jié)構(gòu),將運行庫所有會引起可重入性問題的全局變量都放到該結(jié)構(gòu)中。而這些全局變量則被重新定義為若干個宏,以errno為例,名為“errno”的宏引用指向struct _reent結(jié)構(gòu)類型的一個全局指針,這個指針叫做_impure_ptr。
對于用戶,這一切都被errno宏隱藏了,需要檢查錯誤時,用戶只需要像其他ANSI C環(huán)境下所做的一樣,檢查errno“變量”就可以了。實際上,用戶對errno宏的訪問是返回_impure_ptr->errno的值,而不是一個全局變量的值。
Newlib定義了_reent結(jié)構(gòu)類型的一個靜態(tài)實例,并在系統(tǒng)初始化時用全局指針_impure_ptr指向它。如果系統(tǒng)中只有一個任務(wù),那么系統(tǒng)將正常運行,不需要做額外的工作;如果希望newlib運行在多任務(wù)環(huán)境下,必須完成下面的兩個步驟:
1) 每個任務(wù)提供一個_reent結(jié)構(gòu)的實例并初始化;
2) 任務(wù)上下文切換的時刻重新設(shè)置_impure_ptr指針,使它指向即將投入運行任務(wù)的_reent結(jié)構(gòu)實例。
這樣就可以保障大多數(shù)庫函數(shù)(尤其是stdio庫函數(shù))的可重入性。如果需要可重入的malloc,還必須設(shè)法實現(xiàn)__malloc_lock()和__malloc_unlock()函數(shù),它們在內(nèi)存分配過程中保障堆(heap)在多任務(wù)環(huán)境下的安全。
Newlib的移植
Newlib的所有庫函數(shù)都建立在20個樁函數(shù)的基礎(chǔ)上[2],這20個樁函數(shù)完成一些newlib無法實現(xiàn)的功能:
1) 級I/O和文件系統(tǒng)訪問(open、close、read、write、lseek、stat、fstat、fcntl、link、unlink、rename);
2) 擴大內(nèi)存堆的需求(sbrk);
3) 獲得當(dāng)前系統(tǒng)的日期和時間(gettimeofday、times);
4) 各種類型的任務(wù)管理函數(shù)(execve、fork、getpid、kill、wait、_exit);
這20個樁函數(shù)在語義、語法上與POSIX標(biāo)準(zhǔn)下對應(yīng)的20個同名系統(tǒng)調(diào)用是完全兼容的[3]。成功移植newlib的關(guān)鍵是在目標(biāo)系統(tǒng)環(huán)境下,找到能夠與這些樁函數(shù)銜接的功能函數(shù)并實現(xiàn)這些樁函數(shù)。
Newlib為每個樁函數(shù)提供了可重入的和不可重入的兩種版本。兩種版本的區(qū)別在于,如果不可重入版樁函數(shù)的名字是xxx,則對應(yīng)的可重入版樁函數(shù)的名字是_xxx_r,如close和_close_r,open和_open_r,等等。此外,可重入的樁函數(shù)在參數(shù)表中含有一個_reent結(jié)構(gòu)指針,這個指針使得系統(tǒng)的實現(xiàn)者能在庫和目標(biāo)操作環(huán)境之間傳送上下文相關(guān)的信息,尤其是發(fā)生錯誤時,能夠便捷的傳送errno的值到適當(dāng)?shù)娜蝿?wù)中。
所謂最小實現(xiàn)是指,假定將要移植的目標(biāo)系統(tǒng)中沒有文件系統(tǒng),也沒有符合POSIX標(biāo)準(zhǔn)的任務(wù)管理機制和應(yīng)用編程接口(Application Programming Interface, API),僅僅實現(xiàn)newlib的一個最小移植。在newlib的移植過程中全功能實現(xiàn)的樁函數(shù)只有open、close、read、write和sbrk五個,其他樁函數(shù)僅僅實現(xiàn)一個返回錯誤的空函數(shù)。
任務(wù)管理的execve、fork、getpid、kill、wait和_exit六個樁函數(shù),僅僅實現(xiàn)一個返回-1的空函數(shù),返回之前將errno設(shè)置為ENOTSUP,表示系統(tǒng)不支持該函數(shù)。
與文件相關(guān)的link和unlink樁函數(shù)也僅僅實現(xiàn)一個返回-1的空函數(shù),將errno設(shè)置為EMLINK表示連接過多;lseek函數(shù)則不需要返回任何錯誤,直接返回0,表示操作成功。
fstat和stat樁函數(shù)在newlib中主要用于判斷流的類型(常規(guī)文件、字符設(shè)備、目錄),將其實現(xiàn)為不論輸入?yún)?shù)如何,都返回字符設(shè)備類型的空函數(shù)。
times樁函數(shù)返回當(dāng)前進程中的各種時間信息,如果目標(biāo)系統(tǒng)中的任務(wù)不能提供類似的時間信息,僅僅實現(xiàn)一個返回-1的空函數(shù),將errno設(shè)置為ENOTSUP。
由于newlib認(rèn)為在目標(biāo)系統(tǒng)中fcntl、rename和gettimeofday三個樁函數(shù)缺省是不提供的,所以也不提供這三個樁函數(shù)的實現(xiàn)。
I/O樁函數(shù)的實現(xiàn)
Newlib在使用open、close、read和write樁函數(shù)時嚴(yán)格遵守POSIX標(biāo)準(zhǔn),為了使實現(xiàn)的樁函數(shù)完全符合POSIX,就必須在內(nèi)部機制上實現(xiàn)設(shè)備名表、文件描述符表和驅(qū)動地址表3個表的相關(guān)操作。
4.1 三個表的結(jié)構(gòu)、作用及相關(guān)操作
1) 設(shè)備名表記錄系統(tǒng)中所有設(shè)備的名字及其設(shè)備號。系統(tǒng)初始化時必須將所有的設(shè)備名及其設(shè)備號填入表中備查。
對于設(shè)備名表應(yīng)該實現(xiàn)以下兩個操作:
(1) 設(shè)備名/設(shè)備號注冊函數(shù)NameRegister;
(2) 從設(shè)備名到設(shè)備號的轉(zhuǎn)換函數(shù)NameLookup;
2) 文件描述符表記錄系統(tǒng)中當(dāng)前打開的設(shè)備的設(shè)備號。每個表項代表一個處于打開狀態(tài)的設(shè)備。每個表項的索引值就是需要返回給用戶的文件描述符。
對文件描述符表需要實現(xiàn)以下3個操作:
(1) 文件描述符分配函數(shù)FdAllocate;
(2) 文件描述符釋放函數(shù)FdFree;
(3) 從文件描述符到設(shè)備號的轉(zhuǎn)換函數(shù)Fd2DevCode;
3) 驅(qū)動地址表記錄系統(tǒng)中每個驅(qū)動程序的入口地址。每個表項代表一個驅(qū)動程序,對每個驅(qū)動程序都應(yīng)該實現(xiàn)五個具有統(tǒng)一接口的操組函數(shù):init、open、close、read、write。每個表項在表中的索引值就是該設(shè)備的設(shè)備號。需要注意是每個驅(qū)動程序都必須提供init操作。
對驅(qū)動地址表需要實現(xiàn)以下操作:
初始化驅(qū)動表中的所有驅(qū)動函數(shù)InitAllDrivers;
該操作對表中的每一個驅(qū)動程序調(diào)用init操作,完成表中所有驅(qū)動程序的初始化操作。
在系統(tǒng)初始化的時間,應(yīng)該調(diào)用InitAllDrivers()操作,完成系統(tǒng)中所有驅(qū)動程序的初始化操作。在每個驅(qū)動程序的init操作中,應(yīng)該調(diào)用NameRegister()操作,完成驅(qū)動程序?qū)?yīng)的設(shè)備注冊,以COM1驅(qū)動程序的com1_init()操作為例,它的實現(xiàn)如下:
void com1_init(int devCode)
{
/*首先注冊設(shè)備名和設(shè)備號到設(shè)備名表中*/
NameRegister(“COM1”, devCode);
/*然后完成其他的設(shè)備初始化操作*/
}
只要所有的設(shè)備驅(qū)動程序都遵守這個約定,在系統(tǒng)初始化完成之后,系統(tǒng)中所有的驅(qū)動程序就得到了初始化,并且系統(tǒng)中所有的設(shè)備都注冊到了設(shè)備名表中。后續(xù)的I/O樁函數(shù)的實現(xiàn)就非常容易了。
設(shè)備名表、文件描述符表和驅(qū)動地址表3個表的結(jié)構(gòu)及相關(guān)操作如圖1所示。
open 樁函數(shù)的實現(xiàn)
open樁函數(shù)的實現(xiàn)流程如下:
1) 用NameLookup()操作在設(shè)備名表中搜索匹配的設(shè)備名,并獲得對應(yīng)的設(shè)備號;
2) 用FdAllocate()操作從文件描述符表中分配一個空的表項,填入設(shè)備號,并獲得對應(yīng)的索引號即fd;
3) 通過設(shè)備號直接調(diào)用驅(qū)動地址表中對應(yīng)驅(qū)動程序的open操作;
4) 返回fd。
4.3 read、write和close樁函數(shù)的實現(xiàn)
read和write樁函數(shù)的實現(xiàn)方法完全相同,流程如下:
1) 調(diào)用Fd2DevCode()操作獲得與輸入?yún)?shù)fd對應(yīng)的設(shè)備號devCode;
2) 通過設(shè)備號直接調(diào)用驅(qū)動地址表中對應(yīng)驅(qū)動的read或write操作;
3) 返回實際交換的數(shù)據(jù)量。
close樁函數(shù)的實現(xiàn)與read、write幾乎完全相同,唯一不同之處在于最后調(diào)用FdFree()操作,釋放fd而不是返回實際交換的數(shù)據(jù)量,流程如下:
1) 調(diào)用Fd2DevCode()操作獲得與輸入?yún)?shù)fd對應(yīng)的設(shè)備號devCode;
2) 通過設(shè)備號直接調(diào)用驅(qū)動地址表中對應(yīng)驅(qū)動的close操作;
3) 調(diào)用FdFree()操作釋放fd。
至此,與設(shè)備I/O相關(guān)的四個樁函數(shù)open、close、read和write的實現(xiàn)就全部完成了。
本文沒有介紹驅(qū)動程序的實現(xiàn)方法,并不是驅(qū)動程序不重要,恰恰相反,驅(qū)動程序中必須完成可靠高效的設(shè)備操作,保證驅(qū)動程序的各項操作在語義上與上面4個樁函數(shù)完全一致,并且實質(zhì)性的操作都在驅(qū)動程序中完成。因此,在驅(qū)動程序的實現(xiàn)上必須仔細斟酌。由于篇幅的原因,不再贅述。
5 關(guān)于malloc
大多數(shù)嵌入式操作系統(tǒng)都實現(xiàn)了自己的動態(tài)內(nèi)存分配機制,并且提供了多任務(wù)環(huán)境下對內(nèi)存分配機制的保護措施,如果移植newlib到這樣的系統(tǒng)時,可以放棄newlib自帶的malloc函數(shù)。盡管newlib自帶的malloc非常高效,但是幾乎所有的用戶都習(xí)慣使用malloc來作為動態(tài)內(nèi)存分配器。在這種情況下,最好對系統(tǒng)自帶的動態(tài)內(nèi)存分配API進行封裝,使它不論在風(fēng)格、外觀上,還是在語義上都與malloc完全相同,這對于提高應(yīng)用程序的可移植性大有好處。
對于那些沒有實現(xiàn)動態(tài)內(nèi)存分配機制的嵌入式系統(tǒng)環(huán)境來說,newlib的malloc是一個非常好的選擇,只需實現(xiàn)sbrk樁函數(shù),malloc就可以非常好地工作起來。與之同名的POSIX系統(tǒng)調(diào)用的作用是從系統(tǒng)中獲得一塊內(nèi)存,每當(dāng)malloc需要更多的內(nèi)存時,都會調(diào)用sbrk函數(shù)。
在單任務(wù)環(huán)境下,只需實現(xiàn)sbrk樁函數(shù),malloc就可以正常運行;但在多任務(wù)環(huán)境下,還需實現(xiàn)__malloc_lock()和__malloc_unlock()函數(shù),newlib用這兩個函數(shù)來保護內(nèi)存堆免受沖擊。用戶可利用目標(biāo)環(huán)境中的互斥信號量機制來實現(xiàn)這兩個函數(shù),在__malloc_lock()函數(shù)中申請互斥信號量,而在__malloc_unlock()函數(shù)中釋放同一個互斥信號量。
評論