14 カーネル・モジュールの静的定義トレース
DTraceには、カーネル・モジュールにカスタマイズ・プローブを定義する開発者向けの機能があります。これらの静的プローブは、sdt
プロバイダの追加のプローブとして表示され、sdt
モジュールがロードされていない場合、ほとんどオーバーヘッドがかかりません。たとえば、x86_64の場合、オーバーヘッドはシングルバイトのNOPの後に4バイトのNOPです。この章では、カーネル・モジュールへの静的なプローブの定義と使用方法の完全な例を示します。
カーネル・モジュールでのプローブの名前付けおよび挿入ポイントの選択に関する一般原則は、ユーザー空間のアプリケーションの場合と同じです。DTraceのユーザー・コミュニティにとって容易に理解できるセマンティックな意味を持つプローブを定義する必要があります。通常、プローブの名前は、それらを配置するルーチンと、そのルーチン内での位置に由来します。たとえば、プローブの情報が、foo
というルーチンへのエントリまたは復帰時のデータ値に関する情報である場合は、それらをfoo-entry
とfoo-return
と名付けることができます。このようなプローブから返されるデータ値は、モジュールの内部実装からの中間値ではなく、むしろ、このルーチンをブラック・ボックスとして示す可能性があります。モジュール内の深い位置からデータを収集するには、foo-stage1
またはfoo-post-hardware-init
などの名前を持つ追加のプローブを挿入できます。
ある点では、カーネル・モジュールでの静的なプローブの使用は、ユーザー空間のアプリケーションに比べて、簡単である場合があります。プローブを含めるモジュールを条件付きでコンパイルする場合を除き、ビルド・ファイルの変更は必要ではありません。ソース・コードへのプローブの挿入は、プローブのマクロを生成するためにdtrace -hコマンドを使用できないので、いくらか複雑になります。しかし、DTRACE_PROBE
マクロを使用してプローブを挿入すると、ソース・コードへの変更は比較的シンプルです。
sdt
の静的プローブは、ソース・ファイルと必要なビルド・インフラが存在する任意のOracle Linuxカーネル・モジュールに挿入できますが、DTraceでは、64ビットのカーネル・モジュールの静的定義トレースのみがサポートされています。
sdt
プロバイダの詳細は、「sdtプロバイダ」を参照してください。
ユーザー空間アプリケーションに適用される静的に定義されたトレースの概念の概要は、「ユーザー・アプリケーションの静的定義トレース」を参照してください。
静的プローブ・ポイントの挿入
ソース・コード内に静的プローブを埋め込むことで、モジュールとそのデータの現在の状態を取得できます。
次の疑似キャラクタ・デバイス・ドライバの例は、3つのソース・ファイルで構成されています。
-
revdev.h
-
モジュールのヘッダー・ファイルです。
-
rev_mod.c
-
モジュールのプロパティとその
init
およびexit
ルーチンを定義します。 -
rev_dev.c
-
ドライバの
open
、read
、release
、unlocked_ioctl
およびwrite
ルーチンを定義します。read
、unlocked_ioctl
およびwrite
ルーチンに静的プローブを挿入しますが、必要に応じて、他のルーチンにもプローブを挿入できます。
revdev.hの例
モジュール・ヘッダー・ファイルrevdev.h
は、次の例で太字で示されているように、linux/sdt.h
を含み、プローブ・マクロを定義する行を追加することで準備する必要があります。
#include <asm/uaccess.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/kernel.h> #include <linux/miscdevice.h> #include <linux/module.h> #include <linux/mutex.h> #include <linux/types.h> #include <linux/sdt.h> #define DEVICE "revdev" #define REVDEV_IOCTL_ENTRY_PROBE(name, file, cmd, arg) \ DTRACE_PROBE3(ioctl__##name, struct file *, file, \ unsigned int, cmd, unsigned long, arg) #define REVDEV_IOCTL_RETURN_PROBE(name, str) \ DTRACE_PROBE1(ioctl__##name, struct char *, str) #define REVDEV_READ_ENTRY_PROBE(name,fp,buf) \ DTRACE_PROBE2(read__##name, file *, fp, char *, buf) #define REVDEV_READ_RETURN_PROBE(name,buf,n) \ DTRACE_PROBE2(read__##name, char *, buf, size_t, n) #define REVDEV_WRITE_ENTRY_PROBE(name,fp,buf,n) \ DTRACE_PROBE3(write__##name, file *, fp, char *, buf, size_t, n) #define REVDEV_WRITE_RETURN_PROBE(name,buf,n) \ DTRACE_PROBE2(write__##name, char *, buf, size_t, n)
/lib/modules/`uname -r`/build/include/linux/sdt.h
に定義されたDTRACE_PROBE
マクロは、0から8個の引数をサポートします。
前の例で示したように、挿入したプローブに独自のマクロを定義できます。ユーザー空間の静的なプローブとは異なり、dtrace -hコマンドを使用して適切なプローブ定義を含むヘッダー・ファイルを作成できません。プローブのプロバイダ定義ファイルを作成する必要はありません。
プローブは、DTRACE_PROBE
マクロの最初の引数に応じて名前が付けられています。マクロ名DTRACE_PROBEN
の接尾辞Nは、プローブに渡す引数の数を表します。プローブ・マクロの最初の引数が、プローブ名です。「プローブの宣言」で説明するように、2つの連続するアンダースコアは単一のダッシュに変換されます。残りのマクロ引数は、プローブの起動時に割り当てられるDTrace argn
変数を定義する引数のペアです。次の例に示すように、引数の各ペアは、変数の型と変数名を定義します。
#define REVDEV_WRITE_ENTRY_PROBE(name, fp, buf, n) \ DTRACE_PROBE3(write__##name, file *, fp, char *, buf, size_t, n)
fp
、buf
およびn
の値は、プローブの起動時にDTraceのarg0
、arg1
およびarg2
変数によって使用可能になります。
完全なプローブのプロバイダ、モジュールおよび関数の各要素は、sdt
、ドライバ・モジュール名(.ko
を除く)およびドライバ・ルーチンにちなんで名前が付けられています。
プローブは、sdt
プロバイダの安定性属性を継承します。
rev_mod.cの例
次の例では、モジュールのinit
およびexit
ルーチンのプローブの挿入がなく、変更は行われていません。これらのルーチンへのプローブの挿入に対する制限はありません。
#include "revdev.h" MODULE_AUTHOR("DTrace Example"); MODULE_DESCRIPTION("Using DTrace SDT probes with a device driver"); MODULE_VERSION("v1.0"); MODULE_LICENSE("GPL"); extern const struct file_operations revdev_fops; static struct miscdevice revdev = { .minor = 0, .name = DEVICE, .fops = &revdev_fops, }; DEFINE_MUTEX(revdev_mutex); static int revdev_entry(void){ /* Register device */ int retval; retval = misc_register(&revdev); if (retval < 0) { printk(KERN_ERR "revdev: Could not register device"); return retval; } mutex_init(&revdev_mutex); return 0; } static void revdev_exit(void){ misc_deregister(&revdev); } /* Define module init and exit calls */ module_init(revdev_entry); module_exit(revdev_exit);
rev_dev.cの例
この例では、コードの既存の行は変更されません。read
、unlocked_ioctl
およびwrite
ルーチンのそれぞれに、entry
およびreturn
プローブのための行の挿入のみが必要です。
この例の変更は太字フォントで示されます。
#include "revdev.h" static struct device_buffer { char data[80]; } devbuf; static char *oddeven[] = { "Even", "Odd" }; extern struct mutex revdev_mutex; static long revdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { char *cp; REVDEV_IOCTL_ENTRY_PROBE(entry, file, cmd, arg); cp = oddeven[arg%2]; REVDEV_IOCTL_RETURN_PROBE(return, cp); return -EAGAIN; } static int revdev_open(struct inode *inode, struct file *fp){ if (!mutex_trylock(&revdev_mutex)){ printk(KERN_INFO "revdev: Device already in use"); return -EBUSY; } return 0; } static void revstr(char *s) { /* After Kernighan and Ritchie */ int i, j, t; for (i = 0, j = strlen(s)-1; i < j; i++, j--) t = s[i], s[i] = s[j], s[j] = t; } static ssize_t revdev_read(struct file *fp, char* buf, size_t n, loff_t *o){ int retval; REVDEV_READ_ENTRY_PROBE(entry, fp, devbuf.data); revstr(devbuf.data); n = strlen(devbuf.data); retval = copy_to_user(buf, devbuf.data, n); REVDEV_READ_RETURN_PROBE(return, buf, n); if (retval != 0) return -EINVAL; return 0; } static ssize_t revdev_write(struct file *fp, const char* buf, size_t n, loff_t *o){ int retval; REVDEV_WRITE_ENTRY_PROBE(entry, fp, buf, n); retval = copy_from_user(devbuf.data, buf, n); devbuf.data[n-retval] = '\0'; REVDEV_WRITE_RETURN_PROBE(return, devbuf.data, n); if (retval != 0) return -EINVAL; return 0; } static int revdev_close(struct inode *inode, struct file *fp){ mutex_unlock(&revdev_mutex); return 0; } const struct file_operations revdev_fops = { .owner = THIS_MODULE, .read = revdev_read, .write = revdev_write, .unlocked_ioctl = revdev_ioctl, .open = revdev_open, .release = revdev_close, };
静的プローブを含むモジュールの構築
ノート:
次の例では、DTraceモジュールをサポートするUEKバージョン(Oracle Linux 7の場合はUEK R5またはUEK R4、Oracle Linux 6の場合はUEK R4)とモジュールをリンクする必要があります。
現在の実装でのバグは、SDTプローブを含むモジュールを複数のソース・ファイルで構築する必要があることを意味します。
次のKbuild
とMakefile
を使用して、疑似ドライバ・モジュール例revdev.ko
と、testrevdevというテスト・プログラムを作成します。
Makefileの例
ノート:
Makefileのすべてのコマンドライン(次の例のgcc
で始まるものなど)は、タブで始まる必要があります。
KERNEL_DIR = /lib/modules/`uname -r`/build modules:: testrevdev install:: modules_install testrevdev: testrevdev.c gcc -o testrevdev testrevdev.c %:: $(MAKE) -C $(KERNEL_DIR) M=`pwd` $@
testrevdevのソース・ファイルは、testrevdev.c
です。
testrevdev.cの例
#include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #define DEVICE_FILE "/dev/revdev" int main() { char buf[81]; int i, fd, n; if ((fd = open(DEVICE_FILE, O_RDWR)) != 0) { perror("open"); exit(1); } i=0; while (1) { (i++)%20; printf("Write: "); scanf(" %80[^\n]", buf); n = strlen(buf); if (!strncmp(buf, "exit", 4)) break; else if (!strncmp(buf, "ioctl", 5)) ioctl(fd,128,i); else { write(fd, buf, n); read(fd, buf, n); buf[n]='\0'; printf(" Read: %s\n", buf); } } close(fd); exit(0); }
testrevdevを実行すると、ユーザーの入力した文字列を読み込み、revdev
デバイスに文字列を書き込み、その後デバイスから反転文字列を読み込みます。
入力文字列がioctl
で始まる場合は、オープン・ファイル記述子でioctl
を呼び出すことにより、デバイスのunlocked_ioctl
ルーチンを起動します。入力文字列がexit
で始まる場合は、プログラムを終了します。
モジュールとテスト・プログラムをビルドするには、makeコマンドを使用します。
# make make -C /lib/modules/`uname -r`/build M=`pwd` modules make[1]: Entering directory `/usr/src/kernels/4.1.12-version.el6uek.x86_64' CC [M] /root/revdev/rev_dev.o CC [M] /root/revdev/rev_mod.o SDTSTB /root/revdev/revdev.sdtstub.S AS [M] /root/revdev/revdev.sdtstub.o LD [M] /root/revdev/revdev.o Building modules, stage 2. MODPOST 1 modules SDTINF /root/revdev/revdev.sdtinfo.c CC /root/revdev/revdev.mod.o CTF LD [M] /root/revdev/revdev.ko make[1]: Leaving directory `/usr/src/kernels/4.1.12-version.el6uek.x86_64'
DTraceを使用した静的プローブを含むモジュールのテスト
モジュールに埋め込まれた静的プローブの1つが起動したとき、DTraceを使用して情報を表示できます。
モジュール例revdev.ko
をテストするには:
-
udev
ルールを設定して/dev/revdev
デバイス・ファイルを作成します。# echo "KERNEL==\"revdev\", MODE=\"0660\"" > /etc/udev/rules.d/10-revdev.rules
-
revdev.ko
モジュールをロードします。# insmod revdev.ko
dtraceを使用して、プローブが使用可能かどうかをテストできます。
# dtrace -l -m revdev ID PROVIDER MODULE FUNCTION NAME 4 sdt revdev revdev_ioctl ioctl-return 5 sdt revdev revdev_ioctl ioctl-entry 6 sdt revdev revdev_write write-return 7 sdt revdev revdev_write write-entry 8 sdt revdev revdev_read read-return 9 sdt revdev revdev_read read-entry
-
次のDTraceスクリプト(traceflow)を入力します。
#!/usr/sbin/dtrace -qs #pragma D option nspec=10 self int indent; syscall:::entry /execname == "testrevdev"/ { self->specflag = 0; self->spec = speculation(); self->indent += 2; speculate(self->spec); } syscall:::entry /self->spec/ { speculate(self->spec); printf("%*s ", self->indent, "->"); printf("%s() entry\n",probefunc); self->indent += 2; } syscall:::return /self->spec/ { speculate(self->spec); self->indent -= 2; printf("%*s ", self->indent, "<-"); printf("%s() return\n",probefunc); } syscall:::return /self->spec && self->specflag == 0/ { discard(self->spec); self->indent -= 2; self->spec = 0; } syscall:::return /self->spec && self->specflag == 1/ { commit(self->spec); self->indent -= 2; self->spec = 0; } sdt:revdev::ioctl-entry /self->spec/ { speculate(self->spec); self->specflag = 1; printf("%*s ", self->indent, "=>"); printf("%s() entry file: %s cmd: %d arg: %d\n", probefunc, d_path(&(((struct file *)arg0)->f_path)), arg1, arg2); } sdt:revdev::ioctl-return /self->spec/ { speculate(self->spec); printf("%*s ", self->indent, "<="); printf("%s() return cpstr: %s\n", probefunc, stringof((char*)arg0)); } sdt:revdev::read-entry /self->spec/ { speculate(self->spec); self->specflag = 1; printf("%*s ", self->indent, "=>"); printf("%s() entry file: %s devbuf: %s\n", probefunc, d_path(&(((struct file *)arg0)->f_path)), stringof((char *)arg1)); } sdt:revdev::read-return /self->spec/ { speculate(self->spec); printf("%*s ", self->indent, "<="); printf("%s() return string: %s len: %d\n", probefunc, stringof((char *)arg0), arg1); } sdt:revdev::write-entry /self->spec/ { speculate(self->spec); self->specflag = 1; printf("%*s ", self->indent, "=>"); printf("%s() entry file: %s string: %s len: %d\n", probefunc, d_path(&(((struct file *)arg0)->f_path)), stringof((char *)arg1), arg2); } sdt:revdev::write-return /self->spec/ { speculate(self->spec); printf("%*s ", self->indent, "<="); printf("%s() return string: %s len: %d\n", probefunc, stringof((char *)arg0), arg1); }
traceflowは、挿入されたプローブのいずれかが起動すると、プローブ引数変数(
arg0
,arg1
,arg2
,...)を使用してモジュール内のデータ値の情報を表示します。ノート:
file *
およびchar *
などのポインタ型を返す引数変数は、明示的にキャストする必要があります。スクリプトは、
d_path
とstringof
を使用して印刷可能なファイル・パスと文字列を作成します。たとえば、(struct file *)arg0
は、arg0
の値をファイル・ポインタ(struct file *
)にキャストします。struct file
のメンバーf_path
には、ファイルのパス構造(struct path
)が含まれています。d_path
は、パス・ポインタ(struct path *
)を引数として取るので、&
演算子を使用してstruct path
へのポインタを返します。 -
traceflowを実行可能にします。
# chmod +x traceflow
-
いずれかのウィンドウで、traceflowを実行します。
# ./traceflow
-
別のウィンドウで、testrevdevを実行し、次の例のように入力を入力します。
# ./testrevdev Write: hello Read: olleh Write: world Read: dlrow Write: ioctl Write: ioctl Write: exit
DTraceが
revdev.ko
のプローブの起動に応答すると、traceflowを実行しているウィンドウに次のような結果が表示されます。# ./traceflow -> write() entry => revdev_write() entry file: /dev/revdev string: hello len: 5 <= revdev_write() return string: hello len: 5 <- write() return -> read() entry => revdev_read() entry file: /dev/revdev devbuf: hello <= revdev_read() return string: olleh len: 5 <- read() return -> write() entry => revdev_write() entry file: /dev/revdev string: world len: 5 <= revdev_write() return string: world len: 5 <- write() return -> read() entry => revdev_read() entry file: /dev/revdev devbuf: world <= revdev_read() return string: dlrow len: 5 <- read() return -> ioctl() entry => revdev_ioctl() entry file: /dev/revdev cmd: 128 arg: 3 <= revdev_ioctl() return cpstr: Odd <- ioctl() return -> ioctl() entry => revdev_ioctl() entry file: /dev/revdev cmd: 128 arg: 4 <= revdev_ioctl() return cpstr: Even <- ioctl() return