やりたいこと
ストレージにデータをダラダラと書き込むプロセスとその書き込み先を見つけたい。
いまのところ一番良いと思われる方法
/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
他の方法案
案1.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
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));
案2. lsof
+ awk
をポーリング
-
良い点
-
lsof
とawk
さえ動けば可能。(awk
はフィルタだけなので頑張ればgrep
でも可)
-
-
ダメな点
- ポーリングのインターバルの間にopen/write/closeする場合検出できない。
- ファイルサイズの変化しか見れていないので、writeせずにファイルを作ったり消したりする人や、mmapして汚す人や、メタ情報だけ更新する人などを検出することはできない。
もともとこの方法を使うつもりだったが、検出できないケースが結構あったので却下。
下記のようにlsof
コマンドの出力を加工することで、ストレージに書き込み属性で開かれているファイルの一覧が取得できる。
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"は監視対象のブロックデバイスのメジャー番号とマイナー番号を指定する。
あとは、これを定期的に動かして差分を見れば、書き込んでるプロセスと書き込み先がわかる。
#!/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バイト何か書いてることがわかる。
$ ./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]/
: 書き込み属性で開かれているファイルのみ出力。ちなみに第4要素はFDで、fd値の後ろにアクセスモードを示す文字とロックモードを示す文字が続く。アクセスモードがw (write only)
とu (read write)
のみ取り出している。 -
$5 == "REG"
: regular fileのみ出力。なお第5要素はTYPE。 -
$6 == "8,1"
: 第6要素はDEVICE。調査対象のブロックデバイスのメジャー番号、マイナー番号を指定する。環境によって調整が必要。lsblk
で対象のパーティションのメジャー番号、マイナー番号がわかる。lsblk
がなければls -l
でも可。 -
$9 !~ /^\/(memfd|tmp|run|proc|dev\/shm)/'
: 第9要素はNAME。tmpfsを除外している。代表的なパスを入れたつもりだが、環境によって調整が必要。
-
ちなみに、lsof
には外部プロセスで結果をパースしやすいように出力する-F
オプションが用意されている。
ただ、これ余計パースしにくくなると思うのは自分だけだろうか。。。(実際今回は使っていない)
パースしにくい一番の理由は、せっかく元が行志向のデータなのに、-F
オプションをつけるとそうではなくなることだ。
いっそjsonやxmlのような汎用的なフォーマットで出力されるならまだわかるのだが。。。
気づいてないだけで良いパース方法があるのか?
案3. pidstat -d
+ strace
- 良い点
- わかりやすい
- 起動からの総書き込み量が確認できる
- ダメな点
- プロセスの生存期間が短いと検出できない
-
pidstat
がない環境が結構ある。仮にあっても-d
オプションは/proc/PID/io
に依存しているので、CONFIG_TASK_IO_ACCOUNTING
が有効になっていなければならない。 -
pidstat -d
だけではパーティションがわからない。(strace
まですればわかるが)
案4.systemtapでvfs_writeをprobeする
- 良い点
- ある程度集計してから出力するなど、フォーマットの調整が自在
- ダメな点
- 最初からsystemtapが使えればいいが、カーネルコンフィグの変更が必要な場合が多い
その他
perf
やsar
ではイベント回数や流量はわかっても、プロセス・ファイルごとのプロファイルまでは厳しいのかなと。
よりいい方法があればぜひ教えてください。
まとめ
eMMCなどのフラッシュストレージを使った組込システムでは、
製品寿命を実現するためにストレージへの書き込み回数・量を意識する必要がある。
プログラマのためのフラッシュメモリ入門などにあるように、
下回りやストレージ側も頑張ってくれているが、
もちろんアプリケーションが不要な書き込みをしないことが大前提。
できるだけ簡単にお行儀悪いアプリがいないか確認できるようにしておきたい。
参考
/proc//io
/proc/sys/vm/block-dump
lsof(8) - Linux man page
プログラマのためのフラッシュメモリ入門
テキスト処理にたまに便利なAWK入門