ext4 파일시스템(Filesystem)
범용 Linux 서버에서 가장 널리 쓰이는 ext4를 대상으로 온디스크 블록 그룹 구조, inode/extent 매핑(Mapping), JBD2 저널 트랜잭션(Transaction) 상태 전이, mballoc과 delayed allocation의 공간 배치 전략, writeback/commit 지연(Latency)이 성능과 내구성에 주는 영향, fscrypt·fsverity 보안 기능, 그리고 e2fsprogs(mke2fs/e2fsck/debugfs/resize2fs/e2image) 실전 운용까지 상세히 다룹니다.
핵심 요약
- 계층 이해 — VFS, 캐시(Cache), 하위 FS 경계를 구분합니다.
- 메타데이터 우선 — inode/dentry 일관성을 먼저 확인합니다.
- 저장 정책 — 저널링/압축/할당 정책 차이를 비교합니다.
- 일관성 모델 — 로컬/원격/합성 FS의 반영 시점을 구분합니다.
- 복구 관점 — 장애 시 재구성 경로를 함께 점검합니다.
단계별 이해
- 경계 계층 파악
요청이 VFS에서 어디로 내려가는지 확인합니다. - 메타/데이터 분리
어느 경로에서 무엇이 갱신되는지 나눠 봅니다. - 동기화/플러시(Flush) 확인
쓰기 반영 시점과 순서를 검증합니다. - 복구 시나리오 점검
비정상 종료 후 일관성 회복을 확인합니다.
개요 & 역사
ext4(Fourth Extended Filesystem)는 Linux의 사실상 표준 파일시스템으로, ext2/ext3의 직접적인 후속입니다. 2006년 개발이 시작되어 Linux 2.6.28(2008)에서 안정 상태로 포함되었습니다.
ext 계열 진화
| 파일시스템 | 커널 버전 | 주요 변경 |
|---|---|---|
| ext2 (1993) | 0.99 | Block Group 구조, BSD FFS 기반 설계 |
| ext3 (2001) | 2.4.15 | JBD 저널링 추가 (journal / ordered / writeback) |
| ext4 (2008) | 2.6.28 | Extent, 48/64-bit, delalloc, mballoc, 체크섬(Checksum), fscrypt |
ext4 주요 스펙
| 항목 | 값 |
|---|---|
| 최대 볼륨 크기 | 1 EiB (4K 블록 기준) |
| 최대 파일 크기 | 16 TiB (4K 블록 기준) |
| 최대 파일 수 | 40억 (232) |
| 파일명 길이 | 255 바이트 |
| 블록 크기 | 1K / 2K / 4K (기본 4K) |
| 타임스탬프 범위 | 1901-12-14 ~ 2446-05-10 (나노초 확장) |
| 디렉토리 엔트리 | 무제한 (HTree 인덱싱) |
Feature Flags
ext4는 세 가지 유형의 feature flag로 기능을 관리합니다:
| 유형 | 설명 | 예시 |
|---|---|---|
| COMPAT | 미지원 커널도 읽기/쓰기 가능 | dir_index, resize_inode |
| INCOMPAT | 미지원 커널은 마운트(Mount) 불가 | extents, flex_bg, 64bit |
| RO_COMPAT | 미지원 커널은 읽기전용만 가능 | metadata_csum, bigalloc |
/* fs/ext4/ext4.h */
#define EXT4_FEATURE_COMPAT_DIR_INDEX 0x0020
#define EXT4_FEATURE_INCOMPAT_EXTENTS 0x0040
#define EXT4_FEATURE_INCOMPAT_64BIT 0x0080
#define EXT4_FEATURE_INCOMPAT_FLEX_BG 0x0200
#define EXT4_FEATURE_RO_COMPAT_METADATA_CSUM 0x0400
#define EXT4_FEATURE_RO_COMPAT_BIGALLOC 0x0200
디스크 레이아웃
ext4 볼륨은 고정 크기 Block Group으로 분할됩니다. 각 Block Group은 최대 8 × block_size 개의 블록을 포함합니다 (4K 블록 기준 32,768 블록 = 128 MiB).
ext4_super_block 주요 필드
/* fs/ext4/ext4.h */
struct ext4_super_block {
__le32 s_inodes_count; /* 전체 inode 수 */
__le32 s_blocks_count_lo; /* 전체 블록 수 (하위 32비트) */
__le32 s_r_blocks_count_lo; /* 예약 블록 수 */
__le32 s_free_blocks_count_lo; /* 미사용 블록 수 */
__le32 s_free_inodes_count; /* 미사용 inode 수 */
__le32 s_first_data_block; /* 첫 데이터 블록 (1K 블록이면 1, 4K면 0) */
__le32 s_log_block_size; /* 블록 크기 = 2^(10+값) */
__le32 s_blocks_per_group; /* 그룹당 블록 수 */
__le32 s_inodes_per_group; /* 그룹당 inode 수 */
__le16 s_magic; /* 매직 넘버: 0xEF53 */
__le16 s_inode_size; /* inode 크기 (기본 256) */
__le32 s_feature_compat; /* 호환 기능 플래그 */
__le32 s_feature_incompat; /* 비호환 기능 플래그 */
__le32 s_feature_ro_compat; /* 읽기전용 호환 기능 플래그 */
__le16 s_desc_size; /* 그룹 디스크립터 크기 (64bit: 64) */
__le32 s_blocks_count_hi; /* 전체 블록 수 (상위 32비트) */
__le32 s_checksum_seed; /* 체크섬 시드 (UUID 기반) */
...
};
ext4_group_desc
/* fs/ext4/ext4.h — 간략화 */
struct ext4_group_desc {
__le32 bg_block_bitmap_lo; /* 블록 비트맵 위치 */
__le32 bg_inode_bitmap_lo; /* inode 비트맵 위치 */
__le32 bg_inode_table_lo; /* inode 테이블 시작 위치 */
__le16 bg_free_blocks_count_lo;
__le16 bg_free_inodes_count_lo;
__le16 bg_used_dirs_count_lo;
__le16 bg_flags; /* INODE_UNINIT, BLOCK_UNINIT, INODE_ZEROED */
__le32 bg_checksum; /* CRC32C 체크섬 */
/* 64bit feature 활성 시 상위 32비트 필드들 추가 */
__le32 bg_block_bitmap_hi;
__le32 bg_inode_bitmap_hi;
__le32 bg_inode_table_hi;
...
};
코드 설명
- bg_block_bitmap_lo/hi해당 블록 그룹의 블록 비트맵(Bitmap)이 저장된 물리 블록 주소입니다.
64bitfeature 활성 시_lo와_hi를 합쳐 48비트 주소를 표현합니다. 비트맵의 각 비트는 해당 블록의 사용 여부를 나타냅니다. - bg_inode_bitmap_lo/hiinode 비트맵의 물리 블록 주소로, 그룹 내 각 inode의 할당 상태를 추적합니다.
bg_inode_table_lo/_hi는 inode 테이블의 시작 블록을 가리킵니다. - bg_flags
INODE_UNINIT과BLOCK_UNINIT플래그는 lazy initialization을 지원합니다. 아직 사용되지 않은 그룹은 비트맵을 디스크에서 읽지 않고 커널이 초기화된 것으로 간주하여 마운트 시간을 단축합니다.bg_checksum은 CRC32C로 디스크립터 자체의 무결성을 검증합니다.
Flex Block Groups
flex_bg feature는 여러 Block Group(기본 16개)의 메타데이터(비트맵 + inode 테이블)를 첫 번째 BG에 연속 배치합니다. 이를 통해:
- 메타데이터 접근 시 디스크 시크 횟수를 크게 감소
- 대용량 파일의 데이터 블록이 더 넓은 연속 영역에 분포
s_log_groups_per_flex필드로 flex 그룹 크기 설정 (2의 거듭제곱)
/* flex_bg 크기 확인 */
$ dumpe2fs /dev/sda1 | grep "Flex block group size"
Flex block group size: 16
Extent Tree
ext4는 ext2/ext3의 간접 블록(indirect block) 방식 대신 Extent Tree를 사용합니다. Extent는 연속된 물리 블록의 범위를 하나의 엔트리로 표현하여, 대용량 파일에서 메타데이터 오버헤드(Overhead)를 크게 줄입니다.
간접 블록 vs Extent 비교
| 항목 | 간접 블록 (ext2/ext3) | Extent (ext4) |
|---|---|---|
| 100 MiB 연속 파일 | 25,600 블록 포인터 + 간접 블록 | 1 extent 엔트리 (12 바이트) |
| 최대 파일 크기 | ~4 TiB (4K 블록) | 16 TiB (4K 블록) |
| 트리 깊이 | 최대 3 (triple indirect) | 최대 5 (실제로 0~2) |
| 단편화(Fragmentation) 영향 | 높음 (1:1 매핑) | 낮음 (범위 기반) |
Extent 자료구조
/* fs/ext4/ext4_extents.h */
struct ext4_extent_header {
__le16 eh_magic; /* 매직: 0xF30A */
__le16 eh_entries; /* 현재 엔트리 수 */
__le16 eh_max; /* 최대 엔트리 수 */
__le16 eh_depth; /* 트리 깊이 (0 = 리프) */
__le32 eh_generation;
};
/* 내부 노드 (depth > 0) */
struct ext4_extent_idx {
__le32 ei_block; /* 논리 블록 번호 */
__le32 ei_leaf_lo; /* 자식 노드 물리 블록 (하위) */
__le16 ei_leaf_hi; /* 자식 노드 물리 블록 (상위) */
__le16 ei_unused;
};
/* 리프 노드 (depth == 0) */
struct ext4_extent {
__le32 ee_block; /* 논리 블록 시작 */
__le16 ee_len; /* 연속 블록 수 (최대 32768, MSB=1이면 미초기화) */
__le16 ee_start_hi; /* 물리 블록 시작 (상위) */
__le32 ee_start_lo; /* 물리 블록 시작 (하위) */
};
코드 설명
- ext4_extent_header모든 extent 노드(루트, 내부, 리프)의 시작 부분에 위치하는 12바이트 헤더입니다.
eh_magic(0xF30A)으로 유효성을 검증하고,eh_depth가 0이면 리프 노드(직접 데이터 매핑), 0보다 크면 내부 노드(인덱스)임을 나타냅니다.eh_entries와eh_max로 현재/최대 엔트리 수를 관리합니다. - ext4_extent_idx내부 노드에서 사용하는 인덱스 엔트리입니다.
ei_block은 이 서브트리가 커버하는 논리 블록의 시작 번호이며,ei_leaf_lo/ei_leaf_hi를 합쳐 48비트 물리 블록 주소로 자식 노드를 가리킵니다. - ext4_extent리프 노드의 실제 매핑 엔트리로,
ee_block부터ee_len개의 연속 논리 블록이ee_start_hi:ee_start_lo48비트 물리 블록에 매핑됨을 표현합니다.ee_len의 MSB가 1이면 미초기화(unwritten) extent로, 블록은 할당되었지만 아직 데이터가 기록되지 않은 상태입니다. 이 구조는fs/ext4/ext4_extents.h에 정의되어 있습니다.
i_block[15] 영역(60 바이트)에 extent header + 최대 4개 extent를 직접 저장합니다. 이 영역이 부족하면 외부 블록에 extent tree를 확장합니다.
Extent Tree 구조
ext4_ext_map_blocks() 흐름
논리 블록 번호를 물리 블록으로 매핑하는 핵심 함수입니다:
/* fs/ext4/extents.c - 간략화 흐름 */
int ext4_ext_map_blocks(handle_t *handle, struct inode *inode,
struct ext4_map_blocks *map, int flags)
{
struct ext4_ext_path *path;
/* 1. extent tree 탐색: root → idx → leaf */
path = ext4_find_extent(inode, map->m_lblk, NULL, 0);
/* 2. 리프에서 논리 블록 포함 extent 검색 */
ex = path[depth].p_ext;
if (ex && in_range(map->m_lblk, ex)) {
/* 매핑 존재 → 물리 블록 반환 */
newblock = ex->ee_start + (map->m_lblk - ex->ee_block);
goto out;
}
/* 3. 매핑 없음 → 새 블록 할당 (CREATE 플래그 시) */
if (flags & EXT4_GET_BLOCKS_CREATE) {
newblock = ext4_mb_new_blocks(handle, ...);
ext4_ext_insert_extent(handle, inode, &path, &newex, flags);
}
out:
map->m_pblk = newblock;
return allocated;
}
코드 설명
- ext4_find_extent()extent tree의 루트(inode의
i_block[])에서 시작하여eh_depth만큼 내부 노드를 순회하며 대상 논리 블록map->m_lblk을 포함하는 리프 노드까지의 경로(ext4_ext_path배열)를 구성합니다. 각 레벨에서 이진 검색으로 적절한 인덱스를 찾습니다. - in_range() 검사리프 노드에서 찾은
ext4_extent가 요청한 논리 블록을 실제로 포함하는지 확인합니다. 포함하면ee_start에 오프셋(Offset)을 더해 물리 블록 주소를 바로 계산하여 반환합니다. - EXT4_GET_BLOCKS_CREATE매핑이 없고
CREATE플래그가 설정된 경우,ext4_mb_new_blocks()로 mballoc을 통해 새 물리 블록을 할당하고,ext4_ext_insert_extent()로 extent tree에 새 엔트리를 삽입합니다. 이 과정에서 tree 분할(split)이 발생할 수 있습니다. 이 코드는fs/ext4/extents.c에 위치합니다.
블록 할당
Multi-Block Allocator (mballoc)
ext4의 mballoc은 한 번의 요청으로 여러 블록을 동시에 할당하는 정교한 할당기입니다. ext3의 단일 블록 할당기에 비해 연속 할당률이 크게 향상됩니다.
할당 전략
| 전략 | 설명 |
|---|---|
| Per-inode Preallocation | 파일별 사전 할당 공간 유지 (순차 쓰기 최적화) |
| Per-group Preallocation | Block Group별 사전 할당 (소파일 최적화) |
| Buddy Allocator | 2의 거듭제곱 단위 블록 관리, 비트맵 기반 |
| 정규화(Normalization) | 요청 크기를 2의 거듭제곱으로 올림하여 단편화 방지 |
/* fs/ext4/mballoc.c - 할당 흐름 (간략화) */
ext4_fsblk_t ext4_mb_new_blocks(handle_t *handle,
struct ext4_allocation_request *ar, int *errp)
{
/* 1. 정규화: 요청 크기를 적절히 확대 */
ext4_mb_normalize_request(ac, ar);
/* 2. per-inode preallocation에서 검색 */
ext4_mb_use_inode_pa(ac);
/* 3. per-group preallocation에서 검색 */
ext4_mb_use_group_pa(ac);
/* 4. buddy allocator로 새 할당 */
ext4_mb_regular_allocator(ac);
/* 5. 결과에서 미사용 공간은 preallocation으로 저장 */
ext4_mb_put_pa(ac, ...);
return block;
}
지연 할당 (Delayed Allocation)
ext4의 기본 할당 전략인 delalloc은 write() 시점에 블록을 할당하지 않고, writepages 시점(페이지 캐시(Page Cache) → 디스크 플러시)에 실제 할당합니다.
write() 완료 후에도 디스크에 할당되지 않은 데이터가 유실될 수 있습니다. 이를 완화하기 위해 ext4는 ordered 모드와 함께 데이터를 저널 커밋 전에 디스크에 기록합니다.
bigalloc (클러스터 할당)
bigalloc feature는 블록 비트맵의 단위를 단일 블록에서 클러스터(여러 블록의 그룹)로 변경합니다. 대용량 파일이 주로 저장되는 환경에서 비트맵 메모리 사용량을 줄이고 할당 효율을 높입니다.
/* 클러스터 크기 = 블록 크기 × 2^s_log_cluster_size */
/* 예: 4K 블록 + cluster_size=4 → 64K 클러스터 */
$ mkfs.ext4 -O bigalloc -C 65536 /dev/sda1
저널링 (JBD2)
ext4의 저널링은 JBD2(Journaling Block Device 2) 서브시스템이 담당합니다. JBD2는 ext4와 독립적인 커널 모듈(Kernel Module)로, 메타데이터(및 선택적으로 데이터)의 원자적(Atomic) 갱신을 보장합니다.
JBD2 아키텍처
| 구조체(Struct) | 역할 |
|---|---|
journal_t | 저널 디바이스/영역을 대표. 트랜잭션 목록, 버퍼(Buffer) 관리 |
transaction_t | 하나의 트랜잭션. 수정된 버퍼 목록, 상태, 시퀀스 번호 |
handle_t | 개별 파일시스템 연산의 저널 핸들 (트랜잭션의 하위 단위) |
jbd2_transaction_committed() 함수가 최적화되어, 빠른 스토리지에서 비동기 Direct I/O 실행 시 IOPS 및 처리량(Throughput)이 최대 20% 향상되었습니다. 또한 jbd2_journal_blocks_per_page()가 대형 folio를 지원하도록 변환되어, large folio 환경에서 저널 크레딧 계산이 정확해졌습니다.
트랜잭션 라이프사이클
저널 모드
| 모드 | 저널 대상 | 성능 | 안전성 |
|---|---|---|---|
| journal | 메타데이터 + 데이터 | 가장 느림 (데이터 이중 기록) | 가장 높음 |
| ordered (기본) | 메타데이터만 | 중간 | 높음 (데이터 먼저 기록 보장) |
| writeback | 메타데이터만 | 가장 빠름 | 낮음 (데이터 순서 미보장) |
/* 마운트 시 저널 모드 설정 */
mount -o data=ordered /dev/sda1 /mnt # 기본값
mount -o data=journal /dev/sda1 /mnt # 최고 안전성
mount -o data=writeback /dev/sda1 /mnt # 최고 성능
Fast Commit
Linux 5.10에서 도입된 Fast Commit은 전체 블록을 저널에 복사하는 대신, 변경 사항만 기록하여 커밋 지연을 크게 줄입니다.
/* Fast Commit 활성화 */
tune2fs -O fast_commit /dev/sda1
/* Fast Commit 태그 유형 (fs/ext4/fast_commit.h) */
#define EXT4_FC_TAG_ADD_RANGE 0x0001 /* extent 추가 */
#define EXT4_FC_TAG_DEL_RANGE 0x0002 /* extent 삭제 */
#define EXT4_FC_TAG_CREAT 0x0003 /* 디렉토리 엔트리 생성 */
#define EXT4_FC_TAG_LINK 0x0004 /* 하드링크 생성 */
#define EXT4_FC_TAG_UNLINK 0x0005 /* 디렉토리 엔트리 삭제 */
#define EXT4_FC_TAG_INODE 0x0006 /* inode 변경 */
저널 복구 과정
비정상 종료 후 마운트 시 JBD2가 수행하는 복구 과정:
- SCAN: 저널 시퀀스 번호를 따라가며 유효한 커밋 블록 탐색
- REVOKE: revoke 레코드를 수집하여 이후 덮어쓴 블록을 복구 대상에서 제외
- REPLAY: 커밋된 블록을 원래 위치에 재기록
디렉토리 구현
선형 디렉토리
엔트리 수가 적은 디렉토리는 선형 리스트로 저장됩니다:
/* fs/ext4/ext4.h — 간략화 */
struct ext4_dir_entry_2 {
__le32 inode; /* inode 번호 */
__le16 rec_len; /* 엔트리 전체 길이 (4바이트 정렬) */
__u8 name_len; /* 파일명 길이 */
__u8 file_type; /* 파일 유형 (1=REG, 2=DIR, 7=SYMLINK ...) */
char name[]; /* 파일명 (null 종료 아님) */
};
Hash Tree (HTree) 인덱싱
dir_index feature가 활성화되면(기본), 디렉토리 엔트리 수가 한 블록을 초과할 때 자동으로 HTree(B-tree 변형)로 전환됩니다. 파일명의 해시(Hash)값을 키로 사용하여 O(1)에 가까운 검색 성능을 제공합니다.
/* fs/ext4/namei.c */
struct dx_root {
struct fake_dirent dot; /* "." 엔트리 */
struct fake_dirent dotdot; /* ".." 엔트리 */
struct dx_root_info {
__le32 reserved_zero;
__u8 hash_version; /* 해시 알고리즘 (half_md4, tea, sip) */
__u8 info_length;
__u8 indirect_levels; /* 트리 깊이 (0 또는 1) */
__u8 unused_flags;
} info;
struct dx_entry entries[]; /* {hash, block} 쌍의 배열 */
};
struct dx_entry {
__le32 hash; /* 파일명 해시값 */
__le32 block; /* 해당 리프 블록 번호 */
};
Inline Directory
inline_data feature가 활성화되면, 소규모 디렉토리의 엔트리를 inode 본체 내에 직접 저장하여 별도의 데이터 블록 할당을 피합니다.
확장 속성(Extended Attribute) (xattr) & ACL
xattr 저장 위치
ext4는 xattr을 두 곳에 저장합니다:
- inode body: inode 크기가 128보다 클 때 (기본 256), 여유 공간에 저장
- 외부 블록: inode body에 들어가지 않으면 별도 블록에 저장
/* fs/ext4/xattr.h — 간략화 */
struct ext4_xattr_entry {
__u8 e_name_len; /* 이름 길이 */
__u8 e_name_index; /* 네임스페이스 인덱스 */
__le16 e_value_offs; /* 값의 오프셋 */
__le32 e_value_inum; /* ea_inode 번호 (대용량) */
__le32 e_value_size; /* 값의 크기 */
__le32 e_hash; /* 해시 */
char e_name[]; /* 속성 이름 */
};
xattr 네임스페이스(Namespace)
| 네임스페이스 | 접두어 | 용도 |
|---|---|---|
| user | user. | 일반 사용자 확장 속성 |
| system | system. | POSIX ACL (system.posix_acl_access) |
| security | security. | SELinux 레이블 (security.selinux) |
| trusted | trusted. | CAP_SYS_ADMIN 필요 |
ea_inode (대용량 xattr)
ea_inode feature는 단일 블록(4K)을 초과하는 xattr 값을 별도의 inode에 저장합니다. 이를 통해 최대 수 MiB까지의 xattr 값을 지원합니다.
POSIX ACL
ext4는 POSIX ACL을 system.posix_acl_access와 system.posix_acl_default xattr로 구현합니다:
# ACL 설정 예
setfacl -m u:www-data:rx /var/www/html
getfacl /var/www/html
# file: var/www/html
# owner: root
# group: root
# user::rwx
# user:www-data:r-x
# group::r-x
# mask::r-x
# other::r-x
고급 기능
메타데이터 체크섬
metadata_csum feature는 superblock, group descriptor, inode, extent, 디렉토리 블록, 저널 등 모든 메타데이터에 CRC32C 체크섬을 추가합니다. UUID 기반의 시드(s_checksum_seed)를 사용하여 볼륨 간 블록 혼동을 방지합니다.
/* fs/ext4/super.c — 간략화 */
static __le32 ext4_superblock_csum(struct ext4_sb_info *sbi,
struct ext4_super_block *es)
{
__u32 csum;
csum = ext4_chksum(sbi, ~0, (char *)es,
offsetof(struct ext4_super_block, s_checksum));
return cpu_to_le32(csum);
}
파일시스템 암호화(Encryption) (fscrypt)
fscrypt는 파일 내용, 파일명, symlink 대상을 per-file 키로 암호화합니다. 커널 4.1에서 도입되어 ext4, F2FS, UBIFS에서 지원합니다.
| 항목 | 설명 |
|---|---|
| 암호화 대상 | 파일 내용, 파일명, symlink target |
| 암호화 알고리즘 | AES-256-XTS (내용), AES-256-CTS (파일명) |
| 키 파생 | HKDF-SHA512로 마스터 키에서 per-file 키 유도 |
| 키 관리 | Linux 키링(Keyring) 서브시스템 (fscrypt_add_key) |
# fscrypt 사용 예
fscrypt setup /mnt/encrypted
fscrypt encrypt /mnt/encrypted/private
# 잠금 해제
fscrypt unlock /mnt/encrypted/private
무결성 검증 (fsverity)
fsverity는 Merkle tree 기반 무결성 검증을 제공합니다. 파일을 읽기 전용(Read-Only)으로 설정하고, 각 블록의 해시를 트리 구조로 저장하여 변조를 감지합니다.
# fsverity 활성화
fsverity enable /path/to/file
# Merkle tree 해시는 EOF 뒤에 저장 (사용자에게 투명)
# 읽기 시 블록 단위로 해시 검증 → 불일치 시 -EIO 반환
# 서명된 fsverity (dm-verity와 유사한 신뢰 체인)
fsverity sign /path/to/file cert.pem --key key.pem
fsverity enable /path/to/file --signature=sig.bin
struct inode에서 각 파일시스템별 inode 구조체(ext4_inode_info 등)로 이동되었습니다. 이로 인해 struct inode가 16바이트 축소되어 inode 캐시 효율이 개선되었으며, struct super_operations에 오프셋을 저장하여 간접 호출 없이 빠르게 접근합니다.
대소문자 무관 디렉토리 (casefold)
casefold feature는 디렉토리 검색 시 유니코드 대소문자를 구분하지 않습니다. Wine, Samba 등 Windows 호환 환경에서 유용합니다.
/* casefold 활성화 */
mkfs.ext4 -O casefold /dev/sda1
chattr +F /mnt/shared /* 디렉토리별 활성화 */
Inline Data
inline_data feature는 소파일(약 60바이트 이하)의 데이터를 inode의 i_block[] 영역에 직접 저장합니다. 별도 데이터 블록 할당이 필요 없어 공간 효율과 성능이 향상됩니다.
Project Quotas
Project ID 기반 디스크 사용량 제한입니다. uid/gid 쿼터와 달리, 디렉토리 트리 단위로 쿼터를 적용할 수 있어 컨테이너(Container) 환경에서 유용합니다.
# Project Quota 설정
mkfs.ext4 -O project,quota /dev/sda1
mount -o prjquota /dev/sda1 /mnt
# 프로젝트 ID 할당
chattr +P -p 1000 /mnt/container1
# 쿼터 제한 설정 (1GB)
repquota -Ps /mnt
setquota -P 1000 0 1048576 0 0 /mnt
Large Folio 지원
커널 6.12에서 ext4의 데이터 경로 대부분이 folio 기반으로 변환되었습니다. mballoc 비트맵에 bd_bitmap_folio가 도입되고, 버퍼 읽기/쓰기, 라이트백(Writeback), 제로 레인지(Zero Range) 경로에서 대형 folio를 고려한 블록 오프셋 계산으로 전환되었습니다. 이 준비 작업을 거쳐 커널 6.16에서 일반 파일에 대한 large folio가 활성화되었으며, FS-Mark 벤치마크에서 37.7% 성능 향상이 확인되었습니다.
| 항목 | 기존 (4KB 페이지(Page)) | Large Folio (v6.16+) |
|---|---|---|
| 페이지 캐시 단위 | 4KB (base page) | 16KB~2MB (order-2 ~ order-9) |
| TLB 효율 | 페이지당 1 TLB 엔트리 | folio당 1 TLB 엔트리 (다수 페이지 커버) |
| I/O 병합 | readahead에 의존 | folio 단위 자연 병합 |
| 메타데이터 오버헤드 | 페이지당 struct page | folio당 1개 (나머지 tail page) |
data=journal 모드에서는 large folio가 비활성화됩니다. 이 조합에서는 기존 4KB 단일 페이지 경로가 계속 사용됩니다.
원자적 쓰기 (Atomic Writes, 커널 6.13+)
커널 6.13에서 ext4에 Direct I/O 기반 원자적 쓰기가 도입되었습니다. pwritev2() 시스템 콜(System Call)에 RWF_ATOMIC 플래그를 전달하면, 해당 쓰기가 전부 반영되거나 전혀 반영되지 않음(all-or-nothing)을 보장합니다. 데이터베이스의 이중 쓰기(double write) 오버헤드를 제거하여 쓰기 집약적 워크로드에서 상당한 성능 향상이 기대됩니다.
| 항목 | 설명 |
|---|---|
| 쓰기 인터페이스 | pwritev2(fd, iov, 1, offset, RWF_ATOMIC) |
| 능력 조회 | statx()에 STATX_WRITE_ATOMIC → stx_atomic_write_unit_min/max 반환 |
| 최소/최대 단위 | 파일시스템 블록 크기 (일반적으로 4KB) |
| I/O 모드 | O_DIRECT 전용 (버퍼드 I/O 미지원) |
| iovec 제한 | 단일 iovec만 지원 (stx_atomic_write_segments_max = 1) |
| 하드웨어 요건 | NVMe/SCSI 수준 원자적 쓰기 지원 필수 (소프트웨어 폴백 없음) |
/* 원자적 쓰기 능력 확인 */
struct statx stx;
statx(fd, "", AT_EMPTY_PATH, STATX_WRITE_ATOMIC, &stx);
if (stx.stx_attributes & STATX_ATTR_WRITE_ATOMIC) {
printf("min=%u max=%u segments=%u\n",
stx.stx_atomic_write_unit_min,
stx.stx_atomic_write_unit_max,
stx.stx_atomic_write_segments_max);
}
/* 원자적 쓰기 수행 (O_DIRECT 필수) */
struct iovec iov = { .iov_base = buf, .iov_len = blk_size };
pwritev2(fd, &iov, 1, offset, RWF_ATOMIC);
# 블록 장치의 원자적 쓰기 능력 확인
cat /sys/block/nvme0n1/queue/atomic_write_unit_min
cat /sys/block/nvme0n1/queue/atomic_write_unit_max
forcealign(파일별 할당 정렬 강제)과 extsize(할당 정렬 힌트) 기능이 추가되어, 재포맷 없이도 다중 블록 원자적 쓰기가 가능해졌습니다.
멀티그레인 타임스탬프 (커널 6.13+)
커널 6.13에서 ext4가 멀티그레인 타임스탬프(Multigrain Timestamps)를 지원하게 되었습니다. FS_MGTIME 플래그를 통해 옵트인하며, inode의 mtime/ctime 해상도를 동적으로 조절합니다.
| 항목 | 설명 |
|---|---|
| 동작 원리 | getattr()로 활발히 조회되는 inode는 고해상도(나노초) 타임스탬프, 그 외에는 저해상도(coarse) 사용 |
| 쿼리 표시 | ctime tv_nsec 31번째 비트를 플래그로 사용하여 조회 여부 기록 |
| 주요 용도 | NFS 캐시 검증 — 파일 변경 감지 정확도 개선 |
| 이력 | v6.6에서 최초 도입 → make/rsync 리그레션으로 제거 → v6.13에서 재도입 |
Orphan File (커널 5.15+)
Orphan inode란 열려 있는 상태에서 unlink()된 파일이나 truncate() 도중 크래시된 파일을 말합니다. 파일시스템은 이 inode를 추적하여, 다음 마운트 시 정리(삭제 완료 또는 크기 복원)해야 합니다. 커널 5.15에서 도입된 orphan_file feature는 기존 연결 리스트(Linked List) 방식의 병목(Bottleneck)을 해결합니다.
기존 방식: orphan linked list
슈퍼블록(Superblock)의 s_last_orphan 필드에서 시작해 각 inode의 i_dtime 필드를 다음 orphan inode 번호로 재활용(Recycling)하여 단일 연결 리스트를 구성합니다.
superblock.s_last_orphan → inode A → inode B → inode C → 0
(i_dtime (i_dtime (i_dtime
= ino B) = ino C) = 0)
/* fs/ext4/namei.c — 기존 orphan 추가 (간략화) */
int ext4_orphan_add(handle_t *handle, struct inode *inode)
{
struct super_block *sb = inode->i_sb;
struct ext4_sb_info *sbi = EXT4_SB(sb);
/* 슈퍼블록 잠금 → 직렬화 병목 */
lock_super(sb);
EXT4_I(inode)->i_dtime = sbi->s_es->s_last_orphan;
sbi->s_es->s_last_orphan = cpu_to_le32(inode->i_ino);
unlock_super(sb);
/* 슈퍼블록 + inode 모두 저널에 기록 필요 */
ext4_handle_dirty_metadata(handle, NULL, sbi->s_sbh);
ext4_mark_iloc_dirty(handle, inode, &iloc);
return 0;
}
rm -rf로 수만 개 파일을 삭제하거나 컨테이너 정리 시, 슈퍼블록 잠금 경합(Lock Contention)으로 심각한 병목이 발생합니다.
새 방식: orphan file
전용 inode(EXT4_ORPHAN_FILE_INO = 6)에 orphan 목록을 저장합니다. 이 파일은 여러 블록으로 구성되며, 각 블록이 독립적으로 orphan inode 번호를 기록합니다. CPU별로 서로 다른 블록을 사용하므로 잠금 경합이 제거됩니다.
/* fs/ext4/orphan.c — orphan file 구조 */
struct ext4_orphan_block_tail {
__le32 ob_magic; /* 0x0b10ca04 — 매직 넘버 */
__le32 ob_checksum; /* CRC32C 체크섬 */
};
/* 블록당 orphan 엔트리 수 */
/* (block_size - sizeof(ob_tail)) / sizeof(__le32) */
/* 4K 블록 기준: (4096 - 8) / 4 = 1022개 */
struct ext4_orphan_info {
int of_blocks; /* orphan file 전체 블록 수 */
__u32 of_csum_seed; /* 체크섬 시드 */
struct ext4_orphan_block *of_binfo; /* 블록별 상태 배열 */
};
struct ext4_orphan_block {
atomic_t ob_free_entries; /* 이 블록의 빈 슬롯 수 */
struct buffer_head *ob_bh; /* 이 블록의 버퍼 헤드 */
};
/* fs/ext4/orphan.c — orphan file 기반 추가 (간략화) */
static int ext4_orphan_file_add(handle_t *handle, struct inode *inode)
{
struct ext4_orphan_info *oi = &EXT4_SB(sb)->s_orphan_info;
int start;
/* CPU 번호를 기반으로 시작 블록 선택 → 잠금 경합 제거 */
start = smp_processor_id() * oi->of_blocks / num_online_cpus();
/* 빈 슬롯이 있는 블록 탐색 */
for (i = 0; i < oi->of_blocks; i++) {
int idx = (start + i) % oi->of_blocks;
if (atomic_read(&oi->of_binfo[idx].ob_free_entries) == 0)
continue;
/* 해당 블록만 저널에 기록 — 슈퍼블록 갱신 불필요 */
ext4_journal_get_write_access(handle, sb, bh, EXT4_JTR_ORPHAN_FILE);
/* 빈 슬롯에 inode 번호 기록 */
bdata[j] = cpu_to_le32(inode->i_ino);
atomic_dec(&oi->of_binfo[idx].ob_free_entries);
return 0;
}
/* 모든 블록이 가득 차면 기존 연결 리스트 방식으로 폴백 */
return ext4_orphan_file_add_fallback(handle, inode);
}
| 항목 | orphan linked list (기존) | orphan file (v5.15+) |
|---|---|---|
| 저장 위치 | 슈퍼블록 s_last_orphan + inode i_dtime 체인 | 전용 inode(ino 6)의 데이터 블록 |
| 동시성 | 슈퍼블록 잠금(Lock) → 전역 직렬화 | 블록별 독립 → CPU별 병렬 |
| 저널 비용 | 매번 슈퍼블록 + inode 기록 | 해당 블록 1개만 기록 |
| 용량 | 무제한 (리스트) | 블록당 1,022개 (4K 기준), 가득 차면 리스트로 폴백 |
| 체크섬 | 없음 | 블록별 CRC32C |
| feature flag | 기본 | COMPAT_ORPHAN_FILE |
| e2fsprogs | 항상 지원 | 1.47.1+ 기본 활성화 |
# orphan_file 활성화 (기존 볼륨에 적용)
tune2fs -O orphan_file /dev/sda1
# orphan_file 비활성화
tune2fs -O ^orphan_file /dev/sda1
# 현재 feature 확인
tune2fs -l /dev/sda1 | grep -i orphan
# Filesystem features: ... orphan_file ...
# 새 볼륨 생성 시 (e2fsprogs 1.47.1+ 기본 활성)
mkfs.ext4 /dev/sda1
# orphan_file + metadata_csum_seed 기본 포함
ext4_orphan_cleanup()이 호출되어, orphan file의 각 블록을 순회하며 남아 있는 orphan inode를 정리합니다. unlink된 orphan은 삭제를 완료하고, truncate orphan은 원래 크기로 복원합니다. 이 과정은 저널 복구(SCAN → REVOKE → REPLAY) 이후에 수행됩니다.
핵심 커널 자료구조
ext4_sb_info
슈퍼블록의 메모리 내 표현으로, VFS struct super_block의 s_fs_info에 저장됩니다:
/* fs/ext4/ext4.h */
struct ext4_sb_info {
struct ext4_super_block *s_es; /* 디스크 superblock 포인터 */
struct buffer_head *s_sbh; /* superblock 버퍼 헤드 */
struct ext4_group_desc **s_group_desc; /* 그룹 디스크립터 배열 */
struct journal_s *s_journal; /* JBD2 저널 */
struct ext4_group_info ***s_group_info; /* 그룹별 mballoc 정보 */
unsigned long s_desc_per_block; /* 블록당 그룹 디스크립터 수 */
ext4_group_t s_groups_count; /* 전체 블록 그룹 수 */
unsigned long s_overhead; /* 메타데이터 블록 수 */
unsigned int s_cluster_ratio; /* bigalloc 클러스터 비율 */
unsigned int s_inode_size; /* inode 크기 */
...
};
ext4_inode_info
VFS struct inode를 확장하는 ext4 전용 inode 구조체입니다:
/* fs/ext4/ext4.h — 간략화 */
struct ext4_inode_info {
__le32 i_data[15]; /* extent tree 또는 간접 블록 */
__u32 i_flags; /* ext4 inode 플래그 */
ext4_lblk_t i_dir_start_lookup; /* HTree 검색 힌트 */
/* extent 상태 트리 (메모리 내) */
struct ext4_es_tree i_es_tree;
/* delalloc 예약 블록 */
unsigned int i_reserved_data_blocks;
/* 저널링 */
tid_t i_sync_tid; /* 마지막 동기화 트랜잭션 ID */
tid_t i_datasync_tid; /* 마지막 데이터 동기화 트랜잭션 ID */
struct inode vfs_inode; /* 내장된 VFS inode */
...
};
코드 설명
- i_data[15]디스크 상의
ext4_inode의i_block[15]영역(60바이트)을 메모리에 복사한 것입니다. extent 모드에서는 extent tree의 루트(헤더 + 최대 4개 extent)가 저장되고, 레거시 모드에서는 직접/간접 블록 포인터가 저장됩니다. - i_es_treeextent 상태 트리(extent status tree)는 메모리 내 캐시로, 디스크의 extent tree와 지연 할당 예약 정보를 통합 관리합니다. 논리 블록의 매핑 상태(기록됨, 미기록, 지연할당, 구멍)를 빠르게 조회할 수 있어 불필요한 디스크 읽기를 방지합니다.
- i_reserved_data_blocks지연 할당(delalloc) 시
write()호출마다 실제 블록을 할당하지 않고 예약 카운터만 증가시킵니다. 나중에writepages()에서 mballoc으로 실제 할당 시 이 카운터를 차감합니다. - vfs_inodeVFS
struct inode가 구조체 내부에 내장(embedded)되어 있어,container_of()매크로(Macro)(EXT4_I())로 VFS inode 포인터에서ext4_inode_info를 역참조(Dereference)할 수 있습니다. 이 패턴은 커널 전반에서 사용되는 C 객체 상속 기법입니다.
헬퍼 매크로
/* fs/ext4/ext4.h — 간략화 */
static inline struct ext4_inode_info *EXT4_I(struct inode *inode)
{
return container_of(inode, struct ext4_inode_info, vfs_inode);
}
/* VFS super_block에서 ext4_sb_info 획득 */
static inline struct ext4_sb_info *EXT4_SB(struct super_block *sb)
{
return sb->s_fs_info;
}
성능 튜닝
주요 Mount 옵션
| 옵션 | 설명 | 권장 시나리오 |
|---|---|---|
noatime | 접근 시간 갱신 중지 | 대부분의 워크로드 (메타데이터 쓰기 감소) |
delalloc | 지연 할당 (기본값) | 순차 쓰기가 많은 워크로드 |
discard | TRIM 명령 자동 전송 | SSD (또는 fstrim 크론잡 대체) |
barrier=1 | 쓰기 배리어 활성화 (기본값) | 데이터 무결성 중요 시 |
commit=N | 저널 커밋 간격 (초) | 기본 5초, 배치 성능 시 30~60 |
journal_async_commit | 비동기 커밋 블록 | SSD + 배리어 활성 시 |
max_batch_time=N | 트랜잭션 배치 최대 대기 (us) | 동시 커밋이 많을 때 |
dioread_nolock | Direct I/O 시 inode 잠금 회피 | DB 워크로드 (기본 활성) |
data=ordered | 데이터 → 메타 순서 보장(Ordering) | 기본값, 안전 + 적절한 성능 |
prefetch_block_bitmaps | 마운트 시 블록 비트맵 미리 읽기 (v6.13+ 기본 활성) | HDD 대용량 볼륨 (초기 할당 지연 감소) |
# 일반적인 성능 최적화 마운트
mount -o noatime,delalloc,barrier=1,commit=5 /dev/sda1 /mnt
# SSD 최적화
mount -o noatime,discard,journal_async_commit /dev/nvme0n1p1 /mnt
# DB 워크로드
mount -o noatime,data=writeback,barrier=1,dioread_nolock /dev/sda1 /var/lib/mysql
mkfs 최적화 옵션
| 옵션 | 설명 |
|---|---|
-T usage-type | 워크로드 프로필: largefile, largefile4, small, news |
-i bytes-per-inode | inode 비율 조정 (작은 파일 많으면 줄임) |
-I inode-size | inode 크기 (기본 256, xattr 많으면 512) |
-J size=N | 저널 크기 (MB) |
-E stride=N,stripe-width=M | RAID 정렬 (stride=청크/블록, stripe-width=stride×디스크수) |
# 대용량 파일 서버 (적은 inode, 큰 저널)
mkfs.ext4 -T largefile -J size=1024 /dev/sda1
# RAID-5 (4디스크, 64K 청크, 4K 블록)
mkfs.ext4 -E stride=16,stripe-width=48 /dev/md0
# 소파일이 매우 많은 메일 서버
mkfs.ext4 -T small -i 4096 /dev/sda1
I/O 스케줄러(Scheduler) 선택
| 스케줄러 | 권장 디바이스 | 특징 |
|---|---|---|
none | NVMe SSD | 스케줄링 오버헤드 제거, 디바이스 내부 큐에 위임 |
mq-deadline | SATA SSD, HDD | 지연 시간 보장, 기아(Starvation) 방지 |
bfq | 데스크탑, HDD | 공정성(Fairness), 저지연 대화형 사용 |
kyber | 고성능 NVMe | 경량, 지연 목표 기반 |
# 현재 스케줄러 확인
cat /sys/block/sda/queue/scheduler
# 스케줄러 변경
echo mq-deadline > /sys/block/sda/queue/scheduler
모니터링
# ext4 통계
cat /proc/fs/ext4/sda1/mb_groups # mballoc 그룹 정보
cat /proc/fs/ext4/sda1/mb_stats # mballoc 통계
cat /sys/fs/ext4/sda1/delayed_allocation_blocks # delalloc 대기 블록
cat /sys/fs/ext4/sda1/lifetime_write_kbytes # 총 기록량
cat /sys/fs/ext4/sda1/session_write_kbytes # 세션 기록량
e2fsprogs 도구
e2fsprogs는 ext2/ext3/ext4 파일시스템을 생성, 점검, 조정, 분석, 복구하는 사용자 공간(User Space) 도구 모음입니다. 커널 ext4 드라이버가 런타임 I/O 경로를 담당한다면, e2fsprogs는 파일시스템 수명주기 전체를 다룹니다. 즉 새 볼륨 포맷은 mkfs.ext4/mke2fs, 운영 중 설정 변경은 tune2fs, 장애 시 검사는 e2fsck, 저수준 분석은 debugfs, 크기 조정은 resize2fs, 메타데이터 이미지 백업은 e2image가 맡습니다.
| 도구 | 주 용도 | 실무 포인트 | 대표 예시 |
|---|---|---|---|
mkfs.ext4 / mke2fs | 새 ext4 생성 | 블록 크기, feature flag, RAID 정렬, 루트 디렉토리 초기 내용까지 결정합니다. | mkfs.ext4 -L data -m 1 /dev/sdb1 |
tune2fs / e2label | 슈퍼블록 파라미터 조정 | 예약 블록, 오류 정책, label/UUID, feature 활성화, MMP 정리 등에 사용합니다. | tune2fs -m 1 -e remount-ro /dev/sdb1 |
e2fsck / fsck.ext4 | 검사와 복구 | 읽기 전용 진단, 자동 복구, 백업 슈퍼블록 복구, 저널만 재생 같은 모드가 있습니다. | e2fsck -fn /dev/sdb1 |
dumpe2fs / e2freefrag | 구조와 상태 관찰 | 슈퍼블록, 그룹 디스크립터, free space 단편화 상태를 빠르게 파악합니다. | dumpe2fs -h /dev/sdb1 |
debugfs | 저수준 분석과 제한적 수정 | inode, extent, directory hash, 저널, orphan 목록, 블록-경로 매핑을 직접 확인합니다. | debugfs -R "stat <12>" /dev/sdb1 |
resize2fs / e2undo | 확장·축소·되돌리기 | 온라인 확장은 가능하지만 축소는 오프라인만 지원합니다. -z로 undo 로그를 남길 수 있습니다. | resize2fs /dev/vgdata/lvhome |
e2image | 메타데이터 이미지 저장 | 손상 분석 전에 메타데이터 또는 사용 중 블록만 sparse/raw/QCOW2 형식으로 확보합니다. | e2image -r -p /dev/sdb1 /mnt/backup/sdb1.e2i.raw |
e4defrag, filefrag, badblocks, chattr, lsattr, logsave | 보조 진단과 운영 | 단편화 분석, 배드블록 점검, ext4 속성 플래그, 로그 보존을 담당합니다. | filefrag -v /srv/data/bigfile |
e2fsck, debugfs -w, resize2fs 축소, tune2fs -O ... 같은 메타데이터 변경 작업은 가능하면 언마운트된 장치 또는 스냅샷(Clone)에서 수행하세요. 마운트된 ext4에서 안전하다고 문서화된 작업은 온라인 확장, label/UUID 변경 일부, 읽기 전용 관찰 정도입니다.
작업 전 안전 수칙
- 항상 대상 장치를 식별합니다. 다중 경로(Multipath), LVM, MD RAID, 루프 파일, 클라우드 볼륨이 겹치면 실수 확률이 급격히 올라갑니다.
- 복구 작업 전 이미지를 먼저 확보합니다. 가장 안전한 순서는 스냅샷 생성 ->
e2image또는 블록 복제 -> 읽기 전용 분석입니다. -y와-w는 마지막 단계로 미룹니다. 자동 수정은 편하지만, 잘못된 가정 위에서 실행하면 원인 분석 정보를 잃습니다.- feature flag 변경 후에는
e2fsck -f를 수행합니다.tune2fs -O ...만 실행하고 검사를 생략하면 다음 마운트에서 문제가 터질 수 있습니다. - 축소는 파일시스템 먼저, 그 다음 파티션/논리 볼륨 순서로 수행합니다. 순서를 바꾸면 끝 블록이 잘려 데이터가 손상됩니다.
badblocks는 가능하면mkfs.ext4 -c또는e2fsck -c를 통해 간접 호출하세요. 블록 크기 불일치로 잘못된 결과를 줄일 수 있습니다.
# 1) 실제 장치/마운트 상태를 먼저 확인합니다.
lsblk -o NAME,SIZE,FSTYPE,LABEL,UUID,MOUNTPOINTS
blkid /dev/sdb1
findmnt /dev/sdb1
# 2) 시그니처만 읽어서 기존 파일시스템 흔적을 확인합니다.
wipefs -n /dev/sdb1
# 3) 운영 중 볼륨이면 즉시 수정하지 말고 먼저 메타데이터를 확보합니다.
e2image -r -p /dev/sdb1 /mnt/backup/sdb1.e2i.raw
최신 e2fsprogs 변경 (1.47.x)
| 버전 | 날짜 | 주요 변경 |
|---|---|---|
| 1.47.1 | 2024-05-20 | 64비트 time_t 환경의 2038년 이후 타임스탬프 처리, mke2fs -d tar 입력 지원(libarchive 사용 시), debugfs hash 개선, root_perms 추가 |
| 1.47.2 | 2025-01-01 | tune2fs -r 제거 후 -E revision=로 대체, debugfs orphan 목록 명령 추가, resize2fs orphan file 체크섬 보정 |
| 1.47.3 | 2025-07-08 | mke2fs -d가 fs-verity 메타데이터와 chattr 플래그를 복사, fuse2fs의 FALLOC_FL_ZERO_RANGE·나노초 타임스탬프 지원 강화 |
| 1.47.4 | 2026-03-06 | mke2fs -E root_selinux=... 계열 지원, 복수 -E 사용 가능, 대형 블록 크기 경고 정리, e4defrag 인라인 데이터 크래시 수정 |
e2fsprogs 운용 절차
e2fsprogs를 실무에서 사용할 때는 생성 → 검증 → 운영 중 관찰 → 장애 복구 → 용량 조정 순서로 사고하는 편이 안전합니다. 아래 절차는 서버 운영자가 실제로 가장 많이 반복하는 흐름입니다.
새 ext4 볼륨 생성 절차
- 장치가 비어 있는지, 파티션 경계와 RAID/LVM 상위 계층이 맞는지 확인합니다.
- 워크로드에 맞는 block size, inode 비율, 예약 블록, feature flag를 결정합니다.
mkfs.ext4로 생성한 직후tune2fs -l와dumpe2fs -h로 결과를 검증합니다.- 마운트 후
mount옵션과 성능 카운터를 다시 확인합니다.
# RAID 6 위 데이터 볼륨 예시
wipefs -n /dev/md0
mkfs.ext4 -L data -m 1 -T largefile4 \
-E stride=128,stripe_width=768,lazy_itable_init=1,lazy_journal_init=1 \
/dev/md0
# 생성 결과 검증
tune2fs -l /dev/md0 | grep -E "Filesystem volume name|Filesystem features|Block size|Reserved block count"
dumpe2fs -h /dev/md0
# 마운트 후 확인
mount /dev/md0 /srv/data
findmnt /srv/data
mke2fs -d는 디렉토리 트리 또는 tar 아카이브를 받아 이미지 파일에 초기 내용을 채울 수 있습니다. 임베디드 루트 파일시스템, initramfs 대체 이미지, 가상머신용 기본 디스크 제작에 매우 유용합니다.
정기 점검과 장애 직후 점검
점검은 항상 읽기 전용 확인부터 시작하세요. 커널이 이미 저널을 재생한 뒤라면 단순 재부팅 후에는 흔적이 사라질 수 있으므로, 장애 직후 가능하면 즉시 dmesg, journalctl, e2image, e2fsck -fn 조합으로 상태를 고정하는 편이 좋습니다.
# 읽기 전용 진단
e2fsck -fn /dev/mapper/vgdata-lvhome
# 저널만 재생할 필요가 있는지 확인
e2fsck -E journal_only /dev/mapper/vgdata-lvhome
# 실제 복구는 로그를 남기면서 수행
logsave /var/tmp/e2fsck-home.log e2fsck -f -C 0 /dev/mapper/vgdata-lvhome
# 자동 yes는 마지막 수단입니다.
logsave /var/tmp/e2fsck-home-auto.log e2fsck -f -y /dev/mapper/vgdata-lvhome
| 상황 | 권장 접근 | 피해야 할 실수 |
|---|---|---|
| 부팅 직후 저널 replay 흔적만 의심됨 | e2fsck -E journal_only 또는 읽기 전용 확인 후 정상 마운트 | 증거 수집 없이 바로 -y 실행 |
| 메타데이터 손상 의심 | 언마운트 후 e2fsck -f와 로그 보존 | 마운트 상태에서 일반 e2fsck 실행 |
| 배드블록 의심 | e2fsck -c 또는 오프라인 badblocks 후 결과 반영 | 블록 크기를 모른 채 badblocks 결과를 재사용 |
| 복구 가능성 분석 단계 | e2image로 이미지 확보 후 debugfs 분석 | 원본 장치에서 바로 debugfs -w 사용 |
확장과 축소 절차
온라인 확장은 비교적 단순하지만, 축소는 반드시 오프라인으로 진행해야 하며 순서가 매우 중요합니다. 확장 시에는 아래 계층을 먼저 키우고 파일시스템을 나중에 확장합니다. 반대로 축소 시에는 파일시스템을 먼저 줄이고 그 뒤에 파티션 또는 LV를 줄입니다.
# LVM 위 ext4 온라인 확장
lvextend -L +200G /dev/vgdata/lvhome
resize2fs /dev/vgdata/lvhome
# 축소 전 최소 크기 추정
umount /home
e2fsck -f /dev/vgdata/lvhome
resize2fs -P /dev/vgdata/lvhome
# 목표 크기로 파일시스템을 먼저 축소한 뒤 LV를 줄입니다.
resize2fs /dev/vgdata/lvhome 800G
lvreduce -L 800G /dev/vgdata/lvhome
e2fsck -f /dev/vgdata/lvhome
mount /home
resize2fs -M은 최소 크기로 줄이는 데 유용하지만, 아주 오래된 e2fsprogs나 복잡한 extent 트리, 손상된 메타데이터가 있는 파일시스템에서는 실패 위험이 있습니다. 중요한 데이터가 있으면 먼저 스냅샷 또는 전체 이미지 백업을 확보하세요.
포렌식과 복구 절차
운영 중 손상된 볼륨을 직접 만지기보다, 먼저 이미지를 떠서 복제본에서 구조를 읽는 것이 좋습니다. e2image는 전체 데이터가 아니라 메타데이터 위주로 저장할 수 있어 대형 볼륨도 빠르게 확보할 수 있습니다.
# 메타데이터 중심 raw sparse 이미지 생성
e2image -r -p /dev/sdb1 /mnt/backup/sdb1.e2i.raw
# QCOW2 형식으로 저장할 수도 있습니다.
e2image -Q /dev/sdb1 /mnt/backup/sdb1.e2i.qcow2
# 이미지 파일에서 바로 구조 확인
dumpe2fs -h /mnt/backup/sdb1.e2i.raw
debugfs -R "stats" /mnt/backup/sdb1.e2i.raw
debugfs -R "logdump -a" /mnt/backup/sdb1.e2i.raw
e2fsprogs 명령별 상세
mkfs.ext4 / mke2fs
mkfs.ext4는 사실상 mke2fs -t ext4 래퍼(Wrapper)입니다. ext4의 기본 블록 크기, inode 밀도, feature flag, 저널 크기, RAID 정렬, 초기 디렉토리 트리까지 한 번에 결정하므로 가장 영향력이 큰 명령입니다.
| 옵션 | 의미 | 실무 해설 |
|---|---|---|
-L label | 볼륨 레이블 지정 | 운영 중 사람이 식별하기 쉬운 이름을 붙입니다. |
-m pct | 예약 블록 비율 | 대형 데이터 볼륨은 0~1%로 낮추고, 루트 볼륨은 기본값을 유지하는 경우가 많습니다. |
-T usage-type | 용도별 프로파일 | largefile, largefile4, small 등이 inode 비율과 기본값에 영향을 줍니다. |
-O feature,... | feature flag 지정 | metadata_csum, 64bit, casefold, project 등을 명시적으로 켤 수 있습니다. |
-E stride=, stripe_width= | RAID 정렬 정보 | RAID 5/6에서 read-modify-write를 줄이는 데 중요합니다. |
-E lazy_itable_init=1, lazy_journal_init=1 | 초기화 지연 | mkfs 시간을 크게 줄이지만 첫 마운트 후 백그라운드 초기화가 진행됩니다. |
-E discard | 생성 시 discard 수행 | SSD, thin provisioning, 클라우드 블록 장치(Block Device)에서 초기화 속도를 줄이는 데 유리합니다. |
-E packed_meta_blocks=1 | 메타데이터를 앞부분에 집중 | 일부 flash 장치나 Shingled Drive 계열에서 유리할 수 있습니다. |
-E root_owner=uid:gid | 루트 디렉토리 소유자 지정 | 루트 파일시스템 이미지 제작 시 재현성을 높입니다. |
-d rootdir|tar | 초기 파일 트리 주입 | 1.47.1 이상에서는 libarchive가 있으면 tar 입력도 지원합니다. |
-F | 강제 생성 | 루프 파일이나 시그니처가 있는 장치에 쓸 때 사용하지만, 잘못 쓰면 다른 파일시스템을 덮어쓸 수 있습니다. |
# 일반 데이터 볼륨
mkfs.ext4 -L archive -m 1 /dev/sdb1
# 루트 파일시스템 이미지 파일 생성
truncate -s 8G rootfs.img
mkfs.ext4 -F -L rootfs -m 0 \
-E root_owner=0:0,discard \
-d ./rootfs \
rootfs.img
# casefold + strict encoding
mkfs.ext4 -L shared \
-O casefold,metadata_csum,64bit \
-E encoding=utf8-12.1,encoding_flags=strict \
/dev/sdc1
mkfs.ext4 -n의 의미: 이 옵션은 실제로 포맷하지 않고 “이렇게 만들었다면 어느 블록에 슈퍼블록 백업이 있었을지”를 계산해 보여 줍니다. 손상된 ext4에서 백업 슈퍼블록 위치를 찾을 때 매우 자주 사용합니다.
tune2fs / e2label
tune2fs는 ext4 슈퍼블록의 운영 파라미터를 바꾸는 핵심 도구입니다. 생성 직후 한 번 쓰고 끝나는 것이 아니라, 서비스 수명 내내 반복적으로 사용합니다. 반면 e2label은 label 조회/변경에 특화된 얇은 명령입니다.
| 옵션 | 용도 | 주의점 |
|---|---|---|
-l | 현재 슈퍼블록 정보 출력 | 실제 운영 상태를 빠르게 확인하는 첫 단계입니다. |
-m pct, -r count | 예약 블록 비율/개수 조정 | 루트 볼륨이 아닌 대용량 데이터 볼륨에서는 크게 낮추는 경우가 많습니다. |
-e remount-ro|continue|panic | 에러 감지 시 동작 | 프로덕션 서버는 보통 remount-ro가 안전합니다. |
-c, -i | 정기 fsck 기준 | 현대 배포판은 대개 시간/횟수 기반 검사 빈도를 낮게 둡니다. |
-L, -U | label/UUID 변경 | 자동 마운트와 /etc/fstab 의존성을 함께 검토해야 합니다. |
-O feature,... | feature 활성화/비활성화 | 대부분 언마운트 상태와 후속 e2fsck -f가 필요합니다. |
-E clear_mmp | stale MMP 정리 | 정말 다른 노드가 마운트하지 않았는지 확인한 뒤에만 사용해야 합니다. |
-E orphan_file_size= | orphan file 크기 조정 | 병렬 삭제가 많은 환경에서 확장성을 조정할 때 씁니다. |
-Q usrquota,grpquota,prjquota | 내부 quota inode 설정 | quota 기능을 ext4 내부 inode 방식으로 관리할 때 유용합니다. |
# 현재 ext4 feature와 검사 정책 확인
tune2fs -l /dev/sdb1 | grep -E "Filesystem features|Mount count|Check interval|Reserved block count"
# 예약 블록 비율과 에러 정책 조정
tune2fs -m 1 -e remount-ro /dev/sdb1
# label 변경
e2label /dev/sdb1 archive-2026
# Fast Commit 활성화 후 전체 검사
tune2fs -O fast_commit /dev/sdb1
e2fsck -f /dev/sdb1
# mount count 기반 강제 검사를 사실상 비활성화
tune2fs -c 0 -i 0 /dev/sdb1
tune2fs -r는 오래된 revision 필드 조정 용도에서 제거되고 -E revision=로 대체되었습니다. 최신 문서를 볼 때 오래된 블로그 예제가 그대로 통하지 않을 수 있습니다.
e2fsck / fsck.ext4
e2fsck는 ext4 복구의 중심입니다. 단순한 “오류 고치기” 명령이 아니라, 저널 재생, 5단계 일관성 검사, extent/디렉토리 최적화, quota 재계산, orphan 정리까지 포함하는 강력한 도구입니다. fsck.ext4는 같은 프로그램의 이름 기반 래퍼입니다.
| 옵션 | 용도 | 실무 포인트 |
|---|---|---|
-n | 읽기 전용 확인 | 마운트된 파일시스템에서 허용되는 사실상 유일한 안전한 진단 모드입니다. |
-f | 강제 검사 | clean 상태로 표시되어도 전체 패스를 실행합니다. |
-p | 자동 복구(preen) | 부팅 스크립트에서 자주 사용되며, 사람이 답할 필요 없는 안전한 수정만 수행합니다. |
-y | 모든 질문에 yes | 원인 분석보다 서비스 복구가 더 급한 경우에만 제한적으로 사용하세요. |
-b sb, -B blocksize | 백업 슈퍼블록 사용 | 주 슈퍼블록이 손상됐을 때 핵심입니다. |
-E journal_only | 저널만 재생 | 추가 검사 없이 replay만 수행합니다. |
-E fragcheck | 조각난 파일 보고 | 대규모 조각화 상태를 진단할 때 유용합니다. |
-E discard | 검사 후 free block discard | SSD에는 유리할 수 있지만, 수동 복구 여지를 줄일 수 있습니다. |
-E fixes_only | 손상만 수정 | 최적화는 생략하고 최소 수정에 집중합니다. |
-E unshare_blocks | shared block 해제 | read-only shared_blocks 파일시스템 전환 작업에서 사용합니다. |
-z undo_file | undo 로그 생성 | e2fsprogs 내부 수정 블록을 별도 로그로 저장합니다. |
# 읽기 전용 전체 점검
e2fsck -fn /dev/sdb1
# 부팅 실패 후 저널만 먼저 확인
e2fsck -E journal_only /dev/sdb1
# 백업 슈퍼블록 위치 확인
mkfs.ext4 -n /dev/sdb1
# 백업 슈퍼블록을 사용한 검사 예시
e2fsck -b 32768 -B 4096 /dev/sdb1
# 손상 수정 로그를 남기면서 검사
logsave /var/tmp/e2fsck-sdb1.log \
e2fsck -f -C 0 -z /var/tmp/sdb1.e2undo /dev/sdb1
e2fsck -y는 작업 속도는 빠르지만, 잘못 연결된 외장 디스크나 잘못된 멀티패스 경로에 대해 실행하면 되돌리기 어려운 손상을 만들 수 있습니다. 운영 서버에서는 먼저 -n 또는 스냅샷에서 결과를 확인하세요.
e2fsck 종료 코드 해석
e2fsck와 fsck.ext4는 단순한 0/1 반환이 아니라 조건별 비트 합으로 종료 코드를 돌려줍니다. 따라서 자동화 스크립트에서 “0이 아니면 무조건 실패”로 처리하면 오동작할 수 있습니다. 예를 들어 종료 코드가 3이면 1 + 2가 합쳐진 값으로, “오류를 수정했고 재부팅이 필요함”을 뜻합니다.
| 값 | 의미 | 자동화 해석 |
|---|---|---|
0 | 오류 없음 | 정상 종료입니다. |
1 | 파일시스템 오류 수정됨 | 수정은 성공했지만, 운영 로그에는 반드시 남기는 편이 좋습니다. |
2 | 파일시스템 오류 수정됨, 시스템 재부팅 필요 | 루트 파일시스템 또는 중요한 메타데이터가 바뀐 경우 재부팅 계획을 세웁니다. |
4 | 수정되지 않은 오류가 남음 | 운영 계속 진행보다 추가 점검과 수동 복구가 우선입니다. |
8 | 운영 오류 | 장치 접근 실패, I/O 오류, 잠금 충돌 같은 실행 실패를 의심합니다. |
16 | 사용법 또는 구문 오류 | 명령행 옵션이나 인자 구성이 잘못된 경우입니다. |
32 | 사용자 취소 | 대화형 실행 중 중단되었거나 외부에서 취소된 경우입니다. |
128 | 공유 라이브러리(Shared Library) 오류 | 실행 환경 자체가 깨졌을 가능성이 큽니다. |
1과 2는 “치명적 실패”가 아니라 “복구는 됐지만 후속 조치가 필요함”에 가깝습니다. 반대로 4, 8, 16, 32, 128 비트가 켜져 있으면 운영 자동화에서 실패로 간주하는 편이 안전합니다.
# e2fsck 종료 코드 해석 예시
e2fsck -p /dev/sdb1
rc=$?
# 0: 문제 없음
if (( rc == 0 )); then
echo "정상 종료"
fi
# 1: 수정됨
if (( rc & 1 )); then
echo "파일시스템 오류가 수정되었습니다."
fi
# 2: 재부팅 필요
if (( rc & 2 )); then
echo "재부팅이 필요합니다."
fi
# 4 이상 주요 실패 비트
if (( rc & 4 || rc & 8 || rc & 16 || rc & 32 || rc & 128 )); then
echo "자동 복구 실패 또는 실행 환경 오류"
exit 1
fi
대부분의 다른 e2fsprogs 유틸리티는 세부 비트마스크를 문서화하지 않고, 일반적인 Unix 관례대로 0은 성공, 비0은 실패로 해석하면 충분합니다. 자동화에서 세밀한 분기 처리가 필요한 대표 사례는 사실상 e2fsck입니다.
배포판 부트 처리에서 종료 코드를 다루는 방식
2026년 4월 9일 기준으로 주요 배포판은 대체로 같은 e2fsck 비트마스크를 사용하지만, 실제 운영 결과는 이를 감싸는 fsck 프런트엔드와 systemd-fsck, 그리고 초기 RAM 파일시스템(initramfs) 구성 도구(initramfs-tools 또는 dracut)가 결정합니다. 즉 코드는 공통이고, 부트 체인(Boot Chain)의 해석 방식이 다릅니다.
| 계층 | 무엇을 하는가 | 종료 코드 처리 방식 |
|---|---|---|
e2fsck | ext4 자체 검사/복구 | 문서화된 비트마스크를 직접 반환합니다. |
fsck (util-linux) | 파일시스템별 검사기 호출 프런트엔드 | 여러 파일시스템을 검사하면 개별 종료 코드를 비트 OR 하여 합성합니다. |
systemd-fsck | 부팅 시 mount 단위로 fsck 실행 | 2와 4를 보고 reboot.target 또는 emergency.target으로 전이합니다. |
| initramfs | 실제 루트 마운트 전 초기 검사 | root 파일시스템을 여기서 먼저 처리하면, 이후 본 시스템의 systemd-fsck-root.service는 건너뛸 수 있습니다. |
e2fsck /dev/sdXn를 실행했을 때의 해석 규칙과, 부팅 중 systemd-fsck 또는 fsck -A가 그 결과를 받아 시스템 상태를 바꾸는 규칙은 같지 않습니다. 운영 문서에는 두 층을 분리해서 적는 편이 안전합니다.
util-linux fsck 프런트엔드가 합치는 방식
배포판 부팅 스크립트나 관리자가 fsck -A처럼 여러 파일시스템을 한 번에 검사하면, util-linux의 fsck는 각 검사기의 종료 코드를 비트 OR 하여 최종 종료 코드를 만듭니다. 즉 여러 장치 중 하나가 1, 다른 하나가 4를 반환하면 최종 값은 5가 됩니다.
# 예시: /dev/sda1은 1, /dev/sdb1은 4를 반환했다고 가정
fsck -A
rc=$?
# rc == 5 (1 | 4)
if (( rc & 1 )); then
echo "어떤 파일시스템에서는 오류가 수정되었습니다."
fi
if (( rc & 4 )); then
echo "어떤 파일시스템에서는 수정되지 않은 오류가 남아 있습니다."
fi
fsck -A의 반환값은 “마지막 장치의 값”이 아니라 “전체 장치 결과의 비트 합”입니다. 병렬 또는 다중 마운트 환경에서 단일 장치처럼 해석하면 잘못된 알람을 만들 수 있습니다.
systemd 기반 배포판의 공통 처리
대부분의 최신 배포판은 boot-time fsck를 systemd-fsck-root.service, systemd-fsck-usr.service, systemd-fsck@.service로 처리합니다. 여기서 중요한 차이는 “root와 /usr” 그리고 “그 외 일반 마운트”입니다.
| 유닛 | 대상 | 2 처리 | 4 처리 | 비고 |
|---|---|---|---|---|
systemd-fsck-root.service | root | reboot.target | emergency.target | 단, root가 initrd에서 이미 검사되지 않았을 때만 |
systemd-fsck-usr.service | /usr | reboot.target | emergency.target | /usr 분리 시스템에서만 의미가 큽니다 |
systemd-fsck@.service | 기타 마운트 | 유닛 실패 | 유닛 실패 | nofail/noauto가 아니면 이후 local-fs.target가 emergency.target으로 전이할 수 있습니다 |
또한 fsck.mode=auto|force|skip, fsck.repair=preen|yes|no 커널 파라미터가 공통 인터페이스입니다. 여기서 preen은 “안전한 수정만 자동 수행”, yes는 “모든 질문에 yes”, no는 “질문에 no”를 뜻합니다.
배포판 계열별 실제 동작 포인트
| 배포판 계열 | 초기 부트 스택 | root fsck가 주로 일어나는 위치 | 실제 운영에서 보이는 특징 |
|---|---|---|---|
| Debian / Ubuntu 계열 | initramfs-tools + systemd | initramfs 또는 이후 systemd-fsck-root.service | /run/initramfs/fsck.log에 로그를 남기고, 성공 시 /run/initramfs/fsck-root 마커를 둡니다. |
| RHEL / CentOS Stream / Fedora 계열 | dracut + systemd | 대개 initrd | dracut 기반 initramfs에서 root 장치 준비와 조기 fsck가 이뤄지는 구성이 일반적입니다. |
| SLES / openSUSE 계열 | dracut + systemd | initramfs | 문서상 root 마운트 실패 시 ext3/ext4 검사기가 자동으로 시작되고, 복구 후 root 마운트를 재시도합니다. |
| Arch Linux | systemd 중심 사용자 공간 | initrd에서 검사됐는지 여부에 따라 달라짐 | 최신 Arch 문서는 fsck.mode, fsck.repair를 서비스 자격 증명(Credentials)으로도 주입할 수 있음을 문서화합니다. |
dracut 기반 initramfs를 사용함을 보여 주며, 그 위에서의 fsck 의미는 동일한 systemd-fsck 규칙을 따릅니다. 즉 RHEL/Fedora 항목의 “대개 initrd”는 해당 공통 스택에 대한 해석입니다.
Debian / Ubuntu 계열: initramfs 흔적과 진행 표시
Debian의 initramfs-tools 문서는 initramfs 안에서 실행된 fsck 로그를 /run/initramfs/fsck.log에 남기고, root와 /usr에 대해 각각 /run/initramfs/fsck-root, /run/initramfs/fsck-usr 성공 마커를 둔다고 명시합니다. 이 때문에 Debian/Ubuntu 계열에서는 “root가 이미 initrd에서 검사됐는지”를 부팅 후 확인하기가 상대적으로 쉽습니다.
# Debian/Ubuntu 계열에서 root fsck 흔적 확인 예시
test -e /run/initramfs/fsck-root && echo "root fsck가 initramfs에서 성공했습니다."
test -e /run/initramfs/fsck.log && sed -n '1,120p' /run/initramfs/fsck.log
# systemd 쪽 유닛 결과 확인
systemctl status systemd-fsck-root.service
systemctl status 'systemd-fsck@dev-disk-by\\x2duuid-*.service'
또한 systemd-fsckd가 있으면 콘솔과 Plymouth에 진행률을 합쳐서 보여 주고, Ctrl+C 취소도 처리할 수 있습니다. 따라서 “부팅 중 fsck 퍼센트가 보였다”는 사용자 경험은 실제로는 e2fsck 자체가 아니라 systemd-fsckd가 만든 화면일 수 있습니다.
RHEL / Fedora / SUSE 계열: dracut 단계의 의미
RHEL 문서는 initrd를 dracut 인프라가 만든다고 설명하며, SUSE 문서는 initramfs 안의 init가 실제 root 마운트를 준비한다고 설명합니다. SUSE 문서에는 root 파일시스템 마운트 실패 시 ext3/ext4에서는 파일시스템 검사기가 자동 시작되고, 복구에 성공하면 root 마운트를 다시 시도한다고까지 적혀 있습니다. 즉 dracut 계열 배포판에서는 fsck 결과가 “정적 로그 한 줄”이 아니라 root 장치 준비 -> 검사 -> 복구 -> 재마운트 시도 흐름 안에서 소비됩니다.
# dracut 계열에서 흔히 보는 커널 커맨드라인 제어
cat /proc/cmdline
# 강제 검사
# ... fsck.mode=force fsck.repair=yes
# 검사 생략
# ... fsck.mode=skip
systemd-fsck-root.service는 “실행 안 됨”처럼 보일 수 있습니다. 이 경우를 곧바로 “root fsck가 생략되었다”로 해석하면 안 됩니다.
최신 배포판에서 보이는 systemd 버전 차이
최신 배포판은 모두 fsck.mode, fsck.repair 커널 파라미터를 지원하지만, 최근 systemd는 여기에 서비스 자격 증명(Credentials) 주입을 추가했습니다. 이 차이는 이미지 기반 배포, 클라우드 초기 부트, CI 가상머신 부트 정책 제어에서 꽤 중요합니다.
| 배포판 문서 | 확인된 systemd 버전 | 문서화된 fsck 제어 |
|---|---|---|
Ubuntu 25.04 (plucky) manpage | 257.4-1ubuntu3.2 | 커널 파라미터 fsck.mode, fsck.repair |
| Debian testing manpage | 259-1 | 커널 파라미터 + 서비스 자격 증명 fsck.mode, fsck.repair |
| Arch Linux manpage | 258.2-2 | 커널 파라미터 + 서비스 자격 증명 fsck.mode, fsck.repair |
즉 Ubuntu 계열에서는 아직 “커널 커맨드라인 중심”으로 생각해도 무방한 반면, Debian testing과 Arch 최신 계열은 서비스 관리자 레벨에서 fsck 정책을 주입하는 방향으로 문서가 확장되고 있습니다.
# 부팅 후 현재 배포판이 어떤 systemd 세대를 쓰는지 확인
systemd --version
# 커널 커맨드라인에 설정된 정책 확인
cat /proc/cmdline | tr ' ' '\n' | grep '^fsck\\.'
# systemd 유닛 결과와 부팅 전개 확인
journalctl -b -u systemd-fsck-root.service -u systemd-fsck-usr.service
journalctl -b | grep -E 'fsck|systemd-fsck|e2fsck'
fsck, systemd-fsck, initramfs 구현이 각 배포판에서 어떻게 그 비트를 해석하느냐에 따라 결정됩니다.” 이 한 문장을 넣어 두면 운영자가 root 원인을 훨씬 빨리 좁힐 수 있습니다.
dumpe2fs / e2freefrag
dumpe2fs는 “현재 ext4가 어떤 구조로 만들어졌는가”를 가장 빨리 보여 주는 도구입니다. tune2fs -l가 슈퍼블록 위주라면, dumpe2fs는 그룹 디스크립터와 블록 그룹 구성까지 더 깊게 보여 줍니다. e2freefrag는 여유 공간이 얼마나 잘 이어져 있는지 정량화합니다.
# 헤더만 출력
dumpe2fs -h /dev/sdb1
# 그룹 디스크립터를 machine-readable 형식으로 출력
dumpe2fs -g /dev/sdb1 | head
# bad block 예약 목록 확인
dumpe2fs -b /dev/sdb1
# free space 단편화 확인
e2freefrag /dev/sdb1
dumpe2fs 종료 코드: 매뉴얼상 dumpe2fs는 정상 완료 시 0, 유효한 슈퍼블록을 읽지 못했거나 체크섬 오류가 있거나 장치 사용 충돌이 있으면 비0을 반환합니다. 구조 확인 자동화에서는 단순히 0/비0만 체크해도 충분합니다.
| 보고 싶은 정보 | 권장 명령 | 보는 이유 |
|---|---|---|
| feature flag, inode 크기, UUID | dumpe2fs -h | 생성 정책과 커널 호환성 확인 |
| 블록 그룹별 메타데이터 위치 | dumpe2fs -g | 슈퍼블록/bitmap/inode table 배치 분석 |
| free space 연속성 | e2freefrag | 대형 파일 성능과 조각화 위험 평가 |
debugfs
debugfs는 ext4 내부를 직접 들여다보는 현미경입니다. inode 번호를 보고 경로를 찾아내고, block -> inode -> path를 오가며, extent와 저널 내용을 출력할 수 있습니다. 기본은 읽기 전용이며, 이 상태만으로도 대부분의 분석은 충분합니다.
| 명령 | 역할 | 언제 쓰는가 |
|---|---|---|
stat <inode> | inode 상세 정보 | 크기, 링크 수, 플래그, 블록 매핑을 볼 때 |
dump_extents | extent tree 출력 | 대형 파일 조각화, 논리/물리 매핑 분석 |
htree_dump | 디렉토리 hash tree 확인 | 대형 디렉토리 조회 성능 문제 분석 |
logdump | JBD2 저널 내용 출력 | 저널 replay 전후 상태 추적 |
icheck block | 블록 -> inode 역방향 조회 | 특정 물리 블록이 누구 것인지 확인 |
ncheck inode | inode -> 경로 추적 | 손상된 inode의 실제 파일명 확인 |
imap inode | inode table 상 위치 확인 | 온디스크 inode 테이블 조사 |
lsdel | 삭제된 inode 목록 | 즉시 복구 가능성 탐색 |
rdump | 디렉토리 트리 복사 | 복제본에서 파일 구조를 밖으로 복사 |
# inode 상세 정보
debugfs -R "stat <12>" /dev/sdb1
# extent tree 확인
debugfs -R "dump_extents <12>" /dev/sdb1
# inode 번호에서 경로 찾기
debugfs -R "ncheck 524305" /dev/sdb1
# 특정 물리 블록의 소유 inode 찾기
debugfs -R "icheck 1234567" /dev/sdb1
# inode 테이블 내 실제 위치 확인
debugfs -R "imap <524305>" /dev/sdb1
# 디렉토리 HTree와 저널 확인
debugfs -R "htree_dump /home" /dev/sdb1
debugfs -R "logdump -a" /dev/sdb1
# 삭제 파일 구조를 복제본에서 꺼내기
debugfs -R "rdump /lost+found /mnt/recover/lostfound" /dev/sdb1
debugfs -w는 마지막 수단: 쓰기 모드는 매우 강력하지만 안전장치가 적습니다. 꼭 필요하다면 언마운트된 복제본에서 시작하고, 가능하면 -z undo_file를 함께 사용하세요.
resize2fs / e2undo
resize2fs는 ext4 확장과 축소를 담당합니다. 온라인 확장은 ext4의 강점 중 하나이지만, 축소는 오프라인만 지원합니다. e2undo는 -z로 남겨 둔 undo 로그를 다시 적용해 e2fsprogs가 수정한 메타데이터 블록을 되돌릴 때 사용합니다.
| 옵션 | 의미 | 실무 포인트 |
|---|---|---|
resize2fs device | 아래 장치 크기까지 확장 | 온라인 확장의 기본 형태입니다. |
resize2fs device size | 명시한 크기로 조정 | 축소 시 목표 크기를 명확히 지정할 수 있습니다. |
-P | 최소 크기 추정 | 축소 계획을 세울 때 먼저 봐야 합니다. |
-M | 가능한 최소 크기로 축소 | 자동 축소이지만 여유를 두고 수동 목표 크기를 잡는 편이 더 안전합니다. |
-b | 64bit feature 활성화 | 매우 큰 파일시스템 준비 시 사용합니다. |
-s | 64bit feature 비활성화 | 블록 수가 충분히 작아야 하며, 매우 신중히 사용해야 합니다. |
-z undo_file | undo 로그 생성 | 메타데이터 수정 블록을 별도 파일에 저장합니다. |
# 온라인 확장
resize2fs /dev/vgdata/lvhome
# 축소 전에 최소 크기 추정
resize2fs -P /dev/vgdata/lvhome
# 목표 크기로 축소
resize2fs -z /var/tmp/lvhome.e2undo /dev/vgdata/lvhome 800G
# 문제가 생기면 undo 로그를 적용
e2undo -n /var/tmp/lvhome.e2undo /dev/vgdata/lvhome # dry-run
e2undo /var/tmp/lvhome.e2undo /dev/vgdata/lvhome
e2undo는 e2fsprogs가 기록한 메타데이터 블록을 되감는 도구이지, 전원 장애나 커널 버그로 인한 전체 상태를 복원하는 백업 솔루션은 아닙니다. 따라서 snapshot이나 전체 이미지 백업을 대체하지 못합니다.
보조 도구
| 도구 | 주 용도 | 예시 |
|---|---|---|
e2image | 메타데이터 또는 사용 중 블록만 이미지화 | e2image -r -p /dev/sdb1 sdb1.e2i.raw |
e4defrag | 온라인 단편화 정리 | e4defrag -c /srv/data, e4defrag /srv/data |
badblocks | 배드블록 탐지 | e2fsck -c /dev/sdb1 또는 mkfs.ext4 -c /dev/sdb1 |
filefrag | 개별 파일 extent 배치 확인 | filefrag -v /srv/data/bigfile |
chattr, lsattr | ext4 속성 플래그 조정/조회 | chattr +i config, chattr +P -p 100 /srv/tenantA |
logsave | fsck/debug 로그 저장 | logsave /var/tmp/fsck.log e2fsck -f /dev/sdb1 |
# 조각화 점수만 먼저 확인
e4defrag -c /srv/data
# 개별 파일 extent 확인
filefrag -v /srv/data/bigfile
# immutable 플래그 확인
lsattr /etc/important.conf
chattr +i /etc/important.conf
e2fsprogs 실전 시나리오
시나리오 1: 새 서버 데이터 볼륨 준비
대용량 데이터 볼륨은 루트 볼륨과 요구사항이 다릅니다. 예약 블록 비율을 줄이고, RAID 위라면 stripe 정보를 반영하며, 생성 직후 구조를 검증하는 것이 핵심입니다.
# 새 데이터 볼륨
mkfs.ext4 -L data -m 1 -T largefile4 \
-E stride=128,stripe_width=768 \
/dev/md0
tune2fs -l /dev/md0 | grep -E "Filesystem features|Reserved block count"
mount /dev/md0 /srv/data
시나리오 2: 주 슈퍼블록 손상 복구
마운트가 실패하고 bad magic number in super-block 같은 메시지가 보이면, 먼저 같은 geometry로 가정한 백업 슈퍼블록 위치를 계산한 뒤 e2fsck -b를 시도합니다.
# 실제 포맷은 하지 않고 백업 슈퍼블록 위치만 계산합니다.
mkfs.ext4 -n /dev/sdb1
# 예를 들어 32768 블록에 백업 슈퍼블록이 있다면
e2fsck -b 32768 -B 4096 /dev/sdb1
# 성공하면 즉시 메타데이터 이미지를 다시 확보합니다.
e2image -r -p /dev/sdb1 /mnt/backup/sdb1-postfix.e2i.raw
시나리오 3: 서비스 중 용량 확장
LVM, SAN, 클라우드 블록 장치에서는 ext4 온라인 확장이 매우 흔합니다. 아래 계층의 크기 확장만 끝났다면 resize2fs 하나로 마무리할 수 있습니다.
# 디스크 또는 LV를 먼저 확장
lvextend -L +500G /dev/vgdata/lvhome
# ext4는 마운트된 상태에서 온라인 확장 가능
resize2fs /dev/vgdata/lvhome
df -h /home
시나리오 4: 손상 원인 분석과 파일 회수
서비스 중 장애가 발생했지만 원본 디스크에 추가 쓰기를 최소화해야 하는 상황입니다. 이때는 e2image와 debugfs 조합이 가장 강력합니다.
# 메타데이터 중심 이미지 확보
e2image -r -p /dev/sdb1 /mnt/backup/sdb1.e2i.raw
# 이미지에서 inode와 저널 추적
debugfs -R "stat <524305>" /mnt/backup/sdb1.e2i.raw
debugfs -R "ncheck 524305" /mnt/backup/sdb1.e2i.raw
debugfs -R "logdump -a" /mnt/backup/sdb1.e2i.raw
# 필요하면 디렉토리 구조를 밖으로 복사
debugfs -R "rdump /important /mnt/recover/important" /mnt/backup/sdb1.e2i.raw
- 생성:
mkfs.ext4+tune2fs -l - 점검:
e2fsck -fn+logsave - 구조 관찰:
dumpe2fs -h+debugfs stat - 확장: 아래 계층 확장 후
resize2fs - 복구 전 증거 확보:
e2image -r
ext4 vs 다른 파일시스템 비교
| 항목 | ext4 | XFS | Btrfs | F2FS |
|---|---|---|---|---|
| 최대 볼륨 | 1 EiB | 8 EiB | 16 EiB | 16 TiB |
| 최대 파일 | 16 TiB | 8 EiB | 16 EiB | 3.94 TiB |
| COW | X | reflink 지원 | 기본 COW | X |
| 스냅샷 | X | X | O (subvolume) | X |
| 체크섬(데이터) | X (메타만) | reflink 시 | O (전체) | X |
| 압축 | X | X | O (zstd/lzo) | O (LZO/LZ4/zstd) |
| RAID 내장 | X | X | O (0/1/5/6/10) | X |
| 온라인 축소 | X | X | O | X |
| 온라인 확장 | O | O | O | X |
| 원자적 쓰기 | O (v6.13+, DIO) | O (v6.13+, DIO) | X | X |
| fsck 속도 | 느림 (전체 스캔) | 빠름 (병렬) | 느림 | 보통 |
| 성숙도 | 매우 높음 | 매우 높음 | 높음 | 보통 |
| 주 사용처 | 범용, 루트 FS | 대용량, 엔터프라이즈 | NAS, 데스크탑 | Flash/eMMC |
- 범용 서버 / 루트 FS: ext4 (안정성, 호환성, 복구 도구 완비)
- 대용량 스토리지 / DB: XFS (대파일 성능, 병렬 I/O)
- 스냅샷 / 데이터 무결성: Btrfs (COW, 체크섬, 스냅샷)
- 임베디드 / Flash: F2FS (Flash 특성 최적화)
ext4 주요 버그 및 데이터 손실 사례
ext4는 리눅스에서 가장 널리 사용되는 파일시스템이지만, 개발 과정에서 심각한 버그와 데이터 손실 사례가 있었습니다. 이들 사례를 분석하면 파일시스템 설계의 근본적인 어려움과 POSIX 시맨틱의 한계를 이해할 수 있습니다.
1. Delayed Allocation 데이터 손실 (2008-2009)
ext4의 대표적인 초기 버그로, delayed allocation(지연 할당) 기능이 기존 ext3의 동작과 다르게 동작하면서 광범위한 데이터 손실을 유발한 사례입니다. 많은 프로그램이 임시 파일에 데이터를 쓴 뒤 rename()으로 원본 파일을 대체하는 패턴을 사용했는데, ext4에서는 이 패턴이 안전하지 않았습니다.
rename()으로 기존 파일을 대체하는 과정에서 시스템 크래시가 발생하면, 새 파일이 0바이트로 남게 되었습니다. 구 파일은 이미 rename()에 의해 삭제된 상태이므로, 데이터가 완전히 유실되었습니다. GNOME, KDE 등 주요 데스크탑 환경의 설정 파일이 이 방식으로 손실되는 사례가 다수 보고되었습니다.
원인 분석: delayed allocation은 실제 디스크 블록 할당을 fsync() 또는 writeback 시점까지 지연합니다. ext3에서는 data=ordered 모드에서 블록 할당이 비교적 즉시 이루어져 크래시 후에도 데이터가 보존될 가능성이 높았습니다. 그러나 ext4의 delayed allocation은 메타데이터와 데이터 모두 지연시켜, 크래시 시점에 디스크에 아무것도 기록되지 않은 상태가 발생했습니다.
/* 문제가 된 전형적인 프로그램 패턴 */
fd = open("config.tmp", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, data, len);
close(fd);
/* fsync() 호출 없이 바로 rename — ext4에서 위험! */
rename("config.tmp", "config");
/* 올바른 패턴: rename 전에 fsync() 필수 */
fd = open("config.tmp", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, data, len);
fsync(fd); /* 디스크에 데이터 기록 보장 */
close(fd);
rename("config.tmp", "config");
커널 수정: Theodore Ts'o는 원칙적으로 "프로그램이 fsync()를 호출해야 합니다"고 주장했으나, 커뮤니티의 강한 반발로 결국 ext4에 호환성 패치(Patch)가 적용되었습니다. ext4_da_writepages()에서 rename()이나 truncate()를 감지하면 즉시 블록 할당을 수행하도록 변경되었습니다.
/* fs/ext4/inode.c - delayed allocation 안전 장치 (간략화) */
static int ext4_da_writepages(struct address_space *mapping,
struct writeback_control *wbc)
{
struct inode *inode = mapping->host;
/*
* rename/truncate 감지 시 즉시 할당으로 전환
* EXT4_STATE_DA_ALLOC_CLOSE: close() 시 지연 할당 블록 즉시 기록
*/
if (ext4_test_inode_state(inode, EXT4_STATE_DA_ALLOC_CLOSE)) {
ext4_alloc_da_blocks(inode);
}
/* ... writeback 진행 ... */
}
코드 설명
- ext4_da_writepages()VFS의
writeback경로에서 호출되는 지연 할당의 핵심 함수입니다. 페이지 캐시의 dirty 페이지들을 모아서 mballoc으로 연속 블록을 할당하고 디스크에 기록합니다.fs/ext4/inode.c에 정의되어 있습니다. - EXT4_STATE_DA_ALLOC_CLOSEext4의 delalloc 데이터 손실 논란 이후 추가된 안전 장치입니다.
rename()이나close()시 이 상태 플래그가 설정되면, writeback 시점까지 기다리지 않고ext4_alloc_da_blocks()를 호출하여 즉시 블록을 할당합니다. 이를 통해rename()패턴에서 데이터가 유실되는 문제를 방지합니다.
rename() 후 fsync() 없이도 데이터 영속성을 보장하지 않습니다. 그러나 ext3에서 사실상 보장되었던 동작에 수많은 프로그램이 의존하고 있었습니다. 이 사건은 "사양 vs 실제 동작"의 괴리를 보여주는 대표적 사례이며, 파일시스템 변경이 사용자 공간 프로그램에 미치는 영향을 과소평가해서는 안 되는 교훈을 남겼습니다.
2. JBD2 저널 손상과 복구
JBD2(Journaling Block Device 2)는 ext4의 저널링 엔진으로, 메타데이터(및 선택적으로 데이터)의 원자적 갱신을 보장합니다. 그러나 전원 손실이나 하드웨어 오류 시 저널 자체가 손상되는 사례가 보고되었습니다.
JBD2_FEATURE_INCOMPAT_CSUM_V3 도입: 이 문제를 해결하기 위해 저널 블록에 CRC32C 체크섬을 추가하는 JBD2_FEATURE_INCOMPAT_CSUM_V3 기능이 도입되었습니다. 이전 버전(CSUM_V2)의 체크섬 범위가 불완전했던 문제를 수정하여, 모든 저널 블록(디스크립터, 커밋, 리보크, 데이터 블록)에 대한 완전한 체크섬을 보장합니다.
/* fs/jbd2/commit.c - 저널 커밋 시 체크섬 삽입 (간략화) */
static void jbd2_descriptor_block_csum_set(
journal_t *j, struct buffer_head *bh)
{
struct jbd2_journal_block_tail *tail;
__u32 csum;
if (!jbd2_journal_has_csum_v2or3(j))
return;
tail = (struct jbd2_journal_block_tail *)
(((char *)bh->b_data) + j->j_blocksize -
sizeof(struct jbd2_journal_block_tail));
tail->t_checksum = 0;
csum = jbd2_chksum(j, j->j_csum_seed, bh->b_data, j->j_blocksize);
tail->t_checksum = cpu_to_be32(csum);
}
data 모드별 안전성 비교:
| 저널 모드 | 메타데이터 보호 | 데이터 보호 | 성능 | 크래시 안전성 |
|---|---|---|---|---|
data=journal | O | O (저널에 기록) | 낮음 | 가장 높음 |
data=ordered | O | 순서 보장 | 중간 (기본값) | 높음 |
data=writeback | O | X | 높음 | 낮음 (stale 데이터 노출 가능) |
e2fsck -f로 강제 복구를 수행할 때 저널이 손상된 상태라면, 저널 재생이 파일시스템을 추가적으로 손상시킬 수 있습니다. 심각한 저널 손상 시에는 e2fsck -f -E journal_only로 저널만 먼저 검증하거나, 최악의 경우 tune2fs -O ^has_journal로 저널을 제거한 뒤 복구를 시도해야 합니다. 반드시 복구 전 디스크 이미지 백업(dd 또는 ddrescue)을 수행하십시오.
3. Extent Tree 손상 사례
ext4의 extent tree는 파일의 물리적 블록 매핑을 B-tree 형태로 관리합니다. 대용량 파일이나 심한 단편화 상태에서 extent tree의 깊이(depth)가 증가하며, 이 과정에서 다양한 손상 사례가 보고되었습니다.
- Depth 오류: 대용량 파일의 extent tree depth가 실제 트리 구조와 불일치하여 파일 접근이 완전히 불가능해지는 사례. 특히 extent 분할(split) 과정에서 전원 손실 시 발생
- Status Tree 캐시 불일치: 메모리의 extent status tree 캐시가 디스크의 실제 extent와 달라 잘못된 블록을 읽거나 덮어쓰는 사례. 장시간 운영 중인 서버에서 간헐적으로 보고됨
- e4defrag 중 크래시: 온라인 조각 모음(Defragmentation)(
e4defrag) 실행 중 크래시가 발생하면 extent가 이전 위치와 새 위치 모두에서 참조되어 데이터 중복 또는 손상 발생
Extent 유효성 검증 강화: 이러한 문제들을 해결하기 위해 ext4_ext_check()에서 extent의 논리적 일관성을 엄격하게 검증하도록 강화되었습니다.
/* fs/ext4/extents.c - extent 유효성 검증 (간략화) */
static int ext4_ext_check(struct inode *inode,
struct ext4_extent_header *eh,
int depth, ext4_fsblk_t pblk)
{
const char *error_msg;
/* 매직 넘버 검증 */
if (unlikely(eh->eh_magic != EXT4_EXT_MAGIC)) {
error_msg = "invalid magic number";
goto corrupted;
}
/* depth 범위 검증 */
if (unlikely(eh->eh_depth != depth)) {
error_msg = "unexpected eh_depth";
goto corrupted;
}
/* 엔트리 수 상한 검증 */
if (unlikely(eh->eh_entries > eh->eh_max)) {
error_msg = "invalid eh_entries";
goto corrupted;
}
/* metadata_csum 활성 시 체크섬 검증 */
if (ext4_has_metadata_csum(inode->i_sb) &&
!ext4_extent_block_csum_verify(inode, eh)) {
error_msg = "extent block checksum failed";
goto corrupted;
}
return 0;
corrupted:
ext4_error_inode(inode, "ext4_ext_check",
"%s (depth %d, pblk %llu)",
error_msg, depth, pblk);
return -EFSCORRUPTED;
}
metadata_csum 기능을 활성화하면 extent 블록을 포함한 모든 메타데이터 블록에 CRC32C 체크섬이 추가되어 손상을 조기에 탐지할 수 있습니다. 새 파일시스템 생성 시 mkfs.ext4 -O metadata_csum을 권장합니다. 기존 파일시스템은 tune2fs -O metadata_csum으로 활성화할 수 있으나, 반드시 e2fsck -f를 먼저 수행해야 합니다.
4. ext4 마운트 옵션 보안 문제
ext4의 마운트 옵션 설정에 따라 데이터 무결성과 보안에 심각한 영향을 미칠 수 있습니다. 성능 최적화를 위해 안전 장치를 해제하는 설정이 프로덕션 환경에서 사용되어 문제가 된 사례들입니다.
/* 위험한 마운트 옵션 조합 */
# 절대 프로덕션에서 사용하지 마십시오:
mount -o barrier=0,data=writeback,nodelalloc /dev/sda1 /mnt
# 권장 안전 설정:
mount -o barrier=1,data=ordered,delalloc /dev/sda1 /mnt
# 최대 안전 설정 (성능 저하 감수):
mount -o barrier=1,data=journal,journal_checksum /dev/sda1 /mnt
주요 보안 및 안전성 문제:
| 마운트 옵션 | 위험 수준 | 영향 | 권장사항 |
|---|---|---|---|
barrier=0 | 높음 | 쓰기 순서 미보장으로 저널 무효화(Invalidation) 가능 | BBU RAID에서만 사용 |
nodelalloc | 낮음 | 성능 저하, 단편화 증가 | delayed alloc 버그 우회 시에만 |
max_dir_size_kb 미설정 | 중간 | 거대 디렉토리의 해시 충돌로 DoS 가능 | 공유 시스템에서 제한 설정 |
errors=continue | 높음 | I/O 에러 발생 시 계속 동작 → 데이터 추가 손상 | errors=remount-ro 권장 |
errors=continue는 파일시스템 에러 발생 시에도 계속 동작합니다. 디스크 불량 섹터나 메타데이터 손상 상태에서 계속 쓰기를 수행하면 손상이 확산됩니다. 프로덕션 환경에서는 반드시 errors=remount-ro(읽기 전용 재마운트) 또는 errors=panic(시스템 중단)으로 설정하여, 에러 감지 즉시 추가 손상을 방지해야 합니다. tune2fs -e remount-ro /dev/sdXN으로 설정할 수 있습니다.
Extent Tree: B+tree 구조와 탐색 알고리즘
ext4의 Extent Tree는 B+tree 변형으로, 파일의 논리 블록 번호(logical block number)를 디스크의 물리 블록 번호(physical block number)로 매핑합니다. 이 섹션에서는 앞서 소개한 기본 구조를 넘어, 트리의 분할·병합·깊이 변화, 커널 내부의 탐색 경로 최적화, 그리고 실제 디버깅(Debugging) 사례를 심층적으로 다룹니다.
트리 깊이와 최대 엔트리 수
extent tree의 깊이는 파일 크기와 단편화 정도에 의존합니다. inode 내부(60바이트)에 root 노드가 저장되며, 외부 블록(4096바이트)은 더 많은 엔트리를 수용합니다.
| 위치 | 가용 크기 | 최대 idx/extent | 비고 |
|---|---|---|---|
| inode i_block[15] | 60 바이트 | 4개 | header(12) + entry(12) × 4 |
| 외부 블록 (4K) | 4096 바이트 | 340개 | header(12) + entry(12) × 340 |
| depth=0 (inode 내장) | — | 4 extents | 최대 ~128K 블록 = 512MB (4K 블록) |
| depth=1 | — | 4 × 340 = 1,360 extents | 약 5.3GB ~ 40GB+ |
| depth=2 | — | 4 × 340² = 462,400 extents | 수 TB 규모 |
EXT4_MAX_EXTENT_DEPTH는 5로 정의되어 있지만, 실제 운영 환경에서 depth 3 이상은 극히 드뭅니다. depth=2면 수백만 개의 extent를 수용할 수 있어, 수 TB의 극도로 단편화된 파일도 처리 가능합니다.
B+tree 탐색 경로: ext4_find_extent()
ext4_find_extent()는 root에서 시작하여 이진 탐색으로 각 깊이의 적절한 인덱스 엔트리를 찾고, 해당 자식 블록을 읽어 리프까지 도달합니다.
/* fs/ext4/extents.c - ext4_find_extent() 핵심 로직 (간략화) */
struct ext4_ext_path *
ext4_find_extent(struct inode *inode, ext4_lblk_t block,
struct ext4_ext_path **orig_path, int flags)
{
struct ext4_extent_header *eh;
struct ext4_ext_path *path;
int depth, i;
eh = ext_inode_hdr(inode); /* inode의 i_block[] → extent header */
depth = ext_depth(inode); /* eh->eh_depth */
/* depth+1 개의 path 엔트리 할당 */
path = kcalloc(depth + 2, sizeof(*path), GFP_NOFS);
path[0].p_hdr = eh;
i = 0;
while (i < depth) {
/* 이진 탐색으로 block을 포함하는 idx 찾기 */
ext4_ext_binsearch_idx(inode, path + i, block);
/* idx가 가리키는 자식 블록 읽기 */
path[i + 1].p_bh = read_extent_tree_block(
inode, path[i].p_idx, depth - i - 1, flags);
path[i + 1].p_hdr = ext_block_hdr(path[i + 1].p_bh);
i++;
}
/* 리프 레벨: extent 이진 탐색 */
ext4_ext_binsearch(inode, path + i, block);
return path;
}
Extent 분할과 병합
새로운 extent를 삽입할 때 리프 노드가 가득 차면 분할(split)이 발생합니다. 반대로 인접한 extent가 물리적으로 연속이면 병합(merge)으로 엔트리를 줄입니다.
/* fs/ext4/extents.c - extent 삽입 흐름 (간략화) */
int ext4_ext_insert_extent(handle_t *handle,
struct inode *inode,
struct ext4_ext_path **ppath,
struct ext4_extent *newext, int gb_flags)
{
struct ext4_ext_path *path = *ppath;
int depth = ext_depth(inode);
/* 1단계: 인접 extent와 병합 시도 */
if (ext4_ext_try_to_merge_right(inode, path, newext))
goto merge;
if (ext4_ext_try_to_merge(handle, inode, path, newext))
goto merge;
/* 2단계: 리프에 공간 있으면 직접 삽입 */
if (le16_to_cpu(path[depth].p_hdr->eh_entries) <
le16_to_cpu(path[depth].p_hdr->eh_max)) {
ext4_ext_insert(handle, inode, path, newext);
goto out;
}
/* 3단계: 공간 부족 → 노드 분할 */
ext4_ext_create_new_leaf(handle, inode, gb_flags,
ppath, newext);
/* 분할은 리프→부모→루트 방향으로 전파될 수 있음
루트 분할 시 트리 깊이 증가 (depth++) */
out:
ext4_ext_dirty(handle, inode, path + depth);
return 0;
merge:
ext4_ext_dirty(handle, inode, path + depth);
return 0;
}
Extent Status Tree (메모리 내 캐시)
커널은 extent tree의 디스크 I/O를 줄이기 위해 extent status tree를 메모리에 유지합니다. 이 red-black tree는 각 inode별로 extent의 상태(written, unwritten, delayed, hole)를 캐싱합니다.
/* fs/ext4/extents_status.h */
struct ext4_es_tree {
struct rb_root root; /* red-black tree 루트 */
struct extent_status *cache_es; /* 마지막 조회 캐시 */
};
struct extent_status {
struct rb_node rb_node;
ext4_lblk_t es_lblk; /* 논리 블록 시작 */
ext4_lblk_t es_len; /* 길이 */
ext4_fsblk_t es_pblk; /* 물리 블록 + 상태 비트 */
};
/* 상태 플래그 (es_pblk 상위 비트) */
#define ES_WRITTEN_B 0 /* 디스크에 기록 완료 */
#define ES_UNWRITTEN_B 1 /* 할당됨, 미기록 (fallocate) */
#define ES_DELAYED_B 2 /* 지연 할당 (아직 디스크 블록 없음) */
#define ES_HOLE_B 3 /* 구멍 (sparse file) */
cache_es 필드는 마지막으로 조회한 extent를 캐싱합니다. 순차 읽기/쓰기에서는 대부분 이 캐시에서 즉시 히트하여 red-black tree 탐색을 생략합니다. /proc/fs/ext4/<dev>/es_stats에서 캐시 히트율을 확인할 수 있습니다.
JBD2 커밋 흐름
JBD2(Journaling Block Device 2)의 커밋 과정은 단순한 "기록 후 플러시"가 아닙니다. 트랜잭션 상태 전이, 체크포인트(Checkpoint) 메커니즘, 복구 알고리즘까지 이해해야 ext4의 내구성 보장 방식을 정확히 파악할 수 있습니다.
트랜잭션 상태 전이 상세
JBD2 트랜잭션은 다음 8가지 상태를 순차적으로 거칩니다:
jbd2_journal_commit_transaction() 핵심 흐름
/* fs/jbd2/commit.c - 커밋 함수 핵심 단계 (간략화) */
void jbd2_journal_commit_transaction(journal_t *journal)
{
transaction_t *commit_transaction = journal->j_running_transaction;
/* Phase 0: 트랜잭션 잠금 (T_RUNNING → T_LOCKED) */
jbd2_journal_lock_updates(journal);
commit_transaction->t_state = T_LOCKED;
/* Phase 1: 새 트랜잭션 시작 (T_LOCKED → T_SWITCH → T_FLUSH) */
jbd2_journal_start_new_transaction(journal);
commit_transaction->t_state = T_FLUSH;
jbd2_journal_unlock_updates(journal);
/* Phase 2: ordered 모드 데이터 쓰기 */
if (journal->j_flags & JBD2_ORDERED) {
jbd2_journal_submit_inode_data_buffers(commit_transaction);
jbd2_journal_finish_inode_data_buffers(commit_transaction);
}
/* Phase 3: 저널 디스크립터 블록 + 메타데이터 블록 기록 */
while (bufs) {
jbd2_journal_write_metadata_buffer(...);
submit_bh(REQ_OP_WRITE, bh);
}
/* Phase 4: I/O 완료 대기 */
while (!list_empty(&io_bufs))
wait_on_buffer(bh);
/* Phase 5: 커밋 블록 기록 (T_FLUSH → T_COMMIT) */
commit_transaction->t_state = T_COMMIT;
jbd2_write_commit_record(journal, commit_transaction);
/* Phase 6: 디스크 플러시 (T_COMMIT → T_COMMIT_DFLUSH → T_COMMIT_JFLUSH) */
if (journal->j_flags & JBD2_BARRIER)
blkdev_issue_flush(journal->j_dev);
/* Phase 7: 완료 (T_FINISHED) */
commit_transaction->t_state = T_FINISHED;
jbd2_journal_done_transaction(journal, commit_transaction);
}
코드 설명
- Phase 0-1: T_RUNNING → T_LOCKED → T_FLUSH
jbd2_journal_lock_updates()로 새로운 handle 추가를 차단한 뒤 현재 트랜잭션을T_LOCKED로 전환합니다. 이어서 새 running 트랜잭션을 생성하여 후속 파일시스템 연산이 차단 없이 진행되도록 하고, 커밋 대상을T_FLUSH로 전환합니다. - Phase 2: ordered 모드 데이터
ordered모드에서는 메타데이터 저널 기록 전에 데이터 블록이 먼저 디스크에 도달해야 합니다.jbd2_journal_submit_inode_data_buffers()가 데이터 I/O를 제출하고finish가 완료를 기다려, 크래시 시 메타데이터가 존재하지 않는 데이터를 가리키는 것을 방지합니다. - Phase 3-4: 메타데이터 기록저널 디스크립터 블록(어떤 블록이 어디에 속하는지 기술)과 수정된 메타데이터 블록을 저널 영역에 기록합니다.
jbd2_journal_write_metadata_buffer()는 원본 버퍼를 복사하여 저널에 쓰므로, 원본은 다음 트랜잭션에서 계속 수정될 수 있습니다. - Phase 5-7: 커밋 및 플러시커밋 블록(시퀀스 번호 + CRC32C 체크섬)을 기록하면 해당 트랜잭션은 복구 가능한 상태가 됩니다.
JBD2_BARRIER설정 시blkdev_issue_flush()로 디스크 캐시까지 영속성을 보장합니다.T_FINISHED상태에서 트랜잭션은 체크포인트 대기 목록으로 이동합니다. 이 코드는fs/jbd2/commit.c에 위치합니다.
체크포인트 메커니즘
체크포인트는 커밋된 트랜잭션의 데이터를 원래 디스크 위치에 반영하고, 저널 공간을 재사용 가능하게 만드는 과정입니다.
| 단계 | 설명 | 트리거 조건 |
|---|---|---|
| 1. 체크포인트 대상 선택 | 가장 오래된 커밋된 트랜잭션 선택 | 저널 공간 부족, 주기적 타이머(Timer) |
| 2. 더티 버퍼 기록 | 트랜잭션의 수정된 버퍼를 원래 위치에 기록 | — |
| 3. I/O 완료 대기 | 모든 버퍼의 디스크 기록 완료 확인 | — |
| 4. 저널 헤드 전진 | j_checkpoint_transactions에서 제거, 저널 공간 해제 | — |
/* fs/jbd2/checkpoint.c - 체크포인트 (간략화) */
int jbd2_log_do_checkpoint(journal_t *journal)
{
transaction_t *transaction;
tid_t this_tid;
/* 가장 오래된 체크포인트 대상 트랜잭션 */
transaction = journal->j_checkpoint_transactions;
if (!transaction)
return 0;
this_tid = transaction->t_tid;
/* 트랜잭션의 모든 버퍼를 원래 위치에 기록 */
do {
struct buffer_head *bh = transaction->t_checkpoint_list;
if (buffer_dirty(bh))
write_dirty_buffer(bh, REQ_SYNC);
} while (bh != transaction->t_checkpoint_list);
/* I/O 완료 대기 후 트랜잭션 해제 */
__jbd2_journal_drop_transaction(journal, transaction);
return 1; /* 저널 공간 해제됨 */
}
저널 복구 알고리즘
비정상 종료 후 마운트 시 JBD2는 3단계 복구를 수행합니다. 각 단계의 목적과 revoke 레코드의 역할을 이해하는 것이 중요합니다.
/* fs/jbd2/recovery.c */
int jbd2_journal_recover(journal_t *journal)
{
int err;
recovery_info_t info;
/* Pass 1: SCAN - 유효한 트랜잭션 범위 파악
* 저널 superblock의 시퀀스 번호부터 시작하여
* 유효한 커밋 블록이 있는 마지막 트랜잭션까지 스캔 */
err = do_one_pass(journal, &info, PASS_SCAN);
/* Pass 2: REVOKE - 덮어쓴 블록 목록 수집
* revoke 레코드는 "이 블록은 이후에 다시 변경되었으니
* 오래된 저널 데이터로 복구하지 마라"는 의미 */
err = do_one_pass(journal, &info, PASS_REVOKE);
/* Pass 3: REPLAY - 커밋된 블록을 원래 위치에 복원
* revoke 목록에 없는 블록만 디스크에 재기록 */
err = do_one_pass(journal, &info, PASS_REPLAY);
/* 복구 완료: 저널 superblock 갱신 */
jbd2_journal_clear_revoke(journal);
jbd2_journal_update_sb_log_tail(journal, ...);
return err;
}
mballoc 다중 블록 할당기
ext4의 mballoc(Multi-Block Allocator)은 단일 블록 할당기인 ext3에 비해 연속 블록 할당 성능을 크게 향상시킵니다. 이 섹션에서는 buddy bitmap 구조, 그룹 디스크립터, 선할당(preallocation) 메커니즘, 그리고 정규화(normalization) 전략을 심층적으로 분석합니다.
Buddy Bitmap 구조
각 블록 그룹은 블록 비트맵(디스크)과 buddy 비트맵(메모리)을 가집니다. buddy 비트맵은 2의 거듭제곱 크기별로 연속 빈 블록을 추적합니다.
할당 컨텍스트 (ext4_allocation_context)
/* fs/ext4/mballoc.h - 할당 컨텍스트 (간략화) */
struct ext4_allocation_context {
struct inode *ac_inode; /* 할당 요청 inode */
struct super_block *ac_sb; /* 슈퍼블록 */
/* 원래 요청 */
struct ext4_free_extent ac_o_ex; /* original: 요청한 범위 */
/* 정규화된 요청 */
struct ext4_free_extent ac_g_ex; /* goal: 정규화된 범위 */
/* 최적 결과 */
struct ext4_free_extent ac_b_ex; /* best: 찾은 최적 범위 */
/* 선할당 */
struct ext4_prealloc_space *ac_pa; /* 사용된 선할당 */
__u16 ac_criteria; /* 현재 탐색 기준 (CR 0~3) */
__u16 ac_flags; /* 플래그 (EXT4_MB_HINT_*) */
__u8 ac_status; /* 할당 상태 */
};
/* ext4_free_extent - 빈 블록 범위 표현 */
struct ext4_free_extent {
ext4_lblk_t fe_logical; /* 논리 블록 시작 */
ext4_grpblk_t fe_start; /* 그룹 내 블록 오프셋 */
ext4_group_t fe_group; /* 블록 그룹 번호 */
ext4_grpblk_t fe_len; /* 연속 블록 수 */
};
CR(Criteria) 레벨별 탐색 알고리즘
/* fs/ext4/mballoc.c - ext4_mb_regular_allocator() 핵심 (간략화) */
static noinline_for_stack int
ext4_mb_regular_allocator(struct ext4_allocation_context *ac)
{
int cr;
/* 먼저 선할당 공간에서 할당 시도 */
if (ext4_mb_use_preallocated(ac))
return 0;
/* CR 0부터 CR 3까지 점진적으로 완화 */
for (cr = 0; cr < 4; cr++) {
ac->ac_criteria = cr;
/* 모든 블록 그룹을 순회 */
for (group = ac->ac_g_ex.fe_group; ...;) {
struct ext4_group_info *grp;
grp = ext4_get_group_info(sb, group);
/* CR 0: 정확한 order 매칭만 수락 */
if (cr == 0 && grp->bb_largest_free_order < ac->ac_order)
continue;
/* CR 1: 정확한 order, 전체 그룹 검색 */
/* CR 2: 빈 블록이 있으면 수락 (단편 허용) */
/* CR 3: 정규화 해제, 최소 크기만 할당 */
ext4_mb_find_by_goal(ac, grp);
if (ac->ac_status == AC_STATUS_FOUND)
break;
}
if (ac->ac_status == AC_STATUS_FOUND)
break;
}
return 0;
}
코드 설명
- ext4_mb_use_preallocated()할당 시 먼저 기존 선할당(preallocation) 공간에서 블록을 찾습니다. 이전 할당에서 남은 연속 공간이 있으면 buddy allocator를 거치지 않고 즉시 반환하여 성능을 최적화합니다.
- CR 0 (Exact Order)가장 엄격한 기준으로, inode의 목표 블록 그룹에서 요청 크기와 정확히 일치하는 2의 거듭제곱 order의 buddy를 찾습니다.
bb_largest_free_order가 요청 order보다 작으면 해당 그룹을 즉시 건너뛰어 불필요한 비트맵 로드를 방지합니다. - CR 1-2 (점진적 완화)CR 1은 정확한 order를 유지하되 모든 블록 그룹을 검색합니다. CR 2는 order 제약을 제거하고 빈 블록이 있는 그룹이면 단편화된 공간도 수락합니다. 각 그룹에서
ext4_mb_find_by_goal()로 goal 블록 근처의 최적 빈 영역을 탐색합니다. - CR 3 (최후 수단)정규화를 완전히 해제하고 요청한 최소 블록 수만 할당합니다. 디스크가 거의 가득 찬 상황에서만 도달하며, 이 레벨에서의 빈번한 할당은 심각한 단편화를 의미합니다.
fs/ext4/mballoc.c에서/proc/fs/ext4/<dev>/mb_stats를 통해 각 CR 레벨별 할당 통계를 모니터링할 수 있습니다.
/proc/fs/ext4/<dev>/mb_stats에서 각 CR 레벨별 할당 횟수, 선할당 사용 횟수, buddy 캐시 히트율 등을 확인할 수 있습니다. CR 2 이상이 빈번하면 디스크 단편화가 심한 상태입니다.
Fast Commit
Linux 5.10에서 도입된 Fast Commit은 전체 메타데이터 블록을 저널에 복사하는 기존 JBD2 방식 대신, 변경 사항(delta)만 기록하는 경량 커밋 메커니즘입니다. 이를 통해 fsync() 지연을 크게 줄이면서도 크래시 일관성을 유지합니다.
Fast Commit vs Full Commit 비교
Fast Commit 처리 흐름
/* fs/ext4/fast_commit.c - Fast Commit 주요 구조 (간략화) */
/* FC 영역: 저널의 끝에 위치하는 별도 블록들 */
struct ext4_fc_tl { /* Tag-Length 헤더 */
__le16 fc_tag; /* 태그 유형 */
__le16 fc_len; /* 데이터 길이 */
};
/* fsync() → Fast Commit 시도 */
int ext4_fc_commit(journal_t *journal, tid_t commit_tid)
{
/* 1. FC 가능 여부 확인 */
if (!ext4_fc_eligible(sbi))
return jbd2_complete_transaction(journal, commit_tid);
/* FC 불가 → Full Commit으로 폴백 */
/* 2. FC 추적 목록에서 변경된 inode들 수집 */
list_for_each_entry(ei, &sbi->s_fc_q[FC_Q_MAIN], ...) {
/* 3. 각 inode의 변경사항을 FC 태그로 직렬화 */
ext4_fc_write_inode(inode, ...);
ext4_fc_write_inode_data(inode, ...);
}
/* 4. FC TAIL 태그 기록 (CRC32 포함) */
ext4_fc_write_tail(sb, commit_tid);
/* 5. 디스크 플러시 */
blkdev_issue_flush(journal->j_dev);
return 0;
}
/* FC 불가능 조건 (Full Commit으로 폴백) */
/* - 디렉토리 rename 발생
- 지원하지 않는 연산 (truncate, xattr 변경 등)
- FC 블록 공간 부족
- FC 직렬화 중 에러 */
Fast Commit 복구
크래시 후 마운트 시, FC 복구는 일반 JBD2 복구 이후에 수행됩니다. FC 블록을 순차적으로 읽으며 태그별로 재생(replay)합니다.
/* fs/ext4/fast_commit.c - FC 복구 (간략화) */
int ext4_fc_replay(journal_t *journal, ...)
{
/* FC 블록 순회 */
while ((bh = ext4_fc_replay_next_block(journal, ...))) {
struct ext4_fc_tl *tl = buf;
switch (le16_to_cpu(tl->fc_tag)) {
case EXT4_FC_TAG_ADD_RANGE:
ext4_fc_replay_add_range(sb, tl, ...);
break;
case EXT4_FC_TAG_DEL_RANGE:
ext4_fc_replay_del_range(sb, tl, ...);
break;
case EXT4_FC_TAG_CREAT:
ext4_fc_replay_create(sb, tl, ...);
break;
case EXT4_FC_TAG_INODE:
ext4_fc_replay_inode(sb, tl, ...);
break;
case EXT4_FC_TAG_TAIL:
/* CRC32 검증 → 실패 시 이후 FC 데이터 무시 */
if (!ext4_fc_replay_check_crc(...))
return 0;
break;
}
}
return 0;
}
fast_commit feature flag로 활성화됩니다(tune2fs -O fast_commit). 이 기능이 활성화된 볼륨은 이전 버전의 e2fsck(1.46.2 이전)로 검사할 수 없습니다. 또한 data=journal 모드와는 호환되지 않습니다.
인라인 데이터 (Inline Data)
inline_data feature는 소규모 파일의 데이터를 별도 데이터 블록 대신 inode 자체에 직접 저장합니다. 이를 통해 추가 블록 할당과 디스크 I/O를 피하고, 소파일이 많은 워크로드에서 공간 효율과 성능을 크게 향상시킵니다.
인라인 데이터 저장 레이아웃
인라인 데이터 읽기/쓰기 코드
/* fs/ext4/inline.c - 인라인 데이터 읽기 (간략화) */
int ext4_read_inline_data(struct inode *inode,
void *buffer, unsigned int len,
struct ext4_iloc *iloc)
{
struct ext4_inode *raw_inode;
void *inline_start;
int cp_len, inline_size;
raw_inode = ext4_raw_inode(iloc);
/* 1단계: i_block[] 영역에서 읽기 (최대 60바이트) */
inline_start = (void *)raw_inode->i_block;
cp_len = min(len, (unsigned int)EXT4_MIN_INLINE_DATA_SIZE);
memcpy(buffer, inline_start, cp_len);
/* 2단계: xattr 영역의 추가 데이터 읽기 */
inline_size = ext4_get_inline_data_size(inode);
if (inline_size > EXT4_MIN_INLINE_DATA_SIZE) {
ext4_read_inline_data_from_xattr(inode,
buffer + EXT4_MIN_INLINE_DATA_SIZE,
inline_size - EXT4_MIN_INLINE_DATA_SIZE);
}
return inline_size;
}
/* 인라인 → 일반 파일 변환 (파일 크기 증가 시) */
int ext4_da_convert_inline_data_to_extent(
struct address_space *mapping,
struct inode *inode,
unsigned flags, struct folio **foliop)
{
/* 1. 인라인 데이터를 페이지 캐시로 복사 */
/* 2. inode에서 인라인 데이터 제거 */
/* 3. extent tree 초기화 */
/* 4. 지연 할당 예약 */
return 0;
}
코드 설명
- ext4_read_inline_data()인라인 데이터 읽기는 2단계로 진행됩니다. 먼저
ext4_raw_inode()로 디스크 inode의 원시 포인터를 얻고,i_block[]영역(60바이트)에서 첫 번째 청크를memcpy()로 복사합니다. 이 영역은 일반 파일에서 extent tree가 저장되는 곳이지만,inline_datafeature 활성 시 소형 데이터 저장소로 사용됩니다. - xattr 영역 추가 읽기인라인 데이터가 60바이트(
EXT4_MIN_INLINE_DATA_SIZE)를 초과하면system.dataxattr 영역에서 나머지를 읽습니다. inode 크기가 256바이트일 때 약 100바이트를 추가로 저장할 수 있어, 총 ~160바이트까지 별도 블록 할당 없이 데이터를 보관합니다. - ext4_da_convert_inline_data_to_extent()파일 크기가 인라인 한계를 초과하면 호출되는 변환 함수입니다. 인라인 데이터를 페이지 캐시로 복사한 뒤 inode에서 인라인 플래그를 제거하고, extent tree를 초기화하여 일반 블록 기반 저장으로 전환합니다. 이 과정은 트랜잭션 내에서 원자적으로 수행됩니다.
fs/ext4/inline.c에 정의되어 있습니다.
| 항목 | 인라인 데이터 | 일반 데이터 블록 |
|---|---|---|
| 최대 크기 | ~160바이트 (inode_size=256 기준) | 16TiB |
| 블록 할당 | 불필요 | 필요 (최소 1 블록) |
| 읽기 I/O | inode 읽기에 포함 | 추가 I/O 필요 |
| 적합한 용도 | 심볼릭 링크, 빈 파일, 설정 파일 | 일반 파일 |
| 활성화 | mkfs.ext4 -O inline_data | 기본 |
inline_data는 inode_size가 256바이트 이상이어야 사용할 수 있습니다(기본값이 256이므로 보통 조건을 만족합니다). 또한 디렉토리의 인라인 데이터도 지원되어, 엔트리가 적은 디렉토리는 별도 데이터 블록 없이 inode 내부에 저장됩니다.
bigalloc (클러스터 기반 할당)
bigalloc feature는 블록 비트맵의 할당 단위를 단일 블록(4KB)에서 클러스터(여러 블록의 그룹)로 변경합니다. 대용량 파일이 주로 저장되는 환경에서 비트맵 관리 오버헤드를 줄이고, 대규모 연속 할당 효율을 높입니다.
bigalloc 아키텍처
bigalloc 커널 내부 구현
/* fs/ext4/ext4.h - bigalloc 관련 매크로/함수 */
/* 블록 번호 → 클러스터 번호 변환 */
static inline ext4_grpblk_t
EXT4_B2C(struct ext4_sb_info *sbi, ext4_lblk_t blk)
{
return blk >> sbi->s_log_cluster_size;
}
/* 클러스터 번호 → 블록 번호 변환 (클러스터 시작) */
static inline ext4_lblk_t
EXT4_C2B(struct ext4_sb_info *sbi, ext4_grpblk_t cluster)
{
return cluster << sbi->s_log_cluster_size;
}
/* 블록 수 → 클러스터 수 변환 (올림) */
static inline ext4_grpblk_t
EXT4_NUM_B2C(struct ext4_sb_info *sbi, ext4_lblk_t blks)
{
return (blks + sbi->s_cluster_ratio - 1) >> sbi->s_log_cluster_size;
}
/* bigalloc에서 delalloc 예약 블록 관리
* 같은 클러스터에 속하는 블록은 중복 예약하지 않음 */
static int ext4_cluster_alloc(struct inode *inode,
ext4_lblk_t lblk, int num)
{
struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
ext4_lblk_t end = lblk + num - 1;
ext4_lblk_t cluster_start = EXT4_C2B(sbi, EXT4_B2C(sbi, lblk));
ext4_lblk_t cluster_end = EXT4_C2B(sbi, EXT4_B2C(sbi, end));
/* 클러스터 경계를 고려한 예약 블록 수 계산 */
return EXT4_B2C(sbi, end) - EXT4_B2C(sbi, lblk) + 1;
}
# bigalloc 파일시스템 생성
mkfs.ext4 -O bigalloc -C 65536 /dev/sda1 # 64KB 클러스터
mkfs.ext4 -O bigalloc -C 262144 /dev/sda1 # 256KB 클러스터
mkfs.ext4 -O bigalloc -C 1048576 /dev/sda1 # 1MB 클러스터
# 클러스터 크기 확인
dumpe2fs /dev/sda1 | grep -i cluster
# Cluster size: 65536
inline_data feature와 함께 사용하면 소파일의 단편화를 완화할 수 있습니다.
casefold (대소문자 무관 파일 이름)
casefold feature는 디렉토리 검색 시 유니코드 대소문자 접기(case folding)를 적용하여, "File.TXT"와 "file.txt"를 동일한 파일로 취급합니다. Wine, Samba, Android 등 Windows/macOS 호환이 필요한 환경에서 핵심적인 기능입니다.
유니코드 정규화와 대소문자 접기
casefold 커널 구현
/* fs/ext4/namei.c - casefold 검색 (간략화) */
static int ext4_ci_compare(
const struct inode *parent,
const struct qstr *name, /* 검색할 이름 */
const struct qstr *entry, /* 디렉토리 엔트리 이름 */
bool quick)
{
const struct super_block *sb = parent->i_sb;
const struct unicode_map *um = sb->s_encoding;
int ret;
/* 유니코드 case-insensitive 비교 */
ret = utf8_strncasecmp(um, name, entry);
if (ret < 0) {
/* 유니코드 처리 실패 시: strict 모드면 에러,
아니면 바이트 단위 비교로 폴백 */
if (sb_has_strict_encoding(sb))
return -EINVAL;
return ext4_ci_compare_fallback(name, entry);
}
return ret; /* 0 = 매칭, 양수 = 불일치 */
}
/* SipHash를 사용한 case-insensitive 해시
* HTree에서 casefold된 이름으로 해시를 계산 */
int ext4_fname_setup_ci_filename(
struct inode *dir,
const struct qstr *iname,
struct ext4_filename *fname)
{
const struct unicode_map *um = dir->i_sb->s_encoding;
/* casefold된 이름 생성 */
fname->cf_name.len = utf8_casefold(um, iname,
fname->cf_name.name, EXT4_NAME_LEN);
return 0;
}
# casefold 파일시스템 생성
mkfs.ext4 -O casefold /dev/sda1
# 특정 디렉토리에 casefold 활성화
chattr +F /mnt/shared
# strict 모드 (잘못된 유니코드 시퀀스 거부)
mkfs.ext4 -O casefold -E encoding_flags=strict /dev/sda1
# 유니코드 버전 확인
dumpe2fs /dev/sda1 | grep -i encoding
# Encoding: utf8-14.0
# 동작 테스트
mkdir /mnt/shared && chattr +F /mnt/shared
echo "hello" > /mnt/shared/Test.TXT
cat /mnt/shared/test.txt # "hello" 출력 (대소문자 무관)
unicode 모듈 버전에 의존합니다. 파일시스템 생성 시의 유니코드 버전이 superblock에 기록되며, 이후 커널 업그레이드로 유니코드 버전이 달라져도 하위 호환성이 유지됩니다.
ext4 커널 내부 구조
ext4 커널 코드는 fs/ext4/ 디렉토리에 약 50개의 소스 파일로 구성됩니다. 이 섹션에서는 핵심 자료구조의 관계, VFS 연결 인터페이스, 그리고 코드 모듈 구조를 심층적으로 분석합니다.
ext4 소스 코드 맵
VFS 연산 테이블
/* fs/ext4/super.c - ext4 VFS 연결 */
static const struct super_operations ext4_sops = {
.alloc_inode = ext4_alloc_inode,
.free_inode = ext4_free_in_core_inode,
.write_inode = ext4_write_inode,
.dirty_inode = ext4_dirty_inode,
.evict_inode = ext4_evict_inode,
.put_super = ext4_put_super,
.sync_fs = ext4_sync_fs,
.statfs = ext4_statfs,
.show_options = ext4_show_options,
};
/* fs/ext4/file.c - 일반 파일 연산 */
const struct file_operations ext4_file_operations = {
.llseek = ext4_llseek,
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
.mmap = ext4_file_mmap,
.open = ext4_file_open,
.release = ext4_release_file,
.fsync = ext4_sync_file,
.splice_read = ext4_file_splice_read,
.fallocate = ext4_fallocate,
};
/* fs/ext4/namei.c - 디렉토리 inode 연산 */
const struct inode_operations ext4_dir_inode_operations = {
.create = ext4_create,
.lookup = ext4_lookup,
.link = ext4_link,
.unlink = ext4_unlink,
.symlink = ext4_symlink,
.mkdir = ext4_mkdir,
.rmdir = ext4_rmdir,
.rename = ext4_rename2,
.tmpfile = ext4_tmpfile,
};
ext4 전용 slab 캐시
| 캐시 이름 | 객체 | 용도 |
|---|---|---|
ext4_inode_cache | ext4_inode_info | inode 할당 (VFS inode 내장) |
ext4_free_data | ext4_free_data | 지연 해제 블록 정보 |
ext4_allocation_context | ext4_allocation_context | mballoc 할당 컨텍스트 |
ext4_io_end | ext4_io_end | 비동기 I/O 완료 추적 |
ext4_extent_status | extent_status | extent status tree 노드 |
ext4_pending_reservation | pending_reservation | bigalloc 예약 블록 |
/* fs/ext4/super.c - slab 캐시 초기화 */
static int __init ext4_init_fs(void)
{
/* ext4_inode_info 전용 캐시 (SLAB_RECLAIM_ACCOUNT 플래그) */
ext4_inode_cachep = kmem_cache_create_usercopy(
"ext4_inode_cache",
sizeof(struct ext4_inode_info), 0,
SLAB_RECLAIM_ACCOUNT | SLAB_ACCOUNT,
offsetof(struct ext4_inode_info, i_data),
sizeof_field(struct ext4_inode_info, i_data),
ext4_inode_init_once);
/* mballoc, extent status 등 다른 캐시도 초기화 */
ext4_init_mballoc();
ext4_init_es();
/* 파일시스템 타입 등록 */
register_filesystem(&ext4_fs_type);
return 0;
}
ftrace/bpftrace ext4 성능 분석
ext4와 JBD2는 커널에 다수의 tracepoint를 제공합니다. ftrace, perf, bpftrace를 활용하면 블록 할당, 저널 커밋, extent 매핑 등의 내부 동작을 실시간(Real-time)으로 관찰하고 성능 병목을 정확히 진단할 수 있습니다.
ext4 주요 Tracepoint
| Tracepoint | 위치 | 용도 |
|---|---|---|
ext4:ext4_da_write_begin | delalloc 쓰기 시작 | 지연 할당 쓰기 빈도/크기 추적 |
ext4:ext4_da_write_end | delalloc 쓰기 완료 | 실제 기록된 바이트 수 |
ext4:ext4_writepages | writeback 시작 | 페이지 플러시 빈도/범위 |
ext4:ext4_mb_new_blocks | mballoc 블록 할당 | 할당 크기, 그룹, CR 레벨 |
ext4:ext4_request_blocks | 블록 할당 요청 | 요청 크기 vs 실제 할당 비교 |
ext4:ext4_allocate_blocks | 블록 할당 완료 | 할당된 물리 블록 위치 |
ext4:ext4_ext_map_blocks_enter | extent 매핑 진입 | 논리→물리 블록 매핑 추적 |
ext4:ext4_sync_file_enter | fsync 시작 | fsync 빈도/대상 파일 |
jbd2:jbd2_commit_logging | JBD2 커밋 시작 | 저널 커밋 빈도 |
jbd2:jbd2_end_commit | JBD2 커밋 완료 | 커밋 지연 시간 측정 |
jbd2:jbd2_checkpoint | 체크포인트 시작 | 체크포인트 빈도 |
ftrace 실전 예제
# ext4 tracepoint 목록 확인
ls /sys/kernel/debug/tracing/events/ext4/
ls /sys/kernel/debug/tracing/events/jbd2/
# mballoc 블록 할당 추적
echo 1 > /sys/kernel/debug/tracing/events/ext4/ext4_request_blocks/enable
echo 1 > /sys/kernel/debug/tracing/events/ext4/ext4_allocate_blocks/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 출력 예: ext4_request_blocks: dev=sda1 ino=12345 flags=0x20 len=8 lblk=1000
# ext4_allocate_blocks: dev=sda1 ino=12345 block=50000 len=8
# fsync 지연 시간 추적
echo 1 > /sys/kernel/debug/tracing/events/ext4/ext4_sync_file_enter/enable
echo 1 > /sys/kernel/debug/tracing/events/ext4/ext4_sync_file_exit/enable
# JBD2 커밋 추적
echo 1 > /sys/kernel/debug/tracing/events/jbd2/jbd2_commit_logging/enable
echo 1 > /sys/kernel/debug/tracing/events/jbd2/jbd2_end_commit/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 출력 예: jbd2_commit_logging: dev=sda1 transaction=42 sync=0
# jbd2_end_commit: dev=sda1 transaction=42 head=100
# 추적 비활성화
echo 0 > /sys/kernel/debug/tracing/events/ext4/enable
echo 0 > /sys/kernel/debug/tracing/events/jbd2/enable
bpftrace 실전 예제
/* bpftrace: ext4 fsync 지연 시간 히스토그램 */
#!/usr/bin/env bpftrace
tracepoint:ext4:ext4_sync_file_enter
{
@start[tid] = nsecs;
@file[tid] = args->pathname;
}
tracepoint:ext4:ext4_sync_file_exit
/@start[tid]/
{
$latency_us = (nsecs - @start[tid]) / 1000;
@fsync_lat_us = hist($latency_us);
@slow_fsync[str(@file[tid])] = count();
delete(@start[tid]);
delete(@file[tid]);
}
END
{
printf("\n=== fsync 지연 분포 (us) ===\n");
print(@fsync_lat_us);
printf("\n=== 느린 fsync 파일 Top 10 ===\n");
print(@slow_fsync, 10);
}
/* bpftrace: mballoc CR 레벨별 할당 통계 */
#!/usr/bin/env bpftrace
tracepoint:ext4:ext4_mballoc_alloc
{
@cr_level[args->cr] = count();
@alloc_size = hist(args->result_len);
@group_dist[args->result_group] = count();
}
interval:s:10
{
printf("\n--- 10초간 mballoc 통계 ---\n");
printf("CR 레벨별 할당:\n");
print(@cr_level);
printf("할당 크기 분포:\n");
print(@alloc_size);
clear(@cr_level);
clear(@alloc_size);
}
/* bpftrace: JBD2 커밋 지연과 배치 크기 추적 */
#!/usr/bin/env bpftrace
tracepoint:jbd2:jbd2_commit_logging
{
@commit_start[args->transaction] = nsecs;
}
tracepoint:jbd2:jbd2_end_commit
/@commit_start[args->transaction]/
{
$lat_ms = (nsecs - @commit_start[args->transaction]) / 1000000;
@commit_lat_ms = hist($lat_ms);
delete(@commit_start[args->transaction]);
}
tracepoint:jbd2:jbd2_checkpoint
{
@checkpoint_count = count();
}
perf trace -e 'ext4:*' -e 'jbd2:*' -- dd if=/dev/zero of=/mnt/test bs=4K count=1000 명령으로 파일 쓰기 시 ext4/JBD2의 모든 tracepoint를 한 번에 관찰할 수 있습니다. 또한 perf stat -e 'ext4:ext4_mb_new_blocks'으로 특정 이벤트의 발생 횟수만 빠르게 확인할 수도 있습니다.
성능 튜닝: 워크로드별 최적화
ext4의 성능은 mkfs 옵션, 마운트 옵션, 저널 모드, 커널 파라미터의 조합에 의해 결정됩니다. 이 섹션에서는 워크로드별 최적 설정과 벤치마크 비교, 그리고 실제 튜닝 절차를 상세히 다룹니다.
워크로드별 최적 설정
저널 모드별 성능 특성
| 항목 | data=journal | data=ordered (기본) | data=writeback |
|---|---|---|---|
| 저널 대상 | 메타데이터 + 데이터 | 메타데이터만 | 메타데이터만 |
| 데이터 순서 | 저널에서 보장 | 커밋 전 데이터 기록 | 순서 미보장 |
| 쓰기 증폭 | 2x (데이터 이중 기록) | 1x | 1x |
| fsync 지연 | 높음 | 중간 | 낮음 |
| 순차 쓰기 | 가장 느림 | 중간 | 가장 빠름 |
| 랜덤 쓰기 | 느림 (data 저널링) | 중간 | 빠름 |
| 크래시 안전성 | 최고 (데이터 복구 가능) | 높음 (stale 데이터 방지) | 낮음 (stale 데이터 노출 가능) |
| 권장 용도 | 회계/금융 DB | 범용 서버 | 빌드 서버, 임시 데이터 |
벤치마크 비교 가이드
# fio: 저널 모드별 랜덤 쓰기 성능 비교
# data=ordered
mount -o remount,data=ordered /mnt/test
fio --name=rw_test --directory=/mnt/test \
--rw=randwrite --bs=4k --size=1G \
--numjobs=4 --runtime=60 --time_based \
--ioengine=libaio --iodepth=32 \
--group_reporting --output=ordered.json --output-format=json
# data=writeback
mount -o remount,data=writeback /mnt/test
fio --name=rw_test --directory=/mnt/test \
--rw=randwrite --bs=4k --size=1G \
--numjobs=4 --runtime=60 --time_based \
--ioengine=libaio --iodepth=32 \
--group_reporting --output=writeback.json --output-format=json
# fsync 지연 시간 측정
fio --name=fsync_test --directory=/mnt/test \
--rw=write --bs=4k --size=100M \
--fsync=1 --ioengine=sync \
--group_reporting
# dbench (파일서버 시뮬레이션)
dbench -D /mnt/test 8 -t 60
# 결과 비교: IOPS, BW, lat(avg/p99/p999)
커널 파라미터 튜닝
| 파라미터 | 기본값 | 권장값 | 설명 |
|---|---|---|---|
vm.dirty_ratio | 20 | 10~40 | 전체 메모리 중 더티 페이지(Dirty Page) 비율 상한 (동기 쓰기 시작) |
vm.dirty_background_ratio | 10 | 5~20 | 백그라운드 writeback 시작 비율 |
vm.dirty_expire_centisecs | 3000 | 1500~6000 | 더티 페이지 만료 시간 (cs) |
vm.dirty_writeback_centisecs | 500 | 100~1000 | writeback 스레드(Thread) 깨우기(Wakeup) 간격 (cs) |
vm.vfs_cache_pressure | 100 | 50~200 | inode/dentry 캐시 회수 압력 |
# DB 서버: 더티 페이지 적게 유지 (빠른 플러시)
sysctl -w vm.dirty_ratio=5
sysctl -w vm.dirty_background_ratio=2
sysctl -w vm.dirty_expire_centisecs=1500
# 파일 서버: 더티 페이지 많이 허용 (배치 효율)
sysctl -w vm.dirty_ratio=40
sysctl -w vm.dirty_background_ratio=10
sysctl -w vm.dirty_expire_centisecs=6000
# ext4 전용 sysfs 튜닝
# 예약 블록 비율 변경 (기본 5%)
tune2fs -m 1 /dev/sda1 # 대용량 볼륨에서 공간 절약
# mballoc 통계 확인
cat /proc/fs/ext4/sda1/mb_stats
# mballoc:
# reqs: 12345 (groups_scanned: 678)
# CR 0 hits: 11000 (89.1%)
# CR 1 hits: 800 (6.5%)
# CR 2 hits: 500 (4.1%)
# CR 3 hits: 45 (0.4%)
# 실시간 모니터링
watch -n 1 "cat /sys/fs/ext4/sda1/delayed_allocation_blocks"
data=writeback과 barrier=0는 성능을 크게 높이지만 크래시 안전성을 희생합니다. 프로덕션 환경에서는 항상 barrier=1을 유지하고, 데이터 안전성이 보장되는 범위 내에서만 튜닝하세요.
쓰기 콜 체인
파일에 데이터를 기록할 때 VFS의 write_iter 훅에서 출발하여 블록 할당, 저널 트랜잭션, 페이지 캐시 완료까지 이어지는 전체 경로를 분석합니다. 각 단계에서 호출되는 함수와 그 역할을 순서대로 살펴봅니다.
콜 체인 전체 구조
ext4_file_write_iter() 분석
VFS의 write_iter 진입점(Entry Point)으로, 직접 I/O(Direct I/O)와 버퍼 I/O 경로로 분기합니다:
/* fs/ext4/file.c */
static ssize_t
ext4_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct inode *inode = file_inode(iocb->ki_filp);
/* 1. Direct I/O 경로 분기 */
if (iocb->ki_flags & IOCB_DIRECT) {
if (ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS))
return ext4_dio_write_iter(iocb, from);
}
/* 2. 버퍼 쓰기: inode_lock 획득 후 generic_perform_write 호출 */
inode_lock(inode);
ret = generic_perform_write(iocb, from);
inode_unlock(inode);
/* 3. O_SYNC 또는 IS_SYNC(inode) 이면 즉시 fsync */
if (iocb_is_dsync(iocb))
ret = ext4_sync_file(iocb->ki_filp, 0, 0, 0);
return ret;
}
코드 설명
- 4행
file_inode()로kiocb에서 VFS inode를 추출합니다. - 7행
IOCB_DIRECT플래그가 설정되면 직접 I/O 경로(ext4_dio_write_iter)로 분기합니다. 직접 I/O는 페이지 캐시를 우회하여 블록 디바이스에 직접 기록합니다. - 13행
inode_lock()으로 쓰기 직렬화를 보장한 뒤generic_perform_write()를 호출합니다. 이 함수 내부에서address_space_operations.write_begin/write_end가 페이지 단위로 반복 호출됩니다. - 18행
iocb_is_dsync()가 참이면(O_DSYNC,O_SYNC, 또는 inode에S_SYNC설정) 쓰기 완료 후 즉시ext4_sync_file()을 호출하여 저널 커밋 및 디스크 플러시를 수행합니다.
ext4_write_begin() 분석
address_space_operations.write_begin에 등록된 함수로, 페이지 캐시 folio를 준비하고 JBD2 트랜잭션 handle을 시작합니다:
/* fs/ext4/inode.c */
static int ext4_write_begin(struct file *file,
struct address_space *mapping,
loff_t pos, unsigned len,
struct folio **foliop, void **fsdata)
{
struct inode *inode = mapping->host;
handle_t *handle;
int needed_blocks;
/* 1. 필요한 저널 블록 수 계산
* (메타데이터: extent tree, inode, superblock 등) */
needed_blocks = ext4_writepage_trans_blocks(inode);
/* 2. JBD2 handle 시작 (트랜잭션에 참여) */
handle = ext4_journal_start(inode, EXT4_HT_WRITE_PAGE,
needed_blocks);
if (IS_ERR(handle))
return PTR_ERR(handle);
/* 3. 인라인 데이터 여부 확인 후 folio 준비 */
if (ext4_has_inline_data(inode))
return ext4_write_inline_data_begin(inode, file,
mapping, pos, len, foliop, fsdata);
/* 4. 일반 경로: grab_cache_folio_write_begin()으로
* 페이지 캐시 folio를 잠금 상태로 획득/생성 */
ret = grab_cache_folio_write_begin(mapping, index);
if (ret) { ext4_journal_stop(handle); return ret; }
/* 5. 필요 시 folio에 버퍼 헤드(buffer_head) 연결 */
if (!folio_test_buffers(*foliop))
create_empty_buffers(*foliop, inode->i_sb->s_blocksize, 0);
*fsdata = (void *)handle;
return 0;
}
코드 설명
- 12행
ext4_writepage_trans_blocks()는 단일 페이지를 기록하는 데 필요한 저널 크레딧(journal credit) 수를 계산합니다. extent tree 분할, inode 갱신, 비트맵 갱신 등을 포함하며 일반적으로 8~12 크레딧입니다. - 15행
ext4_journal_start()는 내부적으로jbd2_journal_start()를 호출하여 현재 실행 중인 트랜잭션에 handle을 참여시킵니다. 실행 중인 트랜잭션이 없으면 새로 시작합니다. - 21행
ext4_has_inline_data()가 참이면 inode 내부 저장 경로(ext4_write_inline_data_begin)로 분기합니다. 이 경우 별도 데이터 블록 없이 inode의i_block[]에 데이터를 직접 기록합니다. - 27행
grab_cache_folio_write_begin()은 페이지 캐시에서 해당 인덱스의 folio를 가져오거나 새로 할당합니다. 반환된 folio는 잠긴(locked) 상태이므로 다른 쓰기가 동시에 접근할 수 없습니다. - 35행handle 포인터를
fsdata로 반환합니다. 이후ext4_da_write_end()에서fsdata를 통해 동일한 handle을 참조하여 트랜잭션을 완료합니다.
ext4_inode_info 구조체
VFS struct inode를 내장하는 ext4 전용 메모리 내 inode 표현입니다. container_of() 패턴으로 VFS inode로부터 획득합니다:
/* fs/ext4/ext4.h — 주요 필드 선별 */
struct ext4_inode_info {
__le32 i_data[15]; /* extent root 또는 간접 블록 포인터 */
__u32 i_flags; /* EXT4_EXTENTS_FL, EXT4_INLINE_DATA_FL 등 */
ext4_lblk_t i_dir_start_lookup; /* HTree 검색 힌트 (마지막 검색 위치) */
struct ext4_es_tree i_es_tree; /* 메모리 내 extent status red-black tree */
struct ext4_es_stats i_es_stats; /* extent status tree 통계 */
struct ext4_pending_tree i_pending_tree; /* bigalloc 보류 예약 트리 */
unsigned int i_reserved_data_blocks; /* delalloc 예약 블록 수 */
tid_t i_sync_tid; /* 마지막 동기화 트랜잭션 ID */
tid_t i_datasync_tid; /* 마지막 데이터 동기화 트랜잭션 ID */
struct list_head i_fc_list; /* Fast Commit 추적 목록 연결 */
ext4_lblk_t i_fc_lblk_start; /* Fast Commit 변경 범위 시작 블록 */
ext4_lblk_t i_fc_lblk_len; /* Fast Commit 변경 범위 길이 */
struct rw_semaphore i_data_sem; /* 데이터/extent 읽기-쓰기 세마포어 */
struct rw_semaphore i_mmap_sem; /* mmap 영역 보호 세마포어 */
struct inode vfs_inode; /* 내장된 VFS inode (항상 마지막 필드) */
};
코드 설명
- 3행
i_data[15]는 온디스크 inode의i_block[15]와 대응합니다.EXT4_EXTENTS_FL플래그가 설정되면 이 배열이 extent tree의 root 노드(header + idx/extent)로 해석됩니다. - 4행
i_flags에는EXT4_EXTENTS_FL(extent 사용),EXT4_INLINE_DATA_FL(인라인 데이터),EXT4_JOURNAL_DATA_FL(data=journal 모드),EXT4_NOATIME_FL등이 포함됩니다. - 7행
i_es_tree는 디스크 extent tree의 메모리 내 캐시입니다.WRITTEN,UNWRITTEN,DELAYED,HOLE상태를 red-black tree로 관리합니다. - 11행
i_reserved_data_blocks는 지연 할당(delayed allocation) 예약 블록 수입니다.write()시 블록을 즉시 할당하지 않고 이 카운터를 증가시켜 예약하며, writeback 시 실제 블록 할당이 이루어집니다. - 13~14행
i_sync_tid/i_datasync_tid는fsync()/fdatasync()시 어느 트랜잭션까지 커밋을 기다려야 하는지를 판단하는 데 사용됩니다. - 20행
i_data_sem은 extent tree와 delalloc 예약을 보호하는 읽기-쓰기 세마포어(Semaphore)입니다. 읽기 경로는 읽기 잠금, 쓰기 경로는 쓰기 잠금을 획득합니다. - 23행
vfs_inode는 반드시 구조체의 마지막 필드여야 합니다.EXT4_I(inode)매크로가container_of(inode, struct ext4_inode_info, vfs_inode)로 구현되기 때문입니다.
ext4_extent 구조체 필드 분석
extent tree의 리프 노드에 저장되는 12바이트 자료구조로, 연속된 물리 블록 범위를 표현합니다:
/* fs/ext4/ext4_extents.h — 리프 노드 extent */
struct ext4_extent {
__le32 ee_block; /* 논리 블록 번호 시작 (파일 내 오프셋) */
__le16 ee_len; /* 연속 블록 수 (최대 32768;
* MSB=1이면 미초기화(unwritten) extent) */
__le16 ee_start_hi; /* 물리 블록 번호 상위 16비트 (48비트 주소) */
__le32 ee_start_lo; /* 물리 블록 번호 하위 32비트 */
};
/* 물리 블록 번호 = (ee_start_hi << 32) | ee_start_lo
* 최대 주소 범위: 2^48 블록 × 4KB = 1 EiB */
/* 미초기화(unwritten) extent 여부 확인/설정 */
static inline int ext4_ext_is_unwritten(struct ext4_extent *ext)
{
return le16_to_cpu(ext->ee_len) > EXT_INIT_MAX_LEN;
}
static inline void ext4_ext_mark_unwritten(struct ext4_extent *ext)
{
ext->ee_len |= cpu_to_le16(EXT_INIT_MAX_LEN);
}
/* ee_len의 실제 블록 수 (MSB 마스킹) */
static inline unsigned ext4_ext_get_actual_len(struct ext4_extent *ext)
{
return (le16_to_cpu(ext->ee_len) <= EXT_INIT_MAX_LEN) ?
le16_to_cpu(ext->ee_len) :
(le16_to_cpu(ext->ee_len) - EXT_INIT_MAX_LEN);
}
코드 설명
- 3행
ee_block은 파일 내 논리 블록 오프셋입니다. 4KB 블록 기준으로 최대 2^32 블록 = 16TiB 파일을 표현할 수 있습니다. - 4~5행
ee_len의 최상위 비트(MSB, bit 15)는 미초기화 extent 여부를 나타냅니다. MSB=0이면 초기화된 extent(실제 데이터 존재), MSB=1이면 미초기화 extent(fallocate()로 할당됐지만 데이터 없음)입니다. 실제 길이는 하위 15비트이므로 최대 32767 블록(약 128MB/4KB 블록) 연속 매핑이 가능합니다. - 6~7행
ee_start_hi와ee_start_lo를 조합하여 48비트 물리 블록 번호를 구성합니다. 4KB 블록 기준 최대 2^48 × 4KB = 1 EiB 볼륨을 지원합니다. - 14행
EXT_INIT_MAX_LEN은 32768(0x8000)로 정의됩니다.ee_len이 이 값보다 크면 MSB가 설정된 것이므로 미초기화 extent입니다. - 18~21행
ext4_ext_mark_unwritten()은ee_len에EXT_INIT_MAX_LEN(0x8000)을 OR 연산하여 MSB를 1로 설정합니다.fallocate(FALLOC_FL_KEEP_SIZE)구현 시 사용됩니다. - 24~28행
ext4_ext_get_actual_len()은 MSB 여부에 따라 실제 블록 수를 반환합니다. 미초기화이면EXT_INIT_MAX_LEN을 빼서 순수 블록 수만 반환합니다.
ext4_ext_map_blocks() 상세 분석
논리 블록 번호를 물리 블록 번호로 변환하는 핵심 함수입니다. 캐시 히트, 기존 extent 매핑, 신규 블록 할당을 모두 처리합니다:
/* fs/ext4/extents.c */
int ext4_ext_map_blocks(handle_t *handle, struct inode *inode,
struct ext4_map_blocks *map, int flags)
{
struct ext4_ext_path *path = NULL;
struct ext4_extent newex, *ex;
ext4_fsblk_t newblock = 0;
int err = 0, depth;
/* 1. extent status cache 조회 (메모리 내 캐시 우선) */
if (ext4_es_lookup_extent(inode, map->m_lblk, NULL, &es)) {
if (ext4_es_is_written(&es) || ext4_es_is_unwritten(&es)) {
map->m_pblk = ext4_es_pblock(&es) +
map->m_lblk - es.es_lblk;
goto out; /* 캐시 히트 → 디스크 탐색 불필요 */
}
if (ext4_es_is_delayed(&es))
goto delayed; /* 지연 할당 상태 → 블록 미할당 */
}
/* 2. 디스크 extent tree 탐색 */
path = ext4_find_extent(inode, map->m_lblk, NULL, 0);
if (IS_ERR(path)) { err = PTR_ERR(path); goto out; }
depth = ext_depth(inode);
ex = path[depth].p_ext;
/* 3. 리프에서 논리 블록을 포함하는 extent 확인 */
if (ex && in_range(map->m_lblk, le32_to_cpu(ex->ee_block),
ext4_ext_get_actual_len(ex))) {
newblock = ext4_ext_pblock(ex) +
(map->m_lblk - le32_to_cpu(ex->ee_block));
map->m_flags |= EXT4_MAP_MAPPED;
goto out_cache; /* 매핑 성공 → 결과 캐싱 후 반환 */
}
/* 4. 매핑 없음: CREATE 플래그이면 새 블록 할당 */
if (flags & EXT4_GET_BLOCKS_CREATE) {
newblock = ext4_mb_new_blocks(handle,
&(struct ext4_allocation_request){
.inode = inode,
.logical = map->m_lblk,
.goal = ext4_inode_to_goal_block(inode),
.len = map->m_len,
}, &err);
if (!err)
ext4_ext_insert_extent(handle, inode, &path,
&newex, flags);
}
out_cache:
ext4_es_insert_extent(inode, map->m_lblk, map->m_len,
newblock, EXTENT_STATUS_WRITTEN);
out:
ext4_ext_drop_refs(path);
map->m_pblk = newblock;
return err ? err : map->m_len;
}
코드 설명
- 10~17행
ext4_es_lookup_extent()는 메모리 내 extent status red-black tree를 먼저 조회합니다. 캐시 히트 시 디스크 I/O 없이 바로 결과를 반환하여 성능을 크게 향상시킵니다.DELAYED상태이면 아직 물리 블록이 없으므로 다른 경로로 분기합니다. - 21행
ext4_find_extent()는 extent tree의 root부터 B+tree 탐색을 시작하여 논리 블록m_lblk를 포함하는 리프 노드를 찾습니다. 경로(path) 배열에는 각 깊이별 블록 포인터와 헤더가 저장됩니다. - 29~33행
in_range()로 리프의 extent가 요청한 논리 블록을 포함하는지 확인합니다. 포함되면ee_block과의 오프셋 차이로 물리 블록 번호를 계산합니다. 예: 논리 블록 50, extent 범위 40~100, 물리 시작 1000 → 물리 블록 = 1000 + (50-40) = 1010. - 38~45행
EXT4_GET_BLOCKS_CREATE플래그가 있을 때만 신규 블록을 할당합니다. 이 플래그 없이 매핑이 없으면 hole(m_flags |= EXT4_MAP_HOLE)로 처리됩니다.goal은 지역성(locality) 향상을 위한 선호 블록 그룹 힌트입니다. - 49~50행성공적으로 매핑된 결과를 extent status cache에 삽입(
ext4_es_insert_extent)하여 다음 조회에서 캐시 히트가 되도록 합니다.
ext4_mb_new_blocks() 상세 분석
mballoc(Multi-Block Allocator)의 진입점으로 블록 그룹 선택, 정규화, buddy 탐색을 거쳐 실제 블록을 할당합니다:
/* fs/ext4/mballoc.c */
ext4_fsblk_t ext4_mb_new_blocks(handle_t *handle,
struct ext4_allocation_request *ar, int *errp)
{
struct ext4_allocation_context *ac;
struct super_block *sb = ar->inode->i_sb;
ext4_fsblk_t block = 0;
/* 1. 할당 컨텍스트 초기화 */
ac = kmem_cache_zalloc(ext4_ac_cachep, GFP_NOFS);
if (!ac) { *errp = -ENOMEM; return 0; }
/* 2. 요청 크기 정규화 (2^N 단위로 올림) */
ext4_mb_normalize_request(ac, ar);
/* 3. 선할당(preallocation)에서 우선 검색 */
if (ext4_mb_use_preallocated(ac)) {
block = ext4_grp_offs_to_block(sb, &ac->ac_b_ex);
goto out;
}
/* 4. CR 0~3 순으로 buddy allocator 탐색 */
ext4_mb_regular_allocator(ac);
if (ac->ac_status != AC_STATUS_FOUND) {
*errp = -ENOSPC;
goto out;
}
/* 5. 실제 비트맵에서 블록 마킹 + 저널에 기록 */
ext4_mb_mark_diskspace_used(ac, handle, reserv_clust);
/* 6. 남은 선할당 공간 등록 (다음 할당에 재사용) */
ext4_mb_put_pa(ac, sb, &ac->ac_b_ex);
block = ext4_grp_offs_to_block(sb, &ac->ac_b_ex);
out:
kmem_cache_free(ext4_ac_cachep, ac);
return block;
}
코드 설명
- 10행
ext4_allocation_context는 slab 캐시에서 할당됩니다. 요청 정보(원본·정규화·최적 범위), 사용된 선할당, 탐색 기준(CR 레벨) 등을 모두 담는 컨텍스트 객체입니다. - 13행
ext4_mb_normalize_request()는 요청 블록 수를 2의 거듭제곱 단위로 올림합니다. 예: 3블록 요청 → 4블록으로 정규화. 이를 통해 buddy 비트맵의 2^N 단위 관리와 일치시켜 단편화를 방지합니다. - 16행
ext4_mb_use_preallocated()는 inode별 선할당(per-inode PA)과 그룹별 선할당(per-group PA)을 순서대로 검색합니다. 선할당에서 할당 가능하면 buddy allocator 탐색을 건너뛰어 오버헤드를 줄입니다. - 22행
ext4_mb_regular_allocator()는 CR 0(목표 블록 그룹에서 정확한 order 매칭)부터 CR 3(정규화 해제, 최소 크기)까지 점진적으로 조건을 완화하며 블록을 탐색합니다. CR 값이 클수록 단편화 가능성이 높아지지만 할당 성공률은 높아집니다. - 30행
ext4_mb_mark_diskspace_used()는 선택된 블록 범위를 블록 비트맵에 마킹하고, JBD2 트랜잭션에 비트맵 블록 수정을 기록합니다. 이 단계 이후에 크래시가 발생해도 저널 복구로 비트맵을 복원할 수 있습니다.
jbd2_journal_commit_transaction() 상세 분석
JBD2의 커밋 함수는 단순한 플러시가 아닌 8단계 상태 전이를 거치며, ordered 모드의 데이터 순서 보장과 barrier 기반 영속성(Durability) 보장을 모두 담당합니다:
/* fs/jbd2/commit.c — 핵심 단계 (간략화) */
void jbd2_journal_commit_transaction(journal_t *journal)
{
transaction_t *commit_transaction;
struct buffer_head *descriptor, *wbuf[JBD2_NR_BATCH];
int bufs = 0;
/* Phase 0: T_RUNNING → T_LOCKED
* 새로운 handle이 현재 트랜잭션에 참여하지 못하도록 잠금 */
jbd2_journal_lock_updates(journal);
commit_transaction = journal->j_running_transaction;
commit_transaction->t_state = T_LOCKED;
/* Phase 1: T_LOCKED → T_SWITCH
* 새 running transaction 시작 (이후 handle은 새 트랜잭션에 참여) */
jbd2_journal_start_new_transaction(journal);
commit_transaction->t_state = T_FLUSH;
jbd2_journal_unlock_updates(journal);
/* Phase 2: T_FLUSH — ordered 모드 데이터 I/O 제출
* 메타데이터 저널 기록 전 데이터 블록을 먼저 디스크에 기록 */
if (journal->j_flags & JBD2_ORDERED) {
jbd2_journal_submit_inode_data_buffers(commit_transaction);
jbd2_journal_finish_inode_data_buffers(commit_transaction);
}
/* Phase 3~4: 저널 디스크립터 블록 + 메타데이터 블록 기록 */
while (!list_empty(&commit_transaction->t_buffers)) {
jbd2_journal_write_metadata_buffer(commit_transaction,
jh, &wbuf[bufs++], blocknr);
if (bufs == JBD2_NR_BATCH)
jbd2_write_block_tags(journal, descriptor, bufs, wbuf);
}
/* Phase 5: T_FLUSH → T_COMMIT
* 커밋 블록(시퀀스 번호 + CRC32C) 기록 */
commit_transaction->t_state = T_COMMIT;
jbd2_write_commit_record(journal, commit_transaction);
/* Phase 6: T_COMMIT → T_COMMIT_DFLUSH → T_COMMIT_JFLUSH
* JBD2_BARRIER 플래그 시 blkdev_issue_flush()로
* 디스크 내부 캐시까지 전달 보장 */
if (journal->j_flags & JBD2_BARRIER) {
blkdev_issue_flush(journal->j_dev);
commit_transaction->t_state = T_COMMIT_JFLUSH;
}
/* Phase 7: T_FINISHED
* 완료된 트랜잭션을 j_checkpoint_transactions로 이동 */
commit_transaction->t_state = T_FINISHED;
jbd2_journal_done_transaction(journal, commit_transaction);
}
코드 설명
- 9~12행
jbd2_journal_lock_updates()는 현재 실행 중인 트랜잭션에 새 handle이 참여하지 못하도록 차단합니다. 이미 참여 중인 handle이 모두 완료(jbd2_journal_stop()호출)될 때까지 대기합니다. - 15~18행새 running transaction을 시작하여 이후의 쓰기 연산이 지연 없이 새 트랜잭션에 참여할 수 있도록 합니다. 이것이 JBD2의 "트랜잭션 파이프라인(Pipeline)"의 핵심 — 커밋 중에도 새 연산이 다음 트랜잭션에 쌓입니다.
- 22~25행
ordered모드에서 가장 중요한 단계입니다. 메타데이터를 저널에 기록하기 전에 데이터 블록을 원래 위치에 먼저 기록합니다. 이를 통해 크래시 후 저널 재생 시 메타데이터 포인터가 가리키는 블록에 항상 유효한 데이터가 있음을 보장합니다. - 28~33행수정된 메타데이터 버퍼를 저널 영역에 기록합니다.
JBD2_NR_BATCH(= 64) 단위로 묶어 배치 제출함으로써 I/O 오버헤드를 줄입니다. 각 메타데이터 블록은 저널의 디스크립터 블록에 원본 위치(block number) 태그와 함께 기록됩니다. - 37~38행커밋 블록에는 트랜잭션 시퀀스 번호와 CRC32C 체크섬이 포함됩니다. 이 블록이 디스크에 기록된 순간부터 해당 트랜잭션은 크래시 후 복구 가능한 상태가 됩니다.
- 43~45행
blkdev_issue_flush()는 스토리지 디바이스의 내부 쓰기 캐시를 비우도록 요청합니다. HDD에서는 회전 플래터까지, SSD에서는 플래시 메모리까지 데이터가 도달하도록 보장합니다.mount -o barrier=0이면 이 단계를 건너뛰어 성능은 향상되지만 전원 손실 시 데이터 손실 위험이 생깁니다. - 49~50행
T_FINISHED상태가 된 트랜잭션은 체크포인트 목록(j_checkpoint_transactions)으로 이동합니다. 체크포인터가 메타데이터를 원래 위치에 기록 완료하면 저널 공간이 재사용 가능해집니다.
ext4_da_write_end() 분석
address_space_operations.write_end에 등록된 함수로, 사용자 데이터 복사 완료 후 JBD2 handle을 종료하고 지연 할당 상태를 기록합니다:
/* fs/ext4/inode.c */
static int ext4_da_write_end(struct file *file,
struct address_space *mapping,
loff_t pos, unsigned len, unsigned copied,
struct folio *folio, void *fsdata)
{
struct inode *inode = mapping->host;
handle_t *handle = (handle_t *)fsdata; /* write_begin에서 전달받은 handle */
int ret;
/* 1. 인라인 데이터 경로 분기 */
if (ext4_has_inline_data(inode)) {
ret = ext4_write_inline_data_end(inode, pos, len,
copied, folio);
goto out;
}
/* 2. folio 업데이트 및 아이노드 크기 갱신 */
block_write_end(file, mapping, pos, len, copied, folio, fsdata);
if (pos + copied > inode->i_size)
ext4_update_isize(inode, pos + copied, handle);
/* 3. 지연 할당: folio를 dirty로 표시하고 예약 블록 증가
* 이 시점에서는 실제 물리 블록이 할당되지 않음 */
folio_mark_dirty(folio);
ext4_da_update_reserve_space(inode, copied, 0);
out:
/* 4. folio 잠금 해제 및 참조 카운트 감소 */
folio_unlock(folio);
folio_put(folio);
/* 5. JBD2 handle 종료 (트랜잭션 참여 해제) */
ret = ext4_journal_stop(handle);
return ret ? ret : copied;
}
코드 설명
- 8행
fsdata포인터는ext4_write_begin()에서 설정한 JBD2 handle 포인터입니다. 동일 handle을 사용하여 begin/end가 같은 트랜잭션에 묶임을 보장합니다. - 18행
block_write_end()는 VFS 공통 함수로, folio의 buffer_head 상태를 갱신하고 실제로 복사된 바이트 수를 반환합니다. 부분 복사(partial copy) 시copied < len이 될 수 있습니다. - 19~20행파일 끝을 넘어서는 쓰기라면
i_size를 갱신합니다.ext4_update_isize()는 inode의 크기 필드를 JBD2 트랜잭션 내에서 안전하게 갱신합니다. - 24행
folio_mark_dirty()는 페이지 캐시의 folio를 dirty 상태로 표시합니다. writeback 스레드가 나중에 이 folio를 감지하여ext4_writepages()를 통해 mballoc으로 실제 블록을 할당하고 디스크에 기록합니다. - 25행
ext4_da_update_reserve_space()는 지연 할당 예약 카운터를 업데이트합니다. 이미 예약된 블록이 충분하면 증가 없이 진행하고, 부족하면 추가 예약을 시도합니다. - 32행
ext4_journal_stop()은 handle의 참조 카운트(Reference Count)를 감소시키고, 이 트랜잭션에 참여한 마지막 handle이면 커밋 조건을 검사합니다.commit=N마운트 옵션에 따른 주기적 커밋과 저널 공간 부족 시 즉시 커밋이 여기서 트리거됩니다.
ext4_file_write_iter() → ext4_write_begin()(JBD2 handle 시작) → 사용자 데이터 복사 → ext4_da_write_end()(더티 마킹 + handle 종료) 순으로 진행됩니다. 실제 블록 할당(ext4_ext_map_blocks + ext4_mb_new_blocks)은 writeback 시점에 수행되며, 메타데이터 변경은 JBD2 트랜잭션으로 원자적으로 보호됩니다.
참고자료
커널 공식 문서
- ext4 Data Structures and Algorithms (kernel.org) — ext4 디스크 레이아웃, 자료구조, 알고리즘을 다루는 공식 커널 문서입니다
- ext4 General Information (kernel.org) — 마운트 옵션, sysfs 인터페이스 등 관리자 가이드입니다
- Journalling API (kernel.org) — JBD2 저널링 계층의 커널 API 문서입니다
- fscrypt — Filesystem-level Encryption (kernel.org) — ext4/f2fs에서 사용하는 파일시스템 수준 암호화 문서입니다
- fs-verity (kernel.org) — 파일 단위 무결성 검증 메커니즘 문서입니다
- FIEMAP ioctl (kernel.org) — Extent 매핑 조회 인터페이스 문서입니다
- ext4 Atomic Writes (kernel.org) — ext4 원자적 쓰기 기능의 공식 커널 문서입니다
LWN.net 기사
- ext4: the next generation of ext2/3 (LWN, 2006) — ext4 개발 초기의 설계 방향과 목표를 소개하는 기사입니다
- ext4 and the extents of delay allocation (LWN, 2007) — 지연 할당(delayed allocation) 구현 배경과 동작 원리를 설명합니다
- Improving ext4: bigalloc, inline data, and metadata checksums (LWN, 2011) — bigalloc, 인라인 데이터, 메타데이터 체크섬 기능의 도입 과정을 다룹니다
- Fast commits for ext4 (LWN, 2021) — Fast Commit 저널링 최적화의 설계와 성능 효과를 설명합니다
- The ext4 /dev/random fiasco (LWN, 2013) — ext4 지연 할당과 관련된 데이터 손실 이슈 사례를 분석합니다
- Filesystem-level encryption (fscrypt) (LWN, 2018) — fscrypt 아키텍처와 ext4 적용 사례를 설명합니다
- Support for atomic block writes in 6.13 (LWN, 2024) — ext4/XFS 원자적 쓰기 도입 배경과 RWF_ATOMIC 인터페이스를 설명합니다
- Atomic writes for ext4 (LWN, 2025) — bigalloc·forcealign을 통한 다중 블록 원자적 쓰기 확장을 다룹니다
- ext4: enable large folio for regular files (LWN, 2025) — ext4 large folio 활성화와 37.7% 성능 향상을 다룹니다
- ext4: optimize online defragment (LWN, 2025) — folio 기반 온라인 조각 모음 최적화 패치 논의를 담습니다
- ext4: defer unwritten splitting until I/O completion (LWN, 2026) — 동시 DIO 쓰기 성능 개선을 위한 unwritten extent 분할 지연 설계를 설명합니다
커널 소스 코드
- fs/ext4/ — ext4 파일시스템 전체 소스 디렉토리입니다
- fs/ext4/super.c — 슈퍼블록 관리 및 마운트/언마운트 처리 코드입니다
- fs/ext4/extents.c — Extent Tree B+tree 구현과 블록 매핑 코드입니다
- fs/ext4/mballoc.c — 다중 블록 할당기(mballoc) 핵심 구현입니다
- fs/ext4/inode.c — inode 관리, 읽기/쓰기 경로, 지연 할당 코드입니다
- fs/ext4/namei.c — 디렉토리 관리, htree 인덱스 구현입니다
- fs/ext4/fast_commit.c — Fast Commit 저널링 구현입니다
- fs/ext4/xattr.c — 확장 속성(xattr) 처리 코드입니다
- fs/jbd2/journal.c — JBD2 저널 관리 핵심 코드입니다
- fs/jbd2/commit.c — JBD2 트랜잭션 커밋 흐름 구현입니다
- fs/ext4/ext4.h — ext4 핵심 자료구조 정의(ext4_inode, ext4_extent 등)입니다
도구 및 유틸리티
- e2fsprogs 공식 사이트 — mke2fs, e2fsck, tune2fs, debugfs 등 ext4 관리 도구 모음입니다
- e2fsprogs Release Notes — 최신 릴리스별 추가 기능과 수정 사항을 확인할 수 있습니다
- e2fsprogs Git 저장소 (kernel.org) — e2fsprogs 소스 코드 저장소입니다
- e2fsprogs misc/ 매뉴얼 소스 — mke2fs, e2fsck, tune2fs, debugfs 매뉴얼 원문이 있는 디렉토리입니다
- util-linux fsck(8) — 다중 파일시스템 검사 시 종료 코드를 비트 OR 하는 프런트엔드 동작을 설명합니다
- Debian systemd-fsck(8) — root/
/usr/일반 마운트에 대한reboot.target,emergency.target전이 규칙을 설명합니다 - Ubuntu 25.04 systemd-fsck(8) — Ubuntu 계열의 현재 systemd-fsck 문서와 커널 파라미터 지원 범위를 확인할 수 있습니다
- Debian initramfs-tools(7) —
/run/initramfs/fsck.log,fsck-root,fsck-usr마커 파일 의미를 설명합니다 - systemd-fsckd.service(8) — boot splash 진행률 표시와 Ctrl+C 취소 동작을 설명합니다
- SLES Boot Process Guide — initramfs 단계에서 root 파일시스템 점검과 재마운트 재시도를 설명하는 공식 문서입니다
발표 및 기술 자료
- ext4 Wiki (kernel.org) — ext4 개발 로드맵, FAQ, 설계 문서를 모아둔 위키입니다
- An Empirical Study of File-System Fragmentation in Mobile Storage Systems (FAST '17) — 모바일 환경에서의 ext4 단편화 분석 논문입니다
ext4 최신 변화 요약 (v6.8~v7.0)
ext4는 atomic writes 확장, orphan_file·fast_commit 정착, folio/multigrain 타임스탬프 적용, 블록 할당 확장성 개선, 페이지보다 큰 블록 크기 지원을 중심으로 진화하고 있습니다.
Atomic Writes + bigalloc (v6.13~v6.16)
- 단일 FSB 단위 (v6.13): 기본 4KB 블록 단위 atomic write가 먼저 머지되어 NVMe AWUN이 4KB 이상이면 그대로 노출됩니다. ext4와 XFS에서 Direct I/O 경로를 통해 지원합니다.
- bigalloc 다중 블록 확장 (v6.16): bigalloc 클러스터 단위(최대 64KB)까지 다중 파일시스템 블록 원자적 쓰기가 확장되어 8KB·16KB 페이지 DB 엔진이 torn-write 방지를 커널 기능으로 사용할 수 있습니다. 원자적 쓰기 단위 최솟값은 fs 블록 크기와 하드웨어 AWUN 중 큰 값, 최댓값은 bigalloc 클러스터 크기와 하드웨어 최댓값 중 작은 값으로 결정됩니다.
- 마운트·mkfs:
mkfs.ext4 -O bigalloc -C 16384로 16KB 클러스터를 구성하고,statx로atomic_write_unit_max를 확인하여 런타임 결정이 가능합니다.
orphan_file 기본화 & fast_commit (v6.8~v6.12)
- orphan_file: 기존 superblock 기반 orphan inode 연결 리스트의 락 경합(Contention)을 제거한 별도 파일 구조가
mkfs.ext4기본 옵션으로 승격되었습니다(-O orphan_file). - fast_commit: 2단계 플러시 구조로 full journal commit을 지연시켜, fsync 빈도가 높은 워크로드(데이터베이스·메일 큐)의 지연을 수 배 줄입니다.
- 성능 효과: 대역폭(Bandwidth) 집약 벤치마크(filebench varmail, fio 등)에서 두드러진 IOPS 증가가 관측됩니다.
multigrain TS & folio 정리 (v6.12~v6.14)
- FS_MGTIME 채택: ctime 조회 시 관찰자가 있는 경우에만 선택적으로 나노초 정밀도를 승격하여, 빌드 시스템(Build System) 타임스탬프 스큐 문제를 완화합니다.
- read_folio/write_folio 완전화: 페이지 캐시 경로가 folio 기반으로 완전히 재작성되어 large folio readahead와의 상호작용이 개선되었습니다.
- iomap 적용 확대: buffered I/O 일부 경로까지 iomap 기반으로 전환이 진행 중이며, XFS와 공유하는 iomap 인프라 비중이 점차 확대되고 있습니다.
Large Folio 정규 파일 지원 (v6.16)
- iomap 버퍼 I/O 경로 전환: ext4 정규 파일의 버퍼드 I/O 경로가 iomap 기반으로 전환되면서 large folio 할당이 가능해졌습니다. fsverity, fscrypt, data=journal 모드는 초기 단계에서 제외됩니다.
- 성능 효과: Intel 커널 테스트 로봇이 FS-Mark 벤치마크에서 37.7% 처리량 향상을 보고하였으며, 64K·1M 블록 단위의 버퍼 쓰기에서 최대 167%까지 향상이 관측되었습니다.
- fast_commit 성능 개선 (v6.16): fast commit 경로의 추가 최적화가 함께 적용되어 고빈도 fsync 워크로드에서 지연이 더욱 단축되었습니다.
오류 처리 및 신뢰성 개선 (v6.15)
- errors=remount-ro 모드 수정: 오류 모드가 errors=remount-ro로 설정된 경우 쓰기 오류 발생 시 파일시스템을 올바르게 읽기 전용으로 재마운트하도록 7개 커밋으로 수정되었습니다.
- 쓰기 오류 시 메타데이터 무결성: 데이터 쓰기 실패 상황에서 데이터 손실을 방지하고 메타데이터 일관성을 보장하는 9개 커밋이 포함되었습니다.
- sb_update 간격 조정: 슈퍼블록 갱신 주기(
sb_update)를 마운트 옵션으로 조정할 수 있게 되어 특정 워크로드에서 저널 I/O 빈도를 줄일 수 있습니다. - 디렉터리 선형 검색 개선: 소규모 디렉터리 항목 검색 경로에 선형 검색 최적화가 추가되어 htree 비활성화 환경에서 성능이 향상됩니다.
- 악의적 파일시스템 이미지 강화: 퍼징(Fuzzing)을 통해 발견된 손상된 메타데이터로 인한 커널 패닉 가능성을 차단하는 입력 검증이 강화되었습니다.
블록 할당 확장성 개선 (v6.17)
- mballoc 확장성 패치 (18개 커밋): mballoc(multi-block allocator) 코드를 전반적으로 재작업하여 컨테이너/가상화 환경에서 초당
fallocate처리량이 크게 향상되었습니다. 구체적으로 파일 단편화는 감소하고 여유 공간 단편화는 소폭 증가하는 트레이드오프가 보고되었습니다. - IOCB_DONTCACHE 지원:
write_begin/write_end경로가 리팩토링되어IOCB_DONTCACHE플래그를 지원합니다. 데이터를 페이지 캐시에 남기지 않고 쓰는 경우(예: 스트리밍 I/O)에 캐시 오염을 방지합니다. - 버퍼드 I/O 확장성: iomap 기반으로의 전환이 이 시점에 버퍼드 I/O 경로에서도 진전되어 large folio와의 상호작용이 개선되었습니다.
페이지보다 큰 블록 크기 및 온라인 조각 모음 (v6.19)
- 페이지 크기 초과 블록 지원: 파일시스템 블록 크기가 시스템 페이지 크기(보통 4KB)보다 클 수 있게 되었습니다. NVMe 등 고성능 스토리지에서 버퍼드 I/O 쓰기 성능이 최대 50% 향상되며, large folio 기반 데이터 경로와 시너지를 냅니다.
- 온라인 조각 모음 folio 최적화:
e4defrag(FITRIM/EXT4_IOC_MOVE_EXT)가 개별 buffer_head 대신 folio 단위로 작동하도록 재작성되어, large folio 환경에서 조각 모음 효율이 개선되었습니다. 관련 논의는 LWN.net 기사를 참고하세요. - mballoc per-CPU 캐싱: 블록 요청 경로에 CPU별(per-CPU) 캐시가 도입되어 다중 코어 워크로드에서 락 경합이 줄고 CPU 사용률이 개선되었습니다.
동시 DIO 쓰기 성능 및 신뢰성 (v7.0)
- Unwritten Extent 분할 지연: 동시 Direct I/O 쓰기 시 unwritten(미기록 예약) extent 분할을 쓰기 시점이 아닌 I/O 완료 시점으로 미루도록 변경되었습니다. 이를 통해 병렬 Direct I/O 처리량이 개선됩니다. 자세한 설계는 LWN.net 기사를 참고하세요.
- Extent Status Cache 불필요 무효화 방지: 읽기 요청 처리 시 extent status cache를 불필요하게 무효화하는 경우를 제거하여 캐시 히트율이 향상되었습니다.
- Delayed Allocation 강제 ordered 쓰기 방지: 파일 끝에 추가(append) 쓰기 시 지연 할당 경로에서 불필요한 강제 ordered 쓰기가 발생하지 않도록 수정하여 쓰기 지연(Latency)이 감소하였습니다.
- err_report_sec sysfs 속성:
/sys/fs/ext4/<dev>/err_report_sec속성이 추가되어 파일시스템 불일관성 경고 로그 출력 주기를 설정할 수 있습니다. 기본값은 24시간(86400초)이며, 0으로 설정하면 반복 경고를 비활성화합니다. - 언마운트 중 BUG 방지: 언마운트 과정에서 파일시스템 손상이 감지되었을 때 커널 BUG()를 발생시키지 않고 안전하게 오류를 보고하도록 개선되었습니다.
mount -o ...,dax 등 다른 경로와 상호작용을 확인하고, statx 결과와 /sys/block/<dev>/queue/atomic_write_* 값을 교차 검증하여 블록 장치 레벨 한계를 파악하는 것이 좋습니다.
관련 문서
ext4와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.