디스크 파티션 (Disk Partitions)
MBR/GPT 상세 구조, 확장 파티션(EBR), 커널 파티션 탐지 서브시스템(block/partitions/), gendisk/block_device 자료구조, Device Mapper, sysfs 인터페이스까지 — 리눅스 커널의 디스크 파티션 구현을 소스 코드 수준에서 분석합니다.
개요
디스크 파티션은 하나의 물리 디스크를 논리적으로 분할하여 독립된 영역으로 관리하는 메커니즘입니다. 운영체제는 각 파티션을 별도의 블록 디바이스로 인식하며, 파일시스템 마운트, 스왑 영역, RAID 구성 등 다양한 용도로 활용합니다.
블록 디바이스 기초
블록 디바이스는 고정 크기 블록 단위로 데이터를 읽고 쓰는 장치입니다. 커널은 블록 디바이스에 대해 버퍼링과 캐싱을 제공하며, 임의 접근(random access)을 지원합니다.
/* 블록 디바이스의 기본 단위 */
섹터 (Sector) : 디스크 I/O의 최소 단위 (전통적으로 512B, 최신 디스크는 4096B)
블록 (Block) : 파일시스템 I/O 단위 (보통 4096B = 페이지 크기)
LBA (Logical Block Addressing) : 섹터를 0부터 선형 번호로 접근
CHS (Cylinder-Head-Sector) : 레거시 주소 지정 (실린더/헤드/섹터)
/* 커널 내부에서 섹터 크기 */
#define SECTOR_SHIFT 9 /* 커널은 항상 512B 섹터 기준 */
#define SECTOR_SIZE (1 << SECTOR_SHIFT) /* 512 */
/* 물리 섹터 크기는 별도 관리 */
unsigned short queue_physical_block_size(struct request_queue *q);
unsigned short queue_logical_block_size(struct request_queue *q);
queue_physical_block_size()로 실제 물리 섹터 크기를 확인할 수 있습니다.디스크 구조: MBR vs GPT
MBR (Master Boot Record) 상세
MBR은 디스크의 첫 번째 섹터(LBA 0, 512바이트)에 위치하며, 부트 코드와 파티션 테이블을 포함합니다. 1983년 IBM PC DOS 2.0에서 도입되어 수십 년간 사용된 파티션 스킴입니다.
MBR 512바이트 구조체
/* include/uapi/linux/genhd.h (개념적 구조) */
struct mbr {
uint8_t bootstrap[446]; /* 부트스트랩 코드 (Stage 1 부트로더) */
struct mbr_partition {
uint8_t status; /* 0x80=부팅 가능(active), 0x00=비활성 */
uint8_t first_chs[3]; /* 시작 CHS 주소 */
uint8_t type; /* 파티션 타입 ID (아래 표 참조) */
uint8_t last_chs[3]; /* 끝 CHS 주소 */
uint32_t first_lba; /* 시작 LBA (리틀 엔디안) */
uint32_t num_sectors; /* 총 섹터 수 */
} partition[4]; /* 4개 파티션 엔트리 × 16B = 64B */
uint16_t signature; /* MBR 시그니처: 0xAA55 */
} __attribute__((__packed__)); /* 총 446 + 64 + 2 = 512B */
파티션 엔트리 16바이트 상세
| 오프셋 | 크기 | 필드 | 설명 |
|---|---|---|---|
| 0x00 | 1B | status | 0x80=부팅 가능, 0x00=비활성, 그 외=잘못된 값 |
| 0x01 | 3B | first_chs | 시작 CHS (Head[7:0], Sector[5:0]|Cyl[9:8], Cyl[7:0]) |
| 0x04 | 1B | type | 파티션 타입 ID (0x83=Linux, 0xEE=GPT 등) |
| 0x05 | 3B | last_chs | 끝 CHS (동일 인코딩) |
| 0x08 | 4B | first_lba | 시작 LBA (리틀 엔디안, 최대 2^32-1) |
| 0x0C | 4B | num_sectors | 파티션 크기(섹터 수, 최대 2^32-1) |
CHS ↔ LBA 변환
/* CHS → LBA 변환 공식 */
LBA = (C × HPC + H) × SPT + S - 1
/*
* C = 실린더 번호 (0-based)
* H = 헤드 번호 (0-based)
* S = 섹터 번호 (1-based)
* HPC = Heads Per Cylinder (디스크 지오메트리)
* SPT = Sectors Per Track (디스크 지오메트리)
*/
/* LBA → CHS 역변환 */
C = LBA / (HPC × SPT)
H = (LBA / SPT) % HPC
S = (LBA % SPT) + 1
/* CHS 한계:
* - 10비트 실린더 (0~1023), 8비트 헤드 (0~255), 6비트 섹터 (1~63)
* - 최대 1024 × 256 × 63 × 512 = 8,422,686,720B ≈ 7.84GB
* - 이 한계를 넘으면 CHS 필드는 0xFE, 0xFF, 0xFF로 채워지고 LBA만 사용 */
주요 파티션 타입 ID
| 타입 ID | 설명 | 비고 |
|---|---|---|
| 0x00 | 빈 엔트리 | 사용되지 않는 슬롯 |
| 0x01 | FAT12 | 플로피 디스크, 소형 파티션 |
| 0x04 | FAT16 (<32MB) | 레거시 DOS |
| 0x05 | Extended (CHS) | 확장 파티션 (CHS 주소 사용) |
| 0x06 | FAT16 (32MB~2GB) | DOS 4.0+ |
| 0x07 | NTFS / exFAT | Windows |
| 0x0B | FAT32 (CHS) | Windows 95 OSR2 |
| 0x0C | FAT32 (LBA) | Windows 95 OSR2, LBA 모드 |
| 0x0E | FAT16 (LBA) | LBA 모드 FAT16 |
| 0x0F | Extended (LBA) | 확장 파티션 (LBA 주소 사용) |
| 0x11 | Hidden FAT12 | 복구 파티션 등 |
| 0x27 | Hidden NTFS (WinRE) | Windows 복구 환경 |
| 0x42 | LDM (Dynamic Disk) | Windows Logical Disk Manager |
| 0x82 | Linux swap | 스왑 파티션 |
| 0x83 | Linux | ext2/ext3/ext4/btrfs/xfs 등 |
| 0x85 | Linux Extended | 리눅스 전용 확장 파티션 |
| 0x8E | Linux LVM | LVM Physical Volume |
| 0xEE | GPT Protective MBR | GPT 디스크의 MBR 호환 마커 |
| 0xEF | EFI System Partition | MBR 환경에서의 ESP |
| 0xFD | Linux RAID autodetect | mdadm 자동 감지 |
block/partitions/msdos.c의 msdos_partition() 함수에서 수행됩니다. 타입 ID에 따라 확장 파티션 체이닝, LVM 감지 등을 분기합니다.확장 파티션과 EBR (Extended Boot Record)
MBR은 최대 4개의 주 파티션만 지원합니다. 이 제한을 우회하기 위해 확장 파티션(Extended Partition)과 EBR 체이닝을 사용합니다.
EBR 체이닝 구조
/* EBR(Extended Boot Record) — 각 논리 드라이브 앞에 하나씩 위치
*
* MBR의 확장 파티션 엔트리 (타입 0x05 또는 0x0F)가 가리키는 첫 EBR부터
* 연결 리스트(Linked List)처럼 체이닝됩니다.
*
* 구조: 446B 미사용 + 2개 파티션 엔트리 + 0xAA55
*/
struct ebr {
uint8_t unused[446]; /* 사용되지 않음 (0으로 채움) */
struct mbr_partition entry[2]; /* 엔트리 2개만 사용 */
/* entry[0]: 이 EBR의 논리 드라이브 (first_lba는 EBR 기준 상대 오프셋) */
/* entry[1]: 다음 EBR 위치 (first_lba는 확장 파티션 시작 기준 상대 오프셋) */
/* type=0이면 체인 종료 */
uint8_t unused2[32]; /* 엔트리 3, 4는 미사용 */
uint16_t signature; /* 0xAA55 */
} __attribute__((__packed__));
/* EBR 체이닝 예시 (sda의 확장 파티션이 LBA 2048에서 시작)
*
* MBR.partition[2] → type=0x0F, first_lba=2048 (확장 파티션)
* ├─ EBR @ LBA 2048
* │ entry[0]: 논리 드라이브 sda5 (LBA=2048+63 기준)
* │ entry[1]: 다음 EBR @ LBA 2048+offset → LBA 1026048
* ├─ EBR @ LBA 1026048
* │ entry[0]: 논리 드라이브 sda6 (LBA=1026048+63 기준)
* │ entry[1]: type=0 → 체인 종료
*/
entry[0].first_lba는 해당 EBR의 위치를 기준으로 한 상대 오프셋이고, entry[1].first_lba는 확장 파티션의 시작을 기준으로 한 상대 오프셋입니다. 이 두 기준점이 다르다는 점에 유의해야 합니다./* block/partitions/msdos.c — EBR 체이닝 파싱 (간략화) */
static void parse_extended(struct parsed_partitions *state,
struct block_device *bdev,
sector_t first_sector, sector_t first_size)
{
struct msdos_partition *p;
Sector sect;
unsigned char *data;
sector_t this_sector = first_sector;
while (1) {
data = read_part_sector(state, this_sector, §);
if (!data)
return;
/* 시그니처 확인 */
if (!msdos_magic_present(data + 510))
goto done;
p = (struct msdos_partition *)(data + 0x1BE);
/* entry[0]: 논리 드라이브 등록 */
if (nr_sects(p) && is_extended_partition(p) == 0)
put_partition(state, state->next,
this_sector + start_sect(p),
nr_sects(p));
/* entry[1]: 다음 EBR (확장 파티션 시작 기준 오프셋) */
p++;
if (nr_sects(p) == 0 || !is_extended_partition(p))
goto done; /* 체인 종료 */
this_sector = first_sector + start_sect(p);
done:
put_dev_sector(sect);
if (nr_sects(p) == 0)
break;
}
}
GPT (GUID Partition Table) 상세
GPT는 UEFI 규격의 일부로 정의된 파티션 스킴으로, MBR의 2TB 크기 제한과 4개 파티션 제한을 해결합니다. LBA 1에 주 GPT 헤더, LBA 2~33에 파티션 엔트리 배열이 위치하며, 디스크 끝에 백업 헤더와 엔트리가 미러링됩니다.
Protective MBR
/* GPT 디스크의 LBA 0: Protective MBR
*
* MBR을 인식하는 레거시 도구가 GPT 디스크를 "빈 디스크"로 오인하여
* 덮어쓰는 것을 방지하기 위한 호환 구조
*/
Protective MBR:
partition[0].type = 0xEE /* GPT Protective */
partition[0].first_lba = 1 /* LBA 1부터 */
partition[0].num_sectors = min(디스크 전체 섹터 수 - 1, 0xFFFFFFFF)
partition[1~3] = 0 /* 나머지 엔트리 비움 */
signature = 0xAA55
GPT 헤더 (92바이트)
/* include/linux/gpt.h */
typedef struct _gpt_header {
__le64 signature; /* "EFI PART" (0x5452415020494645) */
__le32 revision; /* GPT 리비전 (보통 0x00010000 = 1.0) */
__le32 header_size; /* 이 헤더의 크기 (보통 92바이트) */
__le32 header_crc32; /* 헤더 CRC32 (이 필드는 0으로 계산) */
__le32 reserved1; /* 예약 (0) */
__le64 my_lba; /* 이 헤더의 LBA (주 헤더: 1) */
__le64 alternate_lba; /* 백업 헤더의 LBA (디스크 마지막 LBA) */
__le64 first_usable_lba; /* 파티션 데이터 시작 가능 LBA (보통 34) */
__le64 last_usable_lba; /* 파티션 데이터 끝 가능 LBA */
efi_guid_t disk_guid; /* 디스크 고유 GUID */
__le64 partition_entry_lba; /* 파티션 엔트리 배열 시작 LBA (주: 2) */
__le32 num_partition_entries; /* 파티션 엔트리 수 (보통 128) */
__le32 sizeof_partition_entry; /* 각 엔트리 크기 (보통 128B) */
__le32 partition_entry_array_crc32; /* 엔트리 배열 전체의 CRC32 */
/* 나머지는 0으로 패딩하여 sector_size까지 채움 */
} gpt_header;
GPT 파티션 엔트리 (128바이트)
/* include/linux/gpt.h */
typedef struct _gpt_entry {
efi_guid_t partition_type_guid; /* 파티션 타입 GUID (아래 표) */
efi_guid_t unique_partition_guid; /* 이 파티션의 고유 GUID */
__le64 starting_lba; /* 파티션 시작 LBA */
__le64 ending_lba; /* 파티션 끝 LBA (inclusive) */
gpt_entry_attributes attributes; /* 속성 플래그 (64비트) */
__le16 partition_name[36]; /* 파티션 이름 (UTF-16LE, 최대 36문자) */
} gpt_entry;
/* 속성 비트 (attributes) */
비트 0 : 플랫폼 필수 (OEM 파티션, 삭제 금지)
비트 1 : EFI 펌웨어 무시
비트 2 : Legacy BIOS 부팅 가능
비트 48~63: GUID별 정의 (Microsoft: 읽기 전용, 숨김, 자동 마운트 안 함 등)
CRC32 무결성 검증
/* block/partitions/efi.c — GPT 무결성 검증 흐름 */
/* 1단계: 헤더 CRC32 검증 */
origcrc = le32_to_cpu(gpt->header_crc32);
gpt->header_crc32 = 0; /* CRC 필드를 0으로 놓고 계산 */
crc = efi_crc32((void *)gpt, le32_to_cpu(gpt->header_size));
if (crc != origcrc)
/* 헤더 손상 → 백업 GPT 시도 */
/* 2단계: 파티션 엔트리 배열 CRC32 검증 */
crc = efi_crc32((void *)ptes,
le32_to_cpu(gpt->num_partition_entries) *
le32_to_cpu(gpt->sizeof_partition_entry));
if (crc != le32_to_cpu(gpt->partition_entry_array_crc32))
/* 엔트리 배열 손상 → 백업 시도 */
/* 3단계: my_lba / alternate_lba 교차 검증
* 주 헤더: my_lba == 1, alternate_lba == 디스크 마지막 LBA
* 백업 헤더: my_lba == 디스크 마지막 LBA, alternate_lba == 1
*/
주요 파티션 타입 GUID
| GUID | 설명 |
|---|---|
C12A7328-F81F-11D2-BA4B-00A0C93EC93B | EFI System Partition (ESP) |
21686148-6449-6E6F-744E-656564454649 | BIOS Boot Partition |
0FC63DAF-8483-4772-8E79-3D69D8477DE4 | Linux filesystem |
0657FD6D-A4AB-43C4-84E5-0933C84B4F4F | Linux swap |
E6D6D379-F507-44C2-A23C-238F2A3DF928 | Linux LVM |
A19D880F-05FC-4D3B-A006-743F0F84911E | Linux RAID |
933AC7E1-2EB4-4F13-B844-0E14E2AEF915 | Linux /home |
4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709 | Linux root (x86-64) |
EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 | Microsoft basic data (NTFS/FAT) |
E3C9E316-0B5C-4DB8-817D-F92DF00215AE | Microsoft Reserved Partition (MSR) |
DE94BBA4-06D1-4D40-A16A-BFD50179D6AC | Microsoft Recovery (WinRE) |
4F68BCE3...는 자동으로 /에, 933AC7E1...는 /home에 마운트됩니다.커널 파티션 탐지 서브시스템
커널은 블록 디바이스가 등록될 때 block/partitions/ 디렉토리의 파서들을 호출하여 파티션 테이블을 자동으로 탐지합니다.
block/partitions/ 아키텍처
block/partitions/
├── check.c /* 파서 오케스트레이터 — check_partition() */
├── core.c /* blk_add_partitions() 진입점 */
├── msdos.c /* MBR/EBR 파서 */
├── efi.c /* GPT 파서 */
├── mac.c /* Apple Partition Map */
├── sun.c /* Sun/Solaris VTOC */
├── sgi.c /* SGI/IRIX dvh */
├── amiga.c /* Amiga RDB */
├── atari.c /* Atari AHDI */
├── ldm.c /* Windows Logical Disk Manager */
├── ultrix.c /* DEC Ultrix */
├── aix.c /* IBM AIX */
├── cmdline.c /* 커널 커맨드라인 파티션 지정 */
└── Kconfig /* 컴파일 옵션 */
check_partition() 호출 흐름
/* block/partitions/core.c */
static int blk_add_partitions(struct gendisk *disk)
{
struct parsed_partitions *state;
state = check_partition(disk); /* 파티션 탐지 */
if (IS_ERR(state))
return PTR_ERR(state);
if (!state)
return 0; /* 파티션 없음 */
/* 탐지된 파티션을 gendisk에 등록 */
for (p = 1; p < state->limit; p++) {
if (state->parts[p].size)
bdev_add_partition(disk, p,
state->parts[p].from,
state->parts[p].size);
}
free_partitions(state);
return 0;
}
/* block/partitions/check.c */
struct parsed_partitions *check_partition(struct gendisk *hd)
{
static int (*check[])(
struct parsed_partitions *) = {
efi_partition, /* GPT 먼저 시도 (우선순위 높음) */
msdos_partition, /* MBR 파서 */
mac_partition, /* Apple PM */
sun_partition, /* Solaris VTOC */
sgi_partition, /* SGI dvh */
/* ... 기타 파서 ... */
NULL
};
/* 파서를 순서대로 시도, 성공하면 즉시 반환 */
for (i = 0; check[i]; i++) {
int res = check[i](state);
if (res > 0)
return state; /* 파티션 발견 */
}
return NULL; /* 인식된 파티션 없음 */
}
struct parsed_partitions
/* block/partitions/check.h */
struct parsed_partitions {
struct gendisk *disk; /* 대상 디스크 */
char name[BDEVNAME_SIZE]; /* 디바이스 이름 (예: "sda") */
struct {
sector_t from; /* 파티션 시작 섹터 */
sector_t size; /* 파티션 크기 (섹터 단위) */
int flags; /* ADDPART_FLAG_NONE / RAID / WHOLEDISK */
} *parts;
int next; /* 다음 파티션 번호 */
int limit; /* 최대 파티션 수 */
bool access_beyond_eod; /* 디스크 끝을 넘는 파티션 감지 */
};
MBR 파서 (msdos.c) 핵심 로직
/* block/partitions/msdos.c — msdos_partition() 간략화 */
int msdos_partition(struct parsed_partitions *state)
{
unsigned char *data;
struct msdos_partition *p;
Sector sect;
data = read_part_sector(state, 0, §); /* LBA 0 읽기 */
if (!data)
return -1;
/* 0xAA55 시그니처 확인 */
if (!msdos_magic_present(data + 510)) {
put_dev_sector(sect);
return 0; /* MBR 아님 */
}
/* GPT Protective MBR인지 확인 */
p = (struct msdos_partition *)(data + 0x1BE);
for (i = 0; i < 4; i++)
if (p[i].sys_ind == 0xEE) {
put_dev_sector(sect);
return 0; /* GPT 디스크 → msdos 파서 스킵 */
}
/* 4개 주 파티션 등록 */
for (i = 0; i < 4; i++, p++) {
if (nr_sects(p) == 0)
continue;
put_partition(state, i + 1, start_sect(p), nr_sects(p));
if (is_extended_partition(p))
parse_extended(state, ...); /* EBR 체이닝 파싱 */
}
put_dev_sector(sect);
return 1; /* 파티션 발견 */
}
GPT 파서 (efi.c) 핵심 로직
/* block/partitions/efi.c — efi_partition() 간략화 */
int efi_partition(struct parsed_partitions *state)
{
gpt_header *gpt = NULL;
gpt_entry *ptes = NULL;
/* 1. Protective MBR 확인 */
if (!is_pmbr_valid(legacymbr, total_sectors))
goto fail;
/* 2. 주 GPT 헤더 읽기 + CRC32 검증 */
gpt = alloc_read_gpt_header(state, 1); /* LBA 1 */
if (!is_gpt_valid(state, 1, &gpt, &ptes)) {
/* 주 GPT 실패 → 백업 GPT 시도 */
if (!is_gpt_valid(state, last_lba, &gpt, &ptes))
goto fail;
pr_warn("primary GPT invalid, using backup\n");
}
/* 3. 파티션 엔트리 순회 및 등록 */
for (i = 0; i < le32_to_cpu(gpt->num_partition_entries); i++) {
if (efi_guidcmp(ptes[i].partition_type_guid, NULL_GUID))
continue; /* 빈 엔트리 스킵 */
put_partition(state, i + 1,
le64_to_cpu(ptes[i].starting_lba),
le64_to_cpu(ptes[i].ending_lba) -
le64_to_cpu(ptes[i].starting_lba) + 1);
}
return 1; /* 파티션 발견 */
}
블록 디바이스 자료구조
커널은 물리 디스크와 파티션을 서로 다른 자료구조로 관리합니다. 디스크 전체는 struct gendisk, 개별 파티션(및 디스크 전체)은 struct block_device로 표현됩니다.
struct gendisk
/* include/linux/blkdev.h */
struct gendisk {
int major; /* 주 번호 (예: SCSI=8) */
int first_minor; /* 첫 번째 부 번호 */
int minors; /* 최대 파티션 수 + 1 */
char disk_name[DISK_NAME_LEN]; /* 디바이스 이름 (예: "sda") */
struct block_device *part0; /* 디스크 전체를 대표하는 block_device */
struct disk_part_tbl *part_tbl; /* 파티션 테이블 (RCU 보호) */
const struct block_device_operations *fops; /* 디바이스 연산 */
struct request_queue *queue; /* I/O 요청 큐 */
sector_t capacity; /* 디스크 전체 크기 (512B 섹터 단위) */
int flags; /* GENHD_FL_REMOVABLE 등 */
struct kobject *slave_dir; /* sysfs /sys/block/sdX/slaves/ */
void *private_data; /* 드라이버별 데이터 */
};
struct block_device
/* include/linux/blk_types.h */
struct block_device {
dev_t bd_dev; /* 디바이스 번호 (major:minor) */
struct inode *bd_inode; /* /dev 엔트리의 inode */
struct gendisk *bd_disk; /* 소속 디스크 */
u8 bd_partno; /* 파티션 번호 (0=전체 디스크) */
sector_t bd_start_sect; /* 파티션 시작 섹터 */
sector_t bd_nr_sectors; /* 파티션 섹터 수 */
bool bd_read_only; /* 읽기 전용 플래그 */
struct block_device *bd_contains; /* 전체 디스크 block_device */
unsigned bd_part_count; /* 열린 파티션 수 */
};
Major/Minor 번호
/* 주요 블록 디바이스 Major 번호 */
Major 8 : SCSI 디스크 (sd) /* sda=8:0, sda1=8:1, ..., sdb=8:16 */
Major 65~71: SCSI 디스크 (확장) /* sdq~sdaf */
Major 259 : Block Extended Major /* NVMe 등 최신 디바이스 */
Major 3 : IDE 디스크 (hd) /* hda=3:0 (레거시) */
Major 253 : Device Mapper /* dm-0, dm-1, ... */
Major 9 : MD RAID /* md0, md1, ... */
/* Minor 번호 할당 규칙 (SCSI 디스크) */
sda = 8:0 /* 디스크 전체 (partno=0) */
sda1 = 8:1 /* 첫 번째 파티션 */
sda2 = 8:2 /* 두 번째 파티션 */
...
sda15= 8:15 /* 15번째 파티션 (SCSI 최대) */
sdb = 8:16 /* 두 번째 디스크 */
sdb1 = 8:17
/* NVMe Minor 할당 */
nvme0n1 = 259:0 /* 첫 번째 NVMe 네임스페이스 */
nvme0n1p1 = 259:1 /* 첫 번째 파티션 */
/dev/sda(minor 0)는 디스크 전체를 나타내며 bd_partno=0입니다. /dev/sda1(minor 1)은 첫 번째 파티션으로 bd_partno=1이고, bd_start_sect와 bd_nr_sectors로 범위가 제한됩니다. I/O 요청 시 파티션의 시작 오프셋이 자동으로 더해집니다.sysfs 인터페이스
커널은 블록 디바이스와 파티션 정보를 sysfs를 통해 유저 공간에 노출합니다.
/sys/block/ 구조
/sys/block/sda/
├── dev 8:0 (major:minor)
├── size 디스크 전체 크기 (512B 섹터 단위)
├── removable 0 또는 1
├── ro 읽기 전용 여부
├── queue/ I/O 스케줄러, 블록 크기 등
│ ├── scheduler 현재 I/O 스케줄러 (mq-deadline, none, bfq)
│ ├── logical_block_size 논리 블록 크기 (보통 512)
│ ├── physical_block_size 물리 블록 크기 (512 또는 4096)
│ ├── nr_requests 큐 깊이
│ └── rotational 회전 장치 여부 (0=SSD, 1=HDD)
├── device/ SCSI 장치 정보 링크
├── stat I/O 통계 (reads, writes, io_ticks 등)
├── sda1/ 파티션 서브디렉토리
│ ├── dev 8:1
│ ├── size 파티션 크기
│ ├── start 파티션 시작 섹터
│ ├── partition 파티션 번호 (1)
│ └── uevent udev 이벤트 정보
├── sda2/
│ └── ...
├── holders/ 이 디스크를 사용하는 DM/MD 장치
└── slaves/ 이 장치의 하위 장치 (DM용)
/sys/class/block/
/* /sys/class/block/은 모든 블록 디바이스(디스크+파티션)를 평탄하게 나열 */
/sys/class/block/
├── sda -> ../../devices/pci.../host0/target.../0:0:0:0/block/sda
├── sda1 -> ../../devices/pci.../host0/target.../0:0:0:0/block/sda/sda1
├── sda2 -> ../../devices/pci.../host0/target.../0:0:0:0/block/sda/sda2
├── nvme0n1 -> ../../devices/pci.../nvme/nvme0/nvme0n1
├── dm-0 -> ../../devices/virtual/block/dm-0
└── ...
/* 파티션 정보 확인 명령어 */
$ cat /sys/block/sda/sda1/start # 파티션 시작 섹터
$ cat /sys/block/sda/sda1/size # 파티션 크기 (섹터)
$ cat /sys/block/sda/sda1/uevent # MAJOR, MINOR, DEVNAME, DEVTYPE=partition
uevent 처리
/* 파티션 발견 시 uevent 발생 흐름 */
blk_add_partitions()
→ bdev_add_partition()
→ device_add()
→ kobject_uevent(&part->bd_device.kobj, KOBJ_ADD)
→ udev/systemd-udevd가 이벤트 수신
→ /dev/sdX1 노드 생성, 심볼릭 링크 생성 등
/* uevent 내용 예시:
* ACTION=add
* DEVTYPE=partition
* DEVNAME=sda1
* MAJOR=8
* MINOR=1
* PARTN=1
*/
Device Mapper
Device Mapper(DM)는 블록 디바이스를 논리적으로 매핑하는 커널 프레임워크입니다. LVM, dm-crypt, dm-raid 등 다양한 스토리지 기능의 기반입니다.
DM 아키텍처
/* Device Mapper 계층 구조
*
* 유저 공간 (dmsetup, lvm2, cryptsetup)
* │ ioctl (/dev/mapper/control)
* ▼
* dm-ioctl.c ──── DM 커널 인터페이스
* │
* ▼
* dm.c / dm-table.c ── DM 코어
* │
* ├── dm-linear.c (선형 매핑)
* ├── dm-stripe.c (스트라이핑)
* ├── dm-crypt.c (암호화)
* ├── dm-raid.c (소프트웨어 RAID)
* ├── dm-cache.c (SSD 캐시)
* ├── dm-thin.c (Thin Provisioning)
* ├── dm-verity.c (무결성 검증)
* ├── dm-integrity.c(무결성 태그)
* └── dm-snap.c (스냅샷)
* │
* ▼
* 하위 블록 디바이스 (sd, nvme, md 등)
*/
/* 핵심 자료구조 */
struct mapped_device {
struct dm_table *map; /* 현재 활성 테이블 */
struct gendisk *disk; /* /dev/dm-N */
char name[16]; /* 매핑 이름 */
};
struct dm_table {
struct dm_target *targets; /* 타겟 배열 */
unsigned num_targets;
};
struct dm_target {
struct target_type *type; /* linear, crypt, raid 등 */
sector_t begin, len; /* 이 타겟이 담당하는 범위 */
void *private; /* 타겟별 데이터 */
};
dm-linear (선형 매핑)
# 가장 기본적인 DM 타겟: 하위 디바이스의 영역을 그대로 매핑
# LVM의 각 Logical Volume이 내부적으로 dm-linear 사용
# dmsetup으로 수동 생성 예시:
$ echo "0 2097152 linear /dev/sda1 0" | dmsetup create my-linear
# ↑시작 ↑크기 ↑타겟 ↑디바이스 ↑오프셋
# → /dev/mapper/my-linear 생성 (sda1의 0~2097152 섹터 매핑)
dm-crypt (디스크 암호화)
# LUKS 컨테이너 생성 및 사용 흐름
$ cryptsetup luksFormat /dev/sda2 # LUKS 헤더 작성
$ cryptsetup luksOpen /dev/sda2 mycrypt # dm-crypt 매핑 생성
# → /dev/mapper/mycrypt 생성 (암호화된 블록 디바이스)
# 내부적으로 dm-crypt 타겟이 I/O를 가로채서 암/복호화:
# 쓰기: 평문 → AES-XTS 암호화 → /dev/sda2에 기록
# 읽기: /dev/sda2에서 읽기 → AES-XTS 복호화 → 평문 반환
# DM 테이블 확인
$ dmsetup table mycrypt
# 0 1953525168 crypt aes-xts-plain64 :64:logon:cryptkey 0 8:2 32768
dm-raid
# dm-raid: MD RAID를 DM 프레임워크로 감싼 타겟
# LVM RAID가 내부적으로 사용
$ lvcreate --type raid1 -m 1 -L 10G -n mirror vg0
# → dm-raid1 타겟으로 /dev/vg0/mirror 생성
# 하위에 2개의 Physical Volume 영역을 미러링
LVM 레이어
/* LVM (Logical Volume Manager) 계층 구조
*
* Logical Volume (LV) ← /dev/vg0/lv_root
* │
* Volume Group (VG) ← vg0 (여러 PV를 하나로 묶음)
* │
* Physical Volume (PV) ← /dev/sda1, /dev/sdb1
* │
* 물리 디스크/파티션
*
* 모든 LV는 내부적으로 DM 디바이스:
* /dev/vg0/lv_root → /dev/dm-0 → dm-linear 타겟(들)
*/
# LVM 기본 명령
$ pvcreate /dev/sda1 # PV 초기화
$ vgcreate vg0 /dev/sda1 # VG 생성
$ lvcreate -L 20G -n lv_root vg0 # LV 생성
$ lvextend -L +10G /dev/vg0/lv_root # LV 확장
# DM 매핑 확인
$ dmsetup ls
vg0-lv_root (253:0)
vg0-lv_swap (253:1)
$ dmsetup table vg0-lv_root
0 41943040 linear 8:1 2048 # sda1의 섹터 2048부터 41943040 섹터
파티션 관리 도구와 커널 인터페이스
유저 공간 도구
| 도구 | 지원 스킴 | 설명 |
|---|---|---|
fdisk | MBR, GPT | 가장 오래된 파티션 도구, 최신 버전은 GPT 지원 |
gdisk | GPT 전용 | GPT 전용 도구 (MBR↔GPT 변환 기능 포함) |
parted | MBR, GPT | GNU Parted, 파티션 크기 조절 가능 |
sfdisk | MBR, GPT | 스크립트용 비대화형 도구 (백업/복원) |
sgdisk | GPT 전용 | gdisk의 스크립트 버전 |
cfdisk | MBR, GPT | curses 기반 UI |
partprobe | - | 커널에 파티션 테이블 재읽기 요청 |
BLKPG ioctl
/* 커널에 파티션 추가/삭제/크기 변경을 직접 요청하는 ioctl */
#include <linux/blkpg.h>
struct blkpg_ioctl_arg {
int op; /* BLKPG_ADD_PARTITION / BLKPG_DEL_PARTITION / BLKPG_RESIZE_PARTITION */
int flags; /* 미사용 */
int datalen; /* sizeof(struct blkpg_partition) */
void *data; /* struct blkpg_partition * */
};
struct blkpg_partition {
long long start; /* 파티션 시작 (바이트) */
long long length; /* 파티션 크기 (바이트) */
int pno; /* 파티션 번호 */
char devname[64]; /* 디바이스 이름 (선택) */
char volname[64]; /* 볼륨 이름 (선택) */
};
/* 사용 예: 파티션 추가 */
struct blkpg_partition part = {
.start = 1048576, /* 1MB (2048 섹터) */
.length = 1073741824, /* 1GB */
.pno = 1,
};
struct blkpg_ioctl_arg arg = {
.op = BLKPG_ADD_PARTITION,
.datalen = sizeof(part),
.data = &part,
};
ioctl(fd, BLKPG, &arg);
partprobe와 BLKRRPART
/* partprobe가 사용하는 ioctl */
ioctl(fd, BLKRRPART, 0);
/* 커널이 디스크의 파티션 테이블을 처음부터 다시 읽어
* 기존 파티션을 삭제하고 새로 등록함.
*
* 주의: 사용 중인 파티션이 있으면 EBUSY 반환.
* 이 경우 BLKPG ioctl로 개별 파티션을 추가/삭제해야 함.
*/
/* 명령행에서의 사용 */
$ partprobe /dev/sda # 특정 디스크
$ partprobe # 모든 디스크
$ blockdev --rereadpt /dev/sda # 동일 효과 (BLKRRPART) */
MBR vs GPT 비교 요약
| 항목 | MBR | GPT |
|---|---|---|
| 도입 시기 | 1983년 (IBM PC DOS 2.0) | 2000년대 초 (EFI/UEFI 규격) |
| 최대 디스크 크기 | 2TB (2^32 x 512B) | 9.4ZB (2^64 x 512B) |
| 최대 파티션 수 | 4개 주 파티션 (확장 포함 시 이론상 무제한) | 128개 (기본, 확장 가능) |
| 주소 방식 | 32비트 LBA (CHS 호환) | 64비트 LBA |
| 파티션 식별 | 1바이트 타입 ID (0x83 등) | 16바이트 GUID |
| 파티션 이름 | 없음 | UTF-16LE, 최대 36문자 |
| 이중화 | 없음 (MBR 손상 시 복구 불가) | 주 + 백업 GPT 헤더/엔트리 |
| 무결성 검증 | 0xAA55 시그니처만 | 헤더 CRC32 + 엔트리 배열 CRC32 |
| 부팅 마커 | status 바이트 (0x80=active) | UEFI Boot Manager / ESP |
| 부트 코드 | 446바이트 (MBR 내장) | Protective MBR (호환용) |
| 호환성 | 모든 BIOS/UEFI | UEFI 필수 (BIOS는 제한적 지원) |
| 커널 파서 | block/partitions/msdos.c | block/partitions/efi.c |
FAT (File Allocation Table) 파일시스템 상세
FAT는 1977년 Microsoft가 8인치 플로피 디스크용으로 설계한 파일시스템으로, 단순한 구조 덕분에 40년 이상 사실상의 범용 교환 포맷으로 살아남았습니다. UEFI ESP(EFI System Partition), USB 스틱, SD 카드, 디지털 카메라, 임베디드 시스템 등 다양한 환경에서 사용됩니다.
FAT 버전별 차이
| 항목 | FAT12 | FAT16 | FAT32 | exFAT |
|---|---|---|---|---|
| 도입 | 1977 (MDOS) | 1984 (DOS 3.0) | 1996 (Win95 OSR2) | 2006 (CE 6.0) |
| FAT 엔트리 크기 | 12비트 | 16비트 | 28비트 (상위 4비트 예약) | 32비트 |
| 최대 클러스터 수 | 4,084 (0xFF4) | 65,524 (0xFFF4) | 268,435,444 (0x0FFFFFF4) | 232-11 |
| 최대 볼륨 크기 | 16MB (4KB 클러스터) | 2GB (32KB 클러스터) | 2TB (32KB 클러스터) | 128PB (이론상) |
| 최대 파일 크기 | 볼륨 크기 | 2GB | 4GB − 1B | 16EB (이론상) |
| 루트 디렉토리 | 고정 영역 (보통 224엔트리) | 고정 영역 (보통 512엔트리) | 일반 클러스터 체인 | 일반 클러스터 체인 |
| LFN (긴 파일명) | VFAT 확장 | VFAT 확장 | VFAT 확장 | 네이티브 (UTF-16) |
| MBR 타입 ID | 0x01 | 0x04/0x06/0x0E | 0x0B/0x0C | 0x07 |
| 커널 소스 | fs/fat/ (공통) | fs/exfat/ | ||
볼륨 레이아웃
BPB (BIOS Parameter Block) 구조
FAT 볼륨의 첫 번째 섹터(부트 섹터)에는 BPB가 위치합니다. BPB는 볼륨 기하 정보와 FAT 메타데이터를 포함하며, FAT12/16과 FAT32에서 구조가 다릅니다.
/* FAT 부트 섹터 공통 필드 (오프셋 0~35) */
struct fat_boot_sector {
__u8 BS_jmpBoot[3]; /* 0x00: 점프 명령어 (EB xx 90 또는 E9 xx xx) */
__u8 BS_OEMName[8]; /* 0x03: OEM 이름 ("MSWIN4.1" 등) */
/* ---- BPB (BIOS Parameter Block) ---- */
__le16 BPB_BytsPerSec; /* 0x0B: 섹터 크기 (512, 1024, 2048, 4096) */
__u8 BPB_SecPerClus; /* 0x0D: 클러스터당 섹터 수 (1,2,4,8,16,32,64,128) */
__le16 BPB_RsvdSecCnt; /* 0x0E: 예약 섹터 수 (FAT12/16: 1, FAT32: 보통 32) */
__u8 BPB_NumFATs; /* 0x10: FAT 복사본 수 (보통 2) */
__le16 BPB_RootEntCnt; /* 0x11: 루트 디렉토리 엔트리 수 (FAT32: 0) */
__le16 BPB_TotSec16; /* 0x13: 총 섹터 수 16비트 (0이면 TotSec32 사용) */
__u8 BPB_Media; /* 0x15: 미디어 타입 (0xF8=HDD, 0xF0=플로피) */
__le16 BPB_FATSz16; /* 0x16: FAT 하나의 섹터 수 (FAT32: 0) */
__le16 BPB_SecPerTrk; /* 0x18: 트랙당 섹터 (CHS 용, INT 13h) */
__le16 BPB_NumHeads; /* 0x1A: 헤드 수 (CHS 용) */
__le32 BPB_HiddSec; /* 0x1C: 숨겨진 섹터 (파티션 시작 LBA) */
__le32 BPB_TotSec32; /* 0x20: 총 섹터 수 32비트 */
} __attribute__((packed));
/* FAT32 확장 BPB (오프셋 36~89) — FAT12/16과 다른 부분 */
struct fat32_extended_bpb {
__le32 BPB_FATSz32; /* 0x24: FAT 하나의 섹터 수 (FAT32 전용) */
__le16 BPB_ExtFlags; /* 0x28: 플래그 (비트7: FAT 미러링 비활성) */
__le16 BPB_FSVer; /* 0x2A: 버전 (0x0000) */
__le32 BPB_RootClus; /* 0x2C: 루트 디렉토리 시작 클러스터 (보통 2) */
__le16 BPB_FSInfo; /* 0x30: FSINFO 섹터 번호 (보통 1) */
__le16 BPB_BkBootSec; /* 0x32: 백업 부트 섹터 (보통 6) */
__u8 BPB_Reserved[12]; /* 0x34: 예약 (0으로 채움) */
__u8 BS_DrvNum; /* 0x40: 드라이브 번호 (0x80=HDD) */
__u8 BS_Reserved1; /* 0x41: 예약 */
__u8 BS_BootSig; /* 0x42: 확장 부트 시그니처 (0x29) */
__le32 BS_VolID; /* 0x43: 볼륨 시리얼 번호 */
__u8 BS_VolLab[11]; /* 0x47: 볼륨 레이블 (11바이트, 패딩) */
__u8 BS_FilSysType[8]; /* 0x52: "FAT32 " (참고용, 판별에 사용 금지) */
} __attribute__((packed));
/* 부트 섹터 끝 시그니처: 오프셋 510~511 = 0x55, 0xAA */
BS_FilSysType 필드("FAT12", "FAT16", "FAT32")는 포맷 도구가 설정하는 참고용 문자열일 뿐, FAT 유형 판별에 사용해서는 안 됩니다. 실제 유형은 반드시 총 클러스터 수로 계산해야 합니다. 리눅스 커널의 fat_fill_super()도 이 규칙을 따릅니다.FSINFO 구조 (FAT32 전용)
FAT32에서 Reserved 영역의 섹터 1에 위치하는 FSINFO는 빈 클러스터 힌트 정보를 제공하여 할당 성능을 향상시킵니다.
/* FAT32 FSINFO 구조 (512바이트) */
struct fat32_fsinfo {
__le32 FSI_LeadSig; /* 0x000: 0x41615252 ("RRaA") */
__u8 FSI_Reserved1[480]; /* 0x004: 예약 (0으로 채움) */
__le32 FSI_StrucSig; /* 0x1E4: 0x61417272 ("rrAa") */
__le32 FSI_Free_Count; /* 0x1E8: 빈 클러스터 수 (0xFFFFFFFF=미확인) */
__le32 FSI_Nxt_Free; /* 0x1EC: 다음 빈 클러스터 힌트 (0xFFFFFFFF=미확인) */
__u8 FSI_Reserved2[12]; /* 0x1F0: 예약 */
__le32 FSI_TrailSig; /* 0x1FC: 0xAA550000 */
};
/* 커널에서의 FSINFO 처리 (fs/fat/inode.c) */
if (sbi->free_clusters != -1 && sbi->prev_free != -1)
/* FSINFO 힌트가 유효하면 빈 클러스터 탐색 시작점으로 사용 */
sbi->free_clus_valid = 1;
FSI_Free_Count와 FSI_Nxt_Free는 힌트일 뿐 정확하지 않을 수 있습니다. 커널은 마운트 시 이 값을 참고하되, 실제 할당 시에는 FAT 테이블을 직접 스캔합니다. fsck.fat -a로 FSINFO를 정확한 값으로 복구할 수 있습니다.FAT 테이블 구조와 클러스터 체인
FAT(File Allocation Table)은 볼륨의 핵심 자료구조입니다. 각 클러스터마다 하나의 FAT 엔트리가 대응하며, 파일이 사용하는 클러스터들을 연결 리스트(체인)로 관리합니다.
# FAT 엔트리 값의 의미 (FAT32 기준, 하위 28비트만 사용)
0x00000000 : 빈 클러스터 (할당 가능)
0x00000002 ~ 0x0FFFFFEF : 다음 클러스터 번호 (체인 연결)
0x0FFFFFF0 ~ 0x0FFFFFF6 : 예약
0x0FFFFFF7 : 배드 클러스터 (손상됨, 할당 제외)
0x0FFFFFF8 ~ 0x0FFFFFFF : EOC (End Of Chain, 파일 끝)
# FAT12/16도 동일한 패턴 (비트 수만 다름)
FAT12: EOC = 0xFF8~0xFFF, Bad = 0xFF7
FAT16: EOC = 0xFFF8~0xFFFF, Bad = 0xFFF7
# 클러스터 번호는 2부터 시작 (0, 1은 예약)
FAT[0] : 미디어 타입 바이트 복사 (0xF8FFFF0F 등)
FAT[1] : EOC 마커 + dirty/error 플래그
/* 예: 파일 A가 클러스터 3→7→8→EOF 순서로 저장된 경우 */
/* 인덱스: [0] [1] [2] [3] [4] [5] [6] [7] [8] */
/* FAT값: media EOC free 7 free free free 8 EOC */
/* 디렉토리 엔트리의 시작 클러스터 = 3
* FAT[3] = 7 → 다음 클러스터는 7
* FAT[7] = 8 → 다음 클러스터는 8
* FAT[8] = EOC → 파일 끝
* 총 3 클러스터 × 클러스터 크기 = 파일이 차지하는 디스크 공간 */
/* 클러스터 번호 → 디스크 섹터 변환 */
sector_t first_sector = data_start + (cluster - 2) * sec_per_clus;
/* 여기서 data_start는:
* FAT12/16: ResvdSecCnt + (NumFATs × FATSz) + RootDirSectors
* FAT32: ResvdSecCnt + (NumFATs × FATSz32)
* RootDirSectors = ((RootEntCnt × 32) + (BytsPerSec - 1)) / BytsPerSec */
BPB_ExtFlags 비트 7이 설정되면 미러링이 비활성되고, 비트 0~3이 가리키는 FAT만 활성화됩니다. 리눅스 커널은 fat_ent_access_init()에서 이를 처리합니다.디렉토리 엔트리 구조
FAT 디렉토리는 32바이트 고정 크기 엔트리의 배열입니다. 각 엔트리는 파일/디렉토리의 이름, 속성, 크기, 시작 클러스터 등을 저장합니다.
/* 표준 디렉토리 엔트리 — 8.3 (Short File Name) 형식 */
struct msdos_dir_entry {
__u8 DIR_Name[11]; /* 0x00: 파일명 8바이트 + 확장자 3바이트 (대문자, 공백 패딩) */
__u8 DIR_Attr; /* 0x0B: 속성 플래그 */
__u8 DIR_NTRes; /* 0x0C: NT 예약 (대소문자 정보) */
__u8 DIR_CrtTimeTenth; /* 0x0D: 생성 시간 10ms 단위 (0~199) */
__le16 DIR_CrtTime; /* 0x0E: 생성 시간 (시:분:초/2) */
__le16 DIR_CrtDate; /* 0x10: 생성 날짜 */
__le16 DIR_LstAccDate; /* 0x12: 최종 접근 날짜 */
__le16 DIR_FstClusHI; /* 0x14: 시작 클러스터 상위 16비트 (FAT32 전용) */
__le16 DIR_WrtTime; /* 0x16: 최종 수정 시간 */
__le16 DIR_WrtDate; /* 0x18: 최종 수정 날짜 */
__le16 DIR_FstClusLO; /* 0x1A: 시작 클러스터 하위 16비트 */
__le32 DIR_FileSize; /* 0x1C: 파일 크기 (바이트, 디렉토리는 0) */
} __attribute__((packed)); /* 총 32바이트 */
/* DIR_Attr 속성 플래그 */
#define ATTR_READ_ONLY 0x01 /* 읽기 전용 */
#define ATTR_HIDDEN 0x02 /* 숨김 */
#define ATTR_SYSTEM 0x04 /* 시스템 */
#define ATTR_VOLUME_ID 0x08 /* 볼륨 레이블 */
#define ATTR_DIR 0x10 /* 서브디렉토리 */
#define ATTR_ARCHIVE 0x20 /* 아카이브 (수정 시 설정) */
/* 0x0F = ATTR_READ_ONLY|ATTR_HIDDEN|ATTR_SYSTEM|ATTR_VOLUME_ID
* → LFN (Long File Name) 엔트리를 나타내는 특수 조합 */
#define ATTR_LONG_NAME 0x0F
/* DIR_Name[0] 특수 값 */
/* 0x00 : 이 엔트리와 이후 모두 비어있음 (탐색 종료) */
/* 0xE5 : 삭제된 엔트리 (재사용 가능) */
/* 0x05 : 실제 첫 바이트가 0xE5인 경우 (일본어 KANJI 호환) */
/* 0x2E : '.' 또는 '..' 디렉토리 엔트리 */
/* FAT 날짜/시간 인코딩 */
/* 날짜: 비트 15~9=연도(0=1980), 비트 8~5=월(1~12), 비트 4~0=일(1~31) */
/* 시간: 비트 15~11=시(0~23), 비트 10~5=분(0~59), 비트 4~0=초/2(0~29) */
static inline void fat_time_decode(__le16 time, __le16 date,
struct timespec64 *ts)
{
ts->tv_sec = mktime64(
(date >> 9) + 1980, /* 연도 (1980 기준) */
(date >> 5) & 0x0F, /* 월 */
date & 0x1F, /* 일 */
time >> 11, /* 시 */
(time >> 5) & 0x3F, /* 분 */
(time & 0x1F) << 1 /* 초 (2초 단위) */
);
}
LFN (Long File Name) — VFAT 확장
8.3 형식의 11바이트 파일명 제한을 극복하기 위해 Windows 95에서 도입된 VFAT은 하위 호환성을 유지하면서 최대 255자의 유니코드 파일명을 지원합니다. LFN은 SFN 엔트리 앞에 하나 이상의 LFN 엔트리를 배치하는 방식입니다.
/* LFN 디렉토리 엔트리 (32바이트) */
struct msdos_dir_slot {
__u8 id; /* 0x00: 시퀀스 번호 (1부터, 마지막은 0x40 OR) */
__le16 name0_4[5]; /* 0x01: 이름 문자 1~5 (UCS-2) */
__u8 attr; /* 0x0B: 항상 0x0F (ATTR_LONG_NAME) */
__u8 reserved; /* 0x0C: 0 */
__u8 alias_checksum; /* 0x0D: SFN 체크섬 (무결성 검증) */
__le16 name5_10[6]; /* 0x0E: 이름 문자 6~11 (UCS-2) */
__le16 start; /* 0x1A: 항상 0 (하위 호환용) */
__le16 name11_12[2]; /* 0x1C: 이름 문자 12~13 (UCS-2) */
} __attribute__((packed));
/* LFN 엔트리당 13개 UCS-2 문자 저장
* 최대 20개 LFN 엔트리 × 13 = 260문자 (실제 최대 255자)
*
* 디스크 배치 순서 (역순):
* LFN #3 (id=0x43, 마지막) ← 이름 문자 27~39
* LFN #2 (id=0x02) ← 이름 문자 14~26
* LFN #1 (id=0x01) ← 이름 문자 1~13
* SFN (8.3 엔트리) ← 실제 메타데이터 (크기, 시작 클러스터 등)
*/
/* SFN 체크섬 계산 (LFN 무결성 검증용) */
static unsigned char fat_checksum(const unsigned char *name)
{
unsigned char sum = 0;
for (int i = 0; i < 11; i++)
sum = (sum >> 1) + (sum << 7) + name[i]; /* ROL + add */
return sum;
}
리눅스 커널의 FAT 구현 (fs/fat/)
fs/fat/
├── inode.c /* 슈퍼블록 초기화(fat_fill_super), inode 연산 */
├── dir.c /* 디렉토리 읽기/검색, LFN 처리 */
├── file.c /* 파일 읽기/쓰기/fallocate */
├── fatent.c /* FAT 엔트리 접근 — 12/16/32비트 핸들러 */
├── cache.c /* FAT 캐시 (빈 클러스터 탐색 최적화) */
├── misc.c /* 유틸리티: 날짜/시간 변환, 클러스터 연산 */
├── namei_vfat.c /* vfat용 이름 해석 (LFN 지원) */
├── namei_msdos.c /* msdos용 이름 해석 (8.3 전용) */
├── nfs.c /* NFS 내보내기 지원 */
└── Kconfig /* 설정 옵션 */
/* FAT 슈퍼블록 정보 — 마운트 시 BPB에서 파싱 (fs/fat/inode.c) */
struct msdos_sb_info {
unsigned short sec_per_clus; /* 클러스터당 섹터 수 */
unsigned short cluster_bits; /* 클러스터 크기 비트 수 (log2) */
unsigned int cluster_size; /* 클러스터 크기 (바이트) */
unsigned int max_cluster; /* 최대 유효 클러스터 번호 + 1 */
unsigned long fat_length; /* FAT 하나의 섹터 수 */
unsigned long data_start; /* 데이터 영역 시작 섹터 */
unsigned long root_cluster; /* 루트 디렉토리 시작 클러스터 (FAT32) */
int free_clusters; /* 빈 클러스터 수 (-1=미확인) */
int prev_free; /* 마지막으로 할당된 클러스터 */
unsigned int fat_bits; /* FAT 엔트리 비트 수 (12, 16, 32) */
struct fat_mount_options options; /* 마운트 옵션 */
struct mutex fat_lock; /* FAT 테이블 접근 뮤텍스 */
/* ... */
};
/* FAT 유형 판별 — fat_fill_super()에서 호출 */
static int fat_calc_dir_size(struct super_block *sb)
{
struct msdos_sb_info *sbi = MSDOS_SB(sb);
unsigned long total_sectors, data_sectors, count_of_clusters;
total_sectors = (sbi->fat_bits == 32) ?
le32_to_cpu(bpb->BPB_TotSec32) :
le16_to_cpu(bpb->BPB_TotSec16);
data_sectors = total_sectors - sbi->data_start;
count_of_clusters = data_sectors / sbi->sec_per_clus;
/* Microsoft FATGEN103 규칙 */
if (count_of_clusters < 4085)
sbi->fat_bits = 12; /* FAT12 */
else if (count_of_clusters < 65525)
sbi->fat_bits = 16; /* FAT16 */
else
sbi->fat_bits = 32; /* FAT32 */
}
/* FAT 엔트리 접근 — fatent.c의 핵심 인터페이스 */
struct fat_entry {
int entry; /* 클러스터 번호 */
union {
__u8 *ent12_p[2]; /* FAT12: 2바이트에 걸친 12비트 */
__le16 *ent16_p; /* FAT16: 16비트 엔트리 포인터 */
__le32 *ent32_p; /* FAT32: 32비트 엔트리 포인터 */
} u;
struct buffer_head *bhs[2]; /* 버퍼 헤드 (FAT12는 2개 필요할 수 있음) */
};
/* FAT 엔트리 읽기/쓰기 함수 포인터 — fat_bits에 따라 자동 선택 */
struct fatent_operations {
void (*ent_blocknr)(struct super_block *, int, int *, int *);
int (*ent_get)(struct fat_entry *);
void (*ent_put)(struct fat_entry *, int);
int (*ent_next)(struct fat_entry *);
};
/* FAT12의 12비트 엔트리 접근이 가장 복잡함:
* - 엔트리가 바이트 경계에 걸침 (1.5바이트/엔트리)
* - 짝수 클러스터: 하위 12비트, 홀수 클러스터: 상위 12비트
* - 버퍼 헤드 경계도 걸칠 수 있어 bhs[2]가 필요 */
마운트 옵션: vfat vs msdos
리눅스에서 FAT 볼륨을 마운트할 때 두 가지 파일시스템 타입을 선택할 수 있습니다.
| 항목 | vfat | msdos |
|---|---|---|
| LFN (긴 파일명) | 지원 (최대 255자) | 미지원 (8.3 형식만) |
| 대소문자 | 보존 (case-insensitive) | 강제 대문자 |
| 유니코드 | 지원 (iocharset 옵션) | 미지원 |
| 커널 모듈 | namei_vfat.c | namei_msdos.c |
| 사용 환경 | 일반적 사용 (권장) | 레거시 호환 필요 시 |
# 주요 마운트 옵션
$ mount -t vfat /dev/sdb1 /mnt/usb \
-o uid=1000,gid=1000 # 파일 소유자/그룹 (FAT에는 유닉스 퍼미션 없음) \
-o umask=022,dmask=022 # 파일/디렉토리 퍼미션 마스크 \
-o fmask=133 # 파일 전용 퍼미션 마스크 \
-o iocharset=utf8 # 파일명 문자셋 (한글 등 표시) \
-o codepage=437 # 8.3 이름 코드페이지 (DOS 레거시) \
-o shortname=mixed # SFN 대소문자: lower/win95/winnt/mixed \
-o flush # 변경 시 즉시 플러시 (이동식 미디어용) \
-o errors=remount-ro # 에러 시 읽기 전용 재마운트 \
-o fat=32 # FAT 비트 수 강제 지정 (자동 감지 오버라이드) \
-o usefree # FSINFO의 빈 클러스터 수 신뢰 \
-o quiet # 퍼미션 관련 경고 억제 \
-o discard # TRIM/discard 명령어 활성화 (SSD용)
# ESP 마운트 예시 (UEFI 펌웨어 파티션)
$ mount -t vfat /dev/sda1 /boot/efi -o umask=077,shortname=winnt
# fstab 예시
UUID=ABCD-1234 /boot/efi vfat umask=077,shortname=winnt 0 2
/dev/sdb1 /mnt/usb vfat uid=1000,gid=1000,utf8 0 0
utf8 옵션은 iocharset=utf8의 단축형입니다. 한국어 파일명이 포함된 USB 드라이브를 사용할 때 반드시 지정하세요. codepage=949를 추가하면 레거시 한국어 DOS 이름도 올바르게 표시됩니다.클러스터 할당과 단편화
/* 빈 클러스터 탐색 알고리즘 (fs/fat/fatent.c) */
int fat_alloc_clusters(struct inode *inode, int *cluster,
int nr_cluster)
{
struct msdos_sb_info *sbi = MSDOS_SB(sb);
int search_from = sbi->prev_free + 1;
/* FSINFO 힌트(prev_free)부터 순환 탐색
* → 최근 해제된 영역 근처에서 먼저 찾아 단편화 감소 */
for (entry = search_from; ; entry++) {
if (entry >= sbi->max_cluster)
entry = 2; /* 끝까지 갔으면 처음부터 */
if (entry == search_from)
return -ENOSPC; /* 한 바퀴 돌았으면 공간 부족 */
if (fat_ent_read(sb, &fatent, entry) == FAT_ENT_FREE) {
/* 빈 클러스터 발견 → 할당 */
fat_ent_write(sb, &fatent, FAT_ENT_EOF);
cluster[count++] = entry;
sbi->prev_free = entry;
sbi->free_clusters--;
}
}
}
/* FAT의 단편화 문제:
* - 연결 리스트 기반이므로 파일 삭제/생성 반복 시 단편화 발생
* - 순차 읽기 시 클러스터마다 FAT 참조 필요 → I/O 패턴 악화
* - 리눅스 커널의 readahead가 어느 정도 완화하지만 근본적 한계
* - 해결: 충분히 큰 클러스터 사용 (32KB~), defrag 도구 사용 */
FAT 특수 주제
FAT와 UEFI ESP
UEFI 규격은 ESP(EFI System Partition)에 FAT32 사용을 의무화합니다(FAT12/16은 이동식 미디어에서만 허용). 이는 모든 UEFI 펌웨어가 FAT32 드라이버를 내장해야 함을 의미합니다.
# ESP 관련 커널/시스템 규격
- UEFI 규격: ESP는 GPT 파티션 타입 C12A7328-F81F-11D2-BA4B-00A0C93EC93B
- MBR 환경: 파티션 타입 0xEF
- 필수 파일시스템: FAT32 (≥ 200MB 권장)
- 부트 로더 경로: \EFI\BOOT\BOOTx64.EFI (기본 폴백)
# 커널에서 ESP 파티션 감지 (block/partitions/efi.c)
# GPT 파서가 타입 GUID를 읽고 PART_EFI_SYSTEM_PARTITION 플래그 설정
# systemd-gpt-auto-generator가 이를 감지하여 /boot/efi에 자동 마운트
exFAT 커널 지원
exFAT는 2019년 Microsoft가 사양을 공개한 후 리눅스 5.4부터 공식 커널 드라이버(fs/exfat/)가 포함되었습니다. Samsung이 기여한 이 드라이버는 FAT32의 4GB 파일 크기 제한을 제거하면서 FAT의 단순함을 유지합니다.
/* exFAT vs FAT32 주요 구조적 차이 */
/* 1. 클러스터 비트맵 (exFAT 고유) — FAT 테이블 외에 별도 비트맵 유지
* → 빈 클러스터 탐색이 O(1)에 가까움 (FAT32는 FAT 순차 스캔 필요) */
struct exfat_bitmap {
unsigned long *map; /* 클러스터당 1비트 */
unsigned int num_clusters;
};
/* 2. 디렉토리 엔트리가 스트림(set) 기반
* - File 엔트리 + Stream Extension + Name 엔트리 조합
* - UTC 타임스탬프 + 10ms 정밀도 */
/* 3. 연속 할당(contiguous) 플래그
* - NoFatChain 비트가 설정되면 FAT 체인 대신 연속 클러스터 가정
* - 순차 읽기 성능 대폭 향상 (FAT 참조 불필요) */
FAT 관련 Kconfig 옵션
CONFIG_FAT_FS=m # FAT 코어 모듈 (FAT12/16/32 공통)
CONFIG_MSDOS_FS=m # msdos 파일시스템 타입 (8.3 이름만)
CONFIG_VFAT_FS=m # vfat 파일시스템 타입 (LFN 지원, 일반적 사용)
CONFIG_FAT_DEFAULT_CODEPAGE=437 # 기본 코드페이지 (한국어: 949)
CONFIG_FAT_DEFAULT_IOCHARSET="iso8859-1" # 기본 문자셋 (한국어: utf8)
CONFIG_FAT_DEFAULT_UTF8=y # UTF-8 기본 활성화 (5.15+)
CONFIG_EXFAT_FS=m # exFAT 파일시스템 (5.4+)
CONFIG_EXFAT_DEFAULT_IOCHARSET="utf8"
FAT 관련 유틸리티
# FAT 볼륨 생성
$ mkfs.fat -F 32 -n MYVOLUME /dev/sdb1 # FAT32 포맷
$ mkfs.fat -F 16 /dev/sdb1 # FAT16 포맷
$ mkfs.fat -F 12 /dev/fd0 # FAT12 (플로피)
$ mkfs.exfat -n EXFAT /dev/sdb1 # exFAT 포맷
# FAT 파일시스템 검사/복구
$ fsck.fat -a /dev/sdb1 # 자동 복구
$ fsck.fat -v /dev/sdb1 # 상세 출력
$ fsck.fat -r /dev/sdb1 # 대화형 복구
# FAT 볼륨 정보 확인
$ fatattr /mnt/usb/file.txt # FAT 속성 확인
$ fatattr +r +h /mnt/usb/file.txt # 읽기전용+숨김 설정
$ fatlabel /dev/sdb1 # 볼륨 레이블 확인
$ fatlabel /dev/sdb1 NEWLABEL # 볼륨 레이블 변경
# FAT 디버깅
$ hexdump -C -n 512 /dev/sdb1 # 부트 섹터 덤프
$ hexdump -C -s 0x200 -n 512 /dev/sdb1 # FSINFO 섹터 (FAT32)
$ dosfsck -n -v /dev/sdb1 # 검사만 (수정 안함)
실용 참고
커널 커맨드라인 파티션 파라미터
# 커널 부트 파라미터로 파티션을 수동 지정할 수 있음 (임베디드 환경에서 유용)
blkdevparts=mmcblk0:1M(boot),16M(kernel),-(rootfs)
# mmcblk0에 3개 파티션: 1MB boot, 16MB kernel, 나머지 rootfs
cmdlinepart=mtd0:1M(bootloader),4M(kernel),-(rootfs)
# MTD 디바이스용 파티션 지정
자주 쓰는 명령어
# 파티션 테이블 확인
$ fdisk -l /dev/sda # MBR/GPT 파티션 목록
$ gdisk -l /dev/sda # GPT 상세 정보
$ parted /dev/sda print # 파티션 정보 출력
$ lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT # 블록 디바이스 트리
$ blkid # 파티션 UUID/타입
# 파티션 테이블 백업/복원
$ sfdisk --dump /dev/sda > sda.dump # MBR/GPT 파티션 테이블 백업
$ sfdisk /dev/sda < sda.dump # 복원
$ sgdisk --backup=sda-gpt.bak /dev/sda # GPT 전용 백업
# 커널 파티션 정보 확인
$ cat /proc/partitions # 커널이 인식한 파티션 목록
$ cat /sys/block/sda/sda1/start # 파티션 시작 섹터
$ cat /sys/block/sda/sda1/size # 파티션 크기 (섹터)
# Device Mapper 확인
$ dmsetup ls # DM 디바이스 목록
$ dmsetup table # DM 매핑 테이블
$ dmsetup info # DM 디바이스 상세 정보
$ lvs / vgs / pvs # LVM 정보
# 파티션 타입 변환
$ sgdisk -g /dev/sda # MBR → GPT 변환
$ sgdisk -m /dev/sda # GPT → MBR 변환 (2TB 이내)
관련 Kconfig 옵션
CONFIG_MSDOS_PARTITION=y # MBR 파티션 지원 (대부분 기본 활성)
CONFIG_EFI_PARTITION=y # GPT 파티션 지원
CONFIG_MAC_PARTITION=y # Apple Partition Map
CONFIG_SUN_PARTITION=y # Sun/Solaris VTOC
CONFIG_BLK_DEV_DM=m # Device Mapper 지원
CONFIG_DM_CRYPT=m # dm-crypt
CONFIG_DM_RAID=m # dm-raid
CONFIG_DM_VERITY=m # dm-verity (무결성 검증)
CONFIG_CMDLINE_PARTITION=y # 커널 커맨드라인 파티션 지정