范小鷗
(吉林建筑工程學(xué)院計(jì)算機(jī)科學(xué)與工程學(xué)院,長春 130118)
線程技術(shù)早在20世紀(jì)60年代已提出,但真正將多線程應(yīng)用到操作系統(tǒng)中還是在20世紀(jì)80年代中期[1-2].現(xiàn)在,多線程技術(shù)已被許多操作系統(tǒng)所支持,包括Windows NT/2000和Linux.
線程是進(jìn)程中的一個(gè)實(shí)體,是被系統(tǒng)獨(dú)立調(diào)度和分派的基本單位,線程自己不擁有系統(tǒng)資源,只擁有一點(diǎn)在運(yùn)行中必不可少的資源,但其可與同屬一個(gè)進(jìn)程的其他線程共享進(jìn)程所擁有的全部資源.一個(gè)線程可創(chuàng)建和撤消另一個(gè)線程,同一進(jìn)程中的多個(gè)線程之間可以并發(fā)執(zhí)行.由于線程之間的相互制約,致使線程在運(yùn)行中呈現(xiàn)出間斷性.線程也有就緒、阻塞和運(yùn)行3種基本狀態(tài)[3].
在操作系統(tǒng)設(shè)計(jì)上,從進(jìn)程演化出線程,目的是更好地支持SMP以及減小(進(jìn)/線程)上下文切換開銷.線程與進(jìn)程相比主要有以下優(yōu)點(diǎn)[4]:
(1)線程和進(jìn)程相比,它是一種非?!肮?jié)儉”的多任務(wù)操作方式.在linux系統(tǒng)下,啟動(dòng)一個(gè)新的進(jìn)程必須分配給它獨(dú)立的地址空間,建立眾多的數(shù)據(jù)表來維護(hù)它的代碼段、堆棧段和數(shù)據(jù)段,這是一種“昂貴”的多任務(wù)工作方式.而運(yùn)行于一個(gè)進(jìn)程中的多個(gè)線程,它們彼此之間使用相同的地址空間,共享大部分?jǐn)?shù)據(jù),啟動(dòng)一個(gè)線程所花費(fèi)的空間遠(yuǎn)遠(yuǎn)小于啟動(dòng)一個(gè)進(jìn)程所花費(fèi)的空間,而且,線程間彼此切換所需的時(shí)間也遠(yuǎn)遠(yuǎn)小于進(jìn)程間切換所需要的時(shí)間;
(2)線程間方便的通信機(jī)制.對不同進(jìn)程,它們具有獨(dú)立的數(shù)據(jù)空間,要進(jìn)行數(shù)據(jù)的傳遞只能通過通信的方式進(jìn)行,這種方式不僅費(fèi)時(shí),而且很不方便.線程則不然,由于同一進(jìn)程下的線程之間共享數(shù)據(jù)空間,所以一個(gè)線程的數(shù)據(jù)可以直接為其他線程所用,這不僅快捷,而且方便.當(dāng)然,數(shù)據(jù)的共享也帶來其他一些問題,有的變量不能同時(shí)被兩個(gè)線程所修改;有的子程序中聲明為static的數(shù)據(jù);更有可能給多線程程序帶來災(zāi)難性的打擊,這些正是編寫多線程程序時(shí)最需要注意的地方;
(3)提高應(yīng)用程序響應(yīng).這對圖形界面的程序尤其有意義,當(dāng)一個(gè)操作耗時(shí)很長時(shí),整個(gè)系統(tǒng)都會等待這個(gè)操作,此時(shí)程序不會響應(yīng)鍵盤、鼠標(biāo)、菜單的操作,而使用多線程技術(shù),將耗時(shí)長的操作(time consuming)置于一個(gè)新的線程,可以避免這種尷尬的情況;
(4)使多CPU系統(tǒng)更加有效.操作系統(tǒng)會保證當(dāng)線程數(shù)不大于CPU數(shù)目時(shí),不同的線程運(yùn)行于不同的CPU上;
(5)改善程序結(jié)構(gòu).一個(gè)既長又復(fù)雜的進(jìn)程,可考慮分為多個(gè)線程,成為幾個(gè)獨(dú)立或半獨(dú)立的運(yùn)行部分,有利于程序的可讀性和可維護(hù)性.
線程機(jī)制Linux Threads定義了一個(gè)struct_pthread_descr_struct數(shù)據(jù)結(jié)構(gòu)來描述線程,并使用全局?jǐn)?shù)組變量_pthread_handles來描述和引用進(jìn)程所轄線程.在_pthread_handles中的前兩項(xiàng),Linux Threads定義了兩個(gè)全局的系統(tǒng)線程:_pthread_initial_thread和_pthread_manager_thread,并用_pthread_main_thread表征_pthread_ manager_thread的父線程(初始為_pthread_initial_thread).struct_pthread_descr_struct是一個(gè)雙環(huán)鏈表結(jié)構(gòu),_ pthread_manager_thread所在的鏈表僅包括它一個(gè)元素,實(shí)際上,_pthread_manager_thread是一個(gè)特殊線程,Linux Threads僅使用了其中的errno,p_pid,p_priority等3個(gè)域.而_pthread_main_thread所在的鏈則將進(jìn)程中所有用戶線程串在了一起.
Linux Threads所采用的是線程一進(jìn)程“一對一”模型(用一個(gè)核心進(jìn)程(也許是輕量進(jìn)程)對應(yīng)一個(gè)線程,將線程調(diào)度等同于進(jìn)程調(diào)度,交給核心完成),調(diào)度交給核心,而在用戶級實(shí)現(xiàn)一個(gè)包括信號處理在內(nèi)的線程管理機(jī)制.“一對一”模型的好處之一是線程的調(diào)度由核心完成了,而其他諸如線程取消、線程間的同步等工作,都是在核外線程庫中完成的.在Linux Threads中,專門為每一個(gè)進(jìn)程構(gòu)造了一個(gè)管理線程,負(fù)責(zé)處理線程相關(guān)的管理工作.當(dāng)進(jìn)程第一次調(diào)用pthread_create()創(chuàng)建一個(gè)線程的時(shí)候,就會創(chuàng)建(clone())并啟動(dòng)管理線程.
在一個(gè)進(jìn)程空間內(nèi),管理線程與其他線程之間通過一對“管理管道(manager_pipe)”來通訊,該管道在創(chuàng)建管理線程之前創(chuàng)建,在成功啟動(dòng)了管理線程之后.管理管道的讀端和寫端分別賦給兩個(gè)全局變量pthread_ manager_reader和_pthread_manager_request之后,每個(gè)用戶線程都通過pthread_manager_request向管理線程發(fā)請求,但管理線程本身并沒有直接使用_pthread_manager_reader,管道的讀端(manager_pipe[0])是作為_ clone()的參數(shù)之一傳給管理線程的,管理線程的工作主要是監(jiān)聽管道讀端,并對從中取出的請求作出反應(yīng).創(chuàng)建管理線程的流程為:
初始化結(jié)束后,在_pthread_manager_thread中,記錄了輕量級進(jìn)程號(輕量級線程(LWP)是一種由內(nèi)核支持的用戶線程.它是基于內(nèi)核線程的高級抽象,因此只有先支持內(nèi)核線程,才能有LWP),以及核外分配和管理的線程id,id=2*PTHREAD_THREADS_MAX+1這個(gè)數(shù)值不會與任何常規(guī)用戶線程id沖突.管理線程作為pthread_create()的調(diào)用者線程的子線程運(yùn)行,而pthread_create()所創(chuàng)建的那個(gè)用戶線程則是由管理線程來調(diào)用clone()創(chuàng)建,因此實(shí)際上是管理線程的子線程(此處子線程的概念應(yīng)該當(dāng)作子進(jìn)程來理解)._ pthread_manager()就是管理線程的主循環(huán)所在,在進(jìn)行一系列初始化工作后,進(jìn)入while(1)循環(huán).在循環(huán)中,線程以2s為timeout查詢(_poll())管理管道的讀端.在處理請求前,檢查其父線程(也就是創(chuàng)建manager的主線程)是否已退出,如果已退出就退出整個(gè)進(jìn)程.如果有退出的子線程需要清理,則調(diào)用pthread_reap_children()清理.然后才是讀取管道中的請求,根據(jù)請求類型執(zhí)行相應(yīng)操作(switch-case).
在Linux Threads中,管理線程的棧和用戶線程的棧是分離的,管理線程在進(jìn)程堆中通過malloc()分配一個(gè)THREAD_MANAGER_STACK_SIZE字節(jié)的區(qū)域作為自己的運(yùn)行棧.用戶線程的棧分配辦法隨著體系結(jié)構(gòu)的不同而不同,主要根據(jù)兩個(gè)宏定義來區(qū)分,一個(gè)是NEED_SEPARATE_REGISTER_STACK,這個(gè)屬性僅在IA64平臺上使用;另一個(gè)是FLOATING_STACK宏,在i386等少數(shù)平臺上使用,此時(shí)用戶線程棧由系統(tǒng)決定具體位置并提供保護(hù).與此同時(shí),用戶還可以通過線程屬性結(jié)構(gòu)來指定使用用戶自定義的棧.
每個(gè)Linux Threads線程都同時(shí)具有線程id和進(jìn)程id,其中進(jìn)程id就是內(nèi)核所維護(hù)的進(jìn)程號,而線程id則由LinuxThreads分配和維護(hù)._pthread_initial_thread的線程id為PTHREAD_THREADS_MAX,_pthread_ manager_thread的是:2*PTHREAD_THREADS_MAX+1,第一個(gè)用戶線程的線程id為PTHREAD_THREADS _MAX+2,此后第n個(gè)用戶線程的線程id遵循以下公式:
tid=n*PTHREAD_THREADS_MAX+n+1
這種分配方式保證了進(jìn)程中所有的線程(包括已經(jīng)退出)都不會有相同的線程id,而線程id的類型pthread_t定義為無符號長整型(unsigned long int),也保證了有理由的運(yùn)行時(shí)間內(nèi)線程id不會重復(fù).從線程id查找線程數(shù)據(jù)結(jié)構(gòu)是在pthread_handle()函數(shù)中完成的,實(shí)際上只是將線程號按PTHREAD_THREADS_ MAX取模,得到的就是該線程在pthread_handles中的索引.
雖然linux中線程實(shí)現(xiàn)機(jī)制日趨成熟且廣泛應(yīng)用,但仍存在不足.比如說,由于計(jì)算線程本地?cái)?shù)據(jù)的方法是基于堆棧地址的位置,因此,對于這些數(shù)據(jù)的訪問速度都很慢.由于Linux Threads是圍繞一個(gè)管理線程來設(shè)計(jì)的,因此會導(dǎo)致較多上下文切換的開銷,這可能妨礙系統(tǒng)的可伸縮性能.由于線程的管理方式及每個(gè)線程都使用了一個(gè)不同的進(jìn)程ID,因此Linux Threads和其他與Posix相關(guān)的線程庫并不兼容.總之,隨著linux內(nèi)核的發(fā)展,其中所使用的線程機(jī)制也將不斷改進(jìn)與完善.
[1]李建軍,陳鴻星,張紅新,張 威.Linux線程庫的實(shí)現(xiàn)機(jī)制[J].計(jì)算機(jī)與現(xiàn)代化,2005(4):98-100.
[2]鄭燕飛,余海燕.Linux的多線程機(jī)制探討與實(shí)踐[J].計(jì)算機(jī)應(yīng)用,2001(1):83-85.
[3]金惠芳,陶利民,張基溫.Linux下多線程技術(shù)分析及應(yīng)用[J].計(jì)算機(jī)系統(tǒng)應(yīng)用,2003(9):30-31.
[4]周 麗,焦程波,蘭巨龍.LINUX系統(tǒng)下多線程與多進(jìn)程性能分析[J].微計(jì)算機(jī)信息,2005(17):123-124.