31-3. Files and Directories (3)

Park Yeongseo·2024년 2월 19일
0

OS

목록 보기
39/54
post-thumbnail

Operating Systems : Three Easy Pieces를 보고 번역 및 정리한 내용들입니다.

11. Making Directories

디렉토리와 관련한 시스템 콜을 통해 디렉토리를 만들고, 읽고, 삭제할 수 있다. 다만 디렉토리에 직접 쓸 수는 없다는 것을 유의하자. 디렉토리의 형식은 파일 시스템의 메타데이터로 생각되며, 디렉토리 데이터의 무결성에 책임을 지니는 것 또한 파일 시스템이다. 따라서 디렉토리와 관련한 갱신은, 예를 들면 파일, 디렉토리를 만드는 것 등을 통해 간접적으로만 가능하다. 이러한 방법으로 파일 시스템은 디렉토리의 내용이 예상되는 바 그대로임을 보장한다.

디렉토리를 만들기 위해서는 mkdir() 시스템 콜을 사용할 수 있으며, 동명의 mkdir 프로그램을 통해 그런 디렉토리를 만들 수 있다. 이제 mkdir 프로그램을 실행해 foo 디렉토리를 만들 때 어떤 일이 일어나는지를 살펴보자.

prompt> strace mkdir foo
...
mkdir("foo", 0777) = 0
...
prompt>

디렉토리가 만들어질 때, 사실 기초적인 최소한의 내용은 들어있지만, 해당 디렉토리는 비어있는 것으로 생각된다. 구체적으로, 빈 디렉토리는 두 개의 엔트리를 가지고 있다. 하나의 엔트리는 자기 자신을 위한 것이고, 나머지 하나는 부모 디렉토리를 위한 것이다. 전자는 "."(점) 디렉토리로, 후자는 ".."(점-점)으로 표현된다. 이 디렉토리는 프로그램 ls-a 플래그를 전달함으로써 확인할 수 있다.

prompt> ls -a
./ ../
prompt> ls -al
total 8
drwxr-x--- 2 remzi remzi 6 Apr 30 16:17 ./
drwxr-x--- 26 remzi remzi 4096 Apr 30 16:17 ../

12. Reading Directories

빈 디렉토리를 만들었으니, 이제는 읽어보도록 하자. 사실 그게 바로 ls 프로그램이 하는 일이다. 직접 간단한 ls 프로그램을 만들어보고 이게 어떻게 작동하는지 보자.

파일을 읽기 위해 그냥 열었던 것과 달리, 여기에서는 다른 시스템 콜들을 사용한다. 아래는 디렉토리의 내용을 출력하는 예시 프로그램이다. 여기서는 세 개의 시스템 콜 opendir(), readdir(), closedir()을 사용해 작업을 하고 있다. 간단한 반복문을 통해 엔트리를 하나씩 읽고, 디렉토리 내 각 파일의 이름과 아이노드 번호를 출력한다.

int main(int argc, char *argv[]) {
	DIR *dp = opendir(".");
	assert(dp != NULL);
	struct dirent *d;
	while ((d = readdir(dp)) != NULL) {
		printf("%lu %s\n", (unsigned long) d->d_ino, d->d_name);
	}
	closedir(dp);
	return 0;
}

아래는 각 디렉토리 엔트리에서 볼 수 있는 정보로, struct dirent 자료 구조에 저장된다.

struct dirent {
	char d_name[256]; // filename
	ino_t d_ino; // inode number
	off_t d_off; // offset to the next dirent
	unsigned short d_reclen; // length of this record
	unsigned char d_type; // type of file
};

디렉토리에 담긴 정보량은 기본적으로 몇 가지의 세부 정보 및 이름과 아이노드 번호의 매핑 정도로, 많지 않다. 따라서 디렉토리 내 파일의 길이 등의 세부 정보를 얻기 위해서는 각 파일에 stat()을 호출해야 한다. 이것이 사실 ls-l 플래그가 주어졌을 때 하는 일이다. lsstrace를 써서 해당 플래그가 있는 경우와 그렇지 않은 경우 각각을 직접 확인해보자.

13. Deleting Directories

마지막으로 디렉토리의 삭제는 rmdir() 호출을 통해 이뤄지며, 이를 위해서는 동명의 rmdir 프로그램을 사용할 수 있다. 하지만 파일 삭제와 달리 디렉토리 삭제는 더 위험하다. 하나의 커맨드로 많은 양의 데이터를 지워버릴 수도 있기 때문이다. 따라서 rmdir()는 "."과 ".."을 제외하고는 디렉토리가 비어있기를 필요로 한다. 만약 비어있지 않은 디렉토리를 삭제하려고 한다면 rmdir() 호출은 그냥 실패한다.

14. Hard Links

이제 어떻게 파일 삭제가 unlink()를 통해 수행되는지를 살펴보자. 이를 위해서는 파일 시스템 트리에 엔트리를 만드는 새로운 방법, link() 시스템 콜을 통한 방법을 이해해야 한다. link() 시스템 콜은 두 개의 인자를 가지는데, 그 중 하나는 오래된 경로명, 나머지 하나는 새 경로명이다. 새 파일 이름을 오래된 파일 이름으로 연결(link)시킨다는 것은, 하나의 파일을 두 가지의 이름으로 부를 수 있도록 한다는 것이다. 커맨드 라인 프로그램 ln이 이를 위해 사용된다.

prompt> echo hello > file
prompt> cat file
hello
prompt> ln file file2
prompt> cat file2
hello

우선은 "hello"라는 단어가 담긴 파일 file을 만든다. 그 다음으로는 ln 프로그램을 이용해 해당 파일로의 하드 링크를 만든다. 이 작업이 끝나면 file 또는 file2라는 이름을 통해 해당 파일에 접근할 수 있게 된다.

link()가 작동하는 방식은 간단히 디렉토리에 새 이름으로 된 엔트리를 만들고, 그것이 기존 이름의 엔트리가 가리키는 파일과 같은 아이노드 번호를 가지게 하는 것이다. 여기서 파일은 복사되지 않는다. 그저 하나의 파일에 대해, 사람이 읽을 수 있는 두 개의 이름(file, file2)이 생기는 것 뿐이다. 디렉토리 내 각 파일의 아이노드 번호를 출력해보면 이러한 사실이 잘 드러난다.

prompt> ls -i file file2
67158084 file
67158084 file2
prompt>

ls-i 플래그를 전달하면 디렉토리 내 각 파일들의 아이노드 번호를 출력할 수 있다. 이를 통해 완전히 동일한 아이노드 번호에 대해 새로운 참조가 발생했을 뿐임을 확인할 수 있다.

그렇다면 이제는 왜 unlink()unlink()로 불리는지를 보자. 파일을 만들 때에는 사실 두 가지 일을 한다. 첫 번째는 파일의 크기는 어떤지, 파일이 디스크의 어떤 블럭에 있는지 등, 파일에 관련한 거의 모든 정보들을 관리하기 위한 구조를 만드는 것이다. 두 번째는 사람이 읽을 수 있는 이름을 그 파일에 연결시키고, 해당 연결을 디렉토리에 집어넣는 것이다.

파일에 하드 링크를 만들면, 파일 시스템은 원 파일명과 새 파일명 사이에서 어떠한 차이점도 인지하지 못한다. 이것들은 모두 아이노드 번호 67158084에서 찾을 수 있는, 파일에 대한 메타데이터에 대한 링크일 뿐이기 때문이다.

그렇기 때문에 파일 시스템을 파일에서 삭제하기 위해 unlink()를 호출한다. 위 예에서는 file을 삭제하고 나서도 해당 파일에 접근할 수 있다. file2라는 이름을 통해서 말이다

prompt> rm file
removed ‘file’
prompt> cat file2
hello

이것이 가능한 이유는 파일시스템이 파일의 연결을 해제 할 때, 해당 아이노드 번호의 참조 카운트(reference count, link count)를 확인하기 때문이다. 이 참조 카운트는 특정 파일에 얼마나 많은 이름이 연결되어있는지를 파일 시스템이 확인할 수 있도록 한다. unlink()가 호출되면 이는 사람이 읽을 수 있는 이름과 주어진 아이노드 번호 사이의 연결을 삭제하고 참조 카운트를 하나 줄인다. 오직 참조 카운트가 0이 되었을 때에만 파일 시스템은 아이노드와, 관련된 데이터 블럭의 할당을 해제해 실제로 파일을 삭제한다.

이 참조 카운트는 stat()을 통해 확인할 수 있다. 하드 링크를 만들고 없앨 때 해당 값이 어떻게 변하는지를 살펴보자. 아래의 예에서는 한 파일에 대한 세 개의 링크를 만들고 삭제한다.

prompt> echo hello > file
prompt> stat file
... Inode: 67158084 Links: 1 ...
prompt> ln file file2
prompt> stat file
... Inode: 67158084 Links: 2 ...
prompt> stat file2
... Inode: 67158084 Links: 2 ...
prompt> ln file2 file3
prompt> stat file
... Inode: 67158084 Links: 3 ...
prompt> rm file
prompt> stat file2
... Inode: 67158084 Links: 2 ...
prompt> rm file2
prompt> stat file3
... Inode: 67158084 Links: 1 ...
prompt> rm file3

15. Symbolic Links

심볼릭 링크(symbolic link, soft link)라 불리는 다른 종류의 링크도 있다. 하드 링크는 조금 제한적이다. 하드링크는 디렉토리에 대해서 만들 수 없고, 다른 디스크 파티션의 파일에 대해서도 만들 수 없다. 이러한 제한으로 인해 새로운 종류의 링크가 만들어진 것이다.

이러한 링크를 만들기 위해서는 똑같이 ln 프로그램을 사용하면되는데, -s 플래그를 줘야 한다.

prompt> echo hello > file
prompt> ln -s file file2
prompt> cat file2
hello

여기서 볼 수 있듯, 심볼릭 링크를 만드는 것은 하드 링크를 만드는 것과 매우 비슷하다. 이렇게 링크를 만들고 나면 같은 파일에 대해 file이라는 이름으로도, file2라는 이름으로도 접근할 수 있게 된다.

하지만 그 둘이 표면적으로는 비슷할지 몰라도, 심볼릭 링크는 하드 링크와는 사실 꽤나 다르다. 그 첫 번째 차이는 심볼릭 링크가 사실은 다른 타입의 파일 그 자체라는 것이다. 앞서는 넓은 의미에서의 파일을 파일(사실은 정규 파일, regular file)과 디렉토리로만 구분했는데, 사실 세 번째 타입도 있으며, 그게 바로 심볼릭 링크다. 심볼릭 링크에 대해 stat을 사용하면 이를 알 수 있다.

prompt> stat file
... regular file ...
prompt> stat file2
... symbolic link ...

ls를 실행시켜도 이를 알 수 있다. ls의 결과로 나타나는 긴 형식의 출력에서 가장 왼쪽 열의 첫 번째 문자는 정규 파일의 경우 '-', 디렉토리의 경우 'd', 심볼릭 링크의 경우 'l'로 나타난다. 이를 통해서는 또한 심볼릭 링크의 크기와 해당 링크가 어떤 것을 가리키고 있는지도 확인할 수 있다.

prompt> ls -al
drwxr-x--- 2 remzi remzi 29 May 3 19:10 ./
drwxr-x--- 27 remzi remzi 4096 May 3 15:14 ../
-rw-r----- 1 remzi remzi 6 May 3 19:10 file
lrwxrwxrwx 1 remzi remzi 4 May 3 19:10 file2 -> file

여기에서 file2의 크기가 4 바이트인 이유는, 이 링크 파일이 링크되는 파일의 경로 명을 데이터로 가지고 있기 때문이다. 링크되고 있는 파일의 이름이 file로 짧기 때문에 file2의 크기도 작아진 것이다. 만약 더 긴 경로명을 사용한다면 링크 파일 또한 더 커진다.

prompt> echo hello > alongerfilename
prompt> ln -s alongerfilename file3
prompt> ls -al alongerfilename file3
-rw-r----- 1 remzi remzi 6 May 3 19:17 alongerfilename
lrwxrwxrwx 1 remzi remzi 15 May 3 19:17 file3 -> alongerfilename

마지막으로 이렇게 심볼릭 링크가 만들어지는 방식은 dangling reference라는 문제가 발생할 가능성을 만든다.

prompt> echo hello > file
prompt> ln -s file file2
prompt> cat file2
hello
prompt> rm file
prompt> cat file2
cat: file2: No such file or directory

위 예에서 볼 수 있듯, 하드 링크와 달리 원 파일 file의 삭제는 해당 경로명으로의 링크를 더 이상 유효하지 않게 만든다.

16. Permission Bits And Access Control Lists

프로세스의 추상화는 CPU와 메모리에 대한 가상화를 제공한다. 이 각각은 프로세스에게 고유한 CPU와 메모리를 가지고 있다는 환상을 주며, 실제로는 그 아래의 OS가 경쟁 대상이 되는 제한적인 물리적 자원을 안전하고 보안적으로 제공하기 위해 다양한 테크닉들을 사용하고 있다.

파일 시스템 또한 디스크에 대한 가상의 시각을 제공하는데, 바로 날 것의 블럭 뭉치를 좀 더 사용자 친화적인 파일과 디렉토리로 바꾸는 것이다. 하지만 이 추상화는 CPU나 메모리의 추상화와는 차이가 있다. 바로 프로세스와 달리 파일은 보통 여러 사용자들 사이에서 공유된다는 점에서 그렇다. 따라서 파일 시스템에서는 이 공유의 정도를 다양하게 만들기 위한 더 종합적인 메커니즘 집합이 사용된다.

그런 메커니즘 중 하나는 고전적인 UNIX의 권한 비트(permission bit)다. 파일 foo.txt의 권한을 보기위해서는 다음과 같이 입력하면 된다.

prompt> ls -l foo.tx
-rw-r--r-- 1 remzi wheel 0 Aug 24 16:29 foo.txt

위 출력의 맨 첫 번째 부분을 보자. 이 부분의 맨 처음 문자는 정규 파일, 디렉토리, 심볼릭 링크 등을 구분하기 위해 쓰이며, 권한과는 상관이 없으므로 지금은 무시한다.

권한은 세 종류로 그룹 지어지는데, 각각 파일의 소유자(owner), 특정 그룹(group), 그리고 나머지(other)가 해당 파일을 가지고 무엇을 할 수 있는지에 대한 것이다. 이 각 그룹이 할 수 있는 일에는 파일을 읽고, 쓰고, 실행하는 것들이 있다.

위 예에서 소유자는 해당 파일을 읽고 쓸 수 있고(rw-), 그룹 wheel의 멤버들과 시스템 내 다른 사람들은 읽을 수만 있다(r--r--).

파일의 소유자는 예를 들면 chmod와 같은 커맨드를 이용해 이 권한을 바꿀 수 있다. 파일 소유자를 제외한 누구도 이 파일에 접근할 수 없게 하기 위해서는 다음과 같이 입력할 수 있다.

prompt> chmod 600 foo.txt

이 커맨드는 파일 소유자의 읽기 허용 비트와 쓰기 허용 비트를 1로 두고(110, 십진법으로는 6), 나머지의 모든 권한 비트는 0으로 둔다. 따라서 전체 권한은 rw-------이 된다.

실행 비트는 특히 더 흥미롭다. 정규 파일의 경우, 이 비트는 프로그램이 실행될 수 있는지 아닌지를 결정한다. 예를 들어 hello.csh라는 간단한 쉘 스크립트가 있다고 하자. 이는 다음과 같이 실행할 수 있다.

prompt> ./hello.csh
hello, from shell world.

하지만 이 파일에 대해 실행 비트를 적절하게 설정하지 않으면 다음과 같은 일이 일어난다.

prompt> chmod 600 hello.csh
prompt> ./hello.csh
./hello.csh: Permission denied.

디렉토리에 대해서 실행 비트는 좀 다르게 동작한다. 구체적으로 이는 사용자가 디렉토리 변경(cd)를 통해 해당 디렉토리에 들어갈 수 있는지 등을 결정한다. 쓰기 허용 비트와 함께 쓰이면 해당 디렉토리 내에서 파일을 만들 수 있는지를 결정하는 데에도 쓰인다. 이에 대해서 더 잘 알고 싶다면 직접 이것저것 해보도록 하자.

권한 비트 외에도, AFS라 불리는 분산 파일 시스템 등의 몇몇 파일 시스템에는 좀 더 정교한 제어가 있다. 예를 들어 AFS는 이것을 디렉토리별 접근 제어 리스트(access control list)를 통해 수행한다. 제어 접근 리스트는 누가 주어진 자원에 접근할 수 있는지를 표현하기 위한 좀 더 일반적이고 강력한 방법이다. 파일 시스템에서 이는 사용자가 누가 파일들을 읽을 수 있고 읽을 수 없는지에 대한 아주 구체적인 리스트를 만들 수 있게 한다. 위에서의 제한적인 소유자/그룹/나머지의 분류보다는 훨씬 일반적으로 사용할 수 있는 방법이다.

예를 들어 다음은 어떤 저자의 AFS 계정에 있는 개인 디렉토리에 대한 접근 제어다. fs listacl 명령으로 볼 수 있다.

prompt> fs listacl private
Access list for private is
Normal rights:
system:administrators rlidwka
remzi rlidwka

이 리스트는 시스템 관리자와 사용자 remzi가 디렉토리 내의 파일을 검색하고, 만들고, 삭제하고 관리할 수 있음을 보여준다. 해당 파일을 읽고, 쓰고, 잠그는 것도 그렇다.

누군가가 이 디렉토리에 접근할 수 있게 하려면 사용자 remzi가 다음과 같은 커맨드를 입력해야 한다.

prompt> fs setacl private/ andrea rl

17. Making And Mounting A File System

지금까지 파일, 디렉토리, 링크에 접근하는 기본 인터페이스들에 대해 알아봤다. 하지만 아직 논의해야 할 한 가지 토픽이 더 남아있다. 여러 파일 시스템들을 어떻게 하나의 전체 디렉토리 트리로 조립할 수 있을까?이 작업은 일단 파일 시스템을 만들고, 그 내용에 접근할 수 있게 마운트(mount)함으로써 가능해진다.

파일 시스템을 만들기 위해서, 대부분의 파일 시스템은 보통 mkfs라 불리는, 바로 이 작업을 해주는 툴을 제공한다. 아이디어는 다음과 같다. 이 툴에 디스크 파티션과 같은 장치(예를 들어 /dev/sda1)와 파일 시스템 타입(e.g. ext3)을 입력으로 주면, 이것은 해당 디스크 파티션에 루트 디렉토리에서 시작하는 빈 파일 시스템을 만든다.

하지만 파일 시스템은 만들어지고 나서 통일된 파일 시스템 트리에서 접근할 수 있어야 한다. 이러한 작업은 mount 프로그램을 통해, 실은 그 아래의 mount() 시스템 콜에 의해 가능해진다. 마운트가 하는 일은 간단히, 이미 존재하는 디렉토리를 마운트 지점(mount point)로 삼아 새 파일 시스템을 디렉토리 트리의 해당 지점에 붙여넣는 것이다.

아마도 다음의 예가 유용할 것이다. 장치 파티션 dev/sda1에 저장된, 마운트되지 않은 ext3 파일 시스템을 하나 생각해보자. 여기에는 두 개의 서브-디렉토리 a, b를 포함하는 루트 디렉토리가 있고, 이 각각의 서브 디렉토리에는 foo라는 이름의 파일이 있다. 이 파일 시스템을 마운트 지점 /home/users에 마운트하고 싶다고 하자. 그렇다면 다음과 같이 입력하면 된다.

prompt> mount -t ext3 /dev/sda1 /home/users

만약 마운트에 성공한다면, 이는 이 새 파일 시스템을 사용 가능하게 만든다. 그렇다면 이제는 파일 시스템에의 접근이 어떻게 이루어지는지를 보자. 루트 디렉토리에 담긴 내용들을 보기 위해서는 ls를 사용한다.

prompt> ls /home/users/
a b

여기서 볼 수 있듯, 경로명 /home/users는 이제 새롭게 마운트된 디렉토리의 루트를 가리킨다. 비슷하게 디렉토리 a, b도 각각 경로명 /home/users/a, /home/users/b를 통해 접근할 수 있다. a, b 아래에 있는 파일들도 마찬가지다. 이게 바로 마운트의 미학이다. 여러 개의 분리된 파일 시스템 대신, 마운트는 모든 파일 시스템들을 하나의 트리로 통합시키고 네이밍을 획일적이고 간편하게 만들어준다.

시스템에 무엇이, 그리고 어디에 마운트 됐는지를 보려면 그냥 mount 프로그램을 실행하면 된다.

/dev/sda1 on / type ext3 (rw)
proc on /proc type proc (rw)
sysfs on /sys type sysfs (rw)
/dev/sda5 on /tmp type ext3 (rw)
/dev/sda7 on /var/vice/cache type ext3 (rw)
tmpfs on /dev/shm type tmpfs (rw)
AFS on /afs type afs (rw)

이는 ext3(표준 디스크-기반 파일 시스템), proc 파일 시스템(현재 프로세스에 대한 정보에 접근하기 위한 파일 시스템), tmpfs(임시 파일들을 위한 파일 시스템), AFS(분산 파일 시스템) 등, 많은 서로 다른 파일 시스템들이 기기의 한 파일 시스템 트리에 융합되어 있음을 보여준다.

18. Summary

UNIX 시스템의 파일 시스템 인터페이스는 보기에는 기초적인 것처럼 보이지만, 사실 마스터하기 위해서는 알아야 할 것들이 많다.

  • 파일은 바이트의 배열로, 만들고, 읽고, 쓰고, 지울 수 있다. 각 파일은 고유한 낮은 수준의 이름을 가지며, 이 이름은 아이노드 번호(inode number, i-number)라 불린다.
  • 디렉토리는 파일이 가지는 사람이 읽을 수 있는 이름과 낮은 수준 이름의 튜플들을 모아둔 것이다. 각 엔트리는 다른 디렉토리 또는 파일을 가리킨다. 각 디렉토리 또한 자신의 i-number를 가진다. 디렉토리는 항상 두 개의 특별한 엔트리를 가진다.(자기 자신을 위한 "."과 부모 디렉토리를 위한 "..")
  • 디렉토리 트리, 또는 디렉토리 계층는 모든 파일과 디렉토리를 하나의 루트에서 시작하는 커다란 트리로 구성한다.
  • 파일에 접근하기 위해서 프로세스는 시스템 콜을 사용해 OS에 권한을 요청해야 한다. 만약 권한이 주어지면 OS는 파일 디스크립터(file descriptor)를 반환하며, 이는 권한과 의도가 허용되는 한, 읽기나 쓰기 접근에 쓰인다.
  • 각 파일 디스크립터는 프로세스 별의, 고유한 엔티티로, open file table의 엔트리를 가리킨다. 이 엔트리는 이 접근이 가리키는 파일이 무엇인지, 파일의 현재 오프셋은 무엇인지, 그리고 다른 관련 정보 등등을 가지고 있다.
  • read()write()의 호출은 현재 오프셋을 자연스럽게 업데이트한다. 이외의 경우, 프로세스는 lseek()을 이용해 그 값을 바꿔, 해당하는 파일의 다른 부분에 임의 접근할 수 있다.
  • 미디어를 강제로 영구 저장하기 위해 프로세스는 fsync()나 관련된 시스템 콜들을 사용해야한다. 하지만 높은 성능을 유지하면서 이를 정확히 수행하는 것은 어려운 일이므로 주의를 기울여야 한다.
  • 같은 파일에 대해, 사람이 읽을 수 있는 이름을 여러 개 만들기 위해서는 하드 링크(hard link)나 심볼릭 링크(symbolic link)를 사용하라. 각각은 서로 다른 환경에서 유용하므로, 사용하기 전에 이 둘의 장단점에 대해 생각해보자. 파일을 삭제하는 것은 해당 디렉토리 계층 구조에서의 마지막 하나 남은 unlink()를 수행하는 것 뿐임을 기억하자.
  • 대부분의 파일 시스템은 공유를 허용하거나 불허하는 메커니즘들을 가지고 있다. 그런 제어의 기초적인 형태는 권한 비트(permissions bits)를 통해 제공되며, 좀 더 세련된 것에는 접근 제어 리스트(access control list)가 있다. 접근 제어 리스트는 누가 정보에 접근해 그것을 조작할 수 있는지에 대한, 좀 더 자세한 제어를 가능케 한다.

0개의 댓글