前言
前面寫了一篇文章《Glibc 線程資源分配與釋放-----線程棧》,其中主要講解了 glibc 在 x86_64 平臺 Linux 系統上的線程棧區管理。但是這并不是全部的線程資源,本文中我們將介紹另外兩類資源的,以 __thread 定義的變量以及 pthread_key_create 創建的鍵值對資源。我們仍然以 x86_64 Linux 平臺為例,分析的源碼是 glibc-2.25。
__thread 變量
__thread 標識符修飾的全局或靜態變量是線程獨立的,線程對該變量的操作對其它線程來說是不可見的。然而線程之間共享內存空間的,因此要達到如些效果就需要針對該變量為每個線程分配變量的存儲位置。在 Glibc 中, 所有的 __thread 變量是與 pthread 關聯存儲的,通過相對于 pthread 變量地址的偏移實現對變量的尋址。即是說,pthread 變量的地址是基址。
在《Glibc 線程資源分配與釋放-----線程棧》中提到,pthread 是存儲在線程棧內存塊中的,在線程棧布局圖中,我們省略了為 __thread 變量預留的內存空間。下圖說明了__thread 變量在線程棧內存空間中的存儲。
下面這段代碼可以驗證這個結論。
#include <pthread.h>
#include <stdio.h>
#include <asm/prctl.h>
#include <sys/prctl.h>
#include <errno.h>
static int __thread var_1 = 0;
static short __thread var_2 = 0;
void* func(void *f) {
long pthread_addr = 0;
arch_prctl(ARCH_GET_FS, &pthread_addr);
printf("&pthread = %p, &var_1 = %p, off_var_1 = %d, &var_2 = %p, off_var_2 = %d\n",
pthread_addr, &var_1, (long)&var_1 - pthread_addr,
&var_2, (long)&var_2 - pthread_addr );
return NULL;
}
int main() {
pthread_t chs[3];
int i = 0;
for (i = 0; i < 3; ++i) {
pthread_create(&chs[i], NULL, func, NULL);
}
for (i = 0; i < 3; ++i) {
pthread_join(chs[i], NULL);
}
return 0;
}
上面這段代碼中,比較難以理解的在于為什么 arch_prctl 函數取得的是 pthread 的地址。在解答這個問題之間,我們需要先看一下創建線程的 clone 函數 以及調用時傳入的 flags 參數 CLONE_SETTLS。
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, void *newtls, pid_t *ctid */ );
CLONE_SETTLS
(since Linux 2.5.32) The TLS (Thread Local Storage) descriptor is set to newtls. The interpretation of newtls and the resulting effect is architecture dependent. On x86, newtls is interpreted as a **struct user_desc ** (See set_thread_area(2)). On x86_64 it is the new value to be set for the %fs base register (See the ARCH_SET_FS argument to arch_prctl(2)). On architectures with a dedicated TLS register, it is the new value of that register.
這段描述說明, 在 x86_64 位系統上,clone 函數傳入的 newtls 參數會作為 fs 寄存器的基地址,并且該地址值能通過 arch_prctl 函數獲得。在《Glibc 線程資源分配與釋放-----線程棧》中我們也看到了,newtls 傳入的實參就是 pthread 變量的地址 (pd)。
if (__glibc_unlikely (ARCH_CLONE (&start_thread, STACK_VARIABLES_ARGS,
clone_flags, pd, &pd->tid, tp, &pd->tid)
== -1))
因此,可以說明 arch_prctl 函數獲得的就是線程 pthread 變量的地址值。執行一次上面程序的結果(每次執行結果可能不同):
&pthread = 0x7f8325948700, &var_1 = 0x7f83259486f8, off_var_1 = -8, &var_2 = 0x7f83259486fc, off_var_2 = -4
&pthread = 0x7f8324f47700, &var_1 = 0x7f8324f476f8, off_var_1 = -8, &var_2 = 0x7f8324f476fc, off_var_2 = -4
&pthread = 0x7f8324546700, &var_1 = 0x7f83245466f8, off_var_1 = -8, &var_2 = 0x7f83245466fc, off_var_2 = -4
可以看出, var_1 與 var_2 確實存入在線程 pthread 地址的下端,不同線程訪問的變量的地址是不相同的,但是變量相對于 pthread 地址的偏移是相同的,在本例中分別是 -8 與 -4。
鍵值對資源
另外一種創建線程特定數據(Tthread-specific data)的方式是通過 pthread_key_create 創建鍵值映射。每個線程通過鍵訪問線程特定的數據。glibc 中鍵集中分配管理,值分開存儲的方式提供 TSD 數據。
鍵的分配
pthread_key_create 創建的鍵事實上是一個無符號的整型數(sysdeps/x86/bits/pthreadtypes.h):
/* Keys for thread-specific data */
typedef unsigned int pthread_key_t;
glibc 定義了一個全局數組用于管理鍵是否已被創建,這個全局數組定義在 nptl/vars.c 中(如下)。每個鍵都會對應于數組中一個 pthread_struct_t 結構體,該結構體描述了鍵是否已正被使用。由數組定義可以看出, 一個進程中最多個通過 pthread_key_create 創建 PTHREAD_KEYS_MAX (1024)個鍵。
/* Table of the key information. */
struct pthread_key_struct __pthread_keys[PTHREAD_KEYS_MAX]
如下 (sysdeps/nptl/internaltypes.h), pthread_key_struct 中定義一個序號值(seq)及一個用于釋放數據的“析構函數” (destr)。
/* Thread-local data handling. */
struct pthread_key_struct
{
/* Sequence numbers. Even numbers indicated vacant entries. Note
that zero is even. We use uintptr_t to not require padding on
32- and 64-bit machines. On 64-bit machines it helps to avoid
wrapping, too. */
uintptr_t seq;
/* Destructor for the data. */
void (*destr) (void *);
};
seq 用于判斷對應的鍵是否被創建,若 seq 是奇數則正被使用,若為偶數則未被使用。 例如,若 __pthread_keys[3].seq &1 == 0 為 True 則說明該鍵 3 沒有被創建,否則已被創建。 destr 允許應用創建鍵時定義一個釋放資源的函數。
鍵值的映射
鍵值的映射信息是存儲在各線程的 pthread 結構體中的。最直接的方法是在每個 pthread 結構體中也定義一個類似于 __pthread_keys 的數組, 該數組中存儲 key-value 的映射關系。不過為了節約內存空間(大部分情況下應用只會使用很少的 key), pthread 并不是直接創建一個長度為 1024 的數組,而是使用了兩級數組的方式來存儲這種映射關系。先來看一下 pthread 結構體中存儲映射關系的變量:
/* We allocate one block of references here. This should be enough
to avoid allocating any memory dynamically for most applications. */
struct pthread_key_data
{
/* Sequence number. We use uintptr_t to not require padding on
32- and 64-bit machines. On 64-bit machines it helps to avoid
wrapping, too. */
uintptr_t seq;
/* Data pointer. */
void *data;
} specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE];
/* Two-level array for the thread-specific data. */
struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE];
在 pthread 中定義了一個結構體 pthread_key_data 存儲指向數據的指針(data)。同樣的,其中 seq 標識對應的鍵是否被創建。pthread 中定義了一個 pthread_key_data 的數組 specific_1stblock 以及指針數組 specific。 當鍵較少時,映射關系直接存儲到 sepcific_1stblock 中,隨著鍵的增加,再分配空間存儲到 specific 中。為了說明這個過程,我們來看一下 pthread_setspecific 函數(nptl/pthread_setspecific.c):
int
__pthread_setspecific (pthread_key_t key, const void *value)
{
struct pthread *self;
unsigned int idx1st;
unsigned int idx2nd;
struct pthread_key_data *level2;
unsigned int seq;
self = THREAD_SELF;
/* Special case access to the first 2nd-level block. This is the
usual case. */
if (__glibc_likely (key < PTHREAD_KEY_2NDLEVEL_SIZE))
{
/* Verify the key is sane. */
if (KEY_UNUSED ((seq = __pthread_keys[key].seq)))
/* Not valid. */
return EINVAL;
level2 = &self->specific_1stblock[key];
/* Remember that we stored at least one set of data. */
if (value != NULL)
THREAD_SETMEM (self, specific_used, true);
}
else
{
if (key >= PTHREAD_KEYS_MAX
|| KEY_UNUSED ((seq = __pthread_keys[key].seq)))
/* Not valid. */
return EINVAL;
idx1st = key / PTHREAD_KEY_2NDLEVEL_SIZE;
idx2nd = key % PTHREAD_KEY_2NDLEVEL_SIZE;
/* This is the second level array. Allocate it if necessary. */
level2 = THREAD_GETMEM_NC (self, specific, idx1st);
if (level2 == NULL)
{
if (value == NULL)
/* We don't have to do anything. The value would in any case
be NULL. We can save the memory allocation. */
return 0;
level2
= (struct pthread_key_data *) calloc (PTHREAD_KEY_2NDLEVEL_SIZE,
sizeof (*level2));
if (level2 == NULL)
return ENOMEM;
THREAD_SETMEM_NC (self, specific, idx1st, level2);
}
/* Pointer to the right array element. */
level2 = &level2[idx2nd];
/* Remember that we stored at least one set of data. */
THREAD_SETMEM (self, specific_used, true);
}
/* Store the data and the sequence number so that we can recognize
stale data. */
level2->seq = seq;
level2->data = (void *) value;
return 0;
}
從函數中可以看出,如果 key 小于第 specific_1stblock 數組大小(PTHREA_KEY_2NDLEVEL_SIZE),則直接將 value 的地址直接存儲于 specific_1stblock[key] 處;如果 key 大于或等 PTHREA_KEY_2NDLEVEL_SIZE, 則會為指針數組 sepcific 分配內存空間(如果未曾分配),并將 value 的地址位于 specific[idx1st][idx2nd] 處。其中 idx1st、idx2nd 分別是 key 除 PTHREA_KEY_2NDLEVEL_SIZE 的商與余數。由于大部分應用使用的 key 的數量很小,所以 specific 數組大部分指針都為 NULL。
可以看出,通過 key 訪問線程特定數據的步驟比 __thread 變量更為復雜一些。在 x86_64 架構上, fs 寄存器已經存儲了線程 pthread 的地址值,因此訪問 __thread 變量直接通過 fs 相對尋址即可,只需要一條指令。而 pthread_getspecific 訪問線程特定數據時,需要通過 specific_1stblock 數組來完成,其中還包括了諸多的有效性檢驗。效率上 __thread 變量的訪問應該會更高一些。但是兩者的差距有多大,需要真實實驗去測試一下。