inode 구조 (inode Structure)
파일 메타데이터의 핵심 단위인 inode를 기준으로 커널이 파일의 정체성과 상태를 어떻게 관리하는지 다룹니다. `struct inode` 주요 필드, inode_operations와 file_operations의 분리 설계, inode/dentry 캐시 결합, 생성·갱신·삭제·회수 경로, 링크 카운트와 권한/타임스탬프 일관성, ext4·XFS·Btrfs 구현 차이를 실제 함수 흐름에 맞춰 상세히 정리합니다.
핵심 요약
- 이름/실체 분리 — dentry는 이름, inode는 파일 실체
- inode cache — 해시 + 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 |
| 링크 | 하드 링크 수, 참조 카운트 | i_nlink, i_count |
| 연산 | 파일시스템별 동작 테이블 | i_op, i_fop |
| 데이터 | 페이지 캐시 연결 | i_mapping, i_data |
| 잠금 | 동시성 제어 | i_lock, i_rwsem |
| 캐시 | 해시, LRU, writeback 리스트 | i_hash, i_lru, i_io_list |
inode 추상화 원리
Unix/Linux 파일시스템의 핵심 설계 원리는 "이름(name)과 메타데이터(metadata)의 분리"입니다:
- dentry (디렉터리 엔트리): 파일의 이름과 부모-자식 관계(경로 구조)를 관리합니다. 하나의 inode에 여러 dentry가 연결될 수 있습니다(하드 링크).
- inode: 파일의 실체 — 크기, 권한, 타임스탬프, 데이터 위치 등 파일 자체의 속성을 저장합니다. 고유한 inode 번호(
i_ino)로 식별됩니다.
이 분리 덕분에 하드 링크(같은 inode에 여러 이름), 파일 이동(dentry만 변경, inode 불변), 삭제된 파일의 계속 접근(열린 파일 디스크립터가 inode 참조 유지) 등이 가능합니다.
VFS inode vs 파일시스템별 inode
커널은 2단계 inode 구조를 사용합니다:
- VFS inode (
struct inode): 모든 파일시스템에 공통인 메타데이터(크기, 권한, 타임스탬프, 참조 카운트 등). VFS 계층이 직접 접근합니다. - FS-specific inode (예:
struct ext4_inode_info): 파일시스템 고유의 데이터(extent 트리, 저널링 정보 등).container_of()매크로로 VFS inode에서 역추적합니다.
각 파일시스템은 자체 inode 구조체에 VFS inode를 임베드합니다. 이 패턴은 상속 없이 다형성을 달성하는 커널의 전형적인 객체 지향 기법입니다.
inode 캐시 동작 원리
VFS는 inode 캐시(icache)를 유지하여 디스크 I/O를 최소화합니다. 동작 원리는 다음과 같습니다:
- 해시 테이블 조회: 파일 접근 시 (superblock, inode 번호) 쌍으로 해시 테이블을 검색합니다. 히트하면 디스크 읽기 없이 즉시 반환합니다.
- LRU 관리: 참조 카운트(
i_count)가 0이 된 inode는 LRU 리스트에 들어갑니다. 즉시 삭제하지 않고 캐시에 유지하여, 재접근 시 빠르게 활용합니다. - 메모리 회수: 메모리 압력 시 커널의 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 */
};
inode_operations
struct inode_operations는 inode 자체에 대한 연산(생성, 검색, 삭제, 속성 변경)을 정의합니다. 각 파일시스템은 자체 콜백을 구현하여 VFS에 등록합니다. NULL인 콜백은 "미지원"을 의미하며, VFS가 기본 동작을 수행하거나 에러를 반환합니다.
struct inode_operations {
struct dentry *(*lookup)(struct inode *, struct dentry *, unsigned int);
int (*create)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t, bool);
int (*link)(struct dentry *, struct inode *, struct dentry *);
int (*unlink)(struct inode *, struct dentry *);
int (*symlink)(struct mnt_idmap *, struct inode *, struct dentry *, const char *);
int (*mkdir)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t);
int (*rmdir)(struct inode *, struct dentry *);
int (*mknod)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t, dev_t);
int (*rename)(struct mnt_idmap *, struct inode *, struct dentry *,
struct inode *, struct dentry *, unsigned int);
int (*setattr)(struct mnt_idmap *, struct dentry *, struct iattr *);
int (*getattr)(struct mnt_idmap *, const struct path *, struct kstat *, u32, unsigned int);
int (*permission)(struct mnt_idmap *, struct inode *, int);
struct posix_acl *(*get_inode_acl)(struct inode *, int, bool);
int (*tmpfile)(struct mnt_idmap *, struct inode *, struct file *, umode_t);
int (*atomic_open)(struct inode *, struct dentry *, struct file *, unsigned, umode_t);
int (*fileattr_set)(struct mnt_idmap *, struct dentry *, struct fileattr *);
int (*fileattr_get)(struct dentry *, struct fileattr *);
};
주요 콜백 상세
| 콜백 | 호출 시점 | 인자 의미 | 반환 |
|---|---|---|---|
lookup | 경로 해석 (path_lookup) | 부모 inode, 자식 dentry | 찾은 dentry 또는 NULL |
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 기능을 지원하기 위한 것으로, 컨테이너 환경에서 마운트별로 UID/GID 매핑을 다르게 적용할 수 있습니다. 기존의 mnt_userns 포인터를 대체하며, 매핑이 필요 없는 경우 nop_mnt_idmap이 전달됩니다.
Casefold: 대소문자 무시 조회
커널 5.2부터 ext4, 5.11부터 F2FS에서 대소문자 무시(case-insensitive) 디렉터리를 지원합니다. 이 기능은 inode_operations의 lookup 콜백에서 Unicode 정규화(NFKD + casefold)를 적용하여 이름 비교를 수행합니다.
/* include/linux/fs.h — casefold 관련 필드 */
struct inode {
/* ... */
unsigned int i_flags; /* S_CASEFOLD 플래그 포함 */
/* ... */
};
/* S_CASEFOLD 매크로 — inode에 casefold 활성화 여부 */
#define S_CASEFOLD (1 << 15)
#define IS_CASEFOLDED(inode) ((inode)->i_flags & S_CASEFOLD)
/* fs/libfs.c — casefold를 적용한 이름 비교 */
int generic_ci_d_compare(const struct dentry *dentry,
unsigned int len, const char *str,
const struct qstr *name)
{
const struct dentry *parent = READ_ONCE(dentry->d_parent);
const struct inode *dir = d_inode_rcu(parent);
const struct unicode_map *um = dir->i_sb->s_encoding;
if (!dir || !IS_CASEFOLDED(dir))
return 1; /* 일반 비교 폴백 */
/* Unicode NFKD+casefold로 정규화하여 비교 */
return utf8_strncasecmp(um, name, &(struct qstr)QSTR_INIT(str, len));
}
| 항목 | 설명 |
|---|---|
| 활성화 | tune2fs -O casefold /dev/sdX (ext4), chattr +F dir/로 디렉터리별 설정 |
| 인코딩 | mkfs.ext4 -O casefold -E encoding=utf8-12.1.0 (Unicode 버전 지정) |
| dentry 캐시 | casefold 디렉터리의 dentry는 d_compare/d_hash를 casefold 버전으로 교체 |
| strict 모드 | encoding_flags=strict — 유효하지 않은 UTF-8 시퀀스 거부 |
| 호환성 | casefold 파일시스템은 커널 5.2 미만에서 마운트 불가 (호환 플래그) |
| 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) | 프로세스간 통신 |
S_IFSOCK | 소켓 | Unix domain socket |
ext4 inode 확장
각 파일시스템은 VFS inode를 자체 구조체에 임베드합니다. ext4의 경우:
struct ext4_inode_info {
__le32 i_data[15]; /* block pointers or extent tree */
__u32 i_flags;
ext4_fsblk_t i_file_acl;
/* ... ext4 specific fields ... */
struct inode vfs_inode; /* VFS inode 임베드 */
};
/* VFS inode에서 ext4 inode로 변환 */
static inline struct ext4_inode_info *EXT4_I(struct inode *inode)
{
return container_of(inode, struct ext4_inode_info, vfs_inode);
}
inode 생명주기
inode는 할당 → 초기화 → 사용 → 해제의 생명주기를 가집니다. 참조 카운트(i_count)와 하드 링크 수(i_nlink)가 모두 0이 되면 삭제됩니다.
/* 새 inode 할당 (파일시스템별 alloc_inode 호출) */
struct inode *inode = new_inode(sb);
/* inode 번호 할당 및 해시 테이블에 삽입 */
inode->i_ino = get_next_ino();
insert_inode_hash(inode);
/* 초기 속성 설정 */
inode->i_mode = S_IFREG | 0644;
inode_init_owner(idmap, inode, dir, mode);
inode->i_op = &myfs_inode_ops;
inode->i_fop = &myfs_file_ops;
inode->i_mapping->a_ops = &myfs_aops;
/* 참조 카운트 관리 */
ihold(inode); /* i_count++ (참조 획득) */
iput(inode); /* i_count-- (참조 해제, 0이면 evict) */
/* inode 삭제 경로 */
/* i_nlink == 0 && i_count == 0 → evict_inode() 호출 */
파일시스템별 inode 할당
각 파일시스템은 alloc_inode()와 free_inode()를 구현하여 자체 확장 inode를 관리합니다:
static struct kmem_cache *myfs_inode_cachep;
static struct inode *myfs_alloc_inode(struct super_block *sb)
{
struct myfs_inode_info *mi;
mi = alloc_inode_sb(sb, myfs_inode_cachep, GFP_KERNEL);
if (!mi)
return NULL;
/* fs-specific 필드 초기화 */
mi->i_disksize = 0;
return &mi->vfs_inode;
}
static void myfs_free_inode(struct inode *inode)
{
kmem_cache_free(myfs_inode_cachep, MYFS_I(inode));
}
static const struct super_operations myfs_sops = {
.alloc_inode = myfs_alloc_inode,
.free_inode = myfs_free_inode,
.write_inode = myfs_write_inode,
.evict_inode = myfs_evict_inode,
};
O_TMPFILE: 이름 없는 inode 생성
커널 3.11에서 도입된 O_TMPFILE 플래그는 디렉터리에 연결되지 않은 inode를 생성합니다. dentry 없이 inode와 file 구조체만 존재하므로, 파일명 충돌·레이스 컨디션·보안 노출 없이 안전하게 임시 파일을 사용할 수 있습니다.
/* 사용자 공간 — O_TMPFILE 사용 패턴 */
int fd = open("/tmp", O_TMPFILE | O_RDWR, 0600);
/* fd로 데이터 기록 */
write(fd, data, len);
/* 패턴 1: 작업 완료 후 원자적으로 이름 부여 (linkat) */
char procpath[64];
snprintf(procpath, sizeof(procpath), "/proc/self/fd/%d", fd);
linkat(AT_FDCWD, procpath, AT_FDCWD, "/tmp/final.dat", AT_SYMLINK_FOLLOW);
/* 패턴 2: 이름 부여 없이 close → inode 자동 삭제 */
close(fd); /* nlink==0, count==0 → evict */
/* 커널 내부 — O_TMPFILE 경로 (fs/namei.c) */
static int do_tmpfile(struct nameidata *nd, unsigned flags,
const struct open_flags *op,
struct file *file)
{
struct inode *dir = nd->path.dentry->d_inode;
/* 1. 디렉터리의 inode_operations->tmpfile 콜백 확인 */
if (!dir->i_op->tmpfile)
return -EOPNOTSUPP;
/* 2. FS별 tmpfile 콜백 호출 → 새 inode 할당 */
/* nlink=0인 상태로 생성, dentry 연결 안 함 */
error = dir->i_op->tmpfile(idmap, dir, file, op->mode);
/* 3. d_tmpfile()으로 특수 dentry 연결 */
/* 해시 테이블 미등록, 부모 미연결 */
return error;
}
/* ext4의 tmpfile 구현 — fs/ext4/namei.c */
static int ext4_tmpfile(struct mnt_idmap *idmap,
struct inode *dir,
struct file *file, umode_t mode)
{
struct inode *inode;
/* 1. 새 inode 할당 + 초기화 */
inode = ext4_new_inode_start_handle(idmap, dir, mode, ...);
/* 2. nlink = 0 (orphan 상태) */
/* 저널의 orphan 리스트에 등록 → 크래시 시 자동 정리 */
ext4_orphan_add(handle, inode);
/* 3. 연산 테이블 연결 */
inode->i_op = &ext4_file_inode_operations;
inode->i_fop = &ext4_file_operations;
/* 4. d_tmpfile()로 file에 연결 */
d_tmpfile(file, inode);
return finish_open_simple(file, 0);
}
| 비교 | mkstemp() | O_TMPFILE |
|---|---|---|
| 이름 노출 | 파일명이 디렉터리에 노출됨 | 이름 없음 (디렉터리 탐색 불가) |
| 레이스 컨디션 | 생성-삭제 사이 윈도우 존재 | 없음 (처음부터 이름 없음) |
| 원자적 게시 | rename으로 구현 (기존 파일 필요) | linkat으로 원자적 게시 |
| 크래시 안전 | 잔여 파일 수동 정리 필요 | orphan list → 자동 정리 |
| 커널 요구 | 모든 커널 | 3.11+, FS가 tmpfile 콜백 구현 필요 |
| 지원 FS | 모든 FS | ext4, XFS, Btrfs, tmpfs 등 |
실전 패턴 — 원자적 파일 교체: O_TMPFILE으로 임시 파일을 생성하고, fsync()로 데이터를 디스크에 확정한 후, linkat()으로 최종 경로에 원자적으로 게시합니다. 이 패턴은 설정 파일, 데이터베이스 WAL, 패키지 매니저 등에서 쓰기 도중 크래시에 안전한 업데이트를 보장합니다. rename() 기반보다 더 견고합니다 — 중간 상태의 파일명이 존재하지 않기 때문입니다.
확장 속성 (xattr)
inode에 추가적인 이름-값 쌍 메타데이터를 저장합니다. 보안 레이블(SELinux), ACL, 사용자 데이터 등에 사용됩니다.
| 네임스페이스 | 접두사 | 용도 |
|---|---|---|
| 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 | 마운트/파일시스템 | 전체 마운트 감시, 접근 제어 가능 |
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 필드 보호용 스핀락. i_state, i_count 등의 변경 시 사용 |
i_rwsem | struct rw_semaphore | 파일 데이터 접근 직렬화. 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까지 함께 비교해야 합니다.
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;
}
해시 테이블 연산
| 함수 | 동작 | 호출 시점 |
|---|---|---|
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 페이지 보유 | __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 대기자 깨우기 | 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);
}
I_DIRTY 세분화의 이유: I_DIRTY_SYNC(atime 같은 경량 메타데이터), I_DIRTY_DATASYNC(size 같은 데이터 관련 메타데이터), I_DIRTY_PAGES(페이지 캐시의 dirty 페이지)로 분리함으로써 fsync()와 fdatasync()가 필요한 최소한의 기록만 수행할 수 있습니다. fdatasync()는 I_DIRTY_DATASYNC | I_DIRTY_PAGES만 기록하고, 순수 메타데이터인 I_DIRTY_SYNC는 건너뜁니다.
address_space와 inode 연결
inode의 i_mapping 필드는 페이지 캐시와의 연결점입니다. struct address_space는 inode에 임베드되어 있으며(i_data 필드), 파일 데이터의 페이지를 관리합니다.
struct address_space {
struct inode *host; /* 소유 inode */
struct xarray i_pages; /* 페이지 캐시 (XArray) */
atomic_t i_mmap_writable; /* VM_SHARED 매핑 수 */
struct rb_root_cached i_mmap; /* private/shared 매핑 트리 */
unsigned long nrpages; /* 총 페이지 수 */
pgoff_t writeback_index; /* writeback 시작 위치 */
const struct address_space_operations *a_ops;
unsigned long flags; /* gfp_mask, 에러 상태 */
struct rw_semaphore i_mmap_rwsem; /* mmap 보호 */
void *private_data; /* FS별 데이터 */
};
/* inode 내부의 임베드된 address_space */
struct inode {
/* ... */
struct address_space *i_mapping; /* 일반적으로 &i_data를 가리킴 */
struct address_space i_data; /* 임베드된 address_space */
/* ... */
};
/* 초기화 시 i_mapping = &i_data로 설정됨 */
/* 블록 디바이스의 경우: 여러 파일이 같은 address_space를 공유할 수 있음 */
주요 address_space_operations 콜백
| 콜백 | 호출 시점 | 역할 |
|---|---|---|
read_folio(file, folio) | 페이지 캐시 miss (read) | 디스크에서 folio(페이지)를 읽어 채움 |
readahead(rac) | 순차 읽기 감지 | 미리 읽기 — 연속 folios를 일괄 제출 |
writepages(mapping, wbc) | writeback 발동 | dirty 페이지를 디스크에 기록 |
dirty_folio(mapping, folio) | folio가 dirty 될 때 | accounting, 저널 예약 등 |
write_begin(file, mapping, pos, len, folio) | 버퍼드 write 시작 | folio 할당 + 블록 매핑 준비 |
write_end(file, mapping, pos, len, copied, folio) | 버퍼드 write 완료 | dirty 마킹 + 메타데이터 갱신 |
invalidate_folio(folio, offset, length) | truncate/hole punch | folio의 전부 또는 일부를 무효화 |
release_folio(folio, gfp) | 메모리 회수 | private 데이터 해제 후 folio 반환 가능 여부 |
XArray로의 전환: 커널 4.20부터 페이지 캐시의 인덱스 구조가 radix tree에서 XArray로 전환되었습니다. XArray는 락 통합, API 간결성, RCU 안전 순회를 제공합니다. i_pages를 통해 파일 오프셋(인덱스)으로 해당 folio를 O(log n)에 검색할 수 있습니다.
inode와 mmap 연동
mmap()은 파일의 address_space를 프로세스의 가상 주소 공간에 매핑합니다. 이 매핑은 inode의 i_mapping을 통해 페이지 캐시와 직접 연결되므로, 여러 프로세스가 같은 파일을 mmap하면 같은 물리 페이지를 공유합니다.
/* 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 (쓰기 시 복사) | 읽기 시 공유, 쓰기 시 분리 | 사본은 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;
}
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로 결과를 반드시 확인하세요.
타임스탬프 정밀도와 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 파이프라인
dirty inode의 데이터를 디스크에 기록하는 과정을 writeback이라 합니다. 커널은 백그라운드 워커 스레드를 통해 비동기적으로 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 성능 튜닝: 데이터베이스 서버에서는 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 내부 트랜잭션 | 모든 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 |
| 암호화 | 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);
}
# 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);
}
삭제된 파일과 열린 fd: unlink()로 파일을 삭제해도 i_nlink만 0이 됩니다. 해당 파일을 열고 있는 프로세스가 있다면(i_count > 0), 실제 evict는 마지막 close()가 수행될 때까지 지연됩니다. 이 동안 /proc/PID/fd/에서 (deleted) 표시로 확인할 수 있으며, 프로세스는 정상적으로 read/write를 계속할 수 있습니다. 이것이 로그 파일 삭제 후에도 디스크 공간이 즉시 해제되지 않는 이유입니다.
inode Truncation 경로
파일 크기를 줄이는 truncate()는 inode의 i_size를 변경하고, 잘려나간 범위의 데이터 블록과 페이지 캐시를 해제하는 복잡한 과정입니다. hole punch(fallocate PUNCH_HOLE)도 유사한 경로를 사용합니다.
/* 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 필드로 연결 리스트 구성 | 슈퍼블록 + 각 inode의 i_dtime |
| ext4 (5.15+) | orphan file (COMPAT_ORPHAN_FILE) | 전용 inode에 orphan 비트맵 저장 — 동시성 향상 |
| XFS | AGI unlinked 해시 체인 | 각 AG 헤더의 unlinked 버킷 |
| Btrfs | orphan item (B-tree) | FS tree에 ORPHAN_ITEM 키 |
Orphan 디버깅: dmesg | grep orphan으로 마운트 시 정리된 orphan 수를 확인할 수 있습니다. 비정상 종료 후 "EXT4-fs: 3 orphan inodes deleted" 같은 메시지가 나타납니다. debugfs -R "lsdel"로 최근 삭제된 inode를 나열할 수 있으며, orphan 파일 기능은 tune2fs -O orphan_file로 활성화합니다 (커널 5.15+).
inode 잠금 체계
inode 관련 동시성 제어는 여러 레벨의 잠금으로 구성됩니다. 잘못된 잠금 순서는 데드락을 유발하므로, 커널은 엄격한 잠금 순서(lock ordering)를 정의합니다.
| 잠금 | 타입 | 보호 대상 | 일반적 잠금 순서 |
|---|---|---|---|
i_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 번호 할당 전략
파일시스템마다 inode 번호를 할당하는 전략이 다릅니다. 이 전략은 파일 생성 성능, inode 고갈 가능성, 32비트/64비트 호환성에 직접적인 영향을 미칩니다.
| 파일시스템 | 할당 방식 | 범위 | 고갈 가능성 | 특이사항 |
|---|---|---|---|---|
| ext4 | 비트맵 (고정 테이블) | mkfs 시 결정 | 있음 | Orlov 할당기로 디렉터리 분산 |
| XFS | AG 내 동적 할당 | 64비트 | 극히 낮음 | AG별 free inode B+tree |
| Btrfs | objectid (단조 증가) | 64비트 | 없음 | 서브볼륨별 독립 번호 공간 |
| tmpfs | get_next_ino() | 32비트 (percpu) | 번호 재활용 | 디스크 없음 |
| NFS | 서버 전달 | 서버 의존 | 서버 의존 | filehandle이 실질적 식별자 |
/* ext4 inode 할당: Orlov 알고리즘 */
/* 디렉터리를 여러 블록 그룹에 분산하여 부모-자식 근접성 유지 */
static int find_group_orlov(struct super_block *sb,
struct inode *parent, ...)
{
/* 최상위 디렉터리: 가장 여유 있는 그룹 선택 */
if (parent == d_inode(sb->s_root)) {
/* free inode 수, free block 수, 디렉터리 수 기준 */
best_group = find_best_group(sb, ...);
}
/* 하위 디렉터리: 부모와 같은 그룹 선호 */
else {
group = ext4_inode_to_goal_block(parent);
/* 부모 근처에서 시작하여 빈 슬롯 탐색 */
}
}
/* XFS inode 할당: finobt (free inode B+tree) */
/* 각 AG에 free inode를 추적하는 B+tree 유지 */
/* → O(log n)에 빈 inode 슬롯 탐색 가능 */
/* tmpfs / procfs 등 pseudo-FS */
unsigned int get_next_ino(void)
{
unsigned int *p = &get_cpu_var(last_ino);
unsigned int res = *p;
/* percpu 카운터로 락-프리 할당 */
*p = ++res;
put_cpu_var(last_ino);
return res;
}
32비트 inode 번호 주의: stat(2)의 st_ino가 32비트인 시스템에서 XFS/Btrfs의 64비트 inode 번호가 잘릴 수 있습니다. 이 경우 -EOVERFLOW가 발생합니다. XFS에서는 inode32 마운트 옵션으로 inode를 32비트 범위 내에 할당하도록 제한할 수 있지만, 대규모 파일시스템에서는 inode 배치 효율이 떨어집니다. 최선의 해결책은 64비트 시스템과 statx(2)를 사용하는 것입니다.
inode Generation 번호 (i_generation)
i_generation 필드는 inode 번호가 재사용될 때 같은 번호의 서로 다른 파일을 구별하기 위한 세대 번호입니다. NFS 파일 핸들의 핵심 구성 요소이며, inode가 삭제되고 같은 번호로 새 파일이 생성되었을 때 오래된 핸들로 접근하는 것을 방지합니다.
/* include/linux/fs.h */
struct inode {
/* ... */
__u32 i_generation; /* 세대 번호 — inode 재사용 시 증가 */
/* ... */
};
/* NFS 파일 핸들 구조 (개념적) */
struct nfs_fh {
__u64 ino; /* inode 번호 */
__u32 generation; /* i_generation — "stale" 검출 핵심 */
__u32 fsid; /* 파일시스템 ID */
};
/* ext4 — inode 할당 시 generation 설정 */
/* fs/ext4/ialloc.c — __ext4_new_inode() */
inode->i_generation = get_random_u32();
/* 또는 이전 값 + 1 (커널 버전에 따라 다름) */
/* NFS 서버 — export 시 파일 핸들 구성 */
/* fs/exportfs/expfs.c */
static int export_encode_fh(struct inode *inode, ...)
{
fh[0] = inode->i_ino;
fh[1] = inode->i_generation; /* 핵심: 세대 번호 포함 */
/* ... */
}
/* NFS 클라이언트가 오래된 핸들로 접근 시 */
/* → 서버가 ino로 inode 검색 후 generation 비교 */
/* → 불일치 → -ESTALE 반환 */
| 파일시스템 | i_generation 관리 | 특성 |
|---|---|---|
| ext4 | 할당 시 랜덤 또는 이전+1 | 디스크 inode에 저장, NFS export 안전 |
| 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 | 무결성 검증 | 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 발생 | 오버헤드 없음 | VFS 계층 매핑만 (무시할 수준) |
| 용도 | 전통적 소유권 | 프로세스 격리 | 컨테이너 파일 공유 |
컨테이너 실전: Docker/Podman에서 호스트 볼륨을 바인드 마운트할 때 UID 불일치 문제가 자주 발생합니다. idmapped mount는 이 문제의 근본적 해결책입니다. Podman 4.0+는 --userns=keep-id 옵션으로 idmapped mount를 자동 활용합니다. 기존의 chown -R이나 uid/gid 동기화 같은 임시 방편이 불필요해집니다.
특수 inode와 의사 파일시스템
리눅스 커널에는 디스크에 저장되지 않는 특수 inode들이 존재합니다. 파이프, 소켓, 익명 inode, procfs/sysfs의 가상 파일들은 모두 메모리 전용 inode로 관리됩니다.
의사 파일시스템의 inode
| 파일시스템 | inode 특성 | 할당 함수 | i_ino 부여 | 해시 삽입 |
|---|---|---|---|---|
pipefs | 파이프 양 끝점 | new_inode_pseudo() | get_next_ino() | 아니오 |
sockfs | 소켓 파일 | new_inode_pseudo() | get_next_ino() | 아니오 |
anon_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 | 부트 로더 |
| 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 인터페이스, 사용자 공간 유틸리티를 활용하여 inode 동작을 실시간으로 관찰할 수 있습니다.
proc/sysfs를 통한 inode 모니터링
# ===== 시스템 전체 inode 상태 =====
# 할당된 inode 수 (nr_inodes, nr_free_inodes)
cat /proc/sys/fs/inode-nr
# 출력: 56789 1234
# 의미: 총 56789개 할당, 1234개 미사용(LRU 캐시)
# inode 상태 상세
cat /proc/sys/fs/inode-state
# nr_inodes nr_free_inodes preshrink unused
# 파일시스템별 inode 사용량
df -i
# Filesystem Inodes IUsed IFree IUse% Mounted on
# /dev/sda1 6553600 234567 6319033 4% /
# tmpfs 505872 5 505867 1% /dev/shm
# slab 캐시에서 inode 메모리 사용량 확인
slabtop -o -s c | head -20
# 또는 특정 캐시만
grep -E 'inode_cache|ext4_inode_cache|xfs_inode' /proc/slabinfo
# ext4_inode_cache 12345 12500 1096 29 8 : tunables ...
# 의미: 12345개 활성 객체, 각 1096바이트
# 특정 파일의 inode 상세 정보
stat /home/user/test.txt
# File: test.txt
# Size: 4096 Blocks: 8 IO Block: 4096 regular file
# Device: 801h/2049d Inode: 1234567 Links: 1
# Access: (0644/-rw-r--r--) Uid: (1000/user) Gid: (1000/user)
# Access: 2024-01-15 10:30:00.000000000 +0900
# Modify: 2024-01-15 10:30:00.000000000 +0900
# Change: 2024-01-15 10:30:00.000000000 +0900
# Birth: 2024-01-15 10:30:00.000000000 +0900
# statx로 확장 정보 (크기, 블록, 마운트 ID, DAX 상태 등)
python3 -c "
import os
r = os.stat('/home/user/test.txt')
print(f'inode: {r.st_ino}')
print(f'nlink: {r.st_nlink}')
print(f'size: {r.st_size}')
print(f'blocks: {r.st_blocks}')
print(f'uid: {r.st_uid}, gid: {r.st_gid}')
print(f'mode: {oct(r.st_mode)}')
"
ftrace/perf를 통한 inode 추적
# ===== ftrace로 inode 관련 함수 추적 =====
# inode 할당/해제 추적
echo 1 > /sys/kernel/debug/tracing/events/writeback/writeback_dirty_inode/enable
echo 1 > /sys/kernel/debug/tracing/events/writeback/writeback_written/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 특정 시스템콜의 inode 경로 추적
perf trace -e 'open*,close,stat*,unlink' ls /tmp/
# 출력: 각 시스템콜의 인자와 반환값
# inode evict 추적 (메모리 압력 진단)
echo 1 > /sys/kernel/debug/tracing/events/writeback/writeback_lazytime_iput/enable
# ===== BPF/bpftrace로 세밀한 추적 =====
# inode 할당 빈도 측정
bpftrace -e 'kprobe:new_inode { @[comm] = count(); }'
# inode evict 추적
bpftrace -e '
kprobe:evict {
$inode = (struct inode *)arg0;
printf("evict: ino=%lu, nlink=%u, i_count=%d\n",
$inode->i_ino, $inode->i_nlink,
$inode->i_count.counter);
}'
# iput() 호출 스택 추적 (누가 참조를 해제하는지)
bpftrace -e '
kprobe:iput {
$inode = (struct inode *)arg0;
if ($inode->i_count.counter == 1) {
printf("last iput: ino=%lu\n", $inode->i_ino);
print(kstack);
}
}'
# writeback dirty inode 추적
bpftrace -e '
kprobe:__mark_inode_dirty {
$inode = (struct inode *)arg0;
$flags = arg1;
printf("dirty: ino=%lu flags=0x%x comm=%s\n",
$inode->i_ino, $flags, comm);
}'
파일시스템별 디버깅 도구
| 도구 | 파일시스템 | 주요 기능 | 예시 명령 |
|---|---|---|---|
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 매핑, 단편화 확인 | 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)로 사용하면 데이터 손상을 유발할 수 있습니다. 읽기 전용 모드(기본)에서만 사용하고, 수정이 필요하면 반드시 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 후 재시작 없음 | 열린 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 우선순위 낮추기, (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 물리 주소 직접 매핑 |
| 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 잠금 규약과 순서를 명세합니다. 파일시스템 개발자 필독 문서입니다.
관련 문서
inode 구조와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.