Rustで作るx86_64 自作OS入門シリーズ
Part1【環境構築】 | Part2【no_std】 | Part3【Hello World】| Part4【VGA】 | Part5【割り込み】 | Part6【ページング】 | Part7【ヒープ】 | Part8【マルチタスク】| Part9【ファイルシステム】 | Part10【ELFローダー】 | Part11【シェル】 | Part12【完結】
はじめに
Part8でマルチタスクが動きました。async/await最高。
今回はファイルシステムを実装します。これがあると一気にOSっぽくなる!ディスクからファイルを読み書きできるようになります。
目次
ファイルシステムとは
ディスク上のデータを「ファイル」として扱う仕組み。
物理的なディスク: [セクタ0][セクタ1][セクタ2]...
↓
ファイルシステム: /home/user/hello.txt
代表的なファイルシステム:
| 名前 | 用途 | 特徴 |
|---|---|---|
| FAT32 | USBメモリ | シンプル、互換性高い |
| ext4 | Linux | ジャーナリング、高性能 |
| NTFS | Windows | ACL、圧縮 |
| APFS | macOS | SSD最適化、暗号化 |
今回は一番シンプルなFAT系を参考に実装します。
RAMディスクを使う理由
本物のディスクドライバ(ATA/SATAなど)を書くのは大変すぎる。
- ATA PIO: レジスタ操作が複雑
- AHCI: さらに複雑、DMAが必要
- NVMe: PCIe知識も必要
なので今回はRAMディスク(メモリ上の仮想ディスク)を使います。原理は同じ!
シンプルなファイルシステム実装
独自のシンプルなファイルシステムを作ります:
+----------------+
| ヘッダー (512B) | ファイル数、バージョンなど
+----------------+
| ファイル情報 1 | 名前、サイズ、データ位置
+----------------+
| ファイル情報 2 |
+----------------+
| ... |
+----------------+
| データ領域 | 実際のファイル内容
+----------------+
構造体定義
// src/fs/mod.rs
pub mod simple_fs;
use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone)]
pub struct FileInfo {
pub name: String,
pub size: usize,
pub is_dir: bool,
}
pub trait FileSystem {
fn read_file(&self, path: &str) -> Option<Vec<u8>>;
fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), &'static str>;
fn list_dir(&self, path: &str) -> Vec<FileInfo>;
fn exists(&self, path: &str) -> bool;
}
SimpleFS実装
// src/fs/simple_fs.rs
use super::{FileInfo, FileSystem};
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
const MAX_FILES: usize = 64;
const MAX_FILENAME: usize = 32;
#[repr(C)]
struct FileEntry {
name: [u8; MAX_FILENAME],
size: u32,
offset: u32,
flags: u8,
_padding: [u8; 3],
}
pub struct SimpleFs {
files: BTreeMap<String, Vec<u8>>,
}
impl SimpleFs {
pub fn new() -> Self {
let mut fs = SimpleFs {
files: BTreeMap::new(),
};
// 初期ファイルを作成
fs.files.insert("/hello.txt".to_string(), b"Hello from SimpleFS!\n".to_vec());
fs.files.insert("/readme.md".to_string(), b"# My OS\nThis is a simple OS.\n".to_vec());
fs
}
}
impl FileSystem for SimpleFs {
fn read_file(&self, path: &str) -> Option<Vec<u8>> {
self.files.get(path).cloned()
}
fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), &'static str> {
self.files.insert(path.to_string(), data.to_vec());
Ok(())
}
fn list_dir(&self, path: &str) -> Vec<FileInfo> {
self.files
.keys()
.filter(|name| {
if path == "/" {
name.matches('/').count() == 1
} else {
name.starts_with(path) && name[path.len()..].matches('/').count() == 1
}
})
.map(|name| FileInfo {
name: name.clone(),
size: self.files.get(name).map(|v| v.len()).unwrap_or(0),
is_dir: false,
})
.collect()
}
fn exists(&self, path: &str) -> bool {
self.files.contains_key(path)
}
}
グローバルファイルシステム
// src/fs/mod.rs
use spin::Mutex;
use lazy_static::lazy_static;
lazy_static! {
pub static ref FILESYSTEM: Mutex<SimpleFs> = Mutex::new(SimpleFs::new());
}
pub fn init() {
// 初期化(lazy_staticで自動的に初期化される)
let fs = FILESYSTEM.lock();
let files = fs.list_dir("/");
serial_println!("FS initialized with {} files", files.len());
}
pub fn read_file(path: &str) -> Option<Vec<u8>> {
FILESYSTEM.lock().read_file(path)
}
pub fn write_file(path: &str, data: &[u8]) -> Result<(), &'static str> {
FILESYSTEM.lock().write_file(path, data)
}
pub fn list_dir(path: &str) -> Vec<FileInfo> {
FILESYSTEM.lock().list_dir(path)
}
catコマンドっぽいもの
// src/main.rs
fn cat(path: &str) {
match fs::read_file(path) {
Some(data) => {
match core::str::from_utf8(&data) {
Ok(s) => println!("{}", s),
Err(_) => println!("(binary file, {} bytes)", data.len()),
}
}
None => println!("cat: {}: No such file", path),
}
}
main.rsに追加
mod fs;
fn kernel_main(boot_info: &'static BootInfo) -> ! {
// ... 前回までの初期化 ...
println!("=== File System Demo ===");
// ファイル一覧
println!("Files in /:");
for file in fs::list_dir("/") {
println!(" {} ({} bytes)", file.name, file.size);
}
// ファイル読み込み
println!("\n--- cat /hello.txt ---");
cat("/hello.txt");
// ファイル書き込み
println!("\n--- Creating /test.txt ---");
fs::write_file("/test.txt", b"This is a test file!\n").unwrap();
println!("--- cat /test.txt ---");
cat("/test.txt");
// 更新後のファイル一覧
println!("\nFiles in / (after write):");
for file in fs::list_dir("/") {
println!(" {} ({} bytes)", file.name, file.size);
}
// ... Executorの起動 ...
}
ビルドして確認
cargo bootimage
Compiling my-os v0.1.0 (C:\Users\Aqua\Documents\Qiita\my-os)
Finished `dev` profile [optimized + debuginfo] target(s) in 3.21s
Created bootimage for `my-os` at `target\x86_64-my_os\debug\bootimage-my-os.bin`
qemu-system-x86_64 -drive format=raw,file=target\x86_64-my_os\debug\bootimage-my-os.bin -serial stdio
=== File System Demo ===
Files in /:
/hello.txt (21 bytes)
/readme.md (33 bytes)
--- cat /hello.txt ---
Hello from SimpleFS!
--- Creating /test.txt ---
--- cat /test.txt ---
This is a test file!
Files in / (after write):
/hello.txt (21 bytes)
/readme.md (33 bytes)
/test.txt (21 bytes)
ファイルが読み書きできてる! めちゃくちゃOSっぽい!
ハマったところ
1. パスの正規化
最初、パスの扱いがバグだらけでした:
// ダメな例
"/hello.txt" // OK
"hello.txt" // 先頭の / がない!
"/dir//file" // スラッシュが重複!
"/dir/./file" // カレントディレクトリ!
正規化関数を追加:
fn normalize_path(path: &str) -> String {
let mut result = String::new();
if !path.starts_with('/') {
result.push('/');
}
for component in path.split('/').filter(|c| !c.is_empty() && *c != ".") {
result.push('/');
result.push_str(component);
}
if result.is_empty() {
result.push('/');
}
result
}
2. ファイル名の長さ
固定長配列で名前を保存してたら、長いファイル名で死んだ:
thread 'main' panicked at 'assertion failed: name.len() <= MAX_FILENAME'
今回はヒープを使える(Part7のおかげ!)のでStringを使って解決。
VFSの概念
本格的なOSではVirtual File System (VFS) レイヤーがあります:
アプリケーション
↓
VFS (統一インターフェース)
↓
+-------+-------+-------+
| ext4 | FAT32 | NFS | ← 各ファイルシステム実装
+-------+-------+-------+
今回のFileSystemトレイトが簡易VFSの役割。将来的には複数のファイルシステムをマウントできるようにしたい!
FATファイルシステムを使う(発展)
実際にFAT32を読みたい場合はfatfsクレートが使えます:
[dependencies]
fatfs = { version = "0.3", default-features = false }
use fatfs::{FileSystem, FsOptions};
let disk = /* ディスクドライバ */;
let fs = FileSystem::new(disk, FsOptions::new())?;
let root_dir = fs.root_dir();
for entry in root_dir.iter() {
let entry = entry?;
println!("{}", entry.file_name());
}
ただし、ディスクドライバが必要なのでそこが大変...
ディレクトリ対応(bonus)
impl SimpleFs {
pub fn mkdir(&mut self, path: &str) -> Result<(), &'static str> {
if self.exists(path) {
return Err("Directory already exists");
}
// ディレクトリは空のエントリとして保存
self.files.insert(format!("{}/", path), Vec::new());
Ok(())
}
pub fn is_dir(&self, path: &str) -> bool {
self.files.contains_key(&format!("{}/", path))
}
}
fs.mkdir("/docs")?;
fs.write_file("/docs/guide.txt", b"User guide...")?;
まとめ
Part9では以下を達成しました:
- ファイルシステムの基本概念
- RAMディスクベースの実装
- ファイルの読み書き
- ディレクトリ一覧
-
FileSystemトレイト(簡易VFS)
メモリ上のファイルシステムだけど、catとかlsっぽいことができるようになった!急にOSっぽくなってテンション上がる。
次回(Part10)はELFローダーを実装して、外部プログラムを実行できるようにします!
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!