プロセス毎のIO統計 /proc/<pid>/io は、プロセス終了時、親プロセスに引き継がれる

# 要約
プロセス毎のIO統計情報である`/proc/<pid>/io`について、該当プロセスが終了したときの振る舞いを調べたときのメモ。

結論としてはタイトルの通り、
`/proc/<pid>/io`に表示される入出力量は、プロセス終了時親プロセスに引き継がれる。

その結果、`system("cp hoge fuga")`のように単発でIOが発生するプロセスを起動した場合でも、きちんと呼び出し元にIO統計情報が計上される。

一方で`init`や`shell`、androidにおける`zygote`などのランチャー系プロセスで多くのioが発生したことになっている場合、子供の罪過を背負っているだけの可能性があるため、注意が必要となる。

その場合、[こちら](https://qiita.com/takeoverjp/items/7d5d74858220d1dff46d)に書いたように`/proc/sys/vm/block_dump`、`systemtap`、`ftrace`等のイベント記録方式のツールで真犯人を突き止める必要がある。

# 詳細
## `/proc/<pid>/io`とは？

```shell-session
$ 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`に計上されているかどうかを確認すればよい。

```shell-session
$ 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](https://elixir.bootlin.com/linux/v5.2.9/source)を参照している。

1. 子プロセスが終了→ZONBIE化
2. 親プロセスが`wait(2)`システムコールを呼ぶ
3. ZONBIE化した子プロセスのリソースを回収するために、`wait_task_zombie()`を呼ぶ
4. `task_io_accounting_add()`で`psig->acio`(current(親)のio情報)に`p->acio`(p(子)のio情報)を足す

```c:kernel/exit.c
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(&current->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(&current->sighand->siglock);
	}

...

	if (state == EXIT_DEAD)
		release_task(p);

...

	return pid;
}
```

IO以外のリソース消費情報も、ここで親プロセスにマージしていることがわかる。


```c:include/linux/task_io_accounting_ops.h
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に移植](https://forum.xda-developers.com/android/software-hacking/script-iotop-android-t2910428
)してくれてる人がいた。ありがたし。

# 参考
[proc filesystem](https://www.kernel.org/doc/Documentation/filesystems/proc.txt)
[sysstat (pidstat\)](http://sebastien.godard.pagesperso-orange.fr/)
[iotop](http://guichaz.free.fr/iotop/)
[dstat](http://dag.wiee.rs/home-made/dstat/)