Linuxで利用可能なディスク容量についてkernelソースを追って確認してみた

投稿者: | 2017年4月25日

Ext4でフォーマットされているディスクを容量限界まで使い切りたいと思っています。
そこでdfとかで残容量がどれくらいか確認していたらどうもコマンドによって得られる結果が違っていてどれを信じれば良いのか分からないので色々調べたことのメモです。

具体的には、dfとtune2fsの結果のうちBlock countが異なっています。

tune2fsの結果

$sudo tune2fs -l /dev/vda1
tune2fs 1.42.13 (17-May-2015)
Filesystem volume name:   
Last mounted on:          /
Filesystem UUID:          f320ede7-aa98-411b-a3f5-8aa678aa59d4
Filesystem magic number:  0xEF53
Filesystem revision #:    1 (dynamic)
Filesystem features:      has_journal ext_attr resize_inode dir_index filetype needs_recovery extent flex_bg sparse_super large_file huge_file uninit_bg dir_nlink extra_isize
Filesystem flags:         signed_directory_hash
Default mount options:    user_xattr acl
Filesystem state:         clean
Errors behavior:          Continue
Filesystem OS type:       Linux
Inode count:              1245184
Block count:              4980480
Reserved block count:     249024
Free blocks:              4215680
Free inodes:              1115859
First block:              0
Block size:               4096
Fragment size:            4096
Reserved GDT blocks:      1022
Blocks per group:         32768
Fragments per group:      32768
Inodes per group:         8192
Inode blocks per group:   512
Flex block group size:    16
Filesystem created:       Thu Apr 13 10:19:41 2017
Last mount time:          Fri Apr 21 04:00:57 2017
Last write time:          Fri Apr 21 04:00:57 2017
Mount count:              5
Maximum mount count:      -1
Last checked:             Thu Apr 13 10:19:41 2017
Check interval:           0 ()
Lifetime writes:          7270 MB
Reserved blocks uid:      0 (user root)
Reserved blocks gid:      0 (group root)
First inode:              11
Inode size:               256
Required extra isize:     28
Desired extra isize:      28
Journal inode:            8
Default directory hash:   half_md4
Directory Hash Seed:      fc2f5507-186e-4828-b6bc-ef1d5ed91409
Journal backup:           inode blocks

注目すべき点は、Block countの値です。ここでは、4980480 となっています。

dfの結果

$ df -B4096
Filesystem     4K-blocks   Used Available Use% Mounted on
udev               61266      0     61266   0% /dev
tmpfs              12452   1766     10686  15% /run
/dev/vda1        4869551 654973   3961458  15% /
tmpfs              62258      0     62258   0% /dev/shm
tmpfs               1280      0      1280   0% /run/lock
tmpfs              62258      0     62258   0% /sys/fs/cgroup
tmpfs              12452      0     12452   0% /run/user/1000

Block sizeは4096なので、-B4096を追加して実行しています。
ここで/dev/vda1のブロック数は4869551 となっています。

dfとtune2fsの違い

それぞれ、ディスクの情報を取得するコマンドですが、block数の総和が異なっています。

tune2fsによるブロック数 4980480
dfによるブロック数 4869551

それぞれの動作の違い

結果が違うのでおそらく計算のもとにしている方法が違うと思ったので、適当にstraceでtune2fsとdfを追いかけてみると

  • tune2fsはデバイスファイルを直接読んでディスクのサイズを返していそう
  • dfはstatfsを利用してディスクのサイズを返していそう

ということが分かりました。

分からない時はカーネルを読む

とりあえず、statfsが実際に読んでいるext4_statfsの中身を読んでいけば良いはず。

  • statfsが呼ばれます (該当箇所)
  • sys_statfsが呼ばれます。マクロで自動生成されています。(該当箇所)
  • user_statfsが呼ばれます。 (該当箇所)
  • vfs_statfsが呼ばれます。(該当箇所)
  • statfs_by_dentryが呼ばれます。(該当箇所)
  • dentry->d_sb->s_op->statfsが呼ばれる。これには実際のファイルシステムに合わせた関数ポインタが入るようになっています。(参考1参考2参考3)
  • ext4_statfsが呼ばれます。(該当箇所)

という流れです。

ext4の初期化(mount_bdevとext4_fill_super)などの仕組みは今のところ強い興味があるわけではないので元気があるときの宿題にしておきます。

ext4_statfsでブロックサイズを計算している箇所

    struct super_block *sb = dentry->d_sb;
    struct ext4_sb_info *sbi = EXT4_SB(sb);
    struct ext4_super_block *es = sbi->s_es;
    ext4_fsblk_t overhead = 0, resv_blocks;
    u64 fsid;
    s64 bfree;
    resv_blocks = EXT4_C2B(sbi, atomic64_read(&sbi->s_resv_clusters));

    if (!test_opt(sb, MINIX_DF))
        overhead = sbi->s_overhead;

    buf->f_type = EXT4_SUPER_MAGIC;
    buf->f_bsize = sb->s_blocksize;
    buf->f_blocks = ext4_blocks_count(es) - EXT4_C2B(sbi, overhead);

ここで、f_blockが知りたい中身になります。
注目する行だけ抜き取ると以下の行だけとなります。

    struct super_block *sb = dentry->d_sb;
    struct ext4_sb_info *sbi = EXT4_SB(sb);
    struct ext4_super_block *es = sbi->s_es;
    ext4_fsblk_t overhead = 0;

    if (!test_opt(sb, MINIX_DF))
        overhead = sbi->s_overhead;

    buf->f_blocks = ext4_blocks_count(es) - EXT4_C2B(sbi, overhead);

ext4_blocks_countの実体としては、

static inline ext4_fsblk_t ext4_blocks_count(struct ext4_super_block *es)
{
    return ((ext4_fsblk_t)le32_to_cpu(es->s_blocks_count_hi) << 32) |
        le32_to_cpu(es->s_blocks_count_lo);
}

es(ext4のsuper block)の情報からblock数を取得してきているだけに見えます。

EXT4_C2Bの実体は、

#define EXT4_C2B(sbi, cluster)  ((cluster) << (sbi)->s_cluster_bits)

となっています。

とここまで来たところで、ext4_blocks_count(es)の値が何か、EXT4_C2B(sbi, overhead)が実際どのようになるのかが分からなかったので確認したくなってきました。

困ったときのprintk

動作のイメージをつかむために動作しているカーネル上の実際の値をprintkで出力してみることにしました。

    struct super_block *sb = dentry->d_sb;
    struct ext4_sb_info *sbi = EXT4_SB(sb);
    struct ext4_super_block *es = sbi->s_es;
    ext4_fsblk_t overhead = 0, resv_blocks;
    u64 fsid;
    s64 bfree;
    resv_blocks = EXT4_C2B(sbi, atomic64_read(&sbi->s_resv_clusters));

    if (!test_opt(sb, MINIX_DF))
        overhead = sbi->s_overhead;
    printk(KERN_ALERT "overhead: %d", overhead);
    printk(KERN_ALERT "ext4_blocks_count(es): %d", ext4_blocks_count(es));
    printk(KERN_ALERT "(sbi)->s_cluster_bits: %d", (sbi)->s_cluster_bits);

こんな感じに変更してカーネルコンパイルして、そのカーネルで起動してみます。

実行結果

dfを実行してsyslogの中身を見てみます。すると以下のようなログが残りました。

Apr 25 12:27:43 packer-qemu kernel: [   83.645888] overhead: 110929
Apr 25 12:27:43 packer-qemu kernel: [   83.646111] ext4_blocks_count(es): 4980480
Apr 25 12:27:45 packer-qemu kernel: [   83.646384] (sbi)->s_cluster_bits: 0

ここで、興味深いのはext4_blocks_count(es)の結果です。
この値は、tune2fs -lのBlock countと一致します。

$ sudo tune2fs -l /dev/vda1 |grep "Block count"
Block count:              4980480

この結果から、

  • ext4_statfsのブロックサイズの結果がtune2fsの結果と異なるのは、EXT4_C2B(sbi, overhead)の値の影響
  • (sbi)->s_cluster_bitsの値も0なので、EXT4_C2Bの演算によってビットシフトは実行されていない
  • EXT4_C2Bの結果はoverheadの値そのもの

ということが分かります。

ここでoverheadの値が変わる条件としては、

    if (!test_opt(sb, MINIX_DF))
        overhead = sbi->s_overhead;

この行が実行される場合のみということになります。

s_overheadはext4_calculate_overheadで計算が行われています。ざっくり見ると、各グループごとのoverheadを計算し足し合わせて、ジャーナルがある場合はそのoverheadも追加している処理に見えます。

MINIX_DF って何

mountのmanが引っかかりました。

システムコール statfs の振る舞いを設定する。 minixdf を指定すると、返り値の f_blocks フィールドにファイルシステムの全ブロック数が入るようになり、 bsddf を指定すると、ext2 ファイルシステムによって利用されていて、 ファイルの保存領域としては使えないブロックの分を引いた値が入る。 デフォルトは bsddf。

いままで読んできたコード内容と一致しますね!
というかマウントオプションでstatfsの結果が変わるなんて知りませんでしたよ!

結論

このあたりで、自分的には調査として十分な結論が得られました。

  • mountのオプション(minixdf/bsddf)でstatfsの返すブロックするに変化が発生する
  • tune2fsはext4として確保しているblock数を返している
  • dfはstatfsを呼び出しマウントオプションに従った動作でext4として確保しているblock数からoverheadを引いたもの、もしくはext4として確保しているblock数を返している

これらの事実から、「通常の環境ではdf(bsddfのstatfs)結果のブロック数を基準としてデータが書き込めるかどうかを判断すればよい」という運用に決定できそうです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です