摘要:NPTL(本地POSIX線程庫)具有較好的性能和穩定性,已成為Glibc的首選線程庫。同步是為了達到多線程協同工作目的而設計的一種機制。線程庫中同步函數的底層實現大多用匯編語言編寫,因此,在將NPTL移植到不同嵌入式硬件平臺時,必須重寫相關代碼。通過引入一個虛擬的鎖設備,實現原子操作,進而實現同步函數,最小化硬件平臺的相關性,方便地實現不同嵌入式平臺上NPTL的同步機制。
關鍵詞:線程;同步;原子操作
中圖分類號:TP316文獻標識碼:A文章編號:1009-3044(2008)31-0880-02
Implementation of Thread Library's Synchronization Mechanism in Embedded System
JIANG Lan-fan
(Software College, Fuzhou University, Fuzhou 350108, China)
Abstract: NPTL(Native Posix Thread Library) have better performance and stability, and have already become the preferred thread library of Glibc. Synchronization is a Mechanism designed to harmonize works between multithread. The bottom implementation of synchronization functions are mostly programmed with assembly language, therefore, related coeds should be rewritten when migrating NPTL to different hardware platforms. Atomic action can be implemented by introducing a virtual lock device, furthermore, synchronization functions can be accomplished. Relativity of hardware platform can be minimized in this way, therefore, NPTL's synchronization Mechanism can be expediently realized in different embedded platforms.
Key words: thread; synchronization; atomic action
1 引言
NPTL(Native Posix Thread Library)[1]使用Linux2.6內核的新特性重寫了Linux的線程庫,取代歷史悠久而備受爭議的LinuxThreads成為Glibc的首選線程庫[2]。與傳統的LinuxThreads線程庫相比,它在性能和穩定性方面都進行了重大的改進。
目前,NPTL支持的硬件平臺有限,尤其在嵌入式領域,普遍使用的Linux線程庫仍是LinuxThreads。因此,研究NPTL在嵌入式系統中移植的關鍵技術具有一定的現實意義。
多個線程并發執行能極大地提高程序的運行效率[3]。但線程之間資源共享的機制也使得多個線程經常需要同時對某一資源進行訪問,形成對共享資源的訪問沖突[4]。解決沖突的一個好辦法就是在設計中引入同步機制。線程庫中同步函數的底層實現大多用匯編語言編寫,因此,在將NPTL移植到不同嵌入式硬件平臺時,必須重寫相關代碼。
通過引入一個虛擬的鎖設備,以系統調用的方式進入核心態以實現原子操作,進而實現同步函數,將硬件平臺相關性最小化,使得移植過程中能夠方便地實現NPTL的同步機制。
2 原子操作
所謂原子操作,就是該操作絕對不會在執行完畢前被任何其它任務或事件打斷,也就是說,它是最小的執行單位。原子操作的三個步驟是:讀數據、修改數據、然后重新寫入新數據。原子操作是同步機制實現的基礎。
設計中主要在線程庫中實現3個原子操作函數,其定義及功能說明如下:
1) int cmpxchg(volatile void *ptr, int old, int new);
此函數比較old值和ptr所指的內存中的值,如果相等,把ptr所指的內存中的值設置為new;如果不相等,則不進行這個設置操作。無論是否進行這個設置操作,函數的返回值都是ptr所指的內存中原來的數值。
2) int xchg(int x, void *ptr);
此函數不進行比較操作,直接將ptr所指的內存地址中的值設為x,并返回ptr所指的內存地址中原來的數值。
3) int atomic_dec_and_test(int *ptr);
此函數原子地將ptr所指內存地址中的值減去1,如果得到的值為0,則返回1,其他情況都返回0。
2.1 原子操作的核心實現
在系統中添加一個虛擬設備文件“lock”,使用該設備文件的ioctl系統調用進入核心態,通過傳入參數的不同便可分別實現3個原子操作函數。
2.1.1 虛擬設備驅動的實現
嵌入式Linux操作系統使用數據結構file-operation為所有的設備文件都提供了統一的操作函數接口。不同類型的文件有不同的file-operation成員函數。每個進程對設備的操作最終都會轉換成對file-operations結構的訪問。在驅動程序中,需根據功能要求,完成file-operations結構中各函數的實現,不需要的函數接口可以直接在file-operations結構中初始化為NULL[5]。file_operations變量會在驅動程序初始化時,注冊到系統內部,當操作系統對設備進行操作時,會調用驅動程序注冊的file-operations結構中的函數,以實現相應功能。
由于并不對應一個具體的硬件設備,因此在初始化操作中,并不需要向系統申請資源,只需向系統注冊該字符設備即可。驅動程序的open、close函數不做任何操作,直接返回0。驅動中file_operations結構的實現如下:
static struct file_operations lock_fops = {
ioctl: lock_ioctl,
open: lock_open,
release: lock_close,
};
2.1.2 ioctl的實現
ioctl是設備驅動程序中對設備的I/O通道進行管理的函數。所謂對I/O通道進行管理,就是對設備的一些特性進行控制,例如串口的傳輸波特率等。ioctl函數是文件結構file_operations中的一個屬性分量,如果在驅動程序中提供了對ioctl的支持,用戶就可以在用戶程序中使用ioctl函數控制設備的I/O通道。核心驅動程序中的ioctl函數的實現形式為:
int lock_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg);
在ioctl函數中,一般用一個switch-case結構來實現,每一個case對應參數cmd傳入的一個命令碼,做出一些相應的操作。在核心驅動程序中定義的命令碼要與應用程序中定義的命令碼一致,以保證命令的正確發送。對應不同的cmd,所傳入的參數個數不同。若應用程序所需傳入的參數個數大于1,在用戶程序中調用ioctl時,可將所需傳入的參數放入一個數組中,此時arg表示用戶程序中參數數組的起始地址。實現所對應的偽碼為:
定義全局命令碼;
int lock_ioctl(…)
{
Switch(cmd){
case cmpxchg()函數的調用:
關中斷;
將各項參數從用戶空間挎貝到內核空間;//可用copy_from_user函數實現
從指定內存中將值挎貝至內核空間;//可用get_user函數實現
if(指定內存中的值與old的值相等) //cmpxchg(ptr, old, new)
將new值挎貝至用戶空間指定內存;//可用put_user函數實現
開中斷;
return 指定內存中的原值;
case xchg()函數的調用:
關中斷;
將各項參數從用戶空間挎貝到內核空間;
從指定內存中將值挎貝至內核空間;
將用戶指定的值挎貝至用戶指定地址;
開中斷;
return 指定內存中的原值;
case atomic_dec_and_test()函數的調用:
關中斷;
從指定內存中將值挎貝至內核空間;
將得到的值減1;
將減1后的值挎貝至用戶指定內存;
開中斷;
if(減1后的值為0)return 1;
elsereturn 0;
}}
2.2 原子操作函數的實現
核外線程庫中的3個原子操作函數都是通過對“lock”設備文件執行ioctl系統調用實現的,主要過程大體相似,只是在使用各原子操作函數時,所需指定的參數個數不同。因此,在調用ioctl時,所需傳入核心的參數個數若大于1個,則將各參數封裝進一個數組中,再傳入數組的首地址;若只需傳入1個參數,則直接傳入該參數的地址即可。具體實現偽碼為:
定義與核心實現相對應的全局命令碼;
int 原子操作函數(…)
{
用數組保存需要的各項參數;
利用open系統調用打開設備文件“/dev/lock”;
用不同的命令碼對已打開的設備文件調用ioctl,并保存ioctl的返回值;
關閉設備文件;
返回相應ioctl調用的返回值;
}
3 同步操作的實現
同步操作是基于原子操作及Linux 2.6核心的futex(快速用戶空間互斥體)機制實現的。用于同步操作的全局變量有3個狀態:0代表沒有上鎖,即資源可用;1代表上了鎖即資源已被占用,但沒有線程在等待此資源;2代表有一個或多個線程在等待這個資源被釋放。采用lll_lock(int *val)/lll_unlock(int *val)函數對來實現NPTL中的同步操作。
3.1 lll_lock()函數的實現
對于lll_lock(int *val)函數,針對不同的*val值,將進行不同的操作:
1) *val==0:在這種情況下,表示資源可用,即當前線程可以獲得這把鎖,同時要把*val設置為1。
2) *val==1:當前線程被阻塞并進入內核睡眠。在這種情況下,意味著資源已被其它線程占用,那么當前線程只能等待。此時,將*val設置為2,然后利用futex系統調用將當前線程掛起。當前線程將在“*val==2”這個futex值上睡眠,直到另外一個線程使用futex系統調用將其喚醒。
3) *val==2:和*val=1情況基本一致,也是進入內核睡眠,只是不需要再設置*val的值。
lll_lock()函數實現的偽碼如下:
lll_lock(int *val)
{
if(cmpxchg(val,0,1)) /*若*val的值為0,則表示此資源可用,上鎖成功*/
{ /*若*val的值不為0,則表示此資源已被占用*/
if(*val的原值為1)
c=xchg(2,val); //原子地將val的值置為2,表示有線程在等待此資源
do{
利用futex系統調用將當前線程在val為2時掛起;
c=xchg(2,val);
} while(c)//循環等待直至val的值為0
}}
3.2 lll_unlock()函數的實現
與lll_lock()函數類似,在lll_unlock()函數中,針對不同的*val值,也將進行不同的操作:
1) *val==2:在這種情況下,表示有線程在等待此資源,此時進行解鎖操作,即將*val的值置0,同時利用futex系統調用喚醒在val上等待的一個線程。
2) *val==1:在這種情況下,表示沒有線程在等待此資源,此時只需進行解鎖操作,不需用futex系統調用進入內核。
lll_unlock()函數實現的偽碼如下:
lll_unlock(int *val)
{
if(!atomic_dec_and_test(val))
{//*val減1后不為0,即*val原值為2,表示有線程在此資源上等待
xchg(0, val);//解鎖操作,原子地將*val的值置0
利用futex系統調用喚醒在val上等待的一個線程;
}}
4 結束語
同步是為了達到多線程協同工作目的而設計的一種機制。通過添加一個虛擬的鎖設備,利用該設備文件的ioctl系統調用,根據傳入參數的不同實現不同的原子操作,進而在應用層實現同步函數,將硬件平臺相關性最小化,使得移植過程中能夠方便地在嵌入式平臺上實現NPTL線程庫的同步機制。
參考文獻
[1] Drepper U,Molnar I.The Native Posix Thread Library for Linux[EB/OL].http://people.redhat.com/drepper/nptl-design.pdf.
[2] 楊沙洲.Linux 線程庫性能測試與分析[EB/OL].http://www.ibm.com/developerworks/cn/linux/l-nptl/index.html.
[3] 劉學超,楊宏偉,李玉霜.Java中同步線程的實現[J].商場現代化,2007(4):69-71.
[4] 黃丹,邵惠鶴.基于Windows CE平臺的多線程編程[J].微計算機信息,2007,23(12):53-55.
[5] 譚躍,蔣新華.Linux中字符設備驅動程序開發的研究[J].福建電腦,2007(11):86-87.