CPU를 가상화한 "프로세스"와 메모리를 가상화한 "주소 공간"은 운영체제를 구성하는 두 개의 핵심 개념이다.
영속 저장 장치(persistent storage) 하드 디스크 드라이브 또는 솔리드 스테이트 드라이브(SSD)와 같은 저장 장치는 영구적으로 정보를 저장한다.
전원 공급이 차단되면 내용이 사라지는 메모리와 다르게 영속 저장 장치는 데이터를 보존한다.
운영체제가 영속 장치를 어떻게 관리해야 할까? API들은 어떤 것이 있는가? 구현의 중요한 측면은 무엇인가?
저장 장치의 가상화에 대한 두 가지 주요 개념은 파일과 디렉터리이다.
파일은 단순히 읽거나 쓸 수 있는 순차적인 바이트의 배열이다.
각 파일은 아이노드 번호(inode number)라고 부르는 저수준의 이름(low-level name)을 갖고 있으며 보통은 숫자로 표현되지만 사용자는 그 이름에 대해서 알지 못한다.
파일 시스템의 역할은 데이터를 디스크에 안전히 저장하고, 데이터가 요청되면 처음 저장했던 데이터를 돌려주는 것이다.
디렉터리 또한 저수준의 이름을 갖는다. 하지만 파일과는 다르게 디렉터리의 내용은 구체적으로 정해져 있다.
디렉터리는 <사용자가 읽을 수 있는 이름, 저수준의 이름> 쌍으로 이루어진 목록을 갖고 있다.
디렉터리의 각 항목은 파일 또는 다른 디렉터리를 가리킨다.
디렉터리 내에 다른 디렉터리를 포함함으로써 사용자는 모든 파일들과 디렉터리들이 저장되어 있는 임의의 디렉터리 트리(directory tree, 또는 디렉터리 계층(directory hierarchy))를 구성할 수 있다.
디렉터리 계층은 루트 디렉터리(root directory)부터 시작하며, 원하는 파일이안 디렉터리의 이름을 표현할 때까지 구분자(separator)를 사용하여 하위 디렉터리를 명시할 수 있다.
open() 시스템 콜을 호출하고 O_CREAT 플래그를 전달하면 프로그램은 새로운 파일을 만들 수 있다.
int fd = open("foo", O_CREAT | O_WRONLY | O_TRUNC);
open()의 중요한 항목은 리턴값이다: 파일 디스크립터(file descriptor).
파일 디스크립터는 프로세스마다 존재하는 정수로서 UNIX 시스템에서 파일을 접근하는 데 사용된다.
이러한 측면에서 파일 디스크립터는 특정 동작에 대한 수행 자격을 부여하는 capability 핸들이다.
파일 디스크립터를 파일 객체를 가리키는 포인터로 볼 수도 있다.
객체를 생성하면, read() 또는 write()와 같은 다른 "메소드"로 파일에 접근할 수 있다.
커맨드 라인을 사용 중이라면 cat이라는 프로그램을 사용하여 파일의 내용을 화면에 덤프할 수 있다.
cat 프로그램은 어떻게 파일 foo에 접근할까?
cat이 가장 먼저 하는 것은 파일을 읽기 위해서 여는 것이다.
두 번째는 O_LARGEFILE 플래그를 사용하여 64 bit 오프셋이 사용되도록 설정하였다.
세 번째 open()이 성공한 후에 이라는 값을 파일 디스크립터로 리턴하였다.
첫 번째 open()임에도 불구하고 0 또는 1이 아닌 3을 리턴한 이유는 프로세스가 이미 세 개의 파일을 열어 놓았기 때문이다.
이미 열려진 세 개의 파일은 표준 입력과 표준 출력, 그리고 오류 메시지를 기록할 수 있는 표주 에러이다.
각각의 파일 디스크립터는 0, 1 그리고 2로 표현된다.
다른 파일을 처음으로 열게 되면 파일 디스크립터는 3일 것이다.
파일 열기가 성공하면 cat은 read() 시스템 콜을 사용하여 파일에서 몇 바이트씩 반복적으로 일근ㄴ다.
read()의 첫 번째 인자는 파일 디스크립터로서 파일 시스템에 어떤 파일을 읽을 것인지 알려준다.
프로세스는 동시에 여러 파일을 열 수 있기 때문에, 디스크립터는 운영체제가 read 명령이 읽어야 할 파일을 알 수 있게 해준다.
두 번째 인자는 read() 결과를 저장할 버퍼를 가리킨다.
세 번째 인자는 버퍼의 크기로서 여기서는 4KB이다.
read()가 성공적으로 리턴하면 읽은 바이트 수를 반환한다. ("hello"의 5개의 문자와 줄의 끝을 표시하는 문자 하나가 있기 때문에 6을 반환함)
write() 시스템 콜이 결과를 쓰는 대상 파일로 파일 디스크립터 1번을 사용하는 것이다.
1번 디스크립터는 표준 출력(STDOUT)으로서 "hello"라는 단어를 화면에 나타내기 위해 사용되고, cat이 수행하는 작업이다.
출력한 이후 cat 프로그램은 파일의 내용을 더 읽으려고 시도하고, 파일에 남은 바이트가 없기 때문에 read()는 0을 리턴한다.
프로그램은 리턴 값으로 파일을 끝까지 다 읽었음을 알게 된다.
그런 후 프로그램은 해당 파일 디스크립터를 인자로 close()를 호출하여 "foo"라는 파일에서 할 일이 다 끝났음을 표시한다.
이제 파일은 닫혔으며 읽기 작업은 완료된다.
파일을 쓰는 과정은 먼저 파일을 쓰기 위해 열고 write() 시스템 콜을 호출한다.
파일이 큰 경우 write() 시스템 콜을 반복적으로 호출할 수 있다.
그 후에 close()가 호출된다.
처음부터 파일을 끝까지 읽거나 처음부터 끝까지 기록하는 것을 순차적이라 한다.
파일의 특정 오프셋부터 읽거나 쓰는 경우 비순차적이라 한다.
off_t lseek(int filddes, off_t offset, int whence);
첫 번째 인자는 파일 디스크립터이다. 두 번째 인자는 offset으로 파일의 특정 위치(file offset)를 가리킨다. 세 번째 인자는 whence라고 부르며 탐색 방식을 결정한다.
whence가 SEEK_SET이면 오프셋은 offset 바이트로 설정된다.
whence가 SEEK_CUR이면 오프셋은 현재 위치에 offset 바이트를 더한 값으로 설정된다.
whence가 SEEK_END이면 오프셋은 파일의 크기에 offset 바이트를 더한 값으로 설정된다.
lseek()는 디스크 암을 이동시키는 디스크의 탐색(seek) 작업과 아무 관계가 없다.
lseek() 호출은 커널 내부에 있는 변수의 값을 변경한다.
파일 시스템은 각 파일에 대한 정보를 보관한다. 파일에 대한 정보를 메타데이터(metadata)라고 부른다.
unlink()라는 시스템 콜을 사용하여 인자로 입력 받은 파일을 삭제하고 성공하면 0을 리턴한다.
디렉터리 관련 시스템 콜들은 디렉터리를 생성하고, 읽고, 삭제한다.
디렉터리에는 절대로 직접 쓸 수 없다.
디렉터리는. 파일 시스템의 메타데이터로 분류되며 항상 간접적으로 변경된다.
파일이나 디렉터리 또는 다른 종류의 객체들을 생성함으로써 디렉터리를 변경할 수 있다.
디렉터리 생성을 위한 시스템 콜로 mkdir()이 있다.
디렉터리의 open은 파일을 open하는 것과는 다른 새로운 시스템 콜을 사용한다.
opendir(), readdir(), 및 closedir()를 사용한다.
디렉터리 항목을 하나씩 읽은 후에 디렉터리의 각 파일의 이름과 아이노드 번호를 출력한다.
rmdir() 시스템 콜을 사용하여 디렉터리를 삭제할 수 있다.
파일 삭제 시 unlink() 시스템 콜을 사용한다.
파일 시스템 트리에 항목을 추가하는 link() 시스템 콜은 두 개의 인자를 받는데, 하나는 원래의 경로명이고, 다른 하나는 새로운 경로명이다.
원래 파일 이름에 새로운 이름을 link(연결)하면 동일한 파일을 접근할 수 있는 새로운 방법을 만들게 된다.
"hello"라는 단어가 저장된 파일을 생성하고 이름을 file이라고 짓는다.
ln 프로그램을 사용하여 이 파일의 하드 링크를 생성한다.
이후부터는 이 파일을 보려면 file 또는 file2를 열면 된다.
link는 새로이 링크하려는 이름 항목을 디렉터리에 생성하고, 원래 파일과 같은 아이노드 번호를 가리키도록 한다.
파일은 복사되지 않으며 대신 같은 파일을 가리키는 두 개의 이름(file과 file2)이 생성된다.
file과 file2는 같은 아이노드 번호를 가진다.
link는 동일한 아이노드 번호에 대한 새로운 링크를 생성한다.
파일을 생성할 때 두 가지 작업을 하게 된다.
하나는 파일 관련 거의 모든 정보를 관리하는 자료 구조(아이노드)를 만드는 것이다.
파일 크기와 디스크 블럭의 위치 등이 포함된다.
두 번째는 해당 파일에 사람이 읽을 수 있는 이름을 연결하고 그 연결 정보를 디렉터리에 생성하는 것이다.
파일 삭제 시 unlink()를 호출한다.
파일 이름 file을 제거한다고 하더라도 여전히 해당 파일을 접근할 수 있다.
파일을 unlink하면 아이노드 번호의 참조 횟수(reference count)를 검사한다.
이 참조 횟수(연결 횟수(link count))가 특정 아이노드에 대해 다른 이름이 몇 개나 연결되어 있는지 관리한다.
unlink()가 호출되면 이름과 해당 아이노드 번호 간의 "연결"을 끊고 참조 횟수를 하나 줄인다.
참조 횟수가 0에 도달하면 파일 시스템은 비로소 아이노드와 관련된 데이터 블럭을 해제하여 파일을 "삭제"한다.
디렉터리에 대해서는 하드 링크를 만들 수 없으며(디렉터리 트리에 순환 구조를 만들까 우려하여), 다른 디스크 파티션에 있는 파일에 대해서도 하드 링크를 걸 수 없기 때문에(아이노드 번호는 하나의 파일 시스템 내에서만 유일하다) 하드 링크는 제한이 많다.
심볼릭 링크를 만들기 위해서 동일한 ln 프로그램을 사용할 수 있다.
심볼릭 링크의 크기(4바이트)
file2의 크기가 4바이트인 이유는 심볼릭 링크는 연결하는 파일의 경로명을 저장하기 때문이다.
심볼릭 링크가 만들어진 방식 때문에 dangling reference라는 문제가 발생할 수 있다.
하드 링크과 다르게 원래의 파일인 file을 삭제하면 심볼릭 링크가 가리키는 실제 파일은 더 이상 존재하지 않게 된다.