본문 바로가기

CS

파일 시스템 구현

파일 시스템은 순수한 소프트웨어다. 다른 가상화들과 다르게 하드웨어가 필요하지 않다. 파일 시스템을 구현하는 다양한 방법이 존재하기 때문에 파일 시스템들이 서로 다른 자료 구조를 갖고 있으며 각각은 장단점이 있다.  파일 시스템에 대해 학습할 때, 두 가지 측면에서 접근하는 게 좋다 그중 하나가 자료구조이고 다른 하나는 접근 방법이다. 파일 시스템이 자신의 데이터와 메타데이터를 관리하기 위해 어떤 종류의 자료구조가 있어야 하는가? 그리고 프로세스가 호출하는 시스템 콜들과 파일 시스템의 자료구조와 어떤 관련이 있는지 어떤 자료구조들이 읽히는지 등을 알아야 한다. 파일 시스템의 자려 구조에 대해 디스크 상의 전체적인 구성을 개발하기 위해서는 디스크를 블록으로 나눠야 한다. 여기서는 단일 블록을 사용하고 크기는 4KB로 정했다.  파일 시스템을 생성하기 위해 블록에 어떤 것을 저장할지 생각해 보자. 먼저 우리가 저장할 데이터가 있다. 그리고 이러한 데이터에 대한 정보인 메타데이터가 있다. 이 메타데이터에는 파일의 크기, 소유자, 접근 권한, 접근과 변경 시간 등 정보들이 있다. 이러한 정보를 아이노드라 하고 이런 아이노드를 관리하는 아이노드 테이블이 있다. 다음으로는 이러한 데이터 블록과 아이노드 블록이 사용 중인지를 표현하는 할당 구조가 필요하다 그래서 각각 데이터 비트맵과 아이노드 비트맵을 만들어서 관리한다. 그리고 나머지 블록을 슈퍼블록으로 할당하고 여기에는 파일 시스템 전체에 대한 정보가 있다. 슈퍼블록은 중요하기 때문에 몇 개 복사해 둔다. 파일 시스템을 마운트 할 때, 운영체제는 우선 슈퍼블록을 읽어 들여서 파일 시스템의 여러 가지 요소들을 초기화한다.

다음은 아이노드에 대해 더 알아보자.  아이노드는 숫자로 불리는데 이 숫자를 사용해 해당 아이노드가 디스크 상에 어디에 있는지를 직접적으로 계산할 수 있다. 아이노드를 설계 시 가장 중요한 결정 중 하나는 데이터 블록의 위치를 표현하는 방법이다. 간단한 방법은 아이노드 내에 여러 개의 직접 포인터를 두는 것이다. 각 포인터는 파일의 디스크 블록 하나를 가리킨다. 이 방법에는 파일 크기에 제한이 있다 파일 크기가 (포인터의 개수) * (블록 크기)로 제한된다.

그렇다면 큰 파일은 어떻게 저정해 야하나?? 그래서 나온 게 멀티 레벨 인덱스이다. 일반적으로 사용되는 방법 중 하나는 간접 포인터라는 특수한 포인터를 사용하는 것이다. 간접 포인터는 데이터 블록을 가리키지 않는다. 간접 포인터가 가리키는 블럭에는 데이터 블럭을 가리키는 포인터들이 저장된다. 직접 포인터와 간접 포인터를 결합해서 사용할 수 있다. 아이노드에는 정해진 수의 직접 포인터, 그리고 하나의 간접 포인터가 있다. 큰 파일에 대해서는 간접 블록이 할당이 되고, 아이노드의 간접 포인터는 이 간접 블록을 가리킨다. 블럭이 4KB이고 디스크 주소가 4 바이트라고 하면 1024개의 포인터들을 추가할 수 있다. 따라서 최대 파일 크기는 (12+1024) * 4K =4144KB가 된다. 하지만 이정도도 큰 파일이 아니다 따라서 포인터를 한번 더 써서 이중 간접 포인터를 추가한다. 이중 간접 포인터가 가리키는 블럭에는 간접 포인터들이 저장되어 있다. 각 간접 블럭은 데이터 블럭을 가리키는 포인터들을 가리키고 있다. 따라서 파일은  4KB * 1024 *1024 , 약 백만개의 4KByte 블럭을 가질수 있다. 즉 4GB가 넘는 크기의 파일을 지원가능하다. 여기다가 더 추가해서 삼중 간접 포인터도 있는데, 요즘 파일시스템들이 주로 이걸 쓴다. 이렇게 되면 디스크 블럭들은 일종의 트리 형태로 구성되어 하나의 파일을 만든다. 약간 치우쳐져 있다. 큰 파일의 경우 파일의 끝부분에 있는 블럭들은 포인터를 세 번 따라가야 실제 블럭을 읽을 수 있다. 왜이렇게 했냐면 대부분의 파일의 크기는 작기 때문이다. 작은 파일은 빨리 읽고 쓸 수 있게 직접 포인터를 갖고 있고, 큰 파일은 간접 블럭을 갖는다.

다음으로 볼 건 디렉터리 구조다. 디렉터리는 (항목의 이름 ,아이노드 번호) 쌍의 배열로 구성되어 있다.여기서 알아야 할 것은 디렉터리 또한 파일이고 아이노드를 갖는것이다. 그대신 구조가 가변 크기 레코드로 구성된 배열이다. 똑같이 데이터 블록을 갖고 있다. 아이노드 또한 아이노드 테이블에 있다. 파일 시스템은 아이노드와 데이터 블럭 사용 여부를 관리해야한다. 이때 비트맵을 탐색하여 갱신한다. 또한 블럭 할당 시 가능하면 여러 개의 블록들이 연속적으로 비어 있는 공간을 찾아서 할당함 이걸 선할 당 정책이라 하는데, 연속적으로 블록이 비어있는 공간을 할당함으로써 해당 파일에 대한 입출력 성능을 개선함.

이제 실행 과정에 대해 알아보자. 파일 시스템이 마운트 되었고 슈퍼블록이 메모리 상에 위치한다고 가정한다. 나머지는 디스크에 존재한다. 간단한 예제로 단순히 파일을 열고 읽고 닫는 상황을 가정함. 디스크에서 가장 먼저 읽을 것은 루트 디렉터리의 아이노드이다. 루트 디렉터리의 아이노드는 Unix에서 2번으로 알려져있다. 파일 시스템은 아이노드 번호 2번을 포함하는 블록을 읽는다. 파일 시스템은 읽어들인 아이노드에서 데이터 블럭의 포인터를 추출한다. 포인터가 가리키는 블럭에는 루트 디렉터리의 내용이 들어 있다. 파일 시스템은 이 포인터들을 사용하여 디렉터리 정보를 읽고 foo 라는 항목을 찾는다.foo파일의 디렉토리 항목을 찾아서 foo의 아이노드 번호를 파악한다. 이 아이노드가 있는 블럭을 읽고 그에 대한 디렉터리 데이터를 읽은 후 bar에 대한 아이노드 번호를 찾는다. 마지막 단계에 open 은 bar에 대한 아이노드를 메모리로 읽어 들인다. 파일 시스템은 최종적으로 해당 파일에 대한 접근 권한을 확인하고, 이 프로세스의 파일 테이블에서 파일 디스크립터를 할당받아 사용자에게 리턴한다. 그 뒤 read 시스템 콜을 통해 파일을 읽음, 첫 읽기는 아이노드를 통해 해당 블럭의 디스크상의 위치를 파악한 후 해당 블럭을 읽는다. 파일을 읽은 후 파일을 마지막으로 읽은 시간을 아이노드에 기록한다. read는 open-file-table에서 해당 파일 디스크립터에 대한 오프셋을 갱신한다. 닫을 때는 할당 파일의 디스크립터를 해제하면 된다.

파일 읽기의 시간 흐름

파일 쓰기는 더 복잡하다. 구체적인 것은 책을 찾아보자

위에서 봤듯이 파일을 읽고 쓰는 것은 많은 I/O를 발생시킨다. 성능개선을 위해 대부분의 파일 시스템들은 자주 사용되는 블럭들을 메모리에 캐싱한다. 캐싱을 하지 않는다면 파일을 여는 동작은 디렉터리 레벨마다 최소한 두 번의 읽기가 필요하다. 초기의 파일 시스템에서는 자주 사용되는 블럭을 저장하기 위해 정적 크기의 캐시를 두었지만 낭비가 많아서 요즘 시스템은 동적 파티션을 사용한다. 운영체제는 가상 메모리 페이지들과 파일 시스템 페이지들을 통합하여 일원화된 페이지 캐시를 만들었다.  캐싱을 하는 경우에 파일 읽기에 대해 알아보면 첫 번째 열기는 디렉터리 아이노드와 데이터로 인해 많은 읽기 I/O를 발생시키겠지만, 그 뒤를 따르는 같은 파일에 대한 파일 열기의 경우 캐시 히트가 돼서 추가 I/O가 필요 없다. 쓰기 캐싱도 보면 쓰기 버퍼링이라고 다수의 쓰기 작업들을 적은 I/O에 일괄처리할 수 있게 한다. 여러 경우들이 있는데, 파일을 생성할 때도 연달아 비트맵에 갱신하는 것보다 두 파일이 생성된 뒤 한 번에 하는 게 효율적이다. 또한 파일을 생성한 후 즉시 삭제하는 경우에도 쓰기를 지연시키면 애초에 디스크에 파일을 생성할 필요가 없다. 위와 같은 이유로 메모리에 지연시간을 둬서 버퍼링 하는데 문제는 디스크에 내려가기 전에 시스템 크래시가 일어나면 정보들이 손실될 수 있다. 그래서 DB 같은 곳에서는 바로바로 디스크에 내리게 한다. fsync를 사용하면 된다. 아니면 캐시를 사용하지 않고 direct I/O 인터페이스를 사용하거나 디스크 인터페이스를 사용하여 파일 시스템 건너뛰고 직접 디스크에 기록한다.

'CS' 카테고리의 다른 글

Pintos Project 4: File Systems  (0) 2021.10.29
크래시 일관성 : FSCK와 저널링  (0) 2021.10.28
RAID  (0) 2021.10.26
하드 디스크 드라이브  (0) 2021.10.25
입/출력 장치 개념  (2) 2021.10.25