Contained WithinFind More DocumentationFeatured Support Resources | Download this book in PDF (2302 KB)
第 27 章 io プロバイダio プロバイダは、ディスク入出力に関連したプローブを使用できるようにします。io プロバイダでは、iostat(1M) のような入出力監視ツールで監視している動作をすばやく調べることができます。たとえば、io プロバイダを使用すると、入出力に関する情報を、デバイス別、入出力の種類別、入出力サイズ別、プロセス別、アプリケーション名別、ファイル名別、またはファイルオフセット別に確認できます。 プローブ表 27–1 は、io プローブの説明です。 表 27–1 io プローブ
io プローブは、周辺機器 に対する入出力要求が発行されたときと、NFS サーバーに対するファイルの読み取り/書き込み要求が発行されたときに起動します。たとえば、NFS サーバーに対してメタデータの要求が発行されても、readdir(3C) によって io プローブがトリガーされることはありません。 引数表 27–2 に、io プローブの引数の型を示します。引数については、表 27–1 を参照してください。 表 27–2 io プローブ引数
io プローブの引数は、buf(9S) 構造体のポインタ、devinfo_t のポインタ、および fileinfo_t のポインタで構成されます。以下では、これらの構造体について詳しく説明します。 bufinfo_t 構造体bufinfo_t 構造体は、入出力要求について説明する抽象化オブジェクトです。start プローブ、done プローブ、wait-start プローブ、および wait-done プローブ内の args[0] は、入出力要求のバッファーをポイントしています。以下は、bufinfo_t 構造体の定義です。 typedef struct bufinfo {
int b_flags; /* flags */
size_t b_bcount; /* number of bytes */
caddr_t b_addr; /* buffer address */
uint64_t b_blkno; /* expanded block # on device */
uint64_t b_lblkno; /* block # on device */
size_t b_resid; /* # of bytes not transferred */
size_t b_bufsize; /* size of allocated buffer */
caddr_t b_iodone; /* I/O completion routine */
dev_t b_edev; /* extended device */
} bufinfo_t;
b_flags メンバーは、複数の状態値のビット単位の論理和であり、入出力バッファーの状態を表します。表 27–3 に、有効な状態値を示します。 表 27–3 b_flags の値
b_bcount フィールドには、入出力要求の一環として転送されるバイト数が入ります。 B_PAGEIO が設定されていない場合、b_addr フィールドには、入出力要求の仮想アドレスが入ります。B_PHYS が設定されていない場合、これはカーネル仮想アドレスです。B_PHYS が設定されている場合は、ユーザー仮想アドレスです。B_PAGEIO が設定されている場合、b_addr フィールドには、カーネル非公開データが入ります。B_PHYS と B_PAGEIO は、どちらか 1 つだけを設定してください。そうしないと、両方とも設定されません。 b_lblkno フィールドは、デバイス上のどの論理ブロックがアクセスされるかを表します。論理ブロックから物理ブロック (シリンダ、トラックなど) へのマッピングは、デバイスごとに定義されています。 b_resid フィールドには、エラーのため転送されなかったバイト数が入ります。 b_bufsize フィールドには、割り当て済みのバッファーのサイズが入ります。 b_iodone フィールドは、入出力の完了時に呼び出されるカーネル内の特定のルーチンを表します。 b_error フィールドには、入出力エラーの際ドライバから返されるエラーコードが入ります。b_error が設定されるときは、b_flags メンバー内にも B_ERROR ビットが設定されます。 b_edev フィールドには、アクセス対象のデバイスのメジャーデバイス番号とマイナーデバイス番号が入ります。コンシューマは、D サブルーチン getmajor() と getminor() を使って、b_edev フィールドからメジャーデバイス番号とマイナーデバイス番号を抽出します。 devinfo_tdevinfo_t 構造体は、デバイスに関する情報を提供します。start プローブ、done プローブ、wait-start プローブ、および wait-done プローブ内の args[1] は、入出力のターゲットデバイスの devinfo_t 構造体をポイントしています。devinfo_t は、次のメンバーから成ります。 typedef struct devinfo {
int dev_major; /* major number */
int dev_minor; /* minor number */
int dev_instance; /* instance number */
string dev_name; /* name of device */
string dev_statname; /* name of device + instance/minor */
string dev_pathname; /* pathname of device */
} devinfo_t;
dev_major フィールドには、メジャーデバイス番号が入ります。詳細については、getmajor(9F) のマニュアルページを参照してください。 dev_minor フィールドには、マイナーデバイス番号が入ります。詳細については、getminor(9F) のマニュアルページを参照してください。 dev_instance フィールドには、デバイスのインスタンス番号が入ります。デバイスのインスタンス番号は、マイナーデバイス番号とは別のものです。マイナーデバイス番号は、デバイスドライバの管理下にある抽象化オブジェクトです。インスタンス番号は、デバイスノードのプロパティです。デバイスノードのインスタンス番号を表示するには、prtconf(1M) を実行します。 dev_name フィールドには、デバイスを管理するデバイスドライバの名前が入ります。デバイスドライバ名を表示するには、prtconf(1M) に -D を指定して実行します。 dev_statname フィールドには、デバイス名が入ります。このデバイス名は、iostat(1M) を実行したとき出力されるデバイス名と同じです。この名前は、kstat(1M) を実行したとき出力されるカーネル統計情報の名前とも一致しています。このフィールドは、iostat や kstat の出力内容に異常があったとき、この出力を実際の入出力アクティビティとすばやく対応付けるために提供されているものです。 dev_pathname フィールドには、デバイスの完全パスが入ります。このパスを prtconf(1M) の引数に指定すると、詳しいデバイス情報を得ることができます。dev_pathname のパスには、デバイスノード、インスタンス番号、およびマイナーノードを表す要素が含まれています。しかし、統計情報名に、これら 3 つの要素全部が含まれているとはかぎりません。デバイスによっては、統計情報名がデバイス名とインスタンス番号で構成されていることがあります。デバイスによってはまた、デバイス名とマイナーノード番号で構成されていることもあります。その結果、2 つのデバイスの dev_statname は同じでも、dev_pathname は異なるということが起こり得ます。 fileinfo_tfileinfo_t 構造体は、ファイルに関する情報を提供します。start プローブ、done プローブ、wait-start プローブ、および wait-done プローブの args[2] は、入出力関連のファイルをポイントしています。ファイル情報が存在するかどうかは、入出力要求のディスパッチ時にこの情報を提供するファイルシステムによって決まります。一部のファイルシステム、特に第三者のファイルシステムは、この情報を提供しない場合があります。また、入出力要求の発行元ファイルシステムのファイル情報が存在しない場合もあります。たとえば、ファイルシステムのメタデータへの入出力には、関連ファイルはありません。さらに、高度に最適化されたファイルシステムは、複数の互いに無関係なファイルからの入出力を集積して、単一の入出力要求を作成することがあります。このようなファイルシステムからは、入出力の大部分を表現するファイルまたは入出力の「一部」を表現するファイルのファイル情報が提供されることがあります。また、このようなファイルシステムからファイル情報がまったく提供されないこともあります。 以下に、fileinfo_t 構造体の定義を示します。 typedef struct fileinfo {
string fi_name; /* name (basename of fi_pathname) */
string fi_dirname; /* directory (dirname of fi_pathname) */
string fi_pathname; /* full pathname */
offset_t fi_offset; /* offset within file */
string fi_fs; /* filesystem */
string fi_mount; /* mount point of file system */
} fileinfo_t;
fi_name フィールドには、ファイル名だけが入ります。ここには、ディレクトリ要素は含まれません。入出力に関連ファイル情報がない場合、fi_name フィールドには文字列 <none> が入ります。まれに、ファイルのパス名が未知であることもあります。この場合、fi_name フィールドには文字列 <unknown> が入ります。 fi_dirname フィールドには、ファイル名のディレクトリ要素だけが入ります。fi_name の場合と同じく、ファイル情報が存在しない場合、この文字列は <none> になります。ファイルのパス名が未知である場合、この文字列は <unknown> になります。 fi_pathname フィールドには、ファイルの完全パス名が入ります。fi_name の場合と同じく、ファイル情報が存在しない場合、この文字列は <none> になります。ファイルのパス名が未知である場合、この文字列は <unknown> になります。 fi_offset フィールドには、ファイル内のオフセットが入ります。ファイル情報が存在しない場合や、ファイルシステムがオフセットを指定していない場合は、-1 が入ります。 例以下は、入出力要求が発行されるたびに関連情報を出力するスクリプトです。 #pragma D option quiet
BEGIN
{
printf("%10s %58s %2s\n", "DEVICE", "FILE", "RW");
}
io:::start
{
printf("%10s %58s %2s\n", args[1]->dev_statname,
args[2]->fi_pathname, args[0]->b_flags & B_READ ? "R" : "W");
}
x86 ラップトップシステム上で Acrobat Reader をコールドスタートした場合、次のような出力が得られます。
入出力が特定のファイル内のデータに関連していない場合、<none> エントリが出力されています。 これらの入出力は、任意の書式のメタデータ用です。ファイルのパス名が未知である場合、<unknown> エントリが出力されています。このような事態はめったに発生しません。 連想配列を使って各入出力の所要時間を追跡することにより、上のスクリプト例を少し複雑にすることもできます。次の例を参照してください。 #pragma D option quiet
BEGIN
{
printf("%10s %58s %2s %7s\n", "DEVICE", "FILE", "RW", "MS");
}
io:::start
{
start[args[0]->b_edev, args[0]->b_blkno] = timestamp;
}
io:::done
/start[args[0]->b_edev, args[0]->b_blkno]/
{
this->elapsed = timestamp - start[args[0]->b_edev, args[0]->b_blkno];
printf("%10s %58s %2s %3d.%03d\n", args[1]->dev_statname,
args[2]->fi_pathname, args[0]->b_flags & B_READ ? "R" : "W",
this->elapsed / 10000000, (this->elapsed / 1000) % 1000);
start[args[0]->b_edev, args[0]->b_blkno] = 0;
}
アイドル状態の x86 ラップトップシステム上で上記スクリプト例を実行し、USB ストレージデバイスを差し込むと、次のような出力が得られます。
この出力から、このシステムの力学についてさまざまな観察が可能です。まず、最初のいくつかの入出力の実行に長い時間がかかっていること (それぞれの所要時間は約 25 ミリ秒) に注目します。これは、cmdk0 デバイスがラップトップによって電源管理されていたためと考えられます。次に、USB 大容量記憶装置の処理のため scsa2usb(7D) ドライバがロードされた際の入出力を観察します。さらに、この装置が報告される時点で、/var/adm/messages への書き込みが発生していることに注目します。最後に、デバイスリンクジェネレータ (ファイル名の末尾が link.so のファイル) の読み取りを観察します。デバイスリンクジェネレータは、新しく接続されたデバイスを処理していると考えられます。 io プロバイダを使用すると、iostat(1M) 出力について詳しい情報を得ることができます。次の例のような iostat 出力を調べるとします。
iotime.d スクリプトを使用して、次の例のように、入出力が行われるたびに確認できます。
この出力からは、archives.tar というファイルが、cmdk0 によって (/export/archives で) 読み取られており、またデバイス sd2 へ (/mnt で) 書き込まれているかのように見えます。しかし、archives.tar という名前のファイルが 2 つ存在し、並行して別々に処理されるということは通常ありえません。さらに詳しく調べたい場合は、デバイス、アプリケーション、プロセス ID、および転送バイト数を集積します。次の例を参照してください。 #pragma D option quiet
io:::start
{
@[args[1]->dev_statname, execname, pid] = sum(args[0]->b_bcount);
}
END
{
printf("%10s %20s %10s %15s\n", "DEVICE", "APP", "PID", "BYTES");
printa("%10s %20s %10d %15@d\n", @);
}
このスクリプトを数秒間実行すると、次のような出力が得られます。
出力から、このアクティビティが、あるデバイスから別のデバイスへの archives.tar ファイルのコピーであったことがわかります。このことから別の疑問が自然にわいてきます: 一方のデバイスの速度がもう一方のデバイスよりも高速なのでしょうか。 コピーするときはどちらのデバイスがリミッタになるのでしょうか。こうした問題に答えるには、各デバイスの秒当たりの転送バイト数より、各デバイスの有効なスループットを把握する必要があります。スループットを特定するには、次のようなスクリプトを使用します。 #pragma D option quiet
io:::start
{
start[args[0]->b_edev, args[0]->b_blkno] = timestamp;
}
io:::done
/start[args[0]->b_edev, args[0]->b_blkno]/
{
/*
* We want to get an idea of our throughput to this device in KB/sec.
* What we have, however, is nanoseconds and bytes. That is we want
* to calculate:
*
* bytes / 1024
* ------------------------
* nanoseconds / 1000000000
*
* But we can't calculate this using integer arithmetic without losing
* precision (the denomenator, for one, is between 0 and 1 for nearly
* all I/Os). So we restate the fraction, and cancel:
*
* bytes 1000000000 bytes 976562
* --------- * ------------- = --------- * -------------
* 1024 nanoseconds 1 nanoseconds
*
* This is easy to calculate using integer arithmetic; this is what
* we do below.
*/
this->elapsed = timestamp - start[args[0]->b_edev, args[0]->b_blkno];
@[args[1]->dev_statname, args[1]->dev_pathname] =
quantize((args[0]->b_bcount * 976562) / this->elapsed);
start[args[0]->b_edev, args[0]->b_blkno] = 0;
}
END
{
printa(" %s (%s)\n%@d\n", @);
}
このスクリプトを数秒間実行すると、次のような出力が得られます。
この出力から、sd2 がリミッタデバイスであることは明白です。sd2 のスループットは毎秒 256 - 512K バイトです。一方、cmdk0 は、毎秒 8M バイトから 64M バイトを超える速さで入出力を行います。このスクリプトは、iostat で出力されるデバイス名と、デバイスの完全パスとの両方を出力します。デバイスについてさらに詳しく知りたい場合は、次の例のように、prtconf にデバイスのパスを指定します。
強調部分からわかるように、このデバイスはリムーバブル USB 記憶装置です。 この節の例では、すべての入出力要求について調べてきました。しかし、1 種類の要求だけを調べたい場合もあります。以下の例では、書き込みが発生しているディレクトリと、この書き込みを実行しているアプリケーションを追跡します。 #pragma D option quiet
io:::start
/args[0]->b_flags & B_WRITE/
{
@[execname, args[2]->fi_dirname] = count();
}
END
{
printf("%20s %51s %5s\n", "WHO", "WHERE", "COUNT");
printa("%20s %51s %5@d\n", @);
}
このスクリプトを、デスクトップの作業負荷で一定期間実行すると、次に示すように、興味深い結果が得られます。
この出力から、事実上すべての書き込みが Mozilla Firebird のキャッシュと関連付けられていることがわかります。<none> エントリが表示されている書き込みは、おそらく UFS ログに関連付けられている書き込みです。この書き込みは、ファイルシステム上の別の書き込みによって引き起こされます。ロギングの詳細については、ufs(7FS) のマニュアルページを参照してください。この例では、io プロバイダを使って、ソフトウェアの上位層で発生した問題の検出方法を示します。この場合、このスクリプトの構成に問題があります。 Web ブラウザのキャッシュが tmpfs(7FS) ファイルシステム内のディレクトリにあれば、この Web ブラウザによる入出力はずっと少なく(おそらく皆無に) なるはずです。 前の例では、start プローブと done プローブしか使われていません。アプリケーションの入出力がブロックされる理由と、その期間を調べたい場合は、wait-start プローブと wait-done プローブを使用します。以下のスクリプトでは、io プローブと sched プローブ (第 26 章sched プロバイダを参照) の両方を使って、StarSuite ソフトウェアの入出力待ち時間と CPU 時間とを比較します。 #pragma D option quiet
sched:::on-cpu
/execname == "soffice.bin"/
{
self->on = vtimestamp;
}
sched:::off-cpu
/self->on/
{
@time["<on cpu>"] = sum(vtimestamp - self->on);
self->on = 0;
}
io:::wait-start
/execname == "soffice.bin"/
{
self->wait = timestamp;
}
io:::wait-done
/self->wait/
{
@io[args[2]->fi_name] = sum(timestamp - self->wait);
@time["<I/O wait>"] = sum(timestamp - self->wait);
self->wait = 0;
}
END
{
printf("Time breakdown (milliseconds):\n");
normalize(@time, 1000000);
printa(" %-50s %15@d\n", @time);
printf("\nI/O wait breakdown (milliseconds):\n");
normalize(@io, 1000000);
printa(" %-50s %15@d\n", @io);
}
StarSuite ソフトウェアのコールドスタート実行中にこのスクリプトを実行すると、次のような結果が得られます。
この出力からわかるように、StarSuite のコールドスタート時間の大部分は入出力待ち時間です。(入出力待ち時間が 13.1 秒であるのに対し、CPU 時間は 3.6 秒。)このスクリプトを StarSuite ソフトウェアのウォームスタートで実行すると、ページキャッシュによって入出力時間が短縮されることがわかります。次の出力例を参照してください。
コールドスタートの出力から、applicat.rdb ファイルが一番入出力待ち時間が長いファイルであることがわかります。これはおそらく、ファイルへの入出力回数が多いためです。このファイルに対して実行された入出力について調べるには、次の D スクリプトを使用します。 io:::start
/execname == "soffice.bin" && args[2]->fi_name == "applicat.rdb"/
{
@ = lquantize(args[2]->fi_offset != -1 ?
args[2]->fi_offset / (1000 * 1024) : -1, 0, 1000);
}
このスクリプトは、fileinfo_t 構造体の fi_offset フィールドを使って、ファイルのどの部分がアクセスされているのかをメガバイトの粒度で調べます。StarSuite ソフトウェアのコールドスタート中にこのスクリプトを実行すると、次のような出力が得られます。
この出力から、ファイルの最初の 6M バイトだけがアクセスされていることがわかります。これは、おそらく、ファイルサイズが 6M バイトであるためです。また、ファイル全体がアクセスされているわけではないこともわかります。StarSuite のコールドスタートにかかる時間を短縮したい場合は、ファイルのアクセスパターンを把握する必要があります。ファイルの必要な部分がほぼ連続しているなら、StarSuite の実行前にスカウトスレッドを実行して、あらかじめファイルへの入出力を行なっておくと、コールドスタート時間を短縮できます。この方法は、ファイルのアクセスの手段が mmap(2) である場合に特に有効です。しかしながら、この方法で短縮できるコールドスタート時間は約 1.6 秒です。これだけのために、アプリケーションをさらに複雑にし、管理負荷を増やすメリットはありません。どちらにしても、io プロバイダで収集したデータを使って、最終的に得られる利益を正確に推測できます。 安定性以下の表に、io プロバイダの安定性を DTrace の安定性機構に従って示します。安定性機構の詳細については、第 39 章安定性を参照してください。
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||