0%

Linux TLS线程局部存储

基础知识

在多线程的程序中,进程中的全局变量与函数内定义的静态(static)变量,是各个线程都可以访问的共享变量,因此往往会有资源条件竞争的问题,常见的处理办法就是使用同步机制来维护资源

线程局部存储(Thread Local Storage,TLS)是一种另类的解决方式:

  • 它存储和维护一些线程相关的数据,存储的数据会被关联到当前线程中去,并不需要锁来维护
  • 本质上是为每一个使用该全局变量的线程都提供一个变量值的副本,每一个线程均可以独立地改变自己的副本,而不会和其它线程的副本冲突

TLS实现方式:API

Linux 中相关的 API:

1
2
3
4
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*)); /* 构建一个pthread_key_t类型,确实是相当于一个key */
int pthread_key_delete(pthread_key_t key); /* 注销一个pthread_key_t */
void *pthread_getspecific(pthread_key_t key); /* 将与pthread_key_t相关联的数据读出来 */
int pthread_setspecific(pthread_key_t key, const void *value); /* 用于将value的副本存储于一数据结构中,并将其与调用线程以及pthread_key_t相关联 */
  • 结构关系图如下:

本人在实际操作这些 API 时遇到了许多问题,先看一个失败的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<stdio.h>  
#include<pthread.h>
#include<string.h>

pthread_key_t p_key;

void *thread_func(void *args)
{
printf("%d is runing in %s\n",*(int*)args,__func__);
*(int*)args = *(int*)args + 1;
printf("%d is runing in %s\n",*(int*)args,__func__);
return (void*)0;
}

int main()
{
pthread_t p[24];
pthread_key_create(&p_key,NULL);
int *a = (int*)pthread_getspecific(p_key);
a = malloc(sizeof(int));
pthread_setspecific(p_key, a);
*a = 1;
for(int i=0;i<24;i++){
pthread_create(&p[i], NULL,thread_func,a);
}
return 0;
}
1
2
3
4
5
6
7
8
9
exp ./test                         
1 is runing in thread_func
2 is runing in thread_func
1 is runing in thread_func
2 is runing in thread_func
3 is runing in thread_func
5 is runing in thread_func
3 is runing in thread_func
......
  • 位于堆区的 a 本来应该充当 TLS 的作用,但各个线程好像还是共用了同一片空间
  • pthread_setspecific 对主线程进行了绑定,而忽略了其他线程(其实我还有多个测试案例:把 pthread_setspecific 放到线程函数里面,使用不同的 pthread_key_t 变量 …… 但这些操作都失败了)
  • 其实这里是我的理解有误区,认为这些 API 函数会自动帮我为不同的线程复制数据

接下来看一个成功的案例:(错误码 errno 的实现原理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<pthread.h>

void *thread_deal_func(void *arg);

#define TASK_NUM 2
pthread_t global_thread_no[TASK_NUM];

static pthread_key_t key;
static pthread_once_t key_once = PTHREAD_ONCE_INIT;

static void make_key(){
(void) pthread_key_create(&key, NULL);
}
int * _errno(){
int *ptr;
(void) pthread_once(&key_once, make_key);
if ((ptr = pthread_getspecific(key)) == NULL)
{
ptr = malloc(sizeof(int));
(void) pthread_setspecific(key, ptr);
}
return ptr ;
}

#define errno_test *_errno()
//int errno_test = 0;

int main(){
errno_test = 100;
int tmp = 0,i = 0;
for(i = 0;i < TASK_NUM; i++){
if((tmp=pthread_create(&global_thread_no[i],NULL,thread_deal_func,&i))!= 0){
printf("can't create thread: %s\n",strerror(tmp));
return -1;
}
}

while(1){
printf("man thread ,errno_test:%d\n",errno_test);
sleep(1);
}
return 0;
}

void *thread_deal_func(void *arg)
{
int number = *(int*)arg;
while(1){
errno_test += 1;
printf("thread number:%d,errno_test:%d\n",number,errno_test);
sleep(1);
}
}
  • 使用 TLS:
1
2
3
4
5
6
7
8
9
10
11
12
13
exp ./test
man thread ,errno_test:100
thread number:1,errno_test:1
thread number:2,errno_test:1
man thread ,errno_test:100
thread number:2,errno_test:2
thread number:1,errno_test:2
thread number:2,errno_test:3
thread number:1,errno_test:3
man thread ,errno_test:100
thread number:2,errno_test:4
man thread ,errno_test:100
thread number:1,errno_test:4
  • 使用全局变量:
1
2
3
4
5
6
7
8
9
10
exp ./test                         
thread number:1,errno_test:101
man thread ,errno_test:101
thread number:2,errno_test:102
thread number:2,errno_test:103
man thread ,errno_test:103
thread number:1,errno_test:104
thread number:2,errno_test:105
thread number:1,errno_test:106
man thread ,errno_test:106
  • 这里的 errno_test 看起来像是全局变量,其实是一个函数的返回值
1
#define	errno_test *_errno()
  • 每次使用 errno_test 时,都会调用 _errno() 函数
  • 在其中会申请与当前线程绑定的变量,并且调用 malloc 为其分配空间:
    • 对于各个线程来说,这些变量的地址不同了
    • 对于用户来说,各个线程拿到不同的变量,不会相互干扰

TLS实现方式:关键字

在 Linux 中还有一种更为高效的线程局部存储方法,就是使用关键字 __thread 来定义变量

  • 凡是带有 __thread 的变量,每个线程都拥有该变量的一份拷贝,且互不干扰
  • 线程局部存储中的变量将一直存在,直至线程终止,当线程终止时会自动释放这一存储

测试案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
#include<stdlib.h>
#include <pthread.h>

__thread int var = 0;

void* task_entry(void* arg){
int idx = (int)arg;
int i;
printf("addr : %p\n",&var);
for (i = 0; i < 5; ++i) {
printf("thread:%d var = %d\n", idx, var += idx);
sleep(1);
}
}

int main(){
pthread_t pid1,pid2;
pthread_create(&pid1,NULL,task_entry,(void *)1);
pthread_create(&pid2,NULL,task_entry,(void *)2);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
exp ./test                         
addr : 0x7f20b3ebe6fc
thread:1 var = 1
addr : 0x7f20b36bd6fc
thread:2 var = 2
thread:1 var = 2
thread:2 var = 4
thread:2 var = 6
thread:1 var = 3
thread:1 var = 4
thread:2 var = 8
thread:2 var = 10
thread:1 var = 5
  • 两个线程并不会干扰,而且地址也不同

现在我们就分析一下 __thread 底层的逻辑,测试案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<stdio.h>
#include<stdlib.h>
#include <pthread.h>

__thread size_t name1 = 0x111111;
__thread size_t name2 = 0x222222;
size_t name3 = 0x333333;
size_t name4 = 0x444444;
__thread size_t name5 = 0x555555;
__thread size_t name6 = 0x666666;

void* task_entry1(){
printf("addr:0x%lx\n",name1);
name1 = 0x777777;
while(1){}
}

void* task_entry2(){
printf("addr:0x%lx\n",name2);
name1 = 0x888888;
while(1){}
}

int main(){
pthread_t pid1,pid2;
pthread_create(&pid1,NULL,task_entry1,NULL);
pthread_create(&pid2,NULL,task_entry2,NULL);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
return 0;
}

打开 GDB 进行调试:

  • 在 GDB 中输入 tls 可以显示 TLS 的位置(在 ubuntu 中 TLS 存储在 FS 寄存器中)
1
2
pwndbg> tls 
tls : 0x7ffff7d99740
  • 而之前添加了 __thread 关键字的变量则存储在 TLS 的上方
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7ffff7d99740-0x20
00:00000x7ffff7d99720 ◂— 0x111111
01:00080x7ffff7d99728 ◂— 0x222222 /* '"""' */
02:00100x7ffff7d99730 ◂— 0x555555 /* 'UUU' */
03:00180x7ffff7d99738 ◂— 0x666666 /* 'fff' */
04:00200x7ffff7d99740 ◂— 0x7ffff7d99740
05:00280x7ffff7d99748 —▸ 0x7ffff7d9a0a0 ◂— 0x1
06:00300x7ffff7d99750 —▸ 0x7ffff7d99740 ◂— 0x7ffff7d99740
07:00380x7ffff7d99758 ◂— 0x1
  • 函数 pthread_create 会在 heap 上申请一片区域:
1
2
3
4
5
6
7
Allocated chunk | PREV_INUSE
Addr: 0x555555559290
Size: 0x131

Allocated chunk | PREV_INUSE
Addr: 0x5555555593c0
Size: 0x131
  • 查看里面记录的信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> telescope 0x555555559290
00:00000x555555559290 ◂— 0x0
01:00080x555555559298 ◂— 0x131
02:00100x5555555592a0 ◂— 0x10
03:00180x5555555592a8 ◂— 0x0
04:00200x5555555592b0 ◂— 0x1
05:00280x5555555592b8 ◂— 0x0
06:00300x5555555592c0 —▸ 0x7ffff7d986e0 ◂— 0x777777 /* 'www' */
07:00380x5555555592c8 ◂— 0x0
08:00400x5555555592d0 —▸ 0x7ffff7d98650 —▸ 0x7ffff7f894a0 (_nl_global_locale) —▸ 0x7ffff7f856c0 (_nl_C_LC_CTYPE) —▸ 0x7ffff7f51fd9 (_nl_C_name) ◂— ...
09:00480x5555555592d8 ◂— 0x0
pwndbg> telescope 0x7ffff7d986e0
00:00000x7ffff7d986e0 ◂— 0x777777 /* 'www' */
01:00080x7ffff7d986e8 ◂— 0x222222 /* '"""' */
02:00100x7ffff7d986f0 ◂— 0x555555 /* 'UUU' */
03:00180x7ffff7d986f8 ◂— 0x666666 /* 'fff' */
04:0020│ r15 0x7ffff7d98700 ◂— 0x7ffff7d98700
05:00280x7ffff7d98708 —▸ 0x5555555592b0 ◂— 0x1
06:00300x7ffff7d98710 —▸ 0x7ffff7d98700 ◂— 0x7ffff7d98700
07:00380x7ffff7d98718 ◂— 0x1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> telescope 0x5555555593c0
00:00000x5555555593c0 ◂— 0x0
01:00080x5555555593c8 ◂— 0x131
02:00100x5555555593d0 ◂— 0x10
03:00180x5555555593d8 ◂— 0x0
04:00200x5555555593e0 ◂— 0x1
05:00280x5555555593e8 ◂— 0x0
06:00300x5555555593f0 —▸ 0x7ffff75976e0 ◂— 0x888888
07:00380x5555555593f8 ◂— 0x0
08:00400x555555559400 —▸ 0x7ffff7597650 —▸ 0x7ffff7f894a0 (_nl_global_locale) —▸ 0x7ffff7f856c0 (_nl_C_LC_CTYPE) —▸ 0x7ffff7f51fd9 (_nl_C_name) ◂— ...
09:00480x555555559408 ◂— 0x0
pwndbg> telescope 0x7ffff75976e0
00:00000x7ffff75976e0 ◂— 0x888888
01:00080x7ffff75976e8 ◂— 0x222222 /* '"""' */
02:00100x7ffff75976f0 ◂— 0x555555 /* 'UUU' */
03:00180x7ffff75976f8 ◂— 0x666666 /* 'fff' */
04:0020│ r9 0x7ffff7597700 ◂— 0x7ffff7597700
05:00280x7ffff7597708 —▸ 0x5555555593e0 ◂— 0x1
06:00300x7ffff7597710 —▸ 0x7ffff7597700 ◂— 0x7ffff7597700
07:00380x7ffff7597718 ◂— 0x1
  • 其实看到这里就很清晰了
  • 函数 pthread_create 在调用时会把主线程 TLS 上方存储 __thread 变量的内存空间给复制一份,添加到自己的 TLS 上方
  • 然后申请一片 heap 空间来记录数据
  • 如果 pthread_create 生成的线程想要写入带有 __thread 关键字的变量,就会操作自己 TLS 上方所记录的变量,而不会影响到其他线程