파일시스템과 VFS
왜 VFS가 필요한가
Linux는 ext4, btrfs, XFS, NFS, tmpfs, procfs, sysfs, FAT32 등 수십 가지 파일시스템을 지원한다. 사용자 프로그램이 open("/proc/cpuinfo", O_RDONLY)를 호출할 때 그것이 procfs인지, ext4인지 몰라도 된다. 이 투명한 추상화가 VFS(Virtual File System)다.
VFS는 커널의 fs/ 디렉토리에 구현되어 있으며, 시스템 콜과 실제 파일시스템 드라이버 사이의 객체 지향 인터페이스 계층이다. 각 파일시스템은 VFS가 정의한 함수 포인터 테이블을 구현해서 커널에 등록한다.
VFS의 네 가지 핵심 객체
VFS는 네 가지 객체로 파일 시스템 전체를 표현한다.
파일시스템 전체 s_type, s_root
s_op, s_blocksize
(이름 제외) i_ino, i_mode
i_size, i_op, i_fop
(이름 → inode) d_name, d_inode
d_parent, d_op
(프로세스·fd당) f_inode, f_pos
f_flags, f_op
superblock — 파일시스템의 닻
struct super_block은 마운트된 파일시스템의 전역 메타데이터를 담는다.
struct super_block {
dev_t s_dev; // 블록 디바이스 번호 (예: /dev/sda1)
unsigned long s_blocksize; // 블록 크기 (보통 4096 bytes)
struct file_system_type *s_type; // "ext4", "btrfs", "tmpfs", ...
struct dentry *s_root; // 이 파일시스템의 루트 dentry
const struct super_operations *s_op; // alloc_inode, write_inode, sync_fs, ...
struct list_head s_inodes; // 모든 inode 연결 리스트
};mount() 시스템 콜이 호출되면 커널은 해당 파일시스템 드라이버의 fill_super() 함수를 호출해 superblock을 초기화하고 루트 inode를 읽어온다.
inode — 파일의 정체성
inode는 파일 이름을 제외한 파일의 모든 메타데이터를 보유한다. 디스크 상의 inode(ext4의 ext4_inode, 160 bytes)와 메모리 상의 VFS inode(struct inode)가 별개로 존재하며, 커널이 디스크 inode를 읽어 메모리 inode를 채운다.
struct inode {
umode_t i_mode; // 파일 유형(S_IFREG, S_IFDIR...) + 권한(rwxrwxrwx)
kuid_t i_uid; // 소유자 UID
kgid_t i_gid; // 소유 그룹 GID
loff_t i_size; // 파일 크기 (bytes)
struct timespec64 i_atime; // 마지막 접근 시각
struct timespec64 i_mtime; // 마지막 내용 수정 시각
struct timespec64 i_ctime; // 마지막 메타데이터 변경 시각
unsigned long i_ino; // inode 번호 (파일시스템 내 고유)
unsigned int i_nlink; // 하드링크 수 (0이 되면 데이터 삭제)
const struct inode_operations *i_op; // create, lookup, mkdir, rename, ...
const struct file_operations *i_fop; // read, write, mmap, ioctl, ...
struct address_space *i_mapping; // 페이지 캐시 연결점
};하드링크는 동일한 inode를 가리키는 여러 dentry다. i_nlink 카운터가 0이 되어야(마지막 링크가 unlink()되어야) inode와 데이터 블록이 실제로 해제된다.
심볼릭 링크는 별도의 inode를 가진 일반 파일로, 내용이 대상 경로 문자열이다. S_IFLNK 타입으로 구분한다.
dentry — 경로와 inode 사이의 다리
dentry(directory entry)는 경로 컴포넌트(파일명 하나)와 inode를 연결한다. /etc/passwd는 세 개의 dentry로 구성된다: /, etc, passwd.
dentry는 디스크에 저장되지 않는다 — 완전히 메모리 내의 캐시다. dcache(dentry cache)는 커널의 주요 경로 조회 캐시로, LRU 방식으로 관리된다.
struct dentry {
struct qstr d_name; // 파일명 문자열 + 해시값
struct inode *d_inode; // 연결된 inode (NULL이면 negative dentry)
struct dentry *d_parent; // 부모 디렉토리 dentry
const struct dentry_operations *d_op;
struct hlist_bl_node d_hash; // dcache 해시 테이블에 연결
};Negative dentry: 존재하지 않는 경로(/usr/bin/nonexistent)도 캐시에 기록해 반복 조회 시 디스크 읽기를 건너뛴다. 대량의 "파일 없음" 조회가 있는 환경에서 성능에 중요하다.
Linux 2.6.38부터 dcache 조회는 RCU 기반 lock-free 패스를 사용한다. d_seq seqlock이 동시 수정을 감지하며, 일반적인 캐시 히트 경로에서 spinlock을 전혀 잡지 않는다.
file — 열린 파일의 상태
open() 시스템 콜은 struct file을 생성하고, 프로세스의 파일 디스크립터 테이블에 등록한 후 fd 번호를 반환한다. 같은 파일을 10번 열면 10개의 file 구조체가 만들어지지만 inode는 하나다.
struct file {
struct path f_path; // dentry와 마운트 정보
struct inode *f_inode; // 단축 참조 (f_path.dentry->d_inode)
loff_t f_pos; // 현재 읽기/쓰기 오프셋 (per-fd, 독립적)
unsigned int f_flags; // O_RDONLY, O_NONBLOCK, O_APPEND, ...
fmode_t f_mode; // FMODE_READ, FMODE_WRITE
const struct file_operations *f_op; // read, write, mmap, ioctl, ...
};f_pos는 file 구조체마다 독립적이므로, 같은 파일을 두 프로세스가 각각 열면 각자의 오프셋을 유지한다. 반면 dup()으로 복제된 fd들은 같은 file을 공유하므로 f_pos도 공유된다.
경로 탐색(pathname lookup): open()의 여정
open("/etc/passwd", O_RDONLY) 호출 시 커널의 do_filp_open() → link_path_walk() 경로:
- 루트 dentry(
/)에서 시작 (절대 경로) 또는 현재 작업 디렉토리 dentry(상대 경로). - 다음 컴포넌트(
etc)를 dcache 해시 테이블에서 RCU 방식으로 조회.
- 캐시 히트: dentry 즉시 반환 → inode 획득. - 캐시 미스: 파일시스템 드라이버의 inode_operations->lookup() 호출 → 디스크 읽기 → 새 dentry 생성 후 캐시에 추가.
- dentry의
d_inode->i_mode로 디렉토리인지 확인, 접근 권한 검사(MAY_EXEC). - 심볼릭 링크면 대상 경로로 재귀 탐색 (최대 40회).
passwd컴포넌트에 동일 과정 반복.- 최종 inode로
struct file생성 → 파일 디스크립터 번호 반환.
# 실시간 경로 탐색 추적 (strace)
strace -e trace=openat,stat ls /etc/passwd
# openat(AT_FDCWD, "/etc/passwd", O_RDONLY) = 3페이지 캐시: 읽기/쓰기 성능의 핵심
페이지 캐시(page cache)는 파일 데이터를 메모리에 캐싱하는 커널의 핵심 성능 요소다. read(), write() 모두 실제로는 페이지 캐시를 통해 이루어진다.
.readpage() → 블록 I/O
submit_bio() → 디스크 읽기
(수 ms) → 캐시 저장 후
유저로 반환
Dirty 표시 후 즉시 반환 → writeback 데몬
dirty 페이지 flush → 디스크
# 페이지 캐시가 얼마나 사용 중인지 확인
free -h
# total used free shared buff/cache available
# Mem: 31G 12G 2.1G 1.2G 17G 17G
# 페이지 캐시 강제 drop (성능 테스트 전 초기화용)
sync && echo 3 > /proc/sys/vm/drop_cachesext4 디스크 레이아웃 심화
ext4는 Linux에서 가장 널리 쓰이는 범용 파일시스템이다. 디스크를 블록 그룹(block group)으로 분할해 지역성을 높이고 단편화를 줄인다.
Sector
(1KB) Super
Block
(1KB) GDT
그룹 서술자
테이블
bitmap block
bitmap inode
table data
blocks
Extent Tree: 데이터 블록 매핑
ext4는 구식 indirect block 방식 대신 Extent Tree를 사용한다. extent는 (논리 블록 번호, 물리 블록 번호, 길이) 형태로 연속된 블록 범위를 하나의 항목으로 기술한다. 1GB 연속 파일을 단 하나의 extent로 표현할 수 있다.
# 파일의 extent 레이아웃 확인
filefrag -v /var/lib/mysql/data/bigfile.ibd
# Filesystem type is: ef53
# Extent 0: 0..2097151 → 131072..2228223 (8192 블록, 연속)ext4 저널링 (JBD2)
ext4는 갑작스러운 전원 차단 시 일관성을 보장하기 위해 저널(journal)을 사용한다. 저널 자체는 파일시스템 내 숨겨진 파일(inode 8)이며, 대개 파일시스템 가운데 위치한다.
| 저널 모드 | 동작 | 성능 | 일관성 |
|---|---|---|---|
writeback | 메타데이터만 저널, 데이터 순서 미보장 | 가장 빠름 | 가장 낮음 |
ordered | 메타데이터 저널 전 데이터 먼저 기록 (기본) | 중간 | 중간 |
journal | 메타데이터 + 데이터 모두 저널 | 가장 느림 | 가장 높음 |
# 마운트 옵션에서 저널 모드 확인
mount | grep ext4
# /dev/sda1 on / type ext4 (rw,relatime,data=ordered)inode_operations vs file_operations
VFS의 핵심은 함수 포인터 테이블이다. 파일시스템 드라이버는 이 테이블을 구현한다.
| 테이블 | 연결 위치 | 주요 함수 |
|---|---|---|
super_operations | superblock.s_op | alloc_inode, write_inode, sync_fs, put_super |
inode_operations | inode.i_op | create, lookup, mkdir, rename, readlink |
file_operations | inode.i_fop → file.f_op | read, write, mmap, ioctl, fsync, poll |
dentry_operations | dentry.d_op | d_revalidate, d_hash, d_compare |
address_space_operations | inode.i_mapping->a_ops | readpage, writepage, writepages, set_page_dirty |
ext4에서 일반 파일의 i_op는 ext4_file_inode_operations, i_fop는 ext4_file_operations로 설정된다.
// ext4가 VFS에 자신을 등록하는 방식 (단순화)
static struct file_system_type ext4_fs_type = {
.name = "ext4",
.mount = ext4_mount, // superblock 초기화
.kill_sb = kill_block_super,
};
// 파일 읽기 경로 (VFS -> ext4)
// vfs_read() -> file->f_op->read() -> ext4_file_read_iter()
// -> generic_file_read_iter() -> 페이지 캐시 조회
// -> ext4_readpage() -> submit_bio() -> 디스크다양한 파일시스템 유형
VFS 덕분에 성격이 완전히 다른 파일시스템들이 동일한 read()/write() 인터페이스로 공존한다.
| 파일시스템 | 유형 | 핵심 특징 |
|---|---|---|
| ext4 | 블록 기반, 저널링 | 안정성·범용성, 대부분의 Linux 루트 FS |
| btrfs | COW B-tree | 스냅샷, 서브볼륨, 인라인 체크섬, 압축 |
| XFS | 블록 기반, 저널링 | 대용량 파일·고병렬 I/O에 최적화 |
| tmpfs | RAM 기반 가상 | 재부팅 시 소멸, /tmp, /dev/shm, cgroup |
| procfs | 가상 (pseudo) | 커널 내부 상태 노출, /proc |
| sysfs | 가상 (pseudo) | 디바이스·드라이버 계층 구조, /sys |
| NFS | 네트워크 | 원격 파일시스템을 로컬처럼 마운트 |
# 마운트된 모든 파일시스템 목록
findmnt --output TARGET,SOURCE,FSTYPE,OPTIONS
# 파일의 inode 번호 및 메타데이터 확인
ls -i /etc/passwd # inode 번호 출력
stat /etc/passwd # 전체 메타데이터 출력
# dcache / inode 캐시 사용 현황
cat /proc/sys/fs/dentry-state
# 1234567 987654 45 0 0 0 (cached, unused, age_limit, ...)
cat /proc/sys/fs/inode-state
# 234567 12345 0 0 0 0 0 (inode 개수, unused, ...)실전: 파일 삭제의 내부 동작
rm /etc/test.conf 명령 실행 시 발생하는 일:
unlinkat()시스템 콜 → VFSvfs_unlink().- 부모 디렉토리 inode의
inode_operations->unlink()호출 (ext4:ext4_unlink()). - 디렉토리 항목(디렉토리 내 dentry 데이터)에서
test.conf삭제,i_nlink--. i_nlink == 0이고 해당 inode를 참조하는file구조체도 없으면 → 데이터 블록 해제, inode 번호 반환.- dcache에서 dentry 제거 (또는 negative로 전환).
열린 채로 삭제(delete-while-open): i_nlink가 0이 되어도 file 구조체가 살아있으면 데이터는 유지된다. 마지막 close()가 호출될 때 비로소 해제된다. 이 특성을 활용해 임시 파일을 만들자마자 unlink()하는 패턴이 있다 — 프로세스가 죽어도 파일이 남지 않는다.
References
- Overview of the Linux Virtual File System — The Linux Kernel documentation
- VFS Objects: superblock, inode, dentry, file — Linux Kernel Internals
- Linux Kernel 2.4 Internals: Virtual Filesystem (VFS) — TLDP
- Kernel Subsystems: Virtual File Sys (VFS) & Device Drivers — CircuitLabs
- File management and VFS — MIMUW
- Understanding Ext4 Disk Layout, Part 1 — Oracle Blogs
- Ext4 Filesystem: data blocks, super blocks, inode structure — Adil, Medium
- An introduction to Linux's EXT4 filesystem — Opensource.com
- Filesystem Management — Linux Kernel Labs documentation