inode 구조 (inode Structure)

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

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

핵심 요약

  • 이름/실체 분리 — dentry는 이름, inode는 파일 실체
  • inode cache — 해시 + 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 구조체는 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
링크하드 링크 수, 참조 카운트i_nlink, i_count
연산파일시스템별 동작 테이블i_op, i_fop
데이터페이지 캐시 연결i_mapping, i_data
잠금동시성 제어i_lock, i_rwsem
캐시해시, LRU, writeback 리스트i_hash, i_lru, i_io_list

inode 추상화 원리

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

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

VFS inode vs 파일시스템별 inode

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

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

inode 캐시 동작 원리

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

  1. 해시 테이블 조회: 파일 접근 시 (superblock, inode 번호) 쌍으로 해시 테이블을 검색합니다. 히트하면 디스크 읽기 없이 즉시 반환합니다.
  2. LRU 관리: 참조 카운트(i_count)가 0이 된 inode는 LRU 리스트에 들어갑니다. 즉시 삭제하지 않고 캐시에 유지하여, 재접근 시 빠르게 활용합니다.
  3. 메모리 회수: 메모리 압력 시 커널의 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 */
};

inode_operations

struct inode_operations는 inode 자체에 대한 연산(생성, 검색, 삭제, 속성 변경)을 정의합니다. 각 파일시스템은 자체 콜백을 구현하여 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경로 해석 (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 기능을 지원하기 위한 것으로, 컨테이너 환경에서 마운트별로 UID/GID 매핑을 다르게 적용할 수 있습니다. 기존의 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 미만에서 마운트 불가 (호환 플래그)
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)프로세스간 통신
S_IFSOCK소켓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() 호출 */

파일시스템별 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,
};

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
이름 노출파일명이 디렉터리에 노출됨이름 없음 (디렉터리 탐색 불가)
레이스 컨디션생성-삭제 사이 윈도우 존재없음 (처음부터 이름 없음)
원자적 게시rename으로 구현 (기존 파일 필요)linkat으로 원자적 게시
크래시 안전잔여 파일 수동 정리 필요orphan list → 자동 정리
커널 요구모든 커널3.11+, FS가 tmpfile 콜백 구현 필요
지원 FS모든 FSext4, XFS, Btrfs, tmpfs 등
💡

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

확장 속성 (xattr)

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

네임스페이스접두사용도
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마운트/파일시스템전체 마운트 감시, 접근 제어 가능

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 필드 보호용 스핀락. i_state, i_count 등의 변경 시 사용
i_rwsemstruct rw_semaphore파일 데이터 접근 직렬화. 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까지 함께 비교해야 합니다.

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_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 페이지 보유__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 대기자 깨우기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);
}
ℹ️

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는 건너뜁니다.

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를 공유할 수 있음 */
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)버퍼드 write 시작folio 할당 + 블록 매핑 준비
write_end(file, mapping, pos, len, copied, folio)버퍼드 write 완료dirty 마킹 + 메타데이터 갱신
invalidate_folio(folio, offset, length)truncate/hole punchfolio의 전부 또는 일부를 무효화
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를 프로세스의 가상 주소 공간에 매핑합니다. 이 매핑은 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 (쓰기 시 복사)읽기 시 공유, 쓰기 시 분리사본은 swap에 기록
MAP_ANONYMOUSinode 없음address_space 없음swap에 기록
truncate와 mmap 경합: 파일이 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() 검사 흐름 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로 결과를 반드시 확인하세요.

타임스탬프 정밀도와 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 파이프라인

dirty inode의 데이터를 디스크에 기록하는 과정을 writeback이라 합니다. 커널은 백그라운드 워커 스레드를 통해 비동기적으로 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 성능 튜닝: 데이터베이스 서버에서는 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 내부 트랜잭션모든 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
암호화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);
}
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);
}
ℹ️

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

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 필드로 연결 리스트 구성슈퍼블록 + 각 inode의 i_dtime
ext4 (5.15+)orphan file (COMPAT_ORPHAN_FILE)전용 inode에 orphan 비트맵 저장 — 동시성 향상
XFSAGI unlinked 해시 체인각 AG 헤더의 unlinked 버킷
Btrfsorphan item (B-tree)FS tree에 ORPHAN_ITEM 키
💡

Orphan 디버깅: 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 번호 할당 전략

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

파일시스템할당 방식범위고갈 가능성특이사항
ext4비트맵 (고정 테이블)mkfs 시 결정있음Orlov 할당기로 디렉터리 분산
XFSAG 내 동적 할당64비트극히 낮음AG별 free inode B+tree
Btrfsobjectid (단조 증가)64비트없음서브볼륨별 독립 번호 공간
tmpfsget_next_ino()32비트 (percpu)번호 재활용디스크 없음
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무결성 검증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 발생오버헤드 없음VFS 계층 매핑만 (무시할 수준)
용도전통적 소유권프로세스 격리컨테이너 파일 공유
💡

컨테이너 실전: Docker/Podman에서 호스트 볼륨을 바인드 마운트할 때 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부트 로더
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 인터페이스, 사용자 공간 유틸리티를 활용하여 inode 동작을 실시간으로 관찰할 수 있습니다.

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 매핑, 단편화 확인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)로 사용하면 데이터 손상을 유발할 수 있습니다. 읽기 전용 모드(기본)에서만 사용하고, 수정이 필요하면 반드시 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 후 재시작 없음 열린 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 우선순위 낮추기, (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 물리 주소 직접 매핑
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 구조와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.