ext4 파일시스템(Filesystem)

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

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

핵심 요약

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

단계별 이해

  1. 경계 계층 파악
    요청이 VFS에서 어디로 내려가는지 확인합니다.
  2. 메타/데이터 분리
    어느 경로에서 무엇이 갱신되는지 나눠 봅니다.
  3. 동기화/플러시(Flush) 확인
    쓰기 반영 시점과 순서를 검증합니다.
  4. 복구 시나리오 점검
    비정상 종료 후 일관성 회복을 확인합니다.
관련 표준: POSIX.1-2024 (파일시스템 시맨틱), 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, 체크섬(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 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

/* 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)이 저장된 물리 블록 주소입니다. 64bit feature 활성 시 _lo_hi를 합쳐 48비트 주소를 표현합니다. 비트맵의 각 비트는 해당 블록의 사용 여부를 나타냅니다.
  • bg_inode_bitmap_lo/hiinode 비트맵의 물리 블록 주소로, 그룹 내 각 inode의 할당 상태를 추적합니다. bg_inode_table_lo/_hi는 inode 테이블의 시작 블록을 가리킵니다.
  • bg_flagsINODE_UNINITBLOCK_UNINIT 플래그는 lazy initialization을 지원합니다. 아직 사용되지 않은 그룹은 비트맵을 디스크에서 읽지 않고 커널이 초기화된 것으로 간주하여 마운트 시간을 단축합니다. bg_checksum은 CRC32C로 디스크립터 자체의 무결성을 검증합니다.

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는 연속된 물리 블록의 범위를 하나의 엔트리로 표현하여, 대용량 파일에서 메타데이터 오버헤드(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_entrieseh_max로 현재/최대 엔트리 수를 관리합니다.
  • ext4_extent_idx내부 노드에서 사용하는 인덱스 엔트리입니다. ei_block은 이 서브트리가 커버하는 논리 블록의 시작 번호이며, ei_leaf_lo/ei_leaf_hi를 합쳐 48비트 물리 블록 주소로 자식 노드를 가리킵니다.
  • ext4_extent리프 노드의 실제 매핑 엔트리로, ee_block부터 ee_len개의 연속 논리 블록이 ee_start_hi:ee_start_lo 48비트 물리 블록에 매핑됨을 표현합니다. ee_len의 MSB가 1이면 미초기화(unwritten) extent로, 블록은 할당되었지만 아직 데이터가 기록되지 않은 상태입니다. 이 구조는 fs/ext4/ext4_extents.h에 정의되어 있습니다.
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;
}
코드 설명
  • 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 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 시점(페이지 캐시(Page Cache) → 디스크 플러시)에 실제 할당합니다.

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와 독립적인 커널 모듈(Kernel Module)로, 메타데이터(및 선택적으로 데이터)의 원자적(Atomic) 갱신을 보장합니다.

JBD2 아키텍처

구조체(Struct)역할
journal_t저널 디바이스/영역을 대표. 트랜잭션 목록, 버퍼(Buffer) 관리
transaction_t하나의 트랜잭션. 수정된 버퍼 목록, 상태, 시퀀스 번호
handle_t개별 파일시스템 연산의 저널 핸들 (트랜잭션의 하위 단위)
v6.11+ 성능 개선: jbd2_transaction_committed() 함수가 최적화되어, 빠른 스토리지에서 비동기 Direct I/O 실행 시 IOPS 및 처리량(Throughput)이 최대 20% 향상되었습니다. 또한 jbd2_journal_blocks_per_page()가 대형 folio를 지원하도록 변환되어, large folio 환경에서 저널 크레딧 계산이 정확해졌습니다.

트랜잭션 라이프사이클

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: 커밋된 블록을 원래 위치에 재기록

디렉토리 구현

선형 디렉토리

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

/* 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을 두 곳에 저장합니다:

  1. inode body: inode 크기가 128보다 클 때 (기본 256), 여유 공간에 저장
  2. 외부 블록: 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)

네임스페이스접두어용도
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);
}

파일시스템 암호화(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
v6.13+ 구조 변경: fscrypt 및 fsverity 포인터가 공용 struct inode에서 각 파일시스템별 inode 구조체(ext4_inode_info 등)로 이동되었습니다. 이로 인해 struct inode16바이트 축소되어 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 pagefolio당 1개 (나머지 tail page)
제한 사항: fsverity, fscrypt, data=journal 모드에서는 large folio가 비활성화됩니다. 이 조합에서는 기존 4KB 단일 페이지 경로가 계속 사용됩니다.
bigalloc과의 시너지: ext4의 bigalloc(클러스터 할당) 기능과 large folio를 결합하면, 클러스터 크기에 맞춘 multi-fsblock atomic write가 가능해집니다. 이는 데이터베이스 페이지 크기(16KB 등)에 맞춘 원자적 기록을 지원하여, 별도의 WAL 없이 데이터 무결성을 보장할 수 있습니다.

원자적 쓰기 (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_ATOMICstx_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
bigalloc 다중 블록 원자적 쓰기: bigalloc 기능과 결합하면 클러스터 크기(예: 16KB, 64KB) 단위의 원자적 쓰기가 가능합니다. bigalloc이 클러스터 단위 정렬을 보장하므로, 데이터베이스 페이지 크기에 맞춘 원자적 기록을 지원합니다. 또한 커널 6.16에서는 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에서 재도입
왜 필요한가: NFS 클라이언트는 서버의 mtime/ctime 변화로 캐시 유효성을 판단합니다. 저해상도 타임스탬프에서는 짧은 시간 내 연속 수정이 같은 타임스탬프를 가져 캐시가 갱신되지 않는 문제가 있었습니다. 멀티그레인 방식은 활발히 조회되는 inode에만 고해상도를 적용하여 오버헤드를 최소화하면서 이 문제를 해결합니다.

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;
}
기존 방식의 문제점: orphan 추가/제거마다 슈퍼블록을 갱신하므로 JBD2 트랜잭션이 직렬화(Serialization)됩니다. 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 기본 포함
마운트 시 orphan 정리: ext4 마운트 과정에서 ext4_orphan_cleanup()이 호출되어, orphan file의 각 블록을 순회하며 남아 있는 orphan inode를 정리합니다. unlink된 orphan은 삭제를 완료하고, truncate orphan은 원래 크기로 복원합니다. 이 과정은 저널 복구(SCAN → REVOKE → REPLAY) 이후에 수행됩니다.

핵심 커널 자료구조

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 구조체입니다:

/* 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_inodei_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지연 할당 (기본값)순차 쓰기가 많은 워크로드
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데이터 → 메타 순서 보장(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-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 스케줄러(Scheduler) 선택

스케줄러권장 디바이스특징
noneNVMe SSD스케줄링 오버헤드 제거, 디바이스 내부 큐에 위임
mq-deadlineSATA 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 변경 일부, 읽기 전용 관찰 정도입니다.
e2fsprogs 운용 흐름 사전 확인 lsblk · blkid · wipefs -n 장치 경계 · UUID · 중복 마운트 생성 · 초기 설정 mkfs.ext4 / mke2fs tune2fs · e2label 운영 중 관찰 dumpe2fs · e2freefrag filefrag · lsattr 확장 · 정리 resize2fs e4defrag · e2undo 오류 감지 · 복구 e2fsck / fsck.ext4 journal_only · backup superblock logsave로 로그 보존 포렌식 · 저수준 분석 e2image로 메타데이터 확보 debugfs stat / ncheck / icheck / logdump 마운트 대신 복제본에서 분석 정상 운영 경로와 장애 대응 경로를 분리해 생각하면 실수를 줄일 수 있습니다.
ext4 볼륨 수명주기에서 e2fsprogs가 맡는 역할

작업 전 안전 수칙

# 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.12024-05-2064비트 time_t 환경의 2038년 이후 타임스탬프 처리, mke2fs -d tar 입력 지원(libarchive 사용 시), debugfs hash 개선, root_perms 추가
1.47.22025-01-01tune2fs -r 제거 후 -E revision=로 대체, debugfs orphan 목록 명령 추가, resize2fs orphan file 체크섬 보정
1.47.32025-07-08mke2fs -d가 fs-verity 메타데이터와 chattr 플래그를 복사, fuse2fsFALLOC_FL_ZERO_RANGE·나노초 타임스탬프 지원 강화
1.47.42026-03-06mke2fs -E root_selinux=... 계열 지원, 복수 -E 사용 가능, 대형 블록 크기 경고 정리, e4defrag 인라인 데이터 크래시 수정
버전 읽는 법: 배포판 기본 패키지는 최신 upstream보다 느릴 수 있습니다. 예를 들어 RHEL 계열 장기 지원 배포판은 오래된 e2fsprogs에 보안 수정만 백포트하는 경우가 많습니다. 따라서 문서의 “지원 여부”는 업스트림 릴리스실제 배포판 패키지 버전을 분리해서 해석해야 합니다.

e2fsprogs 운용 절차

e2fsprogs를 실무에서 사용할 때는 생성검증운영 중 관찰장애 복구용량 조정 순서로 사고하는 편이 안전합니다. 아래 절차는 서버 운영자가 실제로 가장 많이 반복하는 흐름입니다.

새 ext4 볼륨 생성 절차

  1. 장치가 비어 있는지, 파티션 경계와 RAID/LVM 상위 계층이 맞는지 확인합니다.
  2. 워크로드에 맞는 block size, inode 비율, 예약 블록, feature flag를 결정합니다.
  3. mkfs.ext4로 생성한 직후 tune2fs -ldumpe2fs -h로 결과를 검증합니다.
  4. 마운트 후 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, -Ulabel/UUID 변경자동 마운트와 /etc/fstab 의존성을 함께 검토해야 합니다.
-O feature,...feature 활성화/비활성화대부분 언마운트 상태와 후속 e2fsck -f가 필요합니다.
-E clear_mmpstale 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
주의: upstream 1.47.2부터 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 discardSSD에는 유리할 수 있지만, 수동 복구 여지를 줄일 수 있습니다.
-E fixes_only손상만 수정최적화는 생략하고 최소 수정에 집중합니다.
-E unshare_blocksshared block 해제read-only shared_blocks 파일시스템 전환 작업에서 사용합니다.
-z undo_fileundo 로그 생성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 종료 코드 해석

e2fsckfsck.ext4는 단순한 0/1 반환이 아니라 조건별 비트 합으로 종료 코드를 돌려줍니다. 따라서 자동화 스크립트에서 “0이 아니면 무조건 실패”로 처리하면 오동작할 수 있습니다. 예를 들어 종료 코드가 3이면 1 + 2가 합쳐진 값으로, “오류를 수정했고 재부팅이 필요함”을 뜻합니다.

의미자동화 해석
0오류 없음정상 종료입니다.
1파일시스템 오류 수정됨수정은 성공했지만, 운영 로그에는 반드시 남기는 편이 좋습니다.
2파일시스템 오류 수정됨, 시스템 재부팅 필요루트 파일시스템 또는 중요한 메타데이터가 바뀐 경우 재부팅 계획을 세웁니다.
4수정되지 않은 오류가 남음운영 계속 진행보다 추가 점검과 수동 복구가 우선입니다.
8운영 오류장치 접근 실패, I/O 오류, 잠금 충돌 같은 실행 실패를 의심합니다.
16사용법 또는 구문 오류명령행 옵션이나 인자 구성이 잘못된 경우입니다.
32사용자 취소대화형 실행 중 중단되었거나 외부에서 취소된 경우입니다.
128공유 라이브러리(Shared Library) 오류실행 환경 자체가 깨졌을 가능성이 큽니다.
실무 규칙: 12는 “치명적 실패”가 아니라 “복구는 됐지만 후속 조치가 필요함”에 가깝습니다. 반대로 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 종료 코드 처리 부트로더 GRUB 2 / zipl 커널 + initramfs 로드 초기 사용자 공간 initramfs-tools 또는 dracut 필요 시 root fsck 수행 systemd-fsck root / usr / 나머지 마운트 fstab passno 기준 호출 e2fsck 반환값 0, 1, 2, 4, 8... 비트마스크 합산 0 / 1 정상 또는 수정 후 계속 진행 대개 부팅 지속 2 root / usr면 reboot.target 일반 마운트면 유닛 실패 4 uncorrected errors 대개 emergency.target 8 / 16 / 32 / 128 실행 환경 또는 호출 실패 운영자 개입 필요 핵심은 "종료 코드 자체"보다 "어느 계층이 그 코드를 받아 다음 부트 타깃을 결정하는가"입니다.
systemd 기반 배포판에서 fsck 종료 코드가 부트 흐름으로 연결되는 방식
계층무엇을 하는가종료 코드 처리 방식
e2fsckext4 자체 검사/복구문서화된 비트마스크를 직접 반환합니다.
fsck (util-linux)파일시스템별 검사기 호출 프런트엔드여러 파일시스템을 검사하면 개별 종료 코드를 비트 OR 하여 합성합니다.
systemd-fsck부팅 시 mount 단위로 fsck 실행24를 보고 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.servicerootreboot.targetemergency.target단, root가 initrd에서 이미 검사되지 않았을 때만
systemd-fsck-usr.service/usrreboot.targetemergency.target/usr 분리 시스템에서만 의미가 큽니다
systemd-fsck@.service기타 마운트유닛 실패유닛 실패nofail/noauto가 아니면 이후 local-fs.targetemergency.target으로 전이할 수 있습니다

또한 fsck.mode=auto|force|skip, fsck.repair=preen|yes|no 커널 파라미터가 공통 인터페이스입니다. 여기서 preen은 “안전한 수정만 자동 수행”, yes는 “모든 질문에 yes”, no는 “질문에 no”를 뜻합니다.

배포판 계열별 실제 동작 포인트

배포판 계열초기 부트 스택root fsck가 주로 일어나는 위치실제 운영에서 보이는 특징
Debian / Ubuntu 계열initramfs-tools + systemdinitramfs 또는 이후 systemd-fsck-root.service/run/initramfs/fsck.log에 로그를 남기고, 성공 시 /run/initramfs/fsck-root 마커를 둡니다.
RHEL / CentOS Stream / Fedora 계열dracut + systemd대개 initrddracut 기반 initramfs에서 root 장치 준비와 조기 fsck가 이뤄지는 구성이 일반적입니다.
SLES / openSUSE 계열dracut + systemdinitramfs문서상 root 마운트 실패 시 ext3/ext4 검사기가 자동으로 시작되고, 복구 후 root 마운트를 재시도합니다.
Arch Linuxsystemd 중심 사용자 공간initrd에서 검사됐는지 여부에 따라 달라짐최신 Arch 문서는 fsck.mode, fsck.repair를 서비스 자격 증명(Credentials)으로도 주입할 수 있음을 문서화합니다.
해석 기준: 위 표에서 Debian/Ubuntu와 SUSE 계열은 공식 문서가 root 검사 흔적과 재시도 경로를 명시합니다. RHEL/Fedora 계열은 공식 문서가 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
운영 포인트: root가 initrd에서 이미 검사되었다면, 본 시스템으로 넘어온 뒤 systemd-fsck-root.service는 “실행 안 됨”처럼 보일 수 있습니다. 이 경우를 곧바로 “root fsck가 생략되었다”로 해석하면 안 됩니다.

최신 배포판에서 보이는 systemd 버전 차이

최신 배포판은 모두 fsck.mode, fsck.repair 커널 파라미터를 지원하지만, 최근 systemd는 여기에 서비스 자격 증명(Credentials) 주입을 추가했습니다. 이 차이는 이미지 기반 배포, 클라우드 초기 부트, CI 가상머신 부트 정책 제어에서 꽤 중요합니다.

배포판 문서확인된 systemd 버전문서화된 fsck 제어
Ubuntu 25.04 (plucky) manpage257.4-1ubuntu3.2커널 파라미터 fsck.mode, fsck.repair
Debian testing manpage259-1커널 파라미터 + 서비스 자격 증명 fsck.mode, fsck.repair
Arch Linux manpage258.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'
문서에 적어 둘 권장 문장: “ext4의 종료 코드는 e2fsck가 정의하지만, 실제 부팅 결과는 util-linux 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 크기, UUIDdumpe2fs -h생성 정책과 커널 호환성 확인
블록 그룹별 메타데이터 위치dumpe2fs -g슈퍼블록/bitmap/inode table 배치 분석
free space 연속성e2freefrag대형 파일 성능과 조각화 위험 평가

debugfs

debugfs는 ext4 내부를 직접 들여다보는 현미경입니다. inode 번호를 보고 경로를 찾아내고, block -> inode -> path를 오가며, extent와 저널 내용을 출력할 수 있습니다. 기본은 읽기 전용이며, 이 상태만으로도 대부분의 분석은 충분합니다.

명령역할언제 쓰는가
stat <inode>inode 상세 정보크기, 링크 수, 플래그, 블록 매핑을 볼 때
dump_extentsextent tree 출력대형 파일 조각화, 논리/물리 매핑 분석
htree_dump디렉토리 hash tree 확인대형 디렉토리 조회 성능 문제 분석
logdumpJBD2 저널 내용 출력저널 replay 전후 상태 추적
icheck block블록 -> inode 역방향 조회특정 물리 블록이 누구 것인지 확인
ncheck inodeinode -> 경로 추적손상된 inode의 실제 파일명 확인
imap inodeinode 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가능한 최소 크기로 축소자동 축소이지만 여유를 두고 수동 목표 크기를 잡는 편이 더 안전합니다.
-b64bit feature 활성화매우 큰 파일시스템 준비 시 사용합니다.
-s64bit feature 비활성화블록 수가 충분히 작아야 하며, 매우 신중히 사용해야 합니다.
-z undo_fileundo 로그 생성메타데이터 수정 블록을 별도 파일에 저장합니다.
# 온라인 확장
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
undo 로그의 한계: 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, lsattrext4 속성 플래그 조정/조회chattr +i config, chattr +P -p 100 /srv/tenantA
logsavefsck/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: 손상 원인 분석과 파일 회수

서비스 중 장애가 발생했지만 원본 디스크에 추가 쓰기를 최소화해야 하는 상황입니다. 이때는 e2imagedebugfs 조합이 가장 강력합니다.

# 메타데이터 중심 이미지 확보
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
정리: ext4 운영에서 정말 자주 쓰는 조합은 다음과 같습니다.
  • 생성: mkfs.ext4 + tune2fs -l
  • 점검: e2fsck -fn + logsave
  • 구조 관찰: dumpe2fs -h + debugfs stat
  • 확장: 아래 계층 확장 후 resize2fs
  • 복구 전 증거 확보: e2image -r

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
원자적 쓰기O (v6.13+, DIO)O (v6.13+, DIO)XX
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() 패턴에서 데이터가 유실되는 문제를 방지합니다.
교훈: 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 중 크래시: 온라인 조각 모음(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의 마운트 옵션 설정에 따라 데이터 무결성과 보안에 심각한 영향을 미칠 수 있습니다. 성능 최적화를 위해 안전 장치를 해제하는 설정이 프로덕션 환경에서 사용되어 문제가 된 사례들입니다.

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높음쓰기 순서 미보장으로 저널 무효화(Invalidation) 가능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)로 매핑합니다. 이 섹션에서는 앞서 소개한 기본 구조를 넘어, 트리의 분할·병합·깊이 변화, 커널 내부의 탐색 경로 최적화, 그리고 실제 디버깅(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=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)의 커밋 과정은 단순한 "기록 후 플러시"가 아닙니다. 트랜잭션 상태 전이, 체크포인트(Checkpoint) 메커니즘, 복구 알고리즘까지 이해해야 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);
}
코드 설명
  • Phase 0-1: T_RUNNING → T_LOCKED → T_FLUSHjbd2_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;
}
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;
}
코드 설명
  • 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 레벨별 할당 통계를 모니터링할 수 있습니다.
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;
}
코드 설명
  • ext4_read_inline_data()인라인 데이터 읽기는 2단계로 진행됩니다. 먼저 ext4_raw_inode()로 디스크 inode의 원시 포인터를 얻고, i_block[] 영역(60바이트)에서 첫 번째 청크를 memcpy()로 복사합니다. 이 영역은 일반 파일에서 extent tree가 저장되는 곳이지만, inline_data feature 활성 시 소형 데이터 저장소로 사용됩니다.
  • xattr 영역 추가 읽기인라인 데이터가 60바이트(EXT4_MIN_INLINE_DATA_SIZE)를 초과하면 system.data xattr 영역에서 나머지를 읽습니다. 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/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 매핑 등의 내부 동작을 실시간(Real-time)으로 관찰하고 성능 병목을 정확히 진단할 수 있습니다.

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전체 메모리 중 더티 페이지(Dirty Page) 비율 상한 (동기 쓰기 시작)
vm.dirty_background_ratio105~20백그라운드 writeback 시작 비율
vm.dirty_expire_centisecs30001500~6000더티 페이지 만료 시간 (cs)
vm.dirty_writeback_centisecs500100~1000writeback 스레드(Thread) 깨우기(Wakeup) 간격 (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을 유지하고, 데이터 안전성이 보장되는 범위 내에서만 튜닝하세요.

쓰기 콜 체인

파일에 데이터를 기록할 때 VFS의 write_iter 훅에서 출발하여 블록 할당, 저널 트랜잭션, 페이지 캐시 완료까지 이어지는 전체 경로를 분석합니다. 각 단계에서 호출되는 함수와 그 역할을 순서대로 살펴봅니다.

콜 체인 전체 구조

ext4 쓰기 콜 체인 (write_iter → da_write_end) ext4_file_write_iter() VFS file_operations.write_iter 훅 직접 I/O 또는 버퍼 경로 분기 generic_perform_write() 페이지 단위 반복, 사용자 데이터 복사 ext4_write_begin() JBD2 handle 시작, 페이지/버퍼 준비 jbd2_journal_start() 트랜잭션 handle 획득 copy_page_from_iter_atomic() 사용자 공간 → 페이지 캐시 복사 ext4_da_write_end() handle 종료, 지연 할당 상태 기록 jbd2_journal_stop() handle 반환, 커밋 트리거 페이지 더티(Dirty) 마킹 writeback 시 mballoc으로 블록 할당 주 경로 JBD2 측 경로
ext4 버퍼 쓰기 경로: write_iter → write_begin/end → JBD2 handle → 지연 할당

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_tidfsync()/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_hiee_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_lenEXT_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 마운트 옵션에 따른 주기적 커밋과 저널 공간 부족 시 즉시 커밋이 여기서 트리거됩니다.
지연 할당 상태 전이: write() → writeback → 블록 확정 write() 호출 extent status: DELAYED i_reserved_data_blocks++ 페이지 더티 Page Cache dirty folio 누적 물리 블록: 미할당 writeback ext4_writepages() mballoc으로 블록 할당 extent tree 갱신 디스크 기록 WRITTEN 상태 JBD2 트랜잭션 병렬 진행 write_begin: jbd2_journal_start() → handle 획득 (크레딧 예약) write_end: jbd2_journal_stop() → handle 반환, 크레딧 해제 (커밋 주기: commit=5초 또는 저널 부족 시) 크래시 안전성 경계 ① write() 후 writeback 전 크래시 → 데이터 손실 (POSIX 허용, fsync()로 방지) ② writeback 중 JBD2 커밋 완료 후 크래시 → 저널 재생으로 복구 가능 ③ 커밋 블록 기록 전 크래시 → 해당 트랜잭션 전체 취소 (원자성 보장)
지연 할당 상태 전이와 JBD2 트랜잭션의 크래시 안전성 경계
핵심 정리: ext4_file_write_iter()ext4_write_begin()(JBD2 handle 시작) → 사용자 데이터 복사 → ext4_da_write_end()(더티 마킹 + handle 종료) 순으로 진행됩니다. 실제 블록 할당(ext4_ext_map_blocks + ext4_mb_new_blocks)은 writeback 시점에 수행되며, 메타데이터 변경은 JBD2 트랜잭션으로 원자적으로 보호됩니다.

참고자료

커널 공식 문서

LWN.net 기사

커널 소스 코드

도구 및 유틸리티

발표 및 기술 자료

ext4 최신 변화 요약 (v6.8~v7.0)

ext4는 atomic writes 확장, orphan_file·fast_commit 정착, folio/multigrain 타임스탬프 적용, 블록 할당 확장성 개선, 페이지보다 큰 블록 크기 지원을 중심으로 진화하고 있습니다.

Atomic Writes + bigalloc (v6.13~v6.16)

orphan_file 기본화 & fast_commit (v6.8~v6.12)

multigrain TS & folio 정리 (v6.12~v6.14)

Large Folio 정규 파일 지원 (v6.16)

오류 처리 및 신뢰성 개선 (v6.15)

블록 할당 확장성 개선 (v6.17)

페이지보다 큰 블록 크기 및 온라인 조각 모음 (v6.19)

동시 DIO 쓰기 성능 및 신뢰성 (v7.0)

보안 패치 (CVE-2025-38351): ext4(Btrfs, F2FS 포함) 파일시스템 드라이버에서 버퍼 오버플로우 취약점이 발견되었습니다. 하이퍼바이저 접근 권한을 가진 로컬 공격자가 메모리를 덮어쓰거나 서비스 거부를 일으킬 수 있습니다(CVSS 7.8, 높음). 2026년 2월 Ubuntu 보안 업데이트에서 수정되었습니다. 관련 배포판의 보안 업데이트를 즉시 적용하세요.
: atomic write를 실전 배치할 때는 mount -o ...,dax 등 다른 경로와 상호작용을 확인하고, statx 결과와 /sys/block/<dev>/queue/atomic_write_* 값을 교차 검증하여 블록 장치 레벨 한계를 파악하는 것이 좋습니다.

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