운영체제 · 파일시스템 · 이론
파일시스템 — 이름과 실체를 분리한 설계
한 줄 요약
유닉스 파일시스템은 파일의 실체(아이노드)와 이름(디렉터리 항목)을 분리했고, 그 결정 하나가 하드 링크, 열린 파일 삭제, 원자적 교체 같은 동작을 전부 설명한다.
왜 이게 필요했나
파일에는 두 종류의 정보가 있다. 내용과 메타데이터(크기, 권한, 소유자, 수정 시각, 데이터가 어느 블록에 있는지)다. 여기에 이름도 있다. 셋을 한 덩어리로 묶으면 단순하지만, 같은 파일을 두 이름으로 부르거나 이름을 바꾸는 일이 비싸진다.
유닉스의 선택은 메타데이터와 내용은 아이노드에 두고, 이름은 디렉터리에 둔다는 것이었다. 디렉터리는 특별한 파일이며 그 내용은 "이름 → 아이노드 번호" 쌍의 목록이다.
어떻게 동작하나
아이노드에는 파일 크기, 권한, 타임스탬프, 링크 수, 그리고 데이터 블록을 가리키는 포인터가 들어간다. 포인터 구조가 재미있다. 앞쪽 몇 개는 데이터 블록을 직접 가리키고, 그다음은 포인터들의 블록을 가리키는 단일 간접, 그다음은 이중, 삼중 간접이다. 작은 파일은 직접 포인터만으로 끝나 빠르고, 큰 파일도 표현할 수 있다. 계층이 깊어질수록 접근에 필요한 블록 읽기가 늘어나는 대가를 치른다.
이 구조에서 자연스럽게 따라오는 결과들이 있다.
하드 링크는 같은 아이노드를 가리키는 이름을 하나 더 만드는 일이다. 원본과 사본의 구분이 없고 둘은 완전히 대등하다. 아이노드의 링크 수가 하나 늘 뿐이다.
파일 삭제는 사실 이름을 지우는 일이다(그래서 시스템 콜 이름이 unlink 다). 링크 수가 0 이 되고 그 파일을 열어 둔 프로세스도 없을 때 비로소 블록이 회수된다. 그래서 로그 파일을 지웠는데 디스크 여유 공간이 늘지 않는 상황이 벌어진다. 프로세스가 아직 그 파일을 열고 있기 때문이며, 해당 프로세스를 재시작하거나 파일 디스크립터를 닫아야 공간이 돌아온다.
이름 바꾸기(rename) 는 같은 파일시스템 안에서는 디렉터리 항목만 고치므로 원자적이다. 설정 파일을 안전하게 교체하는 표준 방법이 여기서 나온다. 임시 파일에 새 내용을 쓰고, fsync 로 디스크에 내린 다음, rename 으로 갈아 끼운다. 읽는 쪽은 항상 옛 내용이나 새 내용 중 하나를 보고, 반쯤 쓰인 파일은 절대 보지 않는다.
저널링은 크래시 대비 장치다. 메타데이터 변경을 실제 위치에 쓰기 전에 저널에 먼저 기록해 두면, 중간에 전원이 나가도 재부팅 시 저널을 재생해 일관성을 복구할 수 있다. ext4 의 기본 모드는 메타데이터만 저널링하는 ordered 다. 데이터까지 저널링하면 안전하지만 모든 쓰기가 두 번 일어난다.
현장에서 만나는 모습
df 는 여유가 있다는데 쓰기가 실패하는 경우가 있다. 두 가지 중 하나다. 아이노드가 고갈됐거나(df -i 로 확인), 지운 파일을 누가 아직 열고 있어 공간이 회수되지 않았거나다. 후자는 lsof +L1 로 링크 수가 0 인 열린 파일을 찾아낼 수 있다. 로그 로테이션을 잘못 설정해 옛 파일을 지웠는데 애플리케이션이 계속 그 디스크립터에 쓰고 있는 상황이 전형적이다.
이어지는 실습에서 할 것
여기 적힌 것을 전부 손으로 만들어 본다. 아이노드로 같은 파일인지 가려내고, 이름을 뗀 파일을 그대로 읽어 내고, 지운 채 공간을 붙잡고 있는 프로세스를 찾아내는 스크립트를 만든다. 설정 파일을 갈아 끼우는 스크립트도 만드는데, 채점기가 그 파일을 열어 둔 채로 여러분의 교체를 시켜서 읽는 쪽이 반쯤 쓰인 파일을 보는지 확인한다.