핀토스 2주차 후기 및 정리.
https://fluoridated-interest-674.notion.site/syscall-7d206019b8074f08b70673488a1e99a0
pintos가 부팅되고, 주어진 명령인자로 실행하게 됨.
처음에 run testName으로 파일이 실행되게 되고 파일이 실행되면 exec로 빠지게 된다. 그동안 main 스레드는 wait()를 하게 된다. 핀토스는 단일 스레드체계(?) 이고 wait는 자기가 생성한 자식 스레드에서만 한정되게 되어있다. (원래 자식만 기다리는지 확인 필요)
exec에서 받은 filename이 실행가능한 파일인지 아닌지 확인을 먼저함. 헤더를 읽어서 ELF 파일인지 먼저 확인을 하고 그다음에 phdr 을 읽어 정보를 확인한다.
/* Create a minimal stack by mapping a zeroed page at the USER_STACK */
static bool
setup_stack(struct intr_frame *if_)
{
uint8_t *kpage;
bool success = false;
kpage = palloc_get_page(PAL_USER | PAL_ZERO);
if (kpage != NULL)
{
success = install_page(((uint8_t *)USER_STACK) - PGSIZE, kpage, true);
if (success)
if_->rsp = USER_STACK;
else
palloc_free_page(kpage);
}
return success;
}
그런 다음 rsp(스택포인터)fmf USER_STACK 으로 맞춘다. rip를 설정하고, 실행파일을 포인터로 스레드의 구조체에 저장한다. ( 처음에는 리스트로 관리를 했지만, 실행파일은 한가지만 유지될 수있고, 파일을 새로 실행하게 된다면, process_clean 작업에서 모두 날아가게 된다. 그런다음 명령어로 받은 인자들을 스택에 넣어주고, 파일에서 실행될 인자를 rdi 과 rsi에 포인터를 담아준다. 그럼 스레드는 do_iret으로 가 컨텍스트 스위칭을 하게 된다.
/* Use iretq to launch the thread */
void do_iret(struct intr_frame *tf)
{
__asm __volatile(
"movq %0, %%rsp\n"
"movq 0(%%rsp),%%r15\n"
"movq 8(%%rsp),%%r14\n"
"movq 16(%%rsp),%%r13\n"
"movq 24(%%rsp),%%r12\n"
"movq 32(%%rsp),%%r11\n"
"movq 40(%%rsp),%%r10\n"
"movq 48(%%rsp),%%r9\n"
"movq 56(%%rsp),%%r8\n"
"movq 64(%%rsp),%%rsi\n"
"movq 72(%%rsp),%%rdi\n"
"movq 80(%%rsp),%%rbp\n"
"movq 88(%%rsp),%%rdx\n"
"movq 96(%%rsp),%%rcx\n"
"movq 104(%%rsp),%%rbx\n"
"movq 112(%%rsp),%%rax\n"
"addq $120,%%rsp\n"
"movw 8(%%rsp),%%ds\n"
"movw (%%rsp),%%es\n"
"addq $32, %%rsp\n"
"iretq"
:
: "g"((uint64_t)tf)
: "memory");
}
컨텍스트 스위칭을 하며 프로그램이 시작되는 부분으로 Rsp가 점프를 하게된다.프로그램이 실행되면서 필요한 syscall들을 호출하게 된다.
시스템 콜을 호출하게 되면,
bool
create (const char *file, unsigned initial_size) {
return syscall2 (SYS_CREATE, file, initial_size);
}
이런식으로, 호출에 맞는 syscall로 변환된다.
__attribute__((always_inline))
static __inline int64_t syscall (uint64_t num_, uint64_t a1_, uint64_t a2_,
uint64_t a3_, uint64_t a4_, uint64_t a5_, uint64_t a6_) {
int64_t ret;
register uint64_t *num asm ("rax") = (uint64_t *) num_;
register uint64_t *a1 asm ("rdi") = (uint64_t *) a1_;
register uint64_t *a2 asm ("rsi") = (uint64_t *) a2_;
register uint64_t *a3 asm ("rdx") = (uint64_t *) a3_;
register uint64_t *a4 asm ("r10") = (uint64_t *) a4_;
register uint64_t *a5 asm ("r8") = (uint64_t *) a5_;
register uint64_t *a6 asm ("r9") = (uint64_t *) a6_;
__asm __volatile(
"mov %1, %%rax\n"
"mov %2, %%rdi\n"
"mov %3, %%rsi\n"
"mov %4, %%rdx\n"
"mov %5, %%r10\n"
"mov %6, %%r8\n"
"mov %7, %%r9\n"
"syscall\n"
: "=a" (ret)
: "g" (num), "g" (a1), "g" (a2), "g" (a3), "g" (a4), "g" (a5), "g" (a6)
: "cc", "memory");
return ret;
}
알맞는 시스템콜로 변환되어 레지스터에 넣은 값들이 차례대로 위에서부터 rax, rdi, rsi, rdx … 에 들어가게 된다.
그럼 pintos 기준 syscall-entry파일 로 넘어가게 된다.
#include "threads/loader.h"
.text
.globl syscall_entry
.type syscall_entry, @function
syscall_entry:
movq %rbx, temp1(%rip)
movq %r12, temp2(%rip) /* callee saved registers */
movq %rsp, %rbx /* Store userland rsp */
movabs $tss, %r12
movq (%r12), %r12
movq 4(%r12), %rsp /* Read ring0 rsp from the tss */
/* Now we are in the kernel stack */
push $(SEL_UDSEG) /* if->ss */
push %rbx /* if->rsp */
push %r11 /* if->eflags */
push $(SEL_UCSEG) /* if->cs */
push %rcx /* if->rip */
subq $16, %rsp /* skip error_code, vec_no */
push $(SEL_UDSEG) /* if->ds */
push $(SEL_UDSEG) /* if->es */
push %rax
movq temp1(%rip), %rbx
push %rbx
pushq $0
push %rdx
push %rbp
push %rdi
push %rsi
push %r8
push %r9
push %r10
pushq $0 /* skip r11 */
movq temp2(%rip), %r12
push %r12
push %r13
push %r14
push %r15
movq %rsp, %rdi
check_intr:
btsq $9, %r11 /* Check whether we recover the interrupt */
jnb no_sti
sti /* restore interrupt */
no_sti:
movabs $syscall_handler, %r12
call *%r12
popq %r15
popq %r14
popq %r13
popq %r12
popq %r11
popq %r10
popq %r9
popq %r8
popq %rsi
popq %rdi
popq %rbp
popq %rdx
popq %rcx
popq %rbx
popq %rax
addq $32, %rsp
popq %rcx /* if->rip */
addq $8, %rsp
popq %r11 /* if->eflags */
popq %rsp /* if->rsp */
sysretq
.section .data
.globl temp1
temp1:
.quad 0
.globl temp2
temp2:
.quad 0
CPU 레지스터 값들을 커널 스택에 push
: push 되는 순서는 마지막 들어간 데이터부터 interrupt frame 구조체 순서가 되도록 하는 순서로 push💡 이때 SEL_UDSEG는 유저 데이터 세그먼트, SEL_UCSEG는 유저 코드 세그먼트를 의미한다.
커널 스택에 인터럽트 프레임 구조체 형태의 데이터가 다 들어가 있고, rsp는 이 시작점을 가리키는 형태
→ syscall_handler() 호출 시 인자로 rsp를 넘겨줌 (핸들러는 *if 를 받았다고 생각할 수 있음)
/* syscall-entry.S 부분 */
movabs $syscall_handler, %r12
call *%r12
syscall_handler (struct intr_frame *f):
인자로 들어온 f는 커널스택의 rsp.
void syscall_halt(void){
power_off();
}
void syscall_exit(struct intr_frame *f){
thread_current()->exit_code = f->R.rdi;
thread_exit();
}
// 자식 스레드의 상태를 저장하는 구조체! 자식을 한번 기다렸거나, 자기자신(부모)가 종료될때 모두 삭제해주어야 한다.
struct child_info
{
uint32_t tid;
int exit_code;
struct list_elem elem;
};
//* eg) while 돌면서 fd_list 삭제. 순환하면서 삭제하는 부분은 모두 이렇게 구현했다.
// 하지만 remove를 쓰지않고 pop을 해서 쓰면 더욱 편하다. 코드 주석에도 그 방법을 추천함.
void clear_fd_list()
{
struct list *fd_list;
struct fd *delete_fd;
fd_list = &thread_current()->fd_list;
if (list_empty(fd_list))
return;
struct list_elem *cur;
cur = list_begin(fd_list);
while (cur != list_end(fd_list))
{
delete_fd = list_entry(cur, struct fd, elem);
file_lock_acquire();
file_close(delete_fd->file);
file_lock_release();
cur = list_remove(&delete_fd->elem);
free(delete_fd);
}
}
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);
pid_t syscall_fork (struct intr_frame *f){
char * thread_name = f->R.rdi;
int return_value;
return_value = process_fork(thread_name, f);
f->R.rax = return_value;
}
피호출자(callee) 저장 레지스터인 %RBX, %RSP, %RBP와 %R12 - %R15를 제외한 레지스터 값을 복제할 필요가 없습니다. 자식 프로세스의 pid를 반환해야 합니다. 그렇지 않으면 유효한 pid가 아닐 수 있습니다. 자식 프로세스에서 반환 값은 0이어야 합니다. 자식 프로세스에는 파일 식별자 및 가상 메모리 공간을 포함한 복제된 리소스가 있어야 합니다. 부모 프로세스는 자식 프로세스가 성공적으로 복제되었는지 여부를 알 때까지 fork에서 반환해서는 안 됩니다. 즉, 자식 프로세스가 리소스를 복제하지 못하면 부모의 fork() 호출이 TID_ERROR를 반환할 것입니다.
템플릿은 threads/mmu.c의 pml4_for_each를 사용하여 해당되는 페이지 테이블 구조를 포함한 전체 사용자 메모리 공간을 복사하지만, 전달된 pte_for_each_func의 누락된 부분을 채워야 합니다.
(가상 주소) 참조).
/* 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_)
{
// /* Clone current thread to new thread.*/
struct fork_info *fork_info = (struct fork_info *)malloc(sizeof(struct fork_info));
fork_info->parent_t = thread_current();
fork_info->if_ = if_;
// 포크 하기 전에 스택정보(_if)를 미리 복사 떠놓는 중. 포크로 생긴 자식에게 전해주려고
tid_t pid = thread_create(name, PRI_DEFAULT, __do_fork, fork_info);
process_fork_sema_down();
if (!thread_current()->make_child_success)
{
return -1;
}
return pid;
}
/* A thread function that copies parent's execution context.
fork할 때 부모프로세스의 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. */
static void
__do_fork(void *aux)
{
struct intr_frame if_;
struct fork_info *fork_info = (struct fork_info *)aux;
struct thread *parent = fork_info->parent_t;
struct thread *current = thread_current();
/* TODO: somehow pass the parent_if. (i.e. process_fork()'s if_) */
struct intr_frame *syscall_if;
syscall_if = fork_info->if_;
bool succ = true;
/* 1. Read the cpu context to local stack. */
memcpy(&if_, syscall_if, sizeof(struct intr_frame));
/* 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, fork_info))
{
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.*/
copy_fd_list(parent, current);
process_init();
/* Finally, switch to the newly created process. */
if (succ)
{
parent->make_child_success = true;
free(fork_info);
if_.R.rax = 0;
process_fork_sema_up();
thread_yield();
do_iret(&if_);
}
error:
parent->make_child_success = false;
free(fork_info);
thread_current()->exit_code = -1;
del_child_info();
process_fork_sema_up();
thread_exit();
}
/* Duplicate the parent's address space by passing this function to the
* pml4_for_each. This is only for the project 2. */
static bool
duplicate_pte(uint64_t *pte, void *va, void *aux)
{
struct thread *current = thread_current();
struct fork_info *fork_info = (struct fork_info *)aux;
struct thread *parent = fork_info->parent_t;
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.
pml4_get_page는 "물리주소를 찾는 함수"임. 누구의 물리주소를 찾냐면? 유저영역 쪽에 있는 가상주소 va(부모스레드)의 물리주소를 찾는 것.
pml4_get_page가 리턴하는 거는 "커널주소"를 리턴한다. 어떤 커널주소를 리턴하냐면? 찾은 물리주소와 연결된 커널의 주소를 리턴함.
즉 va의 물리주소를 찾아서 그 물리주소와 연결 되어있는 커널주소를 반환하는 함수임. if)물리주소가 매핑 안되어있으면 NULL반환 */
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 NEWPAGE.*/
newpage = palloc_get_page(PAL_USER | PAL_ZERO);
if (newpage == NULL)
{
palloc_free_page(newpage);
return false;
}
/*페이지를 할당 받을 건데 PAL_USER플레그를 줌으로써 유저가 쓸 수 있는 메모리 pool에서 페이지를 가져올거고
PAL_ZERO를 씀으로써 할당받은 페이지 메모리를 0으로 초기화 할 거임.
https://casys-kaist.github.io/pintos-kaist/appendix/memory_allocation.html 참고*/
/* 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. */
palloc_free_page(newpage);
return false;
}
return true;
}
#endif
// exec func parameter : const char *cmd_line
// int syscall_exec (const char *cmd_line){
int syscall_exec (struct intr_frame *f){
//! 지금까지 exec 안되던 이유 = palloc_get_page함수 호출하면서 palloc.h 파일 include 안해서..
//! 저부터 머리 박습니다.. - yj
char *file_name = f->R.rdi;
char *fn_copy ;
check_addr(file_name);
fn_copy = palloc_get_page (0);
if (fn_copy == NULL)
{
syscall_abnormal_exit(EXIT_CODE_ERROR);
palloc_free_page(fn_copy);
f->R.rax = -1;
return -1;
}
strlcpy (fn_copy, file_name, PGSIZE); // filename을 fn_copy로 복사
if (process_exec (fn_copy) < 0) {
palloc_free_page(fn_copy);
f->R.rax = -1;
syscall_abnormal_exit(-1);
}
}
// wait func parameter : pid_t pid
int syscall_wait (struct intr_frame *f){
int pid = f->R.rdi;
struct child_info * child_info = search_children_list(pid);
if(child_info == NULL){
f->R.rax = -1;
return -1;
}
int return_value;
if(child_info->exit_code == EXIT_CODE_DEFAULT){
return_value = process_wait(pid);
f->R.rax = return_value;
}else{
return_value = child_info->exit_code;
f->R.rax = return_value;
}
list_remove(&child_info->elem);
free(child_info);
return return_value;
}
만약 pid (자식 프로세스)가 exit() 함수를 호출하지 않고 커널에 의해서 종료된다면 (e.g exception에 의해서 죽는 경우), wait(pid) 는 -1을 반환해야 합니다.
부모 프로세스가 wait 함수를 호출한 시점에서 이미 종료되어버린 자식 프로세스를 기다리도록 하는 것은 완전히 합당합니다만, 커널은 부모 프로세스에게 자식의 종료 상태를 알려주든지, 커널에 의해 종료되었다는 사실을 알려주든지 해야 합니다.
자식들은 상속되지 않는다는 점을 알아두세요 : 만약 A 가 자식 B를 낳고 B가 자식 프로세스 C를 낳는다면, A는 C를 기다릴 수 없습니다. 심지어 B가 죽은 경우에도요. 프로세스 A가 wait(C) 호출하는 것은 실패해야 합니다.
마찬가지로, 부모 프로세스가 먼저 종료되버리는 고아 프로세스들도 새로운 부모에게 할당되지 않습니다.
wait 을 호출한 프로세스는 이미 pid 에서 wait 을 호출한 상태입니다. 즉, 프로세스는 최대 한 번 주어진 자식 프로세스를 기다려야합니다.
프로세스들은 자식을 얼마든지 낳을 수 있고 그 자식들을 어떤 순서로도 기다릴 (wait) 수 있습니다. 자식 몇개로부터의 신호는 기다리지 않고도 종료될 수 있습니다. (전부를 기다리지 않기도 합니다.)
여러분의 설계는 발생할 수 있는 기다림의 모든 경우를 고려해야합니다. 한 프로세스의 (그 프로세스의 struct thread 를 포함한) 자원들은 꼭 할당 해제되어야 합니다.
부모가 그 프로세스를 기다리든 아니든, 자식이 부모보다 먼저 종료되든 나중에 종료되든 상관없이 이뤄져야 합니다.
최초의 process가 종료되기 전에 Pintos가 종료되지 않도록 하십시오.
제공된 Pintos 코드는 main() (in threads/init.c)에서 process_wait() (in userprog/process.c ) 를 호출하여 Pintos가 최초의 process 보다 먼저 종료되는 것을 막으려고 시도합니다.
여러분은 함수 설명의 제일 위의 코멘트를 따라서 process_wait() 를 구현하고 process_wait() 의 방식으로 wait system call을 구현해야 할 겁니다.
/* 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. */
int process_wait(tid_t child_tid)
{
// 이 함수가 호출되는 init.c의 main 함수를 보면, process_wait() 다음 thread_exit으로 쓰레드를 종료시킴.
// 따라서 이 함수가 child_tid가 종료되기를 기다리는 동안 무한루프를 써서 기다리게 한다.
/* The pintos exit if process_wait (initd),
we recommend you to add infinite loop here before implementing the process_wait. */
struct list *child_list = &thread_current()->children_list;
struct semaphore *sema;
sema = &thread_current()->wait_sema;
while ((search_children_list(child_tid))->exit_code == EXIT_CODE_DEFAULT)
{
wait_sema_down(sema);
}
return search_children_list(child_tid)->exit_code;
}
아래 시스템콜들은 4주차에 제대로 구현할꺼기 때문에 복기를 위한 코드는 생략.