본문 바로가기
OS

파일시스템의 이해

by 진득한진드기 2026. 5. 2.

1. 가상 파일 시스템 (VFS): 리눅스의 위대한 통합 계층

리눅스는 ext4, XFS, NFS, FAT32 등 성격이 완전히 다른 파일 시스템들을 동시에 지원한다. 사용자가 read() 시스템 콜을 호출할 때, 대상이 하드디스크인지 네트워크 드라이브인지 신경 쓰지 않아도 되는 이유는 바로 VFS(Virtual File System) 덕분이다.

VFS는 일종의 객체 지향적 추상화 계층인데, 커널 내부적으로 파일 시스템이 가져야 할 공통적인 구조체를 정의하고, 실제 파일 시스템은 이 구조체의 함수 포인터(인터페이스)를 자신의 구현체로 채우는 방식이다.

VFS를 지탱하는 4대 객체

VFS는 다음 네 가지 핵심 객체를 중심으로 돌아갑니다.

  1. 슈퍼블록 (Super Block): 특정 파일 시스템 전체에 대한 정보를 담고 있습니다. 파일 시스템의 타입, 크기, 상태, 그리고 루트 inode로 가기 위한 시작점.
  2. 아이노드 (Inode): 파일 시스템 내의 개별 객체(파일, 디렉터리 등)의 실체입니다. 파일 이름만 빼고 모든 메타데이터.
  3. 덴트리 (Dentry - Directory Entry): 파일의 이름과 inode를 연결해주는 객체입니다. 경로 탐색 성능을 높이기 위해 메모리에 캐싱.
  4. 파일 객체 (File Object): 프로세스가 파일을 '열었을 때' 생성되는 객체입니다. 현재 읽고 있는 위치(Offset), 열기 모드(Read/Write) 등 프로세스별 상태를 저장.

2. Inode와 Dentry: 이름과 실체의 분리

리눅스 파일 시스템 설계의 핵심은 "파일명은 파일의 속성이 아니다"라는 점.

Inode (Index Node)

struct inode는 파일 시스템이 파일을 식별하는 고유 번호(inode number)를 키로 가집니다. 여기에는 파일 권한, 소유자, 크기, 수정 시간, 그리고 가장 중요한 "데이터 블록의 위치 정보"가 들어있습니다.

  • 특징: 파일 하나당 무조건 하나만 존재합니다.
  • 역할: 하드웨어 디스크 상의 실제 데이터와 VFS를 잇는 다리 역할을 합니다.

Dentry (Directory Entry)

사용자는 /home/user/test.txt와 같은 경로로 접근합니다. 커널은 이 경로를 해석하기 위해 각 경로 컴포넌트(home, user, test.txt)를 덴트리 객체로 만듭니다.

  • Dcache (Dentry Cache): 파일 경로 해석은 매우 무거운 작업입니다. 디스크를 매번 뒤지는 대신, 한 번 찾은 경로는 덴트리 캐시에 저장.
  • 관계: 여러 개의 덴트리가 하나의 아이노드를 가리킬 수 있습니다. 이것이 바로 하드 링크(Hard Link)의 원리.

3. 프로세스 관점에서의 파일 접근: File Descriptor

프로세스가 파일을 열 때, 커널 내부에서는 복잡한 포인터의 사슬이 엮입니다.

fd (File Descriptor)의 실체

우리가 사용하는 int fd는 사실 프로세스의 파일 기술자 배열(File Descriptor Table)의 인덱스 번호일 뿐입니다.

  1. task_struct: 현재 실행 중인 프로세스 정보.
  2. files_struct: 프로세스가 연 파일들의 목록.
  3. fd_array: 여기서 fd 번호를 인덱스로 사용해 struct file을 찾습니다.
  4. struct file: 프로세스가 현재 파일의 어느 지점을 읽고 있는지(f_pos) 관리하며, 실제 파일의 dentry를 가리킵니다.

이 구조 덕분에 서로 다른 프로세스가 같은 파일을 열어도, 각각 별도의 struct file을 가져서 서로 다른 읽기 위치를 유지할 수 있습니다. 하지만 결국 모두 동일한 dentry와 inode를 바라보게 됩니다.


4. 실제 파일 시스템: Ext4의 블록 관리 매커니즘

VFS가 추상적인 틀이라면, ext4는 이를 디스크에 물리적으로 구현한 결과물.

블록 그룹 (Block Group)

ext4는 디스크를 여러 개의 블록 그룹으로 나눕니다. 이는 데이터 흩어짐(Fragmentation)을 방지하여 디스크 헤더의 이동을 최소화하기 위함이다. 각 그룹은 다음을 포함한다.

  • 슈퍼블록 복사본: 안전을 위해 여러 곳에 저장합니다.
  • GDT (Group Descriptor Table): 블록 그룹의 요약 정보.
  • Data/Inode Bitmap: 어떤 블록과 inode가 비어 있는지 0과 1로 표시하기.
  • Inode Table: 실제 struct inode들이 모여 있는 곳.
  • Data Blocks: 실제 파일 내용이 저장되는 곳.

Extents: 현대적 인덱싱 방식

과거(ext2/3)에는 파일이 커지면 간접 포인터(Indirect pointer)를 주렁주렁 매달아 성능이 떨어졌습니다. ext4는 Extent 방식을 쓴다.

  • "이 파일의 데이터는 100번 블록부터 시작해서 연속된 500개 블록에 들어있다"라고 기록합니다.
  • 장점: 대용량 파일 접근 속도가 비약적으로 향상되고 메타데이터 크기가 줄어듭니다.

5. 저널링(Journaling): 시스템 안정성의 핵심

파일을 쓰는 도중에 전원이 나가면 파일 시스템이 깨질 수 있습니다. 이를 방지하기 위해 ext4는 저널링을 수행합니다.

  1. Journal Write: 데이터를 쓰기 전, "나 이제 어디에 뭘 쓸 거야"라고 일기장(Journal)에 먼저 기록합니다.
  2. Checkpoint: 실제 데이터를 디스크에 씁니다.
  3. Commit: 쓰기가 완료되면 저널에서 기록을 지웁니다.
  • 복구: 전원이 켜졌을 때 저널에 기록은 있는데 완료 처리가 안 되어 있다면, 저널을 보고 작업을 재개하거나 취소하여 무결성을 보장한다.

6. 페이지 캐시 (Page Cache)와 I/O 흐름

성능 극대화를 위해 리눅스는 디스크의 내용을 메모리에 캐싱한다.

  1. read() 호출: VFS는 먼저 페이지 캐시를 확인한다.
  2. Cache Hit: 메모리에 데이터가 있다면 디스크 접근 없이 즉시 반환합니다.
  3. Cache Miss: 데이터가 없다면 파일 시스템의 오퍼레이션을 통해 디스크에서 읽어 페이지 캐시에 올린 뒤 사용자에게 전달합니다.
  4. Dirty Page: 사용자가 데이터를 쓰면 일단 메모리의 페이지 캐시만 수정하고 '더럽다(Dirty)'고 표시합니다. 나중에 커널 스레드(pdflush 등)가 이를 모아서 한꺼번에 디스크에 씁니다(Write-back).

7. 보안 및 고급 제어 지점

이러한 구조를 알면 시스템의 약점이나 제어 지점이 보입니다.

  • LSM (Linux Security Module): inode_operations의 함수들이 호출되기 직전, selinux나 apparmor 같은 보안 모듈이 개입하여 권한을 다시 확인한다.
  • eBPF Hooking: vfs_read나 vfs_write 입구에 eBPF 프로그램을 심으면, 어떤 프로세스가 어떤 파일의 어떤 데이터를 읽어가는지 커널 수정 없이 실시간 모니터링 및 차단이 가능하다.
  • Race Condition: open()과 write() 사이의 덴트리/아이노드 상태 변화를 이용한 심볼릭 링크 공격 등이 이 계층의 이해를 바탕으로 수행되니 알아두면 좋다.

파일 시스템 연결 및 나만의 파일 시스템 만들기 과정은~~?

generic_file_iter() 함수가 특정 파일 시스템에 맞는 디스크 연산 함수를 호추할 수 있도록 지원하기 위해 사용되는 자료구조가 struct inode_operation 이다.

 

이 자료구조는 아이노드 객체의 i_op 변수가 가리키고 있으며, include/linux/fs.h 파일에 정의되어 있는데, 이 자료구조에는 inode와 관련된 연산 즉, create, lookup, link, mkdir, mknod, readpage 등의 연산을 나타내는 변수들로 구성된다.

 

그리고 이 변수에는 각 연산을 수행하는 함수의 시작 주소가 등록된다. 마치 file_operations에 각 파일에 고유한 연산이 등록되어 있는 것처럼, inode_operations에는 각 파일시스템에 고유한 연산들이 등록되어 있는 것이다.

 

만일 요청한 파일이 ext2 파일시스템에 속한 것이라면 ext2_readpage() 함수가 호출된다. 이 함수는 디스크에 저장되어있는 ext2의 inode 구조를 이용하여 데이터를 디스크에서 읽는것.

 

지금 까지 배운 내용을 통해서 사용자가 open(), read(), write() 등을 요청 했을 때 어떻게 처리되는지 알수있었다.

이때 f_op와 i_op가 각각 특징 파일에 고유한 연산, 특정 파일시스템에 고유한 연산으로 제어를 전달하는 진입점 역할을 담당한다.

 

새로운 파일을 만들고 이 파일을 접근할때 호출해야하는 함수를 직접 지정할수있을까?

물론 가능하다. 가장 간단한 바법이 바로 디바이스 드라이버이다. 리눅스에서 새로운 장치를 위한 장치 파이릉 연결하려면 이를 위한 새로운 file_operations 구조를 지정하여 커널에 등록해야하는데, 이게 디바이스 드라이버에서 가능하다.

 

그럼 새로운 파일시스템은 어떻게 리눅스와 연결시킬수 있을까? 파일 시스템을 리눅스에 연결하려면 file_operations 구조 뿐만 아니라, inode_operations 구조도 작성하여 커널에 등록해야한다. 그리고 이들을 커널에 등록하기 위한 커널 내부함수가 register_filesystem() 이다.

 

register_filesystem() , struct_file_system_type 이라는 자료구조를 인자로 받는다.

 

이 자료구조는 include/linux/fs.h에 정의 되어있다.

이 자료구조에는 파일시스템의 이름은 나타내기 위한 name 속성을 위한 fs_flags(파일시슽메이 실제 물리적 장치가 필요한지, 읽기 전용인지 등의 정보), 슈퍼블록을 읽어 파티션을 마운트하기 위한 포인터를 담기 위한 함수 mount, 그리고 복수개의 file_system_type 구조를 연결하기 위한 리스트와 모듈 정보등이 들어있다.

 

register_filesystem()을 이용해 커널에 등록된 파일시스템은 하나의 file_system_type 자료구조를 갖게 되며, 커널에 존재하는 모든 file_system_type 구조들은 리스트로 연결된다. 그리고 이 리스트의 시작은 file_systems라는 커널 내 전역 변수가 가리킨다.

 

특정 파일시스템에 댛나 마운트가 요청되면, 커널은 file_systems에서 시작되는 리스트를 검색하여 요청된 파일시스템의 file_system_type 자료구조를 찾는다. 그리고 get_sb에 기록된 함수를 호출하여 파일시스템의 슈퍼 블록 정보를 얻어와서 VFS의 수퍼블록 객체에 저장해놓는다. 슈퍼 블록을 읽으면 해당 파일시스템의 자세한정보를 얻어 올 수 있다. 따라서 파일시스템이 제공하는 inode_operations이나 file_operations 같은 구조를 접근할 수 있게 되는것이다.

 

결국 새로운 파일시스템을 구현하는 것은 슈퍼블록과 관련된 슈퍼블록 연산, inode_operations 구조체와 관련된 연산, file_operations 구조체와 관련된 연산 등을 작성하는 것이다.