본문 바로가기

개발/PintOS

Pintos Project(핀토스 프로젝트) 2 - User Program_File management

이전 프로젝트에서 사용자와 상호작용할 수 있는 System call 몇 가지를 구현했다. 이번 프로젝트는 File Management와 관련된 System Call을 구현한다. 원래는 이전 프로젝트와 이번 프로젝트를 함께 진행해야 하지만 난이도가 워낙 높은 프로젝트이다보니... 학교 측에서 둘로 나눈 것 같다.(감사합니다 선생님...흑흑)

이전에 구현한 System Call과 잘 연동되어야 하는 것은 물론이고 File Processing도 잘 되어야 한다. 예를 들면 한 File을 두 Program이 접근할 경우 Race Condition이 발생할 수 있으므로 이를 잘 처리해야 한다. 이 외에도 파일을 이용한 Operation에 관한 다양한 이슈를 해결하면서 File System Call을 구현한다.

 

무엇을 해야 하는가? 

1. File System Call

- 현재 file에 관한 System call은 거의 구현이 안되어있다. 그 system call을 구현한다. 이번 프로젝트는 저번보다 훨씬 괜찮은 게 File에 관한 거의 모든 함수가 이미 구현되어 있다. 표면적으로는 create, remove, open, filesize, read, write, seek, tell, close 함수만 구현하면 되지만 이들과 연동하려면 process.c의 함수 등 다양한 함수를 수정해야 한다. 

 

2. Denying Writes to Executables

- 실행 중인 파일에 쓰기 작업을 수행하면 예상하지 못한 결과를 얻을 수도 있다. 실제 OS도 이런 경우를 방지하고 있다. file_deny_write(), file_allow_write()를 활용해서 이를 완성할 수 있다.

 

3. Synchronization 

- 현재 PintOS는 File 연산에 대한 동기화가 구현되어있지 않은 상태다. 2번에서 봤다시피 file 연산끼리 동기화가 되어있지 않을 때 동일한 파일을 가지고 작업을 하면 Error가 발생할 확률이 몹시 몹시 높다. 이전 프로젝트에서 process와 thread의 동기화를 담당했던 semaphore, lock 등을 이용해서 동기화를 완수할 수 있다.

 

4. Memory Management 

- Memory Management를 제대로 하지 않으면 Zombie process & Orphan process가 양산되어서 시스템의 성능이 저하되고 심할 경우 종료될 수도 있다. Memory Management를 통해서 메모리 누수가 발생하지 않도록 한다. 

 

구체적으로 해보자

 

file system을 구현하려면 우선 system call handler를 완성해야한다. 

대충 이런식이다. 지난 프로젝트에서 만들었던 system call과 같다. userprog/syscall.c 에 있는 system call handler의 switch 문을 확장하고 각 case마다 "지금 접근하려는 주소가 정상적인 주소인지" 체크한 후 해당하는 함수를 구현한다. 만약 return 값이 있는 system call 이라면 eax로 return 값을 받는다. 이건 manual에 있는 부분이므로 간단하고 행복하게 구현할 수 있다. 예시로 나와있는 remove는 boolean 값을 return 하기 때문에 f->eax로 return 값을 받았다. 

 

이번에 구현해야 하는 건 remove, open, filesize, read, write, seek, tell, close 이다. 파일 관련된 함수는 src/filesys의 file.c, file.h, filesys.h, filesys.c 등에 구현되어 있기 때문에 이들을 참고하면 더욱 좋을 것이다.  PintOS에서 각 파일은 file descriptor로 관리된다. 원래는 그 숫자에 특정한 제한이 있지는 않지만 Manual에서 파일 자체의 이름은 14자를 넘지 않는 다는 사실과 file descriptor의 대략적인 최대 개수를 알려주었으므로 다음과 같은 변수를 thread 구조체에 추가한다. 

 위에 보면 struct file * file_descriptor[128], struct thread * parent, struct semaphore exe_child, int flag가 추가되었다. struct file * file_descriptor[]는 이 Process와 연관된 file descriptor들을 관리하려고 만든 구조체이다. 아까도 말했지만 128로 지정한 이유는 Pintos Manual에 그런 식으로 나와있기 떄문이다.

struct thread * parent를 추가한 이유는 자식 Process가 부모 Process를 알아야 할 수 있는 작업이 있기 때문이다.

struct semaphore exe_child는 자식 Process가 Execution 중인 지 알려준다. 만약 이걸 몰라서 부모가 먼저 죽어버리거나, 자식이 일을 끝냈는 데 뒷정리를 하지 않으면 메모리 누수가 발생할 것이다.

int flag는 비정상적으로 종료된 Child Process가 있는 지 알려주는 변수다. 

 

thread 구조체에 변수를 추가했으므로 init_thread()에서 초기화 작업도 해주어야 한다. 

file_desriptor 배열이 텅 비어있으므로 모든 배열에 NULL 값을 지정한다. 이 file_descriptor에 담기는 struct file은 filesys/file.c에 있다. 그리고 sema와 parent 등을 초기화한다. 여기서 주로 다루는 struct file은 아래와 같은 구조체다.

 

inode는 file에 대한 정보를 담고있는 구조체이고 pos는 말 그대로 position이다. deny_write는 file_deny_write()가 불렸는 지 체크한다. 즉 file synchronization Issue를 해결하는 데 도움을 줄 변수다. file은 열릴 때 하나의 file desccriptor를 부여받는데, 이 file descriptor를 관리하려고 만든 게 file_descriprtor 배열이다. 즉, 각 Process에서 독립적으로 file_descriptor 배열을 통해서 file을 관리한다. 

 

기초작업을 마쳤으니 System call을 짜러 가자. void exit(int status)

이전 프로젝트에서 구현한 System Call 이지만 file operation에 관한 내용을 추가했다. exit을 할 때 현재 thread의 Child Process의 List를 돌면서 Process_wait()를 수행한다. 부모 Process는 절대 자식 Process보다 먼저 죽으면 안 된다. 먼저 죽으면 Orphan Process, Zombie Process 등이 발생하면서 메모리 누수가 발생하기 때문이다. process_wait을 시행함으로써 그런 위험성에서 벗어날 수 있다. 

그리고 현재 thread의 file_descriptor 중 할당된 struct file이 있으면 close를 해준다. 파일을 닫지 않고 Process가 죽으면 다른 Process가 그 file을 쓸 수 없기 때문이다. 

 

Pid_t exec(const char * cmd_lines)

exec도 이전 프로젝트에서 구현했던 System call이지만 file에 관한 operation을 추가했다. 중간에 filesys_open을 추가해서 매개변수로 들어온 file을 open한 다음 file_descriptor를 받아서 file이라는 변수에 assign 한다. 만약 assign을 실패했다면 file이 제대로 열리지 않은 것이므로 -1을 return 한다. 

 

첫번째 사진은 proess_execute() 함수인데 thread_create()를 통해서 child Process를 만든다 Parent Process가 Child보다 먼저 죽는 경우를 막으려고 sema_down을 걸어주고 Child Process가 종료되면 sema_up을 해준다. 두번째 사진은 start_process의 내용인데 LOAD 함수가 정상적으로 끝나면 load가 잘 됐다는 거니까 부모 Process의 sema를 up 해준다. 이렇게 하면 load 함수가 실패하는 경우를 방어할 수 있다. 그리고 load가 잘 안되면 success가 false 일텐데 제일 밑에서 flag를 1로 바꿔줌으로써 Parent Process에서 비정상적으로 종료된 Child Process를 식별할 수 있다. (process_execute 밑에보면 flag가 1일 때의 Error 처리를 구현했다!!) 

bool create (const char * file, unsigned initial_size)

매개변수로 받은 file의 주소값이 NULL이라면 파일을 생성할 수 없으므로 exit(-1)을 호출한다. 그게 아니면 filesys_create()를 통해서 새로운 file을 만든다. 

 

bool remove(const char * file)

매개변수로 들어온 file 값이 비정상적인 값이라서 remove 를 할 수 없다면 exit(-1)을 호출한다.

filesys_remove 함수를 활용해서 파일을 remove 한다. 

int open(const char * file)

매개변수로 받은 file 이름이 비정상적이라면 파일을 열 수 없으므로 검사해서 exit(-1)을 호출한다. 이 검사를 통과했다면 file이 정상적이라는 의미이므로 lock_acquire()를 통해서 다른 Process가 이 파일을 open 하거나 어떤 연산을 할 수 없게 한다. 준비가 끝났으니 filesys_open()을 통해 file을 open하고 그 return 값으로 stuct file * 를 받는다. 즉 현재 opening_file 에는 해당 file의 struct pointer가 들어가 있다. 만약 open에 실패했다면 opening_file은 NULL일 것이므로 -1을 return 한다. 

파일의 포인터도 정상적으로 받았다면 이 Process의 file_descriptor[]를 돌면서 빈 공간을 찾는다. 만약 비어있는 file descriptor number를 찾았는데 현재 thread의 이름과 file의 이름이 같으면 다른 file이 write를 할 수 없도록 file_deny_write()를 호출한다. 

적당한 공간을 찾았다면 해당 번호가 file descriptor가 되므로 이를 return 한다.

아참!! 그 전에 lock_release를 해줘야 한다!!!!

 

int filesize(int fd)

fd_check()로 fd가 정상적인 fd인지 검사한다. 이 fd_check()는 저어ㅓㅓㅓㅓ기 밑에 소개했다.(엄청 간단한 함수라 직접 구현하셔도...) 정상적인 fd라면 미리 구현되어있는 file_length()를 이용해서 filesize를 return 한다. 

int read(int fd, void * buffer, unsigned size)

read와 write 모두 다른 process의 간섭을 거부하므로 lock_acquire()를 통해서 자신만의 안락한 공간을 마련해주도록 하자. 그 후에 fd == 0 이면 STDIN이다.(Pintos MANUAL)... 그러므로 input_getc()로 문자를 받아들인다. 그렇지 않으면 현재 thread의 file_descriptor를 조사해서 NULL이면 읽을 수 없는 파일이므로 lock을 해제하고 exit(-1)을 호출한다. (열리지도 않은 파일을 읽으라고 명령한 것이니 에러인 셈이다.) 그렇지 않다면 file_read()함수를 호출한다. 

 

int write(int fd, const void * buffer, unsigned size)

아까도 말했지만 read와 write는 다른 Process의 간섭을 거부한다. 그러므로 lock_acquire를 통해서 안락한 공간을 확보해주자. 만약 fd가 1이면 STDOUT 이므로 putbuf() 함수로 작업한다. 만약 그게 아니라면 fd가 3 이상이라는 의미다. 0은 STDIN, 1은 STDOUT, 2는 STDERR 이니까... 만약 STDERR가 뜰 때를 대비해서 미리 ret의 초기값을 -1로 줬다. 

아무튼, 현재 thread에서 fd에 해당하는 file이 NULL이라면 열지도 않은 file에 write하라고 명령한 셈이므로 lock_release()를 호출하고 exit(-1)을 한다.(에러 처리인셈!) 그게 아니라면 file_write() 함수로 write 연산을 수행하고 write한 값을 ret 변수에 assign 한다. 그걸 return~~ 

void seek (int fd, unsigned position)

드디어 거의 끝나간다. 이제는 그냥 원래있는 함수 가져다 쓰는 정도다... 

unsigned tell(int fd)

void close(int fd)

 

void fd_check(int fd)

얘는 내가 만든 함수임다 

open, read, write에 있는 lock_acquire와 lock_release로 동기화 문제를 해결할 수 있었고 file_write_deny()함수를 통해서 파일 접근을 제한할 수 있었다. file_close()를 하면 자동으로 file_write_allow()를 해준다. (코드를 다 뜯어보는 거 추천 개존잼)

 

모든 테스트 통과~~