ext4 파일시스템 심화

범용 Linux 서버에서 가장 널리 쓰이는 ext4를 대상으로 온디스크 블록 그룹 구조, inode/extent 매핑, JBD2 저널 트랜잭션 상태 전이, mballoc과 delayed allocation의 공간 배치 전략, writeback/commit 지연이 성능과 내구성에 주는 영향, fscrypt·fsverity 보안 기능, 장애 복구 도구(e2fsck/tune2fs) 활용까지 실무 중심으로 상세히 다룹니다.

전제 조건: VFSBlock I/O 서브시스템 문서를 먼저 읽으세요. 디스크 기반 파일시스템은 저널링·할당기·복구 정책 차이가 핵심이므로, I/O 경로와 on-disk 구조를 함께 봐야 합니다.
일상 비유: 이 주제는 창고 배치와 재고 장부 운영과 비슷합니다. 공간 배치 규칙과 기록 정책이 달라지면 성능·복구·무결성 특성이 크게 달라집니다.

핵심 요약

  • 계층 이해 — VFS, 캐시, 하위 FS 경계를 구분합니다.
  • 메타데이터 우선 — inode/dentry 일관성을 먼저 확인합니다.
  • 저장 정책 — 저널링/압축/할당 정책 차이를 비교합니다.
  • 일관성 모델 — 로컬/원격/합성 FS의 반영 시점을 구분합니다.
  • 복구 관점 — 장애 시 재구성 경로를 함께 점검합니다.

단계별 이해

  1. 경계 계층 파악
    요청이 VFS에서 어디로 내려가는지 확인합니다.
  2. 메타/데이터 분리
    어느 경로에서 무엇이 갱신되는지 나눠 봅니다.
  3. 동기화/플러시 확인
    쓰기 반영 시점과 순서를 검증합니다.
  4. 복구 시나리오 점검
    비정상 종료 후 일관성 회복을 확인합니다.
관련 표준: POSIX.1-2017 (파일시스템 시맨틱), ext4 Documentation (kernel.org 디스크 레이아웃 문서) — ext4는 POSIX 호환 파일시스템으로 구현되어 있습니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

개요 & 역사

ext4(Fourth Extended Filesystem)는 Linux의 사실상 표준 파일시스템으로, ext2/ext3의 직접적인 후속입니다. 2006년 개발이 시작되어 Linux 2.6.28(2008)에서 안정 상태로 포함되었습니다.

ext 계열 진화

파일시스템커널 버전주요 변경
ext2 (1993)0.99Block Group 구조, BSD FFS 기반 설계
ext3 (2001)2.4.15JBD 저널링 추가 (journal / ordered / writeback)
ext4 (2008)2.6.28Extent, 48/64-bit, delalloc, mballoc, 체크섬, 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미지원 커널은 마운트 불가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 Block Group 레이아웃 Block Group 0 (128 MiB @ 4K block) Super Block Group Desc Table Reserved GDT Data Block Bitmap Inode Bitmap Inode Table Data Blocks Block Group 1 ... Block Group N (Superblock 백업은 sparse_super 그룹에만) Flex Block Group (기본 16개 BG) 메타데이터(비트맵+inode 테이블)를 첫 번째 BG에 집중 → 시크 감소

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

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;
    ...
};

Flex Block Groups

flex_bg feature는 여러 Block Group(기본 16개)의 메타데이터(비트맵 + inode 테이블)를 첫 번째 BG에 연속 배치합니다. 이를 통해:

/* 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는 연속된 물리 블록의 범위를 하나의 엔트리로 표현하여, 대용량 파일에서 메타데이터 오버헤드를 크게 줄입니다.

간접 블록 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)
단편화 영향높음 (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; /* 물리 블록 시작 (하위) */
};
inode 내장 Extent: inode의 i_block[15] 영역(60 바이트)에 extent header + 최대 4개 extent를 직접 저장합니다. 이 영역이 부족하면 외부 블록에 extent tree를 확장합니다.

Extent Tree 구조

Extent Tree (depth=1 예시) inode i_block[15] (60 bytes) header(depth=1) + idx[0] + idx[1] Leaf Block (depth=0) header + extent[0..N] Leaf Block (depth=0) header + extent[0..M] 블록 1000-1127 블록 2048-2559 블록 5000-5511 블록 8192-8703

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;
}

블록 할당

Multi-Block Allocator (mballoc)

ext4의 mballoc은 한 번의 요청으로 여러 블록을 동시에 할당하는 정교한 할당기입니다. ext3의 단일 블록 할당기에 비해 연속 할당률이 크게 향상됩니다.

할당 전략

전략설명
Per-inode Preallocation파일별 사전 할당 공간 유지 (순차 쓰기 최적화)
Per-group PreallocationBlock Group별 사전 할당 (소파일 최적화)
Buddy Allocator2의 거듭제곱 단위 블록 관리, 비트맵 기반
정규화(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의 기본 할당 전략인 delallocwrite() 시점에 블록을 할당하지 않고, writepages 시점(페이지 캐시 → 디스크 플러시)에 실제 할당합니다.

Delayed Allocation 흐름 write() 호출 (블록 할당 안함) Page Cache Dirty Pages 누적 writepages() mballoc으로 할당 연속 블록 기록 Disk I/O 장점: 1. 전체 파일 크기를 알고 할당 → 연속 블록 확보 가능 2. 덮어쓰기 후 삭제된 파일의 블록을 불필요하게 할당하지 않음 3. 다수의 소규모 쓰기를 모아 한 번에 대규모 할당 → I/O 효율 극대화
데이터 손실 주의: delalloc 상태에서 시스템 크래시가 발생하면, 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와 독립적인 커널 모듈로, 메타데이터(및 선택적으로 데이터)의 원자적 갱신을 보장합니다.

JBD2 아키텍처

구조체역할
journal_t저널 디바이스/영역을 대표. 트랜잭션 목록, 버퍼 관리
transaction_t하나의 트랜잭션. 수정된 버퍼 목록, 상태, 시퀀스 번호
handle_t개별 파일시스템 연산의 저널 핸들 (트랜잭션의 하위 단위)

트랜잭션 라이프사이클

Running handle 추가 가능 commit Committing 저널에 기록 Committed 저널 기록 완료 Checkpointing 실제 위치에 반영 체크포인트 완료 후 저널 공간 재사용 가능

저널 모드

모드저널 대상성능안전성
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가 수행하는 복구 과정:

  1. SCAN: 저널 시퀀스 번호를 따라가며 유효한 커밋 블록 탐색
  2. REVOKE: revoke 레코드를 수집하여 이후 덮어쓴 블록을 복구 대상에서 제외
  3. REPLAY: 커밋된 블록을 원래 위치에 재기록

디렉토리 구현

선형 디렉토리

엔트리 수가 적은 디렉토리는 선형 리스트로 저장됩니다:

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 변형)로 전환됩니다. 파일명의 해시값을 키로 사용하여 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 본체 내에 직접 저장하여 별도의 데이터 블록 할당을 피합니다.

확장 속성 (xattr) & ACL

xattr 저장 위치

ext4는 xattr을 두 곳에 저장합니다:

  1. inode body: inode 크기가 128보다 클 때 (기본 256), 여유 공간에 저장
  2. 외부 블록: inode body에 들어가지 않으면 별도 블록에 저장
/* xattr 엔트리 헤더 */
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 네임스페이스

네임스페이스접두어용도
useruser.일반 사용자 확장 속성
systemsystem.POSIX ACL (system.posix_acl_access)
securitysecurity.SELinux 레이블 (security.selinux)
trustedtrusted.CAP_SYS_ADMIN 필요

ea_inode (대용량 xattr)

ea_inode feature는 단일 블록(4K)을 초과하는 xattr 값을 별도의 inode에 저장합니다. 이를 통해 최대 수 MiB까지의 xattr 값을 지원합니다.

POSIX ACL

ext4는 POSIX ACL을 system.posix_acl_accesssystem.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);
}

파일시스템 암호화 (fscrypt)

fscrypt는 파일 내용, 파일명, symlink 대상을 per-file 키로 암호화합니다. 커널 4.1에서 도입되어 ext4, F2FS, UBIFS에서 지원합니다.

항목설명
암호화 대상파일 내용, 파일명, symlink target
암호화 알고리즘AES-256-XTS (내용), AES-256-CTS (파일명)
키 파생HKDF-SHA512로 마스터 키에서 per-file 키 유도
키 관리Linux 키링 서브시스템 (fscrypt_add_key)
# fscrypt 사용 예
fscrypt setup /mnt/encrypted
fscrypt encrypt /mnt/encrypted/private
# 잠금 해제
fscrypt unlock /mnt/encrypted/private

무결성 검증 (fsverity)

fsverity는 Merkle tree 기반 무결성 검증을 제공합니다. 파일을 읽기 전용으로 설정하고, 각 블록의 해시를 트리 구조로 저장하여 변조를 감지합니다.

# 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

대소문자 무관 디렉토리 (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 쿼터와 달리, 디렉토리 트리 단위로 쿼터를 적용할 수 있어 컨테이너 환경에서 유용합니다.

# 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.16+)

커널 6.16에서 ext4에 large folio 지원이 추가되었습니다. 기존에는 단일 페이지(4KB) 단위로 페이지 캐시를 관리했으나, large folio를 통해 여러 페이지를 하나의 folio로 묶어 관리합니다.

항목기존 (4KB 페이지)Large Folio (v6.16+)
페이지 캐시 단위4KB (base page)16KB~2MB (order-2 ~ order-9)
TLB 효율페이지당 1 TLB 엔트리folio당 1 TLB 엔트리 (다수 페이지 커버)
I/O 병합readahead에 의존folio 단위 자연 병합
메타데이터 오버헤드페이지당 struct pagefolio당 1개 (나머지 tail page)
bigalloc과의 시너지: ext4의 bigalloc(클러스터 할당) 기능과 large folio를 결합하면, 클러스터 크기에 맞춘 multi-fsblock atomic write가 가능해집니다. 이는 데이터베이스 페이지 크기(16KB 등)에 맞춘 원자적 기록을 지원하여, 별도의 WAL 없이 데이터 무결성을 보장할 수 있습니다.

핵심 커널 자료구조

ext4_sb_info

슈퍼블록의 메모리 내 표현으로, VFS struct super_blocks_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 구조체입니다:

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 */
    ...
};

헬퍼 매크로

/* VFS inode에서 ext4_inode_info 획득 */
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지연 할당 (기본값)순차 쓰기가 많은 워크로드
discardTRIM 명령 자동 전송SSD (또는 fstrim 크론잡 대체)
barrier=1쓰기 배리어 활성화 (기본값)데이터 무결성 중요 시
commit=N저널 커밋 간격 (초)기본 5초, 배치 성능 시 30~60
journal_async_commit비동기 커밋 블록SSD + 배리어 활성 시
max_batch_time=N트랜잭션 배치 최대 대기 (us)동시 커밋이 많을 때
dioread_nolockDirect I/O 시 inode 잠금 회피DB 워크로드 (기본 활성)
data=ordered데이터 → 메타 순서 보장기본값, 안전 + 적절한 성능
# 일반적인 성능 최적화 마운트
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-inodeinode 비율 조정 (작은 파일 많으면 줄임)
-I inode-sizeinode 크기 (기본 256, xattr 많으면 512)
-J size=N저널 크기 (MB)
-E stride=N,stripe-width=MRAID 정렬 (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 스케줄러 선택

스케줄러권장 디바이스특징
noneNVMe SSD스케줄링 오버헤드 제거, 디바이스 내부 큐에 위임
mq-deadlineSATA SSD, HDD지연 시간 보장, 기아 방지
bfq데스크탑, HDD공정성, 저지연 대화형 사용
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 도구

도구용도주요 예시
mkfs.ext4포맷mkfs.ext4 -O ^metadata_csum /dev/sda1
tune2fs기능 플래그/파라미터 변경tune2fs -O fast_commit /dev/sda1
e2fsck파일시스템 검사/복구e2fsck -f -y /dev/sda1
dumpe2fs슈퍼블록/그룹 정보 출력dumpe2fs -h /dev/sda1
debugfs대화형 디버깅debugfs /dev/sda1
resize2fs온라인/오프라인 리사이즈resize2fs /dev/sda1

debugfs 활용 예

# inode 상세 정보
debugfs -R "stat <12>" /dev/sda1

# extent tree 확인
debugfs -R "dump_extents <12>" /dev/sda1
# Level  Entries  Logical      Physical     Length  Flags
#  0/0    1/4     0-127        1000-1127    128
#  0/0    2/4     128-255      2048-2175    128

# 디렉토리 HTree 확인
debugfs -R "htree_dump /home" /dev/sda1

# 저널 내용 확인
debugfs -R "logdump -a" /dev/sda1

# 삭제된 inode 복구 (undelete)
debugfs -w /dev/sda1
debugfs: lsdel          # 삭제된 inode 목록
debugfs: dump <12> /tmp/recovered_file

tune2fs 활용 예

# 현재 feature 확인
tune2fs -l /dev/sda1 | grep "features"

# feature 활성화/비활성화
tune2fs -O fast_commit /dev/sda1      # Fast Commit 활성화
tune2fs -O ^fast_commit /dev/sda1     # Fast Commit 비활성화

# 예약 블록 비율 변경 (기본 5% → 1%)
tune2fs -m 1 /dev/sda1

# 마운트 횟수 기반 fsck 비활성화
tune2fs -c 0 -i 0 /dev/sda1

ext4 vs 다른 파일시스템 비교

항목ext4XFSBtrfsF2FS
최대 볼륨1 EiB8 EiB16 EiB16 TiB
최대 파일16 TiB8 EiB16 EiB3.94 TiB
COWXreflink 지원기본 COWX
스냅샷XXO (subvolume)X
체크섬(데이터)X (메타만)reflink 시O (전체)X
압축XXO (zstd/lzo)O (LZO/LZ4/zstd)
RAID 내장XXO (0/1/5/6/10)X
온라인 축소XXOX
온라인 확장OOOX
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에 호환성 패치가 적용되었습니다. 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 진행 ... */
}
교훈: POSIX 표준은 rename()fsync() 없이도 데이터 영속성을 보장하지 않습니다. 그러나 ext3에서 사실상 보장되었던 동작에 수많은 프로그램이 의존하고 있었습니다. 이 사건은 "사양 vs 실제 동작"의 괴리를 보여주는 대표적 사례이며, 파일시스템 변경이 사용자 공간 프로그램에 미치는 영향을 과소평가해서는 안 된다는 교훈을 남겼습니다.

2. JBD2 저널 손상과 복구

JBD2(Journaling Block Device 2)는 ext4의 저널링 엔진으로, 메타데이터(및 선택적으로 데이터)의 원자적 갱신을 보장합니다. 그러나 전원 손실이나 하드웨어 오류 시 저널 자체가 손상되는 사례가 보고되었습니다.

저널 체크섬 불일치: 전원 손실 시 저널 블록이 부분적으로 기록되면 체크섬이 불일치합니다. 초기 ext4는 저널 체크섬 기능이 없어 손상된 트랜잭션을 유효한 것으로 재생(replay)하여 파일시스템을 더 심각하게 손상시킬 수 있었습니다. 또한 저널 회전(wrapping) 중 — 저널 끝에서 처음으로 되돌아가는 시점 — 전원 손실이 발생하면 트랜잭션이 누락되거나 중복 재생되는 사례가 있었습니다.

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=journalOO (저널에 기록)낮음가장 높음
data=orderedO순서 보장중간 (기본값)높음
data=writebackOX높음낮음 (stale 데이터 노출 가능)
e2fsck 강제 복구 주의: e2fsck -f로 강제 복구를 수행할 때 저널이 손상된 상태라면, 저널 재생이 파일시스템을 추가적으로 손상시킬 수 있습니다. 심각한 저널 손상 시에는 e2fsck -f -E journal_only로 저널만 먼저 검증하거나, 최악의 경우 tune2fs -O ^has_journal로 저널을 제거한 뒤 복구를 시도해야 합니다. 반드시 복구 전 디스크 이미지 백업(dd 또는 ddrescue)을 수행하십시오.

3. Extent Tree 손상 사례

ext4의 extent tree는 파일의 물리적 블록 매핑을 B-tree 형태로 관리합니다. 대용량 파일이나 심한 단편화 상태에서 extent tree의 깊이(depth)가 증가하며, 이 과정에서 다양한 손상 사례가 보고되었습니다.

Extent Tree 주요 손상 유형:
  • Depth 오류: 대용량 파일의 extent tree depth가 실제 트리 구조와 불일치하여 파일 접근이 완전히 불가능해지는 사례. 특히 extent 분할(split) 과정에서 전원 손실 시 발생
  • Status Tree 캐시 불일치: 메모리의 extent status tree 캐시가 디스크의 실제 extent와 달라 잘못된 블록을 읽거나 덮어쓰는 사례. 장시간 운영 중인 서버에서 간헐적으로 보고됨
  • e4defrag 중 크래시: 온라인 조각 모음(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의 마운트 옵션 설정에 따라 데이터 무결성과 보안에 심각한 영향을 미칠 수 있습니다. 성능 최적화를 위해 안전 장치를 해제하는 설정이 프로덕션 환경에서 사용되어 문제가 된 사례들입니다.

barrier=0 (쓰기 배리어 비활성화): 쓰기 배리어를 비활성화하면 디스크 캐시의 쓰기 순서가 보장되지 않습니다. 저널 커밋이 실제 데이터 쓰기보다 먼저 디스크에 도달할 수 있어, 전원 손실 시 저널 재생이 오히려 파일시스템을 손상시킬 수 있습니다. 배터리 백업 캐시(BBU)가 있는 하드웨어 RAID 컨트롤러에서만 안전하게 사용할 수 있습니다.
/* 위험한 마운트 옵션 조합 */
# 절대 프로덕션에서 사용하지 마십시오:
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높음쓰기 순서 미보장으로 저널 무효화 가능BBU RAID에서만 사용
nodelalloc낮음성능 저하, 단편화 증가delayed alloc 버그 우회 시에만
max_dir_size_kb 미설정중간거대 디렉토리의 해시 충돌로 DoS 가능공유 시스템에서 제한 설정
errors=continue높음I/O 에러 발생 시 계속 동작 → 데이터 추가 손상errors=remount-ro 권장
errors=continue의 위험성: 기본값인 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)로 매핑합니다. 이 섹션에서는 앞서 소개한 기본 구조를 넘어, 트리의 분할·병합·깊이 변화, 커널 내부의 탐색 경로 최적화, 그리고 실제 디버깅 사례를 심층적으로 다룹니다.

트리 깊이와 최대 엔트리 수

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=14 × 340 = 1,360 extents약 5.3GB ~ 40GB+
depth=24 × 340² = 462,400 extents수 TB 규모
depth 상한: ext4 코드에서 EXT4_MAX_EXTENT_DEPTH는 5로 정의되어 있지만, 실제 운영 환경에서 depth 3 이상은 극히 드뭅니다. depth=2면 수백만 개의 extent를 수용할 수 있어, 수 TB의 극도로 단편화된 파일도 처리 가능합니다.

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 Tree B+tree 탐색 경로 (depth=2) Root (inode i_block[]) hdr(depth=2) | idx[0]:blk0 | idx[1]:blk5000 | idx[2]:blk10000 ext4_ext_binsearch_idx() → idx[1] read_extent_tree_block() Internal Node (depth=1) hdr(depth=1) | idx[0]:blk5000 | idx[1]:blk6000 | ... | idx[N]:blk9500 ext4_ext_binsearch_idx() → idx[2] (blk7000) read_extent_tree_block() Leaf Node (depth=0) hdr(depth=0) | ext[0]:7000-7127→phys 20000 | ext[1]:7128-7255→phys 30000 | ... ext4_ext_binsearch() → ext[0] (논리 7050 매칭) 결과: 물리 블록 = 20000 + (7050 - 7000) = 20050 map->m_pblk = 20050, 연속 길이 = 78 블록

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) */
성능 영향: extent status tree의 cache_es 필드는 마지막으로 조회한 extent를 캐싱합니다. 순차 읽기/쓰기에서는 대부분 이 캐시에서 즉시 히트하여 red-black tree 탐색을 생략합니다. /proc/fs/ext4/<dev>/es_stats에서 캐시 히트율을 확인할 수 있습니다.

JBD2 커밋 흐름 심화

JBD2(Journaling Block Device 2)의 커밋 과정은 단순한 "기록 후 플러시"가 아닙니다. 트랜잭션 상태 전이, 체크포인트 메커니즘, 복구 알고리즘까지 이해해야 ext4의 내구성 보장 방식을 정확히 파악할 수 있습니다.

트랜잭션 상태 전이 상세

JBD2 트랜잭션은 다음 8가지 상태를 순차적으로 거칩니다:

JBD2 트랜잭션 상태 전이 (jbd2_journal_commit_transaction) T_RUNNING handle 추가 가능 T_LOCKED 새 handle 차단 T_SWITCH 새 트랜잭션 시작 T_FLUSH 데이터 블록 기록 T_COMMIT 커밋 레코드 기록 T_COMMIT_DFLUSH 디스크 플러시 대기 T_COMMIT_JFLUSH 저널 플러시 대기 T_FINISHED 커밋 완료 Checkpointing 원래 위치에 반영 → 저널 공간 해제 핵심 플러시 지점: T_FLUSH: ordered 모드에서 데이터 블록이 디스크에 도달해야 다음 단계 진행 T_COMMIT: 커밋 블록(시퀀스 번호 + 체크섬) 기록 → 이 시점부터 복구 가능 T_COMMIT_DFLUSH/JFLUSH: blkdev_issue_flush() 호출하여 디스크 캐시까지 영속 보장

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);
}

체크포인트 메커니즘

체크포인트는 커밋된 트랜잭션의 데이터를 원래 디스크 위치에 반영하고, 저널 공간을 재사용 가능하게 만드는 과정입니다.

단계설명트리거 조건
1. 체크포인트 대상 선택가장 오래된 커밋된 트랜잭션 선택저널 공간 부족, 주기적 타이머
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;
}
revoke의 중요성: revoke 레코드 없이 단순 재생(replay)하면, 이미 원래 위치에 새로운 데이터가 기록된 블록을 오래된 저널 데이터로 덮어쓸 위험이 있습니다. 예를 들어, 블록 B를 파일 A에 할당 → 저널 기록 → 블록 B를 파일 A에서 해제 → 블록 B를 파일 C에 재할당 → 크래시 시, revoke가 없으면 파일 C의 데이터가 파일 A의 오래된 데이터로 덮어씌워집니다.

mballoc 다중 블록 할당기 심화

ext4의 mballoc(Multi-Block Allocator)은 단일 블록 할당기인 ext3에 비해 연속 블록 할당 성능을 크게 향상시킵니다. 이 섹션에서는 buddy bitmap 구조, 그룹 디스크립터, 선할당(preallocation) 메커니즘, 그리고 정규화(normalization) 전략을 심층적으로 분석합니다.

Buddy Bitmap 구조

각 블록 그룹은 블록 비트맵(디스크)과 buddy 비트맵(메모리)을 가집니다. buddy 비트맵은 2의 거듭제곱 크기별로 연속 빈 블록을 추적합니다.

mballoc Buddy Bitmap 구조와 할당 전략 Block Bitmap (디스크, 1블록 = 4KB) 32,768 비트 = 32,768 블록 (128MB/그룹) ... (1=사용, 0=빈) ext4_mb_init_cache() Buddy Bitmap (메모리, 레벨별) order-0: 1블록 단위 빈 블록 order-1: 2블록 연속 빈 영역 order-2: 4블록 연속 빈 영역 ... order-13: 8192블록 연속 빈 영역 (32MB) bb_counters[order]로 각 레벨 빈 청크 수 추적 할당 전략 (ext4_mb_regular_allocator) 1. Goal 기반 탐색 inode의 마지막 할당 블록 근처에서 시작 2. 정규화 (Normalization) 요청 크기를 2^N으로 올림 (단편화 방지) 예: 3블록 요청 → 4블록으로 정규화 3. 최적 맞춤 탐색 (CR 0~3) CR0: 정확한 order 매칭 (goal 그룹) CR1: 정확한 order (전체 그룹 검색) CR2: goal 무시, 빈 블록 있으면 할당 CR3: 정규화 해제, 정확한 크기만 할당 모든 CR 실패 → ENOSPC 선할당 (Preallocation) Per-inode PA: 파일별 선할당 공간 유지 → 순차 쓰기 시 다음 할당에서 즉시 사용 (pa_lstart, pa_pstart, pa_len) Per-group PA: 블록 그룹별 선할당 → 소파일이 많은 디렉토리에서 같은 그룹에 모아 할당 (locality 향상)

할당 컨텍스트 (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;
}
mballoc 통계 확인: /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 vs Full Commit Full Commit (기존 JBD2) inode 변경 (1 필드 수정) 전체 inode 블록 복사 (4KB → 저널) 디스크립터 블록 + 커밋 블록 디스크 플러시 (barrier) ~3 I/O ops Fast Commit (경량) inode 변경 (1 필드 수정) FC 태그만 기록 (수십 바이트 → 저널) 디스크 플러시 (barrier) ~1 I/O op Fast Commit 태그 유형 (TLV 형식) EXT4_FC_TAG_ADD_RANGE(0x01): extent 추가 → {inode, lblk, pblk, len} EXT4_FC_TAG_DEL_RANGE(0x02): extent 삭제 → {inode, lblk, len} EXT4_FC_TAG_CREAT(0x03): 파일 생성 → {parent_inode, dentry_name, child_inode} EXT4_FC_TAG_LINK(0x04) / UNLINK(0x05): 링크/삭제 → {parent_inode, dentry_name} EXT4_FC_TAG_INODE(0x06): inode 메타데이터 변경 → {inode 전체 복사} | EXT4_FC_TAG_TAIL: FC 블록 종료 마커

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은 fast_commit feature flag로 활성화됩니다(tune2fs -O fast_commit). 이 기능이 활성화된 볼륨은 이전 버전의 e2fsck(1.46.2 이전)로 검사할 수 없습니다. 또한 data=journal 모드와는 호환되지 않습니다.

인라인 데이터 (Inline Data) 심화

inline_data feature는 소규모 파일의 데이터를 별도 데이터 블록 대신 inode 자체에 직접 저장합니다. 이를 통해 추가 블록 할당과 디스크 I/O를 피하고, 소파일이 많은 워크로드에서 공간 효율과 성능을 크게 향상시킵니다.

인라인 데이터 저장 레이아웃

인라인 데이터 저장 레이아웃 (inode_size=256) ext4 On-disk Inode (256 바이트) 고정 필드 (128B) i_block[15] 추가 필드 xattr 영역 남은 공간 (가변) 소파일 (60바이트 이하): i_block[] 영역 직접 사용 데이터 (60B) 블록 할당 불필요, extent tree 불필요 중간 파일 (~160바이트): i_block[] + xattr 영역 활용 데이터 (60B) 추가 데이터 (~100B, xattr 영역) system.data xattr로 저장 일반 파일 (>160바이트): 인라인 → extent 변환 (ext4_da_convert_inline_data_to_extent)

인라인 데이터 읽기/쓰기 코드

/* 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;
}
항목인라인 데이터일반 데이터 블록
최대 크기~160바이트 (inode_size=256 기준)16TiB
블록 할당불필요필요 (최소 1 블록)
읽기 I/Oinode 읽기에 포함추가 I/O 필요
적합한 용도심볼릭 링크, 빈 파일, 설정 파일일반 파일
활성화mkfs.ext4 -O inline_data기본
활성화 조건: inline_datainode_size가 256바이트 이상이어야 사용할 수 있습니다(기본값이 256이므로 보통 조건을 만족합니다). 또한 디렉토리의 인라인 데이터도 지원되어, 엔트리가 적은 디렉토리는 별도 데이터 블록 없이 inode 내부에 저장됩니다.

bigalloc (클러스터 기반 할당) 심화

bigalloc feature는 블록 비트맵의 할당 단위를 단일 블록(4KB)에서 클러스터(여러 블록의 그룹)로 변경합니다. 대용량 파일이 주로 저장되는 환경에서 비트맵 관리 오버헤드를 줄이고, 대규모 연속 할당 효율을 높입니다.

bigalloc 아키텍처

bigalloc: 블록 vs 클러스터 할당 비교 일반 할당 (block_size=4K) Block Bitmap: 32,768 비트 / 그룹 ... 각 비트 = 1블록 (4KB) 1TB 볼륨: 비트맵 32MB 필요, 그룹 8,192개 bigalloc (cluster_size=64K, 16블록) Cluster Bitmap: 32,768 비트 / 그룹 ... 각 비트 = 1클러스터 (64KB) 1TB 볼륨: 비트맵 2MB 필요 (16배 감소), 그룹 512개 클러스터 크기별 비교 클러스터 비트맵 최소할당 적합도 4KB 32MB 4KB 소파일 혼재 16KB 8MB 16KB 중간 파일 64KB 2MB 64KB 대파일 위주 256KB 512KB 256KB 미디어 서버 1MB 128KB 1MB 빅데이터 주의: 소파일에서 내부 단편화 증가 s_cluster_ratio와 블록-클러스터 변환 cluster_num = block_num >> s_log_cluster_size | block_num = cluster_num << s_log_cluster_size s_cluster_ratio = cluster_size / block_size | 예: 64KB / 4KB = 16 → s_log_cluster_size = 4

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
제약사항: bigalloc은 소파일이 많은 환경에서 내부 단편화를 유발합니다. 예를 들어 64KB 클러스터에 1바이트 파일을 저장하면 63KB가 낭비됩니다. 또한 일부 e2fsprogs 도구와의 호환성 문제가 있을 수 있으며, inline_data feature와 함께 사용하면 소파일의 단편화를 완화할 수 있습니다.

casefold (대소문자 무관 파일 이름) 심화

casefold feature는 디렉토리 검색 시 유니코드 대소문자 접기(case folding)를 적용하여, "File.TXT"와 "file.txt"를 동일한 파일로 취급합니다. Wine, Samba, Android 등 Windows/macOS 호환이 필요한 환경에서 핵심적인 기능입니다.

유니코드 정규화와 대소문자 접기

casefold: 유니코드 정규화와 대소문자 접기 흐름 파일명 입력 "Cafe\u0301.TXT" NFD 정규화 unicode_normalize_nfd() 대소문자 접기 unicode_casefold() 결과 "cafe\u0301.txt" SipHash 계산 접힌 이름으로 해시 HTree 검색 hash 기반 디렉토리 탐색 casefold 동작 예시 lookup("FILE.TXT") → casefold → "file.txt" → hash → HTree 검색 → "file.txt" 엔트리 발견 ✓ lookup("File.Txt") → casefold → "file.txt" → hash → 동일한 엔트리 발견 ✓ readdir() → 원본 이름 "File.TXT" 반환 (저장된 원래 대소문자 유지)

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" 출력 (대소문자 무관)
유니코드 버전: casefold의 유니코드 버전은 커널에 내장된 unicode 모듈 버전에 의존합니다. 파일시스템 생성 시의 유니코드 버전이 superblock에 기록되며, 이후 커널 업그레이드로 유니코드 버전이 달라져도 하위 호환성이 유지됩니다.

ext4 커널 내부 구조 심화

ext4 커널 코드는 fs/ext4/ 디렉토리에 약 50개의 소스 파일로 구성됩니다. 이 섹션에서는 핵심 자료구조의 관계, VFS 연결 인터페이스, 그리고 코드 모듈 구조를 심층적으로 분석합니다.

ext4 소스 코드 맵

ext4 커널 내부 구조와 VFS 연결 VFS (Virtual File System) super_operations | inode_operations | file_operations | address_space_operations 슈퍼블록 관리 super.c: 마운트/언마운트 resize.c: 온라인 리사이즈 sysfs.c: sysfs 인터페이스 → ext4_sb_info 관리 inode / 파일 연산 inode.c: inode 읽기/쓰기, ext4_inode_info file.c: 파일 열기/읽기/쓰기/fsync namei.c: 디렉토리 검색/생성/삭제 dir.c: readdir, HTree I/O 경로 ialloc.c: inode 할당 balloc.c: 블록 할당 기본 page-io.c: 페이지 I/O readpage.c: 읽기 경로 Extent 관리 extents.c: B+tree 관리 extents_status.c: 캐시 ext4_extents.h: 자료구조 mballoc / xattr / crypto mballoc.c: 다중 블록 할당기 (buddy) xattr.c: 확장 속성, xattr_security.c crypto.c: fscrypt, verity.c: fsverity Fast Commit fast_commit.c: FC 커밋 FC 태그 직렬화/역직렬화 FC 복구 경로 JBD2 (fs/jbd2/) journal.c: 저널 관리 | commit.c: 커밋 | checkpoint.c: 체크포인트 | recovery.c: 복구 | revoke.c: revoke 관리

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_cacheext4_inode_infoinode 할당 (VFS inode 내장)
ext4_free_dataext4_free_data지연 해제 블록 정보
ext4_allocation_contextext4_allocation_contextmballoc 할당 컨텍스트
ext4_io_endext4_io_end비동기 I/O 완료 추적
ext4_extent_statusextent_statusextent status tree 노드
ext4_pending_reservationpending_reservationbigalloc 예약 블록
/* 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 매핑 등의 내부 동작을 실시간으로 관찰하고 성능 병목을 정확히 진단할 수 있습니다.

ext4 주요 Tracepoint

Tracepoint위치용도
ext4:ext4_da_write_begindelalloc 쓰기 시작지연 할당 쓰기 빈도/크기 추적
ext4:ext4_da_write_enddelalloc 쓰기 완료실제 기록된 바이트 수
ext4:ext4_writepageswriteback 시작페이지 플러시 빈도/범위
ext4:ext4_mb_new_blocksmballoc 블록 할당할당 크기, 그룹, CR 레벨
ext4:ext4_request_blocks블록 할당 요청요청 크기 vs 실제 할당 비교
ext4:ext4_allocate_blocks블록 할당 완료할당된 물리 블록 위치
ext4:ext4_ext_map_blocks_enterextent 매핑 진입논리→물리 블록 매핑 추적
ext4:ext4_sync_file_enterfsync 시작fsync 빈도/대상 파일
jbd2:jbd2_commit_loggingJBD2 커밋 시작저널 커밋 빈도
jbd2:jbd2_end_commitJBD2 커밋 완료커밋 지연 시간 측정
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 활용: 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 옵션, 마운트 옵션, 저널 모드, 커널 파라미터의 조합에 의해 결정됩니다. 이 섹션에서는 워크로드별 최적 설정과 벤치마크 비교, 그리고 실제 튜닝 절차를 상세히 다룹니다.

워크로드별 최적 설정

ext4 워크로드별 튜닝 가이드 데이터베이스 (OLTP) mkfs: -I 512 -J size=1024 mount: noatime,data=writeback barrier=1,dioread_nolock journal_async_commit scheduler: none (NVMe) 핵심: Direct I/O, 큰 저널 DB가 자체 WAL로 보호 파일 서버 (NFS/Samba) mkfs: -T largefile -J size=512 mount: noatime,data=ordered delalloc,barrier=1 commit=30 scheduler: mq-deadline 핵심: 대파일 순차 I/O commit 간격 늘려 배치 효율 메일 서버 (Maildir) mkfs: -T small -i 4096 -O inline_data mount: noatime,data=ordered delalloc,barrier=1 scheduler: mq-deadline 핵심: 소파일 다수, inode 밀도 inline_data로 소파일 최적화 컨테이너 / VM 이미지 mkfs: -O project,quota -E lazy_itable_init=0 mount: noatime,prjquota errors=remount-ro scheduler: none 핵심: 프로젝트 쿼터로 격리 lazy_itable_init=0: 즉시 초기화 SSD / NVMe 최적화 mkfs: -E stride=1,stripe-width=1 -O fast_commit mount: noatime,discard journal_async_commit scheduler: none 핵심: TRIM, 비동기 커밋 또는 fstrim 크론잡 (discard 대신) 빌드 서버 / CI mkfs: -T news (중간 파일 다수) mount: noatime,data=writeback delalloc,commit=60 max_batch_time=15000 scheduler: bfq (공정성) 핵심: writeback으로 최대 성능 데이터 손실 허용 (재빌드 가능)

저널 모드별 성능 특성

항목data=journaldata=ordered (기본)data=writeback
저널 대상메타데이터 + 데이터메타데이터만메타데이터만
데이터 순서저널에서 보장커밋 전 데이터 기록순서 미보장
쓰기 증폭2x (데이터 이중 기록)1x1x
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_ratio2010~40전체 메모리 중 더티 페이지 비율 상한 (동기 쓰기 시작)
vm.dirty_background_ratio105~20백그라운드 writeback 시작 비율
vm.dirty_expire_centisecs30001500~6000더티 페이지 만료 시간 (cs)
vm.dirty_writeback_centisecs500100~1000writeback 스레드 깨우기 간격 (cs)
vm.vfs_cache_pressure10050~200inode/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=writebackbarrier=0는 성능을 크게 높이지만 크래시 안전성을 희생합니다. 프로덕션 환경에서는 항상 barrier=1을 유지하고, 데이터 안전성이 보장되는 범위 내에서만 튜닝하세요.

ext4와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.