inode 구조 (inode Structure)

파일 메타데이터의 핵심 단위인 inode를 기준으로 커널이 파일의 정체성과 상태를 어떻게 관리하는지 다룹니다. `struct inode` 주요 필드, inode_operations와 file_operations의 분리 설계, inode/dentry 캐시(Cache) 결합, 생성·갱신·삭제·회수 경로, 링크 카운트와 권한/타임스탬프 일관성, ext4·XFS·Btrfs 구현 차이를 실제 함수 흐름에 맞춰 상세히 정리합니다.

전제 조건: VFSPage Cache 문서를 먼저 읽으세요. 파일시스템(Filesystem) 공통 계층은 객체 생명주기와 캐시 일관성(Cache Coherency)이 중심이므로, 먼저 추상 계층의 역할 경계를 고정하는 것이 중요합니다.
일상 비유: 이 주제는 도서관 분류 카드와 대출대장과 비슷합니다. 책 본문(데이터)보다 카드/대장(메타데이터) 규칙이 먼저 맞아야 전체 조회와 갱신이 안정적으로 동작합니다.

핵심 요약

  • 이름/실체 분리 — dentry는 이름, inode는 파일 실체
  • inode cache — 해시(Hash) + LRU로 디스크 I/O 절감
  • inode_operations — 파일시스템별 동작 다형성 지점
  • i_nlink — 하드링크 수명 관리 핵심 필드
  • FS-specific inode — ext4/xfs/btrfs 확장 구조

단계별 이해

  1. 객체 관계 이해
    dentry-inode-file-superblock 관계를 먼저 도식으로 잡습니다.
  2. 핵심 필드 확인
    i_mode, i_size, i_ino, i_op 의미를 정리합니다.
  3. 캐시 경로 추적
    lookup hit/miss에서 icache 동작을 확인합니다.
  4. 파일시스템 비교
    ext4/xfs/btrfs inode 확장 포인트를 비교합니다.
관련 표준: POSIX.1-2017 (inode 시맨틱, 하드링크, 퍼미션 모델) — inode 구조체(Struct)는 POSIX 파일 메타데이터 규약을 커널에서 구현합니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

inode 개요

struct inode는 파일시스템의 파일(또는 디렉터리, 심볼릭 링크 등)을 나타내는 메타데이터 객체입니다. 파일명이 아닌 파일 자체의 속성을 저장합니다 — 파일명은 dentry가 담당합니다.

inode라는 이름은 Unix의 원래 논문에서 유래했습니다. Dennis Ritchie와 Ken Thompson은 1974년 논문에서 이 개념을 "index node"의 줄임말로 사용했습니다. 오늘날 리눅스 커널에서 struct inode는 약 600바이트 크기의 구조체로, VFS(Virtual File System) 계층의 핵심 데이터 구조입니다.

VFS 계층에서 inode의 위치 사용자 공간: open(), read(), write(), stat(), unlink() ... 시스템 콜 인터페이스 (sys_open, sys_read, ...) VFS (Virtual File System) superblock inode dentry file address_space ext4 XFS Btrfs tmpfs NFS / FUSE 블록 계층 (block layer) / 네트워크

리눅스 커널에서 inode가 다루는 핵심 정보는 다음과 같습니다:

범주정보관련 필드
식별inode 번호, 소속 파일시스템i_ino, i_sb
유형/권한파일 유형, rwx 권한, 특수 비트i_mode
소유권소유자 UID/GIDi_uid, i_gid
크기파일 크기, 디스크 블록 수i_size, i_blocks
시간접근/수정/변경/생성 시간__i_atime, __i_mtime, __i_ctime
링크하드 링크 수, 참조 카운트(Reference Count)i_nlink, i_count
연산파일시스템별 동작 테이블i_op, i_fop
데이터페이지 캐시(Page Cache) 연결i_mapping, i_data
잠금(Lock)동시성 제어i_lock, i_rwsem
캐시해시, LRU, writeback 리스트i_hash, i_lru, i_io_list

inode 추상화 원리

Unix/Linux 파일시스템의 핵심 설계 원리는 "이름(name)과 메타데이터(metadata)의 분리"입니다:

이 분리 덕분에 하드 링크(같은 inode에 여러 이름), 파일 이동(dentry만 변경, inode 불변), 삭제된 파일의 계속 접근(열린 파일 디스크립터(File Descriptor)가 inode 참조 유지) 등이 가능합니다.

VFS inode vs 파일시스템별 inode

커널은 2단계 inode 구조를 사용합니다:

각 파일시스템은 자체 inode 구조체에 VFS inode를 임베드합니다. 이 패턴은 상속 없이 다형성을 달성하는 커널의 전형적인 객체 지향 기법입니다.

inode 캐시 동작 원리

VFS는 inode 캐시(icache)를 유지하여 디스크 I/O를 최소화합니다. 동작 원리는 다음과 같습니다:

  1. 해시 테이블(Hash Table) 조회: 파일 접근 시 (superblock, inode 번호) 쌍으로 해시 테이블을 검색합니다. 히트하면 디스크 읽기 없이 즉시 반환합니다.
  2. LRU 관리: 참조 카운트(i_count)가 0이 된 inode는 LRU 리스트에 들어갑니다. 즉시 삭제하지 않고 캐시에 유지하여, 재접근 시 빠르게 활용합니다.
  3. 메모리 회수(Memory Reclaim): 메모리 압력 시 커널의 shrinker가 LRU 끝에서부터 inode를 회수합니다. vm.vfs_cache_pressure sysctl로 회수 적극성을 조절합니다 (기본값 100, 높으면 더 적극적 회수).
ℹ️

dentry 캐시와의 관계: dentry 캐시(dcache)와 inode 캐시는 함께 동작합니다. dentry가 해시에서 히트하면 연결된 inode도 캐시에 있습니다. 경로 조회(path_lookup)는 dcache → icache 순서로 진행되어, 자주 접근하는 파일의 경로 해석이 디스크 I/O 없이 완료됩니다.

struct inode 주요 필드

struct inode {
    umode_t             i_mode;      /* 파일 유형 + 권한 */
    unsigned short      i_opflags;
    kuid_t              i_uid;       /* 소유자 UID */
    kgid_t              i_gid;       /* 소유자 GID */
    unsigned int        i_flags;

    const struct inode_operations *i_op;   /* inode 연산 */
    struct super_block  *i_sb;        /* 소속 superblock */
    struct address_space *i_mapping;  /* 페이지 캐시 매핑 */

    unsigned long       i_ino;       /* inode 번호 */
    atomic_t            i_count;     /* 참조 카운트 */
    unsigned int        i_nlink;     /* 하드 링크 수 */
    loff_t              i_size;      /* 파일 크기 (바이트) */
    struct timespec64   __i_atime;   /* 최종 접근 시간 */
    struct timespec64   __i_mtime;   /* 최종 수정 시간 */
    struct timespec64   __i_ctime;   /* 최종 변경 시간 */
    blkcnt_t            i_blocks;    /* 할당된 블록 수 */

    const struct file_operations *i_fop; /* file 연산 */
    struct list_head    i_devices;
    union {
        struct pipe_inode_info *i_pipe;
        struct cdev  *i_cdev;
        char         *i_link;    /* symlink target */
    };
    void                *i_private;  /* fs-specific data */
};
코드 설명
  • i_modeumode_t 타입. 상위 4비트는 파일 유형(S_IFREG/S_IFDIR/S_IFLNK 등), 하위 12비트는 rwxrwxrwx + setuid/setgid/sticky 권한을 인코딩합니다. S_ISREG(), S_ISDIR() 매크로로 판별합니다.
  • i_opflagsinode 연산 최적화 플래그 캐시. IOP_FASTPERM, IOP_LOOKUP, IOP_NOFOLLOW 등 자주 조회되는 콜백 존재 여부를 비트로 기록하여 i_op 포인터 역참조를 줄입니다.
  • i_uid / i_gid커널 내부 kuid_t/kgid_t 타입. 사용자 공간 uid/gid와는 달리 idmapped mount를 통한 변환이 적용된 값입니다. i_uid_into_vfsuid(idmap, inode)로 마운트 컨텍스트에 맞게 변환합니다.
  • i_op파일시스템이 등록하는 struct inode_operations 포인터. lookup, create, unlink, rename, permission 등 inode 단위 연산을 제공합니다. 마운트 시점에 파일 유형에 따라 설정됩니다.
  • i_sb이 inode가 속한 파일시스템의 struct super_block 포인터. 파일시스템 전역 정보(블록 크기, 마운트 옵션, s_op 등)에 접근하는 핵심 경로입니다.
  • i_mapping페이지 캐시와 inode를 연결하는 struct address_space 포인터. 일반적으로 &inode->i_data를 가리키지만, 블록 디바이스 inode는 다른 address_space를 가리킬 수 있습니다.
  • i_ino파일시스템 내 inode 고유 번호. stat(2)의 st_ino 필드로 노출됩니다. iget_locked(sb, ino)로 캐시를 조회할 때 해시 키로 사용됩니다. 64비트지만 stat 호환성을 위해 32비트 범위를 사용하는 파일시스템이 많습니다.
  • i_countVFS 참조 카운트. iget() 계열 함수로 증가, iput()으로 감소합니다. 0이 되면 inode 캐시에서 제거 후 회수(evict) 대상이 됩니다. i_nlink와 독립적으로 관리됩니다.
  • i_nlink하드 링크(Hard Link) 수. link(2)로 증가, unlink(2)/rmdir(2)로 감소합니다. 0이 되면 i_count도 0이 될 때 실제로 inode가 삭제됩니다. set_nlink()/inc_nlink()/drop_nlink() API를 통해 변경해야 합니다.
  • i_size파일 크기(바이트). stat(2)의 st_size로 노출됩니다. i_size_read()/i_size_write()로 원자적으로 접근해야 합니다. Sparse 파일에서 실제 할당 블록보다 클 수 있습니다.
  • __i_atime / __i_mtime / __i_ctime최종 접근/데이터 수정/메타데이터 변경 시각. timespec64로 나노초 정밀도를 제공합니다. 직접 접근하지 말고 inode_get_atime(), inode_set_mtime_to_ts() 등의 접근자(Accessor)를 사용해야 합니다.
  • i_blocks512바이트 단위로 할당된 블록 수. stat(2)의 st_blocks 필드입니다. 스토리지 블록 크기(4096 등)와 무관하게 항상 512 단위로 계산합니다.
  • i_fop파일 객체(struct file)에 등록할 struct file_operations 포인터. open 시 file->f_op에 복사됩니다. read/write/mmap/ioctl 등 fd 단위 연산을 정의합니다.
  • union {i_pipe, i_cdev, i_link}파일 유형별 전용 데이터 공용체. 파이프(pipe_inode_info), 문자 디바이스(cdev), 심볼릭 링크 타겟 경로를 각각 저장합니다. 일반 파일/디렉터리는 이 공용체를 사용하지 않습니다.
  • i_private파일시스템 전용 데이터 포인터. 단, 별도의 fs-private 구조체를 embed하는 방식(container_of 패턴)이 권장됩니다. MYFS_I(inode) 매크로가 이 패턴을 구현합니다.

struct super_operations 주요 콜백

struct super_operations는 파일시스템 전체(슈퍼블록 단위)에 대한 연산을 정의합니다. inode의 할당·해제·읽기·쓰기·evict를 포함하며, 파일시스템 드라이버가 구현하여 super_block.s_op에 등록합니다.

/* include/linux/fs.h — 주요 super_operations 콜백 */
struct super_operations {
    struct inode *(*alloc_inode)(struct super_block *sb); /* fs별 inode 할당 */
    void          (*free_inode) (struct inode *);           /* inode 메모리 해제 */
    void          (*destroy_inode)(struct inode *);         /* 비동기 해제 (call_rcu) */
    void          (*dirty_inode)(struct inode *, int flags); /* dirty 마킹 통지 */
    int           (*write_inode)(struct inode *,
                                  struct writeback_control *); /* inode를 디스크에 기록 */
    int           (*drop_inode) (struct inode *);           /* i_count 0 시 보존 여부 결정 */
    void          (*evict_inode)(struct inode *);           /* inode 최종 제거 (nlink==0) */
    void          (*put_super)  (struct super_block *);     /* umount 시 자원 해제 */
    int           (*sync_fs)   (struct super_block *, int); /* fsync/sync 시 플러시 */
    int           (*freeze_super)(struct super_block *,
                                   enum freeze_holder);     /* 스냅샷 전 freeze */
    int           (*unfreeze_super)(struct super_block *,
                                     enum freeze_holder);   /* freeze 해제 */
    int           (*statfs)    (struct dentry *,
                                  struct kstatfs *);        /* statfs(2) 통계 */
    int           (*show_options)(struct seq_file *,
                                   struct dentry *);         /* /proc/mounts 마운트 옵션 */
    long          (*nr_cached_objects)(struct super_block *,
                                        struct shrink_control *); /* slab shrinker용 */
    long          (*free_cached_objects)(struct super_block *,
                                          struct shrink_control *); /* slab shrinker용 */
};
코드 설명
  • alloc_inode파일시스템별 inode 할당 콜백. 파일시스템 전용 구조체(예: ext4_inode_info)를 kmem_cache에서 할당하고 내부의 VFS inode(vfs_inode 필드)를 초기화하여 반환합니다. 구현하지 않으면 VFS가 struct inode만 할당합니다.
  • free_inode / destroy_inodefree_inode는 RCU 유예기간 후 실제 메모리 해제를 수행합니다. destroy_inode는 RCU 콜백을 등록하는 VFS 내부 래퍼입니다. 파일시스템이 alloc_inode를 구현했다면 반드시 free_inode도 구현해야 합니다.
  • dirty_inodeinode가 dirty로 마킹될 때 파일시스템에 알리는 콜백. ext4는 저널 트랜잭션 컨텍스트를 확인하고 필요시 체크포인트를 요청합니다. flags에는 I_DIRTY_SYNC, I_DIRTY_DATASYNC 등이 있습니다.
  • write_inodewriteback 시 inode 메타데이터를 디스크에 기록합니다. writeback_controlsync_mode에 따라 동기/비동기로 동작합니다. ext4에서는 저널 트랜잭션을 통해 원자적으로 기록합니다.
  • drop_inodei_count가 0이 되었을 때 호출됩니다. 반환값 1이면 즉시 evict, 0이면 LRU 캐시에 보존합니다. generic_drop_inode()i_nlink == 0이거나 파일시스템이 unmount 중이면 1을 반환합니다. NFS는 이를 재정의하여 추가 조건을 검사합니다.
  • evict_inodeinode가 메모리에서 완전히 제거될 때 호출됩니다. i_nlink == 0이면 디스크에서 inode와 데이터 블록을 삭제합니다. 반드시 truncate_inode_pages_final()clear_inode()를 호출해야 합니다.
  • sync_fssync(2)syncfs(2) 시 호출됩니다. 두 번째 인자가 1이면 동기(wait=1), 0이면 비동기 플러시를 요청합니다. ext4는 저널 커밋을 여기서 수행합니다.
  • nr_cached_objects / free_cached_objects메모리 압박 시 slab shrinker가 호출하는 콜백 쌍. 파일시스템이 자체 캐시(예: XFS의 dquot 캐시)를 보유할 때 shrinker 프레임워크와 통합하기 위해 구현합니다.

inode_operations

struct inode_operations는 inode 자체에 대한 연산(생성, 검색, 삭제, 속성 변경)을 정의합니다. 각 파일시스템은 자체 콜백(Callback)을 구현하여 VFS에 등록합니다. NULL인 콜백은 "미지원"을 의미하며, VFS가 기본 동작을 수행하거나 에러를 반환합니다.

struct inode_operations {
    struct dentry *(*lookup)(struct inode *, struct dentry *, unsigned int);
    int (*create)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t, bool);
    int (*link)(struct dentry *, struct inode *, struct dentry *);
    int (*unlink)(struct inode *, struct dentry *);
    int (*symlink)(struct mnt_idmap *, struct inode *, struct dentry *, const char *);
    int (*mkdir)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t);
    int (*rmdir)(struct inode *, struct dentry *);
    int (*mknod)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t, dev_t);
    int (*rename)(struct mnt_idmap *, struct inode *, struct dentry *,
                  struct inode *, struct dentry *, unsigned int);
    int (*setattr)(struct mnt_idmap *, struct dentry *, struct iattr *);
    int (*getattr)(struct mnt_idmap *, const struct path *, struct kstat *, u32, unsigned int);
    int (*permission)(struct mnt_idmap *, struct inode *, int);
    struct posix_acl *(*get_inode_acl)(struct inode *, int, bool);
    int (*tmpfile)(struct mnt_idmap *, struct inode *, struct file *, umode_t);
    int (*atomic_open)(struct inode *, struct dentry *, struct file *, unsigned, umode_t);
    int (*fileattr_set)(struct mnt_idmap *, struct dentry *, struct fileattr *);
    int (*fileattr_get)(struct dentry *, struct fileattr *);
};
코드 설명
  • lookup디렉터리에서 이름으로 자식 inode를 찾는 콜백입니다. d_lookup() 캐시 미스 시 호출되며, 디스크에서 디렉터리 엔트리를 읽어 d_splice_alias()로 dentry 캐시에 등록합니다. 경로 탐색(path_lookupat)의 핵심 단계입니다.
  • create / mkdir / mknod새 파일·디렉터리·특수 파일 생성 콜백입니다. 첫 번째 인자 mnt_idmap은 idmapped mount 지원을 위해 6.x에서 추가되었으며, 전달된 umode_t에 umask가 이미 적용된 상태입니다. 파일시스템은 디스크 inode를 할당하고 디렉터리 엔트리를 기록합니다.
  • rename마지막 인자 unsigned int flagsRENAME_NOREPLACE, RENAME_EXCHANGE, RENAME_WHITEOUT 플래그를 전달합니다. RENAME_EXCHANGE는 두 파일을 원자적으로 교환하며, renameat2() 시스템 콜에서 유래합니다.
  • setattr / getattrsetattrchmod, chown, truncate 등 속성 변경 시 호출됩니다. getattrstat()/statx()에서 호출되며, NFS 같은 파일시스템은 서버에 최신 속성을 질의합니다.
  • permissioninode_permission()에서 호출되는 파일시스템별 권한 검사 콜백입니다. NULL이면 generic_permission()이 대신 사용됩니다. include/linux/fs.h에 정의되어 있습니다.
  • atomic_openNFS 등 네트워크 파일시스템에서 lookup과 open을 하나의 RPC 호출로 통합하는 최적화 콜백입니다. 로컬 파일시스템은 보통 구현하지 않습니다.

주요 콜백 상세

콜백호출 시점인자 의미반환
lookup경로 해석 (path_lookup)부모 inode, 자식 dentry찾은 dentry 또는 NULL
createopen(O_CREAT), creat(2)부모 dir, 새 dentry, mode0 또는 -errno
linklink(2) 시스템콜기존 dentry, 새 부모, 새 dentry0 또는 -errno
unlinkunlink(2) 시스템콜부모 inode, 대상 dentry0 또는 -errno
symlinksymlink(2) 시스템콜부모 inode, 새 dentry, 타겟 경로0 또는 -errno
mkdirmkdir(2) 시스템콜부모 inode, 새 dentry, mode0 또는 -errno
renamerename(2) / renameat2(2)old/new 부모, old/new dentry, flags0 또는 -errno
setattrchmod, chown, truncatedentry, 변경할 속성 (iattr)0 또는 -errno
getattrstat, statx경로, kstat 결과, 요청 마스크0 또는 -errno
permission접근 권한 검사inode, 접근 마스크 (MAY_READ 등)0 또는 -EACCES
tmpfileO_TMPFILE open부모 inode, file, mode0 또는 -errno
atomic_openNFS 등 lookup+open 최적화부모, dentry, file, open 플래그0 또는 -errno
ℹ️

mnt_idmap 파라미터: 커널 6.x부터 inode 연산 콜백에 struct mnt_idmap * 파라미터가 추가되었습니다. 이는 idmapped mounts 기능을 지원하기 위한 것으로, 컨테이너(Container) 환경에서 마운트별로 UID/GID 매핑(Mapping)을 다르게 적용할 수 있습니다. 기존의 mnt_userns 포인터를 대체하며, 매핑이 필요 없는 경우 nop_mnt_idmap이 전달됩니다.

Casefold: 대소문자 무시 조회

커널 5.2부터 ext4, 5.11부터 F2FS에서 대소문자 무시(case-insensitive) 디렉터리를 지원합니다. 이 기능은 inode_operations의 lookup 콜백에서 Unicode 정규화(NFKD + casefold)를 적용하여 이름 비교를 수행합니다.

/* include/linux/fs.h — casefold 관련 필드 */
struct inode {
    /* ... */
    unsigned int i_flags;  /* S_CASEFOLD 플래그 포함 */
    /* ... */
};

/* S_CASEFOLD 매크로 — inode에 casefold 활성화 여부 */
#define S_CASEFOLD    (1 << 15)
#define IS_CASEFOLDED(inode) ((inode)->i_flags & S_CASEFOLD)

/* fs/libfs.c — casefold를 적용한 이름 비교 */
int generic_ci_d_compare(const struct dentry *dentry,
                        unsigned int len, const char *str,
                        const struct qstr *name)
{
    const struct dentry *parent = READ_ONCE(dentry->d_parent);
    const struct inode *dir = d_inode_rcu(parent);
    const struct unicode_map *um = dir->i_sb->s_encoding;

    if (!dir || !IS_CASEFOLDED(dir))
        return 1;  /* 일반 비교 폴백 */

    /* Unicode NFKD+casefold로 정규화하여 비교 */
    return utf8_strncasecmp(um, name, &(struct qstr)QSTR_INIT(str, len));
}
항목설명
활성화tune2fs -O casefold /dev/sdX (ext4), chattr +F dir/로 디렉터리별 설정
인코딩mkfs.ext4 -O casefold -E encoding=utf8-12.1.0 (Unicode 버전 지정)
dentry 캐시casefold 디렉터리의 dentry는 d_compare/d_hash를 casefold 버전으로 교체
strict 모드encoding_flags=strict — 유효하지 않은 UTF-8 시퀀스 거부
호환성casefold 파일시스템은 커널 5.2 미만에서 마운트(Mount) 불가 (호환 플래그)
NFSNFS 서버가 casefold를 인지하지 못하면 대소문자 불일치 가능
Casefold 경로 조회 흐름 open("README.txt") dcache: d_hash + d_compare IS_CASEFOLDED? Yes utf8_strncasecmp() "readme.TXT" = 매칭! No 바이트 단위 비교 Unicode NFKD 정규화: ñ(U+00F1) = n(U+006E) + ◌̃(U+0303) — 합성/분해 형태 모두 매칭
ℹ️

Windows/macOS 호환성: NTFS와 APFS는 기본적으로 대소문자를 무시합니다. Samba/Wine을 통해 Windows 애플리케이션과 상호 운용하거나, 크로스 플랫폼 프로젝트에서 파일명 충돌을 방지하려면 casefold가 유용합니다. 다만 Git 등 대소문자 구분에 의존하는 도구와 충돌할 수 있으므로 주의가 필요합니다.

inode 캐시

VFS는 inode 캐시(icache)를 유지하여 디스크 접근을 최소화합니다. 사용 중이지 않은 inode는 LRU 리스트에 들어가며, 메모리 압력 시 회수됩니다.

경로 조회 dentry 캐시 inode 캐시 page cache 핵심 캐시 포인트 dentry는 이름을 캐시하고 inode는 메타데이터를 캐시합니다. inode의 i_mapping이 page cache와 연결되어 실제 데이터 페이지를 관리합니다.
ℹ️

cat /proc/sys/fs/inode-nr로 현재 할당된 inode 수와 free inode 수를 확인할 수 있습니다. slabtop에서 inode_cache 항목도 참고하세요.

inode 파일 유형

매크로유형설명
S_IFREG일반 파일데이터 저장
S_IFDIR디렉터리다른 파일들의 목록
S_IFLNK심볼릭 링크다른 경로 참조
S_IFBLK블록 디바이스디스크 등
S_IFCHR문자 디바이스터미널, 시리얼 등
S_IFIFOFIFO (named pipe)프로세스(Process)간 통신
S_IFSOCK소켓(Socket)Unix domain socket

ext4 inode 확장

각 파일시스템은 VFS inode를 자체 구조체에 임베드합니다. ext4의 경우:

struct ext4_inode_info {
    __le32  i_data[15];     /* block pointers or extent tree */
    __u32   i_flags;
    ext4_fsblk_t i_file_acl;
    /* ... ext4 specific fields ... */
    struct inode vfs_inode;  /* VFS inode 임베드 */
};

/* VFS inode에서 ext4 inode로 변환 */
static inline struct ext4_inode_info *EXT4_I(struct inode *inode)
{
    return container_of(inode, struct ext4_inode_info, vfs_inode);
}

inode 생명주기

inode는 할당 → 초기화 → 사용 → 해제의 생명주기를 가집니다. 참조 카운트(i_count)와 하드 링크 수(i_nlink)가 모두 0이 되면 삭제됩니다.

new_inode insert hash 활성 사용 iput 감소 evict 링크 또는 참조가 남아 있으면 재사용 경로로 복귀 삭제 조건 i_nlink == 0 이고 i_count == 0 일 때만 실제 inode 회수가 일어납니다.
/* 새 inode 할당 (파일시스템별 alloc_inode 호출) */
struct inode *inode = new_inode(sb);

/* inode 번호 할당 및 해시 테이블에 삽입 */
inode->i_ino = get_next_ino();
insert_inode_hash(inode);

/* 초기 속성 설정 */
inode->i_mode = S_IFREG | 0644;
inode_init_owner(idmap, inode, dir, mode);
inode->i_op = &myfs_inode_ops;
inode->i_fop = &myfs_file_ops;
inode->i_mapping->a_ops = &myfs_aops;

/* 참조 카운트 관리 */
ihold(inode);     /* i_count++ (참조 획득) */
iput(inode);      /* i_count-- (참조 해제, 0이면 evict) */

/* inode 삭제 경로 */
/* i_nlink == 0 && i_count == 0 → evict_inode() 호출 */
코드 설명
  • new_inode(sb)fs/inode.c에 정의된 VFS 함수로, alloc_inode(sb)를 호출하여 slab에서 inode를 할당하고, sb->s_inodes 리스트에 추가합니다. 반환된 inode의 i_state는 0이며, 호출자가 직접 필드를 초기화해야 합니다.
  • get_next_ino()percpu 카운터를 이용해 고유한 inode 번호를 빠르게 할당합니다. tmpfs, procfs 등 디스크 기반이 아닌 의사 파일시스템에서 사용합니다. 디스크 기반 파일시스템은 자체 inode 번호 할당 로직을 가집니다.
  • insert_inode_hash(inode)전역 inode_hashtable에 inode를 삽입합니다. 이후 iget_locked()find_inode_fast()에서 이 inode를 캐시 히트로 찾을 수 있게 됩니다.
  • inode_init_owner()새 inode의 i_uid, i_gid, i_mode를 부모 디렉터리와 현재 프로세스의 자격 증명(credentials) 기반으로 설정합니다. setgid 디렉터리에서는 부모의 GID를 상속합니다.
  • ihold / iputihold()atomic_inc(&inode->i_count)로 참조 카운트를 증가시키고, iput()는 감소시킵니다. i_count가 0이 되면 i_nlink 상태에 따라 LRU 추가 또는 즉시 evict()를 수행합니다.

파일시스템별 inode 할당

각 파일시스템은 alloc_inode()free_inode()를 구현하여 자체 확장 inode를 관리합니다:

static struct kmem_cache *myfs_inode_cachep;

static struct inode *myfs_alloc_inode(struct super_block *sb)
{
    struct myfs_inode_info *mi;
    mi = alloc_inode_sb(sb, myfs_inode_cachep, GFP_KERNEL);
    if (!mi)
        return NULL;
    /* fs-specific 필드 초기화 */
    mi->i_disksize = 0;
    return &mi->vfs_inode;
}

static void myfs_free_inode(struct inode *inode)
{
    kmem_cache_free(myfs_inode_cachep, MYFS_I(inode));
}

static const struct super_operations myfs_sops = {
    .alloc_inode  = myfs_alloc_inode,
    .free_inode   = myfs_free_inode,
    .write_inode  = myfs_write_inode,
    .evict_inode  = myfs_evict_inode,
};
코드 설명
  • myfs_alloc_inode파일시스템 고유의 확장 inode 구조체(myfs_inode_info)를 전용 kmem_cache에서 할당합니다. alloc_inode_sb()는 memcg(메모리 cgroup) 계정을 올바르게 처리하는 래퍼입니다. 반환값은 임베드된 vfs_inode 필드의 주소입니다.
  • return &mi->vfs_inodeVFS는 struct inode 포인터로만 작업합니다. 파일시스템이 확장 필드에 접근할 때는 container_of() 매크로(보통 MYFS_I(inode)로 래핑)를 사용하여 외부 구조체 포인터를 역산합니다.
  • myfs_free_inodeevict()destroy_inode() 경로에서 호출됩니다. MYFS_I(inode)로 확장 구조체 포인터를 얻어 kmem_cache_free()로 해제합니다. RCU 지연 해제가 필요하면 free_inode 대신 destroy_inode + call_rcu() 패턴을 사용합니다.
  • super_operations 등록.alloc_inode.free_inode는 짝으로 구현합니다. .write_inode는 dirty inode를 디스크에 기록하고, .evict_inode는 inode가 메모리에서 제거될 때 파일시스템별 정리(블록 해제, 저널 처리 등)를 수행합니다.

dentry-inode 연결 함수

새로 생성하거나 디스크에서 읽은 inode를 dentry에 연결하는 것은 VFS 경로 조회의 핵심 단계입니다. 연결 방식에 따라 여러 함수가 제공되며, 잘못된 함수를 사용하면 경로 조회 실패, 중복 dentry, NFS 불일치 등이 발생합니다.

d_instantiate() / d_instantiate_new()

/* fs/dcache.c — d_instantiate(): dentry에 inode 연결 */
void d_instantiate(struct dentry *entry, struct inode *inode)
{
    if (inode) {
        security_d_instantiate(entry, inode);  /* LSM 알림 */
    }
    spin_lock(&entry->d_lock);
    __d_set_inode_and_type(entry, inode, add_flags);
    /* dentry의 d_inode = inode 설정 */
    /* inode의 i_dentry 리스트에 이 dentry 추가 (하드 링크 추적) */
    if (inode)
        hlist_add_head(&entry->d_u.d_alias, &inode->i_dentry);
    spin_unlock(&entry->d_lock);
}

/* d_instantiate_new(): d_instantiate + unlock_new_inode 통합 */
void d_instantiate_new(struct dentry *entry, struct inode *inode)
{
    BUG_ON(!(inode->i_state & I_NEW));  /* I_NEW 상태 필수 */
    d_instantiate(entry, inode);
    unlock_new_inode(inode);             /* I_NEW 해제 + 대기자 깨우기 */
}
코드 설명
  • d_instantiate — d_inode 설정dentry의 d_inode 포인터를 inode로 설정합니다. 이 연결이 완료되면 경로 조회에서 이 dentry를 통해 inode에 접근할 수 있습니다. NULL inode를 전달하면 negative dentry(파일 없음 캐시)가 됩니다.
  • d_instantiate — i_dentry 리스트inode의 i_dentry hlist에 dentry를 추가합니다. 하드 링크가 여러 개면 하나의 inode에 여러 dentry가 연결됩니다. d_find_alias(inode)로 inode에서 dentry를 역검색할 수 있습니다.
  • d_instantiate_newcreate, mkdir, mknod 등 새 inode를 생성하는 콜백에서 사용합니다. d_instantiate()unlock_new_inode()를 원자적으로 수행하여, inode가 dentry에 연결되기 전에 다른 스레드가 접근하는 경쟁 조건을 방지합니다.
함수사용 시점I_NEW 처리
d_instantiate_new(dentry, inode)create, mkdir, mknod — 새 inode 생성I_NEW 해제 포함
d_instantiate(dentry, inode)link — 기존 inode에 새 이름 추가I_NEW 해제 안 함
d_add(dentry, inode)단순 FS (ramfs) — instantiate + rehashI_NEW 해제 안 함

d_splice_alias()

lookup 콜백에서 디스크에서 찾은 inode를 dentry에 연결할 때 사용합니다. 동일 inode에 대한 기존 dentry(alias)가 있으면 그것을 반환하고, 없으면 새로 연결합니다.

/* fs/dcache.c — d_splice_alias() 핵심 로직 */
struct dentry *d_splice_alias(struct inode *inode,
                              struct dentry *dentry)
{
    if (!inode) {
        /* 파일 없음 → negative dentry로 캐시 */
        d_add(dentry, NULL);
        return NULL;
    }

    /* 디렉터리 inode: 기존 alias 검색 */
    if (S_ISDIR(inode->i_mode)) {
        struct dentry *alias = __d_find_alias(inode);
        if (alias) {
            /* 기존 alias가 있으면 그것을 반환 (splice) */
            /* 전달된 dentry는 negative로 유지 */
            __d_move(alias, dentry, false);
            return alias;
        }
    }

    /* 비디렉터리이거나 기존 alias 없음 → 직접 연결 */
    d_add(dentry, inode);
    return NULL;
}

/* lookup 콜백에서의 전형적인 사용 */
static struct dentry *myfs_lookup(struct inode *dir,
                                   struct dentry *dentry,
                                   unsigned int flags)
{
    struct inode *inode = NULL;
    ino_t ino;

    /* 디스크에서 이름으로 inode 번호 검색 */
    ino = myfs_find_entry(dir, &dentry->d_name);
    if (ino)
        inode = myfs_iget(dir->i_sb, ino);

    return d_splice_alias(inode, dentry);
}
코드 설명
  • d_splice_alias — NULL inode파일이 존재하지 않으면 negative dentry를 캐시합니다. 다음에 같은 이름을 조회할 때 디스크 접근 없이 즉시 "파일 없음"을 반환합니다.
  • d_splice_alias — 디렉터리 alias디렉터리 inode는 하나의 dentry만 가질 수 있습니다 (하드 링크 금지). 기존 alias가 있으면 __d_move()로 기존 dentry를 새 위치로 이동시킵니다. NFS에서 서버가 같은 디렉터리를 다른 이름으로 보고할 때 중요합니다.
  • d_splice_alias — d_addd_add()d_instantiate() + d_rehash()의 조합입니다. dentry에 inode를 연결하고 dcache 해시 테이블에 등록합니다.

d_make_root()

파일시스템 마운트 시 루트(/) 디렉터리의 dentry를 생성합니다.

/* fs/dcache.c — d_make_root() */
struct dentry *d_make_root(struct inode *root_inode)
{
    struct dentry *dentry;

    if (!root_inode)
        return NULL;

    /* "/" 이름의 dentry 할당 */
    dentry = d_alloc_anon(root_inode->i_sb);
    if (!dentry) {
        iput(root_inode);  /* 실패 시 inode 해제 */
        return NULL;
    }
    d_instantiate(dentry, root_inode);
    return dentry;
}

/* fill_super에서의 사용 */
static int myfs_fill_super(struct super_block *sb, ...)
{
    struct inode *root_inode;

    root_inode = myfs_iget(sb, MYFS_ROOT_INO);
    if (IS_ERR(root_inode))
        return PTR_ERR(root_inode);

    sb->s_root = d_make_root(root_inode);
    if (!sb->s_root)
        return -ENOMEM;  /* d_make_root가 이미 iput() 호출 */
    return 0;
}
ℹ️

d_make_root 실패 시 iput: d_make_root()가 실패하면 전달된 inode에 대해 iput()을 자동으로 호출합니다. 따라서 호출자가 별도로 iput()을 호출하면 이중 해제(Double-free)가 발생합니다. 이 패턴은 커널의 "ownership transfer" 관례를 따릅니다.

O_TMPFILE: 이름 없는 inode 생성

커널 3.11에서 도입된 O_TMPFILE 플래그는 디렉터리에 연결되지 않은 inode를 생성합니다. dentry 없이 inode와 file 구조체만 존재하므로, 파일명 충돌·레이스 컨디션·보안 노출 없이 안전하게 임시 파일을 사용할 수 있습니다.

/* 사용자 공간 — O_TMPFILE 사용 패턴 */
int fd = open("/tmp", O_TMPFILE | O_RDWR, 0600);
/* fd로 데이터 기록 */
write(fd, data, len);

/* 패턴 1: 작업 완료 후 원자적으로 이름 부여 (linkat) */
char procpath[64];
snprintf(procpath, sizeof(procpath), "/proc/self/fd/%d", fd);
linkat(AT_FDCWD, procpath, AT_FDCWD, "/tmp/final.dat", AT_SYMLINK_FOLLOW);

/* 패턴 2: 이름 부여 없이 close → inode 자동 삭제 */
close(fd);  /* nlink==0, count==0 → evict */
/* 커널 내부 — O_TMPFILE 경로 (fs/namei.c) */
static int do_tmpfile(struct nameidata *nd, unsigned flags,
                      const struct open_flags *op,
                      struct file *file)
{
    struct inode *dir = nd->path.dentry->d_inode;

    /* 1. 디렉터리의 inode_operations->tmpfile 콜백 확인 */
    if (!dir->i_op->tmpfile)
        return -EOPNOTSUPP;

    /* 2. FS별 tmpfile 콜백 호출 → 새 inode 할당 */
    /*    nlink=0인 상태로 생성, dentry 연결 안 함 */
    error = dir->i_op->tmpfile(idmap, dir, file, op->mode);

    /* 3. d_tmpfile()으로 특수 dentry 연결 */
    /*    해시 테이블 미등록, 부모 미연결 */
    return error;
}

/* ext4의 tmpfile 구현 — fs/ext4/namei.c */
static int ext4_tmpfile(struct mnt_idmap *idmap,
                        struct inode *dir,
                        struct file *file, umode_t mode)
{
    struct inode *inode;

    /* 1. 새 inode 할당 + 초기화 */
    inode = ext4_new_inode_start_handle(idmap, dir, mode, ...);

    /* 2. nlink = 0 (orphan 상태) */
    /*    저널의 orphan 리스트에 등록 → 크래시 시 자동 정리 */
    ext4_orphan_add(handle, inode);

    /* 3. 연산 테이블 연결 */
    inode->i_op  = &ext4_file_inode_operations;
    inode->i_fop = &ext4_file_operations;

    /* 4. d_tmpfile()로 file에 연결 */
    d_tmpfile(file, inode);
    return finish_open_simple(file, 0);
}
O_TMPFILE inode 생명주기 open(O_TMPFILE) nlink=0, orphan list 활성 사용 read/write/mmap linkat? Yes nlink++ → 영구 파일 orphan 리스트에서 제거 No close(fd) nlink=0, count=0 evict → 데이터+inode 해제 크래시 복구 저널 orphan 리스트에 기록되어 있으므로, fsck/마운트 시 자동으로 nlink=0 inode를 정리합니다.
비교mkstemp()O_TMPFILE
이름 노출파일명이 디렉터리에 노출됨이름 없음 (디렉터리 탐색 불가)
레이스 컨디션생성-삭제 사이 윈도우 존재없음 (처음부터 이름 없음)
원자적(Atomic) 게시rename으로 구현 (기존 파일 필요)linkat으로 원자적 게시
크래시 안전잔여 파일 수동 정리 필요orphan list → 자동 정리
커널 요구모든 커널3.11+, FS가 tmpfile 콜백 구현 필요
지원 FS모든 FSext4, XFS, Btrfs, tmpfs 등
💡

실전 패턴 — 원자적 파일 교체: O_TMPFILE으로 임시 파일을 생성하고, fsync()로 데이터를 디스크에 확정한 후, linkat()으로 최종 경로에 원자적으로 게시합니다. 이 패턴은 설정 파일, 데이터베이스 WAL, 패키지 매니저 등에서 쓰기 도중 크래시에 안전한 업데이트를 보장합니다. rename() 기반보다 더 견고합니다 — 중간 상태의 파일명이 존재하지 않기 때문입니다.

확장 속성 (xattr)

inode에 추가적인 이름-값 쌍 메타데이터를 저장합니다. 보안 레이블(SELinux), ACL, 사용자 데이터 등에 사용됩니다.

네임스페이스(Namespace)접두사용도
useruser.*사용자 정의 메타데이터
securitysecurity.*SELinux, AppArmor 레이블
systemsystem.posix_acl_*POSIX ACL
trustedtrusted.*관리자 전용 메타데이터
/* xattr 핸들러 등록 */
static const struct xattr_handler myfs_xattr_user_handler = {
    .prefix = XATTR_USER_PREFIX,
    .get    = myfs_xattr_get,
    .set    = myfs_xattr_set,
};

static const struct xattr_handler *myfs_xattr_handlers[] = {
    &myfs_xattr_user_handler,
    &myfs_xattr_security_handler,
    NULL,
};

sb->s_xattr = myfs_xattr_handlers;
ext4 xattr 저장 레이아웃 디스크 inode (256바이트) 고정 필드 (128바이트) i_mode, i_uid, i_size, i_blocks, ... 확장 필드 (i_extra_isize) crtime, projid, ... 인라인 xattr 공간 256 - 128 - i_extra_isize 바이트 security.selinux system.posix_acl 공간 부족 시 외부 xattr 블록 (i_file_acl) xattr 헤더 (magic: 0xEA020000) entry[0]: user.myattr = "value1" entry[1]: trusted.overlay.opaque = "y" 파일시스템별 xattr 저장 ext4: 인라인 → 외부 블록 (1개) XFS: attr fork (local → extents → B+tree) Btrfs: 별도 B-tree item (크기 무제한) tmpfs: 커널 메모리 (simple_xattr 리스트)
# xattr 조작 명령어
# 설정
setfattr -n user.description -v "project config" /path/to/file
# 조회
getfattr -n user.description /path/to/file
# 전체 나열
getfattr -d -m ".*" /path/to/file
# 삭제
setfattr -x user.description /path/to/file

# SELinux 보안 컨텍스트 확인
getfattr -n security.selinux /path/to/file

# xattr 크기 제한 확인
# ext4: 개별 값 최대 64KB, 총 1블록(4KB) 제한 (인라인+외부)
# XFS: 개별 64KB, 총 제한 없음 (B+tree 확장)
# Btrfs: 개별 64KB, 총 제한 없음

inode 이벤트 감시 (inotify/fanotify)

inode 변경 사항을 유저스페이스에 알리는 커널 메커니즘입니다:

/* 커널 내부: 파일 변경 시 이벤트 발생 */
fsnotify_modify(file);        /* 파일 내용 수정 */
fsnotify_access(file);        /* 파일 읽기 */
fsnotify_create(dir, dentry); /* 파일 생성 */
fsnotify_delete(dir, dentry); /* 파일 삭제 */

/* VFS 계층에서 자동 호출됨 (vfs_write, vfs_read 등) */
인터페이스대상특징
inotify파일/디렉터리간편한 API, 재귀 감시 미지원
fanotify마운트/파일시스템전체 마운트 감시, 접근 제어(Access Control) 가능

Btrfs의 inode 확장

Btrfs는 전통적 inode 번호 대신 (subvolume_id, objectid) 쌍으로 파일을 식별합니다:

struct btrfs_inode {
    struct inode vfs_inode;

    u64 root_objectid;        /* subvolume ID */
    struct btrfs_key location; /* (objectid, type, offset) */

    u64 disk_i_size;          /* 디스크 상의 크기 */
    u64 generation;           /* CoW 트랜잭션 세대 */
    u64 flags;                /* NODATASUM, COMPRESS 등 */

    struct btrfs_ordered_inode_tree ordered_tree;
    struct list_head delalloc_inodes;
};
💡

stat --format=%i로 inode 번호를, getfattr -d로 확장 속성을 확인할 수 있습니다. Btrfs에서는 btrfs inspect-internal inode-resolve로 inode 번호에서 경로를 역추적할 수 있습니다.

파일시스템별 inode 고려사항

앞서 ext4와 Btrfs의 inode 확장을 살펴보았습니다. 다음으로 파일시스템 설계 시 고려해야 할 inode 관련 공통 사항 — inode 고갈, 크기 제한, 성능 특성 등을 정리합니다.

inode 고갈 문제

# inode 사용량 확인
df -i
# Filesystem        Inodes   IUsed   IFree IUse% Mounted on
# /dev/sda1       6553600  234567 6319033    4% /

# ext4: inode 수는 mkfs 시 결정 (이후 변경 불가!)
mkfs.ext4 -N 10000000 /dev/sda1   # inode 천만 개
mkfs.ext4 -i 4096 /dev/sda1       # 4KB당 1개 inode (소파일 많은 환경)

# XFS: inode는 동적 할당 (고갈 문제 적음)
# Btrfs: inode 번호 동적 (고갈 없음)
inode 고갈은 디스크 여유 공간이 있어도 파일 생성 불가를 초래합니다. 컨테이너 환경, 메일 서버, 캐시 디렉토리 등 소파일이 대량 생성되는 시스템에서 주의가 필요합니다. ext4에서는 생성 시 inode 수가 고정되므로, 워크로드를 예측하여 mkfs 옵션을 설정해야 합니다.

inode 크기와 인라인 데이터

파일시스템기본 inode 크기인라인 데이터xattr 인라인
ext4 256바이트 소파일 데이터를 inode 내 저장 (inline_data 옵션) 잔여 공간에 xattr 저장 (별도 블록 할당 불필요)
XFS 512바이트 인라인 데이터 지원 attr fork에 inline xattr
Btrfs 가변 소파일은 메타데이터 B-tree에 인라인 xattr는 별도 아이템

inode 타임스탬프와 성능

/* inode의 세 가지 타임스탬프 */
struct inode {
    struct timespec64 __i_atime;  /* 마지막 접근 시간 (read) */
    struct timespec64 __i_mtime;  /* 마지막 수정 시간 (write) */
    struct timespec64 __i_ctime;  /* 마지막 변경 시간 (메타데이터) */
    /* ext4는 crtime (생성 시간)도 저장 — statx()로 조회 */
};

/* atime 마운트 옵션과 성능 영향 */
/* noatime   — atime 갱신 완전 비활성화 (최고 성능) */
/* relatime  — mtime보다 오래된 경우에만 atime 갱신 (기본값) */
/* strictatime — 매 접근마다 갱신 (성능 나쁨) */
/* lazytime  — atime을 메모리에서만 갱신, 주기적으로 디스크 기록 (5.6+) */

하드 링크는 여러 dentry가 동일한 inode를 가리키는 구조입니다. 파일의 실체(데이터와 메타데이터)는 하나이고, 이름만 여러 개입니다. i_nlink 필드가 연결된 dentry 수를 추적하며, 이 값이 0이 되고 참조 카운트도 0이면 inode가 해제됩니다.

하드 링크: 여러 dentry → 하나의 inode dentry: "report.txt" /home/user/report.txt dentry: "backup.txt" /home/user/backup.txt dentry: "link3.txt" /tmp/link3.txt inode #12345 i_nlink = 3 i_size = 4096 i_mode = -rw-r--r-- i_uid = 1000 데이터 블록 blk 0 blk 1 blk 2 모든 dentry가 같은 데이터 참조 unlink("backup.txt") 시 dentry 제거 + i_nlink-- (3→2). 데이터는 그대로. i_nlink=0이 되어야 삭제.
/* 하드 링크 생성 — fs/namei.c: vfs_link() */
int vfs_link(struct dentry *old_dentry,
            struct mnt_idmap *idmap,
            struct inode *dir,
            struct dentry *new_dentry,
            struct inode **delegated_inode)
{
    struct inode *inode = d_inode(old_dentry);

    /* 1. 제한 사항 검사 */
    if (S_ISDIR(inode->i_mode))
        return -EPERM;  /* 디렉터리 하드 링크 금지 */

    if (inode->i_nlink >= inode->i_sb->s_max_links)
        return -EMLINK; /* 최대 링크 수 초과 */

    /* 2. 보안 검사 */
    error = security_inode_link(old_dentry, dir, new_dentry);

    /* 3. FS별 link 콜백 호출 */
    error = dir->i_op->link(old_dentry, dir, new_dentry);
    /* → inode->i_nlink++ */
    /* → 새 dentry를 dir에 추가 */

    /* 4. fsnotify 이벤트 발생 */
    fsnotify_link(dir, inode, new_dentry);
    return 0;
}
제한 사항이유파일시스템별
디렉터리 하드 링크 금지디렉터리 그래프에 순환이 생기면 fsck, find, 경로 해석이 무한 루프모든 FS (커널 수준 거부)
FS 경계 불가inode 번호는 FS 내에서만 고유, 다른 FS는 같은 번호 사용 가능모든 FS
최대 링크 수i_nlink 필드 크기 제한ext4: 65,000 (dir_nlink 시 무제한), XFS: 무제한, Btrfs: 65,535
protected_hardlinks보안: 소유하지 않은 파일에 하드 링크 제한sysctl fs.protected_hardlinks=1 (기본 활성)

하드 링크와 달리 reflink는 별도의 inode를 생성하되 데이터 extent를 공유합니다. 쓰기 시 CoW(Copy-on-Write)로 분리되어, 스냅샷과 공간 효율적 복사의 기반 기술입니다.

# reflink 복사 (Btrfs, XFS 4.16+)
cp --reflink=always source.img dest.img
# → 즉시 완료 (데이터 복사 없음, extent 참조만 공유)
# → dest.img에 쓰기 시 해당 extent만 CoW 분리

# reflink 지원 여부 확인
xfs_info /mnt/data | grep reflink
# reflink=1 이면 지원

# 하드 링크 vs reflink vs 일반 복사
# 하드 링크:  같은 inode, 같은 데이터 (모든 변경 공유)
# reflink:   다른 inode, extent 공유 (쓰기 시 분리)
# cp:        다른 inode, 데이터 완전 복사

struct inode 필드별 상세 분석

struct inode는 약 40개 이상의 필드를 가지며, 파일 메타데이터의 모든 측면을 관리합니다. 여기서는 핵심 필드를 그룹별로 분석합니다.

식별 필드 (i_ino, i_sb, i_mode)

필드타입설명접근 함수
i_inounsigned longinode 번호 — 파일시스템 내 고유 식별자stat(2)st_ino
i_sbstruct super_block *소속 superblock 포인터 — 파일시스템 컨텍스트내부 전용
i_modeumode_t파일 유형(상위 4비트) + 권한(하위 12비트)S_ISREG(), S_ISDIR()
i_flagsunsigned int마운트/FS 레벨 플래그 (S_SYNC, S_IMMUTABLE 등)IS_IMMUTABLE()
i_opflagsunsigned shortVFS 내부 최적화 플래그 (IOP_FASTPERM 등)내부 전용
/* i_mode 비트 레이아웃 (16비트) */
/*  ┌─ 파일 유형 (4비트) ─┐┌─ setuid/gid/sticky ─┐┌─ rwx rwx rwx ─┐ */
/*  15  14  13  12          11   10    9             8..6 5..3 2..0    */

#define S_IFMT   00170000  /* 유형 마스크 */
#define S_IFREG  0100000   /* 일반 파일 */
#define S_IFDIR  0040000   /* 디렉터리 */
#define S_IFLNK  0120000   /* 심볼릭 링크 */
#define S_IFBLK  0060000   /* 블록 디바이스 */
#define S_IFCHR  0020000   /* 캐릭터 디바이스 */
#define S_IFIFO  0010000   /* FIFO */
#define S_IFSOCK 0140000   /* 소켓 */

/* 유형 검사 매크로 */
#define S_ISREG(m)  (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m)  (((m) & S_IFMT) == S_IFDIR)
#define S_ISLNK(m)  (((m) & S_IFMT) == S_IFLNK)

/* 권한 비트 */
#define S_ISUID  0004000   /* set-user-ID */
#define S_ISGID  0002000   /* set-group-ID */
#define S_ISVTX  0001000   /* sticky bit */
#define S_IRWXU  00700     /* owner rwx */
#define S_IRWXG  00070     /* group rwx */
#define S_IRWXO  00007     /* others rwx */

소유권/크기 필드

필드타입설명관련 시스템콜
i_uidkuid_t소유자 UID (커널 내부 네임스페이스 인식 타입)chown(2)
i_gidkgid_t소유자 GIDchgrp(2)
i_sizeloff_t파일 크기 (바이트). 최대 2^63-1truncate(2), stat(2)
i_blocksblkcnt_t할당된 512바이트 블록 수 (실제 디스크 사용량)stat(2)st_blocks
i_bytesunsigned shorti_blocks에 포함되지 않는 추가 바이트내부 전용
i_nlinkunsigned int하드 링크 수stat(2)st_nlink
ℹ️

kuid_t/kgid_t와 사용자 네임스페이스: kuid_tkgid_t는 단순 정수가 아닌 구조체 래퍼입니다. 사용자 네임스페이스(user namespace) 환경에서 UID/GID 매핑이 필요하기 때문입니다. from_kuid(), make_kuid() 등의 변환 함수를 사용하여 네임스페이스 간 변환을 수행합니다. 컨테이너 환경에서는 호스트의 UID 1000이 컨테이너 내부에서 root(0)로 매핑될 수 있습니다.

연산 테이블 필드

struct inode는 세 가지 연산 테이블을 참조하여 파일시스템별 다형성을 구현합니다:

필드구조체역할대표 콜백
i_opstruct inode_operationsinode 자체에 대한 연산lookup, create, mkdir, unlink
i_fopstruct file_operations열린 파일에 대한 연산read, write, mmap, fsync
i_mapping->a_opsstruct address_space_operations페이지 캐시 I/O 연산read_folio, writepages, dirty_folio
/* i_op vs i_fop 구분 원리:
 * - i_op: 파일을 "찾고/만들고/삭제"하는 디렉터리 수준 연산
 * - i_fop: 파일을 "열고/읽고/쓰는" 데이터 수준 연산
 *
 * 예: open("/home/user/test.txt", O_RDONLY)
 * 1. VFS가 /home의 i_op->lookup으로 "user" dentry 찾기
 * 2. VFS가 /home/user의 i_op->lookup으로 "test.txt" dentry 찾기
 * 3. test.txt inode의 i_fop을 struct file에 복사
 * 4. 이후 read(fd, ...)는 file->f_op->read_iter 호출
 */

/* 디렉터리 inode의 전형적인 설정 */
static const struct inode_operations myfs_dir_iops = {
    .lookup  = myfs_lookup,
    .create  = myfs_create,
    .mkdir   = myfs_mkdir,
    .unlink  = myfs_unlink,
    .rmdir   = myfs_rmdir,
    .rename  = myfs_rename,
};

/* 일반 파일 inode의 전형적인 설정 */
static const struct inode_operations myfs_file_iops = {
    .setattr = myfs_setattr,
    .getattr = myfs_getattr,
};

static const struct file_operations myfs_file_fops = {
    .read_iter   = generic_file_read_iter,
    .write_iter  = generic_file_write_iter,
    .mmap        = generic_file_mmap,
    .fsync       = generic_file_fsync,
    .splice_read = filemap_splice_read,
    .llseek      = generic_file_llseek,
    .open        = generic_file_open,
};
struct inode 메모리 레이아웃 struct inode i_mode (umode_t) | i_opflags i_uid (kuid_t) | i_gid (kgid_t) i_flags | i_ino (unsigned long) *i_op (inode_operations) *i_fop (file_operations) *i_sb (super_block) *i_mapping (address_space) i_size (loff_t) | i_blocks i_count (atomic_t) | i_nlink __i_atime | __i_mtime | __i_ctime i_lock (spinlock_t) | i_state i_hash | i_io_list | i_lru i_data (address_space, embedded) union { *i_pipe, *i_cdev, *i_link } inode_operations file_operations address_space super_block 값 필드 (인라인) 포인터 필드 (외부 참조)

잠금 및 상태 필드

필드타입설명
i_lockspinlock_tinode 필드 보호용 스핀락(Spinlock). i_state, i_count 등의 변경 시 사용
i_rwsemstruct rw_semaphore파일 데이터 접근 직렬화(Serialization). read/write/truncate 시 사용
i_stateunsigned longinode 상태 플래그 (I_NEW, I_DIRTY_* 등)
i_hashstruct hlist_nodeinode 해시 테이블 연결
i_io_liststruct list_headwriteback I/O 리스트 연결
i_lrustruct list_headLRU 리스트 연결 (미사용 inode 회수용)
i_sb_liststruct list_headsuperblock의 전체 inode 리스트
i_wb_liststruct list_headwriteback 대기 리스트
i_rwsem 잠금 순서: VFS는 엄격한 잠금 순서를 요구합니다. 다중 inode를 잠글 때는 항상 부모 디렉터리 → 자식 순서를 따라야 하며, rename()에서는 두 디렉터리를 inode 주소 순서로 잠급니다 (lock_rename()). 순서를 어기면 데드락이 발생합니다.

inode 할당 과정

새로운 inode가 생성되거나 디스크에서 읽힐 때의 할당 과정을 추적합니다. VFS는 여러 할당 경로를 제공하며, 각각 다른 사용 사례에 최적화되어 있습니다.

주요 할당 함수

함수용도해시 삽입I_NEW 설정
new_inode(sb)새 파일 생성 (create, mkdir)수동아니오
new_inode_pseudo(sb)의사 파일시스템 (pipe, socket)삽입 안 함아니오
iget_locked(sb, ino)디스크에서 inode 읽기 (번호 기반)자동
iget5_locked(sb, hash, test, set, data)커스텀 비교 함수로 inode 검색/생성자동
ilookup(sb, ino)캐시에서만 검색 (할당 안 함)검색만해당 없음
/* ===== 경로 1: 새 파일 생성 (예: create 시스템콜) ===== */
static int myfs_create(struct mnt_idmap *idmap,
                       struct inode *dir,
                       struct dentry *dentry,
                       umode_t mode, bool excl)
{
    struct inode *inode;

    /* 1. 새 inode 할당 (sb->s_op->alloc_inode 호출) */
    inode = new_inode(dir->i_sb);
    if (!inode)
        return -ENOMEM;

    /* 2. inode 번호 할당 */
    inode->i_ino = myfs_alloc_ino(dir->i_sb);

    /* 3. 소유권/권한 설정 */
    inode_init_owner(idmap, inode, dir, mode);

    /* 4. 연산 테이블 연결 */
    inode->i_op  = &myfs_file_iops;
    inode->i_fop = &myfs_file_fops;
    inode->i_mapping->a_ops = &myfs_aops;

    /* 5. 타임스탬프 설정 */
    simple_inode_init_ts(inode);

    /* 6. 해시 테이블에 삽입 */
    insert_inode_hash(inode);

    /* 7. 디스크에 기록 */
    myfs_write_inode_to_disk(inode);

    /* 8. dentry와 연결 */
    d_instantiate_new(dentry, inode);
    return 0;
}

/* ===== 경로 2: 디스크에서 inode 읽기 (예: lookup) ===== */
static struct inode *myfs_iget(struct super_block *sb,
                                unsigned long ino)
{
    struct inode *inode;
    struct myfs_inode_info *mi;

    /* 1. 해시 테이블 검색 또는 새 할당 */
    inode = iget_locked(sb, ino);
    if (!inode)
        return ERR_PTR(-ENOMEM);

    /* 2. 이미 캐시에 있으면 즉시 반환 */
    if (!(inode->i_state & I_NEW))
        return inode;

    /* 3. 새로 할당된 경우: 디스크에서 읽기 */
    mi = MYFS_I(inode);
    myfs_read_inode_from_disk(sb, ino, mi);

    /* 4. VFS inode 필드 채우기 */
    inode->i_mode = myfs_to_vfs_mode(mi->disk_mode);
    inode->i_size = mi->disk_size;
    set_nlink(inode, mi->disk_nlink);

    /* 5. 연산 테이블 연결 (유형에 따라) */
    if (S_ISREG(inode->i_mode)) {
        inode->i_op  = &myfs_file_iops;
        inode->i_fop = &myfs_file_fops;
    } else if (S_ISDIR(inode->i_mode)) {
        inode->i_op  = &myfs_dir_iops;
        inode->i_fop = &myfs_dir_fops;
    }

    /* 6. I_NEW 해제 → 다른 대기자 깨우기 */
    unlock_new_inode(inode);
    return inode;
}
inode 할당 플로우 (iget_locked) iget_locked(sb, ino) 해시 테이블에서 (sb, ino) 검색 캐시 히트? Yes 기존 inode 반환 I_NEW 없음 → 즉시 사용 No alloc_inode(sb) I_NEW 설정 + 해시 삽입 호출자가 디스크에서 읽기 unlock_new_inode()
💡

iget_locked vs iget5_locked: iget_locked()는 단순히 inode 번호로 검색하지만, iget5_locked()는 커스텀 비교 함수(test)를 사용합니다. Btrfs처럼 같은 inode 번호가 여러 서브볼륨에 존재할 수 있는 파일시스템에서는 반드시 iget5_locked()를 사용하여 서브볼륨 ID까지 함께 비교해야 합니다.

iget_locked() 콜 체인 상세 분석

iget_locked(sb, ino)는 inode 캐시 조회 → 미스 시 할당 → 초기화 잠금 획득의 세 단계를 원자적으로 수행합니다. 아래는 fs/inode.c의 실제 구현 흐름입니다.

/* fs/inode.c — iget_locked() 전체 콜 체인 */

/* ── 진입점 ─────────────────────────────────────── */
struct inode *iget_locked(struct super_block *sb, unsigned long ino)
{
    struct hlist_head *head = inode_hashtable + hash(sb, ino);
    struct inode *inode;
again:
    spin_lock(&inode_hash_lock);                  /* 해시 버킷 잠금 */
    inode = find_inode_fast(sb, head, ino);       /* ① 해시 조회 */
    spin_unlock(&inode_hash_lock);
    if (inode) {
        if (IS_ERR(inode))
            return NULL;
        wait_on_inode(inode);                      /* I_NEW 대기 */
        if (unlikely(inode_unhashed(inode))) {
            iput(inode);
            goto again;                            /* evict 경쟁 재시도 */
        }
        return inode;                              /* 캐시 히트 반환 */
    }

    /* ② 캐시 미스: 새 inode 할당 */
    inode = alloc_inode(sb);                      /* s_op->alloc_inode 호출 */
    if (inode) {
        struct inode *old;
        spin_lock(&inode_hash_lock);
        old = find_inode_fast(sb, head, ino);      /* TOCTOU 방지 재검사 */
        if (!old) {
            inode->i_ino = ino;
            spin_lock(&inode->i_lock);
            inode->i_state = I_NEW;                /* ③ 초기화 잠금 표시 */
            hlist_add_head(&inode->i_hash, head); /* 해시에 선점 등록 */
            spin_unlock(&inode->i_lock);
            inode_sb_list_add(inode);             /* sb->s_inodes 목록 추가 */
            spin_unlock(&inode_hash_lock);
            return inode;                          /* I_NEW 상태로 반환 */
        }
        spin_unlock(&inode_hash_lock);
        destroy_inode(inode);                     /* 경쟁 패배: 방금 할당 해제 */
        inode = old;
        wait_on_inode(inode);
    }
    return inode;
}

/* ── ① 해시 조회 ────────────────────────────────── */
static struct inode *find_inode_fast(
    struct super_block *sb, struct hlist_head *head, unsigned long ino)
{
    struct inode *inode;
    hlist_for_each_entry(inode, head, i_hash) {
        if (inode->i_ino != ino || inode->i_sb != sb)
            continue;
        spin_lock(&inode->i_lock);
        if (inode->i_state & (I_FREEING | I_WILL_FREE)) {
            __wait_on_freeing_inode(inode);        /* evict 완료 대기 후 재시도 */
            return ERR_PTR(-1);                   /* 호출자에게 재검사 요청 */
        }
        __iget(inode);                             /* i_count 증가 */
        spin_unlock(&inode->i_lock);
        return inode;
    }
    return NULL;
}

/* ── ② inode 할당 ───────────────────────────────── */
static struct inode *alloc_inode(struct super_block *sb)
{
    const struct super_operations *ops = sb->s_op;
    struct inode *inode;

    if (ops->alloc_inode)
        inode = ops->alloc_inode(sb);              /* fs별 kmem_cache 할당 */
    else
        inode = kmem_cache_alloc(inode_cachep, GFP_KERNEL); /* 기본 slab */
    if (!inode)
        return NULL;
    if (unlikely(inode_init_always(sb, inode))) {  /* VFS 필드 초기화 */
        if (ops->destroy_inode) {
            ops->destroy_inode(inode);
            return NULL;
        }
        kmem_cache_free(inode_cachep, inode);
        return NULL;
    }
    return inode;
}

/* ── ③ 초기화 완료 후 잠금 해제 ─────────────────── */
void unlock_new_inode(struct inode *inode)
{
    spin_lock(&inode->i_lock);
    if (WARN_ON(!(inode->i_state & I_NEW)))        /* 디버그: I_NEW 필수 확인 */
        goto out;
    inode->i_state &= ~I_NEW & ~I_CREATING;       /* I_NEW 비트 제거 */
    smp_mb();                                      /* 메모리 배리어 */
    wake_up_bit(&inode->i_state, __I_NEW);        /* 대기 중인 스레드 깨우기 */
out:
    spin_unlock(&inode->i_lock);
}
코드 설명
  • iget_locked — spin_lock(&inode_hash_lock)전역 해시 테이블을 보호하는 스핀락을 획득합니다. 이 락은 해시 체인의 노드 추가/제거 시에만 보유하며, inode 내용 접근은 inode->i_lock으로 별도 보호합니다. 두 락의 중첩 순서는 항상 inode_hash_lock → i_lock입니다.
  • iget_locked — find_inode_fast()해시 버킷 체인을 선형 탐색합니다. (sb, ino) 쌍으로 일치 여부를 확인합니다. I_FREEING|I_WILL_FREE 상태인 inode는 evict 완료를 기다린 후 ERR_PTR(-1)을 반환하여 again 레이블로 재시도하도록 합니다.
  • iget_locked — wait_on_inode()캐시에서 찾은 inode가 아직 I_NEW 상태(다른 스레드가 초기화 중)이면 wait_on_bit()로 대기합니다. unlock_new_inode()wake_up_bit()를 호출하면 깨어납니다. 이 메커니즘이 동시 iget 경쟁에서 중복 초기화를 방지합니다.
  • iget_locked — TOCTOU 방지 재검사alloc_inode()inode_hash_lock을 해제한 상태에서 호출되므로 그 사이 다른 CPU가 같은 inode를 삽입했을 수 있습니다. 락 재획득 후 find_inode_fast()를 다시 호출하여 이 경쟁 조건을 처리합니다.
  • iget_locked — I_NEW 설정 + hlist_add_head()I_NEW 비트를 설정한 상태로 해시에 먼저 등록함으로써 동시에 같은 inode를 요청하는 다른 스레드가 이 inode를 발견하고 대기하도록 합니다. "초기화 잠금(initialization lock)" 패턴입니다.
  • alloc_inode — ops->alloc_inode(sb)파일시스템이 super_operations.alloc_inode를 구현했으면 호출합니다. 이를 통해 ext4는 ext4_inode_info, XFS는 xfs_inodestruct inode와 함께 단일 slab 객체로 할당합니다. container_of()로 변환합니다.
  • alloc_inode — inode_init_always()VFS inode의 공통 필드를 초기화합니다: i_state=0, i_count=1, i_mapping=&inode->i_data, 대기 큐 초기화 등입니다. 이 함수 내에서 security_inode_alloc()을 호출하여 LSM 보안 필드도 초기화합니다.
  • unlock_new_inode — I_NEW &= ~I_NEW파일시스템의 inode 초기화(read_inode 등)가 완료된 후 호출합니다. I_NEWI_CREATING 비트를 동시에 제거합니다. smp_mb()는 이전 초기화 쓰기가 wake_up_bit() 이전에 모든 CPU에 가시적임을 보장합니다.
iget_locked() 콜 체인 호출자 (파일시스템) VFS (fs/inode.c) s_op 콜백 myfs_iget(sb, ino) iget_locked(sb, ino) find_inode_fast() 캐시 히트 inode 반환 (I_NEW 없음) 캐시 미스 alloc_inode(sb) s_op->alloc_inode() inode_init_always() security_inode_alloc() I_NEW 설정 + 해시 삽입 디스크 읽기 + 필드 초기화 unlock_new_inode() wake_up_bit(__I_NEW)
iget_locked() → find_inode_fast() → alloc_inode() → unlock_new_inode() 콜 체인. 캐시 히트 시 find_inode_fast에서 직접 반환되며, 미스 시 alloc_inode → I_NEW 설정 → 호출자 초기화 → unlock_new_inode 순으로 진행됩니다.

inode 해시 테이블과 룩업

VFS는 전역 해시 테이블 inode_hashtable을 유지하여 빠른 inode 검색을 제공합니다. 키는 (superblock 포인터, inode 번호) 쌍이며, 해시 충돌은 체이닝으로 해결합니다.

/* fs/inode.c — 전역 해시 테이블 */
static struct hlist_head *inode_hashtable __read_mostly;

/* 해시 함수: superblock 주소와 inode 번호를 조합 */
static unsigned long hash(struct super_block *sb,
                          unsigned long hashval)
{
    unsigned long tmp;
    tmp = (hashval * (unsigned long)sb) ^ (GOLDEN_RATIO_PRIME + hashval) /
          L1_CACHE_BYTES;
    tmp = tmp ^ ((tmp ^ GOLDEN_RATIO_PRIME) >> i_hash_shift);
    return tmp & i_hash_mask;
}

/* 해시 테이블 검색 (find_inode_fast) */
static struct inode *find_inode_fast(
    struct super_block *sb,
    struct hlist_head *head,
    unsigned long ino)
{
    struct inode *inode;
    hlist_for_each_entry(inode, head, i_hash) {
        if (inode->i_ino != ino)  /* 번호 불일치 */
            continue;
        if (inode->i_sb != sb)    /* superblock 불일치 */
            continue;
        spin_lock(&inode->i_lock);
        if (inode->i_state & (I_FREEING|I_WILL_FREE)) {
            __wait_on_freeing_inode(inode);
            return NULL;  /* 재검색 필요 */
        }
        __iget(inode);  /* i_count++ */
        spin_unlock(&inode->i_lock);
        return inode;
    }
    return NULL;
}
코드 설명
  • inode_hashtablefs/inode.c에 정의된 전역 해시 테이블입니다. 부팅 시 inode_init()에서 시스템 메모리 크기에 비례하여 할당됩니다. __read_mostly 섹션에 배치되어 캐시 라인 오염을 최소화합니다.
  • hash(sb, hashval)superblock 주소와 inode 번호를 조합하는 해시 함수입니다. GOLDEN_RATIO_PRIME을 곱하고 비트 시프트하여 분포를 균일하게 만듭니다. 결과를 i_hash_mask로 마스킹하여 버킷 인덱스를 얻습니다.
  • find_inode_fast — i_ino, i_sb 비교같은 해시 버킷 내에서 inode 번호(i_ino)와 superblock 포인터(i_sb)를 모두 비교합니다. 서로 다른 파일시스템이 같은 inode 번호를 가질 수 있으므로 두 조건 모두 일치해야 합니다.
  • I_FREEING | I_WILL_FREE 검사해시에서 찾은 inode가 evict 진행 중이면 __wait_on_freeing_inode()로 완료를 대기한 뒤 NULL을 반환하여 호출자가 재검색하도록 합니다. 이미 해제되는 inode의 참조를 획득하는 것을 방지하는 안전 장치입니다.
  • __iget(inode)i_lock을 잡은 상태에서 i_count를 원자적으로 증가시킵니다. LRU에 있던 inode라면 이 시점에 LRU에서 제거되어 활성 상태로 전환됩니다.
inode_hashtable 구조 inode_hashtable[] bucket[0] bucket[1] bucket[2] bucket[3] bucket[4] ... bucket[N-1] inode (sda1, #42) i_hash → next inode (sdb1, #7) i_hash → NULL inode (sda1, #100) i_hash → next inode (sda2, #15) i_hash → next inode (sda1, #88) i_hash → NULL 해시 키 = hash(sb, i_ino) 같은 버킷에 여러 inode가 체이닝됨 (충돌 해결) 검색 시 sb와 i_ino를 모두 비교하여 정확한 inode 특정

해시 테이블 연산

함수동작호출 시점
insert_inode_hash(inode)inode를 해시 테이블에 삽입새 inode 생성 후
__insert_inode_hash(inode, hashval)커스텀 해시값으로 삽입특수 해시가 필요할 때
remove_inode_hash(inode)해시 테이블에서 제거evict_inode() 내부
find_inode_fast(sb, head, ino)해시 체인에서 inode 검색iget_locked() 내부
find_inode(sb, head, test, data)커스텀 비교로 검색iget5_locked() 내부
ilookup(sb, ino)캐시에서만 검색 (미할당)캐시 확인만 필요 시

inode 상태 머신

각 inode는 i_state 필드에 상태 플래그의 조합을 저장합니다. 이 플래그들은 inode의 생명주기 단계를 추적하며, 동시성 제어와 writeback에 핵심적입니다.

플래그의미설정 시점
I_NEW1 << 3새로 할당, 아직 초기화 중iget_locked() 신규 할당 시
I_DIRTY_SYNC1 << 0메타데이터 dirty (atime 등 경량)__mark_inode_dirty(I_DIRTY_SYNC)
I_DIRTY_DATASYNC1 << 1데이터 관련 메타데이터 dirty (size 등)__mark_inode_dirty(I_DIRTY_DATASYNC)
I_DIRTY_PAGES1 << 2dirty 페이지(Page) 보유__mark_inode_dirty(I_DIRTY_PAGES)
I_SYNC1 << 4현재 writeback 진행 중writeback 시작 시
I_WILL_FREE1 << 5evict 예정 (dirty 기록 중)evict() 진입 직후
I_FREEING1 << 6evict 진행 중evict() 본체
I_CLEAR1 << 7evict 완료, 메모리 해제 대기evict() 완료 후
I_REFERENCED1 << 8최근 접근됨 (LRU 2차 기회)inode 접근 시
I_DIO_WAKEUP1 << 9Direct I/O 대기자 깨우기(Wakeup)DIO 완료 시
I_CREATING1 << 15생성 진행 중NFS 등 네트워크 FS
inode 상태 머신 (i_state 전이) I_NEW unlock_new_inode() Active (Clean) mark_inode_dirty() I_DIRTY_* writeback I_SYNC 완료 → clean sync 중 재dirty iput() (nlink==0) evict() I_WILL_FREE LRU 등록 I_FREEING I_CLEAR destroy_inode() 상태 전이 핵심 포인트 1. I_NEW 상태에서는 다른 스레드가 wait_on_inode()으로 대기 2. I_DIRTY는 SYNC|DATASYNC|PAGES 3개 플래그의 조합 3. I_SYNC 중 재dirty되면 writeback 완료 후 재큐잉 4. I_FREEING 상태의 inode를 발견하면 대기 후 재검색 5. I_REFERENCED는 LRU에서 2차 기회(second chance) 제공 6. I_WILL_FREE → I_FREEING 사이에 dirty 데이터 기록
/* inode dirty 마킹 — fs/fs-writeback.c */
void __mark_inode_dirty(struct inode *inode, int flags)
{
    struct super_block *sb = inode->i_sb;
    struct bdi_writeback *wb = NULL;

    /* 이미 설정된 플래그는 무시 */
    if ((inode->i_state & flags) == flags)
        return;

    spin_lock(&inode->i_lock);
    /* I_NEW, I_FREEING, I_WILL_FREE 상태면 무시 */
    if (inode->i_state & (I_NEW | I_FREEING | I_WILL_FREE)) {
        spin_unlock(&inode->i_lock);
        return;
    }

    /* FS에 dirty_inode 콜백이 있으면 호출 */
    if (sb->s_op->dirty_inode)
        sb->s_op->dirty_inode(inode, flags);

    /* dirty 플래그 설정 */
    inode->i_state |= flags;

    /* writeback 큐에 등록 */
    if (!(inode->i_state & I_DIRTY_ALL))
        inode_io_list_move_locked(inode, wb, &wb->b_dirty);

    spin_unlock(&inode->i_lock);
}
코드 설명
  • __mark_inode_dirty — 중복 검사fs/fs-writeback.c에 정의되어 있습니다. 이미 설정된 dirty 플래그를 다시 설정하는 것을 방지하기 위해 (i_state & flags) == flags 조건으로 조기 반환합니다. 이 fast-path는 잠금 없이 수행되어 write-heavy 워크로드에서 경합을 줄입니다.
  • I_NEW | I_FREEING | I_WILL_FREE 검사초기화 중(I_NEW)이거나 해제 중(I_FREEING, I_WILL_FREE)인 inode를 dirty 마킹하면 use-after-free나 writeback 오류가 발생할 수 있으므로 무시합니다.
  • dirty_inode 콜백파일시스템이 super_operations.dirty_inode를 구현했다면 이 시점에 호출합니다. ext4는 이 콜백에서 저널 트랜잭션을 시작하여 메타데이터 변경을 기록합니다. Btrfs는 COW 트리에 dirty 표시를 전파합니다.
  • inode_io_list_move_lockedinode를 BDI writeback의 b_dirty 리스트에 등록합니다. 이후 writeback 스레드(wb_workfn)가 주기적으로 이 리스트를 순회하며 dirty 데이터를 디스크에 기록합니다. /proc/sys/vm/dirty_writeback_centisecs(기본 500 = 5초)가 주기를 결정합니다.
ℹ️

I_DIRTY 세분화의 이유: I_DIRTY_SYNC(atime 같은 경량 메타데이터), I_DIRTY_DATASYNC(size 같은 데이터 관련 메타데이터), I_DIRTY_PAGES(페이지 캐시의 dirty 페이지)로 분리함으로써 fsync()fdatasync()가 필요한 최소한의 기록만 수행할 수 있습니다. fdatasync()I_DIRTY_DATASYNC | I_DIRTY_PAGES만 기록하고, 순수 메타데이터인 I_DIRTY_SYNC는 건너뜁니다.

mark_inode_dirty() 내부 구현 상세

mark_inode_dirty()는 inode가 변경되었음을 VFS에 알려 writeback 대상에 포함시키는 핵심 함수입니다. 내부적으로 __mark_inode_dirty()를 호출하여 BDI(Backing Device Info) writeback 큐에 등록합니다.

호출 함수전달 플래그용도
mark_inode_dirty(inode)I_DIRTY모든 dirty 비트 설정 (메타+데이터)
mark_inode_dirty_sync(inode)I_DIRTY_SYNC경량 메타데이터만 (atime 등)
__mark_inode_dirty(inode, flags)직접 지정세밀한 제어 (FS 내부용)
/* fs/fs-writeback.c — __mark_inode_dirty() 전체 구현 */
void __mark_inode_dirty(struct inode *inode, int flags)
{
    struct super_block *sb = inode->i_sb;
    struct bdi_writeback *wb = NULL;

    /* 1. 이미 해당 dirty 비트가 설정되어 있으면 빠른 종료 */
    if ((inode->i_state & flags) == flags)
        return;

    /* 2. s_op->dirty_inode() 콜백 호출 (ext4: 저널 컨텍스트 확인) */
    if (sb->s_op->dirty_inode)
        sb->s_op->dirty_inode(inode, flags & (I_DIRTY_INODE | I_DIRTY_TIME));

    /* 3. I_DIRTY_TIME → I_DIRTY_SYNC 승격 검사 */
    /*    lazytime이고 DIRTY_TIME만 설정하면 dirty list에 안 넣음 */
    if ((flags & I_DIRTY_TIME) &&
        !(flags & ~I_DIRTY_TIME) &&
        !(inode->i_state & I_DIRTY_INODE))
    {
        spin_lock(&inode->i_lock);
        if (inode->i_state & I_DIRTY_TIME) {
            spin_unlock(&inode->i_lock);
            return;  /* 이미 DIRTY_TIME, b_dirty_time에 있음 */
        }
        inode->i_state |= I_DIRTY_TIME;
        spin_unlock(&inode->i_lock);

        /* b_dirty_time 리스트에만 추가 (즉시 writeback 안 함) */
        inode_io_list_move_locked(inode, wb, &wb->b_dirty_time);
        return;
    }

    /* 4. dirty 비트 설정 + writeback 큐 등록 */
    spin_lock(&inode->i_lock);
    if ((inode->i_state & flags) != flags) {
        const int was_dirty = inode->i_state & I_DIRTY;

        inode->i_state |= flags;  /* dirty 비트 설정 */

        if (!was_dirty) {
            /* ⑤ 새로 dirty → writeback 큐에 등록 */
            inode->dirtied_when = jiffies;   /* dirty 시각 기록 */

            /* cgroup writeback: inode의 소속 wb 결정 */
            wb = locked_inode_to_wb_and_lock_list(inode);

            /* b_dirty 리스트에 추가 */
            inode_io_list_move_locked(inode, wb, &wb->b_dirty);

            spin_unlock(&wb->list_lock);

            /* ⑥ writeback 스레드 깨우기 (dirty_writeback_interval 후) */
            wb_wakeup_delayed(wb);
        }
    }
    spin_unlock(&inode->i_lock);
}
코드 설명
  • 빠른 종료 (이미 dirty)이미 동일한 dirty 비트가 설정되어 있으면 중복 작업을 피합니다. 이 최적화는 inode가 빈번히 수정되는 hot path에서 스핀락 획득을 회피합니다.
  • dirty_inode 콜백파일시스템에 dirty 전환을 알립니다. ext4는 이 콜백에서 현재 저널 트랜잭션 핸들을 확인하고, 필요시 저널 공간을 예약합니다.
  • I_DIRTY_TIME — lazytimelazytime 마운트 옵션이 활성화되면, atime/mtime/ctime 변경은 I_DIRTY_TIME만 설정합니다. 이 inode는 b_dirty_time 리스트에 들어가며, 24시간 또는 sync() 시에만 디스크에 기록됩니다. 일반 dirty(I_DIRTY_INODE)보다 writeback 빈도가 훨씬 낮아 SSD 수명과 성능에 유리합니다.
  • dirtied_when = jiffiesinode가 처음 dirty된 시각을 기록합니다. writeback 스레드는 이 값과 dirty_expire_centisecs(기본 30초)를 비교하여 충분히 오래된 dirty inode부터 기록합니다.
  • locked_inode_to_wb_and_lock_listcgroup writeback 환경에서 inode의 소속 bdi_writeback 구조를 결정합니다. blkcg(Block I/O cgroup)가 활성화되면 프로세스의 cgroup에 따라 다른 wb가 선택되어, 컨테이너별 I/O 격리를 지원합니다.
  • wb_wakeup_delayedBDI writeback 워커 스레드를 dirty_writeback_interval(기본 5초) 후에 깨우도록 타이머를 설정합니다. 즉시 기록이 필요한 경우(IS_SYNC) wb_wakeup()로 즉시 깨웁니다.
💡

dirty_writeback_interval 튜닝: /proc/sys/vm/dirty_writeback_centisecs (기본 500 = 5초)로 writeback 스레드의 주기를 조절합니다. 값을 줄이면 dirty 데이터가 더 빨리 디스크에 기록되어 크래시 시 데이터 손실이 줄지만, I/O 오버헤드(Overhead)가 증가합니다. dirty_expire_centisecs(기본 3000 = 30초)는 dirty inode가 디스크 기록 대상이 되는 최소 나이입니다.

address_space와 inode 연결

inode의 i_mapping 필드는 페이지 캐시와의 연결점입니다. struct address_space는 inode에 임베드되어 있으며(i_data 필드), 파일 데이터의 페이지를 관리합니다.

struct address_space {
    struct inode       *host;        /* 소유 inode */
    struct xarray       i_pages;      /* 페이지 캐시 (XArray) */
    atomic_t           i_mmap_writable; /* VM_SHARED 매핑 수 */
    struct rb_root_cached i_mmap;      /* private/shared 매핑 트리 */
    unsigned long      nrpages;      /* 총 페이지 수 */
    pgoff_t            writeback_index; /* writeback 시작 위치 */
    const struct address_space_operations *a_ops;
    unsigned long      flags;        /* gfp_mask, 에러 상태 */
    struct rw_semaphore i_mmap_rwsem; /* mmap 보호 */
    void              *private_data; /* FS별 데이터 */
};

/* inode 내부의 임베드된 address_space */
struct inode {
    /* ... */
    struct address_space *i_mapping; /* 일반적으로 &i_data를 가리킴 */
    struct address_space  i_data;    /* 임베드된 address_space */
    /* ... */
};

/* 초기화 시 i_mapping = &i_data로 설정됨 */
/* 블록 디바이스의 경우: 여러 파일이 같은 address_space를 공유할 수 있음 */
코드 설명
  • hostaddress_space를 소유하는 inode를 가리킵니다. a_ops 콜백들이 소유 inode의 메타데이터(블록 매핑 등)에 접근할 때 사용합니다.
  • i_pages (XArray)페이지 캐시의 핵심 자료구조입니다. 파일 오프셋(페이지 인덱스)을 키로 하여 struct page(또는 struct folio)를 저장합니다. 커널 4.20에서 기존 radix tree에서 XArray로 교체되었습니다.
  • i_mmap (rb_root_cached)mmap()으로 이 파일을 매핑한 모든 VMA(Virtual Memory Area)를 추적하는 interval tree입니다. truncatehole punch 시 영향받는 매핑을 빠르게 찾아 unmap_mapping_range()로 페이지 테이블 엔트리를 제거합니다.
  • a_opsaddress_space_operationsread_folio(), writepages(), dirty_folio() 등 실제 I/O를 수행하는 콜백 테이블입니다. 파일시스템마다 블록 레이아웃이 다르므로 각자 구현합니다.
  • i_mapping = &i_data일반 파일에서는 inode에 임베드된 i_data를 가리킵니다. 블록 디바이스 파일은 bdev->bd_inode->i_data를 공유하므로 i_mapping이 다른 inode의 address_space를 가리킬 수 있습니다. 이 간접 참조 덕분에 같은 블록 디바이스를 여는 여러 파일이 하나의 페이지 캐시를 공유합니다.
inode → address_space → Page Cache 연결 struct inode i_ino = 42 i_size = 16384 i_mapping ──→ i_data (embedded) i_op, i_fop, ... struct address_space host = &inode i_pages (XArray) ──→ nrpages = 4 a_ops = &myfs_aops i_mmap (VMA tree) XArray (i_pages) idx 0 idx 1 idx 2 idx 3 각 슬롯 = struct folio * (페이지 캐시 folio 포인터) 디스크 블록 (data blocks) read_folio/writepages vm_area_struct (mmap 영역) i_mmap address_space_operations .read_folio() .writepages() .dirty_folio() .write_begin() .write_end() .invalidate_folio()

주요 address_space_operations 콜백

콜백호출 시점역할
read_folio(file, folio)페이지 캐시 miss (read)디스크에서 folio(페이지)를 읽어 채움
readahead(rac)순차 읽기 감지미리 읽기 — 연속 folios를 일괄 제출
writepages(mapping, wbc)writeback 발동dirty 페이지를 디스크에 기록
dirty_folio(mapping, folio)folio가 dirty 될 때accounting, 저널 예약 등
write_begin(file, mapping, pos, len, folio)버퍼(Buffer)드 write 시작folio 할당 + 블록 매핑 준비
write_end(file, mapping, pos, len, copied, folio)버퍼드 write 완료dirty 마킹 + 메타데이터 갱신
invalidate_folio(folio, offset, length)truncate/hole punchfolio의 전부 또는 일부를 무효화(Invalidation)
release_folio(folio, gfp)메모리 회수private 데이터 해제 후 folio 반환 가능 여부
💡

XArray로의 전환: 커널 4.20부터 페이지 캐시의 인덱스 구조가 radix tree에서 XArray로 전환되었습니다. XArray는 락 통합, API 간결성, RCU 안전 순회를 제공합니다. i_pages를 통해 파일 오프셋(인덱스)으로 해당 folio를 O(log n)에 검색할 수 있습니다.

inode와 mmap 연동

mmap()은 파일의 address_space를 프로세스의 가상 주소 공간(Address Space)에 매핑합니다. 이 매핑은 inode의 i_mapping을 통해 페이지 캐시와 직접 연결되므로, 여러 프로세스가 같은 파일을 mmap하면 같은 물리 페이지를 공유합니다.

mmap과 inode/address_space 연결 구조 프로세스 A VMA: 0x7f000000 vm_file → struct file 프로세스 B VMA: 0x7f800000 vm_file → struct file struct inode i_mapping → i_mmap (interval tree) 모든 VMA 추적 address_space i_pages (XArray) pg 0 pg 1 pg 2 a_ops → readahead, writepages page_mkwrite 페이지 폴트 처리 경로 1. 프로세스가 매핑된 주소 접근 → 페이지 폴트 발생 2. filemap_fault(): address_space에서 해당 folio 검색/할당 3. 쓰기 폴트: page_mkwrite() → folio를 dirty로 마킹 → writeback 대상
/* vm_operations_struct — mmap 페이지 폴트 핸들러 */
static const struct vm_operations_struct ext4_file_vm_ops = {
    .fault       = filemap_fault,       /* 읽기 폴트 → 페이지 캐시에서 folio 매핑 */
    .map_pages   = filemap_map_pages,   /* 주변 페이지 선제 매핑 (TLB miss 줄임) */
    .page_mkwrite = ext4_page_mkwrite,  /* 쓰기 폴트 → 블록 할당 + dirty 마킹 */
};

/* page_mkwrite — 쓰기 폴트 시 FS에 알림 */
static vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)
{
    struct inode *inode = file_inode(vmf->vma->vm_file);

    /* 1. i_rwsem 공유 잠금 획득 */
    sb_start_pagefault(inode->i_sb);

    /* 2. 블록이 아직 할당 안 됐으면 할당 (delalloc) */
    ext4_map_blocks(handle, inode, &map, ...);

    /* 3. 페이지를 dirty로 마킹 → writeback 대상에 포함 */
    folio_mark_dirty(folio);
    folio_wait_stable(folio);  /* 저널링: stable write 대기 */

    return VM_FAULT_LOCKED;
}
mmap 유형공유 방식inode 관계writeback
MAP_SHARED동일 물리 페이지 공유i_mapping 페이지 캐시 사용dirty 페이지 → FS writeback
MAP_PRIVATECoW (쓰기 시 복사(Copy-on-Write))읽기 시 공유, 쓰기 시 분리사본은 swap에 기록
MAP_ANONYMOUSinode 없음address_space 없음swap에 기록
truncate와 mmap 경합(Contention): 파일이 mmap된 상태에서 truncate()로 크기를 줄이면, 매핑된 영역 중 잘려나간 부분에 접근 시 SIGBUS가 발생합니다. 커널은 unmap_mapping_range()로 해당 영역의 PTE를 무효화하고, truncate_inode_pages()로 페이지 캐시에서 제거합니다. i_mmap_rwsem이 이 과정의 동시성을 보호합니다.
💡

i_mmap interval tree: address_spacei_mmap 필드는 구간 트리(interval tree)로, 파일의 어떤 범위가 어떤 VMA에 매핑되어 있는지 추적합니다. truncatehole punch 시 영향 받는 모든 VMA를 빠르게 찾아 PTE를 무효화하는 데 사용됩니다. 이 트리 없이는 모든 프로세스의 VMA를 선형 탐색해야 하므로 O(n)이 O(log n + k)로 개선됩니다.

ACL과 권한 검사

파일 접근 시 VFS는 inode_permission()을 통해 권한을 검사합니다. 이 과정에서 전통적인 Unix 권한(rwx), POSIX ACL, 보안 모듈(LSM) 검사가 순차적으로 수행됩니다.

/* fs/namei.c — 권한 검사 진입점 */
int inode_permission(struct mnt_idmap *idmap,
                    struct inode *inode, int mask)
{
    int retval;

    /* 1. sb 레벨 읽기 전용 검사 */
    retval = sb_permission(inode->i_sb, inode, mask);
    if (retval)
        return retval;

    /* 2. FS별 permission 콜백 또는 generic_permission */
    if (inode->i_op->permission)
        retval = inode->i_op->permission(idmap, inode, mask);
    else
        retval = generic_permission(idmap, inode, mask);

    if (retval)
        return retval;

    /* 3. LSM 검사 (SELinux, AppArmor 등) */
    retval = security_inode_permission(inode, mask);
    return retval;
}

/* generic_permission 내부 흐름 */
int generic_permission(struct mnt_idmap *idmap,
                      struct inode *inode, int mask)
{
    /* (a) ACL이 있으면 ACL 검사 우선 */
    if (IS_POSIXACL(inode) && ...) {
        retval = posix_acl_permission(idmap, inode, acl, mask);
    }
    /* (b) 전통적 owner/group/other 검사 */
    else {
        retval = acl_permission_check(idmap, inode, mask);
    }
    /* (c) DAC override: CAP_DAC_OVERRIDE, CAP_DAC_READ_SEARCH */
    if (retval && capable_wrt_inode_uidgid(idmap, inode,
                                            CAP_DAC_OVERRIDE))
        retval = 0;
    return retval;
}
코드 설명
  • inode_permission — sb_permission()슈퍼블록 단위 검사입니다. MS_RDONLY 마운트에서 쓰기(MAY_WRITE) 요청 시 -EROFS를 반환합니다. 또한 MNT_NOEXEC 마운트에서 실행(MAY_EXEC) 요청 시 -EACCES를 반환합니다. inode 자체의 권한 검사 전에 항상 먼저 수행됩니다.
  • inode_permission — i_op->permission()파일시스템별 권한 검사 콜백입니다. NULL이면 generic_permission()을 호출합니다. NFS, CIFS 등 네트워크 파일시스템은 이를 구현하여 서버 측 권한 검사를 추가하거나 서버 응답을 캐싱합니다.
  • inode_permission — security_inode_permission()LSM(Linux Security Module) 훅입니다. SELinux라면 AVC(Access Vector Cache)를 조회하여 보안 정책 허용 여부를 확인합니다. AppArmor라면 프로파일 기반 검사를 수행합니다. DAC 검사가 성공해도 이 단계에서 거부될 수 있습니다.
  • generic_permission — posix_acl_permission()IS_POSIXACL(inode)이고 ACL이 캐시에 있으면 호출됩니다. POSIX ACL은 ACL_USER, ACL_GROUP, ACL_MASK, ACL_OTHER 항목으로 구성되며 전통적인 rwx 비트보다 세밀한 권한 제어를 제공합니다.
  • generic_permission — acl_permission_check()전통적인 Unix DAC 검사입니다. idmap을 통해 idmapped mount의 uid/gid 변환을 적용한 후 소유자(owner)/그룹(group)/기타(other) 순서로 비교합니다. 첫 번째 일치하는 항목의 권한만 적용합니다.
  • generic_permission — capable_wrt_inode_uidgid(CAP_DAC_OVERRIDE)DAC 권한 검사 실패 후 capabilit 오버라이드를 시도합니다. CAP_DAC_OVERRIDE는 읽기/쓰기/실행 권한을 우회합니다. 단, 실행 권한은 owner/group/other 중 하나라도 실행 비트가 있어야 오버라이드됩니다. CAP_DAC_READ_SEARCH는 읽기와 디렉터리 검색만 우회합니다.
inode_permission() 검사 흐름 1. sb_permission() 2. i_op->permission() 3. security_inode_perm() generic_permission() 상세 소유자 검사 (i_uid) POSIX ACL 검사 그룹 검사 (i_gid) 기타(other) 검사 CAP_DAC_OVERRIDE 재검사 허용/거부 반환

POSIX ACL 상세

ACL 타입xattr 이름용도
access ACLsystem.posix_acl_access파일 접근 권한 (기본 rwx 확장)
default ACLsystem.posix_acl_default디렉터리에만 설정, 새 파일에 상속
# POSIX ACL 사용 예시

# 현재 ACL 확인
getfacl /home/user/shared/

# 특정 사용자에게 읽기/쓰기 권한 부여
setfacl -m u:bob:rw /home/user/shared/report.txt

# 그룹에 읽기 권한 부여
setfacl -m g:devteam:r /home/user/shared/report.txt

# default ACL 설정 (새 파일에 자동 적용)
setfacl -d -m u:bob:rwx /home/user/shared/

# ACL 제거
setfacl -b /home/user/shared/report.txt

# 마스크 확인 (effective 권한 상한)
getfacl --omit-header /home/user/shared/report.txt
# user:bob:rw-      #effective:r--  (마스크=r--인 경우)
ACL과 chmod의 상호작용: chmod()를 실행하면 ACL의 mask 엔트리가 변경됩니다. 이는 ACL에서 부여한 권한을 의도치 않게 제한할 수 있습니다. ACL을 사용하는 파일에 chmod를 적용할 때는 getfacl로 결과를 반드시 확인하세요.

setattr/getattr 헬퍼 함수

VFS는 inode 속성 변경(chmod, chown, truncate)과 조회(stat)를 위한 공통 헬퍼를 제공합니다. 파일시스템은 이 헬퍼를 호출한 뒤 FS 고유의 추가 작업(저널, 블록 할당 등)을 수행합니다.

notify_change() — 속성 변경 진입점

chmod(2), chown(2), truncate(2), utimes(2) 등의 시스템콜은 모두 notify_change()를 통해 inode 속성을 변경합니다.

/* fs/attr.c — notify_change() 핵심 흐름 */
int notify_change(struct mnt_idmap *idmap,
                  struct dentry *dentry,
                  struct iattr *attr,
                  struct inode **delegated_inode)
{
    struct inode *inode = dentry->d_inode;
    int error;

    /* 1. SGID 비트 자동 제거 (비소유자 write 시) */
    if (attr->ia_valid & ATTR_MODE) {
        if (!in_group_or_capable(idmap, inode, attr->ia_vfsgid))
            attr->ia_mode &= ~S_ISGID;
    }

    /* 2. 보안/사전 검사 */
    error = security_inode_setattr(idmap, dentry, attr);
    if (error)
        return error;

    /* 3. FS별 setattr 콜백 호출 */
    if (inode->i_op->setattr)
        error = inode->i_op->setattr(idmap, dentry, attr);
    else
        error = simple_setattr(idmap, dentry, attr);

    if (!error) {
        /* 4. fsnotify 이벤트 발생 */
        fsnotify_change(dentry, attr->ia_valid);
    }
    return error;
}

/* simple_setattr — 기본 setattr 구현 (tmpfs, ramfs 등) */
int simple_setattr(struct mnt_idmap *idmap,
                   struct dentry *dentry,
                   struct iattr *iattr)
{
    struct inode *inode = d_inode(dentry);
    int error;

    error = setattr_prepare(idmap, dentry, iattr);
    if (error)
        return error;

    if (iattr->ia_valid & ATTR_SIZE)
        truncate_setsize(inode, iattr->ia_size);

    setattr_copy(idmap, inode, iattr);
    mark_inode_dirty(inode);
    return 0;
}
코드 설명
  • notify_change — SGID 제거파일의 그룹에 속하지 않은 사용자가 mode를 변경할 때, SGID 비트를 자동으로 제거합니다. 이는 보안을 위한 POSIX 요구사항입니다 — SGID 파일을 실행하면 그룹 권한이 상승하므로, 소유 그룹이 아닌 사용자가 임의로 설정할 수 없습니다.
  • notify_change — security_inode_setattrLSM 훅으로, SELinux/AppArmor가 속성 변경을 허용하는지 검사합니다. DAC 검사보다 후에 실행됩니다.
  • simple_setattrinode_operations.setattr가 NULL인 경우의 기본 구현입니다. setattr_prepare()로 사전 검증, truncate_setsize()로 크기 변경, setattr_copy()로 필드 복사, mark_inode_dirty()로 dirty 마킹을 순서대로 수행합니다. tmpfs, ramfs 등 단순 파일시스템이 사용합니다.

setattr_prepare() — 사전 검증

setattr_prepare()는 속성 변경이 허용되는지 사전에 검증합니다. 모든 파일시스템의 setattr 콜백에서 반드시 호출해야 합니다.

/* fs/attr.c — setattr_prepare() */
int setattr_prepare(struct mnt_idmap *idmap,
                    struct dentry *dentry,
                    struct iattr *attr)
{
    struct inode *inode = d_inode(dentry);
    unsigned int ia_valid = attr->ia_valid;

    /* 1. immutable/append-only 검사 */
    if (IS_IMMUTABLE(inode))
        return -EPERM;
    if (IS_APPEND(inode) && (ia_valid & (ATTR_MODE | ATTR_UID |
                               ATTR_GID | ATTR_TIMES_SET | ATTR_SIZE)))
        return -EPERM;

    /* 2. UID 변경: 소유자이거나 CAP_CHOWN 필요 */
    if (ia_valid & ATTR_UID) {
        vfsuid_t vfsuid = make_vfsuid(idmap, ...);
        if (!inode_owner_or_capable(idmap, inode) &&
            !capable_wrt_inode_uidgid(idmap, inode, CAP_CHOWN))
            return -EPERM;
    }

    /* 3. GID 변경: 소유자이거나 CAP_CHOWN 필요 */
    if (ia_valid & ATTR_GID) {
        /* 유사한 검사 */
    }

    /* 4. 크기 변경: 쓰기 권한 + 파일 크기 제한 검사 */
    if (ia_valid & ATTR_SIZE) {
        int error = inode_newsize_ok(inode, attr->ia_size);
        if (error)
            return error;
    }

    /* 5. 모드(권한) 변경: 소유자이거나 CAP_FOWNER 필요 */
    if (ia_valid & ATTR_MODE) {
        if (!inode_owner_or_capable(idmap, inode))
            return -EPERM;
        /* 비소유 그룹이면 SGID 제거 */
    }

    /* 6. 타임스탬프 변경: 소유자이거나 쓰기 권한 */
    if (ia_valid & ATTR_TIMES_SET) {
        if (!inode_owner_or_capable(idmap, inode))
            return -EPERM;
    }

    return 0;
}

setattr_copy() — 필드 복사

/* fs/attr.c — setattr_copy(): iattr에서 inode로 필드 복사 */
void setattr_copy(struct mnt_idmap *idmap,
                  struct inode *inode,
                  const struct iattr *attr)
{
    unsigned int ia_valid = attr->ia_valid;

    /* UID 복사 (idmap 변환 적용) */
    if (ia_valid & ATTR_UID)
        inode->i_uid = vfsuid_into_kuid(attr->ia_vfsuid);

    /* GID 복사 */
    if (ia_valid & ATTR_GID)
        inode->i_gid = vfsgid_into_kgid(attr->ia_vfsgid);

    /* 크기 복사 (truncate_setsize가 이미 처리했을 수 있음) */
    if (ia_valid & ATTR_SIZE)
        i_size_write(inode, attr->ia_size);

    /* 타임스탬프 복사 */
    if (ia_valid & ATTR_ATIME)
        inode_set_atime_to_ts(inode, attr->ia_atime);
    if (ia_valid & ATTR_MTIME)
        inode_set_mtime_to_ts(inode, attr->ia_mtime);
    if (ia_valid & ATTR_CTIME)
        inode_set_ctime_to_ts(inode, attr->ia_ctime);

    /* 모드 복사 + SGID 조건부 제거 */
    if (ia_valid & ATTR_MODE) {
        umode_t mode = attr->ia_mode;
        /* 그룹 실행 비트 없으면 SGID 무의미 → 제거 */
        if (!in_group_or_capable(idmap, inode,
                                i_gid_into_vfsgid(idmap, inode)))
            mode &= ~S_ISGID;
        inode->i_mode = mode;
    }
}
코드 설명
  • setattr_copy — vfsuid_into_kuididmapped mount 환경에서 iattr의 vfsuid(마운트 네임스페이스의 uid)를 커널 내부 kuid로 변환합니다. 일반 마운트에서는 단순 캐스팅(Cast)입니다.
  • setattr_copy — ATTR_MODE + SGID모드를 설정할 때 소유 그룹이 아닌 경우 SGID 비트를 자동 제거합니다. 이는 chmod g+s로 설정된 SGID가 소유 그룹 변경 후에도 남아있으면 보안 위험이 되기 때문입니다.
  • setattr_copy — i_size_write크기 변경은 truncate_setsize()가 이미 처리했을 수 있지만, setattr_copy()에서도 안전하게 중복 설정합니다. i_size_write()를 사용하여 32비트 원자성을 보장합니다.

generic_fillattr() — stat 채우기

stat(2)/statx(2) 시스템콜에서 VFS가 inode_operations.getattr을 호출하면, 파일시스템은 generic_fillattr()를 사용하여 struct kstat를 채웁니다.

/* fs/stat.c — generic_fillattr() */
void generic_fillattr(struct mnt_idmap *idmap,
                      u32 request_mask,
                      struct inode *inode,
                      struct kstat *stat)
{
    /* idmap 변환 적용 후 uid/gid 설정 */
    stat->uid   = vfsuid_into_kuid(i_uid_into_vfsuid(idmap, inode));
    stat->gid   = vfsgid_into_kgid(i_gid_into_vfsgid(idmap, inode));

    stat->dev     = inode->i_sb->s_dev;
    stat->ino     = inode->i_ino;
    stat->mode    = inode->i_mode;
    stat->nlink   = inode->i_nlink;
    stat->size    = i_size_read(inode);
    stat->blocks  = inode->i_blocks;
    stat->blksize = i_blocksize(inode);

    /* 타임스탬프 복사 (접근자 사용) */
    stat->atime = inode_get_atime(inode);
    stat->mtime = inode_get_mtime(inode);
    stat->ctime = inode_get_ctime(inode);

    /* rdev — 블록/캐릭터 디바이스 번호 */
    stat->rdev = inode->i_rdev;
    stat->result_mask |= STATX_BASIC_STATS;
}

/* 파일시스템의 getattr 구현 예시 */
static int myfs_getattr(struct mnt_idmap *idmap,
                        const struct path *path,
                        struct kstat *stat,
                        u32 request_mask, unsigned int query_flags)
{
    struct inode *inode = d_inode(path->dentry);

    /* 기본 필드 채우기 */
    generic_fillattr(idmap, request_mask, inode, stat);

    /* FS 고유 확장 정보 (statx) */
    if (request_mask & STATX_BTIME)
        stat->btime = MYFS_I(inode)->i_crtime;

    stat->attributes |= STATX_ATTR_IMMUTABLE;
    stat->attributes_mask |= STATX_ATTR_IMMUTABLE;
    return 0;
}
inode 필드kstat 필드stat(2) 출력
i_inostat->inost_ino
i_modestat->modest_mode
i_nlinkstat->nlinkst_nlink
i_uid (idmap 변환)stat->uidst_uid
i_gid (idmap 변환)stat->gidst_gid
i_sizestat->sizest_size
i_blocksstat->blocksst_blocks (512B 단위)
__i_atimestat->atimest_atime
__i_mtimestat->mtimest_mtime
__i_ctimestat->ctimest_ctime

타임스탬프 정밀도와 Y2038

inode 타임스탬프는 파일 메타데이터 중 가장 빈번하게 갱신되는 필드입니다. 커널은 struct timespec64로 나노초 정밀도를 지원하지만, 실제 저장 정밀도는 파일시스템에 따라 다릅니다.

파일시스템타임스탬프 범위저장 정밀도crtime (생성 시간)
ext4 (128B inode)1901~20381초미지원
ext4 (256B inode)1901~24461나노초지원 (statx)
XFS1901~24861나노초지원 (statx)
Btrfs1901~24861나노초지원 (statx)
tmpfs1901~24861나노초미지원
FAT1980~21072초10ms (ctime 필드)
NTFS1601~30828100나노초지원
/* VFS 타임스탬프 API (커널 6.x) */

/* 타임스탬프 읽기 — 접근자 함수 사용 필수 */
struct timespec64 ts = inode_get_atime(inode);
struct timespec64 ts = inode_get_mtime(inode);
struct timespec64 ts = inode_get_ctime(inode);

/* 타임스탬프 설정 */
inode_set_atime_to_ts(inode, ts);
inode_set_mtime_to_ts(inode, ts);
inode_set_ctime_to_ts(inode, ts);

/* 현재 시간으로 설정 (FS의 정밀도에 맞춰 자동 truncate) */
inode_set_ctime_current(inode);

/* 정밀도 선언 — superblock에서 설정 */
sb->s_time_gran = 1;           /* 나노초 정밀도 */
sb->s_time_gran = NSEC_PER_SEC; /* 초 단위 정밀도 */
sb->s_time_min  = (s64)0;      /* 최소 타임스탬프 */
sb->s_time_max  = (s64)U32_MAX; /* 최대 타임스탬프 */

/* Y2038 안전성 — timespec64는 64비트 초 필드 사용 */
struct timespec64 {
    time64_t  tv_sec;   /* 64비트 초 (Y2038 안전) */
    long      tv_nsec;  /* 나노초 (0 ~ 999999999) */
};
# statx로 확장 타임스탬프 확인 (crtime 포함)
python3 -c "
import os
result = os.stat('/home/user/test.txt')
print(f'atime: {result.st_atime}')
print(f'mtime: {result.st_mtime}')
print(f'ctime: {result.st_ctime}')
"

# 또는 stat 명령어 사용
stat /home/user/test.txt
# 출력에서 Access, Modify, Change, Birth 시간 확인

# lazytime 마운트 옵션: atime을 메모리에서만 갱신
mount -o remount,lazytime /

# relatime 확인 (기본값)
mount | grep relatime
ℹ️

Y2038 문제와 커널: 32비트 time_t는 2038년 1월 19일에 오버플로됩니다. VFS의 timespec64 전환은 커널 5.x에서 완료되었지만, 디스크 포맷이 32비트인 ext4 (128바이트 inode)에서는 여전히 2038 제한이 있습니다. tune2fs -l /dev/sda1 | grep 'Inode size'로 현재 inode 크기를 확인하고, 256바이트 이상이면 Y2038 안전합니다.

inotify/fanotify 이벤트 통지

VFS 파일 이벤트 통지 시스템은 fsnotify 인프라 위에 구축됩니다. inode에 감시 마크(watch mark)를 부착하면, 해당 inode에서 발생하는 이벤트가 유저스페이스로 전달됩니다.

fsnotify 아키텍처

/* fs/notify 구조 */
/*
 * fsnotify_group  — 이벤트 수신자 (inotify fd 또는 fanotify fd)
 * fsnotify_mark   — inode/mount/sb에 부착된 감시 마크
 * fsnotify_event  — 발생한 이벤트 (큐에 저장)
 */

struct fsnotify_mark {
    struct fsnotify_group *group;    /* 소속 그룹 */
    union {
        struct inode *inode;           /* inode 마크 */
        struct vfsmount *mnt;         /* 마운트 마크 */
        struct super_block *sb;       /* sb 마크 (fanotify) */
    } connector_target;
    __u32              mask;        /* 감시할 이벤트 마스크 */
    __u32              ignore_mask; /* 무시할 이벤트 */
};

/* VFS 내부에서 이벤트 발생 호출 (예: vfs_write 완료 시) */
void fsnotify_modify(struct file *file)
{
    struct inode *inode = file_inode(file);
    if (!(inode->i_fsnotify_mask & FS_MODIFY))
        return;
    fsnotify_parent(file->f_path.dentry, FS_MODIFY, file, FSNOTIFY_EVENT_PATH);
    fsnotify(inode, FS_MODIFY, file, FSNOTIFY_EVENT_PATH, NULL, 0);
}
fsnotify 이벤트 전달 경로 유저 프로세스 write(fd, buf, len) 또는 unlink(), mkdir() VFS 계층 vfs_write() fsnotify_modify() fsnotify core 마크 검색 이벤트 큐잉 inotify 파일/디렉터리 단위 inotify_event → read() fanotify 마운트/파일시스템 단위 FAN_ACCESS_PERM 등 감시 프로그램 (read) 안티바이러스/감사 (read)

inotify vs fanotify 비교

특성inotifyfanotify
감시 단위파일/디렉터리별 watch마운트/파일시스템 전체
재귀 감시미지원 (수동 추가 필요)마운트 전체 자동
접근 제어불가FAN_ACCESS_PERM (허용/거부)
이벤트 정보파일명 포함파일 디스크립터 제공 (fid)
권한 필요일반 사용자CAP_SYS_ADMIN (일부 기능)
커널 버전2.6.13+2.6.37+
주요 용도IDE 파일 감시, 빌드 도구안티바이러스, 감사(audit)
오버플로 처리IN_Q_OVERFLOWFAN_Q_OVERFLOW

writeback 파이프라인(Pipeline)

dirty inode의 데이터를 디스크에 기록하는 과정을 writeback이라 합니다. 커널은 백그라운드 워커 스레드(Thread)를 통해 비동기적으로 writeback을 수행하며, 이 과정에서 inode의 상태 플래그가 핵심 역할을 합니다.

writeback 발동 조건

트리거조건sysctl 관련
주기적 writebackdirty 후 일정 시간 경과dirty_writeback_centisecs (기본 500 = 5초)
dirty 임계값 초과시스템 dirty 비율 초과dirty_background_ratio (기본 10%)
fsync/fdatasync사용자 명시적 요청해당 없음
sync 시스템콜전체 파일시스템 동기화해당 없음
umount파일시스템 언마운트해당 없음
메모리 압력free 메모리 부족dirty_ratio (기본 20%)
Writeback 파이프라인 b_dirty 리스트 inode A (dirty 3초전) inode B (dirty 1초전) inode C (dirty 0.5초전) 5초 경과 b_io 리스트 inode A → I_SYNC writeback worker가 여기서 꺼내 처리 writeback worker 1. do_writepages() 2. a_ops->writepages() 3. write_inode() 4. I_SYNC 해제 디스크 데이터+메타 기록 완료 b_more_io 리스트 I_SYNC 중 재-dirty된 inode → 다음 writeback 라운드에서 처리 주요 sysctl 파라미터 dirty_background_ratio=10 dirty_ratio=20 dirty_writeback_centisecs=500 dirty_expire_centisecs=3000 background 초과 → 비동기 writeback 시작 | ratio 초과 → 프로세스가 직접 writeback (throttle)
/* writeback 워커 핵심 루프 — fs/fs-writeback.c */
static long writeback_sb_inodes(struct super_block *sb,
                               struct bdi_writeback *wb,
                               struct wb_writeback_work *work)
{
    while (!list_empty(&wb->b_io)) {
        struct inode *inode = wb_inode(wb->b_io.prev);

        spin_lock(&inode->i_lock);

        /* I_SYNC 설정 (다른 스레드의 동시 writeback 방지) */
        inode->i_state |= I_SYNC;

        spin_unlock(&inode->i_lock);

        /* dirty 페이지 기록 */
        __writeback_single_inode(inode, &wbc);

        spin_lock(&inode->i_lock);
        inode->i_state &= ~I_SYNC;

        /* writeback 중 재-dirty되었으면 b_more_io로 이동 */
        if (inode->i_state & I_DIRTY)
            inode_io_list_move_locked(inode, wb, &wb->b_more_io);
        else
            list_del_init(&inode->i_io_list);  /* clean */

        spin_unlock(&inode->i_lock);
    }
}
코드 설명
  • writeback_sb_inodesfs/fs-writeback.c에 정의된 writeback 워커의 핵심 루프입니다. BDI(Backing Device Info)의 b_io 리스트에서 dirty inode를 하나씩 꺼내 처리합니다. wb_workfn()wb_do_writeback()wb_writeback() 호출 체인에서 실행됩니다.
  • I_SYNC 설정현재 writeback 진행 중임을 표시합니다. 다른 스레드가 fsync()로 같은 inode를 동시에 기록하려 하면 I_SYNC가 해제될 때까지 inode_wait_for_writeback()에서 대기합니다. 이중 기록을 방지하는 배타적 잠금 역할입니다.
  • __writeback_single_inode실제 I/O를 수행하는 함수입니다. a_ops->writepages()로 dirty 페이지를 기록하고, write_inode()로 메타데이터를 디스크에 씁니다. wbc(writeback_control) 구조체가 기록 범위와 동기화 모드를 제어합니다.
  • b_more_io 이동writeback 도중 프로세스가 같은 inode에 다시 쓰기를 하면 re-dirty 상태가 됩니다. 이 경우 b_more_io 리스트로 이동하여 다음 writeback 라운드에서 재처리됩니다. clean 상태가 되면 i_io_list에서 완전히 제거됩니다.
💡

writeback 성능 튜닝: 데이터베이스 서버에서는 dirty_background_ratio를 낮게(5%) 설정하여 빈번한 소량 기록을, 대용량 순차 쓰기 워크로드에서는 높게(20-40%) 설정하여 대량 배치 기록을 유도합니다. /proc/meminfoDirtyWriteback 값을 모니터링하세요.

파일시스템 Freeze/Thaw와 inode

파일시스템 freeze는 모든 쓰기 I/O를 중단시키고 일관된 스냅샷 상태를 만드는 메커니즘입니다. LVM 스냅샷, 온라인 백업 등에 필수적이며, inode의 dirty 상태와 writeback에 직접적 영향을 줍니다.

/* fs/super.c — freeze/thaw 핵심 경로 */

/* freeze 수준 (단계적 진행) */
enum {
    SB_UNFROZEN      = 0,  /* 정상 */
    SB_FREEZE_WRITE  = 1,  /* 새 쓰기 거부 */
    SB_FREEZE_PAGEFAULT = 2, /* mmap 쓰기 폴트 거부 */
    SB_FREEZE_FS     = 3,  /* FS 내부 트랜잭션 완료 대기 */
    SB_FREEZE_COMPLETE = 4, /* 완전 freeze 완료 */
};

int freeze_super(struct super_block *sb, enum freeze_holder who)
{
    /* 1. SB_FREEZE_WRITE → 새 write/truncate 차단 */
    sb_wait_write(sb, SB_FREEZE_WRITE);

    /* 2. SB_FREEZE_PAGEFAULT → mmap page_mkwrite 차단 */
    sb_wait_write(sb, SB_FREEZE_PAGEFAULT);

    /* 3. dirty inode writeback 강제 실행 + 대기 */
    sync_filesystem(sb);

    /* 4. SB_FREEZE_FS → FS별 freeze 콜백 */
    if (sb->s_op->freeze_fs)
        sb->s_op->freeze_fs(sb);
    /*    ext4: 저널 커밋 + 배리어 */
    /*    XFS: 로그 quiesce */
    /*    Btrfs: 트랜잭션 커밋 */
}
freeze 단계차단 대상inode 영향
SB_FREEZE_WRITEvfs_write(), vfs_truncate()새 dirty 마킹 차단, 진행 중인 write 완료 대기
SB_FREEZE_PAGEFAULTpage_mkwrite()mmap 쓰기 폴트 대기 — dirty 페이지 생성 차단
SB_FREEZE_FSFS 내부 트랜잭션(Transaction)모든 dirty inode writeback 완료, 저널/로그 flush
SB_FREEZE_COMPLETE전체디스크에 모든 inode가 깨끗한(clean) 상태
# 사용자 공간에서 freeze/thaw
fsfreeze -f /mnt/data    # freeze → 모든 I/O 중단
# ... LVM 스냅샷 생성, 블록 레벨 백업 등 ...
fsfreeze -u /mnt/data    # thaw → I/O 재개

# freeze 상태 확인
cat /proc/mounts | grep /mnt/data
# 또는
xfs_freeze -s /mnt/data  # XFS 전용 (fsfreeze와 동일)
freeze 중 주의: fsfreeze -f 상태에서 해당 파일시스템에 쓰기를 시도하는 프로세스는 무한 대기(uninterruptible sleep)에 빠집니다. thaw 없이 오래 유지하면 시스템이 멈춘 것처럼 보일 수 있습니다. 루트 파일시스템은 절대 freeze하지 마세요 — 시스템 전체가 정지합니다.

파일시스템별 inode 구현 비교

각 파일시스템은 VFS struct inode를 자체 구조체에 임베드하여 확장합니다. 이 패턴은 C 언어에서 상속 없이 다형성을 구현하는 커널의 대표적 기법입니다.

파일시스템별 inode 확장 패턴 (container_of) struct inode (VFS 공통) ext4_inode_info i_data[15] (extent/block) i_flags (ext4 고유) i_file_acl i_disksize i_es_tree (extent status) i_reserved_data_blocks vfs_inode (embedded) i_crypt_info, ... xfs_inode i_mount (xfs_mount *) i_ino (xfs_ino_t) i_df (data fork) i_af (attr fork) i_cowfp (CoW fork) i_forkoff (fork offset) vfs_inode (embedded) i_itemp, i_imap, ... btrfs_inode root (btrfs_root *) location (btrfs_key) generation (u64) disk_i_size flags (COMPRESS, ...) ordered_tree vfs_inode (embedded) delalloc_inodes, ...

container_of 패턴

/* container_of 매크로 — include/linux/container_of.h */
#define container_of(ptr, type, member) ({            \
    void *__mptr = (void *)(ptr);                     \
    (type *)(__mptr - offsetof(type, member)); })

/* 각 파일시스템의 변환 매크로 */
/* ext4 */
static inline struct ext4_inode_info *EXT4_I(struct inode *inode)
{
    return container_of(inode, struct ext4_inode_info, vfs_inode);
}

/* XFS */
static inline struct xfs_inode *XFS_I(struct inode *inode)
{
    return container_of(inode, struct xfs_inode, vfs_inode);
}

/* Btrfs */
static inline struct btrfs_inode *BTRFS_I(struct inode *inode)
{
    return container_of(inode, struct btrfs_inode, vfs_inode);
}

/* 사용 예시: VFS에서 FS-specific 데이터 접근 */
static int ext4_file_open(struct inode *inode,
                         struct file *filp)
{
    struct ext4_inode_info *ei = EXT4_I(inode);
    /* ei->i_data, ei->i_flags 등 접근 */
}

파일시스템 inode 확장 필드 비교

필드 카테고리ext4XFSBtrfs
데이터 위치i_data[15] (extent/block)data fork (extent B+tree)file extent item (B-tree)
속성 저장inode 잔여공간 + EA blockattr forkxattr item (B-tree)
inode 번호고정 테이블 인덱스AG 내 동적 할당objectid (서브볼륨별)
CoW 지원미지원reflink (4.16+)기본 (전체 CoW)
인라인 데이터inline_data 옵션local formatinline extent
압축미지원미지원zstd, lzo, zlib
암호화(Encryption)fscrypt미지원미지원 (계획 중)
slab 캐시ext4_inode_cachexfs_inode_cachebtrfs_inode_cache

inode 캐시와 LRU 관리

사용이 끝난 inode(i_count == 0)는 즉시 삭제되지 않고 LRU 리스트에 들어가 캐시됩니다. 이후 동일 파일 재접근 시 디스크 I/O 없이 즉시 반환할 수 있습니다.

LRU 메커니즘

/* iput() — 참조 해제 경로 */
void iput(struct inode *inode)
{
    if (!inode)
        return;

    if (atomic_dec_and_lock(&inode->i_count, &inode->i_lock)) {
        /* i_count가 0이 됨 */
        if (inode->i_nlink &&
            (inode->i_state & ~I_DIRTY_TIME) == 0) {
            /* 링크 남아있고 clean → LRU에 추가 */
            inode_add_lru(inode);
            spin_unlock(&inode->i_lock);
        } else {
            /* nlink==0이면 즉시 evict */
            inode->i_state |= I_WILL_FREE;
            spin_unlock(&inode->i_lock);
            evict(inode);
        }
    }
}

/* LRU 추가 — i_lru 리스트에 연결 */
static void inode_add_lru(struct inode *inode)
{
    if (!(inode->i_sb->s_flags & SB_ACTIVE))
        return;
    /* I_REFERENCED 설정 → 2차 기회 */
    inode->i_state |= I_REFERENCED;
    list_lru_add(&inode->i_sb->s_inode_lru, &inode->i_lru);
    /* percpu 카운터 증가 */
    this_cpu_inc(nr_unused);
}
코드 설명
  • iput — atomic_dec_and_locki_count를 원자적으로 감소시키면서, 0이 되는 순간 i_lock을 획득합니다. 이 원자적 결합은 "감소 후 잠금 사이"에 다른 스레드가 __iget()으로 참조를 가져가는 경쟁 조건을 방지합니다. fs/inode.c에 정의되어 있습니다.
  • i_nlink && clean → LRU 추가링크 카운트가 남아 있고 dirty 상태가 아니면, 이 inode는 나중에 다시 사용될 가능성이 있으므로 즉시 해제하지 않고 LRU(Least Recently Used) 캐시에 추가합니다. 메모리 압박 시 prune_icache_sb()가 LRU에서 오래된 inode를 회수합니다.
  • nlink==0 → evict링크 카운트가 0이면 파일이 삭제된 상태이므로 캐싱할 의미가 없습니다. I_WILL_FREE를 설정하여 다른 스레드에게 evict 예정임을 알리고, evict()를 호출하여 페이지 캐시 해제, 디스크 블록 반환, 해시 테이블 제거를 수행합니다.
  • inode_add_lru — I_REFERENCED2차 기회(second chance) 알고리즘의 핵심입니다. LRU에 추가할 때 I_REFERENCED 비트를 설정합니다. 메모리 회수 시 이 비트가 설정된 inode는 한 번 더 기회를 받고 비트만 클리어됩니다. 두 번째 순회에서도 참조되지 않았으면 그때 evict됩니다.
  • SB_ACTIVE 검사superblock이 비활성 상태(umount 진행 중)이면 LRU에 추가하지 않습니다. umount 경로에서는 모든 inode가 즉시 해제되어야 하므로 캐싱이 무의미합니다.
inode LRU 캐시 관리 활성 inode i_count > 0 해시 테이블에 존재 LRU에 없음 iput() LRU 리스트 (sb->s_inode_lru) MRU A B C D LRU 재접근 → ihold() → LRU에서 제거 메모리 압력 prune_icache_sb() LRU 끝(D)부터 회수 I_REFERENCED → 2차 기회 (건너뜀) evict() → destroy_inode() vm.vfs_cache_pressure 튜닝 = 100 (기본): 균형 잡힌 회수 = 50: inode 캐시 유지 선호 (파일 서버) = 200: 적극적 회수 (메모리 절약)
# inode 캐시 모니터링

# 할당된 inode 수 / free inode 수
cat /proc/sys/fs/inode-nr
# 예: 45678  234  (할당 45678, 빈 234)

# 상태별 inode 수
cat /proc/sys/fs/inode-state
# nr_inodes  nr_free_inodes  preshrink

# slab 캐시에서 inode 캐시 확인
slabtop -o | grep inode_cache
# 또는
cat /proc/slabinfo | grep inode_cache

# vfs_cache_pressure 확인 및 변경
cat /proc/sys/vm/vfs_cache_pressure
# 기본값: 100

# 파일 서버에서 inode 캐시 유지 선호
echo 50 > /proc/sys/vm/vfs_cache_pressure

# dentry 캐시와의 비교
cat /proc/sys/fs/dentry-state
# nr_dentry  nr_unused  age_limit  want_pages
inode 캐시 과다 소비 주의: 수백만 개의 소파일을 보유한 파일 서버에서는 inode 캐시가 수 GB의 메모리를 소비할 수 있습니다. slabtop에서 inode_cachedentry_cache가 메모리의 대부분을 차지하고 있다면, vfs_cache_pressure를 높이거나 drop_caches로 수동 해제를 고려하세요. 단, drop_caches는 프로덕션 환경에서 성능 저하를 유발할 수 있으므로 주의가 필요합니다.

디스크 inode 레이아웃 (ext4)

메모리의 VFS inode와 달리, 디스크 inode는 파일시스템 포맷에 따라 고정된 레이아웃을 가집니다. ext4를 예로 들어 디스크 inode의 물리적 구조를 살펴봅니다.

ext4 디스크 inode 레이아웃 (ext4_inode) ext4_inode (디스크) 0x00 i_mode (2B) + i_uid_lo (2B) 0x04 i_size_lo (4B) 0x08 i_atime (4B) 0x0C i_ctime (4B) 0x10 i_mtime (4B) 0x14 i_dtime (4B, 삭제 시간) 0x18 i_gid_lo (2B) + i_links_count (2B) 0x1C i_blocks_lo (4B) 0x20 i_flags (4B, EXT4_EXTENTS_FL 등) 0x28 i_block[15] (60B) extent tree root 또는 inline data 또는 symlink target 0x64 i_generation (4B, NFS 용) 0x68 i_file_acl_lo (4B) + i_size_hi/dir_acl (4B) --- 128바이트 경계 --- 0x80+ 확장 필드 (256B inode인 경우) crtime, version_hi, *_extra (나노초), xattr 인라인 inode 테이블 위치 Block Group N의 inode table 시작 블록에서 inode 번호로 오프셋 계산: offset = ((ino - 1) % inodes_per_group) * inode_size i_block[15] 해석 EXT4_EXTENTS_FL 설정 시: extent tree root 미설정 시: 직접/간접 블록 포인터 (legacy) inline_data: 소파일 데이터 직접 저장 flex_bg 최적화 여러 블록 그룹의 inode 테이블을 연속 배치 → 순차 스캔 성능 향상 (find, ls -R) 기본 flex_bg_size = 16
/* include/linux/ext4.h — 디스크 inode 구조 */
struct ext4_inode {
    __le16  i_mode;         /* 파일 유형 + 권한 */
    __le16  i_uid;          /* 소유자 UID 하위 16비트 */
    __le32  i_size_lo;      /* 파일 크기 하위 32비트 */
    __le32  i_atime;        /* 접근 시간 (초) */
    __le32  i_ctime;        /* inode 변경 시간 */
    __le32  i_mtime;        /* 데이터 수정 시간 */
    __le32  i_dtime;        /* 삭제 시간 */
    __le16  i_gid;          /* 그룹 GID 하위 16비트 */
    __le16  i_links_count;  /* 하드 링크 수 */
    __le32  i_blocks_lo;    /* 512B 블록 수 */
    __le32  i_flags;        /* ext4 플래그 */
    union {
        struct { __le32 l_i_version; } linux1;
    } osd1;
    __le32  i_block[15];    /* 60바이트: extent tree 또는 블록 포인터 */
    __le32  i_generation;   /* NFS 파일 핸들 세대 */
    __le32  i_file_acl_lo;  /* ACL 블록 (하위) */
    __le32  i_size_high;    /* 파일 크기 상위 32비트 */
    /* --- 128바이트 경계 --- */
    /* 확장 필드 (256B inode) */
    __le16  i_extra_isize;  /* 확장 필드 크기 */
    __le16  i_checksum_hi;  /* crc32c 상위 */
    __le32  i_ctime_extra;  /* ctime 나노초 + 에포크 확장 */
    __le32  i_mtime_extra;  /* mtime 나노초 */
    __le32  i_atime_extra;  /* atime 나노초 */
    __le32  i_crtime;       /* 생성 시간 (초) */
    __le32  i_crtime_extra; /* 생성 시간 나노초 */
    __le32  i_version_hi;   /* NFS 버전 상위 */
    __le32  i_projid;       /* 프로젝트 ID */
};
# ext4 디스크 inode 직접 조회

# debugfs로 inode 정보 확인
debugfs -R "stat <42>" /dev/sda1
# Inode: 42   Type: regular    Mode:  0644   Flags: 0x80000
# Generation: 1234567890   Version: 0x00000001
# User:  1000   Group:  1000   Size: 4096
# File ACL: 0
# Links: 1   Blockcount: 8
# Fragment:  Address: 0    Number: 0    Size: 0
# ctime: 0x65a12345:12345678 -- ...
# atime: 0x65a12346:00000000 -- ...
# mtime: 0x65a12345:12345678 -- ...
# crtime: 0x65a00000:00000000 -- ...
# EXTENTS:
# (0):1234567

# inode 크기 확인
tune2fs -l /dev/sda1 | grep "Inode size"
# Inode size:             256

# inode 테이블 위치 확인
dumpe2fs /dev/sda1 | grep "Inode table"
# Group 0: Inode table at 1025-1536

# statx로 crtime 확인 (유저스페이스)
stat /home/user/test.txt
# Birth: 2024-01-15 10:30:00.123456789 +0900

VFS 객체 관계 종합

지금까지 다룬 inode와 관련된 VFS 핵심 객체들의 관계를 종합적으로 정리합니다. superblock, inode, dentry, file 네 객체가 어떻게 연결되는지 이해하는 것이 VFS 전체를 파악하는 핵심입니다.

VFS 핵심 객체 관계도 struct super_block s_inodes (모든 inode 리스트), s_op, s_type struct inode i_ino, i_mode, i_op i_sb → superblock struct inode (디렉터리) i_op = &dir_iops struct dentry d_name, d_parent, d_inode → inode d_inode d_parent struct dentry (하드링크) 다른 이름, 같은 inode struct file f_pos, f_op, f_path(dentry+vfsmount) f_path.dentry struct file (같은 파일, 다른 fd) address_space i_pages (page cache) i_mapping
객체식별 기준생명주기캐시 전략
super_block파일시스템 (디바이스)mount ~ umount항상 메모리에 유지
inode(sb, i_ino) 쌍최초 접근 ~ evict해시 + LRU
dentry경로명 컴포넌트최초 조회 ~ shrink해시 + LRU
file프로세스별 열린 파일open ~ close캐시 없음 (1:1 매핑)
address_spaceinode에 임베드inode와 동일XArray (radix tree)

inode evict 경로

inode가 시스템에서 완전히 제거되는 과정을 evict라 합니다. evict는 메모리 회수(LRU shrinker) 또는 파일 삭제(i_nlink == 0) 시 발생합니다.

/* fs/inode.c — evict() 핵심 */
static void evict(struct inode *inode)
{
    const struct super_operations *op = inode->i_sb->s_op;

    /* 1. BDI writeback 큐에서 제거 */
    inode_io_list_del(inode);

    /* 2. sb의 inode 리스트에서 제거 */
    inode_sb_list_del(inode);

    /* 3. I_FREEING 상태 설정 (다른 스레드에게 evict 중임을 알림) */
    spin_lock(&inode->i_lock);
    inode->i_state |= I_FREEING;
    spin_unlock(&inode->i_lock);

    /* 4. FS별 evict_inode 호출 */
    if (op->evict_inode)
        op->evict_inode(inode);
    else {
        truncate_inode_pages_final(&inode->i_data);
        clear_inode(inode);
    }

    /* 5. 해시 테이블에서 제거 */
    remove_inode_hash(inode);

    /* 6. I_CLEAR 설정, 대기자 깨우기 */
    spin_lock(&inode->i_lock);
    wake_up_bit(&inode->i_state, __I_NEW);
    inode->i_state = I_FREEING | I_CLEAR;
    spin_unlock(&inode->i_lock);

    /* 7. 메모리 해제 */
    destroy_inode(inode);  /* → free_inode() 또는 kmem_cache_free() */
}

/* FS별 evict_inode 구현 예시 (ext4) */
void ext4_evict_inode(struct inode *inode)
{
    if (inode->i_nlink || is_bad_inode(inode))
        goto no_delete;

    /* nlink == 0: 실제 삭제 */
    dquot_initialize(inode);  /* 쿼타 정보 초기화 */
    ext4_begin_ordered_truncate(inode, 0);
    truncate_inode_pages_final(&inode->i_data);

    /* 저널에 삭제 트랜잭션 기록 */
    handle = ext4_journal_start(inode, EXT4_HT_TRUNCATE, ...);
    ext4_mark_inode_dirty(handle, inode);
    ext4_free_inode(handle, inode);  /* inode 비트맵 해제 */
    ext4_journal_stop(handle);
    return;

no_delete:
    /* 링크 남아있음: 데이터 페이지만 해제 */
    truncate_inode_pages_final(&inode->i_data);
    clear_inode(inode);
}
코드 설명
  • evict — inode_io_list_del()BDI(Backing Device Info) writeback 큐에서 이 inode를 제거합니다. writeback 스레드가 이 inode를 처리 중이었다면 완료 후 제거됩니다. 이후에는 이 inode에 대한 writeback이 스케줄되지 않습니다.
  • evict — inode_sb_list_del()super_block.s_inodes 연결 리스트에서 제거합니다. 이 리스트는 파일시스템 umount 시 전체 inode를 순회하거나 sync_inodes_sb()에서 사용합니다.
  • evict — I_FREEING 설정i_lock 스핀락을 보유한 상태에서 I_FREEING 비트를 설정합니다. 이후 find_inode_fast()가 이 inode를 발견하면 __wait_on_freeing_inode()를 호출하여 evict 완료를 기다립니다. 이 "예고(tombstone)" 메커니즘이 use-after-free를 방지합니다.
  • evict — op->evict_inode(inode)파일시스템별 evict 콜백입니다. NULL이면 VFS가 기본 동작(truncate_inode_pages_final + clear_inode)을 수행합니다. ext4는 ext4_evict_inode()를 구현하여 저널 트랜잭션을 통한 디스크 inode 해제를 처리합니다.
  • evict — remove_inode_hash()i_hash hlist 노드를 해시 테이블에서 분리합니다. 이후에는 iget_locked()로 이 inode를 검색할 수 없습니다. I_FREEING 설정 후, op->evict_inode 호출 후에 수행됩니다.
  • evict — wake_up_bit(__I_NEW) + I_CLEARI_FREEING | I_CLEAR 상태로 전환하고, __wait_on_freeing_inode()에서 대기 중인 스레드들을 깨웁니다. 깨어난 스레드들은 find_inode_fast()에서 재시도하여 이 inode가 없음을 확인합니다.
  • evict — destroy_inode()RCU 유예기간 후 실제 메모리를 해제합니다. s_op->free_inode가 있으면 RCU 콜백으로 등록하고, 없으면 kmem_cache_free(inode_cachep, inode)를 직접 호출합니다. RCU 보호는 rcu_read_lock() 상태에서 inode 포인터를 역참조하는 RCU 워커들이 완료할 때까지 대기합니다.
  • ext4_evict_inode — i_nlink 분기i_nlink != 0이면 메모리에서만 제거(evict)되는 것이므로 디스크 inode는 보존합니다. i_nlink == 0이면 unlink된 파일이므로 디스크의 inode 비트맵과 데이터 블록을 해제합니다. is_bad_inode() 검사는 I/O 에러로 손상된 inode를 안전하게 건너뜁니다.
  • ext4_evict_inode — dquot_initialize()디스크 해제 전에 쿼타(Quota) 컨텍스트를 초기화합니다. inode의 블록과 inode 카운트를 해당 사용자/그룹 쿼타 계정에서 차감하기 위해 필요합니다. 쿼타가 비활성화되어 있으면 no-op입니다.
ℹ️

삭제된 파일과 열린 fd: unlink()로 파일을 삭제해도 i_nlink만 0이 됩니다. 해당 파일을 열고 있는 프로세스가 있다면(i_count > 0), 실제 evict는 마지막 close()가 수행될 때까지 지연(Latency)됩니다. 이 동안 /proc/PID/fd/에서 (deleted) 표시로 확인할 수 있으며, 프로세스는 정상적으로 read/write를 계속할 수 있습니다. 이것이 로그 파일 삭제 후에도 디스크 공간이 즉시 해제되지 않는 이유입니다.

iput() 전체 콜 체인

iput()는 inode 참조를 해제하는 핵심 함수입니다. i_count를 감소시키고, 0이 되면 inode의 i_nlink과 dirty 상태에 따라 LRU 캐시 보존 또는 evict(메모리 해제)를 결정합니다.

/* fs/inode.c — iput() 전체 구현 */

void iput(struct inode *inode)
{
    if (!inode)
        return;

    /* i_count를 감소시키면서 동시에 i_lock을 원자적으로 획득 */
    if (atomic_dec_and_lock(&inode->i_count, &inode->i_lock))
        iput_final(inode);  /* i_count == 0: 최종 처리 */
}

static void iput_final(struct inode *inode)
{
    struct super_block *sb = inode->i_sb;
    const struct super_operations *op = sb->s_op;
    unsigned long state;
    int drop;

    /* ① s_op->drop_inode()로 즉시 evict 여부 결정 */
    if (op->drop_inode)
        drop = op->drop_inode(inode);
    else
        drop = generic_drop_inode(inode);

    if (!drop &&
        !(inode->i_state & I_DONTCACHE) &&
        (sb->s_flags & SB_ACTIVE)) {
        /* ② nlink > 0이고 FS 활성 상태: LRU에 보존 */
        __inode_add_lru(inode, true);
        spin_unlock(&inode->i_lock);
        return;                    /* 캐시에 유지, evict 안 함 */
    }

    state = inode->i_state;
    if (!drop) {
        /* I_DONTCACHE: 캐시 보존하지 않고 즉시 evict */
        WRITE_ONCE(inode->i_state, state | I_WILL_FREE);
        spin_unlock(&inode->i_lock);

        /* ③ dirty inode를 디스크에 기록 후 evict */
        write_inode_now(inode, 1);  /* sync writeback */

        spin_lock(&inode->i_lock);
        WRITE_ONCE(inode->i_state, (state & ~I_WILL_FREE) | I_FREEING);
    } else {
        /* ④ nlink == 0 또는 drop_inode가 1 반환: 즉시 evict */
        WRITE_ONCE(inode->i_state, state | I_FREEING);
    }

    inode_lru_list_del(inode);  /* LRU에서 제거 (있었다면) */
    spin_unlock(&inode->i_lock);

    /* ⑤ 실제 evict 수행 → FS별 evict_inode → destroy_inode */
    evict(inode);
}

/* generic_drop_inode — 기본 drop 정책 */
int generic_drop_inode(struct inode *inode)
{
    /* nlink == 0이면 삭제 → drop=1 → 즉시 evict */
    /* 또는 unhashed이면(remove_inode_hash 호출됨) drop=1 */
    return !inode->i_nlink || inode_unhashed(inode);
}
코드 설명
  • iput — atomic_dec_and_locki_count를 원자적으로 감소시키면서, 결과가 0이면 i_lock을 잠금 상태로 반환합니다. 0이 아니면 아무 잠금 없이 false를 반환합니다. 이 단일 원자 연산으로 "감소 후 검사" 사이의 경쟁 조건을 제거합니다.
  • iput_final — drop_inode()s_op->drop_inode가 NULL이면 generic_drop_inode()를 호출하여 i_nlink == 0이면 1(drop), 아니면 0(보존)을 반환합니다. NFS 등은 이를 재정의하여 서버 측 상태를 추가로 확인합니다.
  • iput_final — __inode_add_lrudrop=0이고 파일시스템이 활성 상태(SB_ACTIVE)이면 inode를 LRU 리스트에 추가합니다. 나중에 같은 파일을 다시 접근하면 디스크 I/O 없이 LRU에서 재활용됩니다. 메모리 압력 시 shrinker가 LRU 끝에서부터 회수합니다.
  • iput_final — I_WILL_FREE + write_inode_nowI_DONTCACHE 플래그가 설정되어 캐시하지 않지만 i_nlink > 0인 경우입니다. dirty 데이터를 먼저 디스크에 기록한 후 evict합니다. I_WILL_FREE 상태는 다른 스레드에게 "이 inode는 곧 해제된다"를 알려 새로운 참조 획득을 방지합니다.
  • iput_final — I_FREEING (drop=1)i_nlink == 0인 삭제된 파일이므로 디스크 writeback 없이 바로 evict로 진행합니다. evict() 내부에서 s_op->evict_inode가 디스크의 inode와 데이터 블록을 해제합니다.
  • iput_final — evict()최종 단계로, 해시 테이블 제거, FS별 evict_inode 호출, 페이지 캐시 해제, 메모리 해제를 수행합니다. 상세 흐름은 inode evict 경로 섹션을 참조하세요.
iput() 결정 트리 iput(inode) atomic_dec_and_lock(i_count) i_count==0? No 반환 (유지) Yes i_nlink==0? No (nlink>0) LRU에 보존 Yes (삭제됨) I_FREEING → evict() evict_inode() → 디스크 블록 해제 remove_inode_hash() destroy_inode() → kmem_cache_free() sb->s_inode_lru에 추가 재접근 시 LRU에서 회수 메모리 부족 시 shrinker가 evict
iput() 호출 시 주의: iput()는 evict 경로에서 s_op->evict_inode()를 호출할 수 있으며, 이는 디스크 I/O를 수반합니다. 따라서 spinlock을 보유한 상태에서 iput()를 호출하면 안 됩니다. 또한 iget()으로 참조를 획득하지 않은 inode에 iput()를 호출하면 use-after-free가 발생합니다.

inode Truncation 경로

파일 크기를 줄이는 truncate()는 inode의 i_size를 변경하고, 잘려나간 범위의 데이터 블록과 페이지 캐시를 해제하는 복잡한 과정입니다. hole punch(fallocate PUNCH_HOLE)도 유사한 경로를 사용합니다.

inode Truncation 경로 truncate(2) / ftruncate(2) do_truncate() → notify_change() inode->i_op->setattr() (FS별 구현) 크기 줄이기 (size↓) 1. truncate_setsize(inode, new_size) → i_size 갱신 + truncate_pagecache() 2. truncate_pagecache(inode, new_size) → unmap_mapping_range() — PTE 무효화 → truncate_inode_pages_range() — 캐시 해제 3. FS별 블록 해제 (ext4_truncate 등) → extent tree/block map 조정 + 저널 크기 늘리기 (size↑) 1. i_size_write(inode, new_size) 2. 블록은 아직 할당 안 됨 (sparse) 3. 읽기 시 0 반환, 쓰기 시 할당 → "파일에 구멍(hole)" 생성 축소 확장 모든 경로에서 i_rwsem을 배타적(exclusive)으로 획득합니다
/* mm/truncate.c — 페이지 캐시 truncation */
void truncate_setsize(struct inode *inode, loff_t newsize)
{
    loff_t oldsize = inode->i_size;

    /* i_size를 원자적으로 갱신 */
    i_size_write(inode, newsize);

    if (newsize > oldsize)
        pagecache_isize_extended(inode, oldsize, newsize);
    else
        truncate_pagecache(inode, newsize);
}

void truncate_pagecache(struct inode *inode, loff_t newsize)
{
    struct address_space *mapping = inode->i_mapping;
    loff_t holebegin = round_up(newsize, PAGE_SIZE);

    /* 1. mmap된 PTE 무효화 → SIGBUS 방지 */
    if (mapping_mapped(mapping))
        unmap_mapping_range(mapping, holebegin, 0, 1);

    /* 2. 페이지 캐시에서 해당 범위 제거 */
    truncate_inode_pages(mapping, newsize);

    /* 3. 재확인 (race 방지) */
    unmap_mapping_range(mapping, holebegin, 0, 1);
}

/* ext4 truncate — fs/ext4/inode.c */
void ext4_truncate(struct inode *inode)
{
    /* 1. 저널 트랜잭션 시작 */
    handle = ext4_journal_start(inode, EXT4_HT_TRUNCATE, needed);

    /* 2. extent tree에서 잘린 범위의 extent 제거 */
    ext4_ext_truncate(handle, inode);
    /*    → 블록 비트맵 해제, 그룹 디스크립터 갱신 */
    /*    → 저널에 메타데이터 변경 기록 */

    /* 3. inode dirty 마킹 */
    ext4_mark_inode_dirty(handle, inode);

    /* 4. 저널 트랜잭션 종료 */
    ext4_journal_stop(handle);
}
Truncation 변형시스템콜효과블록 해제
truncatetruncate(2), ftruncate(2)파일 끝에서부터 자르기
hole punchfallocate(PUNCH_HOLE)파일 중간에 구멍 뚫기
collapse rangefallocate(COLLAPSE_RANGE)범위 제거 + 뒤쪽 당기기
zero rangefallocate(ZERO_RANGE)범위를 0으로 채우기옵션
insert rangefallocate(INSERT_RANGE)빈 공간 삽입 + 뒤쪽 밀기아니오
ℹ️

부분 페이지 처리: truncation 지점이 페이지 경계에 걸리면, 해당 페이지의 잘린 부분만 0으로 채웁니다(folio_zero_range()). 페이지 전체를 버리지 않습니다 — 앞부분의 유효한 데이터는 보존합니다. 이 세밀한 처리가 truncate의 복잡성을 높이는 주요 원인입니다.

Orphan Inode 리스트

Orphan inode는 nlink가 0이지만 아직 사용 중인(열려 있는) inode입니다. unlink 후 열린 fd가 남아있거나, O_TMPFILE로 생성된 파일이 이에 해당합니다. 저널링 파일시스템은 orphan 리스트를 유지하여 크래시 후에도 이런 inode의 공간을 올바르게 회수합니다.

/* 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);

    /* 1. 메모리: 슈퍼블록의 orphan 리스트에 추가 */
    list_add(&EXT4_I(inode)->i_orphan, &sbi->s_orphan);

    /* 2. 디스크: 슈퍼블록의 s_last_orphan 체인에 연결 */
    /*    inode의 i_dtime 필드를 다음 orphan의 ino로 사용 */
    /*    슈퍼블록 → ino_A → ino_B → ino_C → 0 (종료) */
    NEXT_ORPHAN(inode) = le32_to_cpu(sbi->s_es->s_last_orphan);
    sbi->s_es->s_last_orphan = cpu_to_le32(inode->i_ino);

    /* 3. 저널에 기록 → 크래시 시에도 복구 가능 */
    ext4_handle_dirty_metadata(handle, NULL, sbi->s_sbh);
    ext4_mark_iloc_dirty(handle, inode, &iloc);

    return 0;
}

/* fs/ext4/super.c — 마운트 시 orphan 정리 */
static void ext4_orphan_cleanup(struct super_block *sb,
                                struct ext4_super_block *es)
{
    unsigned int s_flags = sb->s_flags;
    int nr_orphans = 0;

    /* s_last_orphan 체인을 순회하며 정리 */
    while (es->s_last_orphan) {
        struct inode *inode;
        ino = le32_to_cpu(es->s_last_orphan);

        inode = ext4_iget(sb, ino, ...);

        if (inode->i_nlink == 0) {
            /* nlink=0: 실제 삭제 수행 */
            iput(inode);  /* → evict_inode → ext4_free_inode */
        } else {
            /* nlink>0: truncate 미완 → 자르기 완료 */
            ext4_truncate(inode);
            iput(inode);
        }
        nr_orphans++;
    }
    /* "EXT4-fs: N orphan inodes cleaned up" 커널 로그 */
}
ext4 Orphan Inode 연결 리스트 (디스크 레이아웃) Superblock s_last_orphan = inode #42 (저널에 기록됨) inode #42 nlink=0 i_dtime → #87 (unlink된 열린 파일) inode #87 nlink=0 i_dtime → #155 (O_TMPFILE) inode #155 nlink=1 i_dtime → 0 (끝) (truncate 미완) Orphan 발생 시나리오 • nlink=0: unlink 후 열린 fd 남음, O_TMPFILE → 크래시 시 inode+데이터 블록 삭제 • nlink>0: truncate 도중 크래시 → 마운트 시 truncate 재실행 ※ i_dtime 필드를 "다음 orphan의 inode 번호" 포인터로 재활용 (nlink=0이면 삭제 시간 불필요)
파일시스템Orphan 구현저장 위치
ext4i_dtime 필드로 연결 리스트(Linked List) 구성슈퍼블록(Superblock) + 각 inode의 i_dtime
ext4 (5.15+)orphan file (COMPAT_ORPHAN_FILE)전용 inode에 orphan 비트맵(Bitmap) 저장 — 동시성 향상
XFSAGI unlinked 해시 체인각 AG 헤더의 unlinked 버킷
Btrfsorphan item (B-tree)FS tree에 ORPHAN_ITEM 키
💡

Orphan 디버깅(Debugging): dmesg | grep orphan으로 마운트 시 정리된 orphan 수를 확인할 수 있습니다. 비정상 종료 후 "EXT4-fs: 3 orphan inodes deleted" 같은 메시지가 나타납니다. debugfs -R "lsdel"로 최근 삭제된 inode를 나열할 수 있으며, orphan 파일 기능은 tune2fs -O orphan_file로 활성화합니다 (커널 5.15+).

inode 잠금 체계

inode 관련 동시성 제어는 여러 레벨의 잠금으로 구성됩니다. 잘못된 잠금 순서는 데드락을 유발하므로, 커널은 엄격한 잠금 순서(lock ordering)를 정의합니다.

잠금타입보호 대상일반적 잠금 순서
i_rwsemrw_semaphore파일 데이터 (read/write/truncate)1 (가장 바깥)
i_mutex (구)mutex (제거됨)i_rwsem으로 대체-
i_lockspinlocki_state, i_count 등 내부 필드2
i_mmap_rwsemrw_semaphoreaddress_space의 i_mmap 트리별도 경로
mapping->invalidate_lockrw_semaphorefolio 무효화 보호i_rwsem 안에서
i_pages lockXArray lock페이지 캐시 XArray 조작최내부
/* 잠금 순서 예시: 버퍼드 write */

/* 1. i_rwsem (exclusive) 획득 */
inode_lock(inode);

    /* 2. 페이지 획득 및 write_begin */
    a_ops->write_begin(file, mapping, pos, len, &folio);

        /* 3. folio lock (xa_lock 아래) */
        folio_lock(folio);

            /* 4. 블록 매핑 */
            ext4_map_blocks(handle, inode, &map, ...);

        folio_unlock(folio);  /* write_begin 내부에서 */

    /* 사용자 데이터 복사 */
    copy_page_from_iter_atomic(page, offset, bytes, from);

    a_ops->write_end(file, mapping, pos, len, copied, folio);

inode_unlock(inode);

/* 다중 inode 잠금 (rename 시) */
/* lock_rename()은 두 디렉터리를 inode 포인터 순서로 잠금 */
struct dentry *lock_rename(struct dentry *p1, struct dentry *p2)
{
    if (p1 == p2) {
        inode_lock_nested(p1->d_inode, I_MUTEX_PARENT);
        return NULL;
    }
    /* 포인터 주소 순서로 잠금 → 데드락 방지 */
    if (p1->d_inode < p2->d_inode) {
        inode_lock_nested(p1->d_inode, I_MUTEX_PARENT);
        inode_lock_nested(p2->d_inode, I_MUTEX_PARENT2);
    } else {
        inode_lock_nested(p2->d_inode, I_MUTEX_PARENT);
        inode_lock_nested(p1->d_inode, I_MUTEX_PARENT2);
    }
    return p1;
}
Lockdep과 잠금 검증: 커널의 CONFIG_LOCKDEP 옵션은 런타임에 잠금 순서 위반을 탐지합니다. inode_lock_nested()의 두 번째 파라미터(잠금 클래스)는 lockdep이 같은 타입의 잠금을 구별하는 데 사용됩니다. 파일시스템 개발 시 반드시 lockdep을 활성화하여 테스트해야 합니다.

inode 유틸리티 함수/매크로 종합

VFS는 inode 필드를 안전하게 조작하기 위한 유틸리티 함수와 매크로를 제공합니다. 필드를 직접 수정하면 원자성(Atomicity) 위반, dirty 마킹 누락, NFS 불일치 등 미묘한 버그가 발생하므로 반드시 이 API를 사용해야 합니다.

i_size_read() / i_size_write()

i_size는 64비트 필드이므로 32비트 아키텍처에서 원자적(Atomic) 접근이 불가능합니다. VFS는 seqcount 기반의 접근자를 제공합니다.

/* include/linux/fs.h — i_size 접근자 */

/* 64비트 아키텍처: 직접 읽기/쓰기 (원자적) */
/* 32비트 아키텍처: seqcount_t로 tear-free 보장 */

static inline loff_t i_size_read(const struct inode *inode)
{
#if BITS_PER_LONG == 32 && defined(CONFIG_SMP)
    loff_t i_size;
    unsigned int seq;

    do {
        seq = read_seqcount_begin(&inode->i_size_seqcount);
        i_size = inode->i_size;
    } while (read_seqcount_retry(&inode->i_size_seqcount, seq));
    return i_size;
#else
    return inode->i_size;  /* 64비트: 단일 load 명령으로 원자적 */
#endif
}

static inline void i_size_write(struct inode *inode, loff_t i_size)
{
#if BITS_PER_LONG == 32 && defined(CONFIG_SMP)
    preempt_disable();
    write_seqcount_begin(&inode->i_size_seqcount);
    inode->i_size = i_size;
    write_seqcount_end(&inode->i_size_seqcount);
    preempt_enable();
#else
    inode->i_size = i_size;
#endif
}
코드 설명
  • i_size_read — seqcount32비트 SMP 시스템에서 64비트 loff_t는 두 번의 32비트 load로 읽힙니다. 그 사이에 writer가 값을 변경하면 상위/하위 32비트가 서로 다른 버전이 되는 "torn read"가 발생합니다. seqcount는 reader가 값을 읽는 동안 writer가 개입했는지 감지하여 재시도합니다. 락-프리(Lock-free) 읽기를 보장하므로 hot path에서 성능 저하가 없습니다.
  • i_size_write — preempt_disablewriter는 write_seqcount_begin/end 사이에서 seqcount를 홀수로 만들어 reader에게 "쓰기 중"을 알립니다. preempt_disable()은 writer가 seqcount 업데이트 중간에 선점(Preemption)되어 reader가 무한 재시도하는 것을 방지합니다.
  • 64비트 아키텍처x86_64, ARM64 등에서는 64비트 load/store가 단일 명령으로 원자적이므로 seqcount가 불필요합니다. 컴파일 시 #if BITS_PER_LONG == 32로 분기됩니다.
직접 inode->i_size에 대입하지 마세요. 32비트 시스템에서 torn read가 발생하면 read()가 잘못된 파일 크기를 보고 엉뚱한 범위의 데이터를 반환합니다. 반드시 i_size_read()/i_size_write()를 사용하세요.

타임스탬프 접근자 함수

커널 6.6부터 inode 타임스탬프 필드(__i_atime, __i_mtime, __i_ctime)는 직접 접근이 금지되고 접근자(Accessor) 함수를 통해서만 조작합니다. multi-grain 타임스탬프(Multi-grain Timestamp) 지원을 위한 리팩토링의 결과입니다.

함수정의 위치역할
inode_get_atime(inode)include/linux/fs.h__i_atime 반환 (struct timespec64)
inode_get_mtime(inode)include/linux/fs.h__i_mtime 반환
inode_get_ctime(inode)include/linux/fs.h__i_ctime 반환
inode_set_atime_to_ts(inode, ts)include/linux/fs.h__i_atime = ts 설정 후 ts 반환
inode_set_mtime_to_ts(inode, ts)include/linux/fs.h__i_mtime = ts 설정 후 ts 반환
inode_set_ctime_to_ts(inode, ts)include/linux/fs.h__i_ctime = ts 설정 후 ts 반환
inode_set_ctime_current(inode)fs/inode.c현재 시간을 ctime에 설정 + coarse-grain 최적화
simple_inode_init_ts(inode)fs/libfs.catime/mtime/ctime 모두 현재 시간으로 초기화
inode_set_atime(inode, sec, nsec)include/linux/fs.h초/나노초 직접 지정
inode_set_mtime(inode, sec, nsec)include/linux/fs.h초/나노초 직접 지정
/* include/linux/fs.h — 타임스탬프 접근자 구현 */
static inline struct timespec64 inode_get_atime(const struct inode *inode)
{
    return inode->__i_atime;
}

static inline struct timespec64
inode_set_atime_to_ts(struct inode *inode, struct timespec64 ts)
{
    inode->__i_atime = ts;
    return ts;
}

static inline struct timespec64
inode_set_atime(struct inode *inode, time64_t sec, long nsec)
{
    struct timespec64 ts = { .tv_sec = sec, .tv_nsec = nsec };
    return inode_set_atime_to_ts(inode, ts);
}

/* fs/inode.c — 현재 시간으로 ctime 설정 */
struct timespec64 inode_set_ctime_current(struct inode *inode)
{
    struct timespec64 now = current_time(inode);
    /* multi-grain: coarse 시간과 비교하여 fine-grain 필요 여부 판단 */
    inode_set_ctime_to_ts(inode, now);
    return now;
}

/* fs/libfs.c — 새 inode 타임스탬프 일괄 초기화 */
struct timespec64 simple_inode_init_ts(struct inode *inode)
{
    struct timespec64 ts = inode_set_ctime_current(inode);
    inode_set_atime_to_ts(inode, ts);
    inode_set_mtime_to_ts(inode, ts);
    return ts;
}

/* 파일시스템에서의 전형적인 사용 */
static int myfs_create(struct mnt_idmap *idmap,
                       struct inode *dir,
                       struct dentry *dentry, umode_t mode, bool excl)
{
    struct inode *inode = new_inode(dir->i_sb);
    simple_inode_init_ts(inode);           /* atime=mtime=ctime=now */
    inode_set_mtime_to_ts(dir,
        inode_set_ctime_current(dir));       /* 부모 dir 시간 갱신 */
    /* ... */
}
ℹ️

Multi-grain 타임스탬프: 커널 6.6+에서 inode_set_ctime_current()는 coarse-grained 시간(jiffies 기반, ~1ms 정밀도)으로 충분한 경우 fine-grained 시간(ktime_get_real_ts64(), 나노초)을 피하여 성능을 향상합니다. stat() 호출이 이전 ctime과 coarse 시간을 비교하여 fine-grain이 필요한지 결정합니다.

하드 링크 수(i_nlink)를 직접 수정하면 dirty 마킹이 누락되어 디스크 불일치가 발생합니다. 반드시 아래 API를 사용해야 합니다.

/* fs/inode.c — nlink 조작 함수 */

/* 절대값 설정 (디스크에서 읽을 때) */
void set_nlink(struct inode *inode, unsigned int nlink)
{
    if (!nlink) {
        clear_nlink(inode);   /* 0이면 별도 처리 */
    } else {
        /* 먼저 I_LINKABLE 비트를 지움 (nlink>0이면 불필요) */
        if (inode->i_nlink == 0)
            inode->i_state &= ~I_LINKABLE;
        inode->i_nlink = nlink;
    }
}

/* nlink 감소 (unlink, rmdir 시) */
void drop_nlink(struct inode *inode)
{
    WARN_ON(inode->i_nlink == 0);  /* 이미 0이면 BUG */
    inode->i_nlink--;
}

/* nlink 0으로 설정 (orphan 처리) */
void clear_nlink(struct inode *inode)
{
    if (inode->i_nlink) {
        inode->i_nlink = 0;
        atomic_long_inc(&inode->i_sb->s_remove_count);
    }
}

/* nlink 증가 (link 시) */
void inc_nlink(struct inode *inode)
{
    if (unlikely(inode->i_nlink == 0))
        WARN_ON(!(inode->i_state & I_LINKABLE));
    inode->i_nlink++;
}

/* 고수준 래퍼: nlink 변경 + dirty 마킹 */
static inline void inode_inc_link_count(struct inode *inode)
{
    inc_nlink(inode);
    mark_inode_dirty(inode);  /* nlink 변경을 디스크에 반영 */
}

static inline void inode_dec_link_count(struct inode *inode)
{
    drop_nlink(inode);
    mark_inode_dirty(inode);
}
코드 설명
  • set_nlink디스크에서 inode를 읽을 때(myfs_iget) 호출합니다. 이 시점에서는 inode가 아직 캐시에 삽입되기 전이므로 mark_inode_dirty()가 불필요합니다. nlink=0이면 clear_nlink()을 통해 s_remove_count를 증가시킵니다.
  • drop_nlink — WARN_ON이미 0인 nlink을 감소시키면 underflow(언더플로)가 발생합니다. WARN_ON은 이를 감지하여 커널 로그에 스택 트레이스(Stack Trace)를 출력합니다. 정상적인 unlink 경로에서는 절대 발생하지 않아야 합니다.
  • clear_nlink — s_remove_counts_remove_count는 이 superblock에서 삭제 예정인 inode 수를 추적합니다. NFS 서버가 "파일시스템에 삭제 대기 중인 파일이 있는지" 판단하는 데 사용합니다.
  • inc_nlink — I_LINKABLEi_nlink=0인 inode의 nlink을 다시 증가시키는 것은 일반적으로 허용되지 않습니다. 예외적으로 O_TMPFILE 파일을 linkat()으로 영구화할 때 I_LINKABLE 상태를 설정하여 이를 허용합니다.
  • inode_inc_link_count / inode_dec_link_count이 고수준 래퍼는 nlink 변경과 dirty 마킹을 원자적으로 수행합니다. 대부분의 파일시스템은 이를 사용하지만, ext4처럼 저널 트랜잭션 내에서 별도 dirty 마킹이 필요한 경우 저수준 inc_nlink()/drop_nlink()을 직접 사용합니다.

inode 상태/플래그 헬퍼 매크로

inode->i_flagsS_* 상수를 검사하는 IS_*() 매크로입니다. 마운트 옵션, 파일시스템 기능, 파일별 속성을 반영합니다.

/* include/linux/fs.h — inode S_* 플래그와 IS_*() 매크로 */

#define S_SYNC        (1 << 0)   /* 동기 쓰기 (mount -o sync) */
#define S_NOATIME     (1 << 1)   /* atime 갱신 안 함 */
#define S_APPEND      (1 << 2)   /* 추가 전용 (chattr +a) */
#define S_IMMUTABLE   (1 << 3)   /* 변경 불가 (chattr +i) */
#define S_DEAD        (1 << 4)   /* 삭제된 디렉터리 */
#define S_NOQUOTA     (1 << 5)   /* 쿼타 면제 */
#define S_DIRSYNC     (1 << 6)   /* 디렉터리 동기 쓰기 */
#define S_NOCMTIME    (1 << 7)   /* VFS가 ctime/mtime 자동 갱신 안 함 */
#define S_SWAPFILE    (1 << 8)   /* 스왑 파일 (이름 변경/삭제 불가) */
#define S_PRIVATE     (1 << 9)   /* LSM 검사 제외 (내부 inode) */
#define S_IMA         (1 << 10)  /* IMA 무결성 검증 대상 */
#define S_AUTOMOUNT   (1 << 11)  /* 자동 마운트 트리거 */
#define S_NOSEC       (1 << 12)  /* 보안 xattr 없음 (캐시 최적화) */
#define S_DAX         (1 << 13)  /* DAX 모드 (페이지 캐시 우회) */
#define S_ENCRYPTED   (1 << 14)  /* fscrypt 암호화 */
#define S_CASEFOLD    (1 << 15)  /* 대소문자 무시 디렉터리 */
#define S_VERITY      (1 << 16)  /* fs-verity 활성화 */

/* 검사 매크로 */
#define IS_IMMUTABLE(inode)   ((inode)->i_flags & S_IMMUTABLE)
#define IS_APPEND(inode)      ((inode)->i_flags & S_APPEND)
#define IS_SYNC(inode)        ((inode)->i_flags & S_SYNC)
#define IS_DIRSYNC(inode)     ((inode)->i_flags & S_DIRSYNC)
#define IS_DEADDIR(inode)     ((inode)->i_flags & S_DEAD)
#define IS_POSIXACL(inode)    ((inode)->i_sb->s_flags & SB_POSIXACL)
#define IS_PRIVATE(inode)     ((inode)->i_flags & S_PRIVATE)
#define IS_AUTOMOUNT(inode)   ((inode)->i_flags & S_AUTOMOUNT)
#define IS_NOSEC(inode)       ((inode)->i_flags & S_NOSEC)
#define IS_SWAPFILE(inode)    ((inode)->i_flags & S_SWAPFILE)
#define IS_DAX(inode)         ((inode)->i_flags & S_DAX)
#define IS_ENCRYPTED(inode)   ((inode)->i_flags & S_ENCRYPTED)
#define IS_VERITY(inode)      ((inode)->i_flags & S_VERITY)
매크로설정 방법VFS 동작 영향
IS_IMMUTABLEchattr +iwrite, truncate, unlink, chmod, chown 모두 -EPERM
IS_APPENDchattr +a추가(append) 쓰기만 허용, 삭제/truncate 금지
IS_SYNCmount -o sync 또는 chattr +S모든 쓰기가 동기적으로 디스크에 기록
IS_DEADDIR커널 내부 (rmdir 후)삭제된 디렉터리에 새 파일 생성 방지
IS_PRIVATE파일시스템 내부LSM 보안 검사 우회 (저널, 쿼타 파일 등)
IS_NOSECVFS 자동 설정보안 xattr 미존재 시 file_remove_privs() 스킵(Skip)
IS_DAXmount -o dax 또는 per-file페이지 캐시 우회, 직접 메모리 매핑
IS_ENCRYPTEDfscrypt 정책 설정 시읽기/쓰기 시 자동 암호화/복호화

make_bad_inode() / is_bad_inode()

I/O 에러나 파일시스템 손상(Corruption)으로 inode를 정상적으로 읽을 수 없을 때, "bad inode"로 표시하여 모든 연산이 -EIO를 반환하도록 합니다.

/* fs/bad_inode.c — bad inode 구현 */

void make_bad_inode(struct inode *inode)
{
    remove_inode_hash(inode);          /* 해시 테이블에서 제거 */
    inode->i_mode = S_IFREG;           /* 유형을 일반 파일로 변경 */
    inode_set_atime(inode, 0, 0);
    inode_set_mtime(inode, 0, 0);
    inode_set_ctime_to_ts(inode, (struct timespec64){0});
    inode->i_op  = &bad_inode_ops;     /* 모든 inode op → -EIO */
    inode->i_fop = &bad_file_ops;      /* 모든 file op → -EIO */
}

bool is_bad_inode(struct inode *inode)
{
    return (inode->i_op == &bad_inode_ops);
}

/* 파일시스템에서의 사용 예 */
struct inode *myfs_iget(struct super_block *sb, unsigned long ino)
{
    struct inode *inode = iget_locked(sb, ino);
    if (!(inode->i_state & I_NEW))
        return inode;

    if (myfs_read_disk_inode(sb, ino, inode)) {
        make_bad_inode(inode);      /* 읽기 실패 → bad 표시 */
        unlock_new_inode(inode);
        iput(inode);
        return ERR_PTR(-EIO);
    }
    unlock_new_inode(inode);
    return inode;
}

file_inode() 매크로

/* include/linux/fs.h */
static inline struct inode *file_inode(const struct file *f)
{
    return f->f_inode;
}

/* f_inode은 file 구조체에 직접 캐싱된 inode 포인터.
 * f->f_path.dentry->d_inode 경로보다 역참조(Dereference)가 1단계 적어
 * 성능상 유리하고, RCU 워크에서도 안전합니다.
 * VFS가 open() 시 f_inode = d_inode(dentry)로 설정합니다. */

/* 사용 예: file_operations 콜백 내부 */
static ssize_t myfs_read(struct file *file, ...)
{
    struct inode *inode = file_inode(file);
    struct myfs_inode_info *mi = MYFS_I(inode);
    /* ... */
}

inode_owner_or_capable() / inode_newsize_ok()

/* fs/inode.c — 소유자 또는 capability 검사 */
bool inode_owner_or_capable(struct mnt_idmap *idmap,
                            const struct inode *inode)
{
    struct user_namespace *ns;
    vfsuid_t vfsuid = i_uid_into_vfsuid(idmap, inode);

    /* 1. 현재 프로세스가 소유자인가? */
    if (vfsuid_eq_kuid(vfsuid, current_fsuid()))
        return true;

    /* 2. CAP_FOWNER capability가 있는가? */
    ns = current_user_ns();
    if (ns_capable(ns, CAP_FOWNER) &&
        kuid_has_mapping(ns, vfsuid_into_kuid(vfsuid)))
        return true;

    return false;
}

/* fs/attr.c — 새 파일 크기 유효성 검사 */
int inode_newsize_ok(const struct inode *inode, loff_t offset)
{
    /* 1. 파일 크기 제한 (rlimit) */
    if (offset > rlimit(RLIMIT_FSIZE))
        return -EFBIG;

    /* 2. 파일시스템 최대 파일 크기 */
    if (offset > inode->i_sb->s_maxbytes)
        return -EFBIG;

    return 0;
}
코드 설명
  • inode_owner_or_capable — vfsuididmapped mount 환경에서 inode의 uid를 마운트 매핑에 따라 변환한 뒤 현재 프로세스의 fsuid와 비교합니다. chmod, utimes 등 소유자만 허용하는 연산에서 사용됩니다.
  • inode_owner_or_capable — CAP_FOWNERCAP_FOWNER는 파일 소유자와 무관하게 소유자 권한이 필요한 연산을 수행할 수 있는 capability입니다. 단, 대상 uid가 현재 사용자 네임스페이스에 매핑되어 있어야 합니다.
  • inode_newsize_ok — RLIMIT_FSIZE프로세스의 파일 크기 제한(ulimit -f)을 초과하면 -EFBIG를 반환하고 SIGXFSZ 시그널(Signal)을 전송합니다. s_maxbytes는 파일시스템의 하드웨어적 최대 크기입니다 (ext4: 16TB, XFS: 8EB).

DIO (Direct I/O) 동기화 헬퍼

Direct I/O와 버퍼드 I/O 간의 경합을 방지하기 위한 카운터 기반 동기화 API입니다.

/* include/linux/fs.h — DIO 동기화 */

static inline void inode_dio_begin(struct inode *inode)
{
    atomic_inc(&inode->i_dio_count);
}

static inline void inode_dio_end(struct inode *inode)
{
    if (atomic_dec_and_test(&inode->i_dio_count))
        wake_up_bit(&inode->i_state, __I_DIO_WAKEUP);
}

/* fs/inode.c — 모든 진행 중 DIO 완료 대기 */
void inode_dio_wait(struct inode *inode)
{
    if (atomic_read(&inode->i_dio_count))
        __inode_dio_wait(inode);
}

/* 전형적 사용: truncate 전에 진행 중 DIO 완료 대기 */
static int myfs_setattr(struct mnt_idmap *idmap,
                        struct dentry *dentry, struct iattr *attr)
{
    struct inode *inode = d_inode(dentry);
    if (attr->ia_valid & ATTR_SIZE) {
        inode_dio_wait(inode);      /* DIO 완료까지 대기 */
        truncate_setsize(inode, attr->ia_size);
    }
    /* ... */
}

VFS 잠금 래퍼

inode->i_rwsem을 직접 조작하는 대신 VFS 래퍼를 사용하면 lockdep 클래스가 올바르게 적용됩니다.

래퍼 함수내부 동작사용 시점
inode_lock(inode)down_write(&inode->i_rwsem)write, truncate, create, mkdir 등
inode_unlock(inode)up_write(&inode->i_rwsem)배타적 잠금 해제
inode_lock_shared(inode)down_read(&inode->i_rwsem)read, readdir, getattr 등
inode_unlock_shared(inode)up_read(&inode->i_rwsem)공유 잠금 해제
inode_trylock(inode)down_write_trylock(&inode->i_rwsem)논블로킹(Non-blocking) 배타적 잠금 시도
inode_trylock_shared(inode)down_read_trylock(&inode->i_rwsem)논블로킹 공유 잠금 시도
inode_is_locked(inode)rwsem_is_locked(&inode->i_rwsem)디버그 assertion
inode_lock_nested(inode, cls)down_write_nested(&inode->i_rwsem, cls)다중 inode 잠금 시 lockdep 클래스 지정
/* include/linux/fs.h — 잠금 래퍼 */
static inline void inode_lock(struct inode *inode)
{
    down_write(&inode->i_rwsem);
}

static inline void inode_unlock(struct inode *inode)
{
    up_write(&inode->i_rwsem);
}

static inline void inode_lock_shared(struct inode *inode)
{
    down_read(&inode->i_rwsem);
}

static inline int inode_trylock(struct inode *inode)
{
    return down_write_trylock(&inode->i_rwsem);
}

static inline void inode_lock_nested(struct inode *inode,
                                     unsigned subclass)
{
    down_write_nested(&inode->i_rwsem, subclass);
}

/* Lockdep 잠금 클래스 상수 */
enum inode_i_mutex_lock_class {
    I_MUTEX_NORMAL,       /* 단일 inode 잠금 */
    I_MUTEX_PARENT,       /* 부모 디렉터리 (create, unlink) */
    I_MUTEX_PARENT2,      /* rename 시 두 번째 부모 */
    I_MUTEX_CHILD,        /* 자식 inode */
    I_MUTEX_XATTR,        /* xattr 연산 중 */
    I_MUTEX_NONDIR2,      /* rename 시 비디렉터리 타겟 */
};

기타 유틸리티

함수/매크로정의 위치역할
inode_needs_sync(inode)include/linux/fs.hIS_SYNC(inode) 또는 IS_DIRSYNC(inode) 검사 — 동기 쓰기 필요 여부
mapping_set_error(mapping, err)include/linux/pagemap.haddress_space에 I/O 에러 기록 — filemap_check_errors()에서 검출
ihold(inode)include/linux/fs.hatomic_inc(&inode->i_count) — 참조 카운트 증가 (caller가 이미 참조를 보유한 상태에서)
inode_sb_list_add(inode)fs/inode.cinode를 superblock의 s_inodes 리스트에 추가
inode_init_once(inode)fs/inode.cslab constructor — rwsem, i_data, 대기 큐 등 1회성 초기화
d_inode(dentry)include/linux/dcache.hdentry->d_inode 반환 — negative dentry 시 NULL
d_inode_rcu(dentry)include/linux/dcache.hRCU 보호 하 dentry->d_inode 읽기
d_is_positive(dentry)include/linux/dcache.hdentry에 inode가 연결되어 있는지 검사
d_is_negative(dentry)include/linux/dcache.hnegative dentry (파일 없음 캐시) 여부 검사

inode 번호 할당 전략

파일시스템마다 inode 번호를 할당하는 전략이 다릅니다. 이 전략은 파일 생성 성능, inode 고갈 가능성, 32비트/64비트 호환성에 직접적인 영향을 미칩니다.

파일시스템할당 방식범위고갈 가능성특이사항
ext4비트맵 (고정 테이블)mkfs 시 결정있음Orlov 할당기로 디렉터리 분산
XFSAG 내 동적 할당64비트극히 낮음AG별 free inode B+tree
Btrfsobjectid (단조 증가)64비트없음서브볼륨별 독립 번호 공간
tmpfsget_next_ino()32비트 (percpu)번호 재활용(Recycling)디스크 없음
NFS서버 전달서버 의존서버 의존filehandle이 실질적 식별자
/* ext4 inode 할당: Orlov 알고리즘 */
/* 디렉터리를 여러 블록 그룹에 분산하여 부모-자식 근접성 유지 */

static int find_group_orlov(struct super_block *sb,
                           struct inode *parent, ...)
{
    /* 최상위 디렉터리: 가장 여유 있는 그룹 선택 */
    if (parent == d_inode(sb->s_root)) {
        /* free inode 수, free block 수, 디렉터리 수 기준 */
        best_group = find_best_group(sb, ...);
    }
    /* 하위 디렉터리: 부모와 같은 그룹 선호 */
    else {
        group = ext4_inode_to_goal_block(parent);
        /* 부모 근처에서 시작하여 빈 슬롯 탐색 */
    }
}

/* XFS inode 할당: finobt (free inode B+tree) */
/* 각 AG에 free inode를 추적하는 B+tree 유지 */
/* → O(log n)에 빈 inode 슬롯 탐색 가능 */

/* tmpfs / procfs 등 pseudo-FS */
unsigned int get_next_ino(void)
{
    unsigned int *p = &get_cpu_var(last_ino);
    unsigned int res = *p;
    /* percpu 카운터로 락-프리 할당 */
    *p = ++res;
    put_cpu_var(last_ino);
    return res;
}
💡

32비트 inode 번호 주의: stat(2)st_ino가 32비트인 시스템에서 XFS/Btrfs의 64비트 inode 번호가 잘릴 수 있습니다. 이 경우 -EOVERFLOW가 발생합니다. XFS에서는 inode32 마운트 옵션으로 inode를 32비트 범위 내에 할당하도록 제한할 수 있지만, 대규모 파일시스템에서는 inode 배치 효율이 떨어집니다. 최선의 해결책은 64비트 시스템과 statx(2)를 사용하는 것입니다.

inode Generation 번호 (i_generation)

i_generation 필드는 inode 번호가 재사용될 때 같은 번호의 서로 다른 파일을 구별하기 위한 세대 번호입니다. NFS 파일 핸들의 핵심 구성 요소이며, inode가 삭제되고 같은 번호로 새 파일이 생성되었을 때 오래된 핸들로 접근하는 것을 방지합니다.

/* include/linux/fs.h */
struct inode {
    /* ... */
    __u32  i_generation;  /* 세대 번호 — inode 재사용 시 증가 */
    /* ... */
};

/* NFS 파일 핸들 구조 (개념적) */
struct nfs_fh {
    __u64  ino;         /* inode 번호 */
    __u32  generation;  /* i_generation — "stale" 검출 핵심 */
    __u32  fsid;        /* 파일시스템 ID */
};

/* ext4 — inode 할당 시 generation 설정 */
/* fs/ext4/ialloc.c — __ext4_new_inode() */
inode->i_generation = get_random_u32();
/* 또는 이전 값 + 1 (커널 버전에 따라 다름) */

/* NFS 서버 — export 시 파일 핸들 구성 */
/* fs/exportfs/expfs.c */
static int export_encode_fh(struct inode *inode, ...)
{
    fh[0] = inode->i_ino;
    fh[1] = inode->i_generation;  /* 핵심: 세대 번호 포함 */
    /* ... */
}

/* NFS 클라이언트가 오래된 핸들로 접근 시 */
/* → 서버가 ino로 inode 검색 후 generation 비교 */
/* → 불일치 → -ESTALE 반환 */
파일시스템i_generation 관리특성
ext4할당 시 랜덤 또는 이전+1디스크 inode에 저장, NFS export 안전
XFSAG별 순차 증가di_gen 필드, 항상 NFS-safe
Btrfs트랜잭션 ID 기반subvolume + objectid + generation 조합
tmpfs항상 0NFS export 미지원 (5.x에서 제한적 지원)
NFS 영향클라이언트가 캐시한 파일 핸들의 generation이 서버와 불일치하면 ESTALE 오류
ℹ️

ESTALE 오류와 대응: NFS에서 파일이 서버에서 삭제된 후 같은 inode 번호로 새 파일이 생성되면 클라이언트의 기존 핸들은 ESTALE을 반환합니다. 이것이 i_generation의 존재 이유입니다. generation 없이는 클라이언트가 완전히 다른 파일의 데이터를 읽을 수 있습니다. statx()STATX_CHANGE_COOKIE 필드도 유사한 목적으로 사용됩니다.

# generation 번호 확인 (ext4)
debugfs -R "stat <inode_number>" /dev/sda1
# ... Generation: 1234567890 ...

# NFS 파일 핸들 정보 확인
nfs4_getfacl /mnt/nfs/file
# filehandle에 generation이 포함됨

# ESTALE 오류 재현
# 서버: rm /export/file && touch /export/file
# 클라이언트: cat /mnt/nfs/file → Stale file handle

inode 보안: LSM 연동

Linux Security Module(LSM) 프레임워크는 inode 수준에서 세밀한 보안 정책을 적용합니다. inode에 보안 레이블(security label)을 부착하고, 모든 접근 시 MAC(Mandatory Access Control) 검사를 수행합니다.

/* inode 내 보안 관련 필드 */
struct inode {
    /* ... */
    void  *i_security;  /* LSM별 보안 데이터 (SELinux: inode_security_struct) */
    /* ... */
};

/* LSM 훅 호출 순서 (inode 생성 시) */
/* 1. security_inode_alloc()    — 보안 구조체 할당 */
/* 2. security_inode_init_security() — 보안 레이블 초기화 */
/* 3. security_inode_post_create()  — 생성 후 처리 */

/* LSM 훅 호출 순서 (inode 접근 시) */
/* inode_permission()에서: */
/* 1. DAC 검사 (전통적 권한 + ACL) */
/* 2. security_inode_permission() ← LSM 훅 */
/*    → SELinux: selinux_inode_permission() */
/*    → AppArmor: apparmor_inode_permission() */

/* SELinux 보안 컨텍스트 확인 */
/* ls -Z /home/user/test.txt */
/* -rw-r--r--. user group unconfined_u:object_r:user_home_t:s0 test.txt */
LSM보안 모델inode 레이블 저장주요 배포판
SELinuxType Enforcement (TE)security.selinux xattrRHEL, Fedora, CentOS
AppArmor경로 기반 (프로파일)xattr 미사용 (경로 기반)Ubuntu, SUSE
SmackSimplified MACsecurity.SMACK64 xattrTizen, 임베디드
IMA/EVM무결성(Integrity) 검증security.ima, security.evm다양
LSM inode 보안 훅 체인 (파일 접근 시) open("/etc/shadow") inode_permission() fs/namei.c 1. DAC 검사 (rwx + ACL) 2. security_inode_permission() SELinux: TE 정책 검사 AppArmor: 프로파일 검사 IMA: 무결성 검증 결과 모든 검사 통과 → 접근 허용 하나라도 거부 → -EACCES LSM 스택: 커널 6.x부터 여러 LSM을 동시에 활성화 가능 (lsm= 부트 파라미터)
💡

LSM 스택킹 (커널 6.x): 이전에는 하나의 major LSM만 활성화할 수 있었지만, 6.x부터 SELinux + AppArmor + Landlock을 동시에 사용할 수 있습니다. inode의 i_security 포인터는 LSM blob으로, 각 LSM의 보안 데이터를 하나의 할당에 연속 저장합니다 (lsm_inode_alloc()). /sys/kernel/security/lsm에서 활성 LSM 목록을 확인할 수 있습니다.

Idmapped Mounts와 inode

커널 5.12에서 도입된 idmapped mounts는 마운트 지점마다 UID/GID 매핑을 다르게 적용합니다. inode의 i_uid/i_gid를 변경하지 않고, VFS 계층에서 매핑을 적용하여 컨테이너 환경에서의 파일 소유권 문제를 해결합니다.

Idmapped Mounts: inode UID 매핑 흐름 디스크 inode i_uid = 1000 i_gid = 1000 (변경 없음!) 마운트 A (호스트) idmap: identity (변환 없음) 보이는 uid: 1000 마운트 B (컨테이너) idmap: 1000 → 0 보이는 uid: 0 (root!) 컨테이너 내부에서 root로 보임 호스트 프로세스 stat() → uid=1000 컨테이너 프로세스 stat() → uid=0 핵심: inode의 실제 uid/gid는 변경되지 않음 VFS 계층(mnt_idmap)에서 매핑을 적용하여 프로세스마다 다른 uid/gid를 보여줌
/* inode_operations 콜백의 mnt_idmap 파라미터 */
int (*create)(struct mnt_idmap *idmap,
             struct inode *dir, struct dentry *dentry,
             umode_t mode, bool excl);

/* UID 매핑 적용 예시 (VFS 내부) */
static inline vfsuid_t i_uid_into_vfsuid(
    struct mnt_idmap *idmap,
    const struct inode *inode)
{
    /* inode의 실제 uid를 mnt_idmap으로 매핑 */
    return make_vfsuid(idmap, i_user_ns(inode), inode->i_uid);
}

/* 파일 생성 시: vfsuid를 inode uid로 역매핑 */
void inode_init_owner(struct mnt_idmap *idmap,
                      struct inode *inode,
                      const struct inode *dir,
                      umode_t mode)
{
    /* 프로세스의 fsuid를 idmap 역변환하여 inode에 저장 */
    vfsuid_t vfsuid = mapped_fsuid(idmap, i_user_ns(dir));
    inode->i_uid = vfsuid_into_kuid(vfsuid);
}
# idmapped mount 설정 (mount_setattr)
# util-linux 2.39+ 또는 mount-idmapped 도구 사용

# 예: 호스트 uid 1000을 컨테이너 uid 0으로 매핑
mount-idmapped --map-mount b:0:1000:1 \
    /host/share /container/rootfs/share

# systemd-nspawn에서 자동 idmapped mount
systemd-nspawn --bind=/host/share:/share \
    --private-users=pick \
    --private-users-ownership=map

# 커널 지원 확인
# CONFIG_IDMAP_MOUNTS=y (5.12+)
비교chownuser namespaceidmapped mount
inode 변경i_uid/i_gid 직접 변경변경 없음변경 없음
공유 가능한 소유자만네임스페이스별마운트별 독립 매핑
성능I/O 발생오버헤드(Overhead) 없음VFS 계층 매핑만 (무시할 수준)
용도전통적 소유권프로세스 격리(Isolation)컨테이너 파일 공유
💡

컨테이너 실전: Docker/Podman에서 호스트 볼륨을 바인드 마운트(Bind Mount)할 때 UID 불일치 문제가 자주 발생합니다. idmapped mount는 이 문제의 근본적 해결책입니다. Podman 4.0+--userns=keep-id 옵션으로 idmapped mount를 자동 활용합니다. 기존의 chown -R이나 uid/gid 동기화 같은 임시 방편이 불필요해집니다.

특수 inode와 의사 파일시스템

리눅스 커널에는 디스크에 저장되지 않는 특수 inode들이 존재합니다. 파이프, 소켓, 익명 inode, procfs/sysfs의 가상 파일들은 모두 메모리 전용 inode로 관리됩니다.

의사 파일시스템의 inode

파일시스템inode 특성할당 함수i_ino 부여해시 삽입
pipefs파이프 양 끝점new_inode_pseudo()get_next_ino()아니오
sockfs소켓 파일new_inode_pseudo()get_next_ino()아니오
anon_inodefsepoll, eventfd, timerfdanon_inode_getfd()고정(단일 inode)아니오
procfs프로세스 정보proc_alloc_inode()proc_inum 해시
sysfs커널 객체 속성sysfs_get_inode()kernfs_node 기반
tmpfs메모리 임시 파일shmem_get_inode()get_next_ino()
devtmpfs디바이스 노드tmpfs 기반동적
cgroup제어 그룹 파일kernfs 기반kernfs_node 기반
/* new_inode_pseudo() — 의사 FS용 inode 할당 */
/* 일반 new_inode()와의 차이: sb의 inode 리스트에 추가하지 않음 */
struct inode *new_inode_pseudo(struct super_block *sb)
{
    struct inode *inode = alloc_inode(sb);
    if (inode) {
        spin_lock(&inode->i_lock);
        inode->i_state = 0;
        spin_unlock(&inode->i_lock);
        /* sb->s_inodes 리스트에 추가하지 않음! */
        /* → umount 시 s_inodes 순회에서 제외 */
    }
    return inode;
}

/* 익명 inode — 단일 공유 inode로 여러 fd 생성 */
/* epoll_create → anon_inode_getfd("[eventpoll]") */
/* timerfd_create → anon_inode_getfd("[timerfd]") */
/* eventfd → anon_inode_getfd("[eventfd]") */
/* userfaultfd → anon_inode_getfd("[userfaultfd]") */

int anon_inode_getfd(const char *name,
                    const struct file_operations *fops,
                    void *priv, int flags)
{
    /* 전역 anon_inode_inode를 공유 사용 */
    /* 새 struct file만 생성하여 fd에 할당 */
    struct file *file;
    int fd;

    fd = get_unused_fd_flags(flags);
    file = anon_inode_getfile(name, fops, priv, flags);
    fd_install(fd, file);
    return fd;
}

/* /proc/PID/fd에서 확인 */
/* $ ls -la /proc/self/fd/3 */
/* lrwx------ 1 user user 64 ... 3 -> anon_inode:[eventpoll] */

파이프와 소켓의 inode

/* 파이프 inode — fs/pipe.c */
struct inode {
    /* ... */
    union {
        struct pipe_inode_info *i_pipe;  /* 파이프일 때 */
        struct cdev  *i_cdev;            /* 캐릭터 디바이스일 때 */
        char         *i_link;             /* 심볼릭 링크 타겟 */
        unsigned      i_dir_seq;          /* 디렉터리일 때 */
    };
};

/* pipe(2) → create_pipe_files() */
/* 1. pipefs에서 new_inode_pseudo() */
/* 2. i_pipe = alloc_pipe_info() */
/* 3. inode->i_fop = &pipefifo_fops */
/* 4. 두 개의 struct file 생성 (읽기/쓰기) */

/* 소켓 inode — net/socket.c */
struct socket_alloc {
    struct socket socket;     /* 소켓 구조체 */
    struct inode vfs_inode;   /* VFS inode (임베드) */
};

/* socket(2) → sock_alloc() */
/* sockfs의 new_inode_pseudo()로 할당 */
/* container_of로 inode ↔ socket 상호 변환 */
static inline struct socket *SOCKET_I(struct inode *inode)
{
    return &container_of(inode, struct socket_alloc,
                         vfs_inode)->socket;
}
inode 유형별 분류 struct inode 디스크 기반 inode ext4 XFS Btrfs 메모리 전용 inode tmpfs procfs sysfs 의사/익명 inode pipefs sockfs anon 핵심 차이점 디스크 기반: iget_locked()으로 할당, 해시 테이블에 삽입, writeback 대상 메모리 전용: new_inode()으로 할당, 해시 테이블에 삽입, evict 시 데이터 소멸 의사/익명: new_inode_pseudo()로 할당, 해시 테이블 미삽입, sb inode 리스트에도 미등록
ℹ️

anon_inode의 특이성: anon_inodefs는 시스템 전체에서 단 하나의 inode만 사용합니다. epoll, eventfd, timerfd 등은 모두 이 단일 inode를 공유하며, 개별 상태는 struct fileprivate_data에 저장됩니다. 따라서 stat()으로 조회하면 모든 익명 fd가 같은 inode 번호를 가집니다. 이 설계는 inode 객체를 최소화하면서도 VFS 인터페이스를 활용할 수 있게 합니다.

예약된 inode 번호

대부분의 파일시스템은 특별한 용도의 예약 inode 번호를 가지고 있습니다.

파일시스템inode 번호용도
ext40존재하지 않는 inode (NULL)
1불량 블록 목록 (bad blocks)
2루트 디렉터리 (/)
3ACL 인덱스 (구)
4ACL 데이터 (구)
ext4 (계속)5부트 로더(Loader)
6미삭제 디렉터리 (undelete)
ext47그룹 디스크립터 예약
ext48저널 (EXT4_JOURNAL_INO)
ext411첫 번째 비예약 inode (기본)
XFS동적루트 디렉터리는 AG 0의 첫 inode
Btrfs256첫 일반 파일 objectid
모든 FS0일반적으로 유효하지 않은 inode
# 루트 디렉터리의 inode 번호 확인
stat -c '%i' /
# ext4: 2 (항상)

# ext4 저널 inode 확인
debugfs -R "stat <8>" /dev/sda1
# Type: regular    Mode: 0600
# Size: 134217728   (128MB 저널)

# 예약된 inode 수 확인
tune2fs -l /dev/sda1 | grep "First inode"
# First inode:              11
# → inode 1~10은 예약됨

# lost+found 디렉터리 inode (ext4)
stat -c '%i' /lost+found
# 일반적으로 11 (첫 비예약 inode)

inode 디버깅과 추적

inode 관련 문제를 진단할 때 사용할 수 있는 도구와 기법을 정리합니다. 커널 트레이싱, proc/sysfs 인터페이스, 사용자 공간(User Space) 유틸리티를 활용하여 inode 동작을 실시간(Real-time)으로 관찰할 수 있습니다.

proc/sysfs를 통한 inode 모니터링

# ===== 시스템 전체 inode 상태 =====

# 할당된 inode 수 (nr_inodes, nr_free_inodes)
cat /proc/sys/fs/inode-nr
# 출력: 56789  1234
# 의미: 총 56789개 할당, 1234개 미사용(LRU 캐시)

# inode 상태 상세
cat /proc/sys/fs/inode-state
# nr_inodes  nr_free_inodes  preshrink  unused

# 파일시스템별 inode 사용량
df -i
# Filesystem      Inodes  IUsed   IFree IUse% Mounted on
# /dev/sda1     6553600 234567 6319033    4% /
# tmpfs          505872      5  505867    1% /dev/shm

# slab 캐시에서 inode 메모리 사용량 확인
slabtop -o -s c | head -20
# 또는 특정 캐시만
grep -E 'inode_cache|ext4_inode_cache|xfs_inode' /proc/slabinfo
# ext4_inode_cache  12345 12500 1096  29    8 : tunables ... 
# 의미: 12345개 활성 객체, 각 1096바이트

# 특정 파일의 inode 상세 정보
stat /home/user/test.txt
# File: test.txt
# Size: 4096       Blocks: 8          IO Block: 4096   regular file
# Device: 801h/2049d     Inode: 1234567     Links: 1
# Access: (0644/-rw-r--r--)  Uid: (1000/user)   Gid: (1000/user)
# Access: 2024-01-15 10:30:00.000000000 +0900
# Modify: 2024-01-15 10:30:00.000000000 +0900
# Change: 2024-01-15 10:30:00.000000000 +0900
# Birth:  2024-01-15 10:30:00.000000000 +0900

# statx로 확장 정보 (크기, 블록, 마운트 ID, DAX 상태 등)
python3 -c "
import os
r = os.stat('/home/user/test.txt')
print(f'inode: {r.st_ino}')
print(f'nlink: {r.st_nlink}')
print(f'size:  {r.st_size}')
print(f'blocks: {r.st_blocks}')
print(f'uid: {r.st_uid}, gid: {r.st_gid}')
print(f'mode: {oct(r.st_mode)}')
"

ftrace/perf를 통한 inode 추적

# ===== ftrace로 inode 관련 함수 추적 =====

# inode 할당/해제 추적
echo 1 > /sys/kernel/debug/tracing/events/writeback/writeback_dirty_inode/enable
echo 1 > /sys/kernel/debug/tracing/events/writeback/writeback_written/enable
cat /sys/kernel/debug/tracing/trace_pipe

# 특정 시스템콜의 inode 경로 추적
perf trace -e 'open*,close,stat*,unlink' ls /tmp/
# 출력: 각 시스템콜의 인자와 반환값

# inode evict 추적 (메모리 압력 진단)
echo 1 > /sys/kernel/debug/tracing/events/writeback/writeback_lazytime_iput/enable

# ===== BPF/bpftrace로 세밀한 추적 =====

# inode 할당 빈도 측정
bpftrace -e 'kprobe:new_inode { @[comm] = count(); }'

# inode evict 추적
bpftrace -e '
kprobe:evict {
    $inode = (struct inode *)arg0;
    printf("evict: ino=%lu, nlink=%u, i_count=%d\n",
           $inode->i_ino, $inode->i_nlink,
           $inode->i_count.counter);
}'

# iput() 호출 스택 추적 (누가 참조를 해제하는지)
bpftrace -e '
kprobe:iput {
    $inode = (struct inode *)arg0;
    if ($inode->i_count.counter == 1) {
        printf("last iput: ino=%lu\n", $inode->i_ino);
        print(kstack);
    }
}'

# writeback dirty inode 추적
bpftrace -e '
kprobe:__mark_inode_dirty {
    $inode = (struct inode *)arg0;
    $flags = arg1;
    printf("dirty: ino=%lu flags=0x%x comm=%s\n",
           $inode->i_ino, $flags, comm);
}'

파일시스템별 디버깅 도구

도구파일시스템주요 기능예시 명령
debugfsext2/3/4inode 직접 조회, 삭제 파일 복구debugfs -R "stat <42>" /dev/sda1
xfs_dbXFSAG, inode, extent 정보 조회xfs_db -c "inode 42" /dev/sdb1
btrfs inspectBtrfsinode에서 경로 역추적btrfs inspect inode-resolve 42 /mnt
filefrag모든 FS파일의 extent 매핑, 단편화(Fragmentation) 확인filefrag -v /home/user/test.txt
xfs_io모든 FS파일 I/O 테스트, fiemap, fsyncxfs_io -c "fiemap" test.txt
lsof모든 FS열린 파일과 inode 확인lsof +D /tmp
fuser모든 FS파일/마운트를 사용하는 프로세스fuser -mv /mnt/data
# ===== ext4 디버깅 실전 예시 =====

# 1. 삭제된 파일이 디스크를 점유하는 문제 진단
# 삭제되었지만 열린 fd가 있어 디스크 해제가 안 됨
lsof +L1
# COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NLINK NODE NAME
# java    12345 app   23u   REG   8,1  2147483648  0  987654 /var/log/app.log (deleted)
# → PID 12345의 fd 23이 삭제된 2GB 파일을 잡고 있음

# 해결: 프로세스 재시작 또는 fd truncate
: > /proc/12345/fd/23  # 파일 크기를 0으로 (재시작 불가 시)

# 2. inode 고갈 진단
df -i /
# IUse% 가 100%이면 inode 고갈
# 소파일이 많은 디렉터리 찾기:
find / -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head -20

# 3. ext4 inode extent 상태 확인
debugfs -R "dump_extents <42>" /dev/sda1
# Level Entries  Logical      Physical Length Flags
#  0/ 0   1/  4    0 -  1023  1234 -  2257   1024

# 4. 삭제된 파일 복구 시도 (ext4)
debugfs /dev/sda1
# debugfs: lsdel
# Inode  Owner  Mode    Size      Blocks   Time deleted
# 987654  1000  100644  4096      8/8      Sat Jan 15 10:30:00 2024
# debugfs: dump <987654> /tmp/recovered_file
debugfs 사용 시 주의: debugfs는 마운트된 파일시스템에서 쓰기 모드(-w)로 사용하면 데이터 손상을 유발할 수 있습니다. 읽기 전용(Read-Only) 모드(기본)에서만 사용하고, 수정이 필요하면 반드시 umount 후 작업하세요. 프로덕션 환경에서는 스냅샷에서 작업하는 것이 안전합니다.

inode와 네트워크 파일시스템

NFS, CIFS/SMB 등 네트워크 파일시스템에서 inode는 로컬 파일시스템과 다른 특수한 도전 과제를 가집니다. 서버와 클라이언트 간의 일관성 유지가 핵심입니다.

NFS inode 특성

특성로컬 FS (ext4)NFS
inode 번호디스크 고정서버에서 전달 (filehandle 기반)
캐시 유효성항상 유효변경 속성(change attribute)으로 검증
타임스탬프 정밀도FS에 따라 나노초서버 의존 (NFSv4: 나노초 가능)
잠금커널 내부 잠금네트워크 잠금 (NLM/NFSv4 lock)
attribute 캐시없음 (항상 최신)ac{reg,dir}{min,max} 옵션으로 제어
inode 재검증불필요d_revalidate → GETATTR RPC
close-to-open해당 없음close 시 flush, open 시 revalidate
/* NFS inode 확장 구조체 */
struct nfs_inode {
    struct inode     vfs_inode;

    /* NFS 파일 핸들 — 서버에서 inode를 식별하는 불투명 토큰 */
    struct nfs_fh    fh;

    /* 변경 속성 — 서버 inode가 변경되면 증가 */
    u64              change_attr;

    /* 속성 캐시 타임아웃 */
    unsigned long    attrtimeo;       /* 현재 유효 기간 */
    unsigned long    attrtimeo_timestamp;
    unsigned long    attr_gencount;

    /* delegation — 서버가 클라이언트에 부여한 권한 */
    struct nfs_delegation *delegation;

    /* 캐시 유효성 카운터 */
    unsigned long    cache_validity;  /* NFS_INO_INVALID_* */
};

/* NFS inode 재검증 플래그 */
#define NFS_INO_INVALID_DATA    (1 << 1)  /* 데이터 캐시 무효 */
#define NFS_INO_INVALID_ATIME   (1 << 2)  /* atime 무효 */
#define NFS_INO_INVALID_ACCESS  (1 << 3)  /* 접근 권한 캐시 무효 */
#define NFS_INO_INVALID_ACL     (1 << 4)  /* ACL 캐시 무효 */
#define NFS_INO_INVALID_SIZE    (1 << 6)  /* 파일 크기 무효 */
#define NFS_INO_INVALID_CHANGE  (1 << 10) /* change attr 무효 */

/* 속성 캐시 마운트 옵션 */
/* mount -t nfs -o acregmin=3,acregmax=60,acdirmin=30,acdirmax=60 */
/* acregmin: 일반 파일 속성 캐시 최소 유효 기간 (초) */
/* acregmax: 일반 파일 속성 캐시 최대 유효 기간 (초) */
/* acdirmin/max: 디렉터리 속성 캐시 유효 기간 */
ℹ️

NFS close-to-open 일관성: NFS 클라이언트는 close() 시 dirty 데이터를 서버로 flush하고, open() 시 서버의 최신 속성을 가져와 캐시를 검증합니다. 이 close-to-open 보장은 단일 파일에 대해 순차적으로 접근하는 경우에만 일관성을 보장합니다. 동시 쓰기가 필요한 경우에는 NFS 잠금 또는 actimeo=0 (속성 캐시 비활성화) 옵션이 필요하지만, 성능이 크게 저하됩니다.

inode 관련 sysctl 튜닝 종합

inode 캐시와 관련된 sysctl 파라미터를 종합적으로 정리합니다. 워크로드에 따른 최적 설정값은 다를 수 있으므로, 벤치마크와 모니터링을 통해 결정해야 합니다.

sysctl기본값설명파일 서버 권장DB 서버 권장
vm.vfs_cache_pressure100dentry/inode 캐시 회수 적극성50 (캐시 유지)150 (메모리 확보)
vm.dirty_background_ratio10비동기 writeback 시작 임계값(%)5 (빈번한 소량)5
vm.dirty_ratio20동기 writeback 강제 임계값(%)40 (대용량 배치)10
vm.dirty_writeback_centisecs500writeback 점검 주기 (1/100초)500100
vm.dirty_expire_centisecs3000dirty 데이터 만료 시간 (1/100초)30001000
fs.inotify.max_user_watches8192프로세스당 최대 inotify 감시 수5242888192
fs.inotify.max_user_instances128사용자당 최대 inotify 인스턴스 수512128
fs.file-max시스템 의존시스템 전체 최대 열린 파일 수20971521048576
# ===== inode 관련 sysctl 설정 예시 =====

# 파일 서버 프로파일
# /etc/sysctl.d/99-fileserver.conf
vm.vfs_cache_pressure = 50
vm.dirty_background_ratio = 5
vm.dirty_ratio = 40
vm.dirty_expire_centisecs = 3000
fs.inotify.max_user_watches = 524288
fs.file-max = 2097152

# DB 서버 프로파일
# /etc/sysctl.d/99-database.conf
vm.vfs_cache_pressure = 150
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10
vm.dirty_writeback_centisecs = 100
vm.dirty_expire_centisecs = 1000

# 적용
sysctl -p /etc/sysctl.d/99-fileserver.conf

# 현재 dirty 상태 모니터링
watch -n 1 'grep -E "Dirty|Writeback|NFS" /proc/meminfo'
# Dirty:              12345 kB
# Writeback:            567 kB
# NFS_Unstable:           0 kB

# inode 캐시 수동 해제 (비상 시만)
echo 2 > /proc/sys/vm/drop_caches  # dentry + inode 캐시 해제
# 주의: 프로덕션에서는 성능 저하 유발
drop_caches 경고: echo 2 > /proc/sys/vm/drop_caches는 모든 dentry와 inode 캐시를 즉시 해제합니다. 이후 모든 파일 접근이 디스크 I/O를 유발하므로 심각한 성능 저하가 발생합니다. 이 명령은 벤치마크의 캐시 워밍 제거 목적으로만 사용하고, 프로덕션 환경에서는 vfs_cache_pressure 튜닝으로 점진적 회수를 유도하는 것이 바람직합니다.

statx 시스템콜과 확장 inode 정보

statx(2)는 기존 stat(2)의 한계를 극복하기 위해 커널 4.11에서 도입된 확장 stat 시스템콜입니다. 생성 시간(birth time), 마운트 ID, DAX 상태 등 기존 stat으로 얻을 수 없었던 inode 정보를 제공합니다.

/* include/uapi/linux/stat.h — statx 구조체 */
struct statx {
    __u32  stx_mask;        /* 유효한 필드 마스크 */
    __u32  stx_blksize;     /* I/O 블록 크기 */
    __u64  stx_attributes;  /* 파일 속성 플래그 */
    __u32  stx_nlink;       /* 하드 링크 수 */
    __u32  stx_uid;         /* 소유자 UID */
    __u32  stx_gid;         /* 소유자 GID */
    __u16  stx_mode;        /* 파일 유형 + 권한 */
    __u64  stx_ino;         /* inode 번호 */
    __u64  stx_size;        /* 파일 크기 */
    __u64  stx_blocks;      /* 512B 블록 수 */
    __u64  stx_attributes_mask;

    /* 타임스탬프 (나노초 정밀도) */
    struct statx_timestamp  stx_atime;
    struct statx_timestamp  stx_btime;  /* 생성 시간! */
    struct statx_timestamp  stx_ctime;
    struct statx_timestamp  stx_mtime;

    /* 디바이스 정보 */
    __u32  stx_rdev_major;
    __u32  stx_rdev_minor;
    __u32  stx_dev_major;
    __u32  stx_dev_minor;

    __u64  stx_mnt_id;      /* 마운트 ID (5.8+) */
    __u32  stx_dio_mem_align;  /* DIO 메모리 정렬 (6.1+) */
    __u32  stx_dio_offset_align; /* DIO 오프셋 정렬 */
};

/* stx_attributes 플래그 */
#define STATX_ATTR_COMPRESSED   0x00000004  /* 압축됨 */
#define STATX_ATTR_IMMUTABLE    0x00000010  /* 변경 불가 */
#define STATX_ATTR_APPEND       0x00000020  /* 추가만 가능 */
#define STATX_ATTR_NODUMP       0x00000040  /* dump 제외 */
#define STATX_ATTR_ENCRYPTED    0x00000800  /* 암호화됨 */
#define STATX_ATTR_VERITY       0x00100000  /* fs-verity */
#define STATX_ATTR_DAX          0x00200000  /* DAX 모드 */

/* 사용 예시 */
struct statx stx;
statx(AT_FDCWD, "/home/user/test.txt",
      AT_STATX_SYNC_AS_STAT, STATX_ALL, &stx);

if (stx.stx_mask & STATX_BTIME)
    printf("Birth: %lld.%09u\n",
           stx.stx_btime.tv_sec, stx.stx_btime.tv_nsec);

stat vs statx 비교

기능stat/fstat/lstatstatx
생성 시간 (birth time)미지원stx_btime
마운트 ID미지원stx_mnt_id
파일 속성 (DAX, verity)미지원stx_attributes
선택적 필드 요청불가 (전부 반환)mask로 필요한 것만
강제 동기화 제어불가AT_STATX_FORCE_SYNC 등
DIO 정렬 정보미지원stx_dio_mem_align (6.1+)
inode 번호 크기ino_t (32/64비트)항상 __u64
커널 버전초기 커널4.11+
💡

statx 동기화 모드: statx()의 flags에 AT_STATX_FORCE_SYNC를 지정하면 NFS 등 네트워크 FS에서 서버의 최신 속성을 강제로 가져옵니다. 반대로 AT_STATX_DONT_SYNC는 캐시만 조회하여 네트워크 오버헤드 없이 빠르게 정보를 얻을 수 있습니다. 기본값인 AT_STATX_SYNC_AS_STAT는 기존 stat과 동일한 동작(로컬 FS는 즉시, NFS는 캐시 정책에 따름)을 합니다.

inode 관련 성능 패턴과 안티패턴

실무에서 자주 마주치는 inode 관련 성능 문제와 해결 패턴을 정리합니다.

패턴/안티패턴 요약

유형패턴상세영향
안티 소파일 대량 생성 + ext4 inode 고정 할당 → inode 고갈 ENOSPC (디스크 여유있어도)
패턴 소파일 워크로드에 XFS/Btrfs 동적 inode 할당 → 고갈 없음 안정적 운영
안티 strictatime 마운트 매 read마다 inode dirty → writeback 불필요한 I/O, 성능 저하
패턴 noatime 또는 lazytime atime 갱신 최소화 I/O 절감, SSD 수명 연장
안티 대량 unlink 후 재시작(Reboot) 없음 열린 fd가 디스크 공간 잡고 있음 디스크 full 지속
패턴 logrotate + copytruncate inode 유지, 내용만 잘라냄 디스크 즉시 해제
안티 수백만 엔트리 단일 디렉터리 디렉터리 lookup 성능 저하 ls, rm 극도로 느림
패턴 해시 기반 서브디렉터리 분산 dir_index (htree) 한계 완화 안정적 조회 성능
안티 inotify로 거대 트리 감시 watch 수 폭발 (재귀 미지원) 메모리 낭비, ENOMEM
패턴 fanotify FAN_MARK_MOUNT 사용 마운트 전체 감시 (단일 mark) 효율적 이벤트 수신
안티 NFS actimeo=0 + 높은 부하 모든 접근마다 GETATTR RPC 네트워크 폭주, 지연 증가
패턴 NFS 기본 캐시 + close-to-open 적절한 캐시로 RPC 절감 합리적 일관성 + 성능
# ===== 성능 문제 진단 원라이너 모음 =====

# 1. 삭제되었지만 열린 파일 (디스크 점유) 찾기
lsof +L1 | awk '{total += $7} END {printf "Total: %.1f GB\n", total/1024/1024/1024}'

# 2. inode 사용률이 높은 파일시스템 찾기
df -i | awk '$5+0 > 80 {print "WARNING:", $0}'

# 3. 디렉터리별 파일 수 상위 20개
find / -xdev -type d -exec sh -c 'echo "$(ls -1 "{}" 2>/dev/null | wc -l) {}"' \; 2>/dev/null | sort -rn | head -20

# 4. inode 캐시 메모리 사용량 (MB)
grep inode_cache /proc/slabinfo | awk '{printf "%.1f MB (%d objects)\n", $3*$4/1024/1024, $2}'

# 5. dirty inode writeback 지연 확인
cat /proc/meminfo | grep -E "Dirty|Writeback"

# 6. 파일 단편화 확인
filefrag -v /path/to/large/file
# extents 수가 많으면 단편화

# 7. 특정 프로세스의 열린 파일 수
ls -la /proc/$PID/fd | wc -l
# 또는 시스템 전체
cat /proc/sys/fs/file-nr
# 할당된 fd  미사용 fd  최대 fd
대규모 rm 주의: 수백만 개의 파일을 rm -rf로 삭제하면 커널이 각 inode를 순차적으로 evict하면서 시스템 전체가 느려질 수 있습니다. 대안: (1) ionice -c3 rm -rf로 I/O 우선순위(Priority) 낮추기, (2) find ... -delete로 배치 삭제, (3) rsync --delete로 빈 디렉터리와 동기화, (4) 파일시스템 재생성(mkfs)이 가장 빠릅니다 (전체 삭제 시).

inode와 Direct I/O

Direct I/O(DIO)는 페이지 캐시를 우회하여 사용자 버퍼와 디스크 간에 직접 데이터를 전송합니다. 데이터베이스처럼 자체 캐시를 가진 애플리케이션에서 이중 캐싱을 피하기 위해 사용됩니다. inode 관점에서 DIO는 특수한 잠금과 일관성 요구사항을 가집니다.

/* Direct I/O와 inode의 상호작용 */

/* 1. DIO 읽기: i_rwsem을 공유(shared) 모드로 획득 */
/*    → 다른 DIO 읽기와 병렬 가능 */
/*    → 버퍼드 write/truncate와는 배타적 */

/* 2. DIO 쓰기: i_rwsem을 배타적(exclusive) 모드로 획득 */
/*    → 다른 모든 I/O와 직렬화 */
/*    (단, inode_dio_wait 패턴으로 최적화 가능) */

/* DIO와 페이지 캐시 일관성 */
ssize_t ext4_dio_write_iter(struct kiocb *iocb,
                           struct iov_iter *from)
{
    struct inode *inode = file_inode(iocb->ki_filp);

    /* i_rwsem 획득 */
    inode_lock(inode);

    /* 페이지 캐시의 dirty 데이터를 먼저 기록 */
    ret = filemap_write_and_wait_range(
        inode->i_mapping, offset, offset + count - 1);

    /* 해당 범위의 페이지 캐시를 무효화 */
    invalidate_inode_pages2_range(
        inode->i_mapping, offset >> PAGE_SHIFT,
        (offset + count - 1) >> PAGE_SHIFT);

    /* DIO 쓰기 진행 중임을 표시 */
    inode_dio_begin(inode);

    /* 실제 DIO 수행 */
    ret = iomap_dio_rw(iocb, from, &ext4_iomap_ops,
                        &ext4_dio_write_ops, ...);

    inode_dio_end(inode);
    inode_unlock(inode);
    return ret;
}

/* inode_dio_begin/end — DIO 진행 추적 */
static inline void inode_dio_begin(struct inode *inode)
{
    atomic_inc(&inode->i_dio_count);
}
static inline void inode_dio_end(struct inode *inode)
{
    if (atomic_dec_and_test(&inode->i_dio_count))
        wake_up_bit(&inode->i_state, __I_DIO_WAKEUP);
}

/* truncate 시 DIO 완료 대기 */
inode_dio_wait(inode);  /* i_dio_count가 0이 될 때까지 대기 */

DIO 정렬 요구사항

요소전통적 요구사항커널 6.1+ (statx)
버퍼 정렬논리 블록 크기 (보통 512B)stx_dio_mem_align
오프셋 정렬논리 블록 크기stx_dio_offset_align
전송 크기논리 블록 크기의 배수오프셋 정렬과 동일
NVMe/4Kn4096바이트디바이스에 따라 자동
# DIO 정렬 요구사항 확인 (커널 6.1+)
python3 -c "
import os, struct
# statx 시스템콜로 DIO 정렬 확인
# stx_dio_mem_align, stx_dio_offset_align 필드 확인
r = os.stat('/home/user/test.txt')
print(f'Block size: {r.st_blksize}')
"

# O_DIRECT로 파일 열기 (사용자 공간)
# dd if=/dev/zero of=test.dat bs=4k count=100 oflag=direct

# xfs_io로 DIO 테스트
xfs_io -d -c "pread 0 4096" /path/to/file  # -d: O_DIRECT

# fio로 DIO 성능 벤치마크
fio --name=dio_test --filename=/path/to/file \
    --rw=randread --bs=4k --direct=1 \
    --numjobs=4 --iodepth=32 --runtime=30
DIO와 페이지 캐시 혼합 사용 주의: 같은 파일에 대해 DIO와 버퍼드 I/O를 혼합하면 데이터 불일치가 발생할 수 있습니다. DIO 쓰기가 페이지 캐시를 우회하므로, 버퍼드 읽기가 캐시의 오래된 데이터를 반환할 수 있습니다. ext4는 DIO 쓰기 전에 캐시를 flush/invalidate하여 이를 방지하지만, 성능 오버헤드가 있습니다. 가능하면 한 파일에 대해 DIO 또는 버퍼드 I/O 중 하나만 사용하세요.

inode와 DAX (Direct Access)

DAX(Direct Access)는 영구 메모리(Persistent Memory, PMEM)에서 페이지 캐시를 완전히 우회하여 사용자 공간에서 스토리지에 직접 load/store 명령으로 접근하는 기술입니다. inode의 S_DAX 플래그가 DAX 모드를 표시하며, address_space의 동작이 근본적으로 달라집니다.

/* DAX 관련 inode 플래그 */
#define S_DAX  (1 << 13)   /* DAX 모드 활성화 */
#define IS_DAX(inode)  ((inode)->i_flags & S_DAX)

/* DAX 모드에서의 read/write — 페이지 캐시 없음! */
/* 1. read: dax_iomap_rw() → 직접 memcpy from PMEM */
/* 2. write: dax_iomap_rw() → 직접 memcpy to PMEM */
/* 3. mmap: dax_iomap_fault() → PMEM 물리 주소를 PTE에 직접 매핑 */

/* ext4 DAX address_space_operations */
static const struct address_space_operations ext4_dax_aops = {
    .writepages      = ext4_dax_writepages,
    .direct_IO       = noop_direct_IO,  /* DIO 경로 미사용 */
    .dirty_folio     = noop_dirty_folio,
};

/* DAX mmap — 페이지 폴트 시 PMEM 직접 매핑 */
static vm_fault_t ext4_dax_huge_fault(struct vm_fault *vmf,
                                      unsigned int order)
{
    /* PMEM의 물리 주소를 PTE에 직접 매핑 */
    /* → 사용자 공간에서 load/store로 직접 접근 */
    /* → memcpy 오버헤드 제거 */
    return dax_iomap_fault(vmf, order, NULL, NULL,
                           &ext4_iomap_ops);
}
비교일반 I/ODirect I/ODAX
페이지 캐시사용우회 (버퍼 복사)없음 (직접 매핑)
데이터 복사커널↔유저 복사DMA 전송load/store 직접 접근
mmap페이지 캐시 매핑-PMEM 물리 주소(Physical Address) 직접 매핑
address_spaceXArray에 folio 관리페이지 캐시 flushXArray에 DAX entry(PFN) 관리
writebackdirty folio → 디스크즉시 디스크없음 (이미 영구 저장)
스토리지SSD/HDDSSD/HDDPMEM (Intel Optane 등)
# DAX 설정
# 1. PMEM 장치 확인
ndctl list -N
# 2. fsdax 모드로 namespace 설정
ndctl create-namespace -m fsdax -e namespace0.0
# 3. 파일시스템 생성 + 마운트
mkfs.ext4 /dev/pmem0
mount -o dax=always /dev/pmem0 /mnt/pmem

# 파일별 DAX 설정 (커널 5.8+, per-file DAX)
xfs_io -c "chattr +x" /mnt/pmem/file   # DAX 활성화
xfs_io -c "chattr -x" /mnt/pmem/file   # DAX 비활성화

# DAX 상태 확인
statx /mnt/pmem/file | grep -i dax
# STATX_ATTR_DAX: set

# 마운트 옵션
# dax=always  — 모든 파일에 DAX 강제
# dax=never   — DAX 비활성화
# dax=inode   — 파일별 FS_XFLAG_DAX 기반 (기본)
DAX 제약: DAX 모드에서는 reflink(CoW 복사), 인라인 데이터, 암호화(fscrypt)동시 사용 불가합니다. 또한 MADV_HUGEPAGE로 투명 대형 페이지를 활용하려면 PMEM이 2MB/1GB 정렬되어야 합니다. DAX 파일에 대한 sendfile()은 일반 read+write 경로로 폴백됩니다.

inode와 디스크 쿼타

디스크 쿼타는 사용자/그룹/프로젝트별로 inode 수블록 사용량을 제한합니다. inode 생성·삭제 시 VFS가 쿼타 검사를 수행하며, 한도 초과 시 -EDQUOT를 반환합니다.

/* include/linux/quota.h — 쿼타 정보 구조 */
struct dquot {
    struct super_block  *dq_sb;
    kqid_t              dq_id;     /* uid/gid/projid */
    struct mem_dqblk    dq_dqb;    /* 사용량 + 한도 */
};

struct mem_dqblk {
    qsize_t  dqb_bhardlimit;  /* 블록 하드 리밋 */
    qsize_t  dqb_bsoftlimit;  /* 블록 소프트 리밋 */
    qsize_t  dqb_curspace;    /* 현재 사용 블록 */
    qsize_t  dqb_ihardlimit;  /* inode 하드 리밋 */
    qsize_t  dqb_isoftlimit;  /* inode 소프트 리밋 */
    qsize_t  dqb_curinodes;   /* 현재 사용 inode 수 */
    struct timespec64 dqb_btime; /* 블록 소프트 리밋 유예 기한 */
    struct timespec64 dqb_itime; /* inode 소프트 리밋 유예 기한 */
};

/* inode 생성 시 쿼타 검사 흐름 */
/* 1. dquot_initialize(dir)    — 부모 디렉터리 쿼타 초기화 */
/* 2. dquot_alloc_inode(inode)  — inode 쿼타 할당 (카운트++) */
/*    → dqb_curinodes++ */
/*    → dqb_ihardlimit 초과 시 -EDQUOT 반환 */
/* 3. dquot_alloc_block(inode, count) — 블록 쿼타 할당 */

/* inode 삭제 시 쿼타 반환 */
/* evict_inode() → dquot_free_inode(inode) */
/*               → dqb_curinodes-- */
쿼타 유형식별자용도설정 명령
User quotaUID사용자별 용량 제한setquota -u user 100M 120M 1000 1200 /
Group quotaGID그룹별 용량 제한setquota -g group 500M 600M 5000 6000 /
Project quotaProject ID디렉터리별 용량 제한xfs_quota -x -c 'limit -p bhard=1G projid' /
# 쿼타 설정 (ext4)
# 1. 마운트 옵션에 쿼타 활성화
mount -o usrquota,grpquota /dev/sda1 /home

# 2. 쿼타 파일 생성 + 초기화
quotacheck -cug /home
quotaon /home

# 3. 사용자 쿼타 설정 (블록 소프트/하드, inode 소프트/하드)
setquota -u user1 100M 120M 10000 12000 /home
# → inode 최대 12,000개, 블록 최대 120MB

# 쿼타 확인
repquota -as /home
# 또는
quota -u user1

# XFS 프로젝트 쿼타 (디렉터리 단위)
echo "1:/home/project_a" >> /etc/projects
echo "project_a:1" >> /etc/projid
xfs_quota -x -c "project -s project_a" /home
xfs_quota -x -c "limit -p bhard=10G 1" /home
ℹ️

소프트 리밋 vs 하드 리밋: 소프트 리밋은 유예 기간(grace period, 기본 7일) 동안 초과를 허용하고, 유예 기간 만료 후 하드 리밋처럼 동작합니다. 하드 리밋은 절대 초과 불가합니다. inode 쿼타는 소파일이 대량 생성되는 환경(메일 서버, npm 캐시)에서 특히 중요합니다 — 블록 쿼타는 여유가 있어도 inode 쿼타에 걸려 파일 생성이 실패할 수 있습니다.

파일시스템 드라이버의 inode 구현 체크리스트

새로운 파일시스템 드라이버를 작성할 때 inode 관련 필수 구현 항목을 체크리스트로 정리합니다.

필수 구현 항목

#항목관련 콜백/함수설명
1FS-specific inode 구조체 정의-struct inode를 임베드한 확장 구조체
2slab 캐시 생성kmem_cache_create()모듈 init에서 inode 전용 slab 캐시 생성
3alloc_inode()s_op->alloc_inodeslab에서 FS-specific inode 할당
4free_inode()s_op->free_inodeRCU 콜백으로 slab 해제
5write_inode()s_op->write_inodedirty inode를 디스크에 기록
6evict_inode()s_op->evict_inodeinode 제거 (nlink==0이면 디스크 해제)
7lookup()i_op->lookup디렉터리에서 이름으로 inode 검색
8create()i_op->create새 일반 파일 inode 생성
9getattr()i_op->getattrstat(2)용 속성 반환 (선택, generic 가능)
10setattr()i_op->setattrchmod/chown/truncate 처리
11read_folio()a_ops->read_folio페이지 캐시 미스 시 디스크에서 읽기
12writepages()a_ops->writepagesdirty 페이지를 디스크에 기록
/* 최소 파일시스템의 inode 구현 뼈대 */

/* 1. FS-specific inode 정의 */
struct myfs_inode_info {
    __u32   i_disk_flags;
    __u64   i_disk_size;
    sector_t i_first_block;
    struct inode vfs_inode;  /* 반드시 임베드 */
};

static inline struct myfs_inode_info *MYFS_I(struct inode *i)
{
    return container_of(i, struct myfs_inode_info, vfs_inode);
}

/* 2. slab 캐시 */
static struct kmem_cache *myfs_inode_cachep;

static int __init myfs_init_inodecache(void)
{
    myfs_inode_cachep = kmem_cache_create(
        "myfs_inode_cache",
        sizeof(struct myfs_inode_info),
        0,
        SLAB_RECLAIM_ACCOUNT | SLAB_ACCOUNT,
        NULL);
    return myfs_inode_cachep ? 0 : -ENOMEM;
}

/* 3-4. alloc/free_inode */
static struct inode *myfs_alloc_inode(struct super_block *sb)
{
    struct myfs_inode_info *mi;
    mi = alloc_inode_sb(sb, myfs_inode_cachep, GFP_KERNEL);
    if (!mi)
        return NULL;
    mi->i_disk_flags = 0;
    mi->i_disk_size  = 0;
    mi->i_first_block = 0;
    return &mi->vfs_inode;
}

static void myfs_free_inode(struct inode *inode)
{
    kmem_cache_free(myfs_inode_cachep, MYFS_I(inode));
}

/* 5. write_inode */
static int myfs_write_inode(struct inode *inode,
                           struct writeback_control *wbc)
{
    struct myfs_inode_info *mi = MYFS_I(inode);
    struct buffer_head *bh;

    /* 디스크의 inode 블록을 읽어서 */
    bh = sb_bread(inode->i_sb, myfs_inode_block(inode));
    /* VFS inode 필드를 디스크 형식으로 변환하여 기록 */
    myfs_fill_disk_inode(bh->b_data, inode, mi);
    mark_buffer_dirty(bh);
    if (wbc->sync_mode == WB_SYNC_ALL)
        sync_dirty_buffer(bh);
    brelse(bh);
    return 0;
}

/* 6. evict_inode */
static void myfs_evict_inode(struct inode *inode)
{
    truncate_inode_pages_final(&inode->i_data);
    if (!inode->i_nlink) {
        /* 실제 삭제: 디스크의 inode와 데이터 블록 해제 */
        myfs_free_disk_inode(inode);
        myfs_free_data_blocks(inode);
    }
    clear_inode(inode);
}

/* 7. super_operations 등록 */
static const struct super_operations myfs_sops = {
    .alloc_inode  = myfs_alloc_inode,
    .free_inode   = myfs_free_inode,
    .write_inode  = myfs_write_inode,
    .evict_inode  = myfs_evict_inode,
    .statfs       = simple_statfs,
    .drop_inode   = generic_delete_inode,
};
💡

FS 드라이버 테스트 도구: 파일시스템 드라이버의 inode 구현을 검증하는 데 유용한 도구: (1) xfstests — 포괄적인 파일시스템 테스트 스위트 (generic/* 테스트가 VFS 호환성 검증), (2) trinity — 시스템콜 퍼저로 엣지 케이스 탐색, (3) CONFIG_LOCKDEP — 잠금 순서 위반 탐지, (4) KASAN / KMEMLEAK — inode slab 메모리 오류 탐지. 반드시 lockdep과 KASAN을 활성화한 커널에서 xfstests의 generic/ 테스트를 전부 통과시켜야 합니다.

자주 발생하는 실수

실수증상해결
unlock_new_inode() 누락다른 스레드가 영원히 I_NEW 대기iget_locked 후 반드시 호출
clear_inode() 누락evict 후 BUG 발생evict_inode에서 반드시 호출
d_instantiate() 누락생성된 파일을 찾을 수 없음create/mkdir에서 반드시 호출
slab SLAB_ACCOUNT 누락cgroup 메모리 accounting 누락kmem_cache_create 플래그 추가
i_nlink 직접 조작NFS 등에서 불일치set_nlink(), inode_inc_link_count() 사용
타임스탬프 직접 설정정밀도 truncation 미적용inode_set_ctime_current() 등 API 사용
i_rwsem 잠금 순서 위반데드락 (lockdep 경고)lock_rename(), nested 레벨 사용
evict에서 I/O 에러 무시디스크 데이터 불일치에러 시 make_bad_inode() 설정

소스 참조 및 더 읽기

inode 구현과 관련된 커널 소스 파일과 외부 참고 자료를 정리합니다.

커널 소스 파일

파일내용
fs/inode.cVFS inode 핵심 구현 (할당, 해시, LRU, evict)
fs/namei.c경로 조회, 권한 검사 (inode_permission)
fs/fs-writeback.cdirty inode writeback 인프라
fs/stat.cstat/statx 시스템콜 구현
fs/notify/fsnotify, inotify, fanotify 구현
fs/posix_acl.cPOSIX ACL 구현
include/linux/fs.hstruct inode, inode_operations 정의
fs/ext4/inode.cext4 inode 읽기/쓰기/삭제
fs/ext4/ialloc.cext4 inode 할당 (Orlov)
fs/xfs/xfs_inode.cXFS inode 구현
fs/btrfs/inode.cBtrfs inode 구현
fs/dcache.cdentry 캐시 — inode와의 연결, 경로 조회
fs/open.c파일 열기 — inode에서 file 구조체로의 전환
mm/filemap.c페이지 캐시 핵심 — address_space 연산 구현
security/selinux/hooks.cSELinux inode 보안 훅 구현

외부 참고 자료

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