TLS讲解

基本概念

线程的访问非常自由,它可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果它知道其他线程的堆栈地址,那么这就是很少见的情况),但实际运用中线程也拥有自己的私有存储空间,包括以下几方面:
栈(尽管并非完全无法被其他线程访问,但一般情况下仍然可以认为是私有的数据)。
线程局部存储(Thread Local Storage, TLS)。线程局部存储是某些操作系统为线程单独提供的私
有空间,但通常只具有很有限的容量。
寄存器(包括PC寄存器),寄存器是执行流的基本数据,因此为线程私有。
实际上,线程私有的数据有:
局部变量(栈、寄存器)
函数的参数(栈、寄存器)
TLS 数据(线程局部存储)
线程共享的数据有:
全局变量
堆上的数据
函数里的静态变量
程序代码,任何线程都有有权利读取并执行任何代码。
打开的文件,A 线程打开的文件可以由 B 线程读写。
一个全局变量如果使用 __thread 关键字修饰,那么这个变量就变成线程私有的 TLS 数据,也就是说每个线程都在自己所属 TLS 中单独保存一份这个变量的副本。例如下面的代码中, a 和 b 都是 TLS 数据,而 c 是全局变量。

// gcc tls.c -o tls -g -pthread
#include <pthread.h>
#include <stdio.h>
#include <stdint-gcc.h>
__thread uint32_t a = 0x114514;
__thread uint32_t b;
uint32_t c = 0x1919810;
void *thread(void *arg) {
   printf("thread: a(%p) = %x, b(%p) = %x, c(%p) = %xn", &a, a, &b, b, &c, c);
   return NULL;
}
int main(void) {
   a = 0x12345678;
   b = 0x87654321;
   c = 0xdeadbeef;
   printf("thread: a(%p) = %x, b(%p) = %x, c(%p) = %xn", &a, a, &b, b, &c, c);
   pthread_t pid;
   pthread_create(&pid, NULL, thread, NULL);
   pthread_join(pid, NULL);
   return 0;
}
/*
thread: a(0x7f1ec78f0738) = 12345678, b(0x7f1ec78f073c) = 87654321,
c(0x562d7468a010) = deadbeef
thread: a(0x7f1ec70ed6f8) = 114514, b(0x7f1ec70ed6fc) = 0, c(0x562d7468a010) =
deadbeef
*/

分析生成的 ELF 文件的节表,发现多出了 .tdata 和 .tbss ,这两个节分别记录已初始化和未初始化
的 TLS 数据。
其中 .tbss 在 ELF 文件中不占用空间, .tdata 在 ELF 中存储了初始化的数据,比如上面的代码中的__thread uint32_t a = 0x114514 。
ELF 加载到内存中后, .tdata 和 .tbss 这两个节合并为一个段,在程序头表中这个段的 p_type 为
PT_TLS(7) 。
TLS(Thread Local Storage)的结构与 TCB(Thread Control Block)以及 dtv(dynamic thread
vector)密切相关,每一个线程中每一个使用了 TLS 功能的模块都拥有一个 TLS Block 。这几者的关系如下图所示:
注意,这里是 x86_64-ABI 要求的 TLS 结构,Glibc 实现的 TLS 结构与上图有一些差异。
根据图中显示的信息,TLS Blocks 可以分为两类:
一类是程序装载时就已经存在的(位于 TCB 前),这一部分 Block 被称为 _static TLS_ 。
一类是右边的 Blocks 是动态分配的,它们被使用 dlopen 函数在程序运行时动态装载的模块所使
用。
TCB 作为线程控制块,保存着 dtv 数组的入口, dtv 数组中的每一项都是 TLS Block 的入口,它们是指向 TLS Blocks 的指针。特别的, dtv 数组的第一个成员是一个计数器,每当程序使用 dlopen 函数或者 dlfree 函数加载或者卸载一个具备 TLS 变量的模块,该计数器的值都会加一,从而保证程序内版本的一致性。 特别的,ELF 文件本身对应的 TLS Block 一定在 dtv 数组中占据索引为 1 的位置,且位置上与 TCB 相邻。 还需要注意的是,图中出现了一个名为 $tp_t$ 的指针,在 i386 架构上,这个指针为gs 段寄存器;在 x86_64 架构上,该指针为 fs 段寄存器。由于该指针与 ELF 文件本身对应的 TLS
Block 之间的偏移是固定的,程序在编译时就可以将 ELF 中线程变量的地址(偏移)硬编码到目标文件中。

主线程 TLS 初始化

前面提到过在 main 开始前会调用 __libc_setup_tls 初始化 TLS 。
在 __libc_setup_tls 函数中,首先会遍历 ELF 的程序头表,找到 p_type 为 PT_TLS(7) 的段,这
个段中就存储着 TLS 的初始化数据。 

/* Look through the TLS segment if there is any. */
   if (_dl_phdr != NULL)
       for (phdr = _dl_phdr; phdr < &_dl_phdr[_dl_phnum]; ++phdr)
           if (phdr->p_type == PT_TLS) {
               /* Remember the values we need. */
               memsz = phdr->p_memsz;
               filesz = phdr->p_filesz;
               initimage = (void *) phdr->p_vaddr + main_map->l_addr;
               align = phdr->p_align;
               if (phdr->p_align > max_align)
                   max_align = phdr->p_align;
               break;
          }

然后通过 brk 调用为 TLS 中的数据以及一个 pthread 结构体分配内存。其中 pthread 结构体的第一项为 tcbhead_t header; ,即前面提到的 TCB 。

/* Align the TCB offset to the maximum alignment, as
      _dl_allocate_tls_storage (in elf/dl-tls.c) does using __libc_memalign
      and dl_tls_static_align. */
   tcb_offset = roundup (memsz + GLRO(dl_tls_static_surplus), max_align);
   tlsblock = __sbrk(tcb_offset + TLS_INIT_TCB_SIZE + max_align);

tcbhead_t 结构体定义如下,也就是很多资料中提到的 TLS 。

typedef struct
{
 void *tcb;
/* Pointer to the TCB. Not necessarily the
  thread descriptor used by libpthread. */
 dtv_t *dtv;
 void *self;
/* Pointer to the thread descriptor. */
 int multiple_threads;
 int gscope_flag;
 uintptr_t sysinfo;
 uintptr_t stack_guard;
 uintptr_t pointer_guard;
 unsigned long int vgetcpu_cache[2];
# ifndef __ASSUME_PRIVATE_FUTEX
 int private_futex;
# else
 int __glibc_reserved1;
# endif
 int __glibc_unused1;
 /* Reservation of some values for the TM ABI. */
 void *__private_tm[4];
 /* GCC split stack support. */
 void *__private_ss;
 long int __glibc_reserved2;
 /* Must be kept even if it is no longer used by glibc since programs,
    like AddressSanitizer, depend on the size of tcbhead_t. */
 __128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));
 void *__padding[8];
} tcbhead_t;

之后初始化 _dl_static_dtv ,也就是前面提到的 dtv 数组,具体过程为:
将 tlsblock 地址关于 max_align 向上对齐。
_dl_static_dtv[0].counter 初始化为 dtv 的数量,由于 _dl_static_dtv 前两项分别用于
记录 dtv 总数和使用的数量,因此这里记录的 dtv 数量是要减去这两项的。
_dl_static_dtv[1].counter 初始化为 0 。
_dl_static_dtv[2] 也就是当前模块对应的 dtv 的 pointer.val 指向 TLS 。
_dl_static_dtv[2].pointer.to_free 置为 NULL 。
将 TLS 的初始数据也就是 PT_TLS 段中的数据复制到 TLS 中。

struct dtv_pointer
{
 void *val;                    /* Pointer to data, or TLS_DTV_UNALLOCATED. */
 void *to_free;                /* Unaligned pointer, for deallocation. */
};
/* Type for the dtv. */
typedef union dtv
{
 size_t counter;
 struct dtv_pointer pointer;
} dtv_t;
/* Number of additional entries in the slotinfo array of each slotinfo
  list element. A large number makes it almost certain take we never
  have to iterate beyond the first element in the slotinfo list. */
#define TLS_SLOTINFO_SURPLUS (62)
dtv_t _dl_static_dtv[2 + TLS_SLOTINFO_SURPLUS];
   /* Align the TLS block. */
   tlsblock = (void *) (((uintptr_t) tlsblock + max_align - 1)
                        & ~(max_align - 1));
   /* Initialize the dtv. [0] is the length, [1] the generation counter. */
   _dl_static_dtv[0].counter = (sizeof(_dl_static_dtv) /
sizeof(_dl_static_dtv[0])) - 2;
   // _dl_static_dtv[1].counter = 0;
would be needed if not already done
   /* Initialize the TLS block. */
   _dl_static_dtv[2].pointer.val = ((char *) tlsblock + tcb_offset
                                    - roundup (memsz, align ?: 1));
   _dl_static_dtv[2].pointer.to_free = NULL;
   /* sbrk gives us zero'd memory, so we don't need to clear the remainder. */
   memcpy(_dl_static_dtv[2].pointer.val, initimage, filesz);
此时 TLS 相关结构之间的关系如下图所示:

另外还会初始化 `link_map` 中的 TLS 相关的数据,由此我们可以知道 `link_map` 中这些字段的含
义:
- `l_tls_offset `:TLS 数据相对于 TCB 的偏移。
- `l_tls_align`:TLS 初始数据的对齐,在 TLS 中 TLS 初始数据关于 `l_tls_align` 向上取整。
- `l_tls_blocksize`:TLS 初始数据的大小,也就是前面提到的 TLS Block 的大小。
- `l_tls_initimage`:TLS 初始数据的地址。也就是 `PT_TLS` 段的地址。
- `l_tls_initimage_size`:`PT_TLS` 段在文件中的大小,也就是 `.tdata` 的大小。
- `l_tls_modid`:模块编号(dtv 中的下标)。
```c
   struct link_map *main_map = GL(dl_ns)[LM_ID_BASE]._ns_loaded;
   main_map->l_tls_offset = roundup (memsz, align ?: 1);
   /* Update the executable's link map with enough information to make
      the TLS routines happy. */
   main_map->l_tls_align = align;
   main_map->l_tls_blocksize = memsz;
   main_map->l_tls_initimage = initimage;
   main_map->l_tls_initimage_size = filesz;
   main_map->l_tls_modid = 1;

创建线程时 TLS 初始化

创建线程的函数 pthread_create 实际调用的是 __pthread_create_2_1 函数,在该函数中调用了
allocate_stack 函数。

# define ALLOCATE_STACK(attr, pd) allocate_stack (attr, pd, &stackaddr)
   struct pthread *pd = NULL;
   int err = ALLOCATE_STACK (iattr, &pd);

在 allocate_stack 函数中会调用 mmap 为线程分配栈空间,然后初始化栈底为一个 pthread 结构体并将指针 pd 指向该结构体。最后调用 _dl_allocate_tls 函数为 TCB 创建 dtv 数组。

   struct pthread *pd;
...
   mem = __mmap (NULL, size, (guardsize == 0) ? prot : PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
  ...
   pd = (struct pthread *) ((((uintptr_t) mem + size) - TLS_TCB_SIZE) &
~__static_tls_align_m1);
...
_dl_allocate_tls (TLS_TPADJ (pd))

 _dl_allocate_tls 函数依次调用 allocate_dtv 和 _dl_allocate_tls_init 分配和初始化 dtv
数组。

void *
_dl_allocate_tls (void *mem)
{
 return _dl_allocate_tls_init (mem == NULL
? _dl_allocate_tls_storage ()
: allocate_dtv (mem));
}

allocate_dtv 函数调用了 calloc 函数为 dtv 数组分配内存,初始化 dtv[0].counter 为数组中
元素数量,并且让 pd->dtv 指向 dtv[1] 。

/* Install the dtv pointer. The pointer passed is to the element with
  index -1 which contain the length. */
# define INSTALL_DTV(descr, dtvp)
((tcbhead_t *) (descr))->dtv = (dtvp) + 1
static void *
allocate_dtv (void *result)
{
 dtv_t *dtv;
 size_t dtv_length;
 /* We allocate a few more elements in the dtv than are needed for the
    initial set of modules. This should avoid in most cases expansions
    of the dtv. */
 dtv_length = GL(dl_tls_max_dtv_idx) + DTV_SURPLUS;
 dtv = calloc (dtv_length + 2, sizeof (dtv_t));
 if (dtv != NULL)
  {
     /* This is the initial length of the dtv. */
     dtv[0].counter = dtv_length;
     /* The rest of the dtv (including the generation counter) is
Initialize with zero to indicate nothing there. */
     /* Add the dtv to the thread data structures. */
     INSTALL_DTV (result, dtv);
  }
 else
   result = NULL;
 return result;
}

_dl_allocate_tls_init 函数会遍历 dl_tls_dtv_slotinfo_list 中的 link_map ,初始化 dtv
数组并将初始数据复制到 TLS 变量中。从这里可以看出,如果一个模块有 TLS 变量,则该模块对应的dtv->pointer.val 指向 TLS 变量的起始地址。

 dtv[map->l_tls_modid].pointer.val = TLS_DTV_UNALLOCATED;
           dtv[map->l_tls_modid].pointer.to_free = NULL;
           
           if (map->l_tls_offset == NO_TLS_OFFSET
               || map->l_tls_offset == FORCED_DYNAMIC_TLS_OFFSET)
               continue;
               
           /* Set up the DTV entry. The simplified __tls_get_addr that
              some platforms use in static programs requires it. */
           dtv[map->l_tls_modid].pointer.val = dest;
           /* Copy the initialization image and clear the BSS part. */
           memset(__mempcpy (dest, map->l_tls_initimage,
                             map->l_tls_initimage_size), '�',
            map->l_tls_blocksize - map->l_tls_initimage_size);

回到 __pthread_create_2_1 函数,在完成了 pthread 的一系列初始化后调用了
THREAD_COPY_STACK_GUARD 和 THREAD_COPY_POINTER_GUARD 两个宏,这两个宏的展开如下:

((pd)->header.stack_guard = ({
   __typeof(({
       struct pthread *__self;
       asm("mov %%fs:%c1,%0":"=r"(__self):"i"(((size_t) (&(((struct pthread *)
0)->header.self)))));
       __self;
  })->header.stack_guard) __value;
   _Static_assert(sizeof(__value) == 1 || sizeof(__value) == 4 ||
sizeof(__value) == 8, "size of per-thread data");
   if (sizeof(__value) == 1)asm volatile("movb %%fs:%P2,%b0":"=q"(__value):"0"
(0), "i"(((size_t) (&(((struct pthread *) 0)->header.stack_guard))))); else if
(sizeof(__value) == 4)asm volatile("movl %%fs:%P1,%0":"=r"(__value):"i"
(((size_t) (&(((struct pthread *) 0)->header.stack_guard))))); else { asm
volatile("movq %%fs:%P1,%q0":"=r"(__value):"i"(((size_t) (&(((struct pthread *)
0)->header.stack_guard))))); }
   __value;
}))
((pd)->header.pointer_guard = ({
   __typeof(({
       struct pthread *__self;
       asm("mov %%fs:%c1,%0":"=r"(__self):"i"(((size_t) (&(((struct pthread *)
0)->header.self)))));
       __self;
  })->header.pointer_guard) __value;
   _Static_assert(sizeof(__value) == 1 || sizeof(__value) == 4 ||
sizeof(__value) == 8, "size of per-thread data");
   if (sizeof(__value) == 1)asm volatile("movb %%fs:%P2,%b0":"=q"(__value):"0"
(0), "i"(((size_t) (&(((struct pthread *) 0)->header.pointer_guard))))); else if
(sizeof(__value) == 4)asm volatile("movl %%fs:%P1,%0":"=r"(__value):"i"
(((size_t) (&(((struct pthread *) 0)->header.pointer_guard))))); else { asm
volatile("movq %%fs:%P1,%q0":"=r"(__value):"i"(((size_t) (&(((struct pthread *)
0)->header.pointer_guard))))); }
   __value;
}))

不难看出这两个宏把当前线程(当前 fs 寄存器还没有指向新线程的 TCB)的 TLS 中的 stack_guard 和
pointer_guard 都复制到子线程的 TLS 的对应位置上。因此可以确定线程的 stack_guard 和
pointer_guard 与主线程相同。
最后需要确定是 fs 寄存器何时被修改,因为 fs 寄存器不能再用户态修改,因此一定是一个系统调用完
成了对 fs 寄存器的修改。
通过调试发现, pthread_create->create_thread->clone 中的 clone 系统调用完成了对 fs 寄存器
的修改。

  • 20
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值