가상 파일시스템(VFS)은 user-space 프로그램에 제공되는 파일과 파일시스템 인터페이스를 구현하는 커널의 서브시스템이다. 모든 파일시스템은 VFS를 의존한다. 이는 프로그램이 표준 Unix 시스템 콜을 이용해 다른 파일시스템을 읽고 쓸수 있도록 도와준다.
VFS는 파일시스템이나 물리적 수단에 구애받지 않고 open()
, read()
, write()
등의 시스템 콜을 가능하게 한다. 다양한 파일시스템과 미디어에 적용되는 시스템 콜을 제공한다는 점 역시 눈여겨볼만하다. 또한 우리는 표준 시스템 콜을 이용해 파일을 복사하거나 옮길 수 있다.
VFS는 다양한 파일시스템이 하나처럼 동작하도록 하는 추상화를 제공한다. 다음 장에서 다룰 block I/O 레이어 역시 VFS와 함께 추상화, 인터페이스를 제공해 user-space의 프로그램이 파일에 접근하기 위한 시스템 콜을 제공한다.
커널이 추상화 레이어를 low-level 파일시스템 인터페이스를 중심으로 구현하기 때문에 어떤 유형의 파일 시스템이라도 동작하는 인터페이스가 존재할 수 있다. 이 추상화 레이어로 하여금 리눅스가 다른 파일시스템을 지원할 수 있다. VFS는 어떤 파일시스템의 공통된 특징과 행동을 대표하는 파일 모델을 제공한다(Unix 스타일의 파일시스템).
추상화 레이어는 기본 개념적인 인터페이스와 모든 파일시스템이 지원하는 데이터 구조체를 정의함으로써 동작한다. 실질적인 파일시스템 코드는 구현의 세부사항을 가린다. 반면, VFS 레이어와 커널의 다른 부분들에서는 각각의 파일시스템이 동일하게 보인다.
파일시스템이 VFS가 기대하는 추상 인터페이스와 데이터 구조체를 제공하도록 프로그래밍 되었다.
역사적으로 유닉스는 4개의 기본 파일시스템을 제공했다. 관련된 추상화로는 파일, 디렉토리, 엔트리, inode, 그리고 마운트 포인트가 있다.
파일시스템은 데이터의 계층적인 저장공간이다. 파일시스템은 파일, 디렉토리, 연관된 제어정보를 포함한다. 파일시스템에서 수행되는 동작으로는 생성, 삭제, 그리고 마운트가 있다. 유닉스에서는 파일시스템은 전역 계층에 namespace라고 불리는 특정한 마운트 포인트에 마운트 된다. 이렇게 되면 마운트된 모든 파일시스템이 하나의 트리의 엔트리로 보여진다. 윈도우와 DOS와 같은 경우 namespace를 드라이브 문자로 분리한다.
파일은 정렬된 바이트 문자열이다. 첫번째 바이트는 파일의 시작을 표시하고, 마지막 바이트는 파일의 마지막을 표시한다. 각각의 파일은 사람이 읽을 수 있는 이름이 할당되어 시스템과 유저가 구별할 수 있다. 파일에서 공통된 동작은 읽기, 쓰기, 생성하기, 그리고 삭제하기이다. 파일의 Unix 개념은 레코드 기반의 파일시스템(OpenVMS의 파일-11)과 완전히 다르다. 레코드 기반의 파일시스템은 파일에 대한 더 풍부하고 구조화된 표현을 제공하여 더 간단하고 유연하다.
파일은 디렉토리에 정리되어있다. 디렉토리는 폴더와 유사하고, 보통 관련된 파일을 포함한다. 디렉토리는 다른 디렉토리(서브 디렉토리)를 포함할 수 있다. 이런 방식으로 디렉토리는 중첩되어 경로를 형성할 수 있다.
/home/wolfman/butter
이 경로에서 /는 루트 디렉토리, home과 wolfman은 디렉토리, butter는 파일이고, 각각이 dentry라고 불리는 디렉토리 엔트리이다. Unix에서 디렉토리는 사실 포함하고 있는 파일들의 리스트에 대한 일반 파일이다. VFS입장에선 디렉토리도 파일이기 때문에 파일에 행해지는 동일한 작업들이 디렉토리에도 수행될 수 있다.
Unix 시스템은 파일의 개념을 접근권한, 크기, 소유자, 생성시간 등 연관된 정보와 분리한다. 이런 정보들은 파일 메타데이터라고 불리고, 파일과 분리된 데이터 구조체인 inode(index node)에 저장된다.
모든 정보는 파일시스템의 제어 정보와 묶여있고, 이는 superblock에 저장되어있다. Superblock은 파일 시스템 전체의 정보를 포함한 데이터 구조체이다. 가끔 집합 데이터는 파일시스템 메타데이터로 여겨진다. 파일시스템 메타데이터는 개별파일과 파일시스템 전체에 대한 정보를 포함한다.
전통적으로 Unix 파일시스템은 이 개념들을 물리 디스크 레이아웃의 부분으로 구현했다. 예를 들어 파일 정보는 디스크의 구분된 블록안의 inode로써 저장되었고, 디렉토리는 파일로 처리되었고, 제어정보는 superblock 중앙에 저장되었다. Unix 파일 개념은 저장매체에 물리적으로 매핑하는 것이다. 리눅스의 VFS는 이런 개념들을 이해하고 구현한 파일시스템과 호환되기 위해 디자인되었다. Unix의 파일시스템이 아닌 FAT 혹은 NTFS는 이런 개념의 생김새에 대해 제공해야한다. VFS의 요구조건을 충족하고 Unix의 패러다임에 대처하기 위해 non-Unix 파일시스템에서 즉석으로 특별한 처리를 진행한다.
VFS는 객체 지향적이다. 데이터 구조체군은 일반 파일 모델을 표현한다. 이 데이터 구조체는 객체와 유사하다. 커널이 객체 지향 패러다음을 직접 지원하지 않는 엄격하게 C로 프로그래밍되었기 때문에, 데이터 구조체는 C 구조체로 표현된다. 구조체는 데이터와 파일시스템에서 구현한 함수들에 대한 포인터로 구성되어있다. 다음 4가지 주요 객체가 있다.
VFS가 디렉토리를 일반 파일로 처리하기 때문에 디렉토리 객체가 따로 없다.
Operation 객체는 위의 각각에 포함되어있으며, 메소드를 표현한다.
operation 객체는 부모 객체에 동작하는 함수들의 포인터로 구현되었다. 많은 메소드에서, 기본 기능이 충분하다면 객체는 일반 함수를 상속할 수 있다. 그렇지 않으면 특별한 파일시스템의 특정 인스턴스는 자신의 파일시스템에 특정된 메소드 포인터를 채워넣는다.
본문에서 객체는 C++과 Java의 클래스와 같은 것이 아닌 구조체(struct)를 의미한다.
등록된 파일시스템 각각은 file_system_type
구조체로 표현된다. 이 객체는 파일시스템과 그 능력에 대해 묘사한다. 게다가 각각의 마운트 포인트는 vfsmount
구조체로 표현된다. 이 구조체는 위치 및 마운트 플래그와 같은 마운트 포인트의 정보를 포함한다. 마지막으로 두개의 프로세스별 구조체는 프로세스에 연관된 파일시스템과 파일에 대해 묘사한다. 각각 fs_struct
구조체와 file
구조체이다.
Superblock은 각각의 파일시스템에서 구현되고 그 파일시스템을 묘사한 정보를 저장하기 위해 사용된다. 디스크 기반이 아닌 파일시스템은 superblock을 즉석으로 생성하고 메모리에 저장한다.
Superblock 객체는 <linux/fs.h>의 super_block
구조체로 표현된다. Superblock 객체는 alloc_suepr()
함수에 의해 생성되고 초기화된다. 마운트되면 파일시스템은 이 함수를 실행시킨다.
Superblock 객체의 s_op가 superblock 관련 함수들의 포인터 테이블을 나타낸다. Superblock 조작 테이블은 super_operations
구조체로 표현되며 <linux/fs.h>에 명시되어있다.
C가 객체 지향 관련 지원이 부족하기 때문에 다음과 같이 코드가 사용된다.
// C++이라면
sb.write_super();
// C이기 때문에
sb->s_op->write_super(sb);
super_operations
의 모든 함수는 프로세스 컨텍스트에서 VFS에 의해 실행된다. dirty_inode()
를 제외하면 block할 수 있다.
Inode 객체는 커널이 파일이나 디렉토리를 조작하기 위해 필요한 정보들을 나타낸다. Unix스타일의 파일시스템에서 이 정보는 디스크의 inode로부터 읽는다. 만약 파일시스템이 inode를 가지고 있지 않다면, 파일시스템은 디스크에 저장된 모든 곳에서 정보를 가져와야한다. Inode가 없는 파일 시스템은 파일에 특정된 정보를 파일의 일부로 저장한다. 몇몇의 현대 파일시스템은 파일 메타데이터를 디스크 상의 데이터베이스의 일부로 저장한다. 어떠한 경우이던, inode 객체는 메모리에 어떤 방법으로든 구성된다.
Inode 객체는 inode
구조체로 표현되며 <linux/fs.h>에 정의되어있다.
Inode는 파일시스템의 각각의 파일을 표현하지만, inode 객체는 파일이 접근되어야만 메모리에서 구성된다. 이는 기기 파일이나 파이프 같은 특별한 파일도 포함한다. 결론적으로 몇몇의 inode 엔트리는 이 특별한 파일들에 연관되어있다.
주어진 파일시스템이 inode 객체에 표현된 속성을 지원하지 않을 수도 있다. 그런 경우 파일시스템은 해당 기능들을 구현함에 있어 자유롭다.
VFS가 inode에 실행할 수 있는 파일시스템의 구현된 함수를 묘사한다.
i->i_op->truncate(i);
inode_operations 구조체는 <linux/fs.h>에 정의되어있다.
VFS는 디렉토리를 파일과 동일하게 처리한다. VFS는 디렉토리에 특화된 동작(경로 확인)을 수행해야할 때가 있다. 이를 해결하기 위해 VFS는 디렉토리 엔트리(dentry)라는 개념을 이용한다. Dentry는 경로상의 특정 컴포넌트이다. 예를 들어 /, bin, vi가 모두 dentry 객체이다. /와 bin은 디렉토리고, vi는 일반 파일이다. 경로를 분해하고 컴포넌트로 접근하는 것은 중요하고 시간이 소요되며 무겁다. Dentry 객체는 이 과정을 쉽게 만든다.
Dentry는 마운트 포인트를 포함할 수도 있다. /mnt/cdrom/foo 라는 경로에서 컴포넌트 /, mnt/ cdrom, foo는 dentry 객체이다. VFS는 디렉토리 동작을 수행할 때 즉석에서 dentry 객체를 만든다.
<linux/dcache.h>에 dentry 구조체가 정의되어 있다.
VFS가 경로 문자열로부터 즉석으로 만들기 때문에 대응되는 디스크 상의 데이터 구조체가 없다. Dentry 객체가 물리적으로 디스크에 저장되지 않기 때문에 dentry 구조체에는 객체가 수정되었는지를 나타내는 플래그가 없다.
Dentry의 상태는 used, unused, negative의 세가지 상태중 하나이다.
Used dentry는 유효한 inode에 대응되며, 객체를 이용하는 한명 이상의 사용자가 있음을 나타낸다. Used dentry는 VFS에 의해 사용중이고 유효한 데이터를 가리키기 때문에 버려질 수 없다.
Unused dentry는 유효한 inode에 대응되나, VFS가 현재 사용중이지 않은 객체이다. 여전히 dentry가 유효한 객체를 가리키고 있기 때문에 캐싱되어있다. Dentry가 바로 삭제되지 않기 때문에 미래에 필요하다면 새로 생성할 필요 없고, 경로명을 탐색하는 과정이 더 빨라질 수 있다. 다만 메모리가 부족한 상황이라면 dentry가 버려질 수 있다.
Negative dentry는 해당 inode가 지워졌거나 경로명이 올바르지 않아서 유효한 inode와 연관이 없다. Dentry는 미래의 탐색이 빠르게 해결되도록 보존된다. 이 역시도 메모리가 부족하면 삭제될 수 있다.
VFS 레이어가 경로명을 dentry 객체로 풀어내고 경로의 마지막에 도착한 후, 모든 작업들을 버려버리는 것은 낭비이다. 대신, 커널은 dcache라는 dentry 캐시에 객체를 캐싱한다. Dcache는 세 부분으로 나누어져있다.
해시 테이블은 dentry_hashtable 배열로 표현된다. 각각의 요소는 dentry list의 포인터이다. 실질적인 해시값은 d_hash()
에 의해 결정된다. 해시 테이블 탐색은 d_lookup()
에 의해 실행된다.
Dcache는 icache(inode 캐시)에 프론트엔드를 제공한다. Dentry 객체와 관련된 inode 객체는 dentry의 사용 카운트를 양수로 유지하기 때문에 해제되지 않는다. 이는 dentry 객체가 inode를 메모리에 고정시켜놓을 수 있도록 한다. Dentry가 캐시되면, 대응되는 inode 역시 캐시된다.
Dentry와 inode를 캐시해놓는 것은 상당히 유용한데, 파일 접근이 spatial locality와 temporal locality 모두를 보이기 때문이다.
<linux/dcache.h>에 dentry_operations 구조체가 정의되어있다.
VFS의 마지막 주요 객체는 파일 객체이다. 파일 객체는 프로세스에 의해 열린 파일을 표현한다. 프로세스는 파일을 직접 다룬다.
파일 객체는 열린 파일의 메모리상의 표현이다. open()
시스템 콜의 응답으로 객체가 생성되고, close()
시스템 콜의 응답으로 삭제된다. 여러 프로세스가 한 파일을 동시에 열고 조작할 수 있기 때문에, 같은 파일에 대한 여러 파일 객체가 존재할 수 있다. 파일 객체는 열린 파일을 프로세스의 관점에서 표현한 것일 뿐이다.
file 구조체는 <linux/fs.h>에 정의되어있다. Dentry 객체와 비슷하게 파일 객체는 디스크 상의 데이터와 대응되지 않는다. 그러므로 객체가 dirty하고 디스크에 다시 작성해야함을 나타내는 플래그가 존재하지 않는다. 파일 객체는 관련된 dentry 객체를 f_dentry 포인터를 통해 가리킨다. Dentry는 또한 연관된 inode를 가리키며, inode가 파일이 dirty한지 반영한다.
파일 관련 동작들은 file_operations 구조체에 정의되어 있고, 이 구조체는 <linux/fs.h>에 정의되어있다.
파일 시스템은 각각의 동작에 유일한 함수를 구현할 수 있고, 아니면 존재하는 일반적인 방법을 사용할 수 있다.
커널은 기본 VFS 객체뿐만 아니라 다른 표준 데이터 구조체를 이용해 파일시스템의 데이터를 관리한다. 리눅스가 다양한 파일시스템을 지원하기 때문에 커널도 각각의 파일시스템에 맞는 구조체를 가지고 있어야한다. file_system_type
구조체를 이용하며, <linux/fs.h>에 정의되어 있다.
get_sb()
함수는 수퍼블럭을 디스크에서 읽고 수퍼블럭 객체를 파일시스템이 로드될 때 이동시킨다. 파일시스템당 file_system_type
은 하나만 존재한다.
파일시스템이 마운트 되면 vfsmount
구조체가 생성된다. 이는 특정 인스턴스(마운트 포인트)를 표현한다. vfsmount
는 <linux/mount.h>에 정의되어 있다.
각각의 프로세스는 열은 파일목록, 루트 파일시스템, 작업중인 디렉토리, 마운트 포인트 등을 가지고 있다. files_struct
, fs_struct
, namespace
의 세 데이터 구조체가 VFS 레이어와 엮인다.
files_struct
는 <linux/fdtable.h>에 정의되어있다. fd_array라는 배열은 열린 파일 객체의 목록을 가리킨다.
fs_struct
구조체는 파일시스템의 정보를 담고있으며, <linux/fs_struct.h>에 정의되어있다. 현재 디렉토리 및 루트 디렉토리 등의 정보를 포함한다.
namespace
는 <linux/mnt_namespace.h>에 정의되어있다. 리눅스 커널 2.4버전부터 프로세스별 네임스페이스가 추가되면서 각각의 프로세스가 마운트된 파일시스템에 유일한 시점을 제공할 수 있었다.