我有同样的问题,把我带到这里,所以我试图验证 Brett 和 Cashew 在上一个答案和 cmets 中解释的内容。
这是一个示例代码:
#include <stdio.h>
#include <pthread.h>
#include <inttypes.h>
#include <unistd.h>
#define N 2
__thread int myVar;
int *commonVar;
void *th(void *arg)
{
int myid = *((int *)arg);
myVar = myid;
printf("thread %d set myVar=%d, &myVar=%p\n", myid, myVar, &myVar);
sleep(1);
printf("thread %d now has myVar=%d\n", myid, myVar);
sleep(1 + myid);
printf("thread %d sees this value at *commonVar=%d, commonVar=%p\n", myid, *commonVar, commonVar);
commonVar = &myVar;
printf("thread %d sets commonVar pointer to his myVar and now *commonVar=%d, commonVar=%p\n", myid, *commonVar, commonVar);
}
int main()
{
int a = 123;
pthread_t t[N];
int arg[N];
commonVar = &a;
printf("size of pointer: %lu bits\n", 8UL * sizeof(&a));
for (int i = 0; i < N; i++)
{
arg[i] = i;
pthread_create(&t[i], 0, th, arg + i);
}
for (int i = 0; i < N; i++)
pthread_join(t[i], 0);
printf("all done\n");
}
它在 32 位 x86 (gcc -m32 -o a a.c -lpthread) 上生成以下输出:
size of pointer: 32 bits
thread 0 set myVar=0, &myVar=0xf7d51b3c
thread 1 set myVar=1, &myVar=0xf7550b3c
thread 0 now has myVar=0
thread 1 now has myVar=1
thread 0 sees this value at *commonVar=123, commonVar=0xffabb390
thread 0 sets commonVar pointer to his myVar and now *commonVar=0, commonVar=0xf7d51b3c
thread 1 sees this value at *commonVar=0, commonVar=0xf7d51b3c
thread 1 sets commonVar pointer to his myVar and now *commonVar=1, commonVar=0xf7550b3c
all done
在 x64 上 (gcc -o a a.c -lpthread):
size of pointer: 64 bits
thread 0 set myVar=0, &myVar=0x7fe5ae27a6fc
thread 1 set myVar=1, &myVar=0x7fe5ada796fc
thread 0 now has myVar=0
thread 1 now has myVar=1
thread 0 sees this value at *commonVar=123, commonVar=0x7ffff6e3e04c
thread 0 sets commonVar pointer to his myVar and now *commonVar=0, commonVar=0x7fe5ae27a6fc
thread 1 sees this value at *commonVar=0, commonVar=0x7fe5ae27a6fc
thread 1 sets commonVar pointer to his myVar and now *commonVar=1, commonVar=0x7fe5ada796fc
all done
观察:1) 我们可以看到线程本地存储 (TLS) 变量按预期工作 - 每个线程都有自己的副本,不会干扰其他线程;2) 指向 TLS 变量的指针可以转换为非 TLS该线程内部的指针,然后由相同或任何其他线程使用来访问转换指针的线程的特定 TLS 局部变量的值。让我们看看这是如何在汇编代码级别实现的:
一、为myVar = myid;行(gcc [-m32] -o a.asm a.c -lpthread -Xlinker -Map=output.map -S)生成的汇编代码:
32 位:
movl -12(%ebp), %eax
movl %eax, %gs:myVar@ntpoff
64 位:
movl -4(%rbp), %eax
movl %eax, %fs:myVar@tpoff
所以我们可以看到,正如 Brett 所提到的,GS 和 FS 寄存器用于在线程中寻址 TLS 变量,从而导致每个线程的线性和物理地址位置不同。
这是为commonVar = &myVar; 行生成的汇编代码:
32 位:
movl commonVar@GOT(%ebx), %eax
movl %gs:0, %ecx
leal myVar@ntpoff, %edx
addl %ecx, %edx
movl %edx, (%eax)
64 位:
movq %fs:0, %rax
addq $myVar@tpoff, %rax
movq %rax, commonVar(%rip)
因此,我们可以看到指向 TLS 变量的指针可以转换为非 TLS 指针(它将使用默认的 DS 段寄存器),并且 gcc 通过使用 ADD 指令手动执行分段算法来编译它,依赖于事实上,在默认 DS==0 的情况下,获得的线性地址(gs:myVar 与 ds:commonVar)将是相同的,因此对于这两种情况,虚拟地址转换的分页部分将是相同的。
最后,有趣的是,当我们打印指向myVar(每个线程输出的第一行)的指针时,我们可以看到不同的地址。这是因为当该指针被传递给printf() 函数时,它首先被转换为基于DS 的指针。例如,在 64 位上它看起来像这样:
...
movq %fs:0, %rax
leaq myVar@tpoff(%rax), %rcx
...
call printf@PLT