핀토스 프로젝트 2의 시작은 시스템 콜부터다. 사실상 argument passing은 몸풀기... 문제는 몸풀기 수준이었던 argument passing조차도 쉽지만은 않았다는 거..!
이번 포스팅에서는 시스템 콜 중에서 파일 입출력과 관련된 시스템 콜들을 다뤄볼 예정이다. 저번에 웹 프록시 서버를 구현할 때 소켓을 파일로 추상화하여 소켓에게도 파일 디스크립터가 붙었는데, 이번에도 파일 디스크립터 개념을 사용한다. 파일 디스크립터와 관련해서는 다뤘던 포스팅이 있으니 참고하자.
https://codable.tistory.com/19
File I/O System Call 목록
1. create
2. remove
3. open
4. filesize
5. read
6. write
7. seek
8. tell
9. close
파일 입출력 관련 시스템 콜을 구현하기 전에, 각 스레드들은 고유한 file descriptor table을 가지고 있어야 한다. 매번 쓰기에 너무 기니까 포스팅에서는 파일 테이블이라고 언급하겠다. 파일을 open 하면 새로 연 파일을 파일 테이블에 추가해 주고, 추가한 위치의 인덱스가 file descriptor라고 알려주어야 한다. 이를 위해 스레드 구조체에 파일 테이블을 추가해 주는데, 이때 파일 테이블을 struct file *fdt[128]과 같이 포인터들을 담은 배열 형태로 선언하면 스레드 구조체의 크기가 너무 커지게 된다! 이를 방지하기 위해 스레드 구조체에는 파일 테이블의 시작 주소만 가지고 있을 수 있게끔 struct file **fdt 형태로 선언해두고, 스레드가 생성될 때 파일 테이블을 동적으로 할당받게끔 해주자.(palloc)
사실 파일 입출력 관련 시스템 콜들은 대부분이 wrapping 함수의 형태를 띠고 있기 때문에 구현에서 크게 어려운 부분은 없었다. 예외 처리만 조금 신경 써서 해주자!
File I/O System Call
1. create(const char *file, unsigned int initial_size)
첫번째로 create 함수이다. 생성할 파일의 이름과 만들 파일의 사이즈를 파라미터로 받고, 디스크에 해당 이름으로 파일을 만드는 시스템 콜이다. 파일 생성에 성공하면 true를, 실패하면 false를 리턴한다. filesys_create 함수를 통해 구현할 수 있다.
2. remove(const char *file)
지울 파일의 이름을 파라미터로 받고, 디스크에서 해당 이름과 같은 이름을 가진 파일을 지우는 시스템 콜이다. 파일 삭제에 성공하면 true를, 실패하면 false를 리턴한다. filesys_remove 함수를 통해 구현할 수 있다.
3. open(const char *file)
파라미터로 받은 file과 같은 이름을 가진 파일을 디스크에서 찾아 연다. filesys_open 함수를 통해 구현할 수 있으며, 파일을 정상적으로 열지 못한 경우 -1을 리턴한다.
4. filesize(int fd)
파라미터로 크기를 구할 파일의 파일 디스크립터를 받는다. 해당 파일 디스크립터에 존재하는 파일의 크기를 리턴하는 함수이다. file_length 함수를 통해 쉽게 구현할 수 있다. 만약 파일을 찾지 못하면 -1을 리턴한다.
5. read(int fd, void *buffer, unsigned int size)
read와 write 함수는 다른 시스템 콜들과는 다르게 락을 활용해야 한다. 파일을 읽는 도중, 또는 파일에 값을 쓰는 과정에서 다른 스레드가 파일에 접근하여 값을 바꿔버릴 수 있으므로, 한 파일에는 하나의 스레드만 접근할 수 있게 하기 위해 락을 사용한다.
파라미터로 파일의 디스크립터 fd, 파일에서 값을 읽어 저장할 buffer, 읽어들일 값의 크기인 size를 받아온다. read의 경우 standard input에서 값을 읽어올 수 있는데, 이 경우 파일 디스크립터 값이 0이다. standard input에서 값을 읽어오는 경우 input_getc 함수를 통해 구현할 수 있고, 다른 파일을 열어서 읽는 경우 file_read 함수를 통해 구현할 수 있다. 파일을 제대로 읽어오지 못하는 경우 -1을 리턴하며, 다른 경우에는 읽어온 값의 크기를 리턴한다.
6. write(int fd, void *buffer, unsigned int size)
read와 마찬가지로 락을 사용하여 구현해야한다. 값을 쓸 파일의 디스크립터 fd, 쓸 값이 들어있는 buffer, 쓸 값의 크기인 size를 파라미터로 받는다. write도 read와 비슷하게 standard output에 값을 쓸 수 있다. 이때 파일 디스크립터 값은 1이며, 콘솔에 값을 쓰는 경우에 해당한다. 함수는 putbuf를 통해 구현할 수 있으며, 다른 파일에 값을 쓰는 경우에는 file_write 함수를 통해 구현할 수 있다. 파일에 값을 쓰지 못한 경우에는 -1을 리턴하며, 값을 쓴 경우에는 쓴 값의 크기를 리턴한다.
7. seek(int fd, unsigned int position)
파일의 offset을 position으로 바꿔주는 함수이다. file_seek 함수를 통해 구현할 수 있다. void 형태로 리턴 값이 없다.
8. tell(int fd)
파일의 offset 값을 리턴하는 함수이다. 존재하지 않는 fd 값이 들어오거나 이미 close 한 파일을 찾는 경우 -1을 리턴한다. file_tell 함수를 통해 구현할 수 있다.
9. close(int fd)
open으로 열었던 파일을 닫는 함수이다. file_close를 통해 구현할 수 있으며, void 형태의 함수로 리턴 값은 없다.
About Memory Allocation
프로젝트 2에는 메모리 누수를 체크하는 multi-oom 테스트가 존재한다. 여기서 open 시스템 콜이 굉장히 많이 호출되는데, 열었던 파일을 제대로 닫아주지 않으면 메모리 누수가 발생하여 테스트를 통과하지 못하게 된다. 처음에 원인이 파일 I/O system call인 open과 close에 있다고 생각하여 조금 자세히 들여봤었다.
file_open
struct file *
filesys_open (const char *name) {
struct dir *dir = dir_open_root ();
struct inode *inode = NULL;
if (dir != NULL)
dir_lookup (dir, name, &inode);
dir_close (dir);
return file_open (inode);
}
file_open 함수를 보기 전에, filesys_open 함수를 먼저 보자. 위의 코드를 보면 파일을 open하기 전에 먼저 디렉터리에서 파일을 찾는 것을 볼 수 있다. 파일이 존재하는 inode를 찾아 그 inode를 파라미터로 넘겨 file_open 함수를 실행시키는 것을 볼 수 있다.
struct file *
file_open (struct inode *inode) {
struct file *file = calloc (1, sizeof *file);
if (inode != NULL && file != NULL) {
file->inode = inode;
file->pos = 0;
file->deny_write = false;
return file;
} else {
inode_close (inode);
free (file);
return NULL;
}
}
file_open 함수의 코드이다. filesys/file.c 에 적혀있다. 코드를 보면 파일을 열때 calloc을 통해 메모리를 동적으로 할당받는 것을 볼 수 있다. 새로 할당받은 파일과 inode를 연결하여 파일 구조체가 실제 값을 참조할 수 있게 해 주었다. 여기에서 동적 할당이 이루어지기 때문에 파일을 닫거나 스레드가 종료할 때 할당받은 파일들을 모두 free 해주어야 한다고 생각했다.
file_close
/* Closes FILE. */
void
file_close (struct file *file) {
if (file != NULL) {
file_allow_write (file);
inode_close (file->inode);
free (file);
}
}
file_close 함수는 file_open 과는 다르게 wrapping 함수가 없었다. 파일 구조체의 주소값을 인자로 받아 free 해주는 함수이다. 중간에 파일의 inode도 close를 해준다. 아마 프로젝트 4에서 좀 더 자세히 다룰 것 같아서 더 깊게 파보지는 않았다. 이렇게 file_close 함수에서 할당받은 메모리를 반환하는 과정을 확인할 수 있었고, 이를 통해 스레드가 갑자기 비정상적으로 종료하거나 아니면 정상적으로 종료하는 상황에서도 열었던 파일들에 대해 close를 진행해 주어야겠다는 생각을 했다. 물론 원인이 여기는 아니었지만... multi-oom 테스트와 관련한 메모리 누수 문제는 다음 포스팅에서 다루도록 하겠다. 아무튼 원인이 여기가 아닐지라도 스레드가 종료할 때 파일 테이블에 존재하는 모든 파일들을 닫아주는 작업이 진행되어야 한다!
'Operating System' 카테고리의 다른 글
[Pintos] Project 2 - System Call(fork, wait, exec, exit) (0) | 2023.05.20 |
---|---|
[Pintos] Project 3 - Memory Management (0) | 2023.05.16 |
[Pintos] Multi-Level Feedback Queue Scheduler(mlfqs) (1) | 2023.05.08 |
[Pintos] Project 2 User Programs - Argument Passing (1) | 2023.05.08 |
[OS] CPU Scheduling (1) | 2023.05.06 |