移植
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個同名系統調用是完全兼容的[3]。成功移植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三個樁函式預設是不提供的,所以也不提供這三個樁函式的實現。
特點
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)在多任務環境下的安全。
函式實現
I/O實現
Newlib在使用open、close、read和write樁函式時嚴格遵守POSIX標準,為了使實現的樁函式完全符合POSIX,就必須在內部機制上實現設備名表、檔案描述符表和驅動地址表3個表的相關操作。
4.1 三個表的結構、作用及相關操作
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。
4.3 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()函式中釋放同一個互斥信號量。