背景
前回の記事で、resize2fsコマンドがどのように1秒未満での容量拡張を実現しているかを知るために、resize2fsコマンドのソースを調査しました。その結果、メタデータの一つであるGlobal Descriptor Tables(GDT)をカーネル内で更新しているからではないか、という示唆を得られました。今回は、実際にカーネルのコードを読んで、この示唆が正しかったことを見ていきたいと思います。
調査対象
今回も新しめのカーネルで調査しました。Amazon Linuxとは多少ソースが異なっているかもしれませんが、本質は大きく変わらないかと思います。
- ファイルシステム: ext4
- Linux kernel version: 4.1.0-rc7
前回の復習
前回調べたときから約1年空いてしまいましたので、軽く前回の復習をします。
ext4のファイルシステムでは、ブロックグループという単位で複数のブロックをグループ化してデータを管理しています。ブロックグループの中にはGroup Descriptor Tables(GDT)というブロック領域があり、ここに全ブロックグループを管理するための情報であるグループディスクリプタを格納しています。全体のブロックグループ数が多ければ多いほど、GDT領域が大きくなります。前回、resize2fsコマンド経由でGDT領域を更新しているのではないか、という示唆を得ました。
![ext4_bg]()
GDT領域で扱うサイズより大きなファイルサイズにオンラインで拡張したいときはどうすれば良いのでしょう?ext4ではこういうときのために、予約済のGDT領域(RGDT)をブロックグループの中に用意しています。オンラインでファイルシステムがリサイズ出来るのは、このRGDTを備えているからと言えます。実際、resize2fsコマンド内ではRGDTで管理しきれるファイルサイズにしようとしているかをチェックしていました。
resize2fsコマンドでは、カーネルにファイルシステムのサイズを変更させるために、EXT4_IOC_RESIZE_FSリクエストのioctl(2)を発行していました。今回の記事ではこのioctl(2)の発行後に、カーネルは何をやっているのかを調べていきます。
リサイズリクエストのioctl(2)の処理内容
ioctl(2)のEXT4_IOC_RESIZE_FSリクエストに関するソースコードから読んでいきたいと思います。繰り返しますが、今回のゴールは容量拡張のメイン処理が何であるかを明らかにすることです。
まずEXT4_IOC_RESIZE_FSでgrepしてみると・・・fs/ext4/ioctl.cにありました。ここから読み進めてみましょう。
554 case EXT4_IOC_RESIZE_FS: {
555 ext4_fsblk_t n_blocks_count;
556 int err = 0, err2 = 0;
557 ext4_group_t o_group = EXT4_SB(sb)->s_groups_count;
558
559 if (EXT4_HAS_RO_COMPAT_FEATURE(sb,
560 EXT4_FEATURE_RO_COMPAT_BIGALLOC)) {
561 ext4_msg(sb, KERN_ERR,
562 "Online resizing not (yet) supported with bigalloc");
563 return -EOPNOTSUPP;
564 }
前回、resize2fsでは-fを付ければbigalloc(ページサイズ以上のブロック単位をサポートする機能)でもresizeできるかのように見えましたが、最新バージョンのカーネルでもサポートされていないようです。not (yet)なので、今後に期待しましょう。
566 if (copy_from_user(&n_blocks_count, (__u64 __user *)arg,
567 sizeof(__u64))) {
568 return -EFAULT;
569 }
570
571 err = ext4_resize_begin(sb);
572 if (err)
573 return err;
ext4_resize_begin()では、現在リサイズしようとしているファイルシステムに対して同時にリサイズ命令しないように、メモリ内のスーパーブロック情報(ext4_sb_infoオブジェクト)にこのファイルシステムはresize中であるというフラグ(EXT4_RESIZE)を立てるという処理をします。
575 err = mnt_want_write_file(filp);
576 if (err)
577 goto resizefs_out;
mnt_want_write_file()では、通常パスでは2つの関数が呼ばれます。
1つ目はsb_start_write()です。この関数は他のプロセスによる書き込みを排除するために呼ばれます。具体的には、ファイルシステムを書き込みfreeze状態(SB_FREEZE_WRITE)とします。freeze状態とは、ファイルシステム用に用意されたロック機構の1つであり、一番緩いロック(=ある程度の競合を許すロック)です。freeze状態にはいくつかレベルが用意されています。レベルが低い順にフローズしていない状態(SB_UNFROZEN)、書き込みfreeze状態(SB_FREEZE_WRITE)、ページフォルトのfreeze状態(SB_FREEZE_PAGEFAULT)、内部のファイルシステム使用freeze状態(SB_FREEZE_FS)、完全なfreeze状態(SB_FREEZE_COMPLETE)です。sb_start_write()を使用する場合、freeze状態が解けるまでsleepして待ち続けます。
2つ目は__mnt_want_write_file()です。この関数はそのファイルシステムへwriteアクセスすることを記録するために呼ばれます。ただ、もしread onlyでファイルシステムがマウントされている場合はエラー(EROFS)となります。エラーとなったときは、書き込みfreeze状態を解除します。
579 err = ext4_resize_fs(sb, n_blocks_count);
いよいよ、今回のメインの関数である、ext4_resize_fs()について見ていきましょう。まず渡している引数を見ていきます。第一引数のsbとはresizeされるファイルシステムのsuperblockの構造体(super_block)です。superblockとは、ファイルシステム全体を管理しているブロックのことです。このsuper_block構造体は全てのファイルシステムで使われる一般的な形式となっており、ファイルシステム特有の情報(private情報)はs_fs_infoにvoid型のポインタとして格納されています。ext4_resize_fs()の第二引数n_block_countは、resize2fsコマンドから渡って来た追加分を含む新しいブロック数です。
ext4_resize_fs()
それでは実際に、ext4_resize_fs()で重要な処理に着目して、何をやっているか理解しましょう。
1896 o_blocks_count = ext4_blocks_count(es);
1897
1898 ext4_msg(sb, KERN_INFO, "resizing filesystem from %llu "
1899 "to %llu blocks", o_blocks_count, n_blocks_count);
1900
1901 if (n_blocks_count < o_blocks_count) {
1902 /* On-line shrinking not supported */
1903 ext4_warning(sb, "can't shrink FS - resize aborted");
1904 return -EINVAL;
1905 }
1906
1907 if (n_blocks_count == o_blocks_count)
1908 /* Nothing need to do */
1909 return 0;
ここでは、現在のブロック数o_blocks_countをsuper blockから読み出し、新しいブロックサイズn_blocks_countと比較しています。onlineでリサイズする場合はshrinkがサポートされていないことがわかります。
1911 n_group = ext4_get_group_number(sb, n_blocks_count - 1);
1912 if (n_group > (0xFFFFFFFFUL / EXT4_INODES_PER_GROUP(sb))) {
1913 ext4_warning(sb, "resize would cause inodes_count overflow");
1914 return -EINVAL;
1915 }
1916 ext4_get_group_no_and_offset(sb, o_blocks_count - 1, &o_group, &offset);
次に、新しいブロックサイズのグループ数n_groupと現在のブロックサイズのグループ数o_group、グループ内のデータを取得しています。新しいグループ数がものすごく大きな値になっていたらここで-EINVALが返ります。
1918 n_desc_blocks = num_desc_blocks(sb, n_group + 1);
1919 o_desc_blocks = num_desc_blocks(sb, sbi->s_groups_count);
ここは、Group Descriptor Tableが何ブロック必要かを、新旧取得しています。
1923 if (EXT4_HAS_COMPAT_FEATURE(sb, EXT4_FEATURE_COMPAT_RESIZE_INODE)) {
1924 if (meta_bg) {
1925 ext4_error(sb, "resize_inode and meta_bg enabled "
1926 "simultaneously");
1927 return -EINVAL;
1928 }
1929 if (n_desc_blocks > o_desc_blocks +
1930 le16_to_cpu(es->s_reserved_gdt_blocks)) {
1931 n_blocks_count_retry = n_blocks_count;
1932 n_desc_blocks = o_desc_blocks +
1933 le16_to_cpu(es->s_reserved_gdt_blocks);
1934 n_group = n_desc_blocks * EXT4_DESC_PER_BLOCK(sb);
1935 n_blocks_count = n_group * EXT4_BLOCKS_PER_GROUP(sb);
1936 n_group--; /* set to last group number */
1937 }
1938
1939 if (!resize_inode)
1940 resize_inode = ext4_iget(sb, EXT4_RESIZE_INO);
1941 if (IS_ERR(resize_inode)) {
1942 ext4_warning(sb, "Error opening resize inode");
1943 return PTR_ERR(resize_inode);
1944 }
1945 }
resize2fsコマンドからオンラインresizeする場合は、resize_inode機能を必須とするので、実質このブロックは必ず通ることになります。
meta_bgについての細かい説明は前回の記事を参考にしていただきたいですが、meta_bgを使用しているときはresizeがサポートされていないことがわかります。
次のチェックは、今から拡張しようとしているファイルシステムを管理するためのGDTが既存のGDTとRGDTで収まりきれるかを確認しています。収まりきれなかった場合は、resize2fsコマンド内でこの拡張操作を禁止していましたが、カーネル内ではRGDTの最大限まで確保するような処理となるようです。
最後にresize_inodeを取得してきます。このinode番号EXT4_RESIZE_INOは7番となります。
1962 /* extend the last group */
1963 if (n_group == o_group)
1964 add = n_blocks_count - o_blocks_count;
1965 else
1966 add = EXT4_BLOCKS_PER_GROUP(sb) - (offset + 1);
1967 if (add > 0) {
1968 err = ext4_group_extend_no_check(sb, o_blocks_count, add);
1969 if (err)
1970 goto out;
1971 }
追加ブロックがある場合は、ext4_group_extend_no_check()で、ext4_group_extend_no_check()のコメントを読むと最後のブロックグループに新しいブロック分を追加処理とのことです。
どうやらこの関数が私たちの目的の関数のようです。この関数を見ていきましょう。
ext4_group_extend_no_check()
ext4_group_extend_no_check()を読み進めると・・・ありました。
1670 ext4_blocks_count_set(es, o_blocks_count + add);
1671 ext4_free_blocks_count_set(es, ext4_free_blocks_count(es) + add);
1672 ext4_debug("freeing blocks %llu through %llu\n", o_blocks_count,
1673 o_blocks_count + add);
super blockが管理しているblock数とfree block数を、super blockに更新しています。
1674 /* We add the blocks to the bitmap and set the group need init bit */
1675 err = ext4_group_add_blocks(handle, sb, o_blocks_count, add);
このext4_group_add_blocks関数はGDTを更新しているように思えますね。
実際のところはどうなっているでしょうか?
ext4_group_add_blocks()
ext4_group_add_blocks()を見ると・・・ありました!
4985 blk_free_count = blocks_freed + ext4_free_group_clusters(sb, desc);
4986 ext4_free_group_clusters_set(sb, desc, blk_free_count);
descはGDTの構造体のポインタで、blocks_freedが追加されるブロック数です。
確かに前回予想した、GDTの更新をしていることがわかります。
まとめ
resize2fsのカーネル処理は、以下のような処理をしていることがわかりました。
1. ユーザー空間からioctl(2)のEXT4_IOC_RESIZE_FSリクエストでresize2fsのカーネルの処理を実行
2. 同時リサイズを避けるために、ファイルシステムにリサイズ中であるフラグを立てる
3. super blockの全ブロック数と全フリーブロック数を更新
4. GDTを更新
オンラインresize2fsがなぜ高速で終わるのか?という疑問に対して、スーパーブロックのブロック数の管理領域とGDTを更新しているだけだからだ、というのがわかりました。
この記事には書きませんでしたが、実はext4_resize_fs()の処理の後に、lazy initializationという機能を使い、未初期化のinode tableをext4lazyinitカーネルスレッドに非同期に初期化させるということもします。少しでも高速化するように色々な工夫がなされていることがわかります。
いくつか読み飛ばした処理もあるので、もっと深く知りたい方は本記事で取り上げた処理以外の部分を読んでみてはいかがでしょうか。