정글사관학교 개발일지/운영체제-PintOS

PintOS Project 2 - User Program (8) System Call(정글사관학교 71일차 TIL) - 프로세스 관련 system call 구현

Woonys 2022. 1. 10. 00:54
반응형

대망의 마지막 시스템 콜 파트.. 하지만 구현이 전부가 아니다. 구현 100점 하면 뭐하나. 이해 못하면 말짱 꽝이다. 어차피 정답은 검색하면 다 나온다. 내가 해야 할 것은 이게 왜 맞고 틀린지, 뭐가 좋고 안 좋은지, 대체 이 흐름이 어떻게 진행되고 있는 건지를 이해하는 것.

 

근-본을 이해하지 못하면 백날 구현해봤자 쓸데없다. 면접 가서 "구현 다했습니다!"라고 하면 "와..구현을 다 하셨다니 대단해요!"라고 할 줄 아나. "그래서 뭘 이해했죠? 뭘 배웠죠? 그걸 왜 썼죠? 왜 쓰면 안되죠? 더 나은 방법은 없나요? 예를 들면 이런 상황에서는 그걸 쓸 수 있나요?" 이런 거 물어본다. 구글링하다 보게 된 면접 후기 글에서 이런 말이 있더라.

 

예를 들어, Deadlock 해결방법에 대한 질문에 프로세스 kill을 한다고 대답하면, 프로세스 kill을 하면 안되는 가상의 상황을 면접관님이 제시를 하셔서 이럴 땐 어떻게 할 것인지를 물어봅니다. 또 그것에 대해 답하면 다른 방법은 없는지, 그것이 적합한 방법인지에 대해서 주관적인 생각을 물어봅니다.

 

구현을 할 때도 마찬가지. 다른 사람 것을 참고하든 직접 구현하든 "뭔가를 구현했다"고 하면 1) 왜 이렇게 구현했는지 근거를 설명할 수 있어야 하고 2)이게 최선인지, 혹은 더 좋은 방법이 없는지 고민한 생각을 말할 수 있어야 한다. 공부를 하면서 착각했던 게, 파고드는 게 완전 아랫단(함수를 타고 들어간다거나 그러다 보면 어셈블리를 만나는 과정 등)까지 내려가는 게 deep하게 공부하는 것이라 생각했는데, 물론 이것도 나름 딥하다고 할 수 있지만 그보다는 좀더 꼬리에 꼬리를 무는 질문을 던지며 개념을 확장해나가는 것이 deep한 공부라고 해야겠다. 개념을 더 판다기보다 이게 왜 이런지를 다양한 방식으로 접근해보며 이해하는 것? 써놓고 보니 아직도 잘 모르는듯..공부하자 공부!

 

무튼 이 점을 계속 마음에 담으며 나머지를 진행해보자. 이제부터는 특히 프로세스 파트라 굉장히 중요하다. 

 

새삼 느끼는데 Gitbook에 중요한 내용들이 다 나와있다. 수시로 읽자. (링크: https://casys-kaist.github.io/pintos-kaist/)

 

Introduction · GitBook

No results matching ""

casys-kaist.github.io

 

 

fork() 구현

 

pid_t fork (const char *thread_name);
현재 프로세스의 복제본으로 새로운 프로세스를 생성하라.(해당 프로세스의 이름은 인자로 들어가는 THREAD_NAME).

%RBX, %RSP, %RBP,  %R12 - %R15(얘네들은 callee-saved register(피호출자 함수에 저장하는 레지스터)) 를 제외한 나머지 레지스터값은 복제할 필요가 없다. 반드시 자식 프로세스의 process id를 반환해라. 그렇지 않으면 유효한 pid가 아니다. 자식 프로세스에서, 리턴값은 반드시 0이어야 한다. 자식 프로세스는 반드시 부모 프로세스로부터 파일 디스크립터, 가상 메모리 공간 등을 포함한 자원을 복사해와야 한다.

부모 프로세스는 자식 프로세스가 성공적으로 복제된 것을 알고 나서 fork로부터 리턴해야 한다. 즉, 만약 자식 프로세스가 자원을 복제하는데 실패하면 부모 프로세스의 fork() 호출은 반드시 TID_ERROR를 반환해야 한다. 이 템플릿은 threads/mmu.c 내에 있는 pml4_for_each() 를 사용해 전체 user memory 공간을 복제하는데, 대응하는 페이지 테이블 구조체를 포함한다. 하지만 빈 부분(pte_for_each_func에서)을 채워야 한다.

 

fork() 함수는 부모 프로세스로부터 자식 프로세스를 생성하는 함수이다. process.c에 가면 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.*/

	return thread_create (name,
			PRI_DEFAULT, __do_fork, thread_current ());
}

process_fork()는 인자로 프로세스의 이름과 인터럽트 프레임 구조체를 받는다. 인터럽트 프레임은 인터럽트가 호출됐을 때 이전에 레지스터에 작업하던 context 정보를 스택에 담는 구조체이다. 부모 프로세스가 갖고 있던 레지스터 정보를 담아 고대로 복사해야 하기 때문이다.

 

반환 값으로 thread_create()를 실행하는데, 그 안에 인자로서  __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.

부모 프로세스의 실행 context를 복사하는 스레드 함수다.
힌트: parent->tf (부모 프로세스 구조체 내 인터럽트 프레임 멤버)는 프로세스의 userland context 정보를 들고 있지 않다.
즉, 당신은 process_fork()의 두번째 인자를 이 함수에 넘겨줘야만 한다.

1. Read the cpu context to local stack.

2. Duplicate PT

즉, __do_fork()는 부모 프로세스의 내용을 자식 프로세스로 복사하는 함수라고 할 수 있겠다.

 

위 글에서 parent->tf는 userland context를 들고 있지 않다고 했다. 그럼 얘는 어디로 간 걸까? syscall을 처리하는 루틴을 따라가보자.

 

1. 유저 프로그램에서 시스템 콜을 부르면 먼저 user/lib/syscall.c 내에 있는 시스템 콜 함수가 호출된다.

2. 시스템 콜 함수는 매크로인 syscall1 ~ syscall6을 통하여 인자의 개수를 7개로 고정한 뒤 syscall 함수를 부른다.

 

*syscall 함수

@/user/lib/syscall.c

__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;
}

3. 위 syscall 함수에서는 레지스터 값을 세팅해준 뒤 syscall instruction(위 코드 밑에서 6번째 줄 "syscall\n"에 해당 - 얘는 어셈블리어!)을 부른다.

4. syscall instruction은 userprog 디렉토리에 있는 syscall_init()를 실행하고, 그 안에서 syscall_entry()로 이어진다. syscall_entry는 syscall_entry.S를 실행한다. 이는 어셈블리어로 구성되어 있다. 여기서부터 사용자 프로그램이 아닌 시스템 콜 코드이다.

5. syscall_entry 함수가 어떻게 진행되고 있는지를 파악해보자.

  • rbx, r12 값을 임시로 저장해 두고 rsp 역시 rbx에 값을 옮겨둔다.
  • 스레드를 생성할 때 저장해 두었던 커널 스택 공간을 가리키는 rsp값(tss에 저장되어 있음)을 꺼내서 유저의 rsp에 덮어쓴다. 이때부터 우리는 커널 스택 공간으로 들어간다.
  • syscall_handler의 첫번째 인자 (%rdi)를 만들기 위해 rsp에 intr_handler 구조체에 들어가는 순서대로 레지스터 값들을 push해준다.
  • rsp 내용을 rdi로 옮겨준다. (movq %rsp, %rdi)
  • syscall_handler를 호출한다.

syscall_entry 함수 펼치기

더보기

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

 

즉, 유저 프로그램 실행 정보는 syscall_handler로 전달되는 intr_frame에 저장된다. 이를 __do_fork에 넘겨주는 방식. 따라서 우리가 구현해야 하는 시스템 콜 핸들러의 fork 함수에는 thread_name과 tf를 인자로 받아야 하며, 이때 전달되는 tf는 시스템 콜 핸들러로 넘어온 f에 정보가 들어있다.

 

자, 다시 한 번 정리해보자. 이게 무슨 말이냐! 전체 그림은 아래와 같다.

 

 

 

1. parent 스레드는 현재 실행 중인 스레드이다. 여기서 함수 foo()를 실행하고 그 안에서 fork()를 실행한다고 하자. 그럼 아래처럼 메모리가 형성될텐데, 이 중 커널 스택과 유저 스택으로 영역이 나뉜다. 여기서 user stack에 foo()와 fork()가 쌓여있다. 여기가 바로 userland context. 유저 프로세스 실행에 대한 맥락이 담겨있기 때문. 스택 포인터를 담고 있는 레지스터 rsp는 현재 user stack에서 fork() 부분을 가리킨다. 여기서 시스템 콜이 들어온 상황.

 

2. 아래 그림에서 유저 스택 부분의 fork() -> syscall!을 보자. 그러면 syscall_init() -> syscall-entry.S로 넘어가서 어셈블리 코드가 실행된다. 코드 흐름을 보면,

 

1. rbx 레지스터에 현재 rsp 레지스터에 담겨있던 유저 스택 포인터 주소값을 임시로 저장한다.

2. rsp 레지스터에 tss에 저장되어 있던 커널 스택 포인터(아래 그림에서 커널 스택을 가리키고 있는 tss->rsp0값) 주소값을 가져와 붙여넣기한다. 즉, 이때부터 rsp 레지스터에는 커널 스택 포인터 값이 들어있다.

3. 이 tf는 커널 스택 안에 있는 스레드 구조체의 멤버로 있으며, switching 시 맥락을 담는 인터럽트 프레임 구조체이다. 이 인터럽트 프레임 내 rsp 값은 이제 커널 스택 포인터를 가리킨다.

 

결론: tf 내 rsp는 커널 스택을 가리키고 있으니 userland context를 담고 있지 않다. 하지만 우리는 fork()를 할 때 user stack 정보 역시 가져와야 한다. (그래야 fork() 이후부터 자식 스레드를 실행할 수 있다. 맥락을 알지 못하면 맨 처음 main()부터 시작하게 될 것) 따라서 user stack 정보를 담는 인터럽트 프레임을 새로 만들어줘야 하는데, 이것이 바로 parent_if에 해당한다. 얘는 커널 스택 안에서 만들어진다.(tf는 커널 스택이 아닌 커널 스레드에 할당되는 페이지 가장 아래에 있는 struct thread 구조체의 멤버)

 

process_fork() 구현

 

위에서 설명한 것처럼, 부모 스레드는 현재 실행 중인 유저 스레드이다. 현재 시스템 콜로 인해 intr_frame 값을 바꾼 상태라 tf.rsp는 유저 stack에서 kernel stack으로 변경되었다. 스레드 이름과 부모 스레드를 이용하여 __do_fork()를 실행해야 하는데, 이를 위해서는 유저 스택의 정보를 인터럽트 프레임 안에 넣어서 넘겨줘야 한다. 이를 위해서는 부모 스레드에 parent_if라는 인터럽트 프레임 멤버를 하나 만들고 여기에 넘겨주는 방식으로 진행해야 한다. 따라서 thread 구조체에 parent_if 멤버를 추가하자.

 

시스템 콜을 호출한 부모 프로세스로부터 자식 프로세스를 생성할 텐데 이에 대해 자식 프로세스를 담아줄 리스트와 리스트 내에 들어갈 element가 필요하다. 이 역시 함께 스레드 내에 만들어준다.

 

또한, fork()를 진행하는 과정에서 세마포어가 등장한다. 왜일까? 부모 프로세스는 thread_create() 함수의 리턴 값으로 받은 tid를 갖고서 자식 프로세스를 찾는다. 이후, 해당 자식의 fork_sema를 sema_down() (자원을 먹음)한다. 이 과정은 자식 프로세스의 정상적인 로드를 위한 것으로, 자식 프로세스는 __do_fork() 를 통해 부모 프로세스 정보를 모두 복사한 뒤 sema_up()을 호출해 세마포어를 해제한다.

 

@/include/threads/thread.h

struct thread {

	/* --- Project2: User programs - system call --- */
	int exit_status; // _exit(), _wait() 구현 때 사용
	struct file **file_descriptor_table; //FDT
	int fdidx; // fd index
    
    struct list child_list; // _fork(), wait() 구현 때 사용
    struct list_elem child_elem; // _fork(), _wait() 구현 때 사용
    struct intr_frame parent_if; // _fork() 구현 때 사용, __do_fork() 함수
};

 

이어서 get_child() 함수를 하나 만들어준다. 이 함수는 pid에 해당하는 자식 스레드 구조체를 위에서 만들어 둔 child_list를 순회하며 찾은 뒤 해당 자식 스레드를 반환한다.

 

get_child()

 

/* --- Project 2: system call --- */
struct thread * get_child(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);
		if (t->tid == pid) {
			return t;
		}
	}
	return NULL;
}

 

자식 스레드를 생성하기 전에 parent_if에 대해 memcpy()를 이용해 인자로 받은 if_ (유저 스택)을 parent 스레드 내에 우리가 설정한 구조체 멤버인 parent_if에 붙여넣기 해준다. 이제 parent_if에는 유저 스택 정보가 담기게 된다.

 

이어서  thread_create()를 진행해 자식 스레드를 생성한다. 이후, 위의 get_child()를 통해 해당 p sema_fork 값이 1이 될 때까지(=자식 스레드 load가 완료될 때까지)를 기다렸다가 끝나면 pid를 반환한다.

 

tid_t
process_fork (const char *name, struct intr_frame *if_ UNUSED) {
	/* Clone current thread to new thread.*/

	/* --- Project 2: system call --- */
	struct thread *parent = thread_current();
	memcpy(&parent->parent_if, if_, sizeof(struct intr_frame)); // 부모 프로세스 메모리를 복사

	tid_t pid = thread_create(name, PRI_DEFAULT, __do_fork, parent); // 전달받은 thread_name으로 __do_fork()를 진행

	if (pid == TID_ERROR) {
		return TID_ERROR;
	}
	/* --- Project 2: system call --- */
	struct thread *child = get_child(pid);
	sema_down(&child->fork_sema);
	return pid;
}

 

__do_fork() 구현

 

1. page table 생성

 

위의 process_fork()에서 저장해뒀던 if_(여기서 if_는 우리가 위에서 만들어준 인터럽트 프레임 f에 해당)를 parent_if에 넣어줬다. 부모의 page table을 복제하기 위해 page table을 생성한다. 이를 위해 제공 함수인 duplicate_pte() 함수를 마저 구현해야 한다.

 

구현 전 duplicate_pte()

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. */

	/* 2. Resolve VA from the parent's page map level 4. */
	parent_page = pml4_get_page (parent->pml4, va);

	/* 3. TODO: Allocate new PAL_USER page for the child and set result to
	 *    TODO: NEWPAGE. */

	/* 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). */

	/* 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 true;
}

 

위에 주어진 번호 지시사항대로 따라가보자.

 

1. 부모의 page가 kernel page인 경우 즉시 false를 리턴한다.

더보기
/* 1. TODO: If the parent_page is kernel page, then return immediately. */
	if is_kernel_vaddr(va) {
		return false;
	}

 

2. 부모 스레드 내 멤버인 pml4를 이용해 부모 페이지를 불러온다. 이때, pml4_get_page() 함수를 이용한다.

더보기
/* 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. 새로운 PAL_USER 페이지를 할당하고 newpage에 저장한다.

더보기

 

/* 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. 부모 페이지를 복사해 3에서 새로 할당받은 페이지에 넣어준다. 이때 부모 페이지가 writable인지 아닌지 확인하기 위해 is_writable() 함수를 이용한다.

더보기
/* 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, 6. 페이지 생성에 실패하면 에러 핸들링이 동작하도록 false를 리턴한다.

더보기
/* 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;
	}

 

 

최종 코드

더보기
 
 
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;
	}
	/* 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;
}

 

2. file object 복제

자식 프로세스가 생성되면 부모 프로세스가 가진 것과 동일한 파일 디스크립터 테이블이 생성된다. 여기서 이해를 돕기 위해 운영체제에서 파일 공유하는 방법에 대해 잠깐 알아보자. (CSAPP 10.8장) 커널은 3개의 관련 자료구조를 사용해서 오픈한 파일을 표현한다.

 

1. file descriptor table: 각 프로세스마다 들고 있는 식별자 테이블. 여기 올라오는 리스트 원소(엔트리)는 프로세스에서 open한 파일 식별자로 인덱싱된다. 각 오픈 식별자 엔트리는 파일 테이블 내에서 한 개의 엔트리(파일)을 가리킨다. 즉, 서로 다른 파일에 대해 인덱스로 구분하여 descriptor table을 관리한다는 뜻.

2. file table: 열려있는 파일은 모든 프로세스들이 공유하는 한 개의 파일 테이블로 표시된다. 이는 우리가 계속 다뤄왔던 파일 디스크립터 테이블(FDT)와 별개로 모든 프로세스가 공유하는 하나의 테이블을 일컫는다. 즉, 이 컴퓨터 전체(=아무래도 운영체제겠지만)에서 관장하는

파일 테이블을 일컫는다. 여기서 각 프로세스별로 파일을 오픈할 때 refcnt(참조 횟수)를 1 증가시키고, 파일을 닫으면 refcnt를 1 감소시킨다. 모든 프로세스에서 동일한 파일을 다 닫으면 refcnt=0이 되고 파일 테이블 엔트리에서 지운다.

3. v-node table: 해당 파일 관련 정보를 담고 있는 테이블 정도로만 알고 있자. 이미지를 보자.

 

부모로부터 자식 프로세스가 fork()를 통해 생성되면 동일한 파일 디스크립터 테이블을 갖는다. 이때, 둘다 File A, B를 open해서 관리하고 있으니 두 테이블의 동일한 위치에서 File A, B를 참조할 것이다. 이는 하나의 open file table과 맵핑되어 있는데, 이 컴퓨터 전체에서 A와 B가 열려있는 횟수(=refcnt)는 각각 2이다.

 

그래서 하고 싶은 말이 무엇이냐, 자식 프로세스의 FDT는 부모의 FDT와 동일하게 해줘야 한다. 이를 위해 부모의 FDT 페이지 배열로부터 값을 하나씩 복사해서 붙여넣자. 여기서 fork()로 인해 refcnt는 1 증가할 것이다. 이 부분은 제공되는 함수 file_duplicate()를 쓰면 간단하다. stdin, stdout은 굳이 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.*/
    
    current->fd_table[0] = parent->fd_table[0];
	current->fd_table[1] = parent->fd_table[1];
    
	for (int i = 2; i < MAX_FD_NUM; i++) {
		struct file * f = parent->fd_table[i];
		if (f == NULL){
			continue;
		}
		current->fd_table[i] = file_duplicate(f);
	}

 

3. sema_up() 실행 및 마무리

 

fdt까지 복사를 완료하면 fork()가 잘 이뤄졌으니 sema_up()을 해준다. 이어서 fork 함수의 결과로 자식 프로세스는 0을 반환해야 하니 if_.R.rax 를 0으로 만들어준다. 

static void
__do_fork (void *aux) {
	
    (...)
    
	/* 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.*/

	if (parent->fdidx == FDCOUNT_LIMIT) {
		goto error;
	}
	current->file_descriptor_table[0] = parent->file_descriptor_table[0];
	current->file_descriptor_table[1] = parent->file_descriptor_table[1];

	for (int i = 2; i < FDCOUNT_LIMIT; i++) {
		struct file * f = parent->file_descriptor_table[i];
		if (f == NULL){
			continue;
		}
		current->file_descriptor_table[i] = file_duplicate(f);
	}
    
	current->fdidx = parent->fdidx;
	sema_up(&current->fork_sema);
	if_.R.rax = 0;
	process_init ();

	/* Finally, switch to the newly created process. */
	if (succ)
		do_iret (&if_);
error:
	current->exit_status = TID_ERROR;
	sema_up(&current->fork_sema);
	exit(TID_ERROR);
	//thread_exit ();
}

 

다음 글에서 exec(), wait() 구현하는 것으로 마무리.

 

참고 글: fork() & exec() 차이

반응형