無料Wikiサービス | デモページ
検索

アクセス数
最近のコメント
kprobe - ななし
ksetの実装 - スーパーコピー
カーネルスレッドとは - ノース
カーネルスレッドとは - nbyst
asmlinkageってなに? - ノース
asmlinkageってなに? - よろしく
はじめ - ノース
はじめ - ノース
はじめ - 楽打連動ユーザー
はじめ - 楽打連動ユーザー
Adsense
広告情報が設定されていません。

スレッドローカルストレージ(TLS)


スレッドはグローバルは変数は共有しますが、マルチスレッドアプリケーションにおいて、スレッド固有のデータを保持したい場合があります。それを実現するのが、スレッドローカルストレージ(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 == &current->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セグメントとして実装しているようです。


最終更新 2011/05/29 22:41:50 - north
(2011/05/29 22:40:36 作成)