Thread Local Storage

来自osdev
跳到导航 跳到搜索

线程本地存储(Thread Local Storage - TLS)是针对每个线程的全局变量。 像GCC这样的编译器提供了一个__thread关键字来按照线程标记全局变量。 这一特性需要程序加载器和线程创建者的支持。

__thread int errno;
int get_errno() { return errno; }

x86-64System V ABI编译器会将此代码编译成如下汇编程序集:

	.globl	errno
	.section	.tbss,"awT",@nobits
	.align 4
	.type	errno, @object
	.size	errno, 4
errno:
	.zero	4
...
	movl	%fs:errno@tpoff, %eax

errno全局变量被放入一个特殊的线程本地bss节(.tbss)(如果已初始化时是.tdata),特殊的操作在程序链接时和程序加载间发生。 创建线程时,会进行per-thread allocation(包含线程本地存储、用户空间线程结构以及其他内容)。 每个线程变量都位于该分配内存中的固定偏移量处。 在上面的示例中,%fs段从线程的用户空间线程结构(%fs因此用作额外寄存器)开始, 同时特别的errno@tpoff链接器符号是从线程的用户空间线程结构到每线程errno值的偏移量。

设计

主线程本地存储副本(Master Thread Local Storage Copy)

该程序包含其线程本地存储(在编译时初始化)的主副本,该主副本在创建线程时使用。 此特殊段由链接器从创建。tdata(初始化的tls)和。tbss(零初始化tls)部分。 您可以通过在ELF程序头中搜索类型为PT_TLS(十进制值7)(与正常的PT_加载相反)的段来找到它。

线程本地存储主段的虚拟地址没有意义,因为它不是在特定的位置加载的,而是由您决定加载它的位置。 请注意,该段与普通段一样具有对齐约束(但链接器为您放置了这些约束)。 除了自己决定加载段的位置外,还可以像加载普通段一样加载该段。

Per-thread allocation

每个线程都有与其关联的内存分配。 它包含用户空间线程结构、线程本地存储,以及可能的其他内容。 每个线程都有一个线程自指针寄存器,该寄存器指向线程的用户空间线程结构,用于快速确定当前线程,并提供对线程本地存储的快速访问。 每个线程的确切语义取决于体系结构及其ABI,以及可执行文件是静态链接还是动态链接。

用户空间线程结构的布局部分由ABI授权。 在某些平台(i386和x86-64)上,它的开头必须有一个指向自身的指针。 除了这些必需的部分之外,结构的其余部分由您决定,它对于许多事情都很有用,比如记住线程终止时必须释放的分配。

线程本地存储位于用户空间线程结构的固定偏移处,因此线程本地存储中的每个变量也位于固定偏移处。 该偏移量在链路时确定,使用特殊的foo@tpoff链接器符号。 定位特定的线程局部变量就像获取线程自指针并添加固定偏移量一样简单。

ABI

这里是System V ABI中实际细节的摘要。

i386

线程自指针寄存器是%gs段的基地址。 它被设置为当前线程的用户空间线程结构的地址。 在当前CPU上切换线程时,更改该CPUGDT的gs段的基址,并重新加载gs寄存器。 指向用户空间线程结构本身的指针是用户空间线程结构的第一个成员。

线程本地存储(在将其大小四舍五入到对齐状态后)位于用户空间线程结构之前。 偏移量为负。 要放置用户空间线程结构和线程本地存储,请执行以下操作:

size_t allocation_alignment = max(master_tls_alignment, alignof(struct uthread));
size_t allocation_size = alignup(master_tls_size, allocation_alignment) + sizeof(struct uthread);
unsigned char* allocation = allocate(allocation_size, allocation_alignment);
struct uthread* uthread = allocation + alignup(master_tls_size, allocation_alignment);
unsigned char* tls = ((unsigned char*) uthread) - alignup(master_tls_size, master_tls_alignment);

请注意,如果线程本地结构的对齐方式小于struct uthread,那么它可能不在per-thread allocation的开头。 线程本地存储和用户空间线程结构正确对齐是至关重要的。 然后初始化用户空间线程结构的自指针和线程本地存储:

uthread->self_pointer = uthread;
memcpy(tls, master_tls, master_tls_size);

x86-64

线程自指针寄存器是%fs段的基地址。 它被设置为当前线程的用户空间线程结构的地址。 切换线程时,设置FSBASEMSR(0xC0000100)。 指向用户空间线程结构本身的指针是用户空间线程结构的第一个成员。

per-thread allocation按照#i386章节中的描述进行安排和放置。

其他

参见tls。pdf请在下面的文档中记录,然后请在此处记录详细信息:

实现

加载程序后,内核应引导主线程的线程本地存储:

  1. 在程序加载期间,在程序头中找到线程本地存储段,并将其加载到某个地方。
  2. 为主线程创建per-thread allocation。
  3. 将主线程本地存储复制到主线程的线程本地存储。
  4. 在主线程的用户空间线程结构中,存储: 每个线程分配的位置/大小、主线程本地存储位置/大小/对齐、主线程的线程本地存储位置/大小、主线程的堆栈位置/大小,等等。 这允许线程生成新线程,并在其自身之后进行清理。
  5. 将主线程的线程自指针寄存器设置为主线程的用户空间线程结构。

这种方法允许在加载程序时立即运行线程本地存储,并使创建新线程变得简单。

为新线程设置线程本地存储很简单:

  1. 为新线程创建per-thread allocation。
  2. 将主线程本地存储复制到新线程的线程本地存储。
  3. 初始化新线程的用户空间线程结构。
  4. 将新线程的线程自指针寄存器设置为新线程的用户空间线程结构。

有些Unix内核(如Linux)实际上没有为主线程设置线程本地存储。 libc需要解析程序的ELF可执行文件,以定位和加载主线程本地存储副本本身,并引导主线程。 This has the obvious disadvantages of having early times where language features doesn't work and that every executable gets linked in an ELF loader (in case it uses thread local storage).

另见

标准

文章

论坛主题

  • Minimal Static Link - sortie posts about reducing startup libc bloat such as thread local storage initialization by moving it into the kernel to much success.

IRC

  • #osdev 2014-11-17 - sortie and maxdev have a conversation about thread local storage.

实现