现在的位置: 首页 > 综合 > 正文

线程局部存储,Part 2:显式TLS

2013年07月11日 ⁄ 综合 ⁄ 共 3220字 ⁄ 字号 评论关闭

原文网址:http://www.nynaeve.net/?p=181

线程局部存储,Part 2:显式TLS

前一篇,我概述了windows中TLS的一些总体设计原则。大家可以从MSDN中得到关于TLS的高层接口和设计方法,但是有意思的却是其底层的实现。

从实现来看,显式TLS API是目前两类实现TLS方法中较简单的一种,因此这种方法很少涉及内部实现的可变部分。正如我上次提到的,显式TLSAPI主要是4个函数。其中最重要的两个是TlsGetValue和TlsSetValue,分别负责设置和获取线程相关的数据。

这两个函数非常简单。其背后的核心机制是他们是使用dwTlsIndex为索引来访问TEB中两个数组的“dumb accessors”(其实内部使用2个数组来实现:TlsSlots和TlsExpansionSlots,这两个函数用于根据索引访问这两个数组)。Vista(32-bit)下这两个函数的为实现代码如下:

LPVOID __stdcall xTlsGetValue(_In_ DWORDdwTlsIndex)

{

       PTEBTeb = xNtCurrentTeb();

       Teb->LastErrorValue= 0;

       if(dwTlsIndex< 64)

              //64个指针大小的空间

              returnTeb->TlsSlots[dwTlsIndex];

 

       if(dwTlsIndex>= 1088){//440h

              //总共有1088个slot,超出就错误了

              xSetLastError(ERROR_INVALID_PARAMETER);

              return0;

       }

 

       if(Teb->TlsExpansionSlots)

              returnTeb->TlsExpansionSlots[dwTlsIndex - 64];

       else

              return0;

}

 

BOOL __stdcall xTlsSetValue(_In_ DWORD dwTlsIndex,_In_ LPVOID lpTlsValue)

{

       PTEB Teb= xNtCurrentTeb();

       if(dwTlsIndex< 64){

              Teb->TlsSlots[dwTlsIndex]= lpTlsValue;

              returnTRUE;

       }

 

       if(dwTlsIndex>= 1088){

              xSetLastError(ERROR_INVALID_PARAMETER);

              return0;

       }

 

       //处理扩展Slot的情况

       if(!Teb->TlsExpansionSlots){

              //第一次进入需要为扩展Slot分配内存

              xRtlAcquirePebLock();

              if(!Teb->TlsExpansionSlots){

                     LPVOIDTmp = xRtlAllocateHeap(Teb->Peb->ProcessHeap, 8, 1024*sizeof(LPVOID));

                     if(!Tmp){

                            //资源不足

                            xRtlReleasePebLock();

                            xSetLastError(0);

                            returnFALSE;

                     }

                     Teb->TlsExpansionSlots= (PVOID*)Tmp;

              }

              xRtlReleasePebLock();

       }

       Teb->TlsExpansionSlots[dwTlsIndex- 64] = lpTlsValue;

       returnTRUE;

}

TlsSlots是TEB结构中一个64个指针大小的数组,它保证每个线程最低具有64个线程局部存储空间。后来,微软觉得供应64个TLS槽(Slot)太少,于是在PEB中增加了TlsExpansionSlots指针,该指针指向额外的1024个TLS槽。且TlsExpansionSlots指向是空间是按需分配的,即在前64个槽用完之后才会分配使用。

(PS:这也是MSDN中所提到的64和1088TLS槽限制的原因吧)

从所有这些考虑和目的来说,TlsAlloc和TlsFree的实现正如你想象的那样:它们获得一个锁,查找未分配的Tls槽(如果找到就返回槽的索引,否则告诉调用者没有空余的槽了)。如果最初的64个槽用完了(TlsSlots用完)且TlsExpansionSlots指针为NULL,则TlsAlloc将会分配1024个TLS槽(每个槽为指针大小),将这块内存清0,然后更新TlsExpansionSlots,使其引用这块内存。

在内部,TlsAlloc和TlsFree利用Rtl Bitmap来记录Tls槽的使用情况;bitmap中的每个位记录一个槽的使用情况(使用或未被使用)。这样既可以快速查找TLS槽的使用映射情况,同时节省了内存空间。

如果您一直看到这里,您可能会疑惑:当进程中已经存在多个线程之后,调用TlsAlloc会发生些什么事情了?乍一看这里会出现问题,TlsAlloc仅为当前线程分配了TlsExpansionSlots内存,其它线程访问已分配的槽应该会出现访问违例错误。当进程中不止一个线程时,对于索引大于等于64的TLS槽将无法正常工作。但事实并不是这样的。在TlsGetValue和TlsSetValue中使用了一个trick,它补偿了TlsAlloc仅为当前线程分配TlsExpansionSlots的限制。

假定,使用的dwTlsIndex》=64时调用TlsGetValue,此时访问的内存位于TlsExpansionSltos所指空间,若该空间对于当前线程没有被分配,函数返回0.(未初始化的TLS槽的默认值,此时完全合理合法)。同样,调用TlsSetValue时,如果TlsExpansionSlts没有分配内存,该函数将按需分配内存,并初始化分配的内存块(全部置0)。

在多线程中,还遗留最后一个苦恼:释放TLS槽。潜在的问题是,当一个线程释放了TLS槽然后又从新分配了它,如此其它线程中该槽的内容将遗留下来(这样默认值就不是0了)。TlsFree采用ThreadZeroTlsCell线程信息类求助内核来解决该问题。当内核接到以ThreadZeroTlsCell为参数的NtSetInformationTHread调用,它枚举当前进程中的所有线程,对于指定的槽写一个指针长度的0值,这样将冲掉旧的值,使该槽处于未分配的初始状态。(严格来说,内核不是必须这样做,但是设计者选择采用这种方式(我没想到有其它方法,各位可以想想))

当一个线程正常退出,如果TlsExpansionSlots指针已经分配了内存,它将被释放。(当然,如果线程调用TerminateThread结束,该块内存就leak了。这也是无数为什么你要远离TerminateThread的原因之一)。

接下来:审查隐式TLS支持(__declspec(thread).

抱歉!评论已关闭.