概要
ファイルシステムとは、記憶媒体(フラッシュメモリ、HDDなど)上のデータを「ファイル」と「ディレクトリ」という階層構造で管理する仕組みだ。OSはファイルシステムを通じて、どのデータがどこに格納されているかを追跡し、読み書き・削除・名前変更などの操作を提供する。
組み込みLinuxシステムでは、使用する記憶媒体の種類によって適切なファイルシステムが異なる。HDDやeMMCのようにブロックデバイスとして扱えるものにはext4やFAT32が使えるが、rawのNANDフラッシュにはUBIFS・JFFS2のようなフラッシュ専用ファイルシステムが必要だ。フラッシュメモリは書き込み回数に上限があり(ウェアレベリングが必要)、ブロック単位でしか消去できないという特性があるため、HDDと同じファイルシステムを直接使うとすぐに劣化する。
RTOSやベアメタル環境では、FatFsのような軽量なファイルシステムライブラリをマイクロコントローラ上で動かし、SDカードやSPI Flashに対してファイル操作を行うケースも多い。
歴史・背景
UNIXのファイルシステムはケン・トンプソンとデニス・リッチーが1969年に設計したUFS(Unix File System)に起源を持つ。「すべてはファイルである」というUNIXの哲学により、デバイスや名前付きパイプもファイルとして扱われ、統一されたインターフェースが実現した。
MS-DOSとともに普及したFAT(File Allocation Table)は1977年にビル・ゲイツとマーク・マクドナルドが設計し、FAT12→FAT16→FAT32と拡張されてきた。シンプルな構造で多くのOSがサポートしていることから、USBメモリやSDカードの標準フォーマットとして現在も広く使われる。
Linuxで主流のext2は1993年にRemy Cardが設計し、ext3(ジャーナリング追加、2001年)、ext4(大容量・高性能化、2008年)へと発展した。組み込みLinuxではeMMCとext4の組み合わせが現在の標準的な構成だ。
フラッシュ専用ファイルシステムはNANDフラッシュの普及とともに発展した。JFFS2(Journalling Flash File System version 2)は2001年にRedHatが開発し、2006年にUBIFS(UBIFS: Unsorted Block Image File System)がその後継として登場した。UBIFSはウェアレベリングをUBIレイヤーに分離することで性能と信頼性を大幅に改善した。
技術仕様
ファイルシステムの種類と特性
| ファイルシステム | 用途 | ジャーナリング | フラッシュ対応 | 最大ファイルサイズ | 主な使用先 |
|---|---|---|---|---|---|
| ext4 | 汎用Linux | あり | 間接(eMMC/SDのFTL経由) | 16TB | Raspberry Pi, BeagleBone, SBC一般 |
| FAT32 | 互換性重視 | なし | 間接(FTL経由) | 4GB | SDカード, USBメモリ, ブートパーティション |
| exFAT | 大容量可搬媒体 | なし | 間接 | 16EB | 大容量SDカード(SDXC) |
| UBIFS | raw NAND | あり | 直接対応 | 非制限 | 産業用機器の raw NAND |
| JFFS2 | raw NAND/NOR | あり(ログ構造) | 直接対応 | 非制限 | 古い組み込みシステム |
| YAFFS2 | raw NAND | あり | 直接対応 | 非制限 | Androidの初期版、組み込み |
| squashfs | 読み取り専用圧縮 | なし | 間接 | 非制限 | ルートファイルシステム(読み取り専用) |
| tmpfs | RAM上の一時領域 | なし | なし(RAM) | RAM容量依存 | /tmp, /run |
| overlayfs | 差分レイヤー | なし(下位依存) | 間接 | 下位依存 | Dockerコンテナ, ルートfs重ね合わせ |
| FatFs | 組み込みライブラリ | なし | 間接(FTL経由) | 4GB(FAT32) | STM32, Arduino + SDカード |
| LittleFS | 組み込みフラッシュ | ログ構造 | 直接対応 | 2GB | ESP32 SPI Flash, MCUフラッシュ |
rawフラッシュとブロックデバイスの違い
NANDフラッシュの構造:
┌─────────────────────────────────────┐
│ NANDフラッシュチップ(ハードウェア) │
│ ┌──────┐┌──────┐┌──────┐┌──────┐ │
│ │Block0││Block1││Block2││Block3│ │ ← 消去単位(128KB〜512KB)
│ │ Page0│└──────┘└──────┘└──────┘ │
│ │ Page1│ │
│ │ Page2│ │ ← 書き込み単位(4KB〜16KB)
│ └──────┘ │
└─────────────────────────────────────┘
rawとして使う場合(MTDデバイス):
/dev/mtd0 → UBI → UBIFS (ウェアレベリング・バッドブロック管理はUBIが担当)
eMMCや内蔵FTL付きNANDとして使う場合(ブロックデバイス):
/dev/mmcblk0 → ext4/FAT (ウェアレベリングはeMMCコントローラが担当)
ext4の内部構造
ext4ディスクレイアウト:
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ ブートセクタ │ ブロックグループ0 │ ブロックグループ1 │ ... │
└──────────────┴──────────────┴──────────────┴──────────────┘
各ブロックグループ:
┌───────────┬──────────┬───────────┬──────────┬──────────────┐
│ スーパーブロック│ GDT │ ブロックBmap│ iノードBmap│ データブロック │
│(FS全体情報)│(グループ記述)│(空き追跡) │(iノード空き)│(ファイルデータ)│
└───────────┴──────────┴───────────┴──────────┴──────────────┘
iノード(ファイルのメタデータ):
- ファイルサイズ、パーミッション、タイムスタンプ
- データブロックへのポインタ(直接/間接/二重間接/三重間接)
- 拡張属性(xattr)
UBIとUBIFSの階層構造
raw NANDフラッシュ上でUBIFSを使う場合、以下の階層になる。
アプリケーション(ファイルシステムAPI)
↕
UBIFS(ファイルシステム層)
↕
UBI(Unsorted Block Images)← ウェアレベリング・バッドブロック管理
↕
MTD(Memory Technology Device)← ハードウェア抽象化
↕
NANDフラッシュコントローラ(ハードウェア)
# UBI/UBIFSのセットアップ(組み込みLinuxでの典型的な手順)
# MTDデバイスにUBIをアタッチ
ubiattach /dev/ubi_ctrl -m 0 -d 0
# UBIボリューム作成
ubimkvol /dev/ubi0 -N rootfs -s 200MiB
# UBIFSとしてマウント
mount -t ubifs ubi0:rootfs /mnt/root
# フォーマット(初回またはリセット時)
ubiformat /dev/mtd0 -f rootfs.ubifs
動作原理
ジャーナリングと電源断耐性
突然の電源断(組み込みでは一般的なリスク)でもファイルシステムの一貫性を保つために、ジャーナリングが重要だ。ジャーナリングは書き込み前に変更内容を「ジャーナル(ログ領域)」に記録し、完全に書き終わった後にコミットする。
電源断が起きる可能性のある操作シーケンス(ジャーナルなし):
1. データブロックを書き込む ← ここで電源断
2. iノードを更新する ← 未実行
3. ディレクトリエントリを追加 ← 未実行
→ ファイルシステムが不整合になり、fsckが必要
ジャーナリングあり:
1. ジャーナルに操作を記録(トランザクション開始)
2. データブロックを書き込む ← ここで電源断
(次回起動時にジャーナルから未完了トランザクションを検出→ロールバック)
→ ファイルシステムの一貫性を素早く回復
ext4のジャーナリングモード:
| モード | 速度 | データ保護 | 説明 |
|---|---|---|---|
| ordered | 中 | メタデータのみ | データを書いてからメタデータをジャーナル(デフォルト) |
| writeback | 速い | メタデータのみ | 順序保証なし(データ損失リスクあり) |
| journal | 遅い | データ+メタデータ | 全データをジャーナル(最も安全) |
LittleFSのログ構造(MCU向け)
ESP32やSTM32などのMCU内蔵フラッシュ用に設計されたLittleFSは、電源断安全性を重視したログ構造ファイルシステムだ。
// LittleFSの使用例(ESP32 Arduino/ESP-IDF)
#include "littlefs/lfs.h"
lfs_t lfs;
lfs_file_t file;
// LittleFS設定
const struct lfs_config cfg = {
.read = flash_read,
.prog = flash_prog,
.erase = flash_erase,
.sync = flash_sync,
.read_size = 16,
.prog_size = 16,
.block_size = 4096, // SPIフラッシュのページサイズ
.block_count = 128, // 128ブロック × 4KB = 512KB
.cache_size = 16,
.lookahead_size = 16,
.block_cycles = 500, // ウェアレベリング目安サイクル数
};
void littlefs_example(void) {
lfs_mount(&lfs, &cfg);
// ファイルに書き込む
lfs_file_open(&lfs, &file, "config.txt", LFS_O_RDWR | LFS_O_CREAT);
const char *data = "sensor_interval=1000\n";
lfs_file_write(&lfs, &file, data, strlen(data));
lfs_file_close(&lfs, &file);
lfs_unmount(&lfs);
}
FatFs(組み込みFATライブラリ)
ChaN氏が開発したFatFsは、RTOSやベアメタルで使える軽量FAT実装だ。SDカードやSPI Flashへのアクセスに広く使われる。
// FatFs使用例(STM32でのSDカード書き込み)
#include "ff.h" // FatFsヘッダ
FATFS fs;
FIL fil;
UINT bw;
void fatfs_write_example(void) {
// マウント
if (f_mount(&fs, "", 1) != FR_OK) {
Error_Handler();
}
// ファイルオープン(作成または追記)
if (f_open(&fil, "log.csv", FA_WRITE | FA_OPEN_APPEND) != FR_OK) {
Error_Handler();
}
// データ書き込み
char buf[64];
snprintf(buf, sizeof(buf), "%lu,%.2f,%.2f\r\n",
HAL_GetTick(), temperature, humidity);
f_write(&fil, buf, strlen(buf), &bw);
// 重要:f_sync()またはf_close()しないとバッファがフラッシュされない
f_sync(&fil); // 電源断対策
f_close(&fil);
f_unmount("");
}
用途・ユースケース
組み込みLinuxのパーティション構成
典型的なEmbedded Linuxデバイス(eMMC搭載SBC)のパーティション構成。
eMMC(例: 8GB)のパーティション構成:
┌──────────────────────────────────────────────────────────┐
│ /dev/mmcblk0 │
│ /dev/mmcblk0boot0 4MB ← ブートROM用(u-bootなど) │
│ /dev/mmcblk0p1 128MB ← FAT32 /boot(カーネル・DTB)│
│ /dev/mmcblk0p2 2GB ← ext4 / (rootfs) │
│ /dev/mmcblk0p3 4GB ← ext4 /data(ユーザーデータ) │
│ /dev/mmcblk0p4 残り ← ext4 /var(ログ等可変データ)│
└──────────────────────────────────────────────────────────┘
マウント構成例(/etc/fstab):
/dev/mmcblk0p1 /boot vfat defaults,ro 0 2
/dev/mmcblk0p2 / ext4 defaults,noatime 0 1
/dev/mmcblk0p3 /data ext4 defaults,noatime 0 2
/dev/mmcblk0p4 /var ext4 defaults,noatime 0 2
tmpfs /tmp tmpfs defaults,size=64m 0 0
tmpfs /var/run tmpfs defaults,mode=0755 0 0
読み取り専用rootfsによる信頼性向上
産業用機器や無人運用デバイスでは、ルートファイルシステムを読み取り専用(read-only)にすることで、電源断時のファイルシステム破損リスクを排除する手法が有効だ。
# /etc/fstabでrootfsをread-onlyにマウント
/dev/mmcblk0p2 / ext4 defaults,ro,noatime 0 1
# 書き込みが必要なディレクトリにはtmpfsかoverlayfsを使う
tmpfs /var/log tmpfs defaults,size=32m 0 0
tmpfs /tmp tmpfs defaults,size=64m 0 0
# overlayfsで/etcを読み書き可能にする(再起動でリセット)
# mount -t overlay overlay -o lowerdir=/etc,upperdir=/tmp/etc_rw,workdir=/tmp/etc_work /etc
A/B パーティション(信頼性の高いOTAアップデート)
A/Bパーティション構成でのOTAアップデート:
現在: パーティションA(稼働中)→ 通常動作
アップデート: パーティションBに新ファームウェアを書き込む
↓
再起動: パーティションBから起動
確認成功: BをActiveにマーク
失敗: 自動的にAにフォールバック
実装例(U-Boot + fw_setenv):
fw_setenv boot_part b # 次回ブートをBパーティションに切り替え
reboot
ログとデータ収集
センサーデータやシステムログをフラッシュメモリに記録する際のファイルシステム選択が重要だ。頻繁な書き込みが想定される場合、ウェアレベリング対応のファイルシステムを選ぶか、書き込み頻度を下げる設計(バッファリング、ラウンドロビン書き込み)が必要になる。
実装・開発のポイント
eMMC/SDカードへのext4最適化
# ext4フォーマット時のオプション(組み込み向け)
mkfs.ext4 \
-L rootfs \ # ラベル
-b 4096 \ # ブロックサイズ(一般的な4KB)
-i 8192 \ # iノード間隔(多くのsmallファイルがあれば小さく)
-O ^has_journal \ # ジャーナル無効(高速化、電源断リスクあり)
-E lazy_itable_init=0 \ # iテーブル初期化をフォーマット時に完了
/dev/mmcblk0p2
# マウントオプションの最適化
# noatime: アクセス時刻の更新を省略(フラッシュへの書き込み削減)
# commit=60: ジャーナルコミット間隔を60秒に(書き込み削減)
mount -o noatime,commit=60 /dev/mmcblk0p2 /
フラッシュ書き込みの最小化
組み込みフラッシュの寿命を延ばすための実装指針。
// 設定データの書き込み最小化の例
// 悪い例:毎回書き込む
void bad_save_config(void) {
for (;;) {
update_settings(); // 設定が変わるたびに
lfs_file_open(...); // フラッシュに書き込む(頻繁すぎる)
lfs_file_write(...);
lfs_file_close(...);
}
}
// 良い例:変更があった時のみ、かつ最小間隔を設ける
static bool config_dirty = false;
static uint32_t last_save_time = 0;
#define SAVE_INTERVAL_MS 5000 // 最低5秒間隔
void check_and_save_config(void) {
if (config_dirty &&
(HAL_GetTick() - last_save_time) > SAVE_INTERVAL_MS) {
save_config_to_flash();
config_dirty = false;
last_save_time = HAL_GetTick();
}
}
void on_config_changed(void) {
config_dirty = true; // 即書き込みではなくフラグを立てるだけ
}
ファイルシステムチェックと修復
# ext4のfsck(起動時の自動チェック)
# /etc/fstabの最後の数字: 0=チェックなし, 1=最優先, 2=2番目
# e2fsck手動実行(アンマウント後)
e2fsck -f /dev/mmcblk0p2
# ジャーナルを有効にして電源断耐性を高める
tune2fs -O has_journal /dev/mmcblk0p2
# NANDフラッシュのバッドブロック確認
mtd_debug read /dev/mtd0 0 1 # 先頭ブロックのテスト
nandtest /dev/mtd0 # 全ブロックテスト(破壊的!)
他技術との比較
| ファイルシステム | ext4 | FAT32 | UBIFS | LittleFS | squashfs |
|---|---|---|---|---|---|
| 用途 | Linux汎用 | 可搬媒体 | raw NAND | MCUフラッシュ | 読み取り専用 |
| ジャーナリング | あり | なし | あり | ログ構造 | なし(必要なし) |
| 最大ボリューム | 1EB | 2TB | 非制限 | 設計依存 | 非制限 |
| 圧縮対応 | なし(標準) | なし | あり(LZO等) | あり | あり(多形式) |
| フラッシュ特化 | なし | なし | あり | あり | なし |
| ウェアレベリング | FTL依存 | FTL依存 | UBIが担当 | 内蔵 | なし |
| 電源断安全 | あり(journal) | なし | あり | あり | 不変 |
| Linuxサポート | 標準 | 標準 | 標準 | ライブラリ必要 | 標準 |
ウェアレベリングとの関係
フラッシュメモリは書き込み回数に上限があり(NANDは数千〜数万回、NORは数万〜数十万回)、特定ブロックへの集中書き込みを避けるウェアレベリングが必要だ。eMMCやSDカードはコントローラが内部でウェアレベリングを行うが、raw NANDではUBIレイヤーがこれを担当する。LittleFSはMCU内蔵フラッシュ向けに内部でウェアレベリングを実装している。
Yocto・Buildrootでのファイルシステムイメージ生成
YoctoやBuildrootはターゲットデバイス向けのファイルシステムイメージ(ext4, squashfs, ubifs等)を自動生成できる。IMAGE_FSTYPES変数でイメージ形式を指定し、ビルドシステムが適切なツール(mkfs.ext4, mksquashfs, mkfs.ubifs)を使ってイメージを生成する。