- 이 글은 무료 OS 관련 책인 Operating Systems: Three Easy Pieces의 39단원, Files and Directories를 정리한 것입니다. 책의 내용을 잘못 이해한 정보가 있을 수도 있으니 의심되면 원본 내용을 확인해주세요.
- 이미 OS에 대해, 심지어 파일과 디렉토리에 대해서도 전반적인 지식이 있는 분들을 위한 정리 글이라 처음이면 이해가 힘들 수도 있습니다. 이 경우 책을 정독하는걸 강력 추천합니다.
persistent storage 가상화에 핵심적인 역할을 하는 가상 객체. (CPU의 프로세스, 메모리의 VM과 비슷한 관계)
persistent storage : memory랑 다르게 전원이 없어도 데이터를 저장할 수 있는 장치.
연속된 바이트로 이루어져있다.
저단계에서는 숫자를 통해 관리 및 구별된다. 이를 inode number이라고 한다. 일반 사용자는 별로 볼 일이 없음.
파일의 형식이 무엇인지는 OS 수준에서는 구별할 필요가 없고, 이 때문에 실제로 인식하지도 않는다.
파일의 일종이다. 이 때문에 inode number도 있다.
보관하는 자료는 (사용자가 보는 파일 이름(문자열), 해당 파일의 inode number)
의 순서쌍들이다. 이 순서쌍은 다른 파일 혹은 디렉토리에 대한 정보를 보관. 추후 구체적으로 나오는데, 우리가 사용하는 파일 경로를 기반으로 파일 위치를 탐색할 때 사용되는 정보고 이를 보관하는 파일을 디렉토리라고 하는 것이다.
우리가 어떤 파일을 찾을 때 사용하는 파일 경로는 (UNIX 기반 파일 시스템의 경우) /
로 구별이 되어 있는데, /
사이에 있는 파일들이 전부 디렉토리 파일 하나씩에 대응된다. 실제로 우리가 찾는 파일인, 파일 경로 상의 마지막 파일 이름이 어느 inode number에 대응되는지는 그 직전 디렉토리 파일에 저장이 되어 있다.
이는 트리 구조를 만들게 되며, 이를 directory tree라고 한다.
file system(파일 시스템)은 위의 파일/디렉토리를 관리하는 체계를 나타낸다. 정확히는 파일 이름이 주어지면, 이를 기반으로 원하는 파일을 찾고 관련 내용물을 읽을 수 있는 체계를 구축한게 파일 시스템이다.
여러 종류가 있는데, 책에서는 UNIX 기반 파일 시스템을 설명하고 있다.
먼저 이 파일 시스템에서 파일의 '이름'을 어떻게 설정하는지에 대해 알아보자. 이 시스템에서 directory tree의 가장 위에는 root directory가 존재한다. /
로 표현한다.
앞에 말했듯이 /
로 하위 디렉토리 및 파일을 구별하며, root directory를 시작으로 만든 경로를 absolute pathname (절대 경로)라고 한다. 예시 : /foo/bar.txt
, /tmp/abc.txt
이 작명 규칙을 기반으로 UNIX 기반 파일 시스템에서 내부적으로 파일을 어떻게 찾고 내용물을 구하는지, 그리고 그 내용물을 우리가 프로그램(책에서는 C/C++ 프로그램)에서 어떻게 확인하고 사용하는지에 대해 이제 알아보자.
open
에다가 O_CREAT
flag를 사용한다.int fd = open("foo", O_CREAT|O_WRONLY|O_TRUNC, S_IRUSR|S_IWUSR);
- 예전에는
creat
를 사용했다.int fd = creat("foo")
- 이는 밑과 동일하다.
int fd = open("foo", O_CREAT|O_WRONLY|O_TRUNC)
- open의 2번재 param은 flag 관련이다.
- O_CREAT : 파일 존재 안할시 파일 생성
- O_WRONLY : 쓰기 전용
- O_TRUNC : 파일 존재시 내용물 전부 삭제
- open의 3번째 param은 권한 관련이다.
open
을 호출할 때 반환되는 int
값을 file descriptor이라고 한다. 프로세스 별로 관리가 되며, UNIX 기반 시스템에서 파일에 접근하는데 사용되는 숫자다. 이 값을 가지고 특정 파일을 읽거나, 뭘 쓰는 것이 가능해진다. 즉 파일 조작을 하려면 일단 open
혹은 다른 시스템 콜 API(creat
)을 활용해 해당 파일의 file descriptor을 일단 확보해야 한다. 물론 거기에 해당 조작에 대한 권한도 있어야겠지만.
프로세스 별로 file descriptor이 관리된다고 했다. 여기서 힌트를 얻어 프로세스 자체에서 file descriptor을 index로 해 어떤 파일 관련 구조물을 관리하고 있다 추측할 수 있다. xv6 kernel의 경우 밑에 보듯이 이 file descriptor은 file
이라는 struct
를 가리키는 pointer을 구하는데 사용됨을 알 수 있다. 파일 정보를 보유하는 struct file
의 구조에 대해선 후술.
struct proc {
...
struct file *ofile[NOFILE]; // Openfiles
...
};
strace
를 활용해 이루어지는 시스템 콜이 뭐가 있는지 분석이 가능하다. 이걸로 교재의 예제를 수행해보도록 하자. 교재에서 언급했지만, 교재에서 나온 출력보다 훨씬 복잡하게 나온다.
cat
은 파일의 내용물을 읽고 표준 출력에 이를 내뱉는 명령어다. 이 때문에 strace
로 분석해보면 O_RDONLY
로 open
을 호출함을 볼 수 있다. 파일에 뭘 쓰지 않을 것이기 때문.
또 open
의 file descriptor이 3을 반환하는데, 0/1/2는 표준 입력/출력/오류를 가리키기 때문이다.
O_LARGEFILE
은 필자가 우분투로 직접 실험했을 때는 나오지 않았었다. off_t
로 못 열고 off64_t
로는 열 수 있는 파일을 off_t
로 열 수 있도록 해주나, 프로그래머 입장에선 실제로 사용할 일은 별로 없고 다른 방식을 추천한다. 그래도 참고로 알아두자. 용도는 off_t
가 file 내 위치 추적 역할을 하는데, 32-bit system의 경우 이를 32-bit 정수를 사용해 파일 크기가 최대 2GB만 되는데 그거보다 큰 파일을 만들고 싶을 때 사용된다. (OS에 따라 offset을 부호가 있는 정수로 쓰냐 아니냐에 따라 4GB도 되는것으로 보인다. (윈도우) xv6도 이론상 4GB가 가능하다.)
파일을 읽을 때는 read
시스템 콜 API를 사용하며 parameter은 각각 file descriptor/buffer/buffer 크기를 나타낸다. strace
의 경우 buffer 위치에 buffer 내 내용물이 출력됨. 반환하는 값은 성공적으로 읽은 문자 개수.
파일을 쓸 때는 write
시스템 콜 API를 사용하며 parameter은 각각 file descriptor/buffer/buffer 크기를 나타낸다. strace
의 경우 buffer 위치에 buffer 내 내용물이 출력됨. 반환하는 값은 성공적으로 쓴 문자 개수. 물론 실제 cat
명령어는 아마 표준 출력으로 출력을 편하게 해주는 write
의 변형, printf
함수를 사용할 것이다.
또 cat
를 보면 본인이 읽는데 성공한 문자가 0개일때까지 read
를 반복하는 것을 볼 수 있다.
작업이 완료되면 close
를 호출한다. parameter은 file descriptor. 해당 descriptor이 본인이랑 연결된 struct file
을 더이상 안 쓰겠다는 뜻이다. 이게 대응되는 struct file
의 파기로 꼭 이어지지는 않는다. 이는 후술.
앞에서 off_t
에 대해 얘기를 했다. struct file
에서 보유 하고 있으며, 해당 descriptor이랑 연결된 파일 내에서의 시작점 기준 상대적 위치를 저장한다. 이걸 보통 file offset이라고 보통 말한다.
처음에 file descriptor이 open
등을 통해 생성되면 파일 시작 부분을 가리키며 off_t
는 0의 값을 가진다. read
나 write
를 하면 거기서 읽은/쓴 byte만큼 off_t
가 이동하게 된다. (implicit offset update)
이 off_t
를 임의로 조작하는 것이 가능하다. (explicit offset update) lseek
라는 시스템 콜 API를 활용하면 된다.
off_t lseek(int fildes, off_t offset, int whence);
option으로는
SEEK_SET
:offset
값으로 offset을 설정SEEK_CUR
: 현재 offset에offset
값을 더한 값으로 offset을 설정SEEK_END
: 파일 크기에offset
값을 더한 값으로 offset을 설정
참고로 struct file
에는 이외에도 연동된 file descriptor 기준 읽기/쓰기가 가능한지, 본인이 나타내는 파일의 inode를 나타내는 struct inode
의 pointer, 파일 참조 count(후술)등 여러 정보를 보유하고 있다.
struct file
이 열려있는 파일들을 나타내기에 이들을 모아서 open file table이라고 보통 부르며, xv6의 경우 이를 나타내는 자료구조가 struct ftable
이다. 그냥 단순 struct file
배열에 spinlock
을 넣은 형태.
서로 다른 file descriptor이 나오며
각각은 offset이 별도로 관리가 된다. (별도의 struct file
을 가지게 되고)
보통은 프로세스마다 고유의 open file table을 가진다.
하지만 fork
를 통해 생성된 child process는 parent process의 open file table을 공유한다. 정확히는 child process가 parent의 ofile
을 복사해가는 것이다. 나눠진 이후에 서로 추가로 여는 file은 공유가 안된다는 점 유의.
아까 파일 참조 count(xv6의 ref
)를 struct file
이 변수 형태로 저장한다고 했는데, 이와 같은 상황에서 활용된다. 예를들어 parent가 파일을 열었는데 그거의 fd가 3이고, 이후 child를 10개를 fork하면 관련 struct file
의 ref
는 11이된다. 하나씩 close
될 때마다 하나씩 줄게 된다.
보통 fork된 process들이 서로 협력하는 일이 많아서 기본적으로 이렇게 공유하면 유용한 경우가 많다고 한다.
int dup(int oldfd);
dup
은 file descriptor을 parameter로 받으며, 해당 file descriptor과 연동된 struct file
을 같이 활용하는 file descriptor을 하나 더 만들라고 할 때 활용된다. 반환값은 그 새로운 file descriptor.int dup2(int oldfd, int newfd);
dup2
는 oldfd
에 연동된 struct file
을 newfd
도 참조하도록 한다. newfd
가 이미 다른 struct file
참조시 close
후에 참조하도록 한다.
유의사항으로 oldfd
가 유효하지 않으면 실패하며 이 때 newfd
가 원래 참조하던 struct file
은 닫히지 않으며, oldfd
가 유효하고 newfd
가 oldfd
랑 같으면 아무 일도 안 일어난다.
반환 값은 newfd
다.
int dup3(int oldfd, int newfd, int flags);
dup3
는 dup2
랑 유사하나, flags
에 O_CLOEXEC
를 지정하면 ...며, oldfd
랑 newfd
가 같으면 EINVAL
오류와 함께 -1을 반환한다.
성공시 반환 값은 newfd
다.
dup
들은 쉘 구현, redirection 등에서 자주 활용된다.
write
를 호출해도 즉시 디스크에 작성되는 것은 아니고, 메모리에다가 일단 저장해놓고 나중에 디스크에 쓴다. (page swap때라든가) 즉시 disk에 쓰는 것을 강요하고 싶을 때 쓰이는게 fsync
다.int fd = open("foo", O_CREAT|O_WRONLY|O_TRUNC, S_IRUSR|S_IWUSR);
assert(fd > -1); //open returns -1 on error
int rc = write(fd, buffer, size);
assert(rc == size);
rc = fsync(fd);
assert(rc == 0);
정상 종료시 0 반환
이 때 foo
를 포함하는 directory file과 관련된 fd
에 fsync
를 호출해야 할 수도 있다. 왜냐하면 이것까지 되어야 해당 directory에서 그 파일을 포함하고 있다는 것을 디스크에 작성하는 것이 즉시 보장되기 때문. 이를 안 하면 버그가 생길 수도 있다.
file의 특정 위치랑 가상 메모리 주소를 연결시키는 것이다. 이를 통해 전원이 꺼져도 유지가 되는 메모리, 즉 persistent memory를 구현하는 것이 가능하다. 해당 process가 CPU를 통해 해당 가상 메모리 주소에 memory load/store을 해도 실제로는 파일에 작성하는 것이 되기 때문. 이때 연결된 파일을 backing file이라 하고, 연결된 갓아 메모리 주소를 in-memory image라고 한다.
책의 예제에서 p
는 pstack_t
라는 type을 가진다는 점 참고.
void *mmap(void addr[.length], size_t length, int prot, int flags, int fd, off_t offset);
addr
: mapping 시작 VM 주소. NULL
이면 커널이 알아서 주소를 찾는다. 지정을 해도 page align을 위반하지 않는 값으로 설정해서 꼭 그 값으로 설정이 되지 않을 수 있다.length
: 파일/mapping된 메모리 크기prot
: 메모리 보호 설정. 파일의 open 모드에 위반되면 절대로 안된다.flags
: 옵션. man page 참고fd
: 해당 공간의 initialization에 사용할 파일과 연동된 file descriptoroffset
: 해당 공간의 initialization에 사용할 파일의 시작 부분쉘에서 mv
를 사용하면 파일 이름을 바꿀 수 있다. 이 때 strace
로 사용하는 시스템 콜을 보면 rename
임을 알 수 있다.
두 문자열을 받는데, 전자는 이름을 바꾸려는 파일의 기존 이름, 후자는 바꿀 이름이다.
atomic operation이다. 즉 성공하거나, 아무 일도 일어나지 않거나 둘 중 하나만 보장된다. 오류가 생겨도 말이다.
책에선 이를 활용해 일반적인 파일 에디터에서 사용할 법한 수정한 파일을 저장하는 방식을 보여주고 있다. 재밌으니 참고. (14pg)
파일에 관한 정보, 즉 메타데이터를 얻고 싶으면 stat
이나 fstat
시스템 콜을 사용하면 된다. 각각 파일 경로 문자열 / file descriptor을 받는다.
정확히는 inode 정보를 알려주는 것이다. 파일 시스템이 파일을 관리하는데 필요로 하는 정보로 이 또한 디스크에 저장되어 있고, 특정 파일의 빠른 접근을 위해 inode를 메모리에 캐싱하는 경우가 많다. (왜 이게 더 빠르지도 후술)
관련 자료구조가 struct 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; // userID of owner
gid_t st_gid; // groupID of owner
dev_t st_rdev; // deviceID (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
로 사용하는 시스템 콜을 보면 unlink
임을 알 수 있다.왜 파일 제거 관련 시스템 콜의 이름이 저런지 알려면 디렉토리에 관해 더 자세히 알아야 한다.
앞에서 얘가 파일이라고 했고, 뭘 저장하는지도 배웠다. 그러나 앞의 파일 관련 시스템콜을 사용해서 이 녀석을 만들거나 수정하거나 하는 것은 안된다. 일반 파일로 취급을 안하고 inode와 파일 이름을 연결짓는 정보를 가진 메타데이터 형태의 파일로 취급하기 때문.
이 때문에 일반적인 파일 생성/삭제와 별도의 시스템 콜들을 가진다. 먼저 생성의 경우 mkdir
을 사용한다. 디렉토리의 이름과 inode 모드 값을 parameter로 받는다.
생성 직후에는 ./
와 ../
가 어떤 inode랑 연결되어 있는지를 저장해 놓는다. 그 외의 정보는 없음.
쉘의 ls
가 현 directory 내 파일들을 전부 출력하는데, 이 때 사용되는 시스템콜이 opendir
, readdir
, closedir
이다.
opendir
은 file descriptor을 반환하지 않고 DIR *
을 반환한다. DIR
은 directory stream이라고 불리며, 일반적인 파일 스트림 (FILE
)과 매우 유사하다. 즉 directory 내용물을 탐색하는데 유용하다.
혹시 file descriptor과 file stream의 차이를 모른다면 이 링크 참고
readdir
을 활용해서 stream을 읽는게 가능하다. opendir
을 처음에 호출하면 directory 내용물의 첫번째 entry에서 시작하게 되는데, 그 entry부터 하나씩 읽을려면 매번 readdir
을 호출해야 한다.
이 때 반환되는 녀석의 type은 struct dirent *
인데 다음과 같은 구조를 가진다.
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset, but position of directory stream
same as return value of telldir) */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported
by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};
이 entry로 directory 내 파일에 대한 구체적인 정보를 파악하긴 힘들기에, 이를 알고 싶으면 각 파일에 대해 개별적으로 stat
을 호출해야 한다. ls
에 -l
을 부여하면 이를 수행한다.
closedir
은 단순히 사용 완료된 directory stream을 닫을 때 쓰인다.
rmdir
시스템 콜을 사용하면 된다. rmdir
command도 사용하는 시스템 콜이다.
유의사항으로 rmdir
시스템 콜은 비어있는 directory에 대해서만 호출이 가능하다.
ln
이라는 command랑 관련이 있고, 관련이 있는 시스템 콜은 link
와 unlink
다. 아까 말한 파일 제거와도 관련이 있던 시스템콜이었다.
link
는 또다른 이름의 파일이, 어떤 존재하는 파일과 '같은' 파일을 가리키도록 설정하는데 쓰인다. 정확히는 같은 inode 파일을 가지도록 한다. 같은 inode 파일을 가진다 -> 같은 메타데이타 -> 해당 file system 하에서는 같은 파일이라는 논리.
즉 open
에서 파일을 만들 때 사실은 다음 두 과정을 거치는 것이다.
그리고 특정 파일의 삭제, 정확히는 해당 파일과 관련된 메타데이터가 더이상 필요 없다고 판별해 관련 정보를 전부 삭제하는 시점은 inode를 참고하고 있는 hard link의 개수가 0개일 때다.
여러 파일이 같은 inode를 참고할 수 있기 때문에 (hard link가 2개 이상일 수 있기 때문에) 파일 삭제를 함부로 할 수는 없고, unlink
를 사용해야 하며. 이 개수가 0이 될 때 (st_nlink
가 0이 될 때) 비로소 해당 정보를 file system에서 파기를 할 수 있는 것이다.
참고로 같은 inode랑 hard link가 되어 있는 파일들은 서로 다른쪽에서 수행한 변동을 확인하는 것이 가능. 말 그대로 파일 공유라고 생각하면 된다.
아까 hard links에서 소개한 링크를 참고해도 좋다.
이것도 ln
에서 구축이 가능한데, -s
flag를 써야 한다.
일단 hard link랑 가장 큰 차이점은, symbolic link로 형성된 파일은 일반 파일로 취급을 하지 않고 특수 파일로 취급한다는 것이다. 그냥 일반 파일이 아닌 고유의 파일이라고 생각하면 된다. (stat
을 통해 확인이 가능)
sycho@DESKTOP-4RPUOID:~$ echo hello > file
sycho@DESKTOP-4RPUOID:~$ ln -s file file2
sycho@DESKTOP-4RPUOID:~$ cat file2
hello
sycho@DESKTOP-4RPUOID:~$ cat file
hello
sycho@DESKTOP-4RPUOID:~$ stat file
File: file
Size: 6 Blocks: 8 IO Block: 4096 regular file
Device: 820h/2080d Inode: 1196 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ sycho) Gid: ( 1000/ sycho)
Access: 2024-02-20 02:59:04.270742503 +0900
Modify: 2024-02-20 02:58:59.930742949 +0900
Change: 2024-02-20 02:58:59.930742949 +0900
Birth: 2024-02-20 02:58:59.930742949 +0900
sycho@DESKTOP-4RPUOID:~$ stat file2
File: file2 -> file
Size: 4 Blocks: 0 IO Block: 4096 symbolic link
Device: 820h/2080d Inode: 2143 Links: 1
Access: (0777/lrwxrwxrwx) Uid: ( 1000/ sycho) Gid: ( 1000/ sycho)
Access: 2024-02-20 02:59:04.270742503 +0900
Modify: 2024-02-20 02:59:02.820742652 +0900
Change: 2024-02-20 02:59:02.820742652 +0900
Birth: 2024-02-20 02:59:02.820742652 +0900
symlink
다. 그래서 얘가 하는게 뭐냐면 목표 파일 경로를 가리키는 특별한 파일을 만드는 것이다. 특징으로file
이 4byte 크기 문자열이기 때문이다.)sycho@DESKTOP-4RPUOID:~$ ls -l files.txt
-rw-r--r-- 1 sycho sycho 12 Jan 3 22:24 files.txt
OS 공부를 하면서 많이 봐왔을텐데, 맨 처음 char(파일 종류를 나타낸다. -
은 일반 파일, d
는 디렉토리, l
은 symbolic link) 이후의 9개 출력이 나타내는 것은 3개씩 나눠서 소유자/그룹/그외에 대한 접근 권한이다. r
은 읽기, w
는 쓰기, e
는 실행 권한이다. 권한이 없으면 -
로 나타낸다.
directory의 경우 좀 특이하다. read 권한은 directory 내 존재하는 파일들이 뭔지 확인하는 권한을, write 권한은 directory 내에 파일을 추가하거나 삭제하는 권한을, execute 권한은 해당 디렉토리에 '접근'(cd
의 대상으로 가능)하게 해주거나 본인 하위 디렉토리로 접근하는것도 하게 해주거나 디렉토리 내 파일들의 구체적인 정보 (ls -l
)를 파악할 수 있게 해준다.
파일 소유자는 권한 수정이 가능하다. chmod
명령어를 쓰면 되며 거기서 chmod
시스템 콜을 호출한다. 정확히는 파일 모드를 바꾸는 것이다.
AFS 외의 몇몇 파일 시스템들은 위처럼 3개로 나누는게 아니라 좀 더 세세하게, 다양한 권한을 관리한다. 정확히는 특정 권한이 있는/없는 사람들을 일일이 기록하는데 이를 access control list라고 한다. 이걸 디렉토리마다 집어넣는다.
위와 같은 체계는 어쨌든 파일 시스템이라는 녀석이 구축을 해가지고 관리하는 것인데, 이 시스템 자체를 처음부터 만들고 싶다면 mkfs
를 사용해야 한다.
mkfs
에 파일 시스템이 관리할 장치랑 어떤 형태의 파일 시스템을 사용할지 지정하면 뚝딱하고 만든다.
그럼 끝인가? 사실 file system 구축이 끝나면 이것을 통합 파일 시스템에다가 연결해야 접근해서 작업하는 것이 가능하다. 통합 파일 시스템(컴퓨터에서 활용하는 파일 시스템)에 이미 존재하는 특정 경로에다가 파일 시스템을 붙여넣는것이라고 생각하면 된다. (정확히는 해당 file system의 metadata 및 각종 정보들을 읽어가지고 거기에 죄다 등록하는 것이다.) 이 과정을 mount (마운트)라고 하며 mount
시스템 콜을 활용하는 mount
프로그램을 사용해야 한다.
이 때 통합 파일 시스템 상에서 연결할 경로를 mount point라고 한다.
반대 과정을 unmounting이라고 하는데, 이 때 unmount되는 파일 시스템을 운용하는 장치에 기록되었어야 하지만 아직 대기중인 정보들도 정보 작성이 된다. 동시에 unmount되는 파일 시스템의 메타데이터도 전부 갱신한다. 위키백과에 따르면 컴퓨터 전원을 끌 때 컴퓨터에 mount된 모든 file system을 unmount시키는 작업을 수행한다고 한다.
USB, DVD의 운용도 이 mounting을 활용하는 것이다.