Zephyr TLS线程本地存储的实现

本文基于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中访问该变量就不会相互影响。本文的测试例程如下:

__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在不同线程之间是按自己的步调进行增加的

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
  [ 7] tdata             PROGBITS        00005eec 005fc0 000004 00 WAT  0   0  4
  [ 8] tbss              NOBITS          00005ef0 005fc4 000088 00 WAT  0   0  4

通过objdump查看map

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的过程,其它的一致)

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,包含关系为

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

内容如下

#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的地址是重合的

  [ 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, 内容如下

#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

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相关的流程

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中

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中

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