[CS:APP Chapter 10] System level I/O
CS:APP 10장을 읽고 작성한 핵심 내용이다.
Unix I/O
모든 file은 byte들의 연속으로 간주한다. 또한 regular file부터 device, socket 등등 모든 것을 file로 간주하기에, kernel은 simple한 I/O interface를 제공할 수 있다.
Types of File
- Regular File : 일반 파일들은 어떠한 데이터도 담을 수 있다. 흔히 text file로 부르는 file은 단지 모든 character가 ASCII(or Unicode)로 이루어진 것으로 간주되며, 이외의 file은 binary file로 분류된다. kernel에게는 다 똑같은 regular file이다.
- Directory File : Directory 조차 file로 간주된다. 여기서 directory란, filename과 inode를 mapping하는 directory entry들로 구성된 file을 말한다. 이처럼 특정 file에 대해 mapping 정보를 가지고 있는 것을 link라고 하며, hard link와 soft link로 구분된다.
- Socket : 서로 다른 프로세스가 communicate하기 위한 file이다. 해당 챕터에서는 핵심적으로 다루지는 않는다.
- Others : named pipes, block devices 등이 있다.
Kernel은 하나의 거대한 tree(= single directory hierarchy)로 이 file들을 전부 다룬다. 이 tree의 root, 즉 시작 directory는 root directory(=”/”)이다.
각 process는 현재 자신이 속해있는 directory인 current working directory, 즉 이 tree에서 본인이 속한 위치에서 작업을 수행하며, cmd에서 cd와 같은 명령어로 이를 수정할 수 있다.
이러한 single directory hierarchy 구조에서 location은 pathname에 의해 특정되는데, pathname은 filename과 /(slash)로 구성된 string이다. 2가지 유형이 있다.
- absolute pathname : 절대경로라고도 하며, “/”로 시작하여 root로부터의 path를 나타낸다.
- relative pathname : 상대경로라고도 하며, filename으로 시작하여 current working directory에서부터의 경로를 나타낸다.
Open, Close, Read, Write Files
이러한 file들을 다룰 수 있는, kernel에서 제공하는 system call들이 있다. 사용자는 device에 직접 접근이 불가하므로, kernel의 system call을 통해 접근한다.
Open
syntax는 아래와 같다.
int open(char *filename, int flags, mode_t mode)
// return new fd number if success, -1 if fail
// every unix I/O function store the cause of error on "errno" global variable
각 매개변수가 의미하는 바는 아래와 같다.
- char *filename : 말 그대로 파일 이름이다. 예를 들어 text file의 경우 foo.txt 등이 된다.
- int flags : 현재 process가 해당 file에 access할 때 어떻게 하는지에 관한 내용이다. 기본적으로 다음과 같이 3가지 권한이 있다.
[O_RDONLY(= read only) / O_WRONLY(= write only) / O_RDWR(= read and write)].
file에 write할 경우 다음과 같은 3가지의 mask를 추가로 설정할 수 있다.
[O_CREAT(= file이 존재하지 않을 때 빈 file을 create) / O_TRUNC(= file이 이미 존재한다면, 내용물을 모두 비운다) / O_APPEND(= 내용을 추가할 때 항상 file의 끝에서만 추가 가능)] - mode_t mode : new file을 만들 때 해당 file에 대한 접근 권한을 설정하는 것으로, flags와 마찬가지로 mask들로 나타낼 수 있다. 해당 자리에 들어가는 mask는 기본적으로 S_IXYYY의 구조를 가지고 있으며, X = R(read) / W(write) / X(execute), YYY = USR(user) / GRP(group) / OTH(others)중 하나로 총 9가지의 mask가 존재한다.
추가로 각 process마다 umask를 가진다. mode로 넘겨준 mask들 중 umask에 해당하는 부분을 지운 후(= mode & ~umask) 권한 비트가 최종적으로 설정된다.
Close
syntax는 아래와 같다.
int close(int fd);
// return 0 if success, -1 if fail
file descriptor 번호를 받아, 해당 file을 close한다. 주의사항으로는 이미 닫은 fd를 또 닫으면 안된다. 이러한 행위는 멀티스레드 작업 환경에서 혼란을 야기할 수 있다.
Read and Write
syntax는 아래와 같다.
ssize_t read(int fd, void *buf, size_t n)
// return number of bytes read if success, 0 if EOF, -1 if fail
ssize_t write(int fd, const void *buf, size_t n)
// return number of bytes write if success, -1 if fail
-
read : file descriptor fd를 받아, 해당 file에서 n byte를 읽어, buf에 저장하는 함수이다. file의 내용을 읽으면, kernel memory에 위치한 open file table의 current file position이 n만큼 이동한다.
-
write : file descriptor fd를 받아, buffer buf에서 n byte만큼 해당 file에 쓰는 함수이다. 마찬가지로 file의 내용을 쓰면, current file position이 n만큼 이동한다.
가끔 어떠한 이유로 read와 write 함수에 요청한 byte 수보다 적게 read 또는 write될 수 있다. 이를 short count라고 하며, error가 아니다.
short count의 원인은 다양하다. read 도중 EOF를 만나거나, terminal에서 사용자로부터 한 줄 단위로 입력을 받거나, network socket에 read 또는 write를 하거나…
추가로 ssize_t와 size_t의 차이는 ssize_t = signed size_t라는 것이다. read와 write는 return value로 음수가 포함될 수 있으므로 signed size_t, 매개변수 n은 음이 아닌 정수여야 하므로 size_t를 사용한다.
read의 경우 buf에 내용을 채우는 것이므로 const 선언이 없고, write의 경우 file에 내용을 쓰는 것이므로 buf 안의 내용을 const 선언을 통해 유지하고, 대신 buf에서 내용을 읽는 것은 buf 내부의 pointer를 움직여서 읽는다.
Metadata of File
file에 관한 정보(access time, inode 등)를 file의 metadata라고 한다. C에서는 stat, fstat 등의 함수를 통해 metadata에 접근할 수 있다.
syntax는 아래와 같다.
int stat(const char *filename, struct stat *buf)
int fstat(int fd, struct stat *buf)
// return 0 if success, -1 if fail
해당 함수들은 모두 두 번째 인자인 buf에 file의 metadata를 저장한다. buf의 type은 stat라는 구조체로, file의 metadata를 저장할 수 있는 변수들이 선언되어 있다.
struct stat 내부의 매개변수들은 아래와 같다.
struct stat{
dev_t st_dev;
ino_t st_ino; // inode
mode_t st_mode; // file type and permission
nlink_t st_nlink; // number of hard links
uid_t st_uid; // user id
gid_t st_gid; // group id
dev_t st_rdev;
off_t st_size; // total size
unsigned long st_blksize; // block size in filesystem I/O
unsigned long st_blocks; // number of 512B block allocated
time_t st_atime; // last access time
time_t st_mtime; // last modification time
time_t st_ctime; // last inode change time
}
-
st_mode : file의 type과 permission 정보를 모두 가지고 있다.
S_IS***(stat.st_mode) macro를 통해 file의 type을 알 수 있으며, S_IXYYY mask를 통해 file의 permission 정보를 알 수 있다.
이때 X = permission(R, W, X)이며, Y = USR, GRP, OTH이다. -
st_nlink : file의 hard link의 개수이다. hard link란, 같은 inode를 가리키는 또 다른 이름이며, ‘ln’ 명령어를 통해 만들 수 있다.
-
st_uid, st_gid : file의 user id와 group id를 담고있다. 구체적인 정보는 getpwuid(stat.st_uid) 및 getgrgid(stat.st_gid)를 통해 알 수 있다.
-
st_size, st_blocks : file의 size와, 이 file에 512B block 몇 개가 할당되었는지에 관한 정보이다.
-
st_xtim[e] : atime, mtime, ctime에 대한 내용은 위 syntax와 같다.
Open file in Unix
Unix에서 open file을 manage하는 방법은 3개의 table을 이용하는 것인데, 각 table의 명칭과 담긴 정보들은 다음과 같다.
-
File Descriptor Table : file descriptor를 index로 하는 table로, 각 entry에 다음 단계의 table인 Open File Table을 가리키는 pointer를 가지고 있다.
process마다 고유한 table이며, process 실행 시 기본적으로 0 = standard input, 1 = standard output, 2 = standard error에 할당된다. -
Open File Table : 현재 file의 current file position과 reference count, 그리고 다음 단계 table인 v-node table을 가지고 있다. 여러 개의 process들에 의해 공유될 수 있다.
-
v-node table : file의 metadata 정보를 담고 있는 table이다. 모든 process에 의해 공유된다. 즉, 같은 file을 여러번 open한 경우에도 v-node table은 1개만 존재한다.
하나의 file에 대해 open을 2번 하게 되면, 서로 다른 fd를 부여받고, 서로 다른 open file table을 가지게 된다. 그러나, v-node table은 서로 같은 것을 가리킨다.
fd가 어떤 open file table을 가리키게 할 지는 dup, dup2 함수를 통해 조정할 수 있으며, syntax는 아래와 같다.
int dup(int oldfd);
int dup2(int oldfd, int newfd);
// return new fd if success, -1 if fail
-
dup : oldfd가 현재 가리키고 있는 open file table을 가리키는 새로운 fd를 반환한다.
-
dup2 : newfd를 close 하고(열려있었다면), oldfd가 가리키고 있는 open file table을 newfd가 가리키게 한다.
두 함수 공통적으로 oldfd가 가리키고 있었던 open file table의 refcnt는 1씩 증가하게 된다. 왜냐하면 해당 open file table을 참조하는 pointer가 하나씩 늘어나기 때문이다.
일반적으로, unix i/o는 극한의 성능 향상을 목표로 할 때나, signal handler 안이나 network socket의 i/o를 처리하는데 사용된다. 다만, C standard library에 비하면 short count를 수동으로 처리해야 한다는 점과, string 입력 처리에 비효율적이라는 단점이 있다.