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) 도구까지 실무 관점에서 심층 분석합니다.
핵심 요약
- 클러스터 힙(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/, 구exfatout-of-tree 드라이버를 대체합니다.
단계별 이해
- 영역 구성
메인/백업 부트 영역 → FAT → 클러스터 힙 순서를 확인합니다. - 빈 공간 찾기
할당 비트맵으로 빈 클러스터를 찾고 FAT에 체인을 기록하는 흐름을 봅니다. - 엔트리 묶음
File + Stream + FileName 세 엔트리가 한 파일을 이루는 구조를 이해합니다. - NoFatChain
연속 배치 시 FAT 추적 없이 오프셋 계산만으로 접근하는 최적화를 익힙니다. - 커널 경로
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) 갱신, 메인라인은 미지원 |
fsck.exfat로 복구가 필요합니다.
디스크 상 구조
exFAT 볼륨은 메인 부트 영역(Main Boot Region), FAT 영역, 클러스터 힙(Cluster Heap)의 세 영역으로 구성됩니다. 메인 부트 영역에는 부트 섹터와 확장 부트 섹터, 그리고 체크섬 영역이 들어가며, 동일 내용의 백업 복사본이 백업 부트 영역에 존재합니다.
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(0x0002VOLUME_DIRTY): 볼륨 더러움, 비트 2(0x0004MEDIA_FAILURE): 미디어 오류. 마운트 해제 시 정상이면 더러움 비트를 0으로 클리어합니다.num_fatsFAT32는 보통 2개 복사본이지만 exFAT는 보통 1개만 씁니다(내결함성보다 공간 절약 우선).
백업 부트 영역과 체크섬
메인 부트 영역(부트 섹터 + 확장 부트 섹터들)의 동일 복사본이 백업 부트 영역에 저장됩니다. 각 부트 영역의 마지막 섹터에는 앞선 부트 섹터들의 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를 크게 줄입니다.
디렉터리 엔트리 타입 시스템
exFAT의 가장 큰 구조적 변화는 디렉터리 엔트리에 타입 시스템을 도입한 것입니다. FAT32의 8.3 엔트리 + LFN 슬롯과 달리, exFAT는 각 32바이트 엔트리의 첫 바이트(TypeCode)와 InUse 비트로 엔트리 종류를 명시합니다.
| TypeCode | 이름 | 분류 | 역할 |
|---|---|---|---|
| 0x81 | Allocation Bitmap | 주(Primary) | 할당 비트맵 클러스터 위치 |
| 0x82 | Up-case Table | 주(Primary) | 대소문자 변환 테이블 위치 |
| 0x83 | Volume Label | 주(Primary) | 볼륨 라벨(선택) |
| 0x85 | File Directory Entry | 주(Primary) | 파일/디렉터리 메타데이터 헤더 |
| 0xC0 | Stream Extension | 종속(Secondary) | 시작 클러스터·크기·이름 해시(Hash) |
| 0xC1 | File 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 필드가 뒤따르는 종속 엔트리 개수를 알려줍니다.
이름 해시(NameHash)로 빠른 검색
Stream Extension 엔트리에는 파일명의 16비트 해시(NameHash)가 저장됩니다. 디렉터리를 순회할 때 커널은 각 File 묶음의 해시를 먼저 비교하고, 해시가 일치할 때만 실제 UTF-16 이름을 비교합니다. 이로 인해 대형 디렉터리에서 파일 검색이 FAT32보다 빠릅니다.
/* 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_sizevssizefallocate로 공간만 예약하면size가valid_size보다 커집니다. 순차 기록 미디어에서 "먼저 공간 잡고 채우기" 패턴에 유용합니다.name_hashUp-case 테이블로 대문자화한 파일명 UTF-16 바이트에exfat_calc_chksum16(..., CS_DEFAULT)를 적용한 16비트 해시. 검색 시 디렉터리를 순회하며 이 16비트만 먼저 비교해 이름 전체 비교를 줄입니다.flagsNoFatChainbit1이 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초보다 향상되었습니다.
| 항목 | FAT32 | exFAT |
|---|---|---|
| 수정 시간 정밀도 | 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.c | file_operations, ioctl, fallocate |
fs/exfat/fatent.c | FAT 엔트리 읽기/쓰기, 체인 관리 |
fs/exfat/balloc.c | 할당 비트맵 관리, 빈 클러스터 탐색 |
fs/exfat/upcase.c | Up-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_cluhint로 지역성을 유지합니다.
읽기와 쓰기 경로
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이면 "비정상 해제"로 판단해 검사를 권장합니다.
마운트 옵션
| 옵션 | 설명 |
|---|---|
uid=, gid= | 소유자/그룹 고정 (FAT 계열 공통) |
umask=, dmask=, fmask= | 권한 마스크 |
iocharset= | 문자셋 (보통 utf8) |
errors=continue|panic|remount-ro | 오류 시 동작 |
utf8 | UTF-8 파일명 처리 |
discard | TRIM/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 이중 갱신이 쓰기 부담을 일부 추가합니다.
튜닝 포인트:
- 클러스터 크기 — 대용량 미디어는 큰 클러스터(1MB 이상)가 낭비·성능 모두 유리. 작은 파일 많으면 작은 클러스터.
discard— 플래시 매체에서 TRIM 활성화.- 단편화 억제 — NoFatChain 혜택을 유지하려면 연속 할당이 중요. 비트맵 hint 기반 할당이 지역성을 돕습니다.
noatime— 접근 시간 갱신 쓰기 감소.
한계와 제약
- 저널링 부재 — 비정상 해제 시
fsck.exfat복구 필요, 손상 위험. - 권한/ACL 부재 — FAT 계열과 같이 마운트 단위 권한만 적용.
- 심볼릭/하드 링크 미지원 — FAT 계열 공통 한계.
- TFAT 미지원 — 트랜잭션 안전 확장을 메인라인 리눅스가 구현하지 않음.
- 특허/라이선스 — 과거 마이크로소프트 특허로 인해 GPL 호환성 논란이 있었으나, 메인라인 병합 시 관련 특허 공개로 해소. 임베디드 제품 일부는 여전 라이선스 확인 필요.
- 구형 OS 호환 — 윈도 XP는 업데이트 없이 exFAT 인식 불가, 구형 기기 호환성은 FAT32가 더 넓음.
사용자 공간 도구 (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 비교
| 항목 | FAT32 | exFAT | NTFS |
|---|---|---|---|
| 최대 파일 | ~4GiB | 16EiB(이론) | 8PiB(이론) |
| 최대 볼륨 | ~8TB(구현) | 128PiB | 8TiB(권장) |
| 빈 공간 탐색 | FAT 순회 | 할당 비트맵 | $Bitmap |
| 저널링 | 없음 | 없음(TFAT 미지원) | $LogFile |
| 연속 파일 최적화 | 없음 | NoFatChain | 있음 |
| 타임스탬프 시간대 | 없음 | UTC 오프셋 | UTC |
| 이기종 호환 | 매우 높음 | 높음(신형 기기) | 윈도 중심 |
| 권한/ACL | 없음 | 없음 | ACL/보안 식별자 |
| 리눅스 메인라인 | vfat(오래전) | 5.7+ | ntfs3 |
| 표준 용도 | 소형 USB/ESP | SDXC/대용량 이동식 | 윈도 시스템 디스크 |
활용 사례
- SDXC / SDUC 카드 — SD 협회 표준이 exFAT를 요구. 64GB 이상 SD카드의 기본 포맷.
- 대용량 USB 드라이브 — 4GiB 이상 단일 파일이 필요한 이동식 매체.
- 외장 저장/캠코더 — 대용량 미디어 파일(4K 영상 등) 저장용.
- 게임 콘솔 이동식 — 일부 콘솔의 외장 저장이 exFAT를 요구.
- 임베디드 대용량 저장 — FAT32 한계를 넘으면서 저널링 오버헤드가 부담인 곳.
커널 설정 옵션
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.4 | 2019년 11월 | 최초 메인라인 exFAT 지원 (MS 사양·특허 공개 이후) |
| 5.7 | 2020년 | 삼성 기여 개선형 드라이버로 교체, 현재 fs/exfat/ 구현의 기반 |
| 6.18 | 2025년 11월 | 할당 비트맵 로딩 시간 최적화(큰 볼륨·작은 클러스터에서 최대 ~16.5배), FS_IOC_GETFSLABEL/SETFSLABEL ioctl, discard/zero_size_dir/errors 재마운트 처리 |
| 7.1 | 2026년 | exFAT 버그 수정·코드 개선 (같은 주기에 NTFS 재작성 병합) |
| 7.2 | 2026년 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를 더 효율적으로 관리합니다.
buffer_head로는 역이식 계획이 없다고 명시합니다. 즉 iomap 전환은 단순 속도 개선을 넘어, exFAT가 향후 커널 전체 저장 스택 개선(큰 폴리오·메타데이터 오버헤드 감소·잠금 경합(Lock Contention) 완화)을 자동 상속받을 자격을 얻은 것입니다. USB/SD 대용량 순차 전송에 직접적 이득입니다.
버그 수정과 안정성
2025년에는 변형된 매체에서 디렉터리 처리가 멈추는 클러스터 체인 루프 상태를 해결하는 견고성 수정이 들어갔습니다. 저널링이 없는 exFAT에서 비정상 매체 처리 안정성은 fsck.exfat와 함께 실무에서 중요한 관심사입니다.
참고자료
- 커널 공식 문서 — exFAT
- 리눅스 소스 — fs/exfat
- Microsoft — exFAT 파일 시스템 사양
- exfatprogs — mkfs.exfat / fsck.exfat
- Wikipedia — exFAT
- Phoronix — Linux 6.18 exFAT 할당 비트맵 최적화
- Phoronix — Linux 7.2 exFAT iomap 전환
- LKML — [GIT PULL] exfat update for 7.2-rc1 (전남재)
- FAT 파일시스템(FAT12/16/32, VFAT) — exFAT의 전신
- VFS 계층 — exFAT가 올라가는 가상 파일시스템(VFS) 기반
- F2FS — 플래시 친화적 저널링 파일시스템 비교
- Block I/O 서브시스템 — 클러스터 입출력 하부