Solaris庫線程實現分析 初版

 
目錄
 
 

 
本文分析Posix線程(pthread),以下統一稱爲庫線程,在libc層和內核態的實現,着重於libc層。至於內核層的分析,需要參考去年寫的一些內核文檔。
主要分爲以下部分:
ü         OpenSolaris線程的分類
ü         庫線程的狀態分類
ü         庫線程的創建
ü         庫線程的運行時控制
ü         庫線程的終止
ü         庫線程終止時的清理
ü         庫線程的取消
ü         庫線程私有數據的實現
                                                     
 
 
2.      OpenSolaris線程的組成和分類
本章主要闡述OpenSolaris操作系統中線程的種類,着重於libc中的線程是如何構成的,簡要地說明內核線程的構成,以及它們之間的對應關係。
OpenSolaris中的線程與linux及其他unix系操作系統基本類似,分爲庫線程和內核線程。庫線程運行在用戶態,而內核線程則在內核態負責參與調度(純內核線程不僅僅是參與調度還要負責對內核進行一定的維護操作)
 
所謂內核線程,即指在內核空間被創建,永遠運行於內核空間,並且是整個操作系統中最基本的調度單位。
內核線程又可以分爲兩大類,
其一,爲純內核線程,比如一些daemon線程,它們不與庫線程發生任何數據交互。
其二,爲用戶口態對應的內核線程序。它們只是在調度器面前代表庫線程。參與調度器的調度活動。
 
內核線程由lwp和kthread組成,它們的分工與用戶態相似,參考庫線程及組成。
 
所謂庫線程是指創建於用戶態,主要運行時間消耗在用戶態,可以使用內核系統調用的線程。本文所說的在庫線程,主要是指OpenSolaris中的libc裏中提供的posix線程。所謂posix線程,即指該類型的線程各方面行爲特徵必須滿足POSIX標準。
 
庫線程主要由以下部分組成:thread id和ulwp。其中thread id是在系統範圍內標識一個線程。在系統調用傳遞參數時也通常使用這個id。ulwp則保存了線程具體的數據內容,例如寄存器棧等。在libc中,還有一個非常重要的概念,那就是進程全局線程數據區,它是對所有進程內庫線程的一個狀態統計,例如進程中線程key目前分配了多少,有多少zombie狀態的線程等等。
 
thread id:一個數值,代表線程的一個id號。在內核態和用戶態中都使用這個id代表一個線程。由於OpenSolaris10中庫線程和內核線程是一對一的關係,因此,thread id實際代表了一對庫線程-內核線程。
 
ulwp-上下文中包含了sigmask,stack,和寄存器棧。
typedef struct ucontext{
         unsigned long   uc_flags;
         struct ucontext *uc_link;
         unsigned long   uc_sigmask[4];
         stack_t         uc_stack;
         mcontext_t      uc_mcontext;
         long            uc_filler[23];
} ucontext_t;
 
mcontext是寄存器棧,在pthread_create->setupcontext時建立。mcontext中包含了線程的執行函數。
typedef struct {
        gregset32_t     gregs;          /* general register set */
        fpregset32_t    fpregs;         /* floating point register set */
} mcontext32_t;
 
ulwp-寄存器棧
     struct regs {
        /*
         * Extra frame for mdb to follow through high level interrupts and
         * system traps. Set them to 0 to terminate stacktrace.
         */
        greg_t r_savfp;        /* a copy of %ebp */
        greg_t r_savpc;        /* a copy of %eip */
        greg_t r_gs;
        greg_t r_fs;
        greg_t r_es;
        greg_t r_ds;
        greg_t r_edi;
        greg_t r_esi;
        greg_t r_ebp;
        greg_t r_esp;
        greg_t r_ebx;
        greg_t r_edx;
        greg_t r_ecx;
        greg_t r_eax;
        greg_t r_trapno;
        greg_t r_err;
        greg_t r_eip;
        greg_t r_cs;
        greg_t r_efl;
        greg_t r_uesp;
        greg_t r_ss;
#define r_r0    r_eax           /* r0 for portability */
#define r_r1    r_edx           /* r1 for portability */
#define r_fp    r_ebp           /* system frame pointer */
#define r_sp    r_uesp          /* user stack pointer */
#define r_pc    r_eip            /* user's instruction pointer */
#define r_ps    r_efl            /* user's EFLAGS */
 
2.2.3.   ulwp-線程私有數據
在單線程程序中,我們經常要用到"全局變量"以實現多個函數間共享數據。在多線程環境下,由於數據空間是共享的,因此全局變量也爲所有線程所共有。但有時應用程序設計中有必要提供線程私有的全局變量,僅在某個線程中有效,但卻可以跨多個函數訪問,比如程序可能需要每個線程維護一個鏈表,而使用相同的函數操作,最簡單的辦法就是使用同名而不同變量地址的線程相關數據結構。這樣的數據結構可以由Posix線程庫維護,稱爲線程私有數據(Thread-specific Data,或TSD)。具體的實現和控制方式,請參考後文<庫線程序私有數據的實現>中的詳細描述。
 
T.B.D
 
ulwp中的ul_uberdata成員(struct uberdata)代表了全局線程數據區uber (super-global) data。包含了以下內容:
ü         tsd key池
ü         tls
ü         hash table
ü         main thread
ü         ulwp_t *all_lwps;      /* circular ul_forw/ul_back list of live lwps */
ü         ulwp_t *all_zombies;   /* circular ul_forw/ul_back list of zombies */
ü         所有進程內線程的鏈表
ü         atforklist(用於存儲pthread_atfork時設置的一些fork前後需要調用的補充函數)
 
thread id和ulwp之間是用hash table聯繫起來的。thread id中包含了hash table用於定位ulwp的hash table index。TIDHASH宏用於從thread id中取出hash table index。通過這個index,從hashtable中找到ulwp。
 
hash table具體由struct uberdata 中的thr_hash_table_t *thr_hash_table來定義。在進程剛被創建時,hash table中只有一項,即[libc_init:udp->thr_hash_table = init_hash_table]。在主線程創建了第二個線程之後,hash table纔會被更新爲1024項。[finish_init:udp->thr_hash_table = htp = (thr_hash_table_t *)data;]。
 
hash table每個入口的具體定義如下:
typedef struct {
         mutex_t hash_lock;      /* lock per bucket */
         cond_t hash_cond;     /* convar per bucket */
         ulwp_t *hash_bucket;   /* hash bucket points to the list of ulwps */
         char    hash_pad[64 -   /* pad out to 64 bytes */
                 (sizeof (mutex_t) + sizeof (cond_t) + sizeof (ulwp_t *))];
 } thr_hash_table_t;
 
整個hash table的實現如下圖所示:

thr_hash_table_t _t
thr_hash_table_t _t
thr_hash_table_t t
thr_hash_table_t t
else ……
ulwp
ulwp
ulwp
Hash table for one process
 
 
 
thread id是由內核分配的。內核也是通過一個lwp hashtable來決定新的thread id。
 
如上所說,庫線程與內核線程之間的對應,主要是因爲庫線程需要在內核態有個代理,參與調度。內核線程在獲得cpu執行權利後,即會推出內核態,轉由庫線程執行。
 
那麼,庫線程與內核線程之間,是1:1還是m:n?
根據Solaris Internal的作者所言,在Opensolaris10中,放棄了m:n的策略,而採用了1:1的模型。
 
OpenSolaris中,線程模型基本如下圖所示:

ulwp
hash table
thread id
lwp
hash table
kthread
user mode
kernel mode

 
庫線程的運行狀態可以總結爲以下幾種:(由於調度器對庫線程來說是透明的,因此庫線程不存在running和ready的區分)
stopping:
在suspend某個thread時,會設置該成員爲1。代表該線程正在執行。判別方式
ulwp->ul_stopping是否爲1。
stopped:
表示該線程已經停止。判別方式ulwp->ul_stop是否爲1。
blocked:
一般是阻塞型的系統調用引起的,比如nano_sleep,lwp_wait等等。沒有具體的
判別方式。
running but cancelled:
pthread_cancel被調用,但是仍然在運行,直致運行到cancelation檢
查點,纔會退出。
running:
ul_stop != 0的情況.包含了[running but cancelled]
detached:
是指線程終止後無須其他線程的後續處理。其他線程也無法利用pthread_join來等待這個線程完成。
 
以上各種狀態之間的關係可以用下圖表示,狀態之間的轉換通過上述的方式:

 
stopping
 
stopped
 
blocked
 
detached
 
canceled
running
 

 
#pragma weak    pthread_create                  = _pthread_create
_pthread_create
      →驗證優先級有效性                          (_validate_rt_prio)
      →_thrp_create
→如果不是第一個線程,則建立完整的hashtable(1024個入口的那個)
→分配ulwp                          (find_stack,ulwp_alloc)
→設置線程運行函數                   (setup_context)
→創建內核線程__lwp_create           (系統調用)
→將lwp插入進程的ulwp隊列
→啓動thread                         (_thrp_continue)
                ->__lwp_continue(syscall)           (關於這個系統調用,請參考4.3
節的說明)
 
T.B.D
_validate_rt_prio
 
將線程的執行函數寫入上下文中,爲寄存器數組第14個元素
 
在分配完棧,校驗完優先級的正確性後,libc調用了__lwp_create
__lwp_create系統調用的實現如下:
/*
 * int
 * __lwp_create(ucontext_t *uc, unsigned long flags, lwpid_t *lwpidp)
 */
 ENTRY(__lwp_create)
 SYSTRAP_RVAL1(lwp_create)//其中,SYSTRAP_RVAL1又有三種不同的實現方法
 SYSLWPERR
 RET
 SET_SIZE(__lwp_create)
 
在內核態,響應__lwp_create調用的,是syslwp_continue例程。159號調用。
1.首先,從用戶態將lwp上下文複製到內核態(copyin)
2.調用lwp_create,創建一個內核線程。(具體內核態如何創建的,需要參考內核部分的線程文檔)。
3.將所有寄存器數值從用戶態傳進來的。
內核態和用戶態同時保存了一份寄存器列表及其中的內容。疑問:他們是如何同步的?
內核態是klwp_t *lwp→lwptoregs(lwp)進行保存。
用戶態是ulwp->uc.uc_mcontext.gregs進行保存。
4.決定並且返回新thread的ID號。
5.主要是設置thread的上下文。從當前thread(currthread)中複製。(lwp_createctx)
   thread上下文是指在調度時保存和恢復寄存器的例程。這些基本系統內有通用的實現。
   lwp的上下文才是保存了寄存器的值。
 
4.5.     啓動thread
有些線程在被創建後並不需要立刻執行,比如創建後需要重新綁定CPU等等,這樣,在內核態創建完線程後,處於stopped狀態,還需要應用程序自己啓動該線程。反過來,如果需要直接在創建後啓動線程,則應該在pthread_create中調用相關接口。
庫線程終止可以使用以下接口,它們分別是線程的主動退出和殺死線程兩種接口。
庫函數
對應的系統調用
_pthread_exit
lwp_exit
pthread_exit
lwp_exit
pthread_kill
lwp_kill
 
調用鏈
_thr_exit->_thr_exit_common->_thrp_unwind->_t_cancel->_thrp_exit
 
_thr_exit
僅僅調用_thr_exit_common
_thr_exit_common
阻塞應用程序的信號接受。
調用_thrp_unwind
_thrp_unwind
僅僅調用_t_cancel
_t_cancel
調用pthread_cleanup_push設置的析構例程。(有關cleanup的機制,請參考《庫線程終止時的清理》)
調用_thrp_exit。
_thrp_exit
如果當前線程爲最後一個非daemon線程序,則退出整個進程。
deallocate thread-specific data
deallocate thread-local storage
Free a ulwp structure
Put non-detached terminated threads in the all_zombies list
Notify everyone waiting for this thread
調用系統調用_lwp_terminate,其實是lwp_exit。
lwp_exit
釋放系統中的相關資源。
 
調用鏈
_thr_kill->__lwp_kill
lwp_kill系統調用在內核態將一個退出的信號插入線程的信號隊列中等待處理。
疑問:將退出的信號插入線程的信號隊列後是如何進行後續處理的?此外,殺死一個內核線程後,又是如何清除它所對應的庫線程的相關資源的?
 
一般來說,Posix的線程終止有兩種情況:正常終止和非正常終止。線程主動調用pthread_exit()或者從線程函數中return都將使線程正常退出,這是可預見的退出方式;非正常終止是線程在其他線程的干預下,或者由於自身運行出錯(比如訪問非法地址)而退出,這種退出方式是不可預見的。
 
不論是可預見的線程終止還是異常終止,都會存在資源釋放的問題,在不考慮因運行出錯而退出的前提下,如何保證線程終止時能順利的釋放掉自己所佔用的資源,特別是鎖資源,就是一個必須考慮解決的問題。
最經常出現的情形是資源獨佔鎖的使用:線程爲了訪問臨界資源而爲其加上鎖,但在訪問過程中被外界取消(如前文所說的cancelation),如果線程處於響應取消狀態,且採用異步方式響應,或者在打開獨佔鎖以前的運行路徑上存在取消點,則該臨界資源將永遠處於鎖定狀態得不到釋放。即,獲取鎖之後被cancelation,而cancelation由不會調用釋放鎖的例程,這樣那些鎖永遠被鎖在那裏了。
外界取消操作是不可預見的,因此的確需要一個機制來簡化用於資源釋放的編程。
 
在POSIX線程API中提供了一個pthread_cleanup_push()/pthread_cleanup_pop()函數對用於自動釋放資源--從pthread_cleanup_push()的調用點到pthread_cleanup_pop()之間的程序段中的終止動作(包括調用pthread_exit()和取消點終止)都將執行pthread_cleanup_push()所指定的清理函數。API定義如下:
void pthread_cleanup_push(void (*routine) (void *), void *arg)
void pthread_cleanup_pop(int execute)
這樣,從pthread_cleanup_push()的調用點到pthread_cleanup_pop()之間的程序段中的終止動作的執行時,都會調用pthread_cleanup_push壓入棧內的清理例程,而pthread_cleanup_pop僅僅是清除那些清理例程,設計用意並非在pthread_cleanup_pop時再執行清理例程。
 
在下面情況下pthread_cleanup_push所指定的thread cleanup handlers會被調用:
1.       調用pthread_exit
2.       相應cancel請求
3.       以非0參數調用pthread_cleanup_pop()。
有一個比較怪異的要求是,由於這兩個函數可能由宏的方式來實現,因此這兩個函數的調用必須得是在同一個Scope之中,並且配對,因爲在pthread_cleanup_push的實現中可能有一個{,而pthread_cleanup_pop可能有一個}。因此,一般情況下,這兩個函數是用於處理意外情況用的,舉例如下:
void *thread_func(void *arg)
{
    pthread_cleanup_push(cleanup, handler”)
     // do something
     Pthread_cleanup_pop(0);
    return((void *)0)
}
 
pthread_cleanup_push()/pthread_cleanup_pop()採用先入後出的棧結構管理,void routine(void *arg)函數在調用pthread_cleanup_push()時壓入清理函數棧,多次對pthread_cleanup_push()的調用將在清理函數棧中形成一個函數鏈,在執行該函數鏈時按照壓棧的相反順序彈出。execute參數表示執行到pthread_cleanup_pop()時是否在彈出清理函數的同時執行該函數,爲0表示不執行,非0爲執行;這個參數並不影響異常終止時清理函數的執行。
pthread_cleanup_push()/pthread_cleanup_pop()是以宏方式實現的,這是pthread.h中的宏定義:
#define pthread_cleanup_push(routine,arg)                                     /
 { struct _pthread_cleanup_buffer _buffer;                                   /
    _pthread_cleanup_push (&_buffer, (routine), (arg));
#define pthread_cleanup_pop(execute)                                          /
    _pthread_cleanup_pop (&_buffer, (execute)); }
 
可見,pthread_cleanup_push()帶有一個"{",而pthread_cleanup_pop()帶有一個"}",因此這兩個函數必須成對出現,且必須位於程序的同一級別的代碼段中才能通過編譯。在下面的例子裏,當線程在"do some work"中終止時,將主動調用pthread_mutex_unlock(mut),以完成解鎖動作。
pthread_cleanup_push(pthread_mutex_unlock, (void *) &mut);
pthread_mutex_lock(&mut);
/* do some work */
pthread_mutex_unlock(&mut);
pthread_cleanup_pop(0);
 
ulwp結構中有以下成員:
caddr32_t       ul_clnup_hdr;   /* head of cleanup handlers list */
這是該ulwp中保存cleanup例程的鏈表。
 
cleanup
typedef struct _cleanup {
uintptr_t       pthread_cleanup_pad[4];
} _cleanup_t;
 
一般情況下,線程在其主體函數退出的時候會自動終止,但同時也可以因爲接收到另一個線程發來的終止(取消)請求而強制終止。
線程取消的方法是向目標線程發Cancel信號,但如何處理Cancel信號則由目標線程自己決定,或者忽略、或者立即終止、或者繼續運行至Cancelation-point(取消點),由不同的Cancelation狀態決定。
線程接收到CANCEL信號的缺省處理(即pthread_create()創建線程的缺省狀態)是繼續運行至取消點,也就是說設置一個CANCELED狀態,線程繼續運行,只有運行至Cancelation-point的時候纔會退出。
 
根據POSIX標準,pthread_join()、pthread_testcancel()、pthread_cond_wait()、pthread_cond_timedwait()、sem_wait()、sigwait()等函數以及read()、write()等會引起阻塞的系統調用都是Cancelation-point,而其他pthread函數都不會引起Cancelation動作。CANCEL信號會使線程從阻塞的系統調用中退出,並置EINTR錯誤碼,因此可以在需要作爲Cancelation-point的系統調用前後調用pthread_testcancel(),從而達到POSIX標準所要求的目標,即如下代碼段:
pthread_testcancel();
retcode = read(fd, buffer, length);
pthread_testcancel();
 
在libc 中,宏PROLOGUE就是cancelation-point的執行檢查,它通常被用在各種會引起系統阻塞的調用之前,如果發現該線程已經處於要被KO的狀態,那麼直接調用lwp_kill或者pthread_exit把這個線程結果掉。

如果線程處於無限循環中,且循環體內沒有執行至取消點的必然路徑,則線程無法由外部其他線程的取消請求而終止。因此在這樣的循環體的必經路徑上應該加入pthread_testcancel()調用。

在ulwp結構中有以下成員:
ü         ul_cancel_pending
ul_cancel_pending == 1的時候,代表該線程序作爲目標線程已經被調用過pthread_cancel()。即,被人殺過一回了。該線程之所以沒有被KO掉,是因爲還沒有到cancelation的檢查點。
ul_cancel_pending == 0 的時候,代表它還好好活着。
 
ü         ul_nocancel
是否允許發生cancelation。ul_nocancel=0,允許發生cancelation。否則,不允許發生。
 
ü         ul_sigdefer
代表是否要延遲信號的處理,不爲0的場合,需要延遲信號處理,即一直運行到checkpoint。只有在ul_cancel_async被設置且ul_sigdefer不爲0的情況下信號得到同步處理。
 
全部集中在_pthread_cancel庫函數中。傳入參數爲目標庫線程的id,然後通過hash table找到相應的目標庫線程ulwp。
 
如果目標庫線程已經被殺過一次,則不進行任何操作。僅僅設置ul_cancel_pending = 1。
 
如果目標庫線程就是調用者本身,那麼首先判別信號是否要延遲處理(ul_sigdefer),如果是延遲處理,則將ul_cancel_pending設置爲1。否則立即調用do_sigcancel例程將自己KO掉。do_sigcancel最終使用_pthread_exit結果自己。
 
如果目標庫線程已經關閉了cancelation選項,也是不進行什麼操作,只是設置ul_cancel_pending = 1。
 
最後,如果目標庫線程是其他線程,則調用系統調用__lwp_kill(tid, SIGCANCEL)結果那個線程。
 
進入內核後,主要是將__lwp_kill(tid, SIGCANCEL);傳入的參數SIGCANCEL添加到內核線程的信號隊列裏去,等待被KO。具體該信號如何被處理,參考信號部分的內容。
 
int pthread_cancel(pthread_t thread)
發送終止信號給thread線程,如果成功則返回0,否則爲非0值。發送成功並不意味着thread會終止。

int pthread_setcancelstate(int state, int *oldstate)
設置本線程對Cancel信號的反應,state有兩種值:PTHREAD_CANCEL_ENABLE(缺省)和PTHREAD_CANCEL_DISABLE,分別表示收到信號後設爲CANCLED狀態和忽略CANCEL信號繼續運行;old_state如果不爲NULL則存入原來的Cancel狀態以便恢復。

int pthread_setcanceltype(int type, int *oldtype)
設置本線程取消動作的執行時機,type由兩種取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS,僅當Cancel狀態爲Enable時有效,分別表示收到信號後繼續運行至下一個取消點再退出和立即執行取消動作(退出);oldtype如果不爲NULL則存入運來的取消動作類型值。
如果是設置爲PTHREAD_CANCEL_ASYCHRONOUS並且設置之前也爲PTHREAD_CANCEL_ASYCHRONOUS,那麼這個函數也成爲了一個cancelation point。

void pthread_testcancel(void)
檢查本線程是否處於Canceld狀態,如果是,則進行取消動作,否則直接返回。
 
pthread_join提供了某一線程等待其他線程終止的功能。它最終調用lwp_wait系統調用。
 
pthread_join只能等待沒有被detach的線程,其次,根據參數不同,它等待的對象也不一樣。如果參數指定了thread id,也thread id不爲0,則該函數將等待一個特定的線程終止。如果thread id爲0,那麼它將等待該進程內部任何一個非detached線程的結束。
調用鏈:_pthread_join-> _thrp_join-> lwp_wait
 
內核態處理:
利用lwp_wait系統調用,等待目標線程結束。lwp_wait是在zombie lwp池中尋找這個被等待的線程,找到則立刻返回,否則循環等待,阻塞用戶態的執行。
 
用戶態處理:
ü         Remove ulwp from the hash table
ü         Remove ulwp from all_zombies list
 
ulwp中有如下成員:
char            ul_dead;        /* this lwp has called thr_exit */
一般情況下,進程中各個線程的運行都是相互獨立的,線程的終止並不會通知,也不會影響其他線程,終止的線程所佔用的資源也並不會隨着線程的終止而得到釋放。所以可以使用pthread_join來等待目標線程的結束。
如果進程中的某個線程執行了pthread_detach(th),則th線程將處於DETACHED狀態,這使得th線程在結束運行時自行釋放所佔用的內存資源,同時也無法由pthread_join()同步,pthread_detach()執行之後,對th請求pthread_join()將返回錯誤。
 
pthread_detach
lwp_detach
 
lwp_detach實際上是在內核態將目標lwp從hash table中刪除,這樣在lwp_join的時候,回返回一個hashtable search error的錯誤,從而引起pthread_join的失敗。
 
在庫中,可以使用lwp_continue系統調用來繼續一個線程的執行。
在內核側,響應的例程爲syslwp_continue->lwp_continue,最終調用內核的dispatch的setrun_locked例程來啓動這個線程。
 
 
私有數據由key來標記,即一個key可以代表一個tsd數據。具體的tsd數據由線程自己提供,而key則是從libc中的全局線程數據區的tsd key池中分配而來。tsd key池在代碼中由typedef struct uberdata來描述。具體可以通過ulwp->ul_uberdata進行訪問。
 
tsd key池由ul_uberdata->tsd_metadata來描述。整個系統中只有一個全局線程數據區,也只有一個tsd key池。因此,key在整個系統中都通用。而key所具體指的內容,則可以根據各個線程的不同而不同。
 
其具體定義如下:
typedef struct {
        mutex_t tsdm_lock;              /* Lock protecting the data */
        uint_t tsdm_nkeys;              /* Number of allocated keys */
        uint_t tsdm_nused;              /* Number of used keys */
        caddr32_t tsdm_destro;          /* Per-key destructors */
        char tsdm_pad[64 -              /* pad to 64 bytes */
                (sizeof (mutex_t) + 2 * sizeof (uint_t) + sizeof (caddr32_t))];
 
其中,tsdm_lock爲保護tsd池的mutex鎖。tsdm_nkeys代表系統總key的總數。tsdm_nused代表已經被佔用的key的總數。tsdm_destro是一個函數數組,爲每個key提供了析構函數。
 
pthread_key_create函數完成了這個任務。處理流程如下:
1. 獲取保護tsd key池的mutex鎖。
2. 如果系統中的key都被使用完畢,則進行擴展。Key總數規定爲2的倍乘。
3. 如果系統中的key總數超過了0x08000000,則返回EAGAIN錯誤。
4. 將tsdm_nused++作爲當前可用key返回。
 
注意點:OpenSolaris中不使用曾經被釋放的key。也就是說,一旦一個key被分配,那麼以後它要麼被廢除,要麼一直被使用,不可能出現刪除後再初始化重複使用的情況。這樣做是基於Solaris多年用戶體驗的基礎上總結出來的結論,用戶寫的程序,一般在刪除key之後,不太會再去重複利用。
 
由pthread_key_delete函數完成此任務。對tsdm_nused,tsdm_nkeys不進行任何操作,因爲被刪除的key不再被重複使用。將對應的析構例程設置爲TSD_UNALLOCATED。
 
在libc中,key只是代表某一類變量,而對於不同的線程,具體這個key代表的變量的內容是什麼,這就是tsd value的事了。將key值作爲index,然後在ulwp中的指針數組裏尋找對應的元素,對應的指針即指向每個線程的具體不同的實現。
由於指針也是佔用空間的,而且系統中可以有0x8000000個key,因此,不可能將所有的key的指針都以靜態變量的方式存放在棧上。因此出現了保存在棧上的tsd和保存在動態分配的內存上的tsd。
 
由於棧中保存的tsd使用起來非常方便,不需要通過內存操作,這樣也就避免了很多鎖操作,因此速度比較快。因此在OpenSolaris中又被稱爲fast thread specific data。ftsd一共可以有9個key。除去第0個key被規定無效以外,實際可以有8個key。
對應的數據結構:
Ulwp->void *ul_ftsd[TSD_NFAST]
 
而通過內存分配而保存的tsd數據操作起來比較慢,因此在OpenSolaris中又被稱爲slow thread specific data。
對應數據結構:
ulwp->ul_stsd->tsd_data
具體定義如下:
typedef union tsd32 {
uint_t tsd_nalloc;              /* Amount of allocated storage */
caddr32_t tsd_pad[TSD_NFAST];
caddr32_t tsd_data[1];
線程可以通過pthread_setspecific函數來設置key相應的tsd value,根據key的值,可以判定該tsd是屬於fast tsd還是slow tsd(<9?)。而pthread_getspecific可以獲取key相應的tsd value。
 
 
 
 
 

 
 
一些這次要調查的接口不太好分類,羅列並且分析在此。
對應文件:src/lib/libc/port/gen/atfork.c
當線程調用fork的時候,整個進程的地址空間都被copy(嚴格來說是copy-on-write)到child。所有Mutex / Reader-Writer Lock / Condition Variable的狀態都被繼承下來。子進程中,只存在一個線程,就是當初調用fork的進程的拷貝。由於不是所有線程都被copy,因此需要將所有的同步對象的狀態進行處理。(如果是調用exec函數的話沒有這個問題,因爲整個地址空間被丟棄了)處理的函數是pthread_atfork:
 
#include <pthread.h>
 int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
 
返回0表示正常,出錯時返回錯誤值
Prepare:在fork創建child進程之前,在parent進程中調用。職責是:獲得所有的鎖。
_prefork_handler在fork1執行fork系統調用之前被調用。
 
Parent:在fork創建child進程之後,但在fork調用返回之前,在parent進程中調用。職責是:釋放在prepare中獲得的所有的鎖。
_postfork_parent_handler也是由fork1調用。
 
Child:在fork創建child進程之後,在fork調用返回值前,在child進程中調用。職責是:釋放在prepare中獲得的所有的鎖。看起來child和Parent這兩個handler做的是重複的工作,不過實際情況不是這樣。由於fork會make一份進程地址空間的copy,所以parent和child是在釋放各自的鎖的copy
_postfork_child_handler也是由fork1調用。
 
POSIX中不提供對pthread_atfork註冊函數的[取消註冊]接口。但是,solaris能保證,如果這個庫被整體卸載,那麼其事前註冊的例程都會失效。
 
 
這個函數不可以從fork的handler中再調用(即atfork註冊的handler)。因爲這樣會沒完沒了的循環調用。這由fork_lock_enter("pthread_atfork")語句進行判斷。
 
typedef struct atfork {
struct atfork *forw;            /* forward pointer */
struct atfork *back;            /* backward pointer */
void (*prepare)(void);          /* pre-fork handler */
void (*parent)(void);           /* post-fork parent handler */
void (*child)(void);            /* post-fork child handler */
} atfork_t;
可見,在libc中保存了一個atfork結構的連表。
此函數將各個接口都保存在一個atfork_t中,然後通過*forw 和*back 將這個結構鏈入當前線程的atforklist 中。這個結構通過lmalloc分配,內存不足,則返回ENOMEM。
 
本函數與內核態實現無關,_prefork_handler,_postfork_parent_handler,以及_postfork_child_handler都是在用戶態由libc的fork處理函數調用的。
 
對應文件:src/lib/libc/port/threads/pthr_attr.c
 
 
 
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章