파일과 파일시스템

파일 리눅스에서 가장 기본적이고 핵심이 되는 추상화 개념이다. 리눅스는 모든 것이 파일이라는 철학을 따른다. 따라서 모든 인터렉션은 실제로 파일이 아닌 것처럼 보일지라도 파일을 읽고, 쓰는 것으로 이루어진다.

파일 디스크립터(File Descriptor)

파일에 접근하려면 먼저 여러 모드(읽기, 쓰기, 읽기/쓰기) 중 하나로 파일을 열어야 한다. 이렇게 열린 파일은 그 파일에 대한 메타데이터와 연결된 파일 디스크립터를 통해 참조할 수 있다. 리눅스 커널 내부에서 이 기술자를 정수(int)로 표현되며 흔히 줄여서 fd 라고 부른다. 파일 디스크립터는 사용자 영역에서 공유되며 응용 프로그램이 파일에 접근할 때 직접 사용한다.

일반 파일

일반 파일은 바이트 스트림이라고 부르는 연속적으로 나열된 바이트 배열저장된 데이터를 의미한다.

파일은 바이트를 읽고 쓰는 것이 가능하다. 이런 작업은 파일 내부의 위치를 지정해서 수행할 수 있는데, 이 위치는 파일 오프셋(File Offset) 혹은 파일 위치(File Position) 라고 한다. 파일 오프셋은 커널이 열린 파일마다 유지하는 메타데이터의 핵심이다. 파일 오프셋의 특징은 아래와 같다.

  • 처음 열리면 파일 오프셋은 0 이다.

  • 파일 오프셋은 바이트 단위로 증감한다.

  • 파일의 끝을 넘어서는 위치에 바이트를 기록하면 파일 끝에서부터 지정한 위치까지의 내용은 0으로 채워진다.

  • 파일의 끝을 넘어서는 위치에 데이터 기록은 가능하지만, 시작 위치보다 앞선 위치는 불가능하다.

  • 파일 중간에 데이터를 기록하면 전에 있던 데이터를 덮어쓴다.

  • 파일 오프셋은 0부터 시작하며 음수 값이 될 수 없다.

파일 크기는 바이트 단위 측정되는데 이를 파일 길이라고 한다. 즉, 길이는 단순히 파일을 구성하는 바이트 배열의 크기이다. 파일 길이는 잘라내기(Truncation) 연산을 통해 변경할 수 있는데 특징은 아래와 같다.

  • 원래 크기보다 작은 사이즈로 잘라낼 경우, 파일 끝에서 잘라낼 크기만큼 바이트를 제거한다.

  • 기존 크기보다 큰 사이즈로 잘라낼 경우, 파일 끝에 추가되는 새로운 데이터는 0 으로 채워진다.

  • 빈 파일(길이가 0 인)은 아무런 데이터도 포함하지 않는다.

하나의 파일은 다른 프로세스나 심지어 동일한 프로세스에서 한 번 이상 열 수 있고, 파일은 열릴 때마다 고유한 파일 디스크립터를 반환한다. 파일 디스크립터는 하나 이상의 프로세스에서 공유되고 사용될 수 있기 때문에 여러 개의 프로세스에서 동시에 같은 파일을 읽거나 쓰는 것이 가능하다. 이와 같은 동시 접근은 각각의 연산 순서에 따라 다른 결과를 발생시키며 예측이 불가능하다.

inode

파일 inode(Information Node, 아이노드) 라고 하는 파일시스템 내에서만 고유한 정수 값으로 참조된다. inode는 변경된 날짜, 소유자, 타입, 길이, 데이터 저장 위치 같은 파일에 관련된 메타데이터를 저장하고 있지만, 파일 이름은 저장하지 않는다. inode는 유닉스 파일시스템에서 디스크에 저장된 물리적인 객체임과 동시에 리눅스 커널에서 자료구조로 표현되는 논리적인 개념이다.

디렉터리와 링크

사용자 영역에서 파일에 접근할 때는 inode 대신 파일 이름을 사용한다. 디렉터리는 파일에 접근하기 위한 이름을 제공하는데 inode 대신에 사람이 읽을 수있는 이름으로 나타낸다. 이러한 이름과 inode 쌍링크라고 한다. 개념적으로 디렉터리는 일반 파일과 유사한 모습이지만 이름과 inode의 맵핑만 저장한다는 점에서 차이가 있다. 커널은 이런 맵핑을 사용해서 이름으로 inode를 찾는 작업을 수행한다.

사용자 영역 애플리케이션에서 어떤 파일을 열겠다고 요청하면,

  1. 커널은 파일 이름이 포함된 디렉터리를 열고 파일을 찾는다.

  2. 커널은 파일 이름으로 inode 번호를 얻는다.

  3. 이렇게 얻은 inode 번호로 inode를 찾는다.

  4. inode에는 디스크에서 파일의 위치 같은 파일 관련 메타데이터가 있다.

실제 디렉터리도 inode가 있어서, 디렉터리 내부의 링크 역시 다른 디렉터리의 inode를 가리킬 수 있다. 즉, 디렉터리는 다른 디렉터리 내부에 존재할 수 있고 계층적인 구조를 형성할 수 있게 된다.

예를 들어 /home/blackbeard/concorde.png 파일 경로를 열겠다고 요청을 하면, 커널은 해당 파일 경로에 속한 각 디렉터리 엔트리(커널 내부에서는 dentry라고 부른)를 탐색해서 다음 항목의 inode를 찾는다. 순차적으로 위 예의 과정을 살펴보면,

  1. 커널은 /로 시작하고 home을 가리키는 inode를 얻은 다음 blackbeard를 가리키는 inode를 얻는다.

  2. 최종적으로 concord.png의 inode를 얻는다.

  3. (리눅스 커널은 dentry 캐시를 사용해서 디렉터리 찾기 결과를 저장하고, 나중에 일시적인 지역성을 활용해 탐색 속도를 높인다.)

비록 디렉터리를 일반 파일처럼 취급하지만, 커널은 사용자 영역에서 디렉터리를 일반 파일처럼 열고 조작하지 못하도록 제한한다. 따라서, 사용자 영역에서는 커널의 중재를 통해 조작할 수 있는 시스템 콜을 이용해 링크 추가와 삭제를 할 수 있다.

하드 링크

하드링크는 다중 링크로 동일한 inode에 대해 여러 가지 이름을 맵핑한다.

하드 링크는 복잡한 파일시스템 구조에서 동일한 데이터를 여러 경로 이름이 가리킬 수 있게 허용한다. 예를 들어, 특정 데이터를 가리키고 있는 inode는 /home/bluebeard/treasure.txt 와 /home/blackbeard/to_steal.txt 로 하드 링크를 걸 수 있다.

파일 삭제는 파일시스템 내에 다른 하드 링크가 존재할 수 있기 때문에 모든 링크가 삭제될 때까지 파일을 삭제하지 못하도록 보장한다. 각 inode는 파일시스템 내부에 링크 카운터를 두어 자신을 가리키는 링크 개수를 추적한다.

  1. 파일 이름의 링크가 해제되면 링크 카운터가 하나 감소하고,

  2. 링크 카운터가 0이 되면 파일 시스템에서 inode와 관련 자료를 실제로 삭제한다.

$ ln source_file target_file

심벌릭 링크

하드 링크가 다른 파일시스템으로 확장되지 못하는 이유는 inode가 속한 파일시스템 외부에서는 inode 번호가 무의미하기 때문이다. 유식스 시스템은 여러 파일시스템에 걸쳐 사용할 수 있는 심벌릭 링크를 제공한다.

심벌릭 링크는 일반 파일처럼 생겼다. 심벌릭 링크에는 링크로 연결할 파일의 완전한 경로 이름을 포함하는 독자적인 inode와 데이터가 담겨 있다. 이를 통해, 심벌릭 링크는 다른 파일시스템에 들어 있는 파일과 디렉터리는 물론이고 심지어 존재하지 않는 파일과 디렉터리(깨진 링크)를 가리킬 수 있다.

심벌릭 링크는 효과적으로 탐색하기 위해 심벌릭 링크와 링크로 연결된 파일을 모두 다뤄야 하기 때문에 하드 링크보다 많은 오버헤드를 초래한다.

$ ln -s source_file target_file

특수 파일

특수 파일파일로 표현되는 커널 객체이고 리눅스에서는 네 종류의 특수 파일을 지원한다.

  • 블록 디바이스 파일

  • 캐릭터 디바이스 파일

  • 네임드 파이프

  • 유닉스 도메인 소켓

유닉스 시스템에서는 하드웨어 장치에 대한 접근도 파일을 거쳐 실행된다. 이런 파일이 파일시스템에 있는 디바이스 파일인데 일반 파일처럼 동작한다. 디바이스 파일을 열고, 읽고, 쓰는 방식으로 사용자 영역에서 하드웨어 장치로 접근을 허용하여 시스템에 있는 (물리적이거나 가상의) 하드웨어 장치를 조작한다. 유닉스에서 하드웨어 장치는 보통 캐릭터 디바이스블록 디바이스로 나뉘고 하드웨어 종류마다 독자적인 특수 디바이스 파일이 존재한다.

캐릭터 디바이스

캐릭터 디바이스는 바이트로 구성된 선형 큐처럼 접근할 수 있다. 디바이스 드라이버는 큐에 바이트를 집어 넣고, 사용자 영역에서는 큐에 쌓인 순서대로 바이트를 읽어낸다. 읽을 문자가 남아 있지 않으면 디바이스는 EOF를 반환한다. 캐릭터 디바이스의 대표적인 예는 키보드이고 캐릭터 디바이스 파일로 접근한다.

블록 디바이스

블록 디바이스는 바이트 배열로 접근한다. 디바이스 드라이버는 위치 지정이 가능한 장치에 여러 바이트를 맵핑하고, 사용자 영역에서는 이 배열이 포함하고 있는 유효한 바이트에 임의로 접근할 수 있다. 블록 디바이스는 보통 저장장치를 말하는데 하드 디스크, 플로피 드라이브, CD-ROM 드라이브, 플래시 메모리 모두 블록 디바이스에 해당한다. 블록 디바이스 역시 블록 디바이스 파일을 통해 접근한다.

네임드 파이프(First In, First Out : FIFO)

네임드 파이프는 IPC 매커니즘으로 특수 파일을 읽고/쓰는 파일 디스크립터 형태로 통신 채널을 제공한다.

일반 파이프특정 프로그램 출력을 파이프를 잇는 것처럼 다른 프로그램 입력으로 연결하기 위한 방법이다. 시스템 콜로 만들어지는 일반 파이프는 파일시스템이 아니라 메모리에 존재한다.

네임드 파이프는 일반 파이프와 동일하게 동작하지만, FIFO라는 특수한 파일을 거쳐 접근한다. 서로 무관한 프로세스도 FIFO 파일을 접근하는 방식으로 프로세스 사이에 통신이 가능해진다.

소켓

소켓은 서로 다른 프로세스 간에 통신할 수 있는 고급 IPC 종류이며, 같은 머신뿐 만 아니라 다른 머신과도 통신이 가능하다.

파일시스템과 네임스페이스

다른 유닉스 시스템과 마찬가지로 리눅스는 파일과 디렉터리를 나타내기 위한 통합된 전역 네임스페이스를 제공한다. 몇몇 운영체제는 디스크와 드라이브를 분리해서 독립적인 네임스페이스로 할당한다. 예를 들어 플로피 디스크에 있는 파일은 A:\plank.jpg 로 접근하지만, 하드 드라이브는 C:\ 로 접근한다. 유닉스에서는 플로피 드라이브에 있는 파일은 /media/floopy/plank.jpg 라는 경로 이름 혹은 /home/captain/stuff/plank.jpg 로 접근 가능하다. 즉, 유닉스에서는 네임스페이스가 통합되어 있다.

파일시스템은 파일과 디렉터리를 정형적이고 유효한 계층 구조 안에 모아 놓은 것이다. 마운트/언마운트 과정을 통해 파일과 디렉터리라는 전역 네임스페이스 안에 파일시스템을 개별적으로 추가/제거할 수 있다. 네임스페이스에서 정해진 장소를 마운트 포인트라고 하며 개별 파일시스템은 이곳에 마운트된다.

리눅스는 메모리에만 존재하는 가상 파일시스템과 네트워크를 통해 다른 머신과 연결하는 네트워크 파일시스템을 지원하지만, 일반적으로 파일시스템은 물리적으로 존재한다(디스크에 저장된다). 물리적인 파일시스템은 CD, 플로피 디스크, SD 카드 같은 블록 저장 디바이스에 존재한다. 어떤 디바이스는 여러개의 파일시스템으로 나워서 따로 관리하기 위해 파티션을 나누기도 한다.

블록 디바이스의 최소 접근 단위섹터이다. 섹터는 디바이스의 물리적인 속성이다. 섹터는 2의 승수로 나타내 512가 일반적이고 블록 디바이스는 섹터보다 더 작은 데이터 단위를 전송하거나 접근하지 못한다. 모든 입출력은 섹터 하나 이상을 기준으로 일어난다.

물리적인 최소 접근 단위가 섹터라면 파일시스템에서 논리적인 최소 접근 단위블록이다. 블록은 파일시스템을 추상화한 개념으로 파일시스템이 존재하는 물리 매체와 무관하다. 블록은 보통 섹터 크기의 2의 승수이며, 섹터보다 크지만 페이지 크기보다는 작다.

Last updated