Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

resize2fsコマンドの先でカーネルは何をしているのか

背景

前回の記事で、resize2fsコマンドがどのように1秒未満での容量拡張を実現しているかを知るために、resize2fsコマンドのソースを調査しました。その結果、メタデータの一つであるGlobal Descriptor Tables(GDT)をカーネル内で更新しているからではないか、という示唆を得られました。今回は、実際にカーネルのコードを読んで、この示唆が正しかったことを見ていきたいと思います。

調査対象

今回も新しめのカーネルで調査しました。Amazon Linuxとは多少ソースが異なっているかもしれませんが、本質は大きく変わらないかと思います。

前回の復習

前回調べたときから約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カーネルスレッドに非同期に初期化させるということもします。少しでも高速化するように色々な工夫がなされていることがわかります。

いくつか読み飛ばした処理もあるので、もっと深く知りたい方は本記事で取り上げた処理以外の部分を読んでみてはいかがでしょうか。