14 カーネル・モジュールの静的定義トレース

DTraceには、カーネル・モジュールにカスタマイズ・プローブを定義する開発者向けの機能があります。これらの静的プローブは、sdtプロバイダの追加のプローブとして表示され、sdtモジュールがロードされていない場合、ほとんどオーバーヘッドがかかりません。たとえば、x86_64の場合、オーバーヘッドはシングルバイトのNOPの後に4バイトのNOPです。この章では、カーネル・モジュールへの静的なプローブの定義と使用方法の完全な例を示します。

カーネル・モジュールでのプローブの名前付けおよび挿入ポイントの選択に関する一般原則は、ユーザー空間のアプリケーションの場合と同じです。DTraceのユーザー・コミュニティにとって容易に理解できるセマンティックな意味を持つプローブを定義する必要があります。通常、プローブの名前は、それらを配置するルーチンと、そのルーチン内での位置に由来します。たとえば、プローブの情報が、fooというルーチンへのエントリまたは復帰時のデータ値に関する情報である場合は、それらをfoo-entryfoo-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

ドライバのopenreadreleaseunlocked_ioctlおよびwriteルーチンを定義します。readunlocked_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)

fpbufおよびnの値は、プローブの起動時にDTraceのarg0arg1および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の例

この例では、コードの既存の行は変更されません。readunlocked_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プローブを含むモジュールを複数のソース・ファイルで構築する必要があることを意味します。

次のKbuildMakefileを使用して、疑似ドライバ・モジュール例revdev.koと、testrevdevというテスト・プログラムを作成します。

Kbuildの例

bj-m            += revdev.o

revdev-y        := rev_dev.o rev_mod.o

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をテストするには:

  1. udevルールを設定して/dev/revdevデバイス・ファイルを作成します。

    # echo "KERNEL==\"revdev\", MODE=\"0660\"" > /etc/udev/rules.d/10-revdev.rules
  2. 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
  3. 次の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_pathstringofを使用して印刷可能なファイル・パスと文字列を作成します。たとえば、(struct file *)arg0は、arg0の値をファイル・ポインタ(struct file *)にキャストします。struct fileのメンバーf_pathには、ファイルのパス構造(struct path)が含まれています。d_pathは、パス・ポインタ(struct path *)を引数として取るので、&演算子を使用してstruct pathへのポインタを返します。

    詳細は、「d_path」および「文字列変換」を参照してください。

  4. traceflowを実行可能にします。

    # chmod +x traceflow
  5. いずれかのウィンドウで、traceflowを実行します。

    # ./traceflow
  6. 別のウィンドウで、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