ch12. virtual filesystem
VFS,
파일시스템 번역기
cp /floppy/TEST /tmp/test 명령을 실행하면 MS-DOS와 Ext2라는 서로 다른 두 파일시스템을 동시에 사용하게 됩니다. 하지만 정작 cp 프로그램 자체는 둘의 차이를 알지 못하죠. 애플리케이션과 파일시스템 사이에서 이를 매끄럽게 번역해 주는 커널 계층이 바로 VFS입니다.
큰 그림: 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 함수로" 적절히 연결해 줍니다.
식당의 메뉴판이 VFS와 같습니다. 손님(애플리케이션)은 '주문'이라는 공통된 인터페이스만 사용합니다. 주방(파일시스템)은 들어온 주문에 맞춰 각자의 조리법(구현 함수)을 실행하죠. 손님은 주방 시스템이 한식 스타일인지 양식 스타일인지 신경 쓸 필요가 없습니다.
핵심 O/X 퀴즈
1. VFS 덕분에 cp와 같은 프로그램은 원본 및 대상 파일의 실제 파일시스템 종류를 알 필요가 없다.
2. VFS는 사용자 공간(User Space) 라이브러리이며 커널 내부와는 관련이 없다.
3. 이번 장에서 다루는 VFS의 4대 핵심 객체는 superblock, inode, file, dentry이다.
VFS의 역할과 파일시스템의 세 부류FILESYSTEM TYPES
리눅스가 강력한 이유 중 하나는 "구조가 전혀 다른 디스크나 네트워크 상의 파일조차 마치 내 로컬 디스크의 파일처럼 자연스럽게 다룰 수 있다"는 점입니다. 이를 위해 VFS는 크게 세 부류의 파일시스템을 지원합니다.
로컬 저장장치의 물리적 디스크 블록을 관리합니다. Ext2/Ext3, ReiserFS, FAT, VFAT, NTFS, ISO9660, UDF, HFS, JFS, XFS 등.
네트워크 너머에 있는 파일을 마치 로컬 파일처럼 접근할 수 있게 해줍니다. NFS, Coda, AFS, CIFS, NCP 등.
물리적 디스크 공간을 관리하는 대신, 커널 내부의 자료구조를 파일 형태의 인터페이스로 사용자에게 노출합니다. /proc, sysfs, tmpfs, devpts, pipefs, sockfs 등.
마운트(Mount)는 단순히 폴더를 연결하는 것이 아니라, "이 지점부터는 다른 파일시스템의 루트 트리를 보여주겠다"는 의미의 '트리 교체' 작업입니다. 따라서 어떤 파일시스템을 특정 디렉터리에 마운트하면 그 디렉터리의 원래 내용은 잠시 가려지고, 마운트를 해제(unmount)하면 다시 나타나게 됩니다.
핵심 O/X 퀴즈
1. VFS가 다루는 파일시스템은 크게 디스크 기반, 네트워크, 특수 파일시스템으로 나눌 수 있다.
2. /proc는 일반적으로 물리적인 디스크 블록을 관리하는 전통적인 디스크 기반 파일시스템이다.
3. 특정 파일시스템을 디렉터리에 마운트하면 그 디렉터리의 원래 내용은 마운트가 유지되는 동안 가려질 수 있다.
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 언어의 구조체와 함수 포인터 테이블을 결합하여 강력한 객체지향적 다형성을 구현한 것입니다.
- file 객체
- 프로세스마다 개별적으로 생성됩니다. 각 프로세스가 파일을 읽고 있는 현재 오프셋(f_pos)은 독립적으로 유지됩니다.
- dentry
- 하나의 파일(inode)에 여러 개의 이름(hard link)이 연결될 수 있습니다.
- inode
- 실제 파일의 고유한 정체성입니다. 단일 파일시스템의 superblock과 연결됩니다.
- 핵심 포인트
- 파일의 이름(dentry), 열린 파일의 세션(file), 실제 파일의 정체성(inode)은 각각 철저히 분리된 객체로 관리됩니다.
핵심 O/X 퀴즈
1. VFS의 Common File Model은 구조가 다른 여러 파일시스템을 공통된 Unix식 파일 모델로 표현하려는 장치이다.
2. 커널은 read() 요청이 들어오면 항상 Ext2 전용 함수로 하드코딩하여 I/O 처리를 수행한다.
3. 하나의 동일한 inode 객체를 여러 dentry 객체가 동시에 가리킬 수 있으며, 이는 hard link 메커니즘과 관련이 있다.
VFS 시스템 콜과 캐시 메커니즘Table 12-1 / Dentry Cache
VFS가 관장하는 영역은 단순히 open, read, write에 그치지 않습니다. 마운트/언마운트, 파일시스템 통계 조회, 디렉터리 조작(생성·삭제), 심볼릭 링크 처리, 권한 변경, 파일 잠금(file lock), 메모리 매핑(mmap), 비동기 I/O, 확장 속성(extended attribute) 조작까지 파일과 관련된 거의 모든 시스템 콜을 포괄합니다.
- 직접 처리
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 등 광범위한 시스템 콜을 총괄한다.
2. lseek() 시스템 콜이 호출되면 커널은 항상 디스크의 파일 내용을 직접 수정하는 물리적 작업을 수행해야 한다.
3. Dentry Cache는 복잡한 경로 이름(pathname)을 실제 inode로 치환하는 과정을 쾌속화하는 데 결정적인 도움을 준다.
Superblock 객체: 마운트된 파일시스템의 총괄 대표자s_op / s_fs_info / dirty 플래그
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 객체는 마운트된 파일시스템 전체의 총체적인 상태와 구조 정보를 저장하는 역할을 한다.
2. s_fs_info 필드에는 구조가 서로 다른 모든 파일시스템들이 완전히 동일하게 공유하는 범용적인 정보만 저장된다.
3. Superblock의 dirty 플래그(s_dirt)가 설정되어 있다면, 이는 커널 메모리와 실제 디스크 상의 슈퍼블록 정보 사이에 불일치가 발생해 추후 동기화가 필요하다는 뜻이다.
Inode 객체: 파일의 변하지 않는 진짜 신분증i_op / 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는 해당 파일이 존재하는 내내 파일 자체를 식별하는 핵심 메타데이터 구조체다.
2. Inode 객체는 개별 프로세스가 파일을 어디까지 읽었는지 나타내는 현재 읽기 위치(offset)를 저장하는 것이 주된 역할이다.
3. Inode operation(i_op)에는 lookup, link, unlink, mkdir, rename과 같이 디렉터리 내부의 이름 공간을 조작하는 동작들이 포함된다.
File 객체: 프로세스와 파일 사이의 '열린 세션'f_pos / f_op / filp slab cache
File 객체는 디스크 상의 파일 데이터 그 자체를 의미하지 않습니다. 특정 프로세스가 대상 파일을 열었을 때 메모리에 동적으로 생성되는 '상호작용 세션(Session)' 객체입니다. 파일이 열릴(open) 때만 일시적으로 만들어지며, 디스크에는 이에 직접 대응하는 데이터 블록 이미지가 전혀 존재하지 않습니다.
- 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 될 때 동적으로 만들어지며, 현재 열려 있는 파일과 프로세스 사이의 세션 상태 정보를 담아낸다.
2. File 객체의 f_pos 값은 모든 시스템 프로세스가 단일하게 공유하는 고정 속성이므로 통일성을 위해 inode에 영구 저장된다.
3. File 객체 내부의 f_op 필드는 향후 호출될 read/write 같은 작업들을 대상 파일시스템의 실제 구현 함수로 정확히 연결하는 라우팅 역할을 한다.
Dentry 객체와 Dentry Cache: 경로 이름을 빠르게 찾는 장치Dentry 4가지 상태 / LRU 캐시
Dentry(Directory Entry)는 "특정 디렉터리 내의 문자열 파일 이름과 그에 상응하는 실제 inode를 하나로 묶어 연결"해 주는 매개체입니다. 만약 /tmp/test라는 경로를 탐색한다면, /, tmp, test 각각에 대한 dentry 객체가 계층적으로 다뤄집니다. 이는 디스크에 직접 저장되는 데이터 구조가 아니라, 빠른 경로 해석 성능을 달성하기 위해 VFS가 메모리 상에 동적으로 찍어내는 논리 객체입니다.
유효 정보 없음
유효하지만 미사용 (캐시)
사용 중 — 해제 불가
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 사이의 연결 고리를 논리적으로 표현한다.
2. Negative dentry란 명칭 그대로 항상 음수로 표현된 inode 번호를 지니고 있는 특수한 dentry를 의미한다.
3. Dentry Cache는 최근 해석을 마친 이름-to-inode 매핑 결과를 메모리에 보관함으로써 차후의 Pathname lookup 속도를 극대화한다.
fs_struct, files_struct, 그리고 fd 배열Figure 12-3 / fget · fput 시스템
활성화된 프로세스가 파일 시스템과 원활하게 상호작용하려면 반드시 두 가지 핵심 상태를 관리해야 합니다. 첫째는 "내가 현재 시스템의 어느 디렉터리에 위치해 있는가(fs_struct)"이고, 둘째는 "내가 현재 어떤 파일들을 열어서 사용하고 있는가(files_struct)"입니다.
- fs_struct
- 프로세스의 논리적 루트(root dentry/vfsmnt)와 현재 작업 중인 디렉터리(current working directory dentry/vfsmnt), 그리고 umask 값을 담습니다.
chroot()시스템 콜을 이용하면 이 논리적 루트를 동적으로 바꿀 수 있습니다. - files_struct
- 프로세스가 현재 열고 있는 파일들의 목록을 철저히 관리합니다. 이 구조체의 핵심은 fd 배열로, 인덱스는 파일 디스크립터(정수)이고 저장된 값은 메모리 상의 실제 file 객체 포인터입니다.
파일 디스크립터(fd)는 그저 평범한 정수 인덱스에 불과합니다. 코드 레벨에서 current->files->fd[fd] 형태로 접근하여 커널 메모리 어딘가에 있는 file 객체의 주소를 찾아내는 '키(Key)' 역할을 할 뿐입니다. dup()나 dup2()를 호출하면 배열의 여러 인덱스 칸이 단 하나의 동일한 file 객체 포인터를 가리키게 복제할 수 있습니다. 커널 내부 함수인 fget()은 인덱스를 통해 file 객체를 안전하게 획득하고 참조 카운터(f_count)를 늘립니다. 반대로 조작이 끝난 후 호출되는 fput()은 카운터를 조용히 줄이고, 그 값이 0으로 떨어지면 객체 자체를 말끔히 정리합니다.
핵심 O/X 퀴즈
1. 파일 디스크립터(fd)는 files_struct 내부의 fd 배열에 접근하기 위한 인덱스 숫자이며, 궁극적으로 file 객체를 찾는 데 쓰인다.
2. 전통적인 Unix 환경에서 fd 0, 1, 2는 각각 표준 입력(stdin), 표준 출력(stdout), 표준 에러(stderr) 채널과 암묵적으로 연결된다.
3. dup2() 시스템 콜을 통해 생성된 두 개의 파일 디스크립터는 독립성을 위해 반드시 서로 다른 file 객체를 가리키도록 설계되었다.
특수 파일시스템과 타입 등록 과정file_system_type / register_filesystem()
특수 파일시스템(Special Filesystem)은 물리적 디스크의 블록을 관리하는 대신, 커널 내부의 역동적인 자료구조나 상태 정보를 친숙한 파일 읽기/쓰기 인터페이스 형태로 래핑하여 사용자에게 제공합니다. 이는 물리적인 블록 디바이스(하드디스크 파티션 등)에 귀속되지 않기 때문에, 커널 측으로부터 Major 번호 0과 파일시스템만의 고유한 Minor 번호로 구성된 가상의 블록 디바이스 식별자를 부여받고 동작을 시작합니다.
- name
- 파일시스템의 대표 문자열 이름 ("ext2", "proc", "tmpfs" 등).
- fs_flags
- 해당 파일시스템 고유의 특성을 명시하는 타입 플래그 묶음.
- get_sb
- 새로운 마운트 과정에서 해당 파일시스템의 superblock 객체를 신규 할당하거나 초기화하는 핵심 메서드. 앞서 보았던 superblock operation(s_op) 테이블에 관련 함수가 없었던 진짜 이유가 바로 이 객체의 존재 때문입니다.
- kill_sb
- 더 이상 쓰이지 않는 superblock을 안전하게 해제 및 제거합니다.
- fs_supers
- 시스템 상에 존재하는 동일한 타입의 superblock들을 한데 엮어 관리하는 리스트.
file_system_type 구조체를 전역 단일 연결 리스트에 당당히 등록합니다.get_fs_type() 함수가 전달된 이름 문자열로 리스트를 스캔하여 해당하는 file_system_type 구조체를 색출해냅니다.get_sb 함수 포인터를 전격 호출하여 슈퍼블록을 할당받고 빈틈없이 초기화합니다.핵심 O/X 퀴즈
1. /proc, sysfs, tmpfs, pipefs 등은 모두 전형적인 특수 파일시스템(Special Filesystem)의 훌륭한 예시로 볼 수 있다.
2. 모든 종류의 특수 파일시스템은 그 특성상 반드시 실제 물리 디스크 파티션 위에 굳건히 자리 잡아야만 작동할 수 있다.
3. file_system_type 구조체 내부의 get_sb는 마운트 절차 진행 시 대상 파일시스템의 superblock 객체를 신규 생성하거나 초기화하는 무거운 역할을 짊어진다.
Mount, Namespace, 그리고 vfsmountCLONE_NEWNS / pivot_root의 세계
Namespace는 '특정 프로세스의 관점에서 바라보는 마운트된 파일시스템들의 전체 트리 구조'를 뜻합니다. 리눅스 시스템에 존재하는 대부분의 프로세스는 1번 프로세스(init)의 장대한 namespace를 고스란히 복제하여 공유합니다. 하지만 clone(CLONE_NEWNS) 시스템 콜을 통해 프로세스 전용의 독립적인 namespace 우주를 새롭게 창조하면, 그 격리된 공간 안에서 수행된 마운트나 언마운트 작업은 오직 동일한 namespace를 공유하는 그룹 내에서만 유효하게 보이며 바깥 세상에는 전혀 영향을 미치지 않습니다.
- 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란 다름 아닌 특정 프로세스가 시각적으로 인식하는 마운트된 파일시스템 트리의 전체적인 구조와 모양새를 의미한다.
2. 리눅스 커널 정책상 동일한 파일시스템을 시스템 트리의 여러 마운트 포인트에 중복해서 연결하는 행위는 엄격히 금지되며 항상 한 번만 허용된다.
3. vfsmount 객체는 구체적인 마운트 포인트, 루트 dentry 위치, 연결된 superblock 포인터, 그리고 복잡한 부모-자식 마운트 관계 트리 등을 꼼꼼하게 추적하고 관리한다.
Mount와 Unmount의 작동 흐름: 터미널 명령에서 Superblock 초기화까지do_kern_mount() / 영원히 가려진 rootfs
터미널에 mount 명령어를 무심코 입력하면, 내부적으로 mount() 시스템 콜이 트리거되고, sys_mount()를 거쳐 거대한 마운트 로직의 본체인 do_mount()로 깊숙이 진입하게 됩니다.
do_new_mount()로 제어권을 넘깁니다.get_fs_type()을 호출해 알맞은 file_system_type 구조체를 찾아낸 뒤, alloc_vfsmnt()로 비어있는 vfsmount 틀을 찍어내고, get_sb()를 발동시켜 본격적인 슈퍼블록 할당 및 초기화 레이스에 돌입합니다.sget() 함수로 혹시나 기존에 메모리에 올라와 있는 동일 슈퍼블록이 있는지 캐시를 뒤집니다. 만약 이번이 완전 처음이라면 fill_super() 함수가 출동해 디스크의 은밀한 곳에서 실제 슈퍼블록 구조를 긁어와 메모리에 예쁘게 채워 넣습니다.Root Filesystem 마운트의 숨겨진 진실: 시스템 부팅 시 커널은 사실 가장 먼저 메모리 기반의 텅 빈 파일시스템인 rootfs를 무조건 마운트합니다. 그 이후 초기화에 필요한 각종 디바이스 드라이버 로딩을 마치면 비로소 실제 디스크 기반의 진짜 Root 파일시스템을 이 rootfs 위에 덮어씌우듯 '오버마운트(Over-mount)' 해버립니다. 처음에 마운트된 가여운 rootfs는 시스템이 꺼질 때까지 언마운트되지 못한 채, 진짜 Root 파일시스템 구조 아래에 영구적으로 어둡게 가려진 채 존재하게 됩니다.
핵심 O/X 퀴즈
1. 일반적인 신규 마운트 절차 진행 시, do_kern_mount() 함수가 알맞은 파일시스템 타입을 물색하고 필수적인 superblock과 vfsmount 객체를 한 쌍으로 빈틈없이 준비해 낸다.
2. 시스템 부팅 시 Root 파일시스템은 어떠한 사전 준비 단계도 없이 다짜고짜 실제 물리 디스크 Root를 마운트해 버리며, rootfs와 같은 중간 특수 단계는 존재하지 않는다.
3. 마운트 해제(unmount) 과정에서는 지목된 대상이 올바른 마운트 포인트가 맞는지, 호출한 프로세스의 namespace 구역 내에 얌전히 속해 있는지, 그리고 충분한 해제 권한을 쥐고 있는지 등을 까다롭게 다중 검증한다.
Pathname Lookup: 복잡한 문자열 경로를 Inode로 치환하는 마법path_lookup() / link_path_walk() / 전능한 nameidata
사용자가 무심결에 입력한 /tmp/test라는 단순한 문자열 경로를 쪼개어 실제 파일의 메타데이터 실체인 inode 객체로 안전하게 변환해 내는 일련의 기나긴 과정을 Pathname Lookup이라고 부릅니다. 이 과정의 중추적인 두뇌 역할은 path_lookup() 함수가 도맡아 수행하며, 험난한 경로 해석의 최종 결과물은 nameidata라는 매우 특별하고 거대한 구조체 바구니 안에 소중히 담기게 됩니다.
- 출발점의 다변화
- 맨 앞이 슬래시(/)인 절대경로(Absolute)라면 프로세스 정보의 current->fs->root에서 여정을 시작하고, 그 외의 상대경로(Relative)라면 current->fs->pwd가 출발점이 됩니다.
- 끊임없는 권한 검증
- 경로 문자열을 쪼갠 중간 요소(Component) 디렉터리들을 통과할 때마다, 해당 프로세스에 탐색(traverse, 디렉터리 실행) 권한이 충분히 있는지 매번 깐깐하게 검사해야 합니다.
- 특수 이름의 함정
- 현재 디렉터리를 뜻하는
.은 제자리에 머물게 하고, 부모 디렉터리를 뜻하는..을 만나면 위로 올라가야 하는데, 이때 실수로 루트 경계(root boundary)나 마운트 경계를 뚫고 나가지 않도록 극도로 조심스럽게 처리해야 합니다. - 마운트 경계 통과 (Mount Crossing)
- 탐색 중인 특정 컴포넌트가 우연히 다른 파일시스템이 연결된 마운트 포인트라면,
follow_mount()함수가 개입해 현재 파일시스템을 미련 없이 버리고 새롭게 마운트된 이질적 파일시스템의 루트로 제어권을 부드럽게 텔레포트시킵니다. - 다중 Namespace 세계관
- 아무리 글자 토씨 하나 안 틀리고 동일한 문자열 경로라 할지라도, 프로세스들이 서로 다른 격리된 namespace에 속해 있다면 최종적으로 가리키게 되는 실제 파일(inode)은 완전히 남남일 수 있습니다.
follow_mount()가 발동하여 새 파일시스템의 루트 꼭대기로 폴짝 뛰어넘습니다. 우리가 터미널에서 /proc, /home, /mnt/usb를 마치 한 덩어리의 거대한 연속된 트리처럼 스무스하게 돌아다닐 수 있는 숨은 비결이 바로 이 녀석 덕분입니다.핵심 O/X 퀴즈
1. 절대경로(Absolute pathname) 탐색은 철저히 프로세스의 fs_struct 내 root 디렉터리를 기점으로 시작하고, 상대경로(Relative)는 현재 작업 디렉터리(pwd)를 베이스캠프 삼아 출발한다.
2. Pathname Lookup 알고리즘은 워낙 구조가 탄탄해서 중간에 마운트 포인트가 끼어있거나 심볼릭 링크가 나타나도 굳이 별도의 특수 상황 처리 로직을 가동할 필요가 전혀 없다.
3. Dentry Cache를 뒤졌는데 원하는 캐시 항목이 쏙 빠져있다면, 커널은 최종적으로 실제 하위 파일시스템의 고유 lookup 메서드를 호출하여 물리적 디렉터리 블록을 싹싹 긁어 읽어 들인다.
Parent Lookup과 Symbolic Link의 무한 추적 한계LOOKUP_PARENT / -ELOOP 방어선 / total_link_count
파일을 새롭게 생성하거나(create), 무자비하게 삭제(unlink), 혹은 이정표 이름을 바꿀 때(rename)는 대상이 되는 맨 끝단의 마지막 파일 이름 그 자체보다, 그 녀석을 따뜻하게 품고 있는 '부모 디렉터리(Parent directory)'의 객체 정보가 실질적인 제어의 핵심 타깃이 됩니다. 이때 조용히 LOOKUP_PARENT 플래그를 넘겨주면, 경로를 파헤치던 link_path_walk() 알고리즘은 맨 마지막 요소를 섣불리 해석하지 않은 채 부모 디렉터리 지점까지만 도달하여 멈추고, 미처 다루지 않은 마지막 문자열 꼬리표는 nameidata.last 변수에 얌전히 따로 떼어 보관해 둡니다.
- 조우 및 침투
- 해석 중인 특정 컴포넌트의 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) 객체 자체가 아니라, 녀석을 고이 품고 있는 진짜 배후인 부모 디렉터리 객체를 손에 넣고자 할 때 요긴하게 쓰인다.
2. 악질적인 사용자가 Symbolic link가 끊임없이 자기 자신을 가리키는 무한 루프 트랩을 설치해 두더라도, 불굴의 리눅스 커널은 메모리가 허락하는 한 아무 제한 없이 끝까지 그 함정을 묵묵히 따라가 준다.
3. 만약 도달한 Symbolic link 파일 안쪽에 적힌 문자열 내용이 슬래시(/)로 시작하는 Absolute pathname 구조를 띠고 있다면, 놀랍게도 커널은 진행 중이던 기존 lookup 진행 상황 일부를 뒤엎고 프로세스의 root 기준점부터 아예 처음인 것처럼 재탐색을 시작할 수 있다.
open, read, write, close: 터미널 cp 명령어 이면의 기나긴 여정filp_open() / open_namei() / dentry_open()의 앙상블
우리가 터미널 창에서 아무 생각 없이 툭 던지는 cp /floppy/TEST /tmp/test라는 마법의 명령어 한 줄은, 커널의 컴컴한 바닥 밑에서 거대한 톱니바퀴처럼 맞물려 돌아가는 read + write + open + close 시스템 콜들의 기가 막히게 정교한 조합 덩어리입니다. 각 단계마다 도대체 어떤 VFS 핵심 객체들이 땀 흘리며 움직이는지 그 적나라한 내부 민낯을 파헤쳐 봅시다.
sys_open()이 문을 두드립니다 → 사용자 메모리 공간의 경로 문자열을 커널로 퍼 나르는 getname() → 배열의 빈칸을 재빠르게 찜하는 get_unused_fd() → 마침내 진짜배기 엔진 filp_open() 가동 → 이어서 open_namei()가 미친 듯이 Pathname Lookup을 돌리며 권한과 파일 존재 유무를 깐깐하게 따짐 → 무사통과하면 dentry_open()이 텅 빈 file 객체 하나를 신규 할당하고 초기화 세팅 시전 (이때 f_op = i_fop 핵심 복사 완료) → 해당 superblock의 "현재 당당히 열려있는 파일 전체 리스트"에 이름을 올림 → 현재 프로세스 fd 배열의 빈칸에 방금 만든 file 객체 메모리 주소를 쾅 찍어 저장 → 마지막으로 fd 정수표 번호를 유저에게 당당히 반환하며 퇴장.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 객체 메모리 주소를 꽉 움켜쥐게 된다.
2. read()와 write() 시스템 콜 엔진은 다름 아닌 file 객체 정중앙에 떡하니 박혀있는 f_op(file operation) 테이블을 교량 삼아, 각 파일시스템별로 숨겨진 실제 구현 로직 함수를 정확하게 호출해 낼 수 있다.
3. close() 시스템 콜은 그 이름에 걸맞게 파일 조작 세션을 깔끔하게 끝마침과 동시에, 항상 디스크 표면의 실제 물리적인 파일 데이터 찌꺼기까지 한 톨 남김없이 영구 삭제해 버리는 파괴적인 명령이다.
File Locking: 여러 프로세스가 하나의 파일을 두고 아귀다툼을 벌일 때FL_FLOCK / FL_POSIX / 자비 없는 Mandatory Lock
만약 통제 불능의 두 프로세스가 우연히 같은 파일의 정확히 똑같은 오프셋 위치에 동시에 쓰기(write) 폭격을 가한다면, 디스크에 남겨진 최종 결과물은 누구도 예측할 수 없는 끔찍한 쓰레기 데이터로 망가져 버립니다. 이 끔찍한 파국을 막아내기 위해 Unix 계열의 운영체제들은 아주 오래전부터 평화를 수호하는 File Locking(파일 잠금)이라는 훌륭한 동시성 제어 메커니즘을 든든하게 제공해 오고 있습니다.
참으로 신사적이지만 취약한 방식입니다. 다른 프로세스들이 파일 잠금 규칙을 "스스로 얌전히 확인하고 따르겠다"고 협력할 때만 실질적인 방어 효과가 나타납니다. 락 규약을 철저히 지키기로 암묵적 약속을 맺은 성실한 프로세스들 무리 사이에서만 가치가 빛을 발하며, 누군가 약속을 깨고 강제로 I/O를 날리면 커널은 전혀 막아주지 않습니다.
철권통치에 가까운 무자비한 커널 강제형 락입니다. 다른 불한당 프로세스가 감히 open, read, write 시스템 콜을 무단 호출하여 락 구역을 침범하려 들면, 자비 없는 커널이 그 자리에서 멱살을 잡고 차단해 버립니다. 이 가혹한 모드를 발동시키려면 파일시스템 자체 마운트 시 MS_MANDLOCK 옵션의 허가가 필요하며, 타깃 파일의 SGID 비트를 켜고 group-execute 비트를 끄는 복잡한 비밀 의식을 치러야 합니다.
- 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 톱니바퀴 장치가 보이지 않는 수면 아래에서 조용히, 그러나 미친 듯이 맹렬하게 요동치고 있습니다.