[PintOS] Project2: System Calls - Ⅱ

김상호·2022년 6월 24일
1

Development Log

목록 보기
35/45

System Calls

과제 목표

구현

exec();
case SYS_EXEC:
	exec(f->R.rdi);
	break;
int exec (const 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) {
		exit(-1);
		return -1;
	}
	strlcpy(fn_copy, file_name, file_size);
	if (process_exec(fn_copy) == -1) {
		exit(-1);
		return -1;
	}
}
  • 인자로 받은 실행 파일을 실행시킨다. 현재 실행 중인 프로세스의 이미지를 이 실행 파일 프로세스의 이미지로 바꿔치기한다. 새로운 프로세스를 생성하는 것은 아니다. fork()가 자신의 복사본을 생성해 실행한다면, 자신의 복사본이 아닌 아예 다른 프로그램을 실행해야 하는 경우에 exec()을 사용한다.

  • file_name을 copy를 해야 하는가?

    • copy를 하지 않으면, process_exec()에서 process_cleanup()을 할 때 해당 file_name의 문자열의 물리적 메모리와의 매핑 정보를 담은 Page Table도 같이 지워지기 때문이다.
    • 여기서 strlcpy()하여 복사된 파일 이름 문자열은 커널 스택에 저장될 것이다. 따라서 process_cleanup() 후에도 커널 스택과 연결된 페이지 테이블을 통해 물리적 메모리와 매핑되어 활용될 수 있으므로, 애초에 fn_copy 문자열을 process_exec()의 인자로 넣어준다.
    • 실제로 파일 이름을 복사하지 않고 그대로 사용해서 process_cleanup() 후에 참조하려 시도하면 page fault가 뜬다.

wait();
case SYS_WAIT:
	f->R.rax = wait(f->R.rdi);
	break; 
int wait (tid_t pid) {
  	return process_wait(pid);
}
  • 자식 프로세스가 올바르게 종료됐는지 확인한 후, 모두 종료될 때까지 대기(SLEEP)한다. 자식 프로세스가 종료되면 자식 프로세스의 종료 상태를 반환한다.

int process_wait (tid_t child_tid UNUSED) {
	/* XXX: Hint) The pintos exit if process_wait (initd), we recommend you
	 * XXX:       to add infinite loop here before
	 * XXX:       implementing the process_wait. */

	struct thread *child = get_child_process(child_tid);

	if (child == NULL)
		return -1;

	sema_down(&child->sema_wait);
	int exit_status = child->exit_status;
	list_remove(&child->child_elem);
	sema_up(&child->sema_exit);
	return exit_status;
}
  • 기다려야 하는 자식 프로세스의 tid를 인자로 입력받는다.
  • 해당 자식 프로세스의 wait_sema를 DOWN해주고 BLOCK된다. 이제 자식 프로세스가 sema_up()으로 명시적으로 풀어주지 않는 이상 부모 프로세스는 계속 잠들어 있는다.
  • 자식 프로세스가 실행되어 작업을 한 후, 종료하기 직전에 부모 프로세스를 깨우고 자기 자신을 BLOCK한다. 이를 통해 부모 프로세스는 자식 프로세스한테서 필요로 하는 정보들을 얻어올 수 있다. 예를 들어 자식 프로세스의 exit_status 등.
  • 부모 프로세스는 자식 프로세스에게서 exit_status를 얻어온다. 그리고 자신의 child_list에서 자식 프로세스를 지운다.
  • 그 후 BLOCK되어있던 자식 프로세스를 깨워 제대로 종료될 수 있도록 한다.
  • 종료된 자식 프로세스의 exit_status를 반환한다.
void process_exit (void) {
    	
        •••
        
	sema_up(&curr->sema_wait);
	sema_down(&curr->sema_exit);

	palloc_free_page(table);
	process_cleanup ();
}
  • 위에 코드 처럼 process_exit()함수에 세마포어를 추가해 자식 프로세스가 종료되었을 때, 부모를 깨울수 있도록 다음과 같이 코드를 추가해준다.

fork();
case SYS_FORK:
	memcpy(&thread_current()->ptf, f, sizeof(struct intr_frame));
	f->R.rax = fork(f->R.rdi);
	break;
int fork (const char *thread_name) {
	check_address(thread_name);
	return process_fork(thread_name, &thread_current()->ptf);
}
  • 현재 프로세스를 복사한 새 자식 프로세스를 만든다. 자식 프로세스는 부모 프로세스와 다른 PID를 갖는다. fork()의 반환값은 부모 프로세스는 자식 프로세스의 PID, 자식 프로세스는 0을 반환받는다. fork() 후 부모와 자식 중 어느 프로세스가 먼저 실행되는지는 스케줄링 방식에 따라 다르다.
tid_t process_fork (const char *name, struct intr_frame *if_ UNUSED) {
	/* Clone current thread to new thread.*/
	struct thread *cur = thread_current();
	tid_t ctid = thread_create (name, PRI_DEFAULT, __do_fork, cur);
	if (ctid == TID_ERROR)
		return TID_ERROR;
	struct thread *child = get_child_process(ctid);
	sema_down(&cur->sema_fork);
	return ctid;
}
  • 현재 실행되고 있는 부모 프로세스를 자식 프로세스에게 복제한다 __do_fork(). 자식이 fork를 완료할 때까지 BLOCK해 있다가, 자식이 fork를 완료하면 새롭게 생성된 자식 프로세스의 pid를 반환한다.

  • 이 때 인자로 받는 if_는 시스템 콜 핸들러 함수에 인자로 들어가는, 시스템 콜을 부른 부모 프로세스의 인터럽트 프레임이다.

  • 어차피 부모 프로세스의 인터럽트 프레임이면 그냥 parent->tf를 하면 되지 뭐하러 parent->parent_ifif를 복사해서 쓰냐?

    • 인터럽트 프레임이 바뀌었을 것이다. 우리에게 필요한 건 시스템 콜을 부르기 전의 부모 프로세스의 레지스터 값들이고, 그게 인자로 받은 if_ = parent->parent_if이다.
    • 지금 부모 프로세스의 tf의 값은 시스템 콜을 부르면서 값이 바뀌어져 있을 것이다. 시스템 콜 엔트리에서 RSP의 값을 Ring0에 맞춰줬기 때문이다.
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->ptf;
	bool succ = true;

	/* 1. Read the cpu context to local stack. */
	memcpy (&if_, parent_if, sizeof (struct intr_frame));
  	if_.R.rax = 0;
	
        •••
        
  • 부모의 CPU 문맥, 즉 인터럽트 프레임 값을 복사하고 RAX에 0을 넣는다.
    • 자식 프로세스는 RAX의 값을 0으로, 부모 프로세스는 후에 return tid;로 RAX의 값을 자식의 PID로 설정한다.
        •••
	int cnt = 2;
	struct file **table = parent->fdt;
	while (cnt < 128) {
		if (table[cnt]) {
			current->fdt[cnt] = file_duplicate(table[cnt]);
		} else {
			current->fdt[cnt] = NULL;
		}
		cnt++;
	}
	current->next_fd = parent->next_fd;

 	sema_up(&parent->sema_fork);

  	process_init ();
	/* Finally, switch to the newly created process. */
	if (succ)
		do_iret (&if_);
error:
	sema_up(&parent->sema_fork);
	exit(TID_ERROR);
}  
  • 파일을 복사한다.

    • 부모 프로세스의 FDT를 하나하나 훑으면서 자신의 FDT로 파일을 복사해간다. fdt와 그에 매핑되는 파일도 모조리 똑같이 복사한다.
  • 자식이 fork가 끝날 때까지 잠들어있던 부모 프로세스를 깨운다.

  • if_의 값들을 모두 레지스터에 넣음으로서 자식 프로세스를 실행시킨다.

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);
	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;
}
  • 자식 프로세스는 부모 프로세스와 별도의 물리 메모리 공간을 가진다. 그 대신, 부모와 자식 프로세스의 가상 메모리 주소가 같으면 같은 물리 메모리 정보에 매핑된다.

PintOS Project2 GIthub 주소 PintOS

0개의 댓글