본문 바로가기
Concepts/OS

File System

by ocwokocw 2024. 11. 3.

- 출처: Operating System - Three Easy Pieces

 

개요

만약 우리가 File system 을 구현해야 한다면 크게 2가지 측면을 고려해야 한다.

  • Data strcutures: data와 metadata를 구성하기 위해 어떤 유형의 구조로 설계할것인가?
  • Access methods: open(), read(), write() 등과 같은 process에 의한 호출을 어떻게 구조에 맵핑할것인가?

전체 구조

vsfs 파일 시스템의 데이터 구조를 설계한다고 가정해보자. 가장 먼저 해야할것은 무엇인가? 가장 먼저 해야하는것은 disk를 "block" 단위로 나누는것이다. 이 "block"을 4 KB 로 사용한다고 가정하자.

위처럼 block에는 0 ~ N-1 주소가 부여되며 각 block의 크기는 4 KB 이다. 예제의 총 block 수는 64 개라고 가정하자.

 

파일 시스템의 가장 원초적인 기능은 사용자의 데이터를 저장해주는것이다. disk의 대부분의 공간은 이를 위해 사용된다. 이 공간을 "data region" 라고 부르고, 64 block중 56 개의 block을 할당한다.

파일 시스템은 각 파일에 대한 정보를 추적관리해야 한다. 이 정보라는 것은 "metadata"의 핵심으로 파일의 크기, 소유자, 접근 권한, 수정 시간 등과 같은 것들이다. 이런 정보들을 저장하기 위해 파일 시스템은 "inode" 라고 하는 개념을 사용한다.

 

물론 이런 "inode" 들을 위해서도 disk 공간을 사용해야 한다. 이 영역을 "inode table" 이라고 한다. disk 에서 inode는 단순 배열이며, 아래와 같이 5 block을 이를 위해 사용한다고 가정하자.

inode는 일반적으로 256 혹은 512 byte로 그렇게 크지 않다. inode를 256 byte 라고 가정할 때 4 KB 크기의 1 block 이면 16 inode (4096/256) 을 가질 수 있다. 우리는 5 block을 사용한다고 가정했으니 우리의 disk 는 총 16 inode / 1 block * 5 block = 80 inode 를 가질 수 있다.  "총 80개의 inode를 가질 수 있다"는 것은 결국 우리의 작은 파일 시스템이 "총 80개의 파일을 가질 수 있다"는것을 의미한다.

 

또 추가적으로 필요한게 있나 싶겠지만 중요한 구성요소가 더 있다. disk 에서 파일을 생성하거나 삭제할때에 해당 block이 비어있어서 사용해도 되는지 아니면 이미 데이터로 채워져있는지 알아야 한다. 이를 위해 "allocation structure" 라는 구성요소가 필요하다. 해당 구성요소의 구현은 자유롭게 선택해도 된다.

  • list: 첫 free block을 가리키고 해당 block이 다음 free block을 가리키고 이런 패턴이 반복되는 방식
  • bitmap: 각 bit가 block이 사용중(1)인지 비어있는지(0)를 가리키는 방식이며, 하나는 inode를 위한 inode bitmap 사용자의 데이터를 위한 data bitmap 을 각각 할당한다.

위의 그림에서 2번째 block인 i가 inode bitmap 이며, d가 data bitmap 이다. 사실 우리의 예제 파일 시스템은 80 개의 파일제한과 56 데이터 block만 있기 때문에 32 K 개의 객체를 추적할 수 있는 4 KB block을 할당한게 낭비이지만(1 block = 4096 byte 이고, byte가 8 bit 이므로 4096 * 8 = 32,768 개의 bit 를 사용할 수 있어서 32 K 개의 객체가 사용중인지 비어있는지 추적이 가능하다.) 간편화를 위해 1 block 씩 할당한다.

 

이제 1 block 만이 남았다. 이 block 은 "superblock" 으로써 파일 시스템 정보가 포함된다. 예를 들면 얼마나 많은 inode와 data block이 존재하는지, 어디서 부터 inode table 이 시작되는지 등이 담겨 있다.

만약 파일 시스템을 mount 하면 OS 는 superblock을 젤 처음에 읽어서 다양한 정보를 알아내고 volume을 파일 시스템 tree로 붙인다. 그래서 해당 volume 내의 파일들에 접근할 때 disk 상에서 어디로 접근해야하는지 알아낼 수 있다.


Inode

index node 의 준말이며, 파일 시스템에서 가장 중요한 요소들 중 하나라고 할 수 있다. 각 inode는 파일의 저-수준의 이름인 "inumber" 숫자로 참조된다. 이 inumber로 inode 의 disk 상에서의 위치를 계산할 수 있어야 한다. 

 

예를 들어 우리의 예제 파일 시스템에서는 inode table 이 20 KB 크기 (4KB 가 5 block) 이고 12 KB (0 - superblock, 4 - inode bitmap, 8 - data bitmap, 12 - inode table) 부터 시작된다. 

inode number 32 를 읽는다고 가정해보면

  • 우선 inode 구역의 offset 을 계산한다. 우리 파일 시스템에서 inode 1개의 크기는 256 byte 이므로, 32 * 256 = 8192 만큼 떨어져있다.
  • 여기에 disk 상에서 inode table의 시작 offset 인 12 KB 더하면 20 KB 가 inode block의 byte 주소가 된다.
  • 하지만 disk는 byte 단위가 아닌 sector 단위로 읽는다. 1 sector를 512 byte 라고 가정하면 20 KB = 20 * 1024 이고, 이를 512 로 나누면 20 * 1024 / 512 = 40 번째 sector를 읽으면 32 inode 를 포함한 block을 읽을 수 있다.

각 inode 내부에는 파일과 관련된 필요한 정보가 포함되어 있다. 예를 들면 해당 type (일반 파일인지 디렉토리 폴더인지 여부), 크기, 할당된 block의 수, 소유자, 접근 권한, 생성일자, 수정일자 등이 포함되어 있다.


Directory

파일 시스템에서 directory 란 (entry name, inode number) 쌍의 목록이라고 할 수 있다. 예를 들어 inode number 5 인 "dir" 내에 3개의 file 이 존재한다고 가정하면 아래와 같이 나타낼 수 있다.

그렇다면 directory는 정확히 어디에 저장될까? 파일 시스템은 directory를 파일의 특수한 유형으로 간주한다. directory는 inode table의 어딘가의 inode 를 가지며 일반 파일("regular file") 대신 "directory"로 마킹된다.


Free space 관리

파일 시스템은 반드시 어떤 inode 와 data block이 사용가능한(free) 상태인지 추적 및 관리해야 한다. 이를 관리하지 않으면 신규 파일 및 directory 생성시 할당할 공간을 찾을 수 없다.

 

파일 생성시에는 파일을 위한 inode를 할당한다. 파일 시스템은 inode bitmap 을 참조하여 사용 가능한 상태인 inode 를 찾는다. 해당 inode를 사용중인 상태(used)로 마킹한다.

 

data block 도 이와 비슷한 과정을 거치지만 고려할점이 하나 더 있는데 성능을 위해 연속된 공간에 할당해야 한다.


접근 경로

파일 시스템이 mount 되어 superblock이 메모리에 존재하는 상태라고 가정해보자. 이 상태에서 만약 특정 경로에 접근한다면 무슨일이 일어날까?

 

Disk로 부터 파일을 읽을 때에는

  • open("/foo/bar") 를 호출하면 파일 시스템은 우선 root 부터 찾기 시작한다. 파일 시스템은 disk 로 부터 root directory의 inode를 읽는다. root의 inumber는 마치 well-knwon port 처럼 대부분의 UNIX 파일 시스템에서 2 값이다. 그래서 파일 시스템은 inumber 2 를 우선 읽는다.
  • 파일 시스템은 inode 내의 data block에 대한 포인터를 참조하여 root directory의 내용을 읽는다. 예제에서는 "/foo" 를 찾기 위해 이런 과정을 수행한다. "/foo"를 찾으면 이에 대한 inode number 도 알 수 있다.
  • 이런 과정을 재귀적으로 수행한다.
  • "/foo/bar" 에 대한 inode number를 찾으면 open() 을 호출하여 memory로 가져온다. 이 과정에서 파일 시스템은 권한 체크 및 프로세스를 위한 file descriptor 를 할당한다.
  • 파일 close 시에는 file descriptor 할당 해제만 하면 되서 disk I/O 가 발생하지 않는다.

Disk에 쓸 때에도 비슷한 과정을 거치지만 할일이 더 많다. 실제로 disk 에 데이터를 쓰는것뿐만 아니라 파일에 어느 block을 할당할지도 결정해야 한다. 때문에 논리적으로 3번의 I/O 가 발생하는데

  • data bitmap을 읽고 새롭게 할당할 block을 사용중이라고 마킹한다.
  • 새로운 상태를 disk에 반영하기 위해 bitmap에 쓴다.
  • 해당 block에 실제로 쓰기 작업을 수행한다.


Caching and Buffering

위에서 살펴보앗듯 파일의 읽기와 쓰기는 disk 에 많은 I/O를 발생시켜야 하기 때문에 생각보다 비싼 작업이다. 때문에 성능 향상을 위해 파일 시스템은 memory에 이를 캐싱한다.

 

초기 파일 시스템은 전체 메모리의 일부 고정된 크기를 할당해서 캐시를 사용했지만 최근에는 가상 메모리 페이지와 파일 시스템 페이지를 단일 페이지 캐시로 통합하면서 보다 유연성을 증대되었다.

 

처음 파일을 열 때에는 많은 I/O가 발생하지만 다음에 같은 파일을 열면 캐시를 사용하므로 I/O가 발생하지 않는다.

 

쓰기의 경우에는 약간 고려할점이 더 있는데 큰 캐시를 사용하면 읽기 I/O를 회피할 수 있지만 쓰기는 결국 데이터를 disk에 도달시켜야하기 때문에 읽기와 같은 방식으로 캐싱이 불가능하다. 때문에 성능 향상을 위해 write buffering을 사용한다. 쓰기를 지연시켜서 파일 시스템이 더 작은 I/O를 발생시켜서 일괄 처리한다. 만약 어플리케이션이 파일을 생성 후 삭제하면 disk 에 파일 생성 쓰기를 지연시켜서 I/O를 완전히 회피할수도 있다. 다만, 무조건 지연시킨다고 좋은것은 아닌데 지연을 많이 시키면 성능은 향상 되지만 애플리케이션이 예상치 못한 종료 상황에서 데이터를 읽을 확률이 커지게 된다.

'Concepts > OS' 카테고리의 다른 글

Limited Direct Execution  (0) 2024.05.25
Process API  (0) 2024.05.15
Process Abstraction  (0) 2024.05.05
Introduction OS  (0) 2024.05.03

댓글