《[计算机软件及应用]Linuxpthread分析文档04.doc》由会员分享,可在线阅读,更多相关《[计算机软件及应用]Linuxpthread分析文档04.doc(40页珍藏版)》请在三一办公上搜索。
1、Linux POSIX Thread分析文档1 综述自从多线程编程的概念出现在 Linux 中以来,Linux 多线程应用的发展总是与两个问题相关联:兼容性、效率。本文从线程模型的基础知识入手,通过分析目前 Linux 平台上最流行的 LinuxThreads以及NPTL线程库的实现及其不足,描述了 Linux 社区是如何看待和解决效率问题的,对于兼容性问题,本文不加以详细描述,对此方面有兴趣的读者可以参考POSIX Threads规范。本文结合Linux Kernel 2.4到2.6中为了提高线程的性能所作的种种努力,描述了LinuxThreads到NPTL两个线程库的设计思想的变化,以及其
2、两者最终的设计结果、它们的设计缺陷以及优化的可能,最后本文对比了这两库的性能差别,并且结合它们的设计说明了性能差距产生的原因。2 基础知识进程是资源管理的最小单位,线程是程序执行的最小单位。在操作系统设计上,从进程演化出线程,最主要的目的就是更好的支持SMP以及减小(进程/线程)上下文切换开销。无论按照怎样的分法,一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(比如cpu、内存、文件等等),而线程将被分配到某个cpu上执行。一个进程可以拥有多个线程,此时,如果进程运行在SMP机器上,它就可以同时使用多个cpu来执行各个线程,达到最大程度的并行,以提高效率。同时,即使是在单cpu的机
3、器上,采用多线程模型来设计程序,正如当年采用多进程模型代替单进程模型一样,使设计更自由,可以面对更加复杂的程序执行逻辑,通过有效的安排不同线程之间的同步关系,可以提高程序的执行效率,同时相对于采用多进程的解决方法来说,线程之间的切换代价明显要低很多。图表 1 线程模式对比线程分为核心级线程和用户级线程两类,分类的标准主要是线程运行在内核内部还是在外部。前者更利于并发使用多处理器的资源,而后者则更多考虑的是用户态与内核态之间切换开销,其具体的优缺点对比如图表 1所示。在目前的商用系统中,通常都将两者结合起来使用,既提供核心线程以满足SMP系统的需要,也支持用线程库的方式在用户态实现另一套线程机制
4、,此时一个核心线程同时成为多个用户态线程的调度者。在线程机制的具体实现上,可以在操作系统内核上实现线程,也可以在核外实现,后者显然要求核内至少实现了进程,而前者则一般要求在核内同时也支持进程。核心级线程模型显然要求前者的支持,而用户级线程模型则不一定基于后者实现。这种差异,正如前所述,是两种分类方式的标准不同带来的。当核内既支持进程也支持线程时,就可以实现一个进程的某个线程由核内调度,而同时它也可以作为用户级线程池的调度者,选择合适的用户级线程在其空间中运行,这种方式既可满足多处理机系统的需要,也可以最大限度的减小调度开销。绝大多数商业操作系统(如Digital Unix、Solaris、Ir
5、ix)都采用的这种能够完全实现POSIX1003.1c标准的线程模型。在核外实现的线程又可以分为一对一、多对一与“多对多”三种模型,第一种用一个核心进程(也许是轻量进程)对应一个用户态线程,将线程调度等同于进程调度,交给核心完成,而后两种则完全在核外实现多线程,调度也在用户态完成。显然,这种核外的线程调度器实际上只需要完成线程运行栈的切换,调度开销非常小,但同时因为核心信号(无论是同步的还是异步的)都是以进程为单位的,因而无法定位到线程,所以这种实现方式不能用于多处理器系统,即该进程的全部线程都仅能被内核认为一个(“多对一”)或者几个(“多对多”)进程而调度。Linux内核只提供了轻量进程的支
6、持,限制了更高效的线程模型的实现,但Linux着重优化了进程的调度开销,一定程度上也弥补了这一缺陷。LinuxThreads所采用的就是线程-进程“一对一”模型,调度交给核心,而在用户级实现一个包括信号处理在内的线程管理机制。而NPTL线程机制则更加利用了2.6内核的优势,同样采用“一对一”模型,使用基于Futex的管理机制替代了原来的LinuxThreads的基于不稳定且低效率的信号处理方式的管理机制,同时利用内核完成了更多的管理工作,去除了管理线程,更好的优化了线程创建、回收以及管理的开销。总体上说NPTL的性能要明显优于LinuxThreads的性能。3 LinuxThreads分析Li
7、nuxThreads采用了“一对一”的线程模型,如图表 2所示。同时其采用了Linux的信号机制(signal)作为线程之间通信和同步的基础。LinuxThreads在设计和开发时正处在2.4内核的时代,当时的用户态进程或者线程的同步的方法仅仅有:信号量,信号,共享内存,管道等较低效率的进程间通信手段。当时的LinuxThreads设计人员选择了信号作为用户态线程的同步的基础。图表 2 一对一线程模型3.1 LinuxThreads架构分析本小节描述LinuxThreads的设计架构,主要包括LinuxThreads依赖的内核功能以及其在用户态中的实现。3.1.1 内核支持在线程概念出现以前,
8、为了减小进程切换的开销,操作系统设计者逐渐修正进程的概念,逐渐允许将进程所占有的资源从其主体剥离出来,允许某些进程共享一部分资源,例如文件、信号,内存,甚至代码,这就发展出轻量级进程的概念。Linux内核在2.0.x版本就已经实现了轻量级进程,应用程序可以通过一个统一的系统调用接口clone(),用不同的参数指定创建轻量级进程还是普通进程。在内核中,clone()调用经过参数传递和解释后会调用do_fork(),这个函数同时也是fork()、vfork()系统调用的最终实现:int do_fork(unsigned long clone_flags, unsigned long stack_s
9、tart, struct pt_regs *regs, unsigned long stack_size)其中的clone_flags取自以下宏的或值: #define CSIGNAL0x000000ff/* signal mask to be sent at exit */#define CLONE_VM0x00000100/* set if VM shared between processes */#define CLONE_FS 0x00000200/* set if fs info shared between processes */#define CLONE_FILES 0x00
10、000400/* set if open files shared between processes */#define CLONE_SIGHAND0x00000800/* set if signal handlers and blocked signals shared */#define CLONE_PID0x00001000/* set if pid shared */#define CLONE_PTRACE0x00002000/* set if we want to let tracing continue on the child too */#define CLONE_VFORK
11、0x00004000/* set if the parent wants the child to wake it up on mm_release */#define CLONE_PARENT0x00008000/* set if we want to have the same parent as the cloner */#define CLONE_THREAD0x00010000/* Same thread group? */#define CLONE_NEWNS0x00020000/* New namespace group? */#define CLONE_SIGNAL (CLON
12、E_SIGHAND | CLONE_THREAD)在do_fork()中,不同的clone_flags将导致不同的行为,对于LinuxThreads,它使用(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND)参数来调用clone()创建线程,表示共享内存、共享文件系统访问计数、共享文件描述符表,以及共享信号处理方式。本节就针对这几个参数,看看Linux内核是如何实现这些资源的共享的。1.CLONE_VMdo_fork()需要调用copy_mm()来设置task_struct中的mm和active_mm项,这两个mm_struct数据与进程所关联
13、的内存空间相对应。如果do_fork()时指定了CLONE_VM开关,copy_mm()将把新的task_struct中的mm和active_mm设置成与current的相同,同时提高该mm_struct的使用者数目(mm_struct:mm_users)。也就是说,轻量级进程与父进程共享内存地址空间,内存访问权限等,由图表 3示意可以看出mm_struct在进程中的地位。图表 3 MM-Struct结构示意图2.CLONE_FStask_struct中利用fs(struct fs_struct *)记录了进程所在文件系统的根目录和当前目录信息,do_fork()时调用copy_fs()复制了
14、这个结构;而对于轻量级进程则仅增加fs-count计数,与父进程共享相同的fs_struct。也就是说,轻量级进程没有独立的文件系统相关的信息,进程中任何一个线程改变当前目录、根目录等信息都将直接影响到其他线程。3.CLONE_FILES 一个进程可能打开了一些文件,在进程结构task_struct中利用files(struct files_struct *)来保存进程打开的文件结构(struct file)信息,do_fork()中调用了copy_files()来处理这个进程属性;轻量级进程与父进程是共享该结构的,copy_files()时仅增加files-count计数。这一共享使得任何线
15、程都能访问进程所维护的打开文件,对它们的操作会直接反映到进程中的其他线程。4.CLONE_SIGHAND 每一个Linux进程都可以自行定义对信号的处理方式,在task_struct中的sig(struct signal_struct)中使用一个struct k_sigaction结构的数组来保存这个配置信息,do_fork()中的copy_sighand()负责复制该信息;轻量级进程不进行复制,而仅仅增加signal_struct:count计数,与父进程共享该结构。也就是说,子进程与父进程的信号处理方式完全相同,而且可以相互更改。do_fork()中所做的工作很多,在此不详细描述。对于SM
16、P系统,所有的进程fork出来后,都被分配到与父进程相同的cpu上,一直到该进程被调度时才会进行cpu选择。 3.1.2 LinuxThread的线程机制LinuxThreads是Linux kernel 2.4平台上使用最为广泛的线程库,由Xavier Leroy (Xavier.Leroyinria.fr)负责开发完成,并已绑定在GLIBC中发行,其在kernel 2.6以后伴随GLIBC 2.3版本的发行被NPTL库替代。它所实现的就是基于核心轻量级进程的“一对一”线程模型,一个线程实体对应一个内核轻量级进程,而线程之间的管理在核外函数库中实现。1. 线程描述数据结构及实现限制Linux
17、Threads定义了一个struct _pthread_descr_struct数据结构来描述线程,并使用全局数组变量_pthread_handles来描述和引用进程所辖线程。在_pthread_handles中的前两项,LinuxThreads定义了两个全局的系统线程:_pthread_initial_thread和_pthread_manager_thread,并用_pthread_main_thread表征_pthread_manager_thread的父线程(初始为_pthread_initial_thread)。struct _pthread_descr_struct是一个双环链表结
18、构,_pthread_manager_thread所在的链表仅包括它一个元素,实际上,_pthread_manager_thread是一个特殊线程,LinuxThreads仅使用了其中的errno、p_pid、p_priority等三个域。而_pthread_main_thread所在的链则将进程中所有用户线程串在了一起。经过一系列pthread_create()之后形成的_pthread_handles数组将如图表 4所示。图表 4 _pthread_handles新创建的线程将首先在_pthread_handles数组中占据一项,然后通过数据结构中的链指针连入以_pthread_main_
19、thread为首指针的链表中。这个链表的使用在介绍线程的创建和释放的时候将提到。LinuxThreads遵循POSIX1003.1c标准,其中对线程库的实现进行了一些范围限制,比如进程最大线程数,线程私有数据区大小等等。在LinuxThreads的实现中,基本遵循这些限制,但也进行了一定的改动,改动的趋势是放松或者说扩大这些限制,使编程更加方便。这些限定宏主要集中在sysdeps/unix/sysv/linux/bits/local_lim.h(不同平台使用的文件位置不同)中,包括如下几个:每进程的私有数据key数,POSIX定义_POSIX_THREAD_KEYS_MAX为128,Linux
20、Threads使用PTHREAD_KEYS_MAX,1024;私有数据释放时允许执行的操作数,LinuxThreads与POSIX一致,定义PTHREAD_DESTRUCTOR_ITERATIONS为4;每进程的线程数,POSIX定义为64,LinuxThreads增大到1024(PTHREAD_THREADS_MAX);线程运行栈最小空间大小,POSIX未指定,LinuxThreads使用PTHREAD_STACK_MIN,16384(字节)。需要注意的是如果启动TLS,则通过硬件负责切换到需要的thread_descr,而如果不启用TLS,则需要软件通过线程栈的位置推算出thread_de
21、scr。TLS(Thread-Local Storage)与TSD(Thread Specific Data)是两种实现线程本地数据的方法,TLS主要基于包括线程寄存器的CPU硬件实现,后文将详细介绍这种模式,TSD是一种采用纯粹软件方法实现线程本地数据的方法,主要通过pthread_create_key等接口创建和访问数据,这种方式并未在本文中介绍,可以参考glibc/linuxthreads/specific.c中的代码实现。TSD实现的线程私有数据的创建和和回收操作的性能明显要差于TLS实现的线程私有数据。2. 管理线程“一对一”模型的好处之一是线程的调度由核心完成了,而其他诸如线程创建
22、、线程取消、线程间的同步等工作,都是在核外线程库中完成的。在LinuxThreads中,专门为每一个进程构造了一个管理线程,负责处理线程相关的管理工作。当进程第一次调用pthread_create()创建一个线程的时候就会自动创建(_clone())并启动管理线程。在一个进程空间内,管理线程与其他线程之间通过一对管理管道(manager_pipe2)来通讯,该管道在创建管理线程之前创建,在成功启动了管理线程之后,管理管道的读端和写端分别赋给两个全局变量_pthread_manager_reader和_pthread_manager_request,之后,每个用户线程都通过_pthread_ma
23、nager_request向管理线程发请求,但管理线程本身并没有直接使用_pthread_manager_reader,管道的读端(manager_pipe0)是作为_clone()的参数之一传给管理线程的,管理线程的工作主要就是监听管道读端,并对从中取出的请求作出反应。创建管理线程的流程如图表 5所示。(全局变量pthread_manager_request初值为-1)图表 5 Manager线程创建过程初始化结束后,在_pthread_manager_thread中记录了轻量级进程号以及核外分配和管理的线程id,2*PTHREAD_THREADS_MAX+1这个数值不会与任何常规用户线程i
24、d冲突。管理线程作为pthread_create()的调用者线程的子线程运行,而pthread_create()所创建的那个用户线程则是由管理线程来调用clone()创建,因此实际上是管理线程的子线程。_pthread_manager()就是管理线程的主循环所在,在进行一系列初始化工作后,进入while(1)循环。在循环中,线程以2秒为timeout查询(_poll())管理管道的读端。在处理请求前,检查其父线程(也就是创建manager的主线程)是否已退出,如果已退出就退出整个进程。如果有退出的子线程需要清理,则调用pthread_reap_children()清理。然后才是读取管道中的请求
25、,根据请求类型执行相应操作(switch-case)。具体的请求处理,请参考manager.c。3. 线程栈在LinuxThreads中,管理线程的栈和用户线程的栈是分离的,管理线程在进程堆中通过malloc()分配一个THREAD_MANAGER_STACK_SIZE字节的区域作为自己的运行栈。用户线程的栈分配办法随着体系结构的不同而不同,主要根据两个宏定义来区分,一个是NEED_SEPARATE_REGISTER_STACK,这个属性仅在IA64平台上使用;另一个是FLOATING_STACK宏,在i386等少数平台上使用,此时用户线程栈由系统决定具体位置并提供保护。与此同时,用户还可以通
26、过线程属性结构来指定使用用户自定义的栈。因篇幅所限,这里只能分析i386平台所使用的两种栈组织方式:FLOATING_STACK方式和用户自定义方式。在FLOATING_STACK方式下,LinuxThreads利用mmap()从内核空间中分配8MB空间(i386系统缺省的最大栈空间大小,如果有运行限制(rlimit),则按照运行限制设置),使用mprotect()设置其中第一页为非访问区。该8M空间的功能分配如图表 6。图表 6 栈结构示意图低地址被保护的页面用来监测栈溢出。对于用户指定的栈,在按照指针对界后,设置线程栈顶,并计算出栈底,不做保护,正确性由用户自己保证。不论哪种组织方式,线程
27、描述结构总是位于栈顶紧邻堆栈的位置。4. 线程id和进程id每个LinuxThreads线程都同时具有线程id和进程id,其中进程id就是内核所维护的进程号,而线程id则由LinuxThreads分配和维护。 _pthread_initial_thread的线程id为PTHREAD_THREADS_MAX,_pthread_manager_thread的是2*PTHREAD_THREADS_MAX+1,第一个用户线程的线程id为PTHREAD_THREADS_MAX+2,此后第n个用户线程的线程id遵循以下公式:tid=n*PTHREAD_THREADS_MAX+n+1这种分配方式保证了进程中
28、所有的线程(包括已经退出)都不会有相同的线程id,而线程id的类型pthread_t定义为无符号长整型(unsigned long int),也保证了有理由的运行时间内线程id不会重复。从线程id查找线程数据结构是在pthread_handle()函数中完成的,实际上只是将线程号按PTHREAD_THREADS_MAX取模,得到的就是该线程在_pthread_handles中的索引。3.1.3 Thread-Local Storage (TLS 扩展)相关说明1. Thread register说明Intel 在x86 的系统结构中把CPU 的执行权限分得很细,分成了从“0 环”至“3 环”共
29、4个“环”,并让CPU 运行于0 环时具有最高的权限,而运行于3 环时则权限最低。但是从后来的发展看,无论是Linux 还是Windows,实际上都只分系统(即内核)和用户两种状态、或称两个空间就够了,因而只使用了4个环中的两个,即0 环(内核)和3 环(用户)。Intel配合操作系统的任务切换,设计了一个独立的“任务状态段”TSS,里面包含了几乎所有寄存器的映像,而通过TSS 的切换来实现任务的切换,而且只要一条指令就能完成这样的切换。这条指令把几乎所有寄存器(除一些“系统寄存器”如GDTR等以外)的当前内容都一下子保存到当前任务的TSS 中;然后通过一个实质上相当于段寄存器的“任务寄存器”
30、TR 切换到目标任务的TSS,就是使TR 改而指向目标任务的TSS;再从这TSS 中恢复目标任务的寄存器映像,切换就完成了。TSS 中还有关于一个任务的重要信息,即其包括从0 环到2 环的堆栈段寄存器和堆栈指针的映像。其设计意图是,当CPU 从外环(例如3 环)进入内环(例如0 环)时,就从当前任务的TSS 中把内环的堆栈指针(以及段寄存器SS 的映像)装入ESP(以及SS)。此外,TSS 中还有一个“I/O 权限位图”,位图中的每一位都代表着I/O 地址空间(共64KB)的一个字节,如果为0 就表示即使在3 环中也可以对此字节执行in、out 等I/O 指令。需要注意的是线程切换需要TSS,
31、因为CPU 在从用户空间进入系统空间时自动到TSS 中去获取系统空间的堆栈指针。此外,如果在用户空间执行in、out 等I/O 指令,CPU 也要到TSS 中去核对I/O 权限位图。即使在切换线程的时候,TSS 不一定要切换,但是里面的ESP0 等字段却还是必须要改变,因为不同线程的系统空间堆栈的位置各不相同。至于I/O 权限位图,也有可能需要改变。在Intel 架构中,段寄存器起着重要的作用。在32 位保护模式中段寄存器以保护为主,此时段寄存器的内容已不再直接与地址有关,而变成了“段选择项”,其主体是用于“段描述表”的下标。下标不同,就选择了描述表中不同的表项,每个表项就是一个“段描述项”,
32、至于段的长度则往往可以覆盖整个4GB 空间。就线程切换而言,与其密切相关的“段描述表”有两个。一个是“全局描述表(Global Descriptor Table)”GDT,一个是“局部描述表(Local Descriptor Table)”。段描述项可以是针对GDT 的,也可以是针对LDT 的。其中LDT 的设计意图是局部于个别的进程(任务),并且其本身也是作为一个段而存在的。CPU 中有个寄存器GDTR,其内容就是GDT 起点的32 位地址。GDT 的最大长度是64K 字节,最多可以容纳8192 个描述项(每个描述项8 个字节),其中的第一个描述项必须是0,表示“非法描述项”(所以段选择项不
33、能为0)。LDT 和TSS 都是作为段而存在的(但GDT 不是),LDT 可用可不用,但如前文所述TSS 是非有不可的。所以Linux的GDT 中必须有TSS 的描述项。既然LDT 和TSS 都是作为段而存在,就应该有相应的段寄存器,这就是LDTR 和TR,只不过因为作用特殊而不明确地称为段寄存器。这样,CPU 中的段寄存器一共是8 个,即CS、DS、SS、ES、FS、GS、LDTR、和TR,其中LDTR 和TR 为系统段寄存器。相比之下,GDT 中最多可以有8191个有效的段描述项,所以只要改变段寄存器中的选择项即下标就可以使其灵活地指向不同的地址段。如前所述,“任务状态段”TSS 的设计意
34、图是保存各个任务的执行环境和状态,而当前任务的TSS 选择项就存储在TR 中。当一个任务暂时放弃或被剥夺运行时,其当前状态、即所有通用寄存器的映像、就保存在TSS 中。TSS 中有三个堆栈指针,即ESP0、ESP1、ESP2、,分别用于0 环、1 环、和2 环。当CPU 从外环通过调用、陷阱、中断、异常进入某个内环时,CPU 就从TSS 取得该内环的堆栈指针。由于3环处于最外围,所以CPU 不可能从某个更外围的环进入3环,所以TSS 中没有用于3环的堆栈指针。而所谓CPU 在系统调用、中断、异常时切换到系统空间堆栈,实际上就是从TSS 中把0环的SS0 和ESP0 装入SS 和ESP,再把原来
35、3环的SS 和ESP 压入0环堆栈。所以,CPU 中物理的堆栈指针ESP 只有一个,而逻辑的堆栈指针却最多可以有4个。不过,在实际的使用中,无论是Linux 还是Windows,都只使用了0环和3环,即其中的两个。由于目前的Linux仅仅使用了TSS中极少的内容和功能,所有线程(不管属于哪一个进程)都共用同一个TSS 数据结构,在切换线程时就需要改变TSS 中的一些字段、即某些寄存器的映像。特别地主要有以下几类l 各个线程的系统空间堆栈位置各不相同,所以字段ESP0 的字段是必须改变的。注意需要恢复的只是系统空间的堆栈指针,用户空间的堆栈指针已经保存在系统空间堆栈的陷阱框架中,返回用户空间时自
36、然就会恢复。l 各个进程可能有自己的I/O 权限位图,这个位图的起点位移保存在进程的EPROCESS 结构中,切换线程时要把它复制到TSS 中。各个进程的内存映射各不相同,各有自己的页面目录。各进程页面目录的起始地址保存在KPROCESS 数据结构中,需要把它设置到控制寄存器CR3 中。l 每个线程在用户空间都有个“线程环境块”TEB,这就是进入用户空间时段寄存器FS 所指的存储段。所以,GDT 中的相应段描述项也需要加以改变,这个也就是TLS在Intel架构上的实现方式。l 如果用到LDT 的话,GDT 中的LDT 段描述项的映像也需要改变。l FPU 状态,如果使用浮点处理器的话,在切换线
37、程时也要切换浮点运算的上下文,就是保存和恢复浮点处理器的状态(FX_SAVE_AREA)。2. TLS数据访问方法与调度过程中的问题这里以x86_64类型的arch作为例子说明TLS数据访问的流程。采用LinuxThreads中代码作为说明。TLS作为一种数据的存储方法,主要负责线程私有数据的存储。对于一种数据的存储方法,存在分配,释放,访问数据空间三个问题。前一小节基本上说明了Intel CPU的基本原理。在Intel架构的CPU中,通过使用FS段寄存器配合GDT寻址数据,线程之间可以不通过切换GDT即可以拥有自己私有的数据地址空间。即通过切换FS,通过FS:段内地址 的方式访问到用户空间的
38、数据。l 分配数据空间方法TLS数据分配的方式区别于通用的malloc数据分配的方式,TLS的数据分配分为动态分配和静态分配的方式。其中后者较为简单。静态分配的方式如图表 7中左上角示意图所示,其对用的代码如表格 1所示。图表 7 TLS动态与静态分配的示意图表格 1中的代码对应了图表 7的左上角的图,左上角的图后面的tlsoffset1,tlsoffset2等即对应了下面代码中的p_nextlive, p_prevlive等。区别在于下文的代码把TCB块放置到了DTV表指针的前面。/descr.hstruct _pthread_descr_struct union struct void *
39、tcb; /* Pointer to the TCB. This is not always the address of this thread descriptor. */ union dtv *dtvp; pthread_descr self; /* Pointer to this structure */ int multiple_threads; data; void *_padding16; p_header; pthread_descr p_nextlive, p_prevlive; /* Double chaining of active threads */ pthread_
40、descr p_nextwaiting; /* Next element in the queue holding the thr */ pthread_descr p_nextlock; /* can be on a queue and waiting on a lock */ pthread_t p_tid; /* Thread identifier */ int p_pid; /* PID of Unix process */ int p_priority; /* Thread priority (= 0 if not realtime) */表格 1 静态分配TLS示意图这样TP的值(
41、即_pthread_descr_struct的对象的指针)实际上就作为FS的值被填写入CPU即可以实现通过FS和offset直接访问p_nextlive等数据信息,表格 2说明了如何将_pthread_descr_struct (descr)写入FS寄存器。# define TLS_INIT_TP(descr, secondcall) ( void *_descr = (descr); tcbhead_t *head = _descr; long int _result; head-tcb = _descr; /* For now the thread descriptor is at the
42、 same address. */ head-self = _descr; asm volatile (syscall : =a (_result) : 0 (unsigned long int) _NR_arch_prctl), D (unsigned long int) ARCH_SET_FS), S (_descr) : memory, cc, r11, cx); _result ? cannot set %fs base address for thread-local storage : 0; )表格 2 分配TLS变量示意动态分配TLS的方法如图表 7右上角的图所示,由glibc负责动态分配并且维护在DTV表格的静态数据指针的后面。l 访问数据空间方法TLS分配出的数据空间可以简单的通过FS:Offset的方式访问,如表格 3所示。# define THREAD_GETMEM(descr, member) ( _typeof_ (descr-member) _value; if (sizeof (_value) = 1) _asm_ _volatile_ (movb %fs:%P2,%b0 : =q (_value) : 0 (0), i (offsetof (struct _pthread_descr_struct, memb