最后看一下什么是线程本地存储(TLS)

​线程本地存储又叫线程局部存储,英文名称为Thread Local Storage,简称TLS,实质上是线程私有的全局变量而已。

普通全局变量在多线程中是共享的,一个线程对其进行了修改,其它所有线程都可以看到,而线程私有的全局变量与普通全局变量不同,线程私有的全局变量是线程的私有财产,每个线程都有自己的一个副本,某个线程对其做出的修改只会修改到自己的副本,并不会修改到其它线程的副本。

来看个普通的全部变量的例子:

#include <stdio.h>#include <pthread.h>int g = 0;  // 1,定义全局变量g并赋初值0void *start(void *arg){  printf("start, g[%p] : %d\n", &g, g); // 4,子线程中打印全局变量g的地址和值  g++; // 5,修改全局变量  return NULL;}int main(int argc, char *argv[]){  pthread_t tid;  g = 100;  // 2,主线程给全局变量g赋值为100  pthread_create(&tid, NULL, start, NULL); // 3, 创建子线程执行start()函数  pthread_join(tid, NULL); // 6,等待子线程运行结束  printf("main, g[%p] : %d\n", &g, g); // 7,打印全局变量g的地址和值  return 0;}

上述程序注释1的地方定义了一个普通全局变量g并设置其初始值为0,程序运行后主线程首先将g修改为100(注释2),然后创建了一个子线程来执行start函数(注释3),start先打印出g的值(注释4)来确认在子线程中可以看到主线程对g的修改,之后修改g的值(注释5),然后start结束运行,主线程在注释6处等待start运行完毕后,在注释7处打印g的值来确认start对g的修改可以影响到主线程对g的读取。

编译并运行上述程序:

$ gcc thread.c -o thread -lpthread$ ./threadstart, g[0x601064] : 100main, g[0x601064] : 101

可以看到,g在两个线程中的内存地址都是一样的,任何一个线程都可以读取到另外一个线程对g的修改,实现了全局变量在多线程中的共享。

之后再来看线程本地存储(TLS)实现的私有的全局变量。

下例代码与上述例子大体相似,差别就是在定义g时新增__thread关键字,如此就g就变成了线程私有的全局变量,案例如下:

#include <stdio.h>#include <pthread.h>__thread int g = 0;  // 1,这里增加了__thread关键字,把g定义成私有的全局变量,每个线程都有一个g变量void *start(void *arg){  printf("start, g[%p] : %d\n", &g, g); // 4,打印本线程私有全局变量g的地址和值  g++; // 5,修改本线程私有全局变量g的值  return NULL;}int main(int argc, char *argv[]){  pthread_t tid;  g = 100;  // 2,主线程给私有全局变量赋值为100  pthread_create(&tid, NULL, start, NULL); // 3,创建子线程执行start()函数  pthread_join(tid, NULL);  // 6,等待子线程运行结束  printf("main, g[%p] : %d\n", &g, g); // 7,打印主线程的私有全局变量g的地址和值  return 0;}

执行结果如下:

$ gcc -g thread.c -o thread -lpthread$ ./threadstart, g[0x7f0181b046fc] : 0main, g[0x7f01823076fc] : 100

可看出g在两线程中内存地址是不一致的,main对g的赋值并未影响到start线程g的值,start对g做的修改也并未影响到main主线程,说明各线程都有个私有的全局变量g。

接下来使用调试工具gdb来调试程序,看看编译器gcc是如何实现这个特性的。

$ gdb ./thread

先在源码中的g = 100处下一个断点,运行程序之后反汇编main,得到如下内容:

(gdb) b thread.c:20Breakpoint 1 at 0x400793: file thread.c, line 20.(gdb) rStarting program: /home/bobo/study/c/threadBreakpoint 1, at thread.c:2020g = 100;(gdb) disassDump of assembler code for function main:  0x0000000000400775 <+0>:push   %rbp  0x0000000000400776 <+1>:mov   %rsp,%rbp  0x0000000000400779 <+4>:sub   $0x20,%rsp  0x000000000040077d <+8>:mov   %edi,-0x14(%rbp)  0x0000000000400780 <+11>:mov   %rsi,-0x20(%rbp)  0x0000000000400784 <+15>:mov   %fs:0x28,%rax  0x000000000040078d <+24>:mov   %rax,-0x8(%rbp)  0x0000000000400791 <+28>:xor   %eax,%eax=> 0x0000000000400793 <+30>:movl   $0x64,%fs:0xfffffffffffffffc  0x000000000040079f <+42>:lea   -0x10(%rbp),%rax  0x00000000004007a3 <+46>:mov   $0x0,%ecx  0x00000000004007a8 <+51>:mov   $0x400736,%edx  0x00000000004007ad <+56>:mov   $0x0,%esi  0x00000000004007b2 <+61>:mov   %rax,%rdi  0x00000000004007b5 <+64>:callq 0x4005e0 <pthread_create@plt>  0x00000000004007ba <+69>:mov   -0x10(%rbp),%rax  0x00000000004007be <+73>:mov   $0x0,%esi  0x00000000004007c3 <+78>:mov   %rax,%rdi  0x00000000004007c6 <+81>:callq 0x400620 <pthread_join@plt>  0x00000000004007cb <+86>:mov   %fs:0xfffffffffffffffc,%eax  0x00000000004007d3 <+94>:mov   %eax,%esi  0x00000000004007d5 <+96>:mov   $0x4008df,%edi  0x00000000004007da <+101>:mov   $0x0,%eax  0x00000000004007df <+106>:callq 0x400600 <printf@plt>  ......

程序停在了g = 100这一行,对应上述指令中有=>标识的一行,这条指令的含义就是将常量100(0x64)复制到内存地址为%fs:0xfffffffffffffffc,fs是段寄存器,0xfffffffffffffffc是有符号的数字-4,所以全局变量g的地址就是fs段基地址减去4。

之前聊《聊一聊基础的CPU寄存器~》时提到过,段基地址就是段的起始地址,为了验证g的地址确实是fs段基地址减去4,需先知道fs段基地址是多少。

虽然可用gdb指令查看fs中的值,但fs存放的是段选择子(segment selector),而不是该段的起始地址,为了拿到该段基地址,就需要调整一下代码了,如下:

#include <stdio.h>#include <unistd.h>#include <pthread.h>#include <asm/prctl.h>#include <sys/prctl.h>__thread int g = 0;void print_fs_base(){  unsigned long addr;  int ret = arch_prctl(ARCH_GET_FS, &addr);  //获取fs段基地址  if (ret < 0) {    perror("error");    return;  }  printf("fs base addr: %p\n", (void *)addr); //打印fs段基址  return;}void *start(void *arg){  print_fs_base(); //子线程打印fs段基地址  printf("start, g[%p] : %d\n", &g, g);  g++;  return NULL;}int main(int argc, char *argv[]){  pthread_t tid;  g = 100;  pthread_create(&tid, NULL, start, NULL);  pthread_join(tid, NULL);  print_fs_base(); //main线程打印fs段基址  printf("main, g[%p] : %d\n", &g, g);  return 0;}

上述代码中主线程和子线程都调用了print_fs_base()函数用于打印fs栈基地址,运行结果如下:

fs base addr: 0x7f36757c8700start, g[0x7f36757c86fc] : 0fs base addr: 0x7f3675fcb700main, g[0x7f3675fcb6fc] : 100

可以看到,子线程fs栈基地址为0x7f36757c8700,g地址为0x7f36757c86fc,正好是fs段基地址减去4,主线程fs栈基地址为0x7f3675fcb700,g地址为0x7f3675fcb6fc,正好也是fs段基地址减去4。

到这里就可以推断出,编译器gcc(还有线程库和内核的支持)使用了CPU的fs段寄存器来实现线程本地存储(TLS),不同线程中fs栈基地址也是不一样的,如此看似是同一个全局变量在不同的线程中,却拥有不同的内存地址,实现了线程私有的全局变量。

本文简单介绍的是AMD64 Linux平台下gcc对本地线程存储的实现。

至此,理解Go里的runtime包中goroutine调度器需要准备的基础知识就结束了,后面的文章会结合实际来慢慢的聊主题内容,也就是goroutine调度器。

以上仅为个人观点,不一定准确,能帮到各位那是最好的。

好啦,到这里本文就结束了,喜欢的话就来个三连击吧。

扫码关注公众号,获取更多优质内容。

最后看一下什么是线程本地存储(TLS) 

上一篇:Centos网络管理(四)-路由转发与静态路由


下一篇:计算文件的SHA256哈希值