Solaris庫執行緒實現分析 初版
目錄
本文分析Posix執行緒(pthread),以下統一稱為庫執行緒,在libc層和核心態的實現,著重於libc層。至於核心層的分析,需要參考去年寫的一些核心文件。
主要分為以下部分:
ü OpenSolaris執行緒的分類
ü 庫執行緒的狀態分類
ü 庫執行緒的建立
ü 庫執行緒的執行時控制
ü 庫執行緒的終止
ü 庫執行緒終止時的清理
ü 庫執行緒的取消
ü 庫執行緒私有資料的實現
2. OpenSolaris執行緒的組成和分類
本章主要闡述OpenSolaris作業系統中執行緒的種類,著重於libc中的執行緒是如何構成的,簡要地說明核心執行緒的構成,以及它們之間的對應關係。
OpenSolaris中的執行緒與linux及其他unix系作業系統基本類似,分為庫執行緒和核心執行緒。庫執行緒執行在使用者態,而核心執行緒則在核心態負責參與排程(純核心執行緒不僅僅是參與排程還要負責對核心進行一定的維護操作)
所謂核心執行緒,即指在核心空間被建立,永遠執行於核心空間,並且是整個作業系統中最基本的排程單位。
核心執行緒又可以分為兩大類,
其一,為純核心執行緒,比如一些daemon執行緒,它們不與庫執行緒發生任何資料互動。
其二,為使用者口態對應的核心執行緒序。它們只是在排程器面前代表庫執行緒。參與排程器的排程活動。
2.1.1. 核心執行緒的組成(lwp-kthread)
核心執行緒由lwp和kthread組成,它們的分工與使用者態相似,參考庫執行緒及組成。
所謂庫執行緒是指建立於使用者態,主要執行時間消耗在使用者態,可以使用核心系統呼叫的執行緒。本文所說的在庫執行緒,主要是指OpenSolaris中的libc裡中提供的posix執行緒。所謂posix執行緒,即指該型別的執行緒各方面行為特徵必須滿足POSIX標準。
庫執行緒主要由以下部分組成:thread id和ulwp。其中thread id是在系統範圍內標識一個執行緒。在系統呼叫傳遞引數時也通常使用這個id。ulwp則儲存了執行緒具體的資料內容,例如暫存器棧等。在libc中,還有一個非常重要的概念,那就是程式全域性執行緒資料區,它是對所有程式內庫執行緒的一個狀態統計,例如程式中執行緒key目前分配了多少,有多少zombie狀態的執行緒等等。
thread id:一個數值,代表執行緒的一個id號。在核心態和使用者態中都使用這個id代表一個執行緒。由於OpenSolaris10中庫執行緒和核心執行緒是一對一的關係,因此,thread id實際代表了一對庫執行緒-核心執行緒。
2.2.2. ulwp-上下文
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 {
greg_t r_savfp; /* a copy of %ebp */
greg_t r_savpc; /* a copy of %eip */
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;
#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系統呼叫在核心態將一個退出的訊號插入執行緒的訊號佇列中等待處理。
疑問:將退出的訊號插入執行緒的訊號佇列後是如何進行後續處理的?此外,殺死一個核心執行緒後,又是如何清除它所對應的庫執行緒的相關資源的?
6. 庫執行緒終止時的清理(cleanup)
一般來說,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;
7. 庫執行緒取消(cancelation)
一般情況下,執行緒在其主體函式退出的時候會自動終止,但同時也可以因為接收到另一個執行緒發來的終止(取消)請求而強制終止。
執行緒取消的方法是向目標執行緒發Cancel訊號,但如何處理Cancel訊號則由目標執行緒自己決定,或者忽略、或者立即終止、或者繼續執行至Cancelation-point(取消點),由不同的Cancelation狀態決定。
執行緒接收到CANCEL訊號的預設處理(即pthread_create()建立執行緒的預設狀態)是繼續執行至取消點,也就是說設定一個CANCELED狀態,執行緒繼續執行,只有執行至Cancelation-point的時候才會退出。
執行緒接收到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();
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則存入運來的取消動作型別值。
傳送終止訊號給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狀態,如果是,則進行取消動作,否則直接返回。
void pthread_testcancel(void)
檢查本執行緒是否處於Canceld狀態,如果是,則進行取消動作,否則直接返回。
8.1. 等待執行緒結束(pthread_join)
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
8.2. detach一個執行緒
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的失敗。
8.3. continue一個執行緒
在庫中,可以使用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提供了解構函式。
9.1.1. 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之後,不太會再去重複利用。
9.1.2. key的刪除
由pthread_key_delete函式完成此任務。對tsdm_nused,tsdm_nkeys不進行任何操作,因為被刪除的key不再被重複使用。將對應的析構例程設定為TSD_UNALLOCATED。
9.2. tsd value
在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];
} tsd32_t;
|
執行緒可以通過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
相關文章
- 執行緒池的實現程式碼分析執行緒
- 聊聊執行緒技術與執行緒實現模型執行緒模型
- java執行緒實現方式Java執行緒
- 如何實現多執行緒執行緒
- 多執行緒原理實現執行緒
- 執行緒池的實現執行緒
- 多執行緒十二之ConcurrentHashMap1.8實現分析執行緒HashMap
- Java 執行緒池中的執行緒複用是如何實現的?Java執行緒
- Java多執行緒實現方式Java執行緒
- 執行緒池的實現原理執行緒
- 執行緒屏障CyclicBarrier實現原理執行緒
- 多執行緒具體實現執行緒
- Java多執行緒的實現Java執行緒
- JavaScript如何實現多執行緒?JavaScript執行緒
- Runnable介面實現多執行緒執行緒
- python多執行緒實現Python執行緒
- NSThread實現多執行緒thread執行緒
- NSThread多執行緒實現thread執行緒
- Java 實現執行緒死鎖Java執行緒
- 【unity】 Loom實現多執行緒UnityOOM執行緒
- 簡易執行緒池實現執行緒
- Java 執行緒池執行原理分析Java執行緒
- boost中asio網路庫多執行緒併發處理實現,以及asio在多執行緒模型中執行緒的排程情況和執行緒安全。執行緒模型
- disruptor如何實現每CPU執行一個執行緒?執行緒
- 利用訊號量實現執行緒順序執行執行緒
- 5招教你實現多執行緒場景下的執行緒安全!執行緒
- Java高併發與多執行緒(二)-----執行緒的實現方式Java執行緒
- SpringBoot執行緒池和Java執行緒池的實現原理Spring Boot執行緒Java
- 多執行緒-匿名內部類的方式實現多執行緒程式執行緒
- 多執行緒實現多工二執行緒
- 多執行緒實現多工一執行緒
- Java中實現執行緒的方式Java執行緒
- python執行緒池的實現Python執行緒
- Java中如何實現執行緒呢?Java執行緒
- 多執行緒爬蟲實現(上)執行緒爬蟲
- java實現多執行緒的方法Java執行緒
- 面試-實現多執行緒的方式面試執行緒
- 執行緒池ThreadPoolExecutor實現原理執行緒thread