ストレージにデータをダラダラと書き込むプロセスの見つけ方

# やりたいこと
ストレージにデータをダラダラと書き込むプロセスとその書き込み先を見つけたい。

# いまのところ一番良いと思われる方法
`/proc/sys/vm/block_dump`+`strace`

```
# echo 1 > /proc/sys/vm/block_dump 
(テストシーケンス)
# dmesg | grep "WRITE block"
[ 1347.340922] qemu-system-aar(5614): WRITE block 405079400 on sda1 (8 sectors)
[ 1347.341256] jbd2/sda1-8(234): WRITE block 470207336 on sda1 (8 sectors)
...
(jbd2はジャーナルファイルの書き出し)

$ strace -p 5614
strace: Process 5614 attached
restart_syscall(<... resuming interrupted futex ...>) = 0
pwrite64(12, "\300;9\230\0\0\0\2\0\0\0\302\0\0\0\0\223\323[)\0\0\0\0\0\0\0\0\0\0\0\0"..., 1024, 17537024) = 1024

$ ls -l /proc/5614/fd/12
lrwx------ 1 user user 64  8月 21 00:10 /proc/5614/fd/12 -> /home/user/buildroot-2019.02.4/output/images/rootfs.ext2
```

# 他の方法案

## 案１．Androidの仕組みを活用

Androidでは、このようなケースのためにftraceのeventを追加している。
"android_fs_datawrite_start"をtraceすれば一発で犯人がわかる。
ftraceだとkernel bootargを指定することで起動直後から解析できるなど、とても使い勝手がいい。
androidでないlinuxだったとしても、ポーティングして使ってももいいかもしれない。

```
echo 1 > /d/tracing/events/android_fs/android_fs_datawrite_start/enable
echo 1 > /d/tracing/tracing_on
cat /d/tracing/trace
```

```c:include/trace/events/android_fs.h
DEFINE_EVENT(android_fs_data_start_template, android_fs_dataread_start,
	TP_PROTO(struct inode *inode, loff_t offset, int bytes,
		 pid_t pid, char *pathname, char *command),
	TP_ARGS(inode, offset, bytes, pid, pathname, command));
```

[android_fs.h](https://android.googlesource.com/kernel/goldfish/+/android-3.18/include/trace/events/android_fs.h?autodive=0%2F%2F%2F)

## 案２. `lsof` + `awk`をポーリング

- 良い点
    - `lsof`と`awk`さえ動けば可能。(`awk`はフィルタだけなので頑張れば`grep`でも可)

- ダメな点
    - ポーリングのインターバルの間にopen/write/closeする場合検出できない。
    - ファイルサイズの変化しか見れていないので、writeせずにファイルを作ったり消したりする人や、mmapして汚す人や、メタ情報だけ更新する人などを検出することはできない。

もともとこの方法を使うつもりだったが、検出できないケースが結構あったので却下。

下記のように`lsof`コマンドの出力を加工することで、ストレージに書き込み属性で開かれているファイルの一覧が取得できる。


```shell
function lsof_writing() {
    lsof +c 0 -d "0-$(ulimit -n)" -c '^lsof' |
        awk 'NR == 1 \
          || $4 ~ /[0-9]+[wu]/ \
          && $5 == "REG" \
          && $6 == "8,1" \
          && $9 !~ /^\/(memfd|tmp|run|proc|dev\/shm)/'
}
```

なお詳細は後述するが、6行目の"8,1"は監視対象のブロックデバイスのメジャー番号とマイナー番号を指定する。
あとは、これを定期的に動かして差分を見れば、書き込んでるプロセスと書き込み先がわかる。

```shell:watch_lsof.sh
#!/bin/bash

function lsof_writing() {
    lsof +c 0 -d "0-$(ulimit -n)" -c '^lsof' |
        awk 'NR == 1 \
          || $4 ~ /[0-9]+[wu]/ \
          && $5 == "REG" \
          && $6 == "8,1" \
          && $9 !~ /^\/(memfd|tmp|run|proc|dev\/shm)/'
}

NEW=$(lsof_writing)
while true; do
      sleep 2
      OLD="$NEW"
      NEW="$(lsof_writing)"
      diff -u <(echo "$OLD") <(echo "$NEW")
done
```


実行例は下記の通り。
`chromium-browser`プロセスが7バイト何か書いてることがわかる。

```shell-session
$ ./watch_lsof.sh 
--- /dev/fd/63	2019-08-19 01:21:28.785795784 +0900
+++ /dev/fd/62	2019-08-19 01:21:28.785795784 +0900
@@ -179,7 +179,7 @@
 chromium-browse 29987 user  277w      REG                8,1  3898089     186392 /home/user/.config/chromium/Default/Extension State/003194.log
 chromium-browse 29987 user  281u      REG                8,1     7168     238130 /home/user/.config/chromium/Default/databases/Databases.db
 chromium-browse 29987 user  282ur     REG                8,1    17408     238134 /home/user/.config/chromium/Default/QuotaManager
-chromium-browse 29987 user  289w      REG                8,1   487243     158273 /home/user/.config/chromium/Default/Current Session
+chromium-browse 29987 user  289w      REG                8,1   487250     158273 /home/user/.config/chromium/Default/Current Session
 chromium-browse 29987 user  292u      REG                8,1        0     237883 /home/user/.config/chromium/Default/Web Data-journal
 chromium-browse 29987 user  331w      REG                8,1      288     135767 /home/user/.config/chromium/Default/File System/001/p/Paths/LOG
 chromium-browse 29987 user  332uW     REG                8,1        0     243892 /home/user/.config/chromium/Default/File System/001/p/Paths/LOCK
```

備忘のために簡単に内容を書いておくと、

- `lsof`のオプション
    - `+c 0` : コマンド名の出力数をデフォルトの9文字から大きくする。
    - `-d "0-$(ulimit -n)"` : fdが数値になっているものに限定して出力する。デフォルトでは、memory-mapped fileなど通常のファイル以外も出力されるため。fdの値の上限は`ulimit`から取得しているが、十分大きければ何でも良い(上限なし指定はできないっぽい)。
    - `-c '^lsof'` : `lsof`自身のfdはノイズなので除去。
- `awk`のパターン : `lsof`のオプションでフィルタしきれないものは`awk`の力を借りた。
    - `NR == 1` : 一応わかりやすいようにヘッダを残す。
    - `$4 ~ /[0-9]+[wu]/` : 書き込み属性で開かれているファイルのみ出力。ちなみに第４要素はFDで、fd値の後ろにアクセスモードを示す文字とロックモードを示す文字が続く。アクセスモードが`w (write only)`と`u (read write)`のみ取り出している。
    - `$5 == "REG"` : regular fileのみ出力。なお第５要素はTYPE。
    - `$6 == "8,1"` : 第６要素はDEVICE。調査対象のブロックデバイスのメジャー番号、マイナー番号を指定する。環境によって調整が必要。`lsblk`で対象のパーティションのメジャー番号、マイナー番号がわかる。`lsblk`がなければ`ls -l`でも可。
    - `$9 !~ /^\/(memfd|tmp|run|proc|dev\/shm)/'` : 第９要素はNAME。tmpfsを除外している。代表的なパスを入れたつもりだが、環境によって調整が必要。

ちなみに、`lsof`には外部プロセスで結果をパースしやすいように出力する`-F`オプションが用意されている。
ただ、これ余計パースしにくくなると思うのは自分だけだろうか。。。（実際今回は使っていない）
パースしにくい一番の理由は、せっかく元が行志向のデータなのに、`-F`オプションをつけるとそうではなくなることだ。
いっそjsonやxmlのような汎用的なフォーマットで出力されるならまだわかるのだが。。。
気づいてないだけで良いパース方法があるのか？

## 案３. `pidstat -d` + `strace`
- 良い点
    - わかりやすい
    - 起動からの総書き込み量が確認できる
- ダメな点
    - プロセスの生存期間が短いと検出できない
    - `pidstat`がない環境が結構ある。仮にあっても`-d`オプションは`/proc/PID/io`に依存しているので、`CONFIG_TASK_IO_ACCOUNTING`が有効になっていなければならない。
    - `pidstat -d`だけではパーティションがわからない。(`strace`まですればわかるが）

## 案４．systemtapでvfs_writeをprobeする
- 良い点
    - ある程度集計してから出力するなど、フォーマットの調整が自在
- ダメな点
    - 最初からsystemtapが使えればいいが、カーネルコンフィグの変更が必要な場合が多い

## その他
`perf`や`sar`ではイベント回数や流量はわかっても、プロセス・ファイルごとのプロファイルまでは厳しいのかなと。
よりいい方法があればぜひ教えてください。

# まとめ
eMMCなどのフラッシュストレージを使った組込システムでは、
製品寿命を実現するためにストレージへの書き込み回数・量を意識する必要がある。
[プログラマのためのフラッシュメモリ入門](https://qiita.com/rarul/items/40da2248dd99a18e1809)などにあるように、
下回りやストレージ側も頑張ってくれているが、
もちろんアプリケーションが不要な書き込みをしないことが大前提。
できるだけ簡単にお行儀悪いアプリがいないか確認できるようにしておきたい。

# 参考
[/proc/<pid>/io](https://www.kernel.org/doc/Documentation/filesystems/proc.txt)
[/proc/sys/vm/block-dump](https://www.kernel.org/doc/html/latest/admin-guide/sysctl/vm.html#block-dump)
[lsof(8) - Linux man page](https://linux.die.net/man/8/lsof)
[プログラマのためのフラッシュメモリ入門](https://qiita.com/rarul/items/40da2248dd99a18e1809)
[テキスト処理にたまに便利なAWK入門](https://qiita.com/muiscript/items/02ddff566231bdb04a75)