-
[PintOS] Project 2 - System Call (2)Projects/Krafton_Jungle_4 2024. 3. 19. 12:53728x90
어제에 이어서 계속 구현 중이다.
오늘은 퀴즈 보는 날인데 나에겐 퀴즈보다 더 구현이 더 중요하다..
create()
create()를 위해선 filesys 폴더에 있는 filesys.c를 이용해야 한다.
write 부분에서 이를 활용하라는 깃북은 없었지만, close에는 있다.
filesys.c의 여러 함수들 중에서 filesys_create()라는 함수를 사용한다. 함수를 살펴보자.
/* Creates a file named NAME with the given INITIAL_SIZE. * Returns true if successful, false otherwise. * Fails if a file named NAME already exists, * or if internal memory allocation fails. */ bool filesys_create (const char *name, off_t initial_size) { disk_sector_t inode_sector = 0; struct dir *dir = dir_open_root (); bool success = (dir != NULL && free_map_allocate (1, &inode_sector) && inode_create (inode_sector, initial_size) && dir_add (dir, name, inode_sector)); if (!success && inode_sector != 0) free_map_release (inode_sector, 1); dir_close (dir); return success; }
inode라는 것이 나오는데 간략하게 보자면 inode는 파일에 대한 메타데이터이다. 동작 순서를 보자면,
- 루트 디렉토리를 연다. 파일 시스템의 최상위 위치에서 작업을 시작할 수 있도록 한다.
- free_map_allocate을 통해 디스크에 비어있는 공간에서 하나의 섹터를 할당받으려고 시도하고, 이는 inode_sector에 저장된다.
- inode_create를 통해 할당받은 섹터에 실제로 inode를 생성한다.
- dir_add를 통해 디렉토리애 새 파일을 추가한다. 파일 이름, 할당받은 inode 섹터의 번호를 디렉토리의 엔트리로 추가한다.
- 만약 파일 생성에 실패하면, 할당받았던 inode_sector를 메모리 누수 방지를 위해 해제한다.
- 필요하지 않은 디렉토리 구조체를 닫는다. 열린 파일 혹은 디렉토리 핸들을 정리한다.
아직 inode나 directory에 대한 공부를 덜해서 자세한 설명은 어려울 것같으니 일단 filesys_create가 어떤 함수인지만 알아놓자.
bool create(const char *file, unsigned initial_size) { if (filesys_create(file, initial_size) == 1) { return 1; } else return 0; }
일단 이렇게 하란대로 만들어 놓고, 다음 syscall 구현을 위해 필요한 write를 구현해보자
write()
내가 참고한 블로그에 의하면, 그리고 나 역시 디버깅한 바에 의하면 exit, halt 등의 시스템 콜을 호출하면 최초로 불리는 시스템 콜이 다름아닌 10번 write이다. 하지만 우리의 write는 구현이 안되어있으므로 무한 loop에 빠진다.
따라서 write에 대한 간단한 구현을 거쳐야 한다.
바로 1의 값을 갖는 파일 디스크립터(STDOUT)을 생성해 write()로 출력해줘야 한다.
파일 디스크립터 번호가 1인 즉, 출력을 하라는 경우에 한해 값을 출력하는 함수를 작성한다.
이 때 putbuf()를 활용한다.
간략하게 설명하자면 putbuf()는 버퍼 안에 들어있는 값 중 사이즈 N만큼을 console로 출력하는 함수이다.
fd 1은 콘솔에 적어준다는 말이 깃북에 나와있다.
int write(int fd, const void *buffer, unsigned size) { if (fd == 1) putbuf(buffer, size); return size; }
나는 여기서 계속 timeout이 나온다. 디버깅을 계속 해봐야겠다.
process_wait()을 수정하였다(약 10억..) 동료들 말에 의하면 루프의 숫자에 따라 pass, fail이 달라진다고 한다.
더불어, syscall_handler()의 케이스의 return 값을 수정해주었다.
void syscall_handler(struct intr_frame *f UNUSED) { // TODO: Your implementation goes here. // printf("system call!\n"); // printf("system call number: %d \n", f->R.rax); check_address(f); // 인자 들어오는 순서 %rdi, %rsi, %rdx, %r10, %r8, %r9 switch (f->R.rax) { case SYS_HALT: halt(); break; case SYS_EXIT: exit(f->R.rdi); break; case SYS_FORK: break; case SYS_EXEC: exec(f->R.rdi); case SYS_WAIT: break; case SYS_CREATE: f->R.rax = create(f->R.rdi, f->R.rsi); case SYS_REMOVE: remove(f->R.rdi); case SYS_OPEN: open(f->R.rdi); case SYS_FILESIZE: filesize(f->R.rdi); case SYS_READ: read(f->R.rdi, f->R.rsi, f->R.rdx); case SYS_WRITE: f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx); break; case SYS_SEEK: seek(f->R.rdi, f->R.rsi); case SYS_TELL: tell(f->R.rdi); case SYS_CLOSE: close(f->R.rdi); } thread_exit(); }
함수들의 return 값을 저장해주는 레지스터는 무엇이었는가? 바로 rax 레지스터였다.
따라서 이 시스템콜 핸들러에서도 rax에 리턴 값들을 저장해야 한다.
리턴 값들이 있는 시스템콜들은 모두 rax에 저장하자.
그런데 halt는 되지만 exit이 안된다. 왜그럴까?...
다른 블로그들을 보니까 스레드의 상태를 exit으로 설정해야 한다는데 이 상태 설정이라는 것은 그냥 플래그 같은 것아니였나..?
스레드 구조체(thread.h)에 새로운 exit_status라는 상태를 설정하고 init_thread에서 이를 0으로 초기화한다.
이 작업을 하는 이유는 정확하게는 모르지만, process_exit에서 thread_current()를 사용하는데 여기서 exit에 대한 출력을 하기 위해 스레드 구조체에 새로운 멤버를 만들어서 끝난 상태를 저장하는 것 같다.
일단 다음으로 넘어가야겠다. 매 번 wait의 루프를 수정해서 테스트 케이스를 넘길 수 없다.
open()
open을 구현하기 위해서 다시금 파일과 파일 디스크립터에 대해서 알아봐야 한다.
파일은 읽거나 쓸 수 있는 순차적 바이트의 배열이다. 숫자로 표현되는 저수준의 이름을 갖는데, 이를 inode number라고 부른다.
각 파일은 inode 번호와 연관되어 있다.
open()은 응용 프로그램이 운영체제에게 파일을 열 수 있는 권한을 요청하는 것이다.
open()은 파일 디스크립터 번호를 리턴한다. 파일 디스크립터는 프로세스마다 존재하는 정수값으로, 열린 파일을 읽고 쓰는데 사용한다.
파일 디스크립터는 다음과 같은 역할을 한다.
- 파일 디스크립터는 동작(ex 파일 열기)에 대한 수행 자격을 부여하는 역할을 한다. (그래서 write의 경우에도 fd값이 1인지 체크했었다)
즉 운영체제가 사용자 프로세스에게 권한을 주기 위해 사용되는 것으로 권한이 허용하는 읽기 쓰기 등을 처리할 수 있다. - 파일 객체 자체를 가리키는 포인터로도 역할한다.
운영체제는 프로세스마다 파일 디스크립터를 따로 관리한다.
즉 스레드(프로세스)가 파일 디스크립터를 관리하고, 파일 디스크립터 자체 값을 갖고 있는 것은 아니고 파일 디스크립터 테이블을 통해 해당 프로세스의 파일 디스크립터를 관리한다고 생각하면 될 것 같다.
파일 디스크립터 테이블도 파일이고, 배열의 형태로 나타낼 수 있다. 스레드마다 갖고 있는 이 배열이 어떤 파일이 '열려 있는지'를 관리한다.
이 때 각 배열 내의 요소는 struct file을 가리키는 포인터에 해당하고, 이 스레드 구조체가 읽고 쓰는 실제 파일의 정보를 추적하는데 쓰인다.
여기까지는 스레드 구조체에 파일 디스크립터 테이블을 넣는 것이 이해가 되었다.
그러나 구조체 내에 파일에 대한 인덱스 값을 넣기 위한 정수 변수 하나를 더 선언해주는데... 이것은 왜 필요한 것일까? 이해가 잘 안되었다.
패배의 기운이 엄습했으나.. 동료의 추천으로 다른 블로그의 수도 코드를 보면서 구현하는 것을 해보기로 하였다.
그리고 동료의 덕분에 exit과 halt 모두 성공하였다.
문제는 syscall_handler에 있었다. 디버깅을 위해서 핸들러에서 break문을 다 빼주었는데, 이것이 문제였다.
그리고 마지막에 thread_exit()을 통해 스레드를 빠져나와야 된다고 생각했는데, 아닌 것 같다. 이 역시 없애니 정상적으로 처리되었다.
void syscall_handler(struct intr_frame *f UNUSED) { // TODO: Your implementation goes here. // printf("system call!\n"); // printf("system call number: %d \n", f->R.rax); check_address(f); // 인자 들어오는 순서 %rdi, %rsi, %rdx, %r10, %r8, %r9 switch (f->R.rax) { case SYS_HALT: halt(); break; case SYS_EXIT: exit(f->R.rdi); break; case SYS_FORK: break; case SYS_EXEC: f->R.rax = exec(f->R.rdi); break; case SYS_WAIT: break; case SYS_CREATE: f->R.rax = create(f->R.rdi, f->R.rsi); break; case SYS_REMOVE: remove(f->R.rdi); break; case SYS_OPEN: open(f->R.rdi); break; case SYS_FILESIZE: filesize(f->R.rdi); break; case SYS_READ: read(f->R.rdi, f->R.rsi, f->R.rdx); break; case SYS_WRITE: f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx); break; case SYS_SEEK: seek(f->R.rdi, f->R.rsi); break; case SYS_TELL: tell(f->R.rdi); break; case SYS_CLOSE: close(f->R.rdi); break; } // thread_exit(); }
이제 다시 블로그를 보면서 천천히 해보자.
기존의 fdt를 포인터 배열로 128크기로 할당했었다. 이를 최대한 활용해보고자 한다.
다시 한 번 open()...
블로그를 보면서 open을 구현하는 와중에 open() 시스템콜 자체로 들어가지 않는 것 같아 해결중이다.
어찌 저찌 디버깅해보니 open()에서 filesys_open(file)부분이 안되는 것 같다.
또 타고 들어가니 inode 부분이 문제가 있는 것 같다. 아마도 file이나 무언가를 잘못 넘겨주었기 때문에 그럴 것이다.
일단 check_address()를 수정했다. 이전에는 interrupt frame 구조체를 직접 받았는데, 그냥 호출자가 해당하는 주소를 넣을 수 있도록
void형으로 바꾸었다.
뭐 어찌 저찌 구현 했다 ^^
make tests/userprog/open-normal.result 로 하면 잘 나온다.int open(const char *file) { check_address(file); // printf("open file: %s\n", file); struct file *f = filesys_open(file); if (f == NULL) { // printf("file open failed\n"); return -1; } int fdnum = process_add_file(file); if (fdnum == -1) { // printf("add file failed\n"); file_close(f); return -1; } return fdnum; }
make check를 돌려 보았는데 여러 가지 되는 것들도 있고 안되는 것들도 있고 하다.
근데 이제 갑자기 argument passing이 다 안된다.
워드 단위로 정렬이 안되었다는 것인데.. 해결해봐야 할 것 같다.
void push_stack(struct intr_frame *intr_f, char **argv, int argc) { char *addrv[128]; for (int i = argc - 1; i >= 0; i--) // 명령어 인자 데이터 넣기 { size_t len = strlen(argv[i]) + 1; intr_f->rsp -= len; memcpy((void *)intr_f->rsp, argv[i], len); addrv[i] = intr_f->rsp; } // 스택 포인터를 8의 배수로 반올림 if (intr_f->rsp % 8 != 0) { intr_f->rsp -= intr_f->rsp % 8; } memset((void *)intr_f->rsp, 0, intr_f->rsp - (uint64_t)intr_f->rsp); intr_f->rsp = intr_f->rsp - sizeof(char *); memset((void *)intr_f->rsp, 0, sizeof(char *)); for (int j = argc - 1; j >= 0; j--) { intr_f->rsp -= sizeof(char *); memcpy((void *)intr_f->rsp, &addrv[j], sizeof(char *)); } intr_f->rsp = intr_f->rsp - sizeof(void (*)()); // return할 함수가 있을 시 return address 설정 memset((void *)intr_f->rsp, 0, sizeof(void (*)())); intr_f->R.rdi = argc; intr_f->R.rsi = intr_f->rsp + 8; }
정렬 성공! 근데 또 다른 문제가 발생한다 ㅋㅋㅋ
thread_current()->name이 argc-dbl-spacet으로 출력된다 ㅋㅋㅋ...아니 뒤에 two까지 name에 들어가는 것 같은데 해결해보자 또..
name에 어떻게 파싱한 값을 넣어주는지 봐야겠다 ...
문제는 process_create_initd에 있었다. 부모 프로세스의 파일 이름을 최대 14까지로 제한해두었던게 문제였다. 15까지로 늘렸더니 통과되었다.
정적 배열으로 파일 이름을 넣어주는 것이 맞나 싶다 너무 포인터에 절여져 있는 것인가? ㅋㅋ
filesys()
fd로 열려있는 파일의 사이즈를 byte 단위로 리턴한다. 일단 슬쩍 봤을 때는 관련한 테스트케이스가 없는 것 같아 확인은 어려울 것 같으나 일단 구현은 해 보았다.
블로그에서는 process_get_file이라는 함수를 새로 선언하여 파일을 가져오는 작업을 하는데 굳이 있어야 하나 싶긴 하다.
int process_get_file(int fd) { struct thread *t = thread_current(); if (t->fdt[fd] == NULL) { return -1; } return fd; }
그리고 요녀석을
int filesize(int fd) { return file_length(process_get_file(fd)); }
요렇게 한번 호출해보았다. 넘어가보자!
seek()
seek는 파일 내에서 다음에 읽거나 쓸 바이트의 위치를 변경한다.
이 위치는 파일의 시작으로부터 바이트 단위로 표현된다.
파일의 현재 끝을 넘어서서 seek를 수행하는 것은 오류를 발생시키지 않는다.
즉 파일 끝을 넘어서 seek를 수행해도 문제가 되지 않는다.
이후에 수행되는 읽기 작업에서 0바이트를 얻는 것은 파일의 끝을 의미한다.
즉 파일의 끝에 도달했을 때 읽기 작업을 수행하면 더 이상 읽을 내용이 없다는 것을 의미한다.
이후에 수행되는 쓰기 작업은 파일을 확장하며, 쓰기가 안 된 공간은 0으로 채워진다.
따라서 파일의 크기가 쓰기 작업에 의해 자동으로 늘어나고, 쓰기되지 않은 공간은 0으로 채워진다.
파일의 크기가 고정되어 있으므로 파일 끝을 넘어서 쓰기 작업을 수행하면 오류가 발생한다. (프로젝트 4에서 수정된다)
딱히 구현 할게 없어 보인다.. 기존에 만들어 두었던 process_get_file 함수를 사용하여 filesys의 file_seek 함수의 인자로 호출하면 된다. void이기 때문에 리턴도 딱히 해주지 않아도 된다.
void seek(int fd, unsigned position) { file_seek(process_get_file(fd), position); }
tell
파일 내에서 다음에 읽거나 쓸 위치를 반환한다.
이 위치는 시작으로부터 바이트 단위로 표현된다.
이 역시 딱히 뭔가 해줄 게 없다. file_tell 함수를 사용하면 되고, file_tell은 파일의 위치(offset)을 반환하므로 return 해주어야 한다.
unsigned tell(int fd) { return file_tell(process_get_file(fd)); }
'Projects > Krafton_Jungle_4' 카테고리의 다른 글
[PintOS] Project 3 - Virtual Memory git book, Introduction (2) 2024.03.23 [PintOS] Project 2 - System Call (3) (0) 2024.03.20 [PintOS] Project 2 - User Memory Access (1) (0) 2024.03.18 [PintOS] Project 2 - Argument Passing (2) (4) 2024.03.16 [PintOS] Project 2 - Argument Passing (1) (0) 2024.03.15