inotifyインスタンスが監視している数がmax_user_watchesを越えた時に`no space left on device`エラーとなる様子を観察したり原因を調べたりする

前回、inotifyで監視しているオブジェクトの数を調べることができそうだ、と言うことがわかった。次は、そもそもなぜno space left on deviceになるのかを調べたい。なお、調査に使ったカーネルはv4.15である。

まずは監視オブジェクトの数がmax_user_watchesを超えるとno space left on deviceになることを確認したい。max_user_watchesの数を小さくして、前回使った検証用プログラムを実行すれば確認できそうである。

echo 16 >  /proc/sys/fs/inotify/max_user_watches

max_user_watchesを16にセットした。17個目のファイルを監視しようとするとエラーとなる、と仮説を立てたのだが、そうはならなかった。

ubuntu@alpaca-dev:~$ cat  /proc/sys/fs/inotify/max_user_watches
16
ubuntu@alpaca-dev:~$ ./main
start watching /tmp/foo_1.txt
start watching /tmp/foo_2.txt
start watching /tmp/foo_3.txt
start watching /tmp/foo_4.txt
start watching /tmp/foo_5.txt
start watching /tmp/foo_6.txt
start watching /tmp/foo_7.txt
start watching /tmp/foo_8.txt
start watching /tmp/foo_9.txt
start watching /tmp/foo_10.txt
start watching /tmp/foo_11.txt
start watching /tmp/foo_12.txt
2019/11/25 14:26:18 no space left on device

max_user_watchesの数を何回か変えてみたのだが、max_user_watches - 4 個までしか監視できず、その後エラーとなっている。

ちょっと謎なので、コードを追いかけていく。no space left on deviceとなるのは inotify_add_watchを実行したタイミングなので、この関数を読んでいく(link)。返り値の取得はinotify_update_watchで行なっているので、そこをみる。

	/* create/update an inode mark */
	ret = inotify_update_watch(group, inode, mask);
	path_put(&path);
fput_and_out:
	fdput(f);
	return ret;
}

inotify_update_watch(link)の返り値の取得はinotify_new_watchで行なっている(既に監視中でない場合)。

	/* try to update and existing watch with the new arg */
	ret = inotify_update_existing_watch(group, inode, arg);
	/* no mark present, try to add a new one */
	if (ret == -ENOENT)
		ret = inotify_new_watch(group, inode, arg);
	mutex_unlock(&group->mark_mutex);

	return ret;
}

inotify_new_watchを読むと、ENOSPCを返り値にセットしている箇所がある。これにより、no space left on deviceとなるケースがあることがわかる。

	/* increment the number of watches the user has */
	if (!inc_inotify_watches(group->inotify_data.ucounts)) {
		inotify_remove_from_idr(group, tmp_i_mark);
		ret = -ENOSPC;
		goto out_err;
	}

inc_inotify_watchesはinc_ucountを呼んでいる。

static inline struct ucounts *inc_inotify_watches(struct ucounts *ucounts)
{
	return inc_ucount(ucounts->ns, ucounts->uid, UCOUNT_INOTIFY_WATCHES);
}

ucountってなんだ…と思いつつ、さらに追いかける。ENOSPCを返すのはinc_ucountがNULLを返している時で、それはatomic_inc_belowの結果による。

struct ucounts *inc_ucount(struct user_namespace *ns, kuid_t uid,
			   enum ucount_type type)
{
	struct ucounts *ucounts, *iter, *bad;
	struct user_namespace *tns;
	ucounts = get_ucounts(ns, uid);
	for (iter = ucounts; iter; iter = tns->ucounts) {
		int max;
		tns = iter->ns;
		max = READ_ONCE(tns->ucount_max[type]);
		if (!atomic_inc_below(&iter->ucount[type], max))
			goto fail;
	}
	return ucounts;
fail:
	bad = iter;
	for (iter = ucounts; iter != bad; iter = iter->ns->ucounts)
		atomic_dec(&iter->ucount[type]);

	put_ucounts(ucounts);
	return NULL;
}

この関数内で使われているucountはuser_namespaceとkuid_tから取得(ない場合は作成)したもの。inotify_new_groupをみるとcurrent_user_ns()current_euidで作成していることがわかる。つまり、名前空間とEUID毎に監視可能なオブジェクトのサイズがmax_user_watchesで決められているのではないか、と考えられる。

inotify_watchers.shを使うとプロセスとそれが監視しているオブジェクト数をみることができる。リンク先のスクリプトはrootユーザの物をみているので以下のように変更し、実行してみると検証用プログラムの他にもファイル監視をしているプログラムがあり(VSCodeなど)、その監視対象オブジェクトの数が4個だった。

set -o errexit
set -o pipefail
lsof +c 0 -n -P -u ubuntu \
        | awk '/inotify$/ { gsub(/[urw]$/,"",$4); print $1" "$2" "$4 }' \
        | while read name pid fd; do \
                exe="$(readlink -f /proc/$pid/exe || echo n/a)"; \
                fdinfo="/proc/$pid/fdinfo/$fd" ; \
                count="$(grep -c inotify "$fdinfo" || true)"; \
                echo "$name $exe $pid $fdinfo $count"; \
        done

さらに、ファイル監視をしているプロセスを止めたところ、検証用プログラムが監視しているオブジェクト数と他のプロセスが監視しているプロセスの合計がmax_user_watchesを越えた瞬間no space left on deviceとなったため、少なくとも同じユーザで実行していればinotifyの監視カウントは共有されているようだ、ということがわかる。

名前空間云々はまだわかっていないが、ある程度このエラーになる理屈が理解でき、inotifyと仲良くなれた。