[OS/OSTEP] 39.interlude-file-directory
# 시작하며
교재의 순서대로라면, 쓰레드가 끝나고 새로운 단원이 시작하고 36챕터부터 새로운 단원이 시작된다. 그러나 현재 수업중인 교수님께서는 39 -> 40 -> 37 순서로 강의를 진행 예정이시다. 또한 36을 생략하셨다. 37을 공부하면서 36의 개념이 필요하다면 별도로 공부해야 할 것 같다.
메모리는 비영속적이기 때문에 전원이 없어지면 저장해두었던 정보들이 없어진다. HDD와 SDD같은 저장장치는 영구적으로 정보를 저장한다. 이번 챕터에서는 그러한 영속데이터를 관리하기 위한 UNIX 파일 시스템에대한 API에 대해서 공부한다.
# 파일과 디렉토리
저장 장치의 가상화에대해서 파일과 디렉토리라는 주요 개념이 개발 됐다. 파일은 단순히 읽거나 쓸 수 있는 순차적인 바이트들의 배열이다. 각 파일은 저수준의 inode-number라는 이름을 가지고 있다. 이 inode-numberE는 숫자로 표현되지만 사용자는 그 이름에 대해서 잘 알지 못한다. inode-number는 어떠한 inode에 대한 ID값이라고 생각 할 수있다. 이 inode에는 메타데이터라는 것이 담겨 있으며, 블락단위로 HDD or SDD에 저장되는 파일들을 포인터로 가르키고 있다고 생각하면 된다. 이 섞여 있는 것들을 한 파일을 위한 inode를 읽으면 종합해서 어떠한 파일인지를 알고, 각각의 블럭들을 불러올 수 있다. 현재로써는 각 파일은 inode-number와 연결 돼 있다고 생각하자.
디렉토리또한 파일과 같이 저수준의 이름 inode-number를 가진다. 하지만 파일과 다르게 디렉토리의 내용은 구체적으로 어떻게 들어가야할지 정해져 있다. <사용자가 정한 이름, 저수준의 inode-number>이다.
위와 같이 디렉토리별 inode가 존재하고 그 inode안에 하나의 블락이 잡힌다. 폴더구조를 관리하기 위해서는 그렇게 큰 크기가 필요하지 않는다고 한다. 그리고 그 블럭안에 <사용자가 정한 이름, 저수준의 inode-number>와 같이 저장되면서 폴더 디렉토리를 결정한다. 이런 것을 디렉토리 트리(directory tree)또는 디렉토리 계층(directory hierarchy)라고 한다. 하나의 폴더에는 기본적으로 자신의 디렉토리( ./ ) 와 부모 디렉토리( ../)을 가리키는 것이 기본적으로 포함된다.
또한 파일들을 보면 파일명이 두 부분으로 구성되어 있다는 것을 알 수 있다. 예를 들면 foo.text와 foo.c, foo.py와 같이 <이름.확장자> 형태로 파일명을 짓는다. 하지만 이러한 것들은 관용적인 표현일 뿐이다. foo.c라고해서 내용이 반드시 C소스 코드일 필요는 없는 것이다. 단지 우리가 사용하는 프로그램들이 읽을때 이 확장자를 토대로 해석하기 때문인 것이다.
# 파일의 생성
open()시스템콜을 활용하면 새로운 파일을 생성 할 수 있다.
다음과 같이 open()을 호출하면서 O_CREAT라는 플래그를 전달하면 새로운 파일을 만들 수 있다. 위의 경우에는 foo라는 파일을 현재 디렉토리에 생성한다. O_WRONLY플래그는 파일이 열렸을 때 쓰기만 가능하도록 플래그를 설정한다. O_TRUNC플래그는 파일이 이미 존재할 때는 파일의 크기를 0byte로 줄여서 기존 내용을 모두 삭제한다.
open()은 파일 디스크럽터(file descriptor)을 리턴한다. 파일 디스크럽터는 프로세스마다 존재하는 정수이다. UNIX시스템에서 파일을 접근하는데 사용한다. 파일 디스크럽터를 파일 객체를 가리키는 포인터라고 볼 수도 있다. 그러한 객체를 생성하면, read() 또는 write()와 같은 다른 메소드로 파일에 접근 할 수 있다.
# 파일의 읽기와 쓰기
이미 존재하고 있는 파일은 cat이라는 프로그램을 사용하면 파일의 내용을 화면에 보여준다.
echo의 출력을 foo로 전송하여 그 파일에 "hello"를 저장하도록 한다. 이후에 cat 명령어를 사용하면 hello가 출력됨을 알 수 있다. 리눅스의 strace와 맥OS의 dtruss를 통해서 어떠한 시스템콜들이 호출됐는지를 살펴 볼 수 있다.
가장 처음으로 open을 통해 파일을 연다. O_RDONLY플래그를 통해서 읽기만 가능토록, O_LARGEFILE플래그를 통해 64bit오프셋을 사용하도록 설정한다. 이후 3이라는 파일 디스크럽트를 얻어냈다. 이것이 3인이유는 보통 STDIN(입력 0) , STDOUT(출력 1), STDERR(에러 2)로 기본적으로 프로세스가 실행하면 디스크럽트가 열리기 때문에 그 다음 번호 3을 부여받는다.
열기에 성공하면 read()시스템 콜을 사용하여 파일에서 몇바이트씩 반복적으로 읽는다. 첫번째 인자는 어떤 파일을 읽은 것인지 파일 디스크럽트를 인자로 주어 알려준다. 두번째 인자는 read()결과를 저장할 버퍼를 가리킨다. 위 예제에서는 strace로 읽은 결과인 "hello"를 두번째 인자 위치해 표시하였다. 세번째 인자는 버퍼의 크기로써 여기서는 4096BYTE(4KB)이다. read가 6을 반환한 이유는 마지막 "\n"문자열이 끝났음을 알려주는 문자를 포함해야하기 때문이다. 이후 write()에서 파일 디스크럽트1 표준 출력(STDOUT)을 줌으로써 터미널에 표시되도록 한다. 그리고 read를 시도하지만 더이상 읽을 것이 없기때문에 끝나게 된다.
# 비순차적 읽기와 쓰기
지금까지 모든 읽고 쓰는 과정은 순차적이었다. 그렇지만 파일의 특정 오프셋부터 읽거나 쓰는것이 유용할때가 있다. 예를들어 문서의 인덱스를 만들고 특정 단어를 찾는다고 가정하면, 문서내의 임의 오프셋에서 읽기를 수행해야 할 것이다. 이 것을 위해서 lseek()이라는 시스템 콜을 사용한다.
첫번째 인자는 파일 디스크럽터이며, 두번째 인자는 offset으로 파일의 특정위치(file offset)을 가리킨다. 세번째 인자는 whence라고 부르며 탐색 방식을 결정한다.
즉 두번째 인자로 fp의 시작위치, 세번째 인자로는 offset의 시작이 어디가 기준이 될지를 정해준다.
위 설명에서 알 수 있듯이, open()한 각 파일에 대해 운영체제는 "현재" 오프셋을 추적하여 다음 읽기 또는 쓰기 위치를 결정한다. 열린 파일의 개념에는 현재 오프셋이 포함된다. 오프셋은 두가지 중 하나의 방법으로 갱신된다. N바이트를 읽거나 쓸 때 현재 오프셋에 N이 더해져, 각 읽기 또는 쓰기는 암묵적으로 오프셋을 갱신한다. lseek로 명시적으로 오프셋을 변경한다.
lseek()는 디스크 암을 이동시키는 디스크의 탐색(seek) 작업과 아무 관계가 없다는 것에 유의해야한다. lseek()호출은 커널 내부에 있는 변수의 값을 변경하는 것이다.
# fsync()을 이용해 즉시 기록
보통 wirte()를 호출하는 목적은 HDD나SDD에 데이터를 저장하기 위해서이다. 그러나 성능 상 이유로 파일 시스템은 일정시간(5~30초)동안 메모리에 버퍼링을 통해 모은 후 한번에 쓰기요청을 통해 저장 장치에 전달된다. 우리가 보는 입장에서는 호출 즉시 쓰기가 되는것 같지만 아니라는 것이다. 그래서 디스크에 쓰기 직전에 데이터가 유실 될 수 있다. 특히 DBMS의 복원 모듈은 강제적으로 즉시 디스크에 기록할 수 있는 기능이 필요하다.
fsync(int fd)를 사용하면 특정 파일 디스크럽트의 더티(Dirty, 갱신된) 데이터를 디스크로 강제로 내려보낸다.
어떤 경우에는 foo가 존재하는 디렉토리도 fsync()를 해주어야 파일 자체와 이파일이 존재하는 디렉토리 모두를 안전하게 디스크에 저장 할 수 있다.
# 파일 이름 변경
파일에 이름을 변경 하기 위해서는 mv 명령어를 사용한다.
mv는 rename()이라는 시스템 콜을 호출하는데, 이 rename()은 원자적이다. 즉 이름이 바뀌기 전 또는 바뀐 두가지의 경우밖에 존재하지 않는다.
# 파일 정보 추출
파일 시스템은 각 파일에 대한 정보를 보관한다 했다. 파일에 대한 정보를 메타데이터라고 했는데 inode에 이 정보가 보관된다고 했었다. 어떤 파일의 stat()이나 fstat()를 호출하면 파일의 메타데이터를 볼 수 있다.
stat의 구조는 아래와 같다.
또한 stat를 호출하면 나오는 결과는 아래와 같다.
# 파일 삭제
rm이라는 명령어는 삭제를 할 수 있도록 해준다. rm을 실행하면 unlink()라는 시스템콜이 호출 되는 것을 알 수 있다.
unlink()는 밑에서 조금 더 자세히 설명 할 것이다. unlink가 호출 되면 현재 참조count를 -1하고 만약 0이 될 경우 데이터를 해제한다는 것만 일단 알아두자.
# 디렉토리 생성
mkdir명령어로 디렉토리를 생성할 수 있다. mkdir()시스템 콜을 호출한다. foo라는 디렉토리를 생성해보자.
디렉토리를 처음 생성하면 비어 있다고 생각들지만, 위에서 언급했듯 .(자신) 과 ..(부모)의 위치를 가르키는 것이 자동적으로 생성된다. 아래 ls명령어를 통해서 생성된 폴더를 확인해 보면 알 수 있다.
# 디렉토리 읽기
ls명령어를 사용하면 현재 디렉토리에 있는 것들을 읽을 수 있다. ls명령어와 유사한 동작을 하는 도구를 직접 만들어 어떻게 동작하는지 살펴보자
이 프로그램은 opendir() readdir(), closedir()을 사용한다.
# 디렉토리 삭제
rmdir()을 통해 디렉토리를 삭제 할 수 있다. 디렉토리를 지우기전에 디렉토리가 비어 있다는 조건이 필요하다. 즉 "."와 ".."외에는 어떤 것도 존재하면 안된다고 한다.
# 하드링크
파일 삭제시 unlink()를 하는 이유를 여기서 알 수 있다.
파일 시스템 트리에 link()라는 시스템콜이 존재하는데, 원래 경로명과 새로운 경로명을 인자로 받는다.
ln을 통해 하드링크를 만들면 file과 file2를 cat해보면 동일한 결과가 출력되는 것을 알 수 있다. ln을 통해 하드링크를 시키게 되면 현재 폴더의 inode에 file2라는 새로운 값이 생기게 된다, 그러나 새로운 파일이 생성되는 것은 아니고 file이 가르키고 있던 파일의 inode-number를 똑같이 가르킨다.
이제 파일 한개를 삭제해보자.
file이 삭제 됐지만 여전히 file2를 실행하면 파일이 존재 함을 알 수 있다. 폴더의 구조는 위에처럼 변화할 것이다. rm시에 unlink()가 호출 된다고 했는데, 각각의 파일의 참초 횟수(reference count)를 검사한다. 이 참조횟수를 -1하고 0에 도달하면 데이터 블럭을 해제하여 파일을 진짜로 삭제하는 것이다.
# 심볼릭 링크 , 소프트 링크
하드 링크는 제약이 많다고 한다. 예를들면 디렉토리에 대해서는 링크를 생성 할 수 없다고 한다. 그렇게 하여 ln에 -s옵션을 주면 소프트 링크를 만들 수 있다.
기능상 동일해 보이지만, 하드링크와 소프트 링크는 아주 다르다. 먼저 심볼릭(소프트) 링크는 다른 형식의 독립된 파일이다. 파일,디렉토리, 소프트 링크 이렇게 3가지의 파일 시스템의 유형 중 하나이다. ls를 통해서 확인해 보자
파일2의 크기가 4인 이유는 파일의 경로명을 저장하기 때문이다. 경로가 더 길어진다면 파일 크기 또한 자동적으로 증가 할 것이다.
하드 링크와는 상반되게 원래 파일을 삭제하면 심볼릭 링크가 가르키는 실제 파일 또한 더이상 존재하지 않게 된다.
# 파일 시스템과 마운트
파일 시스템은 자체적인 디렉토리 구조를 가진다. mkfs를 통해 파일 시스템을 생성한다면, root( "/" )가 되는 폴더 트리를 가지는 파일 시스템이 새로이 생성된다. mount()를 이용하면 새로이 생성된 파일 시스템을 루트 디렉토리에서 시작하는 기존의 디렉토리 구성을 통해 접근할 수 있도록 한다.
아래 마운트 정보를 확인해 볼 수 있다.
# 참고
lseek()