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实际代表了一对库线程-内核线程。
 
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 {
        /*
         * 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

 

 
4.      库线程的创建
#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
 
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。
 
在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
 
 
 
 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值