
본 글의 내용은 Operating Systems: Three Easy Pieces의 File & Directory 챕터를 정리한 것입니다.
이것에 영구 저장장치라는 중요한 요소가 추가된다. HDD나 SSD와 같은 장치가 데이터를 영구적으로 저장한다.
전원이 끊기면 내용이 휘발되는 메모리와 달리 영구 저장 장치는 데이터를 유지한다.
저장 공간 추상화의 첫번째 요소는 파일이다.
파일은 단순히 바이트의 선형적인 배열로, 각 바이트를 읽고 쓸 수 있다.
또한 숫자로 된 low-level의 이름을 가지는데, 그것이 inode number(i-number)이다.
대부분의 OS는 파일의 구조에 대해서 잘 모르며, 단순히 데이터를 디스크에 저장하고, 로드하는 것을 한다.
저장 공간 추상화의 두번째 요소는 디렉토리로, 파일과 동일하게 inode number를 가지지만, 유저 친화적인 이름과 low-level 이름의 쌍을 보관한다.
예시로 파일 foo 가 있고, i-number가 10이라면 (foo, 10) 이 저장되는 것이다.

디렉토리의 각 항목은 다른 디렉토리가 될 수도 있다. 디렉토리가 다른 디렉토리에 포함되면서 디렉토리 트리(혹은 디렉토리 계층)가 구성된다.
디렉토리는 루트 디렉토리(UNIX에서는 / )를 시작으로 일종의 구분 기호를 통해 하위 디렉토리의 이름이 지정된다.
위 그림의 /foo/bar.txt 와 /bar/foo/bar.txt 처럼 다른 위치에 있는 한 같은 이름을 가질 수 있다.
파일의 이름은 보통 마침표로 구분되는데, 마침표의 앞 부분은 이름, 뒷 부분은 파일의 유형이다. 다만 이것은 관례에 불과하며, 뒷 부분이 파일의 실제 유형을 강제할 수 없다.
결론적으로 모든 파일에 이름을 지정 가능한 파일 시스템의 일관성이 훌륭함을 알 수 있다. 파일 시스템이 디스크, USB, CD-ROM, 기타 장치 등의 파일에 접근할 수 있는 통합된 방법을 제공한다.

파일 생성은 open() 에 여러 파라미터를 전달하는 것으로 가능하다.
위 예시에는 O_CREAT , O_WRONLY 등의 플래그가 전달된다.
O_CREAT : 파일이 존재하지 않으면 생성
O_WRONLY : 파일 쓰기만 가능
O_TRUNC : 파일이 이미 존재하면 기존 컨텐츠 제거
S_IRUSR|S_IWUSR : 권한 지정 (파일을 읽고 쓰도록)
이 open() 의 반환 값은 파일 디스크립터(file descriptor)가 되는데, 이것은 프로세스 별로 private한 정수 값일 뿐이다. UNIX 시스템에서 파일에 접근하는 데 사용된다. 파일을 읽고 쓸 수 있는 권한이 있다면 이것으로 파일을 수정할 수 있다.
즉, 파일 디스크립터는 어떤 작업을 수행하게 해줄 핸들이라고 할 수 있다.


위 코드에서는 echo 의 출력을 foo 파일로 리다이렉션하여, 파일에 hello 라는 단어가 기록된다.
이후 cat 으로 파일의 내용을 확인하는데, 어떤 방식이 활용될까?
리눅스에서는 strace 같은 추적 도구로 그 과정을 확인할 수 있다.

첫째로 파일이 읽기 전용(O_RDONLY)으로 열리고, 64비트 오프셋(O_LARGEFILE)이 사용된다.
그리고 open 함수에 의해 파일 디스크립터 3이 반환된다. (프로세스는 보통 표준 입력, 출력, 오류 전용 파일이 이미 열려있어 0, 1, 2가 선점되어 있다)
read 로 읽기가 시작될 때, 파라미터로 파일 디스크립터, 읽을 문자열, 버퍼 크기가 전달된다. 반환 값은 읽은 길이이다.
이제 write 로 표준 출력에 목표 문자열을 기록한다. 이후 read 의 반환 값이 0이므로 close 로 마무리 된다.
💡 Open file table
각 프로세스는 파일 디스크립터 배열을 보관하고 있다. 그리고 파일 디스크립터는 open file table의 항목(entry)을 참조한다. 이 항목에는 디스크립터가 참조하는 파일, 오프셋, 읽기/쓰기 가능 여부 등의 세부 정보를 제공한다.
lseek 을 사용할 수 있다.
마지막 인자 whence 는 시작 위치를 어떤 방식으로 설정할 지 결정한다. SEEK_SET 이면 offset 이 시작 위치가 되며, SEEK_CUR 라면 현재 위치에 offset 을 더한다. SEEK_END 면 파일 크기에 offset 이 더해진 위치로 설정된다.
read 나 write 는 암시적으로 시작 위치를 업데이트 하며, lseek 은 명시적으로 시작 위치를 업데이트 하는 것이다.

오프셋 값은 보통 file 구조체에 기록된다.
xv6 커널에서는 이런 파일 구조체들의 배열이 하나의 lock과 함께 open file table로 사용되기도 한다.

read 와 비교해보면, read 는 반복적으로 호출됐을 때 오프셋은 읽은 만큼 늘어나게 된다.
read 를 호출하는 경우는 위와 같다.
lseek 을 사용한 경우는 위와 같다.fork() & dup()다른 프로세스가 같은 파일을 읽더라도, open file table에는 유니크한 항목이 생긴다.
그러나 부모 프로세스가 fork 를 사용하여 자식을 만든 경우, open file table을 공유하게 된다. 몇개의 프로세스가 파일을 참조하고 있는지도 기록된다.
dup 을 사용하면 파일 디스크립터를 복사할 수 있고, 특정 파일 디스크립터가 바라보는 파일을 변경할 수도 있다.
fsync() 로 즉시 쓰기프로그램에서 write() 를 호출하는 것은, 사실 미래의 어느 시점에 디스크에 기록해달라고 요청하는 것이다. (성능 상의 이유로 디스크에 기록하는 것을 메모리에 버퍼링 했다가 처리한다)
그래서 데이터가 유실될 수도 있고, DBMS 같은 경우 수시로 디스크에 강제 쓰기를 하는 기능이 필요하다.
UNIX에서는 fsync() 를 호출하면 모든 더티 데이터(디스크에 기록 안 된 데이터)를 강제로 디스크에 기록한다.
경우에 따라서 파일을 관리하는 디렉토리에 fsync() 를 호출해야 될 때도 있다.
터미널에서 mv 명령어로 가능한 파일명 변경은, 원자적으로 처리된다.
즉, 변경되기 전 상태와 변경된 후 두가지 경우만 존재할 수 있다.
파일 시스템은 저장하고 있는 각 파일에 대한 정보, 메타데이터를 보관한다.
특정 파일의 메타데이터는 stat 혹은 fstat 시스템 콜로 확인할 수 있다.
메타데이터에는 파일 크기, 로우 레벨 식별자(inode number), 소유권 정보 등이 기록되어 있다.
UNIX에서 rm 명령어로 가능한 파일 제거는 내부적으로 어떤 시스템 콜이 사용될까?
strace 같은 도구로 확인하면 unlink 가 호출되는 것을 확인할 수 있다. 이 시스템 콜이 호출되는 이유를 파악하려면 디렉토리 또한 이해해야 된다.
디렉토리는 파일 시스템에게 메타데이터로 취급되며, 직접 관리되기 때문에 디렉토리 하위에 다른 파일/디렉토리를 추가 하는 등의 작업이 아니면 직접적으로 수정할 수 없다.
mkdir 시스템 콜로 디렉토리가 생성되는데, 빈 디렉토리라도 내부에 두 항목은 존재한다. 바로 . 와 .. 인데, . 는 자기 자신, .. 는 부모 디렉토리를 의미한다.
ls 의 내부 구조를 확인하면 디렉토리 읽기가 얼마나 단순한지 알 수 있다.
opendir() , readdir() , closedir() 세 가지 호출을 사용하여 작업을 수행한다.
rmdir() 을 통해 가능한데, 하위의 다른 많은 것들도 제거할 수 있기 때문에 디렉토리가 비어있어야만 제대로 동작한다.하드 링크를 통해 파일 제거에 unlink() 가 수행되는 이유에 대해 이해할 수 있다.
link() 로 하드 링크를 만들 수 있는데, 이렇게 만들어진 파일은 같은 inode를 참조하게 된다.
unlink() 를 수행하는 이유를 여기서 찾을 수 있다. 파일이 생성될 때 파일의 크기, 디스크의 블록 위치 등을 추적하는 inode 구조체가 생성되고, 파일에 인간 친화적인 이름을 붙인 뒤 디렉토리에 파일 링크가 추가된다.
이것을 확인하기 위해 하드 링크를 만들고, unlink() 를 사용해보자. 여러 개의 하드 링크 중 하나가 unlink() 를 호출해도 여전히 다른 하드 링크들은 실제 파일에 접근할 수 있다.
파일 시스템은 inode 구조체에 참조 횟수를 기록하기 때문에, 참조 수가 0이 될 때만 실제 데이터 블록도 해제한다. (실제로 삭제한다)
하드 링크는 디렉토리나 다른 디스크 파티션을 대상으로 만들 수 없다.
이를 위해 심볼릭 링크를 사용하며, 아래와 같은 명령어로 생성 가능하다. 생성된 링크를 참조하면 원본 파일의 내용이 출력된다.

stat 명령어로 다음과 같은 출력을 볼 수 있다.
심볼릭 링크는 파일의 경로를 데이터로 저장하기 때문에, 경로가 길면 심볼릭 링크의 크기는 커진다.
심볼릭 링크는 dangling reference가 발생할 수 있는데, 이것은 심볼릭 링크가 가리키는 파일이 제거됐을 때 심볼릭 링크가 더 이상 존재하지 않는 경로명을 가리키는 것을 뜻한다.
파일 시스템은 CPU/메모리 가상화와 유사하게, 사용자에게 리소스를 공유할 수 있게 만들어준다.
그러나 파일이 비공개가 아니라는 점에서 CPU/메모리 가상화와는 다르다고 할 수 있다.
파일 공유의 메커니즘의 첫번째 요소는 권한 비트라고 할 수 있는데, UNIX 시스템에서 파일 정보를 확인하면 보통 다음과 같다.

-rw-r--r-- 로 나타난 첫번째 정보에 권한 비트가 담겨져 있다. 첫번째 문자를 제외한 9개의 문자가 파일(및 디렉토리)에 누가 접근할 수 있는지 결정한다.
이 비트는 소유자/그룹/모든 사람이 할 수 있는 작업에 대한 권한을 결정하며, 권한에는 읽기/쓰기/실행이 포함된다.
위 비트를 예시로 들면 첫 세 비트 rw- 로 사용자가 읽기/쓰기 권한이 있는 것을 알 수 있고, 다음의 r-- 로 그룹이 읽을 수 있음을 알 수 있으며, 다음 r-- 로 모두가 읽을 수 있음을 알 수 있다.
이 비트는 소유자가 chmod 명령어로 변경 가능하고, 전달 인자로 숫자를 사용한다. (이진수이므로 읽기가 4, 쓰기가 2, 실행이 1이다. 이 값들을 더해서 전달하면 된다)
디렉토리의 실행 비트는 파일과 조금 다른데, 실행 권한이 허가된다면 디렉토리 내에 파일을 생성하는 등의 작업을 할 수 있다.
권한 비트 외에도 특정 시스템에서는 디렉토리 별 접근 제어 목록(ACL)을 사용하기도 한다. 이것을 통해 파일을 읽고 읽을 수 없는 사람에 대해 매우 구체적으로 정의할 수 있다. 이런 권한은 권한을 허가받은 사용자들이 변경할 수 있다.
파일 시스템을 만들기위해 대부분의 파일 시스템은 생성 도구(보통 mkfs )를 제공한다.
이것에 디스크 파티션(예: /dev/sdal )과 파일 시스템 유형(ext3 등)을 전달하면 해당 파티션에 루트 디렉토리로 시작하는 빈 파일 시스템을 생성한다.
이렇게 생성된 파일 시스템은 정형화된 파일 시스템 트리에서 접근 가능하도록 수정되어야 하고, 이게 마운트를 통해 이루어진다.
마운트란, 간단하게 존재하는 디렉토리를 마운트 대상 지점으로 삼고, 그곳에 새로운 파일 시스템을 붙여넣는 것이다. (예: 파티션 dev/sda1 에 생성한 파일 시스템을 디렉토리 home/users 에 붙여넣는다)
마운트의 장점은 여러 가지 파일 시스템을 하나의 파일 시스템 트리에 통합할 수 있다는 것이며, ext3 (표준 파일 시스템), proc (프로세스에 접근하는 파일 시스템), tmpfs (임시 파일 전용 시스템) 등 다양한 파일 시스템이 한 컴퓨터의 파일 시스템 트리에 통합된다.
💡 파일 시스템 용어
파일 - 생성/읽기/쓰기/제거가 가능한 바이트 배열. 각자 고유 번호를 가지며, 보통 inode number가 그것이다.
디렉토리 - 튜플 모음. 튜플에는 인간 친화적인 이름과 고유 값이 쌍으로 저장된다. 이 데이터 쌍들은 파일 혹은 디렉토리에 대응하며, 디렉토리는 특수하게 자기 자신과 부모를 가리키는
.,..튜플도 저장한다.디렉토리 (계층) 트리 - 모든 파일 및 디렉토리는 루트부터 시작하여 큰 트리로 구성됨
파일 디스크립터 - 시스템 콜을 통해 OS에게 파일 접근 권한을 부여받아 파일 읽기 및 쓰기에 사용할 수 있는 존재. open file table의 항목(entry)을 참조하는 프로세스 별 비공개 객체이다. open file table의 항목은 현재 읽는 오프셋, 대상 파일 등에 대한 정보를 저장한다.
오프셋 변경 -
read(),write()는 오프셋을 자연스럽게 업데이트하고, 명시적으로lseek()을 사용해 변경할 수도 있다.디스크에 기록 -
fsync()를 호출하면 디스크에 데이터를 강제로 갱신할 수 있다. 그러나 성능과 관련이 크기때문에 신중하게 사용해야 한다.하드 링크, 심볼릭 링크 - 동일한 파일에 접근할 수 있는 여러 개의 링크를 만들 수 있는데, 마지막 하드 링크가 제거 되면 실제로 파일이 제거되며, 심볼릭 링크는 참조하는 파일이 사라지면 dangling reference가 발생한다.
권한 비트 - 파일에는 접근 권한을 설정할 수 있으며, 이것은 권한 비트로 설정 가능하다. 특정 시스템에서는 보다 정교한 접근 제어 목록(ACL)을 사용한다.
다른 가상화처럼 파일 시스템은 데이터의 공유를 돕는다.
파일은 단순 바이트의 선형 배열로, 인간 친화적인 이름과 low-level name(inode number)를 갖는다.
파일 접근에 파일 디스크립터가 사용되며, 이것은 프로세스의 open file table의 항목(entry)을 참조한다.
open file table의 항목에는 참조하는 파일, 오프셋, 접근 권한 등의 정보가 저장된다.
파일 읽기 함수는 암시적으로 offset을 바꾸지만, lseek() 과 같이 명시적으로도 변경할 수 있다.
fork() 로 생성한 자식 프로세스는 부모와 동일한 open file table을 사용한다.
데이터가 변경될 때 디스크에 바로 기록되는 것은 아니다. 그러나 fsync() 로 강제 갱신할 수 있다.
파일 제거는 내부적으로 unlink 시스템 콜이 호출되며, 실제 파일을 참조하는 링크가 0개가 되는 순간 실제로 디스크에서 제거된다.
하드 링크와 심볼릭 링크로 파일을 가리킬 수 있으며, 하드 링크는 동일한 inode를 참조하며, 심볼릭 링크는 파일의 경로만을 참조한다.
권한 비트로 파일 접근 권한을 설정할 수 있다. 특정 시스템에서는 접근 제어 목록(ACL)으로 정교한 접근 권한을 설정한다.
디스크 파티션을 파일 시스템 트리에 연결하는 것을 마운트라고 한다.