VFS란 무엇인가

리눅스는 ext4, XFS와 같은 자체 파일 시스템부터 Microsoft의 NTFS, FAT32, 심지어 NFS와 같은 네트워크 파일 시스템에 이르기까지 방대한 종류의 파일 시스템을 지원한다. 여기에 더해 /proc처럼 커널 데이터를 파일 형태로 보여주는 특수 목적의 가상 파일 시스템도 존재한다. 그렇다면 여기서 한 가지 근본적인 질문이 생긴다. 사용자가 /home 디렉터리(ext4)에 있는 파일을 USB 드라이브(FAT32)로 복사하기 위해 cp file_A file_B라는 간단한 명령을 내렸을 때, 운영체제는 어떻게 이 두 개의 완전히 다른 파일 시스템을 이해하고 작업을 처리할 수 있는가? 이 복잡하고 다양한 파일 시스템의 세계를 조율하는 것이 바로 리눅스 커널의 핵심 구성 요소인 가상 파일 시스템(Virtual File System, VFS)이다.
[출처: https://unix.stackexchange.com/questions/437285/is-the-virtual-file-system-vfs-a-program-or-is-it-just-an-interface]

VFS는 사용자 공간의 애플리케이션과 실제 파일 시스템 구현 사이에 존재하는 추상화 계층이다. 애플리케이션은 open(), read(), write()와 같은 표준 유닉스 시스템 콜(POSIX API)을 사용하여 파일에 접근한다. 그러면 VFS는 이 표준화된 요청을 받아, 해당 파일이 위치한 실제 파일 시스템이 이해할 수 있는 구체적인 명령으로 변환해주는 역할을 한다. 즉, VFS 덕분에 애플리케이션 개발자는 자신이 다루는 파일이 ext4에 있는지, NTFS에 있는지 전혀 신경 쓸 필요 없이 일관된 방식으로 파일 시스템과 상호작용할 수 있다.
[출처: https://embedkari.com/linux-filesystem/]

이 개념을 더 쉽게 이해하기 위해 거대한 도서관에 비유해 본다. 이 도서관에는 여러 개의 동(wing)이 있고, 각 동은 정보를 완전히 다른 방식으로 저장한다. 어떤 동은 현대적인 인쇄 서적(ext4)을, 다른 동은 고대 두루마리(FAT32)를, 또 다른 동은 마이크로필름(NTFS)을, 그리고 네 번째 동은 특정 단말기를 통해서만 접근 가능한 디지털 아카이브(NFS)를 보관하고 있다.

도서관 방문객(사용자 또는 애플리케이션)은 두루마리를 펼치는 법이나 마이크로필름 리더기 사용법을 알 필요가 없다. 그저 중앙 안내 데스크(시스템 콜 인터페이스)에 가서 “문서 번호 1234를 읽고 싶습니다”와 같은 표준화된 요청만 하면 된다. 이때 중앙 안내 데스크의 총괄 사서(VFS)는 종합 목록을 확인하여 문서 1234가 고대 역사 동에 있는 두루마리라는 사실을 파악한다. 그리고 두루마리 취급법을 정확히 아는 전문 사서(FAT32 드라이버)를 호출하여 문서를 가져오게 한다. 방문객은 이 복잡한 내부 과정을 전혀 인지하지 못한 채 원하는 정보의 내용만 전달받는다. 이 비유는 VFS가 다양한 파일 시스템 사이에서 만능 번역가이자 총괄 중재자로서 어떻게 작동하는지를 완벽하게 보여준다.

VFS의 역할은 단순히 디스크 기반 파일 시스템의 차이를 중재하는 것을 넘어선다. 이는 리눅스의 핵심 철학인 “모든 것은 파일이다(Everything is a File)”를 가능하게 하는 근본적인 기술이다. 리눅스는 물리적 장치(/dev/sda), 실행 중인 프로세스에 대한 정보(/proc), 심지어 커널 매개변수까지 모두 파일 시스템의 일부로 표현한다. 예를 들어, /proc 디렉터리는 디스크에 저장된 데이터가 아니라 커널이 실시간으로 생성하는 정보를 담고 있는 ‘특수 파일 시스템’이다. 사용자가 cat /proc/cpuinfo와 같은 표준 명령어로 CPU 정보를 볼 수 있는 이유는, VFS가 이 가상 파일 시스템을 일반 디스크 파일 시스템처럼 보이도록 추상화해주기 때문이다. 이처럼 VFS의 강력한 추상화 능력은 저장된 데이터뿐만 아니라 운영체제의 다양한 자원을 일관된 파일 인터페이스로 제공하는 아키텍처의 초석이 된다.

리눅스의 통합 파일 시스템 구조

VFS가 효율적으로 작동하기 위한 전제 조건은 리눅스만의 독특한 파일 시스템 구조에 있다. C:, D:\ 등 여러 개의 독립된 루트로 시작하는 Windows와 달리, 리눅스는 단 하나의 최상위 디렉터리, 즉 루트 디렉터리(/)에서 시작하는 거대한 역트리(inverted tree) 구조를 가진다. 시스템에 존재하는 모든 파일과 디렉터리는 이 단일한 시작점으로부터 경로를 통해 접근할 수 있다.

이 통합된 트리 구조는 무작위로 구성되지 않는다. 대부분의 리눅스 배포판은 파일 시스템 계층 표준(Filesystem Hierarchy Standard, FHS)을 준수하여 주요 디렉터리의 용도를 표준화한다. 예를 들면 다음과 같다.[출처: https://gyires.inf.unideb.hu/GyBITT/20/ch03s04.html]

  • /bin: 필수적인 사용자 바이너리(명령어)
  • /etc: 시스템 전체의 설정 파일
  • /home: 사용자들의 개인 데이터가 저장되는 홈 디렉터리
  • /var: 로그 파일처럼 수시로 내용이 변경되는 가변적인 데이터
  • /dev: 하드 디스크, 키보드 등 물리적 장치를 나타내는 장치 파일

이러한 표준화는 서로 다른 리눅스 시스템 간에도 일관성과 예측 가능성을 보장한다.

도서관 비유를 다시 가져오자면, 리눅스 파일 시스템은 단 하나의 정문(/)을 가진 거대한 건물과 같다. 방문객은 이 정문을 통해 도서관의 모든 곳으로 이동할 수 있다. 로비에 있는 안내판(FHS)은 ‘역사 동’(/usr), ‘자료실’(/var), ‘관리실’(/etc) 등 각 구역의 위치와 용도를 명확히 알려주며, 이 안내 체계는 대부분의 표준 도서관에서 동일하게 사용된다.

이러한 단일 트리 구조는 단순히 Windows와의 외형적 차이점이 아니다. 이는 자원 관리를 근본적으로 단순화하고 다양한 저장 장치를 매끄럽게 통합하기 위한 핵심적인 설계 철학이다. 모든 자원이 루트에서 시작하는 절대 경로로 표현될 수 있기 때문에, 파일(/home/user/doc)이든 하드 드라이브(/dev/sda)든 상관없이 보편적인 주소 지정 체계를 갖게 된다. 이 보편적인 주소 체계가 바로 다음 장에서 설명할 ‘마운트’ 개념을 가능하게 하는 기반이다. 즉, 어떤 종류의 파일 시스템이든 이 단일 트리 구조의 특정 디렉터리에 연결하여 전체 시스템의 일부처럼 사용할 수 있게 된다. 이는 각 파일 시스템에 새로운 드라이브 문자를 할당하여 이름 공간을 분리시키는 방식보다 훨씬 더 유연하고 통합적인 접근법이다.

다양한 파일 시스템 마운트

리눅스의 단일 디렉터리 트리에 어떻게 서로 다른 파일 시스템이 공존할 수 있는지에 대한 해답은 바로 마운트(mount)라는 개념에 있다. 마운트란 하드 디스크 파티션, USB 드라이브, 네트워크 공유 폴더와 같은 물리적 또는 논리적 저장 장치에 존재하는 파일 시스템을, 기존의 디렉터리 트리 내의 특정 디렉터리에 연결하는 과정이다. 이때 연결 지점으로 사용되는 디렉터리를 마운트 포인트(mount point)라고 부른다.

마운트 과정은 다음과 같이 작동한다. 마운트하기 전, 마운트 포인트는 그저 비어 있거나 기존 파일이 들어있는 일반적인 디렉터리이다. 하지만 mount /dev/sdb1 /media/usb와 같은 명령을 실행하면, /dev/sdb1 장치의 파일 시스템 내용이 /media/usb 디렉터리를 통해 보이게 된다. 이 순간부터 /media/usb 디렉터리에 접근하는 모든 작업은 실제로는 /dev/sdb1 장치에 대한 작업이 된다. 마운트된 동안 마운트 포인트 디렉터리의 원래 내용은 일시적으로 가려져 접근할 수 없게 된다.

구체적인 예를 들어본다.

  1. 시스템의 루트 파일 시스템(/)은 ext4로 포맷된 파티션에 설치되어 있다.
  2. 사용자가 NTFS로 포맷된 USB 드라이브를 삽입한다. 시스템은 이 장치를 /dev/sdc1로 인식한다.
  3. 사용자 또는 자동 마운트 서비스가 mount /dev/sdc1 /mnt/windows_drive 명령을 실행한다.

이제 사용자가 터미널에서 cd /mnt/windows_drive로 이동하거나 파일 탐색기에서 해당 폴더를 열면, USB 드라이브에 있는 파일과 폴더들을 볼 수 있다. ls /mnt/windows_drive 명령은 ls /home과 마찬가지로 자연스럽게 작동하며, 이 모든 변환 과정은 VFS가 뒤에서 투명하게 처리한다.

도서관 비유로 설명하자면, 총괄 사서가 희귀 지도 컬렉션(NTFS USB 드라이브)을 새로 들여오기로 결정한다. 지리학 동에 있는 빈 방 하나(/mnt/windows_drive 디렉터리)를 ‘지도 자료실’(마운트 포인트)로 지정하고 컬렉션을 배치한다. 이제 방문객이 ‘지도 자료실’에 들어가면, 비록 여전히 중앙 도서관 건물 안에 있지만 그 방 안에서는 지도 컬렉션만의 특별한 열람 규칙을 따라야 한다.

이러한 수동 마운트는 시스템을 재부팅하면 사라지는 일시적인 연결이다. 시스템이 부팅될 때마다 특정 장치를 자동으로 마운트하려면 /etc/fstab 파일에 해당 장치와 마운트 포인트 정보를 등록해야 한다. 이 파일은 시스템의 영구적인 파일 시스템 구성을 정의하는 청사진 역할을 한다.

결론적으로 마운트는 사용자에게 보이는 논리적이고 통합된 이름 공간과, 실제로는 제각각인 물리적 저장 장치 현실 사이를 잇는 동적인 연결 고리이다. 그리고 VFS는 이 연결이 실질적으로 기능하도록 만드는 엔진이다. mount 명령어는 단순히 디렉터리를 연결하는 것을 넘어, VFS에게 "이제부터 이 경로 아래의 모든 파일 연산은 기본 ext4 드라이버가 아닌, NTFS 드라이버를 통해 처리해야 한다"고 알리는 지시이다. 이 지시를 통해 VFS는 파일 시스템 간의 경계를 설정하고, 경로에 따라 적절한 드라이버로 작업을 전환하는 컨텍스트 스위칭 메커니즘을 구축하게 된다.

VFS의 네 가지 핵심 객체

VFS가 어떻게 마법처럼 다양한 파일 시스템을 추상화하는지 이해하려면, 그 내부에서 사용하는 네 가지 핵심 데이터 구조, 즉 객체(Object)를 살펴봐야 한다. VFS는 C언어로 작성되었지만, 마치 객체 지향 프로그래밍처럼 이 데이터 구조들을 활용하여 작동한다. 각 객체는 파일 시스템의 특정 측면을 표현하며, VFS가 모든 파일 시스템을 일관된 방식으로 바라볼 수 있게 해주는 틀을 제공한다.[출처: https://stackoverflow.com/questions/7864627/why-do-we-need-directory-structure-for-file-system]

각 객체의 역할과 도서관 비유를 먼저 표로 정리하면 다음과 같다. 이 표는 각 개념을 직관적으로 이해하는 데 도움이 될 것이다.

슈퍼블록 객체 (Superblock Object)

  • 역할: 마운트된 단일 파일 시스템 전체를 대표하는 객체이다. mount /dev/sdb1 명령이 실행되면, 커널은 해당 파티션의 메타데이터를 읽어 메모리 상에 이 superblock 객체를 생성한다.
  • 정보: 파일 시스템의 종류(예: ext4, ntfs), 블록 크기, 전체 블록 수와 같은 최상위 정보를 담고 있다.
  • 핵심 요소 (super_operations): 가장 중요한 것은 s_op라는 이름의 함수 포인터 구조체이다. 여기에는 alloc_inode(새 파일 생성), destroy_inode(파일 삭제), sync_fs(디스크 동기화) 등 해당 파일 시스템에 특화된 핵심 기능들의 주소가 저장되어 있다.
  • 비유: 도서관의 특정 동(wing)에 대한 규칙서와 같다. 총괄 사서는 이 규칙서를 참조하여 “이 동에 새 두루마리를 추가하려면 어떻게 해야 하는가?”(alloc_inode) 또는 "반납된 두루마리를 올바르게 보관하는 절차는 무엇인가?"(destroy_inode)와 같은 해당 동의 고유한 절차를 파악한다.

아이노드 객체 (Inode Object)

  • 역할: 디스크에 저장된 개별 파일이나 디렉터리 하나하나를 표현한다. 모든 파일은 고유한 아이노드를 가진다.
  • 정보: 파일의 이름과 경로를 제외한 모든 메타데이터를 담고 있다. 여기에는 접근 권한, 소유자, 파일 크기, 생성/수정 시간, 그리고 가장 중요하게는 파일의 실제 내용이 저장된 디스크 블록들의 위치를 가리키는 포인터가 포함된다.
  • 비유: 도서 카드와 같다. 이 카드에는 저자, 출판일, 크기, 그리고 책이 꽂혀 있는 정확한 서가 위치(데이터 블록 포인터) 등 책에 대한 모든 정보가 담겨 있지만, 카드 자체에 검색 가능한 형태로 책 제목이 적혀 있지는 않다.

덴트리 객체 (Dentry Object)

  • 역할: 사람이 읽을 수 있는 파일 이름을 아이노드와 연결하는 접착제 역할을 한다. 예를 들어 /home/user/file.txt라는 경로는 'home', 'user', 'file.txt'라는 세 개의 덴트리로 분해된다. 각 덴트리는 이름과 해당 아이노드를 연결한다.
  • 핵심 특징 (캐싱): 덴트리는 경로를 찾는 과정에서 동적으로 생성되며, 성능 향상을 위해 덴트리 캐시(dcache)에 저장되어 재사용된다. 디스크에 영구적으로 저장되는 구조가 아니라는 점이 특징이다. 이는 매우 중요한 성능 최적화 기법이다.
  • 비유: 서가 라벨이다. file.txt라는 라벨이 서가에 붙어 있어 특정 책을 가리킨다. 그 책은 고유한 도서 카드 번호(아이노드)를 가지고 있다. 도서관은 모든 라벨의 위치를 담은 빠른 색인(dcache)을 유지하여, 사서가 매번 서가를 직접 돌아다니지 않고도 신속하게 책을 찾을 수 있도록 돕는다.

파일 객체 (File Object)

  • 역할: 특정 프로세스에 의해 열린 파일을 표현한다. 만약 두 개의 다른 프로세스가 동일한 파일을 열면, 각 프로세스는 자신만의 고유한 file 객체를 할당받는다.
  • 정보: 파일과의 상호작용에 대한 정보를 저장한다. 예를 들어, 파일의 어느 부분까지 읽었는지를 나타내는 현재 읽기/쓰기 위치(커서 또는 오프셋, f_pos), 접근 모드(읽기 전용, 쓰기 전용 등)가 여기에 해당한다. 이 객체는 관련된 dentry 객체를 가리키는 포인터를 포함한다.
  • 비유: 독자의 북마크와 같다. 두 사람이 도서관에서 같은 책(동일한 아이노드)을 빌려 읽더라도, 각자는 자신만의 북마크(file 객체)를 사용하여 개인적인 독서 진행 상황(f_pos)을 기록한다.

이러한 객체들의 설계, 특히 파일의 본질(inode)과 파일의 사용 상태(file)를 분리한 것은 다중 사용자, 다중 태스킹 환경을 위한 매우 정교한 설계이다.

하나의 inode는 동시에 여러 개의 file 객체에 의해 참조될 수 있다. 이것이 바로 여러 프로그램이 동시에 같은 로그 파일을 읽을 수 있는 이유이다. 각 file 객체는 자신만의 파일 위치(f_pos)를 유지하므로, 한 프로그램이 파일의 시작 부분을 읽는 동안 다른 프로그램은 파일의 끝 부분을 읽어도 서로 간섭하지 않는다. 이 아키텍처는 견고한 다중 사용자 운영체제를 구축하는 데 있어 근본적인 역할을 한다.

VFS를 통과하는 시스템 콜 추적

지금까지 설명한 모든 개념을 종합하여, 파일 관련 요청이 VFS를 통해 어떻게 처리되는지 구체적인 시나리오를 통해 단계별로 따라가 본다.

시나리오: 사용자가 터미널에 cat /media/usb/report.docx를 입력한다. 시스템의 루트 디렉터리(/)는 ext4 파일 시스템이고, /media/usb는 NTFS로 포맷된 USB 드라이브의 마운트 포인트이다.

  1. 시스템 콜 발생: cat 애플리케이션은 open("/media/usb/report.docx", O_RDONLY)라는 시스템 콜을 실행한다. 프로그램의 제어권이 사용자 공간(user space)에서 커널 공간(kernel space)의 VFS 계층으로 넘어간다.

  2. 경로 탐색 (ext4의 세계): VFS는 루트 디렉터리(/)부터 경로를 해석하기 시작한다.

    • 루트 디렉터리의 아이노드에서 media라는 이름의 덴트리를 찾는다. 이 경로는 루트 파일 시스템에 속해 있으므로, VFS는 ext4의 superblock 객체에 등록된 함수 포인터(s_op)를 사용하여 이 탐색 작업을 수행한다.
    • 다음으로 media 디렉터리 안에서 usb 덴트리를 찾는다. 이 역시 ext4의 함수를 통해 이루어진다.
  3. 경계를 넘어서는 순간: VFS는 내부적으로 관리하는 마운트 포인트 목록을 확인하고, /media/usb가 마운트 포인트임을 발견한다. 이것이 바로 마법이 일어나는 결정적인 전환점이다. VFS는 이제부터의 경로 탐색에는 다른 ‘규칙서’를 사용해야 한다는 것을 인지한다.

  4. 컨텍스트 전환: VFS는 /media/usb에 마운트된 장치와 연관된 superblock 객체를 가져온다. 이 슈퍼블록은 NTFS 파일 시스템의 메타데이터와 함께, 가장 중요하게는 NTFS 드라이버의 함수들을 가리키는 super_operations 포인터를 담고 있다.

  5. 경로 탐색 (NTFS의 세계): 이제 VFS는 report.docx를 찾아야 한다. VFS는 더 이상 ext4의 탐색 함수를 호출하지 않고, 방금 획득한 NTFS 슈퍼블록의 s_op에 등록된 탐색 함수(예: ntfs_lookup)를 호출한다. 이제부터는 NTFS 드라이버가 제어권을 넘겨받아, 자신의 고유한 디스크 구조(예: MFT - Master File Table)를 해석하여 report.docx의 아이노드를 찾는다.

  6. 객체 생성: NTFS 드라이버가 파일을 찾으면, 그 메타데이터를 VFS가 이해할 수 있는 표준 inode 객체에 채워 넣는다. VFS는 cat 프로세스를 위해 file 객체를 생성하고, 이를 덴트리 및 아이노드와 연결한다. 이 file 객체의 file_operations 포인터는 NTFS의 파일 연산 함수들(예: ntfs_read, ntfs_release)을 가리키도록 설정된다.

  7. 사용자 공간으로의 복귀: open() 시스템 콜은 cat 프로세스에게 파일 디스크립터(file descriptor)라는 간단한 정수 값을 반환하며 종료된다. 이 숫자는 해당 프로세스가 개인적으로 관리하는 '열린 파일 목록'의 인덱스이다.

  8. 파일 읽기: 이제 cat 프로세스는 반환받은 파일 디스크립터를 사용하여 read() 시스템 콜을 호출한다. VFS는 이 디스크립터를 이용해 해당하는 file 객체를 찾고, 그 객체의 file_operations 포인터를 따라가 ntfs_read() 함수를 호출한다. NTFS 드라이버는 디스크에서 실제 데이터를 읽어와 VFS에 전달하고, VFS는 최종적으로 이 데이터를 cat 프로세스에게 전달한다.

아무것도 모르는 애플리케이션: 이 모든 복잡한 과정 속에서, cat 애플리케이션은 그저 표준적인 open()read()를 호출했을 뿐이다. 자신이 두 개의 완전히 다른 파일 시스템을 넘나들었다는 사실을 전혀 알지 못한다. VFS가 모든 변환 과정을 완벽하고 투명하게 처리했기 때문이다.

이 과정의 핵심은 VFS 객체 내부에 존재하는 s_op, i_op, f_op와 같은 함수 포인터들이다. 이는 VFS의 다형성(polymorphism)을 구현하는 핵심 메커니즘이다. VFS 코드는 file->f_op->read(...)와 같이 일반적인 형태로 함수를 호출한다. 이때 실제로 어떤 파일 시스템의 read 함수가 실행될지는, 마운트 시점에 file 객체의 f_op 포인터가 어떤 드라이버의 함수 테이블을 가리키도록 설정되었는지에 따라 동적으로 결정된다. 이는 C++나 Java의 가상 함수(virtual method)와 기능적으로 동일하며, 객체 지향 언어가 아닌 C언어 환경에서 객체 지향 설계 패턴을 우아하게 구현한 대표적인 사례이다.

추상화의 힘

리눅스 가상 파일 시스템(VFS)은 파일 시스템 세계의 숨은 영웅이다. 이는 사용자 공간의 애플리케이션을 물리적 저장소의 복잡성으로부터 완벽하게 분리하는 결정적인 추상화 계층을 제공한다. 사용자와 개발자는 VFS 덕분에 파일 시스템의 종류에 구애받지 않고 일관되고 예측 가능한 환경에서 작업할 수 있다.

VFS는 유닉스 원칙에 기반한 ‘공통 파일 모델(Common File Model)’을 모든 파일 시스템에 강제함으로써, 이질적인 기술들을 하나의 통합된 시스템 아래 조화롭게 묶어낸다. 이러한 아키텍처는 뛰어난 확장성을 제공한다. 새로운 파일 시스템을 리눅스에 추가하고자 하는 개발자는 VFS가 정의한 표준 연산들의 집합을 구현하고 커널에 등록하기만 하면 된다. 기존의 수많은 애플리케이션들은 아무런 수정 없이 즉시 새로운 파일 시스템을 사용할 수 있게 된다.

도서관 비유로 마무리하자면, 총괄 사서(VFS)와 그의 표준화된 요청 처리 시스템, 그리고 전문 사서들(드라이버)은 도서관이 끊임없이 성장하고 새로운 형태의 정보 매체를 받아들일 수 있게 해준다. 이 과정에서 도서관 방문객(사용자와 애플리케이션)들은 새로운 시스템에 적응하기 위해 무언가를 다시 배울 필요가 전혀 없다. 이처럼 견고하고, 확장 가능한 설계야말로 리눅스가 오늘날까지 강력한 적응성과 생명력을 유지하는 핵심 비결 중 하나이다.

profile
Incident Response

0개의 댓글