본문 바로가기
OS

[TIL 47] 파일의 읽기와 쓰기

by 진득한진드기 2025. 1. 22.

보는입장에서는 우리가 파일을 열고 닫을 때 매우 간단한 동작이라고 착각하기 쉽다.

 

근데 실제로 컴퓨터 내부에는 엄청 많은 일들이 일어난다.

 

만약에 파일이 4KB 크기를 가지고 있다고 해보자.

 

read()

open('/foo/bar', 'O_RDONLY') 시스템 콜을 하면 파일 시스템은 먼저 파일 bar에 대한 아이노드를 찾아서 파일에 대한 기본적인 정보를 획득해야 한다.

즉 파일 시스템은 아이노드를 찾아야한다. 경로명을 따라가는 것은 항상 파일 시스템의 루트에서 시작하며, 루트 디렉토리는 / 로 표시한다.

 

파일 시스템이 디스크에서 가장 먼저 읽을 것은 루트 디렉토리의 아이노드이다. 아이노드를 찾기 위해 i-number를 알아야하는데 이는 파일 시스템이 마운트 될 때 정해진다.

 

대부분의 Unix 파일 시스템은 루트디렉토리의 아이노드 번호는 2이다.

 

파일 시스템은 읽어들인 아이노드에서 데이터 블럭의 포인터를 볼 것이다. 

 

포인터가 가리키는 블럭에는 루트 디렉터리의 내용이 들어 있다. 파일 시스템은 이 포인터들을 사용하여 디렉터리 정보를 읽고, foo라는 항목을 찾는다. 

 

에를 들어 3000개 경우 모든 항목을 하나의 블럭에 저장할 수 없다. 하나의 디렉터리를 표현하기 위해 다수의 블럭이 사용된다.

하나 또는 그 이상의 디렉터리 데이터 블럭을 읽어서 foo에 대한 항목을 찾을 수 있다.

 

foo 파일의 디렉터리의 항목을 찾아서, foo의 아이노드를 파악한다. 

 

다음에는 원하는 아이노드를 찾는다. foo의 아이노드가 있는 블럭과 그에 대한 디렉터리 데이터를 읽은 후에 마침내 bar에 대한 아이노드 번호를  찾아낸다.

 

마지막으로 open()은 bar에 대한 아이노드를 메모리로 읽어 들인다.

 

그 다음 파일 시스템은 접근 권한을 확인하고, 이 프로세스의 openfile-table파일 디스크립터를 할당받아 사용자에게 리턴한다.

 

여기서 나오는 파일 디스크립터는 전에 소켓 프로그래밍 할 때 사용했던 그 파일 디스크립터가 맞다.

 

특정 실행흐름이 정수값을 확인하고 해당 연결된 파일로 이동이 가능한 값..? 이라고 생각하면되는데.... 너무 표현이 어렵나 싶나...

 

open() 이후에는 read() 시스템 콜을 통해 파일을 읽는다.

 

여기서 부터 중요하니까 위에는 머리에 안들어와도 된다면 무시해도 된다.

 

오프셋 0, 첫번째 위치에서 아이노드를 통해 해당 블럭의 디스크상의 위치를 파악한 후 해당 블럭을 읽는다. 첫 번째 읽기 작업이므로 첫 번째 블럭을 읽는다. 파일을 읽은 후 파일을 마지막으로 읽은 시간을 아이노드에 기록한다. 파일 오프셋은 파일을 읽거나 쓸 때, 해당 수행할 위치를 저장하는 변수라고 생각하면 된다.

 

read() 이후 open-file-table에서 해당 파일 디스크립터에 대한 오프셋을 갱신한다. 다음에 읽기 작업을 수행할 때, 이전에 읽었던 다음 위치부터 읽도록 할 것이다.

 

또 어느시점에 되면 해당 파일을 닫아야되는데 생각보다 이거는 별것없다. 할당된 파일 디스크립터를 해제하면 된다. 

 

그냥 흐름만 보면 위에 해당하는 일들이 파일 시스템이 할것이다. 

 

 

 

 

여기서 I/O 발생하는 횟수를 볼 수 있는데 경로 길이에 비례해서 I/O 발생이 생기는 것을 봐양한다.

 

경로가 하나 추가 될때마다 아이노드와 해당 하는 데이터를 읽어야하기 때문이다. 디렉터리의 수 가 많아지면 상황은 더 안좋아진다.

 

여기서 한 블럭만 읽어서 디렉터리의 내용을 얻었지만 디렉터리가 크다면 원하는 항목을 찾기 위해서 많은 데이터 블럭을 읽어야한다.

 

읽기 만으로 많은 연산이 필요하다..... 쓰기는 누가 봐도 더 한 오버헤드가 필요하다.

 

 

Write()

간단한 흐름을 보면 쓰기는 먼저 파일을 열고 write()를 호출하여 새로운 내용으로 파일을 갱신한다. 최종적으로 파일을 닫는다.

 

읽기와 다르게 파일 쓰기는 블럭 할당이 필요로 할 수 있다. 새로운 파일에 쓸 때에는 각 write()는 데이터를 디스크에 기록해야 할 뿐만 아니라 파일에 어느 블럭을 할당할지를 결정해야 하며 그에따라 디스크에 다른 자료구조들을 갱신해야 한다.

 

그러므로 파일에 대한 쓰기 요청은 논리적으로 다섯 번의 I/O를 생성한다.

하나는 데이터 비트맵을 읽기 위해서 또 다른 하나는 비트맵을 쓰기 위해서 그 다음의 두 개는 아이노드를 읽고 쓰기 위해서 마지막은 실제 블럭을 기록하기 위해 I/O가 발생한다.

 

파일의 생성도 꽤 많은I/O를 발생시키는데

 

아이노드 비트맵을 읽고 또 기록하고 새로운 아이노드 자체를 쓰기 위해 다른 하나는 디렉터리의 데이터 블럭에 쓰기 위해 그리고 한 번의 읽기와 쓰기는 디렉터리 아이노드를 읽고 갱신하기 위해 I/O가 발생된다.

 

 

open()을 호출해서 3개의 4KB 쓰기를 하는 동안에 일어나는 I/O 를 보여준다.

 

쓸 때마다 너무 많은 I/O가 일어나고 있다.

 

이는 흔히 allocating write라 하는데 이를 효율적으로 만드는 방법을 생각해봐야한다.

 

캐싱과 버퍼링

위에서 봤듯이 파일을 읽고 쓰는 것은 많은 I/O가 발생한다. 이는 컴퓨터 전체성능에 안좋은 영향을 미친다.

 

성능 개선을 위해서 대부분의 파일 시스템은 자주 사용되는 메모리 블럭들을 DRAM에 캐싱한다.

 

좀전에 봤던 파일을 여는 과정을 생각해보면, 캐싱을 하지 않는다면 파일을 여는 동작은 디렉터리 레벨 마다 최소한의 두 번의 읽기가 필요하다.

 

경로가 많은 수의 디렉터리로 구성된 경우 파일을 여는데 문자 그대로 수백 번의 읽기를 수행한다.

 

초기 파일시스템에서는 자주 사요되는 블럭들을 위해서 캐시를 도입하였다.

 

가상 메모리에서 봤던 것 처럼 LRU와 기타 다른 캐시 교체 정책들이 캐시에 어떤 블럭들을 남길지 결정해야한다.

 

이 고정 크기의 캐시는 일반적으로 부팅 시에 할당되며 전체 메모리의 약 10%정도 자치한다.

 

고정 크기방식을 사용하면 캐시 자체가 목적마저 고정되므로 유동적인 사용이 불가능하다.

 

즉 낭비가 생긴다는 소리이다.

 

그에 반해 현대 시스템은 동적 파티션 방식을 사용한다. 많은 운영체제는 가상 메모리 페이지들과 파일 시스템 페이지들을 통합하여 일원화된 페이지 캐시를 만들었다.

 

이렇게 하면 어느 한 시점에서 어느 부분에 더 많은 메모리가 필요하냐에 따라 파일 시스템과 가상 메모리에 좀 더 융통성 있게 메모리를 할당 할 수 있다.

 

캐싱하는 경우 파일 열기에 대해 생각해보면, 첫 번째 열기는 디렉터리 아이노드와 데이터로 인해서 많은 읽기 I/O를 발생하겠지만 그 뒤에 파일 열기의 경우 캐시 히트가 되서 추가 I/O가 없다.

 

쓰기 캐싱도 비슷하다.

 

캐시가 충분히 크면 대부분의 읽기 I/O를 제거할 수 있다.

 

하지만 쓰기는 영속성 즉 값에 대한 저장을 위해 해당 블럭을 디스크로 내려야 한다. 

 

캐시는 쓰기 시점을 연기하는 역할을 한다. 

 

이를 쓰기 버퍼링(write buffering)이라 한다.

 

쓰기 버퍼링을 통해 쓰기 요청을 지연시켜 다수의 쓰기 작업들을 적은 수의 I/O로 일괄처리 할 수 도 있고, 쓰기 자체를 미룰수도 있다.

 

예를 들어 파일을 생성하고 즉시 삭제한다고 하면 수 많은 임시파일이 생성되고 삭제될 것이다. 이를 lazy하게 지연하면 이럴 일이 없어진다.

 

그래서 대부분의 파일 시스템은 5초에서 30초 동안 버퍼링한다.

 

또 쓰기 버퍼링으로 인한 예기치 않은 데이터 유실을 피하기 위해 fsync()을 사용한다.

 

이를 호출하면 갱신된 내용이 디스크에 강제적으로 기록된다.

 

캐시를 사용하지 않도록 direct I/O 인터페이스를 사용하거나 디스크 인터페이스를 사용하여 파일 시스템을 건너뛰고 직접 디스크에 기록하는 경우도 있다.

 

대부분의 응용 프로그램은 파일 시스템이 제공하는 버퍼링 기능을 사용하지만, 원치 않을 경우 시스템을 원하는 식으로 설정하는 제어 기능들이 있다.

 

이런 이론들을 봤는데 어려운 내용들이지만 이를 통해 파일 시스템의 놀라운 원리들을 조금이라도 이해했으면 됐다고 생각한다.

 

내가 생각하는 파일 시스템이 진짜 흥미로운건 되게 자유롭다는 것이다.

 

여러 파일 시스템이 있지만 그것들을 다 이해하고 습득하는건 어려운 일이겠지만 그것들에 대한 흥미 만 가졌다면 OK라고 생각한다.