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