이번 장에서는 HDD나 SSD와 같은 영속 저장 장치(persistent storage)에 대해 알아볼 것이다.
핵심 질문 : 어떻게 영속 장치를 관리하는가
- 운영체제가 영속 장치를 어떻게 관리해야 할까?
- API들은 어떤 것이 있는가?
- 구현의 중요한 측면은 무엇인가?
저장 장치의 가상화에 대한 두 가지 주요 개념인 파일과 디렉토리에 대해 알아보자.
파일
디렉터리
<사용자가 읽을 수 있는 이름, 저수준의 이름>
쌍으로 이루어진 목록을 갖고있다./foo/bar.text
)이번에는 기본적인 파일 시스템 인터페이스를 좀 더 상세하게 논의해 볼 것이다.
파일 시스템 인터페이스
- 파일 생성
- 파일 접근
- 파일 삭제
open 시스템 콜을 사용하여 파일을 생성할 수 있다.
int fd = open("foo", O_CREAT | O_WRONLY | O_TRUNC);
open()
을 호출하면서 O_CREAT
플래그를 전달하면 프로그램은 새로운 파일을 만들 수 있다.open()
은 다수의 플래그를 받으며, 이 예제에서는 O_WRONLY
와 O_TRUNC
플래그도 추가로 전달하였다.O_WRONLY
: 파일이 열렸을 때 쓰기만 가능하게끔 허용O_TRUNC
: 파일이 이미 존재할 때는 기존 내용을 모두 삭제파일이 있으면, 그 파일들을 당연히 읽거나 쓰고 싶을 것이다.
커맨드 라인을 사용 중이라면 cat
명령어로 파일을 읽을 수 있다.
> echo hello > foo
> cat foo
hello # 화면에 hello 출력
cat
프로그램은 어떻게 파일foo
에 접근할까?
> strace cat foo
...
open("foo", O_RDONLY|O_LARGEFILE) = 3 # foo 파일 열기
read(3, "hello\n", 4096) = 6 # foo 파일 읽기
write(1, "hello\n", 6) = 6 # stdout에 파일 쓰기
hello # 화면에 hello 출력
read(3, "", 4096) = 0 # foo 파일 읽기
close(3) = 0 # foo 닫기
...
O_RDONLY
플래그 줘서 읽기만 가능O_LARGEFILE
플래그 줘서 64bit 오프셋 사용cat
은 read()
시스템 콜을 사용하여 파일에서 몇 바이트씩 반복적으로 읽는다.fd
가 들어간다"hello\n"
가 들어간 것을 표현한 것이다."hello\n"
는 6바이트)write(1,buf,size)
을 해준다.foo
를 close
파일에 쓰는 것도 비슷한 단계를 거친다.
지금까지 파일을 읽고 쓰는 과정을 논의하였는데, 모든 접근은 순차적이었다. 그렇지만 때로는 파일의 특정 오프셋(위치)부터 읽거나 쓰는 것이 유용할 때가 있다.
임의의 오프셋에서 읽기/쓰기 수행
off_t lseek(int fildes, off_t offset, int whence);
SEEK_SET
: 오프셋은 offset
byte로 설정SEEK_CUR
: 오프셋은 현재 위치에 offset
byte를 더한 것으로 설정SEEK_END
: 오프셋은 파일 크기에 offset
byte를 더한 것으로 설정N
바이트를 읽거나 쓸때, offset+N
으로 암묵적 갱신lseek
를 사용하여 명시적으로 오프셋 갱신write()
호출은 성능상 이유로 파일 시스템은 쓰기들을 일정 시간동안 메모리에 모아(버퍼링) 한 번에 저장 장치에 전달된다.
어떤 프로그램은 쓰기에 있어서 좀 더 강력한 보장을 필요로 한다 (ex. DBMS의 복원 모듈). 이럴 땐 강제적으로 즉시 디스크에 기록할 수 있는 기능이 필요하다.
fsync(int fd)
fd
에 대해 파일의 모든 더티(dirty 즉, 갱신된) 데이터를 디스크로 강제로 내려보내는 유닉스 APIfoo
가 존재하는 디렉터리도 fsync()
해주어야 한다.커맨드 라인에서는 mv
명령으로 파일명을 변경할 수 있다.
> mv foo bar # foo를 bar로 rename
mv
가 rename(char *old, char *new)
라는 시스템 콜을 호출하는 것을 확인할 수 있다. (각각 옛날 이름, 새 이름)rename()
은 시스템 크래시에 대해 원자적으로 구현되었다.foo.txt
이다.int fd = open("foo.txt.tmp", O_WRONLY|O_CREAT|O_TRUNC);
write(fd , buffer , size); // 파일의 새로운 버전 쓰기
fsync(fd);
close(fd);
rename("foo.txt.tmp", "foo.txt");
foo.txt.tmp
생성 후 여기에 기록foo.txt.tmp
를 foo.txt
로 rename파일 시스템은 각 파일에 대한 정보를 보관하고, 이를 메타 데이터라고 한다.
메타 데이터(metadata)
stat()
이나 fstat()
같은 시스템 콜을 사용한다.stat
의 구조struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for filesystem I/O */
blkcnt_t st_blocks; /* number of blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};
리눅스에서는 rm
명령으로 파일을 삭제할 수 있다.
strace를 통해 rm
이 어떻게 동작하는지 알아보자.
> strace rm foo
...
unlink("foo") = 0
...
unlink()
라는 시스템 콜이 호출되는 것을 알 수 있다.unlink()
는 지워져야 하는 파일 이름을 인자로 받은 후에 성공하면 0을 리턴한다.그런데 왜 remove나 delete가 아닌 unlink(연결을 끊다) 일까? 답을 이해하기 위해서는 파일뿐만 아니라 디렉터리에 대해서도 이해해야 한다.
디렉터리 관련 시스템 콜들은 디렉터리를 생성하고, 읽고, 삭제하지만, 디렉터리에는 절대로 직접 쓸 수 없다.
대표적인 디렉터리 생성 명령어는 mkdir
이 있다.
mkdir
동작 과정 (strace)> strace mkdir foo
...
mkdir("foo", 0777) = 0
...
디렉터리 읽기는 ls
와 같은 명령어가 있다. ls
와 유사한 도구를 직접 만들어 어떻게 동작하는지 알아보자.
int main(int argc , char *argv[]) {
DIR *dp = opendir("."); // 디렉터리 열기
assert(dp != NULL);
struct dirent *d;
while ((d = readdir(dp)) != NULL) { // 디렉터리 반복적으로 읽기
printf("%d %s\n", (int) d−>d_ino , d−>d_name);
}
closedir(dp); // 디렉터리 닫기
return 0;
}
opendir()
, readdir()
, 및 closedir()
시스템 콜을 사용한다.디렉터리에는 많은 정보가 있지 않기 때문에 (단순하게 이름과 아이노드 번호를 매핑하는 것 이외에 몇 가지만 제공함) 프로그램은 각 파일에 stat()
을 호출하여 파일 크기와 같은 구체적인 정보를 얻는다.
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 */
};
마지막으로 rmdir()
시스템 콜을 사용하여 디렉터리를 삭제할 수 있다.
rmdir()
은 디렉터리를 지우기 전에 디렉터리가 비어 있어야 한다는 조건이 붙는다.파일 삭제 시 왜 unlink()
를 사용하는지를 이해하기 위해서 이제 파일 시스템 트리에 항목을 추가하는 새로운 시스템 콜 link()
를 알아보자.
하드 링크 <- link()
시스템 콜 사용
link(char *old, char *new)
: 원래 경로명, 새 경로명을 인자로 받는다.ln
이 그 일을 한다.> echo hello > file1
> cat file1
hello
> ln file1 file2 # linking file2 to file
> cat file2
hello
link
는 새로이 링크하려는 이름 항목을 디렉터리에 생성하고, 원래 파일과 같은 아이노드 번호를 가리키도록 한다.> ls −i file1 file2
67158084 file1
67158084 file2 # inode가 같다
link
는 동일한 아이노드 번호(이 예제에서는 67158084)에 대한 새로운 링크를 생성한다.
unlink()
가 왜unlink()
가 되었는지 이제 이해되기 시작했을 것이다.
파일을 생성할 때 사실은 두 가지 작업을 하게 된다.
unlink()
시스템 콜
unlink()
를 호출한다.> rm file1
removed 'file1'
> cat file2
hello
unlink
하면 아이노드 번호의 참조 횟수(reference count)를 검사한다.unlink()
가 호출되면 이름과 해당 아이노드 번호 간의 "연결"을 끊고 참조 횟수를 하나 줄인다. > echo hello > file1
> stat file1
... Inode: 67158084 Links: 1 ...
> ln file1 file2
> stat file1
... Inode: 67158084 Links: 2 ...
> stat file2
... Inode: 67158084 Links: 2 ...
> ln file2 file3
> stat file1
... Inode: 67158084 Links: 3 ...
> rm file1
> stat file2
... Inode: 67158084 Links: 2 ...
> rm file2
> stat file3
... Inode: 67158084 Links: 1 ...
> rm file3
하드 링크의 제한
심볼릭 링크 (또는 소프트 링크)
ln -s
명령어 사용> echo hello > file1
> ln −s file1 file2
> cat file2
hello
stat
명령어를 실행시키면 일반 파일이나 디렉토리와 다른 "symbolic link" 유형이라고 출력된다.ls
명령어를 통해서도 다르다는 것을 알 수 있다. (첫 글자가 l
임)> echo hello > file1
> ln −s file1 file2
> cat file2
hello
> rm file1
> cat file2
cat: file2: No such file1 or directory
다수의 파일 시스템들이 존재할 때 이들을 묶어서 어떻게 하나의 큰 디렉터리 트리를 구성할까?
mkfs
명령어
/dev/sda1
에 시스템 타입 EXT3
의 형식으로 구성된 파일 시스템을 생성할 수 있다.mount
명령어
마운트: 새로이 생성된 파일 시스템을 루트 디렉터리에서 시작하는 기존의 디렉터리 구성을 통해 접근할 수 있도록 해주는 것
mount()
시스템 콜을 사용하여 기존의 디렉터리 중 하나를 마운트 지점(mount point)으로 지정한다.
그리고 나서 마운트 지점에 생성된 파일 시스템을 "붙여 넣는다".
예)
/dev/sda1
에 EXT3
파일 시스템 존재한다 가정 (아직 마운트 되지 않음)a
와b
라는 두 개의 하위 디렉토리가 있고, 각 디렉터리에 foo
라는 파일이 들어있다./
├── a
│ └── foo
└── b
└── foo
/home/users/
위치에 마운트 하기 위해 다음 명령어를 실행한다.> mount −t ext3 /dev/sda1 /home/users
> ls /home/users/
a b
> ls /home/users/a/
foo
여러 개의 개별 적인 파일 시스템을 갖는 대신에 마운트는 모든 파일 시스템들을 하나의 트리 아래에 통합시킨다.
/
├── home
. ├── users
. . ├── a
. . │ └── foo
. └── b
└── foo