이미지 로딩 중...

Rust로 만드는 나만의 OS - FAT32 파일시스템 구현 - 슬라이드 1/11
A

AI Generated

2025. 11. 13. · 2 Views

Rust로 만드는 나만의 OS - FAT32 파일시스템 구현

운영체제 개발의 핵심인 파일시스템을 직접 구현해봅니다. FAT32의 동작 원리부터 Rust로 구현하는 실전 기법까지, 시스템 프로그래밍의 깊이를 경험할 수 있는 완벽한 가이드입니다.


목차

  1. FAT32 구조 이해 - 파일시스템의 핵심 레이아웃
  2. FAT 테이블 읽기 - 클러스터 체인 탐색
  3. 디렉토리 엔트리 파싱 - 파일 메타데이터 추출
  4. 클러스터 읽기 구현 - 디스크 I/O 추상화
  5. 파일 읽기 통합 - 전체 파이프라인 연결
  6. 파일 쓰기 구현 - 빈 클러스터 할당과 FAT 갱신
  7. 디렉토리 생성 - 새 디렉토리 엔트리 추가
  8. LFN(Long File Name) 처리 - 긴 파일명 지원
  9. 에러 처리와 복구 - 파일시스템 안정성 강화
  10. 성능 최적화 - 캐싱과 비동기 I/O

1. FAT32 구조 이해 - 파일시스템의 핵심 레이아웃

시작하며

여러분이 운영체제를 만들면서 "파일을 어떻게 저장하지?"라는 고민을 해본 적 있나요? 디스크는 그냥 거대한 바이트 배열일 뿐인데, 이걸 어떻게 파일과 디렉토리로 구조화할까요?

이런 문제는 모든 OS 개발자가 마주하는 근본적인 도전입니다. 파일시스템 없이는 데이터를 체계적으로 관리할 수 없고, 결국 쓸모없는 OS가 되어버립니다.

바로 이럴 때 필요한 것이 FAT32 파일시스템의 구조 이해입니다. FAT32는 간단하면서도 효율적인 설계로, OS 개발 입문에 최적의 학습 대상입니다.

개요

간단히 말해서, FAT32는 디스크를 클러스터라는 단위로 나누고, FAT(File Allocation Table)이라는 테이블로 클러스터 체인을 관리하는 파일시스템입니다. 실무 관점에서 FAT32를 이해하는 것은 매우 중요합니다.

USB 드라이브, SD 카드 등 임베디드 시스템에서 여전히 널리 사용되고, 구조가 단순해서 직접 구현하기에 적합합니다. 예를 들어, 임베디드 리눅스 부팅 파티션이나 펌웨어 업데이트 시스템 같은 경우에 매우 유용합니다.

전통적인 방법과의 비교를 하면, ext4 같은 현대 파일시스템은 복잡한 B-tree와 저널링을 사용하지만, FAT32는 단순한 테이블과 링크드 리스트 방식으로 훨씬 이해하기 쉽습니다. FAT32의 핵심 특징은 1) Boot Sector에 파일시스템 메타데이터 저장, 2) FAT 테이블로 클러스터 체인 관리, 3) 디렉토리 엔트리로 파일 정보 저장입니다.

이러한 특징들이 중요한 이유는 각각 독립적으로 이해하고 구현할 수 있어, 점진적인 학습이 가능하기 때문입니다.

코드 예제

// FAT32 Boot Sector 구조체 정의
#[repr(C, packed)]
pub struct Fat32BootSector {
    jump_boot: [u8; 3],           // 부트 코드로 점프하는 명령
    oem_name: [u8; 8],            // OEM 이름
    bytes_per_sector: u16,        // 섹터당 바이트 수 (보통 512)
    sectors_per_cluster: u8,      // 클러스터당 섹터 수
    reserved_sectors: u16,        // 예약된 섹터 수
    num_fats: u8,                 // FAT 테이블 개수 (보통 2)
    root_entries: u16,            // 루트 디렉토리 엔트리 수 (FAT32에서는 0)
    total_sectors_16: u16,        // 총 섹터 수 (16비트, FAT32에서는 0)
    media_type: u8,               // 미디어 타입
    fat_size_16: u16,             // FAT 크기 (16비트, FAT32에서는 0)
    sectors_per_track: u16,       // 트랙당 섹터 수
    num_heads: u16,               // 헤드 수
    hidden_sectors: u32,          // 숨겨진 섹터 수
    total_sectors_32: u32,        // 총 섹터 수 (32비트)
    fat_size_32: u32,             // FAT 크기 (32비트)
    ext_flags: u16,               // 확장 플래그
    fs_version: u16,              // 파일시스템 버전
    root_cluster: u32,            // 루트 디렉토리 시작 클러스터
    fs_info: u16,                 // FSInfo 구조체 섹터 번호
    backup_boot_sector: u16,      // 백업 부트 섹터 위치
}

설명

이것이 하는 일: FAT32 Boot Sector는 파일시스템의 메타데이터를 담고 있는 첫 번째 섹터로, OS가 파일시스템을 마운트하는 데 필요한 모든 정보를 제공합니다. 첫 번째로, bytes_per_sectorsectors_per_cluster 필드를 통해 클러스터 크기를 계산합니다.

예를 들어 섹터당 512바이트, 클러스터당 8섹터면 클러스터 크기는 4KB입니다. 이렇게 하는 이유는 작은 단위(섹터)로 관리하면 오버헤드가 크기 때문에 더 큰 단위(클러스터)로 묶어 효율성을 높이기 위함입니다.

두 번째로, reserved_sectorsfat_size_32를 사용해 FAT 테이블과 데이터 영역의 위치를 계산합니다. FAT 테이블은 reserved_sectors 이후에 시작하고, 데이터 영역은 FAT 테이블 이후에 시작됩니다.

내부에서는 reserved_sectors + (num_fats * fat_size_32) 계산을 통해 정확한 오프셋을 구합니다. 세 번째로, root_cluster 필드가 루트 디렉토리의 시작점을 가리킵니다.

FAT32에서는 루트 디렉토리도 일반 파일처럼 클러스터 체인으로 관리되므로, 이 필드가 파일시스템 탐색의 진입점이 됩니다. 최종적으로 이 클러스터를 읽으면 루트 디렉토리의 파일 목록을 얻을 수 있습니다.

여러분이 이 구조체를 사용하면 디스크의 raw 바이트를 의미 있는 파일시스템 정보로 해석할 수 있습니다. 실무에서의 이점은 1) 타입 안전성: Rust의 타입 시스템으로 잘못된 필드 접근 방지, 2) 메모리 레이아웃 보장: #[repr(C, packed)]로 정확한 디스크 구조 매핑, 3) 유지보수성: 구조체 필드명으로 코드 가독성 향상입니다.

실전 팁

💡 #[repr(C, packed)] 속성은 필수입니다. C 언어와 동일한 메모리 레이아웃을 보장하고, padding을 제거해 디스크 구조와 정확히 일치시킵니다.

💡 Boot Sector를 읽을 때는 항상 signature(0xAA55)를 검증하세요. 손상된 파일시스템을 마운트하면 데이터 손실이나 시스템 크래시가 발생할 수 있습니다.

💡 클러스터 크기 계산 결과를 캐싱하세요. bytes_per_sector * sectors_per_cluster는 자주 사용되는 값이므로, 매번 계산하면 성능 저하가 발생합니다.

💡 FAT32에서 root_entriestotal_sectors_16은 항상 0입니다. 이 값이 0이 아니면 FAT12/16 파일시스템이므로, 마운트 전에 파일시스템 타입을 확인하세요.

💡 멀티코어 환경에서는 Boot Sector 읽기를 atomic하게 처리하세요. 여러 스레드가 동시에 마운트를 시도하면 race condition이 발생할 수 있습니다.


2. FAT 테이블 읽기 - 클러스터 체인 탐색

시작하며

여러분이 파일을 읽으려고 할 때, 그 파일이 디스크의 어디에 흩어져 있는지 어떻게 알 수 있을까요? 특히 큰 파일은 연속된 공간에 저장되지 않고 여러 클러스터에 분산되어 있을 수 있습니다.

이런 문제는 파일 fragmentation으로 인해 필연적으로 발생합니다. 파일을 삭제하고 새로 만들다 보면 빈 공간이 조각나고, 연속된 큰 공간을 찾기 어려워집니다.

이를 추적하지 못하면 파일 데이터를 제대로 읽을 수 없습니다. 바로 이럴 때 필요한 것이 FAT 테이블입니다.

FAT는 각 클러스터의 다음 클러스터 번호를 저장해, 링크드 리스트처럼 클러스터 체인을 따라갈 수 있게 해줍니다.

개요

간단히 말해서, FAT 테이블은 클러스터 번호를 인덱스로 사용하는 거대한 배열로, 각 엔트리는 다음 클러스터 번호나 특수 값(EOF, BAD 등)을 담고 있습니다. FAT 테이블이 필요한 이유는 파일이 어떤 클러스터들로 구성되어 있는지 추적하기 위함입니다.

디렉토리 엔트리는 파일의 첫 번째 클러스터만 알려주고, 나머지는 FAT를 따라가면서 찾아야 합니다. 예를 들어, 10MB 파일이 4KB 클러스터에 저장되면 2560개 클러스터가 필요한데, 이들이 디스크 곳곳에 흩어져 있을 수 있습니다.

전통적인 방법과의 비교를 하면, inode 기반 파일시스템은 extent list나 indirect block을 사용하지만, FAT는 단순한 테이블 lookup으로 다음 클러스터를 찾습니다. 기존에는 복잡한 포인터 추적이 필요했다면, 이제는 배열 인덱싱만으로 충분합니다.

FAT 테이블의 핵심 특징은 1) 각 클러스터당 4바이트 엔트리(FAT32), 2) 특수 값으로 상태 표시(0=free, 0x0FFFFFF8-F=EOF, 0x0FFFFFF7=BAD), 3) 이중화(보통 2개 복사본)입니다. 이러한 특징들이 중요한 이유는 간단한 구조로 빠른 탐색과 안정성을 동시에 제공하기 때문입니다.

코드 예제

// FAT 테이블 엔트리 값 정의
const FAT_FREE: u32 = 0x00000000;           // 빈 클러스터
const FAT_BAD: u32 = 0x0FFFFFF7;            // 불량 클러스터
const FAT_EOF: u32 = 0x0FFFFFF8;            // 파일 끝 마커

// 클러스터 체인을 따라가는 함수
pub fn read_cluster_chain(
    fat_table: &[u8],
    start_cluster: u32,
    bytes_per_sector: u16,
) -> Result<Vec<u32>, Error> {
    let mut chain = Vec::new();
    let mut current = start_cluster;

    // EOF를 만날 때까지 FAT 엔트리 따라가기
    while current < FAT_EOF {
        chain.push(current);

        // FAT 테이블에서 다음 클러스터 번호 읽기
        let offset = (current * 4) as usize;  // FAT32는 4바이트 엔트리
        let next = u32::from_le_bytes([
            fat_table[offset],
            fat_table[offset + 1],
            fat_table[offset + 2],
            fat_table[offset + 3],
        ]) & 0x0FFFFFFF;  // 상위 4비트는 예약됨

        // 불량 클러스터 체크
        if next == FAT_BAD {
            return Err(Error::BadCluster(current));
        }

        current = next;
    }

    Ok(chain)
}

설명

이것이 하는 일: read_cluster_chain 함수는 파일의 시작 클러스터를 받아서, FAT 테이블을 따라가며 파일을 구성하는 모든 클러스터 번호를 수집합니다. 첫 번째로, 현재 클러스터 번호에 4를 곱해 FAT 테이블 내 오프셋을 계산합니다.

FAT32는 각 엔트리가 4바이트(32비트)이므로, 클러스터 N의 엔트리는 바이트 오프셋 N*4에 위치합니다. 이렇게 하는 이유는 FAT 테이블이 단순 배열이므로 직접 계산으로 빠르게 접근할 수 있기 때문입니다.

두 번째로, little-endian으로 4바이트를 읽어 u32로 변환하고, 상위 4비트를 마스킹합니다(& 0x0FFFFFFF). FAT32는 실제로 28비트만 사용하고 상위 4비트는 예약되어 있으므로, 이를 무시해야 합니다.

내부에서는 from_le_bytes가 바이트 배열을 정수로 안전하게 변환합니다. 세 번째로, 읽은 값이 EOF(0x0FFFFFF8 이상)인지, BAD(0x0FFFFFF7)인지, 아니면 유효한 다음 클러스터 번호인지 확인합니다.

EOF를 만나면 파일 끝이므로 루프를 종료하고, BAD를 만나면 디스크 손상이므로 에러를 반환합니다. 최종적으로 유효한 클러스터 번호들의 벡터를 얻을 수 있습니다.

여러분이 이 코드를 사용하면 파일이 디스크의 어느 클러스터들에 저장되어 있는지 정확히 알 수 있습니다. 실무에서의 이점은 1) 순차 접근 최적화: 클러스터 목록을 미리 알면 prefetching 가능, 2) 무한 루프 방지: EOF 체크로 손상된 FAT에서도 안전, 3) 에러 처리: 불량 클러스터 감지로 데이터 손실 예방입니다.

실전 팁

💡 무한 루프 방지를 위해 최대 반복 횟수를 설정하세요. FAT가 손상되어 순환 참조가 생기면 영원히 돌 수 있습니다. chain.len() > max_clusters 체크를 추가하세요.

💡 FAT 테이블은 메모리에 캐싱하세요. 디스크 I/O는 느리므로, 전체 FAT를 한 번에 읽어 메모리에 두고 사용하면 성능이 크게 향상됩니다.

💡 상위 4비트 마스킹(& 0x0FFFFFFF)을 잊지 마세요. 일부 구현에서는 이 비트들을 다른 용도로 사용할 수 있어, 마스킹 없이 비교하면 잘못된 결과가 나올 수 있습니다.

💡 이중화된 두 번째 FAT를 활용하세요. 첫 번째 FAT 읽기가 실패하거나 체크섬이 맞지 않으면, 두 번째 FAT로 복구를 시도할 수 있습니다.

💡 대용량 파일 처리 시 Vec 대신 iterator를 고려하세요. 수천 개의 클러스터를 가진 파일에서는 lazy evaluation으로 메모리를 절약할 수 있습니다.


3. 디렉토리 엔트리 파싱 - 파일 메타데이터 추출

시작하며

여러분이 ls 명령을 실행했을 때, 파일 이름, 크기, 날짜가 어떻게 화면에 나타날까요? 클러스터 체인만 있다고 해서 그 안에 무엇이 들었는지 자동으로 알 수는 없습니다.

이런 문제는 파일시스템이 데이터와 메타데이터를 분리해서 관리하기 때문에 발생합니다. 파일 내용은 클러스터에 저장되지만, 파일에 대한 정보(이름, 크기, 속성 등)는 별도의 구조체에 저장됩니다.

이를 읽지 못하면 단순 바이트 덩어리만 볼 뿐입니다. 바로 이럴 때 필요한 것이 디렉토리 엔트리 파싱입니다.

FAT32 디렉토리는 32바이트 엔트리들의 배열로, 각 엔트리가 파일 하나의 메타데이터를 담고 있습니다.

개요

간단히 말해서, 디렉토리 엔트리는 파일 이름, 속성, 생성/수정 시간, 시작 클러스터, 파일 크기를 담은 고정 크기(32바이트) 구조체입니다. 디렉토리 엔트리 파싱이 필요한 이유는 파일시스템 탐색의 핵심이기 때문입니다.

루트 디렉토리의 엔트리를 읽어야 파일 목록을 얻고, 서브디렉토리 엔트리를 따라가야 전체 파일 트리를 탐색할 수 있습니다. 예를 들어, /home/user/document.txt 경로를 찾으려면 root → home → user 순서로 디렉토리 엔트리를 파싱해야 합니다.

전통적인 방법과의 비교를 하면, 현대 파일시스템은 가변 길이 엔트리나 B-tree를 사용하지만, FAT32는 고정 크기 엔트리로 단순합니다. 기존에는 복잡한 트리 탐색이 필요했다면, 이제는 배열 순회만으로 충분합니다.

디렉토리 엔트리의 핵심 특징은 1) 8.3 형식 파일명(8바이트 이름 + 3바이트 확장자), 2) LFN(Long File Name) 엔트리로 긴 이름 지원, 3) 속성 바이트로 파일/디렉토리/시스템 파일 구분입니다. 이러한 특징들이 중요한 이유는 DOS 시대 호환성을 유지하면서도 현대적 기능을 제공하기 때문입니다.

코드 예제

// 디렉토리 엔트리 구조체 (32바이트)
#[repr(C, packed)]
pub struct DirectoryEntry {
    name: [u8; 11],           // 8.3 형식 파일명 (공백 패딩)
    attributes: u8,           // 파일 속성 플래그
    reserved: u8,             // 예약됨
    creation_time_tenth: u8,  // 생성 시간 (0.1초 단위)
    creation_time: u16,       // 생성 시간 (2초 단위)
    creation_date: u16,       // 생성 날짜
    access_date: u16,         // 마지막 접근 날짜
    first_cluster_high: u16,  // 시작 클러스터 상위 16비트
    modified_time: u16,       // 수정 시간
    modified_date: u16,       // 수정 날짜
    first_cluster_low: u16,   // 시작 클러스터 하위 16비트
    file_size: u32,           // 파일 크기 (바이트)
}

// 속성 플래그 정의
const ATTR_READ_ONLY: u8 = 0x01;
const ATTR_HIDDEN: u8 = 0x02;
const ATTR_SYSTEM: u8 = 0x04;
const ATTR_VOLUME_ID: u8 = 0x08;
const ATTR_DIRECTORY: u8 = 0x10;
const ATTR_ARCHIVE: u8 = 0x20;
const ATTR_LONG_NAME: u8 = ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | ATTR_VOLUME_ID;

impl DirectoryEntry {
    // 시작 클러스터 번호 얻기
    pub fn first_cluster(&self) -> u32 {
        ((self.first_cluster_high as u32) << 16) | (self.first_cluster_low as u32)
    }

    // 파일인지 디렉토리인지 확인
    pub fn is_directory(&self) -> bool {
        self.attributes & ATTR_DIRECTORY != 0
    }

    // LFN 엔트리인지 확인
    pub fn is_long_name(&self) -> bool {
        self.attributes == ATTR_LONG_NAME
    }
}

설명

이것이 하는 일: DirectoryEntry 구조체는 디스크의 32바이트를 파일 정보로 해석하고, 헬퍼 메서드로 자주 사용되는 값들을 편리하게 추출합니다. 첫 번째로, name 필드는 11바이트로 8.3 형식을 저장합니다.

처음 8바이트는 파일 이름, 나머지 3바이트는 확장자입니다. 예를 들어 "README.TXT"는 "README TXT"로 저장됩니다(공백 패딩).

이렇게 하는 이유는 DOS 호환성을 위한 것이며, 긴 이름은 별도의 LFN 엔트리로 처리합니다. 두 번째로, first_cluster_highfirst_cluster_low를 결합해 32비트 클러스터 번호를 만듭니다.

FAT32 이전 버전과의 호환성을 위해 16비트씩 나누어 저장하는데, first_cluster() 메서드가 이를 자동으로 결합합니다. 내부에서는 상위 16비트를 왼쪽으로 시프트하고 OR 연산으로 합칩니다.

세 번째로, attributes 바이트의 비트 플래그를 검사해 파일 타입을 판단합니다. ATTR_DIRECTORY 비트가 설정되면 디렉토리, 그렇지 않으면 일반 파일입니다.

ATTR_LONG_NAME 조합이면 LFN 엔트리로, 실제 파일이 아닌 긴 이름 정보만 담고 있습니다. 최종적으로 파일 타입에 따라 적절히 처리할 수 있습니다.

여러분이 이 코드를 사용하면 디렉토리를 읽어 파일 목록을 얻고, 각 파일의 시작 클러스터로 데이터를 읽을 수 있습니다. 실무에서의 이점은 1) 타입 안전성: 구조체로 필드 접근 실수 방지, 2) 편의성: 헬퍼 메서드로 복잡한 계산 숨김, 3) 호환성: #[repr(C, packed)]로 표준 FAT32 레이아웃 준수입니다.

실전 팁

💡 첫 바이트가 0xE5면 삭제된 엔트리, 0x00이면 디렉토리 끝입니다. 이를 체크하지 않으면 삭제된 파일을 읽거나 쓰레기 값을 파싱하게 됩니다.

💡 LFN 엔트리는 역순으로 저장됩니다. 긴 파일명은 여러 LFN 엔트리로 나뉘어 8.3 엔트리 앞에 역순으로 배치되므로, 파싱 시 순서를 뒤집어야 합니다.

💡 날짜/시간 필드는 특수한 비트 패킹 형식입니다. modified_time은 5/6/5비트로 시/분/초를 인코딩하므로, 비트 마스킹과 시프트로 추출해야 합니다.

💡 볼륨 라벨 엔트리(ATTR_VOLUME_ID)는 건너뛰세요. 이는 파일이 아닌 볼륨 이름 저장 용도이므로, 파일 목록에 포함하면 안 됩니다.

💡 대소문자 정보는 reserved 바이트에 인코딩되어 있습니다. 일부 구현에서는 이 바이트로 8.3 이름의 대소문자를 표현하므로, 완벽한 호환성을 원한다면 이를 파싱하세요.


4. 클러스터 읽기 구현 - 디스크 I/O 추상화

시작하며

여러분이 클러스터 번호를 알았다고 해서 자동으로 데이터를 읽을 수 있을까요? 클러스터 번호는 논리적 주소일 뿐, 실제 디스크의 물리적 섹터 번호로 변환해야 합니다.

이런 문제는 추상화 계층 간의 간극에서 발생합니다. 파일시스템은 클러스터 단위로 생각하지만, 디스크 드라이버는 섹터 단위로 작동합니다.

변환 없이는 둘 사이의 소통이 불가능합니다. 바로 이럴 때 필요한 것이 클러스터 읽기 함수입니다.

클러스터 번호를 섹터 번호로 변환하고, 디스크 드라이버를 호출해 실제 데이터를 가져옵니다.

개요

간단히 말해서, 클러스터 읽기는 논리적 클러스터 번호를 물리적 섹터 번호로 변환하고, 해당 섹터들을 읽어 바이트 버퍼로 반환하는 작업입니다. 클러스터 읽기 구현이 필요한 이유는 파일 데이터 접근의 기반이기 때문입니다.

디렉토리 엔트리와 FAT 체인으로 클러스터 번호를 알아냈어도, 실제로 디스크에서 읽어오지 못하면 무용지물입니다. 예를 들어, 100번 클러스터를 읽으려면 부트 섹터 정보로 계산한 섹터 번호에서 데이터를 가져와야 합니다.

전통적인 방법과의 비교를 하면, 저수준 OS는 섹터 단위로 직접 접근하지만, 파일시스템은 클러스터 추상화를 제공합니다. 기존에는 매번 섹터 번호를 계산해야 했다면, 이제는 클러스터 번호만 주면 자동으로 처리됩니다.

클러스터 읽기의 핵심 특징은 1) 섹터 번호 = (클러스터 - 2) * sectors_per_cluster + data_start 공식, 2) 디스크 드라이버 호출 추상화, 3) 바운더리 체크로 안전성 보장입니다. 이러한 특징들이 중요한 이유는 복잡한 계산을 숨기고 안전하고 사용하기 쉬운 인터페이스를 제공하기 때문입니다.

코드 예제

pub struct Fat32FileSystem {
    boot_sector: Fat32BootSector,
    data_start_sector: u32,     // 데이터 영역 시작 섹터
    disk: Box<dyn DiskDriver>,  // 디스크 드라이버 trait object
}

impl Fat32FileSystem {
    // 클러스터를 읽어 바이트 벡터로 반환
    pub fn read_cluster(&self, cluster: u32) -> Result<Vec<u8>, Error> {
        // 유효한 클러스터 번호인지 확인 (2부터 시작)
        if cluster < 2 {
            return Err(Error::InvalidCluster(cluster));
        }

        // 클러스터를 섹터 번호로 변환
        // 클러스터 2 = 데이터 영역의 첫 클러스터
        let first_sector = self.data_start_sector
            + (cluster - 2) * self.boot_sector.sectors_per_cluster as u32;

        // 클러스터 크기 계산
        let cluster_size = self.boot_sector.bytes_per_sector as usize
            * self.boot_sector.sectors_per_cluster as usize;

        // 버퍼 할당 및 섹터 읽기
        let mut buffer = vec![0u8; cluster_size];
        for i in 0..self.boot_sector.sectors_per_cluster {
            let sector = first_sector + i as u32;
            let offset = i as usize * self.boot_sector.bytes_per_sector as usize;

            // 디스크 드라이버 호출
            self.disk.read_sector(
                sector,
                &mut buffer[offset..offset + self.boot_sector.bytes_per_sector as usize]
            )?;
        }

        Ok(buffer)
    }
}

설명

이것이 하는 일: read_cluster 메서드는 클러스터 번호를 받아 해당 클러스터의 모든 데이터를 바이트 벡터로 반환하며, 내부적으로 섹터 번호 변환과 디스크 읽기를 처리합니다. 첫 번째로, 클러스터 번호가 2 이상인지 확인합니다.

FAT32에서 클러스터 0과 1은 예약되어 있어 실제로 사용할 수 없으므로, 유효한 데이터 클러스터는 2부터 시작합니다. 이렇게 하는 이유는 잘못된 클러스터 접근으로 인한 데이터 손상이나 시스템 크래시를 방지하기 위함입니다.

두 번째로, (cluster - 2) * sectors_per_cluster + data_start_sector 공식으로 첫 섹터 번호를 계산합니다. 클러스터 2가 데이터 영역의 첫 번째 클러스터이므로, 2를 빼서 0-based 인덱스로 만들고, 클러스터당 섹터 수를 곱해 오프셋을 구합니다.

내부에서는 이 계산이 정수 오버플로우 없이 안전하게 수행되는지 확인해야 합니다. 세 번째로, 클러스터 크기만큼 버퍼를 할당하고, 루프를 돌며 각 섹터를 순차적으로 읽어 버퍼에 채웁니다.

클러스터가 여러 섹터로 구성되므로, sectors_per_cluster만큼 반복하면서 디스크 드라이버의 read_sector를 호출합니다. 최종적으로 전체 클러스터 데이터를 담은 벡터를 얻습니다.

여러분이 이 코드를 사용하면 클러스터 번호만으로 간단히 데이터를 읽을 수 있습니다. 실무에서의 이점은 1) 추상화: 상위 계층은 섹터나 바이트 오프셋을 몰라도 됨, 2) 안전성: 경계 검사로 잘못된 접근 차단, 3) 유연성: DiskDriver trait으로 다양한 디스크 백엔드 지원(RAM disk, IDE, NVMe 등)입니다.

실전 팁

💡 클러스터 캐시를 구현하세요. 같은 클러스터를 여러 번 읽는 경우가 많으므로, LRU 캐시를 추가하면 디스크 I/O를 크게 줄일 수 있습니다.

💡 정렬되지 않은 버퍼를 사용하면 DMA가 실패할 수 있습니다. 디스크 드라이버가 DMA를 사용한다면, 버퍼를 섹터 크기로 정렬(#[repr(align(512))])해야 합니다.

💡 최대 클러스터 번호도 검증하세요. 클러스터 번호가 파일시스템 범위를 벗어나면 디스크 손상이나 다른 파티션 침범이 발생할 수 있습니다. total_clusters 값과 비교하세요.

💡 에러 처리를 세분화하세요. 디스크 읽기 실패가 하드웨어 문제인지, 잘못된 클러스터 번호인지, 권한 문제인지 구분하면 디버깅이 쉬워집니다.

💡 비동기 I/O를 고려하세요. 여러 클러스터를 읽을 때 async/await를 사용하면, 디스크가 한 섹터를 읽는 동안 CPU가 다른 작업을 할 수 있어 성능이 향상됩니다.


5. 파일 읽기 통합 - 전체 파이프라인 연결

시작하며

여러분이 지금까지 디렉토리 엔트리, FAT 체인, 클러스터 읽기를 개별적으로 구현했습니다. 하지만 실제로 파일을 읽으려면 이 모든 것을 어떻게 연결할까요?

이런 문제는 복잡한 시스템을 만들 때 항상 발생합니다. 각 컴포넌트는 완벽하게 작동해도, 통합 단계에서 인터페이스 불일치나 데이터 흐름 오류가 나타날 수 있습니다.

제대로 연결하지 못하면 전체가 무용지물이 됩니다. 바로 이럴 때 필요한 것이 파일 읽기 통합 함수입니다.

경로를 받아 디렉토리 탐색 → FAT 체인 추출 → 클러스터 읽기 → 데이터 결합의 전체 파이프라인을 실행합니다.

개요

간단히 말해서, 파일 읽기 통합은 파일 경로 문자열을 받아 해당 파일의 전체 내용을 바이트 벡터로 반환하는 고수준 API입니다. 파일 읽기 통합이 필요한 이유는 사용자 친화적인 인터페이스를 제공하기 위함입니다.

사용자는 클러스터나 FAT 같은 내부 구조를 몰라도, 단순히 경로만 주면 파일을 읽을 수 있어야 합니다. 예를 들어, /documents/report.pdf라는 경로만으로 파일 전체를 읽어야지, 디렉토리 엔트리를 파싱하고 FAT를 따라가는 복잡한 과정을 직접 하면 안 됩니다.

전통적인 방법과의 비교를 하면, POSIX의 open/read 시스템 콜과 유사한 추상화입니다. 기존에는 저수준 디스크 작업을 직접 했다면, 이제는 파일시스템이 모든 복잡성을 숨겨줍니다.

파일 읽기 통합의 핵심 특징은 1) 경로 파싱으로 디렉토리 순회, 2) 캐싱으로 중복 작업 제거, 3) 에러 전파로 모든 단계의 실패 처리입니다. 이러한 특징들이 중요한 이유는 강건하고 효율적인 파일시스템 구현의 핵심이기 때문입니다.

코드 예제

impl Fat32FileSystem {
    // 경로로 파일을 읽어 전체 내용 반환
    pub fn read_file(&self, path: &str) -> Result<Vec<u8>, Error> {
        // 경로를 '/'로 분할하여 각 컴포넌트 순회
        let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();

        // 루트 디렉토리에서 시작
        let mut current_cluster = self.boot_sector.root_cluster;
        let mut target_entry: Option<DirectoryEntry> = None;

        // 경로의 각 컴포넌트를 따라가며 탐색
        for (i, component) in components.iter().enumerate() {
            let is_last = i == components.len() - 1;

            // 현재 디렉토리의 엔트리들 읽기
            let entries = self.read_directory(current_cluster)?;

            // 이름이 일치하는 엔트리 찾기
            let entry = entries.iter()
                .find(|e| self.match_name(&e.name, component))
                .ok_or(Error::FileNotFound)?;

            if is_last {
                // 마지막 컴포넌트 = 목표 파일
                target_entry = Some(*entry);
            } else {
                // 중간 컴포넌트 = 디렉토리여야 함
                if !entry.is_directory() {
                    return Err(Error::NotADirectory);
                }
                current_cluster = entry.first_cluster();
            }
        }

        let entry = target_entry.ok_or(Error::FileNotFound)?;

        // FAT 체인을 따라 모든 클러스터 읽기
        let chain = self.read_cluster_chain(entry.first_cluster())?;
        let mut data = Vec::with_capacity(entry.file_size as usize);

        for cluster in chain {
            let cluster_data = self.read_cluster(cluster)?;
            data.extend_from_slice(&cluster_data);
        }

        // 파일 크기만큼만 자르기 (마지막 클러스터는 부분적으로만 사용)
        data.truncate(entry.file_size as usize);
        Ok(data)
    }
}

설명

이것이 하는 일: read_file 메서드는 파일 경로를 받아 루트부터 시작해 각 디렉토리를 순회하고, 최종적으로 목표 파일의 모든 데이터를 읽어 반환합니다. 첫 번째로, 경로를 '/' 구분자로 분할해 컴포넌트 배열을 만듭니다.

예를 들어 "/home/user/file.txt"는 ["home", "user", "file.txt"]가 됩니다. 루트 디렉토리의 클러스터 번호(boot_sector에서 가져옴)를 시작점으로 설정합니다.

이렇게 하는 이유는 파일시스템이 트리 구조이므로 루트부터 한 단계씩 내려가야 하기 때문입니다. 두 번째로, 각 컴포넌트에 대해 현재 디렉토리의 엔트리들을 읽고, 이름이 일치하는 엔트리를 찾습니다.

중간 컴포넌트는 디렉토리여야 하므로 is_directory() 체크를 하고, 다음 클러스터로 이동합니다. 마지막 컴포넌트는 목표 파일이므로 저장해둡니다.

내부에서는 read_directory가 디렉토리 클러스터를 읽어 엔트리 배열을 반환합니다. 세 번째로, 목표 파일의 시작 클러스터로 FAT 체인을 추출하고, 각 클러스터를 순차적으로 읽어 데이터를 결합합니다.

마지막 클러스터는 파일 크기만큼만 사용될 수 있으므로, truncate로 정확한 파일 크기만큼 자릅니다. 최종적으로 파일의 전체 내용을 담은 벡터를 얻습니다.

여러분이 이 코드를 사용하면 파일시스템의 모든 복잡성을 숨기고, 간단한 경로 문자열만으로 파일을 읽을 수 있습니다. 실무에서의 이점은 1) 사용 편의성: 한 줄로 파일 읽기 완료, 2) 에러 처리: ? 연산자로 모든 단계의 에러 자동 전파, 3) 성능: with_capacity로 불필요한 재할당 방지입니다.

실전 팁

💡 경로 캐노니컬라이제이션을 추가하세요. ".", "..", "//" 같은 특수 경로를 정규화하지 않으면 보안 취약점이나 예상치 못한 동작이 발생할 수 있습니다.

💡 대용량 파일은 스트리밍 방식을 고려하세요. 1GB 파일을 Vec에 전부 로드하면 메모리 부족이 발생할 수 있으므로, Read trait으로 청크 단위 읽기를 제공하세요.

💡 심볼릭 링크 루프를 감지하세요. FAT32는 공식적으로 심볼릭 링크를 지원하지 않지만, 일부 확장에서는 가능하므로 무한 루프 방지가 필요합니다.

💡 디렉토리 읽기 결과를 캐싱하세요. 같은 디렉토리를 여러 번 탐색하는 경우가 많으므로, 디렉토리 엔트리 목록을 메모리에 두면 성능이 크게 향상됩니다.

💡 권한 체크를 추가하세요. FAT32는 Unix 권한이 없지만, ATTR_READ_ONLY 속성을 확인해 읽기 전용 파일을 보호할 수 있습니다.


6. 파일 쓰기 구현 - 빈 클러스터 할당과 FAT 갱신

시작하며

여러분이 파일을 읽을 수 있게 되었으니, 이제 새 파일을 만들거나 기존 파일을 수정하고 싶을 것입니다. 하지만 쓰기는 읽기보다 훨씬 복잡합니다.

이런 문제는 파일시스템 상태를 변경해야 하기 때문에 발생합니다. 읽기는 비파괴적이지만, 쓰기는 빈 클러스터를 찾고, FAT를 업데이트하고, 디렉토리 엔트리를 수정해야 합니다.

중간에 실패하면 파일시스템이 손상될 수 있습니다. 바로 이럴 때 필요한 것이 파일 쓰기 구현입니다.

빈 클러스터 할당, FAT 체인 구축, 데이터 쓰기, 디렉토리 엔트리 업데이트의 전체 과정을 트랜잭션처럼 처리합니다.

개요

간단히 말해서, 파일 쓰기는 빈 클러스터를 찾아 데이터를 쓰고, FAT 테이블에 체인을 기록하며, 디렉토리 엔트리에 파일 정보를 업데이트하는 작업입니다. 파일 쓰기 구현이 필요한 이유는 실용적인 파일시스템의 필수 기능이기 때문입니다.

읽기만 가능한 파일시스템은 쓸모가 제한적이며, 로그 파일 생성, 설정 저장, 사용자 데이터 기록 등 대부분의 실제 사용 사례에는 쓰기가 필요합니다. 예를 들어, OS 부팅 로그를 저장하거나 사용자가 문서를 저장하는 경우에 필수적입니다.

전통적인 방법과의 비교를 하면, 복잡한 파일시스템은 저널링이나 copy-on-write를 사용하지만, FAT32는 단순한 in-place 업데이트 방식입니다. 기존에는 복잡한 트랜잭션 로그를 관리해야 했다면, FAT32는 직접 수정으로 간단합니다(대신 안정성은 낮음).

파일 쓰기의 핵심 특징은 1) FAT 테이블 스캔으로 빈 클러스터 탐색, 2) FAT 엔트리 업데이트로 체인 연결, 3) 디렉토리 엔트리 수정으로 메타데이터 갱신입니다. 이러한 특징들이 중요한 이유는 파일시스템 일관성을 유지하면서 데이터를 안전하게 저장하기 위함입니다.

코드 예제

impl Fat32FileSystem {
    // 빈 클러스터 찾기 (FAT 테이블 스캔)
    fn find_free_cluster(&mut self) -> Result<u32, Error> {
        let total_clusters = self.calculate_total_clusters();

        for cluster in 2..total_clusters {
            let fat_entry = self.read_fat_entry(cluster)?;
            if fat_entry == FAT_FREE {
                return Ok(cluster);
            }
        }

        Err(Error::DiskFull)
    }

    // FAT 엔트리 쓰기
    fn write_fat_entry(&mut self, cluster: u32, value: u32) -> Result<(), Error> {
        let offset = (cluster * 4) as usize;
        let masked_value = value & 0x0FFFFFFF;  // 상위 4비트 보존

        // 두 FAT 모두 업데이트 (이중화)
        for fat_num in 0..self.boot_sector.num_fats {
            let fat_offset = self.boot_sector.reserved_sectors as u32
                + fat_num as u32 * self.boot_sector.fat_size_32;
            let sector = fat_offset + (offset / self.boot_sector.bytes_per_sector as usize) as u32;

            // 섹터 읽기 → 수정 → 쓰기
            let mut sector_data = self.read_sector(sector)?;
            let sector_offset = offset % self.boot_sector.bytes_per_sector as usize;
            sector_data[sector_offset..sector_offset + 4]
                .copy_from_slice(&masked_value.to_le_bytes());
            self.write_sector(sector, &sector_data)?;
        }

        Ok(())
    }

    // 파일 쓰기 (새 파일 생성 또는 덮어쓰기)
    pub fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), Error> {
        let cluster_size = self.cluster_size();
        let num_clusters = (data.len() + cluster_size - 1) / cluster_size;

        // 필요한 만큼 빈 클러스터 할당
        let mut clusters = Vec::new();
        for _ in 0..num_clusters {
            clusters.push(self.find_free_cluster()?);
        }

        // FAT 체인 구축
        for i in 0..clusters.len() {
            let next_value = if i == clusters.len() - 1 {
                FAT_EOF  // 마지막 클러스터
            } else {
                clusters[i + 1]  // 다음 클러스터
            };
            self.write_fat_entry(clusters[i], next_value)?;
        }

        // 데이터 쓰기
        for (i, cluster) in clusters.iter().enumerate() {
            let start = i * cluster_size;
            let end = ((i + 1) * cluster_size).min(data.len());
            self.write_cluster(*cluster, &data[start..end])?;
        }

        // 디렉토리 엔트리 생성/업데이트
        self.update_directory_entry(path, clusters[0], data.len() as u32)?;

        Ok(())
    }
}

설명

이것이 하는 일: write_file 메서드는 파일 경로와 데이터를 받아, 빈 클러스터를 찾고, FAT 체인으로 연결하며, 실제 데이터를 쓰고, 디렉토리에 파일 정보를 기록합니다. 첫 번째로, 파일 크기를 클러스터 크기로 나누어 필요한 클러스터 개수를 계산합니다.

예를 들어 10KB 파일을 4KB 클러스터에 저장하면 3개가 필요합니다(올림 계산). 그런 다음 find_free_cluster를 반복 호출해 빈 클러스터를 찾습니다.

이렇게 하는 이유는 연속된 빈 공간을 찾기 어려울 수 있어, 필요한 만큼 흩어진 클러스터를 모아야 하기 때문입니다. 두 번째로, 할당받은 클러스터들을 FAT 체인으로 연결합니다.

첫 번째 클러스터의 FAT 엔트리는 두 번째 클러스터 번호를 가리키고, 이런 식으로 연결하다가 마지막 클러스터는 EOF를 씁니다. write_fat_entry는 두 FAT 복사본을 모두 업데이트해 이중화를 유지합니다.

내부에서는 섹터 단위로 읽고-수정하고-쓰는 read-modify-write 패턴을 사용합니다. 세 번째로, 각 클러스터에 실제 데이터를 씁니다.

전체 데이터를 클러스터 크기로 나누어, 각 청크를 해당 클러스터에 기록합니다. 마지막 클러스터는 부분적으로만 사용될 수 있으므로, .min(data.len())으로 정확한 바이트 수만 씁니다.

최종적으로 update_directory_entry가 파일 이름, 시작 클러스터, 크기를 디렉토리에 기록합니다. 여러분이 이 코드를 사용하면 파일을 생성하고 데이터를 저장할 수 있는 완전한 쓰기 기능을 얻습니다.

실무에서의 이점은 1) 공간 효율: fragmentation을 허용해 빈 공간 최대 활용, 2) 안정성: 이중 FAT로 손상 복구 가능, 3) 유연성: 파일 크기에 상관없이 동적으로 클러스터 할당입니다.

실전 팁

💡 트랜잭션 순서가 중요합니다. 데이터 쓰기 → FAT 갱신 → 디렉토리 업데이트 순서로 해야, 중간에 실패해도 파일시스템이 손상되지 않습니다(고아 클러스터는 생길 수 있지만 일관성은 유지).

💡 빈 클러스터 탐색을 최적화하세요. 매번 처음부터 스캔하면 느리므로, 마지막으로 할당한 위치를 기억해 다음 탐색의 시작점으로 사용하세요(FAT의 next_free 힌트).

💡 부분 쓰기를 지원하세요. 전체 파일을 다시 쓰지 않고, 기존 클러스터를 재사용하면서 변경된 부분만 업데이트하면 성능이 크게 향상됩니다.

💡 클러스터 0으로 초기화하지 마세요. 보안을 위해 빈 클러스터를 할당할 때 이전 데이터를 지우는 것이 좋지만, 성능 저하가 크므로 보안이 중요한 경우에만 사용하세요.

💡 동시 쓰기를 보호하세요. 여러 프로세스가 동시에 파일을 쓰면 FAT가 손상될 수 있으므로, Mutex나 파일 잠금으로 동기화해야 합니다.


7. 디렉토리 생성 - 새 디렉토리 엔트리 추가

시작하며

여러분이 파일을 만들 수 있게 되었으니, 이제 디렉토리를 만들어 파일들을 체계적으로 정리하고 싶을 것입니다. 디렉토리는 단순히 파일의 변형이 아니라 특별한 처리가 필요합니다.

이런 문제는 디렉토리가 자기 자신을 참조하는 순환 구조를 가지기 때문에 발생합니다. 디렉토리는 "."과 ".." 엔트리를 포함해야 하고, 파일과 달리 크기가 동적으로 변하며, 다른 엔트리들을 담는 컨테이너 역할을 합니다.

바로 이럴 때 필요한 것이 디렉토리 생성 함수입니다. 새 클러스터를 할당하고, "."과 ".." 엔트리를 초기화하며, 부모 디렉토리에 새 엔트리를 추가합니다.

개요

간단히 말해서, 디렉토리 생성은 빈 클러스터를 할당해 특수 엔트리(".", "..")를 쓰고, 부모 디렉토리에 새 디렉토리의 정보를 추가하는 작업입니다. 디렉토리 생성이 필요한 이유는 파일시스템의 계층 구조를 만들기 위함입니다.

모든 파일을 루트에 두면 관리가 불가능하므로, 디렉토리로 논리적 그룹을 만들어야 합니다. 예를 들어, /home/user/documents 같은 구조를 만들려면 각 레벨에서 디렉토리를 생성해야 합니다.

전통적인 방법과의 비교를 하면, Unix 파일시스템은 디렉토리를 특수한 파일 타입으로 취급하지만, FAT32는 단순히 속성 플래그로 구분합니다. 기존에는 별도의 inode 타입이 필요했다면, FAT32는 ATTR_DIRECTORY 비트 하나면 충분합니다.

디렉토리 생성의 핵심 특징은 1) "." 엔트리로 자기 참조, 2) ".." 엔트리로 부모 참조, 3) 초기 크기 0(엔트리는 동적 추가)입니다. 이러한 특징들이 중요한 이유는 디렉토리 탐색과 상대 경로 처리를 가능하게 하기 때문입니다.

코드 예제

impl Fat32FileSystem {
    // 새 디렉토리 생성
    pub fn create_directory(&mut self, path: &str) -> Result<(), Error> {
        // 부모 경로와 디렉토리 이름 분리
        let (parent_path, dir_name) = self.split_path(path)?;

        // 빈 클러스터 할당
        let new_cluster = self.find_free_cluster()?;
        self.write_fat_entry(new_cluster, FAT_EOF)?;

        // 부모 디렉토리 클러스터 찾기
        let parent_cluster = if parent_path.is_empty() {
            self.boot_sector.root_cluster
        } else {
            self.find_directory(parent_path)?.first_cluster()
        };

        // "." 엔트리 생성 (자기 자신)
        let dot_entry = DirectoryEntry {
            name: self.format_83_name("."),
            attributes: ATTR_DIRECTORY,
            first_cluster_high: (new_cluster >> 16) as u16,
            first_cluster_low: (new_cluster & 0xFFFF) as u16,
            file_size: 0,  // 디렉토리 크기는 항상 0
            ..Default::default()
        };

        // ".." 엔트리 생성 (부모 디렉토리)
        let dotdot_entry = DirectoryEntry {
            name: self.format_83_name(".."),
            attributes: ATTR_DIRECTORY,
            first_cluster_high: (parent_cluster >> 16) as u16,
            first_cluster_low: (parent_cluster & 0xFFFF) as u16,
            file_size: 0,
            ..Default::default()
        };

        // 새 클러스터에 "."과 ".." 쓰기
        let mut cluster_data = vec![0u8; self.cluster_size()];
        cluster_data[0..32].copy_from_slice(unsafe {
            std::slice::from_raw_parts(&dot_entry as *const _ as *const u8, 32)
        });
        cluster_data[32..64].copy_from_slice(unsafe {
            std::slice::from_raw_parts(&dotdot_entry as *const _ as *const u8, 32)
        });
        self.write_cluster(new_cluster, &cluster_data)?;

        // 부모 디렉토리에 새 엔트리 추가
        self.add_directory_entry(parent_cluster, dir_name, new_cluster, true)?;

        Ok(())
    }
}

설명

이것이 하는 일: create_directory 메서드는 경로를 받아 새 디렉토리를 생성하고, 자기 참조와 부모 참조 엔트리를 설정하며, 부모 디렉토리에 등록합니다. 첫 번째로, 경로를 파싱해 부모 디렉토리와 새 디렉토리 이름을 분리합니다.

예를 들어 "/home/user/docs"는 부모="/home/user", 이름="docs"로 나뉩니다. 그런 다음 빈 클러스터를 할당하고 FAT에 EOF를 써서 단일 클러스터 체인으로 만듭니다.

이렇게 하는 이유는 디렉토리는 처음에 비어있어 한 클러스터면 충분하고, 나중에 엔트리가 많아지면 확장할 수 있기 때문입니다. 두 번째로, "." 엔트리를 생성해 자기 자신의 클러스터를 가리키게 합니다.

이는 현재 디렉토리를 의미하며, cd . 같은 명령을 지원합니다. ".." 엔트리는 부모 디렉토리 클러스터를 가리키며, cd ..로 상위 디렉토리로 이동할 수 있게 합니다.

내부에서는 클러스터 번호를 상위/하위 16비트로 나누어 엔트리 필드에 저장합니다. 세 번째로, 할당받은 새 클러스터에 이 두 엔트리를 씁니다.

클러스터 크기만큼 0으로 초기화한 버퍼에, 처음 32바이트는 "." 엔트리, 다음 32바이트는 ".." 엔트리를 복사합니다. unsafe 블록으로 구조체를 바이트 배열로 변환하는데, #[repr(C, packed)] 덕분에 메모리 레이아웃이 보장됩니다.

최종적으로 부모 디렉토리에 새 디렉토리 엔트리를 추가해 파일시스템 트리에 연결합니다. 여러분이 이 코드를 사용하면 계층적 디렉토리 구조를 만들 수 있습니다.

실무에서의 이점은 1) 표준 호환성: "."과 ".."는 모든 파일시스템의 규약, 2) 탐색 효율: 상대 경로로 빠른 이동 가능, 3) 확장성: 초기에 작게 시작해 필요시 자동 확장입니다.

실전 팁

💡 루트 디렉토리의 ".." 엔트리는 자기 자신을 가리켜야 합니다. 부모가 없으므로, 클러스터 0이나 루트 클러스터를 사용하세요. 잘못하면 cd ..가 이상하게 동작합니다.

💡 디렉토리 엔트리의 file_size는 항상 0입니다. FAT32 표준에서는 디렉토리 크기를 추적하지 않으므로, 0이 아니면 일부 OS에서 오류가 발생할 수 있습니다.

💡 이름 충돌을 체크하세요. 부모 디렉토리에 같은 이름이 이미 있는지 확인하고, 있으면 Error::AlreadyExists를 반환해야 합니다.

💡 타임스탬프를 설정하세요. 생성/수정 날짜 필드를 현재 시간으로 채우면 ls -l 같은 명령이 올바르게 작동합니다. RTC(Real-Time Clock)에서 읽어오세요.

💡 재귀 생성(mkdir -p)을 지원하세요. 중간 경로가 없을 때 자동으로 생성하면 사용자 편의성이 크게 향상됩니다.


8. LFN(Long File Name) 처리 - 긴 파일명 지원

시작하며

여러분이 "MyVeryLongFileName.txt" 같은 긴 이름의 파일을 만들려고 하는데, FAT32의 기본 8.3 형식으로는 "MYVERYL~1.TXT"처럼 잘려버립니다. 이건 너무 불편하지 않나요?

이런 문제는 DOS 시대의 레거시 제약 때문에 발생합니다. 8.3 형식은 1980년대 표준으로, 현대적 파일명 요구사항을 충족하지 못합니다.

긴 이름 없이는 사용자 경험이 크게 저하됩니다. 바로 이럴 때 필요한 것이 LFN(Long File Name) 처리입니다.

LFN은 추가 디렉토리 엔트리를 사용해 최대 255자의 유니코드 파일명을 저장합니다.

개요

간단히 말해서, LFN은 긴 파일명을 13자씩 나누어 여러 특수 엔트리에 저장하고, 마지막에 실제 8.3 엔트리를 두는 방식입니다. LFN 처리가 필요한 이유는 현대 운영체제와의 호환성을 위함입니다.

Windows, Linux, macOS 모두 긴 파일명을 지원하며, 사용자는 의미 있는 이름을 원합니다. 예를 들어, "2024_Annual_Financial_Report_Final.pdf" 같은 이름은 8.3 형식으로는 불가능합니다.

전통적인 방법과의 비교를 하면, ext4 같은 파일시스템은 가변 길이 엔트리를 사용하지만, FAT32는 하위 호환성을 위해 고정 크기 엔트리를 유지하면서 LFN을 "얹습니다". 기존에는 완전히 새로운 포맷이 필요했다면, FAT32는 기존 구조에 투명하게 추가합니다.

LFN의 핵심 특징은 1) 역순 저장(마지막 조각이 먼저), 2) 체크섬으로 8.3 이름과 연결, 3) 특수 속성(0x0F)으로 일반 엔트리와 구분입니다. 이러한 특징들이 중요한 이유는 LFN을 모르는 구형 시스템도 8.3 이름으로 파일에 접근할 수 있기 때문입니다.

코드 예제

// LFN 엔트리 구조체
#[repr(C, packed)]
struct LongFileNameEntry {
    order: u8,              // 순서 (0x40 = 마지막, 0x01-0x14 = 일반)
    name1: [u16; 5],        // 첫 5글자 (유니코드)
    attributes: u8,         // 항상 0x0F (ATTR_LONG_NAME)
    entry_type: u8,         // 항상 0
    checksum: u8,           // 8.3 이름의 체크섬
    name2: [u16; 6],        // 다음 6글자
    first_cluster: u16,     // 항상 0
    name3: [u16; 2],        // 마지막 2글자
}

impl Fat32FileSystem {
    // 8.3 이름의 체크섬 계산
    fn calculate_checksum(short_name: &[u8; 11]) -> u8 {
        let mut sum: u8 = 0;
        for &byte in short_name {
            sum = sum.rotate_right(1).wrapping_add(byte);
        }
        sum
    }

    // 긴 파일명을 LFN 엔트리로 변환
    fn create_lfn_entries(&self, long_name: &str, short_name: &[u8; 11]) -> Vec<LongFileNameEntry> {
        let checksum = Self::calculate_checksum(short_name);
        let utf16: Vec<u16> = long_name.encode_utf16().collect();
        let num_entries = (utf16.len() + 12) / 13;  // 13자씩 나눔
        let mut entries = Vec::new();

        for i in 0..num_entries {
            let is_last = i == num_entries - 1;
            let start = i * 13;
            let end = ((i + 1) * 13).min(utf16.len());

            let mut lfn_entry = LongFileNameEntry {
                order: if is_last { 0x40 | (i + 1) as u8 } else { (i + 1) as u8 },
                attributes: ATTR_LONG_NAME,
                entry_type: 0,
                checksum,
                first_cluster: 0,
                name1: [0xFFFF; 5],
                name2: [0xFFFF; 6],
                name3: [0xFFFF; 2],
            };

            // 이름을 3개 필드에 분산 저장
            let chunk = &utf16[start..end];
            for (j, &ch) in chunk.iter().enumerate() {
                if j < 5 { lfn_entry.name1[j] = ch; }
                else if j < 11 { lfn_entry.name2[j - 5] = ch; }
                else { lfn_entry.name3[j - 11] = ch; }
            }

            // 마지막 문자 뒤에 0x0000 종료 마커
            if end < start + 13 {
                let pos = end - start;
                if pos < 5 { lfn_entry.name1[pos] = 0x0000; }
                else if pos < 11 { lfn_entry.name2[pos - 5] = 0x0000; }
                else { lfn_entry.name3[pos - 11] = 0x0000; }
            }

            entries.push(lfn_entry);
        }

        // 역순으로 반환 (마지막 조각이 먼저 저장되어야 함)
        entries.reverse();
        entries
    }
}

설명

이것이 하는 일: create_lfn_entries 메서드는 긴 파일명 문자열을 받아, UTF-16으로 인코딩하고, 13자씩 나누어 LFN 엔트리 배열을 생성합니다. 첫 번째로, 파일명을 UTF-16으로 인코딩합니다.

FAT32 LFN은 유니코드를 지원하므로, ASCII가 아닌 한글, 일본어 등도 저장할 수 있습니다. 예를 들어 "한글파일.txt"는 UTF-16으로 인코딩되어 LFN 엔트리에 들어갑니다.

이렇게 하는 이유는 국제화 지원과 특수 문자 처리를 위함입니다. 두 번째로, 13자 단위로 나누어 각 LFN 엔트리를 만듭니다.

각 엔트리는 5 + 6 + 2 = 13개의 UTF-16 문자를 담을 수 있으며, name1, name2, name3 필드에 분산 저장됩니다. order 필드에 순서를 기록하고, 마지막 엔트리는 0x40 비트를 설정합니다.

내부에서는 체크섬을 계산해 모든 LFN 엔트리에 넣어, 나중에 8.3 엔트리와 매칭할 수 있게 합니다. 세 번째로, 생성된 엔트리 배열을 역순으로 뒤집습니다.

FAT32 표준에서는 LFN 엔트리가 역순으로 저장되고 마지막에 8.3 엔트리가 옵니다. 예를 들어 "VeryLongName.txt"는 [LFN3(마지막), LFN2, LFN1(첫), 8.3] 순서로 디렉토리에 기록됩니다.

최종적으로 디렉토리에 쓸 준비가 된 엔트리 목록을 얻습니다. 여러분이 이 코드를 사용하면 255자까지의 긴 파일명을 지원할 수 있습니다.

실무에서의 이점은 1) 호환성: 구형 시스템도 8.3 이름으로 접근 가능, 2) 국제화: UTF-16으로 모든 언어 지원, 3) 투명성: 상위 계층은 LFN 복잡성을 몰라도 됨입니다.

실전 팁

💡 체크섬 불일치를 감지하세요. LFN과 8.3 엔트리의 체크섬이 맞지 않으면 손상된 것이므로, 8.3 이름만 사용하거나 에러를 반환해야 합니다.

💡 0xFFFF 패딩을 잊지 마세요. 사용하지 않는 문자 슬롯은 0xFFFF로 채워야 하며, 0x0000으로 하면 일부 시스템에서 문제가 생깁니다.

💡 8.3 이름 자동 생성을 구현하세요. 긴 이름을 "LONGNA~1.TXT" 형식으로 변환하고, 충돌 시 숫자를 증가시키는 로직이 필요합니다.

💡 LFN 읽기 시 역순을 다시 뒤집으세요. 디스크에서 역순으로 저장되어 있으므로, 읽을 때는 다시 정순으로 조합해야 합니다.

💡 삭제 시 모든 LFN 엔트리를 지우세요. 8.3 엔트리만 삭제하면 고아 LFN 엔트리가 남아 디스크 공간을 낭비합니다. 체크섬으로 연관된 LFN을 찾아 모두 0xE5로 표시하세요.


9. 에러 처리와 복구 - 파일시스템 안정성 강화

시작하며

여러분이 파일을 쓰는 중에 전원이 나가거나, 디스크가 물리적으로 손상되면 어떻게 될까요? 파일시스템이 손상되어 모든 데이터를 잃을 수도 있습니다.

이런 문제는 파일시스템이 복잡한 상태 머신이기 때문에 발생합니다. 여러 단계의 쓰기 작업 중 하나라도 실패하면 일관성이 깨지고, 복구하지 않으면 전체 파일시스템이 사용 불가능해집니다.

바로 이럴 때 필요한 것이 에러 처리와 복구 메커니즘입니다. 에러를 감지하고, 가능한 한 복구하며, 최악의 경우에도 데이터 손실을 최소화합니다.

개요

간단히 말해서, 에러 처리는 모든 I/O 작업의 실패를 감지하고, 복구는 손상된 구조를 찾아 수정하거나 우회하는 작업입니다. 에러 처리와 복구가 필요한 이유는 실제 환경의 불완전성 때문입니다.

디스크는 배드 섹터가 생기고, 전원은 언제든 나갈 수 있으며, 버그는 피할 수 없습니다. 강건한 에러 처리 없이는 사소한 문제가 치명적 데이터 손실로 이어집니다.

예를 들어, 중요한 문서를 저장하는 중 시스템이 크래시하면 복구 메커니즘이 있어야 데이터를 살릴 수 있습니다. 전통적인 방법과의 비교를 하면, 저널링 파일시스템(ext4, NTFS)은 트랜잭션 로그로 완벽한 복구를 제공하지만, FAT32는 단순한 이중화와 휴리스틱으로 최선을 다합니다.

기존에는 복잡한 로그 재생이 필요했다면, FAT32는 간단한 체크와 백업 FAT 사용으로 해결합니다. 에러 처리의 핵심 특징은 1) Result 타입으로 모든 에러 전파, 2) 이중 FAT로 손상 감지 및 복구, 3) fsck 스타일 검증으로 일관성 체크입니다.

이러한 특징들이 중요한 이유는 파일시스템 안정성이 데이터 무결성의 기반이기 때문입니다.

코드 예제

// 에러 타입 정의
#[derive(Debug)]
pub enum FatError {
    IoError(std::io::Error),
    InvalidCluster(u32),
    BadCluster(u32),
    FileNotFound,
    NotADirectory,
    DiskFull,
    FatCorrupted { primary: u32, secondary: u32 },
    InvalidBootSector,
}

impl Fat32FileSystem {
    // FAT 일관성 검사 및 복구
    pub fn verify_and_repair_fat(&mut self) -> Result<usize, FatError> {
        let total_clusters = self.calculate_total_clusters();
        let mut repaired = 0;

        for cluster in 2..total_clusters {
            // 두 FAT 엔트리 읽기
            let primary = self.read_fat_entry_from(cluster, 0)?;
            let secondary = self.read_fat_entry_from(cluster, 1)?;

            // 불일치 감지
            if primary != secondary {
                println!("FAT mismatch at cluster {}: primary={:#x}, secondary={:#x}",
                    cluster, primary, secondary);

                // 휴리스틱: EOF, FREE, BAD 같은 특수 값이면 그쪽을 신뢰
                let correct_value = if Self::is_special_value(primary) {
                    primary
                } else if Self::is_special_value(secondary) {
                    secondary
                } else if primary < total_clusters && secondary >= total_clusters {
                    // 유효한 클러스터 번호인 쪽 선택
                    primary
                } else if secondary < total_clusters && primary >= total_clusters {
                    secondary
                } else {
                    // 판단 불가, 사용자에게 물어봐야 함
                    return Err(FatError::FatCorrupted { primary, secondary });
                };

                // 틀린 쪽을 올바른 값으로 복구
                if primary != correct_value {
                    self.write_fat_entry_to(cluster, correct_value, 0)?;
                }
                if secondary != correct_value {
                    self.write_fat_entry_to(cluster, correct_value, 1)?;
                }

                repaired += 1;
            }
        }

        Ok(repaired)
    }

    // 고아 클러스터 체인 찾기
    pub fn find_lost_chains(&self) -> Result<Vec<Vec<u32>>, FatError> {
        let total_clusters = self.calculate_total_clusters();
        let mut referenced = vec![false; total_clusters as usize];

        // 모든 디렉토리 엔트리를 순회하며 참조된 클러스터 표시
        self.mark_referenced_clusters(self.boot_sector.root_cluster, &mut referenced)?;

        // 참조되지 않았지만 사용 중인 클러스터 찾기
        let mut lost_chains = Vec::new();
        for cluster in 2..total_clusters {
            if !referenced[cluster as usize] {
                let entry = self.read_fat_entry(cluster)?;
                if entry != FAT_FREE {
                    // 고아 체인의 시작점 발견
                    let chain = self.read_cluster_chain(cluster)?;
                    lost_chains.push(chain);

                    // 체인의 나머지도 표시 (중복 방지)
                    for &c in &chain {
                        referenced[c as usize] = true;
                    }
                }
            }
        }

        Ok(lost_chains)
    }

    // 특수 FAT 값 확인
    fn is_special_value(value: u32) -> bool {
        value == FAT_FREE || value == FAT_BAD || value >= FAT_EOF
    }
}

설명

이것이 하는 일: verify_and_repair_fat는 두 FAT 복사본을 비교해 불일치를 찾고, 휴리스틱으로 올바른 값을 결정해 복구하며, find_lost_chains는 참조되지 않는 사용 중 클러스터를 찾아냅니다. 첫 번째로, 모든 클러스터에 대해 primary FAT와 secondary FAT를 읽어 비교합니다.

FAT32는 두 개의 동일한 FAT를 유지하므로, 둘이 다르면 손상이 발생한 것입니다. 예를 들어 전원이 나가면서 primary FAT 쓰기가 실패했지만 secondary는 성공했을 수 있습니다.

이렇게 하는 이유는 이중화가 유일한 복구 수단이기 때문입니다. 두 번째로, 불일치가 발견되면 휴리스틱으로 어느 쪽이 올바른지 판단합니다.

EOF, FREE, BAD 같은 특수 값이면 그쪽을 신뢰하고, 유효한 클러스터 범위 내의 값이면 그것을 선택합니다. 판단이 불가능하면 에러를 반환해 사용자가 결정하게 합니다.

내부에서는 올바른 값으로 두 FAT를 모두 업데이트해 일관성을 복원합니다. 세 번째로, 고아 클러스터를 찾기 위해 전체 디렉토리 트리를 순회하며 참조된 클러스터를 표시합니다.

그런 다음 FAT를 스캔해 참조되지 않았지만 사용 중인 클러스터를 찾습니다. 이는 파일이 삭제되었지만 FAT 엔트리가 정리되지 않았거나, 디렉토리 엔트리만 손상된 경우입니다.

최종적으로 이런 고아 체인을 /lost+found에 복구할 수 있습니다. 여러분이 이 코드를 사용하면 손상된 파일시스템을 복구하고 데이터 손실을 최소화할 수 있습니다.

실무에서의 이점은 1) 자동 복구: 사소한 손상은 자동으로 수정, 2) 데이터 구조: 손실된 파일도 고아 체인으로 복구 시도, 3) 투명성: 사용자는 복구 과정을 모르고 계속 사용 가능입니다.

실전 팁

💡 읽기 전용 모드로 먼저 검사하세요. verify_and_repair_fat를 쓰기 권한 없이 실행해 손상 정도를 파악하고, 사용자에게 복구 여부를 물어보는 것이 안전합니다.

💡 복구 전에 백업하세요. 자동 복구가 오히려 상황을 악화시킬 수 있으므로, 가능하면 전체 파티션 이미지를 백업한 후 복구를 시도하세요.

💡 순환 참조를 감지하세요. FAT가 손상되어 클러스터 체인이 루프를 형성하면 무한 루프에 빠지므로, 방문한 클러스터를 추적해야 합니다.

💡 배드 섹터를 표시하세요. 디스크 읽기 에러가 발생하면 해당 클러스터를 FAT에 FAT_BAD로 표시해, 다시 할당되지 않게 하세요.

💡 fsck 스타일 검증을 단계별로 나누세요. 1) Boot Sector 검증, 2) FAT 일관성, 3) 디렉토리 체인, 4) 클러스터 참조 카운트 순서로 체크하면 문제를 체계적으로 찾을 수 있습니다.


10. 성능 최적화 - 캐싱과 비동기 I/O

시작하며

여러분이 파일시스템을 완성했는데, 실제로 사용해보니 너무 느립니다. 작은 파일을 읽는데도 수백 밀리초가 걸리고, 대용량 파일 복사는 몇 분씩 걸립니다.

이런 문제는 순진한 구현이 디스크 I/O를 최적화하지 않기 때문에 발생합니다. 매번 디렉토리를 읽고, FAT를 조회하고, 클러스터를 하나씩 읽으면 디스크 헤드가 왔다갔다하며 시간을 낭비합니다.

바로 이럴 때 필요한 것이 성능 최적화입니다. 캐싱으로 중복 읽기를 제거하고, 비동기 I/O로 대기 시간을 숨기며, prefetching으로 미래 요청을 예측합니다.

개요

간단히 말해서, 성능 최적화는 자주 사용되는 데이터를 메모리에 캐싱하고, 여러 I/O 작업을 병렬로 처리하며, 예측 기반으로 미리 데이터를 가져오는 기법입니다. 성능 최적화가 필요한 이유는 디스크 I/O가 CPU보다 수백만 배 느리기 때문입니다.

똑같은 데이터를 반복해서 읽거나, I/O 완료를 기다리며 CPU를 놀리면 성능이 형편없어집니다. 예를 들어, 디렉토리 목록을 표시할 때마다 FAT를 디스크에서 읽으면, 캐싱으로 메모리에서 읽는 것보다 1000배 느릴 수 있습니다.

전통적인 방법과의 비교를 하면, 고성능 파일시스템(XFS, ZFS)은 복잡한 캐시 계층과 zero-copy를 사용하지만, FAT32는 단순한 LRU 캐시와 readahead로도 큰 향상을 얻을 수 있습니다. 기존에는 정교한 알고리즘이 필요했다면, FAT32는 기본적인 최적화만으로 충분합니다.

성능 최적화의 핵심 특징은 1) LRU 캐시로 클러스터와 디렉토리 엔트리 저장, 2) async/await로 비동기 I/O, 3) sequential read 감지로 prefetch입니다. 이러한 특징들이 중요한 이유는 실용적인 파일시스템의 필수 요소이기 때문입니다.

코드 예제

use std::collections::HashMap;
use std::sync::{Arc, Mutex};

// LRU 캐시로 클러스터 데이터 저장
pub struct ClusterCache {
    cache: Arc<Mutex<HashMap<u32, Vec<u8>>>>,
    capacity: usize,
    lru_order: Arc<Mutex<Vec<u32>>>,
}

impl ClusterCache {
    pub fn new(capacity: usize) -> Self {
        Self {
            cache: Arc::new(Mutex::new(HashMap::new())),
            capacity,
            lru_order: Arc::new(Mutex::new(Vec::new())),
        }
    }

    // 캐시에서 클러스터 읽기
    pub fn get(&self, cluster: u32) -> Option<Vec<u8>> {
        let mut cache = self.cache.lock().unwrap();
        let mut lru = self.lru_order.lock().unwrap();

        if let Some(data) = cache.get(&cluster) {
            // LRU 순서 업데이트 (최근 사용을 끝으로)
            lru.retain(|&c| c != cluster);
            lru.push(cluster);
            Some(data.clone())
        } else {
            None
        }
    }

    // 캐시에 클러스터 저장
    pub fn insert(&self, cluster: u32, data: Vec<u8>) {
        let mut cache = self.cache.lock().unwrap();
        let mut lru = self.lru_order.lock().unwrap();

        // 용량 초과 시 가장 오래된 항목 제거
        if cache.len() >= self.capacity && !cache.contains_key(&cluster) {
            if let Some(oldest) = lru.first() {
                cache.remove(oldest);
                lru.remove(0);
            }
        }

        cache.insert(cluster, data);
        lru.push(cluster);
    }
}

// 비동기 클러스터 읽기
impl Fat32FileSystem {
    pub async fn read_cluster_async(&self, cluster: u32) -> Result<Vec<u8>, FatError> {
        // 캐시 확인
        if let Some(cached) = self.cache.get(cluster) {
            return Ok(cached);
        }

        // 캐시 미스, 디스크에서 읽기
        let first_sector = self.cluster_to_sector(cluster);
        let cluster_size = self.cluster_size();
        let mut buffer = vec![0u8; cluster_size];

        // 모든 섹터를 비동기로 병렬 읽기
        let mut futures = Vec::new();
        for i in 0..self.boot_sector.sectors_per_cluster {
            let sector = first_sector + i as u32;
            let offset = i as usize * self.boot_sector.bytes_per_sector as usize;
            futures.push(self.disk.read_sector_async(sector));
        }

        // 모든 읽기 완료 대기
        let results = futures::future::join_all(futures).await;
        for (i, result) in results.into_iter().enumerate() {
            let data = result?;
            let offset = i * self.boot_sector.bytes_per_sector as usize;
            buffer[offset..offset + data.len()].copy_from_slice(&data);
        }

        // 캐시에 저장
        self.cache.insert(cluster, buffer.clone());
        Ok(buffer)
    }

    // Sequential readahead
    pub async fn read_file_optimized(&self, start_cluster: u32) -> Result<Vec<u8>, FatError> {
        let chain = self.read_cluster_chain(start_cluster)?;

        // 순차 패턴 감지: 클러스터 번호가 연속되는지 확인
        let is_sequential = chain.windows(2).all(|w| w[1] == w[0] + 1);

        if is_sequential && chain.len() > 4 {
            // 순차 읽기: 큰 단위로 한 번에 읽기
            let first_sector = self.cluster_to_sector(chain[0]);
            let total_size = chain.len() * self.cluster_size();
            self.disk.read_multiple_sectors_async(first_sector, total_size).await
        } else {
            // 랜덤 읽기: 개별 클러스터를 병렬로 읽기
            let mut futures = Vec::new();
            for &cluster in &chain {
                futures.push(self.read_cluster_async(cluster));
            }

            let results = futures::future::join_all(futures).await;
            let mut data = Vec::new();
            for result in results {
                data.extend_from_slice(&result?);
            }
            Ok(data)
        }
    }
}

설명

이것이 하는 일: ClusterCache는 최근 사용한 클러스터를 메모리에 보관하고, read_cluster_async는 여러 섹터를 동시에 읽으며, read_file_optimized는 접근 패턴에 따라 최적의 읽기 전략을 선택합니다. 첫 번째로, LRU 캐시는 클러스터 번호를 키로, 데이터를 값으로 하는 HashMap을 유지합니다.

캐시 히트 시 디스크 I/O를 완전히 스킵하고 메모리에서 바로 반환합니다. 용량이 가득 차면 가장 오래 사용되지 않은 항목을 제거합니다(lru_order 벡터의 첫 번째).

이렇게 하는 이유는 temporal locality(최근 사용된 데이터가 다시 사용될 확률이 높음)를 활용하기 위함입니다. 두 번째로, 비동기 I/O는 여러 섹터 읽기를 병렬로 시작하고 모두 완료될 때까지 기다립니다.

join_all로 모든 future를 동시에 실행하면, 디스크가 한 섹터를 읽는 동안 다음 섹터 요청을 큐에 넣을 수 있습니다. 내부에서는 디스크 드라이버가 NCQ(Native Command Queuing) 같은 하드웨어 기능을 활용해 여러 요청을 최적화합니다.

세 번째로, sequential read 최적화는 클러스터 체인이 연속되는지 확인하고, 연속되면 큰 단위로 한 번에 읽습니다. 예를 들어 클러스터 100, 101, 102, 103이면 read_multiple_sectors로 16개 섹터를 한 번에 요청합니다.

이는 디스크 헤드 이동을 최소화하고, DMA 전송 효율을 높입니다. 최종적으로 순차 파일은 랜덤 접근보다 10배 이상 빠르게 읽을 수 있습니다.

여러분이 이 코드를 사용하면 파일시스템 성능을 극적으로 향상시킬 수 있습니다. 실무에서의 이점은 1) 응답성: 캐시 히트로 밀리초 단위 응답, 2) 처리량: 비동기 I/O로 대역폭 최대 활용, 3) 지능성: 패턴 감지로 자동 최적화입니다.

실전 팁

💡 캐시 크기를 동적으로 조정하세요. 사용 가능한 메모리에 따라 캐시 용량을 늘리거나 줄이면, 다양한 환경에서 최적 성능을 얻을 수 있습니다.

💡 Write-through vs write-back을 선택하세요. Write-through는 안전하지만 느리고, write-back은 빠르지만 전원 손실 시 데이터 손실 위험이 있습니다. 사용 사례에 맞게 선택하세요.

💡 FAT 테이블은 항상 캐싱하세요. FAT는 모든 파일 접근에 필요하므로, 전체를 메모리에 두면 성능이 크게 향상됩니다. 수 MB 정도로 크지 않아 부담이 적습니다.

💡 디렉토리 엔트리도 캐싱하세요. 같은 디렉토리를 반복 탐색하는 경우가 많으므로(예: ls를 여러 번 실행), 경로별로 엔트리 목록을 캐싱하면 좋습니다.

💡 Readahead 크기를 조정하세요. 너무 작으면 효과가 없고, 너무 크면 메모리와 대역폭을 낭비합니다. 보통 4-16 클러스터(16-64KB) 정도가 적당합니다.


#Rust#FAT32#FileSystem#OSdev#SystemProgramming#시스템프로그래밍

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.