スレッドローカルストレージ(TLS)その2
int get_thread_area(struct user_desc *u_info)はu_info->entry_numberで指定するTLSディスクリプタテーブルを返します。各プロセスのTLSディスクリプタは、プロセスディスクリプタのthread.tls_array[]に有しています。(プロセス切り替え毎に、これをGDTに設定します。)
do_get_thread_area()は、fill_user_desc()をコールして、取得したTLSディスクリプタテーブルをユーザ引数のu_infoに設定しているだけです。
ディスクリプタのベースは32ビット、リミットは20ビットで、ディスクリプタテーブル内で連続する並びでアサインされていません。そのためbase0+base1+base2=32とlimit0+limit1=20と言う事です。
(リミットは20ビットでも、Gビットをセットするkとで、4Gとなります。Linuxではカーネル/ユーザ用セレクタとしてベースは0で、セグメントサイズが4Gとなるように設定されています。)
下記サンプルはベースアドレス、セグメントサイズ、Gビット(limit_in_pagesに設定される。)、および変数のリニアアドレスを表示しています。(論理アドレスとリニアアドレスですが、セグメントとそのオフセットの構成を論理アドレス、それをセグメントのベースからのオフセットをリニアアドレスと理解していますが・・・)
Gビットは1ですから、4G 空間をアクセス出来ることになるのですが、4Gの配列でテストしてみたらコンパイラで跳ねられました。
do_get_thread_area()は、fill_user_desc()をコールして、取得したTLSディスクリプタテーブルをユーザ引数のu_infoに設定しているだけです。
int do_get_thread_area(struct task_struct *p, int idx, struct user_desc __user *u_info) { struct user_desc info; if (idx == -1 && get_user(idx, &u_info->entry_number)) return -EFAULT; if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX) return -EINVAL; fill_user_desc(&info, idx, &p->thread.tls_array[idx - GDT_ENTRY_TLS_MIN]); if (copy_to_user(u_info, &info, sizeof(info))) return -EFAULT; return 0; }fill_user_desc()はstruct desc_structからstruct user_descに再編集しているだけです。
static void fill_user_desc(struct user_desc *info, int idx, const struct desc_struct *desc) { memset(info, 0, sizeof(*info)); info->entry_number = idx; info->base_addr = get_desc_base(desc); info->limit = get_desc_limit(desc); info->seg_32bit = desc->d; info->contents = desc->type >> 2; info->read_exec_only = !(desc->type & 2); info->limit_in_pages = desc->g; info->seg_not_present = !desc->p; info->useable = desc->avl; #ifdef CONFIG_X86_64 info->lm = desc->l; #endif }struct desc_structは以下のフォーマットをしていて、CPUのフォーマットに準じています。(ちなみに、たぶん以下のような感じ・・・)
limit0 | サイズ |
base0 | ベースアドレス |
base1 | ベースアドレス |
type | セグメントの種類(読み出し/読み書き/実行用とか) |
s | セグメントかそうでないか(ゲートなんかがあります。) |
dp | 特権レベル |
p | セグメントがメモリが存在する。スワップはページ単位のためlinuxでは常に1 |
limit | セグメントサイズ |
avl | Linuxでは使用していない |
l | たぶんページサイズ(4Kか4M) |
d | オペランドのサイズが16ビットか32ビットか |
g | セグメントサイズは4K倍される |
base2 | ベースアドレス |
(リミットは20ビットでも、Gビットをセットするkとで、4Gとなります。Linuxではカーネル/ユーザ用セレクタとしてベースは0で、セグメントサイズが4Gとなるように設定されています。)
struct desc_struct { union { struct { unsigned int a; unsigned int b; }; struct { u16 limit0; u16 base0; unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1; unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8; }; }; } __attribute__((packed));gccでTLSを使った時の、それぞれのスレッドのディスクリプタテーブルを、do_get_thread_area()で調べて見ました。ところでget_thread_area()はglibでインプリメントされていません。本システムコールの使用に当たってはsyscall()で行う必要があります。
下記サンプルはベースアドレス、セグメントサイズ、Gビット(limit_in_pagesに設定される。)、および変数のリニアアドレスを表示しています。(論理アドレスとリニアアドレスですが、セグメントとそのオフセットの構成を論理アドレス、それをセグメントのベースからのオフセットをリニアアドレスと理解していますが・・・)
#include <stdio.h> #include <pthread.h> #include <linux/unistd.h> #include <asm/ldt.h> #include <sys/syscall.h> __thread int a = 0; int b = 0; void *func(void *arg) { int i; struct user_desc u_info; for (i = 6; i <=8; i++) { u_info.entry_number = i; syscall(SYS_get_thread_area, &u_info); printf("base=%x limit=%x g=%d off1=%x 0ff2=%x\n", u_info.base_addr, u_info.limit, u_info.limit_in_pages, &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]# ./a.out base=b770ab70 limit=fffff g=1 off1=b770ab6c 0ff2=8049894 base=0 limit=0 g=0 off1=b770ab6c 0ff2=8049894 base=0 limit=0 g=0 off1=b770ab6c 0ff2=8049894 base=b6d09b70 limit=fffff g=1 off1=b6d09b6c 0ff2=8049894 base=0 limit=0 g=0 off1=b6d09b6c 0ff2=8049894 base=0 limit=0 g=0 off1=b6d09b6c 0ff2=8049894上記の結果から、gccではGDTテーブルの6番目(セグメントで言えば6×8となります。)をスレッド共通に使っています。なお、TLSディスクリプタテーブルのベースは0でなく(てっきり0と思っていました。)、スレッド毎に異なっています。従ってその__thread int aのリニアアドレスは異なっています。ただし論理アドレスでのオフセットは全てのスレッドで同じです。
Gビットは1ですから、4G 空間をアクセス出来ることになるのですが、4Gの配列でテストしてみたらコンパイラで跳ねられました。