はじめに
最初の記事の項目のファイルシステムについて
本記事を通じて学んでいきたいと思います。
ゴール
Unixシステムのファイルシステムの構造について理解する。
ファイルシステムについて
ファイルシステムとは、コンピュータリソースを操作する為のOSが持つ機能です。
今回はファイルシステムの中でも
Linuxでのデファクトスタンダードとなっているext4について確認したいと思います。
ext4ファイルシステムの構造について
データを記憶する為には、記憶装置が必要です。
記憶装置としては、SSDだったり、HDDが使用されます。
これらをシステムで使用する際は、パーティションという区間分けをして使用します。
ファイルシステムではこの区間分けされたパーティションを使用します。(以降はext4として記載しています)
パーティションの中には、ブートブロックと、いくつかのブロックグループが存在します。
ブロックグループの中には、スーパブロック、グループディスクプリタテーブル(GDT)、
GDTの予約領域、i-nodeビットマップ、データビットマップ、i-nodeテーブル、データブロックで構成されています。
ブートブロック:ファイルシステムは使用しない。OSの起動情報が記録される。
スーパブロック:i-nodeテーブルのサイズ情報や論理ブロックサイズ、論理ブロック数が記録される。
GDT:データとi-nodeのビットマップの位置、i-nodeテーブルの位置情報が記録される。
i-nodeビットマップ:i-nodeテーブルでどのエントリが使用されているか記録される。
データビットマップ:ブロックグループ内のデータブロックの使用状況が記録される。
i-nodeテーブル:1エントリにファイルの種別、パーミッション情報、サイズ、データの場所が記録される。
データ領域:ファイルの実データが記録される。
各種領域のダンプを確認してみます。
ブートブロック、スーパブロックのダンプ
最初の1024バイトはブートブロックであり、使用されていないのがわかります。
アドレス0x400からがスーパブロックの領域になります。
どの情報がどこにマッピングされているかについては、公式に記載されています。
スーパブロック以降はグループディスクプリタテーブルの領域となります。
エントリー数について、スーパブロックのダンプ情報から値を算出してみます。
算出方法は以下になります。
blocks_count(ブロック数) / blocks_per_group(グループ毎のブロック数)
blocks_countは0x8000であり、blocks_countは0x10000000である事から、
8192個のエントリーがある事がわかります。
各エントリーは64バイトで構成されるので、64*8192/4KiB = 80ブロック分使用している事がわかります。
それでは、グループディスクプリタとグループディスクプリタの予約領域を含めてダンプして確認してみます。
グループディスクプリタとグループディスクプリタの予約領域のダンプ
グループディスクプリタはアドレス0x1000から始まります。
グループディスクプリタテーブルの予約領域については、80ブロック x 0x1000(4KiB) + 0x1000から始まります。
予約のブロック数は、スーパブロックのs_reserved_gdt_blocksの情報で確認できます。
値を確認すると0x400とわかるので、0x400000分が予約領域で使用されている事わかります。
では、この調子でdataブロックのビットマップを確認してみます。
dataブロックのビットマップのダンプ
アドレス0x400000+0x80000+0x1000からが、dataブロックビットマップとなります。
※スーパーブロックのbitマップブロックの位置と一致しているのも分かります。
i-nodeブロックのビットマップのダンプ
スーパブロックのi-nodeブロックビットマップの位置である、アドレス0x1481000も確認してみます。
公式に記載されているブロックレイアウトと同じではいようです。
レイアウトではdataビットマップの1blockの後にi-nodeブロックが続きますが、
dataビットマップの隣は次のブロックグループのdataビットマップとなっています。
i-nodeテーブルのダンプ
i-nodeテーブルについても同じくスーパブロックの情報を確認して、0x2481000からi-nodeテーブルが記録されている事がわかります。
お試しで、サンプルファイルのi-nodeを確認し、該当箇所のdumpを確認してみます。
i-node情報を取得するには、statコマンドを使用します。
kenta@DESKTOP-RNUJ2RM:~/work/practice/qiita$ stat sample2.c
File: sample2.c
Size: 990 Blocks: 8 IO Block: 4096 通常ファイル
Device: 820h/2080d Inode: 6736 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ kenta) Gid: ( 1000/ kenta)
Access: 2025-01-22 22:10:36.866980000 +0900
Modify: 2025-01-11 16:56:10.855039947 +0900
Change: 2025-01-11 16:56:10.865039946 +0900
Birth: 2025-01-11 16:56:10.855039947 +0900
statで出力されるi-nodeの番号は10進数表記されています。
i-node番号の16進数の1A50から-1を引いた値に対して、0x100分掛けます。
(i-nodeの構造体のサイズが256バイトの為)、計算した値から、0x2481000分進めた位置をdumpしてみます。
0x2481000 + 0x1A4F00 = 0x2626000
最初の4バイトはファイルの種別とパーミッションがエンコードされています。
ext4はリトルエンディアンで記録されるので、0x81a4がエンコードの値になります。
これは通常ファイルで、パーミッションがrw-r--r--である事を表してます。
また、サイズは0x3deで990バイトで、リンクカウントが1になっています。
実際に確認してみます。
合ってそうですね。
また、i-nodeを確認するとデータが記録されている箇所も確認する事ができます。
ext4からファイルのデータブロック位置をextentツリーで管理されています。(extentは複数ブロックの塊を指します。)
dataブロックのダンプ
先程のi-nodeダンプから、i-blocksを確認すると、extetntが指すブロック番号を確認できますが、
Dataブロックがどこから開始されるのかちょっとわからなかったので、
今回はdebugfsとstatを使用してアドレスを確認します。
sudo debugfs -R "stat /home/kenta/work/practice/qiitasample2.c" /dev/sdc
Inode: 6736 Type: regular Mode: 0644 Flags: 0x80000
Generation: 500734895 Version: 0x00000000:00000001
User: 1000 Group: 1000 Project: 0 Size: 990
File ACL: 0
Links: 1 Blockcount: 8
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x6782241a:ce3dd928 -- Sat Jan 11 16:56:10 2025
atime: 0x6794d0d3:58140d6c -- Sat Jan 25 20:53:55 2025
mtime: 0x6782241a:cbdb7f2c -- Sat Jan 11 16:56:10 2025
crtime: 0x6782241a:cbdb7f2c -- Sat Jan 11 16:56:10 2025
Size of extra inode fields: 32
Inode checksum: 0x5f5f7639
EXTENTS:
(0):2127365
statで出力されるEXTENTSの値は10進数表記されています。
EXTENTSの数値はブロック数を表しているので、該当アドレスは0x207605000です。
dumpして中身を確認してみます。
合っているかファイルの中身を確認してみます。
kenta@DESKTOP-RNUJ2RM:~/work/practice/qiita$ cat sample2.c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE (4*1024)
//#define BUFFER_SIZE (1024*1024*1024)
char buffer[BUFFER_SIZE] = {};
int main(int argc, char* argv[])
{
int fd;
int r;
if((fd=open("dummy", O_CREAT|O_WRONLY|O_TRUNC)) == -1){
fprintf(stderr, "open error\n");
exit(-1);
}
r = write(fd, buffer,BUFFER_SIZE);
if(r != BUFFER_SIZE){
fprintf(stderr, "write error");
exit(-1);
}
if(argc > 1){
int opt;
switch(opt = getopt(argc, argv,"sf")){
case 's':
printf("sync\n");
sync();
break;
case 'f':
printf("fsync\n");
if(fsync(fd) == -1){
fprintf(stderr, "fsync error");
exit(-1);
}
break;
default:
break;
}
}
return 0;
}
合ってそうですね。
i-blocksですが、シンボリックファイルの場合はextentの情報ではなくファイル名が格納されます。
ディレクトリのダンプ
先程はサンプルファイルの中身を確認しましたが、次はディレクトリの中身も確認したいと思います。
同じく、debugfsとstatを使用してアドレスを確認します。
該当のExtetentの場所をdumpしてみます。
ディレクトリの中身には3つのエントリがあります。
※「.」と「..」はディレクトリ作成時に必ず作成されます。
最初の4バイトはi-node番号を指し、次の2バイトはディレクトリエントリの長さ、
次の1バイトはファイル名の長さ、次の1バイトはファイルタイプ、次はファイル名の長さ分の文字列(ASCII)が記録されます。
(ここでファイルタイプが入っているのは、トラバーサル中にi-nodeを参照せずにファイルタイプが参照できるようにする為にファイルタイプが入っています。)
dump情報を確認すると、「.」はi-node番号0x13d5であり、「..」はi-node番号0x61c6であり、
「test1」はi-node番号0x19f2である事が確認できます。
statで「test1」を確認するとi-nodeが実際に0x19f2である事がわかります。
kenta@DESKTOP-RNUJ2RM:~/work/practice$ stat sample/test1
File: sample/test1
Size: 5 Blocks: 8 IO Block: 4096 通常ファイル
Device: 820h/2080d Inode: 6642 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ kenta) Gid: ( 1000/ kenta)
Access: 2025-01-25 23:18:16.059327946 +0900
Modify: 2025-01-25 23:18:16.059327946 +0900
Change: 2025-01-25 23:18:16.059327946 +0900
Birth: 2025-01-25 23:18:16.059327946 +0900
ディレクトリはファイル名とi-nodeを紐づけて記録しているだけですね。
最後に
ファイルシステムの構造について、
中身も含めて確認する事で公式のサイトの内容をなぞりながら理解できるので、
読むだけより、より各種データ項目の意味等が理解できました。
ではまた次回~!