SystemTap Begginers Guide 5.2 のメモ

5.2 ディスク

以下のセスクションではディスクとIOの状況について監視するスクリプトを説明する。

5.2.1 ディスク読み書きトラフィックの集計

disktop.stpは実行中のプロセスで最も重いディスク読み書きをしているプロセスを特定する。

#!/usr/bin/stap 
#
# Copyright (C) 2007 Oracle Corp.
#
# Get the status of reading/writing disk every 5 seconds,
# output top ten entries 
#
# This is free software,GNU General Public License (GPL);
# either version 2, or (at your option) any later version.
#
# Usage:
#  ./disktop.stp
#

global io_stat,device
global read_bytes,write_bytes

probe vfs.read.return {
  if ($return>0) {
    if (devname!="N/A") {/*skip read from cache*/
      io_stat[pid(),execname(),uid(),ppid(),"R"] += $return
      device[pid(),execname(),uid(),ppid(),"R"] = devname
      read_bytes += $return
    }
  }
}

probe vfs.write.return {
  if ($return>0) {
    if (devname!="N/A") { /*skip update cache*/
      io_stat[pid(),execname(),uid(),ppid(),"W"] += $return
      device[pid(),execname(),uid(),ppid(),"W"] = devname
      write_bytes += $return
    }
  }
}

probe timer.ms(5000) {
  /* skip non-read/write disk */
  if (read_bytes+write_bytes) {

    printf("\n%-25s, %-8s%4dKb/sec, %-7s%6dKb, %-7s%6dKb\n\n",
           ctime(gettimeofday_s()),
           "Average:", ((read_bytes+write_bytes)/1024)/5,
           "Read:",read_bytes/1024,
           "Write:",write_bytes/1024)

    /* print header */
    printf("%8s %8s %8s %25s %8s %4s %12s\n",
           "UID","PID","PPID","CMD","DEVICE","T","BYTES")
  }
  /* print top ten I/O */
  foreach ([process,cmd,userid,parent,action] in io_stat- limit 10)
    printf("%8d %8d %8d %25s %8s %4s %12d\n",
           userid,process,parent,cmd,
           device[process,cmd,userid,parent,action],
           action,io_stat[process,cmd,userid,parent,action])

  /* clear data */
  delete io_stat
  delete device
  read_bytes = 0
  write_bytes = 0  
}

probe end{
  delete io_stat
  delete device
  delete read_bytes
  delete write_bytes
  printf("END!\n");
}

disktop.stpは最もディスク読み書きが重い順に、
以下のデータを出力する。

  • UID ユーザID ID0はroot
  • PID プロセスID
  • PPID 親プロセスのプロセスID
  • CMD プロセス名
  • DEVICE プロセスが読み書きを行なっているデバイス
  • T Wであれば書き込みを、Rであれば読み込みを行なっている
  • BYTES 読み書きしたサイズ

ctime関数は、gettimeofday_s関数で得たUNIXエポックでの秒数をカレンダー時刻に変換する。
このスクリプト中の$returnはプロセスが仮想ファイルシステムから読み書きしたバイト数が格納されている。

以下は実行例。

sudo stap ./disktop.stp 

Sun Jul 29 05:03:33 2012 , Average:   0Kb/sec, Read:       0Kb, Write:      0Kb

     UID      PID     PPID                       CMD   DEVICE    T        BYTES
    1000     1108      989               thunderbird     dm-1    R          166

Sun Jul 29 05:03:38 2012 , Average:   0Kb/sec, Read:       0Kb, Write:      0Kb

     UID      PID     PPID                       CMD   DEVICE    T        BYTES
    1000     1326      989            gnome-terminal     dm-1    W          560

5.2.2 各ファイル読み書きにおけるIOの時間

このセクションでは、プロセスがファイルに対して読み書きに要する時間を計測する方法を説明する。

iotime.stp

#!/usr/bin/stap

/*
 * Copyright (C) 2006-2007 Red Hat Inc.
 * 
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU General Public License v.2.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Print out the amount of time spent in the read and write systemcall
 * when each file opened by the process is closed. Note that the systemtap 
 * script needs to be running before the open operations occur for
 * the script to record data.
 *
 * This script could be used to to find out which files are slow to load
 * on a machine. e.g.
 *
 * stap iotime.stp -c 'firefox'
 *
 * Output format is:
 * timestamp pid (executabable) info_type path ...
 *
 * 200283135 2573 (cupsd) access /etc/printcap read: 0 write: 7063
 * 200283143 2573 (cupsd) iotime /etc/printcap time: 69
 *
 */

global start
global time_io

function timestamp:long() { return gettimeofday_us() - start }

function proc:string() { return sprintf("%d (%s)", pid(), execname()) }

probe begin { start = gettimeofday_us() }

global filehandles, fileread, filewrite

probe syscall.open.return {
  filename = user_string($filename)
  if ($return != -1) {
    filehandles[pid(), $return] = filename
  } else {
    printf("%d %s access %s fail\n", timestamp(), proc(), filename)
  }
}

probe syscall.read.return {
  p = pid()
  fd = $fd
  bytes = $return
  time = gettimeofday_us() - @entry(gettimeofday_us())
  if (bytes > 0)
    fileread[p, fd] += bytes
  time_io[p, fd] <<< time
}

probe syscall.write.return {
  p = pid()
  fd = $fd
  bytes = $return
  time = gettimeofday_us() - @entry(gettimeofday_us())
  if (bytes > 0)
    filewrite[p, fd] += bytes
  time_io[p, fd] <<< time
}

probe syscall.close {
  if ([pid(), $fd] in filehandles) {
    printf("%d %s access %s read: %d write: %d\n",
           timestamp(), proc(), filehandles[pid(), $fd],
           fileread[pid(), $fd], filewrite[pid(), $fd])
    if (@count(time_io[pid(), $fd]))
      printf("%d %s iotime %s time: %d\n",  timestamp(), proc(),
             filehandles[pid(), $fd], @sum(time_io[pid(), $fd]))
   }
  delete fileread[pid(), $fd]
  delete filewrite[pid(), $fd]
  delete filehandles[pid(), $fd]
  delete time_io[pid(),$fd]
}

iotime.stpは、あるファイルへのシステムコールopen, close, read, writeのそれぞれの時間を追跡する。
それぞれのファイルに関して、システムコールへのアクセスがあると、iotime.stpは読み書きが完了するまでの時間をマイクロ秒単位で数えて、
データ量をバイト単位で追跡する。

iotime.stpは、$countに読み書きしたデータ量をバイト単位で保存する。$returnには、読み書きしたデータ量が保存されている。
$countはデータの読み書きが起こった際にのみ使われる。

sudo stap ./iotime.stp 

1043730 900 (sendmail) access /proc/loadavg read: 26 write: 0
1043736 900 (sendmail) iotime /proc/loadavg time: 7
6048731 900 (sendmail) access /proc/loadavg read: 26 write: 0
6048737 900 (sendmail) iotime /proc/loadavg time: 7
11053729 900 (sendmail) access /proc/loadavg read: 26 write: 0
11053735 900 (sendmail) iotime /proc/loadavg time: 6
16058728 900 (sendmail) access /proc/loadavg read: 26 write: 0
16058734 900 (sendmail) iotime /proc/loadavg time: 7
18589249 679 (crond) access /etc/passwd read: 1800 write: 0
18589255 679 (crond) iotime /etc/passwd time: 12
18589299 679 (crond) access /etc/passwd read: 1800 write: 0
18589302 679 (crond) iotime /etc/passwd time: 5
18589328 679 (crond) access /etc/passwd read: 1800 write: 0
18589331 679 (crond) iotime /etc/passwd time: 5
21063733 900 (sendmail) access /proc/loadavg read: 26 write: 0
21063739 900 (sendmail) iotime /proc/loadavg time: 7

iotime.stpは、以下の情報を出力する。

  • タイムスタンプ マイクロ秒単位
  • プロセスIDとプロセス名
  • access と iotime フラグ
  • アクセスしたファイル

プロセスがファイルに読み書きを行えるようになると、accessかiotimeの行が出力される。
accessは、ファイルにアクセスを始めた事を示し、行末の数値は読み書きを試みたデータサイズをバイト単位で示す。
iotimeは、プロセスが読み書きを実行する過程の時間をマイクロ秒単位で示す。

accessの後にiotimeが現れない場合、それは単純にデータを読み書きしていない事を示す。

5.2.3 累積するIOの追跡

このセクションでは、累積したIOの量を追跡する方法について示す。

traceio.stp

#!/usr/bin/stap
# traceio.stp
# Copyright (C) 2007 Red Hat, Inc., Eugene Teo <eteo@redhat.com>
# Copyright (C) 2009 Kai Meyer <kai@unixlords.com>
#   Fixed a bug that allows this to run longer
#   And added the humanreadable function
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#

global reads, writes, total_io

probe vfs.read.return {
  reads[pid(),execname()] += $return
  total_io[pid(),execname()] += $return
}

probe vfs.write.return {
  writes[pid(),execname()] += $return
  total_io[pid(),execname()] += $return
}

function humanreadable(bytes) {
  if (bytes > 1024*1024*1024) {
    return sprintf("%d GiB", bytes/1024/1024/1024)
  } else if (bytes > 1024*1024) {
    return sprintf("%d MiB", bytes/1024/1024)
  } else if (bytes > 1024) {
    return sprintf("%d KiB", bytes/1024)
  } else {
    return sprintf("%d   B", bytes)
  }
}

probe timer.s(1) {
  foreach([p,e] in total_io- limit 10)
    printf("%8d %15s r: %12s w: %12s\n",
           p, e, humanreadable(reads[p,e]),
           humanreadable(writes[p,e]))
  printf("\n")
  # Note we don't zero out reads, writes and total_io,
  # so the values are cumulative since the script started.
}

traceio.stpは、IOトラフィックを生成する実行ファイルのトップ10を表示する。
加えて、それら実行ファイルがIO読み書きを実施した量の合計を追跡する。
この情報は1秒毎に出力される。

traceio.stpのローカル変数$returnは、disktop.stpと同様に、読み書きしたバイト数を示す。

以下に実行例を示す。

sudo stap ./traceio.stp

    793            Xorg r:        1 MiB w:        0   B
    1246         firefox r:      223 KiB w:      326 KiB
    1494       alsa-sink r:        1 KiB w:        1 KiB
    1494      pulseaudio r:        1 KiB w:        1 KiB
    1326  gnome-terminal r:      940   B w:        1 KiB
    8069          stapio r:      972   B w:      971   B
    4166     threaded-ml r:      115   B w:       60   B
    8007           gdbus r:       -6   B w:      176   B
    1326           gdbus r:       10   B w:      128   B
    8007     ibus-daemon r:       48   B w:       16   B

    1246         firefox r:        6 MiB w:        1 MiB
     793            Xorg r:        1 MiB w:        0   B
    4166 plugin-containe r:        2 KiB w:      999   B
    1326  gnome-terminal r:        1 KiB w:        1 KiB
    8069          stapio r:        1 KiB w:        1 KiB
    1494       alsa-sink r:        1 KiB w:        1 KiB
    1494      pulseaudio r:        1 KiB w:        1 KiB
    4166     threaded-ml r:      147   B w:       67   B
    8007           gdbus r:       -6   B w:      176   B
    1326           gdbus r:       10   B w:      128   B

5.2.4 IO監視(デバイス毎に)

このセクションでは、特定のデバイスに対するIOの状況を監視する方法を説明する。

traceio2.stp

#!/usr/bin/stap

global device_of_interest

probe begin {
  /* The following is not the most efficient way to do this.
      One could directly put the result of usrdev2kerndev()
      into device_of_interest.  However, want to test out
      the other device functions */
  dev = usrdev2kerndev($1)
  device_of_interest = MKDEV(MAJOR(dev), MINOR(dev))
}

probe vfs.write, vfs.read
{
  if (dev == device_of_interest)
    printf ("%s(%d) %s 0x%x\n",
            execname(), pid(), probefunc(), dev)
}

traceio2.stpは、デバイス番号をコマンドライン引数として取る。この値は、'stat -c "0x%D" ディレクトリ名'で得られる。
usrdev2kerndev関数は、デバイス番号をカーネルが理解出来る形式に変換するものである。
usrdef2kerndev関数はMKDEV(),MINOR(),MAJOR()と合わせて、各デバイスのメジャー番号とマイナー番号を決定するために使う。

traceio2.stpの出力は、各プロセスの名前とプロセスID、実行した関数名、カーネルのデバイス番号から成る。

以下は、'stap traceio2.stp 0x805'を実行した例である。
0x805は、/homeのデバイス番号である。/homeは/dev/sda5に含まれおり、そのデバイスが我々が監視したい対象である。

synergyc(3722) vfs_read 0x800005
synergyc(3722) vfs_read 0x800005
cupsd(2889) vfs_write 0x800005
cupsd(2889) vfs_write 0x800005
cupsd(2889) vfs_write 0x800005

5.2.5 ファイルへの読み書きを監視する

このセクションではファイルに対する読み書きをリアルタイムに監視する方法を説明する。

inodewatch.stp

#!/usr/bin/stap

global device_of_interest

probe begin {
  /* The following is not the most efficient way to do this.
      One could directly put the result of usrdev2kerndev()
      into device_of_interest.  However, want to test out
      the other device functions */
  dev = usrdev2kerndev($1)
  device_of_interest = MKDEV(MAJOR(dev), MINOR(dev))
}

probe vfs.write, vfs.read
{
  if (dev == device_of_interest)
    printf ("%s(%d) %s 0x%x\n",
            execname(), pid(), probefunc(), dev)
}
[ito@earth stap]$ cat inodewatch.stp 
#!/usr/bin/stap

probe vfs.write, vfs.read
{
  # dev and ino are defined by vfs.write and vfs.read
  if (dev == MKDEV($1,$2) # major/minor device
      && ino == $3)
    printf ("%s(%d) %s 0x%x/%u\n",
      execname(), pid(), probefunc(), dev, ino)
}

inodewatch.stpは、以下の3つのコマンドライン引数に取る。

  • ファイルのメジャーデバイス番号
  • ファイルのマイナーデバイス番号
  • ファイルのiノード番号

これらの情報は'stat -c "%D %i" ファイル名'で得ることが出来る。
例えば、/etc/crontabを監視したい場合、'stat -c "%D %i" /etc/crontab'を実行する。
このコマンドの実行例は以下の通りである。

805 1078319

805は16進数のデバイス番号である。下位2桁の数字がマイナーデバイス番号で、上位の数字がメジャーデバイス番号である。
1078319はiノード番号である。/etc/crontabの監視を開始するためには、'stap inodewatch.stp 0x8 0x05 1078319
'と実行する。接頭辞0xは16進数を示す。

このコマンドの実行結果は、読み書きを実行しているプロセスの名前とID、実行中の関数(例えばvfs_read, vfs_write)、
バイス番号(16進数)、iノード番号から成る。
以下の実行例は、cat /etc/crontabを実行中に'stap inodewatch.stp 0x8 0x05 1078319'を実行した結果である。

cat(16437) vfs_read 0x800005/1078319
cat(16437) vfs_read 0x800005/1078319
||< 

** 5.2.6 ファイル属性変更の監視

このセクションでは対象ファイルの属性変更をリアルタイムに監視する。

>||
#!/usr/bin/stap

global ATTR_MODE = 1

probe kernel.function("setattr_copy")!,
      kernel.function("generic_setattr")!,
      kernel.function("inode_setattr") {
  dev_nr = $inode->i_sb->s_dev
  inode_nr = $inode->i_ino

  if (dev_nr == MKDEV($1,$2) # major/minor device
      && inode_nr == $3
      && $attr->ia_valid & ATTR_MODE)
    printf ("%s(%d) %s 0x%x/%u %o %d\n",
      execname(), pid(), probefunc(), dev_nr, inode_nr, $attr->ia_mode, uid())
}

inodewatch2.stpは、コマンドライン引数に対象となるファイルのデバイス番号とiノード番号を取る。
この情報について詳しく知りたい場合は、5.2.5を参照せよ。

inodewatch2.stpの出力はinodewatch.stpと似ており、違いは監視対象のファイルの変更された属性と、
変更したユーザーのIDである。

/home/joe/bigfileに対してユーザーjoeが'chmod 777 /home/joe/bigfile'と'chmod 666 /home/joe/bigfile'を実行した場合の例を示す。

chmod(17448) inode_setattr 0x800005/6011835 100777 500
chmod(17449) inode_setattr 0x800005/6011835 100666 500

5.2.7 IOブロックの時間の周期表

このセクションでは、IO要求が完了するまでの待ち時間の合計を追跡する方法を示す。
これは過剰な未完了ブロックIO操作を特定するのに役に立つ。

#!/usr/bin/stap

global req_time, etimes

probe ioblock.request
{
  req_time[$bio] = gettimeofday_us()
}

probe ioblock.end
{
  t = gettimeofday_us()
  s =  req_time[$bio]
  delete req_time[$bio]
  if (s) {
    etimes[devname, bio_rw_str(rw)] <<< t - s
  }
}

/* for time being delete things that get merged with others */
probe kernel.trace("block_bio_frontmerge"),
      kernel.trace("block_bio_backmerge")
{
  delete req_time[$bio]
}

probe timer.s(10), end {
  ansi_clear_screen()
  printf("%10s %3s %10s %10s %10s\n",
         "device", "rw", "total (us)", "count", "avg (us)")
  foreach ([dev,rw] in etimes - limit 20) {
    printf("%10s %3s %10d %10d %10d\n", dev, rw,
           @sum(etimes[dev,rw]), @count(etimes[dev,rw]), @avg(etimes[dev,rw]))
  }
  delete etimes
}

ioblktime.stpは、各デバイスのブロックIOを待つ時間の平均を計算し、10秒毎に出力する。
この頻度は、timer.s(10)の数値を書き換えることで変更出来る。

未完了ブロックIOが多すぎる場合には、MAXMAPENTRIESの数分だけ出力される。
MAXMAPENTRIESの値を変更して実行したい場合には、-DMAXMAPENTRIES=10000をstapのオプションに指定して実行せよ。

以下は実行例である。

   device  rw total (us)      count   avg (us)
      dm-1   W      80425         11       7311
      dm-2   W      76476         10       7647
       sda   W      51706         10       5170
      sda2   W      92400          2      46200

この例は、デバイス名、実行している操作(rw)、全ての操作での待ち時間の合計(total(us))、操作の数(count)、全ての操作での待ち時間の平均時間(avg(us))を示す。
スクリプトによる時間の計算にはマイクロ秒単位の時間がかかる。