线程本地存储


线程本地存储(TLS: thread local storage)为每个线程有独立的存储空间来存储线程变量。当一个线程修改线程变量的值时,不影响另外一个线程读取线程变量的值。因为线程变量在每个线程的TLS都有一份,访问时只能读取所在TLS中的线程变量值。

在x86-64系统下,与线程本地存储有关的寄存器是FS寄存器:

  • FS寄存器:用户态使用FS寄存器保存线程本地存储的基址

可用通过调用系统函数设置或者查询TLS基址:

1
2
int syscall(SYS_arch_prctl, int code, unsigned long addr);
int syscall(SYS_arch_prctl, int code, unsigned long *addr);

其中code的取值:

ARCH_SET_FS

Set the 64-bit base for the FS register to addr.

ARCH_GET_FS

Return the 64-bit base value for the FS register of the current thread in the unsigned long pointed to by addr.

ARCH_SET_GS

Set the 64-bit base for the GS register to addr.

ARCH_GET_GS

Return the 64-bit base value for the GS register of the current thread in the unsigned long pointed to by addr.

Go本身不提供线程本地存储能力(对于Go编程人员来说,直接面对的是协程),而是在内核使用了TLS机制来协助完成协程的调度。

Go内核使用到LTS的有以下几处地方:

  1. 在启动函数runtime·rt0_go
1
2
3
4
5
6
7
LEAQ    runtime·m0+m_tls(SB), DI // 将runtime·m0.tls地址存入DI寄存器
CALL runtime·settls(SB) // 调用runtime·settls函数
...
get_tls(BX) // 将LTS地址(下限地址)保存到BX寄存器
LEAQ runtime·g0(SB), CX // 将runtime·g0地址存入CX寄存器
MOVQ CX, g(BX) // 将CX值(runtime·g0地址)存入LTS地址
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TEXT runtime·settls(SB),NOSPLIT,$32
#ifdef GOOS_android
// Android stores the TLS offset in runtime·tls_g.
SUBQ runtime·tls_g(SB), DI
#else
ADDQ $8, DI // ELF wants to use -8(FS) // 将DI值+8(runtime·m0.tls+8)并存入DI寄存器
#endif
MOVQ DI, SI // 将DI值存入SI寄存器
MOVQ $0x1002, DI // 将0x1002(ARCH_SET_FS)存入DI寄存器
MOVQ $SYS_arch_prctl, AX // 将SYS_arch_prctl函数地址存入AX寄存器
SYSCALL // 调用SYSCALL
CMPQ AX, $0xfffffffffffff001 // 比较AX值和-1比较,判断系统调用范围值是否为-1
JLS 2(PC) // 小于-1,则跳转到PC+2指令即RET
MOVL $0xf1, 0xf1 // crash // 等于-1,则crash
RET
1
#define get_tls(r) MOVQ TLS, r

以上代码的目的是将m0.tls保存到FS寄存器,作为LTS的基址。其中m结构体中的tls为长度为6的uintptr数组,即每个m可以用来作为LTS的空间大小为48字节。因为FS寄存器存储的TLS基址是TLS的上限地址,所以上述汇编runtime·settls要将runtime·m0.tls+8保存到FS寄存器,即TLS空间大小为8字节,即runtime·m0.tls[0]内存空间。

然后将runtime·g0地址保存到LTS中。

1
2
3
4
5
type m struct {
...
tls [tlsSlots]uintptr // thread-local storage (for x86 extern register)
...
}
  1. 在创建m的过程中,调用newosproc创建系统线程,newosproc会调用clone函数,在clone函数中将调用SYS_clone,同样将m.tls[0]作为LTS,并将m.g0保存到LTS中。
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
// int32 clone(int32 flags, void *stk, M *mp, G *gp, void (*fn)(void));
TEXT runtime·clone(SB),NOSPLIT,$0
MOVL flags+0(FP), DI
MOVQ stk+8(FP), SI
MOVQ $0, DX
MOVQ $0, R10
MOVQ $0, R8
// Copy mp, gp, fn off parent stack for use by child.
// Careful: Linux system call clobbers CX and R11.
MOVQ mp+16(FP), R13
MOVQ gp+24(FP), R9
MOVQ fn+32(FP), R12
CMPQ R13, $0 // m
JEQ nog1
CMPQ R9, $0 // g
JEQ nog1
LEAQ m_tls(R13), R8
#ifdef GOOS_android
// Android stores the TLS offset in runtime·tls_g.
SUBQ runtime·tls_g(SB), R8
#else
ADDQ $8, R8 // ELF wants to use -8(FS)
#endif
ORQ $0x00080000, DI //add flag CLONE_SETTLS(0x00080000) to call clone
nog1:
MOVL $SYS_clone, AX
SYSCALL
...
// In child, set up new stack
get_tls(CX)
MOVQ R13, g_m(R9)
MOVQ R9, g(CX)
  1. 在调度器选择可运行g后,使用gogo执行g的过程中将执行的g地址保存到LTS中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
TEXT runtime·gogo(SB), NOSPLIT, $0-8
MOVQ buf+0(FP), BX // FP指向入参起始位置,所以FP+0指向第一个入参,即gobuf
MOVQ gobuf_g(BX), DX // 将gobuf.g存入DX
MOVQ 0(DX), CX // 将gobuf.g存入CX
JMP gogo<>(SB) // 跳转到gogo

TEXT gogo<>(SB), NOSPLIT, $0
get_tls(CX) // 将TLS地址存入CX
MOVQ DX, g(CX) // 将DX的值(gobuf.g)存入CX指向的地址(TLS)
MOVQ DX, R14 // 将DX的值(gobuf.g)存入R14
MOVQ gobuf_sp(BX), SP // 将gobuf.sp存入SP
MOVQ gobuf_ret(BX), AX // 将gobuf.ret存入AX
MOVQ gobuf_ctxt(BX), DX // 将gobuf.ctxt存入DX
MOVQ gobuf_bp(BX), BP // 将gobuf.bp存入BP
MOVQ $0, gobuf_sp(BX) // 将gobuf.sp清零
MOVQ $0, gobuf_ret(BX) // 将gobuf.ret清零
MOVQ $0, gobuf_ctxt(BX) // 将gobuf.ctxt清零
MOVQ $0, gobuf_bp(BX) // 将gobuf.bp清零
MOVQ gobuf_pc(BX), BX // 将gobuf.pc存入BX
JMP BX // 跳转到BX指向的地址
  1. 从LTS中读取的关键函数是getggetg是内建函数,从注释可以看出是从TLS中读取保存的g对象。
1
2
3
4
// getg returns the pointer to the current g.
// The compiler rewrites calls to this function into instructions
// that fetch the g directly (from TLS or from the dedicated register).
func getg() *g

当然还有其它地方使用到了LTS,概括起来就是,在创建m时,将g0地址存放在线程的LTS中;在切换协程时,将切换后的协程地址存放在线程的LTS中;在需要获取当前协程的地方,从LTS中读取当前协程的g地址。