참고로 일부분의 함수들은 성공적으로 작성하였고, 테스트 케이스도 통과를 하였지만 모든 시스템콜을 구현하진 못했습니다. 참고하여 읽어주시면 감사드리겠습니다.
설명하기에 앞서, 통과된 테스트 케이스는 다음과 같습니다. (fork 이전까지 모두 통과)


95개의 테스트 중에 38개의 테스트 케이스가 Fail 됩니다.
보통의 System Calls 관련 파일은 userprog 폴더에 있습니다. 그러나 다른 폴더에 있는 파일들도 사용합니다.
헤더 파일의 경우는 include 폴더에 모두 위치해 있습니다.
Pintos 강의에 따르면 syscall.c에서 syscall_handler 에 number 따라 들어온 시스템 콜을 처리해주면 됩니다. 본격적인 처리함수는 밑에다가 작성하면됩니다.
include → lib → syscall-nr.h 를 통해서 구현해야될 시스템 콜 리스트를 알 수 있습니다. 물론 Git book을 확인하면 어떤식으로 구현해야되는 지도 알수 있습니다.
그러면 각 시스템콜 별 선언과 switch / case 문이 필요합니다. 저는 아래와 같이 작성하였고, 필요에 따라 주석을 제거하여 사용했습니다. 만약에, 구현이 되지 않았는데 주석이 풀려있으면 정의 되어 있지 않아 오류가 생깁니다.
또한 작업전 filesys폴더에 있는 filesys.h와 file.h를 read, write 함수를 구현하는데 사용하므로 미리 include 하는 것을 추천합니다.
해당 파일 초판에 파일 시스템 관련 파일 미리 include
#include "filesys/filesys.h"
#include "filesys/file.h"
시스템콜 별 프로토타입 선언
void syscall_entry (void);
void syscall_handler (struct intr_frame *);
/////////////////////////////////
// 구현할 시스템 호출 함수 프로토타입 추가
void halt (void);
void exit (int status);
// pid_t fork (const char *thread_name);
// int exec (const char *cmd_line);
// int wait (pid_t pid);
bool create (const char *file, unsigned initial_size);
// bool remove (const char *file);
int open (const char *file);
// int filesize (int fd);
int read (int fd, void *buffer, unsigned size);
int write (int fd, const void *buffer, unsigned size);
// void seek (int fd, unsigned position);
// unsigned tell (int fd);
// void close (int fd);
/////////////////////////////////
syscall_handler에 알맞은 case 리스트 작성
/* 주요 시스템 호출 인터페이스 */
void
syscall_handler (struct intr_frame *f UNUSED) {
int number = f->R.rax;
// printf("시스템콜 넘버: %d \n",number); // 시스템콜 디버깅
// TODO: 여기에 구현하면 됩니다.
switch (number)
{
case SYS_HALT:
halt();
break;
case SYS_EXIT:
exit(f->R.rdi); // 첫번째인자부터 exit
break;
// case SYS_FORK:
// f->R.rax = fork((int)f->R.rdi);
// break;
// case SYS_EXEC:
// f->R.rax = exec((int)f->R.rdi);
// break;
// case SYS_WAIT:
// f->R.rax = wait((int)f->R.rdi);
// break;
case SYS_CREATE:
f->R.rax = create((int)f->R.rdi, (void *)f->R.rsi);
break;
// case SYS_REMOVE:
// f->R.rax = remove((int)f->R.rdi);
// break;
case SYS_OPEN:
f->R.rax = open((int)f->R.rdi);
break;
case SYS_FILESIZE:
f->R.rax = filesize((int)f->R.rdi);
break;
case SYS_READ:
f->R.rax = read((int)f->R.rdi, (void *)f->R.rsi, (unsigned)f->R.rdx);
break;
case SYS_WRITE:
f->R.rax = write((int)f->R.rdi, (void *)f->R.rsi, (unsigned)f->R.rdx);
break;
// case SYS_SEEK:
// f->R.rax = seek((int)f->R.rdi, (void *)f->R.rsi);
// break;
// case SYS_TELL:
// f->R.rax = tell((int)f->R.rdi);
// break;
case SYS_CLOSE:
// close((int)f->R.rdi);
break;
default:
thread_exit ();
break;
}
// printf ("system call!\n"); // [디버깅] 시스템콜 확인
// thread_exit ();
}
→ 저같은 경우 fork 이전 write까지 구현완료하여 해당 case들만 열어두고 나머지는 주석처리했습니다.
목표: power_off()을 호출하여 Pintos를 종료하면 됩니다.
halt를 구현했는데 정상적으로 작동이 되지 않아서 디버깅을 권호형과 함께 하였습니다. 결국 process.c의 스택 쌓는 일부분(argc, argv 포인터 설정)과 write 함수가 구현이 되지 않아서 출력되지 않음을 알았습니다.
코드 자체는 git book에 나와있는대로만 작성하면 되서 쉽습니다.
void
halt (void) {
power_off();
}
→ 해당과정에서 user 부분과 커널 부분의 디버깅 프린트를 뽑는 방식이 다름을 알았습니다. printf와 msg 방식 두가지가 있습니다.
현재 사용자 프로그램을 종료하고 status커널로 돌아갑니다. 프로세스의 부모 wait프로세스가 해당 프로세스에 대한 상태(아래 참조)를 가지고 있는 경우, 이 상태가 반환됩니다. 지금은 exit 상태에 대해 출력하도록 구성했습니다.
args 테스트 케이스를 통과하고 싶어서 권호형한테 물어봤더니, exit를 구현해야한다고 합니다. 이과정에서 권호형이 exit 구현 방법을 알려주었습니다.
void
exit (int status) {
struct thread* curr = thread_current();
printf("%s: exit(%d)\n", curr->name, status);
thread_exit();
}
초기 크기가 initial_size바이트 인 새 파일file을 생성합니다. 성공하면 true를 반환하고, 그렇지 않으면 false를 반환합니다.
→ 새 파일을 생성한다고 해서 파일이 열리는 것은 아닙니다.
create 문은 자력으로 풀어보았습니다. 이제부터는 파일 시스템 부분이 들어가므로 filsys 폴더의 filesys / filesys.c 파일을 참고하여 구현해야합니다. GPT는 인터럽트 락을 걸라고 하는데 해당 과정은 프로젝트4에서 하는 일이며 완전한 파일 디스크립터를 구현하는 것이 아니므로 추가하지 않았습니다.
구현과정에서 페이지 폴트가 나와 확인을 해봤더니 예외처리가 문제가 있어, 다음과 같이 수정했습니다.
→ 파일이 없거나, 유효한 주소가 아니거나, 올바른 페이지 접근이 나일때 -1을 반환하며 탈출 하는데 저는 파일이 없을 때만 예외처리해놔서 문제가 생긴것이었습니다. 페이지의 유효성을 확인하는 함수가 pml4_get_page 인데 권호형이 알려주서 겨우 알았습니다. 알맞은 함수를 찾는게 너무 어려운 것 같습니다.
그렇게 유효성 검사를 하고 filesys_create 라는 함수를 통해 바이트에 해당하는 파일을 생성하는 코드를 작성했습니다.
bool create (const char *file, unsigned initial_size) {
if (file == NULL || !is_user_vaddr(file) || pml4_get_page(thread_current()->pml4, file) == NULL)
exit(-1);
bool success = filesys_create(file, initial_size); // filesys/filesys.c 위치
return(success);
// create-bad-ptr만 커널 패닉와서 디버깅중 -> 유저영역에 유효한 주소가 있는지 확인하는 !is_user_vaddr(file) 사용
// 페이지 할당이 잘못되었을때 확인해서 -1 내보내는 pml4_get_page(thread_current()->pml4, file) == NULL 추가
}
create에서 만든 file을 열기만 하면됩니다. 디테일한 내용은 Git book을 참고하시면 좋겠습니다. 생각보다 어려운것은 FD라는 파일디스크립터를 구현하는 것입니다. FD에 따라 파일의 바이트수와 원하는 파일을 가져와 열어볼 수 있습니다.
create와 마찬가지로 filesys/filesys.c 파일을 참고하여 작성하면 됩니다. 파일 유효성 겅사는 creat와 동일하고, 실행은 filesys.c의 filesys_open 코드를 사용하면됩니다.
처음에 구현했을때는 두가지 테스트케이스를 제외하고 pass가 됐습니다. 모두 pass가되기 위해서는 파일 디스크립터를 구현해야합니다. 그래서 추가적으로 syscall.c, thread.c, thread.h 파일들을 수정했습니다.
thread.h
스레드 헤더파일에 스레드 구조체에 fd를 관리할 테이블을 구조체로 선언합니다. struct file **fd_table;
struct thread {
/* Owned by thread.c. */
tid_t tid; /* Thread identifier. */
.
.
.
/** project1-Advanced Scheduler */
int niceness;
int recent_cpu;
struct list_elem all_elem;
//////////////////
struct file **fd_table; // 프로젝트2 fd에서 사용
//////////////////
// Project 1 테스트 케이스를 실행하려면, ifdef로 바꿔야함
#ifdef USERPROG
/* Owned by userprog/process.c. */
uint64_t *pml4; /* Page map level 4 */
#endif
.
.
.
thread.c
스레드 파일에 fd 테이블 구조체 크기만큼 calloc으로 배정합니다.
t->fd_table = calloc(64, sizeof(struct file*));
tid_t
thread_create (const char *name, int priority,
thread_func *function, void *aux) {
.
.
.
/* Initialize thread. */
init_thread (t, name, priority);
tid = t->tid = allocate_tid ();
//////////////////
t->fd_table = calloc(64, sizeof(struct file*)); // 구조체 크기만큼만 배정
//////////////////
/* Call the kernel_thread if it scheduled.
* Note) rdi is 1st argument, and rsi is 2nd argument. */
t->tf.rip = (uintptr_t) kernel_thread;
t->tf.R.rdi = (uint64_t) function;
.
.
.
syscall.c
여기서 파일 디스크립터를 구조체를 통해 구현해서 파일별로 인덱싱(3이상)을 해줘야한다. 왜냐하면, 파일디스크립터는 0은 표준입력, 1은 표준 출력, 2는 표준 에러를 의미하기 때문입니다. 그러므로 3번부터 파일별로 인덱싱하면 됩니다.
[참고] Pintos는 사실 표준에러인 2가 없어서 2부터 배정해도되지만, 실제 OS에 가깝게 구현하기 위해 3번부터 인덱싱하는 것으로 구현했습니다.
이렇게 구현하는 이유는 이미 열려진 파일별로 인덱싱을 하여 관리하기 위해서입니다. 열려진 파일중에 현재 쓰레드에 맞는 파일을 인덱싱을 매칭하여 가져옵니다.
Pintos파일 시스템에서 filesys.c 가 low 레벨의 함수가 있는 파일이고, file.c 가 high 레벨의 함수가 있는 파일입니다. 그래서 filesys.c는 file.c의 내용을 가져다씁니다.
pintos 파일의 흐름
파일의 대강의 흐름도는 syscall → filesys → file 입니다.
트러블 슈팅
전체적인 구현이후, 다른 건 다 되는데 syscall.c 의 open 함수 해당 코드에서 두건의 트러블 슈팅이 있었습니다.
for (int fd = 3; fd < 128; fd++) {
if (curr -> fd_table[fd] == NULL) {
curr -> fd_table[fd] = file;
return fd;
}
}
// file_close(f);
return -1;
fd_table[fd] 를 그냥 fd_table 이라고 써서 올바른 fd번호가 부여되지 않았습니다. 이게 [fd] 가 없어도 코드상 따로 오류 코드가 뜨지 않아서 이유를 못찾았었습니다. 명석이 형이 알려줘서 해결하게됐다. 역시 디버깅은 남이 봐야 잘 보이는 것 같습니다.추가로 파일관련된 부분에서 유효성 검사(NULL인지 확인, 유효한 주소인지, 올바른 페이지인지 확인)부분을 따로 뺄려 했으나 해당 과정에서 문제가 생겨서 그냥 기존 코드대로 유지했습니다.
syscall.c의 open 함수 전체코드
fd값 불러오는 코드도 포함되어 있습니다.
int open (const char *file) {
// file 제목을 받아서 file과 비교하여 확인
// 파일을 여는것은 FD 3이상부터 인덱싱
// 포인터 파일 유효성 검사
if (file == NULL || !is_user_vaddr(file) || pml4_get_page(thread_current()->pml4, file) == NULL)
exit(-1);
struct file *f = filesys_open(file); // f 가 NULL일때 -1 표출
// printf("file adrs: %p\n", f); // 주소 디버깅용
if (f == NULL)
return -1;
// int sucess = file_open(file); // 해당 코드는 file.c를 바로 가서 안된다 filesys 가공후 file로 넘어가야한다.
// if(!(sucess == NULL))
// return sucess;
// else
// return -1;
struct thread* curr = thread_current();
for (int fd = 3; fd < 128; fd++) {
if (curr -> fd_table[fd] == NULL) {
curr -> fd_table[fd] = f;
return fd;
}
}
// file_close(f); // close 구현 후 추가 예정
return -1;
}
사실 read와 write를 구현도중에 필요가 있어서 작성했습니다.
read와 write 시 원하는 파일의 크기를 불러올때 사용하기 위한 함수 입니다. 현재 스레드의 FD 테이블에서 불러옵니다. file_length 라는 함수를 사용하여 구현합니다.
// read와 write를 하다가 filesize 함수가 필요하다고하여 추가로 구현했습니다.
int filesize (int fd) {
if(fd < 0 || fd > 128)
return -1;
struct file *f = thread_current() -> fd_table[fd];
if (f == NULL)
return -1;
// printf("file adrs: %p\n", f); // 파일주소 디버깅용
return file_length(f);
}
말그대로 사용자가 원하는 파일의 내용을 불러옵니다.
권호형이 제공해준 write 코드를 참고하여 read 코드를 작성하였습니다. 파일 디스크립터 0,1,2 내용이 중요했습니다. 그리고 git book 내용을 참고하지 않으면 어떤 함수를 이용하여 구현할 지 몰라서 꼭 참고해야합니다.
fd가 0이면 표준 입력으로 fd == 0이면 파일 사이즈를 불러와야합니다. fd가 3이상, 128 이하인 구간의 경우 현재 스레드의 파일을 읽어옵니다. 이후에 파일, 버퍼, 사이즈에 따른 바이트 수를 리턴합니다.
당연히 유효성 검사도 포함합니다.
int read (int fd, void *buffer, unsigned size) {
if (buffer == NULL || !is_user_vaddr(buffer) || pml4_get_page(thread_current() -> pml4, buffer) == NULL)
exit(-1);
if(fd == 0) { // 파일 디크립터 0, 1, 2에 따라서 다름
for(unsigned i = 0; i < size; i++)
((char *) buffer)[i] = input_getc();
// printf("size: %d\n", size); // 사이즈 디버깅용
return size; // 파일에 사이즈만큼 입력을 함
}
if(fd == 1)
return -1;
if(fd >= 3 && fd < 128) {
struct file *f = thread_current() -> fd_table[fd];
if(f == NULL)
return -1;
int bytes = file_read(f, buffer, size);
return bytes;
}
}
열린 파일에 size 만큼 쓰기를 수행하면 됩니다.
file_write 라는 함수를 통해 원하는 파일의 버퍼, 사이즈 만큼 쓰기 후 바이트 수를 리턴합니다.int write(int fd, const void *buffer, unsigned size){
if (buffer == NULL || !is_user_vaddr(buffer) || pml4_get_page(thread_current()->pml4, buffer) == NULL)
exit(-1);
if(fd == 0) {
return -1;
}
if(fd == 1 || fd == 2){
putbuf(buffer, (size_t)size);
return (int)size;
}
if(fd >= 3 && fd < 128) {
struct file *f = thread_current() -> fd_table[fd];
if(f == NULL)
return -1;
int bytes = file_write(f, buffer, size);
return bytes;
}
}
read, write 테스트 케이스를 확인해보니까 파일을 읽어오는 nomal 테스트 케이스가 pass되지 않았습니다.
예외적인 -1을 리턴하는 read-zero와 같은 케이스는 pass를 했습니다. 코드를 살펴보니 읽어오거나 쓰는 코드가 구현이 안되어 있어서 다시 구현했습니다.
확인을 해보니 filesize가 구현이 되지 않으면 파일을 불러올수 없었습니다. read, write 모두 안되는 이유가 같은 이유였습니다. 그러고 나서도 filesize 함수에서 read 함수로 인자를 넘기지 못하는 오류가 있었는데, 파일 상단에 file.c와 filesys.c include가 되지 않아서 그랬습니다.
→ 여기 문서에서는 미리 해두었고 상단의 모든 코드는 해당 문제를 해결해 완벽작동하는 코드들입니다.
for (int fd = 3; fd < 128; fd++) {
if (curr -> fd_table[fd] == NULL) {
curr -> fd_table[fd] = f;
return fd;
}
이후에 해도 안되길래 봤더니 open 함수에서 curr -> fd_table[fd] = f; 해당부분에 f로 써야한 인자를 file로 잘못써서 그런것이었습니다… 인자 실수를 해서 창피했습니다.
read와 write를 마저 모두 구현하고 테스트 케이스르 돌려보니 정상적으로 작동했습니다.
→ 결론적으로 read, write 파일 관련 테스트 케이스는 모두 통과했습니다.
파일을 닫으면 됩니다. 지금은 프로세스를 종료하면 모든 파일이 닫힙니다. 디테일한 구현은 프로젝트3에서 진행합니다.
switch문에 close관련 문만 case로 만들어줬더니, close관련 모든 테스트 케이스가 통과되었습니다.
case SYS_CLOSE:
// close((int)f->R.rdi);
break;
create, write 등의 함수에선 다음의 코드가 쓰입니다.
if (file == NULL || !is_user_vaddr(file) || pml4_get_page(thread_current()->pml4, file) == NULL)
exit(-1);
해당 코드가 System call 전의 User Memory Access에서 원하는 사용자 메모리 접근 구현입니다.
OS는 데이터 읽을 때 사용자가 잘못된 포인터, 커널 메모리에 대한 포인터, 또는 해당 영역 중 하나의 부분적으로 포함된 블록을 제공하는 경우에 사용자 프로세스를 종료하여야합니다.
그러므로 해당 대상 (파일)이 존재하는지, 유저 공간에 있는 주소인지, 현재 쓰레드의 페이지가 올바른 할당인지 확인하는 코드인 위의 코드를 각 함수에 작성합니다.
대망의 fork 입니다. 부모 프로세스의 자식을 만들때 사용합니다.
여러 시스템 콜과 세마포어 등의 기술들이 들어가서 어렵다고 알고 있습니다. 그리고 잘못하면 process_wait과 관련된 함수가 포함되어 있어 전에 있던 코드들이 작동되지 않을 수도 있습니다.
강의를 살펴보면서 생각을 정리하고 본격적인 코드 작성을 시작했습니다.
pintos에서의 fork가 다른점
새로 알게된 사실은 pintos는 1프로세스당 1쓰레드라 pid, tid를 생각할 필요없이 pid만 고려하여 자식을 fork 해주면 됩니다. 그리고 명석이형 통해 알게된건데 fork 관련된 함수가 있습니다. 그걸 조사해서 구현해봐야겠습니다.
나의 다사다난한 fork 과정들
process.c에 fork 부분을 수정하고 있습니다. todo에 따라서 구현해보겠습니다. mmu.c의 의존도가 높아져서 전체 번역을 해야겠다.
process.c에 있는 duplicate_pte 함수를 구현하고 있습니다. 구현을 완료하고, do_fork를 들어가기 전에 fork 테스트 케이스를 확인해보았습니다.
__do_fork를 구현에 부모파일을 자식 파일에 복사하는걸 다했는데, 페이지 폴트 문제가 생깁니다. 제가 봤을땐 fork의 syscall이 다 구현되지 않아서 그런것 같습니다.
→ 좀 더 확인을 해봤는데, 페이지 폴트만 나고 child status만 나오는 이유가 wait을 구현하지 않아서 그런 것이었다. 그러므로 wait을 구현해보겠다.
먼저 교수님의 말대로 thread.h에 wait 구조체를 추가했습니다.
struct wait **wait_table; // 프로젝트2 fork, wait에서 사용
wait과 포크 과정에는 세마포어가 총 세 개가 필요합니다.
근데 저는 3번 세마포어를 구현하지 않고 exit한 자식 프로세서정보는 structer에 담고 부모와 자식 스레드에 각각 저장하여 자식을 종료해도 정보를 알 수 있게끔 구현하고 싶었습니다.
→ 앞으로 위의 조건들을 고려하여 wait, exec, fork를 종합적으로 구현하겠습니다. 앞으로 시간이 얼마 남지 않아 GPT를 적극활용하여 구현할 것 같습니다.
지금까지 쓴 wait, fork 코드를 엎다.
계속된 page fault 오류로 엎고 처음부터 다시 차근차근 구현하려고 합니다.
먼저 thread.c, thread.h, process.c, syscall.c 등을 수정할 예정입니다. 다른거 다 제외하고 wait과 fork 시스템 콜만 열어두고 구현할 것 같습니다.
12 주차 발제 날까지 fork와 wait에 대해 코드를 디버깅하며 문제를 해결하려했지만, 확인결과 초장부터 잘못됐다고 판단이 되었습니다. (태생이 잘못됐어…) 그러므로 지금까지의 내용을 정리해보도록하겠다. (지금하는중)

위 코드가 무슨 오류일까요… 부모가 자식의 PID를 출력하지 못하고 읽어버립니다…
5월 28일 17:30 경 최종적으로 프로젝트 2 를 마감했습니다. 정리가 완료되는대로 프로젝트 3 돌입합니다.
userprog 폴더에서 make 명령어를 사용하여 컴파일합니다.userprog / build 폴더로 이동하여 원하는 테스트 코드를 실행합니다.make tests/userprog/fork-once.result VERBOSE=1 해당코드는 fork-once 테스트 케이스를 실행해봅니다. → 만약 다른 테스트 케이스를 원한다면 make tests/userprog/fork-once.result VERBOSE=1 에서 fork-once 부분만 원하는 테스트 케이스로 실행하면 됩니다. 그러나 multi-oom 같은 경우에는 테스트 케이스 경로도 바꿔서 써야합니다. make tests/userprog/no-vm/multi-oom.result VERBOSE=1 을 실행하면 됩니다.
PASS로 잘 실행됐다면, 다음과 같은 화면을 볼 수 있습니다. write-nomal 테스트 케이스

https://www.youtube.com/watch?v=sBFJwVeAwEk&t=110s
https://casys-kaist.github.io/pintos-kaist/project2/system_call.html