운영체제 · Pintos · Project 2 · Week 10
Pintos Project 2 최종 구현 정리: main branch 코드가 만들어지는 흐름
최신 main branch 기준으로 Project 2 구현을 다시 조립한다. 따라 만들 수 있도록 구조체, syscall, fd, fork, wait 흐름을 블록 단위로 정리했다.
이 글은 발표용 세계관 글이 아니라, 최종 main branch의 Project 2 구현을 따라 조립하기 위한 기술 지도다. 코드 전체를 무작정 붙이는 글이 아니라, 어떤 블록이 어떤 테스트 요구를 해결하는지 기준으로 정리한다.
0. 최종 main 기준 결과
최신 main 기준 결과는 전체 95개 중 85개 통과다. Project 2 기본 userprog는 64개 중 58개, filesys/base는 13개 중 9개, threads는 18개 전부 통과했다.
| 범위 | 결과 | 의미 |
|---|---|---|
| threads | 18/18 PASS | Project 1 회귀 없음 |
| userprog | 58/64 PASS | args, bad pointer, fd, read/write, fork, wait 주요 흐름 통과 |
| filesys/base | 9/13 PASS | 기본 파일 생성/읽기/쓰기 일부 통과 |
| 남은 실패 | 10 FAIL | `rox-*`, `exec-read`, `multi-child-fd`, random/seek/remove 계열, `multi-oom` |
0-1. 구현 단위 지도
| 구현 단위 | 파일/함수 | 역할 |
|---|---|---|
| thread 장부 확장 | `include/threads/thread.h`, `threads/thread.c:init_thread()` | process마다 pml4, fd table, children, child_info를 들고 시작하게 한다. |
| process lifecycle | `userprog/process.c:process_create_initd()`, `process_exec()`, `setup_args()` | args-*와 exec-*의 기반이 되는 실행 파일 로딩과 argv 배치 흐름이다. |
| syscall handler | `userprog/syscall.c:syscall_handler()` | rax 번호를 읽어 halt/exit/read/write/open/fork/wait/exec로 분기한다. |
| user memory validation | `is_crazy_user_address()`, `is_crazy_user_buffer()`, `copy_user_string()` | bad-*와 boundary-*에서 커널 panic 대신 해당 process만 종료하게 한다. |
| fd table | `fd_allocate()`, `fd_table_get_file()`, `fd_close()` | open/read/write/close 계열에서 파일 원본 대신 번호표로 접근하게 한다. |
| fork와 fd 상속 | `process_fork()`, `duplicate_pte()`, `__do_fork()` | fork-* 테스트에서 주소 공간, register, fd table을 자식에게 복제한다. |
| wait/exit | `process_wait()`, `find_child()`, `process_exit()` | wait-*와 multi-recurse에서 직접 자식 확인, 중복 wait 방지, exit status 회수를 처리한다. |
1. thread.h: process가 들고 다닐 장부를 추가한다
Project 2에서 thread는 단순 실행 단위가 아니라 user process의 장부가 된다. 주소 공간, 종료 상태, fd table, 자식 목록을 모두 들고 있어야 한다.
#ifdef USERPROG
/* Owned by userprog/process.c. */
uint64_t *pml4; /* Page map level 4 */
int exit_status;
struct file **fd_table; /* fd → file* 매핑 테이블(파일 디스크립터) */
int fd_table_size; /* fd_table 배열 크기 */
int fd_next; /* 다음에 할당할 fd 후보 */
struct list children; /* 내가 fork로 만든 자식 process 목록 */
struct child_info *child_info; /* 부모가 나를 wait할 때 보는 내 종료 정보 */
#endif
`child_info`는 부모와 자식이 공유하는 wait용 장부다. 부모는 `children` 리스트에서 이 장부를 찾고, 자식은 종료할 때 여기에 status를 적는다.
struct child_info *child_info; /* 부모가 나를 wait할 때 보는 내 종료 정보 */
#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. */
};
구조체에 필드를 추가하는 것만으로는 부족하다. 새 thread가 만들어질 때 기본값을 세팅해야 이후 fd와 wait 흐름이 쓰레기 값을 보지 않는다.
static void
init_thread (struct thread *t, const char *name, int priority) {
ASSERT (t != NULL);
ASSERT (PRI_MIN <= priority && priority <= PRI_MAX);
ASSERT (name != NULL);
memset (t, 0, sizeof *t);
t->status = THREAD_BLOCKED;
strlcpy (t->name, name, sizeof t->name);
t->tf.rsp = (uint64_t) t + PGSIZE - sizeof (void *);
t->priority = priority;
t->base_priority = priority;/* 아래 3줄추가*/
list_init (&t->donations);
t->wait_on_lock = NULL;
t->magic = THREAD_MAGIC;
#ifdef USERPROG
t->pml4 = NULL;
t->exit_status = -1;
t->fd_table = NULL;
t->fd_table_size = 0;
t->fd_next = 2;
list_init (&t->children);
t->child_info = NULL;
#endif
}
2. process.h: syscall 쪽에서 process 함수를 볼 수 있게 한다
`syscall.c`는 `SYS_FORK`, `SYS_WAIT`, `SYS_EXEC`를 처리하면서 process 계층의 함수를 호출한다. 그래서 헤더에 함수 원형이 있어야 한다.
#ifndef USERPROG_PROCESS_H
#define USERPROG_PROCESS_H
#include "threads/thread.h"
tid_t process_create_initd (const char *file_name);
tid_t process_fork (const char *name, struct intr_frame *if_);
int process_exec (void *f_name);
int process_wait (tid_t);
void process_exit (void);
void process_activate (struct thread *next);
void curtain_call(int status);
#endif /* userprog/process.h */
3. init process와 argument passing: process lifecycle의 시작
처음 유저 프로그램은 `process_create_initd()`에서 시작된다. 전체 명령줄은 보존하고, thread 이름으로 쓸 첫 단어만 따로 잘라야 한다.
tid_t
process_create_initd (const char *file_name) {
char *fn_copy;
char *name_copy;
tid_t tid;
sema_init (&initd_sema, 0);
/* Make a copy of FILE_NAME.
* Otherwise there's a race between the caller and load(). */
fn_copy = palloc_get_page(0);
if (fn_copy == NULL){
return TID_ERROR;
}
strlcpy(fn_copy, file_name, PGSIZE);
name_copy = palloc_get_page(0);
if (name_copy == NULL){
palloc_free_page(fn_copy);
return TID_ERROR;
}
strlcpy(name_copy, file_name, PGSIZE);
char *save_ptr;
char *filename_only = strtok_r(name_copy, " ", &save_ptr);
/* Create a new thread to execute FILE_NAME. */
tid = thread_create(filename_only, PRI_DEFAULT, initd, fn_copy);
palloc_free_page(name_copy);
if (tid == TID_ERROR)
palloc_free_page(fn_copy);
else
initd_tid = tid; /* kernel main이 기다려야 하는 최초 유저 프로세스 tid를 저장 */
return tid;
}
/* A thread function that launches first user process. */
Argument Passing은 `setup_args()`가 맡는다. 문자열을 user stack에 복사하고, `argv[]`, `argc`, fake return address를 호출 규약에 맞게 배치한다.
static bool
setup_args(struct intr_frame *if_, char *cmdline){
char *argv[MAX_ARGS];
char *arg_addr[MAX_ARGS];
int argc = 0;
char *token;
char *save_ptr;
for (token = strtok_r(cmdline, " ", &save_ptr); token != NULL; token = strtok_r(NULL, " ", &save_ptr)){
argv[argc++] = token;
}
for (int i = argc - 1; i >= 0; i--){
size_t len = strlen(argv[i]) + 1;
if_->rsp -= len;
memcpy((void *)if_->rsp, argv[i], len);
arg_addr[i] = (char *)if_->rsp;
}
const size_t word_size = sizeof(char *);
while (if_->rsp % word_size != 0){
if_->rsp--;
*(uint8_t *)if_->rsp = 0;
}
if_->rsp -= word_size;
*(char **)if_->rsp = NULL;
for (int i = argc - 1; i >= 0; i--){
if_->rsp -= word_size;
*(char **)if_->rsp = arg_addr[i];
}
char **argv_entry = (char **)if_->rsp;
if_->rsp -= word_size;
*(char **)if_->rsp = NULL;
if_->R.rdi = (uint64_t)argc;
if_->R.rsi = (uint64_t)argv_entry;
return true;
}
/* Adds a mapping from user virtual address UPAGE to kernel
* virtual address KPAGE to the page table.
* If WRITABLE is true, the user process may modify the page;
* otherwise, it is read-only.
* UPAGE must not already be mapped.
* KPAGE should probably be a page obtained from the user pool
* with palloc_get_page().
* Returns true on success, false if UPAGE is already mapped or
* if memory allocation fails. */
static bool
install_page (void *upage, void *kpage, bool writable) {
struct thread *t = thread_current ();
/* Verify that there's not already a page at that virtual
* address, then map our page there. */
return (pml4_get_page (t->pml4, upage) == NULL
&& pml4_set_page (t->pml4, upage, kpage, writable));
}
#else
/* From here, codes will be used after project 3.
* If you want to implement the function for only project 2, implement it on the
* upper block. */
static bool
lazy_load_segment (struct page *page, void *aux) {
/* TODO: Load the segment from the file */
/* TODO: This called when the first page fault occurs on address VA. */
/* TODO: VA is available when calling this function. */
}
/* Loads a segment starting at offset OFS in FILE at address
* UPAGE. In total, READ_BYTES + ZERO_BYTES bytes of virtual
* memory are initialized, as follows:
*
* - READ_BYTES bytes at UPAGE must be read from FILE
* starting at offset OFS.
*
* - ZERO_BYTES bytes at UPAGE + READ_BYTES must be zeroed.
*
* The pages initialized by this function must be writable by the
* user process if WRITABLE is true, read-only otherwise.
*
* Return true if successful, false if a memory allocation error
* or disk read error occurs. */
static bool
load_segment (struct file *file, off_t ofs, uint8_t *upage,
uint32_t read_bytes, uint32_t zero_bytes, bool writable) {
ASSERT ((read_bytes + zero_bytes) % PGSIZE == 0);
ASSERT (pg_ofs (upage) == 0);
ASSERT (ofs % PGSIZE == 0);
while (read_bytes > 0 || zero_bytes > 0) {
/* Do calculate how to fill this page.
* We will read PAGE_READ_BYTES bytes from FILE
* and zero the final PAGE_ZERO_BYTES bytes. */
size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
/* TODO: Set up aux to pass information to the lazy_load_segment. */
void *aux = NULL;
if (!vm_alloc_page_with_initializer (VM_ANON, upage,
writable, lazy_load_segment, aux))
return false;
/* Advance. */
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes;
upage += PGSIZE;
}
return true;
}
/* Create a PAGE of stack at the USER_STACK. Return true on success. */
static bool
setup_stack (struct intr_frame *if_) {
4. syscall handler: 검문소를 만든다
유저 프로그램의 모든 요청은 `syscall_handler()`로 들어온다. syscall 번호는 `rax`, 인자는 `rdi`, `rsi`, `rdx` 등 레지스터에 들어 있다.
void
syscall_handler (struct intr_frame *f UNUSED) {
uint64_t arg0 = f->R.rdi;
uint64_t arg1 = f->R.rsi;
uint64_t arg2 = f->R.rdx;
// TODO: Your implementation goes here.
switch (f->R.rax){
case SYS_OPEN: {
const char *name = (const char *) arg0;
is_crazy_user_string(name);
struct file *opened = filesys_open (name);
f->R.rax = fd_allocate (opened);
if (f->R.rax == -1 && opened != NULL)
file_close (opened);
break;
}
case SYS_CREATE:{
const char *name = (const char *)arg0;
const unsigned initial_size = (unsigned)arg1;
is_crazy_user_string(name);
f->R.rax = filesys_create (name, initial_size);
break;
}
case SYS_READ: {
int fd = (int) arg0;
void *buffer = (void *) arg1;
unsigned size = (unsigned) arg2;
/* 0바이트 읽기는 아무것도 하지 않고 0 반환 */
if (size == 0) {
f->R.rax = 0;
break;
}
/* fd == 0 일 때 (stdin) 키보드 입력 읽기 */
if (fd == 0) {
is_crazy_user_buffer(buffer, size);
uint8_t *buf = (uint8_t *) buffer;
for (unsigned i = 0; i < size; i++)
buf[i] = input_getc();
f->R.rax = size;
/* fd ==1 일 때 (stdout) 지금 읽기니까 이땐 무조건 -1 반환 */
} else if (fd == 1) {
f->R.rax = -1;
/* fd >=2 일 때 open()으로 연 일반 파일이라고 보고 처리 */
} else if (fd >= 2) {
struct file *read_file = fd_table_get_file(fd);
if (read_file == NULL) {
f->R.rax = -1;
} else {
is_crazy_user_buffer(buffer, size);
f->R.rax = file_read(read_file, buffer, size);
}
} else {
f->R.rax = -1;
}
break;
}
case SYS_FILESIZE:{
const int fd = (int)arg0;
struct file *file = fd_table_get_file(fd);
if (file == NULL){
f->R.rax = -1;
} else {
f->R.rax = file_length(file);
}
break;
}
case SYS_CLOSE:
fd_close ((int) arg0);
break;
case SYS_WRITE:{
const int fd = (int)arg0;
const void *buffer = (const void *)arg1;
const size_t size = (size_t)arg2;
if (size == 0){
f->R.rax = 0;
break;
}
is_crazy_user_buffer(buffer, size);
if (fd == STDOUT_FILENO){
putbuf(buffer, size);
f->R.rax = size;
} else if (fd >= 2) {
struct file *write_file = fd_table_get_file (fd);
if (write_file == NULL){
f->R.rax = -1;
} else {
f->R.rax = file_write(write_file, buffer, size);
}
} else {
f->R.rax = -1;
}
break;
}
case SYS_FORK:
f->R.rax = process_fork ((const char *) arg0, f);
break;
case SYS_WAIT:
f->R.rax = process_wait ((tid_t) arg0);
break;
case SYS_EXEC: {
char *cmd_line = copy_user_string ((const char *) arg0);
if (process_exec (cmd_line) < 0)
curtain_call (-1);
NOT_REACHED ();
}
case SYS_EXIT:
curtain_call ((int) arg0);
// thread_current()->exit_status = (int)f->R.rdi;
// thread_exit();
break;
default:
printf ("system call!\n");
thread_exit ();
break;
}
}
5. user memory validation: 유저 포인터를 믿지 않는다
유저가 넘긴 포인터는 NULL일 수도 있고, 커널 주소일 수도 있고, 매핑되지 않은 주소일 수도 있다. 그래서 문자열과 버퍼를 실제로 쓰기 전에 매번 검열한다.
void
curtain_call (int status) {
thread_current ()->exit_status = status;
thread_exit ();
NOT_REACHED ();
}
static void
is_crazy_user_address (const void *u_addr) {
if (u_addr == NULL || !is_user_vaddr (u_addr) ||
pml4_get_page (thread_current ()->pml4, u_addr) == NULL) {
curtain_call (-1);
}
}
static void
is_crazy_user_buffer (const void *buffer, size_t size) {
const uint8_t *start = buffer;
if (size == 0)
return;
for (size_t i = 0; i < size; i++)
is_crazy_user_address (start + i);
}
static void
is_crazy_user_string (const char *str){
is_crazy_user_address(str);
while(*str != '\0'){
str++;
is_crazy_user_address(str);
}
}
static char *
copy_user_string (const char *user_str) {
/* exec 과정에서 기존 유저 주소 공간이 정리될 수 있으므로,
* 유저 문자열을 커널 페이지에 먼저 복사 */
char *kernel_str = palloc_get_page (0);
if (kernel_str == NULL)
curtain_call (-1);
for (size_t i = 0; i < PGSIZE; i++) {
/* 문자열 중간에 잘못된 유저 주소가 섞여 있으면 프로세스를 종료 */
is_crazy_user_address (user_str + i);
kernel_str[i] = user_str[i];
/* 널 문자를 만나면 문자열 복사가 끝난 것이다. */
if (kernel_str[i] == '\0')
return kernel_str;
}
/* 한 페이지 안에서 문자열이 끝나지 않으면 비정상 인자로 보고 종료 */
palloc_free_page (kernel_str);
curtain_call (-1);
NOT_REACHED ();
}
6. fd table: 파일 원본 대신 번호표를 준다
유저에게 `struct file *`를 직접 주지 않는다. 커널은 `fd_table`에 파일 포인터를 저장하고, 유저에게는 fd 번호만 돌려준다.
static bool
fd_table_expand (struct thread *t) {
int old_size = t->fd_table_size;
int new_size = old_size * 2;
struct file **new_table = malloc (sizeof (struct file *) * new_size);
if (new_table == NULL)
return false;
/* 기존 테이블 값 새 테이블로 복사 */
for (int i = 0; i < old_size; i++)
new_table[i] = t->fd_table[i];
/* 새로 확장한 칸 NULL로 만듦 */
for (int i = old_size; i < new_size; i++)
new_table[i] = NULL;
/* 기존 fd table 배열 반납 */
free (t->fd_table);
t->fd_table = new_table;
t->fd_table_size = new_size;
return true;
}
/* fd_next부터 빈 슬롯을 찾아 파일을 등록하고, 배정된 fd 반환.
빈 슬롯이 없으면 fd_table을 확장한 뒤 다시 찾음 */
static int
fd_allocate (struct file *f) {
struct thread *t = thread_current ();
/* 열린 파일이 없거나, fd_table 없으면 실패 */
if (f == NULL || t->fd_table == NULL)
return -1;
/* fd_next부터 빈 칸 찾기 */
for (int i = t->fd_next; i < t->fd_table_size; i++) {
if (t->fd_table[i] == NULL) {
t->fd_table[i] = f;
t->fd_next = i + 1;
return i;
}
}
if (!fd_table_expand (t))
return -1;
/* fd_table 확장 후 fd_next부터 돌면서 빈 fd 찾기 */
for (int i = t->fd_next; i < t->fd_table_size; i++) {
if (t->fd_table[i] == NULL) {
t->fd_table[i] = f;
t->fd_next = i + 1;
return i;
}
}
return -1;
}
/* fd 번호로 실제 struct file * 찾기 */
static struct file *
fd_table_get_file (int fd) {
struct thread *t = thread_current ();
if (fd < 2 || t->fd_table == NULL || fd >= t->fd_table_size)
return NULL;
return t->fd_table[fd];
}
/* fd를 닫기 */
static void
fd_close (int fd) {
struct thread *t = thread_current ();
if (fd < 2 || t->fd_table == NULL || fd >= t->fd_table_size)
return;
if (t->fd_table[fd] == NULL)
return;
file_close (t->fd_table[fd]);
t->fd_table[fd] = NULL;
/* fd_next보다 더 작은 fd가 비었으면 다음 open 때 그 번호를 재사용 */
if (fd < t->fd_next)
t->fd_next = fd;
}
7. fork: 부모의 문맥과 자원을 자식에게 복제한다
fork는 한 번 호출되지만 부모와 자식에게 서로 다른 반환값을 줘야 한다. 부모는 자식 tid를 받고, 자식은 0을 받아야 한다. 이를 위해 부모의 `intr_frame`을 `fork_args`에 담아 자식에게 넘긴다.
struct fork_args {
struct thread *parent; /* fork를 호출한 부모 thread */
struct intr_frame parent_if; /* fork 순간 부모의 레지스터 상태 */
struct semaphore fork_sema; /* 자식 복사가 끝날 때까지 부모를 재우는 신호 */
bool success; /* 자식 복사가 성공했는지 부모에게 알려주는 값 */
struct child_info *child_info;/* 부모 wait와 자식 exit가 공유할 자식 상태 */
};
tid_t
process_fork (const char *name, struct intr_frame *if_) {
/* 자식에게 넘길 fork 준비물 상자를 만든다.
여기에는 부모 thread, 부모 intr_frame, 동기화용 semaphore가 들어간다. */
struct fork_args *args = palloc_get_page (PAL_ZERO);
if (args == NULL)
return TID_ERROR; /* 준비물 상자를 못 만들면 fork 실패 */
/* 현재 실행 중인 thread가 fork를 호출한 부모다. */
args->parent = thread_current ();
struct thread *parent = args->parent;
/* wait()가 찾을 수 있도록 부모와 자식이 공유할 child_info를 만든다. */
struct child_info *child = malloc (sizeof *child);
if (child == NULL) {
palloc_free_page (args);
return TID_ERROR;
}
child->pid = TID_ERROR; /* thread_create() 성공 후 실제 tid로 채운다. */
child->exit_status = -1; /* 자식이 비정상 종료하거나 아직 상태가 없을 때 기본값 */
child->exited = false; /* 아직 자식은 종료되지 않았다. */
child->waited = false; /* 부모가 아직 이 자식을 wait하지 않았다. */
sema_init (&child->wait_sema, 0); /* 부모가 wait에서 잠들 때 사용할 세마포어 */
args->child_info = child; /* 자식 __do_fork()에게도 같은 child_info를 넘긴다. */
/* fork syscall 순간의 부모 레지스터 상태를 복사한다.
자식은 이 값을 기반으로 fork() 이후 지점부터 실행된다. */
memcpy (&args->parent_if, if_, sizeof args->parent_if);
/* 부모는 자식이 복사 성공/실패를 알려줄 때까지 기다려야 한다.
초기값 0이면 부모가 sema_down()을 호출했을 때 잠든다. */
sema_init (&args->fork_sema, 0);
args->success = false;
/* 자식 thread를 만든다.
aux로 args를 넘겨 자식이 부모 정보와 semaphore에 접근하게 한다. */
tid_t tid = thread_create (name, PRI_DEFAULT, __do_fork, args);
if (tid == TID_ERROR) {
palloc_free_page (args); /* 자식 생성 실패 시 준비물 상자 회수 */
free (child); /* wait용 child_info도 더 이상 쓸 일이 없으므로 회수 */
return TID_ERROR;
}
/* 깃북 요구사항:
부모는 자식이 복제에 성공했는지 실패했는지 알기 전까지 fork에서 반환하면 안 된다. */
sema_down (&args->fork_sema);
/* 자식이 기록한 성공 여부를 확인한다.
args는 아래에서 해제할 것이므로 success 값을 지역 변수로 빼둔다. */
bool success = args->success;
if (success) {
/* wait(pid)가 찾을 수 있게 부모의 children 목록에 등록한다. */
child->pid = tid;
list_push_back (&parent->children, &child->elem);
} else {
/* 자식 복제 실패 시 부모 children에 올리지 않고 정보 객체를 정리한다. */
free (child);
}
palloc_free_page (args); /* 부모가 fork 결과를 확인했으므로 준비물 상자 회수 */
return success ? tid : TID_ERROR;
}
주소 공간 복제는 `pml4_for_each()`와 `duplicate_pte()`가 맡는다. 커널 페이지는 건너뛰고, user page만 새 페이지를 할당해 복사한다.
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 true;
}
/* 2. Resolve VA from the parent's page map level 4. */
/* 2. 부모 pml4에서 va가 실제로 연결된 페이지를 찾는다.
va는 유저 프로그램이 보는 가상주소이고,
parent_page는 커널이 실제 내용을 복사할 수 있는 페이지 주소다. */
parent_page = pml4_get_page (parent->pml4, va);
if (parent_page == NULL)
{
return false; /* 부모 페이지를 못 찾으면 자식에게 복사할 수 없으므로 fork 실패 */
}
/* 3. TODO: Allocate new PAL_USER page for the child and set result to
TODO: NEWPAGE. */
/* 3. 자식이 사용할 새 유저 페이지를 할당한다.
부모 페이지를 그대로 공유하면 부모/자식 메모리가 서로 영향을 주므로,
자식 전용 페이지를 새로 만들고 나중에 부모 내용을 복사해야 한다. */
newpage = palloc_get_page (PAL_USER);
if (newpage == NULL)
{
return false; /* 새 페이지를 못 만들면 주소공간 복사를 계속할 수 없으므로 fork 실패 */
}
/* 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). */
/* 4. 부모 페이지 내용을 자식 새 페이지에 그대로 복사한다.
parent_page와 newpage는 둘 다 실제 페이지 내용에 접근 가능한 주소다. */
memcpy (newpage, parent_page, PGSIZE);
/* 부모 페이지의 쓰기 가능 여부를 확인한다.
자식 페이지도 부모와 같은 권한을 가져야 하므로 이 값을 pml4_set_page()에 넘긴다. */
writable = is_writable (pte);
/* 5. 자식 pml4에 새 페이지를 연결한다.
부모가 쓰던 가상주소 va를 자식도 똑같이 쓰게 하되,
실제 연결되는 페이지는 자식 전용 newpage다. */
if (!pml4_set_page (current->pml4, va, newpage, writable)) {
/* 6. 연결에 실패하면 방금 할당한 newpage를 회수한다.
회수하지 않으면 아무도 참조하지 않는 페이지가 남아서 메모리 누수가 된다. */
palloc_free_page (newpage);
return false;
}
return true;
}
자식 thread 안에서는 `__do_fork()`가 실행된다. 부모 register를 복사하고, 자식의 `rax`를 0으로 바꾸고, pml4와 fd table을 복제한다. 이 지점이 fork 담당 파트에서 말하는 fd 상속의 핵심이다.
static void
__do_fork (void *aux) {
struct intr_frame if_;
/* process_fork()에서 넘긴 aux를 fork_args 타입으로 다시 해석한다.
aux 안에는 부모 thread와 부모 intr_frame이 들어 있다. */
struct fork_args *args = aux;
/* 부모 thread.
뒤에서 duplicate_pte()가 parent->pml4를 보고 부모 주소공간을 복사한다. */
struct thread *parent = args->parent;
/* 현재 thread는 방금 생성된 자식 thread다. */
struct thread *current = thread_current ();
bool succ = true;
/* 1. Read the cpu context to local stack.
부모가 fork syscall을 호출한 순간의 레지스터 상태를 자식의 intr_frame에 복사한다.
이 복사가 있어야 자식도 fork() 다음 줄부터 실행되는 것처럼 동작한다. */
memcpy (&if_, &args->parent_if, sizeof (struct intr_frame));
/* fork()는 부모와 자식에게 서로 다른 반환값을 준다.
부모는 process_fork()의 반환값으로 자식 tid를 받고,
자식은 여기서 rax를 0으로 바꿔 fork()가 0을 반환한 것처럼 만든다. */
if_.R.rax = 0;
/* 2. Duplicate PT */
current->pml4 = pml4_create();
if (current->pml4 == NULL)
goto error;
process_activate (current);
#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.
* 해당 투두 아래에 작성함 */
/* fork()는 부모 프로세스의 열린 파일 목록도 자식에게 복사해야 한다.
부모와 자식이 같은 fd 번호를 쓸 수 있어야 fork-close 같은 테스트가 성립한다. */
current->fd_table_size = parent->fd_table_size;
current->fd_next = parent->fd_next;
current->fd_table = NULL;
/* 부모가 fd_table을 가지고 있으면, 자식도 같은 크기의 fd_table을 새로 만든다.
배열 자체는 새로 만들고, 각 파일 객체는 file_duplicate()로 복제한다. */
if (parent->fd_table != NULL && parent->fd_table_size > 0) {
current->fd_table = malloc (sizeof (struct file *) * current->fd_table_size);
if (current->fd_table == NULL)
goto error;
/* 먼저 전부 NULL로 초기화한다.
중간에 실패했을 때 어디까지 닫아야 하는지 안전하게 만들기 위함이다. */
for (int i = 0; i < current->fd_table_size; i++)
current->fd_table[i] = NULL;
/* fd 0, 1은 stdin/stdout 예약 번호라서 현재 구조에서는 파일 객체 복사 대상이 아니다.
실제 파일은 fd 2번부터 들어간다. */
for (int i = 2; i < parent->fd_table_size; i++) {
if (parent->fd_table[i] == NULL)
continue;
/* 부모 fd가 가리키던 파일을 자식 fd에도 복제해서 연결한다. */
current->fd_table[i] = file_duplicate (parent->fd_table[i]);
/* 복제 중 하나라도 실패하면 이미 복제한 파일들을 닫고 fork 실패로 처리한다. */
if (current->fd_table[i] == NULL) {
for (int j = 2; j < i; j++) {
if (current->fd_table[j] != NULL)
file_close (current->fd_table[j]);
}
free (current->fd_table);
current->fd_table = NULL;
current->fd_table_size = 0;
current->fd_next = 2;
goto error;
}
}
}
process_init ();
/* 여기까지 왔다는 것은 자식 pml4 복사까지 성공했다는 뜻이다.
부모가 sema_down()에서 기다리고 있으므로 성공 여부를 기록하고 깨운다. */
if (succ) {
/* 자식 종료 시 process_exit()가 부모의 child_info에 상태를 남길 수 있게 연결한다.
이 포인터는 부모의 children 리스트에 들어간 객체와 같은 객체다. */
current->child_info = args->child_info;
args->success = true;
sema_up (&args->fork_sema);
do_iret (&if_);
}
error:
/* 중간에 pml4_create()나 pml4_for_each()가 실패하면 여기로 온다.
실패도 부모에게 알려야 부모가 영원히 잠들지 않는다. */
args->success = false;
sema_up (&args->fork_sema);
thread_exit ();
}
8. wait와 exit: 죽음을 장부에 남긴다
부모는 아무 pid나 기다릴 수 없다. `find_child()`로 직접 자식인지 확인하고, `waited`로 중복 wait를 막는다. 자식이 아직 살아 있으면 `wait_sema`에서 잠들고, 종료 후 status를 회수한다.
int
process_wait (tid_t child_tid) {
/* XXX: Hint) The pintos exit if process_wait (initd), we recommend you
* XXX: to add infinite loop here before
* XXX: implementing the process_wait. */
/* 현재 process_wait를 호출한 주체가 kernel main thread라면
pml4가 없는 커널 thread라면
기존 initd_sema 방식으로 최초 유저 프로그램을 기다린다. */
if (thread_current()->pml4 == NULL) {
sema_down(&initd_sema);
return initd_exit_status;
}
/* 자식 여부 확인 */
struct child_info *child = find_child (child_tid);
/* pid에 해당하는 직접 자식이 아니면 wait 실패로 -1 반환 */
if (child == NULL) {
return -1;
}
/* 이미 wait 했으면 -1 반환 */
if (child->waited) {
return -1;
}
/* 같은 pid에 대해 wait를 두 번 성공하지 못하도록 표시 */
child->waited = true;
/* 자식이 아직 종료되지 않았으면 부모가 잠듦 */
if(!child->exited) {
sema_down(&child->wait_sema);
}
/* 자식 종료 후 남긴 상태값 저장 */
int status = child->exit_status;
/* children list에서 제거하고, 메모리 해제 */
list_remove (&child->elem);
free(child);
return status;
}
/* 자식 찾기 */
static struct child_info *find_child (tid_t child_tid){
struct thread *curr = thread_current();
struct list_elem *e;
/* curr->children [ child_info(pid=3).elem ] -> [ child_info(pid=7).elem ] */
for (e = list_begin (&curr->children);
e != list_end (&curr->children);
e = list_next(e)) {
struct child_info *child = list_entry (e, struct child_info, elem);
if(child->pid == child_tid){
return child;
}
}
return NULL;
}
/* Exit the process. This function is called by thread_exit (). */
void
process_exit (void) {
struct thread *curr = thread_current();
/* 유저 프로세스 종료 메시지 출력 */
if (curr->pml4 != NULL) {
printf("%s: exit(%d)\n", curr->name, curr->exit_status);
}
/* initd_sema는 Pintos kernel main이 최초 유저 프로그램 종료를 기다릴 때만 쓴다. */
if (curr->tid == initd_tid) {
initd_exit_status = curr->exit_status;
sema_up(&initd_sema);
}
/* 종료 중인 자식이면 부모가 wait()에서 읽을 종료 정보를 남긴다. */
if (curr->child_info != NULL) {
/* 현재 thread의 종료 상태를 부모가 읽을 child_info에 복사 */
curr->child_info->exit_status = curr->exit_status;
/* 종료 여부를 true로 변경 */
curr->child_info->exited = true;
/* 기다리고 있던 부모를 깨움 */
sema_up (&curr->child_info->wait_sema);
}
process_cleanup ();
}
/* Free the current process's resources. */
static void
process_cleanup (void) {
struct thread *curr = thread_current ();
#ifdef VM
supplemental_page_table_kill (&curr->spt);
#endif
uint64_t *pml4;
/* Destroy the current process's page directory and switch back
* to the kernel-only page directory. */
pml4 = curr->pml4;
if (pml4 != NULL) {
/* Correct ordering here is crucial. We must set
* cur->pagedir to NULL before switching page directories,
* so that a timer interrupt can't switch back to the
* process page directory. We must activate the base page
* directory before destroying the process's page
* directory, or our active page directory will be one
* that's been freed (and cleared). */
curr->pml4 = NULL;
pml4_activate (NULL);
pml4_destroy (pml4);
}
}
9. 남은 한계
Project 2 코드는 syscall을 많이 추가하는 문제가 아니라, 유저가 요청한 자원을 커널 장부에 안전하게 연결하고, 실패했을 때 커널이 죽지 않게 회수하는 문제였다.