Project 2 : System call 나머지
나머지 system call들을 구현해보도록 하자!
syscall.c 헤더파일 추가 및 함수의 원형 선언
// userprog/syscall.c
#include "threads/init.h"
#include "threads/synch.h"
#include "threads/palloc.h"
#include "filesys/filesys.h"
#include "filesys/file.h"
#include "userprog/process.h"
static struct file *find_file_by_fd(int fd);
void check_address(uaddr);
void halt(void);
void exit(int status);
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);
system call handler
// userprog/syscall.c
void syscall_handler(struct intr_frame *f UNUSED)
{
switch (f->R.rax) // rax는 system call number이다.
{
case SYS_HALT:
halt();
break;
case SYS_EXIT:
exit(f->R.rdi); //실행할 때 첫번째 인자가 R.rdi에 저장됨
break;
case SYS_FORK:
f->R.rax = fork(f->R.rdi, f);
break;
case SYS_EXEC:
if (exec(f->R.rdi) == -1)
{
exit(-1);
}
break;
case SYS_WAIT:
f->R.rax = process_wait(f->R.rdi);
break;
case SYS_CREATE:
f->R.rax = create(f->R.rdi, f->R.rsi);
break;
case SYS_REMOVE:
f->R.rax = remove(f->R.rdi);
break;
case SYS_OPEN:
f->R.rax = open(f->R.rdi);
break;
case SYS_FILESIZE:
f->R.rax = filesize(f->R.rdi);
break;
case SYS_READ:
f->R.rax = 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:
f->R.rax = tell(f->R.rdi);
break;
case SYS_CLOSE:
close(f->R.rdi);
break;
default:
exit(-1);
break;
}
}
halt()
// userprog/syscall.c
void halt(void)
{
power_off();
}
PintOS 종료
exit()
// userprog/syscall.c
void exit(int status)
{
struct thread *cur = thread_current();
cur->exit_status = status; // 종료시 상태를 확인, 정상종료면 state = 0
printf("%s: exit(%d)\n", thread_name(), status); // 종료 메시지 출력
thread_exit(); // thread 종료
}
프로세스 종료
thread 구조체를 많이 바꿔야 하는데 지금 한번에 바꿔버리자.
// include/threads/thread.h
struct thread
{
/* Owned by thread.c. */
tid_t tid; /* Thread identifier. 정수형, 프로세스에 1부터 부여 */
enum thread_status status; /* Thread state. */
char name[16]; /* Name (for debugging purposes). */
int priority; /* Priority. 숫자 클수록 우선순위 높음*/
/* Shared between thread.c and synch.c. */
struct list_elem elem; /* List element. */
// 알람시계 일어날 시간.
int64_t wakeup;
// priority donation
int init_priority; // 최초 스레드 우선순위 저장.
struct lock *wait_on_lock; // 현재 thread가 요청했는데 받지못한 lock. 기다리는중
struct list donations; // 자신에게 priority를 나누어준 'thread'의 리스트
struct list_elem donation_elem; // 위의 thread 리스트를 관리하기위한 element. thread 구조체의 elem과 구분.
// project 2 : system call
int exit_status; // 자식 프로세스의 exit 상태를 부모에게 전달
struct file **fd_table; // file descriptor table의 시작주소 가리키게 초기화
int fd_idx; // fd table에 open spot의 index
struct intr_frame parent_if;
struct semaphore fork_sema; // fork한 child의 load를 기다리는 용도
struct list child_list; // parent가 가진 child_list
struct list_elem child_elem;
struct semaphore wait_sema;
struct semaphore free_sema;
struct file *running;
int stdin_count;
int stdout_count;
#ifdef USERPROG
/* Owned by userprog/process.c. */
uint64_t *pml4; /* Page map level 4 */
#endif
#ifdef VM
/* Table for whole virtual memory owned by thread. */
struct supplemental_page_table spt;
#endif
/* Owned by thread.c. */
struct intr_frame tf; /* Information for switching */
unsigned magic; /* Detects stack overflow. */
};
create()
// userprog/syscall.c
bool create(const char *file, unsigned initial_size)
{
check_address(file);
return filesys_create(file, initial_size);
}
파일을 생성한다. 파일 생성에 성공하면 true, 실패하면 false를 반환한다.
remove()
// userprog/syscall.c
bool remove(const char *file)
{
check_address(file);
return filesys_remove(file);
}
파일을 삭제한다. 파일 제거에 성공하면 true, 실패하면 false를 반환한다.
exec()
잠깐!!!
exec()
함수를 구현하기 전에 process_create_initd()
함수를 조금 수정해줘야 한다. 지금 현재는 전체를 넣어주는데, file_name을 분리해서 넣어줘야 한다.
// userprog/process.c
tid_t process_create_initd(const char *file_name)
{
char *fn_copy;
tid_t tid;
/* Make a copy of FILE_NAME.
* Otherwise there's a race between the caller and load(). */
fn_copy = palloc_get_page(0); // page를 할당받고 해당 page에 file_name을 저장해줌
if (fn_copy == NULL)
return TID_ERROR;
strlcpy(fn_copy, file_name, PGSIZE); // page의 크기 즉, PGSIZE는 2^12 (4KB)
// project 2 : system call
// file_name을 분리해서 넣어줘야함
char *save_ptr;
strtok_r(file_name, " ", &save_ptr);
/* Create a new thread to execute FILE_NAME. */
// PRI_DEFAULT : 기본 우선순위 31
// file_name을 이름으로 하고 PRI_DEFAULT를 우선순위로 갖는 새로운 thread 생성, tid에 저장
// thread는 fn_copy를 인자로 받는 initd라는 함수를 실행시킴
tid = thread_create(file_name, PRI_DEFAULT, initd, fn_copy);
if (tid == TID_ERROR)
palloc_free_page(fn_copy);
return tid;
}
이제 exec()
을 구현해보자.
// userprog/syscall.c
int exec(char *file_name)
{
check_address(file_name);
int file_size = strlen(file_name) + 1;
char *fn_copy = palloc_get_page(PAL_ZERO);
if (fn_copy == NULL)
{
exit(-1);
}
strlcpy(fn_copy, file_name, file_size); // file 이름만 복사
if (process_exec(fn_copy) == -1)
{
return -1;
}
NOT_REACHED();
return 0;
}
현재 프로세스를 cmd_line에 이름이 지정된 실행 파일로 변경하고 argument를 전달한다.
이전 포스팅에서 exec()
을 예시로 system call을 어떻게 불러오는지 설명했었다. 기억이 안나면
여기로 https://d-cron.tistory.com/61
다시 review해보자면 큰 그림은 다음과 같다.
여기까지 완료하면 syscall handler가 전달받은 함수와 argument를 가지고 exec()
를 실행시킨다.
그리고 이 때는 kernel thread의 stack은 다 pop되어서 비워져 있다.
아까와 동일한 kernel thread에서 exec()
가 실행되고, 그 안에있는 process_exec()
가 실행되고, 그 안에있는 load()
가 실행되면서 메모리에 실행할 file의 argv,argc를 넣을 user stack을 할당하고 쌓고 , load()
가 끝나면 do_iret()
을 실행시켜서 새로운 user_process를 실행한다!
(test 통과를 위해서는 process_exec()
안에 있는 hex_dump()는 주석처리를 해주던지 지워야 한다)
fork()
시스템 콜을 호출한 부모 프로세스로부터 자식 프로세스를 생성하는 함수이다.
// userprog/syscall.c
tid_t fork(const char *thread_name, struct intr_frame *f)
{
return process_fork(thread_name, f);
}
process_fork()
함수로 가보자.
// userprog/process.c
tid_t process_fork(const char *name, struct intr_frame *if_ UNUSED)
{
// project 2 : system call
/* Clone current thread to new thread.*/
struct thread *cur = thread_current();
// 현재 thread의 parent_if에 if_를 저장
memcpy(&cur->parent_if, if_, sizeof(struct intr_frame));
tid_t tid = thread_create(name, PRI_DEFAULT, __do_fork, cur);
if (tid == TID_ERROR)
{
return TID_ERROR;
}
struct thread *child = get_child_with_pid(tid); // child_list안에서 만들어진 child thread를 찾음
sema_down(&child->fork_sema); // 자식이 메모리에 load 될때까지 기다림(blocked)
if (child->exit_status == -1)
{
return TID_ERROR;
}
return tid;
}
여기 __do_fork()
함수를 이용해서 자식 thread를 생성한다. __do_fork()
함수를 들어가보자.
// userprog/process.c
static void
__do_fork(void *aux) // load로 볼 수도 있다(부모의 것들을 자식에게 다 복사해서 메모리에 올리는 과정)
{
struct intr_frame if_;
struct thread *parent = (struct thread *)aux;
struct thread *current = thread_current();
/* TODO: somehow pass the parent_if. (i.e. process_fork()'s if_) */
struct intr_frame *parent_if;
bool succ = true;
// project 2 : system call
parent_if = &parent->parent_if;
#ifdef DEBUG
printf("[Fork] Forking from %s to %s\n", parent->name, current->name);
#endif
/* 1. Read the cpu context to local stack. */
memcpy(&if_, parent_if, sizeof(struct intr_frame));
// project 2 : system call
if_.R.rax = 0;
/* 2. Duplicate PT */
current->pml4 = pml4_create();
if (current->pml4 == NULL)
goto error;
process_activate(current); //tss를 업데이트 해준다.
#ifdef VM
supplemental_page_table_init(¤t->spt);
if (!supplemental_page_table_copy(¤t->spt, &parent->spt))
goto error;
#else
if (!pml4_for_each(parent->pml4, duplicate_pte, parent))
goto error;
#endif
/* TODO: Your code goes here.
* TODO: Hint) To duplicate the file object, use `file_duplicate`
* TODO: in include/filesys/file.h. Note that parent should not return
* TODO: from the fork() until this function successfully duplicates
* TODO: the resources of parent.*/
// process_init();
// project 2 : system call
if (parent->fd_idx == FDCOUNT_LIMIT)
goto error;
for (int i = 0; i < FDCOUNT_LIMIT; i++)
{
struct file *file = parent->fd_table[i];
if (file == NULL)
continue;
// if 'file' is already duplicated in child don't duplicate again but share it
bool found = false;
if (!found)
{
struct file *new_file;
if (file > 2)
new_file = file_duplicate(file);
else
new_file = file;
current->fd_table[i] = new_file;
}
}
current->fd_idx = parent->fd_idx;
#ifdef DEBUG
printf("[do_fork] %s Ready to switch!\n", current->name);
#endif
sema_up(¤t->fork_sema);
/* Finally, switch to the newly created process. */
if (succ)
do_iret(&if_);
error:
// thread_exit();
// project 2 : system call
current->exit_status = TID_ERROR;
sema_up(¤t->fork_sema);
exit(TID_ERROR);
}
이 함수는 부모의 context를 복사하는 함수다.
여기서 parent_if를 if_에 memcpy하기 때문에(context가 다 저장되기 때문에) 자식프로세스는 parent가 실행되는 그 시점부터 실행된다.
복사가 다 끝나면 sema_up(¤t->fork_sema)
를 통해 sema를 up 시켜준다(기다리고 있는 부모를 위해)
중간에 duplicate_pte
함수가 있는데 이 함수도 채워줘야 한다. 부모의 page table을 복사해서 자신의 pml4를 만드는 함수다
// userprog/process.c
static bool
duplicate_pte(uint64_t *pte, void *va, void *aux)
{
struct thread *current = thread_current();
struct thread *parent = (struct thread *)aux;
void *parent_page;
void *newpage;
bool writable;
/* 1. TODO: If the parent_page is kernel page, then return immediately. */
if (is_kernel_vaddr(va))
{
// return false ends pml4_for_each. which is undesirable - just return true to pass this kernel va
return true;
}
/* 2. Resolve VA from the parent's page map level 4. */
parent_page = pml4_get_page(parent->pml4, va);
if (parent_page == NULL)
{
printf("[fork-duplicate] failed to fetch page for user vaddr 'va'\n");
return false;
}
#ifdef DEBUG
// pte: address pointing to one page table entry
// *pte: page table entry = address of the physical frame
void *test = ptov(PTE_ADDR(*pte)) + pg_ofs(va); // should be same as parent_page -> Yes!
uint64_t va_offset = pg_ofs(va); // should be 0; va comes from PTE, so there must be no 12bit physical offset
#endif
/* 3. TODO: Allocate new PAL_USER page for the child and set result to
* TODO: NEWPAGE. */
newpage = palloc_get_page(PAL_USER);
if (newpage == NULL)
{
printf("[fork-duplicate] failed to palloc new page\n");
return false;
}
/* 4. TODO: Duplicate parent's page to the new page and
* TODO: check whether parent's page is writable or not (set WRITABLE
* TODO: according to the result). */
memcpy(newpage, parent_page, PGSIZE);
writable = is_writable(pte); // pte는 parent_page를 가리키는 주소
/* 5. Add new page to child's page table at address VA with WRITABLE
* permission. */
if (!pml4_set_page(current->pml4, va, newpage, writable))
{
/* 6. TODO: if fail to insert page, do error handling. */
printf("Failed to map user virtual page to given physical frame\n");
return false;
}
#ifdef DEBUG
// TEST) is 'va' correctly mapped to newpage?
if (pml4_get_page(current->pml4, va) != newpage)
printf("Not mapped!"); // never called
printf("--Completed copy--\n");
#endif
return true;
}
fork의 이해를 돕기 위한 그림자료와 설명.
parent kernel thread가 process_fork()
안에 thread_create()
를 통해 child kernel thread를 생성한다. child kernel thread는 __do_fork()
함수를 들고 들어가서 실행시킨다.
parent kernel thread의 thread_create()는 return되고, process_fork() 안에 tid에는 child kernel thread의 tid가 저장된 상태가 되며 sema_down(&child->forksema)
를 통해 자식이 sema_up을 해줄 때까지 기다린다(blocked) 자식이 부모를 다 복사했으면 sema_up을 해주고 do_iret()
을 통해 user process를 시작한다.
wait()
현재 상태는 test확인을 위해 for문을 돌려놓은 상태이다.
//userprog/process.c
int process_wait(tid_t child_tid UNUSED)
{
for simple tests
for (int i = 0; i < 100000000; i++)
{
}
return -1;
}
이제 제대로 wait()
을 구현해보자.
//userprog/process.c
int process_wait(tid_t child_tid UNUSED)
{
struct thread *child = get_child_with_pid(child_tid);
// 본인의 자식이 아닌경우(호출 프로세스의 하위 항목이 아닌 경우)
if (child == NULL)
return -1;
sema_down(&child->wait_sema); // 여기서는 parent가 잠드는 거고
// 여기서부터는 깨어났다.
// 깨어나면 child의 exit_status를 얻는다.
int exit_status = child->exit_status;
// child를 부모 list에서 지운다.
list_remove(&child->child_elem);
// 내가 받았음을 전달하는 sema
sema_up(&child->free_sema);
return exit_status;
}
wait()
을 더 잘 이해하기 위해서는 대응되는 함수인 process_exit()
과 함께 살펴보면 좋다.
//userprog/process.c
void process_exit(void)
{
struct thread *cur = thread_current();
/* TODO: Your code goes here.
* TODO: Implement process termination message (see
* TODO: project2/process_termination.html).
* TODO: We recommend you to implement process resource cleanup here. */
// project 2-4
for (int i = 0; i < FDCOUNT_LIMIT; i++)
{
close(i);
}
// for multi-oom(메모리 누수)
palloc_free_multiple(cur->fd_table, FDT_PAGES);
// for rox- (실행중에 수정 못하도록)
file_close(cur->running);
sema_up(&cur->wait_sema); // 종료되었다고 기다리고 있는 부모 thread에게 signal 보냄-> sema_up에서 val을 올려줌
sema_down(&cur->free_sema); // 부모의 exit_Status가 정확히 전달되었는지 확인(wait)
process_cleanup(); // pml4를 날림(이 함수를 call 한 thread의 pml4)
}
load()
static bool
load(const char *file_name, struct intr_frame *if_)
{
...
// project 2 : system call
// 현재 실행중인 파일의 경우 write 할 수 없도록 설정
t->running = file;
file_deny_write(file);
...
done:
/* We arrive here whether the load is successful or not. */
// file_close(file);
return success;
}
현재 실행중인 파일의 경우 write 할 수 없도록 설정한다(이걸 해야지 rox-관련된 test들 통과 가능)
참고자료
How does thread/process switching work in Pintos? - MPCS 52030 - Operating Systems