X

Oracle Solaris, Oarcle ハードウェア製品に関する情報

  • Sun
    January 24, 2010

DTrace の SDT プロバイダの仕組みに迫る

Guest Author


はじめに

今回は DTrace の SDT プロバイダの仕組みをご紹介します。SDT プロバイダは FBT プロバイダや PID プロバイダと並んで、DTrace の基盤となるプロバイダです。SDT プロバイダの仕組みを理解する事で、DTrace への理解も深まる事と思います。


SDT プロバイダとは

SDT(Statically Defined Tracing) プロバイダはソースコードの中に DTrace のプローブポイントを埋め込むための仕組みです。SDT 以外のプロバイダは DTrace 用にソースコードを書き換えなくとも全ての機能が使用出来ました。たとえば FBT プロバイダはカーネルの関数の開始、終了を追跡する事が出来ますが、カーネルの関数全てに DTrace 用のコードを埋め込んでいる訳ではありません。その代わりに DTrace が実行イメージにパッチを当てて、プローブのコードを動的に埋め込んでいました。この動的なプローブポイントの作成は DTrace の大きなメリットの一つですが、プローブしたいポイントが常に動的に指定しやすい場所にある訳ではありません。プログラム実行者に取っては意味があるポイントでも、コンピュータが処理する上では他の処理と何の違いも無い場合があります。SDT プロバイダは、その様な動的に指定するのが難しい場所、動的に取得するのが難しいデータをトレースする為に、ソースコード上でプローブポイントとデータを指定する手段を提供しています。ソースコードの特定の場所にプローブを記述するので、Statically Defined Tracing (静的に定義されたトレース)という名称になっています。

SDT プロバイダを使用する事で、プログラマはプローブポイントや取得したいデータを自由に決める事が出来ます。埋め込んだプローブは後でプログラムの解析に利用する事が出来ます。Solaris のカーネルの中にも SDT のプローブポイントは既に沢山埋め込まれており、先日ご紹介した IP プロバイダ も SDT プロバイダの一種です。他にも Sched プロバイダや IO プロバイダも SDT プロバイダの機能を利用して実装されています。


SDT プロバイダの使い方

SDT プロバイダのプローブの一覧は dtrace コマンドに -lP sdt オプションを付けて実行すると見る事が出来ます。Solaris 10 10/09 では IO, Sched, Proc プロバイダも SDT プロバイダの一種なので、それらも加えて実行した結果が以下の出力です。ここでリストされているプローブを解析に使用する事が出来ます。

 \*\*\* SDT プロバイダのプローブの一覧の見方 \*\*\*
# dtrace -lP sdt,io,sched,proc
ID PROVIDER MODULE FUNCTION NAME
2679 sdt unix cpupm_utilization_event cpupm-ti-ungoverned
2688 sdt unix page_get_replacement_page page-get
2689 sdt unix page_get_cachelist page-get
2690 sdt unix page_get_freelist page-get
2701 sdt unix cpu_set_curr_clock cpu-change-speed
2702 sdt unix cpupm_utilization_event cpupm-lower-req
2703 sdt unix ecc_page_zero page_zero_result
2704 sdt unix cpupm_utilization_event cpupm-tw-governed
2705 sdt unix cpupm_utilization_event cpupm-ti-governed
...

プローブの一覧は一行につき一つのプローブが表示されます。今回検証した環境では 864 個の SDT のプローブが組み込まれていました。プローブの数は OS のリリースやプラットフォーム、ロードされているカーネルモジュール等によって変わります。

 \*\*\* SDT プロバイダのプローブの数は 864 個 \*\*\*
# dtrace -lP sdt,io,sched,proc | wc -l
865 <-- ヘッダ行を含む出力の行数

続いて、SDT プロバイダの使用例を見てみましょう。下記の例では SDT プロバイダのプローブを使って、カーネルのタスクキューから毎秒幾つのタスクが実行開始されているかを確認しています。ここで使用されている taskq-exec-start はタスクの処理が開始する直前に呼ばれる SDT プロバイダのプローブです。カーネルの負荷が増加すると処理されるタスクの数も増え、taskq-exec-start プローブが呼び出される回数も増加します。ここでは taskq-exec-start プローブが呼ばれる毎に cnt 変数をインクリメントし、Profile プロバイダの tick-1sec プローブを使用して 1 秒毎に cnt の値を出力しています。また、同じく Profile プロバイダの tick-5sec プローブを使用して、実行開始から 5 秒後にスクリプトが終了する様にしています。

 \*\*\* SDT プロバイダの使用例 1 \*\*\*
# dtrace -qn 'sdt:::taskq-exec-start {cnt++} tick-1sec{printf("%d\\n", cnt); cnt=0} tick-5sec{exit(0)}'
46
51
52
55
49

taskq-exec-start のプローブポイントは Solaris のソースコード中の /usr/src/uts/common/os/taskq.c#1504 に埋め込まれています。taskq-exec-start は関数の入り口や出口ではなく、通常のコードの中に位置しています。この様なポイントを DTrace が自動的に見付けてプローブを挿入するのは難しい為、代わりに SDT プロバイダを使用して静的にプローブを埋め込んでいます。実際に観測したい tqent_func() の呼び出しも、タスク毎に変わる関数ポインタになっている為、SDT プロバイダを使用しないと一括して補足する事は困難です。

 \*\*\* taskq-exec-start が埋め込まれているソースコード \*\*\*
1502

rw_enter(&tq->tq_threadlock, RW_READER);
1503

start = gethrtime();
1504

DTRACE_PROBE2(taskq__exec__start, taskq_t \*, tq, <-- ここ!
1505

taskq_ent_t \*, tqe);
1506

tqe->tqent_func(tqe->tqent_arg);
1507

DTRACE_PROBE2(taskq__exec__end, taskq_t \*, tq,
1508

taskq_ent_t \*, tqe);
1509

end = gethrtime();
1510

rw_exit(&tq->tq_threadlock);

以下は同じく taskq-exec-start プローブを利用して、タスクの処理に使用される関数の名前を出力する例です。dtrace コマンドを実行した 10 秒間で、kmem_cache_reap() 関数が 385 回、callout_execute() 関数が 515 回実行されている事が分かります。taskq-exec-start プローブの中で使用している arg1 変数は taskq_ent_t \* 型の変数です。この変数にはタスクに関する情報が含まれており、その中にはタスクで実行される関数のアドレスも入っています。この関数のアドレスに DTrace の sym() アクションを使って関数名を取得しています。arg1 に格納されていたデータはカーネルモジュール内のローカル変数です。通常はローカル変数を DTrace から自動で取得する事は困難ですが、SDT プロバイダを利用する事で、簡単に手繰り寄せる事が出来る様になっています。

 \*\*\* SDT プロバイダの使用例 2 \*\*\*
# dtrace -qn 'sdt:::taskq-exec-start {@agg[sym((uintptr_t)((taskq_ent_t \*)arg1) ->tqent_func)]=count()}
tick-10sec{printa(@agg);exit(0)}'
rpcmod`endpnt_reclaim 1
genunix`kmem_reap_done 1
genunix`kmem_reap_start 1
genunix`kmem_cache_reap 385
genunix`callout_execute 515
~~~~~~~~~~~~~~~ これが関数名 ~~~ 呼び出し回数

以下は SDT プロバイダのプローブを使用して ZFS の ARC のキャッシュヒットとキャッシュミスをカウントする例です。毎秒、キャッシュヒット / ミスが発生した回数を数えて出力しています。この様に、SDT プロバイダを使用するとプログラム内のあらゆるデータを取得する事が出来ます。arc-hit, arc-miss プローブのソースコードは /usr/src/uts/common/fs/zfs/arc.c にあります。

 \*\*\* SDT プロバイダの使用例 3 \*\*\*
# dtrace -qn 'BEGIN{printf("hit\\tmiss\\n")} sdt:::arc-hit{hit++} sdt:::arc-miss{miss++}
tick-1sec{printf("%d\\t%d\\n", hit,miss);hit = miss =0} tick-5sec{exit(0)}'
hit miss
2035 60
0 0
1436 10
886 99
1061 164

Sched プロバイダも使用してみましょう。以下は Sched プロバイダの preempt プローブを使用して、プリエンプションが発生した回数を CPU 毎に集計しています。preempt プローブのソースコードは /usr/src/uts/common/disp/disp.c#687 にあります。

 \*\*\* Sched プロバイダの使用例 \*\*\*
# dtrace -qn 'BEGIN{printf("\\tcpu\\t\\tnum\\n")} sched:::preempt{@agg[cpu]=count()}
tick-1sec{printa(@agg); clear(@agg)} tick-5sec{exit(0)}'
cpu num
3 1
2 4
1 28
0 55
3 0
2 1
1 27
0 46
3 0
2 1
1 12
0 19
3 0
2 1
0 25
1 36
2 0
3 1
0 29
1 36


SDT プロバイダの仕組み

ここからは SDT プロバイダの仕組みを、先ほどの arc-miss プローブを例にして見て行きたいと思います。

 \*\*\* arc-miss プローブ \*\*\*
# dtrace -ln 'sdt:zfs:arc_read_nolock:arc-miss'
ID PROVIDER MODULE FUNCTION NAME
5628 sdt zfs arc_read_nolock arc-miss


DTRACE_PROBE マクロ

SDT プロバイダを使用する為にはソースコードに SDT 用のコードを埋め込む必要があります。埋め込むコードは、使い易い様にマクロとして定義されています。マクロの定義は /usr/src/uts/common/sys/sdt.h にあります。定義されているマクロの名前は、作成するプローブに渡したい変数の個数に応じて、変数を渡さない DTRACE_PROBE マクロ、変数を一つだけ渡す DTRACE_PROBE1 マクロから、変数を 5 個渡す DTRACE_PROBE5 まで、6 個のマクロが用意されています。このマクロに引数を設定してソースコードに記述すると、SDT プロバイダのプローブになります。

 \*\*\* DTRACE_PROBE マクロ \*\*\*
75 #else /\* _KERNEL \*/
76
77 #define

DTRACE_PROBE(name)

{

\\
78

extern void __dtrace_probe_##name(void);

\\
79

__dtrace_probe_##name();

\\
80 }
81
82 #define

DTRACE_PROBE1(name, type1, arg1) {

\\
83

extern void __dtrace_probe_##name(uintptr_t);

\\
84

__dtrace_probe_##name((uintptr_t)(arg1));

\\
85 }
...

実際にソースコード内に記述されている例を見てみましょう。arc-miss プローブのソースコードは /usr/src/uts/common/fs/zfs/arc.c#2797 にあります。"DTRACE_PROBE4(arc__miss, arc_buf_hdr_t \*, hdr, blkptr_t \*, bp, uint64_t, size, zbookmark_t \*, zb);" が埋め込まれたプローブです。最初の引数の "arc__miss" がプローブ名の指定です。"__" は DTrace によって "-" に変換されます。その為、ソースコードからプローブの実装場所を探す際は、プローブ名の "-" を "__" にして検索してください。

 \*\*\* arc-miss プローブのソースコード \*\*\*
2794

mutex_exit(hash_lock);
2795
2796

ASSERT3U(hdr->b_size, ==, size);
2797

DTRACE_PROBE4(arc__miss, arc_buf_hdr_t \*, hdr, blkptr_t \*, bp, <-- ここ!
2798

uint64_t, size, zbookmark_t \*, zb);
2799

ARCSTAT_BUMP(arcstat_misses);
2800

ARCSTAT_CONDSTAT(!(hdr->b_flags & ARC_PREFETCH),
2801

demand, prefetch, hdr->b_type != ARC_BUFC_METADATA,
2802

data, metadata, misses);

DTRACE_PROBE4 はプローブに変数を 4 つ渡すマクロです。プローブに渡す変数はマクロに引数として与えます。DTRACE_PROBE4 マクロは全部で 9 つの引数を取ります。先ほどご説明しました通り、最初の引数はプローブ名です。"arc__miss" という文字列は "arc-miss" というプローブ名に変換されます。2 番目以降の引数は、プローブに渡す変数の型と、その値を格納した変数の名前のペアです。ここで言う変数の名前はマクロを埋め込むソースコード内での変数の名前です。プローブに渡された後の変数の名前は、スクリプト内の変数と名前が衝突しない様に DTrace によって argN という形式で作成されます。たとえば DTRACE_PROBE4 マクロの第 2 引数は "arc_buf_hdr_t \*" で第 3 引数が "hdr" なので、arc-miss プローブに渡される最初の変数は "arc_buf_hdr_t \*" 型の "hdr" 変数の値になり、実際に arc-miss プローブから参照する際は arg0 という名前の変数になります。

同じ様に、第 4 引数は "blkptr_t \*" で第 5 引数が "bp" なので、arc-miss プローブの arg1 変数は "blkptr_t \*" 型の "bp" 変数の値が格納されます。同じく arg2 には "uint64_t" 型の "size" 変数の値が、arg3 には "zbookmark_t \*" 型の "zb" 変数の値が格納されます。以下のコマンドは arc-miss プローブの arg2 変数を出力していますが、実際にはカーネル内の "uint64_t" 型の "size" 変数が出力されていることになります。

 \*\*\* プローブに渡された変数の参照は argN \*\*\*
# dtrace -qn 'sdt:::arc-miss{printf("%u\\n", arg2)}'
2890529992504
2890529992504
2890529992504
2890529992504
2890529992504


__dtrace_probe_[name] エントリ

DTRACE_PROBE マクロを記述したソースコードをコンパイルすると __dtrace_probe_[name] という名前でダミーのシンボルが作成されます。[name] は DTRACE_PROBE マクロの第 1 引数で指定したプローブ名です。また、これと同時にプローブに渡される変数も用意されます。

arc-miss プローブの場合は __dtrace_probe_arc__miss という名前のシンボルが作成されます。"arc__miss" になっているのは先ほど見たソースコードで指定されている通りです。nm コマンドで __dtrace_probe_arc__miss の情報を見てみると UNDEF となっているので、実態は無く、シンボルだけが存在している事が分かります。

 \*\*\* __dtrace_probe_arc__miss は UNDEF \*\*\*
# nm /kernel/drv/amd64/zfs | grep __dtrace_probe_arc__miss
[2672] | 0| 0|NOTY |GLOB |0 |UNDEF |__dtrace_probe_arc__miss


リロケーション

__dtrace_probe_[name] というダミーのシンボルはプログラムがロードされる時に実行時リンカーで書き換えられます。カーネル内のシンボルの場合は krtld がバイナリの書き換えを行います。書き換えの処理は x86_64 Solaris の場合は /usr/src/uts/intel/amd64/krtld/kobj_reloc.c に実装されています。instr が指しているのがダミーのシンボルです。ここではダミーのシンボルの 1 バイト前から、全部で 5 バイトを nop 命令に書き換えています。

 \*\*\* ダミーのシンボルは nop x5 に置き換えられます \*\*\*
86 #define

SDT_NOP

0x90
87 #define

SDT_NOPS

5
88
89 static int
90 sdt_reloc_resolve(struct module \*mp, char \*symname, uint8_t \*instr)
91 {
...
116

for (i = 0; i < SDT_NOPS; i++)
117

instr[i - 1] = SDT_NOP; <-- ここで書き換えています
...

elfdump コマンドで zfs モジュールのリロケーションセクションを見てみると、__dtrace_probe_arc__miss がエントリーされています。オフセットは 0x3663 です。

 \*\*\* リロケーションセクションの __dtrace_probe_arc__miss シンボル \*\*\*
# elfdump -r /kernel/drv/amd64/zfs
Relocation Section: .rela.eh_frame
type offset addend section symbol
...
R_AMD64_PC32 0x3663 0xfffffffffffffffc .rela.text __dtrace_probe_arc__miss

dis コマンドを使用して 0x3663 に何があるかを確かめてみます。dis コマンドはディスアセンブラで、/usr/ccs/bin/dis にあります。"-n" オプションはシンボル名の代わりに絶対アドレスで表示するオプションです。krtld によって書き換えられるのはダミーのシンボルの 1 バイト前から全部で 5 バイトでした。ここでは 0x3662 からの 5 バイトに相当し、丁度 arc_read_nolock() 関数の中の "call +0x5" という命令の部分です。

 \*\*\* __dtrace_probe_arc__miss が指す先には call +0x5 がありました \*\*\*
# dis -n /kernel/drv/amd64/zfs
...
arc_read_nolock()
...
0x365b: 48 8b 75 a8 movq -0x58(%rbp),%rsi
0x365f: 4c 89 e7 movq %r12,%rdi
0x3662: e8 00 00 00 00 call +0x5 <0x3667>
~~~~~~~~~~~~~~ ここが該当部分
0x3667: be 01 00 00 00 movl $0x1,%esi
0x366c: 48 c7 c7 00 00 00 movq $0x0,%rdi

同じ場所をシンボル名込みで見てみると arc_read_nolock+0x3e3 に相当する事が分かります。先ほどの dis コマンドの "-n" オプションを外し、"-F" オプションに関数名の arc_read_nolock を指定して実行します。

 # dis -F arc_read_nolock /kernel/drv/amd64/zfs
disassembly for /kernel/drv/amd64/zfs
arc_read_nolock()
...
arc_read_nolock+0x3db: 48 8b 75 a8 movq -0x58(%rbp),%rsi
arc_read_nolock+0x3df: 4c 89 e7 movq %r12,%rdi
arc_read_nolock+0x3e2: e8 00 00 00 00 call +0x5 <arc_read_nolock+0x3e7> <- これ!!
arc_read_nolock+0x3e7: be 01 00 00 00 movl $0x1,%esi
arc_read_nolock+0x3ec: 48 c7 c7 00 00 00 movq $0x0,%rdi

カーネルにロードされたオブジェクトファイルをディスアセンブルすると、arc_read_nolock+0x3e3 の前後が nop に置き換えられている事が分かります。カーネルにロードされたファイルのディスアセンブルは mdb コマンドを使用します。

 \*\*\* ロードされたバイナリをディスアセンブル \*\*\*
# mdb -k
Loading modules: [ unix krtld genunix specfs dtrace cpu.generic
cpu_ms.AuthenticAMD.15 uppc pcplusmp ufs mpt ip hook neti sctp arp usba fcp
fctl nca lofs zfs md cpc random crypto fcip logindmux ptm sppp nfs ]
> arc_read_nolock+0x3e3::dis
arc_read_nolock+0x3b9: call +0x35ed7 <vdev_is_dead>
arc_read_nolock+0x3be: testl %eax,%eax
arc_read_nolock+0x3c0: je +0x442 <arc_read_nolock+0x802>
arc_read_nolock+0x3c6: movq $0x0,-0x60(%rbp)
arc_read_nolock+0x3ce: movq -0x30(%rbp),%rdi
arc_read_nolock+0x3d2: call +0xc1ee4ce <mutex_exit>
arc_read_nolock+0x3d7: movq 0x20(%rbp),%rdx
arc_read_nolock+0x3db: movq -0x58(%rbp),%rsi
arc_read_nolock+0x3df: movq %r12,%rdi
arc_read_nolock+0x3e2: nop <-- ここから nop が 5 つ
arc_read_nolock+0x3e3: nop
arc_read_nolock+0x3e4: nop
arc_read_nolock+0x3e5: nop
arc_read_nolock+0x3e6: nop
arc_read_nolock+0x3e7: movl $0x1,%esi
arc_read_nolock+0x3ec: movq $0xffffffffc046d070,%rdi
arc_read_nolock+0x3f3: call +0xc220e8d <atomic_add_64>
arc_read_nolock+0x3f8: testb $0x8,0x40(%rbx)
arc_read_nolock+0x3fc: jne +0x2b6 <arc_read_nolock+0x6b2>
arc_read_nolock+0x402: cmpl $0x1,0x54(%rbx)
arc_read_nolock+0x406: movl $0x1,%esi

これで __dtrace_probe_arc__miss がリロケーションされて nop 命令に書き換えられていることが確認出来ました。この nop が DTrace のプローブポイントになる場所です。同じ様な nop は Solaris のカーネルのあちこちに埋め込まれており、DTrace のプローブの発動を待っています。nop 命令が 5 つだけですので、プローブが発動していない間は殆ど性能への影響はありません。


パッチング

該当する DTrace のプローブが有効化されると先ほどの nop がメモリ上でパッチされ、DTrace の処理に接続されます。パッチ処理は x86_64 では /usr/src/uts/intel/dtrace/sdt.c に実装されています。x86 では nop の一つが lock プリフィックスに書き換えられます。SDT_PATCHVAL マクロに格納されている 0xf0 が lock プリフィックスです。

 \*\*\* メモリ上のプログラムがパッチされて 0xf0 に書き換えられています \*\*\*
42 #define

SDT_PATCHVAL

0xf0
...
182

sdp->sdp_patchval = SDT_PATCHVAL;
...
307

while (sdp != NULL) {
308

\*sdp->sdp_patchpoint = sdp->sdp_patchval; <-- ここで書き換えています
309

sdp = sdp->sdp_next;
310

}

'sdt:zfs:arc_read_nolock:arc-miss' プローブを有効にした状態でメモリ上の命令を見てみます。

 \*\*\* arc-miss プローブを有効化 \*\*\*
# dtrace -n 'sdt:zfs:arc_read_nolock:arc-miss'
dtrace: description 'sdt:zfs:arc_read_nolock:arc-miss' matched 1 probe

再び mdb コマンドで確認すると、今度は nop の一つが lock nop に変わっています。プローブを有効にしたため、メモリ上のプログラムにパッチが当たり、命令が書き変わっている事が分かりました。

 \*\*\* arc_read_nolock+0x3e3 が lock nop に変わりました \*\*\*
# mdb -k
Loading modules: [ unix krtld genunix specfs dtrace cpu.generic
cpu_ms.AuthenticAMD.15 uppc pcplusmp ufs mpt ip hook neti sctp arp usba fcp
fctl nca lofs zfs md cpc random crypto fcip logindmux ptm sppp nfs ]
> arc_read_nolock+0x3e3::dis
arc_read_nolock+0x3b9: call +0x35ed7 <vdev_is_dead>
arc_read_nolock+0x3be: testl %eax,%eax
arc_read_nolock+0x3c0: je +0x442 <arc_read_nolock+0x802>
arc_read_nolock+0x3c6: movq $0x0,-0x60(%rbp)
arc_read_nolock+0x3ce: movq -0x30(%rbp),%rdi
arc_read_nolock+0x3d2: call +0xc1ee4ce <mutex_exit>
arc_read_nolock+0x3d7: movq 0x20(%rbp),%rdx
arc_read_nolock+0x3db: movq -0x58(%rbp),%rsi
arc_read_nolock+0x3df: movq %r12,%rdi
arc_read_nolock+0x3e2: nop
arc_read_nolock+0x3e3: lock nop <-- ここ!!
arc_read_nolock+0x3e5: nop
arc_read_nolock+0x3e6: nop
arc_read_nolock+0x3e7: movl $0x1,%esi
arc_read_nolock+0x3ec: movq $0xffffffffc046d070,%rdi
arc_read_nolock+0x3f3: call +0xc220e8d <atomic_add_64>
arc_read_nolock+0x3f8: testb $0x8,0x40(%rbx)
arc_read_nolock+0x3fc: jne +0x2b6 <arc_read_nolock+0x6b2>
arc_read_nolock+0x402: cmpl $0x1,0x54(%rbx)
arc_read_nolock+0x406: movl $0x1,%esi
arc_read_nolock+0x40b: movq $0xffffffffc046d0d0,%rdi


Undefined Opcode Exception

実は、先ほど書き換えに使用した lock nop という命令は CPU には実装されていません。CPU に実装されていない命令は #UD (Undefined Opcode) Exception を発生させます。Solaris では #UD Exception が発生すると invoptrap というハンドラが呼び出され、そのハンドラの中の call dtrace_invop で DTrace に制御を渡しています。

invoptrap は /usr/src/uts/intel/ia32/ml/exception.s#379 に実装されています。

 \*\*\* invoptrap から dtrace_invop を呼び出しています \*\*\*
379

ENTRY_NP(invoptrap)
380
381

XPV_TRAP_POP
382
383

cmpw

$KCS_SEL, 8(%rsp)
384

jne

ud_user
385
386 #if defined(__xpv)
387

movb

$0, 12(%rsp)

/\* clear saved upcall_mask from %cs \*/
388 #endif
389

push

$0

/\* error code -- zero for #UD \*/
390 ud_kernel:
391

push

$0xdddd

/\* a dummy trap number \*/
392

INTR_PUSH
393

movq

REGOFF_RIP(%rsp), %rdi
394

movq

REGOFF_RSP(%rsp), %rsi
395

movq

REGOFF_RAX(%rsp), %rdx
396

pushq

(%rsi)
397

movq

%rsp, %rsi
398

call

dtrace_invop <-- dtrace_invop() の呼び出し

dtrace_invop() は /usr/src/uts/i86pc/os/dtrace_subr.c#dtrace_invop にあります。

 \*\*\* dtrace_invop() の実装 \*\*\*
44 int
45 dtrace_invop(uintptr_t addr, uintptr_t \*stack, uintptr_t eax)
46 {
47

dtrace_invop_hdlr_t \*hdlr;
48

int rval;
49
50

for (hdlr = dtrace_invop_hdlr; hdlr != NULL; hdlr = hdlr->dtih_next) {
51

if ((rval = hdlr->dtih_func(addr, stack, eax)) != 0)
52

return (rval);
53

}
54
55

return (0);
56 }

これは DTrace に処理を渡す為に、わざと CPU に実装されていない命令を実行させている事になります。何故この様な実装にしているのかと言うと、一つにはオーバーヘッドを最小限にする為です。これまで見て来た方法なら、プローブが有効になっていない間のオーバーヘッドは nop が数命令で済みます。これは殆ど無視して良い量です。プローブが有効になったら nop 命令を一つだけ書き換えれば良いので、影響範囲が小さく、安全でもあります。性能と安全は DTrace の実装ポリシーの大きな柱になっています。


まとめ


  • ソースコードに DTRACE_PROBE マクロを挿入すると SDT プロバイダが使える様になります。

  • DTRACE_PROBE マクロは、コンパイルされると __dtrace_probe_[name] というダミーのエントリポイントになりました。

  • バイナリがロードされると、__dtrace_probe_[name] エントリはリンカーのリロケーションにより nop が 5 つに置換されました。

  • DTrace のプローブを有効にすると 2 番目の nop が lock に書き換えられました。

  • lock nop は #UD Exception を発生させ、そのハンドラの中で DTrace に制御を移していました。

  • 以上で DTrace に処理が受け渡されました。これが x86_64 環境での SDT プロバイダの動きです。


SDT プロバイダのオーバーヘッド

DTrace の重要な特徴の一つに、プローブが有効になっていない時は一切のオーバーヘッドが無いという点があります。SDT プロバイダはこの特徴の殆ど唯一の例外です。既に見た様に、SDT はプログラムに nop を埋め込みます。また、プローブに渡す引数はプローブが有効になっていない場合も用意されます。この 2 つはプローブを disable にしていても発生するオーバーヘッドなので disabled probe effect と呼ばれています。

ただし、nop は何もしない命令で、メモリやレジスタを消費しませんし、負荷の量は殆ど無視出来ます。また、プローブに渡す引数も大抵の場合はローカル変数への参照なので、殆ど負荷にはなりません。更に Solaris に埋め込まれた SDT プロバイダのプローブは注意深く場所を選んで配置されており、十分にテストされている為、これらが性能に影響を与える事はありません。

一方、プローブが有効になっている場合の負荷は通常の DTrace の負荷と同じです。こちらも、ポイントを絞って常識的な使い方をしていれば問題になる事は無いと思います。


おわりに

以上、SDT プロバイダーの使い方、仕組み、オーバーヘッドをご紹介しました。SDT プロバイダーは DTrace の中でも重要なプロバイダーの一つです。是非、使い方をマスターして有効に使ってください。

今回はカーネル内の SDT プローブを中心にご説明しましたが、SDT プロバイダが一番効果を発揮するのは実はユーザランドで使用した時です。ユーザランドでの SDT プロバイダの使い方は機会を改めてご説明させて頂きたいと思います。


参考文献


ソースコード

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.