Skip to content
Published on

[オペレーティングシステム] 14. ファイルシステム実装

Authors

ファイルシステム実装

ファイルシステムのユーザーインターフェースを見た後、そのインターフェースの背後でオペレーティングシステムがどのようにファイルシステムを実装しているかを解説します。ディスク上のデータ構造、メモリ内データ構造、割り当て方法、空き領域管理などを扱います。


1. ファイルシステムの階層構造

┌──────────────────────────────────────────┐
│       アプリケーション(ユーザー空間)     │
├──────────────────────────────────────────┤
│       論理ファイルシステム                │
│  (ファイル名 → inode変換、保護検査)     │
├──────────────────────────────────────────┤
│       ファイル構成モジュール              │
│  (論理ブロック → 物理ブロックマッピング) │
├──────────────────────────────────────────┤
│       基本ファイルシステム               │
│  (ブロックI/O要求の発行)               │
├──────────────────────────────────────────┤
I/O制御                            │
│  (デバイスドライバ、割り込みハンドラ)   │
├──────────────────────────────────────────┤
│       ストレージデバイス(ディスク/SSD)   │
└──────────────────────────────────────────┘

2. ディスク上の構造(On-Disk Structures)

ファイルシステムのディスクレイアウト

┌────────┬───────────┬──────────┬──────────┬─────────────────┐
BootSuperFree     │ inode    │ Data BlocksBlockBlockSpaceTable    │                 │
│        │           │ Mgmt     │          │                 │
│ ブート │ メタ      │ビットマップ│ ファイル │ 実際のファイル  │
│ ローダ │ データ    │/リスト   │ メタ     │ データ          │
└────────┴───────────┴──────────┴──────────┴─────────────────┘
構造役割
ブートブロックブートに必要なコード(ブートローダ)
スーパーブロックファイルシステム全体の情報(ブロック数、inode数等)
空き領域管理使用可能なブロックの追跡(ビットマップまたは連結リスト)
inodeテーブル各ファイルのメタデータとブロックポインタ
データブロック実際のファイル/ディレクトリデータ

inode構造

┌─────────────────────────────────────┐
│           inode 構造                 │
├─────────────────────────────────────┤
│ ファイルタイプ、権限(mode)        │
│ 所有者(uid, gid)                  │
│ サイズ(size)                      │
│ タイムスタンプ(atime, mtime, ctime)│
│ リンクカウント                       │
│ ブロックカウント                     │
├─────────────────────────────────────┤
│ 直接ブロックポインタ [0-11] → データ │
│ 単一間接ポインタ     → ポインタブロック│
│ 二重間接ポインタ     → ポインタ²      │
│ 三重間接ポインタ     → ポインタ³      │
└─────────────────────────────────────┘
// 간단화된 inode 구조체 (ext2 기반)
struct inode {
    uint16_t mode;        // 파일 유형 + 권한
    uint16_t uid;         // 소유자 ID
    uint32_t size;        // 파일 크기
    uint32_t atime;       // 접근 시간
    uint32_t mtime;       // 수정 시간
    uint32_t ctime;       // 상태 변경 시간
    uint16_t gid;         // 그룹 ID
    uint16_t links_count; // 하드 링크 수
    uint32_t blocks;      // 할당된 블록 수

    uint32_t direct[12];          // 직접 블록 포인터
    uint32_t single_indirect;     // 단일 간접 포인터
    uint32_t double_indirect;     // 이중 간접 포인터
    uint32_t triple_indirect;     // 삼중 간접 포인터
};

3. メモリ内構造(In-Memory Structures)

ファイルシステムはパフォーマンスのために頻繁に使用する情報をメモリにキャッシュします。

┌───────────────────────────────────────────────┐
│              カーネルメモリ                     │
│                                               │
│  ┌─────────────────┐  ┌───────────────────┐   │
│  │ マウントテーブル │  │ ディレクトリキャッシュ│  │
│  │(FS一覧)       │  │(最近のパス情報)   │   │
│  └─────────────────┘  └───────────────────┘   │
│                                               │
│  ┌─────────────────┐  ┌───────────────────┐   │
│  │ システム全体     │  │ プロセスごとの     │   │
│  │ オープンファイル │  │ オープンファイル   │   │
│  │ テーブル         │  │ テーブル           │   │
│  └─────────────────┘  └───────────────────┘   │
│                                               │
│  ┌─────────────────┐                          │
│  │ バッファキャッシュ│ ← ディスクブロック     │
│  │                 │   キャッシュ             │
│  └─────────────────┘                          │
└───────────────────────────────────────────────┘

4. VFS(Virtual File System)

VFSは複数の種類のファイルシステムを単一のインターフェースに統合する抽象化レイヤーです。

           ユーザープロセス
        open() read() write()
    ┌──────────┴──────────┐
VFS    │  (仮想ファイル       │
    │   システム)          │
    │  vnodeインターフェース│
    └──┬──────┬──────┬─────┘
       │      │      │
    ┌──┴──┐┌──┴──┐┌──┴──┐
    │ext4 ││NFS  ││FAT    └──┬──┘└──┬──┘└──┬──┘
       │      │      │
    ローカル ネットワーク USB
    ディスク サーバー   ドライブ

VFSの主要オブジェクト

// VFS 추상화의 주요 객체 (Linux 기반 의사 코드)

// 1. 슈퍼블록 객체: 파일 시스템 인스턴스 정보
struct super_block {
    struct list_head s_list;       // 슈퍼블록 목록
    dev_t            s_dev;        // 장치 식별자
    unsigned long    s_blocksize;  // 블록 크기
    struct super_operations *s_op; // 연산 테이블
    // ...
};

// 2. inode 객체: 개별 파일 정보
struct inode {
    umode_t          i_mode;       // 파일 유형 + 권한
    uid_t            i_uid;        // 소유자
    loff_t           i_size;       // 파일 크기
    struct inode_operations *i_op; // inode 연산
    struct file_operations  *i_fop;// 파일 연산
    // ...
};

// 3. dentry 객체: 디렉토리 엔트리 (경로명 컴포넌트)
struct dentry {
    struct inode     *d_inode;     // 관련 inode
    struct dentry    *d_parent;    // 부모 디렉토리
    struct qstr      d_name;      // 엔트리 이름
    struct dentry_operations *d_op;
    // ...
};

// 4. file 객체: 열린 파일 인스턴스
struct file {
    struct dentry    *f_dentry;    // 관련 dentry
    loff_t           f_pos;       // 현재 오프셋
    unsigned int     f_flags;     // 열기 플래그
    struct file_operations *f_op; // 파일 연산
    // ...
};

5. ディレクトリ実装

線形リスト

ディレクトリブロック:
┌──────────┬────────┬──────────┬────────┬──────────┬────────┐
"file1"  │ inode  │ "file2"  │ inode  │ "file3"  │ inode  │
│          │  23    │          │  47    │          │  12└──────────┴────────┴──────────┴────────┴──────────┴────────┘

検索: O(n) - ファイル名を先頭から順次探索
利点: 実装が簡単
欠点: 大規模ディレクトリで遅い

ハッシュテーブル

ハッシュ関数: hash("filename") → バケットインデックス

バケット 0: ["file_a", inode 23]["file_d", inode 89]NULL
バケット 1: ["file_b", inode 47]NULL
バケット 2: NULL
バケット 3: ["file_c", inode 12]["file_e", inode 56]NULL
...

検索: O(1) 平均(ハッシュ衝突時は連結リスト探索)
利点: 高速な検索
欠点: 固定サイズ、ハッシュ衝突管理が必要

6. 割り当て方法(Allocation Methods)

ファイルのデータブロックをディスクにどう配置するかを決定します。

連続割り当て(Contiguous Allocation)

ディレクトリエントリ:
┌────────┬───────────┬──────┐
│ ファイル名│開始ブロック│長さ  │
├────────┼───────────┼──────┤
│ file_a │    23│ file_b │    72│ file_c │   144└────────┴───────────┴──────┘

ディスクブロック:
 0  1 [2  3  4] 5  6 [7  8] 9 10 11 12 13 [14 15 16 17]
         file_a          file_b                file_c

利点: 順次/直接アクセスともに効率的、ディスクシーク最小化 欠点: 外部断片化、ファイルサイズ変更が困難

連結割り当て(Linked Allocation)

ディレクトリ:
┌────────┬──────┬──────┐
│ファイル名│開始  │終了  │
├────────┼──────┼──────┤
│ file_a │  91└────────┴──────┴──────┘

ディスクブロック(各ブロックに次ブロックポインタ含む):
ブロック 9 → ブロック 16 → ブロック 25 → ブロック 1NULL
[data|16] [data|25] [data|1]  [data|-1]

利点: 外部断片化なし、ファイルサイズ変更容易 欠点: 直接アクセスが非効率(順次探索が必要)、ポインタ空間オーバーヘッド

FAT(File Allocation Table)

FATテーブル(メモリにキャッシュ):
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐
0123456  │ ← ブロック番号
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
3EOF54EOF1FREE│ ← 次のブロック
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

ファイルA: 開始=0034EOF(ブロック 0, 3, 4ファイルB: 開始=2251EOF(ブロック 2, 5, 1

インデックス割り当て(Indexed Allocation)

ディレクトリ:
┌────────┬─────────────┐
│ファイル名│インデックスブロック│
├────────┼─────────────┤
│ file_a │     19└────────┴─────────────┘

インデックスブロック 19:    データブロック:
┌─────────────┐          ブロック 9:  [data...]
9          │ ────→    ブロック 16: [data...]
16          │ ────→    ブロック 25: [data...]
25          │ ────→    ブロック 1:  [data...]
1          │ ────→
-1 (終端)└─────────────┘

利点: 直接アクセスが効率的、外部断片化なし 欠点: インデックスブロックのオーバーヘッド、小さいファイルに非効率的

割り当て方法の比較

方法順次アクセス直接アクセス断片化ファイル拡張
連続非常に高速高速外部断片化困難
連結普通遅いなし容易
インデックス普通高速なし普通

7. 空き領域管理

ビットマップ(Bit Vector)

ブロック番号: 0  1  2  3  4  5  6  7  8  9  10 11 12
ビットマップ: 1  1  0  1  1  0  0  1  0  0  0  1  0

1 = 使用中, 0 = 空き

空きブロック検索: ビットマップで最初の0ビットを探索
利点: 連続空き領域を見つけやすい
欠点: 大容量ディスクでビットマップが大きくなる
      (1TBディスク、4KBブロック → 32MBビットマップ)
// 비트맵에서 빈 블록 찾기
int find_free_block(uint32_t *bitmap, int total_blocks) {
    int words = total_blocks / 32;
    for (int i = 0; i < words; i++) {
        if (bitmap[i] != 0xFFFFFFFF) {  // 모두 사용 중이 아니면
            // 첫 번째 0 비트 위치 찾기
            for (int j = 0; j < 32; j++) {
                if (!(bitmap[i] & (1 << j))) {
                    return i * 32 + j;
                }
            }
        }
    }
    return -1;  // 빈 블록 없음
}

連結リスト

Free Space Head → ブロック 2 → ブロック 5 → ブロック 6 → ブロック 8NULL

各空きブロックが次の空きブロックを指す
利点: 実装が簡単
欠点: 連続空き領域を見つけにくい、走査コスト

TRIMコマンド

SSDで削除されたブロックを事前に通知してガベージコレクションを最適化します。

TRIMなし:                       TRIM使用時:
1. ファイル削除 → メタのみ除去  1. ファイル削除 → メタ除去
2. SSDはブロックが空と知らない  2. TRIMコマンドでSSDに通知
3. 後の書き込み時:              3. SSDが事前にブロック消去
   読み→消去→書き込みが必要    4. 後の書き込み時: 直接書き込み
   (書き込み性能低下)            (書き込み性能維持)

8. ジャーナリング(Journaling)

システムクラッシュ時にファイルシステムの一貫性を維持するためのメカニズムです。

ジャーナリング動作過程

通常の書き込み(ジャーナリングなし):
1. データブロック書き込み
2. inode更新
3. ビットマップ更新
2段階でクラッシュすると不整合発生!

ジャーナリング書き込み:
┌──────────────────────────────────────┐
│          ジャーナル領域(Log)        │
│                                      │
1. トランザクション開始(TxB)      │
2. メタデータ変更事項を記録          │
3. トランザクションコミット(TxE)   │
│                                      │
│  → コミット完了後、実際の位置に適用  │
│  → 適用完了後、ジャーナル削除        │
└──────────────────────────────────────┘

ジャーナリングモード

モードジャーナルに記録安全性性能
Journalメタデータ + データ最高遅い
Orderedメタデータのみ(データ先に書く)普通
Writebackメタデータのみ(順序保証なし)普通速い
// 저널링 트랜잭션 의사 코드
void journaled_write(struct transaction *tx) {
    // 1. 저널에 트랜잭션 시작 기록
    journal_write_begin(tx);

    // 2. 변경할 메타데이터를 저널에 기록
    journal_write_metadata(tx, inode_data);
    journal_write_metadata(tx, bitmap_data);

    // 3. 저널에 트랜잭션 커밋 기록
    journal_write_commit(tx);

    // 4. 실제 파일 시스템 위치에 적용 (체크포인트)
    write_to_actual_location(inode_data);
    write_to_actual_location(bitmap_data);

    // 5. 저널에서 트랜잭션 제거
    journal_free(tx);
}

// 복구 시: 커밋된 트랜잭션은 재실행, 미커밋은 무시
void recover_from_crash() {
    for_each_transaction(journal) {
        if (transaction_committed(tx))
            replay_transaction(tx);  // 재실행
        else
            discard_transaction(tx); // 폐기
    }
}

9. まとめ

  • ファイルシステム階層: 論理ファイルシステム → ファイル構成モジュール → 基本ファイルシステム → I/O制御
  • ディスク構造: ブートブロック、スーパーブロック、inodeテーブル、データブロック
  • VFS: 複数のファイルシステムを単一インターフェースに抽象化
  • 割り当て方法: 連続(高速だが断片化)、連結(柔軟だが直接アクセス遅い)、インデックス(バランス型)
  • 空き領域管理: ビットマップが最も一般的。SSDではTRIMで最適化
  • ジャーナリング: トランザクションログでファイルシステムの一貫性を保証
クイズ:ファイルシステム実装

Q1. VFSの役割は何ですか?

A1. VFS(Virtual File System)はext4、NFS、FATなど異なるファイルシステムを統一されたインターフェース(open、read、writeなど)で抽象化します。これにより、アプリケーションは基盤となるファイルシステムの種類に関係なく、同じシステムコールでファイルにアクセスできます。

Q2. 連続割り当てとインデックス割り当てのトレードオフは何ですか?

A2. 連続割り当ては順次/直接アクセスともに高速ですが、外部断片化が発生しファイルサイズ変更が困難です。インデックス割り当ては外部断片化がなくファイル拡張が容易ですが、インデックスブロックへの追加空間とI/Oが必要です。

Q3. ジャーナリングのorderedモードはどのように動作しますか?

A3. Orderedモードでは、メタデータのみジャーナルに記録しますが、データブロックはメタデータより先にディスクに記録されます。これにより、クラッシュ時にメタデータがまだ記録されていないデータを指す状況を防止します。Journalモードより性能が良く、データの一貫性も維持します。