ext4のDisk quotaについて調べる機会がありましたので、備忘のためにまとめておきます。
何かお気づきのことがありましたらお気軽にコメント等いただければ幸いです。
Disk quotaとは?
Disk quotaとは、ファイルシステムの使用制限の仕組みのことです。
user, group, projectごとに、容量もしくはinode数の上限を設定することができます。
Androidでも8.1までは、アプリの暴走予防に使われていました。(参考)
ここで、projectとは「ディレクトリ群」のことを指します。
用途ごとにディレクトリをわける運用はよく行われると思いますが、
その用途ごとに対して容量制限をかけることができます。
上限にはsoftlimitとhardlimitの2種類があります。
softlimitを超えるとシステムから警告されますが、まだ書き込めます。
softlimitを超えてしばらく時間が経過するか、hardlimitを超えると書き込みがEDQUOTで失敗します。
Linux kernelにおいて、disk quotaの機能に対応しているファイルシステムは、
fs/quota/Kconfigによるとext2, ext3, ext4, jfs, ocfs2, reiserfs, gfs2, xfsがあります。(5.4.13時点)
本記事はext4を対象にしています。
また、調査したOSSのバージョンは以下のとおりです。
- Linux kernel: 5.4.13
- e2fsprogs: 1.45.5 (07-Jan-2020)
- linuxquota: 4.05
Disk quotaを体感してみる
まずはDisk quotaによる書込制限を実験してみます。
ここでは一番お手軽なUser IDに対する制限を実験します。
1. quota機能を有効にしたext4ファイルシステムを作成する
テスト用のext4ファイルシステムを作成します。
サイズは適当に40MiB (4KiB * 10,000)にしています。
このとき、mkfs.ext4
はデフォルトではDisk quota機能が無効なので、明示的に有効にする必要があります。
$ truncate -s 40M test_ext4.img
$ mkfs.ext4 -q -O quota test_ext4.img
ちゃんとquota機能が有効になっているかはdumpe2fs
コマンドで確認できます。
$ dumpe2fs -h test_ext4.img | grep '^Filesystem features'
dumpe2fs 1.45.5 (07-Jan-2020)
Filesystem features: has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg sparse_super large_file huge_file dir_nlink extra_isize quota metadata_csum
ちなみにtune2fs
コマンドを使うことで、既存のext4 imageに対してquota機能を有効にすることもできます。
$ truncate -s 40M test_ext4_2.img
$ mkfs.ext4 -q test_ext4_2.img
$ dumpe2fs -h test_ext4_2.img | grep '^Filesystem features'
dumpe2fs 1.45.5 (07-Jan-2020)
Filesystem features: has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg sparse_super large_file huge_file dir_nlink extra_isize metadata_csum
$ tune2fs -Q usrquota test_ext4_2.img
tune2fs 1.45.5 (07-Jan-2020)
$ dumpe2fs -h test_ext4_2.img | grep '^Filesystem features'
dumpe2fs 1.45.5 (07-Jan-2020)
Filesystem features: has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg sparse_super large_file huge_file dir_nlink extra_isize quota metadata_csum
2. user quotaのhardlimitをかけてみる
quotaを設定するコマンドは、edquota
とsetquota
があります。
edquota
は、エディタを起動してquotaを設定するという半GUIのような動作です。
記事には向かないので、ここでは引数で設定値を指定できるsetquota
を使います。
edquota
とsetquota
ともに、内部ではブロックデバイスに対してquotactl
システムコールを呼び出します。
quotactl
システムコールは、ブロックデバイスがmount
された状態で呼び出す必要があります。
$ mkdir -p mnt
$ sudo mount -o usrquota test_ext4.img mnt
$ sudo chmod a+w mnt/
これで準備が整いましたので、まずは今のquotaの設定を確認しましょう。
ここではquota
コマンドを使います。
$ quota --verbose --human-readable --filesystem-list mnt/
Disk quotas for user #1000 (uid 1000):
Filesystem space quota limit grace files quota limit grace
/dev/loop19 0K 0K 0K 0 0 0
まだなにも設定していないしデータを書いてもいないので、全て0となっています。
それではsetquota
コマンドを使って、UID 1000に対して容量のquota制限を設定します。
ここではsoftlimitを2MiB, hardlimitを4MiBにしてみます。
$ sudo setquota --user 1000 2048 4096 0 0 mnt
$ quota --verbose --human-readable --filesystem-list mnt/
Disk quotas for user #1000 (uid 1000):
Filesystem space quota limit grace files quota limit grace
/dev/loop19 0K 2048K 4096K 0 0 0
使用容量(space)は変わらず0K
ですが、quotaが2048K
、limitが4096K
になりました。
次に、softlimitを超える3MiBのデータを書き込んでみます。
$ dd if=/dev/zero of=mnt/1000 bs=1K count=3K
3072+0 レコード入力
3072+0 レコード出力
3145728 bytes (3.1 MB, 3.0 MiB) copied, 0.00996037 s, 316 MB/s
$ quota --verbose --human-readable --filesystem-list mnt/
Disk quotas for user #1000 (uid 1000):
Filesystem space quota limit grace files quota limit grace
/dev/loop19 3072K* 2048K 4096K 7days 1 0 0
書き込みは成功していますが、使用容量(space)に*
がつきました。
いよいよ、hardlimitを超える8MiBのデータを書き込んでみます。
$ dd if=/dev/zero of=mnt/1000 bs=1K count=8K
dd: 'mnt/1000' の書き込みエラー: ディスク使用量制限を超過しました
4097+0 レコード入力
4096+0 レコード出力
4194304 bytes (4.2 MB, 4.0 MiB) copied, 0.0121523 s, 345 MB/s
$ quota --verbose --human-readable --filesystem-list mnt/
Disk quotas for user #1000 (uid 1000):
Filesystem space quota limit grace files quota limit grace
/dev/loop19 4096K* 2048K 4096K 7days 1 0 0
$ df -h mnt
Filesystem Size Used Avail Use% Mounted on
/dev/loop19 35M 4.8M 28M 15% mnt
filesystemとしてはまだ28Mの空きがあるのに、
hardlimitとして設定した4MiBで書き込みが中断されることが確認できました。
ちなみに、quota
コマンドではカレントユーザの設定値が出力されます。
-u
オプションを指定することでUIDを指定することはできますが、
すべてのUIDに対して出力することはできません。
これはquotactl
システムコールのI/Fから来る制約で、
一覧が欲しい場合は有効な可能性のあるUIDに対して手当たりしだい呼び出す必要があります。
似たようなコマンドとして、repquota
コマンドがあります。
これは/etc/passwd
からUIDのリストを生成します。
つまり、今回の実験やAndroidのように/etc/passwd
がない環境では機能しないので注意が必要です。
3. Disk imageからquotaの状況を確認する
先程はquota
コマンドを使って、block device経由でquotaの状況を確認しました。
せっかくなので直接disk imageからもquotaの状況を確認してみます。
$ sudo umount mnt/
$ debugfs -R 'list_quota user' test_ext4.img
debugfs 1.45.5 (07-Jan-2020)
user id blocks quota limit inodes quota limit
0 13312 0 0 2 0 0
1000 4194304 2048 4096 1 0 0
単位が不揃いなので若干読みづらいですが、
UID 1000のlimitが4,096
KiBに対して使用容量(blocks)が4,194,304
byteとなっているので上限に達していることがわかります。
この方法の場合、disk imageを直接参照しているため、
quota fileに記録されているすべてのUIDの状況が参照できます。
ちなみにlist_quota
は、lq
という省略形を使うこともできます。
$ debugfs -R 'lq user' test_ext4.img
debugfs 1.45.5 (07-Jan-2020)
user id blocks quota limit inodes quota limit
0 13312 0 0 2 0 0
1000 4194304 2048 4096 1 0 0
netlinkによるquotaの監視
続いては、quotaのsoftlimit/hardlimitを超えたことを検出するための仕組みについて紹介します。
書き込んでいるプロセス自身は、hardlimitを超えたときwrite(2)
の結果としてEDQUOT
が返ります。
ただそれだとシステムの監視目的では不便です。
昔はコンソールにメッセージが出力されてたらしいのですが、
今はnetlink(7)
という仕組みを使って監視できるようになっています。
netlink(7)
とは、socketを介してuser processとkernelとで双方向通信する仕組みです。
有名所では、ip monitor
コマンドでrouting情報等の変化監視に使われていたり、
monit
コマンドでプロセスの死活監視に使われていたりします。
サンプルコード
ということで、実際にnetlinkを使ってquotaの監視を見ます。
エラー処理は省略しています。
#include <asm/types.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/netlink.h>
#include <linux/genetlink.h>
#include <linux/quota.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int
main(void)
{
struct sockaddr_nl src_addr;
// 1. PF_NETLINKのsocketを作成
int sock = socket (PF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
memset (&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
// 2. AF_NETLINKでbind
bind (sock, (struct sockaddr*)&src_addr, sizeof(src_addr));
// 3. 受信対象のgroup IDをsetsockoptで設定
int group = GENL_ID_VFS_DQUOT;
setsockopt (sock, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP,
&group, sizeof(group));
// 4. 受信用バッファを準備
struct sockaddr_nl nla;
char buf[8192];
struct iovec iov = {buf, sizeof(buf)};
struct msghdr msg = {
.msg_name = (void *) &nla,
.msg_namelen = sizeof(nla),
.msg_iov = &iov,
.msg_iovlen = 1,
};
do
{
// 5. kernelからのメッセージを受信
int len = recvmsg (sock, &msg, 0);
// 6. kernelからのメッセージをparse
for (struct nlmsghdr *nh = (struct nlmsghdr *)buf;
NLMSG_OK(nh, len);
nh = NLMSG_NEXT(nh, len))
{
struct genlmsghdr *gh = NLMSG_DATA(nh);
char * genlmsg_data = (char *) gh + GENL_HDRLEN;
struct nlattr * attr[QUOTA_NL_A_MAX+1];
attr[1] = (struct nlattr *)genlmsg_data;
for (int i = 1; i < QUOTA_NL_A_MAX; i++)
attr[i+1] = (struct nlattr *)((char *)attr[i] + attr[i]->nla_len);
printf ("--------------------------------\n");
u_int32_t qtype = *(u_int32_t*)((char *)attr[QUOTA_NL_A_QTYPE] + NLA_HDRLEN);
u_int64_t excess_id = *(u_int64_t*)((char *)attr[QUOTA_NL_A_EXCESS_ID] + NLA_HDRLEN);
u_int32_t warning = *(u_int32_t*)((char *)attr[QUOTA_NL_A_WARNING] + NLA_HDRLEN);
u_int32_t dev_major = *(u_int32_t*)((char *)attr[QUOTA_NL_A_DEV_MAJOR] + NLA_HDRLEN);
u_int32_t dev_minor = *(u_int32_t*)((char *)attr[QUOTA_NL_A_DEV_MINOR] + NLA_HDRLEN);
u_int64_t caused_id = *(u_int64_t*)((char *)attr[QUOTA_NL_A_CAUSED_ID] + NLA_HDRLEN);
printf ("QUOTA_NL_A_QTYPE = %u (%s)\n", qtype, qtype_to_str(qtype));
printf ("QUOTA_NL_A_EXCESS_ID = %lu\n", excess_id);
printf ("QUOTA_NL_A_WARNING = %u (%s)\n", warning, warning_to_str(warning));
printf ("QUOTA_NL_A_DEV_MAJOR = %u\n", dev_major);
printf ("QUOTA_NL_A_DEV_MINOR = %u\n", dev_minor);
printf ("QUOTA_NL_A_CAUSED_ID = %lu\n", caused_id);
}
}
while (1);
close (sock);
}
コメントでも記載していますが、下記の流れで受信できます。
- PF_NETLINKのsocketを作成
- AF_NETLINKでbind
- 受信対象のgroup IDをsetsockoptで設定
- 受信用バッファを準備
- kernelからのメッセージを受信
- kernelからのメッセージをparse
6でkernelから来るメッセージの形式は下記のとおりです。
型名 | サイズ[byte] | 中身 |
---|---|---|
struct nlmsghdr | NLMSG_HDRLEN (16) | nlmsg_len: messageの長さ(76) nlmsg_type: message種別(GENL_ID_VFS_DQUOT) nlmsg_flags: フラグ(0) nlmsg_seq: シーケンス番号 nlmsg_pid: 送信元pid (quota通知の場合kernelなので0) |
struct genlmsghdr | GENNL_HDRLEN (4) | cmd: command種別(QUOTA_NL_C_WARNING) version: GENL_ID_VFS_DQUOTのversion(5.4.13時点では1) |
struct nlattr | NLA_HDRLEN(4) | nla_len: attributeの長さ(8) nla_type: attribute種別(QUOTA_NL_A_QTYPE) |
u32 | 4 | quotaの種別(USRQUOTA or GRPQUOTA or PRJQUOTA) |
struct nlattr | NLA_HDRLEN(4) | nla_len: attributeの長さ(12) nla_type: attribute種別(QUOTA_NL_A_EXCESS_ID) |
u64 | 8 | 通知の要因となったUID or GID or ProjectID (QTYPEに対応したID) |
struct nlattr | NLA_HDRLEN(4) | nla_len: attributeの長さ(8) nla_type: attribute種別(QUOTA_NL_A_WARNING) |
u32 | 4 | 通知のイベント種別。以下のいずれか。 #define QUOTA_NL_IHARDWARN 1 /* Inode hardlimit reached / #define QUOTA_NL_ISOFTLONGWARN 2 / Inode grace time expired / #define QUOTA_NL_ISOFTWARN 3 / Inode softlimit reached / #define QUOTA_NL_BHARDWARN 4 / Block hardlimit reached / #define QUOTA_NL_BSOFTLONGWARN 5 / Block grace time expired / #define QUOTA_NL_BSOFTWARN 6 / Block softlimit reached / #define QUOTA_NL_IHARDBELOW 7 / Usage got below inode hardlimit / #define QUOTA_NL_ISOFTBELOW 8 / Usage got below inode softlimit / #define QUOTA_NL_BHARDBELOW 9 / Usage got below block hardlimit / #define QUOTA_NL_BSOFTBELOW 10 / Usage got below block softlimit */ |
struct nlattr | NLA_HDRLEN(4) | nla_len: attributeの長さ(8) nla_type: attribute種別(QUOTA_NL_A_DEV_MAJOR) |
u32 | 4 | 通知の要因となったブロックデバイスのメジャー番号 |
struct nlattr | NLA_HDRLEN(4) | nla_len: attributeの長さ(8) nla_type: attribute種別(QUOTA_NL_A_DEV_MINOR) |
u32 | 4 | 通知の要因となったブロックデバイスのマイナー番号 |
struct nlattr | NLA_HDRLEN(4) | nla_len: attributeの長さ(12) nla_type: attribute種別(QUOTA_NL_A_CAUSED_ID) |
u64 | 8 | 通知の要因となったファイル操作を行ったUID |
もうちょっと楽できないの?
netlinkを使ってquotaの監視をするquota_nld
というコマンドがあります。
quotaの制限を超えたら、D-busにメッセージを送ってくれます。
D-busを使っているシステムなら便利ですね。
D-bus使ってない and/or 使いたくないんだけど?
libnl
というライブラリがあります。
quota_nld
もこのライブラリを使って実装しています。
ただこのライブラリ、あまり抽象度は高くないです。
というかほぼそのままです。
メッセージ解析時のキャストなどをきれいに書くためのユーティリティ関数群ぐらいで捉えた方がいいです。
このライブラリを使う場合でも、メッセージのフォーマットと前述のサンプルコードの流れは把握しておく必要があります。
quota fileのフォーマット
filesystemにquotaの上限が設定できることは確認できました。
それではその上限値と、今使っているサイズはどこに保存されているのでしょうか?
答えはもちろんfilesystemの中、です。
metaデータとしてsuperblockに保存されてそうですが、
ext4の場合usrquota、grpquota、prjquotaがそれぞれ普通にinodeを持つファイルとして、存在します。
ただし、ファイルツリーとしては見えない、隠しinodeという扱いです。
inode番号はusrquotaには3
が、grpquotaには4
が予約されています(Special inodes
)。
prjquotaはというと、superblockにinode番号が保持されており、そこから辿れます。
prjquotaは後から追加されたため、このような構成になっているようです。
なお、usrquota/grpquotaのinode番号もsuperblockには保持されています(The Super Block)。
quotaファイルを取り出してみる
まずはquotaファイルのinode番号を確認してみましょう。
superblockの情報を見るには、dumpe2fs
コマンドが便利です。
$ dumpe2fs -h test_ext4.img | grep 'quota inode'
dumpe2fs 1.45.5 (07-Jan-2020)
User quota inode: 3
Group quota inode: 4
前述の通り、予約されたinode番号が使われていることがわかります。
次に、inode番号3
のusrquotaファイルの中身を取り出します。
普通のファイルはmount
することで参照できますが、
quotaファイルは隠しinodeなので参照できません。
ということで、debugfs
コマンドを使って取り出してみます。
$ debugfs -R 'dump <3> userquota.dump' test_ext4.img
debugfs 1.45.5 (07-Jan-2020)
$ ls -l userquota.dump
-rw-r--r-- 1 test test 7168 2月 1 00:24 userquota.dump
$ hexdump userquota.dump
0000000 1f11 d9c0 0001 0000 3a80 0009 3a80 0009
0000010 0000 0000 0007 0000 0000 0000 0005 0000
0000020 0000 0000 0000 0000 0000 0000 0000 0000
*
0000400 0002 0000 0000 0000 0000 0000 0000 0000
0000410 0000 0000 0000 0000 0000 0000 0000 0000
*
0000800 0003 0000 0000 0000 0000 0000 0000 0000
0000810 0000 0000 0000 0000 0000 0000 0000 0000
*
0000c00 0004 0000 0000 0000 0000 0000 0006 0000
0000c10 0000 0000 0000 0000 0000 0000 0000 0000
*
0001000 0005 0000 0000 0000 0000 0000 0000 0000
0001010 0000 0000 0000 0000 0000 0000 0000 0000
*
0001400 0000 0000 0000 0000 0002 0000 0000 0000
0001410 0000 0000 0000 0000 0000 0000 0000 0000
0001420 0000 0000 0000 0000 0002 0000 0000 0000
0001430 0000 0000 0000 0000 0000 0000 0000 0000
0001440 3400 0000 0000 0000 0000 0000 0000 0000
0001450 0000 0000 0000 0000 03e8 0000 9069 c947
0001460 0000 0000 0000 0000 0000 0000 0000 0000
0001470 0001 0000 0000 0000 1000 0000 0000 0000
0001480 0800 0000 0000 0000 0000 0040 0000 0000
0001490 811d 5e3d 0000 0000 0000 0000 0000 0000
00014a0 0000 0000 0000 0000 0000 0000 0000 0000
*
0001ba0 0005 0000 0000 0000 0000 0000 0000 0000
0001bb0 0000 0000 0000 0000 0000 0000 0000 0000
*
0001c00
7,168
byteのバイナリデータが取得できました。
そこはかとなく0x400 (=1,024)
単位の構造が見えますね。
quotaファイルの構造
quotaファイルは頻繁に更新・参照され、かつファイルシステムの容量を消費するという厳しい制約があるので、
参照の計算量を抑えつつ空間消費量も減らすために結構複雑な構造をしています。
一言で言えば4階層の可変長連想配列とでも言うのでしょうか。。。
なおquotaファイルの構造は仕様とか見つからなかったので、
ここで述べていることはdebugfs
のコードから読みとったものになります。
- 3種類のblock(1KiB)の集合からなる
- header block
- 必ず先頭に配置されるblock
- quotaファイルを示すマジックナンバーや構造のバージョン、blockの個数などの情報が含まれる
- inner block
- 全部で4階層ある
- 次の階層のblock番号(4byte)が256個格納される
- どの要素を参照するかは、IDによって一意に決まる。
- 例えば、IDが
0x12345678
の場合、1階層目では0x12
番目が、2階層目では0x34
番目が参照される。
- leaf block
- 最初にblockの使用状況などを保存するヘッダ(16byte)がある
- その後、IDごとのquota設定状況を保存するエントリ(72byte or 48byte)が繰り返される
- QFMT_VFS_V1の場合、容量やinode数などが64bitなので72byte x 14個
- QFMT_VFS_V0の場合、容量やinode数などが32bitなので48byte x 21個
- この配列の中で目的のIDのエントリを見つけるのは線形探索(たかだか21個)
- little endian
終わりに
いつもにも輪をかけて誰得感の強い記事になってしまいました。。。
ただあまり情報がないところなので、いつかまた調べないといけなくなった未来の自分に向けて公開しておきます。
参考
- quota - Linux kernel documentation
- The Linux Kernel Archives
- Ext4 Disk Layout - Ext4
- E2fsprogs: Ext2 Filesystem Utilities
- ext2/e2fsprogs.git - Ext2/3/4 filesystem userspace utilities
- Linux DiskQuota | SourceForge.net
- libnl - Netlink Protocol Library Suite
- netlink(7) - Linux manual page
- takeoverjp/quota_monitor: QUOTA_NL_C_WARNING sample