[Project 2] User Program (2)

혀누·2022년 1월 11일
0

Pintos

목록 보기
8/11
post-thumbnail

fork 는 thread safe 하지 않다. 하지만 여전히 강력하다.

Remind.

지난번 포스트에 이어서 Project2 User Program에 대해 알아보자.

직전까지 했던게 OS가 사용자 프로그램을 어떻게 실행시키는지 구체적인 함수 스택영역에서 확인해보았다면 오늘은 그렇게 나눠놓은 이유중 하나인 'System Call' 중에서도 fork를 중점으로 알아볼것이다.

What happens during calling 'Fork'?

User가 fork 라는 system call 을 부르면 제일먼저 user/syscall.c에 있는 fork함수가 호출된다.

이 함수는 다시 그 파일의 위에 있는 static inline int64_t syscall을 부르게 된다.

여기서는 레지스터에 주어진 인자에 맞추어 집어넣은 다음 syscall_entry를 호출하게 된다.

이 과정에서 중요한 것은 아직까지는 User space에서 일어나고 있다는 것이다.

그러다가 syscall_entry에서 "movq 4(%r12), %rsp" 가 불러지면 User space를 가르키고 있던 rsp가 해당 User process를 만들어낸 kernel thread를 가르키게 바뀌게 되며, 비로소 kernel space로 오게되었다.

여기서 중요하게 사용되는 것이 'tss' 인데 추후 설명하도록 하겠다.

syscall entry에서 interrupt frame정보를 채운 후 "call syscall_handler" 를 통해 userprog/syscall.c 에 있는 syscall_handler함수로 넘어간다. 이후 User syscall에서 넘긴 rax와 rdi에 맞추어 어떤 시스템콜을 불렀는지 switch로 확인하고 이후 fork로 넘어간다. 그리고 fork case에서는 process_fork를 호출한다.

process_fork 가 본격적으로 fork를 수행하는 코드이기 때문에 한번 자세히 보도록 하자.

tid_t
process_fork (const char *name, struct intr_frame *if_ UNUSED) {
	/* Clone current thread to new thread.*/
	struct thread *cur = thread_current();
	memcpy(&cur->parent_if, if_, sizeof(struct intr_frame));
	tid_t tid = thread_create(name, PRI_DEFAULT, __do_fork, cur);
	if (tid == TID_ERROR) {
		return TID_ERROR;
	}
	struct thread *child = get_child_with_pid(tid);
	sema_down(&child->fork_sema); // 자식이 로드 될때까지 기다림.
	if (child->exit_status == -1) {
		return TID_ERROR;
	}
	return tid;
}

수행하는 함수중 thread_create가 바로 child process 를 실행하는 kernel thread를 만드는 함수이다.

그렇다면 이렇게 만들어진 child process 를 실행하는 kernel thread에서는 어떤 일이 일어날까?

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;
	bool succ = true;
	parent_if = &parent->parent_if;

	/* 1. Read the cpu context to local stack. */
	memcpy (&if_, parent_if, sizeof (struct intr_frame));
	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 (&current->spt);
	if (!supplemental_page_table_copy (&current->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.*/

	if (parent->fd_idx == FDCOUNT_LIMIT)
		goto error;


	for (int i = 0; i < FDCOUNT_LIMIT; i++) {
		struct file *file = parent->fd_table[i];
		if (file == NULL)
			continue;
		
		bool found = false;
		if (!found) {
			struct file *new_file;
			if (file > 2) {
				new_file = file_duplicate(file);
			} else {
				new_file = file;
			}
			current->fd_table[i] = new_file;
		}
	}
	current->fd_idx = parent->fd_idx;
	// child loaded successfully, wake up parent in process_fork
	sema_up(&current->fork_sema);

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

do_fork 가 하는 일의 목적은 분명하다. 부모 프로세스를 그대로 실행해야 하기때문에,
1. 부모의 파일정보와 동일한 파일들을 메모리에 올려야하고
2. 해당 파일들에 접근할 수 있도록 부모의 pml4 table을 복사해야하며
3. 부모 프로세스에서 어디까지 진행했었는지 instruction 정보들도 알아야 한다.

이모든 과정들을 거치는 와중에 한가지 짚고 넘어갔으면 좋을 부분은 바로 'tss'다.

tss 는 x86-64 pintos에서 사용하는 장치로 User 가 system call을 호출할시에 kernel 영역의 어디로 rsp를 옮겨야할지를 알려주는 녀석이라고 생각하면 좋다.

앞선 포스트에서 언급했듯이 각 User process는 자신의 머리에 실제 물리 메모리와 1:1 mapping된 kernel영역을 이고있기 때문에 여러 User process의 kernel thread가 존재할수 있다. 그렇다면 그중에서도 User process는 자신을 실행시킨 kernel thread에 접근해야 한다.

이때 thread_create 단계에서 tss update를 통해 tss 라는 레지스터 정보를 해당 kernel thread의 특정 지점을 가리키게 만들어놓고 추후에 system call 이 불렸을때, 이를 바로 rsp에 갈아끼우면 User space에서 Kernel space로 옮겨올 수 있게 된다.

이러한 tss update의 과정은 위의 fork 과정에서는 do_fork안에 있는 process_activate에서 일어나게 되며 이때 tss update의 결과로 새롭게 실행되는 child user process의 system call은 child kernel thread가 handling 할 수 있게 된다.

참고로 tss 의 구조체는 다음과 같다.

struct task_state {
	uint32_t res1;
	uint64_t rsp0;
	uint64_t rsp1;
	uint64_t rsp2;
	uint64_t res2;
	uint64_t ist1;
	uint64_t ist2;
	uint64_t ist3;
	uint64_t ist4;
	uint64_t ist5;
	uint64_t ist6;
	uint64_t ist7;
	uint64_t res3;
	uint16_t res4;
	uint16_t iomb;
}__attribute__ ((packed));

위의 과정들을 거쳐 메모리에 적재가 끝나면 "sema-down"에서 기다리고있는 parent kernel thread를 깨우기 위해 child kernel thread 는 "sema-up"을 호출한다. 그리고 최종적으로 부모의 intr-frame정보를 복사하고 child의 메모리 적재한 정보를 담은 "if-" 를 "do-iret" 함으로써 child process를 실행하게 된다.

profile
개발자(물리)

0개의 댓글