BSD界隈四方山話

第82回DTraceの使い方 その2

D言語の基本

DTraceで使用するスクリプト(D言語、Dプログラム、Dスクリプトなどと呼ばれていることが多いみたいです)はawk(1)に着想を得て開発されたと言われています。今回はこの言語の基本的な構造を説明するとともに、いくつかのサンプルを掲載しながら使用方法を紹介します。

DTraceを実行するコマンドはdtrace(1)です。使い方もawk(1)と似ています。引数に直接コードを書く場合には次のようにオプション-nにコードを指定します。

リスト dtrace(1)コマンドの使い方
dtrace -n 'Dプログラム'

コードをファイルに書いてある場合には、次のようにオプション-sにそのファイルのパスを渡します。

リスト dtrace(1)コマンドの使い方その2
dtrace -s Dプログラムファイル.d

シバンにdtrace(1)を指定すればシェルスクリプトのようにDプログラムを実行させることができます。シバンにdtrace(1)を指定する場合、ファイルの先頭を次のように書きます。シバンというのはファイルの先頭に書いてある#!です。これに続いて実行するプログラムのパスを書きます。

リスト dtrace(1)コマンドの使い方その3
#!/usr/sbin/dtrace -s
…略…

基本的にはこんな感じでコマンドを実行します。Dプログラムの中身ですが、次のような構造になっています。⁠プローブ /プレディケート/ { アクション }」といった構造をひたすら繰り返すだけです。

リスト Dプログラムの基本的な構造
プローブ /プレディケート/ { アクション }
プローブ /プレディケート/ { アクション }
プローブ /プレディケート/ { アクション }
…

プローブというのはチェックする対象のようなものだと思ってください。その次のプレディケートというのは、プローブで得られたもののなかから特定の条件に合うものに絞込みを行うもの、さらにその次のアクションというのは、プローブとプレディケートをくぐり抜けたものに対して適用する処理内容です。awk(1)は「条件 { アクション } 」をひたすら繰り返すといった作りになっているので、基本的にこれとよく似た作りになっています。

プローブは:で区切られた指定で、ワイルドカードや省略などが使えます。プレディケートはそのものを省略することもできます。このシンタックスを詳しく説明するのが教科書になってくるわけですが、読んでも覚えるのは難しいので、以降はサンプルを見ながらこの構造を思い出して頭の中でロジックを整理していこうと思います。

dtrace(1)の簡単なサンプル

awk(1)には特別な条件として処理がはじまる前に一致するBEGINと処理が終わったあとに一致するENDという指定がありますが、これがdtrace(1)にも用意されています。これを使うと。いわゆるメッセージを出力するだけの、よく入門書の最初に掲載される「Hello World」プログラムを作ることができます。こんな感じです。

 Hello World!その1
# dtrace -n 'BEGIN { trace("Hello World!"); }'
dtrace: description 'BEGIN ' matched 1 probe
CPU     ID                    FUNCTION:NAME
  0      1                           :BEGIN   Hello World!
^C

#

trace()という処理がメッセージを出力する処理を行っています。この関数は指定されたデータの型に応じてそれぞれ適切な出力を行うというものです。ここでは指定した文字列がそのまま出力されている様子を確認できます。

trace()のほかに、出力メッセージをフォーマットにしたがって出力するためのprintf()という関数も用意されています。C言語に詳しい方はこっちの方がなじみがあるでしょう。

 Hello Worldその2
# dtrace -n 'BEGIN { printf("Hello World!"); }'
dtrace: description 'BEGIN ' matched 1 probe
CPU     ID                    FUNCTION:NAME
  0      1                           :BEGIN Hello World!
^C

#

dtrace(1)はawk(1)に着想を得て開発された言語と言われていますが、組み込み関数や組み込み変数、またシンタックスなどはC言語を真似ています。awk(1)にC言語のニュアンスを合わせたようなものになっています。

次の例はopen(2)システムコールが呼ばれたタイミングで、open(2)システムコールを読んだプロセス名とその引数を表示するDプログラムです。ようするに、ファイルを開いたプロセスと、そのファイルパスを表示するというものです。syscall::open:entryがプローブ、プレディケートは省略されていて、アクションが{ printf("%s %s", execname, copyinstr(arg0)); }です。

 open(2)システムコールを呼んだプログラムとその引数を出力
# dtrace -n 'syscall::open:entry { printf("%s %s", execname, copyinstr(arg0)); }'
dtrace: description 'syscall::open:entry ' matched 2 probes
CPU     ID                    FUNCTION:NAME
  0  58065                       open:entry vmtoolsd /etc/resolv.conf
  0  58065                       open:entry date /etc/localtime
  0  58065                       open:entry date /usr/share/zoneinfo/posixrules
  0  58065                       open:entry cal /etc/localtime
  0  58065                       open:entry cal /usr/share/zoneinfo/posixrules
  0  58065                       open:entry cal /Users/daichi/.termcap.db
  0  58065                       open:entry cal /Users/daichi/.termcap
  0  58065                       open:entry cal /usr/share/misc/termcap.db
  0  58065                       open:entry vmtoolsd /etc/resolv.conf
^C

root@virt:~ #

execnameは組み込み変数で、プロセス名に展開されます。copyinstr(arg0)というのは、arg0が組み込み変数で、対象となっているものの引数を指しています。copyinstr()は指定されたデータを文字列と仮定してそのデータをユーザランドへコピーせよ、という指定になります。つまり、printf("%s %s", execname, copyinstr(arg0))という処理が、プロセス名とopen(2)システムコールの引数を文字列というデータとして表示する、といった内容になります。vmtoolsd /etc/resolv.confとかdate /etc/localtimeとかといった出力がこの部分の出力になっています。

copyinstr()は奇妙な指定のように思えると思いますが、DTraceは基本的にカーネルの中で動いているので、これを使ってコピーしないとデータを扱うことができません。このあたりの処理はawk(1)にはないdtrace(1)独特のものです。

次のDプログラムはfork系のシステムコールが呼ばれたときと終了したときに一致し、fork系のシステムコールを実行したプロセスのプロセスIDを出力します。

 fork系システムコールを実行したプロセスのプロセスIDを出力
# dtrace -n 'syscall::*fork: { trace(pid); }'
dtrace: description 'syscall::*fork: ' matched 16 probes
CPU     ID                    FUNCTION:NAME
  0  58059                       fork:entry     47996
  0  58060                      fork:return     47996
  0  58187                      vfork:entry     47996
  0  58188                     vfork:return     47996
  0  58187                      vfork:entry     47996
  0  58188                     vfork:return     47996
  0  58059                       fork:entry     47996
  0  58060                      fork:return     47996
  0  58187                      vfork:entry     47996
  0  58188                     vfork:return     47996
  0  58187                      vfork:entry     47996
  0  58188                     vfork:return     47996
^C

#

syscall::*fork:にはワイルドカードが使われています。*forkに一致するシステムコールがヒットするわけです。そしてこちらにはentryという指定がありません。このため、entryとreturnという2つのプローブが表示されています。entryが呼ばれたとき、returnが終わるとき、のような指定になっています。

次の例はexec系のシステムコールが呼ばれた場合に、そのenterとreturnのタイミングでプロセス名を表示するといったものです。

 exec系システムコールを実行したプロセスと実行されたプロセスが終了するときが拾われている
# dtrace -n 'syscall::exec*: { trace(execname); }'
dtrace: description 'syscall::exec*: ' matched 2 probes
CPU     ID                    FUNCTION:NAME
  0  58173                     execve:entry   fish
  0  58174                    execve:return   date
  0  58173                     execve:entry   fish
  0  58174                    execve:return   hostname
  0  58173                     execve:entry   fish
  0  58174                    execve:return   tr
  0  58173                     execve:entry   fish
  0  58174                    execve:return   sed
  0  58173                     execve:entry   fish
  0  58174                    execve:return   awk
^C

#

上記の例ですと、fishというインタラクティブシェルからコマンドが実行されexecve(2)システムコール経由でそのコマンドが実行され終了していることがわかります。

プレディケートと、アグレゲーションという機能を使った場合の例が次です。システムコールが呼ばれた回数を集計し、呼び出されたシステムコールをシステムコールごとに集計して出力しています。対象はdateという名前のプログラムに限定されています。

 プレディケートとアグレゲーションも使った場合の利用例
# dtrace -n 'syscall:::entry /execname == "date"/ { @[probefunc] = count(); }'
dtrace: description 'syscall:::entry ' matched 1082 probes
^C

  exit                                                              3
  ioctl                                                             3
  lseek                                                             3
  readlink                                                          3
  sysarch                                                           3
  write                                                             3
  __sysctl                                                          6
  access                                                            6
  open                                                              6
  issetugid                                                         9
  openat                                                            9
  read                                                             12
  close                                                            15
  fstat                                                            18
  lstat                                                            18
  munmap                                                           18
  mmap                                                             33
  sigprocmask                                                      36
  #

syscall:::entryというプローブですべてのシステムコールが呼ばれたタイミングに一致します。/execname == "date"/というプレディケートで、そこから名前がdateというもののみに絞込みを行ってやります。

@[probefunc] = count();というアクションがアグレゲーションというDTraceでもっとも特徴的な機能を使った処理になっています。呼び出された関数名(この場合はシステムコール名)ごとに、その呼ばれた回数をカウントして保存しておけ、という意味になります。このコマンドを実行するとCtrl-Cが押されるまでデータが集計され、Ctrl-Cが押されると集計結果が表示されます。

アグレゲーションは手続き型のプログラミング言語ではあまり見かけないものなので、これをみただけでは飲み込みずらい機能かもしれません。しかし強力で便利な機能ですし、同じことをCで書くことを考えるとすげー楽であるということがよくわかると思います。

サンプルコマンドは"DTrace Dynamic Tracing In Oracle Solaris, Mac OS X & FreeBSD", by Brendan Gregg and Jim Mauro P.19〜44より抜粋(コマンドの一部はFreeBSD向けに変更)

勉強会

第60回 2017年2月23日(木)19:00~FreeBSD勉強会

発表内容検討中。発表ネタをお持ちの方、ぜひご連絡ください。

参加申請はこちらから。

第62回 3月23日(木)19:00~FreeBSD勉強会:リキャップ・ザ・AsiaBSDCon 2017 ~日本語でふりかえるABC~

2017年3月9~12日まで、東京でAsiaBSDCon 2017が開催される予定です。ぜひこのカンファレンスにご参加いただきたいわけなのですが、なかにはどうしても仕事の都合で参加できなかったとか、正直英語がよくわからなかったとか、そういった方もいらっしゃるのではないかと思います。

3月のFreeBSD勉強会では、AsiaBSDCon 2017のあとというこのタイミングを活かして、AsiaBSDCon 2017の発表内容を振り返ってみよう、というのをやってみようと思います。AsiaBSDConに参加しているにもかかわらず、これまで一度もプロシーディングを読み返したことすらないというあなた、ぜひプロシーディングを持参してご参加ください :) AsiaBSDConに参加できなかったというあなたも、この機会をお見逃しなく(できればAsiaBSDConそのものに参加した方が絶対的によいです、あしからず⁠⁠。

FreeBSD勉強会 発表者募集

FreeBSD勉強会では発表者を募集しています。FreeBSDに関して発表を行いたい場合、@daichigoto までメッセージをお願いします。1時間半~2時間ほどの発表資料を作成していただき発表をお願いできればと思います。

おすすめ記事

記事・ニュース一覧