exFAT 파일시스템(Extended File Allocation Table)

exFAT(Extended File Allocation Table)는 마이크로소프트(Microsoft)가 FAT32의 4GiB 파일 크기 한계를 극복하고 대용량 이동식 매체용으로 설계한 파일시스템(Filesystem)입니다. 이 문서는 exFAT의 온디스크(On-disk) 구조(메인·백업 부트 영역, FAT, 클러스터 힙), 32비트 FAT 엔트리와 NoFatChain 최적화, 디렉터리 엔트리의 타입 시스템(File/Stream/FileName/Bitmap/Up-case), 할당 비트맵(Allocation Bitmap), Up-case 테이블, 타임스탬프와 UTC 오프셋(Offset), 리눅스 커널 exfat 드라이버 구조, 마운트(Mount) 옵션, 일관성 모델과 성능, 그리고 exfatprogs 사용자 공간(User Space) 도구까지 실무 관점에서 심층 분석합니다.

전제 조건: FAT 파일시스템(FAT12/16/32, VFAT) 문서를 먼저 읽으세요. exFAT는 FAT의 후계지만 클러스터 체인 개념과 FAT 엔트리의 기본 의미는 FAT32를 계승합니다. 또 VFS 계층Block I/O 지식이 필요합니다.
일상 비유: exFAT는 FAT32 창고에 빈 자리 표지판(비트맵)과 긴 이름표를 새로 단 확장형 창고와 비슷합니다. 빈 자리를 표 하나씩 뒤지지 않고 점유 판으로 한눈에 찾고(할당 비트맵), 파일 하나에 여러 장의 이름표(타입별 엔트리 묶음)를 붙여 크기·클러스터·이름을 따로 기록합니다.

핵심 요약

  • 클러스터 힙(Cluster Heap) — FAT32의 데이터 영역에 해당, 클러스터 2부터 시작합니다.
  • 32비트 FAT — 여전히 FAT 테이블을 쓰지만 연속 파일은 NoFatChain으로 FAT 생략이 가능합니다.
  • 할당 비트맵(Allocation Bitmap) — 빈/사용 클러스터를 비트 단위로 기록, 빈 공간 탐색을 빠르게 합니다.
  • 디렉터리 엔트리 타입 — File(0x85) + Stream Extension(0xC0) + File Name(0xC1)의 묶음으로 한 파일을 표현합니다.
  • UTC 오프셋 — 타임스탬프에 시간대 정보를 온디스크에 저장, FAT의 시간대 모호성을 해소합니다.
  • 리눅스 드라이버 — 5.7부터 메인라인 fs/exfat/, 구 exfat out-of-tree 드라이버를 대체합니다.

단계별 이해

  1. 영역 구성
    메인/백업 부트 영역 → FAT → 클러스터 힙 순서를 확인합니다.
  2. 빈 공간 찾기
    할당 비트맵으로 빈 클러스터를 찾고 FAT에 체인을 기록하는 흐름을 봅니다.
  3. 엔트리 묶음
    File + Stream + FileName 세 엔트리가 한 파일을 이루는 구조를 이해합니다.
  4. NoFatChain
    연속 배치 시 FAT 추적 없이 오프셋 계산만으로 접근하는 최적화를 익힙니다.
  5. 커널 경로
    VFS 연산이 exfat의 비트맵·FAT·엔트리 조작으로 어떻게 매핑(Mapping)되는지 추적합니다.

개요

exFAT(Extended File Allocation Table)는 2006년 마이크로소프트가 윈도 임베디드(Windows Embedded CE 6.0)와 함께 발표한 파일시스템으로, FAT32의 한계를 넘어서면서도 NTFS의 복잡성과 저널링 오버헤드(Overhead)는 피하는 설계 목표를 가집니다. SD 카드 협회는 SDXC(Secure Digital eXtended Capacity)와 SDUC 카드의 기본 파일시스템으로 exFAT를 채택했고, 이후 USB 드라이브·외장 저장·대용량 이동식 매체에 널리 퍼졌습니다.

핵심 차이점은 FAT32와 달리 4GiB 이상의 단일 파일을 지원하고, 할당 비트맵으로 빈 클러스터 탐색을 가속하며, 디렉터리 엔트리에 타입 시스템을 도입해 파일 메타데이터와 이름·스트림을 분리 저장한다는 점입니다. 또 타임스탬프에 UTC 오프셋을 온디스크에 기록하여, FAT32의 시간대 해석 모호성을 해소합니다.

리눅스 커널은 5.4(2019년 11월)에 마이크로소프트가 사양과 특허를 공개(2019년 8월 OIN 기증)하며 처음으로 메인라인 exFAT 지원을 포함했고, 5.7(2020년)에는 삼성(Samsung)이 기여한 개선된 드라이버로 교체되어 현재의 fs/exfat/ 구현이 자리 잡았습니다. 이전에는 out-of-tree exfat 드라이버와 exfat-fuse 사용자 공간 구현이 쓰였으나, 현재는 메인라인 드라이버가 표준이며 전남재(Namjae Jeon)이 관리자(maintainer)로 유지보수하고 있습니다.

용어 정리:
  • 클러스터 힙(Cluster Heap) — exFAT에서 데이터 클러스터 영역을 부르는 이름. 클러스터 2부터 시작합니다.
  • 할당 비트맵(Allocation Bitmap) — 각 클러스터의 사용 여부를 1비트로 기록한 비트맵.
  • NoFatChain — 파일이 연속 클러스터에 배치되었을 때 FAT 추적을 생략하는 최적화 플래그.
  • Up-case 테이블 — 파일명 대소문자 변환을 위한 유니코드 매핑 테이블.

설계 목표와 FAT32 대비 개선

exFAT는 FAT32의 구조적 한계를 직접적으로 해결하면서도 NTFS처럼 무겁지 않기를 원했습니다. 주요 설계 목표와 결과를 정리합니다.

설계 목표FAT32의 한계exFAT의 해법
대용량 단일 파일4GiB - 1바이트64비트 파일 크기(이론 16EiB)
대용량 볼륨~8TB(구현 한계)최대 128PiB(2^57바이트 영역)
빈 공간 탐색FAT 전체 순회할당 비트맵으로 O(1) 수준 탐색
큰 클러스터최대 32KB(권장)최대 32MB 클러스터(섹터 32KB * 2^25)
타임스탬프 시간대시간대 정보 없음UTC 오프셋 온디스크 저장
긴 파일명LFN 슬롯(역순, 체크섬 짝맞춤)전용 File Name 엔트리(UTF-16, 정방향)
연속 파일 성능FAT 체인 필수 추적NoFatChain으로 FAT 추적 생략
트랜잭션(Transaction) 안전없음TFAT(선택)로 원자적(Atomic) 갱신, 메인라인은 미지원
주의: exFAT는 FAT32의 한계를 벗어났지만 저널링(Journaling)은 여전히 없습니다. TFAT(Transaction-safe FAT)라는 원자적 갱신 확장이 사양에 정의되어 있으나, 리눅스 메인라인 드라이버와 일반 구현은 TFAT를 지원하지 않으므로, 비정상 해제 시 fsck.exfat로 복구가 필요합니다.

디스크 상 구조

exFAT 볼륨은 메인 부트 영역(Main Boot Region), FAT 영역, 클러스터 힙(Cluster Heap)의 세 영역으로 구성됩니다. 메인 부트 영역에는 부트 섹터와 확장 부트 섹터, 그리고 체크섬 영역이 들어가며, 동일 내용의 백업 복사본이 백업 부트 영역에 존재합니다.

exFAT 볼륨 영역 배치 메인 부트 영역 부트 섹터 + BPB 확장 부트 섹터들 체크섬 섹터 백업 부트 영역에 동일 복사본 존재 FAT 영역 32비트 FAT 엔트리 배열 num_fats개 클러스터 힙 (Cluster Heap) 클러스터 2부터 시작 루트 디렉터리 (root_dir) 비트맵/Up-case도 여기에 저장 파일·디렉터리 데이터 클러스터 주소 증가

FAT 영역은 BPB의 fat_offset 섹터에서 시작하고 클러스터 힙은 cluster_heap_offset 섹터에서 시작합니다. 모든 오프셋은 섹터 단위로 표현되며, 섹터 크기는 bytes_per_sector_shift(2의 거듭제곱)로 정의됩니다.

부트 영역과 BPB

exFAT의 부트 섹터는 FAT32와 다른 전용 BPB 구조를 가집니다. 가장 두드러진 차이는 섹터 크기와 클러스터 당 섹터 수를 직접 값이 아니라 2의 거듭제곱 지수로 저장한다는 점입니다. 리눅스 커널의 struct boot_sector(fs/exfat/exfat_raw.h)가 이를 모델링합니다.

/* fs/exfat/exfat_raw.h — exFAT 부트 섹터 (구조체명은 struct boot_sector) */
struct boot_sector {
    __u8  jmp_boot[3];            /* 점프 명령 */
    __u8  fs_name[8];            /* "EXFAT   " (8바이트) */
    __u8  must_be_zero[53];       /* FAT32 BPB 자리, 반드시 0 */
    __le64 partition_offset;     /* 파티션 시작 LBA */
    __le64 vol_length;           /* 볼륨 전체 섹터 수 */
    __le32 fat_offset;           /* FAT 시작 섹터 (오프셋) */
    __le32 fat_length;           /* FAT 당 섹터 수 */
    __le32 clu_offset;           /* 클러스터 힙 시작 섹터 */
    __le32 clu_count;            /* 클러스터 힙 내 클러스터 수 */
    __le32 root_cluster;         /* 루트 디렉터리 시작 클러스터 */
    __le32 vol_serial;           /* 볼륨 일련 번호 */
    __u8  fs_revision[2];       /* 파일시스템 개정(major, minor) */
    __le16 vol_flags;            /* 볼륨 플래그 */
    __u8  sect_size_bits;        /* 섹터 크기 = 2^이값 (9=512) */
    __u8  sect_per_clus_bits;    /* 클러스터 크기 = 섹터 * 2^이값 */
    __u8  num_fats;              /* FAT 개수 (보통 1) */
    __u8  drv_sel;
    __u8  percent_in_use;
    __u8  reserved[7];
    __u8  boot_code[390];        /* 부트스트랩 코드 */
    __le16 signature;            /* 0xAA55 부트 시그니처 */
} __packed;
코드 설명
  • must_be_zero[53]FAT12/16/32 BPB 자리를 0으로 채웁니다. 이 값이 0이 아니면 FAT32로 오인될 수 있어, 커널은 exFAT 식별 시 이 필드로 구분합니다.
  • bytes_per_sector_shift섹터 크기를 지수로 저장. 값 9면 2^9 = 512바이트, 12면 4096바이트입니다.
  • sec_per_clus_shift클러스터 당 섹터 수도 지수. 클러스터 크기 = bytes_per_sector * 2^sec_per_clus_shift. 최대 32MB 클러스터까지 표현 가능합니다.
  • vol_flags비트 1(0x0002 VOLUME_DIRTY): 볼륨 더러움, 비트 2(0x0004 MEDIA_FAILURE): 미디어 오류. 마운트 해제 시 정상이면 더러움 비트를 0으로 클리어합니다.
  • num_fatsFAT32는 보통 2개 복사본이지만 exFAT는 보통 1개만 씁니다(내결함성보다 공간 절약 우선).

백업 부트 영역과 체크섬

exFAT 부트 섹터 바이트 단위 필드 레이아웃 FAT32 BPB 자리(오프셋 11~63)는 반드시 0 — 이것이 FAT32와의 식별 열쇠 jmp_boot 0~2 (3B) fs_name 3~10 (8B) "EXFAT " must_be_zero[53] 11~63 (53B) — FAT32 BPB 자리, 0이어야 exFAT 0이 아니면 FAT32로 오인됨 partition_offset 64~71 (8B) 시작 LBA vol_length 72~79 (8B) 전체 섹터 수 fat_offset/length clu_offset/clu_count root_cluster 80~99 (4B씩) vol_serial(4) | fs_revision(2) | vol_flags(2) 100~107 — vol_flags 비트1=VOLUME_DIRTY, 비트2=MEDIA_FAILURE sect_size_bits(1)|sect_per_clus_bits(1)|num_fats(1)|drv_sel(1)|percent_in_use(1)|reserved[7] boot code + 0xAA55 부트스트랩 코드 영역 백업 부트 영역 = 동일 복사본 + 마지막 섹터에 32비트 체크섬 커널 파생 계산 클러스터 크기 = 2^bytes_per_sector_shift × 2^sec_per_clus_shift 바이트 FAT 오프셋(바이트) = fat_offset × 2^bytes_per_sector_shift, 클러스터 N 섹터 = cluster_heap_offset + (N-2)×2^sec_per_clus_shift

메인 부트 영역(부트 섹터 + 확장 부트 섹터들)의 동일 복사본이 백업 부트 영역에 저장됩니다. 각 부트 영역의 마지막 섹터에는 앞선 부트 섹터들의 32비트 체크섬(checksum)이 들어 있어, 메인이 손상되면 백업에서 복구할 수 있습니다. 리눅스 드라이버는 마운트 시 메인 부트 섹터를 우선 사용하되, 무결성(Integrity) 검증에 체크섬을 활용합니다.

FAT 엔트리와 NoFatChain

exFAT도 FAT 테이블을 가지지만, 엔트리는 항상 32비트(4바이트) 고정입니다. 클러스터 번호와 특수값의 의미는 FAT32와 유사합니다.

엔트리 값의미
0x00000000빈 클러스터(사용 가능)
0x00000002 ~ 0xFFFFFFF6다음 클러스터 번호
0xFFFFFFF7불량 클러스터(Bad Cluster)
0xFFFFFFF8 ~ 0xFFFFFFFF체인 끝(EOC)

하지만 exFAT는 FAT를 반드시 채울 필요가 없습니다. 파일이 연속된 클러스터에 배치되었다면, Stream Extension 엔트리에 NoFatChain 플래그를 설정하여 FAT 추적을 생략하고 시작 클러스터와 길이만으로 오프셋을 계산합니다. 이는 대용량 미디어 파일처럼 연속 배치가 자연스러운 경우 FAT 조회 I/O를 크게 줄입니다.

NoFatChain의 효과: FAT가 연속 파일의 모든 엔트리를 기록하지 않아도 되므로, FAT 영역이 작아지고 읽기 경로의 메타데이터 I/O가 줄어듭니다. 다만 파일이 단편화(Fragmentation)되면 NoFatChain을 끄고 FAT를 채워야 하므로, 단편화 억제가 exFAT 성능에 중요합니다.
NoFatChain(연속) vs FAT 체인(단편화) 접근 비교 NoFatChain: 산술만으로 접근 Stream 엔트리 start=100, len=4 clus 100 데이터 clus 101 clus 102 clus 103 오프셋 i → 클러스터 100 + i/cluster_size (FAT 조회 0회) FAT 체인: 매 클러스터 FAT 조회 Stream 엔트리 start=200, NoFatChain=0 FAT [200]→305 200 305 412 다음 클러스터마다 FAT 엔트리 디스크 읽기 발생 할당/갱신 시 비트맵 + FAT 이중 기록 연속 할당(NoFatChain=1): 1) 비트맵에서 연속 빈 비트 N개 탐색 2) 비트맵 N비트 1로 설정 3) FAT 엔트리 기록 생략(0xEOC 유지 또는 미갱신) 4) Stream 엔트리에 start + ValidDataLen + NoFatChain 플래그 기록 단편화 할당(NoFatChain=0): 1) 비트맵에서 빈 비트 탐색(불연속 가능) 2) 비트맵 갱신 3) FAT에 다음 클러스터 연결 기록(매 단계 디스크 쓰기) → 일관성: 비트맵과 FAT가 같은 클러스터 상태를 이중으로 표현, fsck.exfat가 교차 검증

디렉터리 엔트리 타입 시스템

exFAT의 가장 큰 구조적 변화는 디렉터리 엔트리에 타입 시스템을 도입한 것입니다. FAT32의 8.3 엔트리 + LFN 슬롯과 달리, exFAT는 각 32바이트 엔트리의 첫 바이트(TypeCode)와 InUse 비트로 엔트리 종류를 명시합니다.

TypeCode이름분류역할
0x81Allocation Bitmap주(Primary)할당 비트맵 클러스터 위치
0x82Up-case Table주(Primary)대소문자 변환 테이블 위치
0x83Volume Label주(Primary)볼륨 라벨(선택)
0x85File Directory Entry주(Primary)파일/디렉터리 메타데이터 헤더
0xC0Stream Extension종속(Secondary)시작 클러스터·크기·이름 해시(Hash)
0xC1File Name종속(Secondary)UTF-16 파일명 일부분(최대 15자)

InUse 비트는 TypeCode의 최상위 비트(0x80)로, 엔트리가 유효한지(1) 삭제되었는지(0)를 나타냅니다. 삭제는 InUse 비트를 0으로 내리는 것으로 이루어집니다.

File Directory Entry 묶음

하나의 파일 또는 디렉터리는 File(0x85) + Stream Extension(0xC0) + File Name(0xC1) N개의 연속 엔트리 묶음으로 표현됩니다. File 엔트리의 SecondaryCount 필드가 뒤따르는 종속 엔트리 개수를 알려줍니다.

한 파일의 엔트리 묶음 (SecondaryCount=3 예) File (0x85) SecondaryCount, 속성, 타임스탬프 생성/수정/접근 + UTC 오프셋 헤더 역할 Stream Extension (0xC0) 시작 클러스터, 데이터 길이 이름 해시, NoFatChain 플래그 데이터 위치 정보 File Name 1 (0xC1) UTF-16 15자 File Name 2 (0xC1) UTF-16 15자 File: 속성·시간 (무엇) | Stream: 위치·크기 (어디에, 얼마) | Name: 이름 (무엇이라 부르는가) 읽을 때: File 헤더 → SecondaryCount만큼 종속 엔트리 순회 → Stream에서 클러스터 얻고 Name들 합쳐 이름 복원 이름 해시(NameHash): Stream Extension에 저장, 검색 시 이름 전체 비교 전 빠른 거름 한 File Name 엔트리당 최대 15자의 UTF-16 → 255자 이름은 최대 17개 File Name 엔트리

이름 해시(NameHash)로 빠른 검색

Stream Extension 엔트리에는 파일명의 16비트 해시(NameHash)가 저장됩니다. 디렉터리를 순회할 때 커널은 각 File 묶음의 해시를 먼저 비교하고, 해시가 일치할 때만 실제 UTF-16 이름을 비교합니다. 이로 인해 대형 디렉터리에서 파일 검색이 FAT32보다 빠릅니다.

File(0x85) / Stream(0xC0) / File Name(0xC1) 32바이트 필드 맵 File Directory Entry (0x85) Type 0x85 SecCnt 1 SetChk 2~3 Attr 4~5 Rsvd1 6~7 time+date (생성/수정/접근) 8~19, 6×__le16 create 8~11 / modify 12~15 / access 16~19 time_cs 20~21 (2B) UTC tz×3 22~24 (3B) Rsvd2[7] (25~31) — 엔트리는 32바이트(0~31) Stream Extension (0xC0) Type 0xC0 Flags 1 Rsvd 2 NameLen 3 NameHash 4~5 Rsvd2 6~7 ValidSize 8~15 (8B) Rsvd3 16~19 StartClu 20~23 Size 24~31 (8B) Flags(bit0=AllocPossible, bit1=NoFatChain) File Name (0xC1) Type 0xC1 Flags 1 unicode_0_14[15] (2~31, UTF-16 15자) 끝에 0x0000 종단, 이후 0xFFFF 패딩 엔트리 묶음 읽기 절차 (exfat_get_dentry_set) 1) File(0x85) 읽기 → SecondaryCount로 종속 엔트리 개수 확인, SetChksum으로 묶음 무결성 검증 2) Stream(0xC0) 읽기 → StartClu/ValidSize/Size/Flags(NoFatChain) 획득, NameHash로 검색 거름 3) File Name(0xC1)들을 순서대로 읽어 UTF-16 이름 복원, NameHash 재계산으로 교차 검증 4) 삭제 시: 묶음의 모든 TypeCode 최상위 비트(InUse)를 0으로 클리어, 비트맵·FAT에서 해제 → FAT32 LFN과 달리 정방향 순서·타입 명시·해시 거름으로 검색·복구가 단순
/* fs/exfat/exfat_raw.h — 디렉터리 엔트리는 하나의 struct exfat_dentry 공용체 */
struct exfat_dentry {
    __u8 type;              /* TypeCode + InUse(최상위 비트) */
    union {
        struct {            /* 0x85 File Directory Entry */
            __u8   num_ext;        /* 종속 엔트리 수(SecondaryCount) */
            __le16 checksum;       /* 묶음 전체 16비트 체크섬 */
            __le16 attr;           /* 파일 속성 */
            __le16 reserved1;
            __le16 create_time, create_date;   /* 8~11 */
            __le16 modify_time, modify_date;   /* 12~15 */
            __le16 access_time, access_date;   /* 16~19 */
            __u8   create_time_cs, modify_time_cs;  /* 20~21, 1/100초 */
            __u8   create_tz, modify_tz, access_tz;  /* 22~24, UTC 오프셋 */
            __u8   reserved2[7];
        } file;
        struct {            /* 0xC0 Stream Extension */
            __u8   flags;         /* bit0: AllocationPossible, bit1: NoFatChain */
            __u8   reserved1;
            __u8   name_len;      /* 파일명 UTF-16 글자 수 */
            __le16 name_hash;     /* 파일명 16비트 해시 */
            __le16 reserved2;
            __le64 valid_size;    /* 유효 데이터 길이 */
            __le32 reserved3;
            __le32 start_clu;     /* 시작 클러스터 */
            __le64 size;          /* 할당된 전체 길이 */
        } stream;
        struct {            /* 0xC1 File Name */
            __u8   flags;
            __le16 unicode_0_14[15];  /* UTF-16 15자 (이 슬롯의 전부) */
        } name;
        struct { ... } bitmap;   /* 0x81 Allocation Bitmap */
        struct { ... } upcase;    /* 0x82 Up-case Table */
    } dentry;
} __packed;

/* 이름 해시 = up-case 한 UTF-16 이름에 exfat_calc_chksum16 적용 */
/* p_uniname->name_hash = exfat_calc_chksum16(upname, unilen << 1, 0, CS_DEFAULT); */
코드 설명
  • num_ext + checksumFile 헤더가 묶음의 크기(SecondaryCount)와 무결성(16비트 체크섬)을 한 번에 알립니다. FAT32 LFN의 체크섬처럼 묶음 전체가 손상되지 않았는지 검증합니다.
  • valid_size vs sizefallocate로 공간만 예약하면 sizevalid_size보다 커집니다. 순차 기록 미디어에서 "먼저 공간 잡고 채우기" 패턴에 유용합니다.
  • name_hashUp-case 테이블로 대문자화한 파일명 UTF-16 바이트에 exfat_calc_chksum16(..., CS_DEFAULT)를 적용한 16비트 해시. 검색 시 디렉터리를 순회하며 이 16비트만 먼저 비교해 이름 전체 비교를 줄입니다.
  • flags NoFatChainbit1이 1이면 Stream의 start_clu에서 산술로 오프셋을 계산하고 FAT를 따라가지 않습니다.

할당 비트맵(Allocation Bitmap)

exFAT는 FAT 외에 할당 비트맵을 추가로 관리합니다. 클러스터 힙의 각 클러스터에 대해 1비트를 할당하여, 사용 중이면 1, 비어 있으면 0을 기록합니다. 빈 클러스터를 찾을 때 FAT 전체를 순회하지 않고 비트맵에서 0인 비트를 찾으면 됩니다.

할당 비트맵 자체도 클러스터 힙 내의 한 클러스터(또는 여러 클러스터)에 저장되며, 그 위치는 Allocation Bitmap 디렉터리 엔트리(0x81)에 기록됩니다. 볼륨당 비트맵이 하나또는 두 개(비트맵 쌍)일 수 있으며, 메인라인 리눅스 드라이버는 하나의 비트맵을 사용합니다.

/* fs/exfat/balloc.c — 빈 클러스터 할당 흐름 (단순화) */
int exfat_alloc_cluster(struct exfat_sb_info *sbi, unsigned int num,
                       struct exfat_chain *chain)
{
    unsigned int hint = sbi->bitmap.cur_clu;
    unsigned int clu;

    /* 1) 비트맵에서 hint 위치부터 빈(0) 비트 탐색 */
    clu = exfat_find_free_bitmap(sbi, hint);
    if (clu == EXFAT_EOF_CLUSTER)
        return -ENOSPC;

    /* 2) 비트맵을 1로 설정, FAT 갱신(필요 시) */
    exfat_set_bitmap(sbi, clu);
    chain->dir = clu;

    /* 3) 다음 탐색을 위한 hint 갱신 */
    sbi->bitmap.cur_clu = clu + num;
    return 0;
}
코드 설명
  • hint마지막으로 할당한 클러스터 근처에서 탐색을 시작해 지역성(locality)을 높입니다. 단편화도 줄이는 효과가 있습니다.
  • exfat_find_free_bitmap()비트맵에서 0인 비트를 찾습니다. FAT 순회가 아니라 비트 검사이므로, FAT32의 빈 클러스터 탐색보다 훨씬 빠릅니다.
  • FAT 갱신NoFatChain이 아니면 FAT 엔트리에 다음 클러스터를 연결합니다. 비트맵과 FAT 두 곳을 함께 갱신해야 하므로 일관성 유지가 중요합니다.

Up-case 테이블

exFAT는 파일명의 대소문자 구분 처리를 위해 Up-case 테이블을 온디스크에 저장합니다. 이 테이블은 유니코드(Unicode) 코드 포인트를 대응하는 대문자 코드 포인트로 매핑하며, 대소문자 무시(namei) 검색이나 이름 해시 계산에 사용됩니다.

FAT32는 이 변환을 OS/드라이버 구현에 맡겨 플랫폼마다 차이가 있었지만, exFAT는 변환표를 볼륨 자체에 두어 모든 구현이 동일한 대소문자 규칙을 따르도록 했습니다. Up-case Table 디렉터리 엔트리(0x82)가 테이블의 클러스터 위치와 길이를 가리킵니다. 테이블이 손상되거나 없으면 대소문자 무시 검색이 제한될 수 있습니다.

타임스탬프와 UTC 오프셋

FAT32는 타임스탬프에 시간대 정보가 없어, 같은 파일이 다른 OS에서 다른 시각으로 해석되는 문제가 있었습니다. exFAT는 각 타임스탬트(생성·수정·접근) 옆에 UTC 오프셋 필드를 두어, 기록 당시의 시간대를 온디스크에 함께 저장합니다.

오프셋은 15분 단위로 인코딩되며, 부호 비트로 동/서 반구를 구분합니다. 덕분에 exFAT 파일을 다른 시스템에서 읽어도 정확한 절대 시각(UTC)을 복원할 수 있습니다. 타임스탬트 자체의 정밀도는 10ms 단위(수정/생성)로 FAT32의 2초보다 향상되었습니다.

UTC 오프셋 1바이트 인코딩 (각 타임스탬트 옆) 비트 6~0: 오프셋 값 (15분 단위) 0~127 → 0분~1905분 (최대 31시간45분) 비트 7: 부호 (0=서반구, 1=동반구) 값 0x80 = 오프셋 정보 없음 예: 한국(KST, UTC+9) → 540분, 540/15 = 36 → 부호 1 → 0x80 | 36 = 0xA4 예: UTC-5(미 동부) → 300분, 300/15 = 20 → 부호 0 → 0x14 복원: UTC = 로컬타임 - 오프셋x15분(부호 적용) → FAT32 시간대 모호성 해소
항목FAT32exFAT
수정 시간 정밀도2초10ms
시간대 정보없음 (로컬 시각 그대로, 해석 모호)UTC 오프셋 1바이트 온디스크 저장
접근 시간날짜만10ms 정밀도 + UTC 오프셋
생성 시간선택, 10ms 필드10ms + UTC 오프셋

리눅스 커널 exfat 드라이버 구조

메인라인 exfat 드라이버는 fs/exfat/에 있으며, 5.7에서 병합된 뒤 지속적으로 개선되었습니다. 소스 구성을 정리합니다.

소스 파일역할
fs/exfat/super.c슈퍼블록(Superblock), 마운트/언마운트, BPB 해석, 볼륨 정보
fs/exfat/inode.c아이노드(Inode) 할당/해제, 매핑, bmap, truncate
fs/exfat/dir.c디렉터리 순회, 엔트리 타입 처리, 이름 해시
fs/exfat/namei.c생성/삭제/조회, 파일명 처리
fs/exfat/file.cfile_operations, ioctl, fallocate
fs/exfat/fatent.cFAT 엔트리 읽기/쓰기, 체인 관리
fs/exfat/balloc.c할당 비트맵 관리, 빈 클러스터 탐색
fs/exfat/upcase.cUp-case 테이블 로드/조회
fs/exfat/core.c클러스터 입출력(I/O), 체인 유틸리티
fs/exfat/exfat_fs.h드라이버 내부 구조체(Struct): exfat_sb_info, exfat_inode_info
fs/exfat/exfat_raw.h온디스크 구조체, 상수

드라이버 내부 상태의 핵심은 struct exfat_sb_info(슈퍼블록 정보: FAT 오프셋, 클러스터 힙 오프셋, 비트맵 위치, 루트 디렉터리 클러스터 등)와 struct exfat_inode_info(시작 클러스터, NoFatChain 여부, 엔트리 위치)입니다.

/* fs/exfat/exfat_fs.h — exFAT 전용 아이노드 정보 (핵심 필드) */
struct exfat_inode_info {
    struct exfat_chain dir;     /* 부모 디렉터리 클러스터 체인 */
    int entry;               /* 부모 디렉터리 내 엔트리 인덱스 */
    unsigned int type;       /* TYPE_FILE / TYPE_DIR */
    unsigned short attr;   /* 파일 속성 */
    unsigned int start_clu;  /* 시작 클러스터 */
    unsigned char flags;    /* NoFatChain 등 */
    struct exfat_hint hint_bmap, hint_stat;  /* 접근 힌트 */
    loff_t i_pos;           /* 온디스크 엔트리 위치 */
    loff_t valid_size;      /* 유효 데이터 길이 */
    struct inode vfs_inode;
    struct timespec64 i_crtime;  /* 생성 시간 */
};

/* 클러스터 체인 표현 */
struct exfat_chain {
    unsigned int dir;    /* 시작 클러스터 */
    unsigned int size;   /* 클러스터 수 (0 = 끝까지) */
    unsigned char flags; /* ALLOC_NO_FAT_CHAIN(0x03) 등 */
};
코드 설명
  • dir + entry이 파일의 디렉터리 엔트리가 부모 디렉터리의 몇 번째 엔트리인지 가리킵니다. FAT의 i_pos와 같은 식별자 역할입니다.
  • start_clu + flags데이터 시작 클러스터와 NoFatChain 여부. NoFatChain이면 오프셋 계산으로 직접 접근합니다.
  • valid_size실제 기록된 유효 데이터 길이. size보다 작을 수 있어, fallocate로 공간만 예약한 경우 구분합니다.
  • exfat_chainFAT 체인의 시작·길이·플래그를 하나로 묶어 전달하는 경량 구조체입니다.

이름 해시 계산과 비트맵 탐색 실제 구현

exFAT 검색·할당의 핵심 두 루틴을 실제 코드로 봅니다. 이름 해시는 Up-case 테이블로 대문자 변환 후 단순 합 해시로, 비트맵 탐색은 비트 단위 순회로 이루어집니다.

/* fs/exfat/nls.c — 이름 해시는 up-case 한 UTF-16 이름에 exfat_calc_chksum16 적용 */
/* p_uniname->name_hash = exfat_calc_chksum16(upname, unilen << 1, 0, CS_DEFAULT); */

/* fs/exfat/exfat_api.c — exFAT 범용 16비트 체크섬 (회전 오른쪽 1 + 바이트 합) */
u16 exfat_calc_chksum16(void *data, int len, u16 chksum, int type)
{
    int i;
    u8 *c = (u8 *)data;

    for (i = 0; i < len; i++, c++) {
        if (type == CS_DIR_ENTRY && (i == 2 || i == 3))
            continue;          /* 디렉터리 엔트리 체크섬 필드 자체는 제외 */
        chksum = ((chksum & 1) ? 0x8000 : 0) + (chksum >> 1) + *c;
    }
    return chksum;
}
/* CS_DEFAULT(이름 해시): 건너뛸 영역 없이 모든 바이트 누적 */

/* fs/exfat/balloc.c — 비트맵에서 빈(0) 클러스터 탐색 (핵심) */
int exfat_find_free_bitmap(struct exfat_sb_info *sbi, unsigned int hint)
{
    unsigned int clu, byte_idx, bit_idx;
    unsigned char *bitmap = sbi->bitmap.addr;

    for (clu = hint; clu < sbi->num_clusters; clu++) {
        byte_idx = clu >> 3;   bit_idx = clu & 7;
        if (!(bitmap[byte_idx] & (1 < bit_idx)))   /* 0 = 빈 */
            return clu;
    }
    /* 끝까지 없으면 처음부터 hint까지 재탐색 (순환) */
    return EXFAT_EOF_CLUSTER;
}
코드 설명
  • exfat_toupper온디스크 Up-case 테이블을 조회해 소문자를 대문자로 변환합니다. 테이블이 볼륨에 있으므로 모든 구현이 동일한 해시를 냅니다.
  • 비트 검사bitmap[clu>>3] & (1<<(clu&7))로 한 클러스터당 1비트를 검사. FAT32의 FAT 엔트리 순회(4바이트씩)보다 32배 작은 메모리 접근입니다.
  • 순환 재탐색끝까지 찾지 못하면 hint 앞쪽을 다시 봅니다. sbi->bitmap.cur_clu hint로 지역성을 유지합니다.

읽기와 쓰기 경로

exFAT도 VFS 주소 공간(Address Space) 연산으로 페이지 캐시(Page Cache)와 연동합니다. 읽기 시 exfat_bmap()이 파일 오프셋을 물리 섹터로 변환하는데, NoFatChain이면 단순 산술(start_clu + offset/cluster_size)로 끝나고, 아니면 FAT를 따라 클러스터를 찾습니다. 아래 코드는 7.2 이전 buffer_head 기반 경로이며, 7.2부터는 iomap으로 전환되어 자체 write_begin/write_end·직접 I/O 연산이 iomap 공통 코드로 대체되었습니다(자세한 내용은 최신 커널 동향 참조).

/* fs/exfat/fatent.c — 클러스터 체인 따라가기 (NoFatChain 분기, 단순화) */
int exfat_get_cluster(struct exfat_inode_info *ei, unsigned int clu_idx)
{
    unsigned int clu = ei->start_clu;

    if (ei->flags & ALLOC_NO_FAT_CHAIN) {
        /* 연속: 산술만으로 클러스터 계산, FAT 조회 0회 */
        return clu + clu_idx;
    } else {
        /* 단편화: FAT 엔트리를 clu_idx번 따라가기 */
        while (clu_idx--)
            clu = exfat_ent_get(ei->vfs_inode.i_sb, clu);
        return clu;
    }
    /* 디스크 섹터 = data_start + (clu - 2) << sect_per_clus_bits */
}

/* fs/exfat/inode.c — exfat_get_block: 논리 블록 → 물리 매핑 */
int exfat_get_block(struct inode *inode, sector_t iblock,
                    struct buffer_head *bh, int create)
{
    unsigned int clu_idx = iblock >> sbi->sect_per_clus_bits;
    int clu = exfat_get_cluster(EXFAT_I(inode), clu_idx);
    sector_t phys = exfat_cluster_to_sector(sbi, clu);
    map_bh(bh, inode->i_sb, phys + (iblock & clus_mask));
    return 0;
}
코드 설명
  • NoFatChain 분기chain->dir + clu_idx로 끝. 대용량 영상 파일처럼 연속 배치 시 FAT I/O가 전혀 없어 읽기가 거의 raw 디스크 속도입니다.
  • FAT 체인 분기exfat_ent_get로 매 단계 FAT 엔트리(4바이트)를 읽습니다. 단편화가 심하면 메타데이터 I/O가 읽기 지연(Latency)의 주원인입니다.
  • exfat_cluster_to_sectorcluster_heap_offset + (clu - 2) << sec_per_clus_shift. 지수 시프트로 클러스터→섹터 변환, 곱셈 대신 시프트라 빠릅니다.

쓰기 시 새 클러스터는 비트맵에서 찾아 할당하고, FAT에 체인을 연결한 뒤 Stream Extension 엔트리의 시작 클러스터·길이를 갱신합니다. 파일이 커질 때마다 비트맵과 FAT, 디렉터리 엔트리 세 곳이 갱신되므로, 순서가 무결성에 영향을 줍니다.

fallocate와 공간 예약

메인라인 exfat 드라이버는 fallocate를 지원하여, 파일에 클러스터를 미리 예약(allocate)할 수 있습니다. 이때 valid_size는 그대로 두고 size만 키워 공간만 확보한 상태를 만듭니다. 대용량 미디어 캡처처럼 "미리 공간을 잡고 순차 기록"하는 패턴에 유용합니다.

일관성 모델과 복구

exFAT는 저널링이 없지만, FAT32보다 일관성 복구가 수월한 면이 있습니다. 할당 비트맵과 FAT가 이중으로 클러스터 상태를 기록하므로, 한쪽이 손상되어도 다른 쪽으로 교차 검증할 수 있습니다. fsck.exfat는 비트맵과 FAT, 디렉터리 엔트리를 비교해 불일치를 찾아냅니다.

볼륨 플래그(vol_flags)의 ActiveDirty 비트가 마운트 시 1로 설정되고 정상 언마운트 시 0으로 클리어됩니다. 다음 마운트 시 이 비트가 1이면 "비정상 해제"로 판단해 검사를 권장합니다.

한계: 비트맵·FAT·엔트리 삼중 갱신이 원자적이지 않으므로, 기록 도중 전원 차단 시 세 곳 중 일부만 반영될 수 있습니다. 복구 도구가 교차 검증으로 대부분 복구하지만, 일관성이 완전히 보장되지는 않습니다. 쓰기 빈번·중요 데이터에는 ext4F2FS 같은 저널링 파일시스템이 더 안전합니다.

마운트 옵션

옵션설명
uid=, gid=소유자/그룹 고정 (FAT 계열 공통)
umask=, dmask=, fmask=권한 마스크
iocharset=문자셋 (보통 utf8)
errors=continue|panic|remount-ro오류 시 동작
utf8UTF-8 파일명 처리
discardTRIM/discard 명령 전달 (SSD/SD)
time_offset=UTC 오프셋이 없는 구 버전 호환용
tz=UTC타임스탬프 UTC 해석
# SDXC 카드 마운트 (일반 사용자 + UTF-8 + TRIM)
mount -t exfat /dev/sdb1 /mnt/sd -o uid=1000,gid=1000,utf8,discard,flush

# /etc/fstab 예 (외장 SSD)
/dev/sdb1  /mnt/sd  exfat  defaults,uid=1000,gid=1000,utf8,discard,noatime  0 0

# 드라이버 로드 (필요 시)
modprobe exfat
grep EXFAT /boot/config-$(uname -r)
코드 설명
  • discardSD/SSD에서 삭제된 클러스터를 TRIM으로 알려줘 마모 균등화(wear leveling)와 성능에 도움입니다. 카드가 TRIM을 지원하는지 먼저 확인해야 합니다.
  • flush이동식 매체에서 더 자주 플러시(Flush)해 뽑기 전 손실을 줄입니다.

성능 특성

exFAT는 FAT32 대비 메타데이터 경로가 효율적입니다. 할당 비트맵으로 빈 공간 탐색이 빠르고, NoFatChain으로 연속 파일 읽기 시 FAT I/O가 사라지며, 이름 해시로 디렉터리 검색이 빠릅니다. 큰 클러스터(최대 32MB) 지원으로 대용량 미디어 파일에 낭비 없이 큰 클러스터를 쓸 수 있습니다.

반면 저널링 부재로 메타데이터 갱신이 많은 메타데이터 집약적 워크로드(수많은 작은 파일 생성/삭제)에서는 ext4/F2FS보다 불리할 수 있습니다. 또 비트맵과 FAT 이중 갱신이 쓰기 부담을 일부 추가합니다.

튜닝 포인트:

한계와 제약

사용자 공간 도구 (exfatprogs)

exFAT 생성·검사·라벨 변경은 exfatprogs 패키지가 담당합니다. 구 exfat-utils를 대체한 현재 표준입니다.

# exFAT 파일시스템 생성
mkfs.exfat /dev/sdb1
mkfs.exfat -L MYCARD /dev/sdb1            # 볼륨 라벨

# 클러스터 크기 지정 (페이지/섹터 단위가 아닌 바이트 또는 배수)
mkfs.exfat -c 1048576 /dev/sdb1           # 1MB 클러스터

# 일관성 검사
fsck.exfat /dev/sdb1                      # 검사만
fsck.exfat -y /dev/sdb1                   # 자동 복구
fsck.exfat -v /dev/sdb1                   # 상세

# 라벨 조회/변경
tune.exfat -l /dev/sdb1                   # 라벨 출력
tune.exfat -L NEWLABEL /dev/sdb1          # 라벨 변경

# 볼륨 정보 상세
dump.exfat /dev/sdb1                      # 부트 섹터/BPB 덤프
fsck.exfat -v -n /dev/sdb1                # 변경 없이 상세 검사
코드 설명
  • mkfs.exfat -c클러스터 크기를 바이트 단위로 지정. 매체 용도에 맞춰 큰 파일이면 크게, 작은 파일 많으면 작게.
  • fsck.exfat비트맵·FAT·디렉터리 엔트리 교차 검증으로 불일치·잃은 클러스터·단절 체인을 복구합니다. 마운트 해제 상태에서 실행해야 합니다.
  • dump.exfat부트 섹터와 BPB 필드를 사람이 읽기 쉽게 출력해 볼륨 구조 진단에 씁니다.

FAT32 / exFAT / NTFS 비교

항목FAT32exFATNTFS
최대 파일~4GiB16EiB(이론)8PiB(이론)
최대 볼륨~8TB(구현)128PiB8TiB(권장)
빈 공간 탐색FAT 순회할당 비트맵$Bitmap
저널링없음없음(TFAT 미지원)$LogFile
연속 파일 최적화없음NoFatChain있음
타임스탬프 시간대없음UTC 오프셋UTC
이기종 호환매우 높음높음(신형 기기)윈도 중심
권한/ACL없음없음ACL/보안 식별자
리눅스 메인라인vfat(오래전)5.7+ntfs3
표준 용도소형 USB/ESPSDXC/대용량 이동식윈도 시스템 디스크

활용 사례

커널 설정 옵션

CONFIG_EXFAT_FS=m         # exFAT 드라이버 (5.7+ 개선형, 7.2+ iomap)
CONFIG_EXFAT_DEFAULT_IOCHARSET="utf8"

# 7.2부터 EXFAT_FS는 FS_IOMAP를 select — buffer_head 경로에서 iomap로 전환
# 7.2 이전(6.x)에서는 LEGACY_DIRECT_IO를 select했으나 iomap 전환으로 제거됨

대부분의 배포판은 EXFAT_FS를 모듈로 제공하므로 modprobe exfat으로 사용 가능합니다. 구형 커널(5.7 이전)에서는 out-of-tree 드라이버나 exfat-fuse를 써야 하지만, 현재는 메인라인 드라이버가 표준입니다. 참고로 과거 일부 버전에 존재했던 CONFIG_EXFAT_DEBUG_MSG 옵션은 현재 메인라인에서 제거되어 더 이상 별도 디버그 설정이 없습니다.

exFAT 메인라인 드라이버는 5.7 이후 활발히 유지보수되며, 최신 커널에서 구조적 성능 개선이 잇따랐습니다. 본문 기준(2026년 7월) 주요 마일스톤을 정리합니다.

커널 버전시기주요 변화
5.42019년 11월최초 메인라인 exFAT 지원 (MS 사양·특허 공개 이후)
5.72020년삼성 기여 개선형 드라이버로 교체, 현재 fs/exfat/ 구현의 기반
6.182025년 11월할당 비트맵 로딩 시간 최적화(큰 볼륨·작은 클러스터에서 최대 ~16.5배), FS_IOC_GETFSLABEL/SETFSLABEL ioctl, discard/zero_size_dir/errors 재마운트 처리
7.12026년exFAT 버그 수정·코드 개선 (같은 주기에 NTFS 재작성 병합)
7.22026년 6월 병합buffer_head → iomap 전환 (아래 상세)

7.2의 iomap 전환: 왜 구조적 혁신인가

Linux 7.2(2026년 6월 20일 병합, 관리자 전남재)에서 exFAT는 I/O 백엔드를 기존 buffer_head 기반 자체 페이지 캐시 코드에서 커널의 현대적 I/O 추상 계층인 iomap으로 전환했습니다. 이는 ext4/XFS가 오래전 채택한 같은 인프라입니다.

iomap은 파일의 논리 바이트 범위를 물리 저장 블록으로 매핑하고, 버퍼(Buffer)를 경유하는 버퍼드 I/O·직접 I/O·SEEK_HOLE/SEEK_DATA를 한 곳에서 처리합니다. 전환 후 exFAT는 더 이상 자체 write_begin/write_end·직접 I/O 주소 공간 연산을 구현하지 않고 iomap에 위임합니다. buffer_head 연결 리스트(Linked List) 대신 폴리오(folio) 단위 비트맵으로 큰 연속 I/O를 더 효율적으로 관리합니다.

큰 폴리오 자격: 커널 문서는 large folio 지원을 iomap 경로로만 제공하며 buffer_head로는 역이식 계획이 없다고 명시합니다. 즉 iomap 전환은 단순 속도 개선을 넘어, exFAT가 향후 커널 전체 저장 스택 개선(큰 폴리오·메타데이터 오버헤드 감소·잠금 경합(Lock Contention) 완화)을 자동 상속받을 자격을 얻은 것입니다. USB/SD 대용량 순차 전송에 직접적 이득입니다.

버그 수정과 안정성

2025년에는 변형된 매체에서 디렉터리 처리가 멈추는 클러스터 체인 루프 상태를 해결하는 견고성 수정이 들어갔습니다. 저널링이 없는 exFAT에서 비정상 매체 처리 안정성은 fsck.exfat와 함께 실무에서 중요한 관심사입니다.

참고자료

다음 학습: