2023. 5. 5. 09:06ㆍComputer Science
Pintos Project 2 관련 포스팅 목록
- Argument Passing (User 프로그램 인자 설정하기)
- System Calls - 1 (System Calls이 호출되는 과정)
- System Calls - 2 (exec, wait, fork) & Deny Write on Executables
- System Calls - 3 (File System) & User Memory
1. exec
1-1. exec 요구사항
- 현재 프로세스를 cmd_line에 주어진 실행 파일로 변경하고, 필요한 인수를 전달합니다.
- 이 함수는 성공한 경우 반환하지 않습니다.
- 프로그램이 로드되거나 실행될 수 없는 경우 종료 상태 -1로 프로세스가 종료됩니다.
- 이 함수는 exec를 호출한 스레드의 이름을 변경하지 않습니다.
- 파일 디스크립터는 exec 호출을 통해 변경되지 않고 열린 상태를 유지합니다.
(exec 함수를 호출하여 프로세스를 새로운 실행 파일로 대체하더라도, 이미 열려있는 파일 디스크립터들은 그대로 유지됩니다.)
1-2. exec 구현하기
1) syscall_handler()
- exec을 호출하는 case문을 추가한다.
/* userprog/syscall.c */
void syscall_handler(struct intr_frame *f UNUSED)
{
...
case SYS_EXEC:
f->R.rax = exec(f->R.rdi);
break;
}
2) exec()
💡 Goal : process_exec 함수를 이용해서 인자로 전달받은
cmd_line
을 실행한다.
cmd_line
의 주소를 검증한다.
check_address(cmd_line);
cmd_line
을 process_exec에 전달할 복사본을 만든다.- 🤔 Why? 그냥 넘기면 안 되나?
- process_exec 함수 안에서 전달 받은 인자를 parsing하는 과정이 있기 때문에 복사본을 만들어서 전달해야 한다.
cmd_line은 const char* 타입이라서 수정할 수 없음! - process.c 파일의 process_create_initd 함수에서 file_name의 복사본을 만들고 실행시키는 부분을 참고해서 구현할 수 있다. (단, 과제 안내에서 exec를 호출한 스레드의 이름을 변경하지 말라고 했으므로, process_create_initd에서 인자를 수정하는 부분 전까지만 참고한다.)
char *cmd_line_copy;
cmd_line_copy = palloc_get_page(0);
if (cmd_line_copy == NULL)
exit(-1); // 메모리 할당 실패 시 status -1로 종료한다.
strlcpy(cmd_line_copy, cmd_line, PGSIZE); // cmd_line을 복사한다.
- 복사본을 인자로 process_exec 함수를 호출하고, 로드되거나 실행될 수 없는 경우에는 status -1로 프로세스를 종료시킨다.
- process_exec 함수는 실패 시 -1을 리턴하므로 -1인 경우에 에러 핸들링을 해주면 된다.
// 스레드의 이름을 변경하지 않고 바로 실행한다.
if (process_exec(cmd_line_copy) == -1)
exit(-1); // 실패 시 status -1로 종료한다.
- 이 함수는 성공한 경우 반환하지 않으므로 별도의 return은 없다!
🖥 exec() 전체 코드
/* userprog/syscall.c */
int exec(const char *cmd_line); // 선언
...
int exec(const char *cmd_line)
{
check_address(cmd_line);
// process.c 파일의 process_create_initd 함수와 유사하다.
// 단, 스레드를 새로 생성하는 건 fork에서 수행하므로
// 이 함수에서는 새 스레드를 생성하지 않고 process_exec을 호출한다.
// process_exec 함수 안에서 filename을 변경해야 하므로
// 커널 메모리 공간에 cmd_line의 복사본을 만든다.
// (현재는 const char* 형식이기 때문에 수정할 수 없다.)
char *cmd_line_copy;
cmd_line_copy = palloc_get_page(0);
if (cmd_line_copy == NULL)
exit(-1); // 메모리 할당 실패 시 status -1로 종료한다.
strlcpy(cmd_line_copy, cmd_line, PGSIZE); // cmd_line을 복사한다.
// 스레드의 이름을 변경하지 않고 바로 실행한다.
if (process_exec(cmd_line_copy) == -1)
exit(-1); // 실패 시 status -1로 종료한다.
}
exec 끝~!
2. fork
2-1. fork 요구사항
- 현재 프로세스를 복제하여 THREAD_NAME이라는 이름을 가진 새로운 프로세스를 생성해야 합니다.
- 레지스터 값은 %RBX, %RSP, %RBP, %R12부터 %R15까지인 callee-saved register만 복제해야 합니다.
→ 🚨 레지스터 값을 쟤네만 복제하라는 뜻이 아님!
→ general purpose register 중에서는 callee-saved register 제외하고는 백업 안해도 된다.
즉, %rip, %ds, %ss, %es, %cs 등 특수한 역할을 하는 레지스터들도 복사를 해줘야한다! - 함수는 자식 프로세스의 pid를 반환해야 하며, 그렇지 않은 경우 유효한 pid가 될 수 없습니다.
- 자식 프로세스에서는 반환 값이 0이어야 합니다.
- 자식 프로세스는 파일 디스크립터와 가상 메모리 공간을 포함한 리소스를 복제해야 합니다.
- 부모 프로세스는 자식 프로세스가 성공적으로 복제되었는지 알 때까지 fork() 호출에서 반환되지 않아야 합니다.
- 즉, 자식 프로세스가 리소스를 복제하지 못한 경우 부모의 fork() 호출은 TID_ERROR를 반환해야 합니다.
- 해당 템플릿은 threads/mmu.c 파일의 pml4_for_each() 함수를 사용하여 사용자 메모리 공간 전체와 해당 페이지 테이블 구조를 복사합니다.
- pte_for_each_func의 누락된 부분을 채워주어야 합니다.
2-2. fork 구현하기 (+ Deny Write on Executables)
1) syscall_handler()
- fork를 호출하는 case문을 추가한다.
/* userprog/syscall.c */
void syscall_handler(struct intr_frame *f UNUSED)
{
...
case SYS_FORK:
f->R.rax = fork(f->R.rdi, f);
break;
}
2) fork()
- process_fork 함수를 호출한다.
/* userprog/syscall.c */
int fork(const char *thread_name, struct intr_frame *f); // 선언
int fork(const char *thread_name, struct intr_frame *f)
{
return process_fork(thread_name, f);
}
3) process_fork()
💡 Goal : 현재 프로세스를 복제하여 name이라는 이름의 프로세스를 만들고, 새로 만든 프로세스의 스레드 ID를 리턴한다.
- 현재 구현 상태👇🏻 (이 함수의 내용을 수정해야 한다.)
/* Clones the current process as `name`. Returns the new process's thread id, or
* TID_ERROR if the thread cannot be created. */
tid_t process_fork(const char *name, struct intr_frame *if_ UNUSED)
{
/* Clone current thread to new thread.*/
return thread_create(name,
PRI_DEFAULT, __do_fork, thread_current());
}
3-1) if_를 parent_if에 복사
- 현재 스레드에 인자로 전달 받은 if_를 저장할 수 있도록 필드를 새로 만들어 저장한다.
- Why?
- 스레드를 생성할 때 __do_fork 함수를 이용하는데
__do_fork의 주석 Hint 부분을 보면, parent->tf에는 userland context가 들어있지 않으니 process_fork의 두번째 인자를 이 함수에 전달할 수 있게 해야 한다고 말하고 있다.
(Userland context는 유저 모드에서 실행 중인 프로세스의 상태와 관련된 정보를 의미한다.)
- 스레드를 생성할 때 __do_fork 함수를 이용하는데
__do_fork 주석 내용
/* A thread function that copies parent's execution context.
* Hint) parent->tf does not hold the userland context of the process.
* That is, you are required to pass second argument of process_fork to
* this function. */
- 부모의 실행 컨텍스트를 복사하는 스레드 함수입니다.
- 힌트) parent->tf는 프로세스의 사용자 및 컨텍스트를 보유하지 않습니다.
- 즉, process_fork의 두 번째 인수를 이 함수에 전달해야 합니다.
- 🤔 왜 parent->tf를 사용할 수 없는 걸까?
- fork를 하면 자식은 부모의
tf
를 물려받아야 하는데, 지금은 fork()를 수행하면서 context switch가 일어난 상태로, 현재 부모의tf
에는 커널이 작업하던 정보가 저장되어 있다. - 자식에게 물려줘야 하는
tf
는 커널이 작업하던 정보가 아닌 user-level에서 부모 프로세스가 작업하던 정보를 물려줘야 한다. - user_level에서의 부모 프로세스가 실행되던 정보는 시스템콜이 호출될 때 syscall_handler에
f
로 들어온다. - 따라서,
f
를 fork 함수에 전달해서 활용해야 한다. - 👇🏻 부모의 tf를 나타내는 cur→tf와 fork 인자로 전달 받은 if를 출력해본 결과이다. 주소를 변환해보면 부모의 tf는 커널 영역의 값이고 fork 인자로 받은 if는 유저 영역의 값을 가지고 있다.
- fork를 하면 자식은 부모의
struct thread
- 그래서, 일단 스레드 구조체에 f를 넣기 위해 필드를 하나 새로 만든다.
/* threads/thread.h */
struct thread
{
...
struct file **fdt;
int next_fd;
struct intr_frame parent_if; // 추가
...
}
process_fork
- parent_if에 인자로 전달 받은 if를 복사한다.
struct thread *cur = thread_current();
memcpy(&cur->parent_if, if_, sizeof(struct intr_frame));
- 전달받은 if와 cur→parent_if에 if값을 복사한 상태 👇🏻 (이렇게 만들어줘야 한다!)
3-2) 새 스레드 생성
process_fork
- 스레드가 생성되고 나서 실행할 함수로
__do_fork
를 지정하고, 인자로 부모가 될 스레드(위에서 if_를 복사해서 넣은 스레드)를 넣어준다.
// 현재 스레드를 fork한 new 스레드를 생성한다.
tid_t tid = thread_create(name, PRI_DEFAULT, __do_fork, cur);
if (tid == TID_ERROR)
return TID_ERROR;
3-3) 자식 관계 설정
- 새로 생성되는 스레드를 현재 실행중인 스레드의 자식으로 지정해준다.
struct thread
- 자식 리스트를 설정하기 위한 필드
child_list
와child_elem
을 추가한다.
/* threads/thread.h */
struct thread
{
...
struct file **fdt;
int next_fd;
struct intr_frame parent_if;
struct list child_list; // 추가
struct list_elem child_elem; // 추가
...
}
init_thread
- 새로 만든 child_list를 초기화해준다.
/* threads/thread.c */
static void
init_thread(struct thread *t, const char *name, int priority)
{
...
list_init(&(t->child_list));
}
thread_create
- 현재 스레드의 child_list에 새로 만든 스레드를 추가한다.
// 현재 스레드의 자식으로 추가
list_push_back(&thread_current()->child_list, &t->child_elem);
3-4) 로드가 완료될 때까지 대기
- 이제 스레드는 thread_create로 생성되고 ready_list에 들어간 상태다. 이 스레드가 스케줄링되어서 실행되면 thread_create 함수에 넣어준 __do_fork 함수가 호출되어 load가 진행된다.
- 부모는 이 load가 완료될 때까지 대기해야 한다.
- semaphore를 활용해 load가 완료될 때까지 부모를 재우자~!
struct thread
- load_sema 필드를 추가한다.
/* threads/thread.h */
struct thread
{
...
struct file **fdt;
int next_fd;
struct intr_frame parent_if;
struct list child_list;
struct list_elem child_elem;
struct semaphore load_sema; // 추가
...
}
init_thread
- load_sema를 초기화한다.
/* threads/thread.c */
static void
init_thread(struct thread *t, const char *name, int priority)
{
...
sema_init(&t->load_sema, 0);
}
process_fork
- 생성하면서 반환 받은 pid를 이용해서 방금 생성한 자식 스레드를 찾는다.
- pid로 자식을 찾는 함수 get_child_process는 아래에서 새로 선언한다.
- 자식 스레드의 load_sema를 이용해 sema_down을 호출한다.
// 자식이 로드될 때까지 대기하기 위해서 방금 생성한 자식 스레드를 찾는다.
struct thread *child = get_child_process(pid);
// 현재 스레드는 생성만 완료된 상태이다. 생성되어서 ready_list에 들어가고 실행될 때 __do_fork 함수가 실행된다.
// __do_fork 함수가 실행되어 로드가 완료될 때까지 부모는 대기한다.
sema_down(&child->load_sema);
get_child_process
- pid를 인자로 받아 자식 스레드를 반환하는 함수를 새로 선언한다.
(process_fork랑 process_wait에서 쓰인다.)
/* userprog/process.h */
struct thread *get_child_process(int pid);
/* userprog/process.c */
// 자식 리스트에서 원하는 프로세스를 검색하는 함수
struct thread *get_child_process(int pid)
{
/* 자식 리스트에 접근하여 프로세스 디스크립터 검색 */
struct thread *cur = thread_current();
struct list *child_list = &cur->child_list;
for (struct list_elem *e = list_begin(child_list); e != list_end(child_list); e = list_next(e))
{
struct thread *t = list_entry(e, struct thread, child_elem);
/* 해당 pid가 존재하면 프로세스 디스크립터 반환 */
if (t->tid == pid)
return t;
}
/* 리스트에 존재하지 않으면 NULL 리턴 */
return NULL;
}
3-5) 자식 프로세스의 pid 반환
- 과제에서 요구한대로 자식 프로세스의 pid를 반환하고 종료한다.
process_fork
// 자식 프로세스의 pid를 반환한다.
return pid;
🖥 process_fork 전체 코드
/* Clones the current process as `name`. Returns the new process's thread id, or
* TID_ERROR if the thread cannot be created. */
tid_t process_fork(const char *name, struct intr_frame *if_ UNUSED)
{
/* Clone current thread to new thread.*/
// 현재 스레드의 parent_if에 복제해야 하는 if를 복사한다.
struct thread *cur = thread_current();
memcpy(&cur->parent_if, if_, sizeof(struct intr_frame));
// 현재 스레드를 fork한 new 스레드를 생성한다.
tid_t pid = thread_create(name, PRI_DEFAULT, __do_fork, cur);
if (pid == TID_ERROR)
return TID_ERROR;
// 자식이 로드될 때까지 대기하기 위해서 방금 생성한 자식 스레드를 찾는다.
struct thread *child = get_child_process(pid);
// 현재 스레드는 생성만 완료된 상태이다. 생성되어서 ready_list에 들어가고 실행될 때 __do_fork 함수가 실행된다.
// __do_fork 함수가 실행되어 로드가 완료될 때까지 부모는 대기한다.
sema_down(&child->load_sema);
// 자식 프로세스의 pid를 반환한다.
return pid;
}
4) __do_fork()
4-1) parent_if 할당
- 인자로 전달 받은(process_fork에서 전달한) 부모 스레드의 parent_if 필드의 값을 parent_if에 할당한다.
/* TODO: somehow pass the parent_if. (i.e. process_fork()'s if_) */
struct intr_frame *parent_if = &parent->parent_if;
4-2) 자식 프로세스의 리턴값 지정
- 자식 프로세스의 리턴값은 0으로 지정한다.
/* 1. Read the cpu context to local stack. */
memcpy(&if_, parent_if, sizeof(struct intr_frame));
if_.R.rax = 0; // 자식 프로세스의 리턴값은 0
4-3) 파일 디스크립터 테이블의 파일 복제
- 주석 내용대로 file_duplicate 함수를 활용하면 파일을 복제할 수 있다.
/* 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.*/
// FDT 복제
for (int i = 0; i < FDT_COUNT_LIMIT; i++)
{
struct file *file = parent->fdt[i];
if (file == NULL)
continue;
if (file > 2)
file = file_duplicate(file);
current->fdt[i] = file;
}
// next_fd도 복제
current->next_fd = parent->next_fd;
4-4) 부모 프로세스의 대기 해제
- 로드가 완료되었으니 sema_up을 호출해 부모 프로세스의 대기를 해제시킨다.
// 로드가 완료될 때까지 기다리고 있던 부모 대기 해제
sema_up(¤t->load_sema);
🖥 __do_fork 전체 코드
static void
__do_fork(void *aux)
{
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 = &parent->parent_if;**
bool succ = true;
/* 1. Read the cpu context to local stack. */
memcpy(&if_, parent_if, sizeof(struct intr_frame));
if_.R.rax = 0; // 자식 프로세스의 리턴값은 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.*/
// FDT 복사
for (int i = 0; i < FDT_COUNT_LIMIT; i++)
{
struct file *file = parent->fdt[i];
if (file == NULL)
continue;
if (file > 2)
file = file_duplicate(file);
current->fdt[i] = file;
}
current->next_fd = parent->next_fd;
// 로드가 완료될 때까지 기다리고 있던 부모 대기 해제
sema_up(¤t->load_sema);
process_init();
/* Finally, switch to the newly created process. */
if (succ)
do_iret(&if_);
error:
sema_up(¤t->load_sema);
exit(TID_ERROR);
}
5) duplicate_pte
- 페이지 테이블을 복제하는 데 사용되는 함수 duplicate_pte의 빈 부분을 채운다.
- why? 과제에서 애초에 채우라고 비워서 줬음ㅎㅋ
- 주석을 보고 시키는대로 하면 된당
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. */
parent_page = pml4_get_page(parent->pml4, va);
if (parent_page == NULL)
return false;
/* 3. TODO: Allocate new PAL_USER page for the child and set result to
* TODO: NEWPAGE. */
newpage = palloc_get_page(PAL_USER | PAL_ZERO);
if (newpage == NULL)
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);
/* 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. */
return false;
}
return true;
}
6) load
- 현재 실행 중인 파일을 수정하는 일이 발생하면 안되겠지!
- 실행 중인 파일에 대한 쓰기 작업을 거부하는 코드를 추가한다.
file_deny_write()
함수를 사용- User Programs 5번째 과제임 (Deny Write on Executables)
- 이 부분을 구현하면 rox 관련 테스트들을 통과할 수 있다. (rox: Read Only for eXecutable)
- 파일을 닫으면 쓰기 작업이 다시 허용된다ㅠㅠ 따라서 여기서 파일을 닫지 않고 스레드가 종료될 때 파일을 닫을 수 있게 스레드 구조체에 로드한 파일을 저장할 수 있게 필드를 추가한다.
struct thread
- 현재 스레드의 실행중인 파일을 저장할 running 필드를 추가한다.
/* threads/thread.h */
struct thread
{
...
struct file **fdt;
int next_fd;
struct intr_frame parent_if;
struct list child_list;
struct list_elem child_elem;
struct semaphore load_sema;
struct file *running; // 추가
...
}
load
- 현재 load 함수에서는 로드가 완료되면 파일을 바로 close하는데, 여기서 close하지 않고 스레드가 삭제될 때 파일을 닫도록 변경해야 한다. —> file_close(file); 부분을 제거한다.
- Why? 파일을 닫으면 쓰기 작업이 다시 허용되기 때문이다. 따라서 프로세스의 실행 파일에 대한 쓰기를 거부하려면 해당 파일을 프로세스가 실행 중인 동안 계속 열어두어야 한다.
static bool
load(const char *file_name, struct intr_frame *if_)
{
...
// 스레드가 삭제될 때 파일을 닫을 수 있게 구조체에 파일을 저장해둔다.
t->running = file;
// 현재 실행중인 파일은 수정할 수 없게 막는다.
file_deny_write(file);
/* Set up stack. */
if (!setup_stack(if_)) // user stack 초기화
goto done;
/* Start address. */
if_->rip = ehdr.e_entry; // entry point 초기화
// rip: 프로그램 카운터(실행할 다음 인스트럭션의 메모리 주소)
/* TODO: Your code goes here.
* TODO: Implement argument passing (see project2/argument_passing.html). */
success = true;
done:
/* We arrive here whether the load is successful or not. */
// 파일을 여기서 닫지 않고 스레드가 삭제될 때 process_exit에서 닫는다.
// file_close(file);
return success;
}
process_exit
- 프로세스가 종료될 때 현재 실행 중인 파일을 닫는다.
void process_exit(void)
{
...
file_close(cur->running); // 현재 실행 중인 파일을 닫는다.
process_cleanup();
}
3. wait
3-1. wait 요구사항
- wait() 함수는 자식 프로세스 pid를 기다리고 자식의 종료 상태(exit status)를 검색합니다.
- pid가 아직 살아있는 경우 종료될 때까지 기다립니다.
- 자식이 종료되면 자식의 종료 상태를 반환합니다.
- pid가 exit()를 호출하지 않고 커널에 의해 종료된 경우(예: exception으로 인해 종료) wait(pid)는 -1을 반환해야 합니다.
- 부모 프로세스가 호출을 기다리는 동안에 이미 종료된 자식 프로세스를 기다리고 있을 수 있지만, 커널은 여전히 부모가 자식의 종료 상태를 검색하거나 자식이 커널에 의해 종료되었음을 알 수 있도록 허용해야 합니다.
- 다음 중 어느 하나의 조건이 참이면 wait()은 즉시 실패하고 -1을 반환해야 합니다:
- pid가 호출하는 프로세스의 직접적인 자식을 참조하지 않는 경우
- 호출 프로세스가 성공적인 fork() 호출의 반환값으로 받은 pid만 호출 프로세스의 직접적인 자식에 해당한다.
- 자식은 상속되지 않습니다.
A가 자식 B를 생성하고 B가 자식 프로세스 C를 생성하는 경우 A는 C를 기다릴 수 없습니다.(B가 죽더라도)
따라서 A가 프로세스 C를 기다리는 wait(C) 호출은 실패해야 합니다.
마찬가지로, 고아 프로세스는 새로운 부모를 할당받지 않습니다.
- wait를 호출한 프로세스가 이미 pid에 대해 wait를 호출한 경우
즉, 한 프로세스는 특정 자식에 대해 최대 한 번까지만 기다릴 수 있습니다.
- pid가 호출하는 프로세스의 직접적인 자식을 참조하지 않는 경우
- 프로세스는 임의의 수의 자식을 생성하고, 임의의 순서로 그들을 기다릴 수 있으며, 자식의 일부 또는 전체에 대해 기다리지 않고 종료할 수도 있습니다.
- 모든 wait이 발생할 수 있는 방법을 고려해야 합니다.
- struct thread를 포함한 프로세스의 모든 리소스는 프로세스의 부모가 프로세스를 기다리는지 여부와
자식이 부모보다 먼저 종료되었는지 여부에 관계없이 해제되어야 합니다. - initial process가 종료될 때까지 Pintos가 종료되지 않도록 보장해야 합니다.
- 제공된 Pintos 코드는 main() (threads/init.c)에서 process_wait() (userprog/process.c에 있는)를 호출하여 이를 수행하려고 시도합니다.
- 구현 방법: process_wait()를 함수 맨 위에 있는 주석에 따라 구현한 다음, process_wait()을 기반으로 wait 시스템 호출을 구현하는 것을 제안합니다.
3-2. wait 구현하기
1) syscall_handler()
- wait를 호출하는 case문을 추가한다.
/* userprog/syscall.c */
void syscall_handler(struct intr_frame *f UNUSED)
{
...
case SYS_WAIT:
f->R.rax = wait(f->R.rdi);
break;
}
2) wait()
- process_wait 함수를 호출한다.
/* userprog/syscall.c */
int wait(int pid); // 선언
int wait(int pid)
{
return process_wait(pid);
}
3) process_wait()
- 과제 Docs에서 process_wait()를 주석에 따라 구현하고 활용하라고 안내하고 있으니, 이 함수를 완성해서 활용하자!
process_wait 주석 내용
/* Waits for thread TID to die and returns its exit status. If
* it was terminated by the kernel (i.e. killed due to an
* exception), returns -1. If TID is invalid or if it was not a
* child of the calling process, or if process_wait() has already
* been successfully called for the given TID, returns -1
* immediately, without waiting.
*
* This function will be implemented in problem 2-2. For now, it
* does nothing. */
- process_wait 함수는 스레드 식별자 **TID가 종료될 때까지 기다리고, exit status를 반환**합니다.
- 스레드가 커널에 의해 종료되었을 경우(즉, 예외로 인해 종료된 경우) -1을 반환합니다.
- TID가 유효하지 않거나 호출하는 프로세스의 자식 스레드가 아니거나,
이미 해당 TID에 대해 process_wait()가 호출되었다면 즉시 -1을 반환합니다.
process_wait
1) get_child_process 함수를 만들어서 사용한다. 인자로 받은 tid를 갖는 자식이 없는 경우에는 -1을 반환하고 종료한다.
2) 찾은 자식이 sema_up 해줄때까지 (종료될 때까지) 대기한다.
3) 자식에게서 종료 signal이 도착하면 자식 리스트에서 해당 자식을 제거한다.
4) 자식이 완전히 종료되어도 괜찮은지 대기하고 있으므로, sema_up으로 signal을 보내 완전히 종료되게 해주고,
5) 자식의 exit_status를 반환하고 함수를 종료한다.
int process_wait(tid_t child_tid UNUSED)
{
struct thread *child = get_child_process(child_tid);
if (child == NULL) // 1) 자식이 아니면 -1을 반환한다.
return -1;
// 2) 자식이 종료될 때까지 대기한다. (process_exit에서 자식이 종료될 때 sema_up 해줄 것이다.)
sema_down(&child->wait_sema);
// 3) 자식이 종료됨을 알리는 `wait_sema` signal을 받으면 현재 스레드(부모)의 자식 리스트에서 제거한다.
list_remove(&child->child_elem);
// 4) 자식이 완전히 종료되고 스케줄링이 이어질 수 있도록 자식에게 signal을 보낸다.
sema_up(&child->exit_sema);
return child->exit_status; // 5) 자식의 exit_status를 반환한다.
}
struct thread
- 스레드 구조체에 대기를 위한 필드 exit_sema와 wait_sema를 추가한다.
/* threads/thread.h */
struct thread
{
...
struct file **fdt;
int next_fd;
struct intr_frame parent_if;
struct list child_list;
struct list_elem child_elem;
struct semaphore load_sema;
struct semaphore exit_sema; // 추가
struct semaphore wait_sema; // 추가
struct file *running; // 현재 실행중인 파일
...
}
init_thread
- exit_sema와 wait_sema를 초기화한다.
/* threads/thread.c */
static void
init_thread(struct thread *t, const char *name, int priority)
{
...
sema_init(&t->exit_sema, 0);
sema_init(&t->wait_sema, 0);
}
4) process_exit()
process_exit
1) FDT의 모든 파일을 닫고 메모리도 반환한다.
2) 현재 실행 중인 파일도 닫는다.
3) 자식이 종료되기를 기다리고 있는 (wait) 부모에게 sema_up으로 signal을 보낸다.
4) 부모가 wait을 마무리하고 나서 signal을 보내줄 때까지 대기한다.
- 이 대기가 풀리고 나면 스케줄링이 이어진다.
- thread_exit 함수에서 process_exit 이후에 do_schedule(THREADY_DYING)이 진행되는 것을 볼 수 있음
/* Exit the process. This function is called by thread_exit (). */
void process_exit(void)
{
struct thread *cur = thread_current();
// 1) FDT의 모든 파일을 닫고 메모리를 반환한다.
for (int i = 2; i < FDT_COUNT_LIMIT; i++)
close(i);
palloc_free_page(cur->fdt);
file_close(cur->running); // 2) 현재 실행 중인 파일도 닫는다.
process_cleanup();
// 3) 자식이 종료될 때까지 대기하고 있는 부모에게 signal을 보낸다.
sema_up(&cur->wait_sema);
// 4) 부모의 signal을 기다린다. 대기가 풀리고 나서 do_schedule(THREAD_DYING)이 이어져 다른 스레드가 실행된다.
sema_down(&cur->exit_sema);
}
'Computer Science' 카테고리의 다른 글
[Pintos-KAIST] Project 2 :: System Calls - 1 (System Calls이 호출되는 과정) (0) | 2023.05.09 |
---|---|
[Pintos-KAIST] Project 1 :: Priority Donation (0) | 2023.04.26 |
[Pintos-KAIST] Project 1 :: Priority Scheduling (0) | 2023.04.25 |