inode 구조 (inode Structure)
파일 메타데이터의 핵심 단위인 inode를 기준으로 커널이 파일의 정체성과 상태를 어떻게 관리하는지 다룹니다. `struct inode` 주요 필드, inode_operations와 file_operations의 분리 설계, inode/dentry 캐시(Cache) 결합, 생성·갱신·삭제·회수 경로, 링크 카운트와 권한/타임스탬프 일관성, ext4·XFS·Btrfs 구현 차이를 실제 함수 흐름에 맞춰 상세히 정리합니다.
핵심 요약
- 이름/실체 분리 — dentry는 이름, inode는 파일 실체
- inode cache — 해시(Hash) + LRU로 디스크 I/O 절감
- inode_operations — 파일시스템별 동작 다형성 지점
- i_nlink — 하드링크 수명 관리 핵심 필드
- FS-specific inode — ext4/xfs/btrfs 확장 구조
단계별 이해
- 객체 관계 이해
dentry-inode-file-superblock 관계를 먼저 도식으로 잡습니다. - 핵심 필드 확인
i_mode,i_size,i_ino,i_op의미를 정리합니다. - 캐시 경로 추적
lookup hit/miss에서 icache 동작을 확인합니다. - 파일시스템 비교
ext4/xfs/btrfs inode 확장 포인트를 비교합니다.
inode 개요
struct inode는 파일시스템의 파일(또는 디렉터리, 심볼릭 링크 등)을 나타내는 메타데이터 객체입니다. 파일명이 아닌 파일 자체의 속성을 저장합니다 — 파일명은 dentry가 담당합니다.
inode라는 이름은 Unix의 원래 논문에서 유래했습니다. Dennis Ritchie와 Ken Thompson은 1974년 논문에서 이 개념을 "index node"의 줄임말로 사용했습니다. 오늘날 리눅스 커널에서 struct inode는 약 600바이트 크기의 구조체로, VFS(Virtual File System) 계층의 핵심 데이터 구조입니다.
리눅스 커널에서 inode가 다루는 핵심 정보는 다음과 같습니다:
| 범주 | 정보 | 관련 필드 |
|---|---|---|
| 식별 | inode 번호, 소속 파일시스템 | i_ino, i_sb |
| 유형/권한 | 파일 유형, rwx 권한, 특수 비트 | i_mode |
| 소유권 | 소유자 UID/GID | i_uid, i_gid |
| 크기 | 파일 크기, 디스크 블록 수 | i_size, i_blocks |
| 시간 | 접근/수정/변경/생성 시간 | __i_atime, __i_mtime, __i_ctime |
| 링크 | 하드 링크 수, 참조 카운트(Reference Count) | i_nlink, i_count |
| 연산 | 파일시스템별 동작 테이블 | i_op, i_fop |
| 데이터 | 페이지 캐시(Page Cache) 연결 | i_mapping, i_data |
| 잠금(Lock) | 동시성 제어 | i_lock, i_rwsem |
| 캐시 | 해시, LRU, writeback 리스트 | i_hash, i_lru, i_io_list |
inode 추상화 원리
Unix/Linux 파일시스템의 핵심 설계 원리는 "이름(name)과 메타데이터(metadata)의 분리"입니다:
- dentry (디렉터리 엔트리): 파일의 이름과 부모-자식 관계(경로 구조)를 관리합니다. 하나의 inode에 여러 dentry가 연결될 수 있습니다(하드 링크).
- inode: 파일의 실체 — 크기, 권한, 타임스탬프, 데이터 위치 등 파일 자체의 속성을 저장합니다. 고유한 inode 번호(
i_ino)로 식별됩니다.
이 분리 덕분에 하드 링크(같은 inode에 여러 이름), 파일 이동(dentry만 변경, inode 불변), 삭제된 파일의 계속 접근(열린 파일 디스크립터(File Descriptor)가 inode 참조 유지) 등이 가능합니다.
VFS inode vs 파일시스템별 inode
커널은 2단계 inode 구조를 사용합니다:
- VFS inode (
struct inode): 모든 파일시스템에 공통인 메타데이터(크기, 권한, 타임스탬프, 참조 카운트 등). VFS 계층이 직접 접근합니다. - FS-specific inode (예:
struct ext4_inode_info): 파일시스템 고유의 데이터(extent 트리, 저널링(Journaling) 정보 등).container_of()매크로(Macro)로 VFS inode에서 역추적(Backtrace)합니다.
각 파일시스템은 자체 inode 구조체에 VFS inode를 임베드합니다. 이 패턴은 상속 없이 다형성을 달성하는 커널의 전형적인 객체 지향 기법입니다.
inode 캐시 동작 원리
VFS는 inode 캐시(icache)를 유지하여 디스크 I/O를 최소화합니다. 동작 원리는 다음과 같습니다:
- 해시 테이블(Hash Table) 조회: 파일 접근 시 (superblock, inode 번호) 쌍으로 해시 테이블을 검색합니다. 히트하면 디스크 읽기 없이 즉시 반환합니다.
- LRU 관리: 참조 카운트(
i_count)가 0이 된 inode는 LRU 리스트에 들어갑니다. 즉시 삭제하지 않고 캐시에 유지하여, 재접근 시 빠르게 활용합니다. - 메모리 회수(Memory Reclaim): 메모리 압력 시 커널의 shrinker가 LRU 끝에서부터 inode를 회수합니다.
vm.vfs_cache_pressuresysctl로 회수 적극성을 조절합니다 (기본값 100, 높으면 더 적극적 회수).
dentry 캐시와의 관계: dentry 캐시(dcache)와 inode 캐시는 함께 동작합니다. dentry가 해시에서 히트하면 연결된 inode도 캐시에 있습니다. 경로 조회(path_lookup)는 dcache → icache 순서로 진행되어, 자주 접근하는 파일의 경로 해석이 디스크 I/O 없이 완료됩니다.
struct inode 주요 필드
struct inode {
umode_t i_mode; /* 파일 유형 + 권한 */
unsigned short i_opflags;
kuid_t i_uid; /* 소유자 UID */
kgid_t i_gid; /* 소유자 GID */
unsigned int i_flags;
const struct inode_operations *i_op; /* inode 연산 */
struct super_block *i_sb; /* 소속 superblock */
struct address_space *i_mapping; /* 페이지 캐시 매핑 */
unsigned long i_ino; /* inode 번호 */
atomic_t i_count; /* 참조 카운트 */
unsigned int i_nlink; /* 하드 링크 수 */
loff_t i_size; /* 파일 크기 (바이트) */
struct timespec64 __i_atime; /* 최종 접근 시간 */
struct timespec64 __i_mtime; /* 최종 수정 시간 */
struct timespec64 __i_ctime; /* 최종 변경 시간 */
blkcnt_t i_blocks; /* 할당된 블록 수 */
const struct file_operations *i_fop; /* file 연산 */
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct cdev *i_cdev;
char *i_link; /* symlink target */
};
void *i_private; /* fs-specific data */
};
코드 설명
- i_mode
umode_t타입. 상위 4비트는 파일 유형(S_IFREG/S_IFDIR/S_IFLNK 등), 하위 12비트는 rwxrwxrwx + setuid/setgid/sticky 권한을 인코딩합니다.S_ISREG(),S_ISDIR()매크로로 판별합니다. - i_opflagsinode 연산 최적화 플래그 캐시.
IOP_FASTPERM,IOP_LOOKUP,IOP_NOFOLLOW등 자주 조회되는 콜백 존재 여부를 비트로 기록하여i_op포인터 역참조를 줄입니다. - i_uid / i_gid커널 내부
kuid_t/kgid_t타입. 사용자 공간 uid/gid와는 달리 idmapped mount를 통한 변환이 적용된 값입니다.i_uid_into_vfsuid(idmap, inode)로 마운트 컨텍스트에 맞게 변환합니다. - i_op파일시스템이 등록하는
struct inode_operations포인터. lookup, create, unlink, rename, permission 등 inode 단위 연산을 제공합니다. 마운트 시점에 파일 유형에 따라 설정됩니다. - i_sb이 inode가 속한 파일시스템의
struct super_block포인터. 파일시스템 전역 정보(블록 크기, 마운트 옵션,s_op등)에 접근하는 핵심 경로입니다. - i_mapping페이지 캐시와 inode를 연결하는
struct address_space포인터. 일반적으로&inode->i_data를 가리키지만, 블록 디바이스 inode는 다른 address_space를 가리킬 수 있습니다. - i_ino파일시스템 내 inode 고유 번호. stat(2)의
st_ino필드로 노출됩니다.iget_locked(sb, ino)로 캐시를 조회할 때 해시 키로 사용됩니다. 64비트지만 stat 호환성을 위해 32비트 범위를 사용하는 파일시스템이 많습니다. - i_countVFS 참조 카운트.
iget()계열 함수로 증가,iput()으로 감소합니다. 0이 되면 inode 캐시에서 제거 후 회수(evict) 대상이 됩니다.i_nlink와 독립적으로 관리됩니다. - i_nlink하드 링크(Hard Link) 수.
link(2)로 증가,unlink(2)/rmdir(2)로 감소합니다. 0이 되면i_count도 0이 될 때 실제로 inode가 삭제됩니다.set_nlink()/inc_nlink()/drop_nlink()API를 통해 변경해야 합니다. - i_size파일 크기(바이트). stat(2)의
st_size로 노출됩니다.i_size_read()/i_size_write()로 원자적으로 접근해야 합니다. Sparse 파일에서 실제 할당 블록보다 클 수 있습니다. - __i_atime / __i_mtime / __i_ctime최종 접근/데이터 수정/메타데이터 변경 시각.
timespec64로 나노초 정밀도를 제공합니다. 직접 접근하지 말고inode_get_atime(),inode_set_mtime_to_ts()등의 접근자(Accessor)를 사용해야 합니다. - i_blocks512바이트 단위로 할당된 블록 수. stat(2)의
st_blocks필드입니다. 스토리지 블록 크기(4096 등)와 무관하게 항상 512 단위로 계산합니다. - i_fop파일 객체(struct file)에 등록할
struct file_operations포인터. open 시file->f_op에 복사됩니다. read/write/mmap/ioctl 등 fd 단위 연산을 정의합니다. - union {i_pipe, i_cdev, i_link}파일 유형별 전용 데이터 공용체. 파이프(pipe_inode_info), 문자 디바이스(cdev), 심볼릭 링크 타겟 경로를 각각 저장합니다. 일반 파일/디렉터리는 이 공용체를 사용하지 않습니다.
- i_private파일시스템 전용 데이터 포인터. 단, 별도의 fs-private 구조체를 embed하는 방식(container_of 패턴)이 권장됩니다.
MYFS_I(inode)매크로가 이 패턴을 구현합니다.
struct super_operations 주요 콜백
struct super_operations는 파일시스템 전체(슈퍼블록 단위)에 대한 연산을 정의합니다. inode의 할당·해제·읽기·쓰기·evict를 포함하며, 파일시스템 드라이버가 구현하여 super_block.s_op에 등록합니다.
/* include/linux/fs.h — 주요 super_operations 콜백 */
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb); /* fs별 inode 할당 */
void (*free_inode) (struct inode *); /* inode 메모리 해제 */
void (*destroy_inode)(struct inode *); /* 비동기 해제 (call_rcu) */
void (*dirty_inode)(struct inode *, int flags); /* dirty 마킹 통지 */
int (*write_inode)(struct inode *,
struct writeback_control *); /* inode를 디스크에 기록 */
int (*drop_inode) (struct inode *); /* i_count 0 시 보존 여부 결정 */
void (*evict_inode)(struct inode *); /* inode 최종 제거 (nlink==0) */
void (*put_super) (struct super_block *); /* umount 시 자원 해제 */
int (*sync_fs) (struct super_block *, int); /* fsync/sync 시 플러시 */
int (*freeze_super)(struct super_block *,
enum freeze_holder); /* 스냅샷 전 freeze */
int (*unfreeze_super)(struct super_block *,
enum freeze_holder); /* freeze 해제 */
int (*statfs) (struct dentry *,
struct kstatfs *); /* statfs(2) 통계 */
int (*show_options)(struct seq_file *,
struct dentry *); /* /proc/mounts 마운트 옵션 */
long (*nr_cached_objects)(struct super_block *,
struct shrink_control *); /* slab shrinker용 */
long (*free_cached_objects)(struct super_block *,
struct shrink_control *); /* slab shrinker용 */
};
코드 설명
- alloc_inode파일시스템별 inode 할당 콜백. 파일시스템 전용 구조체(예:
ext4_inode_info)를 kmem_cache에서 할당하고 내부의 VFS inode(vfs_inode필드)를 초기화하여 반환합니다. 구현하지 않으면 VFS가struct inode만 할당합니다. - free_inode / destroy_inode
free_inode는 RCU 유예기간 후 실제 메모리 해제를 수행합니다.destroy_inode는 RCU 콜백을 등록하는 VFS 내부 래퍼입니다. 파일시스템이alloc_inode를 구현했다면 반드시free_inode도 구현해야 합니다. - dirty_inodeinode가 dirty로 마킹될 때 파일시스템에 알리는 콜백. ext4는 저널 트랜잭션 컨텍스트를 확인하고 필요시 체크포인트를 요청합니다. flags에는
I_DIRTY_SYNC,I_DIRTY_DATASYNC등이 있습니다. - write_inodewriteback 시 inode 메타데이터를 디스크에 기록합니다.
writeback_control의sync_mode에 따라 동기/비동기로 동작합니다. ext4에서는 저널 트랜잭션을 통해 원자적으로 기록합니다. - drop_inode
i_count가 0이 되었을 때 호출됩니다. 반환값 1이면 즉시 evict, 0이면 LRU 캐시에 보존합니다.generic_drop_inode()는i_nlink == 0이거나 파일시스템이 unmount 중이면 1을 반환합니다. NFS는 이를 재정의하여 추가 조건을 검사합니다. - evict_inodeinode가 메모리에서 완전히 제거될 때 호출됩니다.
i_nlink == 0이면 디스크에서 inode와 데이터 블록을 삭제합니다. 반드시truncate_inode_pages_final()과clear_inode()를 호출해야 합니다. - sync_fs
sync(2)나syncfs(2)시 호출됩니다. 두 번째 인자가 1이면 동기(wait=1), 0이면 비동기 플러시를 요청합니다. ext4는 저널 커밋을 여기서 수행합니다. - nr_cached_objects / free_cached_objects메모리 압박 시 slab shrinker가 호출하는 콜백 쌍. 파일시스템이 자체 캐시(예: XFS의 dquot 캐시)를 보유할 때 shrinker 프레임워크와 통합하기 위해 구현합니다.
inode_operations
struct inode_operations는 inode 자체에 대한 연산(생성, 검색, 삭제, 속성 변경)을 정의합니다. 각 파일시스템은 자체 콜백(Callback)을 구현하여 VFS에 등록합니다. NULL인 콜백은 "미지원"을 의미하며, VFS가 기본 동작을 수행하거나 에러를 반환합니다.
struct inode_operations {
struct dentry *(*lookup)(struct inode *, struct dentry *, unsigned int);
int (*create)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t, bool);
int (*link)(struct dentry *, struct inode *, struct dentry *);
int (*unlink)(struct inode *, struct dentry *);
int (*symlink)(struct mnt_idmap *, struct inode *, struct dentry *, const char *);
int (*mkdir)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t);
int (*rmdir)(struct inode *, struct dentry *);
int (*mknod)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t, dev_t);
int (*rename)(struct mnt_idmap *, struct inode *, struct dentry *,
struct inode *, struct dentry *, unsigned int);
int (*setattr)(struct mnt_idmap *, struct dentry *, struct iattr *);
int (*getattr)(struct mnt_idmap *, const struct path *, struct kstat *, u32, unsigned int);
int (*permission)(struct mnt_idmap *, struct inode *, int);
struct posix_acl *(*get_inode_acl)(struct inode *, int, bool);
int (*tmpfile)(struct mnt_idmap *, struct inode *, struct file *, umode_t);
int (*atomic_open)(struct inode *, struct dentry *, struct file *, unsigned, umode_t);
int (*fileattr_set)(struct mnt_idmap *, struct dentry *, struct fileattr *);
int (*fileattr_get)(struct dentry *, struct fileattr *);
};
코드 설명
- lookup디렉터리에서 이름으로 자식 inode를 찾는 콜백입니다.
d_lookup()캐시 미스 시 호출되며, 디스크에서 디렉터리 엔트리를 읽어d_splice_alias()로 dentry 캐시에 등록합니다. 경로 탐색(path_lookupat)의 핵심 단계입니다. - create / mkdir / mknod새 파일·디렉터리·특수 파일 생성 콜백입니다. 첫 번째 인자
mnt_idmap은 idmapped mount 지원을 위해 6.x에서 추가되었으며, 전달된umode_t에 umask가 이미 적용된 상태입니다. 파일시스템은 디스크 inode를 할당하고 디렉터리 엔트리를 기록합니다. - rename마지막 인자
unsigned int flags에RENAME_NOREPLACE,RENAME_EXCHANGE,RENAME_WHITEOUT플래그를 전달합니다.RENAME_EXCHANGE는 두 파일을 원자적으로 교환하며,renameat2()시스템 콜에서 유래합니다. - setattr / getattr
setattr는chmod,chown,truncate등 속성 변경 시 호출됩니다.getattr는stat()/statx()에서 호출되며, NFS 같은 파일시스템은 서버에 최신 속성을 질의합니다. - permission
inode_permission()에서 호출되는 파일시스템별 권한 검사 콜백입니다.NULL이면generic_permission()이 대신 사용됩니다.include/linux/fs.h에 정의되어 있습니다. - atomic_openNFS 등 네트워크 파일시스템에서 lookup과 open을 하나의 RPC 호출로 통합하는 최적화 콜백입니다. 로컬 파일시스템은 보통 구현하지 않습니다.
주요 콜백 상세
| 콜백 | 호출 시점 | 인자 의미 | 반환 |
|---|---|---|---|
lookup | 경로 해석 (path_lookup) | 부모 inode, 자식 dentry | 찾은 dentry 또는 NULL |
create | open(O_CREAT), creat(2) | 부모 dir, 새 dentry, mode | 0 또는 -errno |
link | link(2) 시스템콜 | 기존 dentry, 새 부모, 새 dentry | 0 또는 -errno |
unlink | unlink(2) 시스템콜 | 부모 inode, 대상 dentry | 0 또는 -errno |
symlink | symlink(2) 시스템콜 | 부모 inode, 새 dentry, 타겟 경로 | 0 또는 -errno |
mkdir | mkdir(2) 시스템콜 | 부모 inode, 새 dentry, mode | 0 또는 -errno |
rename | rename(2) / renameat2(2) | old/new 부모, old/new dentry, flags | 0 또는 -errno |
setattr | chmod, chown, truncate | dentry, 변경할 속성 (iattr) | 0 또는 -errno |
getattr | stat, statx | 경로, kstat 결과, 요청 마스크 | 0 또는 -errno |
permission | 접근 권한 검사 | inode, 접근 마스크 (MAY_READ 등) | 0 또는 -EACCES |
tmpfile | O_TMPFILE open | 부모 inode, file, mode | 0 또는 -errno |
atomic_open | NFS 등 lookup+open 최적화 | 부모, dentry, file, open 플래그 | 0 또는 -errno |
mnt_idmap 파라미터: 커널 6.x부터 inode 연산 콜백에 struct mnt_idmap * 파라미터가 추가되었습니다. 이는 idmapped mounts 기능을 지원하기 위한 것으로, 컨테이너(Container) 환경에서 마운트별로 UID/GID 매핑(Mapping)을 다르게 적용할 수 있습니다. 기존의 mnt_userns 포인터를 대체하며, 매핑이 필요 없는 경우 nop_mnt_idmap이 전달됩니다.
Casefold: 대소문자 무시 조회
커널 5.2부터 ext4, 5.11부터 F2FS에서 대소문자 무시(case-insensitive) 디렉터리를 지원합니다. 이 기능은 inode_operations의 lookup 콜백에서 Unicode 정규화(NFKD + casefold)를 적용하여 이름 비교를 수행합니다.
/* include/linux/fs.h — casefold 관련 필드 */
struct inode {
/* ... */
unsigned int i_flags; /* S_CASEFOLD 플래그 포함 */
/* ... */
};
/* S_CASEFOLD 매크로 — inode에 casefold 활성화 여부 */
#define S_CASEFOLD (1 << 15)
#define IS_CASEFOLDED(inode) ((inode)->i_flags & S_CASEFOLD)
/* fs/libfs.c — casefold를 적용한 이름 비교 */
int generic_ci_d_compare(const struct dentry *dentry,
unsigned int len, const char *str,
const struct qstr *name)
{
const struct dentry *parent = READ_ONCE(dentry->d_parent);
const struct inode *dir = d_inode_rcu(parent);
const struct unicode_map *um = dir->i_sb->s_encoding;
if (!dir || !IS_CASEFOLDED(dir))
return 1; /* 일반 비교 폴백 */
/* Unicode NFKD+casefold로 정규화하여 비교 */
return utf8_strncasecmp(um, name, &(struct qstr)QSTR_INIT(str, len));
}
| 항목 | 설명 |
|---|---|
| 활성화 | tune2fs -O casefold /dev/sdX (ext4), chattr +F dir/로 디렉터리별 설정 |
| 인코딩 | mkfs.ext4 -O casefold -E encoding=utf8-12.1.0 (Unicode 버전 지정) |
| dentry 캐시 | casefold 디렉터리의 dentry는 d_compare/d_hash를 casefold 버전으로 교체 |
| strict 모드 | encoding_flags=strict — 유효하지 않은 UTF-8 시퀀스 거부 |
| 호환성 | casefold 파일시스템은 커널 5.2 미만에서 마운트(Mount) 불가 (호환 플래그) |
| NFS | NFS 서버가 casefold를 인지하지 못하면 대소문자 불일치 가능 |
Windows/macOS 호환성: NTFS와 APFS는 기본적으로 대소문자를 무시합니다. Samba/Wine을 통해 Windows 애플리케이션과 상호 운용하거나, 크로스 플랫폼 프로젝트에서 파일명 충돌을 방지하려면 casefold가 유용합니다. 다만 Git 등 대소문자 구분에 의존하는 도구와 충돌할 수 있으므로 주의가 필요합니다.
inode 캐시
VFS는 inode 캐시(icache)를 유지하여 디스크 접근을 최소화합니다. 사용 중이지 않은 inode는 LRU 리스트에 들어가며, 메모리 압력 시 회수됩니다.
cat /proc/sys/fs/inode-nr로 현재 할당된 inode 수와 free inode 수를 확인할 수 있습니다. slabtop에서 inode_cache 항목도 참고하세요.
inode 파일 유형
| 매크로 | 유형 | 설명 |
|---|---|---|
S_IFREG | 일반 파일 | 데이터 저장 |
S_IFDIR | 디렉터리 | 다른 파일들의 목록 |
S_IFLNK | 심볼릭 링크 | 다른 경로 참조 |
S_IFBLK | 블록 디바이스 | 디스크 등 |
S_IFCHR | 문자 디바이스 | 터미널, 시리얼 등 |
S_IFIFO | FIFO (named pipe) | 프로세스(Process)간 통신 |
S_IFSOCK | 소켓(Socket) | Unix domain socket |
ext4 inode 확장
각 파일시스템은 VFS inode를 자체 구조체에 임베드합니다. ext4의 경우:
struct ext4_inode_info {
__le32 i_data[15]; /* block pointers or extent tree */
__u32 i_flags;
ext4_fsblk_t i_file_acl;
/* ... ext4 specific fields ... */
struct inode vfs_inode; /* VFS inode 임베드 */
};
/* VFS inode에서 ext4 inode로 변환 */
static inline struct ext4_inode_info *EXT4_I(struct inode *inode)
{
return container_of(inode, struct ext4_inode_info, vfs_inode);
}
inode 생명주기
inode는 할당 → 초기화 → 사용 → 해제의 생명주기를 가집니다. 참조 카운트(i_count)와 하드 링크 수(i_nlink)가 모두 0이 되면 삭제됩니다.
/* 새 inode 할당 (파일시스템별 alloc_inode 호출) */
struct inode *inode = new_inode(sb);
/* inode 번호 할당 및 해시 테이블에 삽입 */
inode->i_ino = get_next_ino();
insert_inode_hash(inode);
/* 초기 속성 설정 */
inode->i_mode = S_IFREG | 0644;
inode_init_owner(idmap, inode, dir, mode);
inode->i_op = &myfs_inode_ops;
inode->i_fop = &myfs_file_ops;
inode->i_mapping->a_ops = &myfs_aops;
/* 참조 카운트 관리 */
ihold(inode); /* i_count++ (참조 획득) */
iput(inode); /* i_count-- (참조 해제, 0이면 evict) */
/* inode 삭제 경로 */
/* i_nlink == 0 && i_count == 0 → evict_inode() 호출 */
코드 설명
- new_inode(sb)
fs/inode.c에 정의된 VFS 함수로,alloc_inode(sb)를 호출하여 slab에서 inode를 할당하고,sb->s_inodes리스트에 추가합니다. 반환된 inode의i_state는 0이며, 호출자가 직접 필드를 초기화해야 합니다. - get_next_ino()percpu 카운터를 이용해 고유한 inode 번호를 빠르게 할당합니다. tmpfs, procfs 등 디스크 기반이 아닌 의사 파일시스템에서 사용합니다. 디스크 기반 파일시스템은 자체 inode 번호 할당 로직을 가집니다.
- insert_inode_hash(inode)전역
inode_hashtable에 inode를 삽입합니다. 이후iget_locked()나find_inode_fast()에서 이 inode를 캐시 히트로 찾을 수 있게 됩니다. - inode_init_owner()새 inode의
i_uid,i_gid,i_mode를 부모 디렉터리와 현재 프로세스의 자격 증명(credentials) 기반으로 설정합니다. setgid 디렉터리에서는 부모의 GID를 상속합니다. - ihold / iput
ihold()는atomic_inc(&inode->i_count)로 참조 카운트를 증가시키고,iput()는 감소시킵니다.i_count가 0이 되면i_nlink상태에 따라 LRU 추가 또는 즉시evict()를 수행합니다.
파일시스템별 inode 할당
각 파일시스템은 alloc_inode()와 free_inode()를 구현하여 자체 확장 inode를 관리합니다:
static struct kmem_cache *myfs_inode_cachep;
static struct inode *myfs_alloc_inode(struct super_block *sb)
{
struct myfs_inode_info *mi;
mi = alloc_inode_sb(sb, myfs_inode_cachep, GFP_KERNEL);
if (!mi)
return NULL;
/* fs-specific 필드 초기화 */
mi->i_disksize = 0;
return &mi->vfs_inode;
}
static void myfs_free_inode(struct inode *inode)
{
kmem_cache_free(myfs_inode_cachep, MYFS_I(inode));
}
static const struct super_operations myfs_sops = {
.alloc_inode = myfs_alloc_inode,
.free_inode = myfs_free_inode,
.write_inode = myfs_write_inode,
.evict_inode = myfs_evict_inode,
};
코드 설명
- myfs_alloc_inode파일시스템 고유의 확장 inode 구조체(
myfs_inode_info)를 전용kmem_cache에서 할당합니다.alloc_inode_sb()는 memcg(메모리 cgroup) 계정을 올바르게 처리하는 래퍼입니다. 반환값은 임베드된vfs_inode필드의 주소입니다. - return &mi->vfs_inodeVFS는
struct inode포인터로만 작업합니다. 파일시스템이 확장 필드에 접근할 때는container_of()매크로(보통MYFS_I(inode)로 래핑)를 사용하여 외부 구조체 포인터를 역산합니다. - myfs_free_inode
evict()→destroy_inode()경로에서 호출됩니다.MYFS_I(inode)로 확장 구조체 포인터를 얻어kmem_cache_free()로 해제합니다. RCU 지연 해제가 필요하면free_inode대신destroy_inode+call_rcu()패턴을 사용합니다. - super_operations 등록
.alloc_inode와.free_inode는 짝으로 구현합니다..write_inode는 dirty inode를 디스크에 기록하고,.evict_inode는 inode가 메모리에서 제거될 때 파일시스템별 정리(블록 해제, 저널 처리 등)를 수행합니다.
dentry-inode 연결 함수
새로 생성하거나 디스크에서 읽은 inode를 dentry에 연결하는 것은 VFS 경로 조회의 핵심 단계입니다. 연결 방식에 따라 여러 함수가 제공되며, 잘못된 함수를 사용하면 경로 조회 실패, 중복 dentry, NFS 불일치 등이 발생합니다.
d_instantiate() / d_instantiate_new()
/* fs/dcache.c — d_instantiate(): dentry에 inode 연결 */
void d_instantiate(struct dentry *entry, struct inode *inode)
{
if (inode) {
security_d_instantiate(entry, inode); /* LSM 알림 */
}
spin_lock(&entry->d_lock);
__d_set_inode_and_type(entry, inode, add_flags);
/* dentry의 d_inode = inode 설정 */
/* inode의 i_dentry 리스트에 이 dentry 추가 (하드 링크 추적) */
if (inode)
hlist_add_head(&entry->d_u.d_alias, &inode->i_dentry);
spin_unlock(&entry->d_lock);
}
/* d_instantiate_new(): d_instantiate + unlock_new_inode 통합 */
void d_instantiate_new(struct dentry *entry, struct inode *inode)
{
BUG_ON(!(inode->i_state & I_NEW)); /* I_NEW 상태 필수 */
d_instantiate(entry, inode);
unlock_new_inode(inode); /* I_NEW 해제 + 대기자 깨우기 */
}
코드 설명
- d_instantiate — d_inode 설정dentry의
d_inode포인터를 inode로 설정합니다. 이 연결이 완료되면 경로 조회에서 이 dentry를 통해 inode에 접근할 수 있습니다. NULL inode를 전달하면 negative dentry(파일 없음 캐시)가 됩니다. - d_instantiate — i_dentry 리스트inode의
i_dentryhlist에 dentry를 추가합니다. 하드 링크가 여러 개면 하나의 inode에 여러 dentry가 연결됩니다.d_find_alias(inode)로 inode에서 dentry를 역검색할 수 있습니다. - d_instantiate_new
create,mkdir,mknod등 새 inode를 생성하는 콜백에서 사용합니다.d_instantiate()과unlock_new_inode()를 원자적으로 수행하여, inode가 dentry에 연결되기 전에 다른 스레드가 접근하는 경쟁 조건을 방지합니다.
| 함수 | 사용 시점 | I_NEW 처리 |
|---|---|---|
d_instantiate_new(dentry, inode) | create, mkdir, mknod — 새 inode 생성 | I_NEW 해제 포함 |
d_instantiate(dentry, inode) | link — 기존 inode에 새 이름 추가 | I_NEW 해제 안 함 |
d_add(dentry, inode) | 단순 FS (ramfs) — instantiate + rehash | I_NEW 해제 안 함 |
d_splice_alias()
lookup 콜백에서 디스크에서 찾은 inode를 dentry에 연결할 때 사용합니다. 동일 inode에 대한 기존 dentry(alias)가 있으면 그것을 반환하고, 없으면 새로 연결합니다.
/* fs/dcache.c — d_splice_alias() 핵심 로직 */
struct dentry *d_splice_alias(struct inode *inode,
struct dentry *dentry)
{
if (!inode) {
/* 파일 없음 → negative dentry로 캐시 */
d_add(dentry, NULL);
return NULL;
}
/* 디렉터리 inode: 기존 alias 검색 */
if (S_ISDIR(inode->i_mode)) {
struct dentry *alias = __d_find_alias(inode);
if (alias) {
/* 기존 alias가 있으면 그것을 반환 (splice) */
/* 전달된 dentry는 negative로 유지 */
__d_move(alias, dentry, false);
return alias;
}
}
/* 비디렉터리이거나 기존 alias 없음 → 직접 연결 */
d_add(dentry, inode);
return NULL;
}
/* lookup 콜백에서의 전형적인 사용 */
static struct dentry *myfs_lookup(struct inode *dir,
struct dentry *dentry,
unsigned int flags)
{
struct inode *inode = NULL;
ino_t ino;
/* 디스크에서 이름으로 inode 번호 검색 */
ino = myfs_find_entry(dir, &dentry->d_name);
if (ino)
inode = myfs_iget(dir->i_sb, ino);
return d_splice_alias(inode, dentry);
}
코드 설명
- d_splice_alias — NULL inode파일이 존재하지 않으면 negative dentry를 캐시합니다. 다음에 같은 이름을 조회할 때 디스크 접근 없이 즉시 "파일 없음"을 반환합니다.
- d_splice_alias — 디렉터리 alias디렉터리 inode는 하나의 dentry만 가질 수 있습니다 (하드 링크 금지). 기존 alias가 있으면
__d_move()로 기존 dentry를 새 위치로 이동시킵니다. NFS에서 서버가 같은 디렉터리를 다른 이름으로 보고할 때 중요합니다. - d_splice_alias — d_add
d_add()는d_instantiate()+d_rehash()의 조합입니다. dentry에 inode를 연결하고 dcache 해시 테이블에 등록합니다.
d_make_root()
파일시스템 마운트 시 루트(/) 디렉터리의 dentry를 생성합니다.
/* fs/dcache.c — d_make_root() */
struct dentry *d_make_root(struct inode *root_inode)
{
struct dentry *dentry;
if (!root_inode)
return NULL;
/* "/" 이름의 dentry 할당 */
dentry = d_alloc_anon(root_inode->i_sb);
if (!dentry) {
iput(root_inode); /* 실패 시 inode 해제 */
return NULL;
}
d_instantiate(dentry, root_inode);
return dentry;
}
/* fill_super에서의 사용 */
static int myfs_fill_super(struct super_block *sb, ...)
{
struct inode *root_inode;
root_inode = myfs_iget(sb, MYFS_ROOT_INO);
if (IS_ERR(root_inode))
return PTR_ERR(root_inode);
sb->s_root = d_make_root(root_inode);
if (!sb->s_root)
return -ENOMEM; /* d_make_root가 이미 iput() 호출 */
return 0;
}
d_make_root 실패 시 iput: d_make_root()가 실패하면 전달된 inode에 대해 iput()을 자동으로 호출합니다. 따라서 호출자가 별도로 iput()을 호출하면 이중 해제(Double-free)가 발생합니다. 이 패턴은 커널의 "ownership transfer" 관례를 따릅니다.
O_TMPFILE: 이름 없는 inode 생성
커널 3.11에서 도입된 O_TMPFILE 플래그는 디렉터리에 연결되지 않은 inode를 생성합니다. dentry 없이 inode와 file 구조체만 존재하므로, 파일명 충돌·레이스 컨디션·보안 노출 없이 안전하게 임시 파일을 사용할 수 있습니다.
/* 사용자 공간 — O_TMPFILE 사용 패턴 */
int fd = open("/tmp", O_TMPFILE | O_RDWR, 0600);
/* fd로 데이터 기록 */
write(fd, data, len);
/* 패턴 1: 작업 완료 후 원자적으로 이름 부여 (linkat) */
char procpath[64];
snprintf(procpath, sizeof(procpath), "/proc/self/fd/%d", fd);
linkat(AT_FDCWD, procpath, AT_FDCWD, "/tmp/final.dat", AT_SYMLINK_FOLLOW);
/* 패턴 2: 이름 부여 없이 close → inode 자동 삭제 */
close(fd); /* nlink==0, count==0 → evict */
/* 커널 내부 — O_TMPFILE 경로 (fs/namei.c) */
static int do_tmpfile(struct nameidata *nd, unsigned flags,
const struct open_flags *op,
struct file *file)
{
struct inode *dir = nd->path.dentry->d_inode;
/* 1. 디렉터리의 inode_operations->tmpfile 콜백 확인 */
if (!dir->i_op->tmpfile)
return -EOPNOTSUPP;
/* 2. FS별 tmpfile 콜백 호출 → 새 inode 할당 */
/* nlink=0인 상태로 생성, dentry 연결 안 함 */
error = dir->i_op->tmpfile(idmap, dir, file, op->mode);
/* 3. d_tmpfile()으로 특수 dentry 연결 */
/* 해시 테이블 미등록, 부모 미연결 */
return error;
}
/* ext4의 tmpfile 구현 — fs/ext4/namei.c */
static int ext4_tmpfile(struct mnt_idmap *idmap,
struct inode *dir,
struct file *file, umode_t mode)
{
struct inode *inode;
/* 1. 새 inode 할당 + 초기화 */
inode = ext4_new_inode_start_handle(idmap, dir, mode, ...);
/* 2. nlink = 0 (orphan 상태) */
/* 저널의 orphan 리스트에 등록 → 크래시 시 자동 정리 */
ext4_orphan_add(handle, inode);
/* 3. 연산 테이블 연결 */
inode->i_op = &ext4_file_inode_operations;
inode->i_fop = &ext4_file_operations;
/* 4. d_tmpfile()로 file에 연결 */
d_tmpfile(file, inode);
return finish_open_simple(file, 0);
}
| 비교 | mkstemp() | O_TMPFILE |
|---|---|---|
| 이름 노출 | 파일명이 디렉터리에 노출됨 | 이름 없음 (디렉터리 탐색 불가) |
| 레이스 컨디션 | 생성-삭제 사이 윈도우 존재 | 없음 (처음부터 이름 없음) |
| 원자적(Atomic) 게시 | rename으로 구현 (기존 파일 필요) | linkat으로 원자적 게시 |
| 크래시 안전 | 잔여 파일 수동 정리 필요 | orphan list → 자동 정리 |
| 커널 요구 | 모든 커널 | 3.11+, FS가 tmpfile 콜백 구현 필요 |
| 지원 FS | 모든 FS | ext4, XFS, Btrfs, tmpfs 등 |
실전 패턴 — 원자적 파일 교체: O_TMPFILE으로 임시 파일을 생성하고, fsync()로 데이터를 디스크에 확정한 후, linkat()으로 최종 경로에 원자적으로 게시합니다. 이 패턴은 설정 파일, 데이터베이스 WAL, 패키지 매니저 등에서 쓰기 도중 크래시에 안전한 업데이트를 보장합니다. rename() 기반보다 더 견고합니다 — 중간 상태의 파일명이 존재하지 않기 때문입니다.
확장 속성 (xattr)
inode에 추가적인 이름-값 쌍 메타데이터를 저장합니다. 보안 레이블(SELinux), ACL, 사용자 데이터 등에 사용됩니다.
| 네임스페이스(Namespace) | 접두사 | 용도 |
|---|---|---|
| user | user.* | 사용자 정의 메타데이터 |
| security | security.* | SELinux, AppArmor 레이블 |
| system | system.posix_acl_* | POSIX ACL |
| trusted | trusted.* | 관리자 전용 메타데이터 |
/* xattr 핸들러 등록 */
static const struct xattr_handler myfs_xattr_user_handler = {
.prefix = XATTR_USER_PREFIX,
.get = myfs_xattr_get,
.set = myfs_xattr_set,
};
static const struct xattr_handler *myfs_xattr_handlers[] = {
&myfs_xattr_user_handler,
&myfs_xattr_security_handler,
NULL,
};
sb->s_xattr = myfs_xattr_handlers;
# xattr 조작 명령어
# 설정
setfattr -n user.description -v "project config" /path/to/file
# 조회
getfattr -n user.description /path/to/file
# 전체 나열
getfattr -d -m ".*" /path/to/file
# 삭제
setfattr -x user.description /path/to/file
# SELinux 보안 컨텍스트 확인
getfattr -n security.selinux /path/to/file
# xattr 크기 제한 확인
# ext4: 개별 값 최대 64KB, 총 1블록(4KB) 제한 (인라인+외부)
# XFS: 개별 64KB, 총 제한 없음 (B+tree 확장)
# Btrfs: 개별 64KB, 총 제한 없음
inode 이벤트 감시 (inotify/fanotify)
inode 변경 사항을 유저스페이스에 알리는 커널 메커니즘입니다:
/* 커널 내부: 파일 변경 시 이벤트 발생 */
fsnotify_modify(file); /* 파일 내용 수정 */
fsnotify_access(file); /* 파일 읽기 */
fsnotify_create(dir, dentry); /* 파일 생성 */
fsnotify_delete(dir, dentry); /* 파일 삭제 */
/* VFS 계층에서 자동 호출됨 (vfs_write, vfs_read 등) */
| 인터페이스 | 대상 | 특징 |
|---|---|---|
| inotify | 파일/디렉터리 | 간편한 API, 재귀 감시 미지원 |
| fanotify | 마운트/파일시스템 | 전체 마운트 감시, 접근 제어(Access Control) 가능 |
Btrfs의 inode 확장
Btrfs는 전통적 inode 번호 대신 (subvolume_id, objectid) 쌍으로 파일을 식별합니다:
struct btrfs_inode {
struct inode vfs_inode;
u64 root_objectid; /* subvolume ID */
struct btrfs_key location; /* (objectid, type, offset) */
u64 disk_i_size; /* 디스크 상의 크기 */
u64 generation; /* CoW 트랜잭션 세대 */
u64 flags; /* NODATASUM, COMPRESS 등 */
struct btrfs_ordered_inode_tree ordered_tree;
struct list_head delalloc_inodes;
};
stat --format=%i로 inode 번호를, getfattr -d로 확장 속성을 확인할 수 있습니다. Btrfs에서는 btrfs inspect-internal inode-resolve로 inode 번호에서 경로를 역추적할 수 있습니다.
파일시스템별 inode 고려사항
앞서 ext4와 Btrfs의 inode 확장을 살펴보았습니다. 다음으로 파일시스템 설계 시 고려해야 할 inode 관련 공통 사항 — inode 고갈, 크기 제한, 성능 특성 등을 정리합니다.
inode 고갈 문제
# inode 사용량 확인
df -i
# Filesystem Inodes IUsed IFree IUse% Mounted on
# /dev/sda1 6553600 234567 6319033 4% /
# ext4: inode 수는 mkfs 시 결정 (이후 변경 불가!)
mkfs.ext4 -N 10000000 /dev/sda1 # inode 천만 개
mkfs.ext4 -i 4096 /dev/sda1 # 4KB당 1개 inode (소파일 많은 환경)
# XFS: inode는 동적 할당 (고갈 문제 적음)
# Btrfs: inode 번호 동적 (고갈 없음)
mkfs 옵션을 설정해야 합니다.
inode 크기와 인라인 데이터
| 파일시스템 | 기본 inode 크기 | 인라인 데이터 | xattr 인라인 |
|---|---|---|---|
| ext4 | 256바이트 | 소파일 데이터를 inode 내 저장 (inline_data 옵션) | 잔여 공간에 xattr 저장 (별도 블록 할당 불필요) |
| XFS | 512바이트 | 인라인 데이터 지원 | attr fork에 inline xattr |
| Btrfs | 가변 | 소파일은 메타데이터 B-tree에 인라인 | xattr는 별도 아이템 |
inode 타임스탬프와 성능
/* inode의 세 가지 타임스탬프 */
struct inode {
struct timespec64 __i_atime; /* 마지막 접근 시간 (read) */
struct timespec64 __i_mtime; /* 마지막 수정 시간 (write) */
struct timespec64 __i_ctime; /* 마지막 변경 시간 (메타데이터) */
/* ext4는 crtime (생성 시간)도 저장 — statx()로 조회 */
};
/* atime 마운트 옵션과 성능 영향 */
/* noatime — atime 갱신 완전 비활성화 (최고 성능) */
/* relatime — mtime보다 오래된 경우에만 atime 갱신 (기본값) */
/* strictatime — 매 접근마다 갱신 (성능 나쁨) */
/* lazytime — atime을 메모리에서만 갱신, 주기적으로 디스크 기록 (5.6+) */
하드 링크와 inode 공유
하드 링크는 여러 dentry가 동일한 inode를 가리키는 구조입니다. 파일의 실체(데이터와 메타데이터)는 하나이고, 이름만 여러 개입니다. i_nlink 필드가 연결된 dentry 수를 추적하며, 이 값이 0이 되고 참조 카운트도 0이면 inode가 해제됩니다.
/* 하드 링크 생성 — fs/namei.c: vfs_link() */
int vfs_link(struct dentry *old_dentry,
struct mnt_idmap *idmap,
struct inode *dir,
struct dentry *new_dentry,
struct inode **delegated_inode)
{
struct inode *inode = d_inode(old_dentry);
/* 1. 제한 사항 검사 */
if (S_ISDIR(inode->i_mode))
return -EPERM; /* 디렉터리 하드 링크 금지 */
if (inode->i_nlink >= inode->i_sb->s_max_links)
return -EMLINK; /* 최대 링크 수 초과 */
/* 2. 보안 검사 */
error = security_inode_link(old_dentry, dir, new_dentry);
/* 3. FS별 link 콜백 호출 */
error = dir->i_op->link(old_dentry, dir, new_dentry);
/* → inode->i_nlink++ */
/* → 새 dentry를 dir에 추가 */
/* 4. fsnotify 이벤트 발생 */
fsnotify_link(dir, inode, new_dentry);
return 0;
}
| 제한 사항 | 이유 | 파일시스템별 |
|---|---|---|
| 디렉터리 하드 링크 금지 | 디렉터리 그래프에 순환이 생기면 fsck, find, 경로 해석이 무한 루프 | 모든 FS (커널 수준 거부) |
| FS 경계 불가 | inode 번호는 FS 내에서만 고유, 다른 FS는 같은 번호 사용 가능 | 모든 FS |
| 최대 링크 수 | i_nlink 필드 크기 제한 | ext4: 65,000 (dir_nlink 시 무제한), XFS: 무제한, Btrfs: 65,535 |
| protected_hardlinks | 보안: 소유하지 않은 파일에 하드 링크 제한 | sysctl fs.protected_hardlinks=1 (기본 활성) |
Reflink (CoW 복사)
하드 링크와 달리 reflink는 별도의 inode를 생성하되 데이터 extent를 공유합니다. 쓰기 시 CoW(Copy-on-Write)로 분리되어, 스냅샷과 공간 효율적 복사의 기반 기술입니다.
# reflink 복사 (Btrfs, XFS 4.16+)
cp --reflink=always source.img dest.img
# → 즉시 완료 (데이터 복사 없음, extent 참조만 공유)
# → dest.img에 쓰기 시 해당 extent만 CoW 분리
# reflink 지원 여부 확인
xfs_info /mnt/data | grep reflink
# reflink=1 이면 지원
# 하드 링크 vs reflink vs 일반 복사
# 하드 링크: 같은 inode, 같은 데이터 (모든 변경 공유)
# reflink: 다른 inode, extent 공유 (쓰기 시 분리)
# cp: 다른 inode, 데이터 완전 복사
struct inode 필드별 상세 분석
struct inode는 약 40개 이상의 필드를 가지며, 파일 메타데이터의 모든 측면을 관리합니다. 여기서는 핵심 필드를 그룹별로 분석합니다.
식별 필드 (i_ino, i_sb, i_mode)
| 필드 | 타입 | 설명 | 접근 함수 |
|---|---|---|---|
i_ino | unsigned long | inode 번호 — 파일시스템 내 고유 식별자 | stat(2)의 st_ino |
i_sb | struct super_block * | 소속 superblock 포인터 — 파일시스템 컨텍스트 | 내부 전용 |
i_mode | umode_t | 파일 유형(상위 4비트) + 권한(하위 12비트) | S_ISREG(), S_ISDIR() 등 |
i_flags | unsigned int | 마운트/FS 레벨 플래그 (S_SYNC, S_IMMUTABLE 등) | IS_IMMUTABLE() 등 |
i_opflags | unsigned short | VFS 내부 최적화 플래그 (IOP_FASTPERM 등) | 내부 전용 |
/* i_mode 비트 레이아웃 (16비트) */
/* ┌─ 파일 유형 (4비트) ─┐┌─ setuid/gid/sticky ─┐┌─ rwx rwx rwx ─┐ */
/* 15 14 13 12 11 10 9 8..6 5..3 2..0 */
#define S_IFMT 00170000 /* 유형 마스크 */
#define S_IFREG 0100000 /* 일반 파일 */
#define S_IFDIR 0040000 /* 디렉터리 */
#define S_IFLNK 0120000 /* 심볼릭 링크 */
#define S_IFBLK 0060000 /* 블록 디바이스 */
#define S_IFCHR 0020000 /* 캐릭터 디바이스 */
#define S_IFIFO 0010000 /* FIFO */
#define S_IFSOCK 0140000 /* 소켓 */
/* 유형 검사 매크로 */
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
/* 권한 비트 */
#define S_ISUID 0004000 /* set-user-ID */
#define S_ISGID 0002000 /* set-group-ID */
#define S_ISVTX 0001000 /* sticky bit */
#define S_IRWXU 00700 /* owner rwx */
#define S_IRWXG 00070 /* group rwx */
#define S_IRWXO 00007 /* others rwx */
소유권/크기 필드
| 필드 | 타입 | 설명 | 관련 시스템콜 |
|---|---|---|---|
i_uid | kuid_t | 소유자 UID (커널 내부 네임스페이스 인식 타입) | chown(2) |
i_gid | kgid_t | 소유자 GID | chgrp(2) |
i_size | loff_t | 파일 크기 (바이트). 최대 2^63-1 | truncate(2), stat(2) |
i_blocks | blkcnt_t | 할당된 512바이트 블록 수 (실제 디스크 사용량) | stat(2)의 st_blocks |
i_bytes | unsigned short | i_blocks에 포함되지 않는 추가 바이트 | 내부 전용 |
i_nlink | unsigned int | 하드 링크 수 | stat(2)의 st_nlink |
kuid_t/kgid_t와 사용자 네임스페이스: kuid_t와 kgid_t는 단순 정수가 아닌 구조체 래퍼입니다. 사용자 네임스페이스(user namespace) 환경에서 UID/GID 매핑이 필요하기 때문입니다. from_kuid(), make_kuid() 등의 변환 함수를 사용하여 네임스페이스 간 변환을 수행합니다. 컨테이너 환경에서는 호스트의 UID 1000이 컨테이너 내부에서 root(0)로 매핑될 수 있습니다.
연산 테이블 필드
struct inode는 세 가지 연산 테이블을 참조하여 파일시스템별 다형성을 구현합니다:
| 필드 | 구조체 | 역할 | 대표 콜백 |
|---|---|---|---|
i_op | struct inode_operations | inode 자체에 대한 연산 | lookup, create, mkdir, unlink |
i_fop | struct file_operations | 열린 파일에 대한 연산 | read, write, mmap, fsync |
i_mapping->a_ops | struct address_space_operations | 페이지 캐시 I/O 연산 | read_folio, writepages, dirty_folio |
/* i_op vs i_fop 구분 원리:
* - i_op: 파일을 "찾고/만들고/삭제"하는 디렉터리 수준 연산
* - i_fop: 파일을 "열고/읽고/쓰는" 데이터 수준 연산
*
* 예: open("/home/user/test.txt", O_RDONLY)
* 1. VFS가 /home의 i_op->lookup으로 "user" dentry 찾기
* 2. VFS가 /home/user의 i_op->lookup으로 "test.txt" dentry 찾기
* 3. test.txt inode의 i_fop을 struct file에 복사
* 4. 이후 read(fd, ...)는 file->f_op->read_iter 호출
*/
/* 디렉터리 inode의 전형적인 설정 */
static const struct inode_operations myfs_dir_iops = {
.lookup = myfs_lookup,
.create = myfs_create,
.mkdir = myfs_mkdir,
.unlink = myfs_unlink,
.rmdir = myfs_rmdir,
.rename = myfs_rename,
};
/* 일반 파일 inode의 전형적인 설정 */
static const struct inode_operations myfs_file_iops = {
.setattr = myfs_setattr,
.getattr = myfs_getattr,
};
static const struct file_operations myfs_file_fops = {
.read_iter = generic_file_read_iter,
.write_iter = generic_file_write_iter,
.mmap = generic_file_mmap,
.fsync = generic_file_fsync,
.splice_read = filemap_splice_read,
.llseek = generic_file_llseek,
.open = generic_file_open,
};
잠금 및 상태 필드
| 필드 | 타입 | 설명 |
|---|---|---|
i_lock | spinlock_t | inode 필드 보호용 스핀락(Spinlock). i_state, i_count 등의 변경 시 사용 |
i_rwsem | struct rw_semaphore | 파일 데이터 접근 직렬화(Serialization). read/write/truncate 시 사용 |
i_state | unsigned long | inode 상태 플래그 (I_NEW, I_DIRTY_* 등) |
i_hash | struct hlist_node | inode 해시 테이블 연결 |
i_io_list | struct list_head | writeback I/O 리스트 연결 |
i_lru | struct list_head | LRU 리스트 연결 (미사용 inode 회수용) |
i_sb_list | struct list_head | superblock의 전체 inode 리스트 |
i_wb_list | struct list_head | writeback 대기 리스트 |
rename()에서는 두 디렉터리를 inode 주소 순서로 잠급니다 (lock_rename()). 순서를 어기면 데드락이 발생합니다.
inode 할당 과정
새로운 inode가 생성되거나 디스크에서 읽힐 때의 할당 과정을 추적합니다. VFS는 여러 할당 경로를 제공하며, 각각 다른 사용 사례에 최적화되어 있습니다.
주요 할당 함수
| 함수 | 용도 | 해시 삽입 | I_NEW 설정 |
|---|---|---|---|
new_inode(sb) | 새 파일 생성 (create, mkdir) | 수동 | 아니오 |
new_inode_pseudo(sb) | 의사 파일시스템 (pipe, socket) | 삽입 안 함 | 아니오 |
iget_locked(sb, ino) | 디스크에서 inode 읽기 (번호 기반) | 자동 | 예 |
iget5_locked(sb, hash, test, set, data) | 커스텀 비교 함수로 inode 검색/생성 | 자동 | 예 |
ilookup(sb, ino) | 캐시에서만 검색 (할당 안 함) | 검색만 | 해당 없음 |
/* ===== 경로 1: 새 파일 생성 (예: create 시스템콜) ===== */
static int myfs_create(struct mnt_idmap *idmap,
struct inode *dir,
struct dentry *dentry,
umode_t mode, bool excl)
{
struct inode *inode;
/* 1. 새 inode 할당 (sb->s_op->alloc_inode 호출) */
inode = new_inode(dir->i_sb);
if (!inode)
return -ENOMEM;
/* 2. inode 번호 할당 */
inode->i_ino = myfs_alloc_ino(dir->i_sb);
/* 3. 소유권/권한 설정 */
inode_init_owner(idmap, inode, dir, mode);
/* 4. 연산 테이블 연결 */
inode->i_op = &myfs_file_iops;
inode->i_fop = &myfs_file_fops;
inode->i_mapping->a_ops = &myfs_aops;
/* 5. 타임스탬프 설정 */
simple_inode_init_ts(inode);
/* 6. 해시 테이블에 삽입 */
insert_inode_hash(inode);
/* 7. 디스크에 기록 */
myfs_write_inode_to_disk(inode);
/* 8. dentry와 연결 */
d_instantiate_new(dentry, inode);
return 0;
}
/* ===== 경로 2: 디스크에서 inode 읽기 (예: lookup) ===== */
static struct inode *myfs_iget(struct super_block *sb,
unsigned long ino)
{
struct inode *inode;
struct myfs_inode_info *mi;
/* 1. 해시 테이블 검색 또는 새 할당 */
inode = iget_locked(sb, ino);
if (!inode)
return ERR_PTR(-ENOMEM);
/* 2. 이미 캐시에 있으면 즉시 반환 */
if (!(inode->i_state & I_NEW))
return inode;
/* 3. 새로 할당된 경우: 디스크에서 읽기 */
mi = MYFS_I(inode);
myfs_read_inode_from_disk(sb, ino, mi);
/* 4. VFS inode 필드 채우기 */
inode->i_mode = myfs_to_vfs_mode(mi->disk_mode);
inode->i_size = mi->disk_size;
set_nlink(inode, mi->disk_nlink);
/* 5. 연산 테이블 연결 (유형에 따라) */
if (S_ISREG(inode->i_mode)) {
inode->i_op = &myfs_file_iops;
inode->i_fop = &myfs_file_fops;
} else if (S_ISDIR(inode->i_mode)) {
inode->i_op = &myfs_dir_iops;
inode->i_fop = &myfs_dir_fops;
}
/* 6. I_NEW 해제 → 다른 대기자 깨우기 */
unlock_new_inode(inode);
return inode;
}
iget_locked vs iget5_locked: iget_locked()는 단순히 inode 번호로 검색하지만, iget5_locked()는 커스텀 비교 함수(test)를 사용합니다. Btrfs처럼 같은 inode 번호가 여러 서브볼륨에 존재할 수 있는 파일시스템에서는 반드시 iget5_locked()를 사용하여 서브볼륨 ID까지 함께 비교해야 합니다.
iget_locked() 콜 체인 상세 분석
iget_locked(sb, ino)는 inode 캐시 조회 → 미스 시 할당 → 초기화 잠금 획득의 세 단계를 원자적으로 수행합니다. 아래는 fs/inode.c의 실제 구현 흐름입니다.
/* fs/inode.c — iget_locked() 전체 콜 체인 */
/* ── 진입점 ─────────────────────────────────────── */
struct inode *iget_locked(struct super_block *sb, unsigned long ino)
{
struct hlist_head *head = inode_hashtable + hash(sb, ino);
struct inode *inode;
again:
spin_lock(&inode_hash_lock); /* 해시 버킷 잠금 */
inode = find_inode_fast(sb, head, ino); /* ① 해시 조회 */
spin_unlock(&inode_hash_lock);
if (inode) {
if (IS_ERR(inode))
return NULL;
wait_on_inode(inode); /* I_NEW 대기 */
if (unlikely(inode_unhashed(inode))) {
iput(inode);
goto again; /* evict 경쟁 재시도 */
}
return inode; /* 캐시 히트 반환 */
}
/* ② 캐시 미스: 새 inode 할당 */
inode = alloc_inode(sb); /* s_op->alloc_inode 호출 */
if (inode) {
struct inode *old;
spin_lock(&inode_hash_lock);
old = find_inode_fast(sb, head, ino); /* TOCTOU 방지 재검사 */
if (!old) {
inode->i_ino = ino;
spin_lock(&inode->i_lock);
inode->i_state = I_NEW; /* ③ 초기화 잠금 표시 */
hlist_add_head(&inode->i_hash, head); /* 해시에 선점 등록 */
spin_unlock(&inode->i_lock);
inode_sb_list_add(inode); /* sb->s_inodes 목록 추가 */
spin_unlock(&inode_hash_lock);
return inode; /* I_NEW 상태로 반환 */
}
spin_unlock(&inode_hash_lock);
destroy_inode(inode); /* 경쟁 패배: 방금 할당 해제 */
inode = old;
wait_on_inode(inode);
}
return inode;
}
/* ── ① 해시 조회 ────────────────────────────────── */
static struct inode *find_inode_fast(
struct super_block *sb, struct hlist_head *head, unsigned long ino)
{
struct inode *inode;
hlist_for_each_entry(inode, head, i_hash) {
if (inode->i_ino != ino || inode->i_sb != sb)
continue;
spin_lock(&inode->i_lock);
if (inode->i_state & (I_FREEING | I_WILL_FREE)) {
__wait_on_freeing_inode(inode); /* evict 완료 대기 후 재시도 */
return ERR_PTR(-1); /* 호출자에게 재검사 요청 */
}
__iget(inode); /* i_count 증가 */
spin_unlock(&inode->i_lock);
return inode;
}
return NULL;
}
/* ── ② inode 할당 ───────────────────────────────── */
static struct inode *alloc_inode(struct super_block *sb)
{
const struct super_operations *ops = sb->s_op;
struct inode *inode;
if (ops->alloc_inode)
inode = ops->alloc_inode(sb); /* fs별 kmem_cache 할당 */
else
inode = kmem_cache_alloc(inode_cachep, GFP_KERNEL); /* 기본 slab */
if (!inode)
return NULL;
if (unlikely(inode_init_always(sb, inode))) { /* VFS 필드 초기화 */
if (ops->destroy_inode) {
ops->destroy_inode(inode);
return NULL;
}
kmem_cache_free(inode_cachep, inode);
return NULL;
}
return inode;
}
/* ── ③ 초기화 완료 후 잠금 해제 ─────────────────── */
void unlock_new_inode(struct inode *inode)
{
spin_lock(&inode->i_lock);
if (WARN_ON(!(inode->i_state & I_NEW))) /* 디버그: I_NEW 필수 확인 */
goto out;
inode->i_state &= ~I_NEW & ~I_CREATING; /* I_NEW 비트 제거 */
smp_mb(); /* 메모리 배리어 */
wake_up_bit(&inode->i_state, __I_NEW); /* 대기 중인 스레드 깨우기 */
out:
spin_unlock(&inode->i_lock);
}
코드 설명
- iget_locked — spin_lock(&inode_hash_lock)전역 해시 테이블을 보호하는 스핀락을 획득합니다. 이 락은 해시 체인의 노드 추가/제거 시에만 보유하며, inode 내용 접근은
inode->i_lock으로 별도 보호합니다. 두 락의 중첩 순서는 항상inode_hash_lock → i_lock입니다. - iget_locked — find_inode_fast()해시 버킷 체인을 선형 탐색합니다.
(sb, ino)쌍으로 일치 여부를 확인합니다.I_FREEING|I_WILL_FREE상태인 inode는 evict 완료를 기다린 후ERR_PTR(-1)을 반환하여again레이블로 재시도하도록 합니다. - iget_locked — wait_on_inode()캐시에서 찾은 inode가 아직
I_NEW상태(다른 스레드가 초기화 중)이면wait_on_bit()로 대기합니다.unlock_new_inode()가wake_up_bit()를 호출하면 깨어납니다. 이 메커니즘이 동시iget경쟁에서 중복 초기화를 방지합니다. - iget_locked — TOCTOU 방지 재검사
alloc_inode()는inode_hash_lock을 해제한 상태에서 호출되므로 그 사이 다른 CPU가 같은 inode를 삽입했을 수 있습니다. 락 재획득 후find_inode_fast()를 다시 호출하여 이 경쟁 조건을 처리합니다. - iget_locked — I_NEW 설정 + hlist_add_head()
I_NEW비트를 설정한 상태로 해시에 먼저 등록함으로써 동시에 같은 inode를 요청하는 다른 스레드가 이 inode를 발견하고 대기하도록 합니다. "초기화 잠금(initialization lock)" 패턴입니다. - alloc_inode — ops->alloc_inode(sb)파일시스템이
super_operations.alloc_inode를 구현했으면 호출합니다. 이를 통해 ext4는ext4_inode_info, XFS는xfs_inode를struct inode와 함께 단일 slab 객체로 할당합니다.container_of()로 변환합니다. - alloc_inode — inode_init_always()VFS inode의 공통 필드를 초기화합니다:
i_state=0,i_count=1,i_mapping=&inode->i_data, 대기 큐 초기화 등입니다. 이 함수 내에서security_inode_alloc()을 호출하여 LSM 보안 필드도 초기화합니다. - unlock_new_inode — I_NEW &= ~I_NEW파일시스템의 inode 초기화(
read_inode등)가 완료된 후 호출합니다.I_NEW와I_CREATING비트를 동시에 제거합니다.smp_mb()는 이전 초기화 쓰기가wake_up_bit()이전에 모든 CPU에 가시적임을 보장합니다.
inode 해시 테이블과 룩업
VFS는 전역 해시 테이블 inode_hashtable을 유지하여 빠른 inode 검색을 제공합니다. 키는 (superblock 포인터, inode 번호) 쌍이며, 해시 충돌은 체이닝으로 해결합니다.
/* fs/inode.c — 전역 해시 테이블 */
static struct hlist_head *inode_hashtable __read_mostly;
/* 해시 함수: superblock 주소와 inode 번호를 조합 */
static unsigned long hash(struct super_block *sb,
unsigned long hashval)
{
unsigned long tmp;
tmp = (hashval * (unsigned long)sb) ^ (GOLDEN_RATIO_PRIME + hashval) /
L1_CACHE_BYTES;
tmp = tmp ^ ((tmp ^ GOLDEN_RATIO_PRIME) >> i_hash_shift);
return tmp & i_hash_mask;
}
/* 해시 테이블 검색 (find_inode_fast) */
static struct inode *find_inode_fast(
struct super_block *sb,
struct hlist_head *head,
unsigned long ino)
{
struct inode *inode;
hlist_for_each_entry(inode, head, i_hash) {
if (inode->i_ino != ino) /* 번호 불일치 */
continue;
if (inode->i_sb != sb) /* superblock 불일치 */
continue;
spin_lock(&inode->i_lock);
if (inode->i_state & (I_FREEING|I_WILL_FREE)) {
__wait_on_freeing_inode(inode);
return NULL; /* 재검색 필요 */
}
__iget(inode); /* i_count++ */
spin_unlock(&inode->i_lock);
return inode;
}
return NULL;
}
코드 설명
- inode_hashtable
fs/inode.c에 정의된 전역 해시 테이블입니다. 부팅 시inode_init()에서 시스템 메모리 크기에 비례하여 할당됩니다.__read_mostly섹션에 배치되어 캐시 라인 오염을 최소화합니다. - hash(sb, hashval)superblock 주소와 inode 번호를 조합하는 해시 함수입니다.
GOLDEN_RATIO_PRIME을 곱하고 비트 시프트하여 분포를 균일하게 만듭니다. 결과를i_hash_mask로 마스킹하여 버킷 인덱스를 얻습니다. - find_inode_fast — i_ino, i_sb 비교같은 해시 버킷 내에서 inode 번호(
i_ino)와 superblock 포인터(i_sb)를 모두 비교합니다. 서로 다른 파일시스템이 같은 inode 번호를 가질 수 있으므로 두 조건 모두 일치해야 합니다. - I_FREEING | I_WILL_FREE 검사해시에서 찾은 inode가 evict 진행 중이면
__wait_on_freeing_inode()로 완료를 대기한 뒤NULL을 반환하여 호출자가 재검색하도록 합니다. 이미 해제되는 inode의 참조를 획득하는 것을 방지하는 안전 장치입니다. - __iget(inode)
i_lock을 잡은 상태에서i_count를 원자적으로 증가시킵니다. LRU에 있던 inode라면 이 시점에 LRU에서 제거되어 활성 상태로 전환됩니다.
해시 테이블 연산
| 함수 | 동작 | 호출 시점 |
|---|---|---|
insert_inode_hash(inode) | inode를 해시 테이블에 삽입 | 새 inode 생성 후 |
__insert_inode_hash(inode, hashval) | 커스텀 해시값으로 삽입 | 특수 해시가 필요할 때 |
remove_inode_hash(inode) | 해시 테이블에서 제거 | evict_inode() 내부 |
find_inode_fast(sb, head, ino) | 해시 체인에서 inode 검색 | iget_locked() 내부 |
find_inode(sb, head, test, data) | 커스텀 비교로 검색 | iget5_locked() 내부 |
ilookup(sb, ino) | 캐시에서만 검색 (미할당) | 캐시 확인만 필요 시 |
inode 상태 머신
각 inode는 i_state 필드에 상태 플래그의 조합을 저장합니다. 이 플래그들은 inode의 생명주기 단계를 추적하며, 동시성 제어와 writeback에 핵심적입니다.
| 플래그 | 값 | 의미 | 설정 시점 |
|---|---|---|---|
I_NEW | 1 << 3 | 새로 할당, 아직 초기화 중 | iget_locked() 신규 할당 시 |
I_DIRTY_SYNC | 1 << 0 | 메타데이터 dirty (atime 등 경량) | __mark_inode_dirty(I_DIRTY_SYNC) |
I_DIRTY_DATASYNC | 1 << 1 | 데이터 관련 메타데이터 dirty (size 등) | __mark_inode_dirty(I_DIRTY_DATASYNC) |
I_DIRTY_PAGES | 1 << 2 | dirty 페이지(Page) 보유 | __mark_inode_dirty(I_DIRTY_PAGES) |
I_SYNC | 1 << 4 | 현재 writeback 진행 중 | writeback 시작 시 |
I_WILL_FREE | 1 << 5 | evict 예정 (dirty 기록 중) | evict() 진입 직후 |
I_FREEING | 1 << 6 | evict 진행 중 | evict() 본체 |
I_CLEAR | 1 << 7 | evict 완료, 메모리 해제 대기 | evict() 완료 후 |
I_REFERENCED | 1 << 8 | 최근 접근됨 (LRU 2차 기회) | inode 접근 시 |
I_DIO_WAKEUP | 1 << 9 | Direct I/O 대기자 깨우기(Wakeup) | DIO 완료 시 |
I_CREATING | 1 << 15 | 생성 진행 중 | NFS 등 네트워크 FS |
/* inode dirty 마킹 — fs/fs-writeback.c */
void __mark_inode_dirty(struct inode *inode, int flags)
{
struct super_block *sb = inode->i_sb;
struct bdi_writeback *wb = NULL;
/* 이미 설정된 플래그는 무시 */
if ((inode->i_state & flags) == flags)
return;
spin_lock(&inode->i_lock);
/* I_NEW, I_FREEING, I_WILL_FREE 상태면 무시 */
if (inode->i_state & (I_NEW | I_FREEING | I_WILL_FREE)) {
spin_unlock(&inode->i_lock);
return;
}
/* FS에 dirty_inode 콜백이 있으면 호출 */
if (sb->s_op->dirty_inode)
sb->s_op->dirty_inode(inode, flags);
/* dirty 플래그 설정 */
inode->i_state |= flags;
/* writeback 큐에 등록 */
if (!(inode->i_state & I_DIRTY_ALL))
inode_io_list_move_locked(inode, wb, &wb->b_dirty);
spin_unlock(&inode->i_lock);
}
코드 설명
- __mark_inode_dirty — 중복 검사
fs/fs-writeback.c에 정의되어 있습니다. 이미 설정된 dirty 플래그를 다시 설정하는 것을 방지하기 위해(i_state & flags) == flags조건으로 조기 반환합니다. 이 fast-path는 잠금 없이 수행되어 write-heavy 워크로드에서 경합을 줄입니다. - I_NEW | I_FREEING | I_WILL_FREE 검사초기화 중(
I_NEW)이거나 해제 중(I_FREEING,I_WILL_FREE)인 inode를 dirty 마킹하면 use-after-free나 writeback 오류가 발생할 수 있으므로 무시합니다. - dirty_inode 콜백파일시스템이
super_operations.dirty_inode를 구현했다면 이 시점에 호출합니다. ext4는 이 콜백에서 저널 트랜잭션을 시작하여 메타데이터 변경을 기록합니다. Btrfs는 COW 트리에 dirty 표시를 전파합니다. - inode_io_list_move_lockedinode를 BDI writeback의
b_dirty리스트에 등록합니다. 이후 writeback 스레드(wb_workfn)가 주기적으로 이 리스트를 순회하며 dirty 데이터를 디스크에 기록합니다./proc/sys/vm/dirty_writeback_centisecs(기본 500 = 5초)가 주기를 결정합니다.
I_DIRTY 세분화의 이유: I_DIRTY_SYNC(atime 같은 경량 메타데이터), I_DIRTY_DATASYNC(size 같은 데이터 관련 메타데이터), I_DIRTY_PAGES(페이지 캐시의 dirty 페이지)로 분리함으로써 fsync()와 fdatasync()가 필요한 최소한의 기록만 수행할 수 있습니다. fdatasync()는 I_DIRTY_DATASYNC | I_DIRTY_PAGES만 기록하고, 순수 메타데이터인 I_DIRTY_SYNC는 건너뜁니다.
mark_inode_dirty() 내부 구현 상세
mark_inode_dirty()는 inode가 변경되었음을 VFS에 알려 writeback 대상에 포함시키는 핵심 함수입니다. 내부적으로 __mark_inode_dirty()를 호출하여 BDI(Backing Device Info) writeback 큐에 등록합니다.
| 호출 함수 | 전달 플래그 | 용도 |
|---|---|---|
mark_inode_dirty(inode) | I_DIRTY | 모든 dirty 비트 설정 (메타+데이터) |
mark_inode_dirty_sync(inode) | I_DIRTY_SYNC | 경량 메타데이터만 (atime 등) |
__mark_inode_dirty(inode, flags) | 직접 지정 | 세밀한 제어 (FS 내부용) |
/* fs/fs-writeback.c — __mark_inode_dirty() 전체 구현 */
void __mark_inode_dirty(struct inode *inode, int flags)
{
struct super_block *sb = inode->i_sb;
struct bdi_writeback *wb = NULL;
/* 1. 이미 해당 dirty 비트가 설정되어 있으면 빠른 종료 */
if ((inode->i_state & flags) == flags)
return;
/* 2. s_op->dirty_inode() 콜백 호출 (ext4: 저널 컨텍스트 확인) */
if (sb->s_op->dirty_inode)
sb->s_op->dirty_inode(inode, flags & (I_DIRTY_INODE | I_DIRTY_TIME));
/* 3. I_DIRTY_TIME → I_DIRTY_SYNC 승격 검사 */
/* lazytime이고 DIRTY_TIME만 설정하면 dirty list에 안 넣음 */
if ((flags & I_DIRTY_TIME) &&
!(flags & ~I_DIRTY_TIME) &&
!(inode->i_state & I_DIRTY_INODE))
{
spin_lock(&inode->i_lock);
if (inode->i_state & I_DIRTY_TIME) {
spin_unlock(&inode->i_lock);
return; /* 이미 DIRTY_TIME, b_dirty_time에 있음 */
}
inode->i_state |= I_DIRTY_TIME;
spin_unlock(&inode->i_lock);
/* b_dirty_time 리스트에만 추가 (즉시 writeback 안 함) */
inode_io_list_move_locked(inode, wb, &wb->b_dirty_time);
return;
}
/* 4. dirty 비트 설정 + writeback 큐 등록 */
spin_lock(&inode->i_lock);
if ((inode->i_state & flags) != flags) {
const int was_dirty = inode->i_state & I_DIRTY;
inode->i_state |= flags; /* dirty 비트 설정 */
if (!was_dirty) {
/* ⑤ 새로 dirty → writeback 큐에 등록 */
inode->dirtied_when = jiffies; /* dirty 시각 기록 */
/* cgroup writeback: inode의 소속 wb 결정 */
wb = locked_inode_to_wb_and_lock_list(inode);
/* b_dirty 리스트에 추가 */
inode_io_list_move_locked(inode, wb, &wb->b_dirty);
spin_unlock(&wb->list_lock);
/* ⑥ writeback 스레드 깨우기 (dirty_writeback_interval 후) */
wb_wakeup_delayed(wb);
}
}
spin_unlock(&inode->i_lock);
}
코드 설명
- 빠른 종료 (이미 dirty)이미 동일한 dirty 비트가 설정되어 있으면 중복 작업을 피합니다. 이 최적화는 inode가 빈번히 수정되는 hot path에서 스핀락 획득을 회피합니다.
- dirty_inode 콜백파일시스템에 dirty 전환을 알립니다. ext4는 이 콜백에서 현재 저널 트랜잭션 핸들을 확인하고, 필요시 저널 공간을 예약합니다.
- I_DIRTY_TIME — lazytime
lazytime마운트 옵션이 활성화되면, atime/mtime/ctime 변경은I_DIRTY_TIME만 설정합니다. 이 inode는b_dirty_time리스트에 들어가며, 24시간 또는sync()시에만 디스크에 기록됩니다. 일반 dirty(I_DIRTY_INODE)보다 writeback 빈도가 훨씬 낮아 SSD 수명과 성능에 유리합니다. - dirtied_when = jiffiesinode가 처음 dirty된 시각을 기록합니다. writeback 스레드는 이 값과
dirty_expire_centisecs(기본 30초)를 비교하여 충분히 오래된 dirty inode부터 기록합니다. - locked_inode_to_wb_and_lock_listcgroup writeback 환경에서 inode의 소속
bdi_writeback구조를 결정합니다. blkcg(Block I/O cgroup)가 활성화되면 프로세스의 cgroup에 따라 다른 wb가 선택되어, 컨테이너별 I/O 격리를 지원합니다. - wb_wakeup_delayedBDI writeback 워커 스레드를
dirty_writeback_interval(기본 5초) 후에 깨우도록 타이머를 설정합니다. 즉시 기록이 필요한 경우(IS_SYNC)wb_wakeup()로 즉시 깨웁니다.
dirty_writeback_interval 튜닝: /proc/sys/vm/dirty_writeback_centisecs (기본 500 = 5초)로 writeback 스레드의 주기를 조절합니다. 값을 줄이면 dirty 데이터가 더 빨리 디스크에 기록되어 크래시 시 데이터 손실이 줄지만, I/O 오버헤드(Overhead)가 증가합니다. dirty_expire_centisecs(기본 3000 = 30초)는 dirty inode가 디스크 기록 대상이 되는 최소 나이입니다.
address_space와 inode 연결
inode의 i_mapping 필드는 페이지 캐시와의 연결점입니다. struct address_space는 inode에 임베드되어 있으며(i_data 필드), 파일 데이터의 페이지를 관리합니다.
struct address_space {
struct inode *host; /* 소유 inode */
struct xarray i_pages; /* 페이지 캐시 (XArray) */
atomic_t i_mmap_writable; /* VM_SHARED 매핑 수 */
struct rb_root_cached i_mmap; /* private/shared 매핑 트리 */
unsigned long nrpages; /* 총 페이지 수 */
pgoff_t writeback_index; /* writeback 시작 위치 */
const struct address_space_operations *a_ops;
unsigned long flags; /* gfp_mask, 에러 상태 */
struct rw_semaphore i_mmap_rwsem; /* mmap 보호 */
void *private_data; /* FS별 데이터 */
};
/* inode 내부의 임베드된 address_space */
struct inode {
/* ... */
struct address_space *i_mapping; /* 일반적으로 &i_data를 가리킴 */
struct address_space i_data; /* 임베드된 address_space */
/* ... */
};
/* 초기화 시 i_mapping = &i_data로 설정됨 */
/* 블록 디바이스의 경우: 여러 파일이 같은 address_space를 공유할 수 있음 */
코드 설명
- host이
address_space를 소유하는 inode를 가리킵니다.a_ops콜백들이 소유 inode의 메타데이터(블록 매핑 등)에 접근할 때 사용합니다. - i_pages (XArray)페이지 캐시의 핵심 자료구조입니다. 파일 오프셋(페이지 인덱스)을 키로 하여
struct page(또는struct folio)를 저장합니다. 커널 4.20에서 기존 radix tree에서 XArray로 교체되었습니다. - i_mmap (rb_root_cached)
mmap()으로 이 파일을 매핑한 모든 VMA(Virtual Memory Area)를 추적하는 interval tree입니다.truncate나hole punch시 영향받는 매핑을 빠르게 찾아unmap_mapping_range()로 페이지 테이블 엔트리를 제거합니다. - a_ops
address_space_operations는read_folio(),writepages(),dirty_folio()등 실제 I/O를 수행하는 콜백 테이블입니다. 파일시스템마다 블록 레이아웃이 다르므로 각자 구현합니다. - i_mapping = &i_data일반 파일에서는 inode에 임베드된
i_data를 가리킵니다. 블록 디바이스 파일은bdev->bd_inode->i_data를 공유하므로i_mapping이 다른 inode의address_space를 가리킬 수 있습니다. 이 간접 참조 덕분에 같은 블록 디바이스를 여는 여러 파일이 하나의 페이지 캐시를 공유합니다.
주요 address_space_operations 콜백
| 콜백 | 호출 시점 | 역할 |
|---|---|---|
read_folio(file, folio) | 페이지 캐시 miss (read) | 디스크에서 folio(페이지)를 읽어 채움 |
readahead(rac) | 순차 읽기 감지 | 미리 읽기 — 연속 folios를 일괄 제출 |
writepages(mapping, wbc) | writeback 발동 | dirty 페이지를 디스크에 기록 |
dirty_folio(mapping, folio) | folio가 dirty 될 때 | accounting, 저널 예약 등 |
write_begin(file, mapping, pos, len, folio) | 버퍼(Buffer)드 write 시작 | folio 할당 + 블록 매핑 준비 |
write_end(file, mapping, pos, len, copied, folio) | 버퍼드 write 완료 | dirty 마킹 + 메타데이터 갱신 |
invalidate_folio(folio, offset, length) | truncate/hole punch | folio의 전부 또는 일부를 무효화(Invalidation) |
release_folio(folio, gfp) | 메모리 회수 | private 데이터 해제 후 folio 반환 가능 여부 |
XArray로의 전환: 커널 4.20부터 페이지 캐시의 인덱스 구조가 radix tree에서 XArray로 전환되었습니다. XArray는 락 통합, API 간결성, RCU 안전 순회를 제공합니다. i_pages를 통해 파일 오프셋(인덱스)으로 해당 folio를 O(log n)에 검색할 수 있습니다.
inode와 mmap 연동
mmap()은 파일의 address_space를 프로세스의 가상 주소 공간(Address Space)에 매핑합니다. 이 매핑은 inode의 i_mapping을 통해 페이지 캐시와 직접 연결되므로, 여러 프로세스가 같은 파일을 mmap하면 같은 물리 페이지를 공유합니다.
/* vm_operations_struct — mmap 페이지 폴트 핸들러 */
static const struct vm_operations_struct ext4_file_vm_ops = {
.fault = filemap_fault, /* 읽기 폴트 → 페이지 캐시에서 folio 매핑 */
.map_pages = filemap_map_pages, /* 주변 페이지 선제 매핑 (TLB miss 줄임) */
.page_mkwrite = ext4_page_mkwrite, /* 쓰기 폴트 → 블록 할당 + dirty 마킹 */
};
/* page_mkwrite — 쓰기 폴트 시 FS에 알림 */
static vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)
{
struct inode *inode = file_inode(vmf->vma->vm_file);
/* 1. i_rwsem 공유 잠금 획득 */
sb_start_pagefault(inode->i_sb);
/* 2. 블록이 아직 할당 안 됐으면 할당 (delalloc) */
ext4_map_blocks(handle, inode, &map, ...);
/* 3. 페이지를 dirty로 마킹 → writeback 대상에 포함 */
folio_mark_dirty(folio);
folio_wait_stable(folio); /* 저널링: stable write 대기 */
return VM_FAULT_LOCKED;
}
| mmap 유형 | 공유 방식 | inode 관계 | writeback |
|---|---|---|---|
MAP_SHARED | 동일 물리 페이지 공유 | i_mapping 페이지 캐시 사용 | dirty 페이지 → FS writeback |
MAP_PRIVATE | CoW (쓰기 시 복사(Copy-on-Write)) | 읽기 시 공유, 쓰기 시 분리 | 사본은 swap에 기록 |
MAP_ANONYMOUS | inode 없음 | address_space 없음 | swap에 기록 |
truncate()로 크기를 줄이면, 매핑된 영역 중 잘려나간 부분에 접근 시 SIGBUS가 발생합니다. 커널은 unmap_mapping_range()로 해당 영역의 PTE를 무효화하고, truncate_inode_pages()로 페이지 캐시에서 제거합니다. i_mmap_rwsem이 이 과정의 동시성을 보호합니다.
i_mmap interval tree: address_space의 i_mmap 필드는 구간 트리(interval tree)로, 파일의 어떤 범위가 어떤 VMA에 매핑되어 있는지 추적합니다. truncate나 hole punch 시 영향 받는 모든 VMA를 빠르게 찾아 PTE를 무효화하는 데 사용됩니다. 이 트리 없이는 모든 프로세스의 VMA를 선형 탐색해야 하므로 O(n)이 O(log n + k)로 개선됩니다.
ACL과 권한 검사
파일 접근 시 VFS는 inode_permission()을 통해 권한을 검사합니다. 이 과정에서 전통적인 Unix 권한(rwx), POSIX ACL, 보안 모듈(LSM) 검사가 순차적으로 수행됩니다.
/* fs/namei.c — 권한 검사 진입점 */
int inode_permission(struct mnt_idmap *idmap,
struct inode *inode, int mask)
{
int retval;
/* 1. sb 레벨 읽기 전용 검사 */
retval = sb_permission(inode->i_sb, inode, mask);
if (retval)
return retval;
/* 2. FS별 permission 콜백 또는 generic_permission */
if (inode->i_op->permission)
retval = inode->i_op->permission(idmap, inode, mask);
else
retval = generic_permission(idmap, inode, mask);
if (retval)
return retval;
/* 3. LSM 검사 (SELinux, AppArmor 등) */
retval = security_inode_permission(inode, mask);
return retval;
}
/* generic_permission 내부 흐름 */
int generic_permission(struct mnt_idmap *idmap,
struct inode *inode, int mask)
{
/* (a) ACL이 있으면 ACL 검사 우선 */
if (IS_POSIXACL(inode) && ...) {
retval = posix_acl_permission(idmap, inode, acl, mask);
}
/* (b) 전통적 owner/group/other 검사 */
else {
retval = acl_permission_check(idmap, inode, mask);
}
/* (c) DAC override: CAP_DAC_OVERRIDE, CAP_DAC_READ_SEARCH */
if (retval && capable_wrt_inode_uidgid(idmap, inode,
CAP_DAC_OVERRIDE))
retval = 0;
return retval;
}
코드 설명
- inode_permission — sb_permission()슈퍼블록 단위 검사입니다.
MS_RDONLY마운트에서 쓰기(MAY_WRITE) 요청 시-EROFS를 반환합니다. 또한MNT_NOEXEC마운트에서 실행(MAY_EXEC) 요청 시-EACCES를 반환합니다. inode 자체의 권한 검사 전에 항상 먼저 수행됩니다. - inode_permission — i_op->permission()파일시스템별 권한 검사 콜백입니다.
NULL이면generic_permission()을 호출합니다. NFS, CIFS 등 네트워크 파일시스템은 이를 구현하여 서버 측 권한 검사를 추가하거나 서버 응답을 캐싱합니다. - inode_permission — security_inode_permission()LSM(Linux Security Module) 훅입니다. SELinux라면 AVC(Access Vector Cache)를 조회하여 보안 정책 허용 여부를 확인합니다. AppArmor라면 프로파일 기반 검사를 수행합니다. DAC 검사가 성공해도 이 단계에서 거부될 수 있습니다.
- generic_permission — posix_acl_permission()
IS_POSIXACL(inode)이고 ACL이 캐시에 있으면 호출됩니다. POSIX ACL은ACL_USER,ACL_GROUP,ACL_MASK,ACL_OTHER항목으로 구성되며 전통적인 rwx 비트보다 세밀한 권한 제어를 제공합니다. - generic_permission — acl_permission_check()전통적인 Unix DAC 검사입니다.
idmap을 통해 idmapped mount의 uid/gid 변환을 적용한 후 소유자(owner)/그룹(group)/기타(other) 순서로 비교합니다. 첫 번째 일치하는 항목의 권한만 적용합니다. - generic_permission — capable_wrt_inode_uidgid(CAP_DAC_OVERRIDE)DAC 권한 검사 실패 후 capabilit 오버라이드를 시도합니다.
CAP_DAC_OVERRIDE는 읽기/쓰기/실행 권한을 우회합니다. 단, 실행 권한은 owner/group/other 중 하나라도 실행 비트가 있어야 오버라이드됩니다.CAP_DAC_READ_SEARCH는 읽기와 디렉터리 검색만 우회합니다.
POSIX ACL 상세
| ACL 타입 | xattr 이름 | 용도 |
|---|---|---|
| access ACL | system.posix_acl_access | 파일 접근 권한 (기본 rwx 확장) |
| default ACL | system.posix_acl_default | 디렉터리에만 설정, 새 파일에 상속 |
# POSIX ACL 사용 예시
# 현재 ACL 확인
getfacl /home/user/shared/
# 특정 사용자에게 읽기/쓰기 권한 부여
setfacl -m u:bob:rw /home/user/shared/report.txt
# 그룹에 읽기 권한 부여
setfacl -m g:devteam:r /home/user/shared/report.txt
# default ACL 설정 (새 파일에 자동 적용)
setfacl -d -m u:bob:rwx /home/user/shared/
# ACL 제거
setfacl -b /home/user/shared/report.txt
# 마스크 확인 (effective 권한 상한)
getfacl --omit-header /home/user/shared/report.txt
# user:bob:rw- #effective:r-- (마스크=r--인 경우)
chmod()를 실행하면 ACL의 mask 엔트리가 변경됩니다. 이는 ACL에서 부여한 권한을 의도치 않게 제한할 수 있습니다. ACL을 사용하는 파일에 chmod를 적용할 때는 getfacl로 결과를 반드시 확인하세요.
setattr/getattr 헬퍼 함수
VFS는 inode 속성 변경(chmod, chown, truncate)과 조회(stat)를 위한 공통 헬퍼를 제공합니다. 파일시스템은 이 헬퍼를 호출한 뒤 FS 고유의 추가 작업(저널, 블록 할당 등)을 수행합니다.
notify_change() — 속성 변경 진입점
chmod(2), chown(2), truncate(2), utimes(2) 등의 시스템콜은 모두 notify_change()를 통해 inode 속성을 변경합니다.
/* fs/attr.c — notify_change() 핵심 흐름 */
int notify_change(struct mnt_idmap *idmap,
struct dentry *dentry,
struct iattr *attr,
struct inode **delegated_inode)
{
struct inode *inode = dentry->d_inode;
int error;
/* 1. SGID 비트 자동 제거 (비소유자 write 시) */
if (attr->ia_valid & ATTR_MODE) {
if (!in_group_or_capable(idmap, inode, attr->ia_vfsgid))
attr->ia_mode &= ~S_ISGID;
}
/* 2. 보안/사전 검사 */
error = security_inode_setattr(idmap, dentry, attr);
if (error)
return error;
/* 3. FS별 setattr 콜백 호출 */
if (inode->i_op->setattr)
error = inode->i_op->setattr(idmap, dentry, attr);
else
error = simple_setattr(idmap, dentry, attr);
if (!error) {
/* 4. fsnotify 이벤트 발생 */
fsnotify_change(dentry, attr->ia_valid);
}
return error;
}
/* simple_setattr — 기본 setattr 구현 (tmpfs, ramfs 등) */
int simple_setattr(struct mnt_idmap *idmap,
struct dentry *dentry,
struct iattr *iattr)
{
struct inode *inode = d_inode(dentry);
int error;
error = setattr_prepare(idmap, dentry, iattr);
if (error)
return error;
if (iattr->ia_valid & ATTR_SIZE)
truncate_setsize(inode, iattr->ia_size);
setattr_copy(idmap, inode, iattr);
mark_inode_dirty(inode);
return 0;
}
코드 설명
- notify_change — SGID 제거파일의 그룹에 속하지 않은 사용자가 mode를 변경할 때, SGID 비트를 자동으로 제거합니다. 이는 보안을 위한 POSIX 요구사항입니다 — SGID 파일을 실행하면 그룹 권한이 상승하므로, 소유 그룹이 아닌 사용자가 임의로 설정할 수 없습니다.
- notify_change — security_inode_setattrLSM 훅으로, SELinux/AppArmor가 속성 변경을 허용하는지 검사합니다. DAC 검사보다 후에 실행됩니다.
- simple_setattr
inode_operations.setattr가 NULL인 경우의 기본 구현입니다.setattr_prepare()로 사전 검증,truncate_setsize()로 크기 변경,setattr_copy()로 필드 복사,mark_inode_dirty()로 dirty 마킹을 순서대로 수행합니다. tmpfs, ramfs 등 단순 파일시스템이 사용합니다.
setattr_prepare() — 사전 검증
setattr_prepare()는 속성 변경이 허용되는지 사전에 검증합니다. 모든 파일시스템의 setattr 콜백에서 반드시 호출해야 합니다.
/* fs/attr.c — setattr_prepare() */
int setattr_prepare(struct mnt_idmap *idmap,
struct dentry *dentry,
struct iattr *attr)
{
struct inode *inode = d_inode(dentry);
unsigned int ia_valid = attr->ia_valid;
/* 1. immutable/append-only 검사 */
if (IS_IMMUTABLE(inode))
return -EPERM;
if (IS_APPEND(inode) && (ia_valid & (ATTR_MODE | ATTR_UID |
ATTR_GID | ATTR_TIMES_SET | ATTR_SIZE)))
return -EPERM;
/* 2. UID 변경: 소유자이거나 CAP_CHOWN 필요 */
if (ia_valid & ATTR_UID) {
vfsuid_t vfsuid = make_vfsuid(idmap, ...);
if (!inode_owner_or_capable(idmap, inode) &&
!capable_wrt_inode_uidgid(idmap, inode, CAP_CHOWN))
return -EPERM;
}
/* 3. GID 변경: 소유자이거나 CAP_CHOWN 필요 */
if (ia_valid & ATTR_GID) {
/* 유사한 검사 */
}
/* 4. 크기 변경: 쓰기 권한 + 파일 크기 제한 검사 */
if (ia_valid & ATTR_SIZE) {
int error = inode_newsize_ok(inode, attr->ia_size);
if (error)
return error;
}
/* 5. 모드(권한) 변경: 소유자이거나 CAP_FOWNER 필요 */
if (ia_valid & ATTR_MODE) {
if (!inode_owner_or_capable(idmap, inode))
return -EPERM;
/* 비소유 그룹이면 SGID 제거 */
}
/* 6. 타임스탬프 변경: 소유자이거나 쓰기 권한 */
if (ia_valid & ATTR_TIMES_SET) {
if (!inode_owner_or_capable(idmap, inode))
return -EPERM;
}
return 0;
}
setattr_copy() — 필드 복사
/* fs/attr.c — setattr_copy(): iattr에서 inode로 필드 복사 */
void setattr_copy(struct mnt_idmap *idmap,
struct inode *inode,
const struct iattr *attr)
{
unsigned int ia_valid = attr->ia_valid;
/* UID 복사 (idmap 변환 적용) */
if (ia_valid & ATTR_UID)
inode->i_uid = vfsuid_into_kuid(attr->ia_vfsuid);
/* GID 복사 */
if (ia_valid & ATTR_GID)
inode->i_gid = vfsgid_into_kgid(attr->ia_vfsgid);
/* 크기 복사 (truncate_setsize가 이미 처리했을 수 있음) */
if (ia_valid & ATTR_SIZE)
i_size_write(inode, attr->ia_size);
/* 타임스탬프 복사 */
if (ia_valid & ATTR_ATIME)
inode_set_atime_to_ts(inode, attr->ia_atime);
if (ia_valid & ATTR_MTIME)
inode_set_mtime_to_ts(inode, attr->ia_mtime);
if (ia_valid & ATTR_CTIME)
inode_set_ctime_to_ts(inode, attr->ia_ctime);
/* 모드 복사 + SGID 조건부 제거 */
if (ia_valid & ATTR_MODE) {
umode_t mode = attr->ia_mode;
/* 그룹 실행 비트 없으면 SGID 무의미 → 제거 */
if (!in_group_or_capable(idmap, inode,
i_gid_into_vfsgid(idmap, inode)))
mode &= ~S_ISGID;
inode->i_mode = mode;
}
}
코드 설명
- setattr_copy — vfsuid_into_kuididmapped mount 환경에서
iattr의 vfsuid(마운트 네임스페이스의 uid)를 커널 내부 kuid로 변환합니다. 일반 마운트에서는 단순 캐스팅(Cast)입니다. - setattr_copy — ATTR_MODE + SGID모드를 설정할 때 소유 그룹이 아닌 경우 SGID 비트를 자동 제거합니다. 이는
chmod g+s로 설정된 SGID가 소유 그룹 변경 후에도 남아있으면 보안 위험이 되기 때문입니다. - setattr_copy — i_size_write크기 변경은
truncate_setsize()가 이미 처리했을 수 있지만,setattr_copy()에서도 안전하게 중복 설정합니다.i_size_write()를 사용하여 32비트 원자성을 보장합니다.
generic_fillattr() — stat 채우기
stat(2)/statx(2) 시스템콜에서 VFS가 inode_operations.getattr을 호출하면, 파일시스템은 generic_fillattr()를 사용하여 struct kstat를 채웁니다.
/* fs/stat.c — generic_fillattr() */
void generic_fillattr(struct mnt_idmap *idmap,
u32 request_mask,
struct inode *inode,
struct kstat *stat)
{
/* idmap 변환 적용 후 uid/gid 설정 */
stat->uid = vfsuid_into_kuid(i_uid_into_vfsuid(idmap, inode));
stat->gid = vfsgid_into_kgid(i_gid_into_vfsgid(idmap, inode));
stat->dev = inode->i_sb->s_dev;
stat->ino = inode->i_ino;
stat->mode = inode->i_mode;
stat->nlink = inode->i_nlink;
stat->size = i_size_read(inode);
stat->blocks = inode->i_blocks;
stat->blksize = i_blocksize(inode);
/* 타임스탬프 복사 (접근자 사용) */
stat->atime = inode_get_atime(inode);
stat->mtime = inode_get_mtime(inode);
stat->ctime = inode_get_ctime(inode);
/* rdev — 블록/캐릭터 디바이스 번호 */
stat->rdev = inode->i_rdev;
stat->result_mask |= STATX_BASIC_STATS;
}
/* 파일시스템의 getattr 구현 예시 */
static int myfs_getattr(struct mnt_idmap *idmap,
const struct path *path,
struct kstat *stat,
u32 request_mask, unsigned int query_flags)
{
struct inode *inode = d_inode(path->dentry);
/* 기본 필드 채우기 */
generic_fillattr(idmap, request_mask, inode, stat);
/* FS 고유 확장 정보 (statx) */
if (request_mask & STATX_BTIME)
stat->btime = MYFS_I(inode)->i_crtime;
stat->attributes |= STATX_ATTR_IMMUTABLE;
stat->attributes_mask |= STATX_ATTR_IMMUTABLE;
return 0;
}
| inode 필드 | kstat 필드 | stat(2) 출력 |
|---|---|---|
i_ino | stat->ino | st_ino |
i_mode | stat->mode | st_mode |
i_nlink | stat->nlink | st_nlink |
i_uid (idmap 변환) | stat->uid | st_uid |
i_gid (idmap 변환) | stat->gid | st_gid |
i_size | stat->size | st_size |
i_blocks | stat->blocks | st_blocks (512B 단위) |
__i_atime | stat->atime | st_atime |
__i_mtime | stat->mtime | st_mtime |
__i_ctime | stat->ctime | st_ctime |
타임스탬프 정밀도와 Y2038
inode 타임스탬프는 파일 메타데이터 중 가장 빈번하게 갱신되는 필드입니다. 커널은 struct timespec64로 나노초 정밀도를 지원하지만, 실제 저장 정밀도는 파일시스템에 따라 다릅니다.
| 파일시스템 | 타임스탬프 범위 | 저장 정밀도 | crtime (생성 시간) |
|---|---|---|---|
| ext4 (128B inode) | 1901~2038 | 1초 | 미지원 |
| ext4 (256B inode) | 1901~2446 | 1나노초 | 지원 (statx) |
| XFS | 1901~2486 | 1나노초 | 지원 (statx) |
| Btrfs | 1901~2486 | 1나노초 | 지원 (statx) |
| tmpfs | 1901~2486 | 1나노초 | 미지원 |
| FAT | 1980~2107 | 2초 | 10ms (ctime 필드) |
| NTFS | 1601~30828 | 100나노초 | 지원 |
/* VFS 타임스탬프 API (커널 6.x) */
/* 타임스탬프 읽기 — 접근자 함수 사용 필수 */
struct timespec64 ts = inode_get_atime(inode);
struct timespec64 ts = inode_get_mtime(inode);
struct timespec64 ts = inode_get_ctime(inode);
/* 타임스탬프 설정 */
inode_set_atime_to_ts(inode, ts);
inode_set_mtime_to_ts(inode, ts);
inode_set_ctime_to_ts(inode, ts);
/* 현재 시간으로 설정 (FS의 정밀도에 맞춰 자동 truncate) */
inode_set_ctime_current(inode);
/* 정밀도 선언 — superblock에서 설정 */
sb->s_time_gran = 1; /* 나노초 정밀도 */
sb->s_time_gran = NSEC_PER_SEC; /* 초 단위 정밀도 */
sb->s_time_min = (s64)0; /* 최소 타임스탬프 */
sb->s_time_max = (s64)U32_MAX; /* 최대 타임스탬프 */
/* Y2038 안전성 — timespec64는 64비트 초 필드 사용 */
struct timespec64 {
time64_t tv_sec; /* 64비트 초 (Y2038 안전) */
long tv_nsec; /* 나노초 (0 ~ 999999999) */
};
# statx로 확장 타임스탬프 확인 (crtime 포함)
python3 -c "
import os
result = os.stat('/home/user/test.txt')
print(f'atime: {result.st_atime}')
print(f'mtime: {result.st_mtime}')
print(f'ctime: {result.st_ctime}')
"
# 또는 stat 명령어 사용
stat /home/user/test.txt
# 출력에서 Access, Modify, Change, Birth 시간 확인
# lazytime 마운트 옵션: atime을 메모리에서만 갱신
mount -o remount,lazytime /
# relatime 확인 (기본값)
mount | grep relatime
Y2038 문제와 커널: 32비트 time_t는 2038년 1월 19일에 오버플로됩니다. VFS의 timespec64 전환은 커널 5.x에서 완료되었지만, 디스크 포맷이 32비트인 ext4 (128바이트 inode)에서는 여전히 2038 제한이 있습니다. tune2fs -l /dev/sda1 | grep 'Inode size'로 현재 inode 크기를 확인하고, 256바이트 이상이면 Y2038 안전합니다.
inotify/fanotify 이벤트 통지
VFS 파일 이벤트 통지 시스템은 fsnotify 인프라 위에 구축됩니다. inode에 감시 마크(watch mark)를 부착하면, 해당 inode에서 발생하는 이벤트가 유저스페이스로 전달됩니다.
fsnotify 아키텍처
/* fs/notify 구조 */
/*
* fsnotify_group — 이벤트 수신자 (inotify fd 또는 fanotify fd)
* fsnotify_mark — inode/mount/sb에 부착된 감시 마크
* fsnotify_event — 발생한 이벤트 (큐에 저장)
*/
struct fsnotify_mark {
struct fsnotify_group *group; /* 소속 그룹 */
union {
struct inode *inode; /* inode 마크 */
struct vfsmount *mnt; /* 마운트 마크 */
struct super_block *sb; /* sb 마크 (fanotify) */
} connector_target;
__u32 mask; /* 감시할 이벤트 마스크 */
__u32 ignore_mask; /* 무시할 이벤트 */
};
/* VFS 내부에서 이벤트 발생 호출 (예: vfs_write 완료 시) */
void fsnotify_modify(struct file *file)
{
struct inode *inode = file_inode(file);
if (!(inode->i_fsnotify_mask & FS_MODIFY))
return;
fsnotify_parent(file->f_path.dentry, FS_MODIFY, file, FSNOTIFY_EVENT_PATH);
fsnotify(inode, FS_MODIFY, file, FSNOTIFY_EVENT_PATH, NULL, 0);
}
inotify vs fanotify 비교
| 특성 | inotify | fanotify |
|---|---|---|
| 감시 단위 | 파일/디렉터리별 watch | 마운트/파일시스템 전체 |
| 재귀 감시 | 미지원 (수동 추가 필요) | 마운트 전체 자동 |
| 접근 제어 | 불가 | FAN_ACCESS_PERM (허용/거부) |
| 이벤트 정보 | 파일명 포함 | 파일 디스크립터 제공 (fid) |
| 권한 필요 | 일반 사용자 | CAP_SYS_ADMIN (일부 기능) |
| 커널 버전 | 2.6.13+ | 2.6.37+ |
| 주요 용도 | IDE 파일 감시, 빌드 도구 | 안티바이러스, 감사(audit) |
| 오버플로 처리 | IN_Q_OVERFLOW | FAN_Q_OVERFLOW |
writeback 파이프라인(Pipeline)
dirty inode의 데이터를 디스크에 기록하는 과정을 writeback이라 합니다. 커널은 백그라운드 워커 스레드(Thread)를 통해 비동기적으로 writeback을 수행하며, 이 과정에서 inode의 상태 플래그가 핵심 역할을 합니다.
writeback 발동 조건
| 트리거 | 조건 | sysctl 관련 |
|---|---|---|
| 주기적 writeback | dirty 후 일정 시간 경과 | dirty_writeback_centisecs (기본 500 = 5초) |
| dirty 임계값 초과 | 시스템 dirty 비율 초과 | dirty_background_ratio (기본 10%) |
| fsync/fdatasync | 사용자 명시적 요청 | 해당 없음 |
| sync 시스템콜 | 전체 파일시스템 동기화 | 해당 없음 |
| umount | 파일시스템 언마운트 | 해당 없음 |
| 메모리 압력 | free 메모리 부족 | dirty_ratio (기본 20%) |
/* writeback 워커 핵심 루프 — fs/fs-writeback.c */
static long writeback_sb_inodes(struct super_block *sb,
struct bdi_writeback *wb,
struct wb_writeback_work *work)
{
while (!list_empty(&wb->b_io)) {
struct inode *inode = wb_inode(wb->b_io.prev);
spin_lock(&inode->i_lock);
/* I_SYNC 설정 (다른 스레드의 동시 writeback 방지) */
inode->i_state |= I_SYNC;
spin_unlock(&inode->i_lock);
/* dirty 페이지 기록 */
__writeback_single_inode(inode, &wbc);
spin_lock(&inode->i_lock);
inode->i_state &= ~I_SYNC;
/* writeback 중 재-dirty되었으면 b_more_io로 이동 */
if (inode->i_state & I_DIRTY)
inode_io_list_move_locked(inode, wb, &wb->b_more_io);
else
list_del_init(&inode->i_io_list); /* clean */
spin_unlock(&inode->i_lock);
}
}
코드 설명
- writeback_sb_inodes
fs/fs-writeback.c에 정의된 writeback 워커의 핵심 루프입니다. BDI(Backing Device Info)의b_io리스트에서 dirty inode를 하나씩 꺼내 처리합니다.wb_workfn()→wb_do_writeback()→wb_writeback()호출 체인에서 실행됩니다. - I_SYNC 설정현재 writeback 진행 중임을 표시합니다. 다른 스레드가
fsync()로 같은 inode를 동시에 기록하려 하면I_SYNC가 해제될 때까지inode_wait_for_writeback()에서 대기합니다. 이중 기록을 방지하는 배타적 잠금 역할입니다. - __writeback_single_inode실제 I/O를 수행하는 함수입니다.
a_ops->writepages()로 dirty 페이지를 기록하고,write_inode()로 메타데이터를 디스크에 씁니다.wbc(writeback_control) 구조체가 기록 범위와 동기화 모드를 제어합니다. - b_more_io 이동writeback 도중 프로세스가 같은 inode에 다시 쓰기를 하면 re-dirty 상태가 됩니다. 이 경우
b_more_io리스트로 이동하여 다음 writeback 라운드에서 재처리됩니다. clean 상태가 되면i_io_list에서 완전히 제거됩니다.
writeback 성능 튜닝: 데이터베이스 서버에서는 dirty_background_ratio를 낮게(5%) 설정하여 빈번한 소량 기록을, 대용량 순차 쓰기 워크로드에서는 높게(20-40%) 설정하여 대량 배치 기록을 유도합니다. /proc/meminfo의 Dirty와 Writeback 값을 모니터링하세요.
파일시스템 Freeze/Thaw와 inode
파일시스템 freeze는 모든 쓰기 I/O를 중단시키고 일관된 스냅샷 상태를 만드는 메커니즘입니다. LVM 스냅샷, 온라인 백업 등에 필수적이며, inode의 dirty 상태와 writeback에 직접적 영향을 줍니다.
/* fs/super.c — freeze/thaw 핵심 경로 */
/* freeze 수준 (단계적 진행) */
enum {
SB_UNFROZEN = 0, /* 정상 */
SB_FREEZE_WRITE = 1, /* 새 쓰기 거부 */
SB_FREEZE_PAGEFAULT = 2, /* mmap 쓰기 폴트 거부 */
SB_FREEZE_FS = 3, /* FS 내부 트랜잭션 완료 대기 */
SB_FREEZE_COMPLETE = 4, /* 완전 freeze 완료 */
};
int freeze_super(struct super_block *sb, enum freeze_holder who)
{
/* 1. SB_FREEZE_WRITE → 새 write/truncate 차단 */
sb_wait_write(sb, SB_FREEZE_WRITE);
/* 2. SB_FREEZE_PAGEFAULT → mmap page_mkwrite 차단 */
sb_wait_write(sb, SB_FREEZE_PAGEFAULT);
/* 3. dirty inode writeback 강제 실행 + 대기 */
sync_filesystem(sb);
/* 4. SB_FREEZE_FS → FS별 freeze 콜백 */
if (sb->s_op->freeze_fs)
sb->s_op->freeze_fs(sb);
/* ext4: 저널 커밋 + 배리어 */
/* XFS: 로그 quiesce */
/* Btrfs: 트랜잭션 커밋 */
}
| freeze 단계 | 차단 대상 | inode 영향 |
|---|---|---|
SB_FREEZE_WRITE | vfs_write(), vfs_truncate() | 새 dirty 마킹 차단, 진행 중인 write 완료 대기 |
SB_FREEZE_PAGEFAULT | page_mkwrite() | mmap 쓰기 폴트 대기 — dirty 페이지 생성 차단 |
SB_FREEZE_FS | FS 내부 트랜잭션(Transaction) | 모든 dirty inode writeback 완료, 저널/로그 flush |
SB_FREEZE_COMPLETE | 전체 | 디스크에 모든 inode가 깨끗한(clean) 상태 |
# 사용자 공간에서 freeze/thaw
fsfreeze -f /mnt/data # freeze → 모든 I/O 중단
# ... LVM 스냅샷 생성, 블록 레벨 백업 등 ...
fsfreeze -u /mnt/data # thaw → I/O 재개
# freeze 상태 확인
cat /proc/mounts | grep /mnt/data
# 또는
xfs_freeze -s /mnt/data # XFS 전용 (fsfreeze와 동일)
fsfreeze -f 상태에서 해당 파일시스템에 쓰기를 시도하는 프로세스는 무한 대기(uninterruptible sleep)에 빠집니다. thaw 없이 오래 유지하면 시스템이 멈춘 것처럼 보일 수 있습니다. 루트 파일시스템은 절대 freeze하지 마세요 — 시스템 전체가 정지합니다.
파일시스템별 inode 구현 비교
각 파일시스템은 VFS struct inode를 자체 구조체에 임베드하여 확장합니다. 이 패턴은 C 언어에서 상속 없이 다형성을 구현하는 커널의 대표적 기법입니다.
container_of 패턴
/* container_of 매크로 — include/linux/container_of.h */
#define container_of(ptr, type, member) ({ \
void *__mptr = (void *)(ptr); \
(type *)(__mptr - offsetof(type, member)); })
/* 각 파일시스템의 변환 매크로 */
/* ext4 */
static inline struct ext4_inode_info *EXT4_I(struct inode *inode)
{
return container_of(inode, struct ext4_inode_info, vfs_inode);
}
/* XFS */
static inline struct xfs_inode *XFS_I(struct inode *inode)
{
return container_of(inode, struct xfs_inode, vfs_inode);
}
/* Btrfs */
static inline struct btrfs_inode *BTRFS_I(struct inode *inode)
{
return container_of(inode, struct btrfs_inode, vfs_inode);
}
/* 사용 예시: VFS에서 FS-specific 데이터 접근 */
static int ext4_file_open(struct inode *inode,
struct file *filp)
{
struct ext4_inode_info *ei = EXT4_I(inode);
/* ei->i_data, ei->i_flags 등 접근 */
}
파일시스템 inode 확장 필드 비교
| 필드 카테고리 | ext4 | XFS | Btrfs |
|---|---|---|---|
| 데이터 위치 | i_data[15] (extent/block) | data fork (extent B+tree) | file extent item (B-tree) |
| 속성 저장 | inode 잔여공간 + EA block | attr fork | xattr item (B-tree) |
| inode 번호 | 고정 테이블 인덱스 | AG 내 동적 할당 | objectid (서브볼륨별) |
| CoW 지원 | 미지원 | reflink (4.16+) | 기본 (전체 CoW) |
| 인라인 데이터 | inline_data 옵션 | local format | inline extent |
| 압축 | 미지원 | 미지원 | zstd, lzo, zlib |
| 암호화(Encryption) | fscrypt | 미지원 | 미지원 (계획 중) |
| slab 캐시 | ext4_inode_cache | xfs_inode_cache | btrfs_inode_cache |
inode 캐시와 LRU 관리
사용이 끝난 inode(i_count == 0)는 즉시 삭제되지 않고 LRU 리스트에 들어가 캐시됩니다. 이후 동일 파일 재접근 시 디스크 I/O 없이 즉시 반환할 수 있습니다.
LRU 메커니즘
/* iput() — 참조 해제 경로 */
void iput(struct inode *inode)
{
if (!inode)
return;
if (atomic_dec_and_lock(&inode->i_count, &inode->i_lock)) {
/* i_count가 0이 됨 */
if (inode->i_nlink &&
(inode->i_state & ~I_DIRTY_TIME) == 0) {
/* 링크 남아있고 clean → LRU에 추가 */
inode_add_lru(inode);
spin_unlock(&inode->i_lock);
} else {
/* nlink==0이면 즉시 evict */
inode->i_state |= I_WILL_FREE;
spin_unlock(&inode->i_lock);
evict(inode);
}
}
}
/* LRU 추가 — i_lru 리스트에 연결 */
static void inode_add_lru(struct inode *inode)
{
if (!(inode->i_sb->s_flags & SB_ACTIVE))
return;
/* I_REFERENCED 설정 → 2차 기회 */
inode->i_state |= I_REFERENCED;
list_lru_add(&inode->i_sb->s_inode_lru, &inode->i_lru);
/* percpu 카운터 증가 */
this_cpu_inc(nr_unused);
}
코드 설명
- iput — atomic_dec_and_lock
i_count를 원자적으로 감소시키면서, 0이 되는 순간i_lock을 획득합니다. 이 원자적 결합은 "감소 후 잠금 사이"에 다른 스레드가__iget()으로 참조를 가져가는 경쟁 조건을 방지합니다.fs/inode.c에 정의되어 있습니다. - i_nlink && clean → LRU 추가링크 카운트가 남아 있고 dirty 상태가 아니면, 이 inode는 나중에 다시 사용될 가능성이 있으므로 즉시 해제하지 않고 LRU(Least Recently Used) 캐시에 추가합니다. 메모리 압박 시
prune_icache_sb()가 LRU에서 오래된 inode를 회수합니다. - nlink==0 → evict링크 카운트가 0이면 파일이 삭제된 상태이므로 캐싱할 의미가 없습니다.
I_WILL_FREE를 설정하여 다른 스레드에게 evict 예정임을 알리고,evict()를 호출하여 페이지 캐시 해제, 디스크 블록 반환, 해시 테이블 제거를 수행합니다. - inode_add_lru — I_REFERENCED2차 기회(second chance) 알고리즘의 핵심입니다. LRU에 추가할 때
I_REFERENCED비트를 설정합니다. 메모리 회수 시 이 비트가 설정된 inode는 한 번 더 기회를 받고 비트만 클리어됩니다. 두 번째 순회에서도 참조되지 않았으면 그때 evict됩니다. - SB_ACTIVE 검사superblock이 비활성 상태(umount 진행 중)이면 LRU에 추가하지 않습니다. umount 경로에서는 모든 inode가 즉시 해제되어야 하므로 캐싱이 무의미합니다.
# inode 캐시 모니터링
# 할당된 inode 수 / free inode 수
cat /proc/sys/fs/inode-nr
# 예: 45678 234 (할당 45678, 빈 234)
# 상태별 inode 수
cat /proc/sys/fs/inode-state
# nr_inodes nr_free_inodes preshrink
# slab 캐시에서 inode 캐시 확인
slabtop -o | grep inode_cache
# 또는
cat /proc/slabinfo | grep inode_cache
# vfs_cache_pressure 확인 및 변경
cat /proc/sys/vm/vfs_cache_pressure
# 기본값: 100
# 파일 서버에서 inode 캐시 유지 선호
echo 50 > /proc/sys/vm/vfs_cache_pressure
# dentry 캐시와의 비교
cat /proc/sys/fs/dentry-state
# nr_dentry nr_unused age_limit want_pages
slabtop에서 inode_cache와 dentry_cache가 메모리의 대부분을 차지하고 있다면, vfs_cache_pressure를 높이거나 drop_caches로 수동 해제를 고려하세요. 단, drop_caches는 프로덕션 환경에서 성능 저하를 유발할 수 있으므로 주의가 필요합니다.
디스크 inode 레이아웃 (ext4)
메모리의 VFS inode와 달리, 디스크 inode는 파일시스템 포맷에 따라 고정된 레이아웃을 가집니다. ext4를 예로 들어 디스크 inode의 물리적 구조를 살펴봅니다.
/* include/linux/ext4.h — 디스크 inode 구조 */
struct ext4_inode {
__le16 i_mode; /* 파일 유형 + 권한 */
__le16 i_uid; /* 소유자 UID 하위 16비트 */
__le32 i_size_lo; /* 파일 크기 하위 32비트 */
__le32 i_atime; /* 접근 시간 (초) */
__le32 i_ctime; /* inode 변경 시간 */
__le32 i_mtime; /* 데이터 수정 시간 */
__le32 i_dtime; /* 삭제 시간 */
__le16 i_gid; /* 그룹 GID 하위 16비트 */
__le16 i_links_count; /* 하드 링크 수 */
__le32 i_blocks_lo; /* 512B 블록 수 */
__le32 i_flags; /* ext4 플래그 */
union {
struct { __le32 l_i_version; } linux1;
} osd1;
__le32 i_block[15]; /* 60바이트: extent tree 또는 블록 포인터 */
__le32 i_generation; /* NFS 파일 핸들 세대 */
__le32 i_file_acl_lo; /* ACL 블록 (하위) */
__le32 i_size_high; /* 파일 크기 상위 32비트 */
/* --- 128바이트 경계 --- */
/* 확장 필드 (256B inode) */
__le16 i_extra_isize; /* 확장 필드 크기 */
__le16 i_checksum_hi; /* crc32c 상위 */
__le32 i_ctime_extra; /* ctime 나노초 + 에포크 확장 */
__le32 i_mtime_extra; /* mtime 나노초 */
__le32 i_atime_extra; /* atime 나노초 */
__le32 i_crtime; /* 생성 시간 (초) */
__le32 i_crtime_extra; /* 생성 시간 나노초 */
__le32 i_version_hi; /* NFS 버전 상위 */
__le32 i_projid; /* 프로젝트 ID */
};
# ext4 디스크 inode 직접 조회
# debugfs로 inode 정보 확인
debugfs -R "stat <42>" /dev/sda1
# Inode: 42 Type: regular Mode: 0644 Flags: 0x80000
# Generation: 1234567890 Version: 0x00000001
# User: 1000 Group: 1000 Size: 4096
# File ACL: 0
# Links: 1 Blockcount: 8
# Fragment: Address: 0 Number: 0 Size: 0
# ctime: 0x65a12345:12345678 -- ...
# atime: 0x65a12346:00000000 -- ...
# mtime: 0x65a12345:12345678 -- ...
# crtime: 0x65a00000:00000000 -- ...
# EXTENTS:
# (0):1234567
# inode 크기 확인
tune2fs -l /dev/sda1 | grep "Inode size"
# Inode size: 256
# inode 테이블 위치 확인
dumpe2fs /dev/sda1 | grep "Inode table"
# Group 0: Inode table at 1025-1536
# statx로 crtime 확인 (유저스페이스)
stat /home/user/test.txt
# Birth: 2024-01-15 10:30:00.123456789 +0900
VFS 객체 관계 종합
지금까지 다룬 inode와 관련된 VFS 핵심 객체들의 관계를 종합적으로 정리합니다. superblock, inode, dentry, file 네 객체가 어떻게 연결되는지 이해하는 것이 VFS 전체를 파악하는 핵심입니다.
| 객체 | 식별 기준 | 생명주기 | 캐시 전략 |
|---|---|---|---|
super_block | 파일시스템 (디바이스) | mount ~ umount | 항상 메모리에 유지 |
inode | (sb, i_ino) 쌍 | 최초 접근 ~ evict | 해시 + LRU |
dentry | 경로명 컴포넌트 | 최초 조회 ~ shrink | 해시 + LRU |
file | 프로세스별 열린 파일 | open ~ close | 캐시 없음 (1:1 매핑) |
address_space | inode에 임베드 | inode와 동일 | XArray (radix tree) |
inode evict 경로
inode가 시스템에서 완전히 제거되는 과정을 evict라 합니다. evict는 메모리 회수(LRU shrinker) 또는 파일 삭제(i_nlink == 0) 시 발생합니다.
/* fs/inode.c — evict() 핵심 */
static void evict(struct inode *inode)
{
const struct super_operations *op = inode->i_sb->s_op;
/* 1. BDI writeback 큐에서 제거 */
inode_io_list_del(inode);
/* 2. sb의 inode 리스트에서 제거 */
inode_sb_list_del(inode);
/* 3. I_FREEING 상태 설정 (다른 스레드에게 evict 중임을 알림) */
spin_lock(&inode->i_lock);
inode->i_state |= I_FREEING;
spin_unlock(&inode->i_lock);
/* 4. FS별 evict_inode 호출 */
if (op->evict_inode)
op->evict_inode(inode);
else {
truncate_inode_pages_final(&inode->i_data);
clear_inode(inode);
}
/* 5. 해시 테이블에서 제거 */
remove_inode_hash(inode);
/* 6. I_CLEAR 설정, 대기자 깨우기 */
spin_lock(&inode->i_lock);
wake_up_bit(&inode->i_state, __I_NEW);
inode->i_state = I_FREEING | I_CLEAR;
spin_unlock(&inode->i_lock);
/* 7. 메모리 해제 */
destroy_inode(inode); /* → free_inode() 또는 kmem_cache_free() */
}
/* FS별 evict_inode 구현 예시 (ext4) */
void ext4_evict_inode(struct inode *inode)
{
if (inode->i_nlink || is_bad_inode(inode))
goto no_delete;
/* nlink == 0: 실제 삭제 */
dquot_initialize(inode); /* 쿼타 정보 초기화 */
ext4_begin_ordered_truncate(inode, 0);
truncate_inode_pages_final(&inode->i_data);
/* 저널에 삭제 트랜잭션 기록 */
handle = ext4_journal_start(inode, EXT4_HT_TRUNCATE, ...);
ext4_mark_inode_dirty(handle, inode);
ext4_free_inode(handle, inode); /* inode 비트맵 해제 */
ext4_journal_stop(handle);
return;
no_delete:
/* 링크 남아있음: 데이터 페이지만 해제 */
truncate_inode_pages_final(&inode->i_data);
clear_inode(inode);
}
코드 설명
- evict — inode_io_list_del()BDI(Backing Device Info) writeback 큐에서 이 inode를 제거합니다. writeback 스레드가 이 inode를 처리 중이었다면 완료 후 제거됩니다. 이후에는 이 inode에 대한 writeback이 스케줄되지 않습니다.
- evict — inode_sb_list_del()
super_block.s_inodes연결 리스트에서 제거합니다. 이 리스트는 파일시스템 umount 시 전체 inode를 순회하거나sync_inodes_sb()에서 사용합니다. - evict — I_FREEING 설정
i_lock스핀락을 보유한 상태에서I_FREEING비트를 설정합니다. 이후find_inode_fast()가 이 inode를 발견하면__wait_on_freeing_inode()를 호출하여 evict 완료를 기다립니다. 이 "예고(tombstone)" 메커니즘이 use-after-free를 방지합니다. - evict — op->evict_inode(inode)파일시스템별 evict 콜백입니다.
NULL이면 VFS가 기본 동작(truncate_inode_pages_final + clear_inode)을 수행합니다. ext4는ext4_evict_inode()를 구현하여 저널 트랜잭션을 통한 디스크 inode 해제를 처리합니다. - evict — remove_inode_hash()
i_hashhlist 노드를 해시 테이블에서 분리합니다. 이후에는iget_locked()로 이 inode를 검색할 수 없습니다.I_FREEING설정 후,op->evict_inode호출 후에 수행됩니다. - evict — wake_up_bit(__I_NEW) + I_CLEAR
I_FREEING | I_CLEAR상태로 전환하고,__wait_on_freeing_inode()에서 대기 중인 스레드들을 깨웁니다. 깨어난 스레드들은find_inode_fast()에서 재시도하여 이 inode가 없음을 확인합니다. - evict — destroy_inode()RCU 유예기간 후 실제 메모리를 해제합니다.
s_op->free_inode가 있으면 RCU 콜백으로 등록하고, 없으면kmem_cache_free(inode_cachep, inode)를 직접 호출합니다. RCU 보호는rcu_read_lock()상태에서 inode 포인터를 역참조하는 RCU 워커들이 완료할 때까지 대기합니다. - ext4_evict_inode — i_nlink 분기
i_nlink != 0이면 메모리에서만 제거(evict)되는 것이므로 디스크 inode는 보존합니다.i_nlink == 0이면 unlink된 파일이므로 디스크의 inode 비트맵과 데이터 블록을 해제합니다.is_bad_inode()검사는 I/O 에러로 손상된 inode를 안전하게 건너뜁니다. - ext4_evict_inode — dquot_initialize()디스크 해제 전에 쿼타(Quota) 컨텍스트를 초기화합니다. inode의 블록과 inode 카운트를 해당 사용자/그룹 쿼타 계정에서 차감하기 위해 필요합니다. 쿼타가 비활성화되어 있으면 no-op입니다.
삭제된 파일과 열린 fd: unlink()로 파일을 삭제해도 i_nlink만 0이 됩니다. 해당 파일을 열고 있는 프로세스가 있다면(i_count > 0), 실제 evict는 마지막 close()가 수행될 때까지 지연(Latency)됩니다. 이 동안 /proc/PID/fd/에서 (deleted) 표시로 확인할 수 있으며, 프로세스는 정상적으로 read/write를 계속할 수 있습니다. 이것이 로그 파일 삭제 후에도 디스크 공간이 즉시 해제되지 않는 이유입니다.
iput() 전체 콜 체인
iput()는 inode 참조를 해제하는 핵심 함수입니다. i_count를 감소시키고, 0이 되면 inode의 i_nlink과 dirty 상태에 따라 LRU 캐시 보존 또는 evict(메모리 해제)를 결정합니다.
/* fs/inode.c — iput() 전체 구현 */
void iput(struct inode *inode)
{
if (!inode)
return;
/* i_count를 감소시키면서 동시에 i_lock을 원자적으로 획득 */
if (atomic_dec_and_lock(&inode->i_count, &inode->i_lock))
iput_final(inode); /* i_count == 0: 최종 처리 */
}
static void iput_final(struct inode *inode)
{
struct super_block *sb = inode->i_sb;
const struct super_operations *op = sb->s_op;
unsigned long state;
int drop;
/* ① s_op->drop_inode()로 즉시 evict 여부 결정 */
if (op->drop_inode)
drop = op->drop_inode(inode);
else
drop = generic_drop_inode(inode);
if (!drop &&
!(inode->i_state & I_DONTCACHE) &&
(sb->s_flags & SB_ACTIVE)) {
/* ② nlink > 0이고 FS 활성 상태: LRU에 보존 */
__inode_add_lru(inode, true);
spin_unlock(&inode->i_lock);
return; /* 캐시에 유지, evict 안 함 */
}
state = inode->i_state;
if (!drop) {
/* I_DONTCACHE: 캐시 보존하지 않고 즉시 evict */
WRITE_ONCE(inode->i_state, state | I_WILL_FREE);
spin_unlock(&inode->i_lock);
/* ③ dirty inode를 디스크에 기록 후 evict */
write_inode_now(inode, 1); /* sync writeback */
spin_lock(&inode->i_lock);
WRITE_ONCE(inode->i_state, (state & ~I_WILL_FREE) | I_FREEING);
} else {
/* ④ nlink == 0 또는 drop_inode가 1 반환: 즉시 evict */
WRITE_ONCE(inode->i_state, state | I_FREEING);
}
inode_lru_list_del(inode); /* LRU에서 제거 (있었다면) */
spin_unlock(&inode->i_lock);
/* ⑤ 실제 evict 수행 → FS별 evict_inode → destroy_inode */
evict(inode);
}
/* generic_drop_inode — 기본 drop 정책 */
int generic_drop_inode(struct inode *inode)
{
/* nlink == 0이면 삭제 → drop=1 → 즉시 evict */
/* 또는 unhashed이면(remove_inode_hash 호출됨) drop=1 */
return !inode->i_nlink || inode_unhashed(inode);
}
코드 설명
- iput — atomic_dec_and_lock
i_count를 원자적으로 감소시키면서, 결과가 0이면i_lock을 잠금 상태로 반환합니다. 0이 아니면 아무 잠금 없이 false를 반환합니다. 이 단일 원자 연산으로 "감소 후 검사" 사이의 경쟁 조건을 제거합니다. - iput_final — drop_inode()
s_op->drop_inode가 NULL이면generic_drop_inode()를 호출하여i_nlink == 0이면 1(drop), 아니면 0(보존)을 반환합니다. NFS 등은 이를 재정의하여 서버 측 상태를 추가로 확인합니다. - iput_final — __inode_add_lru
drop=0이고 파일시스템이 활성 상태(SB_ACTIVE)이면 inode를 LRU 리스트에 추가합니다. 나중에 같은 파일을 다시 접근하면 디스크 I/O 없이 LRU에서 재활용됩니다. 메모리 압력 시 shrinker가 LRU 끝에서부터 회수합니다. - iput_final — I_WILL_FREE + write_inode_now
I_DONTCACHE플래그가 설정되어 캐시하지 않지만i_nlink > 0인 경우입니다. dirty 데이터를 먼저 디스크에 기록한 후 evict합니다.I_WILL_FREE상태는 다른 스레드에게 "이 inode는 곧 해제된다"를 알려 새로운 참조 획득을 방지합니다. - iput_final — I_FREEING (drop=1)
i_nlink == 0인 삭제된 파일이므로 디스크 writeback 없이 바로 evict로 진행합니다.evict()내부에서s_op->evict_inode가 디스크의 inode와 데이터 블록을 해제합니다. - iput_final — evict()최종 단계로, 해시 테이블 제거, FS별 evict_inode 호출, 페이지 캐시 해제, 메모리 해제를 수행합니다. 상세 흐름은 inode evict 경로 섹션을 참조하세요.
iput()는 evict 경로에서 s_op->evict_inode()를 호출할 수 있으며, 이는 디스크 I/O를 수반합니다. 따라서 spinlock을 보유한 상태에서 iput()를 호출하면 안 됩니다. 또한 iget()으로 참조를 획득하지 않은 inode에 iput()를 호출하면 use-after-free가 발생합니다.
inode Truncation 경로
파일 크기를 줄이는 truncate()는 inode의 i_size를 변경하고, 잘려나간 범위의 데이터 블록과 페이지 캐시를 해제하는 복잡한 과정입니다. hole punch(fallocate PUNCH_HOLE)도 유사한 경로를 사용합니다.
/* mm/truncate.c — 페이지 캐시 truncation */
void truncate_setsize(struct inode *inode, loff_t newsize)
{
loff_t oldsize = inode->i_size;
/* i_size를 원자적으로 갱신 */
i_size_write(inode, newsize);
if (newsize > oldsize)
pagecache_isize_extended(inode, oldsize, newsize);
else
truncate_pagecache(inode, newsize);
}
void truncate_pagecache(struct inode *inode, loff_t newsize)
{
struct address_space *mapping = inode->i_mapping;
loff_t holebegin = round_up(newsize, PAGE_SIZE);
/* 1. mmap된 PTE 무효화 → SIGBUS 방지 */
if (mapping_mapped(mapping))
unmap_mapping_range(mapping, holebegin, 0, 1);
/* 2. 페이지 캐시에서 해당 범위 제거 */
truncate_inode_pages(mapping, newsize);
/* 3. 재확인 (race 방지) */
unmap_mapping_range(mapping, holebegin, 0, 1);
}
/* ext4 truncate — fs/ext4/inode.c */
void ext4_truncate(struct inode *inode)
{
/* 1. 저널 트랜잭션 시작 */
handle = ext4_journal_start(inode, EXT4_HT_TRUNCATE, needed);
/* 2. extent tree에서 잘린 범위의 extent 제거 */
ext4_ext_truncate(handle, inode);
/* → 블록 비트맵 해제, 그룹 디스크립터 갱신 */
/* → 저널에 메타데이터 변경 기록 */
/* 3. inode dirty 마킹 */
ext4_mark_inode_dirty(handle, inode);
/* 4. 저널 트랜잭션 종료 */
ext4_journal_stop(handle);
}
| Truncation 변형 | 시스템콜 | 효과 | 블록 해제 |
|---|---|---|---|
| truncate | truncate(2), ftruncate(2) | 파일 끝에서부터 자르기 | 예 |
| hole punch | fallocate(PUNCH_HOLE) | 파일 중간에 구멍 뚫기 | 예 |
| collapse range | fallocate(COLLAPSE_RANGE) | 범위 제거 + 뒤쪽 당기기 | 예 |
| zero range | fallocate(ZERO_RANGE) | 범위를 0으로 채우기 | 옵션 |
| insert range | fallocate(INSERT_RANGE) | 빈 공간 삽입 + 뒤쪽 밀기 | 아니오 |
부분 페이지 처리: truncation 지점이 페이지 경계에 걸리면, 해당 페이지의 잘린 부분만 0으로 채웁니다(folio_zero_range()). 페이지 전체를 버리지 않습니다 — 앞부분의 유효한 데이터는 보존합니다. 이 세밀한 처리가 truncate의 복잡성을 높이는 주요 원인입니다.
Orphan Inode 리스트
Orphan inode는 nlink가 0이지만 아직 사용 중인(열려 있는) inode입니다. unlink 후 열린 fd가 남아있거나, O_TMPFILE로 생성된 파일이 이에 해당합니다. 저널링 파일시스템은 orphan 리스트를 유지하여 크래시 후에도 이런 inode의 공간을 올바르게 회수합니다.
/* fs/ext4/namei.c — orphan 리스트 추가 */
int ext4_orphan_add(handle_t *handle, struct inode *inode)
{
struct super_block *sb = inode->i_sb;
struct ext4_sb_info *sbi = EXT4_SB(sb);
/* 1. 메모리: 슈퍼블록의 orphan 리스트에 추가 */
list_add(&EXT4_I(inode)->i_orphan, &sbi->s_orphan);
/* 2. 디스크: 슈퍼블록의 s_last_orphan 체인에 연결 */
/* inode의 i_dtime 필드를 다음 orphan의 ino로 사용 */
/* 슈퍼블록 → ino_A → ino_B → ino_C → 0 (종료) */
NEXT_ORPHAN(inode) = le32_to_cpu(sbi->s_es->s_last_orphan);
sbi->s_es->s_last_orphan = cpu_to_le32(inode->i_ino);
/* 3. 저널에 기록 → 크래시 시에도 복구 가능 */
ext4_handle_dirty_metadata(handle, NULL, sbi->s_sbh);
ext4_mark_iloc_dirty(handle, inode, &iloc);
return 0;
}
/* fs/ext4/super.c — 마운트 시 orphan 정리 */
static void ext4_orphan_cleanup(struct super_block *sb,
struct ext4_super_block *es)
{
unsigned int s_flags = sb->s_flags;
int nr_orphans = 0;
/* s_last_orphan 체인을 순회하며 정리 */
while (es->s_last_orphan) {
struct inode *inode;
ino = le32_to_cpu(es->s_last_orphan);
inode = ext4_iget(sb, ino, ...);
if (inode->i_nlink == 0) {
/* nlink=0: 실제 삭제 수행 */
iput(inode); /* → evict_inode → ext4_free_inode */
} else {
/* nlink>0: truncate 미완 → 자르기 완료 */
ext4_truncate(inode);
iput(inode);
}
nr_orphans++;
}
/* "EXT4-fs: N orphan inodes cleaned up" 커널 로그 */
}
| 파일시스템 | Orphan 구현 | 저장 위치 |
|---|---|---|
| ext4 | i_dtime 필드로 연결 리스트(Linked List) 구성 | 슈퍼블록(Superblock) + 각 inode의 i_dtime |
| ext4 (5.15+) | orphan file (COMPAT_ORPHAN_FILE) | 전용 inode에 orphan 비트맵(Bitmap) 저장 — 동시성 향상 |
| XFS | AGI unlinked 해시 체인 | 각 AG 헤더의 unlinked 버킷 |
| Btrfs | orphan item (B-tree) | FS tree에 ORPHAN_ITEM 키 |
Orphan 디버깅(Debugging): dmesg | grep orphan으로 마운트 시 정리된 orphan 수를 확인할 수 있습니다. 비정상 종료 후 "EXT4-fs: 3 orphan inodes deleted" 같은 메시지가 나타납니다. debugfs -R "lsdel"로 최근 삭제된 inode를 나열할 수 있으며, orphan 파일 기능은 tune2fs -O orphan_file로 활성화합니다 (커널 5.15+).
inode 잠금 체계
inode 관련 동시성 제어는 여러 레벨의 잠금으로 구성됩니다. 잘못된 잠금 순서는 데드락을 유발하므로, 커널은 엄격한 잠금 순서(lock ordering)를 정의합니다.
| 잠금 | 타입 | 보호 대상 | 일반적 잠금 순서 |
|---|---|---|---|
i_rwsem | rw_semaphore | 파일 데이터 (read/write/truncate) | 1 (가장 바깥) |
i_mutex (구) | mutex (제거됨) | i_rwsem으로 대체 | - |
i_lock | spinlock | i_state, i_count 등 내부 필드 | 2 |
i_mmap_rwsem | rw_semaphore | address_space의 i_mmap 트리 | 별도 경로 |
mapping->invalidate_lock | rw_semaphore | folio 무효화 보호 | i_rwsem 안에서 |
i_pages lock | XArray lock | 페이지 캐시 XArray 조작 | 최내부 |
/* 잠금 순서 예시: 버퍼드 write */
/* 1. i_rwsem (exclusive) 획득 */
inode_lock(inode);
/* 2. 페이지 획득 및 write_begin */
a_ops->write_begin(file, mapping, pos, len, &folio);
/* 3. folio lock (xa_lock 아래) */
folio_lock(folio);
/* 4. 블록 매핑 */
ext4_map_blocks(handle, inode, &map, ...);
folio_unlock(folio); /* write_begin 내부에서 */
/* 사용자 데이터 복사 */
copy_page_from_iter_atomic(page, offset, bytes, from);
a_ops->write_end(file, mapping, pos, len, copied, folio);
inode_unlock(inode);
/* 다중 inode 잠금 (rename 시) */
/* lock_rename()은 두 디렉터리를 inode 포인터 순서로 잠금 */
struct dentry *lock_rename(struct dentry *p1, struct dentry *p2)
{
if (p1 == p2) {
inode_lock_nested(p1->d_inode, I_MUTEX_PARENT);
return NULL;
}
/* 포인터 주소 순서로 잠금 → 데드락 방지 */
if (p1->d_inode < p2->d_inode) {
inode_lock_nested(p1->d_inode, I_MUTEX_PARENT);
inode_lock_nested(p2->d_inode, I_MUTEX_PARENT2);
} else {
inode_lock_nested(p2->d_inode, I_MUTEX_PARENT);
inode_lock_nested(p1->d_inode, I_MUTEX_PARENT2);
}
return p1;
}
CONFIG_LOCKDEP 옵션은 런타임에 잠금 순서 위반을 탐지합니다. inode_lock_nested()의 두 번째 파라미터(잠금 클래스)는 lockdep이 같은 타입의 잠금을 구별하는 데 사용됩니다. 파일시스템 개발 시 반드시 lockdep을 활성화하여 테스트해야 합니다.
inode 유틸리티 함수/매크로 종합
VFS는 inode 필드를 안전하게 조작하기 위한 유틸리티 함수와 매크로를 제공합니다. 필드를 직접 수정하면 원자성(Atomicity) 위반, dirty 마킹 누락, NFS 불일치 등 미묘한 버그가 발생하므로 반드시 이 API를 사용해야 합니다.
i_size_read() / i_size_write()
i_size는 64비트 필드이므로 32비트 아키텍처에서 원자적(Atomic) 접근이 불가능합니다. VFS는 seqcount 기반의 접근자를 제공합니다.
/* include/linux/fs.h — i_size 접근자 */
/* 64비트 아키텍처: 직접 읽기/쓰기 (원자적) */
/* 32비트 아키텍처: seqcount_t로 tear-free 보장 */
static inline loff_t i_size_read(const struct inode *inode)
{
#if BITS_PER_LONG == 32 && defined(CONFIG_SMP)
loff_t i_size;
unsigned int seq;
do {
seq = read_seqcount_begin(&inode->i_size_seqcount);
i_size = inode->i_size;
} while (read_seqcount_retry(&inode->i_size_seqcount, seq));
return i_size;
#else
return inode->i_size; /* 64비트: 단일 load 명령으로 원자적 */
#endif
}
static inline void i_size_write(struct inode *inode, loff_t i_size)
{
#if BITS_PER_LONG == 32 && defined(CONFIG_SMP)
preempt_disable();
write_seqcount_begin(&inode->i_size_seqcount);
inode->i_size = i_size;
write_seqcount_end(&inode->i_size_seqcount);
preempt_enable();
#else
inode->i_size = i_size;
#endif
}
코드 설명
- i_size_read — seqcount32비트 SMP 시스템에서 64비트
loff_t는 두 번의 32비트 load로 읽힙니다. 그 사이에 writer가 값을 변경하면 상위/하위 32비트가 서로 다른 버전이 되는 "torn read"가 발생합니다.seqcount는 reader가 값을 읽는 동안 writer가 개입했는지 감지하여 재시도합니다. 락-프리(Lock-free) 읽기를 보장하므로 hot path에서 성능 저하가 없습니다. - i_size_write — preempt_disablewriter는
write_seqcount_begin/end사이에서 seqcount를 홀수로 만들어 reader에게 "쓰기 중"을 알립니다.preempt_disable()은 writer가 seqcount 업데이트 중간에 선점(Preemption)되어 reader가 무한 재시도하는 것을 방지합니다. - 64비트 아키텍처x86_64, ARM64 등에서는 64비트 load/store가 단일 명령으로 원자적이므로 seqcount가 불필요합니다. 컴파일 시
#if BITS_PER_LONG == 32로 분기됩니다.
inode->i_size에 대입하지 마세요. 32비트 시스템에서 torn read가 발생하면 read()가 잘못된 파일 크기를 보고 엉뚱한 범위의 데이터를 반환합니다. 반드시 i_size_read()/i_size_write()를 사용하세요.
타임스탬프 접근자 함수
커널 6.6부터 inode 타임스탬프 필드(__i_atime, __i_mtime, __i_ctime)는 직접 접근이 금지되고 접근자(Accessor) 함수를 통해서만 조작합니다. multi-grain 타임스탬프(Multi-grain Timestamp) 지원을 위한 리팩토링의 결과입니다.
| 함수 | 정의 위치 | 역할 |
|---|---|---|
inode_get_atime(inode) | include/linux/fs.h | __i_atime 반환 (struct timespec64) |
inode_get_mtime(inode) | include/linux/fs.h | __i_mtime 반환 |
inode_get_ctime(inode) | include/linux/fs.h | __i_ctime 반환 |
inode_set_atime_to_ts(inode, ts) | include/linux/fs.h | __i_atime = ts 설정 후 ts 반환 |
inode_set_mtime_to_ts(inode, ts) | include/linux/fs.h | __i_mtime = ts 설정 후 ts 반환 |
inode_set_ctime_to_ts(inode, ts) | include/linux/fs.h | __i_ctime = ts 설정 후 ts 반환 |
inode_set_ctime_current(inode) | fs/inode.c | 현재 시간을 ctime에 설정 + coarse-grain 최적화 |
simple_inode_init_ts(inode) | fs/libfs.c | atime/mtime/ctime 모두 현재 시간으로 초기화 |
inode_set_atime(inode, sec, nsec) | include/linux/fs.h | 초/나노초 직접 지정 |
inode_set_mtime(inode, sec, nsec) | include/linux/fs.h | 초/나노초 직접 지정 |
/* include/linux/fs.h — 타임스탬프 접근자 구현 */
static inline struct timespec64 inode_get_atime(const struct inode *inode)
{
return inode->__i_atime;
}
static inline struct timespec64
inode_set_atime_to_ts(struct inode *inode, struct timespec64 ts)
{
inode->__i_atime = ts;
return ts;
}
static inline struct timespec64
inode_set_atime(struct inode *inode, time64_t sec, long nsec)
{
struct timespec64 ts = { .tv_sec = sec, .tv_nsec = nsec };
return inode_set_atime_to_ts(inode, ts);
}
/* fs/inode.c — 현재 시간으로 ctime 설정 */
struct timespec64 inode_set_ctime_current(struct inode *inode)
{
struct timespec64 now = current_time(inode);
/* multi-grain: coarse 시간과 비교하여 fine-grain 필요 여부 판단 */
inode_set_ctime_to_ts(inode, now);
return now;
}
/* fs/libfs.c — 새 inode 타임스탬프 일괄 초기화 */
struct timespec64 simple_inode_init_ts(struct inode *inode)
{
struct timespec64 ts = inode_set_ctime_current(inode);
inode_set_atime_to_ts(inode, ts);
inode_set_mtime_to_ts(inode, ts);
return ts;
}
/* 파일시스템에서의 전형적인 사용 */
static int myfs_create(struct mnt_idmap *idmap,
struct inode *dir,
struct dentry *dentry, umode_t mode, bool excl)
{
struct inode *inode = new_inode(dir->i_sb);
simple_inode_init_ts(inode); /* atime=mtime=ctime=now */
inode_set_mtime_to_ts(dir,
inode_set_ctime_current(dir)); /* 부모 dir 시간 갱신 */
/* ... */
}
Multi-grain 타임스탬프: 커널 6.6+에서 inode_set_ctime_current()는 coarse-grained 시간(jiffies 기반, ~1ms 정밀도)으로 충분한 경우 fine-grained 시간(ktime_get_real_ts64(), 나노초)을 피하여 성능을 향상합니다. stat() 호출이 이전 ctime과 coarse 시간을 비교하여 fine-grain이 필요한지 결정합니다.
nlink 조작 함수
하드 링크 수(i_nlink)를 직접 수정하면 dirty 마킹이 누락되어 디스크 불일치가 발생합니다. 반드시 아래 API를 사용해야 합니다.
/* fs/inode.c — nlink 조작 함수 */
/* 절대값 설정 (디스크에서 읽을 때) */
void set_nlink(struct inode *inode, unsigned int nlink)
{
if (!nlink) {
clear_nlink(inode); /* 0이면 별도 처리 */
} else {
/* 먼저 I_LINKABLE 비트를 지움 (nlink>0이면 불필요) */
if (inode->i_nlink == 0)
inode->i_state &= ~I_LINKABLE;
inode->i_nlink = nlink;
}
}
/* nlink 감소 (unlink, rmdir 시) */
void drop_nlink(struct inode *inode)
{
WARN_ON(inode->i_nlink == 0); /* 이미 0이면 BUG */
inode->i_nlink--;
}
/* nlink 0으로 설정 (orphan 처리) */
void clear_nlink(struct inode *inode)
{
if (inode->i_nlink) {
inode->i_nlink = 0;
atomic_long_inc(&inode->i_sb->s_remove_count);
}
}
/* nlink 증가 (link 시) */
void inc_nlink(struct inode *inode)
{
if (unlikely(inode->i_nlink == 0))
WARN_ON(!(inode->i_state & I_LINKABLE));
inode->i_nlink++;
}
/* 고수준 래퍼: nlink 변경 + dirty 마킹 */
static inline void inode_inc_link_count(struct inode *inode)
{
inc_nlink(inode);
mark_inode_dirty(inode); /* nlink 변경을 디스크에 반영 */
}
static inline void inode_dec_link_count(struct inode *inode)
{
drop_nlink(inode);
mark_inode_dirty(inode);
}
코드 설명
- set_nlink디스크에서 inode를 읽을 때(
myfs_iget) 호출합니다. 이 시점에서는 inode가 아직 캐시에 삽입되기 전이므로mark_inode_dirty()가 불필요합니다.nlink=0이면clear_nlink()을 통해s_remove_count를 증가시킵니다. - drop_nlink — WARN_ON이미 0인 nlink을 감소시키면 underflow(언더플로)가 발생합니다.
WARN_ON은 이를 감지하여 커널 로그에 스택 트레이스(Stack Trace)를 출력합니다. 정상적인 unlink 경로에서는 절대 발생하지 않아야 합니다. - clear_nlink — s_remove_count
s_remove_count는 이 superblock에서 삭제 예정인 inode 수를 추적합니다. NFS 서버가 "파일시스템에 삭제 대기 중인 파일이 있는지" 판단하는 데 사용합니다. - inc_nlink — I_LINKABLE
i_nlink=0인 inode의 nlink을 다시 증가시키는 것은 일반적으로 허용되지 않습니다. 예외적으로O_TMPFILE파일을linkat()으로 영구화할 때I_LINKABLE상태를 설정하여 이를 허용합니다. - inode_inc_link_count / inode_dec_link_count이 고수준 래퍼는 nlink 변경과 dirty 마킹을 원자적으로 수행합니다. 대부분의 파일시스템은 이를 사용하지만, ext4처럼 저널 트랜잭션 내에서 별도 dirty 마킹이 필요한 경우 저수준
inc_nlink()/drop_nlink()을 직접 사용합니다.
inode 상태/플래그 헬퍼 매크로
inode->i_flags의 S_* 상수를 검사하는 IS_*() 매크로입니다. 마운트 옵션, 파일시스템 기능, 파일별 속성을 반영합니다.
/* include/linux/fs.h — inode S_* 플래그와 IS_*() 매크로 */
#define S_SYNC (1 << 0) /* 동기 쓰기 (mount -o sync) */
#define S_NOATIME (1 << 1) /* atime 갱신 안 함 */
#define S_APPEND (1 << 2) /* 추가 전용 (chattr +a) */
#define S_IMMUTABLE (1 << 3) /* 변경 불가 (chattr +i) */
#define S_DEAD (1 << 4) /* 삭제된 디렉터리 */
#define S_NOQUOTA (1 << 5) /* 쿼타 면제 */
#define S_DIRSYNC (1 << 6) /* 디렉터리 동기 쓰기 */
#define S_NOCMTIME (1 << 7) /* VFS가 ctime/mtime 자동 갱신 안 함 */
#define S_SWAPFILE (1 << 8) /* 스왑 파일 (이름 변경/삭제 불가) */
#define S_PRIVATE (1 << 9) /* LSM 검사 제외 (내부 inode) */
#define S_IMA (1 << 10) /* IMA 무결성 검증 대상 */
#define S_AUTOMOUNT (1 << 11) /* 자동 마운트 트리거 */
#define S_NOSEC (1 << 12) /* 보안 xattr 없음 (캐시 최적화) */
#define S_DAX (1 << 13) /* DAX 모드 (페이지 캐시 우회) */
#define S_ENCRYPTED (1 << 14) /* fscrypt 암호화 */
#define S_CASEFOLD (1 << 15) /* 대소문자 무시 디렉터리 */
#define S_VERITY (1 << 16) /* fs-verity 활성화 */
/* 검사 매크로 */
#define IS_IMMUTABLE(inode) ((inode)->i_flags & S_IMMUTABLE)
#define IS_APPEND(inode) ((inode)->i_flags & S_APPEND)
#define IS_SYNC(inode) ((inode)->i_flags & S_SYNC)
#define IS_DIRSYNC(inode) ((inode)->i_flags & S_DIRSYNC)
#define IS_DEADDIR(inode) ((inode)->i_flags & S_DEAD)
#define IS_POSIXACL(inode) ((inode)->i_sb->s_flags & SB_POSIXACL)
#define IS_PRIVATE(inode) ((inode)->i_flags & S_PRIVATE)
#define IS_AUTOMOUNT(inode) ((inode)->i_flags & S_AUTOMOUNT)
#define IS_NOSEC(inode) ((inode)->i_flags & S_NOSEC)
#define IS_SWAPFILE(inode) ((inode)->i_flags & S_SWAPFILE)
#define IS_DAX(inode) ((inode)->i_flags & S_DAX)
#define IS_ENCRYPTED(inode) ((inode)->i_flags & S_ENCRYPTED)
#define IS_VERITY(inode) ((inode)->i_flags & S_VERITY)
| 매크로 | 설정 방법 | VFS 동작 영향 |
|---|---|---|
IS_IMMUTABLE | chattr +i | write, truncate, unlink, chmod, chown 모두 -EPERM |
IS_APPEND | chattr +a | 추가(append) 쓰기만 허용, 삭제/truncate 금지 |
IS_SYNC | mount -o sync 또는 chattr +S | 모든 쓰기가 동기적으로 디스크에 기록 |
IS_DEADDIR | 커널 내부 (rmdir 후) | 삭제된 디렉터리에 새 파일 생성 방지 |
IS_PRIVATE | 파일시스템 내부 | LSM 보안 검사 우회 (저널, 쿼타 파일 등) |
IS_NOSEC | VFS 자동 설정 | 보안 xattr 미존재 시 file_remove_privs() 스킵(Skip) |
IS_DAX | mount -o dax 또는 per-file | 페이지 캐시 우회, 직접 메모리 매핑 |
IS_ENCRYPTED | fscrypt 정책 설정 시 | 읽기/쓰기 시 자동 암호화/복호화 |
make_bad_inode() / is_bad_inode()
I/O 에러나 파일시스템 손상(Corruption)으로 inode를 정상적으로 읽을 수 없을 때, "bad inode"로 표시하여 모든 연산이 -EIO를 반환하도록 합니다.
/* fs/bad_inode.c — bad inode 구현 */
void make_bad_inode(struct inode *inode)
{
remove_inode_hash(inode); /* 해시 테이블에서 제거 */
inode->i_mode = S_IFREG; /* 유형을 일반 파일로 변경 */
inode_set_atime(inode, 0, 0);
inode_set_mtime(inode, 0, 0);
inode_set_ctime_to_ts(inode, (struct timespec64){0});
inode->i_op = &bad_inode_ops; /* 모든 inode op → -EIO */
inode->i_fop = &bad_file_ops; /* 모든 file op → -EIO */
}
bool is_bad_inode(struct inode *inode)
{
return (inode->i_op == &bad_inode_ops);
}
/* 파일시스템에서의 사용 예 */
struct inode *myfs_iget(struct super_block *sb, unsigned long ino)
{
struct inode *inode = iget_locked(sb, ino);
if (!(inode->i_state & I_NEW))
return inode;
if (myfs_read_disk_inode(sb, ino, inode)) {
make_bad_inode(inode); /* 읽기 실패 → bad 표시 */
unlock_new_inode(inode);
iput(inode);
return ERR_PTR(-EIO);
}
unlock_new_inode(inode);
return inode;
}
file_inode() 매크로
/* include/linux/fs.h */
static inline struct inode *file_inode(const struct file *f)
{
return f->f_inode;
}
/* f_inode은 file 구조체에 직접 캐싱된 inode 포인터.
* f->f_path.dentry->d_inode 경로보다 역참조(Dereference)가 1단계 적어
* 성능상 유리하고, RCU 워크에서도 안전합니다.
* VFS가 open() 시 f_inode = d_inode(dentry)로 설정합니다. */
/* 사용 예: file_operations 콜백 내부 */
static ssize_t myfs_read(struct file *file, ...)
{
struct inode *inode = file_inode(file);
struct myfs_inode_info *mi = MYFS_I(inode);
/* ... */
}
inode_owner_or_capable() / inode_newsize_ok()
/* fs/inode.c — 소유자 또는 capability 검사 */
bool inode_owner_or_capable(struct mnt_idmap *idmap,
const struct inode *inode)
{
struct user_namespace *ns;
vfsuid_t vfsuid = i_uid_into_vfsuid(idmap, inode);
/* 1. 현재 프로세스가 소유자인가? */
if (vfsuid_eq_kuid(vfsuid, current_fsuid()))
return true;
/* 2. CAP_FOWNER capability가 있는가? */
ns = current_user_ns();
if (ns_capable(ns, CAP_FOWNER) &&
kuid_has_mapping(ns, vfsuid_into_kuid(vfsuid)))
return true;
return false;
}
/* fs/attr.c — 새 파일 크기 유효성 검사 */
int inode_newsize_ok(const struct inode *inode, loff_t offset)
{
/* 1. 파일 크기 제한 (rlimit) */
if (offset > rlimit(RLIMIT_FSIZE))
return -EFBIG;
/* 2. 파일시스템 최대 파일 크기 */
if (offset > inode->i_sb->s_maxbytes)
return -EFBIG;
return 0;
}
코드 설명
- inode_owner_or_capable — vfsuididmapped mount 환경에서 inode의 uid를 마운트 매핑에 따라 변환한 뒤 현재 프로세스의 fsuid와 비교합니다.
chmod,utimes등 소유자만 허용하는 연산에서 사용됩니다. - inode_owner_or_capable — CAP_FOWNER
CAP_FOWNER는 파일 소유자와 무관하게 소유자 권한이 필요한 연산을 수행할 수 있는 capability입니다. 단, 대상 uid가 현재 사용자 네임스페이스에 매핑되어 있어야 합니다. - inode_newsize_ok — RLIMIT_FSIZE프로세스의 파일 크기 제한(
ulimit -f)을 초과하면-EFBIG를 반환하고SIGXFSZ시그널(Signal)을 전송합니다.s_maxbytes는 파일시스템의 하드웨어적 최대 크기입니다 (ext4: 16TB, XFS: 8EB).
DIO (Direct I/O) 동기화 헬퍼
Direct I/O와 버퍼드 I/O 간의 경합을 방지하기 위한 카운터 기반 동기화 API입니다.
/* include/linux/fs.h — DIO 동기화 */
static inline void inode_dio_begin(struct inode *inode)
{
atomic_inc(&inode->i_dio_count);
}
static inline void inode_dio_end(struct inode *inode)
{
if (atomic_dec_and_test(&inode->i_dio_count))
wake_up_bit(&inode->i_state, __I_DIO_WAKEUP);
}
/* fs/inode.c — 모든 진행 중 DIO 완료 대기 */
void inode_dio_wait(struct inode *inode)
{
if (atomic_read(&inode->i_dio_count))
__inode_dio_wait(inode);
}
/* 전형적 사용: truncate 전에 진행 중 DIO 완료 대기 */
static int myfs_setattr(struct mnt_idmap *idmap,
struct dentry *dentry, struct iattr *attr)
{
struct inode *inode = d_inode(dentry);
if (attr->ia_valid & ATTR_SIZE) {
inode_dio_wait(inode); /* DIO 완료까지 대기 */
truncate_setsize(inode, attr->ia_size);
}
/* ... */
}
VFS 잠금 래퍼
inode->i_rwsem을 직접 조작하는 대신 VFS 래퍼를 사용하면 lockdep 클래스가 올바르게 적용됩니다.
| 래퍼 함수 | 내부 동작 | 사용 시점 |
|---|---|---|
inode_lock(inode) | down_write(&inode->i_rwsem) | write, truncate, create, mkdir 등 |
inode_unlock(inode) | up_write(&inode->i_rwsem) | 배타적 잠금 해제 |
inode_lock_shared(inode) | down_read(&inode->i_rwsem) | read, readdir, getattr 등 |
inode_unlock_shared(inode) | up_read(&inode->i_rwsem) | 공유 잠금 해제 |
inode_trylock(inode) | down_write_trylock(&inode->i_rwsem) | 논블로킹(Non-blocking) 배타적 잠금 시도 |
inode_trylock_shared(inode) | down_read_trylock(&inode->i_rwsem) | 논블로킹 공유 잠금 시도 |
inode_is_locked(inode) | rwsem_is_locked(&inode->i_rwsem) | 디버그 assertion |
inode_lock_nested(inode, cls) | down_write_nested(&inode->i_rwsem, cls) | 다중 inode 잠금 시 lockdep 클래스 지정 |
/* include/linux/fs.h — 잠금 래퍼 */
static inline void inode_lock(struct inode *inode)
{
down_write(&inode->i_rwsem);
}
static inline void inode_unlock(struct inode *inode)
{
up_write(&inode->i_rwsem);
}
static inline void inode_lock_shared(struct inode *inode)
{
down_read(&inode->i_rwsem);
}
static inline int inode_trylock(struct inode *inode)
{
return down_write_trylock(&inode->i_rwsem);
}
static inline void inode_lock_nested(struct inode *inode,
unsigned subclass)
{
down_write_nested(&inode->i_rwsem, subclass);
}
/* Lockdep 잠금 클래스 상수 */
enum inode_i_mutex_lock_class {
I_MUTEX_NORMAL, /* 단일 inode 잠금 */
I_MUTEX_PARENT, /* 부모 디렉터리 (create, unlink) */
I_MUTEX_PARENT2, /* rename 시 두 번째 부모 */
I_MUTEX_CHILD, /* 자식 inode */
I_MUTEX_XATTR, /* xattr 연산 중 */
I_MUTEX_NONDIR2, /* rename 시 비디렉터리 타겟 */
};
기타 유틸리티
| 함수/매크로 | 정의 위치 | 역할 |
|---|---|---|
inode_needs_sync(inode) | include/linux/fs.h | IS_SYNC(inode) 또는 IS_DIRSYNC(inode) 검사 — 동기 쓰기 필요 여부 |
mapping_set_error(mapping, err) | include/linux/pagemap.h | address_space에 I/O 에러 기록 — filemap_check_errors()에서 검출 |
ihold(inode) | include/linux/fs.h | atomic_inc(&inode->i_count) — 참조 카운트 증가 (caller가 이미 참조를 보유한 상태에서) |
inode_sb_list_add(inode) | fs/inode.c | inode를 superblock의 s_inodes 리스트에 추가 |
inode_init_once(inode) | fs/inode.c | slab constructor — rwsem, i_data, 대기 큐 등 1회성 초기화 |
d_inode(dentry) | include/linux/dcache.h | dentry->d_inode 반환 — negative dentry 시 NULL |
d_inode_rcu(dentry) | include/linux/dcache.h | RCU 보호 하 dentry->d_inode 읽기 |
d_is_positive(dentry) | include/linux/dcache.h | dentry에 inode가 연결되어 있는지 검사 |
d_is_negative(dentry) | include/linux/dcache.h | negative dentry (파일 없음 캐시) 여부 검사 |
inode 번호 할당 전략
파일시스템마다 inode 번호를 할당하는 전략이 다릅니다. 이 전략은 파일 생성 성능, inode 고갈 가능성, 32비트/64비트 호환성에 직접적인 영향을 미칩니다.
| 파일시스템 | 할당 방식 | 범위 | 고갈 가능성 | 특이사항 |
|---|---|---|---|---|
| ext4 | 비트맵 (고정 테이블) | mkfs 시 결정 | 있음 | Orlov 할당기로 디렉터리 분산 |
| XFS | AG 내 동적 할당 | 64비트 | 극히 낮음 | AG별 free inode B+tree |
| Btrfs | objectid (단조 증가) | 64비트 | 없음 | 서브볼륨별 독립 번호 공간 |
| tmpfs | get_next_ino() | 32비트 (percpu) | 번호 재활용(Recycling) | 디스크 없음 |
| NFS | 서버 전달 | 서버 의존 | 서버 의존 | filehandle이 실질적 식별자 |
/* ext4 inode 할당: Orlov 알고리즘 */
/* 디렉터리를 여러 블록 그룹에 분산하여 부모-자식 근접성 유지 */
static int find_group_orlov(struct super_block *sb,
struct inode *parent, ...)
{
/* 최상위 디렉터리: 가장 여유 있는 그룹 선택 */
if (parent == d_inode(sb->s_root)) {
/* free inode 수, free block 수, 디렉터리 수 기준 */
best_group = find_best_group(sb, ...);
}
/* 하위 디렉터리: 부모와 같은 그룹 선호 */
else {
group = ext4_inode_to_goal_block(parent);
/* 부모 근처에서 시작하여 빈 슬롯 탐색 */
}
}
/* XFS inode 할당: finobt (free inode B+tree) */
/* 각 AG에 free inode를 추적하는 B+tree 유지 */
/* → O(log n)에 빈 inode 슬롯 탐색 가능 */
/* tmpfs / procfs 등 pseudo-FS */
unsigned int get_next_ino(void)
{
unsigned int *p = &get_cpu_var(last_ino);
unsigned int res = *p;
/* percpu 카운터로 락-프리 할당 */
*p = ++res;
put_cpu_var(last_ino);
return res;
}
32비트 inode 번호 주의: stat(2)의 st_ino가 32비트인 시스템에서 XFS/Btrfs의 64비트 inode 번호가 잘릴 수 있습니다. 이 경우 -EOVERFLOW가 발생합니다. XFS에서는 inode32 마운트 옵션으로 inode를 32비트 범위 내에 할당하도록 제한할 수 있지만, 대규모 파일시스템에서는 inode 배치 효율이 떨어집니다. 최선의 해결책은 64비트 시스템과 statx(2)를 사용하는 것입니다.
inode Generation 번호 (i_generation)
i_generation 필드는 inode 번호가 재사용될 때 같은 번호의 서로 다른 파일을 구별하기 위한 세대 번호입니다. NFS 파일 핸들의 핵심 구성 요소이며, inode가 삭제되고 같은 번호로 새 파일이 생성되었을 때 오래된 핸들로 접근하는 것을 방지합니다.
/* include/linux/fs.h */
struct inode {
/* ... */
__u32 i_generation; /* 세대 번호 — inode 재사용 시 증가 */
/* ... */
};
/* NFS 파일 핸들 구조 (개념적) */
struct nfs_fh {
__u64 ino; /* inode 번호 */
__u32 generation; /* i_generation — "stale" 검출 핵심 */
__u32 fsid; /* 파일시스템 ID */
};
/* ext4 — inode 할당 시 generation 설정 */
/* fs/ext4/ialloc.c — __ext4_new_inode() */
inode->i_generation = get_random_u32();
/* 또는 이전 값 + 1 (커널 버전에 따라 다름) */
/* NFS 서버 — export 시 파일 핸들 구성 */
/* fs/exportfs/expfs.c */
static int export_encode_fh(struct inode *inode, ...)
{
fh[0] = inode->i_ino;
fh[1] = inode->i_generation; /* 핵심: 세대 번호 포함 */
/* ... */
}
/* NFS 클라이언트가 오래된 핸들로 접근 시 */
/* → 서버가 ino로 inode 검색 후 generation 비교 */
/* → 불일치 → -ESTALE 반환 */
| 파일시스템 | i_generation 관리 | 특성 |
|---|---|---|
| ext4 | 할당 시 랜덤 또는 이전+1 | 디스크 inode에 저장, NFS export 안전 |
| XFS | AG별 순차 증가 | di_gen 필드, 항상 NFS-safe |
| Btrfs | 트랜잭션 ID 기반 | subvolume + objectid + generation 조합 |
| tmpfs | 항상 0 | NFS export 미지원 (5.x에서 제한적 지원) |
| NFS 영향 | 클라이언트가 캐시한 파일 핸들의 generation이 서버와 불일치하면 ESTALE 오류 | |
ESTALE 오류와 대응: NFS에서 파일이 서버에서 삭제된 후 같은 inode 번호로 새 파일이 생성되면 클라이언트의 기존 핸들은 ESTALE을 반환합니다. 이것이 i_generation의 존재 이유입니다. generation 없이는 클라이언트가 완전히 다른 파일의 데이터를 읽을 수 있습니다. statx()의 STATX_CHANGE_COOKIE 필드도 유사한 목적으로 사용됩니다.
# generation 번호 확인 (ext4)
debugfs -R "stat <inode_number>" /dev/sda1
# ... Generation: 1234567890 ...
# NFS 파일 핸들 정보 확인
nfs4_getfacl /mnt/nfs/file
# filehandle에 generation이 포함됨
# ESTALE 오류 재현
# 서버: rm /export/file && touch /export/file
# 클라이언트: cat /mnt/nfs/file → Stale file handle
inode 보안: LSM 연동
Linux Security Module(LSM) 프레임워크는 inode 수준에서 세밀한 보안 정책을 적용합니다. inode에 보안 레이블(security label)을 부착하고, 모든 접근 시 MAC(Mandatory Access Control) 검사를 수행합니다.
/* inode 내 보안 관련 필드 */
struct inode {
/* ... */
void *i_security; /* LSM별 보안 데이터 (SELinux: inode_security_struct) */
/* ... */
};
/* LSM 훅 호출 순서 (inode 생성 시) */
/* 1. security_inode_alloc() — 보안 구조체 할당 */
/* 2. security_inode_init_security() — 보안 레이블 초기화 */
/* 3. security_inode_post_create() — 생성 후 처리 */
/* LSM 훅 호출 순서 (inode 접근 시) */
/* inode_permission()에서: */
/* 1. DAC 검사 (전통적 권한 + ACL) */
/* 2. security_inode_permission() ← LSM 훅 */
/* → SELinux: selinux_inode_permission() */
/* → AppArmor: apparmor_inode_permission() */
/* SELinux 보안 컨텍스트 확인 */
/* ls -Z /home/user/test.txt */
/* -rw-r--r--. user group unconfined_u:object_r:user_home_t:s0 test.txt */
| LSM | 보안 모델 | inode 레이블 저장 | 주요 배포판 |
|---|---|---|---|
| SELinux | Type Enforcement (TE) | security.selinux xattr | RHEL, Fedora, CentOS |
| AppArmor | 경로 기반 (프로파일) | xattr 미사용 (경로 기반) | Ubuntu, SUSE |
| Smack | Simplified MAC | security.SMACK64 xattr | Tizen, 임베디드 |
| IMA/EVM | 무결성(Integrity) 검증 | security.ima, security.evm | 다양 |
LSM 스택킹 (커널 6.x): 이전에는 하나의 major LSM만 활성화할 수 있었지만, 6.x부터 SELinux + AppArmor + Landlock을 동시에 사용할 수 있습니다. inode의 i_security 포인터는 LSM blob으로, 각 LSM의 보안 데이터를 하나의 할당에 연속 저장합니다 (lsm_inode_alloc()). /sys/kernel/security/lsm에서 활성 LSM 목록을 확인할 수 있습니다.
Idmapped Mounts와 inode
커널 5.12에서 도입된 idmapped mounts는 마운트 지점마다 UID/GID 매핑을 다르게 적용합니다. inode의 i_uid/i_gid를 변경하지 않고, VFS 계층에서 매핑을 적용하여 컨테이너 환경에서의 파일 소유권 문제를 해결합니다.
/* inode_operations 콜백의 mnt_idmap 파라미터 */
int (*create)(struct mnt_idmap *idmap,
struct inode *dir, struct dentry *dentry,
umode_t mode, bool excl);
/* UID 매핑 적용 예시 (VFS 내부) */
static inline vfsuid_t i_uid_into_vfsuid(
struct mnt_idmap *idmap,
const struct inode *inode)
{
/* inode의 실제 uid를 mnt_idmap으로 매핑 */
return make_vfsuid(idmap, i_user_ns(inode), inode->i_uid);
}
/* 파일 생성 시: vfsuid를 inode uid로 역매핑 */
void inode_init_owner(struct mnt_idmap *idmap,
struct inode *inode,
const struct inode *dir,
umode_t mode)
{
/* 프로세스의 fsuid를 idmap 역변환하여 inode에 저장 */
vfsuid_t vfsuid = mapped_fsuid(idmap, i_user_ns(dir));
inode->i_uid = vfsuid_into_kuid(vfsuid);
}
# idmapped mount 설정 (mount_setattr)
# util-linux 2.39+ 또는 mount-idmapped 도구 사용
# 예: 호스트 uid 1000을 컨테이너 uid 0으로 매핑
mount-idmapped --map-mount b:0:1000:1 \
/host/share /container/rootfs/share
# systemd-nspawn에서 자동 idmapped mount
systemd-nspawn --bind=/host/share:/share \
--private-users=pick \
--private-users-ownership=map
# 커널 지원 확인
# CONFIG_IDMAP_MOUNTS=y (5.12+)
| 비교 | chown | user namespace | idmapped mount |
|---|---|---|---|
| inode 변경 | i_uid/i_gid 직접 변경 | 변경 없음 | 변경 없음 |
| 공유 가능 | 한 소유자만 | 네임스페이스별 | 마운트별 독립 매핑 |
| 성능 | I/O 발생 | 오버헤드(Overhead) 없음 | VFS 계층 매핑만 (무시할 수준) |
| 용도 | 전통적 소유권 | 프로세스 격리(Isolation) | 컨테이너 파일 공유 |
컨테이너 실전: Docker/Podman에서 호스트 볼륨을 바인드 마운트(Bind Mount)할 때 UID 불일치 문제가 자주 발생합니다. idmapped mount는 이 문제의 근본적 해결책입니다. Podman 4.0+는 --userns=keep-id 옵션으로 idmapped mount를 자동 활용합니다. 기존의 chown -R이나 uid/gid 동기화 같은 임시 방편이 불필요해집니다.
특수 inode와 의사 파일시스템
리눅스 커널에는 디스크에 저장되지 않는 특수 inode들이 존재합니다. 파이프, 소켓, 익명 inode, procfs/sysfs의 가상 파일들은 모두 메모리 전용 inode로 관리됩니다.
의사 파일시스템의 inode
| 파일시스템 | inode 특성 | 할당 함수 | i_ino 부여 | 해시 삽입 |
|---|---|---|---|---|
pipefs | 파이프 양 끝점 | new_inode_pseudo() | get_next_ino() | 아니오 |
sockfs | 소켓 파일 | new_inode_pseudo() | get_next_ino() | 아니오 |
anon_inodefs | epoll, eventfd, timerfd | anon_inode_getfd() | 고정(단일 inode) | 아니오 |
procfs | 프로세스 정보 | proc_alloc_inode() | proc_inum 해시 | 예 |
sysfs | 커널 객체 속성 | sysfs_get_inode() | kernfs_node 기반 | 예 |
tmpfs | 메모리 임시 파일 | shmem_get_inode() | get_next_ino() | 예 |
devtmpfs | 디바이스 노드 | tmpfs 기반 | 동적 | 예 |
cgroup | 제어 그룹 파일 | kernfs 기반 | kernfs_node 기반 | 예 |
/* new_inode_pseudo() — 의사 FS용 inode 할당 */
/* 일반 new_inode()와의 차이: sb의 inode 리스트에 추가하지 않음 */
struct inode *new_inode_pseudo(struct super_block *sb)
{
struct inode *inode = alloc_inode(sb);
if (inode) {
spin_lock(&inode->i_lock);
inode->i_state = 0;
spin_unlock(&inode->i_lock);
/* sb->s_inodes 리스트에 추가하지 않음! */
/* → umount 시 s_inodes 순회에서 제외 */
}
return inode;
}
/* 익명 inode — 단일 공유 inode로 여러 fd 생성 */
/* epoll_create → anon_inode_getfd("[eventpoll]") */
/* timerfd_create → anon_inode_getfd("[timerfd]") */
/* eventfd → anon_inode_getfd("[eventfd]") */
/* userfaultfd → anon_inode_getfd("[userfaultfd]") */
int anon_inode_getfd(const char *name,
const struct file_operations *fops,
void *priv, int flags)
{
/* 전역 anon_inode_inode를 공유 사용 */
/* 새 struct file만 생성하여 fd에 할당 */
struct file *file;
int fd;
fd = get_unused_fd_flags(flags);
file = anon_inode_getfile(name, fops, priv, flags);
fd_install(fd, file);
return fd;
}
/* /proc/PID/fd에서 확인 */
/* $ ls -la /proc/self/fd/3 */
/* lrwx------ 1 user user 64 ... 3 -> anon_inode:[eventpoll] */
파이프와 소켓의 inode
/* 파이프 inode — fs/pipe.c */
struct inode {
/* ... */
union {
struct pipe_inode_info *i_pipe; /* 파이프일 때 */
struct cdev *i_cdev; /* 캐릭터 디바이스일 때 */
char *i_link; /* 심볼릭 링크 타겟 */
unsigned i_dir_seq; /* 디렉터리일 때 */
};
};
/* pipe(2) → create_pipe_files() */
/* 1. pipefs에서 new_inode_pseudo() */
/* 2. i_pipe = alloc_pipe_info() */
/* 3. inode->i_fop = &pipefifo_fops */
/* 4. 두 개의 struct file 생성 (읽기/쓰기) */
/* 소켓 inode — net/socket.c */
struct socket_alloc {
struct socket socket; /* 소켓 구조체 */
struct inode vfs_inode; /* VFS inode (임베드) */
};
/* socket(2) → sock_alloc() */
/* sockfs의 new_inode_pseudo()로 할당 */
/* container_of로 inode ↔ socket 상호 변환 */
static inline struct socket *SOCKET_I(struct inode *inode)
{
return &container_of(inode, struct socket_alloc,
vfs_inode)->socket;
}
anon_inode의 특이성: anon_inodefs는 시스템 전체에서 단 하나의 inode만 사용합니다. epoll, eventfd, timerfd 등은 모두 이 단일 inode를 공유하며, 개별 상태는 struct file의 private_data에 저장됩니다. 따라서 stat()으로 조회하면 모든 익명 fd가 같은 inode 번호를 가집니다. 이 설계는 inode 객체를 최소화하면서도 VFS 인터페이스를 활용할 수 있게 합니다.
예약된 inode 번호
대부분의 파일시스템은 특별한 용도의 예약 inode 번호를 가지고 있습니다.
| 파일시스템 | inode 번호 | 용도 |
|---|---|---|
| ext4 | 0 | 존재하지 않는 inode (NULL) |
| 1 | 불량 블록 목록 (bad blocks) | |
| 2 | 루트 디렉터리 (/) | |
| 3 | ACL 인덱스 (구) | |
| 4 | ACL 데이터 (구) | |
| ext4 (계속) | 5 | 부트 로더(Loader) |
| 6 | 미삭제 디렉터리 (undelete) | |
| ext4 | 7 | 그룹 디스크립터 예약 |
| ext4 | 8 | 저널 (EXT4_JOURNAL_INO) |
| ext4 | 11 | 첫 번째 비예약 inode (기본) |
| XFS | 동적 | 루트 디렉터리는 AG 0의 첫 inode |
| Btrfs | 256 | 첫 일반 파일 objectid |
| 모든 FS | 0 | 일반적으로 유효하지 않은 inode |
# 루트 디렉터리의 inode 번호 확인
stat -c '%i' /
# ext4: 2 (항상)
# ext4 저널 inode 확인
debugfs -R "stat <8>" /dev/sda1
# Type: regular Mode: 0600
# Size: 134217728 (128MB 저널)
# 예약된 inode 수 확인
tune2fs -l /dev/sda1 | grep "First inode"
# First inode: 11
# → inode 1~10은 예약됨
# lost+found 디렉터리 inode (ext4)
stat -c '%i' /lost+found
# 일반적으로 11 (첫 비예약 inode)
inode 디버깅과 추적
inode 관련 문제를 진단할 때 사용할 수 있는 도구와 기법을 정리합니다. 커널 트레이싱, proc/sysfs 인터페이스, 사용자 공간(User Space) 유틸리티를 활용하여 inode 동작을 실시간(Real-time)으로 관찰할 수 있습니다.
proc/sysfs를 통한 inode 모니터링
# ===== 시스템 전체 inode 상태 =====
# 할당된 inode 수 (nr_inodes, nr_free_inodes)
cat /proc/sys/fs/inode-nr
# 출력: 56789 1234
# 의미: 총 56789개 할당, 1234개 미사용(LRU 캐시)
# inode 상태 상세
cat /proc/sys/fs/inode-state
# nr_inodes nr_free_inodes preshrink unused
# 파일시스템별 inode 사용량
df -i
# Filesystem Inodes IUsed IFree IUse% Mounted on
# /dev/sda1 6553600 234567 6319033 4% /
# tmpfs 505872 5 505867 1% /dev/shm
# slab 캐시에서 inode 메모리 사용량 확인
slabtop -o -s c | head -20
# 또는 특정 캐시만
grep -E 'inode_cache|ext4_inode_cache|xfs_inode' /proc/slabinfo
# ext4_inode_cache 12345 12500 1096 29 8 : tunables ...
# 의미: 12345개 활성 객체, 각 1096바이트
# 특정 파일의 inode 상세 정보
stat /home/user/test.txt
# File: test.txt
# Size: 4096 Blocks: 8 IO Block: 4096 regular file
# Device: 801h/2049d Inode: 1234567 Links: 1
# Access: (0644/-rw-r--r--) Uid: (1000/user) Gid: (1000/user)
# Access: 2024-01-15 10:30:00.000000000 +0900
# Modify: 2024-01-15 10:30:00.000000000 +0900
# Change: 2024-01-15 10:30:00.000000000 +0900
# Birth: 2024-01-15 10:30:00.000000000 +0900
# statx로 확장 정보 (크기, 블록, 마운트 ID, DAX 상태 등)
python3 -c "
import os
r = os.stat('/home/user/test.txt')
print(f'inode: {r.st_ino}')
print(f'nlink: {r.st_nlink}')
print(f'size: {r.st_size}')
print(f'blocks: {r.st_blocks}')
print(f'uid: {r.st_uid}, gid: {r.st_gid}')
print(f'mode: {oct(r.st_mode)}')
"
ftrace/perf를 통한 inode 추적
# ===== ftrace로 inode 관련 함수 추적 =====
# inode 할당/해제 추적
echo 1 > /sys/kernel/debug/tracing/events/writeback/writeback_dirty_inode/enable
echo 1 > /sys/kernel/debug/tracing/events/writeback/writeback_written/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 특정 시스템콜의 inode 경로 추적
perf trace -e 'open*,close,stat*,unlink' ls /tmp/
# 출력: 각 시스템콜의 인자와 반환값
# inode evict 추적 (메모리 압력 진단)
echo 1 > /sys/kernel/debug/tracing/events/writeback/writeback_lazytime_iput/enable
# ===== BPF/bpftrace로 세밀한 추적 =====
# inode 할당 빈도 측정
bpftrace -e 'kprobe:new_inode { @[comm] = count(); }'
# inode evict 추적
bpftrace -e '
kprobe:evict {
$inode = (struct inode *)arg0;
printf("evict: ino=%lu, nlink=%u, i_count=%d\n",
$inode->i_ino, $inode->i_nlink,
$inode->i_count.counter);
}'
# iput() 호출 스택 추적 (누가 참조를 해제하는지)
bpftrace -e '
kprobe:iput {
$inode = (struct inode *)arg0;
if ($inode->i_count.counter == 1) {
printf("last iput: ino=%lu\n", $inode->i_ino);
print(kstack);
}
}'
# writeback dirty inode 추적
bpftrace -e '
kprobe:__mark_inode_dirty {
$inode = (struct inode *)arg0;
$flags = arg1;
printf("dirty: ino=%lu flags=0x%x comm=%s\n",
$inode->i_ino, $flags, comm);
}'
파일시스템별 디버깅 도구
| 도구 | 파일시스템 | 주요 기능 | 예시 명령 |
|---|---|---|---|
debugfs | ext2/3/4 | inode 직접 조회, 삭제 파일 복구 | debugfs -R "stat <42>" /dev/sda1 |
xfs_db | XFS | AG, inode, extent 정보 조회 | xfs_db -c "inode 42" /dev/sdb1 |
btrfs inspect | Btrfs | inode에서 경로 역추적 | btrfs inspect inode-resolve 42 /mnt |
filefrag | 모든 FS | 파일의 extent 매핑, 단편화(Fragmentation) 확인 | filefrag -v /home/user/test.txt |
xfs_io | 모든 FS | 파일 I/O 테스트, fiemap, fsync | xfs_io -c "fiemap" test.txt |
lsof | 모든 FS | 열린 파일과 inode 확인 | lsof +D /tmp |
fuser | 모든 FS | 파일/마운트를 사용하는 프로세스 | fuser -mv /mnt/data |
# ===== ext4 디버깅 실전 예시 =====
# 1. 삭제된 파일이 디스크를 점유하는 문제 진단
# 삭제되었지만 열린 fd가 있어 디스크 해제가 안 됨
lsof +L1
# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
# java 12345 app 23u REG 8,1 2147483648 0 987654 /var/log/app.log (deleted)
# → PID 12345의 fd 23이 삭제된 2GB 파일을 잡고 있음
# 해결: 프로세스 재시작 또는 fd truncate
: > /proc/12345/fd/23 # 파일 크기를 0으로 (재시작 불가 시)
# 2. inode 고갈 진단
df -i /
# IUse% 가 100%이면 inode 고갈
# 소파일이 많은 디렉터리 찾기:
find / -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head -20
# 3. ext4 inode extent 상태 확인
debugfs -R "dump_extents <42>" /dev/sda1
# Level Entries Logical Physical Length Flags
# 0/ 0 1/ 4 0 - 1023 1234 - 2257 1024
# 4. 삭제된 파일 복구 시도 (ext4)
debugfs /dev/sda1
# debugfs: lsdel
# Inode Owner Mode Size Blocks Time deleted
# 987654 1000 100644 4096 8/8 Sat Jan 15 10:30:00 2024
# debugfs: dump <987654> /tmp/recovered_file
debugfs는 마운트된 파일시스템에서 쓰기 모드(-w)로 사용하면 데이터 손상을 유발할 수 있습니다. 읽기 전용(Read-Only) 모드(기본)에서만 사용하고, 수정이 필요하면 반드시 umount 후 작업하세요. 프로덕션 환경에서는 스냅샷에서 작업하는 것이 안전합니다.
inode와 네트워크 파일시스템
NFS, CIFS/SMB 등 네트워크 파일시스템에서 inode는 로컬 파일시스템과 다른 특수한 도전 과제를 가집니다. 서버와 클라이언트 간의 일관성 유지가 핵심입니다.
NFS inode 특성
| 특성 | 로컬 FS (ext4) | NFS |
|---|---|---|
| inode 번호 | 디스크 고정 | 서버에서 전달 (filehandle 기반) |
| 캐시 유효성 | 항상 유효 | 변경 속성(change attribute)으로 검증 |
| 타임스탬프 정밀도 | FS에 따라 나노초 | 서버 의존 (NFSv4: 나노초 가능) |
| 잠금 | 커널 내부 잠금 | 네트워크 잠금 (NLM/NFSv4 lock) |
| attribute 캐시 | 없음 (항상 최신) | ac{reg,dir}{min,max} 옵션으로 제어 |
| inode 재검증 | 불필요 | d_revalidate → GETATTR RPC |
| close-to-open | 해당 없음 | close 시 flush, open 시 revalidate |
/* NFS inode 확장 구조체 */
struct nfs_inode {
struct inode vfs_inode;
/* NFS 파일 핸들 — 서버에서 inode를 식별하는 불투명 토큰 */
struct nfs_fh fh;
/* 변경 속성 — 서버 inode가 변경되면 증가 */
u64 change_attr;
/* 속성 캐시 타임아웃 */
unsigned long attrtimeo; /* 현재 유효 기간 */
unsigned long attrtimeo_timestamp;
unsigned long attr_gencount;
/* delegation — 서버가 클라이언트에 부여한 권한 */
struct nfs_delegation *delegation;
/* 캐시 유효성 카운터 */
unsigned long cache_validity; /* NFS_INO_INVALID_* */
};
/* NFS inode 재검증 플래그 */
#define NFS_INO_INVALID_DATA (1 << 1) /* 데이터 캐시 무효 */
#define NFS_INO_INVALID_ATIME (1 << 2) /* atime 무효 */
#define NFS_INO_INVALID_ACCESS (1 << 3) /* 접근 권한 캐시 무효 */
#define NFS_INO_INVALID_ACL (1 << 4) /* ACL 캐시 무효 */
#define NFS_INO_INVALID_SIZE (1 << 6) /* 파일 크기 무효 */
#define NFS_INO_INVALID_CHANGE (1 << 10) /* change attr 무효 */
/* 속성 캐시 마운트 옵션 */
/* mount -t nfs -o acregmin=3,acregmax=60,acdirmin=30,acdirmax=60 */
/* acregmin: 일반 파일 속성 캐시 최소 유효 기간 (초) */
/* acregmax: 일반 파일 속성 캐시 최대 유효 기간 (초) */
/* acdirmin/max: 디렉터리 속성 캐시 유효 기간 */
NFS close-to-open 일관성: NFS 클라이언트는 close() 시 dirty 데이터를 서버로 flush하고, open() 시 서버의 최신 속성을 가져와 캐시를 검증합니다. 이 close-to-open 보장은 단일 파일에 대해 순차적으로 접근하는 경우에만 일관성을 보장합니다. 동시 쓰기가 필요한 경우에는 NFS 잠금 또는 actimeo=0 (속성 캐시 비활성화) 옵션이 필요하지만, 성능이 크게 저하됩니다.
inode 관련 sysctl 튜닝 종합
inode 캐시와 관련된 sysctl 파라미터를 종합적으로 정리합니다. 워크로드에 따른 최적 설정값은 다를 수 있으므로, 벤치마크와 모니터링을 통해 결정해야 합니다.
| sysctl | 기본값 | 설명 | 파일 서버 권장 | DB 서버 권장 |
|---|---|---|---|---|
vm.vfs_cache_pressure | 100 | dentry/inode 캐시 회수 적극성 | 50 (캐시 유지) | 150 (메모리 확보) |
vm.dirty_background_ratio | 10 | 비동기 writeback 시작 임계값(%) | 5 (빈번한 소량) | 5 |
vm.dirty_ratio | 20 | 동기 writeback 강제 임계값(%) | 40 (대용량 배치) | 10 |
vm.dirty_writeback_centisecs | 500 | writeback 점검 주기 (1/100초) | 500 | 100 |
vm.dirty_expire_centisecs | 3000 | dirty 데이터 만료 시간 (1/100초) | 3000 | 1000 |
fs.inotify.max_user_watches | 8192 | 프로세스당 최대 inotify 감시 수 | 524288 | 8192 |
fs.inotify.max_user_instances | 128 | 사용자당 최대 inotify 인스턴스 수 | 512 | 128 |
fs.file-max | 시스템 의존 | 시스템 전체 최대 열린 파일 수 | 2097152 | 1048576 |
# ===== inode 관련 sysctl 설정 예시 =====
# 파일 서버 프로파일
# /etc/sysctl.d/99-fileserver.conf
vm.vfs_cache_pressure = 50
vm.dirty_background_ratio = 5
vm.dirty_ratio = 40
vm.dirty_expire_centisecs = 3000
fs.inotify.max_user_watches = 524288
fs.file-max = 2097152
# DB 서버 프로파일
# /etc/sysctl.d/99-database.conf
vm.vfs_cache_pressure = 150
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10
vm.dirty_writeback_centisecs = 100
vm.dirty_expire_centisecs = 1000
# 적용
sysctl -p /etc/sysctl.d/99-fileserver.conf
# 현재 dirty 상태 모니터링
watch -n 1 'grep -E "Dirty|Writeback|NFS" /proc/meminfo'
# Dirty: 12345 kB
# Writeback: 567 kB
# NFS_Unstable: 0 kB
# inode 캐시 수동 해제 (비상 시만)
echo 2 > /proc/sys/vm/drop_caches # dentry + inode 캐시 해제
# 주의: 프로덕션에서는 성능 저하 유발
echo 2 > /proc/sys/vm/drop_caches는 모든 dentry와 inode 캐시를 즉시 해제합니다. 이후 모든 파일 접근이 디스크 I/O를 유발하므로 심각한 성능 저하가 발생합니다. 이 명령은 벤치마크의 캐시 워밍 제거 목적으로만 사용하고, 프로덕션 환경에서는 vfs_cache_pressure 튜닝으로 점진적 회수를 유도하는 것이 바람직합니다.
statx 시스템콜과 확장 inode 정보
statx(2)는 기존 stat(2)의 한계를 극복하기 위해 커널 4.11에서 도입된 확장 stat 시스템콜입니다. 생성 시간(birth time), 마운트 ID, DAX 상태 등 기존 stat으로 얻을 수 없었던 inode 정보를 제공합니다.
/* include/uapi/linux/stat.h — statx 구조체 */
struct statx {
__u32 stx_mask; /* 유효한 필드 마스크 */
__u32 stx_blksize; /* I/O 블록 크기 */
__u64 stx_attributes; /* 파일 속성 플래그 */
__u32 stx_nlink; /* 하드 링크 수 */
__u32 stx_uid; /* 소유자 UID */
__u32 stx_gid; /* 소유자 GID */
__u16 stx_mode; /* 파일 유형 + 권한 */
__u64 stx_ino; /* inode 번호 */
__u64 stx_size; /* 파일 크기 */
__u64 stx_blocks; /* 512B 블록 수 */
__u64 stx_attributes_mask;
/* 타임스탬프 (나노초 정밀도) */
struct statx_timestamp stx_atime;
struct statx_timestamp stx_btime; /* 생성 시간! */
struct statx_timestamp stx_ctime;
struct statx_timestamp stx_mtime;
/* 디바이스 정보 */
__u32 stx_rdev_major;
__u32 stx_rdev_minor;
__u32 stx_dev_major;
__u32 stx_dev_minor;
__u64 stx_mnt_id; /* 마운트 ID (5.8+) */
__u32 stx_dio_mem_align; /* DIO 메모리 정렬 (6.1+) */
__u32 stx_dio_offset_align; /* DIO 오프셋 정렬 */
};
/* stx_attributes 플래그 */
#define STATX_ATTR_COMPRESSED 0x00000004 /* 압축됨 */
#define STATX_ATTR_IMMUTABLE 0x00000010 /* 변경 불가 */
#define STATX_ATTR_APPEND 0x00000020 /* 추가만 가능 */
#define STATX_ATTR_NODUMP 0x00000040 /* dump 제외 */
#define STATX_ATTR_ENCRYPTED 0x00000800 /* 암호화됨 */
#define STATX_ATTR_VERITY 0x00100000 /* fs-verity */
#define STATX_ATTR_DAX 0x00200000 /* DAX 모드 */
/* 사용 예시 */
struct statx stx;
statx(AT_FDCWD, "/home/user/test.txt",
AT_STATX_SYNC_AS_STAT, STATX_ALL, &stx);
if (stx.stx_mask & STATX_BTIME)
printf("Birth: %lld.%09u\n",
stx.stx_btime.tv_sec, stx.stx_btime.tv_nsec);
stat vs statx 비교
| 기능 | stat/fstat/lstat | statx |
|---|---|---|
| 생성 시간 (birth time) | 미지원 | stx_btime |
| 마운트 ID | 미지원 | stx_mnt_id |
| 파일 속성 (DAX, verity) | 미지원 | stx_attributes |
| 선택적 필드 요청 | 불가 (전부 반환) | mask로 필요한 것만 |
| 강제 동기화 제어 | 불가 | AT_STATX_FORCE_SYNC 등 |
| DIO 정렬 정보 | 미지원 | stx_dio_mem_align (6.1+) |
| inode 번호 크기 | ino_t (32/64비트) | 항상 __u64 |
| 커널 버전 | 초기 커널 | 4.11+ |
statx 동기화 모드: statx()의 flags에 AT_STATX_FORCE_SYNC를 지정하면 NFS 등 네트워크 FS에서 서버의 최신 속성을 강제로 가져옵니다. 반대로 AT_STATX_DONT_SYNC는 캐시만 조회하여 네트워크 오버헤드 없이 빠르게 정보를 얻을 수 있습니다. 기본값인 AT_STATX_SYNC_AS_STAT는 기존 stat과 동일한 동작(로컬 FS는 즉시, NFS는 캐시 정책에 따름)을 합니다.
inode 관련 성능 패턴과 안티패턴
실무에서 자주 마주치는 inode 관련 성능 문제와 해결 패턴을 정리합니다.
패턴/안티패턴 요약
| 유형 | 패턴 | 상세 | 영향 |
|---|---|---|---|
| 안티 | 소파일 대량 생성 + ext4 | inode 고정 할당 → inode 고갈 | ENOSPC (디스크 여유있어도) |
| 패턴 | 소파일 워크로드에 XFS/Btrfs | 동적 inode 할당 → 고갈 없음 | 안정적 운영 |
| 안티 | strictatime 마운트 | 매 read마다 inode dirty → writeback | 불필요한 I/O, 성능 저하 |
| 패턴 | noatime 또는 lazytime | atime 갱신 최소화 | I/O 절감, SSD 수명 연장 |
| 안티 | 대량 unlink 후 재시작(Reboot) 없음 | 열린 fd가 디스크 공간 잡고 있음 | 디스크 full 지속 |
| 패턴 | logrotate + copytruncate | inode 유지, 내용만 잘라냄 | 디스크 즉시 해제 |
| 안티 | 수백만 엔트리 단일 디렉터리 | 디렉터리 lookup 성능 저하 | ls, rm 극도로 느림 |
| 패턴 | 해시 기반 서브디렉터리 분산 | dir_index (htree) 한계 완화 | 안정적 조회 성능 |
| 안티 | inotify로 거대 트리 감시 | watch 수 폭발 (재귀 미지원) | 메모리 낭비, ENOMEM |
| 패턴 | fanotify FAN_MARK_MOUNT 사용 | 마운트 전체 감시 (단일 mark) | 효율적 이벤트 수신 |
| 안티 | NFS actimeo=0 + 높은 부하 | 모든 접근마다 GETATTR RPC | 네트워크 폭주, 지연 증가 |
| 패턴 | NFS 기본 캐시 + close-to-open | 적절한 캐시로 RPC 절감 | 합리적 일관성 + 성능 |
# ===== 성능 문제 진단 원라이너 모음 =====
# 1. 삭제되었지만 열린 파일 (디스크 점유) 찾기
lsof +L1 | awk '{total += $7} END {printf "Total: %.1f GB\n", total/1024/1024/1024}'
# 2. inode 사용률이 높은 파일시스템 찾기
df -i | awk '$5+0 > 80 {print "WARNING:", $0}'
# 3. 디렉터리별 파일 수 상위 20개
find / -xdev -type d -exec sh -c 'echo "$(ls -1 "{}" 2>/dev/null | wc -l) {}"' \; 2>/dev/null | sort -rn | head -20
# 4. inode 캐시 메모리 사용량 (MB)
grep inode_cache /proc/slabinfo | awk '{printf "%.1f MB (%d objects)\n", $3*$4/1024/1024, $2}'
# 5. dirty inode writeback 지연 확인
cat /proc/meminfo | grep -E "Dirty|Writeback"
# 6. 파일 단편화 확인
filefrag -v /path/to/large/file
# extents 수가 많으면 단편화
# 7. 특정 프로세스의 열린 파일 수
ls -la /proc/$PID/fd | wc -l
# 또는 시스템 전체
cat /proc/sys/fs/file-nr
# 할당된 fd 미사용 fd 최대 fd
rm -rf로 삭제하면 커널이 각 inode를 순차적으로 evict하면서 시스템 전체가 느려질 수 있습니다. 대안: (1) ionice -c3 rm -rf로 I/O 우선순위(Priority) 낮추기, (2) find ... -delete로 배치 삭제, (3) rsync --delete로 빈 디렉터리와 동기화, (4) 파일시스템 재생성(mkfs)이 가장 빠릅니다 (전체 삭제 시).
inode와 Direct I/O
Direct I/O(DIO)는 페이지 캐시를 우회하여 사용자 버퍼와 디스크 간에 직접 데이터를 전송합니다. 데이터베이스처럼 자체 캐시를 가진 애플리케이션에서 이중 캐싱을 피하기 위해 사용됩니다. inode 관점에서 DIO는 특수한 잠금과 일관성 요구사항을 가집니다.
/* Direct I/O와 inode의 상호작용 */
/* 1. DIO 읽기: i_rwsem을 공유(shared) 모드로 획득 */
/* → 다른 DIO 읽기와 병렬 가능 */
/* → 버퍼드 write/truncate와는 배타적 */
/* 2. DIO 쓰기: i_rwsem을 배타적(exclusive) 모드로 획득 */
/* → 다른 모든 I/O와 직렬화 */
/* (단, inode_dio_wait 패턴으로 최적화 가능) */
/* DIO와 페이지 캐시 일관성 */
ssize_t ext4_dio_write_iter(struct kiocb *iocb,
struct iov_iter *from)
{
struct inode *inode = file_inode(iocb->ki_filp);
/* i_rwsem 획득 */
inode_lock(inode);
/* 페이지 캐시의 dirty 데이터를 먼저 기록 */
ret = filemap_write_and_wait_range(
inode->i_mapping, offset, offset + count - 1);
/* 해당 범위의 페이지 캐시를 무효화 */
invalidate_inode_pages2_range(
inode->i_mapping, offset >> PAGE_SHIFT,
(offset + count - 1) >> PAGE_SHIFT);
/* DIO 쓰기 진행 중임을 표시 */
inode_dio_begin(inode);
/* 실제 DIO 수행 */
ret = iomap_dio_rw(iocb, from, &ext4_iomap_ops,
&ext4_dio_write_ops, ...);
inode_dio_end(inode);
inode_unlock(inode);
return ret;
}
/* inode_dio_begin/end — DIO 진행 추적 */
static inline void inode_dio_begin(struct inode *inode)
{
atomic_inc(&inode->i_dio_count);
}
static inline void inode_dio_end(struct inode *inode)
{
if (atomic_dec_and_test(&inode->i_dio_count))
wake_up_bit(&inode->i_state, __I_DIO_WAKEUP);
}
/* truncate 시 DIO 완료 대기 */
inode_dio_wait(inode); /* i_dio_count가 0이 될 때까지 대기 */
DIO 정렬 요구사항
| 요소 | 전통적 요구사항 | 커널 6.1+ (statx) |
|---|---|---|
| 버퍼 정렬 | 논리 블록 크기 (보통 512B) | stx_dio_mem_align |
| 오프셋 정렬 | 논리 블록 크기 | stx_dio_offset_align |
| 전송 크기 | 논리 블록 크기의 배수 | 오프셋 정렬과 동일 |
| NVMe/4Kn | 4096바이트 | 디바이스에 따라 자동 |
# DIO 정렬 요구사항 확인 (커널 6.1+)
python3 -c "
import os, struct
# statx 시스템콜로 DIO 정렬 확인
# stx_dio_mem_align, stx_dio_offset_align 필드 확인
r = os.stat('/home/user/test.txt')
print(f'Block size: {r.st_blksize}')
"
# O_DIRECT로 파일 열기 (사용자 공간)
# dd if=/dev/zero of=test.dat bs=4k count=100 oflag=direct
# xfs_io로 DIO 테스트
xfs_io -d -c "pread 0 4096" /path/to/file # -d: O_DIRECT
# fio로 DIO 성능 벤치마크
fio --name=dio_test --filename=/path/to/file \
--rw=randread --bs=4k --direct=1 \
--numjobs=4 --iodepth=32 --runtime=30
inode와 DAX (Direct Access)
DAX(Direct Access)는 영구 메모리(Persistent Memory, PMEM)에서 페이지 캐시를 완전히 우회하여 사용자 공간에서 스토리지에 직접 load/store 명령으로 접근하는 기술입니다. inode의 S_DAX 플래그가 DAX 모드를 표시하며, address_space의 동작이 근본적으로 달라집니다.
/* DAX 관련 inode 플래그 */
#define S_DAX (1 << 13) /* DAX 모드 활성화 */
#define IS_DAX(inode) ((inode)->i_flags & S_DAX)
/* DAX 모드에서의 read/write — 페이지 캐시 없음! */
/* 1. read: dax_iomap_rw() → 직접 memcpy from PMEM */
/* 2. write: dax_iomap_rw() → 직접 memcpy to PMEM */
/* 3. mmap: dax_iomap_fault() → PMEM 물리 주소를 PTE에 직접 매핑 */
/* ext4 DAX address_space_operations */
static const struct address_space_operations ext4_dax_aops = {
.writepages = ext4_dax_writepages,
.direct_IO = noop_direct_IO, /* DIO 경로 미사용 */
.dirty_folio = noop_dirty_folio,
};
/* DAX mmap — 페이지 폴트 시 PMEM 직접 매핑 */
static vm_fault_t ext4_dax_huge_fault(struct vm_fault *vmf,
unsigned int order)
{
/* PMEM의 물리 주소를 PTE에 직접 매핑 */
/* → 사용자 공간에서 load/store로 직접 접근 */
/* → memcpy 오버헤드 제거 */
return dax_iomap_fault(vmf, order, NULL, NULL,
&ext4_iomap_ops);
}
| 비교 | 일반 I/O | Direct I/O | DAX |
|---|---|---|---|
| 페이지 캐시 | 사용 | 우회 (버퍼 복사) | 없음 (직접 매핑) |
| 데이터 복사 | 커널↔유저 복사 | DMA 전송 | load/store 직접 접근 |
| mmap | 페이지 캐시 매핑 | - | PMEM 물리 주소(Physical Address) 직접 매핑 |
| address_space | XArray에 folio 관리 | 페이지 캐시 flush | XArray에 DAX entry(PFN) 관리 |
| writeback | dirty folio → 디스크 | 즉시 디스크 | 없음 (이미 영구 저장) |
| 스토리지 | SSD/HDD | SSD/HDD | PMEM (Intel Optane 등) |
# DAX 설정
# 1. PMEM 장치 확인
ndctl list -N
# 2. fsdax 모드로 namespace 설정
ndctl create-namespace -m fsdax -e namespace0.0
# 3. 파일시스템 생성 + 마운트
mkfs.ext4 /dev/pmem0
mount -o dax=always /dev/pmem0 /mnt/pmem
# 파일별 DAX 설정 (커널 5.8+, per-file DAX)
xfs_io -c "chattr +x" /mnt/pmem/file # DAX 활성화
xfs_io -c "chattr -x" /mnt/pmem/file # DAX 비활성화
# DAX 상태 확인
statx /mnt/pmem/file | grep -i dax
# STATX_ATTR_DAX: set
# 마운트 옵션
# dax=always — 모든 파일에 DAX 강제
# dax=never — DAX 비활성화
# dax=inode — 파일별 FS_XFLAG_DAX 기반 (기본)
reflink(CoW 복사), 인라인 데이터, 암호화(fscrypt)와 동시 사용 불가합니다. 또한 MADV_HUGEPAGE로 투명 대형 페이지를 활용하려면 PMEM이 2MB/1GB 정렬되어야 합니다. DAX 파일에 대한 sendfile()은 일반 read+write 경로로 폴백됩니다.
inode와 디스크 쿼타
디스크 쿼타는 사용자/그룹/프로젝트별로 inode 수와 블록 사용량을 제한합니다. inode 생성·삭제 시 VFS가 쿼타 검사를 수행하며, 한도 초과 시 -EDQUOT를 반환합니다.
/* include/linux/quota.h — 쿼타 정보 구조 */
struct dquot {
struct super_block *dq_sb;
kqid_t dq_id; /* uid/gid/projid */
struct mem_dqblk dq_dqb; /* 사용량 + 한도 */
};
struct mem_dqblk {
qsize_t dqb_bhardlimit; /* 블록 하드 리밋 */
qsize_t dqb_bsoftlimit; /* 블록 소프트 리밋 */
qsize_t dqb_curspace; /* 현재 사용 블록 */
qsize_t dqb_ihardlimit; /* inode 하드 리밋 */
qsize_t dqb_isoftlimit; /* inode 소프트 리밋 */
qsize_t dqb_curinodes; /* 현재 사용 inode 수 */
struct timespec64 dqb_btime; /* 블록 소프트 리밋 유예 기한 */
struct timespec64 dqb_itime; /* inode 소프트 리밋 유예 기한 */
};
/* inode 생성 시 쿼타 검사 흐름 */
/* 1. dquot_initialize(dir) — 부모 디렉터리 쿼타 초기화 */
/* 2. dquot_alloc_inode(inode) — inode 쿼타 할당 (카운트++) */
/* → dqb_curinodes++ */
/* → dqb_ihardlimit 초과 시 -EDQUOT 반환 */
/* 3. dquot_alloc_block(inode, count) — 블록 쿼타 할당 */
/* inode 삭제 시 쿼타 반환 */
/* evict_inode() → dquot_free_inode(inode) */
/* → dqb_curinodes-- */
| 쿼타 유형 | 식별자 | 용도 | 설정 명령 |
|---|---|---|---|
| User quota | UID | 사용자별 용량 제한 | setquota -u user 100M 120M 1000 1200 / |
| Group quota | GID | 그룹별 용량 제한 | setquota -g group 500M 600M 5000 6000 / |
| Project quota | Project ID | 디렉터리별 용량 제한 | xfs_quota -x -c 'limit -p bhard=1G projid' / |
# 쿼타 설정 (ext4)
# 1. 마운트 옵션에 쿼타 활성화
mount -o usrquota,grpquota /dev/sda1 /home
# 2. 쿼타 파일 생성 + 초기화
quotacheck -cug /home
quotaon /home
# 3. 사용자 쿼타 설정 (블록 소프트/하드, inode 소프트/하드)
setquota -u user1 100M 120M 10000 12000 /home
# → inode 최대 12,000개, 블록 최대 120MB
# 쿼타 확인
repquota -as /home
# 또는
quota -u user1
# XFS 프로젝트 쿼타 (디렉터리 단위)
echo "1:/home/project_a" >> /etc/projects
echo "project_a:1" >> /etc/projid
xfs_quota -x -c "project -s project_a" /home
xfs_quota -x -c "limit -p bhard=10G 1" /home
소프트 리밋 vs 하드 리밋: 소프트 리밋은 유예 기간(grace period, 기본 7일) 동안 초과를 허용하고, 유예 기간 만료 후 하드 리밋처럼 동작합니다. 하드 리밋은 절대 초과 불가합니다. inode 쿼타는 소파일이 대량 생성되는 환경(메일 서버, npm 캐시)에서 특히 중요합니다 — 블록 쿼타는 여유가 있어도 inode 쿼타에 걸려 파일 생성이 실패할 수 있습니다.
파일시스템 드라이버의 inode 구현 체크리스트
새로운 파일시스템 드라이버를 작성할 때 inode 관련 필수 구현 항목을 체크리스트로 정리합니다.
필수 구현 항목
| # | 항목 | 관련 콜백/함수 | 설명 |
|---|---|---|---|
| 1 | FS-specific inode 구조체 정의 | - | struct inode를 임베드한 확장 구조체 |
| 2 | slab 캐시 생성 | kmem_cache_create() | 모듈 init에서 inode 전용 slab 캐시 생성 |
| 3 | alloc_inode() | s_op->alloc_inode | slab에서 FS-specific inode 할당 |
| 4 | free_inode() | s_op->free_inode | RCU 콜백으로 slab 해제 |
| 5 | write_inode() | s_op->write_inode | dirty inode를 디스크에 기록 |
| 6 | evict_inode() | s_op->evict_inode | inode 제거 (nlink==0이면 디스크 해제) |
| 7 | lookup() | i_op->lookup | 디렉터리에서 이름으로 inode 검색 |
| 8 | create() | i_op->create | 새 일반 파일 inode 생성 |
| 9 | getattr() | i_op->getattr | stat(2)용 속성 반환 (선택, generic 가능) |
| 10 | setattr() | i_op->setattr | chmod/chown/truncate 처리 |
| 11 | read_folio() | a_ops->read_folio | 페이지 캐시 미스 시 디스크에서 읽기 |
| 12 | writepages() | a_ops->writepages | dirty 페이지를 디스크에 기록 |
/* 최소 파일시스템의 inode 구현 뼈대 */
/* 1. FS-specific inode 정의 */
struct myfs_inode_info {
__u32 i_disk_flags;
__u64 i_disk_size;
sector_t i_first_block;
struct inode vfs_inode; /* 반드시 임베드 */
};
static inline struct myfs_inode_info *MYFS_I(struct inode *i)
{
return container_of(i, struct myfs_inode_info, vfs_inode);
}
/* 2. slab 캐시 */
static struct kmem_cache *myfs_inode_cachep;
static int __init myfs_init_inodecache(void)
{
myfs_inode_cachep = kmem_cache_create(
"myfs_inode_cache",
sizeof(struct myfs_inode_info),
0,
SLAB_RECLAIM_ACCOUNT | SLAB_ACCOUNT,
NULL);
return myfs_inode_cachep ? 0 : -ENOMEM;
}
/* 3-4. alloc/free_inode */
static struct inode *myfs_alloc_inode(struct super_block *sb)
{
struct myfs_inode_info *mi;
mi = alloc_inode_sb(sb, myfs_inode_cachep, GFP_KERNEL);
if (!mi)
return NULL;
mi->i_disk_flags = 0;
mi->i_disk_size = 0;
mi->i_first_block = 0;
return &mi->vfs_inode;
}
static void myfs_free_inode(struct inode *inode)
{
kmem_cache_free(myfs_inode_cachep, MYFS_I(inode));
}
/* 5. write_inode */
static int myfs_write_inode(struct inode *inode,
struct writeback_control *wbc)
{
struct myfs_inode_info *mi = MYFS_I(inode);
struct buffer_head *bh;
/* 디스크의 inode 블록을 읽어서 */
bh = sb_bread(inode->i_sb, myfs_inode_block(inode));
/* VFS inode 필드를 디스크 형식으로 변환하여 기록 */
myfs_fill_disk_inode(bh->b_data, inode, mi);
mark_buffer_dirty(bh);
if (wbc->sync_mode == WB_SYNC_ALL)
sync_dirty_buffer(bh);
brelse(bh);
return 0;
}
/* 6. evict_inode */
static void myfs_evict_inode(struct inode *inode)
{
truncate_inode_pages_final(&inode->i_data);
if (!inode->i_nlink) {
/* 실제 삭제: 디스크의 inode와 데이터 블록 해제 */
myfs_free_disk_inode(inode);
myfs_free_data_blocks(inode);
}
clear_inode(inode);
}
/* 7. super_operations 등록 */
static const struct super_operations myfs_sops = {
.alloc_inode = myfs_alloc_inode,
.free_inode = myfs_free_inode,
.write_inode = myfs_write_inode,
.evict_inode = myfs_evict_inode,
.statfs = simple_statfs,
.drop_inode = generic_delete_inode,
};
FS 드라이버 테스트 도구: 파일시스템 드라이버의 inode 구현을 검증하는 데 유용한 도구: (1) xfstests — 포괄적인 파일시스템 테스트 스위트 (generic/* 테스트가 VFS 호환성 검증), (2) trinity — 시스템콜 퍼저로 엣지 케이스 탐색, (3) CONFIG_LOCKDEP — 잠금 순서 위반 탐지, (4) KASAN / KMEMLEAK — inode slab 메모리 오류 탐지. 반드시 lockdep과 KASAN을 활성화한 커널에서 xfstests의 generic/ 테스트를 전부 통과시켜야 합니다.
자주 발생하는 실수
| 실수 | 증상 | 해결 |
|---|---|---|
unlock_new_inode() 누락 | 다른 스레드가 영원히 I_NEW 대기 | iget_locked 후 반드시 호출 |
clear_inode() 누락 | evict 후 BUG 발생 | evict_inode에서 반드시 호출 |
d_instantiate() 누락 | 생성된 파일을 찾을 수 없음 | create/mkdir에서 반드시 호출 |
| slab SLAB_ACCOUNT 누락 | cgroup 메모리 accounting 누락 | kmem_cache_create 플래그 추가 |
| i_nlink 직접 조작 | NFS 등에서 불일치 | set_nlink(), inode_inc_link_count() 사용 |
| 타임스탬프 직접 설정 | 정밀도 truncation 미적용 | inode_set_ctime_current() 등 API 사용 |
| i_rwsem 잠금 순서 위반 | 데드락 (lockdep 경고) | lock_rename(), nested 레벨 사용 |
| evict에서 I/O 에러 무시 | 디스크 데이터 불일치 | 에러 시 make_bad_inode() 설정 |
소스 참조 및 더 읽기
inode 구현과 관련된 커널 소스 파일과 외부 참고 자료를 정리합니다.
커널 소스 파일
| 파일 | 내용 |
|---|---|
fs/inode.c | VFS inode 핵심 구현 (할당, 해시, LRU, evict) |
fs/namei.c | 경로 조회, 권한 검사 (inode_permission) |
fs/fs-writeback.c | dirty inode writeback 인프라 |
fs/stat.c | stat/statx 시스템콜 구현 |
fs/notify/ | fsnotify, inotify, fanotify 구현 |
fs/posix_acl.c | POSIX ACL 구현 |
include/linux/fs.h | struct inode, inode_operations 정의 |
fs/ext4/inode.c | ext4 inode 읽기/쓰기/삭제 |
fs/ext4/ialloc.c | ext4 inode 할당 (Orlov) |
fs/xfs/xfs_inode.c | XFS inode 구현 |
fs/btrfs/inode.c | Btrfs inode 구현 |
fs/dcache.c | dentry 캐시 — inode와의 연결, 경로 조회 |
fs/open.c | 파일 열기 — inode에서 file 구조체로의 전환 |
mm/filemap.c | 페이지 캐시 핵심 — address_space 연산 구현 |
security/selinux/hooks.c | SELinux inode 보안 훅 구현 |
외부 참고 자료
- docs.kernel.org — Overview of the Linux Virtual File System: VFS 공식 문서. struct inode, inode_operations, super_operations, address_space_operations의 상세 명세를 제공합니다.
- LWN.net — VFS 관련 아티클 인덱스: VFS 변경사항, 새 API, 성능 개선에 대한 심층 기사 모음입니다.
- LWN.net — Filesystem timestamp granularity: VFS 타임스탬프 정밀도 개선과 Y2038 관련 변경사항을 다룹니다.
- docs.kernel.org — ext4 Data Structures and Algorithms: ext4 디스크 레이아웃, inode table, extent tree의 상세 구조를 설명합니다.
- LWN.net — Rethinking the inode number space: 64비트 inode 번호, 32비트 호환성 문제, ino 재활용에 관한 논의입니다.
- docs.kernel.org — Filesystem locking: VFS 잠금 규약과 순서를 명세합니다. 파일시스템 개발자 필독 문서입니다.
- Bootlin Elixir — include/linux/fs.h: struct inode, struct inode_operations, struct super_operations 등 VFS 핵심 자료구조의 정의를 온라인에서 확인할 수 있습니다.
- Bootlin Elixir — fs/inode.c: inode 할당(iget, new_inode), 해시 테이블 관리, evict, writeback 등 VFS inode 핵심 구현 소스입니다.
- Bootlin Elixir — fs/namei.c: 경로 조회(path lookup), 심볼릭 링크 추적, 권한 검사 등 inode 기반 네임스페이스 처리 소스입니다.
- Bootlin Elixir — fs/stat.c: stat, fstat, statx 시스템콜 구현 소스. inode 메타데이터를 사용자 공간으로 전달하는 과정을 확인할 수 있습니다.
- LWN.net — A walk through the Linux VFS, part 1: VFS의 전체 구조를 단계별로 설명하는 기사입니다. inode, dentry, superblock 간의 관계를 이해하는 데 유용합니다.
- LWN.net — Overlayfs and inode numbers: 오버레이 파일시스템에서 inode 번호 처리 방식과 관련 문제점을 논의합니다.
- LWN.net — The new mount API: 새로운 마운트 API(fsopen/fsmount)와 superblock/inode 초기화 방식의 변경을 설명합니다.
- LWN.net — Btrfs and the slow inode cache: Btrfs의 inode 캐시 성능 문제와 해결 방안에 대한 심층 분석입니다.
- LWN.net — Modernizing the inode time stamps: inode 타임스탬프의 나노초 정밀도 개선, struct timespec64 전환에 대한 논의입니다.
- docs.kernel.org — Pathname lookup: 경로 조회(path lookup)의 동작 원리를 설명하는 공식 문서입니다. RCU-walk, ref-walk 모드에서 inode가 어떻게 참조되는지 다룹니다.
- docs.kernel.org — Porting to new VFS APIs: VFS API 변경 이력과 파일시스템 포팅 가이드입니다. inode_operations 변경사항을 추적하는 데 유용합니다.
- docs.kernel.org — Directory locking: 디렉터리 inode에 대한 i_rwsem 잠금 규칙과 교착 방지 전략을 설명합니다.
- LWN.net — VFS parallel lookups: 디렉터리 inode에서 병렬 경로 조회를 가능하게 한 d_in_lookup 메커니즘을 설명합니다.
- LWN.net — The inode security label revalidation problem: 보안 레이블(SELinux xattr) 재검증과 inode 보안 구조체 관련 논의입니다.
관련 문서
inode 구조와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.