https://blog.csdn.net/dayenglish/article/details/20232131
多线程程序当中,因为线程之间共享进程的数据,所以在访问全局数据的时候,可能需要加锁互斥访问。但是全局数据的互斥访问只有在多个线程之间协作进行处理的时候,才有必要。但是如果数据在各个线程之间都需要单独的副本,比如说VC CRT库当中的errno变量,我们希望这个变量仅仅由当前线程错误来设置,并且只影响当前线程,那么这种线程之间共享的副本数据该怎么实现呢?
微软提供的一种实现就是TLS(线程局部存储)。TLS有两种实现方法,第一是静态的实现,那就是利用连接器扩展__declspec(thread)修饰所定义的变量,这使得数据在各个线程当中都有副本;另一种方法是利用系统提供的函数动态实现。
首先分析静态实现。在《windows核心编程》里面,作者提到每一个TLS的静态支持需要额外的三条指令。利用实验验证下。
#include<stdio.h>
__declspec(thread) int a;
int main()
{
a=0;
printf("%d",a);
return 0;
}
上面是一个很简单的VC环境下面的程序,利用VC的调试功能对他进行反汇编。得到的汇编代码如下所示:
1: #include<stdio.h>
2: __declspec(thread) int a;
3: int main()
4: {
00401010 push ebp
00401011 mov ebp,esp
00401013 sub esp,40h
00401016 push ebx
00401017 push esi
00401018 push edi
00401019 lea edi,[ebp-40h]
0040101C mov ecx,10h
00401021 mov eax,0CCCCCCCCh
00401026 rep stos dword ptr [edi]
5: a=0;
00401028 mov eax,[__tls_index (00427e58)]
0040102D mov ecx,dword ptr fs:[2Ch]
00401034 mov edx,dword ptr [ecx+eax*4]
00401037 mov dword ptr [edx+104h],0
6: printf("%d",a);
00401041 mov eax,[__tls_index (00427e58)]
00401046 mov ecx,dword ptr fs:[2Ch]
0040104D mov edx,dword ptr [ecx+eax*4]
00401050 mov eax,dword ptr [edx+104h]
00401056 push eax
00401057 push offset string "%d" (0042201c)
0040105C call printf (00401090)
00401061 add esp,8
7: return 0;
00401064 xor eax,eax
8: }
从编号4到5之间的是一些系统加进去的代码,就是初始化局部变量。从这里也可以看出来实际上的变量a是不占用局部变量的存储空间的。代码段5到6表示给a赋值0。总共四条指令,第一条得到索引值,第二条得到一个指针,第三条得到a所在的内存地址,第四条赋值(之所以书上说多三条是因为不论怎样赋值语句都是必须的,多的是前面的寻址指令)。
有了上面的反汇编代码,接着开始分析三条寻址指令的含义。第一个得到索引,因为是在用户控件内,所以很正常,但是第二条指令通过FS寄存器来寻址,就有疑问了。为什么利用FS寻址?在微软的官方文档里面的解释是:fs:[0]在内核状态指向KPCB,而在用户状态指向TEB。如何使得一个指针指向两个结构体呢?通过查看reactOS的源代码可以知道KPCR的第一个成员是NT_TIB,很巧的是TEB的第一个成员也是NT_TIB,这样的话上面的就不难理解
了。接着往下看TEB的偏移为0X2C的成员变量,这个成员变量是PVOID,因为是以四个字节为单位,所以EAX需要乘以4。 下面开始分析动态实现,TLS的动态实现利用到四个函数TlsAlloc,TlsFree,TlsGetValue和TlsSetValue。通过ReactOS的源代码我们可以有一个比较清晰的了解这四个函数的实现。TlsAlloc分配一个索引值给当前的线程,首先函数申请当前进程的互斥锁,以便互斥访问索引,然后通过PEB的设置位图找到未被使用的索引值,如果返回0XFFFFFFFF那么,标示都被使用了,则到扩展位图里面搜索,直到找到或者失败为止。而之所以会有一个扩展过程是因为如果所有的都需要临时分配的话那么开销太大,所以只有浪费一部分内存,预先分配64果然存储槽。
DWORD WINAPI TlsAlloc(VOID)
{
ULONG Index;
PTEB Teb;
PPEB Peb;
Teb = NtCurrentTeb();
Peb = Teb->ProcessEnvironmentBlock;
RtlAcquirePebLock();
Index = RtlFindClearBitsAndSet(Peb->TlsBitmap, 1, 0);
if (Index != 0xFFFFFFFF)
{
Teb->TlsSlots[Index] = 0;初始化所有的槽为空
RtlReleasePebLock();
return Index;
}
Index = RtlFindClearBitsAndSet(Peb->TlsExpansionBitmap, 1, 0);
if (Index != 0xFFFFFFFF)
{
if (!Teb->TlsExpansionSlots)
{
Teb->TlsExpansionSlots = RtlAllocateHeap(RtlGetProcessHeap(),
HEAP_ZERO_MEMORY,
TLS_EXPANSION_SLOTS *
sizeof(PVOID));
}
if (!Teb->TlsExpansionSlots)
{
RtlClearBits(Peb->TlsExpansionBitmap, Index, 1);
Index = 0xFFFFFFFF;
BaseSetLastNTError(STATUS_NO_MEMORY);
}
else
{
Teb->TlsExpansionSlots[Index] = 0;
Index += TLS_MINIMUM_AVAILABLE;
}
}
else
{
BaseSetLastNTError(STATUS_NO_MEMORY);
}
RtlReleasePebLock();
return Index;
}
BOOL WINAPI TlsFree(IN DWORD Index)
{
BOOL BitSet;
PPEB Peb;
ULONG TlsIndex;
PVOID TlsBitmap;
NTSTATUS Status;
Peb = NtCurrentPeb();
RtlAcquirePebLock();
if (Index >= TLS_MINIMUM_AVAILABLE)
{
TlsIndex = Index - TLS_MINIMUM_AVAILABLE;
if (TlsIndex >= TLS_EXPANSION_SLOTS)
{
BaseSetLastNTError(STATUS_INVALID_PARAMETER);
RtlReleasePebLock();
return FALSE;
}
else
{
TlsBitmap = Peb->TlsExpansionBitmap;
Index = TlsIndex;
}
}
else
{
TlsBitmap = Peb->TlsBitmap;
}
BitSet = RtlAreBitsSet(TlsBitmap, Index, 1);最主要的是这一句,将位映射图改为FREE,前面都是对位映射以及索引的设置,由于TlsGetValue不测试Index的值所以在free之后,如果没改变数值的话,可以的到原有的值。
if (BitSet)
{
Status = NtSetInformationThread(NtCurrentThread(),
ThreadZeroTlsCell,
&Index,
sizeof(DWORD));
if (!NT_SUCCESS(Status))
{
BaseSetLastNTError(STATUS_INVALID_PARAMETER);
RtlReleasePebLock();
return FALSE;
}
RtlClearBits(TlsBitmap, Index, 1);
}
else
{
BaseSetLastNTError(STATUS_INVALID_PARAMETER);
RtlReleasePebLock();
return FALSE;
}
RtlReleasePebLock();
return TRUE;
}
LPVOID WINAPI TlsGetValue(IN DWORD Index)
{
PTEB Teb;
Teb = NtCurrentTeb();//此处得到当前线程的环境块,所以各个线程之间的数据是不共享的
Teb->LastErrorValue = 0;
if (Index < TLS_MINIMUM_AVAILABLE)
{
return Teb->TlsSlots[Index];
}
if (Index >= TLS_EXPANSION_SLOTS + TLS_MINIMUM_AVAILABLE)
{
BaseSetLastNTError(STATUS_INVALID_PARAMETER);
return NULL;
}
Teb->LastErrorValue = 0;
if (!Teb->TlsExpansionSlots) return NULL;
return Teb->TlsExpansionSlots[Index - TLS_MINIMUM_AVAILABLE];
}
BOOL WINAPI TlsSetValue(IN DWORD Index,
IN LPVOID Value)
{
DWORD TlsIndex;
PTEB Teb = NtCurrentTeb();
if (Index < TLS_MINIMUM_AVAILABLE)
{
Teb->TlsSlots[Index] = Value;
return TRUE;
}
TlsIndex = Index - TLS_MINIMUM_AVAILABLE;
if (TlsIndex >= TLS_EXPANSION_SLOTS)
{
BaseSetLastNTError(STATUS_INVALID_PARAMETER);
return FALSE;
}
if (!Teb->TlsExpansionSlots)
{
RtlAcquirePebLock();
if (!Teb->TlsExpansionSlots)
{
Teb->TlsExpansionSlots = RtlAllocateHeap(RtlGetProcessHeap(),
HEAP_ZERO_MEMORY,
TLS_EXPANSION_SLOTS *
sizeof(PVOID));
if (!Teb->TlsExpansionSlots)
{
RtlReleasePebLock();
BaseSetLastNTError(STATUS_NO_MEMORY);
return FALSE;
}
}
RtlReleasePebLock();
}
Teb->TlsExpansionSlots[TlsIndex] = Value;
return TRUE;
}
以上实现当中,由于微软觉得64个TLS存储太少,后来进行了扩充。在超过64个TLS的需求之后,进行动态分配,这样就会浪费内存。每个线程需要额外的4K来存储TLS槽。