VFS 계층 (Virtual Filesystem Switch)

Linux 커널의 파일시스템(Filesystem) 추상화 계층인 VFS를 중심으로 superblock, inode, dentry, file 객체의 역할과 수명주기, namei 기반 경로 탐색, open/read/write/fsync 호출의 디스패치(Dispatch) 경로, 페이지 캐시(Page Cache) 및 writeback 연동, flock/fcntl/OFD 잠금(Lock) 모델, 권한·보안 검사 지점을 실제 커널 코드 관점에서 상세히 설명합니다.

전제 조건: 시스템 콜(System Call)Page Cache 문서를 먼저 읽으세요. 파일시스템 공통 계층은 시스템 콜 진입점(Entry Point)과 캐시 일관성(Cache Coherency)이 중심이므로, 먼저 유저-커널 경계를 고정하는 것이 중요합니다.
일상 비유: 이 주제는 도서관 분류 카드와 대출대장과 비슷합니다. 책 본문(데이터)보다 카드/대장(메타데이터) 규칙이 먼저 맞아야 전체 조회와 갱신이 안정적으로 동작합니다.

핵심 요약

  • VFS 추상화 계층 — 다양한 파일시스템에 통일된 인터페이스를 제공하는 커널 중간 계층입니다.
  • super_block — 마운트된 파일시스템 인스턴스를 나타내며, 블록 크기·상태·파일시스템 고유 정보를 보관합니다.
  • inode — 파일의 메타데이터(크기, 권한, 타임스탬프 등)를 보관하는 핵심 객체입니다.
  • dentry (Directory Entry) 캐시 — 경로명을 inode로 매핑하는 캐시로, 경로 탐색(Path Lookup) 성능을 좌우합니다.
  • struct file — 프로세스가 열어 둔 파일 핸들을 나타내며, 파일 오프셋(offset)과 접근 모드를 추적합니다.
  • file_operations / inode_operations — 각 파일시스템이 구현해야 하는 콜백 함수 테이블로, VFS와 하위 FS의 연결점입니다.
  • Path Lookup (namei) — 사용자 경로 문자열을 dentry/inode로 변환하는 과정으로, symlink·마운트포인트 횡단을 포함합니다.
  • dcache / icache — dentry 캐시와 inode 캐시가 메모리에서 메타데이터를 유지하여 디스크 접근을 최소화합니다.

단계별 이해

  1. VFS 4대 객체 구분 — superblock, inode, dentry, file 객체의 역할과 관계를 파악합니다.

    마운트 시 superblock이 생성되고, 파일 접근 시 dentry → inode → file 순서로 객체가 연결됩니다.

  2. 시스템 콜에서 VFS 진입 추적open(), read(), write()가 VFS 계층을 거쳐 하위 FS에 도달하는 경로를 따라갑니다.

    시스템 콜 → VFS 공통 로직 → file_operations 콜백 → 실제 파일시스템 구현 순서로 흐릅니다.

  3. 경로 탐색(Path Lookup) 이해 — 문자열 경로가 커널에서 어떻게 dentry/inode로 변환되는지 확인합니다.

    dcache 히트 시 즉시 반환되고, 미스(miss) 시 실제 파일시스템의 lookup()을 호출하여 디스크에서 읽어옵니다.

  4. operations 테이블 구조 파악 — 하위 파일시스템이 VFS에 자신을 등록하는 방식을 확인합니다.

    file_operations, inode_operations, super_operations 등 콜백 테이블을 채워 VFS가 적절한 함수를 호출하게 합니다.

  5. 마운트와 네임스페이스 이해mount() 시스템 콜이 VFS 트리에 파일시스템을 연결하는 과정을 확인합니다.

    mount namespace에 따라 같은 경로가 다른 파일시스템을 가리킬 수 있으며, 이는 컨테이너 격리의 기반입니다.

  6. 캐시 동작과 메모리 압박 대응 점검 — dcache/icache가 메모리 부족 시 어떻게 축소되는지 확인합니다.

    메모리 압박 시 shrinker가 LRU 기반으로 사용 빈도가 낮은 dentry/inode를 회수합니다.

관련 표준: POSIX.1-2017 (파일 I/O 시맨틱, 디렉토리, 퍼미션) — VFS는 POSIX 파일 시맨틱의 커널 측 구현입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

VFS 개요

VFS(Virtual Filesystem Switch)는 다양한 파일시스템(ext4, XFS, btrfs, NFS 등)에 대해 통일된 인터페이스를 제공하는 커널 추상화 계층입니다. 사용자 프로그램은 open(), read(), write() 등의 시스템 콜을 통해 어떤 파일시스템이든 동일한 방식으로 접근합니다.

VFS 계층 구조 User Process: open(), read(), write() System Call Interface (sys_open, sys_read, ...) VFS Layer (superblock, inode, dentry, file) ext4 XFS btrfs Block Layer → Storage Devices
VFS: 사용자 공간(User Space)과 실제 파일시스템 사이의 추상화 계층

VFS 핵심 객체

Superblock

struct super_block은 마운트(Mount)된 파일시스템을 나타냅니다. 파일시스템 유형, 블록 크기, 마운트 옵션, 루트 dentry 등의 정보를 포함합니다.

struct super_operations {
    struct inode *(*alloc_inode)(struct super_block *sb);
    void (*destroy_inode)(struct inode *);
    void (*dirty_inode)(struct inode *, int flags);
    int (*write_inode)(struct inode *, struct writeback_control *);
    int (*statfs)(struct dentry *, struct kstatfs *);
    int (*sync_fs)(struct super_block *sb, int wait);
    /* ... */
};

struct super_block 핵심 필드

struct super_block은 마운트된 파일시스템 인스턴스를 메모리에서 표현하는 핵심 객체입니다. alloc_super()로 할당되고 destroy_super()로 해제되며, 파일시스템이 언마운트될 때까지 존재합니다.

/* include/linux/fs.h — struct super_block 핵심 필드 (축약) */
struct super_block {
    /* ── 연결 구조 ────────────────────────────────────── */
    struct list_head      s_list;      /* 전역 super_block 리스트 (sb_lock 보호) */
    dev_t                 s_dev;       /* 블록 장치 번호 (식별자) */

    /* ── 블록 크기 ────────────────────────────────────── */
    unsigned char         s_blocksize_bits;  /* 블록 크기의 비트 수 (log2) */
    unsigned long         s_blocksize;       /* 블록 크기 (바이트, 512~65536) */
    loff_t                s_maxbytes;        /* 파일 최대 크기 (FS 제한) */

    /* ── 파일시스템 타입 및 연산 ──────────────────────── */
    struct file_system_type  *s_type;   /* 등록된 파일시스템 유형 */
    const struct super_operations *s_op; /* superblock 연산 테이블 */
    const struct xattr_handler * const *s_xattr; /* 확장 속성 핸들러 배열 */
    const struct export_operations *s_export_op; /* NFS 파일핸들 내보내기 연산 */

    /* ── 마운트 플래그 ────────────────────────────────── */
    unsigned long         s_flags;     /* 마운트 플래그 (아래 참조) */
    /*  SB_RDONLY    (1<<0)  읽기 전용 마운트
        SB_NOSUID   (1<<1)  SUID/SGID 비트 무시
        SB_NODEV    (1<<2)  장치 파일 접근 차단
        SB_NOEXEC   (1<<3)  실행 파일 실행 차단
        SB_NOATIME  (1<<10) atime 갱신 비활성화
        SB_POSIXACL (1<<16) POSIX ACL 지원
        SB_ACTIVE   (1<<30) 마운트 완료 — 언마운트 전까지 설정
        SB_BORN     (1<<29) fill_super() 완료 후 설정     */

    /* ── 매직 넘버 및 루트 ───────────────────────────── */
    unsigned long         s_magic;     /* 파일시스템 식별 매직 (EXT4_SUPER_MAGIC 등) */
    struct dentry         *s_root;     /* 마운트 루트 dentry */

    /* ── 참조 카운트 ──────────────────────────────────── */
    int                   s_count;     /* 내부 참조 카운트 (sb_lock 보호) */
    atomic_t              s_active;    /* 활성 참조 수 (0이 되면 언마운트 가능) */

    /* ── 블록 장치 및 I/O 백엔드 ─────────────────────── */
    struct block_device   *s_bdev;     /* 연결된 블록 장치 (없으면 NULL) */
    struct backing_dev_info *s_bdi;    /* writeback 방향 제어 (BDI) */

    /* ── freeze/thaw 지원 ────────────────────────────── */
    struct sb_writers     s_writers;   /* freeze 진행 중 쓰기 차단용 퍼센트 카운터 */

    /* ── 캐시 LRU 리스트 ─────────────────────────────── */
    struct list_lru       s_dentry_lru; /* 미사용 dentry LRU — shrinker가 회수 */
    struct list_lru       s_inode_lru;  /* 미사용 inode LRU — shrinker가 회수 */

    /* ── FS별 private 데이터 ──────────────────────────── */
    void                  *s_fs_info;  /* 파일시스템 고유 데이터 (ext4_sb_info 등) */
};
alloc_super / destroy_super 수명주기: superblock은 alloc_super()로 슬랩(Slab) 캐시에서 할당되고, 파일시스템의 fill_super() 콜백으로 초기화됩니다. 언마운트 시 s_active가 0이 되면 kill_sb()destroy_super() 경로로 해제됩니다. s_fs_info는 각 파일시스템이 자유롭게 사용하는 private 포인터로, ext4의 경우 struct ext4_sb_info를 가리킵니다.

struct inode 핵심 필드

struct inode는 VFS에서 파일 하나를 대표하는 메모리 내 객체입니다. 디스크 파일시스템의 on-disk inode를 읽어 메모리에 생성하며, 파일의 메타데이터와 연산 테이블을 모두 포함합니다.

/* include/linux/fs.h — struct inode 핵심 필드 (축약) */
struct inode {
    umode_t               i_mode;      /* 파일 유형 + 퍼미션 (S_IFREG, 0644 등) */
    kuid_t                i_uid;       /* 소유자 UID */
    kgid_t                i_gid;       /* 소유 그룹 GID */
    unsigned int          i_flags;     /* S_SYNC, S_IMMUTABLE 등 */

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

    unsigned long         i_ino;       /* inode 번호 */
    union {
        const unsigned int i_nlink;   /* 하드 링크 수 */
        unsigned int      __i_nlink;
    };
    dev_t                 i_rdev;      /* 디바이스 파일의 장치 번호 */
    loff_t                i_size;      /* 파일 크기 (바이트) */
    struct timespec64     i_atime;     /* 최종 접근 시간 */
    struct timespec64     i_mtime;     /* 최종 수정 시간 */
    struct timespec64     __i_ctime;   /* 최종 변경 시간 (메타데이터) */

    atomic_t              i_count;     /* 참조 카운트 */
    atomic_t              i_writecount;/* 쓰기 참조 카운트 */

    const struct file_operations  *i_fop;  /* 기본 파일 연산 */
    struct file_lock_context *i_flctx;   /* 파일 잠금 컨텍스트 */
    struct address_space  i_data;      /* 자체 address_space */
    struct list_head      i_devices;   /* 디바이스 리스트 */

    void                  *i_private;  /* FS별 private 데이터 */
};
i_op vs i_fop: i_op(inode_operations)는 메타데이터 연산(create, mkdir, lookup, rename 등)을 담당하고, i_fop(file_operations)는 데이터 연산(read, write, mmap 등)을 담당합니다. 파일을 open()하면 i_fopstruct filef_op로 복사됩니다. 자세한 내용은 inode 구조 문서를 참고하세요.

struct inode_operations

inode_operations는 파일/디렉토리의 메타데이터 변경을 위한 콜백(Callback) 테이블입니다. 디렉토리 inode와 일반 파일 inode에 서로 다른 연산 테이블이 설정됩니다.

/* include/linux/fs.h — inode_operations 주요 콜백 */
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 (*rename)(struct mnt_idmap *, struct inode *,
                 struct dentry *, struct inode *,
                 struct dentry *, unsigned int);

    /* 속성/퍼미션 */
    int (*permission)(struct mnt_idmap *, struct inode *, int);
    int (*setattr)(struct mnt_idmap *, struct dentry *,
                  struct iattr *);
    int (*getattr)(struct mnt_idmap *, const struct path *,
                  struct kstat *, u32, unsigned int);

    /* 확장 속성 */
    ssize_t (*listxattr)(struct dentry *, char *, size_t);

    /* 파일 범위 매핑 (iomap 기반) */
    int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64, u64);
};
lookup 콜백의 역할: lookup은 경로 탐색의 핵심입니다. 디렉토리 inode에서 이름으로 자식 dentry를 찾을 때 호출됩니다. dcache에 해당 dentry가 없을 때만 실제 디스크를 읽는 이 콜백이 호출되며, 결과는 dcache에 캐싱됩니다. 반환되는 dentry가 NULL이면 negative dentry로 캐싱되어 "파일 없음" 조회를 가속합니다.

Dentry (Directory Entry)

struct dentry는 경로명의 각 구성 요소를 나타냅니다. /home/user/file.txt에서 /, home, user, file.txt 각각이 dentry입니다. dentry 캐시(dcache)는 경로 탐색 속도를 크게 향상시킵니다.

/* include/linux/dcache.h — struct dentry 핵심 필드 (축약) */
struct dentry {
    unsigned int          d_flags;     /* DCACHE_* 플래그 */
    seqcount_spinlock_t   d_seq;       /* RCU-walk용 시퀀스 카운트 */
    struct hlist_bl_node  d_hash;      /* dcache 해시 테이블 연결 */
    struct dentry         *d_parent;   /* 부모 dentry */
    struct qstr           d_name;      /* 이름 (해시 + 문자열) */
    struct inode          *d_inode;    /* 연결된 inode (NULL = negative) */
    unsigned char         d_iname[DNAME_INLINE_LEN]; /* 짧은 이름 인라인 저장 */

    struct lockref        d_lockref;   /* 참조 카운트 + 스핀락 */
    const struct dentry_operations *d_op; /* dentry 연산 테이블 */
    struct super_block    *d_sb;       /* 소속 superblock */
    void                  *d_fsdata;   /* FS별 private 데이터 */

    struct list_head      d_lru;       /* LRU 리스트 (미사용 dentry) */
    struct list_head      d_child;     /* 부모의 자식 리스트 */
    struct list_head      d_subdirs;   /* 하위 dentry 리스트 */
    union {
        struct hlist_node d_alias;    /* inode의 alias 리스트 */
        struct hlist_bl_node d_in_lookup_hash; /* in-lookup 해시 */
        struct rcu_head  d_rcu;      /* RCU 해제용 */
    } d_u;
};

dentry_operations

dentry_operations는 dentry의 유효성 검사 및 비교 방식을 커스터마이즈합니다. 네트워크 파일시스템(NFS 등)에서 서버와의 일관성 확인에 특히 중요합니다.

struct dentry_operations {
    int (*d_revalidate)(struct dentry *, unsigned int);
        /* dcache 히트 후 유효성 재검사 — NFS에서 서버 확인 */
    int (*d_weak_revalidate)(struct dentry *, unsigned int);
        /* RCU-walk 중의 가벼운 재검사 */
    int (*d_hash)(const struct dentry *, struct qstr *);
        /* 이름 해시 계산 — 대소문자 무시 FS에서 커스텀 */
    int (*d_compare)(const struct dentry *,
                     unsigned int, const char *, const struct qstr *);
        /* 이름 비교 — casefold FS에서 대소문자 무시 비교 */
    void (*d_release)(struct dentry *);
        /* dentry 해제 시 정리 콜백 */
    void (*d_iput)(struct dentry *, struct inode *);
        /* dentry에서 inode 분리 시 콜백 */
    char *(*d_dname)(struct dentry *, char *, int);
        /* 동적 이름 생성 — pipe, socket 등 */
};

File Object

struct file은 프로세스(Process)가 파일을 열 때 생성되는 런타임 객체입니다. 파일 오프셋(Offset), 접근 모드, file_operations 등을 포함합니다.

struct file 핵심 필드

struct filealloc_empty_file()로 슬랩(Slab) 캐시에서 할당됩니다. open() 시스템 콜에서 생성되어 파일 디스크립터(File Descriptor) 테이블에 등록되고, fput()으로 참조가 해제되면 최종 정리됩니다.

/* include/linux/fs.h — struct file 핵심 필드 (축약) */
struct file {
    union {
        struct llist_node     f_llist;    /* RCU 해제용 연결 리스트 */
        struct rcu_head       f_rcuhead;
        unsigned int          f_iocb_flags;
    };

    /* ── 경로 및 inode ────────────────────────────────── */
    struct path            f_path;      /* vfsmount + dentry 쌍 — 마운트 컨텍스트 포함 */
    struct inode           *f_inode;    /* 캐싱된 inode 포인터 (f_path.dentry->d_inode) */

    /* ── 연산 테이블 ──────────────────────────────────── */
    const struct file_operations *f_op; /* open() 시 inode의 i_fop에서 복사 */

    /* ── 동시성 제어 ──────────────────────────────────── */
    spinlock_t             f_lock;      /* f_ep_links, f_flags 갱신 보호 */
    atomic_long_t          f_count;     /* 참조 카운트 (get_file/fput 쌍으로 관리) */

    /* ── 플래그 및 접근 모드 ──────────────────────────── */
    unsigned int           f_flags;     /* open() 인수 플래그 (아래 참조) */
    /*  O_RDONLY   (0)        읽기 전용
        O_WRONLY   (1)        쓰기 전용
        O_RDWR     (2)        읽기·쓰기
        O_NONBLOCK (04000)    비차단 I/O
        O_APPEND   (02000)    항상 끝에 쓰기
        O_SYNC     (04010000) 동기 I/O
        O_DIRECT   (040000)   페이지 캐시 우회  */
    fmode_t                f_mode;      /* 커널 내부 접근 모드 (아래 참조) */
    /*  FMODE_READ          읽기 가능
        FMODE_WRITE         쓰기 가능
        FMODE_LSEEK         llseek 가능
        FMODE_PREAD         pread 가능
        FMODE_PWRITE        pwrite 가능
        FMODE_EXEC          실행용으로 열림
        FMODE_RANDOM        랜덤 접근 힌트
        FMODE_NOREUSE       재사용 불가  */

    /* ── 파일 위치 ────────────────────────────────────── */
    loff_t                 f_pos;       /* 현재 파일 오프셋 (read/write 후 갱신) */

    /* ── 비동기 I/O 소유자 ───────────────────────────── */
    struct fown_struct     f_owner;     /* SIGIO/SIGURG 전달 대상 (pid, uid) */

    /* ── 자격증명 ────────────────────────────────────── */
    const struct cred     *f_cred;     /* open() 시점의 호출자 자격증명 (불변) */

    /* ── readahead 상태 ───────────────────────────────── */
    struct file_ra_state  f_ra;        /* 순차 읽기 readahead 상태 (창 크기 등) */

    /* ── 페이지 캐시 매핑 ─────────────────────────────── */
    struct address_space  *f_mapping;  /* 연결된 address_space (보통 inode->i_mapping) */

    /* ── FS/드라이버 private ──────────────────────────── */
    void                   *private_data; /* 파일시스템·드라이버 자유 영역 (소켓, pipe 등) */
} __randomize_layout
  __attribute__((aligned(4)));  /* 포인터 정렬 보장 */
alloc_empty_file / fput 수명주기: open() 시스템 콜은 alloc_empty_file()struct file을 할당한 뒤 do_dentry_open()에서 f_op, f_path, f_inode를 채웁니다. 파일 디스크립터를 close()하거나 프로세스가 종료되면 fput()이 호출되고, f_count가 0이 되는 시점에 f_op->release()가 실행된 후 슬랩 캐시로 반환됩니다. f_pathvfsmount는 마운트 참조를 유지하므로 파일이 열려 있는 한 해당 파일시스템은 강제 언마운트되지 않습니다.
struct file_operations {
    struct module *owner;
    loff_t (*llseek)(struct file *, loff_t, int);
    ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
    int (*open)(struct inode *, struct file *);
    int (*release)(struct inode *, struct file *);
    int (*mmap)(struct file *, struct vm_area_struct *);
    long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
    int (*fsync)(struct file *, loff_t, loff_t, int);
    /* ... */
};

경로 탐색 (Path Lookup)

VFS는 /home/user/file.txt 같은 경로를 순차적으로 탐색합니다. 각 구성 요소마다 dentry 캐시를 검색하고, 없으면 실제 파일시스템의 lookup 연산을 호출합니다.

💡

dentry 캐시는 RCU로 보호되어 경로 탐색 시 잠금 없이 빠르게 수행됩니다 (RCU-walk 모드). RCU-walk가 실패하면 ref-walk 모드로 폴백합니다.

파일시스템 등록

VFS에 파일시스템을 등록하려면 struct file_system_type을 정의하고 register_filesystem()으로 전역 리스트에 추가합니다. 커널은 file_systems 단방향 연결 리스트로 등록된 모든 파일시스템 유형을 관리합니다.

/* include/linux/fs.h — file_system_type 핵심 필드 */
struct file_system_type {
    const char          *name;             /* "ext4", "xfs" 등 파일시스템 이름 */
    int                 fs_flags;          /* FS_REQUIRES_DEV, FS_USERNS_MOUNT 등 */

    /* 레거시 mount API (5.1 이전) */
    struct dentry *(*mount)(struct file_system_type *,
                            int, const char *, void *);
    void (*kill_sb)(struct super_block *); /* kill_block_super / kill_litter_super 등 */

    /* 새 mount API (5.2+, fs_context 기반) */
    int (*init_fs_context)(struct fs_context *);
    const struct fs_parameter_spec *parameters;  /* 마운트 옵션 명세 */

    struct module       *owner;            /* THIS_MODULE — 모듈 참조 카운트 */
    struct file_system_type *next;          /* file_systems 연결 리스트 */
    struct hlist_head   fs_supers;         /* 이 유형의 모든 superblock 리스트 */
};
/* fs/filesystems.c — register_filesystem() 내부 구현 */
static struct file_system_type *file_systems;  /* 전역 연결 리스트 헤드 */
static DEFINE_RWLOCK(file_systems_lock);

int register_filesystem(struct file_system_type *fs)
{
    struct file_system_type **p;
    BUG_ON(fs->next != NULL);

    write_lock(&file_systems_lock);
    /* 이름 중복 검사: 동일 이름이 이미 등록되면 -EBUSY */
    p = find_filesystem(fs->name, strlen(fs->name));
    if (*p)
        res = -EBUSY;
    else
        *p = fs;    /* 리스트 끝에 추가 */
    write_unlock(&file_systems_lock);
    return res;
}

/* find_filesystem: 이름으로 연결 리스트 순회 */
static struct file_system_type **find_filesystem(
    const char *name, unsigned len)
{
    struct file_system_type **p;
    for (p = &file_systems; *p; p = &(*p)->next)
        if (strncmp((*p)->name, name, len) == 0 &&
            !(*p)->name[len])
            break;
    return p;
}

/* unregister_filesystem: 리스트에서 제거 */
int unregister_filesystem(struct file_system_type *fs)
{
    struct file_system_type **tmp;
    write_lock(&file_systems_lock);
    tmp = &file_systems;
    while (*tmp) {
        if (*tmp == fs) {
            *tmp = fs->next;   /* 리스트에서 제거 */
            fs->next = NULL;
            write_unlock(&file_systems_lock);
            return 0;
        }
        tmp = &(*tmp)->next;
    }
    write_unlock(&file_systems_lock);
    return -EINVAL;
}
/* 파일시스템 등록 — 일반적인 패턴 */
static struct file_system_type myfs_type = {
    .owner    = THIS_MODULE,
    .name     = "myfs",
    .mount    = myfs_mount,       /* 레거시 API */
    .kill_sb  = kill_litter_super, /* 비디바이스 FS용 */
};

static int __init myfs_init(void)
{
    return register_filesystem(&myfs_type);
}

static void __exit myfs_exit(void)
{
    unregister_filesystem(&myfs_type);
}
fs_flags 주요 값:
  • FS_REQUIRES_DEV — 블록 디바이스(Block Device)가 필요한 디스크 기반 파일시스템 (ext4, XFS 등)
  • FS_BINARY_MOUNTDATA — 마운트 데이터가 문자열이 아닌 바이너리 (NFS)
  • FS_HAS_SUBTYPE — 하위 유형 지원 (FUSE: fuse.sshfs)
  • FS_USERNS_MOUNT — 비특권 사용자 네임스페이스에서 마운트 가능
  • FS_RENAME_DOES_D_MOVE — rename 시 VFS가 아닌 FS가 d_move() 수행
kill_sb 변형: kill_block_super()는 블록 디바이스 FS용, kill_litter_super()는 가상/비디바이스 FS용, kill_anon_super()는 익명 FS용입니다. 모두 내부에서 generic_shutdown_super()를 호출하여 dirty inode writeback → s_op->put_super() → superblock 해제를 수행합니다. /proc/filesystems에서 등록된 파일시스템 목록을 확인할 수 있으며, "nodev"가 표시된 것은 블록 디바이스가 불필요한 유형입니다.

페이지 캐시 (Page Cache)

페이지 캐시는 Linux 커널에서 디스크 기반 파일 데이터를 메모리에 캐싱하는 핵심 메커니즘입니다. 파일을 읽으면 디스크에서 가져온 데이터가 페이지 캐시에 저장되고, 이후 동일 데이터에 대한 읽기는 디스크 I/O 없이 메모리에서 직접 서비스됩니다. 쓰기 역시 먼저 페이지 캐시에 기록된 후 비동기적으로 디스크에 반영(writeback)됩니다.

참고: address_space 구조체(Struct), folio 기반 캐시, readahead 알고리즘, writeback 메커니즘, LRU 페이지(Page) 회수, 튜닝 가이드 등의 상세 내용은 페이지 캐시 (Page Cache) 전용 페이지를 참고하세요.

마운트 서브시스템

Linux의 마운트는 struct mountstruct vfsmount로 관리됩니다. 마운트 네임스페이스별로 독립된 마운트 트리를 가집니다.

/* 파일시스템 마운트 구현 (new mount API, 5.2+) */
static int myfs_get_tree(struct fs_context *fc)
{
    return get_tree_nodev(fc, myfs_fill_super);
}

static const struct fs_context_operations myfs_context_ops = {
    .get_tree = myfs_get_tree,
};

static int myfs_init_fs_context(struct fs_context *fc)
{
    fc->ops = &myfs_context_ops;
    return 0;
}

static struct file_system_type myfs_type = {
    .name              = "myfs",
    .init_fs_context   = myfs_init_fs_context,
    .kill_sb           = kill_litter_super,
};
/* fill_super 패턴 — 마운트 시 superblock 초기화의 핵심 */
static int myfs_fill_super(struct super_block *sb, struct fs_context *fc)
{
    struct inode *root_inode;

    /* 1. superblock 기본 설정 */
    sb->s_magic     = MYFS_MAGIC;
    sb->s_blocksize = PAGE_SIZE;
    sb->s_blocksize_bits = PAGE_SHIFT;
    sb->s_maxbytes  = MAX_LFS_FILESIZE;
    sb->s_op        = &myfs_super_ops;   /* super_operations 등록 */
    sb->s_time_gran = 1;                 /* 타임스탬프 정밀도 (나노초) */

    /* 2. FS별 private 데이터 할당 */
    sb->s_fs_info = kzalloc(sizeof(struct myfs_sb_info), GFP_KERNEL);
    if (!sb->s_fs_info)
        return -ENOMEM;

    /* 3. 루트 inode 생성 */
    root_inode = new_inode(sb);
    root_inode->i_ino  = 1;
    root_inode->i_mode = S_IFDIR | 0755;
    root_inode->i_op   = &myfs_dir_inode_ops;
    root_inode->i_fop  = &simple_dir_operations;
    set_nlink(root_inode, 2);

    /* 4. 루트 dentry 연결 */
    sb->s_root = d_make_root(root_inode);
    /* d_make_root: dentry 할당 + inode 연결
     * 실패 시 iput(root_inode)까지 자동 처리 */
    if (!sb->s_root)
        return -ENOMEM;

    return 0;
}
get_tree 변형: get_tree_bdev()는 블록 디바이스 기반 FS에서 struct block_device를 열고 superblock을 생성합니다. get_tree_nodev()는 디바이스 없는 가상 FS용, get_tree_single()은 전역 단일 인스턴스 FS(sysfs, proc 등)용입니다. 모두 내부에서 sget_fc()를 호출하여 기존 superblock 재사용 또는 신규 할당을 결정합니다.
레거시 mount vs 새 mount API: 커널 5.2 이전에는 .mount 콜백에서 mount_bdev()/mount_nodev()를 호출했습니다. 5.2+에서는 fs_context 기반 API(.init_fs_context)가 권장되며, 마운트 옵션 파싱(fs_parameter_spec)과 superblock 생성을 분리합니다. 레거시 .mount를 구현한 파일시스템은 legacy_init_fs_context()로 자동 래핑됩니다.

Direct I/O

Direct I/O는 페이지 캐시를 우회하여 유저 버퍼(Buffer)와 디스크 간에 직접 데이터를 전송합니다. 데이터베이스 등 자체 캐싱을 하는 어플리케이션에 유용합니다.

/* O_DIRECT로 열면 direct_IO가 호출됨 */
int fd = open("/data/file", O_RDWR | O_DIRECT);

/* 커널 내부: generic_file_read_iter()에서 분기 */
if (iocb->ki_flags & IOCB_DIRECT) {
    /* a_ops->direct_IO() 호출 */
    retval = mapping->a_ops->direct_IO(iocb, iter);
} else {
    /* 페이지 캐시 경유 읽기 */
    retval = filemap_read(iocb, iter, retval);
}

파일 잠금 (File Locking)

리눅스 커널은 여러 프로세스가 동일 파일에 동시 접근할 때 데이터 일관성을 보장하기 위해 VFS 계층에서 다양한 파일 잠금 메커니즘을 제공합니다. 잠금은 크게 POSIX 잠금(fcntl), BSD 잠금(flock), OFD 잠금(Open File Description) 세 가지로 구분됩니다.

Linux File Locking 계층 구조 flock(fd, op) BSD 잠금 (전체 파일) fcntl(fd, F_SETLK) POSIX 잠금 (바이트 범위) fcntl(fd, F_OFD_SETLK) OFD 잠금 (바이트 범위) VFS Lock Layer (fs/locks.c) struct file_lock → inode->i_flctx (file_lock_context) 로컬 FS (ext4, XFS, Btrfs) 네트워크 FS (NFS, CIFS) F_SETLEASE / F_NOTIFY (inode 수준 알림)

struct file_lock 커널 구조

모든 파일 잠금은 커널 내부에서 struct file_lock으로 표현되며, inode의 file_lock_context에 리스트로 관리됩니다.

/* include/linux/filelock.h — 파일 잠금 핵심 구조체 */
struct file_lock {
    struct file_lock  *fl_blocker;    /* 이 잠금을 차단 중인 잠금 */
    struct list_head  fl_list;        /* file_lock_context 리스트 연결 */
    struct list_head  fl_blocked_requests; /* 차단된 대기 요청 */
    struct list_head  fl_blocked_member;   /* blocker의 blocked_requests 멤버 */

    fl_owner_t        fl_owner;       /* POSIX: files_struct, OFD: struct file */
    unsigned int      fl_flags;       /* FL_POSIX, FL_FLOCK, FL_OFDLCK 등 */
    unsigned char     fl_type;        /* F_RDLCK, F_WRLCK, F_UNLCK */
    pid_t             fl_pid;         /* 잠금 소유 프로세스 PID */

    /* 바이트 범위 */
    loff_t            fl_start;       /* 잠금 시작 오프셋 */
    loff_t            fl_end;         /* 잠금 끝 오프셋 (OFFSET_MAX = EOF) */

    struct file       *fl_file;       /* 잠금이 설정된 struct file */
    unsigned int      fl_wait_time;   /* 대기 시간 통계 */

    const struct file_lock_operations  *fl_ops;  /* FS별 콜백 */
    const struct lock_manager_operations *fl_lmops; /* 잠금 관리자 (NFS 등) */

    union {
        struct nfs_lock_info   nfs_fl;   /* NFS 전용 */
        struct nfs4_lock_info  nfs4_fl;  /* NFSv4 전용 */
    } fl_u;
};

/* inode별 잠금 컨텍스트 — inode->i_flctx */
struct file_lock_context {
    struct list_head  flc_flock;   /* flock() 잠금 리스트 */
    struct list_head  flc_posix;   /* POSIX/OFD 잠금 리스트 */
    struct list_head  flc_lease;   /* lease 잠금 리스트 */
    spinlock_t        flc_lock;    /* 리스트 보호 스핀락 */
};
fl_flags 구분: 커널은 fl_flags 필드로 잠금 유형을 구분합니다. FL_POSIX(프로세스 기반 POSIX 잠금), FL_FLOCK(open file description 기반 BSD 잠금), FL_OFDLCK(OFD 잠금)가 있으며, 각각 충돌 검사 및 해제 규칙이 다릅니다. FL_LEASE는 lease 잠금을 나타냅니다.

POSIX 잠금 (fcntl — F_SETLK / F_SETLKW / F_GETLK)

POSIX 잠금은 fcntl() 시스템 콜로 바이트 범위 단위의 잠금을 설정합니다. 잠금의 소유 단위는 프로세스(PID)이며, 같은 프로세스 내에서는 여러 fd에서 설정한 잠금이 하나로 병합됩니다.

#include <fcntl.h>

struct flock fl = {
    .l_type   = F_WRLCK,     /* F_RDLCK(공유), F_WRLCK(배타), F_UNLCK(해제) */
    .l_whence = SEEK_SET,    /* 오프셋 기준: SEEK_SET, SEEK_CUR, SEEK_END */
    .l_start  = 0,           /* 잠금 시작 오프셋 */
    .l_len    = 0,           /* 0 = 파일 끝까지 (현재+미래 영역 포함) */
    .l_pid    = 0,           /* F_GETLK 시 커널이 채움 */
};

/* F_SETLK: 비차단 — 충돌 시 즉시 EAGAIN 반환 */
int ret = fcntl(fd, F_SETLK, &fl);

/* F_SETLKW: 차단 — 잠금 획득까지 대기 (인터럽트 가능, EINTR) */
int ret = fcntl(fd, F_SETLKW, &fl);

/* F_GETLK: 충돌하는 잠금 조회 — 없으면 fl.l_type = F_UNLCK */
fcntl(fd, F_GETLK, &fl);
if (fl.l_type != F_UNLCK)
    printf("PID %d가 잠금 보유\\n", fl.l_pid);
POSIX 잠금의 치명적 함정: POSIX 잠금은 프로세스 기반이므로, 같은 파일을 여러 fd로 열어도 잠금이 공유됩니다. 어떤 fd라도 close()하면 해당 프로세스의 모든 POSIX 잠금이 해제됩니다. 이는 라이브러리가 내부적으로 같은 파일을 열고 닫으면 애플리케이션의 잠금이 예기치 않게 해제되는 심각한 문제를 초래합니다.
/* POSIX 잠금의 위험한 동작 — close()에 의한 잠금 해제 */
int fd1 = open("data.db", O_RDWR);
int fd2 = open("data.db", O_RDWR);

/* fd1에 배타 잠금 설정 */
struct flock fl = { .l_type = F_WRLCK, .l_whence = SEEK_SET };
fcntl(fd1, F_SETLK, &fl);   /* 잠금 획득 성공 */

/* fd2를 닫으면... fd1의 잠금도 해제됨! */
close(fd2);  /* ⚠ fd1의 POSIX 잠금이 함께 해제됨 */

/* 커널 내부 (fs/locks.c):
 * locks_remove_posix() → 같은 fl_owner(files_struct)의
 * 모든 POSIX 잠금을 순회하며 제거 */
/* fs/locks.c — POSIX 잠금 설정 핵심 경로 */
int posix_lock_file(struct file *filp,
                    struct file_lock *request,
                    struct file_lock *conflock)
{
    struct file_lock_context *ctx;
    struct file_lock *fl;

    ctx = locks_get_lock_context(file_inode(filp), request->fl_type);

    spin_lock(&ctx->flc_lock);
    /* flc_posix 리스트에서 같은 owner의 겹치는 잠금 검색 */
    list_for_each_entry(fl, &ctx->flc_posix, fl_list) {
        if (!posix_same_owner(fl, request))
            continue;
        /* 겹치는 범위: 병합, 분할, 또는 교체 */
        if (locks_overlap(fl, request)) {
            /* 기존 잠금과 새 잠금의 범위를 조정
             * - 완전 포함: 기존 잠금 교체
             * - 부분 겹침: 분할하여 최대 2개 잠금 생성
             * - 인접: 동일 타입이면 병합 */
        }
    }
    /* 다른 owner와 충돌 검사 */
    list_for_each_entry(fl, &ctx->flc_posix, fl_list) {
        if (posix_same_owner(fl, request))
            continue;
        if (locks_conflict(fl, request)) {
            /* 충돌: F_SETLK → -EAGAIN, F_SETLKW → 대기 */
        }
    }
    spin_unlock(&ctx->flc_lock);
}

BSD 잠금 (flock)

flock()파일 전체에 대한 잠금을 설정합니다. POSIX 잠금과 달리 소유 단위가 open file description(fd가 아닌 커널의 struct file)이므로, dup()/fork()로 복제된 fd는 같은 잠금을 공유하지만, 같은 파일을 별도로 open()하면 독립적인 잠금이 됩니다.

#include <sys/file.h>

int fd = open("data.db", O_RDWR);

/* 배타 잠금 (비차단) */
if (flock(fd, LOCK_EX | LOCK_NB) == -1) {
    if (errno == EWOULDBLOCK)
        fprintf(stderr, "다른 프로세스가 잠금 보유 중\\n");
}

/* 공유 잠금 (차단) */
flock(fd, LOCK_SH);   /* 배타 잠금 해제까지 대기 */

/* 잠금 해제 */
flock(fd, LOCK_UN);

/* 또는 fd를 close()하면 자동 해제 */
close(fd);
/* flock()과 dup()/fork()의 잠금 공유 */
int fd1 = open("data.db", O_RDWR);
flock(fd1, LOCK_EX);

int fd2 = dup(fd1);
/* fd2는 fd1과 같은 struct file → 같은 잠금 공유
 * fd2를 close()해도 fd1이 열려 있으면 잠금 유지 */
close(fd2);  /* 잠금 여전히 유지됨 (struct file 참조 카운트 > 0) */

int fd3 = open("data.db", O_RDWR);
/* fd3는 새로운 struct file → 독립적인 잠금
 * fd3로 LOCK_EX 시도하면 fd1의 잠금과 충돌 */
flock(fd3, LOCK_EX | LOCK_NB);  /* EWOULDBLOCK */
/* fs/locks.c — flock 커널 구현 */
SYSCALL_DEFINE2(flock, unsigned int, fd, unsigned int, cmd)
{
    struct file_lock fl = {
        .fl_file  = f.file,
        .fl_flags = FL_FLOCK,
        .fl_type  = (cmd & LOCK_SH) ? F_RDLCK : F_WRLCK,
        .fl_end   = OFFSET_MAX,  /* 항상 전체 파일 */
    };
    /* fl_owner는 struct file 포인터
     * → dup()/fork()로 공유된 fd는 같은 owner */
    return locks_lock_file_wait(f.file, &fl);
}
실용적 선택: 단순히 파일 전체를 잠그는 용도(PID 파일, 설정 파일 직렬화(Serialization) 등)에는 flock()이 POSIX 잠금보다 안전합니다. close()에 의한 예기치 않은 잠금 해제 문제가 없고, API가 단순합니다. 단, NFS에서는 flock()이 로컬에서만 동작하거나 POSIX 잠금으로 에뮬레이션될 수 있으므로 주의가 필요합니다.

OFD 잠금 (Open File Description Locks, Linux 3.15+)

OFD 잠금은 POSIX 잠금의 설계 결함을 해결하기 위해 Linux 3.15(2014)에 도입되었습니다. 바이트 범위 잠금을 지원하면서도 소유 단위가 프로세스가 아닌 open file description(struct file)입니다. 멀티스레드 프로그램에서 가장 권장되는 잠금 방식입니다.

#include <fcntl.h>

struct flock fl = {
    .l_type   = F_WRLCK,
    .l_whence = SEEK_SET,
    .l_start  = 0,
    .l_len    = 4096,    /* 처음 4KB 영역만 잠금 */
    .l_pid    = 0,       /* OFD 잠금에서는 반드시 0으로 설정 */
};

/* F_OFD_SETLK: 비차단 OFD 잠금 */
int ret = fcntl(fd, F_OFD_SETLK, &fl);

/* F_OFD_SETLKW: 차단 OFD 잠금 */
int ret = fcntl(fd, F_OFD_SETLKW, &fl);

/* F_OFD_GETLK: 충돌 잠금 조회 */
fcntl(fd, F_OFD_GETLK, &fl);
/* OFD 잠금의 핵심 장점: close() 안전성 */
int fd1 = open("data.db", O_RDWR);
int fd2 = open("data.db", O_RDWR);

/* fd1에 OFD 잠금 설정 */
struct flock fl = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_pid = 0 };
fcntl(fd1, F_OFD_SETLK, &fl);

/* fd2를 닫아도 fd1의 잠금은 유지됨! (POSIX 잠금과 다름) */
close(fd2);  /* ✓ fd1의 OFD 잠금 영향 없음 */

/* 멀티스레드에서도 안전 — 각 스레드가 독립 fd로 독립 잠금 가능 */
/* Thread A: fd_a = open(...); fcntl(fd_a, F_OFD_SETLK, ...);
 * Thread B: fd_b = open(...); fcntl(fd_b, F_OFD_SETLK, ...);
 * → 서로 다른 struct file → 독립적 잠금으로 충돌 검사 수행 */
/* fs/locks.c — OFD 잠금 커널 처리 */
static int do_lock_file_wait(struct file *filp,
                             unsigned int cmd,
                             struct file_lock *fl)
{
    /* OFD 잠금: fl_owner = struct file 포인터 */
    if (fl->fl_flags & FL_OFDLCK) {
        fl->fl_owner = filp;  /* ← POSIX는 current->files */
        fl->fl_pid = 0;       /* PID 무의미 */
    }
    /* 잠금 충돌 검사는 POSIX와 동일한 posix_lock_file() 사용
     * 단, owner 비교가 struct file 기반으로 변경됨 */
    return posix_lock_file(filp, fl, NULL);
}

잠금 유형 비교

파일 잠금 메커니즘 비교: POSIX vs BSD vs OFD POSIX (fcntl) BSD (flock) OFD (fcntl F_OFD_*) 1단계: 같은 파일 2번 open() fd1 = open("file") fd2 = open("file") 같은 PID → 같은 owner fd1 = open("file") fd2 = open("file") 다른 struct file → 다른 owner fd1 = open("file") fd2 = open("file") 다른 struct file → 다른 owner 2단계: fd1에 잠금 설정 fcntl(fd1, F_SETLK, WRLCK) ✓ 잠금 설정됨 (프로세스 레벨) flock(fd1, LOCK_EX) ✓ 잠금 설정됨 (fd1의 struct file) fcntl(fd1, F_OFD_SETLK) ✓ 잠금 설정됨 (fd1의 struct file) 3단계: close(fd2) 실행 close(fd2) ✗ fd1 잠금도 해제됨! (치명적 결함) close(fd2) ✓ fd1 잠금 유지 (다른 struct file) close(fd2) ✓ fd1 잠금 유지 (다른 struct file) 멀티 스레드: ✗ 위험 같은 PID → 잠금 충돌 △ 주의 전체 파일만 잠금 ✓ 안전 fd별 독립 잠금 권장: 멀티스레드 환경에서는 OFD 잠금, 단순 전체 파일 잠금에는 flock(), POSIX는 레거시 호환용
POSIX 잠금의 close() 해제 문제와 멀티스레드 안전성을 OFD/BSD 잠금과 비교한 시나리오입니다.
속성 POSIX (fcntl) BSD (flock) OFD (Linux 3.15+)
바이트 범위 지원 (l_start, l_len) 전체 파일만 지원 (l_start, l_len)
소유 단위 프로세스 (PID) open file description open file description
close() 동작 같은 inode의 아무 fd close 시 모든 잠금 해제 해당 struct file의 마지막 fd close 시 해제 해당 struct file의 마지막 fd close 시 해제
fork() 상속 상속되지 않음 (자식은 별도 프로세스) fd 복제로 잠금 공유 fd 복제로 잠금 공유
dup() 동작 같은 프로세스이므로 잠금 공유 같은 struct file이므로 잠금 공유 같은 struct file이므로 잠금 공유
멀티스레드 안전 위험 (프로세스 전체 공유) 주의 필요 안전 (fd별 독립 잠금)
NFS 지원 완전 지원 (NLM/NFSv4) 제한적 (로컬 또는 에뮬레이션) 완전 지원
데드락 감지 지원 (EDEADLK) 미지원 미지원
커널 플래그 FL_POSIX FL_FLOCK FL_OFDLCK

Lease 잠금 (F_SETLEASE)

Lease 잠금은 다른 프로세스가 파일을 열거나 truncate하려 할 때 잠금 보유자에게 시그널(기본 SIGIO)로 알림을 보내는 메커니즘입니다. 삼바(Samba) 서버의 oplock(Opportunistic Lock) 구현에 핵심적으로 사용되며, 로컬 파일의 변경 감시에도 활용됩니다.

#include <fcntl.h>
#include <signal.h>

static void lease_break_handler(int sig, siginfo_t *info, void *ctx)
{
    int fd = info->si_fd;     /* lease가 깨진 fd */
    /* 정리 작업 수행 후 lease 해제 */
    fcntl(fd, F_SETLEASE, F_UNLCK);
}

int main(void)
{
    struct sigaction sa = {
        .sa_sigaction = lease_break_handler,
        .sa_flags = SA_SIGINFO,
    };
    sigaction(SIGIO, &sa, NULL);

    int fd = open("data.db", O_RDWR);

    /* 시그널을 받을 프로세스 설정 */
    fcntl(fd, F_SETOWN, getpid());
    fcntl(fd, F_SETSIG, SIGIO);

    /* 쓰기 lease 설정 — 다른 프로세스가 open() 시 알림 */
    if (fcntl(fd, F_SETLEASE, F_WRLCK) == -1)
        perror("lease 설정 실패");

    /* 읽기 lease: 다른 프로세스의 쓰기용 open() 시 알림
     * 쓰기 lease: 다른 프로세스의 모든 open() 시 알림 */
}
/* fs/locks.c — lease break 커널 경로 */
int break_lease(struct inode *inode, unsigned int mode)
{
    struct file_lock_context *ctx = inode->i_flctx;
    struct file_lock *fl;
    int error = 0;

    percpu_down_read(&file_rwsem);
    spin_lock(&ctx->flc_lock);
    list_for_each_entry(fl, &ctx->flc_lease, fl_list) {
        /* 충돌하는 lease가 있으면 break 시그널 전송 */
        if (locks_conflict(fl, &breaker)) {
            /* lease 보유자에게 SIGIO 전송 */
            fl->fl_lmops->lm_break(fl);
            /* /proc/sys/fs/lease-break-time 초 동안 대기
             * (기본 45초) — 응답 없으면 lease 강제 해제 */
        }
    }
    spin_unlock(&ctx->flc_lock);
    percpu_up_read(&file_rwsem);
    return error;
}
lease-break-time: /proc/sys/fs/lease-break-time (기본 45초)은 lease 보유자가 lease를 해제하거나 다운그레이드할 때까지 대기하는 최대 시간입니다. 이 시간 내에 응답하지 않으면 커널이 lease를 강제 해제합니다. Samba 서버에서는 이 값을 클라이언트 응답 시간에 맞게 조정합니다.

Advisory vs Mandatory 잠금

리눅스의 파일 잠금은 기본적으로 advisory(권고)입니다. 잠금을 확인하지 않는 프로세스는 잠금된 파일에 자유롭게 읽기/쓰기할 수 있습니다. Mandatory(강제) 잠금은 커널이 모든 read()/write()에서 잠금을 강제하는 방식이지만, 리눅스에서는 Linux 5.15부터 완전히 제거되었습니다.

# Mandatory 잠금 (Linux 5.14 이하에서만 동작, 이후 제거됨)
# 1. 파일시스템을 -o mand 옵션으로 마운트
mount -o mand /dev/sda1 /mnt/data

# 2. 파일에 set-group-ID + group-execute 제거
chmod g+s,g-x /mnt/data/protected_file
# → 커널이 read()/write() 시 잠금을 강제 확인
Mandatory 잠금 제거 (Linux 5.15): Mandatory 잠금은 커널 5.15에서 완전히 제거되었습니다 (commit f7e33bdb). 제거 이유:
  • 정확한 구현이 불가능 — read()/write()를 잠금 검사와 원자적(Atomic)으로 수행할 수 없어 경쟁 조건(Race Condition) 존재
  • mmap()을 통한 우회가 가능 — mandatory 잠금이 설정된 파일도 mmap()으로 매핑(Mapping)하면 잠금 없이 접근 가능
  • 심각한 성능 저하 — 모든 I/O 경로에 잠금 검사 오버헤드(Overhead) 추가
  • DoS 공격 벡터 — 악의적인 프로세스가 잠금을 영구히 보유하여 다른 프로세스 차단 가능
새로운 코드에서는 advisory 잠금만 사용하십시오. 강제 잠금이 필요한 경우 애플리케이션 수준에서 구현하거나 F_SETLEASE를 활용하십시오.

데드락 감지

POSIX 잠금은 커널 수준의 데드락 감지를 지원합니다. F_SETLKW로 대기 시, 커널은 잠금 대기 그래프를 순회하여 순환 의존성을 감지하면 EDEADLK을 반환합니다.

/* fs/locks.c — POSIX 잠금 데드락 감지 */
static int posix_locks_deadlock(struct file_lock *caller_fl,
                                struct file_lock *block_fl)
{
    struct file_lock *fl;
    int count = 0;

    /* 잠금 대기 체인을 따라 순환 탐색
     * caller → block_fl의 owner → 그 owner가 대기 중인 잠금 → ...
     * 최종적으로 caller에 도달하면 데드락 */
next_task:
    if (posix_same_owner(fl, caller_fl))
        return 1;  /* 데드락 감지! → EDEADLK */
    /* 과도한 순회 방지: 최대 깊이 제한 */
    if (count++ > 10)
        return 1;  /* 보수적으로 데드락 판정 */
    goto next_task;
}
flock/OFD 데드락: flock()과 OFD 잠금은 커널 수준 데드락 감지를 지원하지 않습니다. 두 프로세스가 서로의 flock을 기다리면 영구히 교착 상태(Deadlock)에 빠집니다. 애플리케이션에서 LOCK_NB/F_OFD_SETLK(비차단(Non-blocking))와 타임아웃 로직을 조합하여 데드락을 방지해야 합니다.

/proc/locks — 잠금 모니터링

/proc/locks는 시스템의 모든 파일 잠금 상태를 실시간(Real-time)으로 보여줍니다.

# /proc/locks 출력 형식
cat /proc/locks
# 순번: 타입  방식     범위  PID  major:minor:inode  시작     끝
# 1: POSIX  ADVISORY  WRITE 1234 08:01:131073       0        EOF
# 2: FLOCK  ADVISORY  WRITE 5678 08:01:131074       0        EOF
# 3: OFDLCK ADVISORY  READ  9012 08:01:131075       0        4095
# 4: LEASE  ADVISORY  WRITE 3456 08:01:131076       0        EOF
# 5: POSIX  ADVISORY  READ  7890 08:01:131073       4096     8191
# 6: POSIX  ADVISORY  WRITE 1111 00:33:27 -> 1234   0        EOF
#    ↑ -> PID는 이 잠금이 PID 1234의 잠금에 의해 차단 중임을 의미
# 실용적인 잠금 디버깅 명령어

# 특정 파일의 잠금 상태 확인 (inode 번호로 필터)
stat -c '%i' /path/to/file  # inode 번호 확인
grep '08:01:131073' /proc/locks

# lslocks — 사람이 읽기 쉬운 형태로 잠금 표시 (util-linux)
lslocks
# COMMAND  PID   TYPE  SIZE MODE  M START   END PATH
# mysqld   1234  POSIX  16K WRITE 0 0       EOF /var/lib/mysql/ib_logfile0
# smbd     5678  FLOCK   4K WRITE 0 0       EOF /var/run/samba/locking.tdb

# 특정 프로세스의 잠금만 확인
lslocks -p 1234

# 잠금 차단 관계 추적 — /proc/locks의 "->" 마커 확인
grep '\->' /proc/locks
# blocked_lock -> blocking_pid 형태로 대기 중인 잠금 표시
잠금 문제 진단 흐름: 프로세스가 멈춘 것처럼 보일 때 — (1) lslocks로 잠금 상태 확인 → (2) /proc/locks에서 -> 마커로 차단 관계 파악 → (3) /proc/<pid>/stack으로 커널 콜스택 확인 (locks_lock_file_wait이 보이면 잠금 대기 중) → (4) 차단하는 프로세스의 /proc/<pid>/fd로 어떤 fd가 잠금을 보유하는지 확인.

파일 잠금 주요 함정과 모범 사례

함정 설명 해결책
POSIX close() 해제 같은 파일의 아무 fd를 close()하면 프로세스의 모든 POSIX 잠금이 해제됨 OFD 잠금 또는 flock() 사용
fork() 후 POSIX 잠금 자식 프로세스는 부모의 POSIX 잠금을 상속하지 않으나, flock/OFD는 fd 공유로 잠금이 공유됨 fork() 후 잠금 재설정 또는 CLOEXEC 사용
NFS flock() NFS에서 flock()은 서버에 전달되지 않거나 POSIX 잠금으로 에뮬레이션됨 NFS에서는 fcntl() POSIX/OFD 잠금 사용
잠금 없는 I/O Advisory 잠금은 잠금을 확인하지 않는 프로세스에 무력 모든 참여 프로세스가 동일한 잠금 프로토콜 사용 필수
잠금 범위 확장 l_len=0은 현재 EOF를 넘어 미래에 추가될 데이터까지 잠금 의도치 않은 범위 잠금 주의, 필요한 범위만 지정
POSIX vs flock 독립 POSIX 잠금과 flock()은 서로 충돌을 감지하지 않음 프로젝트 내에서 하나의 잠금 방식만 일관되게 사용
/* 모범 사례: OFD 잠금 기반 안전한 파일 갱신 패턴 */
int safe_update_file(const char *path,
                     const void *data, size_t len)
{
    int fd = open(path, O_RDWR);
    if (fd < 0) return -1;

    /* OFD 배타 잠금 (전체 파일) */
    struct flock fl = {
        .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_pid = 0,
    };
    if (fcntl(fd, F_OFD_SETLKW, &fl) == -1)
        goto err;

    /* 데이터 쓰기 */
    if (write(fd, data, len) != (ssize_t)len)
        goto err;

    /* 디스크에 동기화 */
    if (fsync(fd) == -1)
        goto err;

    /* close()가 OFD 잠금을 자동 해제 */
    close(fd);
    return 0;

err:
    close(fd);
    return -1;
}

dentry 캐시 (dcache) 상세

dcache는 경로명 → inode 매핑을 캐싱하여 디스크 접근을 최소화합니다. 해시 테이블(Hash Table)로 구현되며, dentry 상태에 따라 관리됩니다:

상태설명i_count
Used유효한 inode에 연결, 참조 중> 0
Unused유효하지만 참조 없음 (LRU)= 0
Negative존재하지 않는 경로 (negative lookup 캐싱)N/A
ℹ️

Negative dentry 캐싱은 존재하지 않는 파일에 대한 반복적인 stat() 호출의 비용을 줄입니다. /proc/sys/fs/dentry-state로 dcache 통계를 확인할 수 있습니다.

RCU-walk 경로 탐색

Linux 2.6.38에서 도입된 RCU-walk는 경로 탐색 시 dentry의 참조 카운트(Reference Count)를 증가시키지 않고 RCU로 보호합니다. 잠금 경쟁이 없어 멀티코어 확장성이 뛰어납니다.

/* 경로 탐색 모드 */
/* 1. RCU-walk (fast path): 잠금 없이 dcache 탐색 */
/*    - d_seq (seqcount)로 dentry 유효성 확인 */
/*    - 실패 시 ref-walk로 폴백 */

/* 2. ref-walk (slow path): dentry 참조 카운트 사용 */
/*    - d_lock spinlock 획득 필요 */
/*    - 항상 성공하지만 느림 */

/* fs/namei.c: path_lookupat() */
static int path_lookupat(struct nameidata *nd,
                         unsigned flags, struct path *path)
{
    /* 먼저 RCU-walk 시도 */
    err = link_path_walk(s, nd);
    if (!err && !(nd->flags & LOOKUP_RCU))
        err = complete_walk(nd);
    /* RCU-walk 실패 시 LOOKUP_REVAL로 ref-walk 재시도 */
}

open() 시스템 콜 흐름

파일을 여는 open() 시스템 콜은 VFS에서 가장 복잡한 경로 중 하나입니다. 경로 탐색, 권한 검사, inode 할당, struct file 생성, fd 테이블 등록까지 여러 단계를 거칩니다.

open() 시스템 콜 내부 흐름 sys_openat2() / sys_open() get_unused_fd_flags() -- fd 번호 할당 path_openat() -- 경로 탐색 + 파일 열기 link_path_walk() -> RCU-walk 우선, 실패 시 ref-walk may_open() -- DAC/MAC 권한 검사 + LSM 호출 O_CREAT: vfs_create() vfs_open() -- struct file 초기화 f_op->open(inode, file) 파일시스템별 open 콜백 (ext4_file_open 등) break_lease() -- lease 잠금 충돌 확인 fd_install(fd, file) -- fd 테이블에 등록, fd 반환
open() 시스템 콜의 VFS 내부 처리 흐름. 경로 탐색 -> 권한 검사 -> FS별 open -> fd 등록 순서로 진행됩니다.
/* fs/open.c — open 시스템 콜 핵심 경로 (축약) */
long do_sys_openat2(int dfd, const char __user *filename,
                    struct open_how *how)
{
    struct open_flags op;
    struct filename *tmp;
    int fd;

    /* 1. 플래그 검증 및 변환 */
    build_open_flags(how, &op);

    /* 2. 사용 가능한 fd 번호 할당 */
    fd = get_unused_fd_flags(how->flags);

    /* 3. 유저 공간 경로 문자열 복사 */
    tmp = getname(filename);

    /* 4. 경로 탐색 + 파일 열기 (핵심) */
    struct file *f = do_filp_open(dfd, tmp, &op);

    if (IS_ERR(f)) {
        put_unused_fd(fd);
        fd = PTR_ERR(f);
    } else {
        /* 5. struct file을 fd 테이블에 등록 */
        fd_install(fd, f);
    }
    return fd;
}

/* fs/namei.c — do_filp_open()에서 호출되는 핵심 함수 */
static struct file *path_openat(struct nameidata *nd,
                                const struct open_flags *op,
                                unsigned flags)
{
    struct file *file;

    /* struct file 할당 (슬랩 캐시: filp_cachep) */
    file = alloc_empty_file(op->open_flag, current_cred());

    /* 경로의 마지막 구성 요소 전까지 탐색 */
    const char *s = path_init(nd, flags);
    link_path_walk(s, nd);

    /* 마지막 구성 요소 처리: lookup 또는 create */
    do_open(nd, file, op);
    /* → may_open() 권한 검사
     * → vfs_open() → f_op->open() 콜백
     * → break_lease() 잠금 확인 */

    return file;
}
fd 테이블 구조: 프로세스의 fd 테이블은 struct files_struct로 관리됩니다. fd_install()current->files->fdt->fd[fd] = file 형태로 struct file 포인터를 저장합니다. 이 배열은 NR_OPEN_DEFAULT(64)로 시작하여 필요 시 동적으로 확장됩니다. 최대 fd 수는 /proc/sys/fs/nr_open(기본 1048576)과 ulimit으로 제한됩니다.

read/write 디스패치 경로

read()write() 시스템 콜은 VFS를 통해 파일시스템별 구현으로 디스패치됩니다. 현대 커널은 read_iter/write_iter(iov_iter 기반) 인터페이스를 우선 사용합니다.

/* fs/read_write.c — read() 시스템 콜 진입점 */
SYSCALL_DEFINE3(read, unsigned int, fd,
                char __user *, buf, size_t, count)
{
    struct fd f = fdget_pos(fd);
    /* fdget_pos: fd → struct file 조회 + 위치 잠금 */

    loff_t pos = file_pos_read(f.file);
    ret = vfs_read(f.file, buf, count, &pos);
    file_pos_write(f.file, pos);
    fdput_pos(f);
    return ret;
}

ssize_t vfs_read(struct file *file, char __user *buf,
                size_t count, loff_t *pos)
{
    /* 접근 권한 확인: 읽기 모드로 열렸는지 */
    if (!(file->f_mode & FMODE_READ))
        return -EBADF;

    /* LSM 보안 모듈 검사 */
    ret = rw_verify_area(READ, file, pos, count);

    /* 디스패치: read_iter → read → 순서로 시도 */
    if (file->f_op->read_iter) {
        /* iov_iter 기반 — 현대 커널의 표준 경로 */
        ret = new_sync_read(file, buf, count, pos);
    } else if (file->f_op->read) {
        /* 레거시 read 콜백 */
        ret = file->f_op->read(file, buf, count, pos);
    }
    return ret;
}

/* new_sync_read: read_iter 래퍼 */
static ssize_t new_sync_read(struct file *filp,
                             char __user *buf,
                             size_t len, loff_t *ppos)
{
    struct iov_iter iter;
    struct kiocb kiocb;

    init_sync_kiocb(&kiocb, filp);
    kiocb.ki_pos = *ppos;
    iov_iter_ubuf(&iter, ITER_DEST, buf, len);

    /* 파일시스템의 read_iter 콜백 호출 */
    ret = call_read_iter(filp, &kiocb, &iter);
    /* → generic_file_read_iter() (대부분의 FS)
     *   → filemap_read()          (페이지 캐시 읽기)
     *   → a_ops->readahead()      (미리 읽기) */
    *ppos = kiocb.ki_pos;
    return ret;
}
iov_iter 통합: read_iter/write_iter 인터페이스는 struct iov_iter를 통해 다양한 버퍼 유형(유저 버퍼, 커널 버퍼, bvec, pipe, xarray)을 통일적으로 처리합니다. readv()/writev()(scatter-gather I/O), splice(), io_uring 등 모든 I/O 경로가 이 인터페이스를 공유합니다.
/* write() 경로에서 핵심적인 차이점 */
ssize_t vfs_write(struct file *file, const char __user *buf,
                 size_t count, loff_t *pos)
{
    /* 쓰기 모드 확인 */
    if (!(file->f_mode & FMODE_WRITE))
        return -EBADF;

    /* immutable 파일 검사 */
    if (IS_IMMUTABLE(file_inode(file)))
        return -EPERM;

    /* 파일 크기 제한 (RLIMIT_FSIZE) 검사 */
    ret = rw_verify_area(WRITE, file, pos, count);

    /* 쓰기 권한 획득: sb_start_write() (freeze 보호) */
    file_start_write(file);

    if (file->f_op->write_iter)
        ret = new_sync_write(file, buf, count, pos);
        /* → generic_file_write_iter()
         *   → generic_perform_write()
         *   → a_ops->write_begin() / a_ops->write_end()
         *   → 페이지 캐시에 기록, dirty 마킹 */

    file_end_write(file);
    return ret;
}
sb_start_write / file_start_write: 모든 쓰기 연산은 sb_start_write()로 superblock의 freeze 카운터를 확인합니다. 파일시스템이 freeze 상태(백업 등)이면 쓰기가 차단됩니다. fsfreeze --freeze /mnt 명령으로 트리거되며, 이 동안 모든 쓰기 요청은 thaw될 때까지 대기합니다.

권한 및 보안 검사

VFS는 파일 접근 시 여러 계층의 보안 검사를 수행합니다. DAC(Discretionary Access Control), ACL, capability, LSM(Linux Security Module) 순서로 검사가 진행됩니다.

/* fs/namei.c — 권한 검사 핵심 흐름 */
int inode_permission(struct mnt_idmap *idmap,
                     struct inode *inode, int mask)
{
    int retval;

    /* 1. superblock 수준 읽기 전용 검사 */
    if (sb_permission(inode->i_sb, inode, mask))
        return -EACCES;

    /* 2. DAC: POSIX 퍼미션 (rwxrwxrwx) 검사 */
    if (inode->i_op->permission)
        retval = inode->i_op->permission(idmap, inode, mask);
    else
        retval = generic_permission(idmap, inode, mask);

    /* 3. LSM: SELinux, AppArmor, Smack 등의 MAC 검사 */
    if (!retval)
        retval = security_inode_permission(inode, mask);

    return retval;
}

/* generic_permission: 표준 POSIX 퍼미션 검사 */
int generic_permission(struct mnt_idmap *idmap,
                      struct inode *inode, int mask)
{
    /* (a) owner 확인 → user 비트(rwx) 검사 */
    /* (b) group 확인 → group 비트 검사 */
    /* (c) 기타 → other 비트 검사 */

    /* ACL 검사 (POSIX ACL이 설정된 경우) */
    ret = check_acl(idmap, inode, mask);
    if (ret != -EAGAIN)
        return ret;

    /* capability 검사: CAP_DAC_OVERRIDE, CAP_DAC_READ_SEARCH */
    if (capable_wrt_inode_uidgid(idmap, inode, CAP_DAC_OVERRIDE))
        return 0;

    return -EACCES;
}
검사 계층 메커니즘 설정 방법 검사 함수
DAC POSIX 퍼미션 (rwx) chmod, chown generic_permission()
POSIX ACL 확장 ACL 엔트리 setfacl check_acl()
Capability 세분화된 권한 setcap, prctl capable_wrt_inode_uidgid()
LSM SELinux/AppArmor 정책 파일 security_inode_permission()
Mount 읽기 전용(Read-Only), noexec, nosuid mount -o sb_permission()
idmapped mounts (5.12+): struct mnt_idmap은 Linux 5.12에서 도입된 ID 매핑 마운트를 지원합니다. 컨테이너(Container) 환경에서 호스트와 다른 UID/GID 네임스페이스(Namespace)를 사용할 때, 파일시스템 자체를 변경하지 않고 마운트 수준에서 UID/GID를 매핑합니다. 권한 검사 시 mnt_idmap을 통해 매핑된 ID로 변환한 뒤 비교합니다. 자세한 내용은 네임스페이스 문서를 참고하세요.

확장 속성 (Extended Attributes)

확장 속성(xattr)은 파일/디렉토리에 추가 메타데이터를 key-value 쌍으로 저장하는 메커니즘입니다. SELinux 레이블, POSIX ACL, 사용자 정의 메타데이터 등이 xattr로 구현됩니다.

/* include/linux/xattr.h — xattr 네임스페이스 접두사 */
#define XATTR_SECURITY_PREFIX  "security."   /* SELinux, Smack 등 */
#define XATTR_SYSTEM_PREFIX    "system."     /* POSIX ACL 등 */
#define XATTR_TRUSTED_PREFIX   "trusted."    /* 관리자 전용 */
#define XATTR_USER_PREFIX      "user."       /* 일반 사용자 */

/* 대표적인 xattr 키 */
#define XATTR_NAME_POSIX_ACL_ACCESS  "system.posix_acl_access"
#define XATTR_NAME_POSIX_ACL_DEFAULT "system.posix_acl_default"
#define XATTR_NAME_SELINUX           "security.selinux"
#define XATTR_NAME_CAPS              "security.capability"
/* VFS xattr 핸들러 구조체 */
struct xattr_handler {
    const char *name;       /* 정확한 이름 매칭 (name이 설정된 경우) */
    const char *prefix;     /* 접두사 매칭 (prefix가 설정된 경우) */
    int flags;              /* 핸들러별 플래그 */
    int (*get)(const struct xattr_handler *,
              struct dentry *, struct inode *,
              const char *, void *, size_t);
    int (*set)(const struct xattr_handler *,
              struct mnt_idmap *, struct dentry *,
              struct inode *, const char *,
              const void *, size_t, int);
};

/* 파일시스템은 s_xattr 배열로 핸들러를 등록 */
static const struct xattr_handler *ext4_xattr_handlers[] = {
    &ext4_xattr_user_handler,
    &ext4_xattr_trusted_handler,
    &ext4_xattr_security_handler,
    NULL,
};
# xattr 사용자 공간 명령어

# 사용자 확장 속성 설정
setfattr -n user.description -v "프로젝트 설정 파일" config.yaml

# 확장 속성 조회
getfattr -n user.description config.yaml
# file: config.yaml
# user.description="프로젝트 설정 파일"

# 모든 확장 속성 나열
getfattr -d -m '.*' config.yaml

# SELinux 컨텍스트 확인 (security.selinux xattr)
getfattr -n security.selinux /etc/passwd

# POSIX ACL 확인 (system.posix_acl_access xattr)
getfacl /var/log/syslog
xattr 크기 제한: xattr의 저장 방식은 파일시스템마다 다릅니다. ext4는 inode 내 여유 공간(인라인)이나 별도의 xattr 블록을 사용하며, 단일 xattr의 최대 크기는 블록 크기에 의해 제한됩니다(4KB 블록 시 약 4KB). Btrfs는 리프 노드에 저장하며 상대적으로 제한이 적습니다. XFS는 별도의 속성 B+tree를 사용하여 대용량 xattr도 지원합니다.

statx() — 확장 파일 정보 조회

statx()(Linux 4.11+)는 기존 stat()/fstat()을 대체하는 확장 파일 정보 조회 시스템 콜입니다. 필요한 필드만 선택적으로 요청하여 성능을 최적화하고, 기존 stat에 없던 생성 시간(birth time), 속성 플래그, 마운트 ID 등의 정보를 제공합니다.

/* include/uapi/linux/stat.h — struct 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 블록 수 */

    /* 타임스탬프 (나노초 정밀도) */
    struct statx_timestamp  stx_atime;   /* 최종 접근 */
    struct statx_timestamp  stx_btime;   /* 생성 시간 (birth) */
    struct statx_timestamp  stx_ctime;   /* 최종 변경 (메타) */
    struct statx_timestamp  stx_mtime;   /* 최종 수정 (데이터) */

    __u32  stx_rdev_major;  /* 디바이스 major */
    __u32  stx_rdev_minor;  /* 디바이스 minor */
    __u32  stx_dev_major;   /* 파일시스템 디바이스 major */
    __u32  stx_dev_minor;   /* 파일시스템 디바이스 minor */
    __u64  stx_mnt_id;      /* 마운트 ID (5.8+) */

    __u64  stx_attributes_mask; /* 지원되는 속성 마스크 */
};
/* statx() 사용 예시 */
#include <sys/stat.h>
#include <fcntl.h>

struct statx stx;

/* 특정 필드만 요청하여 효율적 조회 */
int ret = statx(AT_FDCWD, "/path/to/file",
               AT_SYMLINK_NOFOLLOW,
               STATX_SIZE | STATX_BTIME | STATX_MNT_ID,
               &stx);

if (ret == 0) {
    printf("크기: %llu\\n", stx.stx_size);

    /* 생성 시간 (btime) — stat()에서는 불가능 */
    if (stx.stx_mask & STATX_BTIME)
        printf("생성: %lld.%09u\\n",
               stx.stx_btime.tv_sec, stx.stx_btime.tv_nsec);

    /* 마운트 ID (mount namespace 내 고유 식별자) */
    if (stx.stx_mask & STATX_MNT_ID)
        printf("마운트 ID: %llu\\n", stx.stx_mnt_id);

    /* 파일 속성: 압축, 암호화, immutable 등 */
    if (stx.stx_attributes & STATX_ATTR_COMPRESSED)
        printf("압축된 파일\\n");
    if (stx.stx_attributes & STATX_ATTR_ENCRYPTED)
        printf("암호화된 파일\\n");
    if (stx.stx_attributes & STATX_ATTR_VERITY)
        printf("fsverity 보호 파일\\n");
}
stat() vs statx() 차이: stat()은 항상 모든 필드를 채우므로 불필요한 디스크 접근이 발생할 수 있습니다(예: atime 갱신을 위한 inode 읽기). statx()mask 파라미터로 필요한 필드만 요청하여 파일시스템이 불필요한 작업을 건너뛸 수 있습니다. 또한 stx_btime(생성 시간)은 stat()에서는 전혀 얻을 수 없으며, ext4, XFS, Btrfs 등이 지원합니다.

splice() / sendfile() — 제로 카피 전송

splice()sendfile()는 유저 공간 버퍼를 거치지 않고 커널 내부에서 파이프/소켓(Socket)과 파일 간 데이터를 직접 전송하는 제로 카피(zero-copy) 메커니즘입니다.

/* splice: 파이프를 매개로 두 fd 간 제로 카피 전송 */
#include <fcntl.h>

int pipefd[2];
pipe(pipefd);

/* 파일 → 파이프 (파일 데이터를 파이프에 연결) */
ssize_t n = splice(file_fd, &file_off,
                   pipefd[1], NULL,
                   len, SPLICE_F_MOVE | SPLICE_F_MORE);

/* 파이프 → 소켓 (네트워크 전송) */
splice(pipefd[0], NULL,
       socket_fd, NULL,
       n, SPLICE_F_MOVE | SPLICE_F_MORE);

/* sendfile: splice의 편의 래퍼 (내부적으로 splice 사용) */
sendfile(socket_fd, file_fd, &offset, count);
/* 웹 서버(nginx, Apache)에서 정적 파일 전송에 핵심 사용 */
/* fs/splice.c — VFS splice 핵심 경로 */
long do_splice(struct file *in, loff_t *off_in,
              struct file *out, loff_t *off_out,
              size_t len, unsigned int flags)
{
    if (ipipe && opipe) {
        /* pipe → pipe: 직접 연결 */
        return splice_pipe_to_pipe(ipipe, opipe, len, flags);
    }
    if (ipipe) {
        /* pipe → file: f_op->splice_write() */
        return do_splice_from(ipipe, out, off_out, len, flags);
    }
    if (opipe) {
        /* file → pipe: f_op->splice_read() */
        return do_splice_to(in, off_in, opipe, len, flags);
        /* → generic_file_splice_read()
         *   → 페이지 캐시 페이지를 파이프 버퍼에 직접 연결
         *   → 데이터 복사 없이 페이지 참조만 전달 */
    }
}
copy_file_range (4.5+): copy_file_range()는 같은 파일시스템 내에서 서버 측 복사(reflink)를 수행할 수 있습니다. Btrfs, XFS(reflink 활성 시)에서는 실제 데이터 복사 없이 extent 참조만 공유하여 즉시 완료됩니다. 파일시스템이 reflink을 지원하지 않으면 커널이 페이지 캐시를 통한 일반 복사로 폴백합니다.

저널링과 데이터 일관성 (Journaling & Consistency)

저널링 모드

모드저널 대상안전성성능ext4 옵션
journal 메타데이터 + 데이터 최고 (완전한 원자적 쓰기) 낮음 (모든 데이터 이중 기록) data=journal
ordered (기본) 메타데이터만 높음 (데이터 먼저 기록 후 메타데이터 커밋) 중간 data=ordered
writeback 메타데이터만 낮음 (데이터와 메타데이터 순서 무보장) 높음 data=writeback

fsync, fdatasync, barrier

/* fsync 호출 시 커널 내부 흐름 */
/*
 * 1. 더티 페이지 캐시 → 디스크 writeback 시작
 * 2. 저널 커밋 (메타데이터 일관성)
 * 3. 블록 디바이스에 flush/FUA 명령 전송
 * 4. 디스크 캐시 → 영구 저장 매체에 도달 보장
 */

/* file_operations의 fsync 구현 */
static int ext4_sync_file(struct file *file,
        loff_t start, loff_t end, int datasync)
{
    /* datasync=1이면 fdatasync → 메타데이터 중 크기/데이터 변경만 */
    /* datasync=0이면 fsync → 모든 메타데이터(mtime, 권한 등) 포함 */
    filemap_write_and_wait_range(mapping, start, end);
    return ext4_force_commit(inode->i_sb);
}

/* barrier — 디스크 쓰기 순서 보장 */
/* mount -o barrier (기본 활성) / nobarrier (위험!) */
/* 비활성화 시: 정전으로 저널이 손상될 수 있음 */
/* 배터리 백업 캐시(BBU) 있는 RAID 컨트롤러에서만 nobarrier 고려 */
fsync 성능 함정:
  • fsync storm — 많은 프로세스가 동시에 fsync하면 저널 커밋이 직렬화되어 I/O 병목(Bottleneck). 특히 데이터베이스(WAL 파일)에서 문제
  • rename + fsync 패턴 — 안전한 파일 교체: write(tmp) → fsync(tmp) → rename(tmp, target) → fsync(dir). 디렉토리 fsync 누락 시 rename이 유실될 수 있음
  • EXT4 delayed allocation — 데이터가 디스크에 기록되기 전에 크래시하면 파일 크기만 늘어나고 내용이 0으로 채워질 수 있음 (data=ordered가 이를 방지)

Writeback 메커니즘

/* 더티 페이지 → 디스크 기록 경로 */
/*
 * 1. write() → 페이지 캐시에 기록 → 페이지를 dirty 마킹
 * 2. 주기적 writeback (dirty_writeback_centisecs = 500 = 5초)
 *    또는 dirty 비율 임계치 초과 시 background writeback
 * 3. flusher 스레드가 address_space_operations->writepages() 호출
 * 4. 블록 I/O 계층 → 디스크
 */

/* 핵심 writeback 파라미터 */
/* /proc/sys/vm/ */
dirty_ratio = 20              /* 전체 메모리 대비 dirty 비율 임계치 (동기 쓰기) */
dirty_background_ratio = 10  /* background writeback 시작 임계치 */
dirty_expire_centisecs = 3000 /* 30초 이상 dirty인 페이지 강제 기록 */
dirty_writeback_centisecs = 500 /* flusher 주기 (5초) */

Extent 기반 할당 (ext4, XFS, Btrfs)

파일시스템할당 방식최대 extent 크기특징
ext4 Extent tree (최대 4 depth) 128MB (단일 extent) delayed allocation, fallocate, bigalloc 지원
XFS B+ tree extent map 8EB (이론적) 실시간 서브볼륨, reflinking(CoW), reverse mapping
Btrfs CoW B-tree 128MB (기본) 스냅샷, 압축, 체크섬(Checksum), RAID, 서브볼륨

파일시스템 성능 고려사항

파일시스템 성능 최적화 체크리스트:
  • 마운트 옵션noatime (읽기 시 atime 갱신 비활성화 → 메타데이터 쓰기 감소), discard/fstrim (SSD TRIM)
  • 블록 크기 — 대용량 순차 I/O에서는 4KB보다 큰 블록이 유리하나 소파일 많으면 공간 낭비
  • I/O 스케줄러(Scheduler) — SSD: none 또는 mq-deadline, HDD: bfq
  • Direct I/O vs Buffered I/O — DB처럼 자체 캐시가 있는 경우 O_DIRECT로 이중 캐싱 회피
  • fallocate — 사전 할당으로 단편화(Fragmentation) 방지. DB 파일, 로그 파일에 효과적
  • 디렉토리 인덱싱 — ext4 dir_index (기본 활성), XFS는 B+ tree 디렉토리. 수만 파일 디렉토리에서 중요
  • 저널 크기 — 대용량 쓰기 워크로드에서 저널이 작으면 빈번한 커밋으로 성능 저하. mkfs.ext4 -J size=

VFS 관련 주요 버그 사례

VFS는 리눅스 커널에서 가장 오래되고 복잡한 서브시스템 중 하나이며, 역사적으로 심각한 보안 취약점(Vulnerability)과 데이터 무결성(Integrity) 결함이 발견되어 왔습니다. 아래는 커널 개발자가 반드시 알아야 할 주요 사례입니다.

보안 경고: 심볼릭 링크를 통한 TOCTOU(Time-of-Check to Time-of-Use) 공격은 로컬 권한 상승의 대표적인 벡터입니다. /tmp와 같은 공유 writable 디렉토리에서 특히 위험하며, 공격자가 심볼릭 링크 해석과 실제 파일 접근 사이의 시간 차를 악용하여 임의 파일에 접근할 수 있습니다.

TOCTOU 경쟁 조건은 파일 경로를 확인(check)하는 시점과 실제 사용(use)하는 시점 사이에 심볼릭 링크가 변경될 수 있다는 점을 악용합니다. 예를 들어, 권한 있는 프로세스가 /tmp/myapp.log를 열기 전에 공격자가 이를 /etc/shadow로의 심볼릭 링크로 교체하면 민감한 파일이 덮어씌워질 수 있습니다.

커널은 이를 방어하기 위해 다음 메커니즘을 제공합니다:

/* 유저스페이스 방어: O_NOFOLLOW와 openat() 사용 */
#include <fcntl.h>

/* 심볼릭 링크를 따라가지 않음 — 심볼릭 링크이면 ELOOP 반환 */
int fd = open("/tmp/myapp.log", O_WRONLY | O_CREAT | O_NOFOLLOW, 0644);

/* openat()으로 디렉토리 기준 상대 경로 사용 — TOCTOU 창 최소화 */
int dirfd = open("/tmp", O_RDONLY | O_DIRECTORY);
int fd = openat(dirfd, "myapp.log", O_WRONLY | O_CREAT | O_NOFOLLOW, 0644);
/* 커널 내부: namei.c의 심볼릭 링크 탐색 제어 플래그 */
#define LOOKUP_FOLLOW         0x0001  /* 마지막 컴포넌트에서 심볼릭 링크 따라감 */
#define LOOKUP_NO_SYMLINKS    0x010000 /* 경로 전체에서 심볼릭 링크 금지 */

/* AT_SYMLINK_NOFOLLOW: fstatat(), linkat() 등에서 심볼릭 링크 미추적 */
fstatat(dirfd, "target", &st, AT_SYMLINK_NOFOLLOW);
sysctl 보호: fs.protected_symlinks = 1 (기본값)은 sticky 비트가 설정된 디렉토리(예: /tmp)에서 심볼릭 링크 소유자가 아닌 사용자가 해당 링크를 따라가는 것을 차단합니다. 이는 커널 3.6에서 도입되었으며, 대부분의 TOCTOU 공격을 효과적으로 완화합니다.

fsync() 에러 처리 결함 (2018)

데이터 무결성 위험: 2018년까지 리눅스 커널의 writeback 에러 처리에는 심각한 결함이 있었습니다. fsync()가 writeback 에러를 보고한 후, 같은 파일 디스크립터(File Descriptor)에서 다시 fsync()를 호출하면 에러가 이미 소비되어 두 번째 호출은 성공(0)을 반환했습니다. 이로 인해 PostgreSQL 등의 데이터베이스에서 데이터가 디스크에 기록되었다고 잘못 판단하는 심각한 무결성 문제가 발생했습니다.

문제의 핵심은 AS_EIO/AS_ENOSPC 플래그가 address_space 전역 상태로 관리되어, 한 번 확인하면 클리어되는 구조였습니다. 여러 파일 디스크립터가 동일 inode를 참조할 때, 하나의 fd에서 에러를 확인하면 다른 fd에서는 에러를 감지할 수 없었습니다.

/* 수정 전: 에러 플래그가 전역적으로 클리어됨 (문제 코드) */
static int filemap_check_errors(struct address_space *mapping)
{
    int ret = 0;
    /* test_and_clear — 한 번 확인하면 에러가 사라짐 */
    if (test_and_clear_bit(AS_EIO, &mapping->flags))
        ret = -EIO;
    if (test_and_clear_bit(AS_ENOSPC, &mapping->flags))
        ret = -ENOSPC;
    return ret;
}
/* 수정 후: errseq_t 기반 에러 추적 (commit 735e4ae5ba28) */
typedef struct {
    u32 counter;  /* 상위 비트: 에러 코드, 하위 비트: 시퀀스 번호 */
} errseq_t;

/* 각 fd가 마지막으로 관찰한 에러 시퀀스를 기록 */
struct file {
    /* ... */
    errseq_t  f_wb_err;  /* 이 fd가 마지막으로 본 writeback 에러 시퀀스 */
};

/* errseq_check_and_advance: fd별로 새로운 에러만 보고 */
int errseq_check_and_advance(errseq_t *eseq, errseq_t *since)
{
    errseq_t old = READ_ONCE(*eseq);
    /* since 이후 새로운 에러가 있으면 보고하고 시퀀스 전진 */
    if (errseq_sample(old) != errseq_sample(*since)) {
        *since = old;
        return -(old >> ERRSEQ_SHIFT);
    }
    return 0;
}
교훈: errseq_t 도입 이후에도 애플리케이션은 fsync() 반환값을 반드시 확인해야 합니다. 에러 발생 시 파일을 닫고 다시 여는 것만으로는 에러 상태가 해소되지 않으며, 데이터를 다시 써야 합니다. 데이터베이스 개발자는 WAL(Write-Ahead Log) 전략과 함께 fsync() 에러를 치명적 오류로 처리해야 합니다.

O_DIRECT와 버퍼 I/O 혼합 시 데이터 손상

데이터 손상 위험: 같은 파일에 대해 O_DIRECT(직접 I/O)와 일반 buffered I/O를 동시에 사용하면 페이지 캐시와 디스크 데이터 간 일관성이 파괴됩니다. 커널은 이 혼합 사용을 경고하거나 방지하지 않으며, 이는 미정의 동작(undefined behavior)으로 분류됩니다.

O_DIRECT는 페이지 캐시를 우회하여 디스크에 직접 읽고 쓰는 반면, buffered I/O는 페이지 캐시를 통해 데이터를 관리합니다. 두 경로가 동시에 동작하면 다음과 같은 시나리오가 발생합니다:

/* 위험한 혼합 사용 예시 — 절대 이렇게 하지 마십시오 */

/* 프로세스 A: buffered write → 데이터가 페이지 캐시에 저장 */
int fd_buf = open("data.db", O_WRONLY);
write(fd_buf, buf_a, 4096);  /* 페이지 캐시에 기록 (dirty page) */

/* 프로세스 B: O_DIRECT read → 디스크에서 직접 읽음 */
int fd_dio = open("data.db", O_RDONLY | O_DIRECT);
read(fd_dio, buf_b, 4096);   /* 디스크의 오래된 데이터를 읽음! */

/* 프로세스 C: O_DIRECT write → 디스크에 직접 기록 */
int fd_dio2 = open("data.db", O_WRONLY | O_DIRECT);
write(fd_dio2, buf_c, 4096); /* 디스크에 기록, 페이지 캐시는 여전히 구 데이터 */

/* 이후 buffered read → 페이지 캐시의 stale 데이터 반환! */
read(fd_buf, buf_d, 4096);   /* 프로세스 C의 쓰기가 보이지 않음 */
데이터베이스의 O_DIRECT 전용 사용 이유: PostgreSQL, MySQL(InnoDB), Oracle 등의 데이터베이스는 자체 버퍼 풀을 관리하므로 커널 페이지 캐시와의 이중 캐싱을 피하기 위해 O_DIRECT를 사용합니다. 이 경우 모든 I/O를 반드시 O_DIRECT로 통일해야 하며, 같은 파일에 buffered I/O를 혼합하면 데이터 손상이 발생할 수 있습니다. 파일시스템에 따라 O_DIRECT 쓰기 시 invalidate_inode_pages2()로 페이지 캐시를 무효화(Invalidation)하려 시도하지만, 이는 완전한 보호를 보장하지 않습니다.

경로 탐색(Path Lookup) 성능 버그

VFS의 경로 탐색(path lookup)은 /usr/local/bin/program과 같은 경로를 구성 요소별로 분해하여 최종 dentry/inode에 도달하는 과정입니다. 이 과정에서 심볼릭 링크 해석, 마운트 포인트 횡단 등이 발생하며, 깊은 중첩이나 과도한 심볼릭 링크에서 심각한 성능 및 안정성 문제가 발생할 수 있습니다.

무한 루프 방지: 심볼릭 링크가 서로를 가리키는 순환 참조가 존재하면, LOOKUP_FOLLOW 플래그를 사용하는 경로 탐색이 무한 루프에 빠질 수 있습니다. 이를 방지하기 위해 커널은 MAXSYMLINKS (40)를 도입하여 단일 경로 탐색에서 따라갈 수 있는 심볼릭 링크 수를 제한합니다. 이 한계를 초과하면 ELOOP 에러가 반환됩니다.
/* include/linux/namei.h */
#define MAXSYMLINKS 40  /* 단일 경로 탐색에서 최대 심볼릭 링크 추적 횟수 */

/* fs/namei.c: 경로 탐색의 심볼릭 링크 추적 루프 */
static int trailing_symlink(struct nameidata *nd)
{
    nd->total_link_count++;
    if (nd->total_link_count >= MAXSYMLINKS)
        return -ELOOP;  /* "Too many levels of symbolic links" */
    /* ... 심볼릭 링크 해석 계속 ... */
}

RCU-walk과 REF-walk 모드의 성능 차이도 중요한 고려사항입니다:

/* fs/namei.c: 경로 탐색의 두 가지 모드 */

/*
 * RCU-walk (빠른 경로): 락 없이 RCU 보호 하에 dentry 탐색
 * - d_seq seqcount로 동시 수정 감지
 * - 실패 시 REF-walk로 폴백
 * - 대부분의 경로 탐색은 RCU-walk으로 완료됨
 */
static int link_path_walk(const char *name, struct nameidata *nd)
{
    /* LOOKUP_RCU 플래그가 설정되면 RCU-walk 모드 */
    if (nd->flags & LOOKUP_RCU) {
        /* seqcount 기반 락프리 탐색 — 매우 빠름 */
        /* 동시 수정 감지 시 unlazy_walk()으로 REF-walk 전환 */
    }
    /* ... */
}

/*
 * REF-walk (느린 경로): dentry 참조 카운트 획득 후 탐색
 * - d_lock spinlock 사용으로 경합 발생 가능
 * - 마운트 포인트 횡단, 권한 검사 등 복잡한 경우에 사용
 * - RCU-walk 실패 시 처음부터 REF-walk로 재시도
 */
static int unlazy_walk(struct nameidata *nd)
{
    /* RCU-walk → REF-walk 전환 */
    /* 이 전환이 빈번하면 심각한 성능 저하 */
    nd->flags &= ~LOOKUP_RCU;
    /* dentry와 vfsmount의 참조 카운트 획득 */
    /* ... */
}
dcache 메모리 소비: 대규모 파일시스템에서 dcache(디렉토리 엔트리 캐시)는 수 GB의 메모리를 소비할 수 있습니다. 수백만 개의 파일이 있는 환경에서 ls -R이나 find /와 같은 명령은 dcache를 급격히 팽창시킵니다. 메모리 압박 시 커널의 dcache/icache shrinker가 동작하지만, 이 과정에서 LRU 리스트 탐색과 negative dentry 정리에 상당한 CPU 시간이 소요될 수 있습니다. /proc/sys/fs/dentry-state로 현재 dcache 상태를 모니터링할 수 있습니다.

VFS 디버깅(Debugging) 및 모니터링 도구

VFS 관련 문제를 진단할 때 활용할 수 있는 커널 인터페이스와 트레이싱 도구를 정리합니다.

# /proc, /sys를 통한 VFS 상태 모니터링

# 1. dcache / inode 캐시 통계
cat /proc/sys/fs/dentry-state
# nr_dentry  nr_unused  age_limit  want_pages  dummy  dummy
# 85291      68432      45         0            0      0

cat /proc/sys/fs/inode-nr
# nr_inodes  nr_free_inodes
# 42156      312

# 2. 파일 핸들 사용량
cat /proc/sys/fs/file-nr
# allocated  free  max
# 9344       0     9223372036854775807

# 3. 슬랩 캐시에서 VFS 객체 확인
slabtop -o | grep -E 'dentry|inode|filp|names_cache'
# OBJS   ACTIVE  USE  OBJ SIZE  SLABS  OBJ/SLAB  CACHE SIZE  NAME
# 85320  84918   99%  0.19K     4266   20        17064K      dentry
# 42288  41976   99%  0.61K     2643   16        21144K      ext4_inode_cache
# 9408   9344    99%  0.26K      588   16        2352K       filp

# 4. 특정 프로세스의 열린 fd 확인
ls -la /proc/<pid>/fd/
readlink /proc/<pid>/fd/3  # fd 3이 가리키는 파일
cat /proc/<pid>/fdinfo/3   # 오프셋, flags, mnt_id 등

# 5. 마운트 정보 상세 확인
cat /proc/self/mountinfo
# mount_id parent_id major:minor root mount_point options ... fs_type source super_options
# ftrace / perf를 활용한 VFS 트레이싱

# vfs_read/vfs_write 호출 추적
echo 1 > /sys/kernel/debug/tracing/events/syscalls/sys_enter_openat/enable
cat /sys/kernel/debug/tracing/trace_pipe

# perf로 VFS 함수 통계
perf stat -e 'vfs:*' -a -- sleep 5

# bpftrace: 파일별 read 지연 시간 히스토그램
bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; }
kretprobe:vfs_read /@start[tid]/ {
    @usecs = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]); }'

# bpftrace: 어떤 프로세스가 어떤 파일을 열고 있는지
bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
    printf("%-16s %-6d %s\n", comm, pid, str(args.filename)); }'
dcache/icache 수동 해제: 메모리 압박 테스트나 벤치마크에서 echo 2 > /proc/sys/vm/drop_caches로 dcache/icache를 수동 해제할 수 있습니다. 값 1은 페이지 캐시, 2는 dentries/inodes, 3은 모두 해제합니다. 프로덕션 환경에서의 남용은 성능 저하를 초래하므로 주의하세요.

VFS 객체 생명주기

VFS의 네 핵심 객체(superblock, inode, dentry, file)는 각각 독립적인 생명주기를 가지며, 참조 카운트와 캐시 정책에 의해 할당·사용·캐싱·회수됩니다. 이 섹션에서는 객체 간 관계와 상태 전이를 시각적으로 정리합니다.

VFS 객체 생명주기 super_block mount() 시 할당 s_active 참조 카운트 umount() 시 해제 inode iget_locked() 할당 i_count 참조 카운트 iput() → 캐시/회수 dentry d_alloc() 할당 d_count 참조 카운트 LRU → shrink 회수 file alloc_file() 할당 f_count 참조 카운트 fput() → RCU 해제 1:N 1:N N:1 공통 생명주기 단계 Alloc Active Use Cache (LRU) Evict / Free 캐시 히트 → 재사용 참조 카운트 규칙: • super_block: deactivate_locked_super() → s_active == 0 시 kill_sb() 호출 • inode: iput() → i_count == 0 시 inode_lru에 추가 (dirty면 writeback 후) • dentry: dput() → d_lockref.count == 0 시 d_lru에 추가, shrinker 콜백으로 회수 • file: fput() → f_count == 0 시 __fput()을 task_work 또는 delayed_fput_list로 예약 • file 해제는 RCU grace period 이후 실행 (fd_install() 이후 lockless lookup 보호)
VFS 핵심 4객체의 생명주기: 할당 → 사용 → 캐싱 → 회수
inode 상태 플래그 전이 I_NEW iget_locked() 설정 I_DIRTY_* I_DIRTY_SYNC | I_DIRTY_DATASYNC | I_DIRTY_PAGES mark_inode_dirty() 설정 I_SYNC writeback 진행 중 Clean 캐시 유지 / LRU unlock_new_inode() writeback 시작 writeback 완료 write() → 다시 dirty I_FREEING evict() 진행 중 I_WILL_FREE dirty writeback 후 해제 예정 iput() → i_count==0 dirty + drop writeback 후 destroy_inode()
inode 상태 플래그 전이: I_NEW → I_DIRTY → I_SYNC → Clean → I_FREEING
/* iget_locked / iput 사용 패턴 */
struct inode *myfs_iget(struct super_block *sb, unsigned long ino)
{
    struct inode *inode;

    inode = iget_locked(sb, ino);   /* 캐시에서 찾거나 새로 할당 */
    if (!inode)
        return ERR_PTR(-ENOMEM);

    if (!(inode->i_state & I_NEW))   /* 캐시 히트 — 이미 초기화됨 */
        return inode;

    /* 새로 할당된 inode: 디스크에서 읽어 초기화 */
    inode->i_op  = &myfs_inode_ops;
    inode->i_fop = &myfs_file_ops;
    /* ... on-disk inode 데이터 읽기 ... */

    unlock_new_inode(inode);          /* I_NEW 플래그 해제 → 다른 스레드 접근 허용 */
    return inode;
}

/* iput(): 참조 카운트 감소, 0이 되면 LRU 또는 evict */
iput(inode);  /* i_count-- → 0이면: dirty? writeback 후 evict, clean? LRU 추가 */
/* super_operations: 파일시스템이 등록하는 객체 생명주기 콜백 */
static const struct super_operations myfs_super_ops = {
    .alloc_inode    = myfs_alloc_inode,    /* slab에서 inode 할당 */
    .free_inode     = myfs_free_inode,     /* RCU callback으로 해제 */
    .write_inode    = myfs_write_inode,    /* dirty inode를 디스크에 기록 */
    .evict_inode    = myfs_evict_inode,    /* inode 회수 전 정리 */
    .dirty_inode    = myfs_dirty_inode,    /* mark_inode_dirty() 시 호출 */
    .put_super      = myfs_put_super,      /* superblock 해제 */
    .sync_fs        = myfs_sync_fs,        /* sync/syncfs 시 호출 */
    .statfs         = myfs_statfs,         /* df/statfs 통계 */
};
free_inode vs destroy_inode: 커널 5.4+에서는 free_inode가 RCU 콜백으로 호출되어 destroy_inode를 대체합니다. free_inode를 사용하면 파일시스템이 별도의 call_rcu()를 호출할 필요가 없습니다.

namei 경로 탐색

VFS의 경로 탐색(path lookup)은 fs/namei.cpath_openat()에서 시작하여 link_path_walk()가 경로 구성요소를 하나씩 해석합니다. dcache 조회, 실제 파일시스템 lookup, 마운트 교차, 심볼릭 링크 추적까지 단계별 흐름을 살펴봅니다.

link_path_walk() 내부 흐름 경로 문자열 입력 1. '/' 스킵 + 다음 component 분리 2. __d_lookup_rcu() / __d_lookup() 캐시 히트 3. d_revalidate() (NFS 등 원격 FS) 4. i_op->lookup() (캐시 미스 시) 5. __follow_mount_rcu() / follow_mount() 6. symlink? → get_link() → 재귀 symlink 재귀 (최대 40회) 7. 마지막 component 처리 (open/create) RCU-walk 모드 실패 시 REF-walk로 fallback (LOOKUP_RCU) 에러 경로 -ENOENT, -ELOOP, -EACCES 경로 "/mnt/data/file.txt" → '/', 'mnt', '/', 'data', '/', 'file.txt' 순차 처리
link_path_walk(): 경로 구성요소를 하나씩 해석하는 핵심 루프
/* fs/namei.c — path_openat() 핵심 흐름 (축약) */
static struct file *path_openat(struct nameidata *nd,
                                 const struct open_flags *op, unsigned flags)
{
    struct file *file;
    int error;

    file = alloc_empty_file(op->open_flag, current_cred());

    /* LOOKUP_RCU (RCU-walk) 또는 0 (REF-walk) */
    if (unlikely(flags & LOOKUP_RCU))
        nd->flags |= LOOKUP_RCU;

    /* 경로 시작점 설정: 절대 경로 → root, 상대 경로 → cwd */
    set_nameidata(nd, ...);

    /* 핵심: link_path_walk → 마지막 component까지 순회 */
    error = link_path_walk(pathname, nd);
    if (!error)
        error = do_last(nd, file, op);  /* 마지막 component: open 또는 create */

    if (unlikely(error == -ESTALE && !(flags & LOOKUP_REVAL))) {
        flags |= LOOKUP_REVAL;         /* NFS stale → 재시도 */
        goto retry;
    }
    return file;
}
/* walk_component(): 하나의 경로 구성요소를 처리하는 핵심 함수 */
static int walk_component(struct nameidata *nd, int flags)
{
    struct dentry *dentry;

    /* 1단계: "." / ".." 특수 처리 */
    if (unlikely(nd->last_type != LAST_NORM))
        return handle_dots(nd, nd->last_type);

    /* 2단계: dcache 조회 (RCU-walk에서는 __d_lookup_rcu) */
    dentry = lookup_fast(nd);
    if (IS_ERR(dentry))
        return PTR_ERR(dentry);

    if (unlikely(!dentry)) {
        /* 3단계: 캐시 미스 → 느린 경로 (i_op->lookup 호출) */
        dentry = lookup_slow(&nd->last, nd->path.dentry, nd->flags);
    }

    /* 4단계: 마운트 교차 확인 (다른 FS가 이 지점에 마운트?) */
    return step_into(nd, flags, dentry);  /* → handle_mounts() 내부 호출 */
}
/* 심볼릭 링크 루프 방지: MAXSYMLINKS = 40 (include/linux/namei.h) */
#define MAXSYMLINKS  40

/* nameidata 내부에서 link depth 추적 */
struct nameidata {
    unsigned depth;           /* 현재 symlink 깊이 */
    int total_link_count;     /* 전체 탐색에서 만난 symlink 수 */
    /* ... */
};

/* pick_link()에서 검사 */
if (unlikely(nd->total_link_count++ >= MAXSYMLINKS))
    return ERR_PTR(-ELOOP);   /* "Too many levels of symbolic links" */

/* nested depth 제한: 최대 8단계 (스택 오버플로 방지) */
if (unlikely(nd->depth >= MAX_NESTED_LINKS))
    return ERR_PTR(-ELOOP);
RCU-walk 성능: 대부분의 경로 탐색은 RCU-walk 모드에서 완료되어 어떤 락도 잡지 않습니다. dcache 미스나 permission 체크에서 d_seq 변경이 감지되면 unlazy_walk()로 REF-walk에 fallback합니다. 벤치마크에서 경로 탐색의 ~99%가 RCU-walk로 완료됩니다.

커널 소스 분석: open() 호출 체인과 핵심 구조체

이 절은 sys_open()에서 파일을 여는 순간부터 link_path_walk()가 경로를 해석하는 과정까지의 전체 호출 체인과, VFS의 두 핵심 연산 테이블인 inode_operations, file_operations의 각 필드를 커널 소스 수준에서 분석합니다.

sys_open() → do_filp_open() → path_openat() → link_path_walk() 호출 체인

open() 호출 체인: 시스템 콜 → 경로 탐색 sys_open() / sys_openat2() arch/x86/entry → fs/open.c : do_sys_openat2() build_open_flags() do_filp_open() fs/namei.c — nameidata 초기화, RCU/REF 모드 결정 set_nameidata() path_openat() fs/namei.c — alloc_empty_file() → link_path_walk() → do_last() O_CREAT: lookup_open() → vfs_create() | 기존 파일: open_last_lookups() path_init() 후 호출 link_path_walk() fs/namei.c — 경로 구성요소 반복 처리 may_lookup() 권한 확인 → walk_component() 반복 lookup_fast() dcache 조회 (RCU-walk) lookup_slow() i_op→lookup() (캐시 미스) 캐시 미스 step_into() → do_last() → vfs_open() 마운트 교차 · may_open() · f_op→open() 콜백 호출
sys_open() 호출 체인: 시스템 콜 진입 → 경로 탐색 → dcache 조회 → 파일 열기 완료. path_openat()이 전체 흐름의 중심이며 link_path_walk()가 경로 구성요소를 반복 처리합니다.

struct inode_operations 필드별 분석

inode_operations는 파일/디렉토리의 메타데이터 조작을 담당하는 콜백 테이블입니다. VFS가 특정 파일시스템 연산을 수행할 때 이 테이블의 함수 포인터를 호출합니다. 일반 파일 inode와 디렉토리 inode는 각각 다른 연산 테이블을 등록합니다.

/* include/linux/fs.h — struct inode_operations 주요 필드 전체 (축약) */
struct inode_operations {
    /* ── 디렉토리 전용 연산 ───────────────────────────────── */
    struct dentry *(*lookup)(struct inode *, struct dentry *, unsigned int); /* dcache 미스 시 호출 — FS에서 디스크를 조회하여 dentry 채움 */
    int (*create)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t, bool); /* O_CREAT|O_EXCL 시 새 inode 생성 */
    int (*link)(struct dentry *, struct inode *, struct dentry *);             /* 하드 링크 생성 — i_nlink 증가 */
    int (*unlink)(struct inode *, struct dentry *);                              /* 이름 제거 — i_nlink 감소, 0이면 inode 삭제 */
    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); /* 특수 파일 생성 (블록/문자/FIFO/소켓) */
    int (*rename)(struct mnt_idmap *, struct inode *, struct dentry *,
                 struct inode *, struct dentry *, unsigned int);              /* 파일/디렉토리 이름 변경·이동 */

    /* ── 심볼릭 링크 전용 ─────────────────────────────────── */
    const char *(*get_link)(struct dentry *, struct inode *, struct delayed_call *); /* 심볼릭 링크 대상 문자열 반환 */

    /* ── 권한 검사 ────────────────────────────────────────── */
    int (*permission)(struct mnt_idmap *, struct inode *, int);               /* DAC(임의적 접근 제어) 권한 검사 — NULL이면 generic_permission() 사용 */

    /* ── 속성 조회/변경 ───────────────────────────────────── */
    int (*getattr)(struct mnt_idmap *, const struct path *,
                  struct kstat *, u32, unsigned int);                        /* stat()/statx() 구현 — FS별 추가 속성 반환 */
    int (*setattr)(struct mnt_idmap *, struct dentry *, struct iattr *);      /* chmod/chown/truncate — inode 속성 변경 */

    /* ── 확장 속성(xattr) ─────────────────────────────────── */
    ssize_t (*listxattr)(struct dentry *, char *, size_t);                  /* 모든 xattr 이름 목록 반환 */
    int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64, u64);   /* 파일 물리 레이아웃(extent) 조회 */
    int (*update_time)(struct inode *, int);                                 /* atime/mtime/ctime 갱신 — NULL이면 generic_update_time() */
    int (*atomic_open)(struct inode *, struct dentry *,
                      struct file *, unsigned, umode_t);                   /* lookup+create+open을 원자적으로 수행 (NFS, ceph 등 최적화) */
    int (*tmpfile)(struct mnt_idmap *, struct inode *, struct file *, umode_t); /* O_TMPFILE 구현 — 이름 없는 임시 파일 생성 */
    struct posix_acl *(*get_acl)(struct mnt_idmap *, struct dentry *, int); /* POSIX ACL 조회 */
    int (*set_acl)(struct mnt_idmap *, struct dentry *, struct posix_acl *, int); /* POSIX ACL 설정 */
} ____cacheline_aligned;
코드 설명
  • lookupdcache(dentry 캐시) 조회 실패 시 VFS가 호출합니다. 파일시스템은 디스크에서 해당 이름의 inode를 찾아 dentry에 채워 반환합니다. ext4의 경우 ext4_lookup()이 이 역할을 수행합니다.
  • createopen(O_CREAT|O_EXCL) 또는 creat() 호출 시 VFS가 호출합니다. mnt_idmap 매개변수는 Linux 6.3+에서 추가된 ID 매핑(마운트별 uid/gid 변환) 컨텍스트입니다.
  • unlink디렉토리 항목을 제거합니다. i_nlink가 0이 되면 evict_inode()를 통해 실제 inode도 삭제됩니다. 열려 있는 파일은 마지막 close()까지 데이터를 유지합니다.
  • get_link심볼릭 링크의 대상 경로 문자열을 반환합니다. delayed_call을 통해 반환된 버퍼의 해제를 지연 처리합니다. 반환 값이 ERR_PTR(-ECHILD)이면 RCU-walk를 포기하고 ref-walk로 전환합니다.
  • permissionDAC 권한 검사를 커스터마이즈합니다. NULL이면 generic_permission()이 사용됩니다. NFS 같은 원격 파일시스템은 서버 측 권한을 로컬에 반영하기 위해 이 콜백을 구현합니다.
  • setattrchmod(), chown(), truncate() 등 inode 속성 변경 요청을 처리합니다. struct iattria_valid 비트마스크로 변경할 속성을 지정합니다. 저널링 파일시스템은 여기서 트랜잭션을 시작합니다.
  • atomic_openlookup, create, open 세 단계를 하나의 네트워크 요청으로 합칠 수 있는 최적화 콜백입니다. NFS4, CephFS가 구현합니다. 구현 시 반환 값 의미가 복잡하므로(finish_open()/finish_no_open() 사용) 주의가 필요합니다.
  • ____cacheline_aligned구조체를 캐시 라인 크기(보통 64바이트)로 정렬합니다. 다수의 CPU가 이 테이블을 동시 조회할 때 false sharing(가짜 공유)으로 인한 캐시 무효화를 방지합니다.

struct file_operations 필드별 분석

file_operations는 열린 파일의 데이터 접근과 제어를 위한 콜백 테이블입니다. open()inode->i_fopfile->f_op에 복사되며, 이후 모든 I/O 연산은 이 테이블을 통해 파일시스템 구현으로 디스패치됩니다.

/* include/linux/fs.h — struct file_operations 전체 필드 (핵심 주석) */
struct file_operations {
    struct module  *owner;          /* 소유 모듈 (THIS_MODULE) — 사용 중 모듈 언로드 방지 */

    /* ── 파일 위치 제어 ───────────────────────────────────── */
    loff_t          (*llseek)(struct file *, loff_t, int);             /* lseek()/llseek() — NULL이면 no_llseek(오류) 또는 noop_llseek */

    /* ── 레거시 동기 I/O (현대 커널에서 사용 감소) ─────────── */
    ssize_t         (*read)(struct file *, char __user *, size_t, loff_t *); /* 단순 동기 읽기 — read_iter 없을 때만 사용 */
    ssize_t         (*write)(struct file *, const char __user *, size_t, loff_t *); /* 단순 동기 쓰기 */

    /* ── iov_iter 기반 I/O (권장) ─────────────────────────── */
    ssize_t         (*read_iter)(struct kiocb *, struct iov_iter *);  /* scatter-gather·AIO·io_uring 통합 읽기 */
    ssize_t         (*write_iter)(struct kiocb *, struct iov_iter *); /* scatter-gather·AIO·io_uring 통합 쓰기 */

    /* ── 비동기 I/O ───────────────────────────────────────── */
    int             (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *, unsigned int); /* io_uring 폴링 모드 완료 수거 */

    /* ── 디렉토리 열거 ────────────────────────────────────── */
    int             (*iterate_shared)(struct file *, struct dir_context *); /* getdents64() 구현 — 공유 잠금으로 동시 조회 허용 */

    /* ── 이벤트/폴링 ──────────────────────────────────────── */
    __poll_t        (*poll)(struct file *, struct poll_table_struct *); /* select()/poll()/epoll() — 읽기/쓰기 준비 여부 반환 */

    /* ── 제어 인터페이스 ──────────────────────────────────── */
    long            (*unlocked_ioctl)(struct file *, unsigned int, unsigned long); /* BKL 없는 ioctl — 대부분의 드라이버/FS가 사용 */
    long            (*compat_ioctl)(struct file *, unsigned int, unsigned long);   /* 32비트 프로세스가 64비트 커널에서 ioctl 호출 시 */

    /* ── 메모리 매핑 ──────────────────────────────────────── */
    int             (*mmap)(struct file *, struct vm_area_struct *);  /* mmap() 구현 — VMA에 vm_ops 설정 */
    unsigned long   mmap_supported_flags;                                /* 지원하는 MAP_* 플래그 비트마스크 */

    /* ── 파일 열기/닫기 ───────────────────────────────────── */
    int             (*open)(struct inode *, struct file *);           /* FS별 open 초기화 — private_data 설정 등 */
    int             (*flush)(struct file *, fl_owner_t id);           /* fd를 닫기 전 flush — NFS에서 에러 전파에 사용 */
    int             (*release)(struct inode *, struct file *);        /* 마지막 close() 시 호출 — 리소스 해제 */

    /* ── 동기화 ───────────────────────────────────────────── */
    int             (*fsync)(struct file *, loff_t, loff_t, int datasync); /* fsync()/fdatasync() — dirty 데이터/메타데이터를 디스크에 반영 */
    int             (*fasync)(int, struct file *, int);               /* O_ASYNC (SIGIO) 비동기 I/O 알림 등록/해제 */

    /* ── 잠금 ─────────────────────────────────────────────── */
    int             (*flock)(struct file *, int, struct file_lock *); /* BSD flock() 구현 — NULL이면 기본 VFS 구현 사용 */

    /* ── 제로 카피 전송 ───────────────────────────────────── */
    ssize_t         (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); /* splice() 읽기 측 — 파이프에 페이지 참조 이전 */
    ssize_t         (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); /* splice() 쓰기 측 */

    /* ── 사전 할당 및 홀 ──────────────────────────────────── */
    long            (*fallocate)(struct file *, int mode, loff_t, loff_t); /* fallocate() — 사전 할당/홀 펀치/제로 범위/해제 */

    /* ── 알림 ─────────────────────────────────────────────── */
    void            (*show_fdinfo)(struct seq_file *m, struct file *f); /* /proc/PID/fdinfo/N 출력 — FS별 추가 정보 */
} __randomize_layout;
코드 설명
  • owner이 파일 연산 테이블이 속한 커널 모듈을 가리킵니다. THIS_MODULE로 설정하면 파일이 열려 있는 동안 모듈 참조 카운트가 유지되어 rmmod가 차단됩니다. 내장 파일시스템은 NULL로 설정합니다.
  • read_iter / write_iter현대 커널의 표준 I/O 인터페이스입니다. struct kiocb에 I/O 컨텍스트(오프셋, 플래그, 완료 콜백)를 담고, struct iov_iter로 scatter-gather 버퍼를 추상화합니다. readv(), writev(), AIO, io_uring이 모두 이 경로를 사용합니다.
  • iopollio_uring의 폴링 모드(IORING_SETUP_IOPOLL)에서 커널이 완료 이벤트를 주기적으로 수거할 때 호출합니다. 인터럽트 대신 바쁜 대기(busy-wait)로 완료를 확인하여 초저지연 스토리지(NVMe)에서 레이턴시를 줄입니다.
  • iterate_sharedgetdents64() 시스템 콜이 디렉토리 항목을 열거할 때 호출합니다. shared 접미사가 나타내듯 여러 프로세스가 동시에 읽기 잠금을 공유할 수 있습니다. dir_context.actor 콜백으로 각 항목을 처리합니다.
  • openVFS의 vfs_open()struct file을 할당한 후 호출합니다. ext4의 경우 ext4_file_open()이 암호화 컨텍스트 확인, verity 검증 등을 수행합니다. 이 콜백이 반환하는 에러는 fd를 닫고 사용자에게 전달됩니다.
  • flush vs releaseflushdup()된 fd 중 하나가 닫힐 때마다 호출됩니다(마지막이 아닐 수도 있음). release는 참조 카운트가 0이 되는 마지막 close()에서만 호출됩니다. NFS는 flush에서 서버 에러를 사용자에게 반환합니다.
  • fallocatemode=0이면 공간을 사전 할당합니다. FALLOC_FL_PUNCH_HOLE이면 중간 데이터를 제거합니다(hole). FALLOC_FL_ZERO_RANGE는 0으로 채우고, FALLOC_FL_COLLAPSE_RANGE는 범위를 제거하고 파일을 축소합니다.
  • __randomize_layoutGCC RANDSTRUCT 플러그인(CONFIG_RANDSTRUCT)이 이 구조체의 필드 순서를 빌드 시 무작위로 섞습니다. 이는 함수 포인터 테이블의 예측 가능한 오프셋을 악용하는 커널 취약점 공격을 어렵게 합니다.

path_openat() 소스 분석

path_openat()do_filp_open()에서 호출되는 실제 경로 탐색과 파일 열기의 핵심 함수입니다. struct nameidata를 사용하여 경로 탐색 상태를 추적하며, RCU-walk 실패 시 재시도(retry) 로직을 포함합니다.

/* fs/namei.c — path_openat() 핵심 로직 (Linux 6.x 기준, 주석 추가) */
static struct file *path_openat(struct nameidata *nd,
                                 const struct open_flags *op,
                                 unsigned int flags)
{
    struct file  *file;
    int           error;

    /* 1) struct file 할당 — 슬랩 캐시 filp_cachep 에서 할당
     *    open_flag: O_RDONLY/O_WRONLY/O_RDWR 등의 접근 플래그
     *    current_cred(): 현재 프로세스의 자격 증명 (euid, egid 등) */
    file = alloc_empty_file(op->open_flag, current_cred());
    if (IS_ERR(file))
        return file;

retry:
    /* 2) 경로 시작점 초기화
     *    절대 경로('/'로 시작): current->fs->root (루트 dentry)
     *    상대 경로: current->fs->pwd (현재 작업 디렉토리)
     *    AT_FDCWD 이외의 dirfd: 해당 fd의 dentry */
    const char *s = path_init(nd, flags);
    if (IS_ERR(s)) {
        error = PTR_ERR(s);
        goto out;
    }

    /* 3) 마지막 구성요소 직전까지 경로 순회
     *    각 '/' 구분 구성요소마다 walk_component() 호출
     *    RCU-walk 실패 시 -ECHILD 반환 → unlazy_walk() → REF-walk */
    error = link_path_walk(s, nd);
    if (!error)
        /* 4) 마지막 구성요소 처리: open 또는 create */
        error = do_last(nd, file, op);

    /* 5) NFS stale dentry 처리: LOOKUP_REVAL 플래그로 재시도 */
    if (unlikely(error == -ESTALE)) {
        if (!(flags & LOOKUP_REVAL)) {
            flags |= LOOKUP_REVAL;
            terminate_walk(nd);
            goto retry;            /* NFS stale → 재검증 후 재시도 */
        }
    }

out:
    terminate_walk(nd);
    if (likely(!error))
        return file;
    fput(file);               /* 실패 시 파일 참조 해제 */
    return ERR_PTR(error);
}
코드 설명
  • 1행: alloc_empty_file()filp_cachep 슬랩 캐시에서 struct file을 할당합니다. 이 시점에서 파일 구조체는 비어 있으며 이후 경로 탐색이 성공해야 완전히 초기화됩니다. 할당 실패 시 ERR_PTR(-ENOMEM)을 반환합니다.
  • retry: 레이블NFS와 같은 원격 파일시스템에서 dentry가 서버 측에서 만료(stale)된 경우 LOOKUP_REVAL 플래그를 추가하고 처음부터 재시도합니다. 무한 루프를 방지하기 위해 이미 LOOKUP_REVAL이 설정된 경우에는 에러를 그대로 전달합니다.
  • 2행: path_init()탐색 시작점을 nd->pathnd->inode에 설정합니다. 절대 경로는 current->fs->root, 상대 경로는 current->fs->pwd가 출발점입니다. openat()의 dirfdAT_FDCWD가 아닌 경우 해당 fd의 디렉토리를 시작점으로 사용합니다.
  • 3행: link_path_walk()경로 문자열을 '/' 기준으로 분리하여 각 구성요소를 순차 처리합니다. 내부에서 walk_component()를 반복 호출하여 dcache 조회, 마운트 교차, 심볼릭 링크 추적을 수행합니다. 마지막 구성요소(파일명)는 처리하지 않고 nd->last에 남겨둡니다.
  • 4행: do_last()마지막 경로 구성요소(파일명)를 처리합니다. 파일이 존재하면 open_last_lookups()may_open()vfs_open()을, O_CREAT인 경우 lookup_open()vfs_create()vfs_open()을 호출합니다.
  • 5행: -ESTALE 처리NFS 서버가 dentry를 만료시킨 경우 d_revalidate()-ESTALE을 반환합니다. LOOKUP_REVAL 플래그를 추가하여 모든 dentry를 서버에 재검증하며 경로 탐색을 재시도합니다.
  • fput(file)경로 탐색 실패 시 앞서 할당한 struct file의 참조를 해제합니다. fput()은 참조 카운트가 0이 되면 __fput()을 호출하여 슬랩 캐시로 반환합니다.

lookup_fast() 소스 분석

lookup_fast()는 dcache에서 경로 구성요소를 조회하는 빠른 경로입니다. RCU-walk 모드에서는 어떤 락도 잡지 않고 d_seq 시퀀스 번호 검증만으로 dentry의 일관성을 확인합니다.

/* fs/namei.c — lookup_fast() 핵심 흐름 (RCU-walk / REF-walk 분기) */
static struct dentry *lookup_fast(struct nameidata *nd)
{
    struct dentry *dentry, *parent = nd->path.dentry;
    int status = 1;

    /* ── RCU-walk 경로 ──────────────────────────────────── */
    if (nd->flags & LOOKUP_RCU) {
        /* 락 없이 해시 테이블에서 dentry 검색
         * d_seq: 수정 중인 dentry를 감지하기 위한 시퀀스 카운터
         * 읽기 전후로 d_seq가 같으면 dentry가 안정적임 */
        dentry = __d_lookup_rcu(parent, &nd->last, &nd->next_seq);
        if (unlikely(!dentry)) {
            /* dcache 미스: REF-walk로 전환하거나 lookup_slow() 필요 */
            if (!try_to_unlazy(nd))
                return ERR_PTR(-ECHILD);
            return NULL;       /* walk_component에서 lookup_slow 호출 */
        }

        /* d_inode 검증: RCU 시퀀스가 변경되지 않았는지 확인 */
        nd->inode = dentry->d_inode;
        if (unlikely(read_seqcount_retry(&dentry->d_seq, nd->next_seq))) {
            /* 레이스(race) 감지: 탐색 중 dentry가 수정됨 → REF-walk 재시도 */
            if (!try_to_unlazy_next(nd, dentry))
                return ERR_PTR(-ECHILD);
        }

        /* d_revalidate: NFS 등 원격 FS는 서버와의 일관성 확인 필요
         * RCU 모드에서는 LOOKUP_RCU 플래그로 비블록킹 검증 시도 */
        status = d_revalidate(nd->path.mnt, dentry, nd->flags);
    } else {
        /* ── REF-walk 경로 — dcache 해시 탐색 (dentry 참조 카운트 증가) */
        dentry = __d_lookup(parent, &nd->last);
        if (unlikely(!dentry))
            return NULL;           /* 캐시 미스 → lookup_slow()로 진행 */

        status = d_revalidate(nd->path.mnt, dentry, nd->flags);
    }

    if (unlikely(status <= 0)) {
        if (!status)
            d_invalidate(dentry); /* 무효 dentry를 캐시에서 제거 */
        dput(dentry);
        return ERR_PTR(status < 0 ? status : -ESTALE);
    }
    return dentry;               /* 유효한 dentry 반환 */
}
코드 설명
  • LOOKUP_RCU 조건RCU-walk 모드에서는 어떤 스핀락도 잡지 않습니다. 대신 d_seq 시퀀스 카운터의 even/odd 값으로 dentry 수정 중 여부를 판단합니다(시퀀스 잠금과 유사). 이 덕분에 경로 탐색 핫 패스에서 CPU 간 캐시 라인 경합이 없습니다.
  • __d_lookup_rcu()부모 dentry의 자식 해시 테이블을 RCU 보호 아래 탐색합니다. nd->last(경로 구성요소 문자열)와 해시 값으로 일치하는 dentry를 찾습니다. 반환 시 nd->next_seq에 현재 d_seq를 저장합니다.
  • try_to_unlazy()dcache 미스 또는 d_seq 레이스가 감지될 때 RCU 읽기 잠금을 유지하면서 REF-walk로 전환을 시도합니다. 전환 성공 시 true를 반환하며 이후 lookup_slow()로 진행합니다. 전환 실패(경합이 너무 심한 경우) 시 -ECHILD를 반환하여 path_openat()이 처음부터 다시 시작합니다.
  • read_seqcount_retry()d_seq를 읽기 전에 저장한 시퀀스 번호와 현재 값을 비교합니다. 값이 다르다면 읽기 도중 dentry가 수정되었음을 의미하므로 데이터가 불일관 상태일 수 있습니다. 이는 세마포어 없는 낙관적 잠금(optimistic locking) 패턴입니다.
  • d_revalidate()파일시스템별 dentry_operations->d_revalidate 콜백을 호출합니다. ext4 같은 로컬 파일시스템은 이 콜백을 구현하지 않지만(항상 유효), NFS는 서버에 파일이 여전히 존재하는지 확인합니다. RCU 모드에서는 블록킹이 허용되지 않으므로 비블록킹 검증만 수행합니다.
  • d_invalidate()유효하지 않은(stale) dentry를 dcache 해시 테이블에서 제거합니다. 다음 경로 탐색에서 이 dentry가 재사용되지 않도록 합니다. dput()으로 참조 카운트를 감소시킨 후 카운트가 0이 되면 슬랩 캐시로 반환됩니다.

vfs_read() 소스 분석

vfs_read()sys_read()에서 호출되는 VFS 레벨 읽기 함수입니다. 권한 검사, 파일시스템 콜백 디스패치, 위치 업데이트까지 모든 버퍼드 읽기의 공통 경로를 담당합니다.

/* fs/read_write.c — vfs_read() 전체 흐름 (주요 단계 주석 추가) */
ssize_t vfs_read(struct file *file, char __user *buf,
                 size_t count, loff_t *pos)
{
    ssize_t ret;

    /* ① 기본 모드 검사: 읽기 권한으로 열린 파일인지 확인
     *    FMODE_READ 비트가 없으면 EBADF 반환 */
    if (!(file->f_mode & FMODE_READ))
        return -EBADF;

    /* ② read 또는 read_iter 콜백이 있는지 확인
     *    둘 다 없으면 이 파일 유형은 읽기를 지원하지 않음 (EINVAL) */
    if (!(file->f_mode & FMODE_CAN_READ))
        return -EINVAL;

    /* ③ 사용자 버퍼 유효성 검사
     *    access_ok(): 버퍼 주소가 사용자 공간에 속하는지 확인
     *    NULL 버퍼나 커널 주소 접근 시도를 차단 */
    if (unlikely(!access_ok(buf, count)))
        return -EFAULT;

    /* ④ rw_verify_area(): 범위 검사 + LSM 보안 훅 호출
     *    - 파일 오프셋 + count 가 loff_t 범위를 초과하지 않는지 검사
     *    - security_file_permission(file, MAY_READ) 호출
     *      → SELinux/AppArmor 등 MAC 정책 확인
     *    - 파일 잠금 범위 확인 (mandatory lock 모드) */
    ret = rw_verify_area(READ, file, pos, count);
    if (ret)
        return ret;

    /* ⑤ count 상한 조정: MAX_RW_COUNT(0x7ffff000, ~2GB) 초과 방지
     *    단일 read() 로 읽을 수 있는 최대 바이트 수 제한 */
    if (count > MAX_RW_COUNT)
        count = MAX_RW_COUNT;

    /* ⑥ 파일시스템 콜백 디스패치
     *    read_iter 우선: scatter-gather / AIO / io_uring 통합 경로
     *    read 폴백: 레거시 드라이버/FS용 */
    if (file->f_op->read)
        ret = file->f_op->read(file, buf, count, pos);
    else if (file->f_op->read_iter)
        ret = new_sync_read(file, buf, count, pos);
    else
        ret = -EINVAL;

    /* ⑦ 성공 시 후처리:
     *    - inotify/fanotify IN_ACCESS 이벤트 알림
     *    - 파일 접근 시간(atime) 갱신 (relatime 마운트 옵션 고려) */
    if (ret > 0) {
        fsnotify_access(file);
        add_rchar(current(), ret); /* 프로세스 I/O 통계 갱신 (/proc/PID/io) */
    }
    inc_syscr(current());         /* 시스템 콜 읽기 횟수 카운터 증가 */
    return ret;
}
코드 설명
  • ① FMODE_READ 검사open(O_WRONLY)로 열린 파일에 read()를 시도하면 이 검사에서 -EBADF가 반환됩니다. f_modedo_filp_open()에서 open 플래그를 기반으로 설정됩니다.
  • ② FMODE_CAN_READ 검사파일 연산 테이블에 readread_iter 콜백이 없는 파일 유형(예: 쓰기 전용 파이프 끝)에 대한 검사입니다. FMODE_CAN_READinit_file()에서 f_op를 설정할 때 자동으로 결정됩니다.
  • ③ access_ok()아키텍처별 매크로로 버퍼 주소가 유효한 사용자 공간 주소 범위에 속하는지 확인합니다. x86_64에서는 사용자 공간이 0~TASK_SIZE_MAX(약 128TB) 범위입니다. 이 검사는 사용자가 커널 주소를 버퍼로 지정하는 공격을 차단합니다.
  • ④ rw_verify_area()오버플로 검사(pos + countMAX_LFS_FILESIZE를 초과하지 않는지)와 security_file_permission() 호출을 수행합니다. SELinux라면 파일의 보안 레이블과 프로세스 도메인을 비교하는 selinux_file_permission()이 여기서 호출됩니다.
  • ⑤ MAX_RW_COUNT0x7ffff000(약 2GB - 4KB)으로 정의됩니다. 단일 시스템 콜에서 너무 많은 데이터를 처리하면 커널 내부에서 ssize_t 범위를 초과하거나, 다른 프로세스의 스케줄링을 지나치게 지연시킬 수 있어 이 상한을 둡니다.
  • ⑥ read vs read_iter 디스패치현대 파일시스템(ext4, XFS, btrfs 등)과 최신 드라이버는 모두 read_iter를 구현합니다. new_sync_read()struct kiocbstruct iov_iter를 초기화한 후 call_read_iter()를 통해 f_op->read_iter를 호출합니다.
  • ⑦ fsnotify_access()inotify, fanotify 리스너가 있는 경우 IN_ACCESS 이벤트를 큐에 넣습니다. 리스너가 없으면 이 함수는 즉시 반환(no-op)되어 오버헤드가 없습니다. add_rchar()/proc/PID/iorchar 카운터를 갱신합니다.

dentry 캐시 LRU 관리

dcache는 경로 탐색 성능의 핵심이며, 참조되지 않는 dentry를 LRU 리스트에 보관하여 재사용합니다. 메모리 압박 시 shrink_dcache_sb() 또는 shrinker 콜백을 통해 오래된 dentry부터 회수합니다.

dentry 캐시 LRU 관리 흐름 d_alloc() d_add() → 해시 삽입 Active (d_count > 0) 경로 탐색에서 사용 중 dput() → d_count == 0 LRU 리스트에 추가 sb->s_dentry_lru (per-superblock LRU) oldest dentry dentry ... dentry newest dcache 히트 → 재활성화 shrink_dcache_sb() d_lru_shrink_move() → __dentry_kill() d_shrink_del() → d_iput() → dentry_free() shrinker 호출 트리거 • 메모리 압박: kswapd / direct reclaim → super_cache_scan() • 수동: echo 2 > /proc/sys/vm/drop_caches Negative dentry: d_inode == NULL "파일 없음"도 캐시 — 반복 ENOENT 조회 가속, 과도 시 dentry-negative 상한 적용
dcache LRU: dput()으로 LRU 진입, shrinker가 메모리 압박 시 oldest부터 회수
/* d_alloc() — 새 dentry 할당 */
struct dentry *d_alloc(struct dentry *parent,
                       const struct qstr *name)
{
    struct dentry *dentry = kmem_cache_alloc(dentry_cache, GFP_KERNEL);
    /* d_lockref.count = 1 (활성 참조) */
    dentry->d_lockref.count = 1;
    dentry->d_flags = 0;
    dentry->d_inode = NULL;         /* 아직 inode 미연결 (negative) */
    dentry->d_parent = dget(parent);
    /* ... name 복사, 자식 리스트에 추가 ... */
    return dentry;
}

/* d_splice_alias() — inode를 연결하며 기존 별칭 처리 */
struct dentry *d_splice_alias(struct inode *inode,
                              struct dentry *dentry)
{
    /* 동일 inode에 이미 dentry가 있으면 그것을 반환 (hard link 등) */
    if (inode && S_ISDIR(inode->i_mode)) {
        struct dentry *new = __d_find_any_alias(inode);
        if (new) {
            __d_move(new, dentry, false);
            return new;
        }
    }
    __d_add(dentry, inode);  /* 해시 테이블에 삽입 */
    return NULL;
}
/* dentry LRU 추가/제거 (d_lru_add / d_lru_del) */

/* dput() → d_count == 0 시 LRU에 추가 */
static void d_lru_add(struct dentry *dentry)
{
    d_lru_list_add(dentry, &dentry->d_sb->s_dentry_lru);
    /* this_cpu_inc(nr_dentry_unused) — 전역 미사용 dentry 카운트 */
}

/* dcache 재참조 시 LRU에서 제거 */
static void d_lru_del(struct dentry *dentry)
{
    d_lru_list_del(dentry);
    /* this_cpu_dec(nr_dentry_unused) */
}

/* shrinker 콜백: super_cache_scan() → prune_dcache_sb()
 * 메모리 압박 시 LRU에서 oldest부터 sc->nr_to_scan개 회수 */

writeback 전체 흐름

파일 쓰기 시 데이터는 먼저 페이지 캐시에 기록되고, 이후 비동기적으로 디스크에 플러시됩니다. mark_inode_dirty()로 시작하여 wb_workfn() 워커 스레드(Thread)가 실제 I/O를 수행하는 전체 파이프라인(Pipeline)을 살펴봅니다.

dirty page writeback 전체 흐름 write() / writev() 페이지 캐시에 기록 mark_inode_dirty() I_DIRTY_PAGES 설정 wb->b_dirty 리스트 BDI writeback 큐에 추가 wakeup_flusher 타이머/threshold wb_workfn() — Flusher Worker Thread wb_do_writeback() → wb_writeback() → writeback_sb_inodes() writeback_sb_inodes(): b_io 리스트에서 inode 순회 inode별 __writeback_single_inode() 호출 a_ops->writepages() / write_cache_pages() dirty 페이지 수집 → folio_batch → 파일시스템별 기록 submit_bio() → Block Layer → 디스크 bio → request → I/O 스케줄러 → device driver 트리거 • 주기적 (dirty_ expire) • threshold • sync() • fsync() • 메모리 완료: clear_page_dirty_for_io() → I_DIRTY 해제 → inode를 b_dirty에서 제거
writeback 파이프라인: write() → dirty 마킹 → flusher 워커 → writepages → BIO → 디스크
/* bdi_writeback: 디바이스별 writeback 상태 */
struct bdi_writeback {
    struct backing_dev_info *bdi;

    /* dirty inode 리스트 (3개로 분류) */
    struct list_head b_dirty;       /* dirty inode 대기열 */
    struct list_head b_io;          /* writeback 진행 중인 inode */
    struct list_head b_more_io;     /* 한 라운드에서 완료 못 한 inode */

    struct delayed_work dwork;      /* wb_workfn 실행용 work */
    unsigned long last_old_flush;   /* 마지막 주기적 flush 시각 */

    struct list_head work_list;     /* 대기 중인 writeback 작업 */
    /* ... */
};
/* writeback_control: writeback 동작 제어 */
struct writeback_control {
    long nr_to_write;         /* 기록할 페이지 수 (감소시킴) */
    long pages_skipped;       /* 건너뛴 페이지 수 */

    loff_t range_start;       /* 기록 범위 시작 */
    loff_t range_end;         /* 기록 범위 끝 */

    enum writeback_sync_modes sync_mode;  /* WB_SYNC_NONE / WB_SYNC_ALL */

    unsigned for_kupdate:1;   /* 주기적 writeback (dirty_expire) */
    unsigned for_background:1;/* background threshold 초과 */
    unsigned for_sync:1;      /* sync() / syncfs() 호출 */
    unsigned for_reclaim:1;   /* 메모리 회수 경로 */
};

/* dirty 임계값: /proc/sys/vm/ 아래 설정
 * dirty_background_ratio (10%) — background writeback 시작
 * dirty_ratio (20%)            — 프로세스가 직접 writeback (throttle)
 * dirty_expire_centisecs (3000) — 30초 이상 된 dirty 페이지 flush
 * dirty_writeback_centisecs (500) — flusher 깨우기 주기 (5초) */
writeback throttling: dirty 페이지가 dirty_ratio를 초과하면 balance_dirty_pages()에서 프로세스가 직접 writeback에 참여하며 I/O 대역폭(Bandwidth)에 비례하여 sleep합니다. 이것이 "I/O가 느려지는" 주요 원인입니다.

마운트 트리와 네임스페이스

Linux의 마운트 시스템은 계층적 트리 구조를 형성하며, struct mount가 각 마운트 지점을 나타냅니다. 마운트 네임스페이스와 propagation(shared/slave/private/unbindable) 모드는 컨테이너 격리(Isolation)의 핵심입니다.

마운트 트리 구조와 Propagation mnt_namespace A (호스트) / (rootfs) /home (ext4) /data (xfs) /data/sub (btrfs) mnt_namespace B (컨테이너) / (overlay) /proc (proc) /sys (sysfs) Mount Propagation 모드 MS_SHARED 마운트/언마운트 양방향 전파 MS_SLAVE master→slave 단방향 전파 MS_PRIVATE 전파 없음 (기본값) MS_UNBINDABLE 전파 없음 + bind mount 불가 struct mount: mnt_parent(부모), mnt_mounts(자식), mnt_share(shared peer), mnt_slave(slave) struct vfsmount: mnt_root(dentry), mnt_sb(super_block) — 읽기 전용 최소 구조체 struct mnt_namespace: list(마운트 목록), seq(마운트 변경 시퀀스), poll 알림 mount_hashtable: 마운트 지점 빠른 조회 (parent vfsmount + dentry 해시)
마운트 트리: namespace별 독립된 마운트 계층, propagation으로 이벤트 전파 제어
# mount propagation 설정

# shared: 마운트 이벤트가 양방향 전파
mount --make-shared /mnt/data

# slave: master→slave 단방향 전파 (컨테이너에서 호스트 마운트 수신)
mount --make-slave /mnt/data

# private: 전파 완전 차단 (기본값)
mount --make-private /mnt/data

# unbindable: 전파 차단 + bind mount 불가
mount --make-unbindable /mnt/data

# recursive 변경 (하위 마운트 모두 포함)
mount --make-rshared /mnt/data

# Docker/Podman은 컨테이너 루트를 rslave로 설정하여
# 호스트의 마운트 변경을 수신하되 컨테이너 마운트는 호스트에 영향 없음
# /proc/self/mountinfo 파싱: 마운트 트리 상세 정보
cat /proc/self/mountinfo
# 형식: mount_id parent_id major:minor root mount_point options ... - fs_type source super_options
# 예시:
# 25 1 8:1 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered
# 30 25 8:2 / /home rw,relatime shared:2 - ext4 /dev/sda2 rw
# "shared:N" — peer group ID, "master:N" — slave의 master

# findmnt: 트리 형태로 마운트 표시
findmnt --tree --output TARGET,SOURCE,FSTYPE,PROPAGATION
# TARGET       SOURCE    FSTYPE PROPAGATION
# /            /dev/sda1 ext4   shared
# ├─/home      /dev/sda2 ext4   shared
# ├─/data      /dev/sdb1 xfs    private
# └─/proc      proc      proc   private

file_operations 디스패치 상세

사용자 공간의 read()/write() 호출은 VFS 계층을 거쳐 최종적으로 각 파일시스템이 등록한 file_operations의 콜백에 도달합니다. 이 디스패치 체인을 따라가며 각 단계의 역할을 살펴봅니다.

sys_read → 파일시스템별 read_iter 디스패치 sys_read(fd, buf, count) ksys_read() → fdget_pos(fd) → struct file vfs_read(file, buf, count, pos) rw_verify_area() → security_file_permission() f_op->read_iter 존재? → call_read_iter(file, &kiocb, &iter) ext4_file_read_iter() → generic_file_read_iter() → filemap_read() (페이지캐시) xfs_file_read_iter() → xfs_file_buffered_read() 또는 xfs_file_dio_read() (DIO) btrfs_file_read_iter() → generic_file_read_iter() + 체크섬 검증 a_ops->read_folio() / readahead() → 페이지 캐시 적재 or iomap_dio_rw() → submit_bio() (Direct I/O 경로) struct kiocb ki_pos: 현재 오프셋 ki_flags: IOCB_DIRECT 등 copy_to_iter() → 사용자 버퍼에 복사
read() 디스패치: sys_read → vfs_read → f_op->read_iter → 파일시스템별 구현
/* file_operations 구조체 — 핵심 콜백 필드 */
struct file_operations {
    struct module *owner;

    /* 읽기/쓰기: iter 기반 (현대 커널 권장) */
    ssize_t (*read_iter)(struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter)(struct kiocb *, struct iov_iter *);

    /* splice: zero-copy 파이프 연동 */
    ssize_t (*splice_read)(struct file *, loff_t *,
                          struct pipe_inode_info *, size_t, unsigned int);

    /* mmap: 메모리 매핑 */
    int (*mmap)(struct file *, struct vm_area_struct *);

    /* 디렉토리 열거 */
    int (*iterate_shared)(struct file *, struct dir_context *);

    /* ioctl */
    long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);

    /* 동기화 */
    int (*fsync)(struct file *, loff_t, loff_t, int datasync);

    /* fallocate: 사전 할당 / hole punch / zero range */
    long (*fallocate)(struct file *, int, loff_t, loff_t);

    /* 잠금 */
    int (*flock)(struct file *, int, struct file_lock *);
    /* ... */
};
/* read_iter / write_iter 패턴: kiocb + iov_iter */
static ssize_t myfs_file_read_iter(struct kiocb *iocb,
                                    struct iov_iter *to)
{
    struct file *file = iocb->ki_filp;
    struct inode *inode = file_inode(file);

    /* Direct I/O 경로 */
    if (iocb->ki_flags & IOCB_DIRECT) {
        /* 페이지 캐시 우회 → iomap 또는 직접 BIO */
        return myfs_direct_read(iocb, to);
    }

    /* Buffered I/O: generic 구현 사용 (페이지 캐시 경유) */
    return generic_file_read_iter(iocb, to);
    /* → filemap_read() → a_ops->read_folio() */
}

/* iov_iter: scatter-gather 버퍼 추상화
 * ITER_UBUF (단일 사용자 버퍼), ITER_IOVEC (여러 사용자 버퍼),
 * ITER_BVEC (커널 bio_vec), ITER_KVEC (커널 kvec),
 * ITER_PIPE (파이프 페이지) */

address_space_operations

address_space_operations(a_ops)는 페이지 캐시와 실제 저장소 사이의 데이터 전송을 담당하는 콜백 테이블입니다. 각 파일시스템은 이 인터페이스를 구현하여 페이지 읽기/쓰기, dirty 관리, Direct I/O를 제공합니다.

address_space_operations 주요 콜백 흐름 struct address_space i_pages (xarray) + a_ops (콜백 테이블) 읽기 콜백 read_folio(file, folio) 단일 folio를 디스크에서 적재 (동기, page fault/readpage용) readahead(rac) 선행 읽기: 여러 folio를 비동기 적재 (순차 읽기 최적화) 쓰기 콜백 writepages(mapping, wbc) dirty 페이지 일괄 기록 (writeback 워커에서 호출) dirty_folio(mapping, folio) folio를 dirty로 마킹 (저널 FS: 트랜잭션에 연결) Direct I/O 경로 direct_IO(iocb, iter) (레거시, iomap으로 대체 중) a_ops 콜백 호출 경로 read_folio: filemap_read() 캐시 미스 → filemap_get_pages() → read_folio() readahead: page_cache_ra_unbounded() → read_pages() → readahead() writepages: wb_workfn() → writeback_sb_inodes() → __writeback_single_inode() → writepages() dirty_folio: __set_page_dirty() → a_ops->dirty_folio() (ext4: ext4_dirty_folio → 저널 체크)
a_ops: 페이지 캐시와 저장소를 연결하는 핵심 콜백 인터페이스
/* address_space_operations 구현 템플릿 */
static const struct address_space_operations myfs_aops = {
    /* 읽기: 단일 folio 적재 (page fault, readpage) */
    .read_folio     = myfs_read_folio,
    /* 읽기: 선행 읽기 (readahead window) */
    .readahead      = myfs_readahead,

    /* 쓰기: dirty 페이지 일괄 기록 */
    .writepages     = myfs_writepages,
    /* 쓰기: 단일 페이지 기록 (writepage 레거시, writepages 권장) */
    .writepage      = myfs_writepage,

    /* dirty 마킹: write() 후 호출 */
    .dirty_folio    = filemap_dirty_folio,  /* 대부분 generic 사용 */

    /* write_begin / write_end: buffered write 경로
     * write_begin: 블록 할당 + folio 준비
     * write_end: dirty 마킹 + i_size 갱신 */
    .write_begin    = myfs_write_begin,
    .write_end      = myfs_write_end,

    /* invalidate: truncate/hole punch 시 페이지 무효화 */
    .invalidate_folio = myfs_invalidate_folio,

    /* release: folio 회수 전 정리 (private 데이터 해제) */
    .release_folio  = myfs_release_folio,

    /* Direct I/O (레거시, iomap 사용 시 불필요) */
    .direct_IO      = noop_direct_IO,

    /* swap: swap 파일 지원 */
    .swap_activate  = myfs_swap_activate,
};
folio 전환: 커널 5.16+에서 struct page 기반 API가 struct folio로 전환되고 있습니다. read_folioreadpage를, dirty_folioset_page_dirty를 대체합니다. folio는 compound page를 자연스럽게 지원하여 대형 I/O에 유리합니다.

iomap 프레임워크

iomap은 파일시스템의 블록 매핑 로직을 VFS/페이지 캐시에서 분리하는 현대적 프레임워크입니다. XFS에서 시작하여 ext4, gfs2, zonefs 등으로 확산되었으며, buffered I/O, Direct I/O, DAX, fiemap, seek hole/data를 통합 처리합니다.

iomap 프레임워크 아키텍처 VFS: read_iter / write_iter / page fault iomap_read_folio() iomap_writepages() Buffered I/O iomap_dio_rw() Direct I/O 페이지 캐시 우회 dax_iomap_rw() DAX (PMEM) CPU load/store 직접 iomap_fiemap() iomap_seek_hole/data() extent 쿼리 iomap_iter() — 공통 반복 루프 오프셋을 청크 단위로 분할, 각 청크마다 begin → actor → end ops->iomap_begin() FS가 구현: 논리→물리 블록 매핑 struct iomap 채움 ops->iomap_end() 후처리: 에러 시 할당 취소 delalloc → 실제 extent 변환 struct iomap — 매핑 결과 addr: 물리 블록 주소 (디스크상 위치) offset: 파일 내 논리 오프셋 length: 매핑 길이 bdev: 블록 디바이스 dax_dev: DAX 디바이스 (PMEM용) type: IOMAP_MAPPED IOMAP_HOLE IOMAP_DELALLOC IOMAP_UNWRITTEN IOMAP_INLINE flags: IOMAP_F_NEW, IOMAP_F_DIRTY, ...
iomap: iomap_begin()으로 블록 매핑 → 타입별 actor 실행 → iomap_end() 후처리
/* iomap_ops: 파일시스템이 구현하는 매핑 콜백 */
struct iomap_ops {
    /* 논리 → 물리 블록 매핑 */
    int (*iomap_begin)(struct inode *inode, loff_t pos,
                       loff_t length, unsigned flags,
                       struct iomap *iomap, struct iomap *srcmap);

    /* 매핑 후처리 (optional) */
    int (*iomap_end)(struct inode *inode, loff_t pos,
                     loff_t length, ssize_t written,
                     unsigned flags, struct iomap *iomap);
};

/* flags: IOMAP_WRITE, IOMAP_ZERO, IOMAP_DIRECT, IOMAP_REPORT 등 */
/* iomap_begin 콜백 예제: XFS 스타일 */
static int myfs_iomap_begin(struct inode *inode, loff_t pos,
                            loff_t length, unsigned flags,
                            struct iomap *iomap, struct iomap *srcmap)
{
    struct myfs_extent ext;
    int ret;

    /* 1. extent tree에서 논리→물리 매핑 조회 */
    ret = myfs_get_extent(inode, pos, length, &ext);
    if (ret)
        return ret;

    /* 2. iomap 구조체 채우기 */
    iomap->offset = ext.logical;
    iomap->length = ext.length;
    iomap->bdev   = inode->i_sb->s_bdev;

    if (ext.flags & MYFS_EXTENT_HOLE) {
        iomap->type = IOMAP_HOLE;          /* 할당 안 된 영역 */
        iomap->addr = IOMAP_NULL_ADDR;
    } else if (ext.flags & MYFS_EXTENT_DELALLOC) {
        iomap->type = IOMAP_DELALLOC;      /* 지연 할당 예약만 */
        iomap->addr = IOMAP_NULL_ADDR;
    } else if (ext.flags & MYFS_EXTENT_UNWRITTEN) {
        iomap->type = IOMAP_UNWRITTEN;     /* 할당 완료, 미기록 */
        iomap->addr = ext.physical;
    } else {
        iomap->type = IOMAP_MAPPED;        /* 정상 매핑 */
        iomap->addr = ext.physical;
    }

    /* 쓰기 시: extent가 없으면 할당 */
    if ((flags & IOMAP_WRITE) && iomap->type == IOMAP_HOLE) {
        ret = myfs_alloc_extent(inode, pos, length, &ext);
        iomap->type = IOMAP_MAPPED;
        iomap->addr = ext.physical;
        iomap->flags |= IOMAP_F_NEW;
    }

    return 0;
}
iomap 채택 현황: XFS(완전), ext4(Direct I/O + buffered write 일부), gfs2, zonefs, erofs가 iomap을 사용합니다. btrfs는 자체 extent 관리 복잡성으로 아직 미전환입니다. iomap은 기존 buffer_head 기반 코드를 대체하여 대형 folio 지원과 코드 중복 제거에 기여합니다.

close() / fput() 시스템 콜 흐름

파일 디스크립터(File Descriptor)를 닫는 경로는 단순해 보이지만 레퍼런스 카운팅(Reference Counting), 태스크 워크(Task Work), RCU 유예 기간이 얽혀 있습니다. 핵심은 f_count가 0이 되는 시점과 실제 자원이 해제되는 시점이 다를 수 있다는 것입니다.

sys_close() → filp_close() 경로

/* fs/open.c — sys_close() 진입점 */
SYSCALL_DEFINE1(close, unsigned int, fd)
{
    int retval = close_fd(fd);
    /* ERESTARTSYS 재시작 방지: 이미 fd를 할당 해제했으므로 그냥 반환 */
    if (unlikely(retval == -ERESTARTSYS ||
                 retval == -ERESTARTNOINTR ||
                 retval == -ERESTARTNOHAND ||
                 retval == -ERESTART_RESTARTBLOCK))
        retval = -EINTR;
    return retval;
}

/* fs/file.c — close_fd(): fd 슬롯 해제 + filp_close() 호출 */
int close_fd(unsigned int fd)
{
    struct files_struct *files = current->files;
    struct file *file;

    spin_lock(&files->file_lock);
    file = pick_file(files, fd);  /* fdtable에서 제거 */
    spin_unlock(&files->file_lock);

    if (!file)
        return -EBADF;

    return filp_close(file, files);
}

/* fs/open.c — filp_close(): flush + POSIX 잠금 해제 */
int filp_close(struct file *filp, fl_owner_t id)
{
    int retval = 0;

    if (filp->f_op->flush)
        retval = filp->f_op->flush(filp, id);  /* 예: NFS 위임 반납 */

    if (likely(!(filp->f_mode & FMODE_PATH))) {
        dnotify_flush(filp, id);
        locks_remove_posix(filp, id);  /* POSIX 잠금 전부 해제 */
    }

    fput(filp);   /* f_count 감소 → 0이면 __fput() 예약 */
    return retval;
}
close_fd() vs put_unused_fd(): close_fd()는 fdtable에서 슬롯을 제거하고 filp_close()까지 호출합니다. 반면 put_unused_fd()get_unused_fd_flags()로 예약만 했다가 실패한 경우처럼, 아직 struct file이 연결되지 않은 fd 슬롯을 반환할 때 사용합니다.

fput() → __fput() 지연 실행

/* fs/file_table.c — fput(): f_count 감소, 0이면 지연 실행 예약 */
void fput(struct file *file)
{
    if (atomic_long_dec_and_test(&file->f_count)) {
        struct task_struct *task = current;

        if (likely(!in_interrupt() && !(task->flags & PF_KTHREAD))) {
            /* 일반 프로세스 컨텍스트: task_work로 현재 태스크 종료 시 실행 */
            init_task_work(&file->f_u.fu_rcuhead, ____fput);
            if (!task_work_add(task, &file->f_u.fu_rcuhead, TWA_RESUME))
                return;
        }
        /* 인터럽트·커널 스레드 컨텍스트: delayed_fput_list에 추가 */
        delayed_fput(file);
    }
}

/* delayed_fput: 전용 워크큐(workqueue)에서 처리 */
static void delayed_fput(struct file *file)
{
    llist_add(&file->f_u.fu_llist, &delayed_fput_list);
    schedule_delayed_work(&delayed_fput_work, 1);
}
왜 지연 실행인가? __fput()mntput(), dput(), iput()을 호출하며 잠금을 취득하고 파일시스템 콜백을 수행합니다. 인터럽트 컨텍스트에서는 이런 무거운 작업이 금지되므로, task_work나 delayed_work를 통해 안전한 프로세스 컨텍스트에서 실행합니다.
/* fs/file_table.c — __fput(): 실제 struct file 자원 해제 */
static void __fput(struct file *file)
{
    struct dentry *dentry = file->f_path.dentry;
    struct vfsmount *mnt = file->f_path.mnt;
    struct inode *inode = file->f_inode;

    security_file_free(file);

    if (unlikely(file->f_flags & FASYNC)) {
        if (file->f_op->fasync)
            file->f_op->fasync(-1, file, 0);  /* FASYNC 해제 */
    }

    if (file->f_op->release)
        file->f_op->release(inode, file);    /* FS별 release 콜백 */

    if (unlikely(S_ISCHR(inode->i_mode) && inode->i_cdev))
        cdev_put(inode->i_cdev);

    fops_put(file->f_op);                    /* module refcount 감소 */
    put_pid(file->f_owner.pid);

    if (likely(file->f_mode & FMODE_OPENED))
        inode_dec_open_file_count(inode);

    fmode_set_opened(file, 0);

    dput(dentry);                             /* dentry 참조 반납 */
    mntput(mnt);                              /* vfsmount 참조 반납 */

    file_free(file);                          /* kmem_cache_free() */
}

RCU 유예 기간과 락리스(Lockless) fd 조회

커널은 fd 테이블을 RCU(Read-Copy-Update)로 보호합니다. rcu_read_lock() 구간에서 fget_light()로 파일 포인터를 가져오므로, close_fd()가 슬롯을 지운 직후에도 RCU 유예 기간(Grace Period) 동안 포인터가 유효하게 유지됩니다. fput()f_count를 0으로 만들어도 task_work가 실행될 때까지 실제 메모리 해제가 일어나지 않으므로 use-after-free가 방지됩니다.

/* include/linux/file.h — RCU-safe fd → struct file 변환 */
static inline struct file *__fget_light(unsigned int fd, fmode_t mask)
{
    struct files_struct *files = current->files;
    struct file *file;

    /* RCU read lock 구간에서 fdtable 포인터 읽기 */
    rcu_read_lock();
    file = files_lookup_fd_rcu(files, fd);
    if (file) {
        if (unlikely(file->f_mode & mask)) {
            file = NULL;
        } else {
            /* f_count++ : 조회 중 fput()이 와도 안전 */
            if (!atomic_long_inc_not_zero(&file->f_count))
                file = NULL;
        }
    }
    rcu_read_unlock();
    return file;
}

dup() / fork()와 f_count

dup()이나 fork()는 동일한 struct file을 여러 fd가 가리키게 합니다. f_countstruct file의 참조 카운트로, 모든 참조자가 fput()을 호출해야 __fput()이 실행됩니다.

close() / fput() 흐름 sys_close(fd) SYSCALL_DEFINE1 close_fd(fd) pick_file(): fdtable 슬롯 제거 filp_close(filp, id) f_op->flush() — NFS 위임 반납 등 locks_remove_posix() — POSIX 잠금 해제 fput(file) atomic_long_dec_and_test(&f_count) f_count > 0 반환 (파일 유지) dup/fork 공유 중 f_count == 0 컨텍스트 판단 in_interrupt() || PF_KTHREAD? 일반 프로세스 task_work_add() 태스크 종료 시 ____fput() 인터럽트/kthread delayed_fput_list schedule_delayed_work() __fput(file) f_op->release() → mntput() → dput() → file_free() inode 참조 반납 → iput() → 필요시 evict_inode()
sys_close() → close_fd() → filp_close() → fput() → __fput() 흐름. f_count가 0이 될 때만 __fput()이 예약됩니다.
/* dup()/fork() 시 f_count 증가 예시 */

/* dup(): 동일 struct file에 새 fd 추가 */
SYSCALL_DEFINE1(dup, unsigned int, fildes)
{
    return ksys_dup(fildes);
    /* 내부에서 get_file(file) → f_count++ */
}

/* fork(): 자식이 부모의 fdtable을 복사 */
/* copy_files() → dup_fd() → get_file() 로 f_count++ */

/* 결과: dup() 또는 fork() 후 close()는 f_count만 감소
 * 마지막 close()에서 __fput()이 실행됨 */
int fd1 = open("file", O_RDWR);    /* f_count = 1 */
int fd2 = dup(fd1);                /* f_count = 2 */
close(fd1);                        /* f_count = 1, __fput 미실행 */
close(fd2);                        /* f_count = 0, __fput() 예약 */
close() 에러 무시 금지: NFS 등 네트워크 파일시스템에서는 f_op->flush()가 서버에 지연된 쓰기를 플러시하므로, close()의 반환값이 -EIO가 될 수 있습니다. 이를 무시하면 데이터 손실이 발생할 수 있습니다.

truncate / fallocate VFS 경로

파일 크기를 변경하거나 디스크 공간을 예약하는 시스템 콜은 VFS 계층을 통해 파일시스템 고유 코드로 디스패치됩니다. truncate(2)는 파일을 줄이거나 늘리고, fallocate(2)는 실제 데이터 없이 블록을 미리 할당합니다.

sys_truncate() → do_truncate() 경로

/* fs/open.c — sys_truncate() 진입점 */
SYSCALL_DEFINE2(truncate, const char __user *, path, long, length)
{
    return do_sys_truncate(getname(path), length);
}

SYSCALL_DEFINE2(ftruncate, unsigned int, fd, unsigned long, length)
{
    return do_sys_ftruncate(fd, length, 1);
}

/* 공통: do_truncate() 호출 */
int do_truncate(struct mnt_idmap *idmap, struct dentry *dentry,
               loff_t length, unsigned int time_attrs,
               struct file *filp)
{
    int ret;
    struct iattr newattrs;

    /* iattr: 변경할 속성 기술 */
    newattrs.ia_size = length;
    newattrs.ia_valid = ATTR_SIZE | time_attrs;
    if (filp) {
        newattrs.ia_file = filp;
        newattrs.ia_valid |= ATTR_FILE;
    }

    inode_lock(dentry->d_inode);
    /* notify_change() → i_op->setattr() */
    ret = notify_change(idmap, dentry, &newattrs, NULL);
    inode_unlock(dentry->d_inode);

    return ret;
}

/* fs/attr.c — notify_change(): 보안 검사 + setattr 디스패치 */
int notify_change(struct mnt_idmap *idmap, struct dentry *dentry,
                  struct iattr *attr, struct inode **delegated_inode)
{
    struct inode *inode = dentry->d_inode;
    /* 1. security_inode_setattr() — LSM 검사 */
    /* 2. i_op->setattr(): FS별 구현 호출 */
    return inode->i_op->setattr(idmap, dentry, attr);
}

truncate_setsize() → 페이지 캐시 정리

/* mm/truncate.c — 파일시스템 setattr 내부에서 호출 */
void truncate_setsize(struct inode *inode, loff_t newsize)
{
    loff_t oldsize = inode->i_size;
    i_size_write(inode, newsize);

    if (newsize > oldsize)
        pagecache_isize_extended(inode, oldsize, newsize);

    /* 줄어든 영역의 페이지 캐시 제거 */
    truncate_pagecache(inode, newsize);
}

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

    /* new 이후 모든 페이지 무효화 */
    unmap_mapping_range(mapping, holebegin, 0, 1);
    invalidate_mapping_pages(mapping, holebegin >> PAGE_SHIFT, -1);
}

/* 범위 지정 버전 (fallocate punch_hole 등에서 사용) */
void truncate_inode_pages_range(struct address_space *mapping,
                                loff_t lstart, loff_t lend)
{
    /* lstart ~ lend 범위 페이지만 제거 */
    truncate_inode_pages_final(mapping);  /* 모든 dirty 페이지 처리 후 */
}
vmtruncate 패턴의 변화: 구 커널에서는 vmtruncate()가 truncate의 표준 경로였습니다. 현대 커널에서는 각 파일시스템이 i_op->setattr() 내에서 직접 truncate_setsize()truncate_inode_pages_range()를 호출합니다. 이로써 파일시스템이 블록 해제와 페이지 캐시 정리 순서를 직접 제어할 수 있습니다.

fallocate() → vfs_fallocate() 경로

/* fs/open.c — sys_fallocate() */
SYSCALL_DEFINE4(fallocate, int, fd, int, mode,
                loff_t, offset, loff_t, len)
{
    struct fd f = fdget(fd);
    int error;

    error = vfs_fallocate(f.file, mode, offset, len);
    fdput(f);
    return error;
}

/* fs/open.c — vfs_fallocate(): 권한 검사 + f_op->fallocate() */
int vfs_fallocate(struct file *file, int mode, loff_t offset, loff_t len)
{
    struct inode *inode = file_inode(file);

    /* 쓰기 권한 검사 */
    if (!(file->f_mode & FMODE_WRITE))
        return -EBADF;

    /* 정규 파일만 허용 */
    if (!S_ISREG(inode->i_mode) && !S_ISDIR(inode->i_mode))
        return -ESPIPE;

    /* file_operations에 fallocate가 없으면 EOPNOTSUPP */
    if (!file->f_op->fallocate)
        return -EOPNOTSUPP;

    sb_start_write(inode->i_sb);
    int ret = file->f_op->fallocate(file, mode, offset, len);
    sb_end_write(inode->i_sb);
    return ret;
}

fallocate 모드 플래그

플래그 동작 지원 FS
0 (기본) 0x00 범위 내 블록을 미리 할당. 파일 크기를 필요시 확장. FALLOC_FL_KEEP_SIZE 없으면 크기 변경 ext4, XFS, btrfs
FALLOC_FL_KEEP_SIZE 0x01 블록을 할당하되 i_size는 변경하지 않음. 파일 끝 너머 영역도 예약 가능 ext4, XFS, btrfs
FALLOC_FL_PUNCH_HOLE 0x02 지정 범위의 블록 해제 (hole 생성). 반드시 FALLOC_FL_KEEP_SIZE와 함께 사용 ext4, XFS, btrfs, f2fs
FALLOC_FL_COLLAPSE_RANGE 0x08 범위를 파일에서 완전히 제거하고 뒤 데이터를 앞으로 당김 (파일 크기 감소) ext4, XFS, f2fs
FALLOC_FL_ZERO_RANGE 0x10 범위를 0으로 초기화. 블록 할당은 유지하되 데이터만 제로화 ext4, XFS, btrfs
FALLOC_FL_INSERT_RANGE 0x20 범위 위치에 빈 공간 삽입하고 뒤 데이터를 뒤로 밀어냄 (파일 크기 증가) ext4, XFS, f2fs
FALLOC_FL_UNSHARE_RANGE 0x40 공유(CoW/reflink) 블록을 전용 복사본으로 만들어 독립화 btrfs, XFS
truncate / fallocate VFS 경로 sys_truncate() 경로명 기반 sys_ftruncate() fd 기반 sys_fallocate() fd + mode + offset + len do_truncate(idmap, dentry, length, ...) struct iattr 구성 → inode_lock() → notify_change() vfs_fallocate() 권한·타입 검사 → f_op->fallocate() notify_change() / security_inode_setattr() LSM 보안 검사 → inode->i_op->setattr() i_op->setattr() — FS별 구현 ext4_setattr(), xfs_setattr() 등 truncate_setsize(inode, newsize) i_size_write(inode, newsize) → truncate_pagecache(inode, newsize) FS 블록 할당/해제 ext4: ext4_ext_truncate() XFS: xfs_itruncate_extents() truncate_inode_pages_range() 페이지 캐시 정리: unmap_mapping_range() + invalidate_mapping_pages() fallocate mode 예시 0: 블록 미리 할당 + 파일 크기 확장 PUNCH_HOLE: 범위 블록 해제 (hole 생성) COLLAPSE_RANGE: 범위 제거 → 뒤 당김
truncate/ftruncate는 do_truncate() → notify_change() → i_op->setattr() → truncate_setsize() 경로를, fallocate는 vfs_fallocate() → f_op->fallocate() 경로를 사용합니다.
PUNCH_HOLE 사용 시 주의: FALLOC_FL_PUNCH_HOLE은 반드시 FALLOC_FL_KEEP_SIZE와 OR해서 사용해야 합니다. 해제된 범위는 sparse hole이 되어 이후 읽으면 0이 반환됩니다. 데이터베이스나 로그 파일에서 공간 재활용에 유용합니다.

파일 삭제와 이름 변경은 디렉토리 inode의 엔트리를 조작하는 작업입니다. 주의할 점은 unlink()가 inode를 즉시 해제하지 않는다는 것으로, 파일이 열려 있으면 마지막 close()까지 실제 데이터가 유지됩니다.

/* fs/namei.c — sys_unlinkat() 진입점 */
SYSCALL_DEFINE3(unlinkat, int, dfd, const char __user *, pathname, int, flag)
{
    if (flag & AT_REMOVEDIR)
        return do_rmdir(dfd, getname(pathname));
    return do_unlinkat(dfd, getname(pathname));
}

static long do_unlinkat(int dfd, struct filename *name)
{
    struct dentry *dentry;
    struct path path;
    struct qstr last;
    int type;
    struct inode *inode;
    int error;

    /* 경로 탐색: 부모 디렉토리까지 resolve */
    error = filename_parentat(dfd, name, 0, &path, &last, &type);

    inode_lock_nested(path.dentry->d_inode, I_MUTEX_PARENT);
    dentry = __lookup_hash(&last, path.dentry, 0);
    inode = dentry->d_inode;

    /* 마운트포인트, 디렉토리는 unlink 불가 */
    error = vfs_unlink(idmap, path.dentry->d_inode, dentry, &delegated_inode);

    inode_unlock(path.dentry->d_inode);
    return error;
}

/* fs/namei.c — vfs_unlink(): 보안 검사 + i_op->unlink() */
int vfs_unlink(struct mnt_idmap *idmap, struct inode *dir,
               struct dentry *dentry, struct inode **delegated_inode)
{
    struct inode *target = dentry->d_inode;
    int error;

    dget(dentry);
    inode_lock(target);

    /* nlink > 0 확인, 마운트포인트 체크 */
    error = dir->i_op->unlink(dir, dentry);  /* FS별 구현 */

    if (!error) {
        dont_mount(dentry);
        detach_mounts(dentry);
    }

    inode_unlock(target);
    if (!error)
        d_delete(dentry);   /* dcache에서 제거 */
    dput(dentry);
    return error;
}
/* include/linux/fs.h — nlink 조작 함수 */

/* 하드 링크 수 1 감소 (unlink, rmdir에서 사용) */
static inline void drop_nlink(struct inode *inode)
{
    WARN_ON(inode->__i_nlink == 0);
    inode->__i_nlink--;
    if (!inode->i_nlink)
        atomic_long_inc(&inode->i_sb->s_remove_count);
}

/* nlink를 0으로 강제 설정 (unlink + 모든 링크 한번에 제거) */
static inline void clear_nlink(struct inode *inode)
{
    if (inode->i_nlink) {
        inode->__i_nlink = 0;
        atomic_long_inc(&inode->i_sb->s_remove_count);
    }
}

/* FS 구현 예: ext4_unlink() 핵심 흐름 */
static int ext4_unlink(struct inode *dir, struct dentry *dentry)
{
    struct inode *inode = d_inode(dentry);
    /* 저널 트랜잭션 시작 */
    /* 디렉토리 엔트리 제거: ext4_delete_entry() */
    drop_nlink(inode);        /* nlink 감소 */
    /* nlink == 0이면 inode 삭제 예약 */
    ext4_orphan_add(handle, inode);  /* orphan list: 재부팅 복구용 */
    /* 저널 트랜잭션 종료 */
    return 0;
}
d_delete() vs d_drop(): d_delete()는 dentry를 unhashed 상태로 만들어 dcache 조회에서 숨깁니다. nlink가 0이 아니라면 negative dentry로 전환합니다. d_drop()은 단순히 dcache 해시에서 제거하며, 주로 NFS 같은 네트워크 파일시스템에서 일관성 검사 실패 후 무효화 용도로 사용합니다.

파일이 열려 있는 상태에서 unlink()를 호출하면 디렉토리 엔트리만 제거되고 inode와 데이터 블록은 마지막 파일 디스크립터가 닫힐 때까지 유지됩니다. 이는 임시 파일 관용구(open() + unlink())의 동작 기반입니다.

/* 임시 파일 관용구: 디렉토리 엔트리 없이 파일 사용 */
int fd = open("/tmp/work", O_RDWR | O_CREAT, 0600);
unlink("/tmp/work");
/* 이후 /tmp/work는 디렉토리에서 보이지 않지만,
 * fd는 여전히 유효. 데이터는 close(fd)까지 보존됨 */
write(fd, data, len);
close(fd);  /* 여기서 inode 참조 카운트 = 0 → evict_inode() */

sys_rmdir() → vfs_rmdir() 경로

/* fs/namei.c — vfs_rmdir() */
int vfs_rmdir(struct mnt_idmap *idmap, struct inode *dir,
              struct dentry *dentry)
{
    int error;

    dget(dentry);
    inode_lock(dentry->d_inode);

    /* 디렉토리가 비어 있는지 확인 (ENOTEMPTY) */
    error = dir->i_op->rmdir(dir, dentry);  /* e.g. ext4_rmdir() */

    if (!error) {
        shrink_dcache_parent(dentry);
        dentry->d_inode->i_flags |= S_DEAD;
        dont_mount(dentry);
        detach_mounts(dentry);
        d_delete(dentry);  /* dcache에서 제거 */
    }

    inode_unlock(dentry->d_inode);
    dput(dentry);
    return error;
}

sys_renameat2() → vfs_rename() 경로

/* fs/namei.c — sys_renameat2() → vfs_rename() */
SYSCALL_DEFINE5(renameat2, int, olddfd, const char __user *, oldname,
                int, newdfd, const char __user *, newname, unsigned int, flags)
{
    return do_renameat2(olddfd, getname(oldname), newdfd,
                        getname(newname), flags);
}

/* 크로스 디렉토리 rename: 잠금 순서 보장 필수 */
int vfs_rename(struct renamedata *rd)
{
    bool is_dir = d_is_dir(rd->old_dentry);
    struct inode *source = rd->old_dentry->d_inode;
    struct inode *target = rd->new_dentry->d_inode;

    /* security_inode_rename() 검사 */
    /* source 잠금 */
    inode_lock(source);
    if (target)
        inode_lock(target);

    error = rd->old_dir->i_op->rename(
        rd->delegated_inode, rd->old_dir, rd->old_dentry,
        rd->new_dir, rd->new_dentry, rd->flags);

    if (!error) {
        if (target && !is_dir)
            drop_nlink(target);  /* 덮어쓴 파일 nlink 감소 */
        d_move(rd->old_dentry, rd->new_dentry); /* dcache 위치 변경 */
    }
    return error;
}

renameat2 플래그

플래그 동작 비고
0 (기본) old → new로 이동. new가 이미 존재하면 교체(원자적) POSIX rename() 시맨틱
RENAME_NOREPLACE new가 이미 존재하면 EEXIST 반환. 교체하지 않음 Linux 3.15+, i_op->rename 지원 FS만
RENAME_EXCHANGE old와 new를 원자적으로 교체. 둘 다 존재해야 함 Linux 3.15+, ext4/XFS/btrfs 지원
RENAME_WHITEOUT old 위치에 whiteout 항목 생성 (overlayfs용) Linux 3.18+, overlayfs copy-up에 사용

크로스 디렉토리 lock_rename()

/* fs/namei.c — 크로스 디렉토리 rename 시 데드락 방지 잠금 */
struct inode *lock_rename(struct dentry *p1, struct dentry *p2)
{
    struct inode *inode;

    if (p1 == p2) {
        inode_lock_nested(p1->d_inode, I_MUTEX_PARENT);
        return NULL;
    }

    /* 공통 조상 찾기 → 계층 순서로 잠금 취득 (데드락 방지) */
    inode = lock_rename_child(p1, p2);
    return inode;
}

void unlock_rename(struct dentry *p1, struct dentry *p2)
{
    inode_unlock(p1->d_inode);
    if (p1 != p2)
        inode_unlock(p2->d_inode);
}
/* 규칙: 항상 lock_rename() / unlock_rename() 쌍으로 사용
 * 직접 inode_lock()을 양쪽 디렉토리에 걸면 데드락 위험 */
크로스 디렉토리 rename의 위험: 두 디렉토리의 inode를 동시에 잠글 때는 반드시 lock_rename()을 사용해야 합니다. 직접 inode_lock()을 순서 없이 호출하면 다른 스레드와 잠금 순서가 뒤바뀌어 AB-BA 데드락(Deadlock)이 발생합니다. lock_rename()은 공통 조상을 기준으로 계층 순서를 보장합니다.
unlink 수명주기 — 열린 파일 + unlink 시나리오 초기 상태: fd=5 열림, dentry → inode (nlink=1, i_count=2) fdtable[5] → struct file (f_count=1) → dentry → inode / dir → dentry → inode unlink("/path/file") unlink() 완료: 디렉토리 엔트리 제거, inode nlink=0 dir의 dentry 제거 (d_delete), inode.i_nlink=0, i_count=2 (struct file 참조 유지) orphan list에 inode 등록 (ext4: 재부팅 후 복구 보장) fd=5는 여전히 유효 디렉토리에서 보이지 않음 ls로 조회 불가 (dentry 제거됨) 새로운 open() 불가 fd=5로 계속 접근 가능 read/write/fstat 정상 동작 데이터 블록 유지 (i_count > 0) close(fd=5) fput() → f_count=0 → __fput() 예약 iput() 호출 → i_count 감소 → i_count==0 판단 iput_final() → evict(inode) i_op->evict_inode(): 데이터 블록 해제 + on-disk inode 삭제 완전 삭제: 데이터 블록 반환, inode 해제 orphan list에서 제거, 디스크 공간 반환, struct inode 메모리 해제
unlink() 후에도 열린 fd가 있으면 i_count > 0을 유지합니다. 마지막 close() 시점에 evict_inode()로 실제 데이터 블록이 해제됩니다.
임시 파일 안전 패턴: open() 직후 unlink()를 호출하면 디렉토리에 남은 흔적 없이 파일을 사용할 수 있습니다. 프로세스가 비정상 종료해도 OS가 마지막 fd를 닫으면서 자동으로 정리됩니다. 현대 커널에서는 O_TMPFILE 플래그로 이 패턴을 더 안전하게 지원합니다.

mkdir / mknod / symlink / link VFS 경로

파일 생성 계열 시스템 콜은 VFS를 통해 각 파일시스템(Filesystem)의 inode_operations 콜백으로 디스패치됩니다. VFS 공통 함수인 vfs_mkdir(), vfs_mknod(), vfs_symlink(), vfs_link()는 권한 검사, LSM 훅(Hook), fsnotify 알림을 일관되게 처리한 뒤 실제 파일시스템 구현을 호출합니다.

vfs_mkdir() 흐름

mkdir() 시스템 콜은 do_mkdirat()을 거쳐 vfs_mkdir()에 도달합니다. VFS는 보안 훅을 검사한 뒤 디렉토리 inode의 i_op->mkdir()를 호출합니다. 파일시스템은 새 inode를 할당하고 d_instantiate()로 dentry와 연결합니다.

/* fs/namei.c — vfs_mkdir() 핵심 흐름 (축약) */
int vfs_mkdir(struct mnt_idmap *idmap, struct inode *dir,
              struct dentry *dentry, umode_t mode)
{
    int error;

    /* 1. 권한 검사: MAY_WRITE | MAY_EXEC on dir */
    error = may_create(idmap, dir, dentry);
    if (error)
        return error;

    /* 2. i_op->mkdir 존재 여부 확인 */
    if (!dir->i_op->mkdir)
        return -EPERM;

    /* 3. LSM 보안 훅 */
    error = security_inode_mkdir(dir, dentry, mode);
    if (error)
        return error;

    /* 4. 파일시스템 구현 호출 (예: ext4_mkdir) */
    error = dir->i_op->mkdir(idmap, dir, dentry, mode);
    if (error)
        return error;

    /* 5. fsnotify 이벤트 발송 */
    fsnotify_mkdir(dir, dentry);
    return 0;
}

ext4_mkdir() 구현 예제

ext4의 ext4_mkdir()는 디렉토리 생성의 대표적인 구현입니다. 새 inode를 할당하고 ... 엔트리를 추가한 뒤 d_instantiate()로 dentry를 완성합니다.

/* fs/ext4/namei.c — ext4_mkdir() (단순화) */
static int ext4_mkdir(struct mnt_idmap *idmap,
                      struct inode *dir, struct dentry *dentry,
                      umode_t mode)
{
    handle_t *handle;
    struct inode *inode;
    int err, credits;

    /* 링크 수 한계 검사 (EXT4_LINK_MAX = 65000) */
    if (EXT4_DIR_LINK_MAX(dir))
        return -EMLINK;

    /* JBD2 트랜잭션 시작 */
    credits = EXT4_DATA_TRANS_BLOCKS(dir->i_sb) +
              EXT4_INDEX_EXTRA_TRANS_BLOCKS + 3;
    handle = ext4_journal_start(dir, EXT4_HT_DIR, credits);
    if (IS_ERR(handle))
        return PTR_ERR(handle);

    /* 새 inode 할당 (S_IFDIR | mode) */
    inode = ext4_new_inode_start_handle(idmap, dir,
                S_IFDIR | mode, &dentry->d_name, 0, NULL,
                EXT4_HT_DIR, credits);
    if (IS_ERR(inode)) {
        err = PTR_ERR(inode);
        goto out_stop;
    }

    /* inode_operations / file_operations 설정 */
    inode->i_op  = &ext4_dir_inode_operations;
    inode->i_fop = &ext4_dir_operations;

    /* . 과 .. 디렉토리 엔트리 추가 */
    err = ext4_init_new_dir(handle, dir, inode);
    if (err)
        goto out_clear_inode;

    /* 부모 디렉토리 링크 수 증가 */
    ext4_inc_count(dir);
    ext4_update_dx_flag(dir);
    err = ext4_mark_inode_dirty(handle, dir);

    /* dentry ↔ inode 연결 — dcache에 등록 */
    d_instantiate(dentry, inode);
    unlock_new_inode(inode);

out_stop:
    ext4_journal_stop(handle);
    return err;
}

vfs_mknod() — 장치/FIFO/소켓 노드

vfs_mknod()는 일반 파일이 아닌 특수 파일(디바이스 파일, FIFO, 유닉스 소켓)을 생성합니다. 파일시스템의 i_op->mknod()를 호출하고, 파일시스템은 내부적으로 init_special_inode()를 사용해 inode 유형에 맞는 file_operations를 설정합니다.

/* fs/namei.c — vfs_mknod() (축약) */
int vfs_mknod(struct mnt_idmap *idmap, struct inode *dir,
              struct dentry *dentry, umode_t mode, dev_t dev)
{
    int error;

    /* 블록/문자 장치 생성: CAP_MKNOD 권한 필요 */
    if (S_ISBLK(mode) || S_ISCHR(mode)) {
        error = capable(CAP_MKNOD) ? 0 : -EPERM;
        if (error)
            return error;
    }

    error = may_create(idmap, dir, dentry);
    if (error)
        return error;

    if (!dir->i_op->mknod)
        return -EPERM;

    /* LSM: 장치 유형별 훅 (SELinux, AppArmor 등) */
    error = security_inode_mknod(dir, dentry, mode, dev);
    if (error)
        return error;

    error = dir->i_op->mknod(idmap, dir, dentry, mode, dev);
    if (!error)
        fsnotify_create(dir, dentry);  /* inotify/fanotify 알림 */
    return error;
}

/* 파일시스템 내부: init_special_inode() 로 유형 결정 */
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
    inode->i_mode = mode;
    if (S_ISCHR(mode)) {
        inode->i_fop = &def_chr_fops;   /* 문자 장치 ops */
        inode->i_rdev = rdev;
    } else if (S_ISBLK(mode)) {
        inode->i_fop = &def_blk_fops;   /* 블록 장치 ops */
        inode->i_rdev = rdev;
    } else if (S_ISFIFO(mode)) {
        inode->i_fop = &pipefifo_fops;  /* FIFO(named pipe) ops */
    } else if (S_ISSOCK(mode)) {
        inode->i_fop = &bad_sock_fops;  /* 소켓 — 직접 open 불가 */
    }
}

심볼릭 링크(Symbolic Link)는 vfs_symlink()를 통해 생성됩니다. 파일시스템은 i_op->symlink()에서 링크 대상 경로를 inode에 저장합니다. 짧은 경로는 page_symlink()를 통해 페이지 캐시에, 긴 경로는 inode 내부 버퍼에 저장할 수 있습니다.

/* fs/namei.c — vfs_symlink() (축약) */
int vfs_symlink(struct mnt_idmap *idmap, struct inode *dir,
                struct dentry *dentry, const char *oldname)
{
    int error;

    error = may_create(idmap, dir, dentry);
    if (error)
        return error;

    if (!dir->i_op->symlink)
        return -EPERM;

    error = security_inode_symlink(dir, dentry, oldname);
    if (error)
        return error;

    /* FS가 링크 대상을 inode에 저장 */
    error = dir->i_op->symlink(idmap, dir, dentry, oldname);
    if (!error)
        fsnotify_create(dir, dentry);
    return error;
}

/* VFS 유틸: 짧은 심볼릭 링크 경로를 페이지 캐시에 저장 */
int page_symlink(struct inode *inode, const char *symname, int len);

/* 심볼릭 링크 읽기: i_op->get_link() 콜백 */
/* page_get_link()는 페이지 캐시에서 경로 문자열 반환 */
const char *page_get_link(struct dentry *dentry, struct inode *inode,
                          struct delayed_call *callback);

하드 링크(Hard Link)는 vfs_link()를 통해 생성되며, 기존 inode의 링크 수(i_nlink)를 증가시킵니다. 디렉토리 하드 링크는 .. 루프를 방지하기 위해 거부됩니다.

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

    /* 디렉토리 하드 링크 금지 */
    if (S_ISDIR(inode->i_mode))
        return -EPERM;

    /* 링크 수 한계 확인 */
    if (inode->i_nlink == 0 && !(inode->i_state & I_LINKABLE))
        return -ENOENT;
    if (inode_nlink_locked(inode) >= dir->i_sb->s_max_links)
        return -EMLINK;

    error = security_inode_link(old_dentry, dir, new_dentry);
    if (error)
        return error;

    /* FS 구현 호출 — inode_inc_link_count()로 i_nlink 증가 */
    error = dir->i_op->link(old_dentry, dir, new_dentry);
    if (!error)
        fsnotify_link(dir, inode, new_dentry);
    return error;
}

/* inode nlink 관리 헬퍼 */
static inline void inode_inc_link_count(struct inode *inode)
{
    inc_nlink(inode);              /* i_nlink 원자적 증가 */
    mark_inode_dirty(inode);       /* writeback 대기열에 추가 */
}

static inline void inode_dec_link_count(struct inode *inode)
{
    drop_nlink(inode);             /* i_nlink 감소 — 0이면 iput()이 삭제 트리거 */
    mark_inode_dirty(inode);
}

fsnotify 알림 통합

파일 생성·변경 이벤트는 fsnotify 서브시스템을 통해 inotify, fanotify, dnotify로 전달됩니다. VFS 공통 경로에서 이벤트가 발생하므로, 파일시스템 구현과 무관하게 모든 파일시스템에서 동작합니다.

VFS 함수 fsnotify 이벤트 함수 inotify 이벤트
vfs_create() fsnotify_create(dir, dentry) IN_CREATE
vfs_mkdir() fsnotify_mkdir(dir, dentry) IN_CREATE | IN_ISDIR
vfs_mknod() fsnotify_create(dir, dentry) IN_CREATE
vfs_symlink() fsnotify_create(dir, dentry) IN_CREATE
vfs_link() fsnotify_link(dir, inode, new_dentry) IN_CREATE
vfs_unlink() fsnotify_unlink(dir, dentry) IN_DELETE
vfs_rmdir() fsnotify_rmdir(dir, dentry) IN_DELETE | IN_ISDIR

보안 훅 (LSM)

각 생성 연산마다 LSM(Linux Security Module) 훅이 실제 파일시스템 콜백 직전에 호출됩니다. SELinux, AppArmor, Smack 등 LSM은 이 훅에서 정책을 검사합니다.

VFS 함수 LSM 훅 검사 내용
vfs_create() security_inode_create() 파일 생성 권한, SELinux 레이블
vfs_mkdir() security_inode_mkdir() 디렉토리 생성 권한
vfs_mknod() security_inode_mknod() 특수 파일 생성 (장치 접근 포함)
vfs_symlink() security_inode_symlink() 심볼릭 링크 대상 경로 검사
vfs_link() security_inode_link() 하드 링크 생성 — 크로스 레이블 차단
vfs_unlink() security_inode_unlink() 파일 삭제 권한

실용 예제 — 사용자 공간에서 VFS 경로까지

다음은 사용자 프로그램의 호출이 VFS를 통해 파일시스템 구현에 도달하는 전체 경로입니다.

/* --- 사용자 공간 --- */
mkdir("/tmp/mydir", 0755);
mknod("/dev/mydev", S_IFCHR | 0660, MKDEV(200, 0));
symlink("/usr/bin/python3", "/usr/local/bin/python");
link("/etc/hosts", "/tmp/hosts.bak");

/* --- 커널 진입 (syscall) --- */
/* sys_mkdirat  → do_mkdirat   → vfs_mkdir   → i_op->mkdir  */
/* sys_mknodat  → do_mknodat   → vfs_mknod   → i_op->mknod  */
/* sys_symlinkat → do_symlinkat → vfs_symlink → i_op->symlink */
/* sys_linkat   → do_linkat    → vfs_link    → i_op->link   */

/* --- 각 단계 공통 흐름 --- */
/*  1. filename_create(): 부모 dentry 조회 + 새 dentry 생성 */
/*  2. may_create(): MAY_WRITE | MAY_EXEC 권한 검사          */
/*  3. security_inode_*()/security_inode_mkdir() LSM 훅     */
/*  4. i_op->mkdir()/mknod()/symlink()/link() FS 구현       */
/*    → inode 할당 → d_instantiate() → inode_init_owner()   */
/*  5. fsnotify_mkdir()/fsnotify_create() 알림 발송          */
/*  6. done_path_create(): 잠금 해제 + dentry 참조 반환      */
d_instantiate()의 역할: d_instantiate(dentry, inode)는 새로 생성된 inode를 dentry와 연결하여 negative dentry(파일 없음 상태)를 positive dentry로 전환합니다. 이 호출 이전까지 dentry는 dcache에서 검색되지 않으므로, 다른 프로세스가 중간 상태의 파일을 볼 수 없습니다. unlock_new_inode()를 반드시 함께 호출해야 I_NEW 플래그가 해제되어 다른 태스크가 inode를 사용할 수 있습니다.

notify_change / setattr / chmod / chown 경로

파일 속성 변경(권한, 소유자, 타임스탬프 등)은 notify_change()라는 VFS 공통 진입점을 통해 처리됩니다. 시스템 콜 레이어에서 chmod, chown, utimes 등이 모두 이 경로를 공유합니다.

시스템 콜 진입점

/* 속성 변경 시스템 콜 → VFS 공통 경로 */

/* chmod/fchmod/fchmodat */
sys_fchmodat2() → do_fchmodat() → chmod_common()
  → notify_change(idmap, dentry, &newattrs, NULL)

/* chown/lchown/fchown/fchownat */
sys_fchownat() → do_fchownat() → chown_common()
  → notify_change(idmap, dentry, &newattrs, NULL)

/* utimes/utimensat — 타임스탬프 변경 */
sys_utimensat() → do_utimes() → utimes_common()
  → notify_change(idmap, dentry, &newattrs, NULL)

/* truncate/ftruncate — 크기 변경도 notify_change 경유 */
sys_ftruncate() → do_ftruncate()
  → notify_change(idmap, dentry, &newattrs, NULL)

struct iattr — 속성 변경 요청 구조체

struct iattr는 변경할 속성의 종류(ia_valid)와 새 값을 담는 컨테이너입니다. ia_validATTR_* 비트 플래그로 어떤 속성을 변경할지 지정합니다.

/* include/linux/fs.h — struct iattr */
struct iattr {
    unsigned int    ia_valid;    /* ATTR_* 플래그 — 변경 대상 속성 마스크 */
    umode_t         ia_mode;     /* ATTR_MODE: 새 파일 모드 (퍼미션 + 유형 비트) */
    vfsuid_t        ia_vfsuid;   /* ATTR_UID:  새 소유자 UID (idmap 적용 후) */
    vfsgid_t        ia_vfsgid;   /* ATTR_GID:  새 소유 그룹 GID */
    loff_t          ia_size;     /* ATTR_SIZE: 새 파일 크기 (truncate 시) */
    struct timespec64 ia_atime;  /* ATTR_ATIME: 새 접근 시간 */
    struct timespec64 ia_mtime;  /* ATTR_MTIME: 새 수정 시간 */
    struct timespec64 ia_ctime;  /* ATTR_CTIME: 새 변경 시간 (inode 변경) */
    struct file     *ia_file;    /* ATTR_FILE:  truncate 시 열린 파일 참조 */
};
ia_valid 플래그 값 (비트) 의미
ATTR_MODE (1 << 0) 파일 퍼미션/모드 변경 (ia_mode 적용)
ATTR_UID (1 << 1) 소유자 UID 변경 (ia_vfsuid 적용)
ATTR_GID (1 << 2) 소유 그룹 GID 변경 (ia_vfsgid 적용)
ATTR_SIZE (1 << 3) 파일 크기 변경 (truncate/extend)
ATTR_ATIME (1 << 4) 접근 시간 명시적 설정
ATTR_MTIME (1 << 5) 수정 시간 명시적 설정
ATTR_CTIME (1 << 6) inode 변경 시간 설정
ATTR_ATIME_SET (1 << 7) atime을 현재 시간 대신 ia_atime으로 설정
ATTR_MTIME_SET (1 << 8) mtime을 현재 시간 대신 ia_mtime으로 설정
ATTR_FILE (1 << 11) 열린 파일 참조 포함 (truncate 시)
ATTR_KILL_SUID (1 << 12) 쓰기 후 SUID 비트 자동 제거
ATTR_KILL_SGID (1 << 13) 쓰기 후 SGID 비트 자동 제거
ATTR_TIMES_SET ATTR_ATIME_SET | ATTR_MTIME_SET 타임스탬프 명시적 설정 (utimensat)

notify_change() 내부 흐름

notify_change()는 VFS 속성 변경의 중심 함수입니다. 유효성 검사, LSM 훅, 파일시스템 setattr 콜백, 그리고 fsnotify 알림을 순서대로 처리합니다.

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

    /* 1. ATTR_SIZE: 파일 크기 변경은 쓰기 권한 필요 */
    if (attr->ia_valid & ATTR_SIZE) {
        error = inode_newsize_ok(inode, attr->ia_size);
        if (error)
            return error;
    }

    /* 2. setattr_prepare(): iattr 유효성 검사 */
    error = setattr_prepare(idmap, dentry, attr);
    if (error)
        return error;

    /* 3. ATTR_KILL_SUID / ATTR_KILL_SGID 자동 처리 */
    if ((attr->ia_valid & ATTR_MODE) ||
        ((attr->ia_valid & ATTR_UID) &&
         !uid_eq(attr->ia_vfsuid, inode->i_uid)))
        attr->ia_valid |= ATTR_KILL_SUID;

    /* 4. LSM 보안 훅 */
    error = security_inode_setattr(idmap, dentry, attr);
    if (error)
        return error;

    /* 5. FS 구현 호출 또는 VFS 기본 처리 */
    if (inode->i_op->setattr)
        error = inode->i_op->setattr(idmap, dentry, attr);
    else
        error = simple_setattr(idmap, dentry, attr);

    if (!error) {
        /* 6. fsnotify: IN_ATTRIB 이벤트 발송 */
        fsnotify_change(dentry, attr->ia_valid);
        /* 7. EVM 무결성 업데이트 */
        evm_inode_post_setattr(idmap, dentry, attr->ia_valid);
    }
    return error;
}

setattr_prepare() — 속성 변경 사전 검증

setattr_prepare()는 inode 변경 요청의 권한과 일관성을 검사합니다. 과거 inode_change_ok()에서 역할이 분리된 현재 인터페이스입니다.

/* fs/attr.c — setattr_prepare() 주요 검사 항목 */
int setattr_prepare(struct mnt_idmap *idmap,
                    struct dentry *dentry,
                    struct iattr *attr)
{
    struct inode *inode = d_inode(dentry);

    /* ATTR_UID / ATTR_GID: 소유자 변경은 CAP_CHOWN 또는 루트만 */
    if (attr->ia_valid & (ATTR_UID | ATTR_GID)) {
        if (!capable(CAP_CHOWN) &&
            !uid_eq(current_fsuid(), inode->i_uid))
            return -EPERM;
    }

    /* ATTR_MODE: 소유자 또는 CAP_FSETID만 모드 변경 가능 */
    if (attr->ia_valid & ATTR_MODE) {
        if (!inode_owner_or_capable(idmap, inode))
            return -EPERM;
        /* 그룹 실행 없이 SGID 설정 금지 (CAP_FSETID 없을 때) */
        if ((attr->ia_mode & S_ISGID) &&
            !in_group_p(i_gid_into_vfsgid(idmap, inode)) &&
            !capable_wrt_inode_uidgid(idmap, inode, CAP_FSETID))
            attr->ia_mode &= ~S_ISGID;
    }

    /* ATTR_ATIME_SET / ATTR_MTIME_SET: 명시적 시간 설정은 소유자만 */
    if (attr->ia_valid & ATTR_TIMES_SET) {
        if (!inode_owner_or_capable(idmap, inode))
            return -EPERM;
    }

    return 0;
}

setattr_copy() — inode에 속성 적용

파일시스템의 i_op->setattr() 구현 내부에서 setattr_copy()를 호출하여 struct iattr의 새 값을 실제 inode 필드에 복사합니다.

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

    if (ia_valid & ATTR_UID)
        inode->i_uid = mapped_fsuid(idmap, attr->ia_vfsuid);
    if (ia_valid & ATTR_GID)
        inode->i_gid = mapped_fsgid(idmap, attr->ia_vfsgid);
    if (ia_valid & ATTR_ATIME)
        inode->i_atime = attr->ia_atime;
    if (ia_valid & ATTR_MTIME)
        inode->i_mtime = attr->ia_mtime;
    if (ia_valid & ATTR_CTIME)
        inode_set_ctime_to_ts(inode, attr->ia_ctime);
    if (ia_valid & ATTR_MODE) {
        umode_t mode = attr->ia_mode;
        /* CAP_FSETID 없으면 SGID 비트 제거 */
        if (!in_group_p(i_gid_into_vfsgid(idmap, inode)) &&
            !capable_wrt_inode_uidgid(idmap, inode, CAP_FSETID))
            mode &= ~S_ISGID;
        inode->i_mode = mode;
    }
}

/* 일반 파일시스템의 setattr 구현 패턴 */
int simple_setattr(struct mnt_idmap *idmap,
                   struct dentry *dentry,
                   struct iattr *iattr)
{
    struct inode *inode = d_inode(dentry);
    int error;

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

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

    setattr_copy(idmap, inode, iattr);
    mark_inode_dirty(inode);
    return 0;
}
inode_change_ok() 이름 변경: 리눅스 커널 5.12 이전에는 속성 변경 검증 함수가 inode_change_ok()라는 이름이었습니다. 5.12부터 idmapped mount 지원을 위해 setattr_prepare()로 분리·재설계되었으며, struct mnt_idmap 파라미터가 추가되었습니다. 드라이버 코드에서 inode_change_ok()를 발견하면 구버전 커널 대상 코드입니다.

security_inode_setattr() LSM 훅

security_inode_setattr()는 속성 변경 시 LSM이 정책을 적용하는 지점입니다. SELinux는 여기서 파일의 보안 레이블 변경이 허용되는지 검사합니다. IMA(Integrity Measurement Architecture)도 이 훅을 사용하여 파일 변경 이벤트를 기록합니다.

/* include/linux/security.h — LSM 훅 선언 */
int security_inode_setattr(struct mnt_idmap *idmap,
                           struct dentry *dentry,
                           struct iattr *attr);

/* SELinux 구현 — selinux_inode_setattr() 주요 검사:
 *  - ATTR_MODE: file__setattr 권한 검사
 *  - ATTR_UID/ATTR_GID: file__chown 권한 검사
 *  - 보안 레이블 변경: relabelfrom/relabelto 권한
 */

/* IMA — ima_inode_setattr(): 속성 변경 이벤트 기록 */
/* EVM — evm_inode_setattr(): 무결성 서명 검증/업데이트 */
utimensat()와 notify_change(): utimensat() 시스템 콜로 타임스탬프를 변경할 때도 notify_change()를 통해 ATTR_ATIME | ATTR_MTIME 플래그와 함께 처리됩니다. POSIX는 파일 소유자와 CAP_FOWNER 권한을 가진 프로세스만 임의 시간으로 설정할 수 있도록 규정합니다. UTIME_NOW 또는 UTIME_OMIT를 사용하는 경우는 쓰기 권한만으로도 허용됩니다.

VFS 핵심 유틸리티 함수 및 매크로

VFS 계층과 파일시스템 드라이버를 개발·분석할 때 반복적으로 사용되는 유틸리티 함수와 매크로를 정리합니다. 올바른 사용 패턴과 흔한 실수를 함께 설명합니다.

에러 포인터 처리: IS_ERR / PTR_ERR / ERR_PTR

커널 함수는 NULL 대신 에러 코드를 포인터에 인코딩하여 반환합니다. 유저 공간 주소와 충돌하지 않는 MAX_ERRNO보다 큰 주소 공간(0xFFFFFFFFFFFFF001~0xFFFFFFFFFFFFFFFF)을 사용합니다.

/* include/linux/err.h — 에러 포인터 API */

/* 에러 코드를 포인터로 인코딩 */
static inline void *ERR_PTR(long error);

/* 포인터에서 에러 코드 추출 */
static inline long PTR_ERR(const void *ptr);

/* 포인터가 에러인지 확인 */
static inline bool IS_ERR(const void *ptr);

/* NULL도 에러로 처리 */
static inline bool IS_ERR_OR_NULL(const void *ptr);

/* 타입 변환 없이 에러 포인터 전달 */
static inline void *ERR_CAST(const void *ptr);

/* 올바른 사용 패턴 */
struct inode *inode = iget_locked(sb, ino);
if (IS_ERR(inode))
    return PTR_ERR(inode);   /* -ENOMEM 등 */

/* ERR_CAST: 다른 포인터 타입으로 에러 전달 */
struct dentry *d = ERR_CAST(inode);   /* inode* → dentry* 타입 전환 */

파일 유형 검사 매크로

매크로 검사 대상 i_mode 비트
S_ISREG(m) 일반 파일 S_IFREG = 0100000
S_ISDIR(m) 디렉토리 S_IFDIR = 0040000
S_ISLNK(m) 심볼릭 링크 S_IFLNK = 0120000
S_ISCHR(m) 문자 장치 S_IFCHR = 0020000
S_ISBLK(m) 블록 장치 S_IFBLK = 0060000
S_ISFIFO(m) FIFO (named pipe) S_IFIFO = 0010000
S_ISSOCK(m) 유닉스 도메인 소켓 S_IFSOCK = 0140000
/* 사용 예: inode 유형에 따라 분기 */
if (S_ISREG(inode->i_mode))
    /* 일반 파일 처리 */;
else if (S_ISDIR(inode->i_mode))
    /* 디렉토리 처리 */;
else if (S_ISLNK(inode->i_mode))
    /* 심볼릭 링크 → follow_link() 필요 */;

inode 플래그 검사 매크로

inode의 i_flags 필드에 설정된 속성 플래그를 검사하는 매크로입니다. 파일시스템이나 사용자가 ioctl(FS_IOC_SETFLAGS)로 설정할 수 있습니다.

매크로 플래그 비트 의미
IS_RDONLY(inode) SB_RDONLY (sb 기준) 읽기 전용 마운트
IS_SYNC(inode) S_SYNC 쓰기마다 즉시 동기화 (sync writes)
IS_DIRSYNC(inode) S_DIRSYNC 디렉토리 갱신을 즉시 동기화
IS_IMMUTABLE(inode) S_IMMUTABLE 불변 파일 — 수정/삭제/링크 불가
IS_APPEND(inode) S_APPEND 추가 전용 — 덮어쓰기 불가 (로그 파일용)
IS_NOATIME(inode) S_NOATIME atime 갱신 비활성화
IS_NOQUOTA(inode) S_NOQUOTA 쿼터 추적 제외 (내부 FS 파일용)

파일 접근 모드: FMODE_*

struct filef_mode 필드에 설정되는 플래그로, 파일 핸들의 접근 가능한 연산을 나타냅니다.

플래그 의미
FMODE_READ 읽기 가능 (O_RDONLY 또는 O_RDWR)
FMODE_WRITE 쓰기 가능 (O_WRONLY 또는 O_RDWR)
FMODE_LSEEK lseek 허용 (순차 전용 장치에서 미설정)
FMODE_PREAD pread() 허용
FMODE_PWRITE pwrite() 허용
FMODE_EXEC 실행 파일로 open됨 (execve 경로)
FMODE_NOREUSE 파일 핸들 재사용 금지
FMODE_OPENED f_op->open() 완료 표시

dentry 헬퍼 함수

/* include/linux/dcache.h — dentry 상태 조회 헬퍼 */

/* dentry에서 inode 반환 (없으면 NULL — negative dentry) */
static inline struct inode *d_inode(const struct dentry *dentry);

/* ovl/union FS: 하위 계층의 실제 inode 반환 */
static inline struct inode *d_backing_inode(const struct dentry *dentry);

/* dentry에 inode가 연결되어 있는지 (positive dentry) */
static inline bool d_really_is_positive(const struct dentry *dentry);

/* inode가 없는지 (negative dentry — 파일 없음 캐시) */
static inline bool d_really_is_negative(const struct dentry *dentry);

/* 파일 유형 확인 */
static inline bool d_is_dir(const struct dentry *dentry);
static inline bool d_is_reg(const struct dentry *dentry);
static inline bool d_is_symlink(const struct dentry *dentry);

/* 마운트 포인트인지 확인 */
static inline bool d_mountpoint(const struct dentry *dentry);

경로 및 파일 헬퍼

/* include/linux/fs.h — struct file 헬퍼 */

/* 열린 파일의 dentry 반환 */
static inline struct dentry *file_dentry(const struct file *file);

/* 열린 파일의 inode 반환 */
static inline struct inode *file_inode(const struct file *file);

/* 열린 파일의 경로 문자열 반환 (버퍼에 기록) */
char *file_path(struct file *file, char *buf, int buflen);

/* include/linux/dcache.h — 경로 문자열 변환 */

/* dentry → 절대 경로 (버퍼 끝에서부터 기록) */
char *dentry_path_raw(const struct dentry *dentry, char *buf, int buflen);

/* path 구조체 → 문자열 (마운트 포인트 횡단 포함) */
char *d_path(const struct path *path, char *buf, int buflen);

/* 마운트 네임스페이스 루트 기준 절대 경로 */
char *d_absolute_path(const struct path *path, char *buf, int buflen);

/* 올바른 사용 패턴: 반환 포인터는 buf 내부를 가리킴 */
char buf[PATH_MAX];
char *p = d_path(&file->f_path, buf, PATH_MAX);
if (!IS_ERR(p))
    pr_info("path: %s\n", p);

참조 카운팅 함수

VFS 객체는 모두 참조 카운팅으로 수명을 관리합니다. 카운트가 0이 되면 자동으로 해제됩니다. 참조 획득과 해제를 정확히 쌍으로 맞추지 않으면 메모리 누수나 use-after-free가 발생합니다.

객체 참조 획득 참조 해제 내부 카운터
struct inode ihold(inode) iput(inode) i_count (atomic_t)
struct dentry dget(dentry) dput(dentry) d_lockref.count
struct file get_file(file) / fget(fd) fput(file) f_count (atomic_long_t)
struct vfsmount mntget(mnt) mntput(mnt) mnt_count (refcount_t)
struct super_block atomic_inc(&sb->s_active) deactivate_super(sb) s_active (atomic_t)

inode 초기화 헬퍼

/* fs/inode.c — 새 inode 초기화 */

/* alloc_inode() 내부에서 호출: 공통 필드 초기화 */
int inode_init_always(struct super_block *sb, struct inode *inode);

/* 소유자/그룹/umask 적용: 새 파일 생성 시 필수 */
void inode_init_owner(struct mnt_idmap *idmap, struct inode *inode,
                      const struct inode *dir, umode_t mode);

/* 특수 파일 inode 초기화 (문자/블록 장치, FIFO, 소켓) */
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev);

/* 새 inode 생성 시 전형적인 패턴 */
struct inode *inode = new_inode(sb);
if (!inode)
    return -ENOMEM;

inode_init_owner(idmap, inode, dir, mode);
inode->i_op  = &myfs_file_inode_ops;
inode->i_fop = &myfs_file_ops;
inode->i_ino = get_next_ino();
set_nlink(inode, 1);

d_instantiate(dentry, inode);   /* dentry와 연결 */
unlock_new_inode(inode);         /* I_NEW 플래그 해제 */

전체 헬퍼 함수 요약 표

카테고리 함수/매크로 헤더 파일 주요 용도
에러 포인터 IS_ERR(ptr) linux/err.h 반환값이 에러 포인터인지 확인
PTR_ERR(ptr) linux/err.h 에러 코드 추출 (long 반환)
ERR_PTR(err) linux/err.h 에러 코드를 포인터로 인코딩
ERR_CAST(ptr) linux/err.h 타입 변환 없이 에러 포인터 전달
IS_ERR_OR_NULL(ptr) linux/err.h NULL과 에러 포인터 동시 검사
파일 유형 S_ISREG(m) linux/stat.h 일반 파일 여부
S_ISDIR(m) linux/stat.h 디렉토리 여부
S_ISLNK(m) linux/stat.h 심볼릭 링크 여부
S_ISCHR(m) linux/stat.h 문자 장치 여부
S_ISBLK(m) linux/stat.h 블록 장치 여부
S_ISFIFO(m) linux/stat.h FIFO 여부
S_ISSOCK(m) linux/stat.h 유닉스 소켓 여부
inode 플래그 IS_RDONLY(inode) linux/fs.h 읽기 전용 마운트 여부
IS_SYNC(inode) linux/fs.h 동기 쓰기 모드 여부
IS_IMMUTABLE(inode) linux/fs.h 불변 파일 여부
IS_APPEND(inode) linux/fs.h 추가 전용 파일 여부
IS_NOATIME(inode) linux/fs.h atime 비활성화 여부
IS_DIRSYNC(inode) linux/fs.h 디렉토리 동기 갱신 여부
IS_NOQUOTA(inode) linux/fs.h 쿼터 추적 제외 여부
dentry d_inode(dentry) linux/dcache.h dentry의 inode 반환
d_really_is_positive(d) linux/dcache.h positive dentry 여부
d_really_is_negative(d) linux/dcache.h negative dentry 여부
d_is_dir(dentry) linux/dcache.h 디렉토리 dentry 여부
d_is_symlink(dentry) linux/dcache.h 심볼릭 링크 dentry 여부
파일 file_dentry(file) linux/fs.h 열린 파일의 dentry
file_inode(file) linux/fs.h 열린 파일의 inode
file_path(file, buf, len) linux/fs.h 열린 파일의 경로 문자열
경로 문자열 d_path(path, buf, len) linux/dcache.h 마운트 기준 절대 경로
dentry_path_raw(d, buf, len) linux/dcache.h dentry 기준 경로 (마운트 무시)
d_absolute_path(path, buf, len) linux/dcache.h 네임스페이스 루트 기준 절대 경로
참조 카운팅 ihold(inode) / iput(inode) linux/fs.h inode 참조 획득/해제
dget(dentry) / dput(dentry) linux/dcache.h dentry 참조 획득/해제
fget(fd) / fput(file) linux/file.h 파일 디스크립터 기반 file 참조
mntget(mnt) / mntput(mnt) linux/mount.h vfsmount 참조 획득/해제
에러 경로에서의 참조 카운팅 주의사항:
  • new_inode()로 inode를 할당한 후 에러가 발생하면 반드시 iput(inode)를 호출해야 합니다. d_instantiate()를 호출하기 전이라면 dentry에 연결되지 않았으므로 iput()이 유일한 해제 수단입니다.
  • iput(inode)i_count가 0이 될 때 evict()를 호출하여 inode를 메모리에서 제거합니다. i_nlink == 0이면 디스크에서도 삭제됩니다.
  • dget()으로 얻은 dentry는 항상 dput()으로 해제해야 합니다. path_put()dput()mntput()을 함께 호출하는 편의 함수입니다.
  • fget()으로 얻은 struct file은 반드시 fput()으로 해제해야 하며, 인터럽트 컨텍스트에서는 task_work를 통해 처리합니다.

inode 캐시 (icache) 내부 구현

VFS inode 캐시(icache)는 디스크에서 읽은 inode 메타데이터를 메모리에 보관하여 반복적인 디스크 I/O를 방지합니다. 핵심 자료구조는 전역 해시 테이블 inode_hashtable과 superblock 별 s_inodes 리스트입니다.

inode_hashtable 해시 버킷 구조

inode_hashtable은 부팅 시 inode_init()에서 alloc_large_system_hash()로 할당되는 전역 해시 테이블입니다. 각 버킷은 struct hlist_bl_head(비트-락 내장 hlist)로 구성됩니다. 해시 키는 (superblock 포인터, inode 번호) 쌍으로 결정됩니다.

/* fs/inode.c — 전역 inode 해시 테이블 */
static struct hlist_bl_head *inode_hashtable __read_mostly;
static unsigned int         i_hash_mask  __read_mostly;
static unsigned int         i_hash_shift __read_mostly;

/* 해시 함수: (sb, ino) → 버킷 인덱스 */
static inline struct hlist_bl_head *inode_hashtable_bucket(
        struct super_block *sb, unsigned long ino)
{
    return inode_hashtable +
           hash_long(hash_long((unsigned long)sb, i_hash_shift) ^ ino,
                     i_hash_shift);
}

/* fs/inode.c — inode_init() 부팅 시 테이블 크기 결정 */
void __init inode_init(void)
{
    inode_hashtable = alloc_large_system_hash(
        "Inode-cache",
        sizeof(struct hlist_bl_head),
        ihash_entries,             /* 부팅 파라미터 ihash_entries= */
        14,                         /* 최소 비트 수 */
        HASH_ZERO,
        &i_hash_shift,
        &i_hash_mask,
        0, 0);
}

iget_locked() / iget5_locked() 조회 경로

파일시스템은 inode가 필요할 때 iget_locked() 또는 iget5_locked()를 호출합니다. 두 함수 모두 내부적으로 find_inode() / find_inode_fast()를 거쳐 해시 버킷을 탐색한 뒤, 캐시 미스(miss) 시 새 inode를 할당하고 I_NEW 상태로 반환합니다.

/* fs/inode.c — iget_locked(): ino 번호로 조회 */
struct inode *iget_locked(struct super_block *sb, unsigned long ino)
{
    struct hlist_bl_head *head = inode_hashtable_bucket(sb, ino);
    struct inode *inode;

    /* 1. 해시 버킷 탐색 */
    inode = find_inode_fast(sb, head, ino);
    if (inode) {
        wait_on_inode(inode);     /* I_NEW 해제 대기 */
        return inode;              /* 캐시 히트 */
    }

    /* 2. 캐시 미스: 새 inode 할당 */
    inode = alloc_inode(sb);
    if (!inode)
        return NULL;

    inode->i_ino = ino;
    inode_sb_list_add(inode);  /* sb->s_inodes 에 추가 */

    /* 3. I_NEW 설정 후 해시 삽입 → 호출자가 fill_inode() 수행 */
    inode_set_flags(inode, I_NEW, I_NEW);
    insert_inode_locked(inode);
    return inode;
}

/* iget5_locked(): test/set 콜백으로 FS별 커스텀 식별 */
struct inode *iget5_locked(struct super_block *sb,
                           unsigned long hashval,
                           int (*test)(struct inode *, void *),
                           int (*set) (struct inode *, void *),
                           void *data)
{
    struct hlist_bl_head *head = inode_hashtable +
                                  (hashval & i_hash_mask);
    struct inode *inode;

    inode = find_inode(sb, head, test, data);
    if (inode) {
        wait_on_inode(inode);
        return inode;
    }

    inode = alloc_inode(sb);
    if (!inode)
        return NULL;

    if (set(inode, data)) {
        inode->i_state = I_NEW;
        iput(inode);
        return NULL;
    }
    inode->i_state = I_NEW;
    hlist_bl_add_head(&inode->i_hash, head);
    inode_sb_list_add(inode);
    return inode;
}
ilookup() vs iget_locked(): ilookup() / ilookup5()는 할당 없이 기존 캐시만 탐색합니다. 새 inode를 할당하지 않으므로 GFP 락 없이도 안전하게 호출 가능합니다. NFS 클라이언트처럼 "캐시에 있으면 반환, 없으면 실패"가 필요한 경우에 사용합니다.
/* fs/inode.c — ilookup(): 캐시 전용 조회 (할당 없음) */
struct inode *ilookup(struct super_block *sb, unsigned long ino)
{
    struct hlist_bl_head *head = inode_hashtable_bucket(sb, ino);
    struct inode *inode;
again:
    inode = find_inode_fast(sb, head, ino);
    if (inode) {
        if (IS_ERR(inode))
            goto again;
        wait_on_inode(inode);
    }
    return inode;   /* 캐시 미스 시 NULL 반환 */
}

inode 상태 플래그

inode는 i_state 필드로 현재 상태를 추적합니다. 여러 플래그가 동시에 설정될 수 있으며 각 상태 전이는 inode 락(inode->i_lock 스핀락)으로 보호됩니다.

/* include/linux/fs.h — inode 상태 플래그 */
#define I_DIRTY_SYNC     (1 << 0)  /* 동기 writeback 필요 (atime 등) */
#define I_DIRTY_DATASYNC (1 << 1)  /* 데이터 flush 필요 (fsync 대상) */
#define I_DIRTY_PAGES    (1 << 2)  /* dirty 페이지 존재 */
#define I_NEW            (1 << 3)  /* 할당됐으나 아직 초기화 중 */
#define I_WILL_FREE      (1 << 4)  /* 해제 예정 — 해시 제거 완료 전 */
#define I_FREEING        (1 << 5)  /* 현재 해제 중 — 새 참조 금지 */
#define I_CLEAR          (1 << 6)  /* clear_inode() 완료 */
#define I_SYNC           (1 << 7)  /* writeback 진행 중 */
#define I_REFERENCED     (1 << 8)  /* 최근 참조됨 (second-chance LRU) */
#define I_LINKABLE       (1 << 10) /* O_TMPFILE 링크 가능 */
#define I_DIRTY_TIME     (1 << 11) /* lazytime — 시간 필드만 dirty */

#define I_DIRTY  (I_DIRTY_SYNC | I_DIRTY_DATASYNC | I_DIRTY_PAGES)

insert_inode_locked() 및 evict() 흐름

/* fs/inode.c — 해시 삽입: hlist_bl 체인에 추가 */
void insert_inode_locked(struct inode *inode)
{
    struct super_block *sb = inode->i_sb;
    struct hlist_bl_head *head =
        inode_hashtable_bucket(sb, inode->i_ino);

    hlist_bl_lock(head);
    inode->i_state |= I_NEW | I_CREATING;
    hlist_bl_add_head(&inode->i_hash, head);  /* 버킷 선두에 삽입 */
    hlist_bl_unlock(head);
}

/* fs/inode.c — evict(): inode 해제 메인 경로 */
static void evict(struct inode *inode)
{
    const struct super_operations *op = inode->i_sb->s_op;

    if (inode->i_state & I_DIRTY_ALL)
        write_inode_now(inode, 1);

    if (op->evict_inode)
        op->evict_inode(inode);
    else {
        truncate_inode_pages_final(&inode->i_data);
        clear_inode(inode);
    }

    remove_inode_hash(inode);
    wake_up_bit(&inode->i_state, __I_NEW);
    destroy_inode(inode);
}

inode 슬랩 캐시와 alloc_inode_sb()

각 파일시스템은 struct inode를 포함하는 더 큰 FS별 inode 구조를 위해 전용 슬랩 캐시를 만듭니다. alloc_inode_sb()는 superblock에 묶인 슬랩 캐시에서 메모리를 할당하므로 메모리 cgroup 계정이 올바르게 처리됩니다.

/* fs/ext4/super.c — ext4 inode 슬랩 캐시 등록 */
static int __init init_inodecache(void)
{
    ext4_inode_cache = kmem_cache_create(
        "ext4_inode_cache",
        sizeof(struct ext4_inode_info),
        0,
        (SLAB_RECLAIM_ACCOUNT | SLAB_ACCOUNT),
        init_once);
    return ext4_inode_cache ? 0 : -ENOMEM;
}

/* ext4_alloc_inode: ext4_inode_info 를 alloc_inode_sb()로 할당 후
 * vfs_inode 필드로 VFS inode 포인터 반환 */
static struct inode *ext4_alloc_inode(struct super_block *sb)
{
    struct ext4_inode_info *ei;
    ei = alloc_inode_sb(sb, ext4_inode_cache, GFP_NOFS);
    if (!ei)
        return NULL;
    ei_init(ei);
    return &ei->vfs_inode;
}

icache shrinker: super_cache_scan() → prune_icache_sb()

메모리 압박 시 VM subsystem의 shrinker 프레임워크가 super_cache_scan()을 호출하며, 이 함수는 prune_icache_sb()를 통해 sb->s_inode_lru에서 오래된 inode를 회수합니다.

/* fs/super.c — icache shrinker 콜백 */
static unsigned long super_cache_scan(struct shrinker *shrink,
                                       struct shrink_control *sc)
{
    struct super_block *sb = shrink->private_data;
    long freed = 0;

    freed += prune_dcache_sb(sb, sc);     /* dentry 먼저 회수 */
    freed += prune_icache_sb(sb, sc);     /* 이후 inode 회수 */
    return freed;
}

/* fs/inode.c — prune_icache_sb(): LRU에서 inode 축출 */
long prune_icache_sb(struct super_block *sb,
                    struct shrink_control *sc)
{
    LIST_HEAD(freeable);
    long freed;

    freed = list_lru_shrink_walk(&sb->s_inode_lru, sc,
                                  inode_lru_isolate, &freeable);
    dispose_list(&freeable);   /* evict() 호출 → slab 해제 */
    return freed;
}
inode 캐시 해시 테이블 조회 흐름 iget_locked(sb, ino) hash_long(sb, shift) ^ ino → 버킷 inode_hashtable [ 0 … N-1 ] (hlist_bl_head) [0] [1] [bucket] [N-1] inode A sb=X, ino=5 inode B ← sb=X, ino=42 inode C sb=Y, ino=42 NULL sb==X && ino==42 일치 캐시 히트? YES iget_ref() 반환 wait_on_inode() 후 NO alloc_inode(sb) I_NEW 설정, 해시 삽입 evict() 해제 경로 clear_inode() → remove_inode_hash() destroy_inode() → kmem_cache_free() sb->s_inodes 리스트 inode_sb_list_add() inode_sb_list_lock 보호 shrinker: super_cache_scan() → prune_icache_sb() → inode_lru_isolate() → evict() — 메모리 압박 시 호출
inode 캐시: 해시 버킷 체인 탐색 → 캐시 히트/미스 분기 → evict() 해제 경로
성능 포인트: iget_locked()는 잠금을 최소화하기 위해 hlist_bl_head의 비트-락을 사용합니다. 일반 스핀락 대신 버킷 주소의 최하위 비트(bit 0)를 락 플래그로 사용하여 64바이트 캐시 라인 내에 락을 내장합니다. I_NEW 상태인 inode는 unlock_new_inode()가 호출될 때까지 다른 태스크가 wait_on_inode()에서 블록됩니다.

fd 테이블 (file descriptor table) 구조 상세

리눅스 커널은 프로세스의 열린 파일 목록을 task_struct → files_struct → fdtable 3단 계층으로 관리합니다. 작은 프로세스는 임베디드 테이블을 사용하여 추가 할당을 피하고, fd가 늘어나면 동적으로 테이블을 확장합니다.

핵심 구조체: files_struct / fdtable

/* include/linux/fdtable.h — fdtable 구조체 */
struct fdtable {
    unsigned int    max_fds;         /* 현재 테이블 최대 fd 수 */
    struct file   **fd;              /* struct file* 배열 포인터 */
    unsigned long  *close_on_exec;   /* exec 시 close 비트맵 (FD_CLOEXEC) */
    unsigned long  *open_fds;        /* 열린 fd 비트맵 */
    unsigned long  *full_fds_bits;   /* open_fds 2단계 요약 비트맵 */
    struct rcu_head rcu;             /* RCU 해제 콜백 */
};

/* include/linux/fdtable.h — files_struct: 프로세스별 fd 테이블 */
struct files_struct {
    atomic_t          count;           /* 참조 카운트 (CLONE_FILES 공유 시 >1) */
    bool              resize_in_progress;
    wait_queue_head_t resize_wait;

    struct fdtable __rcu *fdt;      /* 현재 fdtable 포인터 (RCU 읽기) */
    struct fdtable       fdtab;      /* 임베디드 초기 fdtable (소형 최적화) */

    /* ── 임베디드 소형 테이블 (NR_OPEN_DEFAULT = BITS_PER_LONG) ── */
    unsigned long   close_on_exec_init[1]; /* 64비트: fd 0~63 범위 */
    unsigned long   open_fds_init[1];
    unsigned long   full_fds_bits_init[1];
    struct file    *fd_array[NR_OPEN_DEFAULT]; /* 64개 슬롯 인라인 */

    spinlock_t      file_lock ____cacheline_aligned_in_smp;
    unsigned int    next_fd;
};
NR_OPEN_DEFAULT: 64비트 시스템에서 NR_OPEN_DEFAULT = BITS_PER_LONG = 64입니다. 대부분의 프로세스는 64개 미만의 fd를 사용하므로 fd_array[]와 비트맵 init 배열이 files_struct 내에 인라인으로 포함되어 별도 메모리 할당이 불필요합니다. fd가 64를 초과하면 expand_files()로 동적 할당이 발생합니다.

fd 할당: get_unused_fd_flags() → __alloc_fd()

/* fs/file.c — fd 할당 진입점 */
int get_unused_fd_flags(unsigned flags)
{
    return __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags);
}

int __alloc_fd(struct files_struct *files,
              unsigned start, unsigned end, unsigned flags)
{
    struct fdtable *fdt;
    int fd, error;

    spin_lock(&files->file_lock);
repeat:
    fdt = files_fdtable(files);
    fd = start;

    if (fd < files->next_fd)
        fd = files->next_fd;
    if (fd < fdt->max_fds)
        fd = find_next_fd(fdt, fd);  /* full_fds_bits 2단계 탐색 */

    error = expand_files(files, fd);
    if (error < 0)
        goto out;
    if (error)
        goto repeat;

    if (fd >= end) { error = -EMFILE; goto out; }

    __set_open_fd(fd, fdt);
    if (flags & O_CLOEXEC)
        __set_close_on_exec(fd, fdt);
    else
        __clear_close_on_exec(fd, fdt);

    files->next_fd = fd + 1;
    error = fd;
out:
    spin_unlock(&files->file_lock);
    return error;
}

fd_install() — RCU 기반 file 설치

/* fs/file.c — open() 완료 후 file 포인터를 fd 슬롯에 원자 설치 */
void fd_install(unsigned int fd, struct file *file)
{
    struct files_struct *files = current->files;
    struct fdtable      *fdt;

    rcu_read_lock_sched();
    fdt = rcu_dereference_sched(files->fdt);
    BUG_ON(fdt->fd[fd] != NULL);
    rcu_assign_pointer(fdt->fd[fd], file);
    rcu_read_unlock_sched();
}

expand_fdtable() → alloc_fdtable()

/* fs/file.c — fdtable 동적 확장 */
static int expand_fdtable(struct files_struct *files, unsigned int nr)
{
    struct fdtable *new_fdt, *cur_fdt;

    spin_unlock(&files->file_lock);
    new_fdt = alloc_fdtable(nr);   /* nr을 BITS_PER_LONG 단위 올림 */
    spin_lock(&files->file_lock);

    cur_fdt = files_fdtable(files);
    if (!new_fdt) return -ENOMEM;
    if (nr <= cur_fdt->max_fds) {
        free_fdtable(new_fdt);
        return 1;
    }
    copy_fdtable(new_fdt, cur_fdt);
    rcu_assign_pointer(files->fdt, new_fdt);
    if (cur_fdt != &files->fdtab)
        call_rcu(&cur_fdt->rcu, free_fdtable_rcu);
    return 1;
}

close_fd() / dup2 / fork 시 처리

/* fs/file.c — close_fd() */
int close_fd(unsigned fd)
{
    struct files_struct *files = current->files;
    struct file *file;

    spin_lock(&files->file_lock);
    file = pick_file(files, fd);
    spin_unlock(&files->file_lock);

    if (!file) return -EBADF;
    return filp_close(file, files);
}

/* fs/file.c — do_dup2(): dup2(oldfd, newfd) 원자적 교체 */
static int do_dup2(struct files_struct *files,
                  struct file *file, unsigned fd, unsigned flags)
{
    struct file *tofree;
    struct fdtable *fdt = files_fdtable(files);

    tofree = fdt->fd[fd];
    get_file(file);
    rcu_assign_pointer(fdt->fd[fd], file);
    __set_open_fd(fd, fdt);
    if (flags & O_CLOEXEC)
        __set_close_on_exec(fd, fdt);
    else
        __clear_close_on_exec(fd, fdt);

    spin_unlock(&files->file_lock);
    if (tofree)
        filp_close(tofree, files);
    return fd;
}

/* kernel/fork.c — CLONE_FILES면 공유, 아니면 복사 */
static int copy_files(unsigned long clone_flags, struct task_struct *tsk)
{
    struct files_struct *oldf = current->files;

    if (clone_flags & CLONE_FILES) {
        atomic_inc(&oldf->count);
        tsk->files = oldf;
    } else {
        struct files_struct *newf = dup_fd(oldf, NR_OPEN_MAX, &tsk->files);
        if (IS_ERR(newf)) return PTR_ERR(newf);
    }
    return 0;
}
fd 테이블 구조 계층 task_struct pid / comm *files ───→ files_struct count (refcount) *fdt ──────→ fdtab (inline) file_lock / next_fd 임베디드 테이블 (fd 0~63) fd_array[NR_OPEN_DEFAULT] open_fds_init[1] close_on_exec_init[1] fd < 64: 추가 할당 없음 fdtable max_fds **fd ──────→ *close_on_exec *open_fds *full_fds_bits (2단계 비트맵) rcu (RCU 해제) expand 시 RCU 교체 struct file* [] fd[0] → stdin fd[1] → stdout fd[2] → stderr fd[3] → file A fd[N] → NULL fd 할당 흐름: get_unused_fd_flags() → __alloc_fd() → fd_install() find_next_fd() full_fds_bits 탐색 expand_files() 필요 시 확장 set_open_fd() open_fds 비트 세트 fd_install() rcu_assign_pointer(fd[N]) fd 반환 userspace로 제한: RLIMIT_NOFILE → EMFILE | sysctl_nr_open → EMFILE | file-max → ENFILE fork(CLONE_FILES): files_struct 공유 (count++) | fork(): dup_fd() 복사
fdtable 3단 계층: task_struct → files_struct(임베디드 64슬롯) → fdtable(동적 확장) → struct file* 배열
RCU와 fdtable: fdt 포인터는 RCU로 보호되어 읽기 측은 락 없이 rcu_dereference()로 접근합니다. 테이블 교체 시 rcu_assign_pointer()로 새 포인터를 설치하고 이전 테이블은 call_rcu()로 유예 기간 후 해제합니다. fd 슬롯 자체도 rcu_assign_pointer() / rcu_dereference()로 접근하므로 fdget()을 통한 빠른 경로는 락이 필요 없습니다.

io_uring과 VFS 연동

io_uring은 리눅스 5.1(2019년)에 도입된 고성능 비동기 I/O 인터페이스입니다. 커널-유저 공유 링 버퍼(ring buffer)를 통해 시스템 콜 오버헤드를 최소화하고, 전통적인 sys_read() / sys_write() 경로를 우회하여 VFS 계층과 직접 연동합니다.

io_uring 기본 구조: SQ/CQ 링 버퍼

/* include/uapi/linux/io_uring.h — Submission Queue Entry */
struct io_uring_sqe {
    __u8    opcode;      /* IORING_OP_READ, IORING_OP_WRITE 등 */
    __u8    flags;       /* IOSQE_FIXED_FILE, IOSQE_IO_LINK 등 */
    __u16   ioprio;
    __s32   fd;          /* 대상 fd (고정 파일이면 인덱스) */
    union { __u64 off; __u64 addr2; };
    union { __u64 addr; __u64 splice_off_in; };
    __u32   len;
    union {
        __kernel_rwf_t  rw_flags;
        __u32           fsync_flags;
        /* ... */
    };
    __u64   user_data;   /* CQE에 그대로 반환되는 식별자 */
};

/* Completion Queue Entry */
struct io_uring_cqe {
    __u64   user_data;
    __s32   res;         /* 결과값 (양수: 성공 바이트, 음수: errno) */
    __u32   flags;
};

io_read() → vfs_read() 연동 경로

io_uring이 SQE를 처리할 때 io_issue_sqe()를 거쳐 opcode별 핸들러를 호출합니다. IORING_OP_READio_read()로 처리되고, 내부에서 kiocb를 설정한 뒤 vfs_iocb_iter_read()를 통해 일반 VFS 경로로 진입합니다.

/* io_uring/rw.c — io_read() VFS 연동 핵심 */
static int io_read(struct io_kiocb *req, unsigned int issue_flags)
{
    struct io_rw    *rw    = io_kiocb_to_cmd(req, struct io_rw);
    struct kiocb   *kiocb = &rw->kiocb;
    ssize_t         ret;

    kiocb->ki_filp  = req->file;
    kiocb->ki_pos   = rw->addr;
    kiocb->ki_flags = iocb_flags(req->file);

    /* IOCB_NOWAIT: 블록되면 즉시 EAGAIN 반환 */
    if (issue_flags & IO_URING_F_NONBLOCK)
        kiocb->ki_flags |= IOCB_NOWAIT;

    /* IOCB_HIPRI: 폴링(polled) I/O 완료 요청 */
    if (req->flags & REQ_F_POLLED)
        kiocb->ki_flags |= IOCB_HIPRI;

    io_rw_init_file(req, FMODE_READ);
    ret = vfs_iocb_iter_read(req->file, kiocb, &rw->iter);

    if (ret == -EAGAIN && (issue_flags & IO_URING_F_NONBLOCK)) {
        /* NOWAIT 실패: io-wq 워커로 블로킹 재시도 */
        io_req_task_queue_reissue(req);
        return IOU_ISSUE_SKIP_COMPLETE;
    }
    return io_rw_done(req, ret);
}

/* io_write(): vfs_iocb_iter_write() 호출 */
static int io_write(struct io_kiocb *req, unsigned int issue_flags)
{
    struct io_rw  *rw    = io_kiocb_to_cmd(req, struct io_rw);
    struct kiocb  *kiocb = &rw->kiocb;
    ssize_t        ret;

    kiocb->ki_flags |= IOCB_WRITE;
    if (issue_flags & IO_URING_F_NONBLOCK)
        kiocb->ki_flags |= IOCB_NOWAIT;

    ret = vfs_iocb_iter_write(req->file, kiocb, &rw->iter);

    /* -EIOCBQUEUED: file_operations->write_iter()가 ki_complete 콜백으로 나중에 완료 */
    if (ret != -EIOCBQUEUED)
        return io_rw_done(req, ret);
    return IOU_ISSUE_SKIP_COMPLETE;
}

uring_cmd: VFS passthrough

IORING_OP_URING_CMD는 파일시스템이나 디바이스 드라이버가 io_uring 고유 명령을 구현할 수 있도록 하는 passthrough 메커니즘입니다. file_operations->uring_cmd()를 직접 호출하여 커널 네트워크 소켓이나 NVMe passthrough 등에서 활용됩니다.

/* include/linux/fs.h — file_operations에 uring_cmd 추가 */
struct file_operations {
    /* ... 기존 ops ... */
    int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
    int (*uring_cmd_iopoll)(struct io_uring_cmd *,
                            io_comp_batch *, unsigned int poll_flags);
};

/* io_uring/uring_cmd.c — uring_cmd 처리 */
int io_uring_cmd(struct io_kiocb *req, unsigned int issue_flags)
{
    struct io_uring_cmd *ioucmd = io_kiocb_to_cmd(req, struct io_uring_cmd);
    struct file         *file   = req->file;

    if (!file->f_op->uring_cmd)
        return -EOPNOTSUPP;

    return file->f_op->uring_cmd(ioucmd, issue_flags);
    /* NVMe: nvme_uring_cmd(), 소켓: io_uring_sendmsg() 등 */
}

Fixed Files: IORING_REGISTER_FILES

일반 I/O에서는 매번 fdget()fdput()으로 fd → struct file* 변환과 참조 카운트 조작이 발생합니다. 고정 파일(Fixed Files)은 미리 파일 배열을 등록하여 이 비용을 제거합니다.

/* 유저스페이스: io_uring 고정 파일 등록 */
int fds[] = { fd0, fd1, fd2 };
io_uring_register(ring_fd, IORING_REGISTER_FILES,
                  fds, sizeof(fds) / sizeof(fds[0]));

/* SQE에서 고정 파일 사용 */
sqe->fd    = 1;            /* fds[] 배열의 인덱스 */
sqe->flags = IOSQE_FIXED_FILE;

/* io_uring/filetable.c — 등록된 파일로 직접 변환 (fdget 없이) */
struct file *io_file_get_fixed(struct io_ring_ctx *ctx,
                               struct io_kiocb   *req, int fd)
{
    unsigned long  file_ptr;
    struct file   *file = NULL;

    if (unlikely((unsigned int)fd >= ctx->nr_user_files))
        return NULL;

    file_ptr = io_fixed_file_slot(&ctx->file_table, fd)->file_ptr;
    file = (struct file *)(file_ptr & ~FFS_MASK);
    req->flags |= (__force unsigned)(file_ptr & FFS_MASK);
    return file;
}

io-wq 워커와 전통 I/O 우회

/* io_uring/io_uring.c — opcode 디스패치 */
static int io_issue_sqe(struct io_kiocb *req, unsigned int issue_flags)
{
    switch (req->opcode) {
    case IORING_OP_READ:
        ret = io_read(req, issue_flags);    /* → vfs_iocb_iter_read() */
        break;
    case IORING_OP_WRITE:
        ret = io_write(req, issue_flags);   /* → vfs_iocb_iter_write() */
        break;
    case IORING_OP_FSYNC:
        ret = io_fsync(req, issue_flags);   /* → vfs_fsync_range() */
        break;
    case IORING_OP_URING_CMD:
        ret = io_uring_cmd(req, issue_flags); /* → f_op->uring_cmd() */
        break;
    /* ... 50개 이상 opcode ... */
    }
    return ret;
}

/* Linked SQE: 체인 연산 (예: read 후 write) */
sqe1->flags |= IOSQE_IO_LINK;   /* SQE1 성공해야 SQE2 실행 */
sqe2->flags  = 0;               /* 체인 종단 */

/* io-wq: IOCB_NOWAIT EAGAIN 시 블로킹 재시도를 위한 커널 워커 풀 */
struct io_wq {
    struct io_wq_acct  acct[2];   /* bounded / unbounded 큐 */
    struct io_ring_ctx *ctx;
    /* ... */
};
전통 sys_read vs io_uring 성능 비교:
  • sys_read(): 시스템 콜 진입 → 컨텍스트 전환 → 블로킹 가능 → 반환 (연산당 2회 이상 컨텍스트 전환)
  • io_uring SQPOLL 모드: SQE ring tail 증가만으로 제출 완료 — 시스템 콜 0회, 컨텍스트 전환 0회 (커널 sq_thread 폴링)
  • 고정 파일: fdget() / fdput() 생략 → 캐시 라인 접촉 감소
  • IOCB_NOWAIT: 페이지 캐시 히트 시 즉시 완료, 미스 시에만 io-wq 블로킹 재시도
  • IOCB_HIPRI: NVMe 폴링 완료 — IRQ 없이 SQ/CQ tail 폴링으로 최저 레이턴시
io_uring과 VFS 연동 경로 User Space SQ Ring (mmap 공유) SQE 배열 → SQ tail++ io_uring_enter() 또는 SQPOLL (syscall 없음) CQ Ring (mmap 공유) CQE 완료 → 유저 폴링 ─── kernel space ─── io_submit_sqes() SQE 파싱 → io_init_req() io_issue_sqe() opcode 디스패치 Fixed Files io_file_get_fixed() (fdget 없음) VFS Layer vfs_iocb_iter_read() / vfs_iocb_iter_write() / vfs_fsync_range() IOCB_HIPRI (폴링) NVMe SQ poll (IRQ 없음) IOCB_NOWAIT EAGAIN? NO 즉시 완료 CQE 생성 → CQ ring YES io-wq 워커 블로킹 재시도 후 CQE IORING_OP_URING_CMD passthrough io_uring_cmd() → file->f_op->uring_cmd() → NVMe passthrough / 소켓 zero-copy → io_uring_cmd_done() 드라이버가 CQE를 채우고 io_uring_cmd_done()으로 완료 통지 Linked SQE (IOSQE_IO_LINK) SQE1 (READ+LINK) → SQE2 (WRITE) 순차 실행 SQE1 실패 시 SQE2 -ECANCELED 취소 SQPOLL 모드 커널 sq_thread가 SQ ring 폴링 → syscall 없음 sq_thread_idle ms 후 슬립, 다음 제출 시 깨어남
io_uring VFS 연동: SQE 제출 → io_issue_sqe() → VFS 계층 → IOCB_NOWAIT 분기 → 즉시 완료 또는 io-wq 재시도
io_uring 보안 고려사항: io_uring은 강력한 성능을 제공하지만 커널 버전에 따라 여러 CVE(권한 상승, UAF 등)가 보고되었습니다. Android는 기본적으로 io_uring을 비활성화하고, Chrome OS도 특정 격리 컨텍스트에서 제한합니다. 프로덕션 환경에서는 seccomp 필터를 통해 허용 opcode를 제한하거나 IORING_SETUP_SUBMIT_ALL 플래그와 CAP_SYS_ADMIN 인증을 권장합니다.

참고자료

커널 공식 문서

커널 소스 코드 (Bootlin Elixir)

LWN.net 기사

서적 및 외부 자료

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