FAT 파일시스템(FAT12/16/32, VFAT)
FAT(File Allocation Table) 파일시스템(Filesystem)은 가장 오래되고 널리 쓰이는 디스크 파일시스템 중 하나입니다. 이 문서는 FAT12·FAT16·FAT32의 온디스크(On-disk) 구조, FAT 테이블 엔트리의 해석, 8.3 단명명(Short File Name)과 VFAT가 도입한 LFN(긴 파일명, Long File Name) 인코딩, 리눅스 커널의 fat/vfat/msdos 드라이버 구조, 마운트(Mount) 옵션, 읽기·쓰기 경로, 클러스터 체인(Cluster Chain) 할당과 탐색, 그리고 dosfstools 사용자 공간(User Space) 도구 활용까지 실무 관점에서 심층 분석합니다.
핵심 요약
- FAT 테이블 — 클러스터별로 다음 클러스터 번호를 적어 파일 데이터를 사슬처럼 연결합니다.
- FAT12/16/32 — 한 엔트리의 비트 폭(12/16/32)이자 곧 주소 가능 클러스터 수의 한계입니다.
- 클러스터(Cluster) — 할당의 최소 단위, 여러 섹터(Sector)를 묶은 블록입니다.
- 8.3 단명명 — 원래 FAT의 파일명 규칙, 본명 8자 + 확장자 3자입니다.
- VFAT / LFN — 단명명 위에 숨겨진 긴 파일명 엔트리로 유니코드(Unicode) 긴 이름을 지원합니다.
- 커널 드라이버 — 공통
fat코드 위에vfat(LFN)과msdos(8.3 전용)가 올라갑니다.
단계별 이해
- 디스크 영역 구성
부트 섹터 → FAT → (루트 디렉터리) → 데이터 영역 순서로 배치됨을 확인합니다. - FAT 테이블 읽기
한 엔트리가 다음 클러스터 번호를 가리킨다는 점과 특수값(끝·불량·빈)을 익힙니다. - 디렉터리 엔트리 해석
32바이트 엔트리에서 이름·속성·시작 클러스터·크기를 추출하는 과정을 봅니다. - LFN 인코딩
VFAT가 단명명 앞에 여러 개의 LFN 슬롯을 두어 긴 이름을 복원하는 원리를 이해합니다. - 커널 경로 매핑(Mapping)
VFS의 아이노드/디렉터리 연산이 FAT의 클러스터 체인·엔트리 조작으로 어떻게 번역되는지 추적합니다.
개요
FAT(File Allocation Table)는 1977년 마이크로소프트(Microsoft)의 마크 맥도널드(Marc McDonald)가 초기 디스크 베이직(BASIC)용으로 설계한 파일시스템으로, 이후 MS-DOS의 기본 파일시스템이 되었습니다. 구조가 단순하고 구현이 쉬워 거의 모든 운영체제가 읽고 쓸 수 있어, UEFI 시스템 파티션(ESP), USB 플래시 드라이브, SD/SDHC 카드, 디지털 카메라, 임베디드 펌웨어(Firmware) 파티션 등 이기종 호환성이 필요한 곳에 널리 쓰입니다.
FAT의 핵심은 이름 그대로 파일 할당 테이블입니다. 볼륨의 모든 클러스터에 대해 하나의 엔트리(Entry)를 가지는 배열을 디스크에 저장하고, 각 엔트리는 해당 클러스터가 비어 있는지, 다음 클러스터 번호는 몇 번인지, 아니면 체인의 끝인지를 나타냅니다. 파일의 데이터가 여러 클러스터에 걸쳐 있으면 FAT 엔트리를 따라 사슬(체인)을 추적합니다. 별도의 아이노드(inode) 테이블이 없고 디렉터리 엔트리 자체가 시작 클러스터와 크기를 가지는 것이 ext4 같은 파일시스템과 가장 다른 점입니다. FAT의 단순함은 입출력(I/O) 오버헤드(Overhead)가 적다는 장점이 됩니다.
리눅스 커널은 FAT를 세 가지 마운트 타입으로 제공합니다. 공통 fat 모듈이 핵심 로직(슈퍼블록 읽기, FAT 엔트리 조작, 클러스터 입출력, 디렉터리 순회)을 담당하고, 그 위에 vfat(긴 파일명 LFN 지원)과 msdos(8.3 단명명만 지원, 레거시)가 이름 처리만 다르게 구현합니다. 별도의 exfat 드라이버는 구조가 다른 별개 파일시스템으로 exFAT 문서에서 다룹니다. 이 계층은 가상 파일시스템(VFS) 아래에 올라갑니다.
- 섹터(Sector) — 블록 디바이스의 물리적 입출력 단위, 전통적으로 512바이트.
- 클러스터(Cluster) — FAT의 할당 단위,
sec_per_clus개 섹터의 묶음. - FAT 엔트리(Entry) — FAT 테이블의 한 칸, 클러스터 하나의 상태를 표현.
- 체인(Chain) — 한 파일이 점유한 클러스터들이 FAT 엔트리로 연결된 사슬.
FAT의 역사와 버전 발전
FAT는 저장 매체의 용량이 커짐에 따라 엔트리 폭을 늘려 주소 가능 클러스터 수를 확장하는 방식으로 발전했습니다. 버전명의 숫자가 곧 FAT 엔트리 하나의 비트 폭입니다.
| 버전 | 등장 시기 | 엔트리 폭 | 최대 클러스터 | 주요 용도 |
|---|---|---|---|---|
| FAT12 | 1980년 (MS-DOS 1.0) | 12비트 | 4,084 | 플로피 디스크, 작은 볼륨 |
| FAT16 | 1983년 (MS-DOS 2.0) | 16비트 | 65,524 | 초기 하드디스크, 2GB 한계 |
| FAT32 | 1996년 (Windows 95 OSR2) | 32비트(상위 4비트 예약) | 268,435,446 | 대용량 이동식 매체, SDHC |
FAT32는 기술적으로 32비트 엔트리를 가지지만, 마이크로소프트의 사양에서 상위 4비트는 예약 필드로 취급하여 실제로는 28비트 클러스터 번호(최대 약 2억 6천만 클러스터)만 사용합니다. 리눅스 커널도 이 관례를 따릅니다.
VFAT는 1995년 Windows 95와 함께 도입된 이름 처리 확장입니다. 파일시스템의 온디스크 구조 자체는 FAT12/16/32와 동일하지만, 디렉터리에 추가적인 LFN(긴 파일명) 엔트리를 숨겨 넣어 최대 255자의 유니코드(Unicode) 파일명을 지원합니다. 리눅스에서 mount -t vfat이 바로 이 LFN 처리를 켜는 마운트 타입입니다.
디스크 상 구조
FAT 볼륨은 선형 주소 공간(Address Space)에 몇 개의 고정 영역을 순서대로 배치합니다. FAT12/16과 FAT32는 루트 디렉터리의 위치가 다릅니다.
FAT32에서는 루트 디렉터리가 고정 영역이 아니라 데이터 영역 내의 한 클러스터 체인(보통 2번 클러스터)에 위치합니다. 이 덕분에 FAT32 루트 디렉터리는 엔트리 수 제한 없이 클러스터를 늘려 확장할 수 있습니다.
부트 섹터와 BPB
FAT의 첫 섹터(부트 섹터)는 부트스트랩 코드와 함께 BIOS Parameter Block(BPB)이라는 구조체(Struct)를 담습니다. BPB가 볼륨의 모든 기하 정보를 제공하므로, 커널은 마운트 시 이 섹터 하나를 읽어 전체 볼륨의 해석 방법을 알 수 있습니다. 리눅스 커널의 struct fat_boot_sector는 이 구조를 모델링합니다.
/* include/uapi/linux/msdos_fs.h — 부트 섹터/BPB (핵심 필드) */
/* sector_size/dir_entries/sectors는 정렬 경계를 넘어 __u8[2]로 저장 */
struct fat_boot_sector {
__u8 ignored[3]; /* 점프 명령(jmp_boot) */
__u8 system_id[8]; /* OEM 이름, 예: "MSDOS5.0" */
__u8 sector_size[2]; /* 섹터 당 바이트 (get_unaligned_le16) */
__u8 sec_per_clus; /* 클러스터 당 섹터 수 */
__le16 reserved; /* 예약 섹터 수 (FAT 앞) */
__u8 fats; /* FAT 테이블 개수 (보통 2) */
__u8 dir_entries[2]; /* 루트 디렉터리 엔트리 수 (FAT12/16) */
__u8 sectors[2]; /* 전체 섹터 수 (16비트, 작은 볼륨) */
__u8 media; /* 미디어 기술자 (예: 0xF8 고정디스크) */
__le16 fat_length; /* FAT 당 섹터 수 (FAT12/16) */
__le16 secs_track; /* 트랙 당 섹터 수 */
__le16 heads; /* 헤드 수 */
__le32 hidden; /* 숨은 섹터 수 */
__le32 total_sect; /* 전체 섹터 수 (32비트, 큰 볼륨) */
/* FAT12/16과 FAT32의 확장 BPB는 공용체로 분기 */
union {
struct { /* FAT16 확장 BPB */
__u8 drive_number, state, signature;
__u8 vol_id[4], vol_label[11], fs_type[8];
} fat16;
struct { /* FAT32 확장 BPB */
__le32 length; /* FAT 당 섹터 수 (FAT32) */
__le16 flags; /* 비트8: FAT 미러링, 하위4: 활성 FAT */
__u8 version[2];
__le32 root_cluster; /* 루트 디렉터리 시작 클러스터 */
__le16 info_sector; /* FSINFO 섹터 번호 */
__le16 backup_boot; /* 백업 부트 섹터 */
} fat32;
};
};
코드 설명
sector_size/sec_per_clus클러스터 크기 =sector_size * sec_per_clus로 계산됩니다. 모든 할당의 최소 단위가 됩니다.reservedFAT 테이블 앞의 예약 섹터 수. FAT12/16은 보통 1, FAT32는 32 정도로 큽니다.fatsFAT 테이블 복사본 개수. 보통 2이며, 한 쪽이 손상 시 예비용으로 씁니다.dir_entriesFAT12/16 전용. 루트 디렉터리가 고정 영역이라 엔트리 수가 제한적입니다(보통 512).sectorsvstotal_sect볼륨이 작으면sectors(16비트)를, 크면 0으로 두고total_sect(32비트)를 사용합니다.root_clusterFAT32 전용. 루트 디렉터리가 데이터 영역의 어느 클러스터에서 시작하는지 가리킵니다.
FSINFO 섹터(FAT32)
FAT32는 예약 영역에 FSINFO라는 추가 섹터를 둡니다. 이 섹터는 사용 가능한(free) 클러스터 수의 힌트와 다음 빈 클러스터 탐색 시작점을 캐싱하여, 빈 클러스터를 찾기 위해 FAT 전체를 매번 훑지 않도록 돕습니다. 다만 이 값은 힌트일 뿐이며, 마운트 시 정확하지 않으면 커널이 FAT를 다시 계산합니다.
/* fs/fat/fat.h — FSINFO 구조 (FAT32 전용) */
struct fat_boot_fsinfo {
__le32 signature1; /* 0x41615252 (RRaA) — 선두 식별 */
__le32 reserved1[120];
__le32 signature2; /* 0x61417272 (rrAa) — 실제 데이터 시작 */
__le32 free_clusters; /* 사용 가능 클러스터 수 힌트 (-1 = 모름) */
__le32 next_cluster; /* 다음 빈 클러스터 탐색 시작점 힌트 */
__le32 reserved2[4];
};
코드 설명
- 두 개의 시그니처
RRaA와rrAa로 FSINFO 섹터가 유효한지 검증합니다. 둘 중 하나라도 틀리면 커널은 힌트를 무시하고 FAT를 직접 계산합니다. free_clusters = -1힌트를 모를 때의 값.statfs의 사용 가능 공간 보고에 영향을 주므로, 잘못된 값은 성능 저하(전체 FAT 순회)로 이어집니다.next_cluster다음 할당 탐색 시작점. 단편화가 적을수록 힌트 근처에 빈 클러스터가 있어 탐색이 짧아집니다.
FAT12/16/32 결정과 차이
세 FAT 변종은 클러스터 수에 따라 자동으로 결정됩니다. 마운트 시 커널은 전체 데이터 클러스터 수를 계산한 뒤 아래 기준으로 변종을 정합니다.
| 데이터 클러스터 수 | FAT 변종 | 엔트리 폭 |
|---|---|---|
| 0 ~ 4,084 | FAT12 | 12비트 |
| 4,085 ~ 65,524 | FAT16 | 16비트 |
| 65,525 ~ 268,435,446 | FAT32 | 32비트(28비트 사용) |
리눅스는 fat=12|16|32 마운트 옵션으로 자동 판별을 덮어쓸 수 있지만, 실제 클러스터 수와 맞지 않는 변종을 강제하면 동작이 깨지므로 특수한 진단 목적에만 씁니다.
FAT 테이블 엔트리
FAT 테이블은 클러스터 번호를 인덱스로 하는 배열입니다. 클러스터 N의 상태는 FAT의 N번째 엔트리에 들어 있습니다. 엔트리가 가질 수 있는 값은 세 가지 부류로 나뉩니다.
| 의미 | FAT12 | FAT16 | FAT32 |
|---|---|---|---|
| 빈 클러스터(사용 가능) | 0x000 | 0x0000 | 0x00000000 |
| 불량 클러스터(Bad) | 0xFF7 | 0xFFF7 | 0x0FFFFFF7 |
| 체인 끝(EOC, End Of Chain) | 0xFF8 ~ 0xFFF | 0xFFF8 ~ 0xFFFF | 0x0FFFFFF8 ~ 0x0FFFFFFF |
| 다음 클러스터 번호 | 0x002 ~ 0xFEF | 0x0002 ~ 0xFFEF | 0x00000002 ~ 0x0FFFFFEF |
FAT32의 상위 4비트는 예약 필드이므로, 커널은 엔트리 값을 읽을 때 0x0FFFFFFF로 마스킹하여 실제 28비트 클러스터 번호만 취합니다. 클러스터 0과 1은 실제 데이터 클러스터가 아니라 FAT의 첫 두 엔트리로, 미디어 기술자와 예약 값으로 채워집니다. 따라서 데이터 영역은 항상 클러스터 2부터 시작합니다.
12비트 엔트리의 특이한 패킹
FAT12는 12비트 엔트리를 바이트 경계에 맞추기 위해 두 엔트리를 3바이트에 패킹합니다. 이 때문에 FAT12의 엔트리 읽기/쓰기는 바이트 정렬 처리가 필요해 커널에 별도의 12비트 접근 루틴이 존재합니다. 리눅스의 fat_access()는 변종에 따라 12/16/32비트 경로로 분기합니다.
/* fs/fat/fatent.c — FAT 엔트리 읽기 핵심 흐름 (단순화) */
int fat_access(struct super_block *sb, int nr, int new_value)
{
struct fat_entry fatent;
fatent_init(&fatent);
/* nr 클러스터의 FAT 엔트리 위치를 찾는다 */
if (fat_ent_read(sb, &fatent, nr) < 0)
return -EIO;
if (new_value >= 0)
fat_ent_write(sb, &fatent, new_value, 1); /* 1 = FAT 대기(writeback) */
return fatent.value;
}
코드 설명
fatentFAT 엔트리를 읽기/쓰기 위한 임시 상태 구조체. 오프셋(Offset)·버퍼(Buffer) 헤드·현재 값을 보관합니다.fat_ent_read()변종에 따라 12/16/32비트 엔트리를 디스크에서 읽어 해석합니다. FAT12는 인접 엔트리를 함께 읽어 nibble을 분해합니다.new_value >= 0음수가 아니면 쓰기 모드.-1을 주면 읽기 전용(Read-Only)으로 현재 값만 반환합니다.- 반환값읽은 엔트리 값. 이 값이 EOC 범위면 체인의 끝, 0이면 빈 클러스터, 그 외면 다음 클러스터 번호입니다.
FAT12의 12비트 엔트리 패킹
FAT12는 12비트 엔트리를 바이트 경계에 맞추기 위해 두 엔트리를 3바이트에 묶어 저장합니다. 이 패킹은 읽기/쓰기마다 인접 엔트리를 함께 읽어 니블(nibble, 4비트) 단위로 분해·조립해야 하므로, 커널에 FAT12 전용 접근 루틴이 존재합니다.
/* fs/fat/fatent.c — FAT12 엔트리 읽기/쓰기 핵심 (단순화) */
/* 엔트리 nr의 오프셋 = nr * 3 / 2 바이트 */
static void fat12_ent_get(struct fat_entry *fatent, __u8 *content)
{
int next = le16_to_cpu(*(__le16 *)content);
if (fatent->entry & 1) /* 홀수 엔트리 */
fatent->value = next >> 4;
else /* 짝수 엔트리 */
fatent->value = next & 0x0fff;
}
static void fat12_ent_put(struct fat_entry *fatent, int new)
{
__le16 *lep = (__le16 *)fatent->bhs[0]->b_data + fatent->u.ent12_p;
int old = le16_to_cpu(*lep);
if (fatent->entry & 1)
*lep = cpu_to_le16((old & 0x000f) | (new << 4));
else
*lep = cpu_to_le16((old & 0xf000) | (new & 0x0fff));
/* 두 복사본 FAT 모두 갱신은 fat_ent_write에서 수행 */
}
코드 설명
entry & 1엔트리 인덱스의 홀짝으로 니블 추출 방향이 바뀝니다. 짝수는 하위 12비트, 홀수는 상위 12비트(4비트 시프트)를 씁니다.*lep16비트 읽기한 엔트리가 두 바이트에 걸치므로 16비트 단위로 읽어 시프트·마스크합니다. 바이트 정렬 경계를 넘는 접근이 핵심입니다.- read-modify-write쓰기 시 인접 엔트리의 니블을 보존하기 위해 먼저 16비트를 읽고 해당 니블만 교체합니다. FAT12 쓰기가 FAT16/32보다 비싼 이유입니다.
클러스터 체인 탐색과 할당
파일의 데이터는 연속적이지 않아도 됩니다. 파일이 점유한 클러스터들이 FAT 엔트리로 연결된 체인을 이루며, 디렉터리 엔트리가 그 체인의 시작 클러스터를 가리킵니다. 읽을 때는 시작 클러스터부터 FAT를 따라 EOC를 만날 때까지 다음 클러스터를 구해 나갑니다.
새 클러스터를 할당할 때는 빈(0) 엔트리를 FAT에서 찾아 연결합니다. FAT32는 FSINFO의 next_cluster 힌트에서 탐색을 시작해 효율을 높이고, 힌트가 실패하면 처음부터 다시 훑습니다. 빈 클러스터 탐색은 볼륨이 단편화(fragmentation)되면 비용이 커지므로, 임베디드 환경에서는 주기적 단편화 해제(defrag)가 의미가 있을 수 있습니다.
디렉터리 엔트리 구조
FAT의 디렉터리는 32바이트 고정 길이 엔트리의 단순한 배열입니다. 디렉터리 자체도 하나의 파일처럼 클러스터 체인(루트 디렉터리만 FAT12/16에서는 고정 영역)으로 저장됩니다. 한 엔트리의 기본 형태를 8.3 단명명 엔트리라 합니다.
/* fs/fat/fat.h — 8.3 디렉터리 엔트리 (msdos_dir_entry) */
struct msdos_dir_entry {
__u8 name[MSDOS_NAME]; /* 8.3 단명명 (11바이트, 본명+확장자) */
__u8 attr; /* 속성 플래그 */
__u8 lcase; /* 소문자 표시 (본명/확장자) */
__u8 ctime_cs; /* 생성 시간, 1/100초 */
__le16 ctime; /* 생성 시간 */
__le16 cdate; /* 생성 날짜 */
__le16 adate; /* 최종 접근 날짜 */
__le16 starthi; /* 시작 클러스터 상위 16비트 (FAT32) */
__le16 time; /* 최종 수정 시간 */
__le16 date; /* 최종 수정 날짜 */
__le16 start; /* 시작 클러스터 하위 16비트 */
__le32 size; /* 파일 크기 (바이트) */
};
/* 속성(attr) 플래그 */
ATTR_NONE = 0x00,
ATTR_RO = 0x01, /* 읽기 전용 */
ATTR_HIDDEN = 0x02, /* 숨김 */
ATTR_SYS = 0x04, /* 시스템 */
ATTR_VOLUME = 0x08, /* 볼륨 라벨 */
ATTR_DIR = 0x10, /* 디렉터리 */
ATTR_ARCH = 0x20, /* 아카이브 */
ATTR_EXT = 0x0F; /* LFN 엔트리 식별값 */
코드 설명
name[11]본명 8바이트 + 확장자 3바이트를 공백으로 채운 대문자 문자열로 저장합니다. 점(.)은 저장하지 않습니다.starthi+startFAT32에서는 상위 16비트(starthi)와 하위 16비트(start)를 합쳐 32비트 시작 클러스터를 만듭니다. FAT12/16은starthi가 0입니다.lcaseFAT는 이름을 대문자로 저장하므로, 원래 소문자였음을 표시하는 비트가 들어 있습니다. 표시 방식은 OS마다 차이가 있어 호환성 문제의 원인이 되기도 합니다.ATTR_EXT(0x0F)읽기전용+숨김+시스템+볼륨을 모두 합한 값은 정상 파일이 가질 수 없으므로, VFAT는 이 값을 LFN 엔트리의 식별자로 재활용(Recycling)합니다.
첫 바이트가 0x00이면 "이후 엔트리 모두 사용 안 함"(디렉터리 끝)을, 0xE5이면 "삭제된 엔트리"를 의미합니다. 삭제는 엔트리 첫 바이트를 0xE5로 바꾸고 해당 클러스터들을 FAT에서 0(빈)으로 표시하는 것만으로 이루어지므로, 데이터 복구 도구가 삭제 직후 내용을 살려낼 수 있는 근거가 됩니다.
삭제와 복구의 내부 동작
FAT의 삭제는 놀랍도록 가볍습니다. unlink가 호출되면 커널은 다음만 수행합니다.
- 디렉터리 엔트리의 첫 바이트를
0xE5로 덮어쓴다(나머지 31바이트는 그대로). - 파일이 점유한 클러스터 체인을 FAT에서
0x00000000(빈)으로 표시한다. - FSINFO의
free_clusters를 증가시킨다(FAT32).
데이터 클러스터의 실제 내용은 전혀 지워지지 않습니다. 따라서 삭제 직후 새 쓰기가 해당 클러스터를 덮어쓰기 전이면, 복구 도구가 0xE5 엔트리의 start/size 필드와 FAT 잔여 정보로 클러스터를 되찾을 수 있습니다. 다만 체인이 이미 0으로 지워졌기 때문에, 파일이 단편화되어 있으면 클러스터 순서를 복원하기 어렵습니다. 이것이 연속 배치 파일이 복구에 유리한 이유입니다.
VFAT와 LFN(긴 파일명)
FAT의 8.3 단명명은 현대적인 긴 파일명에 부족합니다. VFAT는 온디스크 구조를 바꾸지 않으면서 LFN을 지원하는 교묘한 인코딩을 사용합니다. 긴 이름을 가진 파일은 여러 개의 LFN 슬롯(역순) + 하나의 8.3 단명명 엔트리로 디렉터리에 기록됩니다.
LFN 슬롯도 32바이트 엔트리이지만 속성이 ATTR_EXT(0x0F)로 표시되어, LFN을 모르는 구형 도구는 이를 "볼륨 라벨 같은 무시해도 되는 엔트리"로 건너뜁니다. 각 LFN 슬롯은 순서 번호(마지막 슬롯에 0x40 비트 설정)와 이름의 일부분(유니코드 13자)을 담습니다.
/* fs/fat/misc.c — 8.3 이름에서 LFN 체크섬 계산 (마이크로소프트 공식 알고리즘) */
static unsigned char fat_checksum(const __u8 *name)
{
unsigned char s = 0;
int i;
for (i = 11; i; i--)
s = ((s & 1) ? 0x80 : 0) + (s >> 1) + *name++;
return s;
}
코드 설명
name11바이트8.3 단명명의 본명 8바이트 + 확장자 3바이트(공백 포함)를 그대로 넘깁니다. 점(.)은 입력에 없습니다.- 회전 시프트각 글자마다
s를 오른쪽 1비트 회전(최하위 비트가 최상위로)한 뒤 글자 값을 더합니다. 단순 합이 아닌 위치 의성을 부여해 충돌을 줄입니다. - 검증 용도모든 LFN 슬롯의
chk필드와, 마지막 8.3 엔트리 이름으로 다시 계산한 값이 같아야 짝이 맞습니다. 한 슬롯이라도 틀리면 커널은 긴 이름을 버립니다.
단명명 생성과 충돌
긴 이름을 8.3으로 줄일 때 VFAT는 앞부분을 자르고 ~1, ~2 같은 일련번호를 붙입니다(예: my report.txt → MYREPO~1.TXT). 같은 단명명이 이미 존재하면 일련번호를 올려 충돌을 피합니다. 리눅스 vfat 드라이버의 shortname 마운트 옵션이 이 동작을 제어합니다(lower, win95, winnt, mixed 모드).
리눅스 커널 FAT 드라이버 구조
리눅스의 FAT 구현은 소스 트리 fs/fat/에 있으며, 공통 코드와 이름 처리 계층으로 분리되어 있습니다. 이 구조 덕분에 FAT12/16/32의 온디스크 처리는 한 번만 구현하고, 이름 규칙만 두 가지로 제공합니다.
| 소스 파일 | 역할 |
|---|---|
fs/fat/inode.c | 슈퍼블록 읽기, 마운트 옵션 파싱, 아이노드 할당/해제, BPB 해석 |
fs/fat/fatent.c | FAT 엔트리 읽기/쓰기, 빈 클러스터 탐색(fat_alloc_clusters) |
fs/fat/dir.c | 디렉터리 순회, 엔트리 검색, LFN 디코딩/인코딩 공통 |
fs/fat/fat.h | 온디스크 구조체, msdos_inode_info, 상수 |
fs/fat/misc.c | 시간 변환, 이름 변환 등 유틸리티 |
fs/fat/namei_msdos.c | msdos 타입: 8.3 단명명 전용 이름 처리 |
fs/fat/namei_vfat.c | vfat 타입: LFN 생성/조회, 단명명 충돌 해결 |
FAT에는 별도의 아이노드 테이블이 없으므로, 커널은 메모리에 struct msdos_inode_info를 두어 아이노드(inode)에 매핑합니다. 이 구조체는 시작 클러스터, 파일 위치 정보(inode의 i_pos로 디렉터리 내 엔트리 위치 인코딩), 권한/소유자 등 VFS가 요구하는 메타데이터를 보관합니다.
/* fs/fat/fat.h — FAT 전용 아이노드 정보 (핵심 필드) */
struct msdos_inode_info {
spinlock_t cache_lru_lock;
struct list_head cache_lru; /* 클러스터 → 오프셋 매핑 캐시(LRU) */
int nr_caches;
unsigned int cache_valid_id;
loff_t mmu_private; /* 물리 할당 크기(mmap 추적) */
int i_start; /* 첫 클러스터 (또는 0) */
int i_logstart; /* 논리 첫 클러스터 */
int i_attrs; /* 미사용 속성 비트 */
loff_t i_pos; /* 온디스크 엔트리 위치 (디렉터리+오프셋 인코딩) */
struct rw_semaphore truncate_lock; /* bmap vs truncate 보호 */
struct timespec64 i_crtime; /* 생성 시간 */
struct inode vfs_inode; /* VFS 아이노드 (포함 관계) */
};
코드 설명
i_posFAT의 "아이노드 번호" 역할. 디렉터리의 어느 클러스터의 어느 오프셋에 엔트리가 있는지를 하나의 64비트 값으로 인코딩합니다. 같은 파일은 항상 같은i_pos를 가집니다.cache_lru파일 오프셋 → 디스크 클러스터 매핑을 LRU 캐시로 보관. FAT 체인 추적 비용을 줄입니다.vfs_inodecontainer_of로msdos_inode_info를 얻는 표준 패턴. VFS 아이노드를 직접 포함합니다.
마운트 경로: fat_fill_super
마운트 시 커널은 부트 섹터를 읽어 BPB를 해석하고, 변종을 결정한 뒤 슈퍼블록을 구성합니다. 이 과정이 FAT의 모든 오프셋 계산의 출발점입니다.
/* fs/fat/inode.c — fat_fill_super 핵심 흐름 (단순화) */
int fat_fill_super(struct super_block *sb, struct fs_context *fc)
{
struct msdos_sb_info *sbi;
struct buffer_head *bh;
struct fat_boot_sector *b;
bh = sb_bread(sb, 0); /* 0번 섹터 = 부트 섹터 */
b = (struct fat_boot_sector *)bh->b_data;
sbi->sec_per_clus = b->sec_per_clus;
sbi->sector_size = get_unaligned_le16(b->sector_size); /* __u8[2] → 16비트 */
sbi->cluster_size = sbi->sector_size * sbi->sec_per_clus;
sbi->fats = b->fats;
sbi->fat_length = le16_to_cpu(b->fat_length); /* FAT12/16 */
if (is_fat32(b)) { /* FAT32 여부 판별 */
sbi->fat_length = le32_to_cpu(b->fat32.length);
sbi->root_cluster = le32_to_cpu(b->root_cluster);
fat_read_bpb(...); /* FSINFO 로드, free_clusters 검증 */
}
/* 데이터 시작 섹터와 전체 데이터 클러스터 수 계산 */
sbi->data_start = sbi->fat_start + sbi->fats * sbi->fat_length
+ sbi->root_dir_sectors; /* FAT12/16: +루트 */
sbi->data_clusters = (total_sect - sbi->data_start) / sbi->sec_per_clus;
/* 클러스터 수로 FAT12/16/32 결정 + fatent_ops 선택 */
fat_clusters_calc(sbi);
sbi->fatent_ops = (sbi->fat_bits == 32) ? &fat32_ops
: (sbi->fat_bits == 16) ? &fat16_ops : &fat12_ops;
return fat_read_root(sb); /* 루트 아이노드 생성 */
}
코드 설명
fatent_ops변종별 FAT 엔트리 접근 연산 테이블(fat12_ops/fat16_ops/fat32_ops). 이후 모든 FAT 접근이 이 테이블로 다형적으로 분기합니다.data_clusters이 값이 4,085/65,525 임계값을 넘는지로 변종을 최종 확정합니다. BPB 공용체(Union)의fat32.length필드 존재만으로 FAT32를 판별하지 않습니다.fat_read_bpbFSINFO 시그니처 검증과free_clusters힌트 신뢰 여부를 결정합니다. 힌트가 의심되면 전체 FAT 순회로 재계산합니다.
FAT 엔트리 연산 디스패치와 클러스터 할당
fat_ent_read는 변종별 ops로 디스패치(Dispatch)되며, fat_alloc_clusters는 빈 엔트리를 찾아 체인을 확장합니다. 할당 경로가 쓰기 성능을 좌우합니다.
/* fs/fat/fatent.c — 빈 클러스터 탐색·할당 (단순화) */
int fat_alloc_clusters(struct msdos_sb_info *sbi, int *cluster,
int nr_clus)
{
struct fat_entry fatent;
int i, err, new_clu, last = 0;
fatent_init(&fatent);
/* hint(next_cluster)에서부터 FAT를 순회하며 0(빈) 엔트리 탐색 */
err = fat_ent_read(sbi->sb, &fatent, sbi->prev_free + 1);
while (err == 0 && fatent.value != 0)
err = fat_ent_next(sbi, &fatent); /* 다음 엔트리로 이동 */
for (i = 0; i < nr_clus && err == 0; i++) {
new_clu = fatent.entry;
/* FAT에 EOC 기록 (두 복사본 모두), 이전 끝 클러스터와 연결 */
fat_ent_write(sbi->sb, &fatent, EOF_FAT(sbi), 1);
if (last) fat_ent_write(..., last, new_clu, 1);
last = new_clu;
cluster[i] = new_clu;
sbi->prev_free = new_clu; /* hint 갱신 */
}
/* FSINFO free_clusters 감소·갱신 (FAT32) */
return err;
}
코드 설명
prev_freehint마지막으로 할당한 위치 근처에서 탐색을 시작해 지역성을 높이고 단편화를 줄입니다. FSINFO의next_cluster와 대응됩니다.EOF_FAT(sbi)변종별 EOC 값(0x0FFFFFF8등). 새 클러스터를 체인 끝으로 표시합니다.- 두 복사본 갱신
fat_ent_write의 인자1은 FAT 대기(writeback)를 의미하며,fats개 복사본을 모두 갱신합니다. FAT12는 read-modify-write 비용이 추가됩니다.
권한과 소유자는 FAT 온디스크에 저장되지 않으므로, 마운트 시 지정한 uid/gid/umask/dmode/fmask 값으로 통일 적용합니다. 이것이 FAT에 chown/chmod가 의미 없는 이유입니다.
읽기와 쓰기 경로
FAT 드라이버는 VFS의 표준 주소 공간 연산(address_space_operations)을 통해 페이지 캐시(Page Cache)와 연동합니다. 파일 읽기는 VFS가 페이지(Page)를 요청하면 FAT가 클러스터 체인을 따라 해당 오프셋의 물리 블록을 찾아 블록 계층에 넘기는 흐름입니다.
/* fs/fat/inode.c — fat_get_block: 논리 블록 → 물리 블록 매핑 (단순화) */
int fat_get_block(struct inode *inode, sector_t iblock,
struct buffer_head *bh_result, int create)
{
struct msdos_sb_info *sbi = MSDOS_SB(inode->i_sb);
unsigned long max_blocks = bh_result->b_size >> inode->i_blkbits;
sector_t phys, last_blocks;
int err, offset;
/* 1) iblock(파일 내 논리 블록) → 클러스터 체인 상 물리 섹터 */
err = fat_get_cluster(inode, iblock, &phys, &last_blocks, create);
if (err)
return err;
/* 2) 클러스터 시작 섹터 = data_start + (cluster - 2) * sec_per_clus */
offset = (iblock & (sbi->sec_per_clus - 1));
map_bh(bh_result, inode->i_sb, phys + offset);
bh_result->b_size = last_blocks << inode->i_blkbits;
return 0;
}
/* fs/fat/fatent.c — fat_get_cluster: 체인 따라가며 클러스터 찾기 (핵심) */
/* 클러스터 번호 c → 디스크 섹터 = data_start + (c - 2) * sec_per_clus */
static int fat_get_cluster(struct inode *inode, sector_t iblock,
sector_t *phys, int *last, int create)
{
struct msdos_sb_info *sbi = MSDOS_SB(inode->i_sb);
int cluster = MSDOS_I(inode)->i_start; /* 시작 클러스터 */
int clus_per_block = sbi->cluster_size >> inode->i_blkbits;
int fclus = iblock / clus_per_block; /* 파일 내 논리 클러스터 번호 */
/* 캐시 hit 시 즉시 반환, miss면 fat_ent_read로 체인 추적 */
while (fclus-- > 0) {
cluster = fat_ent_read(inode->i_sb, &fatent, cluster);
if (cluster < 2 || fat_eof(sbi, cluster))
return create ? fat_extend(...) : -EIO;
}
*phys = fat_clus_to_blknr(sbi, cluster); /* data_start + (c-2)*sec_per_clus */
return 0;
}
코드 설명
fat_get_cluster루프시작 클러스터부터 FAT 엔트리를 따라fclus번 이동합니다. 매번 디스크 FAT 읽기가 발생할 수 있어,msdos_inode_info.cache_lru가 오프셋→클러스터 매핑을 캐싱해 반복 추적을 줄입니다.fat_clus_to_blknr클러스터 번호를 디스크 섹터로 변환하는 핵심 공식:data_start + (cluster - 2) * sec_per_clus. 클러스터 2번이 데이터 영역 첫 클러스터임을 반영합니다.create경로쓰기 중 체인이 끝나면fat_extend가fat_alloc_clusters로 새 클러스터를 이어 붙입니다. 읽기면-EIO로 파일 끝 넘음을 알립니다.map_bh물리 블록 번호를 버퍼 헤드에 기록하면 페이지 캐시와 블록 계층이 해당 디스크 위치로 I/O를 수행합니다.
쓰기 경로에서 파일이 커지면 fat_get_cluster의 create 분기가 fat_alloc_clusters로 새 클러스터를 할당하고 체인 끝에 연결한 뒤 FAT를 갱신합니다. FAT 갱신은 메타데이터 쓰기이므로 데이터와 함께 안정적으로 반영되어야 하지만, FAT에는 저널링(Journaling)이 없어 갱신 도중 정전이 발생하면 FAT 테이블과 실제 데이터 영역이 불일치할 수 있습니다.
쓰기 순서와 무결성
FAT는 순서에 의존한 비저널링 일관성 모델을 가집니다. 여기서 무결성(Integrity)은 쓰기 순서와 FAT 테이블 갱신 시점에 달려 있습니다. 일반적인 안전한 쓰기 순서는 다음과 같습니다.
- 데이터 클러스터에 실제 내용을 쓴다.
- FAT 테이블에 새 클러스터를 체인에 연결한다 (두 복사본 모두).
- 디렉터리 엔트리에 시작 클러스터·크기를 기록한다.
이 순서를 지키면 중간 단계에서 멈춰도 FAT에 연결만 되고 데이터가 없거나, 엔트리가 아직 갱신되지 않은 상태가 되어 fsck로 복구 가능한 형태가 됩니다. 다만 실제 커널의 세부 순서는 지연(Latency) 할당(delalloc)과 페이지 캐시(writeback) 경로로 인해 복잡하며, sync 마운트 옵션으로 순차 반영을 강제해 안전성을 높일 수 있습니다.
마운트 옵션
FAT 마운트 옵션은 호환성과 이름 처리에 직접 영향을 주므로 실무에서 중요합니다. 자주 쓰는 옵션을 정리합니다.
| 옵션 | 설명 |
|---|---|
uid=, gid= | 모든 파일의 소유자/그룹을 고정값으로 지정 |
umask=, dmask=, fmask= | 권한 마스크(디렉터리/파일 분리 가능) |
codepage= | 8.3 단명명 디코딩에 쓸 코드페이지(예: 437) |
iocharset= | 내부 문자셋 변환(예: utf8) |
shortname= | 단명명 표시 모드(lower/win95/winnt/mixed) |
utf8 | LFN을 UTF-8로 처리 |
fat=12|16|32 | FAT 변종 강제(진단용) |
check=relaxed|normal|strict | 이름 검증 엄격도 |
tz=UTC | 타임스탬프를 UTC로 해석 |
time_offset= | 타임스탬프 오프셋(분 단위) |
quiet | 호환되지 않는 옵션 경고 억제 |
showexec | 실행 파일 확장자에 실행 비트 표시 |
sync | 모든 쓰기를 즉시 반영(안전, 느림) |
flush | 쓰기를 더 자주 플러시(Flush) — 이동식 매체 권장 |
errors=panic|remount-ro | FAT 오류 시 동작 |
nfs=stale_rw|nostale_ro | NFS 내보내기 호환성 모드 |
# 일반적인 vfat 마운트 (한글 파일명 + 일반 사용자 접근)
mount -t vfat /dev/sdc1 /mnt/usb -o uid=1000,gid=1000,utf8,umask=022,flush
# UEFI 시스템 파티션(ESP)을 읽기 전용으로 마운트
mount -t vfat /dev/sda1 /mnt/efi -o ro,umask=0077
# /etc/fstab 영구 설정 예 (이동식 USB)
/dev/sdc1 /mnt/usb vfat defaults,uid=1000,gid=1000,utf8,flush,noatime 0 0
코드 설명
flush이동식 매체에서 데이터 손실을 줄이기 위해 자주 플러시합니다. 느리지만 뽑기 전 안전합니다.utf8+codepage한글/다국어 파일명이 깨지지 않도록 LFN은 UTF-8로, 단명명은 지정 코드페이지로 처리합니다.umask=022소유자는 읽기/쓰기, 그 외는 읽기 전용. FAT는 실행 비트 개념이 없으므로 마스크로만 권한을 흉내냅니다.
한계와 제약
FAT의 단순함은 곧 한계이기도 합니다. 현대 파일시스템 관점에서 주요 제약을 정리합니다.
- 파일 크기 한계 — 최대 4GiB - 1바이트.
size필드가 32비트 부호 없는 정수입니다. - 단편화 — 연속 할당을 시도하지만 사용·삭제 반복 시 단편화가 발생하며, 온라인 단편화 해제(defrag)의 안전성이 ext4보다 떨어집니다.
- 권한/소유자 부재 — 온디스크에 저장되지 않아 마운트 단위로만 적용됩니다.
- 심볼릭 링크 불가 — 하드 링크·심볼릭 링크를 지원하지 않습니다(일부 툴이 사이드카 파일로 흉내낼 뿐).
- 저널링 부재 — 비정상 해제 시
fsck가 필요하며, 손상 범위가 클 수 있습니다. - 타임스탬프 정밀도 — 최종 수정/접근/생성 시간이 2초 단위(수정)로 정밀도가 낮고, 시간대(timezone) 정보가 온디스크에 없어 해석이 모호합니다.
- FAT12/16 루트 디렉터리 제한 — 엔트리 수가 고정되어 루트에 너무 많은 파일을 두면 생성 실패합니다.
- 볼륨 크기 한계 — FAT32의 32KB 클러스터 기준 약 8TB가 일반적 구현 한계이며, 일부 도구는 32GB로 제한하기도 합니다.
성능 특성과 튜닝
FAT는 메타데이터가 단순해 순차 읽기에서 오버헤드가 적습니다. 반면 FAT 체인 추적과 빈 클러스터 탐색이 디스크 접근을 유발하므로, 대용량·단편화 환경에서는 메타데이터 I/O가 병목(Bottleneck)이 됩니다.
튜닝 포인트를 정리합니다.
- 클러스터 크기 — 큰 클러스터는 FAT 테이블을 작게 만들어 체인 추적 비용을 줄이지만, 작은 파일이 많으면 낭비(슬랙 공간)가 커집니다. 용도에 맞춰
mkfs.fat의-s(섹터/클러스터)로 조정합니다. - 두 번째 FAT 생략 —
fat=32에서 복사본을 쓰지 않으려 일부 구현은OPTIONS로 제어하지만, 기본 커널은 두 FAT를 모두 갱신합니다. 쓰기 부담을 줄이려면 안전성을 희생하는 트레이드오프가 필요합니다. noatime— 접근 시간 갱신을 끄면 읽기 시 발생하는 메타데이터 쓰기를 줄입니다.flush/sync선택 — 안전성과 성능의 트레이드오프. 이동식 매체는flush, 고정 대용량은 기본 writeback이 더 빠릅니다.- 읽기 성능 — 페이지 캐시와 readahead가 잘 동작하므로, 순차 읽기는 빠릅니다. 다만 직접 I/O(O_DIRECT) 지원이 제한적입니다.
사용자 공간 도구 (dosfstools)
FAT 생성·검사·복구는 dosfstools 패키지가 담당합니다. 커널 드라이버와 별개의 사용자 공간 도구입니다.
# FAT32 파일시스템 생성 (지정 클러스터 크기)
mkfs.fat -F 32 -s 64 /dev/sdc1 # 64섹터 = 32KB 클러스터
# FAT16 / FAT12 강제
mkfs.fat -F 16 /dev/sdc1
mkfs.fat -F 12 /dev/fd0 # 플로피
# 볼륨 라벨 지정
mkfs.fat -F 32 -n MYVOLUME /dev/sdc1
fatlabel /dev/sdc1 NEWLABEL
# 일관성 검사 및 복구
fsck.fat -v /dev/sdc1 # 검사만
fsck.fat -a /dev/sdc1 # 자동 복구
fsck.fat -V /dev/sdc1 # 상세 + 볼륨 정보
# 부트 섹터/BPB 상세 덤프
fsck.fat -V -v /dev/sdc1
dumpe2fs와 유사한 역할은 fsck.fat -V와 parted/fdisk로 확인
코드 설명
-F 12|16|32FAT 변종을 강제합니다. 생략하면 매체 크기에 따라 자동 선택합니다.-s N클러스터 당 섹터 수. 성능과 슬랙 공간의 균형을 잡는 핵심 인자입니다.fsck.fat -a체인 단절·교차 연결(cross-linked) 클러스터·잃은 클러스터(lost)를 자동으로 정리합니다. 마운트 해제한 상태에서 실행해야 합니다.
활용 사례
- UEFI 시스템 파티션(ESP) — 펌웨어 표준이 FAT를 요구하므로, 모든 UEFI 시스템의 ESP는 FAT입니다.
- USB 플래시 / SD 카드 — 윈도·맥·리눅스·카메라·차량 오디오 등 이기종 호환성이 필요한 이동식 매체의 사실상 표준입니다.
- 임베디드 펌웨어 파티션 — 부트로더(Bootloader)·펌웨어 업데이트 파티션이 FAT인 경우가 많아, MTD 상의 FAT 계층도 쓰입니다.
- 플로피 디스크 이미지 — FAT12는 가상 플로피·설치 이미지에 여전히 쓰입니다.
- 소형 부트 파티션 — Raspberry Pi 등 일부 보드의 부트 파티션이 FAT입니다.
커널 설정 옵션
CONFIG_FAT_FS=y # 공통 FAT 코드 (필수)
CONFIG_MSDOS_FS=m # msdos 타입 (8.3 전용, 레거시)
CONFIG_VFAT_FS=m # vfat 타입 (LFN 지원, 일반적 선택)
# 기본 마운트 옵션값 (커널 빌드 시 지정)
CONFIG_FAT_DEFAULT_CODEPAGE=437
CONFIG_FAT_DEFAULT_IOCHARSET="iso8859-1"
# UTF-8 LFN 처리를 기본으로
# CONFIG_FAT_DEFAULT_UTF8=y (사용자 공간 utf8 옵션과 함께)
# 단위 테스트 (KUnit)
# CONFIG_FAT_KUNIT_TEST=y — FAT 자료구조/동작 단위 테스트 빌드
대부분의 배포판은 VFAT_FS와 FAT_FS를 모듈로 제공하므로, modprobe vfat으로 불러올 수 있습니다. msdos는 8.3만 지원하므로 특수 호환 상황이 아니면 거의 쓰지 않습니다. FAT 드라이버는 select BUFFER_HEAD·select NLS·select LEGACY_DIRECT_IO에 의존하며, 본문 작성 기준(2026년 7월) 아직 iomap으로 전환되지 않아 exFAT(7.2 전환)과 달리 큰 폴리오(large folio) 등 향후 저장 스택 개선의 직접 수혜 대상이 아닙니다.
다른 파일시스템과 비교
| 항목 | FAT32 (VFAT) | ext4 | NTFS | exFAT |
|---|---|---|---|---|
| 최대 파일 | ~4GiB | 16TiB(블록 4K 기준) | 8PiB(이론) | 16EiB(이론) |
| 최대 볼륨 | ~8TB(구현 한계) | 1EiB | 8TiB(권장) | 128PiB |
| 저널링 | 없음 | JBD2 | $LogFile | 없음 |
| 권한/ACL | 없음(마운트 단위) | POSIX + ACL | ACL/보안 식별자 | 없음 |
| 타임스탬프 | 2초 단위, 시간대 없음 | 나노초 | 100ns, UTC | 10ms + UTC 오프셋 |
| 이기종 호환 | 매우 높음 | 낮음(리눅스 중심) | 윈도 중심 | 높음(이동식) |
| 리눅스 쓰기 드라이버 | vfat(메인라인) | ext4(메인라인) | ntfs3(메인라인) | exfat(5.7+ 메인라인) |
최신 커널 동향
FAT(vfat/msdos) 드라이버는 1990년대 초반 메인라인 편입 이후 매우 성숙하여 구조적 변화보다는 유지보수·안정성 수정 위주로 진행됩니다. 본문 기준(2026년 7년) 주요 점검 사항을 정리합니다.
- 안정적 유지 — 온디스크 구조가 사실상 고정된 표준이므로, 새 기능보다는 버그 수정·경고 정리·KUnit 테스트 보강(
CONFIG_FAT_KUNIT_TEST)이 주를 이룹니다. - 여전히 buffer_head — FAT 드라이버는
buffer_head기반 페이지 캐시 경로를 유지합니다. 반면 같은 FAT 계열 후계인 exFAT는 Linux 7.2에서 iomap으로 전환되어 큰 폴리오 등 향후 저장 스택 개선을 상속받습니다. FAT에 iomap 전환은 아직 발표되지 않았습니다. - TFAT 미지원 유지 — 트랜잭션 안전 확장(TFAT)은 사양에 정의되어 있으나 메인라인 FAT/exFAT 모두 구현하지 않아, 비정상 해제 시
fsck.fat복구가 여전히 필요합니다. - UEFI ESP 요구 지속 — UEFI 펌웨어가 FAT를 시스템 파티션으로 요구하므로, ESP 용도로는 FAT의 중요성이 최신 커널에서도 변하지 않습니다.
참고자료
- 커널 공식 문서 — FAT (VFAT, MSDOS)
- 리눅스 소스 — fs/fat
- Wikipedia — File Allocation Table
- dosfstools — mkfs.fat / fsck.fat
- Microsoft — FAT/exFAT 사양
- LWN.net — 커널 변경사항 커버리지
- exFAT 파일시스템 — FAT의 후계, 대용량 이동식 매체용
- VFS 계층 — FAT가 올라가는 가상 파일시스템 기반
- Block I/O 서브시스템 — 클러스터 입출력의 하부 계층
- 디스크 파티션 — FAT 볼륨이 자리잡는 파티션 구조