리눅스 커널의 이해
12

ch12. virtual filesystem

linuxfilesystemkernel
VIRTUAL FILESYSTEM · CH.12
LECTURE NOTE — LINUX KERNEL, CHAPTER 12

VFS,
파일시스템 번역기

cp /floppy/TEST /tmp/test 명령을 실행하면 MS-DOS와 Ext2라는 서로 다른 두 파일시스템을 동시에 사용하게 됩니다. 하지만 정작 cp 프로그램 자체는 둘의 차이를 알지 못하죠. 애플리케이션과 파일시스템 사이에서 이를 매끄럽게 번역해 주는 커널 계층이 바로 VFS입니다.

Application: cp, cat, ls, vim …
VFS — Common File Model (superblock · inode · file · dentry)
Ext2 / Ext3
FAT / NTFS
NFS / CIFS
/proc / sysfs
사용자: 하나의 파일 인터페이스 VFS: 번역 · 분기 · 캐시 각 파일시스템: 실제 구현
01

큰 그림: VFS는 "파일시스템 번역기"다Figure 12-1

cp /floppy/TEST /tmp/test 명령어를 실행할 때, cp 프로그램은 원본이 MS-DOS 기반이고 대상이 Ext2라는 사실을 인지하고 있을까요?

그렇지 않습니다. VFS(Virtual Filesystem)는 사용자 애플리케이션과 실제 파일시스템 구현부 사이에 위치한 커널 계층입니다. 프로그램은 그저 open, read, write, close 같은 익숙한 시스템 콜만 호출하면 됩니다. 커널 안의 VFS가 알아서 "이 파일은 MS-DOS용 read 함수로, 저 파일은 Ext2용 write 함수로" 적절히 연결해 줍니다.

📋
superblock
마운트된 파일시스템 전체의 대표 객체
🪪
inode
파일의 고유한 신분증 — 본질적 정체성
📂
file
프로세스가 파일을 열고 상호작용하는 세션 기록
📇
dentry
문자열 경로 이름과 inode를 연결하는 매개체

식당의 메뉴판이 VFS와 같습니다. 손님(애플리케이션)은 '주문'이라는 공통된 인터페이스만 사용합니다. 주방(파일시스템)은 들어온 주문에 맞춰 각자의 조리법(구현 함수)을 실행하죠. 손님은 주방 시스템이 한식 스타일인지 양식 스타일인지 신경 쓸 필요가 없습니다.

핵심 O/X 퀴즈

1. VFS 덕분에 cp와 같은 프로그램은 원본 및 대상 파일의 실제 파일시스템 종류를 알 필요가 없다.
OVFS가 파일시스템 간의 복잡한 차이를 추상화하여 숨겨줍니다.
2. VFS는 사용자 공간(User Space) 라이브러리이며 커널 내부와는 관련이 없다.
XVFS는 리눅스 커널 내부에 존재하는 강력한 추상화 계층입니다.
3. 이번 장에서 다루는 VFS의 4대 핵심 객체는 superblock, inode, file, dentry이다.
O이 네 가지 주요 객체가 VFS의 공통 파일 모델을 구성합니다.
02

VFS의 역할과 파일시스템의 세 부류FILESYSTEM TYPES

리눅스가 강력한 이유 중 하나는 "구조가 전혀 다른 디스크나 네트워크 상의 파일조차 마치 내 로컬 디스크의 파일처럼 자연스럽게 다룰 수 있다"는 점입니다. 이를 위해 VFS는 크게 세 부류의 파일시스템을 지원합니다.

세 가지 파일시스템 부류
Disk-based

로컬 저장장치의 물리적 디스크 블록을 관리합니다. Ext2/Ext3, ReiserFS, FAT, VFAT, NTFS, ISO9660, UDF, HFS, JFS, XFS 등.

Network

네트워크 너머에 있는 파일을 마치 로컬 파일처럼 접근할 수 있게 해줍니다. NFS, Coda, AFS, CIFS, NCP 등.

Special

물리적 디스크 공간을 관리하는 대신, 커널 내부의 자료구조를 파일 형태의 인터페이스로 사용자에게 노출합니다. /proc, sysfs, tmpfs, devpts, pipefs, sockfs 등.

마운트(Mount)는 단순히 폴더를 연결하는 것이 아니라, "이 지점부터는 다른 파일시스템의 루트 트리를 보여주겠다"는 의미의 '트리 교체' 작업입니다. 따라서 어떤 파일시스템을 특정 디렉터리에 마운트하면 그 디렉터리의 원래 내용은 잠시 가려지고, 마운트를 해제(unmount)하면 다시 나타나게 됩니다.

핵심 O/X 퀴즈

1. VFS가 다루는 파일시스템은 크게 디스크 기반, 네트워크, 특수 파일시스템으로 나눌 수 있다.
O이 세 부류가 VFS 아래에서 통일된 인터페이스로 조화롭게 동작합니다.
2. /proc는 일반적으로 물리적인 디스크 블록을 관리하는 전통적인 디스크 기반 파일시스템이다.
X/proc는 메모리 상의 커널 자료구조를 파일처럼 보여주는 특수 파일시스템(Special Filesystem)입니다.
3. 특정 파일시스템을 디렉터리에 마운트하면 그 디렉터리의 원래 내용은 마운트가 유지되는 동안 가려질 수 있다.
O언마운트하면 가려졌던 원래 내용이 다시 정상적으로 보입니다.
03

Common File Model: 공통 인터페이스로 추상화하기Figure 12-2 / 함수 포인터 기반 다형성

Ext2와 FAT은 내부 구조가 완전히 다릅니다. 하지만 커널은 Common File Model을 도입해 모든 파일시스템이 전통적인 Unix 파일 모델처럼 보이도록 추상화합니다. 심지어 FAT처럼 Unix식 디렉터리 개념이 없는 파일시스템조차도, VFS가 기대하는 형태로 메모리 상에서 구조를 동적으로 만들어냅니다.

함수 포인터 기반 디스패치: 커널은 read() 시스템 콜을 특정 파일시스템의 함수로 하드코딩하지 않습니다. 대신 file->f_op->read(...)와 같은 형태로 추상화하여 호출합니다. 이렇게 하면 MS-DOS 파일은 MS-DOS용 코드가, Ext2 파일은 Ext2용 코드가 자동으로 실행됩니다. C 언어의 구조체와 함수 포인터 테이블을 결합하여 강력한 객체지향적 다형성을 구현한 것입니다.

Figure 12-2 — 세 프로세스가 같은 파일을 열었을 때의 객체 관계
file 객체
프로세스마다 개별적으로 생성됩니다. 각 프로세스가 파일을 읽고 있는 현재 오프셋(f_pos)은 독립적으로 유지됩니다.
dentry
하나의 파일(inode)에 여러 개의 이름(hard link)이 연결될 수 있습니다.
inode
실제 파일의 고유한 정체성입니다. 단일 파일시스템의 superblock과 연결됩니다.
핵심 포인트
파일의 이름(dentry), 열린 파일의 세션(file), 실제 파일의 정체성(inode)은 각각 철저히 분리된 객체로 관리됩니다.

핵심 O/X 퀴즈

1. VFS의 Common File Model은 구조가 다른 여러 파일시스템을 공통된 Unix식 파일 모델로 표현하려는 장치이다.
O파일시스템마다 상이한 구현을 하나의 일관된 인터페이스로 감싸줍니다.
2. 커널은 read() 요청이 들어오면 항상 Ext2 전용 함수로 하드코딩하여 I/O 처리를 수행한다.
Xfile->f_op->read() 형태로 동적 연결하여, 대상 파일시스템에 맞는 구현 함수를 호출합니다.
3. 하나의 동일한 inode 객체를 여러 dentry 객체가 동시에 가리킬 수 있으며, 이는 hard link 메커니즘과 관련이 있다.
Ohard link는 서로 다른 여러 경로 이름(dentry)이 단 하나의 실체(inode)를 공유하는 구조입니다.
04

VFS 시스템 콜과 캐시 메커니즘Table 12-1 / Dentry Cache

VFS가 관장하는 영역은 단순히 open, read, write에 그치지 않습니다. 마운트/언마운트, 파일시스템 통계 조회, 디렉터리 조작(생성·삭제), 심볼릭 링크 처리, 권한 변경, 파일 잠금(file lock), 메모리 매핑(mmap), 비동기 I/O, 확장 속성(extended attribute) 조작까지 파일과 관련된 거의 모든 시스템 콜을 포괄합니다.

VFS 단독 처리 vs 하위 파일시스템으로 위임
직접 처리
lseek() — 메모리 내 file 객체의 f_pos(현재 읽기 위치) 필드만 수정하면 되므로 즉시 처리됩니다. close() — file 객체의 참조 카운트를 줄이고 자원을 정돈하는 커널 레벨의 작업이 주를 이룹니다.
하위로 분기
read(), write(), mkdir() 등 — 실제 디스크의 데이터나 메타데이터 변경이 수반되어야 하므로 하위 파일시스템의 구체적인 구현부로 제어권을 넘깁니다.

Dentry Cache는 커널이 유지하는 "최근 방문 경로 단기 기억장치"입니다. tmp라는 이름이 디렉터리 내에서 어떤 inode를 가리키는지 한 번 찾은 후 메모리에 기억해 두면, 다음번 탐색 속도가 비약적으로 빨라집니다. 이는 CPU의 하드웨어 캐시와는 구분되는 소프트웨어 기반의 디스크 캐시로, 느린 디스크 조회를 최소화하기 위해 고안되었습니다.

핵심 O/X 퀴즈

1. VFS는 open, read, write뿐 아니라 mount, pathname 관련 호출, file lock, mmap 등 광범위한 시스템 콜을 총괄한다.
OTable 12-1에 명시된 것처럼 VFS의 책임 범위는 매우 넓습니다.
2. lseek() 시스템 콜이 호출되면 커널은 항상 디스크의 파일 내용을 직접 수정하는 물리적 작업을 수행해야 한다.
Xlseek()는 물리적 디스크 조작 없이 메모리 상의 file 객체에 있는 f_pos 필드 값만 가볍게 변경합니다.
3. Dentry Cache는 복잡한 경로 이름(pathname)을 실제 inode로 치환하는 과정을 쾌속화하는 데 결정적인 도움을 준다.
O최근에 탐색을 마친 '이름-to-inode' 매핑 정보를 RAM에 안전하게 보관합니다.
05

Superblock 객체: 마운트된 파일시스템의 총괄 대표자s_op / s_fs_info / dirty 플래그

Superblock은 개별 파일이 아닌, '마운트된 파일시스템 전체'의 상태와 메타데이터를 대변하는 핵심 객체입니다. 파일시스템 전체를 관장하는 주민등록부와 같은 역할을 합니다.

Superblock 주요 필드
s_type
파일시스템 타입 포인터 (예: ext2, proc 등).
s_op
Superblock operation table — alloc_inode, read_inode, write_inode, sync_fs, statfs 등 파일시스템 전체 단위의 연산을 정의합니다.
s_root
이 특정 파일시스템의 최상위 루트 디렉터리에 해당하는 dentry 객체.
s_fs_info
파일시스템별 고유 정보. VFS의 공통 모델과는 무관한, 각 파일시스템만의 특수한 데이터(예: Ext2의 블록 비트맵)를 저장합니다.
s_dirt
Dirty flag — 메모리 상의 메타데이터가 변경되어 추후 실제 디스크에 동기화(반영)해야 할 내용이 있음을 커널에 알리는 표시입니다.
리스트 연결
전체 마운트된 superblock을 엮는 전역 리스트 및 파일시스템 타입별 fs_supers 리스트에 포함됩니다.

왜 superblock operation(s_op)에는 get_super 메서드가 없을까요? Superblock 객체 자체가 아직 메모리에 생성되지 않은 시점에서는 해당 객체에 속한 메서드를 호출할 방도가 없기 때문입니다. 따라서 슈퍼블록을 메모리에 할당하고 디스크에서 읽어와 초기화하는 역할은 뒤에서 설명할 file_system_type 구조체의 get_sb 메서드가 전담하게 됩니다.

핵심 O/X 퀴즈

1. Superblock 객체는 마운트된 파일시스템 전체의 총체적인 상태와 구조 정보를 저장하는 역할을 한다.
O단일 파일 단위가 아닌 파일시스템이라는 거대한 컨테이너 전체를 대변하는 객체입니다.
2. s_fs_info 필드에는 구조가 서로 다른 모든 파일시스템들이 완전히 동일하게 공유하는 범용적인 정보만 저장된다.
X오히려 VFS 추상화로 담아내기 어려운 각 파일시스템만의 고유한 세부 정보(예: 비트맵, 블록 그룹 디스크립터 등)가 저장됩니다.
3. Superblock의 dirty 플래그(s_dirt)가 설정되어 있다면, 이는 커널 메모리와 실제 디스크 상의 슈퍼블록 정보 사이에 불일치가 발생해 추후 동기화가 필요하다는 뜻이다.
O아직 디스크에 반영되지 않은 메모리 상의 변경점(Dirty state)이 남아있음을 명시합니다.
06

Inode 객체: 파일의 변하지 않는 진짜 신분증i_op / Inode 상태 / 관리 리스트

파일의 이름은 언제든 유연하게 바뀔 수 있지만, inode는 파일의 본질적인 정체성을 나타냅니다. 파일시스템 내부에서 특정 파일을 유일하게 추적하고 식별하는 절대적인 기준은 파일 이름이 아닌 이 inode 객체입니다.

Inode 주요 필드
i_ino
Inode 번호 — 특정 파일시스템 내에서 부여되는 유일무이한 식별자입니다.
i_count
해당 객체가 현재 얼마나 많은 곳에서 참조되고 있는지 나타내는 레퍼런스 카운터.
i_mode
파일의 기본 타입(일반, 디렉터리, 소켓 등)과 권한(rwx) 정보.
i_nlink
이 inode를 참조하고 있는 Hard link의 총개수.
i_size
파일의 실제 크기 (바이트 단위).
i_atime / i_mtime / i_ctime
파일 접근(Access) / 내용 수정(Modify) / inode 메타데이터 변경(Change) 시각 기록.
i_op
Inode operation table — create, lookup, link, unlink, mkdir, rename, readlink 등 파일 및 디렉터리 이름 공간을 조작하는 함수들 모음.
i_fop
이 파일이 제공하는 기본 file operation table. 파일이 open 될 때 file 객체의 f_op 필드로 고스란히 복사됩니다.

모든 inode는 현재 상태에 따라 다음 세 가지 리스트 중 하나에 반드시 속하게 됩니다. 현재 프로세스에 의해 활발히 쓰이고 있는 in-use 리스트, 데이터는 유효하지만 지금 당장은 쓰이지 않는 valid unused 리스트(효율적인 캐시 역할 수행), 그리고 메타데이터가 변경되어 디스크로의 쓰기 작업이 예약된 dirty 리스트입니다. 더불어 고속 검색을 지원하기 위해 'inode 번호와 소속 superblock 주소'를 결합한 값을 키(Key)로 삼아 전역 해시 테이블에도 꼼꼼히 등록됩니다.

핵심 O/X 퀴즈

1. 파일 이름은 변경되거나 여러 개 존재할 수 있지만, inode는 해당 파일이 존재하는 내내 파일 자체를 식별하는 핵심 메타데이터 구조체다.
O이름표(dentry)는 쉽게 갈아끼울 수 있지만, 내용물의 본질(inode)은 확고히 유지됩니다.
2. Inode 객체는 개별 프로세스가 파일을 어디까지 읽었는지 나타내는 현재 읽기 위치(offset)를 저장하는 것이 주된 역할이다.
X읽기 위치(f_pos)는 각 프로세스의 독립적 상태이므로 file 객체에 보관됩니다. Inode는 파일 자체의 공통 메타데이터를 담습니다.
3. Inode operation(i_op)에는 lookup, link, unlink, mkdir, rename과 같이 디렉터리 내부의 이름 공간을 조작하는 동작들이 포함된다.
O디렉터리 항목을 추가하거나 지우는 등 이름 공간 레벨의 조작이 i_op를 통해 유연하게 이루어집니다.
07

File 객체: 프로세스와 파일 사이의 '열린 세션'f_pos / f_op / filp slab cache

File 객체는 디스크 상의 파일 데이터 그 자체를 의미하지 않습니다. 특정 프로세스가 대상 파일을 열었을 때 메모리에 동적으로 생성되는 '상호작용 세션(Session)' 객체입니다. 파일이 열릴(open) 때만 일시적으로 만들어지며, 디스크에는 이에 직접 대응하는 데이터 블록 이미지가 전혀 존재하지 않습니다.

File 객체 주요 필드
f_pos
현재 파일 내의 오프셋(읽기/쓰기 커서 위치). 같은 파일을 열었더라도 프로세스마다 각자의 오프셋이 다르기 때문에 file 객체에 위치합니다.
f_dentry
현재 세션과 연결된 dentry 객체.
f_vfsmnt
해당 파일이 포함된 구체적인 마운트 지점 정보.
f_op
File operation table. open 시점에 대상 inode의 i_fop로부터 고스란히 복사됩니다. 실제 I/O를 수행하는 llseek, read, write, mmap, poll, ioctl, fsync, lock 등의 함수 포인터를 품고 있습니다.
f_count
객체 참조 카운터. fork()나 dup() 시스템 콜을 호출하면 다수의 파일 디스크립터(fd)가 단일 file 객체를 가리키게 되어 이 카운터가 증가합니다.
f_flags / f_mode
파일을 열 때 지정한 접근 플래그(O_RDONLY 등)와 모드 정보.

inode가 "파일 자체의 변하지 않는 신분증"이라면, file 객체는 "파일을 열고 이용 중인 손님의 임시 이용권"에 비유할 수 있습니다. 손님마다 발급받은 이용권이 다르므로, 현재 읽고 있는 페이지(f_pos)도 제각각이고 접근 권한 모드(읽기 전용, 쓰기 전용 등)도 다릅니다. 이 임시 이용권은 손님이 작업을 모두 마치고 나갈 때(close) 커널에 안전하게 반납 및 파기됩니다.

File 객체는 속도 향상을 위해 filp라는 전용 슬랩(slab) 캐시를 통해 할당됩니다. VFS 계층이 파일을 새로 열어야 할 때 get_empty_filp() 함수를 호출하여 깨끗한 객체를 하나 얻어내고, 그 안에 inode의 i_fop 테이블을 복사해 넣는 식으로 초기화를 진행합니다.

핵심 O/X 퀴즈

1. File 객체는 파일이 open 될 때 동적으로 만들어지며, 현재 열려 있는 파일과 프로세스 사이의 세션 상태 정보를 담아낸다.
O디스크 상에는 존재하지 않고 오직 메모리 상에서만 관리되는 순수 커널 메모리 객체입니다.
2. File 객체의 f_pos 값은 모든 시스템 프로세스가 단일하게 공유하는 고정 속성이므로 통일성을 위해 inode에 영구 저장된다.
Xf_pos는 엄연히 각 프로세스별 독립적인 세션 상태이므로 inode가 아닌 file 객체 내부에 자리합니다.
3. File 객체 내부의 f_op 필드는 향후 호출될 read/write 같은 작업들을 대상 파일시스템의 실제 구현 함수로 정확히 연결하는 라우팅 역할을 한다.
Ofile->f_op->read()와 같은 함수 포인터 호출을 통해 VFS의 추상화가 완성됩니다.
08

Dentry 객체와 Dentry Cache: 경로 이름을 빠르게 찾는 장치Dentry 4가지 상태 / LRU 캐시

Dentry(Directory Entry)는 "특정 디렉터리 내의 문자열 파일 이름과 그에 상응하는 실제 inode를 하나로 묶어 연결"해 주는 매개체입니다. 만약 /tmp/test라는 경로를 탐색한다면, /, tmp, test 각각에 대한 dentry 객체가 계층적으로 다뤄집니다. 이는 디스크에 직접 저장되는 데이터 구조가 아니라, 빠른 경로 해석 성능을 달성하기 위해 VFS가 메모리 상에 동적으로 찍어내는 논리 객체입니다.

free
유효 정보 없음
unused
유효하지만 미사용 (캐시)
in use
사용 중 — 해제 불가
negative
inode 없음 — "없음" 자체를 캐시

Negative dentry 구조의 묘미: 존재하지 않는 잘못된 파일 경로를 조회했을 때, 커널은 번거롭게 디스크를 뒤진 끝에 "그런 파일은 없다"는 결론을 냅니다. 놀랍게도 커널은 이 결론 자체를 negative dentry 형태로 캐싱해 둡니다. 덕분에 이후 동일한 오타 경로에 반복적으로 접근하더라도 매번 값비싼 디스크 I/O를 발생시키지 않고 메모리 선에서 신속히 에러를 반환할 수 있습니다.

현재 쓰이지 않는 unused dentry들은 LRU(Least Recently Used) 리스트로 정렬되어 관리됩니다. 메모리 압박이 심해져 캐시를 덜어내야 할 때면 가장 오랫동안 쓰이지 않은 꼬리 부분부터 가지치기가 이루어집니다. 중요한 점은, unused dentry가 특정 inode를 쥐고 참조하고 있다면 해당 inode 역시 메모리에 계속 살아남아 캐시 히트율을 높인다는 것입니다. 다시 말해, dentry cache가 inode cache의 수명을 튼튼하게 연장해 주는 앵커(Anchor) 역할을 수행합니다.

핵심 O/X 퀴즈

1. Dentry 객체는 긴 경로(pathname)를 구성하는 개별 문자열 요소와 해당 파일 inode 사이의 연결 고리를 논리적으로 표현한다.
O경로를 쪼갠 각 구성 요소(Component)마다 별도의 dentry 객체가 동적으로 생성될 수 있습니다.
2. Negative dentry란 명칭 그대로 항상 음수로 표현된 inode 번호를 지니고 있는 특수한 dentry를 의미한다.
XNegative dentry는 연결된 실제 inode가 없는(NULL) 상태를 뜻하며, 파일이 존재하지 않는다는 사실 자체를 캐싱할 때 쓰입니다.
3. Dentry Cache는 최근 해석을 마친 이름-to-inode 매핑 결과를 메모리에 보관함으로써 차후의 Pathname lookup 속도를 극대화한다.
OLRU 기반 리스트로 촘촘히 관리되며, 부가적으로 Inode cache까지 함께 유지시키는 강력한 성능 향상 기법입니다.
09

fs_struct, files_struct, 그리고 fd 배열Figure 12-3 / fget · fput 시스템

활성화된 프로세스가 파일 시스템과 원활하게 상호작용하려면 반드시 두 가지 핵심 상태를 관리해야 합니다. 첫째는 "내가 현재 시스템의 어느 디렉터리에 위치해 있는가(fs_struct)"이고, 둘째는 "내가 현재 어떤 파일들을 열어서 사용하고 있는가(files_struct)"입니다.

fs_struct vs files_struct
fs_struct
프로세스의 논리적 루트(root dentry/vfsmnt)와 현재 작업 중인 디렉터리(current working directory dentry/vfsmnt), 그리고 umask 값을 담습니다. chroot() 시스템 콜을 이용하면 이 논리적 루트를 동적으로 바꿀 수 있습니다.
files_struct
프로세스가 현재 열고 있는 파일들의 목록을 철저히 관리합니다. 이 구조체의 핵심은 fd 배열로, 인덱스는 파일 디스크립터(정수)이고 저장된 값은 메모리 상의 실제 file 객체 포인터입니다.
fd 0
stdin
fd 1
stdout
fd 2
stderr
fd 3
file*
fd 4
file*
fd 5
NULL
fd 6
NULL

파일 디스크립터(fd)는 그저 평범한 정수 인덱스에 불과합니다. 코드 레벨에서 current->files->fd[fd] 형태로 접근하여 커널 메모리 어딘가에 있는 file 객체의 주소를 찾아내는 '키(Key)' 역할을 할 뿐입니다. dup()dup2()를 호출하면 배열의 여러 인덱스 칸이 단 하나의 동일한 file 객체 포인터를 가리키게 복제할 수 있습니다. 커널 내부 함수인 fget()은 인덱스를 통해 file 객체를 안전하게 획득하고 참조 카운터(f_count)를 늘립니다. 반대로 조작이 끝난 후 호출되는 fput()은 카운터를 조용히 줄이고, 그 값이 0으로 떨어지면 객체 자체를 말끔히 정리합니다.

핵심 O/X 퀴즈

1. 파일 디스크립터(fd)는 files_struct 내부의 fd 배열에 접근하기 위한 인덱스 숫자이며, 궁극적으로 file 객체를 찾는 데 쓰인다.
Ofd는 해당 배열을 조회하기 위한 가벼운 정수 인덱스입니다.
2. 전통적인 Unix 환경에서 fd 0, 1, 2는 각각 표준 입력(stdin), 표준 출력(stdout), 표준 에러(stderr) 채널과 암묵적으로 연결된다.
O운영체제가 프로세스 생성 시 관례적으로 열어두는 표준 I/O 스트림입니다.
3. dup2() 시스템 콜을 통해 생성된 두 개의 파일 디스크립터는 독립성을 위해 반드시 서로 다른 file 객체를 가리키도록 설계되었다.
Xdup2()의 존재 목적 자체가 두 개의 서로 다른 fd 번호가 단일한 file 객체를 똑같이 가리키게끔 복제하는 데 있습니다.
10

특수 파일시스템과 타입 등록 과정file_system_type / register_filesystem()

특수 파일시스템(Special Filesystem)은 물리적 디스크의 블록을 관리하는 대신, 커널 내부의 역동적인 자료구조나 상태 정보를 친숙한 파일 읽기/쓰기 인터페이스 형태로 래핑하여 사용자에게 제공합니다. 이는 물리적인 블록 디바이스(하드디스크 파티션 등)에 귀속되지 않기 때문에, 커널 측으로부터 Major 번호 0과 파일시스템만의 고유한 Minor 번호로 구성된 가상의 블록 디바이스 식별자를 부여받고 동작을 시작합니다.

file_system_type 구조체 분석
name
파일시스템의 대표 문자열 이름 ("ext2", "proc", "tmpfs" 등).
fs_flags
해당 파일시스템 고유의 특성을 명시하는 타입 플래그 묶음.
get_sb
새로운 마운트 과정에서 해당 파일시스템의 superblock 객체를 신규 할당하거나 초기화하는 핵심 메서드. 앞서 보았던 superblock operation(s_op) 테이블에 관련 함수가 없었던 진짜 이유가 바로 이 객체의 존재 때문입니다.
kill_sb
더 이상 쓰이지 않는 superblock을 안전하게 해제 및 제거합니다.
fs_supers
시스템 상에 존재하는 동일한 타입의 superblock들을 한데 엮어 관리하는 리스트.
1
register_filesystem() 실행. 시스템 부팅 시나 커널 모듈 적재(Load) 시 자신의 file_system_type 구조체를 전역 단일 연결 리스트에 당당히 등록합니다.
2
mount 요청 발생. 마운트 명령이 떨어지면, 커널의 get_fs_type() 함수가 전달된 이름 문자열로 리스트를 스캔하여 해당하는 file_system_type 구조체를 색출해냅니다.
3
get_sb 메서드 호출. 찾아낸 구조체 내부의 get_sb 함수 포인터를 전격 호출하여 슈퍼블록을 할당받고 빈틈없이 초기화합니다.
4
unregister_filesystem() 수행. 모듈이 언로드(Unload)되어 메모리에서 내려갈 때 커널 리스트에서 자신의 등록 정보를 조용히 말소합니다.

핵심 O/X 퀴즈

1. /proc, sysfs, tmpfs, pipefs 등은 모두 전형적인 특수 파일시스템(Special Filesystem)의 훌륭한 예시로 볼 수 있다.
O디스크 공간을 직접 점유하지 않고 특정한 시스템 목적을 위해 파일 인터페이스만 제공하는 녀석들입니다.
2. 모든 종류의 특수 파일시스템은 그 특성상 반드시 실제 물리 디스크 파티션 위에 굳건히 자리 잡아야만 작동할 수 있다.
X물리 매체가 없기 때문에 커널이 부여하는 가상의(Dummy) 블록 디바이스 식별자에 의존하여 동작합니다.
3. file_system_type 구조체 내부의 get_sb는 마운트 절차 진행 시 대상 파일시스템의 superblock 객체를 신규 생성하거나 초기화하는 무거운 역할을 짊어진다.
OSuperblock 객체조차 없는 극초기 시점에 호출되어야 하므로 s_op가 아닌 개별 타입 구조체 레벨에 자리하고 있습니다.
11

Mount, Namespace, 그리고 vfsmountCLONE_NEWNS / pivot_root의 세계

Namespace'특정 프로세스의 관점에서 바라보는 마운트된 파일시스템들의 전체 트리 구조'를 뜻합니다. 리눅스 시스템에 존재하는 대부분의 프로세스는 1번 프로세스(init)의 장대한 namespace를 고스란히 복제하여 공유합니다. 하지만 clone(CLONE_NEWNS) 시스템 콜을 통해 프로세스 전용의 독립적인 namespace 우주를 새롭게 창조하면, 그 격리된 공간 안에서 수행된 마운트나 언마운트 작업은 오직 동일한 namespace를 공유하는 그룹 내에서만 유효하게 보이며 바깥 세상에는 전혀 영향을 미치지 않습니다.

vfsmount 주요 구조체 필드
mnt_parent
현재 자신이 마운트된 부모 파일시스템 측의 vfsmount 객체 포인터.
mnt_mountpoint
부모 파일시스템 트리 내에서 현재 자신이 접붙여진 구체적인 마운트 포인트(Mount point) dentry.
mnt_root
현재 접붙여진 파일시스템 관점에서의 가장 꼭대기, 즉 최상위 루트 dentry.
mnt_sb
현재 파일시스템을 대변하는 든든한 superblock 객체 포인터.
mnt_flags
보안 강화를 위한 MNT_NOSUID, MNT_NODEV, MNT_NOEXEC 등의 마운트 옵션 플래그 모음.
리스트 연결망
시스템 고속 마운트 해시 테이블 + 소속 namespace의 로컬 리스트 + 얽히고설킨 부모-자식 간 마운트 관계 트리 리스트.

Superblock과 vfsmount의 결정적 차이: Superblock은 파일시스템 그 자체의 고유한 본질과 내재된 메타데이터를 대표하는 반면, vfsmount는 "해당 파일시스템이 현재 작동 중인 시스템 트리의 어느 위치(경로)에 정확히 접목되어 있는가"라는 위상학적 상태를 대표합니다. 따라서 하나의 동일한 파일시스템 디바이스를 시스템 트리의 여러 다른 경로에 중복 마운트할 경우, 묵직한 superblock 객체는 단 하나만 메모리에 적재되고, 이를 가볍게 참조하는 여러 개의 vfsmount 객체들이 각기 다른 마운트 포인트를 책임지게 됩니다.

핵심 O/X 퀴즈

1. Namespace란 다름 아닌 특정 프로세스가 시각적으로 인식하는 마운트된 파일시스템 트리의 전체적인 구조와 모양새를 의미한다.
OCLONE_NEWNS 플래그를 활용해 프로세스별로 완전히 격리되고 독립된 자기만의 namespace를 구축할 수 있습니다.
2. 리눅스 커널 정책상 동일한 파일시스템을 시스템 트리의 여러 마운트 포인트에 중복해서 연결하는 행위는 엄격히 금지되며 항상 한 번만 허용된다.
X동일한 파일시스템을 여러 경로에 얼마든지 다중 마운트할 수 있습니다. 이 경우 단일 슈퍼블록을 다수의 vfsmount가 지혜롭게 공유하는 형태가 됩니다.
3. vfsmount 객체는 구체적인 마운트 포인트, 루트 dentry 위치, 연결된 superblock 포인터, 그리고 복잡한 부모-자식 마운트 관계 트리 등을 꼼꼼하게 추적하고 관리한다.
O마운트된 '지점(경로)'과 '파일시스템의 물리적 실체(데이터)'를 논리적으로 결합해 주는 접착제 역할을 탁월하게 수행합니다.
12

Mount와 Unmount의 작동 흐름: 터미널 명령에서 Superblock 초기화까지do_kern_mount() / 영원히 가려진 rootfs

터미널에 mount 명령어를 무심코 입력하면, 내부적으로 mount() 시스템 콜이 트리거되고, sys_mount()를 거쳐 거대한 마운트 로직의 본체인 do_mount()로 깊숙이 진입하게 됩니다.

1
do_mount() 진입 및 분기. 전달받은 마운트 플래그에 따라 작업 경로가 쩍 갈라집니다. MS_REMOUNT 옵션이라면 단순 마운트 옵션만 변경하고, MS_BIND라면 바인드 마운트를 처리하며, MS_MOVE라면 마운트 포인트를 통째로 이사시킵니다. 이도 저도 아닌 일반 마운트라면 do_new_mount()로 제어권을 넘깁니다.
2
do_new_mount() → do_kern_mount() 콤보. 내부적으로 get_fs_type()을 호출해 알맞은 file_system_type 구조체를 찾아낸 뒤, alloc_vfsmnt()로 비어있는 vfsmount 틀을 찍어내고, get_sb()를 발동시켜 본격적인 슈퍼블록 할당 및 초기화 레이스에 돌입합니다.
3
get_sb_bdev()의 은밀한 디스크 접근 (디스크 기반 FS 한정). 물리적 block device를 조심스레 열고, sget() 함수로 혹시나 기존에 메모리에 올라와 있는 동일 슈퍼블록이 있는지 캐시를 뒤집니다. 만약 이번이 완전 처음이라면 fill_super() 함수가 출동해 디스크의 은밀한 곳에서 실제 슈퍼블록 구조를 긁어와 메모리에 예쁘게 채워 넣습니다.
4
do_add_mount() → graft_tree() 피날레. 깔끔하게 준비를 마친 vfsmount 객체를 프로세스의 namespace 리스트에 등록하고, 고속 검색 해시 테이블에 끼워 넣은 뒤, 부모 vfsmount의 자식 리스트에 단단히 묶어주는 것으로 기나긴 마운트 여정을 성공적으로 마무리합니다.

Root Filesystem 마운트의 숨겨진 진실: 시스템 부팅 시 커널은 사실 가장 먼저 메모리 기반의 텅 빈 파일시스템인 rootfs를 무조건 마운트합니다. 그 이후 초기화에 필요한 각종 디바이스 드라이버 로딩을 마치면 비로소 실제 디스크 기반의 진짜 Root 파일시스템을 이 rootfs 위에 덮어씌우듯 '오버마운트(Over-mount)' 해버립니다. 처음에 마운트된 가여운 rootfs는 시스템이 꺼질 때까지 언마운트되지 못한 채, 진짜 Root 파일시스템 구조 아래에 영구적으로 어둡게 가려진 채 존재하게 됩니다.

핵심 O/X 퀴즈

1. 일반적인 신규 마운트 절차 진행 시, do_kern_mount() 함수가 알맞은 파일시스템 타입을 물색하고 필수적인 superblock과 vfsmount 객체를 한 쌍으로 빈틈없이 준비해 낸다.
Oget_fs_type → alloc_vfsmnt → get_sb로 이어지는 물 흐르듯 자연스러운 삼단 콤보 시퀀스입니다.
2. 시스템 부팅 시 Root 파일시스템은 어떠한 사전 준비 단계도 없이 다짜고짜 실제 물리 디스크 Root를 마운트해 버리며, rootfs와 같은 중간 특수 단계는 존재하지 않는다.
X커널은 언제나 초기에 가벼운 빈 rootfs를 먼저 띄운 뒤, 그 위에 실제 Root 디스크를 덮어 마운트하는 정교한 투 스텝(Two-step) 전략을 취합니다.
3. 마운트 해제(unmount) 과정에서는 지목된 대상이 올바른 마운트 포인트가 맞는지, 호출한 프로세스의 namespace 구역 내에 얌전히 속해 있는지, 그리고 충분한 해제 권한을 쥐고 있는지 등을 까다롭게 다중 검증한다.
Osys_umount() → do_umount() → umount_tree() 순서로 이어지는 철저하고 안전한 정리 알고리즘입니다.
13

Pathname Lookup: 복잡한 문자열 경로를 Inode로 치환하는 마법path_lookup() / link_path_walk() / 전능한 nameidata

사용자가 무심결에 입력한 /tmp/test라는 단순한 문자열 경로를 쪼개어 실제 파일의 메타데이터 실체인 inode 객체로 안전하게 변환해 내는 일련의 기나긴 과정을 Pathname Lookup이라고 부릅니다. 이 과정의 중추적인 두뇌 역할은 path_lookup() 함수가 도맡아 수행하며, 험난한 경로 해석의 최종 결과물은 nameidata라는 매우 특별하고 거대한 구조체 바구니 안에 소중히 담기게 됩니다.

Pathname Lookup 알고리즘이 악명 높게 복잡한 이유들
출발점의 다변화
맨 앞이 슬래시(/)인 절대경로(Absolute)라면 프로세스 정보의 current->fs->root에서 여정을 시작하고, 그 외의 상대경로(Relative)라면 current->fs->pwd가 출발점이 됩니다.
끊임없는 권한 검증
경로 문자열을 쪼갠 중간 요소(Component) 디렉터리들을 통과할 때마다, 해당 프로세스에 탐색(traverse, 디렉터리 실행) 권한이 충분히 있는지 매번 깐깐하게 검사해야 합니다.
특수 이름의 함정
현재 디렉터리를 뜻하는 .은 제자리에 머물게 하고, 부모 디렉터리를 뜻하는 ..을 만나면 위로 올라가야 하는데, 이때 실수로 루트 경계(root boundary)나 마운트 경계를 뚫고 나가지 않도록 극도로 조심스럽게 처리해야 합니다.
마운트 경계 통과 (Mount Crossing)
탐색 중인 특정 컴포넌트가 우연히 다른 파일시스템이 연결된 마운트 포인트라면, follow_mount() 함수가 개입해 현재 파일시스템을 미련 없이 버리고 새롭게 마운트된 이질적 파일시스템의 루트로 제어권을 부드럽게 텔레포트시킵니다.
다중 Namespace 세계관
아무리 글자 토씨 하나 안 틀리고 동일한 문자열 경로라 할지라도, 프로세스들이 서로 다른 격리된 namespace에 속해 있다면 최종적으로 가리키게 되는 실제 파일(inode)은 완전히 남남일 수 있습니다.
1
초고속 Dentry Cache 스캔. 찾고자 하는 이름이 이미 캐시에 존재한다면 빙고! 그 즉시 inode를 반환하며 경로 해석을 초고속으로 마무리 짓습니다.
2
real_lookup()의 험난한 디스크 I/O. 캐시 히트에 실패(Cache miss)했다면 어쩔 수 없습니다. 대상 파일시스템이 제공하는 고유한 inode lookup 메서드를 이용해 실제 물리 디렉터리 블록을 힘겹게 읽어내고, 새끈한 dentry와 inode 객체를 메모리에 찍어낸 뒤 캐시에 예쁘게 꽂아 넣습니다.
3
마운트 경계의 매끄러운 통과. 탐색 도중 마운트 포인트를 밟게 되면 follow_mount()가 발동하여 새 파일시스템의 루트 꼭대기로 폴짝 뛰어넘습니다. 우리가 터미널에서 /proc, /home, /mnt/usb를 마치 한 덩어리의 거대한 연속된 트리처럼 스무스하게 돌아다닐 수 있는 숨은 비결이 바로 이 녀석 덕분입니다.
4
대망의 마지막 Component 정산. 경로의 맨 끝에 후행 슬래시(Trailing slash)가 떡하니 붙어있다면 그 대상은 무조건 디렉터리여야만 합니다. 만약 LOOKUP_FOLLOW 옵션 깃발이 펄럭이고 있다면 기어코 심볼릭 링크(symlink) 안으로 파고들어 최종 목적지를 밝혀냅니다. 다 뒤졌는데도 흔적이 없다면 냉정하게 -ENOENT 에러를 토해냅니다.

핵심 O/X 퀴즈

1. 절대경로(Absolute pathname) 탐색은 철저히 프로세스의 fs_struct 내 root 디렉터리를 기점으로 시작하고, 상대경로(Relative)는 현재 작업 디렉터리(pwd)를 베이스캠프 삼아 출발한다.
Ofs_struct 구조체가 머금고 있는 root 포인터와 pwd 포인터가 각각 이정표 역할을 충실히 해냅니다.
2. Pathname Lookup 알고리즘은 워낙 구조가 탄탄해서 중간에 마운트 포인트가 끼어있거나 심볼릭 링크가 나타나도 굳이 별도의 특수 상황 처리 로직을 가동할 필요가 전혀 없다.
X오히려 Mount crossing 통과, 복잡다단한 Symlink 재귀 처리, 구간별 권한 검증 등 수많은 예외 처리와 안전장치가 Lookup 핵심 흐름의 근간을 이룹니다.
3. Dentry Cache를 뒤졌는데 원하는 캐시 항목이 쏙 빠져있다면, 커널은 최종적으로 실제 하위 파일시스템의 고유 lookup 메서드를 호출하여 물리적 디렉터리 블록을 싹싹 긁어 읽어 들인다.
Oreal_lookup() 내부 로직이 각 파일시스템 타입별로 정의된 i_op->lookup 인터페이스를 정직하게 호출해 냅니다.
14

Parent Lookup과 Symbolic Link의 무한 추적 한계LOOKUP_PARENT / -ELOOP 방어선 / total_link_count

파일을 새롭게 생성하거나(create), 무자비하게 삭제(unlink), 혹은 이정표 이름을 바꿀 때(rename)는 대상이 되는 맨 끝단의 마지막 파일 이름 그 자체보다, 그 녀석을 따뜻하게 품고 있는 '부모 디렉터리(Parent directory)'의 객체 정보가 실질적인 제어의 핵심 타깃이 됩니다. 이때 조용히 LOOKUP_PARENT 플래그를 넘겨주면, 경로를 파헤치던 link_path_walk() 알고리즘은 맨 마지막 요소를 섣불리 해석하지 않은 채 부모 디렉터리 지점까지만 도달하여 멈추고, 미처 다루지 않은 마지막 문자열 꼬리표는 nameidata.last 변수에 얌전히 따로 떼어 보관해 둡니다.

악명 높은 Symbolic Link 딥 다이브 처리 흐름
조우 및 침투
해석 중인 특정 컴포넌트의 inode 내부에 follow_link 메서드가 장착되어 있다면, 지체 없이 do_follow_link()를 발동시킵니다.
안전망 (Depth 제한)
무한 루프의 늪에 빠지지 않도록 현재 프로세스의 link_count를 감시합니다. 재귀 호출 깊이가 6번을 초과하는 순간 가차 없이 -ELOOP 에러를 내뿜습니다. 또한, 전체 추적 과정에서의 링크 누적 해석 횟수(total_link_count)가 40개에 도달하면 역시 그 자리에서 즉시 추적을 포기하고 뻗어버립니다.
내용물 재해석의 늪
__vfs_follow_link() 녀석이 심볼릭 링크 껍데기를 까고 그 안에 숨겨져 있던 새로운 pathname 문자열을 끄집어낸 뒤, 이를 다시 link_path_walk() 엔진에 통째로 집어넣고 무시무시한 재귀 해석을 시작합니다.
Absolute vs Relative의 기로
까발린 링크 속 문자열 내용이 슬래시(/)로 당당히 시작한다면, 쿨하게 프로세스의 root 기점에서부터 탐색을 완전히 새로 고침(리셋)합니다. 반면 상대 경로 형태라면, 링크를 만나기 직전까지 애써 탐색해 둔 현재 디렉터리 위치를 그대로 이어받아 조심스레 나아갑니다.

Symbolic link는 단순히 다른 파일의 이름을 빌려 쓰는 얄팍한 별명이 절대 아닙니다. 커널 입장에서는 "경로 탐색(lookup) 도중 갑자기 툭 튀어나와서, 원래 탐색 흐름 중간에 억지로 끼어들어 새롭게 파싱되어야만 하는 불청객 같은 또 다른 경로 문자열"에 가깝습니다. 고로 Pathname Lookup 알고리즘은 사용자가 던진 문자열 한 줄을 처음부터 끝까지 무지성으로 한 번만 쭉 읽고 끝나는 우스운 루프가 아닙니다. 심볼릭 링크 지뢰를 밟을 때마다 재귀적으로 새로운 차원의 경로를 파싱하고, 무사히 해석을 마친 후 원래의 탐색 맥락으로 힘겹게 되돌아와야 하는 고도의 스택(Stack) 구조를 필연적으로 띠게 됩니다.

핵심 O/X 퀴즈

1. 은밀한 LOOKUP_PARENT 옵션은 경로의 맨 마지막 요소(Component) 객체 자체가 아니라, 녀석을 고이 품고 있는 진짜 배후인 부모 디렉터리 객체를 손에 넣고자 할 때 요긴하게 쓰인다.
O파일을 아예 세상에 없던 걸로 만들거나(create) 지우는(unlink) 조작은 결국 부모 디렉터리의 내부 목록 테이블을 뜯어고쳐야 하기에 필수적인 절차입니다.
2. 악질적인 사용자가 Symbolic link가 끊임없이 자기 자신을 가리키는 무한 루프 트랩을 설치해 두더라도, 불굴의 리눅스 커널은 메모리가 허락하는 한 아무 제한 없이 끝까지 그 함정을 묵묵히 따라가 준다.
X단일 깊이 제한 link_count 최대 6번, 누적 해석 제한 total_link_count 최대 40개라는 강력한 2중 방어선 덕분에 시스템이 멈추기 전 깔끔하게 -ELOOP 오류를 뱉어내며 탈출합니다.
3. 만약 도달한 Symbolic link 파일 안쪽에 적힌 문자열 내용이 슬래시(/)로 시작하는 Absolute pathname 구조를 띠고 있다면, 놀랍게도 커널은 진행 중이던 기존 lookup 진행 상황 일부를 뒤엎고 프로세스의 root 기준점부터 아예 처음인 것처럼 재탐색을 시작할 수 있다.
O기존에 쥐고 있던 낡은 경로 결과물은 깨끗하게 릴리즈(release)해 버리고, 쿨하게 프로세스 root에서부터 산뜻하게 재시작 버튼을 누릅니다.
15

open, read, write, close: 터미널 cp 명령어 이면의 기나긴 여정filp_open() / open_namei() / dentry_open()의 앙상블

우리가 터미널 창에서 아무 생각 없이 툭 던지는 cp /floppy/TEST /tmp/test라는 마법의 명령어 한 줄은, 커널의 컴컴한 바닥 밑에서 거대한 톱니바퀴처럼 맞물려 돌아가는 read + write + open + close 시스템 콜들의 기가 막히게 정교한 조합 덩어리입니다. 각 단계마다 도대체 어떤 VFS 핵심 객체들이 땀 흘리며 움직이는지 그 적나라한 내부 민낯을 파헤쳐 봅시다.

open
가장 먼저 sys_open()이 문을 두드립니다 → 사용자 메모리 공간의 경로 문자열을 커널로 퍼 나르는 getname() → 배열의 빈칸을 재빠르게 찜하는 get_unused_fd() → 마침내 진짜배기 엔진 filp_open() 가동 → 이어서 open_namei()가 미친 듯이 Pathname Lookup을 돌리며 권한과 파일 존재 유무를 깐깐하게 따짐 → 무사통과하면 dentry_open()이 텅 빈 file 객체 하나를 신규 할당하고 초기화 세팅 시전 (이때 f_op = i_fop 핵심 복사 완료) → 해당 superblock의 "현재 당당히 열려있는 파일 전체 리스트"에 이름을 올림 → 현재 프로세스 fd 배열의 빈칸에 방금 만든 file 객체 메모리 주소를 쾅 찍어 저장 → 마지막으로 fd 정수표 번호를 유저에게 당당히 반환하며 퇴장.
read
건네받은 fd 번호표를 들이밀며 fget_light()가 file 객체를 획득 → 파일이 열릴 당시 f_mode에 읽기 권한이 포함되어 있는지 재차 확인 → 메모리 영역 안전을 검증하는 access_ok()와 범위를 체크하는 rw_verify_area() 콤보 → 드디어 궁극의 인터페이스 file->f_op->read() 펀치 작렬 (여기서 하위 파일시스템 구체 구현부로 빨려 들어감) → 성공적으로 읽어 들인 바이트 수만큼 현재 읽기 커서인 f_pos 전진 갱신 → fput_light()로 객체 락을 풀며 쿨하게 퇴장.
write
기본적인 뼈대는 read의 흐름과 소름 돋게 판박이입니다. 단지 방아쇠를 당기는 함수가 file->f_op->write() 일 뿐입니다. 참고로 요청한 크기보다 실제 디스크에 덜 쓰인 partial write 사태가 발생하더라도 커널 입장에서는 무조건적인 에러로 간주하지 않을 수 있다는 점이 백미입니다.
close
이제 파티를 끝낼 sys_close()의 시간입니다 → fd 배열 칸을 가차 없이 NULL로 덧칠하여 연결 고리를 박살 냄 → 프로세스의 open_fds 비트맵 현황에서 해당 비트를 깔끔하게 clear → 진짜배기 filp_close() 호출: 그동안 쌓아둔 버퍼 잔해를 밀어내는 flush 한방, 혹시 걸려있을지 모를 mandatory lock 무장 해제, 그리고 fput()을 날려 해당 file 객체의 목숨줄인 참조 카운트(f_count)를 깎아내림. 만약 이 카운트가 바닥(0)을 쳤다면 해당 file 객체 메모리 자체를 흔적도 없이 파괴하고, 관련되어 있던 dentry와 마운트 포인트 참조 수치도 연쇄적으로 줄이며 화려했던 세션을 영원히 종결시킴.

가장 흔한 오해의 파괴: close() 시스템 콜은 디스크 상의 원본 파일 데이터를 단 한 바이트도 삭제하지 않습니다. 녀석의 진짜 임무는 그저 프로세스가 손에 쥐고 흔들던 fd(파일 디스크립터)와의 논리적인 연결 고리를 싹둑 끊어내고, file 객체의 참조 카운트를 낮춰 메모리 자원을 깨끗하게 정돈하는 '세션 종료' 작업에 불과합니다. 참조 카운트가 완전히 0이 되어 file 객체가 공중 분해되더라도 디스크에 누워있는 파일 원본 데이터는 머리카락 한 올 상하지 않으며, 실제 물리적/논리적 파일 삭제는 오직 무시무시한 unlink() 시스템 콜만이 합법적으로 집행할 수 있습니다.

핵심 O/X 퀴즈

1. 기나긴 open() 시스템 콜의 대장정이 최종적으로 성공의 축배를 들면, 프로세스의 files_struct 안쪽 fd 배열 중 어느 한 칸이 따끈따끈한 새 file 객체 메모리 주소를 꽉 움켜쥐게 된다.
Ocurrent->files->fd[fd] 구조체 배열 공간에 생성된 file 객체의 포인터(주소값)가 정확히 안착합니다.
2. read()와 write() 시스템 콜 엔진은 다름 아닌 file 객체 정중앙에 떡하니 박혀있는 f_op(file operation) 테이블을 교량 삼아, 각 파일시스템별로 숨겨진 실제 구현 로직 함수를 정확하게 호출해 낼 수 있다.
OVFS가 자랑하는 위대한 추상화 설계의 핵심이자, file->f_op->read/write()로 이어지는 객체지향적 다형성의 백미입니다.
3. close() 시스템 콜은 그 이름에 걸맞게 파일 조작 세션을 깔끔하게 끝마침과 동시에, 항상 디스크 표면의 실제 물리적인 파일 데이터 찌꺼기까지 한 톨 남김없이 영구 삭제해 버리는 파괴적인 명령이다.
Xclose()는 단지 fd 연결을 끊고 불필요해진 메모리 참조만 줄일 뿐입니다. 파일 메타데이터 및 실질적 데이터 파괴는 온전히 별개의 unlink() 명령이 짊어진 숙명입니다.
16

File Locking: 여러 프로세스가 하나의 파일을 두고 아귀다툼을 벌일 때FL_FLOCK / FL_POSIX / 자비 없는 Mandatory Lock

만약 통제 불능의 두 프로세스가 우연히 같은 파일의 정확히 똑같은 오프셋 위치에 동시에 쓰기(write) 폭격을 가한다면, 디스크에 남겨진 최종 결과물은 누구도 예측할 수 없는 끔찍한 쓰레기 데이터로 망가져 버립니다. 이 끔찍한 파국을 막아내기 위해 Unix 계열의 운영체제들은 아주 오래전부터 평화를 수호하는 File Locking(파일 잠금)이라는 훌륭한 동시성 제어 메커니즘을 든든하게 제공해 오고 있습니다.

양대 산맥: Advisory Lock vs Mandatory Lock
Advisory Lock (권고적 잠금)

참으로 신사적이지만 취약한 방식입니다. 다른 프로세스들이 파일 잠금 규칙을 "스스로 얌전히 확인하고 따르겠다"고 협력할 때만 실질적인 방어 효과가 나타납니다. 락 규약을 철저히 지키기로 암묵적 약속을 맺은 성실한 프로세스들 무리 사이에서만 가치가 빛을 발하며, 누군가 약속을 깨고 강제로 I/O를 날리면 커널은 전혀 막아주지 않습니다.

Mandatory Lock (강제적 잠금)

철권통치에 가까운 무자비한 커널 강제형 락입니다. 다른 불한당 프로세스가 감히 open, read, write 시스템 콜을 무단 호출하여 락 구역을 침범하려 들면, 자비 없는 커널이 그 자리에서 멱살을 잡고 차단해 버립니다. 이 가혹한 모드를 발동시키려면 파일시스템 자체 마운트 시 MS_MANDLOCK 옵션의 허가가 필요하며, 타깃 파일의 SGID 비트를 켜고 group-execute 비트를 끄는 복잡한 비밀 의식을 치러야 합니다.

커널 내부를 관통하는 두 가지 Lock 구현체 타입
FL_FLOCK 타입
주로 file 객체에 깊숙이 들러붙는 형태의 락입니다. 터미널의 고전적인 flock() 시스템 콜 녀석이 이 방식을 애용하며, LOCK_SH(착한 공유 락), LOCK_EX(나만 쓸래 독점 락), LOCK_UN(해방의 락 해제) 모드를 지원합니다. 흥미롭게도 file 객체가 수명을 다해 fput()으로 산화될 때 이 락들도 흔적 없이 흩어지며, fork()로 자식을 낳으면 이 락 상태마저 고스란히 유산으로 상속된다는 독특한 특징이 있습니다.
FL_POSIX 타입
프로세스 정보와 inode 객체 양쪽 모두에 교묘하게 다리를 걸치는 형태입니다. 주로 fcntl() 시스템 콜(F_GETLK/F_SETLK/F_SETLKW)의 심장부에서 맹활약하며, 파일 전체가 아닌 바이트 단위의 임의의 세밀한 구역(Range) 하나하나에 정밀 타격 락을 거는 가공할 기능을 자랑합니다. 프로세스가 죽거나 fd가 닫히면 신기루처럼 자동 해제되며, fork()를 하더라도 자식에게는 이 무거운 락이 절대 상속되지 않고 끊어집니다.

동일한 가여운 파일 하나를 향해 무수히 쏟아지는 각종 락 요청들은, 결국 해당 파일의 영혼인 inode 객체 내부에 자리 잡은 i_flock 단일 연결 리스트로 꾸역꾸역 집결하게 됩니다. 이때 __posix_lock_file()라는 심판관 함수가 출동해, 같은 inode에 이미 주렁주렁 매달려 있는 기존 FL_POSIX 락들의 범위를 하나하나 까보며 새로 들어온 락 영역과 무자비하게 충돌하는지(Overlap) 계산기를 두드립니다. 만약 불운하게도 뼈아픈 충돌이 발생했고, 새로 요청한 락이 기다림(Blocking)을 감수하는 성격이라면 치명적인 데드락(Deadlock)이 걸렸는지 먼저 재빠르게 검사한 뒤 통과되면 조용히 waiter 큐에 밀어 넣어 깊은 잠에 빠뜨립니다.

마지막 한 문장의 여운: VFS는 결코 "파일시스템 사이를 대충 연결해 주는 얄팍한 브릿지" 수준이 아닙니다. 파일시스템 타입 구조체의 우아한 등록 절차부터 시작해, 광활한 Namespace 우주 안의 마운트 포인트 조각배 띄우기, 어지러운 Pathname 문자열의 dentry/inode 추적기, 프로세스가 애지중지 열어둔 file 객체 세션의 생로병사 관리, 그리고 최종 목적지인 read/write 요청을 각 파일시스템의 심장부로 찔러넣는 라우팅 역할까지 전부 아우릅니다. 거기에 그치지 않고 Dentry 캐싱과 철저한 Lock 메커니즘으로 전체 시스템의 아슬아슬한 성능과 동시성 생태계까지 혼자서 묵묵히 통제해 내죠. 여러분이 그저 까만 터미널 창에 무심코 cat, cp, ls를 짧게 타이핑하고 엔터 키를 내리치는 바로 그 찰나의 순간, 이 거대하고도 정교한 괴물 같은 VFS 톱니바퀴 장치가 보이지 않는 수면 아래에서 조용히, 그러나 미친 듯이 맹렬하게 요동치고 있습니다.

핵심 O/X 퀴즈

1. Advisory lock 메커니즘은 시스템 내의 다른 프로세스들이 자발적이고 협력적으로 lock 규칙을 성실히 확인해 주어야만 그 방어적 효력이 발휘된다.
O신사협정과도 같아서, 이 규칙에 동의하고 협력하지 않는 무법자 프로세스에게는 속수무책으로 뚫리고 마는 한계를 지닙니다.
2. 고전적인 flock() 시스템 콜은 일반적으로 파일 전체가 아닌 수 바이트 단위의 임의 영역 하나만을 정밀하게 콕 집어 잠그는 고도의 POSIX 영역 단위 락 기능을 제공한다.
Xflock() 녀석은 파일 몸통 전체를 뭉텅이로 휘감아버리는 다소 무식한 advisory lock에 불과합니다. 바이트 단위의 임의 구역 정밀 타격 락은 fcntl() 기반의 FL_POSIX 타입 락이 전담하는 고급 기술입니다.
3. FL_POSIX 계열 락은 특정 프로세스와 타깃 inode 객체 양쪽 모두의 상태에 깊숙이 얽혀 동작하며, 대표적으로 fcntl() 시스템 콜을 기반으로 한 복잡한 file locking 환경에서 그 모습을 드러낸다.
O명령어 F_SETLK나 F_SETLKW 등을 이용해 섬세하게 설정되며, 해당 프로세스가 죽거나 관련 fd가 닫히는 순간 미련 없이 자동으로 흔적을 지우고 해제되는 깔끔한 락입니다.

VFS는 파일시스템 타입 구조체 등록부터 시작해 → 거대한 Namespace 트리 배치 → 복잡다단한 Pathname 룩업 추적 → 독립된 File 객체 세션 관리 → 객체지향적 함수 포인터 분기 처리 → 그리고 캐시(Cache)와 잠금(Lock)을 통한 극강의 성능 및 동시성 제어까지 완벽히 조율해 내는 마에스트로입니다. 가볍게 실행한 cat, cp, ls 명령어 단 한 줄의 이면에는, 이 거대하고 정교한 VFS 메커니즘 전반이 톱니바퀴처럼 조용히 맞물려 돌아가고 있습니다.

강의노트 — Linux Kernel, Chapter 12 "The Virtual Filesystem" 기반 재구성