[OS/OSTEP] 40.file-VSFS(Very Simple File System)
# 시작하며
파일 시스템에는 다양한 종류가 있지만, VSFS(Very-Simple-File-System)이라는 말 그대로 매우 간단한 파일 시스템 구조를 살펴볼 것이다. 파일 시스템은 순수한 소프트웨어이다. 하드웨어의 도움을 받았던 CPU가상화와 메모리 가상화와는 그런 의미에서 조금 다른 성격을 가진다.
# 생각하는 방법
파일 시스템은 두가지 측면에서 이해해야한다. 첫번째는 파일 시스템의 자료구조이다. 파일 시스템이 자신의 데이터와 메타데이터(I-node)를 관리하기 위해서 디스크 상에서 어떤 자료구조를 가져야 하는 것일까? VSFS에서는 블럭과 다른 객체들을 배열과 같은 간단한 자료구조에 저장하지만, SGI나 XFS같은 파일 시스템들에서는 복잡한 트리 기반의 자료구조를 사용한다.
두번째 측면은 접근 방법(access method)이다. 프로세스가 호출 하는 open(), read(), write()와 같은 명령들을 실행했을때 어떤 자료구조들이 읽히는 것일까?
# 전체 구성
VSFS의 자료구조를 구현하기 위해서, 디스크를 블럭(Blcok)단위로 나눈다. 일반적으로 사용되는 4KB가 하나의 블럭이 된다. 뒤에서 나오지만 블럭은 섹터(Sector)단위로 한번더 나뉜다. 4KB의 블럭이 64개 있는 256KB의 크기를 가지는 파티션을 생각해보도록 하자.
이제 블럭에 어떠한 것들이 저장 되어야 할지 생각해 보도록하자. 가장 먼저 사용자 데이터가 필요하다. 사용자 데이터는 대부분의 공간을 차지하고, 실질적으로 저장되는 데이터 들이다. 우리가 저장이라는 생각을 하면 파일이 저장되는데 그 때 떠오르는 공간들이라고 생각하면 된다. 이 영역을 데이터 영역(data region)이라고 한다.
이 예에서는 데이터영역을 위해 56개의 블럭을 할당했다. 4KB*56 = 224KB 의 영역이 순수 데이터들의 저장을 위해서 할당 된 것이다.
두번째로 필요한 공간은 메타 데이터를 저장하기 위해 필요했던 I-node 이다. 이 공간의 영역들을 아이노드 테이블이라고 한다.
일반 적으로 아이노드의 크기는 128Byte ~ 256Byte를 가진다. 그렇게 크지 않다. VSFS의 예에서는 256Byte의 공간을 가진다고 가정한다. 그렇다면 한 블락에 아이노드가 16개 들어갈 수 있다. 16개씩 5개의 블락이 있기 때문에 80개의 데이터를 위한 아이노드가 생성이 가능하다. 데이터 영역은 56개였기 때문에 충분히 표현이 가능하다.
아직 3개의 블락이 더 남았다. 앞전 페이징을 공부하면서 디스크가 사용중인지를 표현하기 위해서 비트맵(bitmap)을 사용한다 했는데 그 것을 위해 두개의 블락을 할당한다. 데이터 영역의 사용중임을 알기 위한 데이터 비트맵(data bitmap)과 아이노드영역의 사용중임을 구분하기 위한 아이노드 비트맵(i-node bitmap)으로 나뉜다. 한 블락은 4KB 즉 4096Byte이고 이것은 32Kbit이다 즉 32K개의 사용, 미사용중임을 구분할 수 있기 때문에 VSFS의 예에서는 차고 넘친다.
이제 맨 앞 하나의 블럭이 남았다. 해당 블럭은 슈퍼블럭(superblock)으로 사용된다. 이 블럭은 파일 시스템 전체에 대한 정보를 담고 있다. 파일 시스템에 대한 메타 정보인 셈이다. 몇개의 아이노드(80개)와 데이터 블럭(56개)이 있는지 아이노드 테이블은 어디서 시작하는지, 파일 시스템 자체를 구분할 수 있는 매직 넘버등을 기록해둔다. 슈퍼블럭이 없어지면 해당 파일 시스템에 쓰인 모든 값들이 무용지물이기 때문에 매우 중요하다. 그래서 몇개의 복사본을 저장해두어 관리한다고 한다.
# 파일 구성 - 아이노드(I-Node)
아이노드는 인덱스 노드(index-node)의 줄임말이다. 각 아이노드는 아이넘버(i-number)라고 불리는 식별숫자를 가지고 있다. 이러한 아이넘버를 사용하여 해당 아이노드가 디스크 상에 어디에 있는지를 직접 알 수 있다.
디스크는 섹터단위로 접근을 하는데, 이 섹터는 512Byte의 크기를 가진다. 만약 32번째 I-node를 읽기 위해서는 32*sizeOf(i-node)를 한 다음에 아이노드 테이블의 시작 주소 12KB에 더하면 된다. 32번 아이노드가 존재하는 블럭을 가져오기 위해서는 파일 시스템은 섹터 주소에 대한 읽기 요청을 하여 해당 아이노드 블럭을 가져온다.
아이노드에는 파일에 대한 정보가 다 들어있다. 파일의 종류, 크기, 할당 된 블럭 수, 보호정보, 시간 정보, 데이터 블럭이 디스크 어디에 존재하는지와 같은 정보들이 담겨 있다.
블럭의 위치를 표현하기 위해서 포인터를 사용한다. 이 포인터에는 직접 포인터(direct pointer)를 통해서 직접 디스크 블럭을 관리하거나, 간접 포인터(indirect pointer)방식을 사용해서 멀티 레벨 인덱스 방법을 사용한다.
위와 같이 direct, indirect, double indirect, triple indirect방식을 사용함으로써 큰 용량에 대한 블럭 위치들을 기억할 수 있게 된다.
# 디렉터리 구조
파일의 아이노드를 살펴봤다면, 디렉터리 구조를 살펴봐야한다. 먼저 디렉터리는 저번 시간에서 공부했던거와 같이 <항목의 이름, 아이노드 번호>쌍의 배열로 구성되어 있다.
디렉토리에서 파일이나 폴더에 대한 inum(i-node)가 존재하기 때문에, 해당 파일이나 폴더를 읽기위해서는 해당 i-num을 활용하여 inode를 읽게 된다.
# 실행흐름 - 읽기
파일을 읽고 쓰는 과정에서 파일시스템을 어떻게 활용하는지 살펴보자. 첫 예제로는 /foo/bar을 읽고 닫는 상황을 가정해보자. 경로명을 따라가는 것은 항상 파일 시스템의 루트에서부터 시작한다. " / " 루트 디렉터리는 파일시스템의 최상단이다.
먼저 루트 디렉토리에서 foo 디렉토리를 찾아야 한다. 그렇기 때문에 루트 아이노드를 읽은후, 데이터 블럭으로 이동해서 foo의 아이노드를 찾아온다. 그리고는 foo디렉토리에서 bar를 찾기 위해서 다시, foo의 아이노드에 접근한 후, 데이터 블럭으로 이동해 bar의 아이노드를 찾아온다. bar의 i-node를 찾았다면 이제는 read()가 가능하기 때문에 읽게 된다. 읽은 후에는 마지막으로 읽은 시간을 갱신해야하기 때문에 bar의 아이노드가 변경(wirte)된다.
# 실행흐름 - 쓰기
디스크 쓰기도 비슷한 과정을 밟는다. 먼저 파일을 연후, write()를 호출하여 새로운 내용으로 파일을 갱신한다. 읽기와는 다르게 파일 쓰기는 블럭 할당이 필요하기 때문에 bitmap에 추가적인 쓰기가 발생해야한다. 아래 예제는 /foo/bar를 생성하고 그 안에 세개의 블럭이 쓰이는 과정이다.
먼저 루트 디렉토리에서 똑같이 foo를 찾기 위해 루트의 아이노드에 접근하여 데이터블럭을 읽고 그 속에서 foo의 아이노드를 알아낸다. 이제 foo라는 폴더 안에 bar라는 새로운 파일을 생성해야 한다. 파일 생성전에는 당연히 i-node가 먼저 만들어져야 할 것이다. 그렇기 때문에 foo의 데이터블럭에는 새로 생성될 bar의 아이노드의 위치를 적어야한다. 그전에 아이노드 비트맵을 읽고 빈곳에 할당한다. 그리고 foo의 데이터 블럭에 할당 된 아이노드의 번호를 적은후, bar의 아이노드를 읽은 후 새로 생성한다. bar의 아이노드가 변경된 것을 foo에 수정시간을 반영시킨다.
이제 파일의 위치를 잡았다면, 파일에 데이터를 적어야 한다. 그러기 위해서 생성했던 bar의 아이노드를 읽고, 새로운 블럭을 할당 받기위해 데이터 비트맵을 읽고 갱신한다. 그리고 블럭에 write하고 아이노드에 변경사항을 저장시킨다. 이 과정을 두번 더 반복하여 총 세개의 데이터 블럭에 데이터를 할당한다.
# 캐싱과 버퍼링
파일을 읽고 쓰는데에는 많은 I/O를 발생시킨다. 만약 파일을 여는 동작에 대해 별도로 캐싱을 하지 않는다면, 매번 처음부터 다시 접근해야 할 것이다. 그렇기 때문에 캐싱을 통해서 속도 향상을 야기해야한다. 여기서는 고정된 크기의 캐시를 사용하기도 하지만, 고정된 캐시크기는 메모리 영역을 풀로 사용하지 않으면 낭비되기 때문에, 융통성 있게 동적으로 캐시를 관리하기도 한다.
또한 쓰기 요청을 한번에 처리하지 않고, 모아두었다고 처리하기도 하면서 디스크에 쓸때 들이는 자원의 낭비를 최소화 한다.
# 마치며
파일시스템은 설계가 자유롭기 때문에 파일 시스템마다 최적화가 달라 질 것이다. 지금까지 살펴본 예는 VSFS의 예였다는 것을 명심하자.