Zephyr TLS线程本地存储的实现

Creative Commons
本作品采用知识共享署名

本文基于Cortex-M架构分析说明Zephyr中TLS(Thread Local Storage)的实现原理。

什么是TLS

TLS是Thread Local Storage的缩写,中文叫做线程本地存储。在Zephyr的所有线程共享同一个地址空间,因此对于Zephyr线程来说一个变量是全局的或者是静态访问的都是同一份,当其中一个线程对其进行了修改,就会影响到其他所有的线程。在一些场景下,只希望变量在它所在的线程内是全局可访问的,但能被其他线程访问到,以保持数据的线程独立性,操作系统提供TLS(Thread Local Storage)线程本地存储特性来达到该目的。
简单的说就是:定义一个全局变量后,在不同的线程中都有自己的内存空间。

Zephyr如何使用TLS

非常简单,在prj.conf中添加配置项CONFIG_THREAD_LOCAL_STORAGE=y,将使用关键字’__thread’声明TLS变量,之后在不同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
32
33
34
35
36
37
38
39
40
__thread int tls_check = 2;
__thread int tls_checka;
__thread int tls_checkb[32];
__thread int tls_checkc;

void tls_fun(void)
{
int tmp = tls_check;
tmp++;
tls_check = tmp;
tls_checka = tmp;
tls_checkb[0] = tmp;
tls_checkc = tmp;
}

void tls_check1(void)
{
int i = 0;
while (1) {
tls_fun();
printk("%s[%d]: check %d[%p] %d[%p] diff %x\n", __FUNCTION__, i++, tls_check, &tls_check, tls_checkc, &tls_checkc, &tls_checkc - &tls_check);
k_msleep(1000);
}
}

void tls_check2(void)
{
int i = 0;
while (1) {
tls_fun();
printk("%s[%d]: check %d[%p] %d[%p] diff %x\n", __FUNCTION__, i++, tls_check, &tls_check, tls_checkc, &tls_checkc, &tls_checkc - &tls_check);
k_msleep(2000);
}
}


K_THREAD_DEFINE(tls_check1_id, STACKSIZE, tls_check1, NULL, NULL, NULL,
PRIORITY, 0, 0);
K_THREAD_DEFINE(tls_check2_id, STACKSIZE, tls_check2, NULL, NULL, NULL,
PRIORITY, 0, 0);

线程tls_check1/tls_check2通过函数tls_fun访问TLS变量tls_check/tls_checka/tls_checkb/tls_checkc, 这几个变量分别是tls_check1和tls_check2各一份,两个线程相互不干扰。下面是运行结果,可以看到监控tls_check和tls_checkc在不同线程之间是按自己的步调进行增加的

1
2
3
4
5
6
tls_check1[172]: check 173[0x20002c3c] 173[0x20002cc4] diff 22
tls_check1[173]: check 174[0x20002c3c] 174[0x20002cc4] diff 22
tls_check2[87]: check 88[0x2000543c] 88[0x200054c4] diff 22
tls_check1[174]: check 175[0x20002c3c] 175[0x20002cc4] diff 22
tls_check1[175]: check 176[0x20002c3c] 176[0x20002cc4] diff 22
tls_check2[88]: check 89[0x2000543c] 89[0x200054c4] diff 22

TLS实现

TLS的使用方法和使用的编程语言/编译工具相关,实现方法和操作系统/体系结构/编译工具相关。Zephyr使用的C语言,GCC编译器,这里我们分析cortex-m下Zephyr TLS的实现。

GCC对TLS的支持方式

GCC通过下面两个手段提TLS功能

  1. GCC下当变量使用’__thread’进行声明后,编译/链接时会将该变量放入.tdata或.tbss段
  2. 对于使用__thread声明的变量的代码,汇编时插入代码,通过__aeabi_read_tp获取TLS变量基地址,再通过固定的偏移访问TLS变量
    在步骤1中TLS变量的相对偏移就已经确认,步骤2插入代码的时候根据访问TLS变量的不同使用不同的偏移地址, 对于前面的示例代码编译的结果:
    通过readelf查看section, 能看到tdata/tbss
    1
    2
    [ 7] tdata             PROGBITS        00005eec 005fc0 000004 00 WAT  0   0  4
    [ 8] tbss NOBITS 00005ef0 005fc4 000088 00 WAT 0 0 4

通过objdump查看map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tdata           0x0000000000005eec        0x4
*(SORT_BY_ALIGNMENT(.tdata) SORT_BY_ALIGNMENT(.tdata.*) SORT_BY_ALIGNMENT(.gnu.linkonce.td.*))
.tdata.tls_check
0x0000000000005eec 0x4 app/libapp.a(main.c.obj)
0x0000000000005eec tls_check

tbss 0x0000000000005ef0 0x88
*(SORT_BY_ALIGNMENT(.tbss) SORT_BY_ALIGNMENT(.tbss.*) SORT_BY_ALIGNMENT(.gnu.linkonce.tb.*) SORT_BY_ALIGNMENT(.tcommon))
.tbss.tls_checka
0x0000000000005ef0 0x4 app/libapp.a(main.c.obj)
0x0000000000005ef0 tls_checka
.tbss.tls_checkb
0x0000000000005ef4 0x80 app/libapp.a(main.c.obj)
0x0000000000005ef4 tls_checkb
.tbss.tls_checkc
0x0000000000005f74 0x4 app/libapp.a(main.c.obj)
0x0000000000005f74 tls_checkc

通过反汇编可以看到访问TLS变量的方法,见注释(只注释tls_check的过程,其它的一致)

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
void tls_fun(void)
{
3fc: b580 push {r7, lr}
3fe: b082 sub sp, #8
400: af00 add r7, sp, #0
int tmp = tls_check;
402: f005 fc8b bl 5d1c <__aeabi_read_tp> ;调用函数__aeabi_read_tp获取TLS存放的位置,放入r0
406: 4603 mov r3, r0 ; TLS的基地址放入r3
408: 4a10 ldr r2, [pc, #64] ; (44c <CONFIG_MAIN_STACK_SIZE+0x4c>), 从44c处加载tls_check的偏移地址,44c处存放的是00000008,tls_check的偏移地址为00000008
40a: 589b ldr r3, [r3, r2] ; 取tls_check的值放入r3,r3基地址+0x00000008就是tls_check的地址
40c: 607b str r3, [r7, #4]
tmp++;
40e: 687b ldr r3, [r7, #4]
410: 3301 adds r3, #1
412: 607b str r3, [r7, #4]
tls_check = tmp;
414: f005 fc82 bl 5d1c <__aeabi_read_tp>
418: 4602 mov r2, r0
41a: 490c ldr r1, [pc, #48] ; (44c <CONFIG_MAIN_STACK_SIZE+0x4c>)
41c: 687b ldr r3, [r7, #4]
41e: 5053 str r3, [r2, r1]
tls_checka = tmp;
420: f005 fc7c bl 5d1c <__aeabi_read_tp>
424: 4602 mov r2, r0
426: 490a ldr r1, [pc, #40] ; (450 <CONFIG_MAIN_STACK_SIZE+0x50>)
428: 687b ldr r3, [r7, #4]
42a: 5053 str r3, [r2, r1]
tls_checkb[0] = tmp;
42c: f005 fc76 bl 5d1c <__aeabi_read_tp>
430: 4602 mov r2, r0
432: 4908 ldr r1, [pc, #32] ; (454 <CONFIG_MAIN_STACK_SIZE+0x54>)
434: 687b ldr r3, [r7, #4]
436: 5053 str r3, [r2, r1]
tls_checkc = tmp;
438: f005 fc70 bl 5d1c <__aeabi_read_tp>
43c: 4602 mov r2, r0
43e: 4906 ldr r1, [pc, #24] ; (458 <CONFIG_MAIN_STACK_SIZE+0x58>)
440: 687b ldr r3, [r7, #4]
442: 5053 str r3, [r2, r1]
}
444: bf00 nop
446: 3708 adds r7, #8
448: 46bd mov sp, r7
44a: bd80 pop {r7, pc}
44c: 00000008 andeq r0, r0, r8 ;tls_check偏移地址
450: 0000000c andeq r0, r0, ip ;tls_checka偏移地址
454: 00000010 andeq r0, r0, r0, lsl r0 ;tls_checkb偏移地址
458: 00000090 muleq r0, r0, r0 ;tls_checkc偏移地址

0000045c <tls_check1>:
tls_checkc = tmp;
}

从map文件和汇编可以对比确认各个变量之间的相对位置。

从上面的流程可以看出来,TLS变量的访问并不是绝对地址,而是通过aeabi_read_tp获取基地址,然后通过偏移地址进行方法。如果不同的thread下aeabi_read_tp返回的基地址不一样那么访问TLS变量就在不同内存上,这就达到了TLS在thread之间独立的目的。

Zephyr上TLS的实现

从上段内容的分析可以看只要OS放置好TLS的section和实现了__aeabi_read_tp在不同线程之间的切换,就支持TLS。下图示意了Zephyr如何实现TLS

  1. 创建线程时,在线程堆栈中开辟TLS变量空间,struct thread的tls字段指向该空间
  2. 开辟TLS变量空间后,将tdata拷贝到TLS变量空间内,并清0其中的bss
  3. 在线程发生切换时,改变z_arm_tls_ptr的内容为当前线程的tls,这样__aeabi_read_tp返回的就是当前线程堆栈中TLS变量空间的基地址

下面详细分析实现过程

链接加载文件

前面分析可以看到创建线程时需要从tdata进行拷贝数据,并对bss清0,因此需要一个位置保存tdata和tbss,Zephyr在链接加载文件中提供tdata和tbss的放置位置,文件是zephyr/include/linker/thread-local-storage.ld,包含关系为

1
include/arch/arm/aarch32/cortex_m/scripts/linker.ld:#include <linker/thread-local-storage.ld>

内容如下

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
#ifdef CONFIG_THREAD_LOCAL_STORAGE

SECTION_DATA_PROLOGUE(tdata,,)
{
*(.tdata .tdata.* .gnu.linkonce.td.*);
} GROUP_ROM_LINK_IN(RAMABLE_REGION, ROMABLE_REGION)

SECTION_DATA_PROLOGUE(tbss,,)
{
*(.tbss .tbss.* .gnu.linkonce.tb.* .tcommon);
} GROUP_ROM_LINK_IN(RAMABLE_REGION, ROMABLE_REGION)

/*
* These needs to be outside of the tdata/tbss
* sections or else they would be considered
* thread-local variables, and the code would use
* the wrong values.
*/
#ifdef CONFIG_XIP
/* The "master copy" of tdata should be only in flash on XIP systems */
PROVIDE(__tdata_start = LOADADDR(tdata));
#else
PROVIDE(__tdata_start = ADDR(tdata));
#endif
PROVIDE(__tdata_size = SIZEOF(tdata));
PROVIDE(__tdata_end = __tdata_start + __tdata_size);
PROVIDE(__tdata_align = ALIGNOF(tdata));

PROVIDE(__tbss_start = ADDR(tbss));
PROVIDE(__tbss_size = SIZEOF(tbss));
PROVIDE(__tbss_end = __tbss_start + __tbss_size);
PROVIDE(__tbss_align = ALIGNOF(tbss));

PROVIDE(__tls_start = __tdata_start);
PROVIDE(__tls_end = __tbss_end);
PROVIDE(__tls_size = __tbss_end - __tdata_start);

#endif /* CONFIG_THREAD_LOCAL_STORAGE */

tdata内是要初始化的变量,gcc为其保留空间并进行初始化。tbss是不需要初始化的变量,gcc为了节省内存空间并没有实际的为tbss保留空间,这一点从section的信息也可以看到tbss和rodata的地址是重合的

1
2
3
[ 7] tdata             PROGBITS        00005eec 005fc0 000004 00 WAT  0   0  4
[ 8] tbss NOBITS 00005ef0 005fc4 000088 00 WAT 0 0 4
[ 9] rodata PROGBITS 00005ef0 005fc4 000174 00 A 0 0 4

__aeabi_read_tp

该函数实现在zephyr/arch/arm/core/aarch32/cortex_m/__aeabi_read_tp.S, 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <toolchain.h>

_ASM_FILE_PROLOGUE

GTEXT(__aeabi_read_tp)

GDATA(z_arm_tls_ptr)

SECTION_FUNC(TEXT, __aeabi_read_tp)
/* Grab the TLS pointer and store in R0 */
ldr r0, =z_arm_tls_ptr
ldr r0, [r0]
bx lr

__aeabi_read_tp函数返回的就是z_arm_tls_ptr变量的值,该变量定义在zephyr/arch/arm/core/common/tls.c

1
K_APP_DMEM(z_libc_partition) uintptr_t z_arm_tls_ptr;

线程切换时前会将当前thread的tls字段值送到z_arm_tls_ptr内,从而达到切换TLS基地址的目的,这部分流程代码在zephyr/arch/arm/core/aarch32/swap_helper.S中做thread上下文切换的地方,下面代码做了简化只列出TLS相关的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SECTION_FUNC(TEXT, z_arm_pendsv)
...
str r2, [r1, #_kernel_offset_to_current] ;取得当前thread管理结构体的地址
#if defined(CONFIG_THREAD_LOCAL_STORAGE)
/* Grab the TLS pointer */
ldr r4, =_thread_offset_to_tls ;取得tls的偏移地址
adds r4, r2, r4 ;取得tls的地址
ldr r0, [r4] ;将tls的值放入r0

#if defined(CONFIG_CPU_CORTEX_M)
/* For Cortex-M, store TLS pointer in a global variable,
* as it lacks the process ID or thread ID register
* to be used by toolchain to access thread data.
*/
ldr r4, =z_arm_tls_ptr
str r0, [r4] ;将tls的值放入z_arm_tls_ptr
#endif

#endi

线程堆栈TLS初始化

在线程创建时会使用arch_tls_stack_setup函数为该线程在堆栈中开辟TLS空间:
z_setup_new_thread->setup_thread_stack->arch_tls_stack_setup
实现在zephyr/arch/arm/core/common/tls.c中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
size_t arch_tls_stack_setup(struct k_thread *new_thread, char *stack_ptr)
{
//计算tdata/tbss大小
stack_ptr -= z_tls_data_size();
//将tdata拷贝到堆栈上,并清0 bss
z_tls_copy(stack_ptr);

//gcc的特性在TLS顶端空出8个字节,这也就是为什么前面示例中tls_check的偏移地址是0x00000008
stack_ptr -= sizeof(uintptr_t) * 2;

//将TLS变量在堆栈中的基地址保存在tls字段中
new_thread->tls = POINTER_TO_UINT(stack_ptr);

return (z_tls_data_size() + (sizeof(uintptr_t) * 2));
}

tdata/tbss大小计算和拷贝函数实现在zephyr/kernel/include/kernel_tls.h中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static inline size_t z_tls_data_size(void)
{
size_t tdata_size = ROUND_UP(__tdata_size, __tdata_align);
size_t tbss_size = ROUND_UP(__tbss_size, __tbss_align);

return tdata_size + tbss_size;
}


static inline void z_tls_copy(char *dest)
{
size_t tdata_size = (size_t)__tdata_size;
size_t tbss_size = (size_t)__tbss_size;

//拷贝tdata
memcpy(dest, __tdata_start, tdata_size);

//清0 bss
dest += ROUND_UP(tdata_size, __tdata_align);
memset(dest, 0, tbss_size);
}

关于Zephyr使用TLS的看法

TLS实现原理来看我们知道Zephyr下无论线程是否真的要使用TLS变量,每个线程都会为TLS变量准备一份存储空间,并且这份空间是放放到线程堆栈中的。因此不适合将大的变量作为TLS,这样会造成内存的浪费。

参考

https://docs.zephyrproject.org/latest/reference/kernel/other/thread_local_storage.html
https://www.akkadia.org/drepper/tls.pdf