最終更新: 2026-11-10
SylFS(Sylph Filesystem / Sylphid FS) は、AtmOS のためにゼロから設計・実装された高性能・高信頼性を誇る独自ファイルシステムです。
組み込み環境(Raspberry Pi 3B+ / ARMv8 64-bit)における極めて過酷な運用条件(突然の電源断、microSD カードのビット腐敗、マルチコアからの並行アクセス、フラッシュメモリの摩耗)に耐えうる堅牢性と、Web ブラウザのマルチメディアキャッシュやシステムフォント(数 MB 〜 数十 MB)をストレスなく扱う高速性を両立させる設計思想を持っています。
/cache/)やテンポラリ(/tmp/)は専用の広大な
RAMDisk(揮発性メモリ、最大 332MB)へ自動的にルーティングされます。RecoverableMutex(FS_LOCK)で保護されています。万一いずれかのコアがパニックした場合でも、該当コアが保持していたロックのみを強制回収(force
release)し、OS 全体のハングアップを回避します。SylFS は、アプリケーション層から物理ストレージハードウェアまで、明確に分離された多層アーキテクチャで構成されています。
+-----------------------------------------------------------------------+
| アプリケーション層 (Web Browser, Desk, Shell) |
| |
| Aura スクリプト (fs.*, ls, cat, save, show, find, cp, mv, rm...) |
+-----------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------+
| VFS / SylFS Core インターフェース |
| |
| mount() / save_file() / read_file_checked() / delete_file() |
| find_by_label() / list_files() / list_dir() |
| FS_LOCK (RecoverableMutex) 排他制御 & パニック時ロック自動回収 |
+-----------------------------------------------------------------------+
| |
v (通常パス / 設定 / アセット) v (一時パス: /cache/, /tmp/)
+------------------------------------+ +-----------------------+
| Persistent SylFS (不揮発性) | | Volatile SylFS (RAM) |
| - COW B-Tree インデックス | | - 専用 RAMDisk |
| - エクステントアロケータ | | - 寿命保護・高速処理 |
| - 空きセクタビットマップ | +-----------------------+
+------------------------------------+ |
| |
v |
+-----------------------------------------------------------v-----------+
| MirroredBlockDevice (2重化 & 自己修復レイヤ) |
| |
| - 508B データ + 4B CRC32 チェックサム付与・検証 |
| - SD カード: ミラーリング有効 (Primary + Mirror, 容量 50%) |
| - RAMDisk: ミラーリング無効 (単一領域, 容量 100% 活用) |
| - 読み出し時 CRC 不一致の検知とプライマリ自動書き戻し (Healing) |
+-----------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------+
| BlockDevice (ハードウェア抽象化層) |
| |
| - SDCard: EMMC/SDHOST コントローラ (オフセット 256MiB, FAT32 保護) |
| - RAMDisk: 物理連続メモリ直接マッピング (ヒープ後方 332MB) |
+-----------------------------------------------------------------------+
物理ストレージの最小読み書き単位は 512 バイト です。SylFS ではデータの信頼性を物理ブロックレベルで担保するため、セクタの末尾 4 バイトを CRC32 チェックサム領域として予約しています。
+---------------------------------------------------------------+
| 物理セクタ (512 Bytes) |
+-----------------------------------------------+---------------+
| 有効データ領域 (DATA_SIZE_PER_SECTOR) | CRC32 チェック |
| 508 Bytes | 4 Bytes |
+-----------------------------------------------+---------------+
0 508 512
SECTOR_SIZE: 512
バイト(物理デバイスとの最小転送単位)DATA_SIZE_PER_SECTOR: 508
バイト(ファイルシステム構造体が利用可能な最大長)すべてのデータブロック、メタデータブロック、インデックスノード、ビットマップブロックは、この
508 バイト境界内に完全に収まるよう #[repr(C, packed)]
かつコンパイル時アサーション(assert!(size_of::<T>() <= DATA_SIZE_PER_SECTOR))により保証されています。
SylFS の物理デバイス(またはパーティション)上のレイアウトは以下のとおりです。
Sector 0 Sector 1..N Sector N+1..
+---------------+---------------------+-----------------------------------------+
| Superblock | Allocation Bitmap | Data Blocks / Metadata / B-Tree Nodes |
| (1 Sector) | (動的セクタ数) | (COW 動的割り当て領域) |
+---------------+---------------------+-----------------------------------------+
SUPERBLOCK_SECTOR):
ファイルシステムの根本情報(マジック、バージョン、B-Tree
ルートセクタ、ビットマップ開始セクタ・セクタ長)を保持。BITMAP_START_SECTOR 〜
BITMAP_START_SECTOR + bitmap_sectors - 1):
全セクタの割当状態を 1 ビット単位で管理するビットマップ。FileMetadata)、インデックスノード(BTreeNode)、溢れエクステントセクタが混在して動的に確保されるプール。ファイルシステムの最重要ヘッダであり、常にセクタ 0 に配置されます。変更のコミット時に最後に書き込まれます。
#[repr(C, packed)]
#[derive(Clone, Copy)]
pub struct Superblock {
pub magic: [u8; 8], // 0x00..0x08: "SYLFS\0\0\0"
pub version: u32, // 0x08..0x0C: バージョン番号 (現在 1)
pub root_sector: u32, // 0x0C..0x10: B-Tree インデックスのルートセクタ番号
pub bitmap_start: u32, // 0x10..0x14: ビットマップ領域の開始セクタ (通常 1)
pub bitmap_sectors: u32, // 0x14..0x18: ビットマップ領域が占有するセクタ数
}| オフセット | フィールド名 | 型 | 説明 |
|---|---|---|---|
0x000 |
magic |
[u8; 8] |
SylFS 識別マジック
(SYLFS\0\0\0 =
[0x53, 0x59, 0x4C, 0x46, 0x53, 0x00, 0x00, 0x00]) |
0x008 |
version |
u32 |
フォーマットバージョン(現行バージョンは
1) |
0x00C |
root_sector |
u32 |
ファイル名/ラベル探索用 B-Tree
の根ノードが格納されているセクタ番号(空の場合は 0) |
0x010 |
bitmap_start |
u32 |
空き領域管理ビットマップの開始セクタ(1) |
0x014 |
bitmap_sectors |
u32 |
ビットマップが占めるセクタ数(ボリューム容量から動的算出) |
[!NOTE]
Superblockの構造体サイズは 24 バイトであり、セクタの先頭 24 バイトに書き込まれます。残りの 484 バイトは予約領域(将来の機能拡張用パディング)となります。
各ファイルの属性、名前、ラベル、サイズ、およびデータブロックを指すエクステントリストを格納します。1 ファイルにつき 1 つのセクタを消費します。
#[repr(C, packed)]
#[derive(Clone, Copy)]
pub struct FileMetadata {
pub filename: [u8; 128], // 0x000..0x080: パス名 (UTF-8, NUL終端)
pub label_data: [u8; 64], // 0x080..0x0C0: ラベル一覧 (カンマ区切り, UTF-8, NUL終端)
pub size: u64, // 0x0C0..0x0C8: ファイルサイズ (バイト単位, 最大 16EB)
pub extent_count: u32, // 0x0C8..0x0CC: 有効エクステントの総数
pub overflow_sector: u64, // 0x0CC..0x0D4: 溢れエクステントセクタへのポインタ (0なら無し)
pub extents: [Extent; MAX_INLINE_EXTENTS],// 0x0D4..0x1F4: インラインエクステント配列 (18エントリ)
}| オフセット | フィールド名 | サイズ | 説明 |
|---|---|---|---|
0x000 |
filename |
128 バイト | ファイルの絶対パスまたは名前(UTF-8、最大
127 文字+NUL終端)。先頭バイトが 0
の場合は「削除済み」とみなす |
0x080 |
label_data |
64 バイト | カンマ区切りのラベル文字列(例:
"sys,font,cache"、最大 63 文字+NUL終端) |
0x0C0 |
size |
8 バイト (u64) |
ファイルの正確な実データ長(バイト数)。264 − 1 バイトまでサポート |
0x0C8 |
extent_count |
4 バイト (u32) |
ファイル全体を構成するエクステント(連続領域)の総数 |
0x0CC |
overflow_sector |
8 バイト (u64) |
インライン(18本)に収まらなかったエクステントを連鎖格納するセクタ番号(不要時は
0) |
0x0D4 |
extents |
288 バイト | Extent 構造体(16 バイト)×
18 エントリ(インラインエクステント) |
合計サイズ: 128 + 64 + 8 + 4 + 8 + 288 = 500 バイト( ≤ 508 バイトで 1 セクタに完全に収まる)。
ファイルを構成するデータセクタの連続領域を効率的に表現します。
#[repr(C, packed)]
#[derive(Clone, Copy, Default)]
pub struct Extent {
pub start: u64, // 開始セクタ番号
pub count: u64, // 連続セクタ数 (0なら未使用)
}従来のブロックポインタ列挙方式では、1
セクタあたり数十〜数百個のセクタ番号しか保持できず、間接ブロックを用いても数
MB で容量上限に達していました。SylFS
では連続したセクタ確保が行われた場合、どれほど巨大なファイルであっても単一のエクステント(例:
start: 1000, count: 20480 = 10MB)で表現できます。
overflow_sector)ファイルの断片化(フラグメンテーション)が進み、エクステント数が 18 個を超えた場合、残りのエクステントは溢れセクタのリンクリストへ書き出されます。
FileMetadata
overflow_sector ──> [ Overflow Sector 1 ]
├── next_sector: 1542 ──> [ Overflow Sector 2 ]
├── Extent 19 ├── next_sector: 0 (終端)
├── Extent 20 ├── Extent 50
└── ... (最大31エントリ) └── ...
next_sector(次の溢れセクタ番号、0
で終端)Extent 配列((508 − 8)/16 = 31 エントリ)ディレクトリ探索および高速ファイル検索を行う COW B-Tree ノードのバイナリ表現です。
pub const MAX_KEYS: usize = 24;
pub const KEY_LEN: usize = 16; // 128-bit FNV-1a ハッシュ
#[repr(C, packed)]
#[derive(Clone, Copy)]
pub struct BTreeNode {
pub node_type: u8, // 0: Leaf, 1: Internal
pub num_keys: u8, // 現在格納されているキー数 (最大 24)
pub padding: [u8; 2], // 4バイト境界アライメントパディング
pub keys: [[u8; KEY_LEN]; MAX_KEYS + 1], // キー配列 (25エントリ: 分割時の一時オーバーフロー用+1)
pub values: [u32; MAX_KEYS + 2], // 値配列 (26エントリ: 葉=Metadataセクタ, 内部=子ノードセクタ)
}keys):
ファイル名文字列から計算された FNV-1a 128-bit ハッシュ値(16
バイト)。values):
node_type == 0):
values[i] は該当ファイルの FileMetadata
が格納されたセクタ番号。node_type == 1):
values[i]
は左側の子ノードセクタ番号、values[num_keys]
は最右端の子ノードセクタ番号。SylFS におけるファイルの保存・更新は、厳密な COW 手順に従って実行されます。途中で電源が断たれた場合でも、ディスク上の旧構造は一切破壊されず残存します。
sequenceDiagram
autonumber
participant App as 呼び出し元 (Aura / Kernel)
participant FS as SylFS Core
participant Bmp as 空きビットマップ
participant Dev as MirroredBlockDevice
participant Disk as 物理ストレージ (SD / RAM)
App->>FS: save_file("sample.txt", data, "text")
Note over FS: 1. データセクタの確保と書き込み
FS->>Bmp: allocate_sector() × 必要数
Bmp-->>FS: セクタ番号 (連続ならエクステント集約)
FS->>Dev: write_sector(data_sec, data_chunk)
Dev->>Disk: プライマリ & ミラー書き込み (CRC32付与)
Note over FS: 2. メタデータの作成と書き込み
FS->>Bmp: allocate_sector() (メタデータ用)
Bmp-->>FS: meta_sec
FS->>Dev: write_sector(meta_sec, FileMetadata)
Note over FS: 3. B-Tree インデックスへの COW 再帰挿入
FS->>FS: insert_recursive() (根から葉までのパスを複製)
FS->>Bmp: allocate_sector() (更新されたノード用)
FS->>Dev: write_sector(new_node_sec, new_node)
Note over FS: 古いノードは replaced リストへ保持
Note over FS: 4. Superblock 更新によるアトミックコミット
FS->>Dev: write_sector(0, NewSuperblock { root: new_root })
Dev->>Disk: 【ここで状態が確定】
Note over FS: 5. 不要となった旧セクタの安全解放
FS->>Bmp: deallocate_sector() (replaced 節点)
FS->>Bmp: free_file_sectors() (上書き前の旧メタデータ&データ)
FS-->>App: Ok(())
手順 4(Superblock
の更新)が完了する前にクラッシュが発生した場合、セクタ 0 は古い
root_sector
を指したままです。新しく書き込まれたデータセクタや節点セクタは孤立しますが、次回の通常操作やファイルシステムチェック時に安全に再利用され、既存ファイルシステムが不整合になることは構造上あり得ません。
B-tree
の値は、同じハッシュのメタデータの連鎖(collision_next_sector)の先頭を指す。保存し直すときは連鎖をたどって、次のように扱う。
| 連鎖の中の状況 | メタデータ | 前の版の解放 |
|---|---|---|
| 同じパスが先頭(衝突が無い普通の場合) | 新しいセクタに書き、木の値を差し替える | 確定の後でデータ・溢れ・メタデータを全部解放 |
| 同じパスが途中(衝突。まれ) | そのセクタをその場で書き換える(前の節を書き換えないため)。木は先頭を指したまま | 確定の後でデータ・溢れだけ解放 |
| 削除済みの節がある | それを使い直す。途中なら木は先頭を指したまま | なし(削除のときに解放済み) |
| どれも無い | 新しいセクタに書き、連鎖の先頭につなぐ | なし(連鎖の別のファイルは解放しない) |
前の版のデータは、確定より前には決して解放しない。確定の前に電源が切れても、前の版のメタデータが解放済みのセクタを指すことは無い。 2026-11-10 まではこの順序が崩れていた。前の版のデータを確定の前に解放してメタデータをその場で書き換えていたほか、衝突した新しいファイルを保存すると、連鎖の先頭の別のファイルを「前の版」として解放していた。 途中の節をその場で書き換える場合だけは、確定の前に電源が切れると新しい版が先に見えることがある(データは書き終えてから書き換えるので、壊れはしない)。
この OS では、電源はいつでもぶつ切りされる前提で書く。
切り替えの単位は 1 セクタの書き込み。 ミラーのある記憶装置(microSD)では、1 セクタを主 → ミラーの順に書き、読むときは CRC の合わない方を捨てる。主を書いている途中で切れると主の CRC が合わず、ミラー(古い内容)が使われて主も直る。主を書き終えた後に切れると、主(新しい内容)が使われる。したがって 1 セクタの書き込みは、電源断でも「古い内容か新しい内容のどちらか」になる(論理的な判断。中途半端な内容は読まれない)。RAMDisk は揮発なので、電源断の後に残る物が無い。
すべての操作は次の順に書く。
ビットマップの確保は 1 の前に書くので、電源断で残るのは「確保したが切り替える前」と「切り替えた後で解放する前」の漏れだけになる。
| 操作 | 新しい物を書く | 切り替え(1 セクタ) | 後で返す物 |
|---|---|---|---|
保存(save_file) |
データ・溢れ・メタデータ・木の写し | スーパーブロック(連鎖の途中の上書きは、そのメタデータ 1 セクタ) | 前の版のデータ・溢れ・メタデータ・写す前の木 |
削除(delete_file) |
木の写し(連鎖の先頭のとき) | スーパーブロック、または手前の節 1 セクタ | データ・溢れ・メタデータ・写す前の木 |
名前の変更(rename_file) |
新しいメタデータ・連鎖の手前の写し・木の写し(2 つのキー) | スーパーブロック | 古いメタデータ・写す前の節と木 |
切り詰め(truncate_file) |
新しい溢れのチェーン | そのメタデータ 1 セクタ | 要らなくなったデータ・古い溢れのチェーン |
| 伸長 | (保存と同じ) | ||
| ラベルの更新 | なし | そのメタデータ 1 セクタ | なし |
起動時の回収(recover_after_power_loss、mount
で呼ぶ)。
スーパーブロックから届くセクタ(木の節点・メタデータの連鎖・データ・溢れ・ビットマップ)に印を付け、ビットマップを「届くセクタだけ使用中」に書き直す。電源断で漏れたセクタはここで戻る。届くのに空きのセクタ(破損)は使用中に直す。読めないセクタがあるときは、その先に印を付けられない恐れがあるので、漏れは解放しない(データを壊す側に倒さない)。
試験(arm/v1/tests/src/test_fs_power_loss.rs)。
RAMDisk の write_budget で「n
回書いた後の書き込みを全部失敗させる」(電源断と同じく、以後ディスクは変わらない)。保存・上書き・削除・名前の変更(溢れのある大きいファイルも)・切り詰め(溢れが残る/無くなる)・伸長・ラベルの更新のそれぞれで、切れ目
n を 0 から最後まで全部試し、開き直して回収した後に、fsck が健全で漏れが
0、ファイルの集合と中身が操作の「前」か「後」のどちらかにそっくり一致することを確かめる。修正前は、名前の変更(2
つの名前が同じデータを指す)と切り詰め(古い版の末尾をその場で 0
で埋めていたため、前とも後とも違う中身)が失敗した。
MirroredBlockDevice
は、ブロックデバイス層の上位に位置し、ストレージの障害耐性を提供します。
flowchart TD
Start([セクタ読み込み要求: sector]) --> ReadP[プライマリセクタ読み込み]
ReadP --> CheckP{プライマリの<br/>CRC32 は正常か?}
CheckP -- 一致 (健全) --> ReturnData[バッファへデータ返却: 成功]
CheckP -- 不一致 (破損検知) --> IsMirrored{デバイスは<br/>二重化対象か?}
IsMirrored -- No (RAMDisk) --> ErrRAM[CRC mismatch エラー返却]
IsMirrored -- Yes (SDCard) --> WarnP[警告ログ出力 & ミラー探索開始]
WarnP --> ReadM[ミラーセクタ読み込み<br/>sector + usable_sectors]
ReadM --> CheckM{ミラーの<br/>CRC32 は正常か?}
CheckM -- 一致 (健全) --> Heal[★ 自己修復: Self-Healing<br/>ミラーの健全データを<br/>プライマリセクタへ書き戻し]
Heal --> ReturnData
CheckM -- 不一致 (両系破損) --> FatalErr[二重系全損エラー: Dual-redundancy failure<br/>データ復旧不能]
容量計算:
should_mirror() == true
となり、デバイスの物理セクタ数の半分(usable_sectors = total_sectors / 2)が利用可能セクタ数となります。自己修復の実装コード(抜粋):
if crc_stored == crc_calc {
// プライマリ正常
data[..DATA_SIZE_PER_SECTOR].copy_from_slice(&buf[..DATA_SIZE_PER_SECTOR]);
return Ok(());
} else if self.mirrored {
crate::warn!("CRC mismatch on Primary Sector {}. Initiating Self-Healing...", sector);
let mirror_sector = sector + self.usable_sectors;
if self.backend.read_raw_sector(mirror_sector, &mut buf).is_ok() {
if check_crc(&buf) {
crate::info!("Mirror Sector {} is healthy. Healing Primary Sector {}...", mirror_sector, sector);
let _ = self.backend.write_raw_sector(sector, &buf); // 修復書き込み
data[..DATA_SIZE_PER_SECTOR].copy_from_slice(&buf[..DATA_SIZE_PER_SECTOR]);
return Ok(());
}
}
return Err("read_sector: both primary and mirror corrupted");
}SylFS では、単一の木構造の中で「葉ノード」と「内部ノード」で異なる分割規則を採用したハイブリッド方式を用いています。
MAX_KEYS = 24
を超過した際)、中央のキー Kmid
は親ノードへ分離キーとして「コピー引き上げ」され、同時に右の兄弟ノードの先頭にも残されます。これにより、葉ノードを順次走査するだけで全エントリを取りこぼしなく列挙できます。pub fn fnv1a_128(data: &[u8]) -> [u8; 16] {
let mut hash: u128 = 0x6c62272e07bb014262b821756295c58d;
for &b in data {
hash ^= b as u128;
hash = hash.wrapping_mul(0x0000000001000000000000000000013B);
}
hash.to_le_bytes()
}ファイルシステム全体のセクタ数に応じて、必要なビットマップセクタ数が自動算出されます。
$$\text{bitmap\_sectors} = \left\lceil \frac{\text{total\_usable\_sectors}}{508 \times 8} \right\rceil$$
1 つのビットマップセクタ(508 バイト)で 508 × 8 = 4, 064 セクタ(約 2MB)を管理できます。1GB のストレージであっても約 515 セクタ(約 260KB)のビットマップで全域を完全にカバーします。
next_bitmap_hint)ファイルを保存する際、毎回ビットマップの先頭(セクタ
1)から走査すると、ディスクが使用されるにつれて走査コストが O(N2)
に悪化し、メガバイト単位のファイル書き込み時に数秒のレイテンシが発生します。
SylFS では next_bitmap_hint
に前回確保したビットマップセクタ番号を記憶させ、次回の探索をその位置から開始します。これにより連続確保が事実上
O(1)
で完了し、ディスク満杯時にはリング状に一巡して全空き領域を漏れなく検知します。
ビットマップセクタの総ビット数は通常、デバイスの有効セクタ数よりわずかに多くなります(端数)。SylFS
は allocated_sector >= total_usable_sectors
を厳格に検査し、実在しない架空のセクタ番号が払い出されるのを防ぎます。
pub const TEMP_PREFIXES: &[&str] = &["/cache/", "/tmp/"];
pub fn target_for(path: &str) -> StorageTarget {
let p = path.trim();
for prefix in TEMP_PREFIXES {
if p.starts_with(prefix) {
return StorageTarget::Volatile; // RAM 上。電源断で消えてよい
}
}
StorageTarget::Persistent // SD カード。電源断後も残す
}mmu::DEVICE_REGION_START)までの広大な物理
RAM 領域(実測約 332MB)をヒープ外メモリとして直接 SylFS
に割り当てます。/cache/ 配下に保存され、SD カードの NAND
フラッシュ書き換え回数を一切消費しません。SylFS は、マルチコア並行アクセスおよびパニック時の耐障害性を確保するため、以下の同期機構を備えています。
FS_LOCK
(RecoverableMutex<()>)pub static FS_LOCK: crate::kernel::reclock::RecoverableMutex<()> =
crate::kernel::reclock::RecoverableMutex::new(());get_fs() を呼び出してビットマップや B-Tree
を操作すると、セクタの二重割り当てやインデックス破壊が発生します。let _guard = crate::kernel::fs::FS_LOCK.lock();
を取得しなければなりません。force_release_locks): AtmOS は
panic = "abort"
で動作しており、スタックの巻き戻し(Unwinding)が行われません。デコーダ等の処理中に特定コアでパニックが発生した場合、通常のミューテックスではロックが永久に握られたままになり、OS
全体がデッドロック(ハングアップ)します。 RecoverableMutex
はロックを保持している Core ID を記録しており、パニックハンドラから
force_release_locks(core_id)
を呼び出すことで、異常終了したコアのロックのみを安全に解放し、健全な他コアの
FS 処理を継続させます。pub fn mount() -> Result<(), &'static str>ファイルシステムをマウントします。microSD
カード(SDHOST)の初期化を試行し、検出できた場合は SDCard
バックエンドでマウントします。初期化に失敗した場合は Dedicated RAMDisk
へ自動フォールバックします。未フォーマットの場合は自動的にフォーマットを実行します。また、/cache/
等の一時ファイル用 RAMDisk
の紐付けと、プリインストールアプリ(APP_ASSETS)の整合性確認を行います。
pub fn format_device(device: &mut MirroredBlockDevice<block::BackendDevice>) -> Result<Superblock, &'static str>ブロックデバイスを SylFS 形式で初期化(フォーマット)します。ビットマップ領域およびセクタ 0 のスーパーブロックを正しく書き込みます。
pub fn is_mounted() -> boolSylFS がマウント済みであるかを返します。
pub fn get_fs() -> &'static mut SylFSグローバルな SylFS
インスタンスへの可変参照を取得します。呼び出し時は必ず
FS_LOCK を保持している必要があります。
pub fn save_file(&mut self, filename: &str, content: &[u8], labels: &str) -> Result<(), &'static str>ファイルを保存または完全上書きします(COW 方式)。 -
引数: - filename: 保存先のパス(例:
"/user/memo.txt"、最大 127 バイト) - content:
ファイルデータの生バイト列 - labels:
検索用ラベル(カンマ区切り、例: "user,doc"、最大 63
バイト) - 戻り値: 成功時は
Ok(())、容量不足やハードウェア書き込み失敗時は
Err(&'static str)
pub fn read_file_checked(&mut self, filename: &str) -> Result<Option<(FileMetadata, Vec<u8>)>, &'static str>指定されたパスのファイルを読み込みます。「存在しない」ことと「破損等の異常で読めない」ことを明確に区別して返します。
- 引数: - filename: 読み出すファイルのパス
- 戻り値: - Ok(Some((meta, data))):
正常に読み出し完了 - Ok(None): ファイルが存在しない(正常)
- Err("metadata sector unreadable"):
メタデータが破損しており読み込めない -
Err("filename hash collision"): ハッシュ衝突が発生した -
Err("extent list unreadable"):
エクステント情報が読み込めない -
Err("data sector unreadable"):
データブロックが破損しており両系復旧も失敗した -
Err("file data is truncated"):
実データが途中で途切れている
pub fn read_file(&mut self, filename: &str) -> Option<(FileMetadata, Vec<u8>)>read_file_checked
の簡易ラッパーです。エラー発生時はカーネルエラーログを出力し
None を返します。
pub fn delete_file(&mut self, filename: &str) -> Result<(), &'static str>ファイルを削除します。 - メタデータセクタ内の
filename[0] を
0(NUL)に書き換えて論理削除マークを付与します。 -
当該ファイルが占有していた全データセクタおよび溢れエクステントセクタをビットマップ上で即座に解放(deallocate_sector)します。
- B-Tree
のダングリングポインタ化を防ぐため、メタデータセクタ自身は予約セクタとして残します。
pub fn list_files(&mut self) -> Vec<FileMetadata>ファイルシステム内に存在するすべての有効な通常ファイル(削除済みを除く)のメタデータ一覧を返します。SD カードおよび RAMDisk(一時領域)の双方が結合されます。
pub fn list_dir(&mut self, prefix: &str) -> Vec<FileMetadata>指定されたディレクトリパス(接頭辞
prefix)で始まるファイルをすべて列挙します。
pub fn find_by_label(&mut self, label: &str) -> Vec<FileMetadata>指定されたラベル(例:
"font")を持つすべてのファイルをメタデータ走査により高速抽出します。
pub fn fsck(&mut self) -> Result<FsckReport, &'static str>ファイルシステム全体の整合性検査(fsck)を実行し、統計情報と健全性フラグを返します。
- 検査項目(2026-11-10 に到達性の検査へ強化): -
スーパーブロック・ビットマップ・B-tree
の全節点・メタデータ(衝突の連鎖の 2
個目以降も)・データ・溢れのチェーンから実際に届くセクタに印を付ける。
- 印とビットマップを突き合わせる:
使用中だが届かない=漏れ(leaked_sectors)、届くのに空き=破損(unallocated_refs。次の確保で上書きされる)、2
か所から届く=二重参照(double_refs)。範囲外の番号・読めないセクタは
corrupted_sectors。 - is_healthy
は破損・二重参照・範囲外が 0
のとき真。漏れはデータを壊さないので健全のまま、数だけ報告する。 -
戻り値: 成功時は
Ok(FsckReport)(is_healthy,
total_sectors, used_sectors,
file_count, extent_count,
btree_node_count, corrupted_sectors,
leaked_sectors, unallocated_refs,
double_refs) -
以前はビットマップの使用数を数え、木の葉が指す先頭のメタデータだけを見ていたので、漏れも解放済みのセクタを指す破損も、衝突の連鎖の
2 個目以降のファイルも見えなかった。 - 試験
arm/v1/tests/src/test_fs_fsck_random.rs
は、断片化させた上で保存・上書き・削除・名前の変更・切り詰め・伸長を無作為に約
3,000 回繰り返し、毎回この fsck(漏れも 0)と中身を確かめる。
Aura シェルおよびスクリプト環境から直接呼び出し可能なファイルシステム関連の組み込み関数一覧です。
| コマンド構文 | 引数 | 説明 | 実行例 |
|---|---|---|---|
ls [options] [path] |
options: "-a",
"-l", path: ディレクトリ |
ファイル・ディレクトリの一覧を表示 | ls("-la" "/user/") |
cd path |
path:
移動先ディレクトリパス |
カレントワーキングディレクトリ
(cwd) を変更 |
cd("/user/docs") |
pwd |
なし | 現在のカレントワーキングディレクトリを表示 | pwd() |
mkdir path |
path:
作成するディレクトリ名 |
ディレクトリを作成(内部で
.keep ファイルを生成) |
mkdir("/user/music") |
cp src dst |
src: コピー元,
dst: コピー先 |
ファイルを複製 | cp("a.txt" "b.txt") |
mv src dst |
src: 移動元,
dst: 移動先 |
ファイルを移動(リネーム) | mv("old.txt" "new.txt") |
rm path |
path:
削除対象ファイルパス |
ファイルを削除し、データセクタを解放 | rm("temp.log") |
cat path |
path: ファイルパス |
テキストファイルの内容を文字列として取得・表示 | cat("/sys/config.json") |
grep pattern path |
pattern: 検索文字列,
path: ファイル |
ファイル内を行単位で部分一致検索 | grep("ERROR" "/tmp/log") |
head path [lines] |
path: ファイル,
lines: 行数 (省略時10) |
ファイルの先頭 N 行を取得 | head("/sys/hosts" 5) |
tail path [lines] |
path: ファイル,
lines: 行数 (省略時10) |
ファイルの末尾 N 行を取得 | tail("/tmp/log" 20) |
save name data [labels] |
name: パス,
data: 文字列, labels: タグ |
ファイルを新規保存または完全上書き | save("m.txt" "hello" "memo") |
show filename |
filename: パス |
メタ情報(ラベル、サイズ)とともにファイル内容を表示 | show("m.txt") |
find label |
label: 検索タグ文字列 |
指定ラベルを持つ全ファイルを一覧表示 | find("memo") |
addlabel filename label |
filename: パス,
label: 追加タグ |
既存ファイルに新しいラベルを付与 | addlabel("m.txt" "work") |
fsck |
なし | ファイルシステム整合性検査: ディスクの健全性・使用セクタ・ファイル統計を表示 | fsck() |
corrupt sector |
sector: 破壊するセクタ番号
(整数) |
デバッグ専用: 指定セクタをゼロクリア破壊(自己修復テスト用) | corrupt(120) |
| エラーメッセージ | 発生原因 | システムの動作・対処法 |
|---|---|---|
read_sector: both primary and mirror corrupted |
プライマリセクタ・ミラーセクタの双方が物理的または電気的に破損し、CRC32 検証に失敗した | 復旧不能エラー。データ損失を防止するため不正な値は返さず即座にエラーを伝播する |
save_file: no free data sector
(Disk Full) |
ストレージの空きセクタビットマップに
0(空き)が存在しない |
保存処理を中断し、それまでに確保したデータセクタを直ちに全解放してセクタリークを防ぐ |
filename hash collision |
異なるファイル名から同一の FNV-1a 128-bit ハッシュ値が生成された | 誤ったファイル内容を返さないよう安全に中断。128-bit ハッシュのため実用上の発生確率は約 10−19 以下 |
metadata sector unreadable |
ファイルのメタデータセクタの読み込みに失敗した(CRC 不一致等) | ファイルが存在しないのではなくストレージ障害と判定。誤って既定値で上書き破棄しないよう上位層にエラー通知 |
btree: node sector out of range |
B-Tree ノード内に記録されていた子セクタ番号がデバイスの有効セクタ数を超えている | インデックス破損を検知。クラッシュを未然に防ぎ、不正な探索ループを打ち切る |
file data is truncated |
メタデータに記録されたファイルサイズに達する前にセクタチェーンが途切れた | 不完全なデータを正常として返さず、データ破損として呼び出し元へ報告する |
SylFS では、Aura の corrupt
コマンドおよびカーネル単体テストを通じて、意図的に特定セクタの生データを破壊し、自己修復機能が正しく動作するかを検証できます。
;; 1. テストファイルを保存
(save "/user/test.txt" "AtmOS Resilient Storage Data" "test")
;; 2. ファイルのメタ情報とセクタを確認して生セクタを意図的に破壊
(corrupt 5)
;; 3. 再度読み出しを試みる
;; カーネルは CRC 不一致を検知し、ミラーセクタから自動修復して正常に内容を表示する
(cat "/user/test.txt")
;; ログ出力:
;; [WARN ] CRC mismatch on Primary Sector 5. Initiating Self-Healing...
;; [INFO ] Mirror Sector 524293 is healthy. Healing Primary Sector 5...
;; => "AtmOS Resilient Storage Data"| 相対パス | 主要責務 |
|---|---|
arm/v1/src/kernel/fs/mod.rs |
SylFS コア実装、Superblock/FileMetadata 定義、VFS API、空きセクタビットマップアロケータ、マウント処理、自己テスト |
arm/v1/src/kernel/fs/block.rs |
BlockDevice
トレイト、RAMDisk(ヒープ/物理RAM)、SDCard(SDHOST/FAT保護)、MirroredBlockDevice(二重化・自己修復・CRC32) |
arm/v1/src/kernel/fs/btree.rs |
ディスクベースの COW B-Tree
インデックス、BTreeNode、ノード分割・再帰挿入・探索アルゴリズム |
arm/v1/src/kernel/fs/checksum.rs |
CRC32(IEEE 802.3 多項式:
0xEDB88320)高速計算エンジン |
arm/v1/src/kernel/fs/storage_policy.rs |
パス別ストレージ振り分けポリシー(/cache/・/tmp/
の揮発性 RAMDisk 誘導) |
arm/v1/src/os_lib/aura/builtins/os_fs.rs |
Aura
スクリプト環境向け組み込みコマンドバインディング(ls,
cat, save, find 等) |
カーネルのソースコード(src/)に一切テストコードを混入させず、arm/v1/tests/
ディレクトリ配下の独立テストクレートから検証されます。ホスト環境(macOS
/ Linux)上で make test
により数ミリ秒で全件実行されます。
| テストファイル | テスト項目数 | 主な検証内容 |
|---|---|---|
arm/v1/tests/src/test_fs.rs |
24 件 | 基本的な保存・読み出し、上書き、削除、ラベル検索、ディレクトリ列挙、ROM フォールバック |
arm/v1/tests/src/test_fs_space_reuse.rs |
18 件 | ファイル上書き・削除時におけるデータセクタ、溢れエクステント、旧 B-Tree 節点の確実な解放とセクタ再利用(セクタリーク防止) |
arm/v1/tests/src/test_fs_error_propagation.rs |
15 件 | セクタ読み込み失敗・破損時における「存在しない」と「読めない」の厳格な区別、エラーの握り潰し防止 |
arm/v1/tests/src/kernel/sylfs/ |
32 件 | B-Tree の大規模ノード分割(1,000キー挿入)、自己修復(プライマリ破壊からの書き戻し復旧)、両系破損検知 |
use crate::kernel::fs::{get_fs, FS_LOCK};
pub fn save_system_config(config_json: &str) -> Result<(), &'static str> {
// 1. マルチコア排他ロックを取得
let _guard = FS_LOCK.lock();
let fs = get_fs();
// 2. ラベル "sys,config" を付与してアトミック保存
fs.save_file("/sys/config.json", config_json.as_bytes(), "sys,config")?;
crate::info!("[CONFIG] System configuration successfully updated.");
Ok(())
}
pub fn load_system_config() -> Result<String, &'static str> {
let _guard = FS_LOCK.lock();
let fs = get_fs();
// 存在しないことと破損を厳格に区別
match fs.read_file_checked("/sys/config.json")? {
Some((_meta, data)) => {
let text = alloc::string::String::from_utf8(data)
.map_err(|_| "Invalid UTF-8 in config")?;
Ok(text)
}
None => {
crate::warn!("[CONFIG] Config not found, generating default.");
Ok(alloc::string::String::from("{}"))
}
}
};; メモを作成して保存
(save "/user/notes/meeting.txt" "2026-11-09: SylFS 仕様策定ミーティング議事録" "work,meeting,spec")
(save "/user/notes/todo.txt" "1. ブラウザキャッシュ検証\n2. QEMU 音声出力テスト" "work,todo")
;; "meeting" タグがついたメモを一覧検索
(find "meeting")
;; 内容を確認
(cat "/user/notes/meeting.txt");; ディレクトリ作成
(mkdir "/user/archive")
;; ファイルをアーカイブへ移動
(mv "/user/notes/meeting.txt" "/user/archive/meeting_20261109.txt")
;; 一覧確認 (詳細表示)
(ls "-l" "/user/archive/")
;; 不要になったファイルの削除
(rm "/user/notes/todo.txt");; 最新のログ末尾 15 行を確認
(tail "/tmp/system.log" 15)
;; エラー行を抽出
(grep "ERROR" "/tmp/system.log")delete_file、2026-11-10)collision_next_sector
を次の節へつなぎ替える。連鎖の先頭なら、木の値を次の節(無ければ 0
=「削除した」印)へ差し替えてスーパーブロックを書き、確定する。search と一覧(collect_leaf_entries)は値 0
のキーを「無い」として扱う。以前は印を付けたメタデータを必ず 1
セクタ残したので、別々のパスを作っては消すと空きが減り続けた(512
セクタで 473 個目に満杯)。
一覧(collect_leaf_entries)の打ち切りは、深さ 32
で行う。以前はたどった節点の総数が 100
を超えると打ち切っていたので、ファイルが約 1,200 個を超えると一覧・fsck
から黙って漏れた(3000 個中 1483 個しか出なかった)。
rename_file、2026-11-10)以前は、新しい名前を確定した後に別の書き込みで古いメタデータへ削除の印を付けていたので、その間で電源が切れると 2 つの名前が同じデータを指したまま残った(電源断の試験で再現)。さらに以前は古い名前を先に削除済みにしていたので、木への登録に失敗するとファイルが消えた。
| 定数名 | 値 | 定義場所 | 説明 |
|---|---|---|---|
SECTOR_SIZE |
512 |
block.rs |
物理ストレージセクタサイズ(バイト) |
DATA_SIZE_PER_SECTOR |
508 |
block.rs |
セクタ内の有効データ領域長(512 - 4 バイト CRC) |
SYLFS_MAGIC |
b"SYLFS\0\0\0" |
mod.rs |
スーパーブロック識別マジック(8 バイト) |
SUPERBLOCK_SECTOR |
0 |
mod.rs |
スーパーブロックが配置される固定セクタ番号 |
BITMAP_START_SECTOR |
1 |
mod.rs |
空きセクタビットマップの開始セクタ番号 |
MAX_INLINE_EXTENTS |
18 |
mod.rs |
FileMetadata
内に直接保持可能なエクステント数 |
EXTENTS_PER_OVERFLOW_SECTOR |
31 |
mod.rs |
1 つの溢れセクタに格納可能なエクステント数 ((508 − 8)/16) |
MAX_KEYS |
24 |
btree.rs |
B-Tree ノード 1 つあたりに保持できる最大キー数 |
KEY_LEN |
16 |
btree.rs |
B-Tree インデックスキーのバイト長(128-bit ハッシュ) |
TEMP_PREFIXES |
&["/cache/", "/tmp/"] |
storage_policy.rs |
揮発性 RAMDisk に自動退避するパス接頭辞 |
| 項目 | 限界値 | 根拠・制約要因 |
|---|---|---|
| 単一ファイル最大サイズ | 264 − 1 バイト ( ≈ 18 EB) | FileMetadata.size(u64)およびエクステント表現による |
| ボリューム最大容量 | 232 × 508 バイト ( ≈ 2 TB) | セクタ番号の表現形式(u32)による物理限界 |
| ファイルパス最大長 | 127 バイト (UTF-8) | FileMetadata.filename(128
バイト、NUL終端必須) |
| ラベル文字列最大長 | 63 バイト (UTF-8) | FileMetadata.label_data(64
バイト、NUL終端必須) |
| 最大インラインエクステント数 | 18 エントリ | メタデータを 508 バイト以内へ収めるためのコンパイル時制限 |
| 最大溢れエクステント数 | 実質無制限 | 64-bit リンクリストチェーン構造による動的拡張 |
| B-Tree 最大分岐数 (Fan-out) | 25 分岐 | MAX_KEYS + 1 による(1 ノード
24 キー、25 ポインタ) |