摘要:在Linux2.6中,NPTL(native posix thread library)已取代LinuxThreads成為glibc的線程庫,但是在嵌入式操作系統中普遍使用的基于POSIX 標準的線程庫仍是LinuxThreads。分析了NPTL線程庫的內存管理機制,基于嵌入式操作系統uClinux無MMU的特性,修改了線程棧及uClibe庫,實現了NPTL在uClinux上的移植,并在兼容性與效率兩方面相對于LinuxThreads線程庫進行了測試。
0 引言
與進程相比,線程是一種非常“節儉”的多任務操作方式且線程間擁有更加方便的通信機制 。因此,線程的引入對于日趨復雜的操作系統而言意義重大。
目前,在嵌入式操作系統中普遍使用的基于POSIX標準的線程庫是LinuxThreads。雖然這種實現機制已經在不少的應用當中表現出了較好的性能,但仍存在一定的問題。NPTL(nativeposixthread Ubrary) 是RedHat公司牽頭研發的新一代線程庫,它在一定程度上彌補了LinuxThreads的缺點。本文將實現NPTL在嵌入式操作系統uClinux上的移植。
1 NPTL內存管理機制分析
uClinux同標準Linux的區別就在于內存管理。對于uClinux來說,其設計針對沒有MMU的處理器,所以uClinux采用實存儲器管理策略,所有程序中訪問的地址都是實際的物理地址。根據uClinux的特點,NPTL的移植工作將主要集中在內存管理上。
NPTL定義了一個struct pthread數據結構來描述線程。在此數據結構中與內存管理相關的幾項屬性有:
Struct pthread{
……
list tlist;/*用于將線程棧鏈入一個雙循環鏈表中*/
bool user stack;/*標識線程是否為用戶自定義方式*/
void *stackblock;/*線程棧起始地址*/
size_t stackblock_size;/*線程棧大小*/
size_t guardsize;/*保護區大小*/
……
};
為了節省開銷,NPTL采用了以下兩項措施來優化內存管理。
(1)合并必要的內存塊。線程描述數據結構與線程局部存儲都放在堆棧上,可用的堆棧從這兩個結構向下開始(如果是向上的堆棧,則從滿足這兩個結構的內存向上開始)。線程棧的分配方法隨著體系結構的不同而不同,這里只分析i386等平臺上(包括一般嵌入式平臺)所使用的兩種棧組織方式:系統分配方式和用戶自定義方式。
在系統分配方式下,NPTL利用mmap()建立一定大小的從物理內存空間到進程虛存區間的映射,并使用mprotect()設置其中頁為非訪問區,用來監測棧溢出。其線程棧空間的功能分配如圖1所示。
圖1 棧結構
對于用戶自定義方式,按照用戶所指定的地址與大小,計算出線程棧頂。在這種方式下并不調用mprotect()進行保護,正確性由用戶自己保證。
(2)緩存已結束線程的線程棧。內存處理,尤其是存儲單元的分配比較慢,因此,用于堆棧和線程描述數據結構的內存塊在線程結束時并未馬上釋放,而是保留在特定隊列中。NPTL中為每個進程維護著3個靜態隊列:
(1)stack_cache:已結束線程的線程棧緩存隊列;
(2)stack_used:未結束線程的線程棧隊列,其中的線程都是在系統分配方式下創建;
(3)stack user:未結束線程的線程棧隊列,其中的線程都是在用戶自定義方式下創建。
與這些隊列相關的靜態變量有:
(1)stack_cache_maxsize:stack cache隊列大小的上限;
(2)stack_cache_actsize:當前stack_cache隊列的大小。
這些隊列都采用雙循環鏈表結構,利用線程描述數據結構中的list_t list鏈接。隊列結構如圖2所示。
圖2 線程棧隊列
系統分配方式下,在創建線程時,首先嘗試從stack_cache隊列中找出一個合適的棧結構,將其用作新線程的棧結構而無需重新分配。若stack_cache隊列為空或找不到合適的棧結構,再調用mmap函數進行分配。在對stack_cache隊列進行操作時,要注意調整stack_cache_actsize的大小。若stack_cache_actsize超過stack_cache_maxsize的限制,就要從stack_cache隊列的尾部開始,釋放一部分內存塊。通過這種方法,避免了內存塊的頻繁分配與釋放,而且在線程結束時,線程描述結構中的某些信息仍處于有用狀態,當這些棧結構被重用時就不用重新初始化這些信息,節約了系統時間,提高了系統效率。
2 基于uClinux的移植
2.1 線程棧的分配
基于無MMU特性,uClinux重新定義封裝了原有的內存分配函數。mmap()系統調用終由內核函數kmalloc()實現,分配一塊物理內存區,返回的是該物理內存區的起始地址。uClibc中所定義的mallocO函數不再以系統調用brk()實現,而是以設置好特定參數的mmap()實現。因此,在將NTPL移植到uClibc中時,可直接調用malloc()函數分配指定大小的線程棧,相應地,用free()函數進行內存的釋放。由于uClinux對內存空間沒有保護,因此在調用malloc分配了線程棧空間后,并不調用mproteet()設置保護區。這樣,程序的正確性就不得不由開發人員來保證。根據嵌入式操作系統的特點,在分配線程棧時可將其默認大小_default_stacksize的值設為16K。修改后的線程棧空間的功能分配如圖3所示。
圖3 修改后的棧結構
2.2 與線程棧隊列相關的修改
在線程庫中,頻繁地使用了THREAD_SELF宏,此宏用于表示當前運行線程的線程描述數據結構指針。由于提供了對TLS(thread local storage)支持,在i386中,通過寄存器GS可以很容易地得到當前運行線程的線程描述數據結構指針。但在uCliux中,并未實現對TLS的支持,因此,要重新定義THREAD_SELF宏。這主要是通過在stack_used和_stack_user兩個隊列中尋找包含當前運行線程堆棧地址的線程棧結構來實現的。HREAD_SELF宏定義的算法如下:
#define THREAD_SELF \
{
struct pthread *results=NULL;
CURRENT_STACK_FRAME /*利用硬件寄存器取出當前運行線程堆棧地址*/
list_for_each(entry,&stack_used)
{/*從隊列頭開始遍歷stack_used隊列 */
struct pthread *curr;
/*利用結構成員list在數據結構struct pthread中的偏移量,計算出其屬主數據結構struct pthread的地址 */
curr=list_entry(entry,struct pthread,list);
if(CURRENT_STACK_FRAME>=curr->stackblock&&
CURRENT_STACK_FRAME<(curr->stackblock+curp->stackblock_size))
{/*找到當前運行線程的線程棧結構 */
result=curr;
return result;
}
}
list_for_each(entry, &_stack_user)
{ /*按上述步驟從隊列頭開始遍歷stack_user隊列 */
……
}
return result;
)
NPTL中利用靜態變量stack_cache_maxsize限定了stack_cache的容量。在i386體系中stack_cache_maxsize的大小定義為40MB。顯然,這對于uClinux而言是不可能實現的,應針對具體系統資源進行適當調整。
2.3 c庫的修改
uClinux小型化的另一個做法是精簡了應用程序庫,這使得線程庫中一些重要的系統調用在uClibc中缺少必要的接口,而無法得以實現。在本系統中,需要增加clone與futex兩個系統調用接口到uClibc中,以保證線程庫的正確運行。在NPTL中利用靜態變量stack_cache_lock與lll_lock()/lll_unlock()這對宏保證了線程棧隊列操作的原子性。在glibc中,lll_lock()/lll_unlock()終是通過核心的futex(Fast User Space Mutex)機制實現的。Futex是一種序列化事件使得它們不會相互沖突的機制,它能令調用者在內核中等待,也可以在中斷或在超時以后被喚醒。在具體實現時需重定義這對宏,并添加到uClibc中。lll_lock()/lll_unlock()的宏定義算法如下:
#define lll_lock(stack_cache_lock) \
{
atomic inc(stack_cache_lock); /*將stack_cache_lock原子地加1 */
int val= stack_cache_lock;
/*若此時stack_cache_lock不等于1,說明已有別的線程在對隊列進行操作,則利用系統調用futex掛起當前線程等待 */
if(val !=1)
futex(&stack_cache_lock,FUTEX_WAIT,val);
}
#define lll_unlock(stack_cache_lock) \
{
atomic dec(stack_cache_lock); /*將stack_cache_lock原子地減1*/
/*若stack_cache_lock不等于0,說明還有別的線程在等待,則利用系統調用futex喚醒一個等待線程 */
if(stack_cache_lock!=0)
futex(&stack_cache_lock,FUTEX_WAKE,1);
}
3 性能測試與分析
測試的硬件平臺采用了ADI公司的ADSP-BF533處理器,32MB的內存,4MB大小Flash。軟件平臺選用uClinux2.6內核。
3.1 兼容性
在將NPTL移植到uClinux上后,就其POSIX兼容性進行了一系列測試,主要結果如表1所示。
表1 兼容性測試結果對照
從表1中的結果可以看出,在NPTL中已經可以實現線程組的概念,而且特定信號的發送將會影響整個進程,這樣就可以實現對多線程進程的工作控制。雖然因為核心的限制,NPTL仍然不是100%POSIX兼容的,但相對LinuxThreads已經有很大程度上的改進了。
3.2 效率
為了進行對比分析,本測試將分別完成對NPTL與LinuxThreads兩種線程庫在線程創建/銷毀時間上的統計。測試程序交替創建一定量線程,創建的線程不進行任何實際操作,創建后即刻返回。在主線程中調用pthread_ join函數來等待子線程返回。利用clock0函數來統計所消耗的處理機時間,clock()返回值的單位為微秒。
將測試程序分別靜態編譯鏈接LinuxThreads線程庫與NPTL線程庫,并將生成的可執行程序置于目標板上運行。經過多次測試并平均后:LinuxThreads線程庫線程的創建/銷毀時間約為153 us,而NPTL線程庫線程的創建/銷毀時間約為68us。
可見,NPTL 在線程創建/銷毀方面的開銷要明顯低于LinuxThreads。這主要是因為NPTL不再像LinuxThreads那樣需要使用用戶級的管理線程來維護線程的創建和銷毀,同時,stack cache隊列的應用使得在多個線程交替運行時內存塊的分配與釋放不再那么頻繁。
4 結束語
由于嵌入式系統軟硬件條件上的限制,使得NPTL的高效性并不能完全得以體現。但相對于LinuxThreads而言,在性能上仍有一定的提高。更重要的是,NPTL相對于LinuxThreads在很大程度上改進了與POSIX 的兼容性。可以預見,隨著Linux在嵌入式領域的擴展,NPTL也將在嵌入式系統中發揮越來越重要的作用。