pintos-kaist 프로젝트 : System call (3)

Leesoft·2023년 3월 3일
0

학교 과제 : PintOS

목록 보기
3/6
post-thumbnail

이 포스팅은 제가 친구와 PintOS 과제를 하면서 떠올린 생각이나 삽질을 하는 과정을 의식의 흐름대로 적은 글이며 글을 작성한 이후 원래 코드에서 일부 오타나 버그가 수정되었을 수 있습니다. 즉 이 포스팅은 정답을 알려주는 포스팅이 아님을 밝힙니다.

exec()

exec()은 일단 겉보기에는...? 짤 게 많이 없어 보인다. 나중에 울면서 고칠지도 모르지만... 스타트는 역시 process.c로 옮기는 건가?

int _exec(const char *file)
{
	if (process_exec((void *)file) < 0)
		return -1;
	NOT_REACHED();
}

/* Switch the current execution context to the f_name.
 * Returns -1 on fail. */
int process_exec(void *f_name)
{
	char *file_name = f_name;
	bool success;

	/* We cannot use the intr_frame in the thread structure.
	 * This is because when current thread rescheduled,
	 * it stores the execution information to the member. */
	struct intr_frame _if;
	_if.ds = _if.es = _if.ss = SEL_UDSEG;
	_if.cs = SEL_UCSEG;
	_if.eflags = FLAG_IF | FLAG_MBS;

	/* We first kill the current context */
	process_cleanup();

	/* And then load the binary */
	success = load(file_name, &_if);

	/* If load failed, quit. */
	palloc_free_page(file_name);
	if (!success)
	{
		thread_exit(); /* IMPLEMENTED IN PROJECT 2-3. */
		return -1;
	}

	/* Start switched process. */
	do_iret(&_if);
	NOT_REACHED();
}

이미 fork()가 완료된 프로세스의 내용을 비우고 새로운 프로그램을 올리는 것 같은데 그 코드 역시 있어서, 실패하면 스레드를 종료하기만 하면 될 것 같다.

wait()

이제 제일 길고 어려운 wait()를 짜 보자! 지금까지는 무한 루프를 넣어 놓은 상태이다.

int _wait(pid_t pid)
{
	return process_wait(pid);
}

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

	/* IMPLEMENTED IN PROJECT 2-3. */
	while (1)
	{
	}

	return -1;
}

wait()는 특정 pid를 가진 자식 프로세스가 종료될 때까지 기다리고, 자식 프로세스가 종료될 때의 exit code를 받는다. 일단, wait()가 실패할 조건부터 살펴보자.

pid does not refer to a direct child of the calling process. pid is a direct child of the calling process if and only if the calling process received pid as a return value from a successful call to fork. Note that children are not inherited: if A spawns child B and B spawns child process C, then A cannot wait for C, even if B is dead. A call to wait(C) by process A must fail. Similarly, orphaned processes are not assigned to a new parent if their parent process exits before they do.

즉, 우리가 짠 코드에서는 child_threads 리스트 안에 있는 프로세스만 wait()를 할 수 있으니, 여기에서 주어진 pid를 갖는 자식 프로세스가 없을 때 바로 리턴하는 부분을 구현하자.

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

	/* IMPLEMENTED IN PROJECT 2-3. */
	// while (1)
	// {
	// }

	/* If no child with pid, it immediately fail and returns -1. */
	/* Remove current thread from child_threads of parent thread. */
	if (list_empty(&curr->child_threads))
		return -1;
	struct thread *child_thread = NULL;
	struct list_elem *e;
	for (e = list_begin(&curr->child_threads);
		 e != list_end(&curr->child_threads);
		 e = list_next(e))
	{
		struct thread *t = list_entry(e, struct thread, c_elem);
		if (t->tid == curr->tid)
			child_thread = t;
	}
	if (t == NULL)
		return -1;
}

시작 전에 리스트가 비어 있는지 체크하고, 자식 프로세스를 찾지 못하면 -1을 반환하도록 하였다. 다음은 wait()가 실패할 두 번째 조건이다.

The process that calls wait has already called wait on pid. That is, a process may wait for any given child at most once.

그런데, 생각해 보면 pid에 해당하는 자식 프로세스가 없으면 wait()는 실패할 것이고, 있으면 자식 프로세스가 종료될 때까지 다른 코드가 실행되지 않을 것이므로 어떤 프로세스가 wait()를 하고 있는데 그 상태에서 또 wait()를 하는 상황은 생기지 않을 것 같다.

다음으로 wait()를 본격적으로 구현하기 위해, 세마포어를 사용해서 부모 프로세스에서 wait()가 호출되면 세마포어를 내리고 대기 상태에 들어가며 자식 프로세스는 종료될 때 세마포어를 올려 준다면 될 것 같다는 생각을 직관적으로 했다. 그렇다면 자식 프로세스가 세마포어를 올리기 위해, thread 구조체에 세마포어를 선언하고 init_thread()에서 초기화해주자.

struct thread
{
	// ...
    /* Semaphore of child process. Signals to a parent when exitting.*/
	struct semaphore wait_sema; 
    // ...
};

static void
init_thread(struct thread *t, const char *name, int priority)
{
	// ...
	sema_init(&t->wait_sema, 0);
}

그렇다면 wait_sema는 0으로 초기화되어 있고, 여기서 부모 프로세스가 sema_down()을 시도하면 스레드가 BLOCKED 상태로 바뀌므로 어디선가 sema_up()을 할 때까지 기다리게 된다. 어디서 sema_up()을 해 주어야 할까? 그건 바로 프로세스가 종료되는 시점이다. 프로세스가 종료되고 나서 sema_up()을 한다면, 그때 부모 스레드가 sema_down()을 넘어 진행할 수 있게 되니 wait()의 의도대로 구현되었다고 볼 수 있겠다.

또한, wait() 이후에 그냥 함수가 종료되는 것이 아니라 자식 프로세스의 exit_status를 받아와야 하는데, 자식 프로세스에서 실행되는 _exit() 함수 내부에서 부모 스레드에게 status를 넘겨줄 방법이 없으니, thread 구조체 내에 이를 넘겨줄 수 있는 공간인 int child_exit_status를 만들어 주도록 하자.

struct thread
{
	// ..
	struct thread *parent_thread; /* Parent thread. IMPLEMENTED IN PROJECT 2-3. */
	struct list child_threads;	  /* List of child threads. IMPLEMENTED IN PROJECT 2-3. */
	struct list_elem c_elem;	  /* Child thread element. IMPLEMENTED IN PROJECT 2-3. */

	struct semaphore wait_sema; /* Semaphore of child process. Signals to a parent when exitting. IMPLEMENTED IN PROJECT 2-3. */
	int child_exit_status;		/* Exit status of child 
    // ...
};

int process_wait(tid_t child_tid UNUSED)
{
	/* If no child with pid, it immediately fail and returns -1. */
	if (list_empty(&curr->child_threads))
		return -1;
	struct thread *child_thread = NULL;
	struct list_elem *e;
	for (e = list_begin(&curr->child_threads);
		 e != list_end(&curr->child_threads);
		 e = list_next(e))
	{
		struct thread *t = list_entry(e, struct thread, c_elem);
		if (t->tid == child_tid)
		{
			// ...
			child_thread = t;
			sema_down(&child_thread->wait_sema);
			/* Now child_exit_status is available. */
			return curr->child_exit_status;
		}
	}
	/* Search miss. */
	if (child_thread == NULL)
		return -1;
}

void process_exit(void)
{
	struct thread *curr = thread_current();

	/* IMPLEMETED IN PROJECT 2-3. */
	palloc_free_page((void *)curr->fd_table);
	if (curr->parent_thread != NULL)
	{
		/* Remove current thread from child_threads of parent thread. */
		struct list_elem *e;
		for (e = list_begin(&curr->parent_thread->child_threads);
			 e != list_end(&curr->parent_thread->child_threads);
			 e = list_next(e))
		{
			struct thread *t = list_entry(e, struct thread, c_elem);
			if (t->tid == curr->tid)
			{
				list_remove(e);
				break;
			}
		}
		/* Now there is no parent of this thread. */
		curr->parent_thread != NULL;
	}
	while (!list_empty(&curr->child_threads))
	{
		struct list_elem *child_thread = list_pop_back(&curr->child_threads);
		list_entry(child_thread, struct thread, c_elem)->parent_thread = NULL;
	}

	process_cleanup();

	/* IMPLEMENTED IN PROJECT 2-3.
	   Signals termination to the parent. */
	sema_up(&curr->wait_sema);
}

처음에 sema_up() 부분을 process_cleanup() 함수 내에 넣어서 정리하려 했으나, process_cleanup() 함수는 프로세스가 종료될 때뿐만 아니라 프로세스에 프로그램을 올려서 실행할 때, 즉 process_exec() 함수 내부에서도 실행되기 때문에 저기에 넣으면 process_exec() 부분에서 sema_up()이 실행되어서 User program이 시작도 하지 않고 main 스레드가 종료되는 문제가 발생할 수 있다. 이것 때문에 상당한 삽질을 했으니 주의하도록 하자...

이쯤 하고 args-single args 프로그램을 돌려 봤더니, 이번에도 아무 출력 없이 핀토스가 종료되는 문제가 발생했고 여기저기 printf를 찍어본 결과 아무 system call도 호출되지 않았다. 이번에는 왜 그럴까?

지금까지 나는 User program을 실행할 때, 항상 fork()가 실행되고 exec()이 실행되는 줄 알았다. 즉 main이 돌아가면 이걸 fork()로 복제한 뒤 exec()을 실행하는 줄 알았는데 아니었다.

/* Starts the first userland program, called "initd", loaded from FILE_NAME.
 * The new thread may be scheduled (and may even exit)
 * before process_create_initd() returns. Returns the initd's
 * thread id, or TID_ERROR if the thread cannot be created.
 * Notice that THIS SHOULD BE CALLED ONCE. */
tid_t process_create_initd(const char *file_name);

바로 첫 User program을 실행할 때는, main 스레드가 fork()되는 것이 아니라 이 함수를 통해 실행되는 것이다! 그렇다면 이런 문제가 발생한다. 우리는 프로세스 간 부모-자식 관계를 fork()exit() 안에서 정의하였다. 부모-자식 관계가 만들어지는 곳은 fork(), 없어지는 곳은 exit()였는데 사실은 이 함수, process_create_initd()에서도 만들어져야 했던 것이다. 이를 위해 PintOS가 처음 부팅될 때 실행되는 init.c를 보자.

// init.c
/* Runs the task specified in ARGV[1]. */
static void
run_task(char **argv)
{
	const char *task = argv[1];

	printf("Executing '%s':\n", task);
#ifdef USERPROG
	if (thread_tests)
	{
		run_test(task);
	}
	else
	{
		process_wait(process_create_initd(task));
	}
#else
	run_test(task);
#endif
	printf("Execution of '%s' complete.\n", task);
}

init.c에서는 process_create_initd() 함수를 통해 첫 Userland program의 pid를 받고, 이 첫 Userland program이 종료되면 PintOS 역시 종료된다.

mainargs-single 프로그램을 실행했는데, 우리가 짠 프로그램에서는 main의 자식 프로세스로 args-single을 지정하는 과정이 없었고, 따라서 main의 자식 프로세스 리스트에 아무것도 없어서 wait()가 바로 실패하고 핀토스가 종료되었던 것이다.

이를 수정하기 위해, fork()를 짰던 것처럼, process_create_initd() 함수 내에서 thread_create()에서 실행할 스레드 함수의 인자 const char *file_name를 확장하여, file_name뿐만 아니라 부모 스레드의 주소인 thread_current(), 또 부모 스레드의 정보가 옮겨지는 동안 부모 스레드의 종료를 막는 initd_sema(fork()에서 지역 변수로 구현한 fork_sema와 비슷하다)를 bundle로 묶어 넘겨주었다. 이렇게 bundle 만드는 팀은 우리밖에 없을 것 같긴 한데, 한두 번 쓰고 말 세마포어의 경우 굳이 thread 내부에 정의하는 것보다 함수 내부에서 지역 변수로 선언해버리면 할 일이 끝났을 때 바로 없어지니 되게 좋다고 생각한다!

tid_t process_create_initd(const char *file_name)
{
	char *fn_copy;
	tid_t tid;

	// ...

	/* IMPLEMENTED IN PROJECT 2-3.
	   Match first userland program to be child of thread main. */
	struct semaphore initd_sema;
	void *bundle[3] = {(void *)fn_copy, (void *)thread_current(), (void *)&initd_sema};
	sema_init(&initd_sema, 0);
	tid = thread_create(file_name, PRI_DEFAULT, initd, bundle);
	sema_down(&initd_sema);

	/* Create a new thread to execute FILE_NAME. */
	/* DELETED IN PROJECT 2-3.
	tid = thread_create(file_name, PRI_DEFAULT, initd, fn_copy); */

	if (tid == TID_ERROR)
	{
		sema_up(&initd_sema);
		palloc_free_page(fn_copy);
	}
	return tid;
}

static void
initd(void *aux)
{
	void **bundle = (void **)aux;
	char *f_name = (char *)bundle[0];
	struct thread *parent = (struct thread *)bundle[1];
	struct thread *child = thread_current();
	struct semaphore *initd_sema = (struct semaphore *)bundle[2];
	list_push_back(&parent->child_threads, &child->c_elem);
	child->parent_thread = parent;
	sema_up(initd_sema);
#ifdef VM
	supplemental_page_table_init(&thread_current()->spt);
#endif

	process_init();

	if (process_exec(f_name) < 0)
		PANIC("Fail to launch initd\n");
	NOT_REACHED();
}

이쯤하고 make check를 해 보자!

95개의 Test 중 48개가 실패했고, 프로세스 관련 system call과 파일 I/O 관련 system call을 통과하지 못한 것이 많지만, 그래도 지금까지 Project 2를 하면서 wait()를 짜기 전까지 어떤 테스트 케이스도 돌려 보지 못해서 너무 답답했는데 드디어 제대로 테스트를 하고 틀린 부분을 고쳐 가며 진행할 수 있게 된 것만 해도 좋다고 생각한다!

다음 게시글부터는 실패한 테스트 케이스를 고치는 방향으로 계속 진행해야지!

profile
🧑‍💻 이제 막 시작한 빈 집 블로그...

0개의 댓글