スレッドローカルストレージ(TLS)
Rev.1を表示中。最新版はこちら。
スレッドはグローバルは変数は共有しますが、マルチスレッドアプリケーションにおいて、スレッド固有のデータを保持したい場合があります。それを実現するのが、スレッドローカルストレージ(TLS)です。func()を2つのスレッドとして作成します。そこでは、__thread int aとint bをインクリメントした値を表示しています。最初のスレッドではa = 1 b=1、次のスレッドではa = 1 b=2となっています。int aとしたならa=2なるところです。すなわち__thread int aはスレッド固有の変数というわけです。
#include <stdio.h> #include <pthread.h> __thread int a = 0; int b = 0; void *func(void *arg) { a++; b++; printf("a = %d b=%d\n", a, b); } int main() { pthread_t tid0, tid1; pthread_create(&tid0, NULL, func, NULL); pthread_create(&tid1, NULL, func, NULL); pthread_join(tid0, NULL); pthread_join(tid1, NULL); } [root@localhost kitamura]# gcc -pthread test.c [root@localhost kitamura]# ./a.out a = 1 b=1 a = 1 b=2下のリストは上記プログラムをobjdumpした内の、funcを抜き出したものです。a++の処理は、mov %gs:0xfffffffc,%eaxとgsセグメントでオーパプレフィックスとしてaを取得しています。反面b++ではmov 0x804981c,%eaxでdsセグメントとして取得しています。TLSはDSセグメントでない、別のセグメント下に配置することで実現しています。
080484c4 <func>: 80484c4: 55 push %ebp 80484c5: 89 e5 mov %esp,%ebp 80484c7: 83 ec 18 sub $0x18,%esp 80484ca: 65 a1 fc ff ff ff mov %gs:0xfffffffc,%eax 80484d0: 83 c0 01 add $0x1,%eax 80484d3: 65 a3 fc ff ff ff mov %eax,%gs:0xfffffffc 80484d9: a1 1c 98 04 08 mov 0x804981c,%eax 80484de: 83 c0 01 add $0x1,%eax 80484e1: a3 1c 98 04 08 mov %eax,0x804981c 80484e6: 8b 0d 1c 98 04 08 mov 0x804981c,%ecx 80484ec: 65 8b 15 fc ff ff ff mov %gs:0xfffffffc,%edx 80484f3: b8 54 86 04 08 mov $0x8048654,%eax 80484f8: 89 4c 24 08 mov %ecx,0x8(%esp) 80484fc: 89 54 24 04 mov %edx,0x4(%esp) 8048500: 89 04 24 mov %eax,(%esp) 8048503: e8 dc fe ff ff call 80483e4 <printf@plt> 8048508: c9 leave 8048509: c3 retコンパイラは__threadで定義された変数があると、pthread_createの実処理であるclone()のオプションにCLONE_SETTLSを付加してコールするようです(たぶん)。clone()からcopy_process()/copy_thread()とコールされ、ここでオプションに CLONE_SETTLSが設定されていると、do_set_thread_area()がコールされます。
do_set_thread_area()では、ユーザが設定したstruct user_descを、グローバルディスクリプタテーブル(GDT)のTLSエントリーに設定します。struct user_descはディスクリプタテーブルの属性(ベースアドレスとかサイズ等)です。なおLinuxではGDTをすべてのプロセスで使用します。LDTは使用しません。
TLSのGDT内のエントリーポイントはGDT_ENTRY_TLS_MINからGDT_ENTRY_TLS_MAXとなります。X86-32では6から8までの3ポイントを有しています。引数のidxがセットするエントリーポイントになりますが、idx==-1ならユーザが設定するinfo.entry_numberがエントリーポイントになります。なおodx==-1で引数のcan_allocate!=0なら、空いているエントリーポイントを探します。これらの引数でset_tls_desc()をコールします。
int do_set_thread_area(struct task_struct *p, int idx, struct user_desc __user *u_info, int can_allocate) { struct user_desc info; if (copy_from_user(&info, u_info, sizeof(info))) return -EFAULT; if (idx == -1) idx = info.entry_number; if (idx == -1 && can_allocate) { idx = get_free_idx(); if (idx < 0) return idx; if (put_user(idx, &u_info->entry_number)) return -EFAULT; } if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX) return -EINVAL; set_tls_desc(p, idx, &info, 1); return 0; }set_tls_desc()では、スレッドp->thread->tls_array[]にstruct user_descからstruct desc_structにセットします。(プロセス毎にTLS用ディスクリプタテーブルを有すると言う事です。)struct desc_structは、CPUのディスクリプタフォーマットと思われます。fill_ldt()でこのstruct desc_structに編集したのち、load_TLS()でこのディスクリプタテーブルをGDTに設定します。
static void set_tls_desc(struct task_struct *p, int idx, const struct user_desc *info, int n) { struct thread_struct *t = &p->thread; struct desc_struct *desc = &t->tls_array[idx - GDT_ENTRY_TLS_MIN]; int cpu; cpu = get_cpu(); while (n-- > 0) { if (LDT_empty(info)) desc->a = desc->b = 0; else fill_ldt(desc, info); ++info; ++desc; } if (t == ¤t->thread) load_TLS(t, cpu); put_cpu(); }load_TLS()はマクロでnative_load_tls()に展開されます。ここではget_cpu_gdt_table()動作しているCPUのGDTを取得し、そこにTLS用のディスクリプタを設定します。これでこのセグメント値で参照するアドレスはスレッド固有のものとなるわけです。
#define load_TLS(t, cpu) native_load_tls(t, cpu) static inline void native_load_tls(struct thread_struct *t, unsigned int cpu) { unsigned int i; struct desc_struct *gdt = get_cpu_gdt_table(cpu); for (i = 0; i < GDT_ENTRY_TLS_ENTRIES; i++) gdt[GDT_ENTRY_TLS_MIN + i] = t->tls_array[i]; }タスクスイッチングの__switch_to()では、次の動作するプロセスのの引数でload_TLS()をコールしています。従ってプロセス毎にTLS(6,7,8のどれか)をセグメントとして参照しても、ディスクリプタテーブルが異なるため、その値はスレッド固有の物となるわけです。
補足(以下は推測です。)
gccでサポートするTLSは、mov %gs:0xfffffffc,%eaxから、メモリー空間は4Gとなるようで、上位アドレスから配置されるようです。カーネルはTLS空間を共有する必要がないからだと思います。上で見てきたようにTLSは3エントリー有しています。これはTLS1はスレッドA,Bで、TLS2はスレッドB,Cとかいった使い方を可能にするためかと思います。ただしこの場合、アセンブラレベルで、TLSのセグメント値を意識しながらプログラミングしていく必要があるかと思います。なお、gccでは、すべてのスレッドは一意のTLSエントリー下でのアクセスで、そのセグメントをgsセグメントとして実装しているようです。