要約
プロセス毎のIO統計情報である/proc/<pid>/io
について、該当プロセスが終了したときの振る舞いを調べたときのメモ。
結論としてはタイトルの通り、
/proc/<pid>/io
に表示される入出力量は、プロセス終了時親プロセスに引き継がれる。
その結果、system("cp hoge fuga")
のように単発でIOが発生するプロセスを起動した場合でも、きちんと呼び出し元にIO統計情報が計上される。
一方でinit
やshell
、androidにおけるzygote
などのランチャー系プロセスで多くのioが発生したことになっている場合、子供の罪過を背負っているだけの可能性があるため、注意が必要となる。
その場合、こちらに書いたように/proc/sys/vm/block_dump
、systemtap
、ftrace
等のイベント記録方式のツールで真犯人を突き止める必要がある。
詳細
/proc/<pid>/io
とは?
$ cat /proc/1/io
rchar: 105055437
wchar: 155459325
syscr: 75214
syscw: 13748
read_bytes: 398270464
write_bytes: 57344
cancelled_write_bytes: 8192
- 各プロセスの起動からの入出力サマリ
-
CONFIG_TASK_IO_ACCOUNTING
が必要 - 単純な
read
/write
システムコールの総量だけでなく、bio_submit
された総量、つまりブロックデバイスに限定した入出力量もわかる - デバイス・パーティションごとの入出力量はわからない
-
pidstat -d
、iotop
、dstat --top-bio
などで参照されている
本題:プロセス終了時どうふるまうか?
考えられるふるまいとしては、以下ぐらいか。
1. 親プロセスに計上される
2. どのプロセスにも引き継がれず、情報が消えてなくなる
3. どのプロセスにも引き継がれず、別の場所に記録される
実験
実験はとても簡単。
shell
からIOが発生するプロセスを起動し、そのIOがshell
に計上されているかどうかを確認すればよい。
$ cat /proc/$$/io
rchar: 142110
wchar: 923
syscr: 76
syscw: 35
read_bytes: 0
write_bytes: 0
cancelled_write_bytes: 0
$ dd if=/dev/zero of=test bs=1024 count=1024
1024+0 レコード入力
1024+0 レコード出力
1048576 bytes (1.0 MB, 1.0 MiB) copied, 0.0062553 s, 168 MB/s
$ cat /proc/$$/io
rchar: 1197869
wchar: 1049982
syscr: 1168
syscw: 1115
read_bytes: 0
write_bytes: 1048576
cancelled_write_bytes: 0
shell
プロセスからフォークしたdd
プロセスのwrite量(1MiB=1,048,576)が、
shell
プロセスに計上されていることがわかる。
ちなみに、普通の環境で上記実験をすると、bashがhistoryファイルに書き込みを行うため、
多分1MiBぴったりより少し多くなると思う。
上記実験時は、HISTFILEをtmpfs上のファイルに変更することで誤差が現れるのを防いでいる。
実装
ふるまいは確認できたので、次に実装を確認する。
以下、ソースコードはカーネルバージョン 5.2.9を参照している。
- 子プロセスが終了→ZONBIE化
- 親プロセスが
wait(2)
システムコールを呼ぶ - ZONBIE化した子プロセスのリソースを回収するために、
wait_task_zombie()
を呼ぶ -
task_io_accounting_add()
でpsig->acio
(current(親)のio情報)にp->acio
(p(子)のio情報)を足す
static int wait_task_zombie(struct wait_opts *wo, struct task_struct *p)
{
...
if (state == EXIT_DEAD && thread_group_leader(p)) {
...
thread_group_cputime_adjusted(p, &tgutime, &tgstime);
spin_lock_irq(¤t->sighand->siglock);
write_seqlock(&psig->stats_lock);
psig->cutime += tgutime + sig->cutime;
psig->cstime += tgstime + sig->cstime;
psig->cgtime += task_gtime(p) + sig->gtime + sig->cgtime;
psig->cmin_flt +=
p->min_flt + sig->min_flt + sig->cmin_flt;
psig->cmaj_flt +=
p->maj_flt + sig->maj_flt + sig->cmaj_flt;
psig->cnvcsw +=
p->nvcsw + sig->nvcsw + sig->cnvcsw;
psig->cnivcsw +=
p->nivcsw + sig->nivcsw + sig->cnivcsw;
psig->cinblock +=
task_io_get_inblock(p) +
sig->inblock + sig->cinblock;
psig->coublock +=
task_io_get_oublock(p) +
sig->oublock + sig->coublock;
maxrss = max(sig->maxrss, sig->cmaxrss);
if (psig->cmaxrss < maxrss)
psig->cmaxrss = maxrss;
task_io_accounting_add(&psig->ioac, &p->ioac);
task_io_accounting_add(&psig->ioac, &sig->ioac);
write_sequnlock(&psig->stats_lock);
spin_unlock_irq(¤t->sighand->siglock);
}
...
if (state == EXIT_DEAD)
release_task(p);
...
return pid;
}
IO以外のリソース消費情報も、ここで親プロセスにマージしていることがわかる。
static inline void task_blk_io_accounting_add(struct task_io_accounting *dst,
struct task_io_accounting *src)
{
dst->read_bytes += src->read_bytes;
dst->write_bytes += src->write_bytes;
dst->cancelled_write_bytes += src->cancelled_write_bytes;
}
static inline void task_chr_io_accounting_add(struct task_io_accounting *dst,
struct task_io_accounting *src)
{
dst->rchar += src->rchar;
dst->wchar += src->wchar;
dst->syscr += src->syscr;
dst->syscw += src->syscw;
}
static inline void task_io_accounting_add(struct task_io_accounting *dst,
struct task_io_accounting *src)
{
task_chr_io_accounting_add(dst, src);
task_blk_io_accounting_add(dst, src);
}
まとめ
pidstat
等のコマンドになって綺麗に出力されると鵜呑みにしてしまいがちだが、
正しく情報を解釈するためには、実装を意識する必要があることを再確認した。
この手の落とし穴は特にパフォーマンス計測ツールでよくハマる気がする。
おまけ
iotop
もdstat
もpythonなんだなぁ。
組込みでもマジメにパフォーマンス見るためには、まず最初にpython実行環境をクロスコンパイルするべきなのかも。
ちなみにiotop
はAndroid用にshellに移植してくれてる人がいた。ありがたし。