이 포스팅은 제가 친구와 PintOS 과제를 하면서 떠올린 생각이나 삽질을 하는 과정을 의식의 흐름대로 적은 글이며 글을 작성한 이후 원래 코드에서 일부 오타나 버그가 수정되었을 수 있습니다. 즉 이 포스팅은 정답을 알려주는 포스팅이 아님을 밝힙니다.
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()
를 짜 보자! 지금까지는 무한 루프를 넣어 놓은 상태이다.
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 receivedpid
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 towait(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 역시 종료된다.
즉 main
이 args-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()
를 짜기 전까지 어떤 테스트 케이스도 돌려 보지 못해서 너무 답답했는데 드디어 제대로 테스트를 하고 틀린 부분을 고쳐 가며 진행할 수 있게 된 것만 해도 좋다고 생각한다!
다음 게시글부터는 실패한 테스트 케이스를 고치는 방향으로 계속 진행해야지!