이전 글
- [PintOS project] 1. Part 1: Threds - 4.4 BSD like scheduler
https://velog.io/@takealittletime/PintOS-project-1.-Part-1-Threds-4.4-BSD-like-scheduler
이전 주차에서 우리는 커널 영역만을 다루었다.
이번 주차에서는 본격적으로 OS에서 사용자 프로그램 (User Program)을 실행시키는 부분을 학습한다.
CLI 환경에서 커맨드 라인으로 내가 특정 명령어를 실행했을 때, OS는 이러한 명령어의 실행을 위해 어떤 작업을 수행할까?
- 파일 시스템 (Filesystem) 이슈
- 가상 메모리(Virtual Memory) 할당
- 사용자 스택 (User stack) 세팅
- OS는 사용자 프로그램이 끝날 때 까지 대기해야 한다.
이번 주차에 사용자 프로그램을 실행할 때 PintOS에서 동작 수행은 다음 Flow Chart와 같다.
우리는 process_create_initd()
, process_exec()
함수를 수정 해 해당 과제를 해결할 것이다.
/* threads/thread.c */
// 현재 실행 중인 스레드와 레디 리스트의 가장 앞 스레드의 우선 순위를 확인해 스케줄
void
schedule_by_priority () {
struct thread * curr = thread_current();
if (!list_empty(&ready_list)){
struct thread *highest_priority_thread = list_entry(list_front(&ready_list), struct thread, elem);
if (curr->priority < highest_priority_thread->priority)
thread_yield();
}
2주차 과제를 해결하면서 test case를 실행하는데에 있어 위의 코드가 문제가 되었다.
결론부터 이야기하면, 위의 코드를 아래와 같이 수정해주면 해결된다.
/* threads/thread.c */
// 현재 실행 중인 스레드와 레디 리스트의 가장 앞 스레드의 우선 순위를 확인해 스케줄
void
schedule_by_priority () {
struct thread * curr = thread_current();
if (!list_empty(&ready_list)){
struct thread *highest_priority_thread = list_entry(list_front(&ready_list), struct thread, elem);
if (curr->priority < highest_priority_thread->priority)
if (!intr_context()){
thread_yield();
}
else{
intr_yield_on_return();
}
}
}
🛠️ 위와 같이 수정해주어야 했던 이유?
thread_yield()
함수를 까보면 코드가 아래와 같다./* Yields the CPU. The current thread is not put to sleep and
may be scheduled again immediately at the scheduler's whim. */
void
thread_yield (void) {
struct thread *curr = thread_current ();
enum intr_level old_level;
ASSERT (!intr_context ());
old_level = intr_disable ();
if (curr != idle_thread)
list_insert_ordered(&ready_list, & curr->elem, cmp_thread_priority, NULL); //우선순위 기준으로 삽입
// list_push_back (&ready_list, &curr->elem);
do_schedule (THREAD_READY);
intr_set_level (old_level);
}
ASSERT (!intr_context());
문에서 thread_yield()
가 interrupt_context()
가 아닐 때만 실행되도록 해주고 있는데, 2주차 test case를 실행하던 중 아래와 같은 문제가 발생했다.위의 ASSERT (!intr_context());
문에 걸려 Kernel PANIC
이 발생했다. 인터럽트 상황에 해당 함수가 호출 되었다는 이야기이다.
backtrace
를 찍어보니, 인터럽트 상황에 schedule_by_priority()
함수 실행되면서 thread_yield()
가 호출되고, 이에 따라Kernel PANIC
이 발생한 모양이었다.
문제를 해결하기 위해, if
조건 분기를 이용 해 인터럽트 상황에는 thread_yield()
가 아니라 intr_yield_on_return()
함수가 호출 되도록 수정해주었을 뿐이다.
** intr_yield_on_return()
함수는 인터럽트 핸들러의 실행이 끝난 후 thread의 스케줄링이 이루어지도록 하는 함수이다.
커맨드 라인에 ./echo x y z
와 같이 명령을 입력했다고 생각해보자. 위의 명령어 중 echo
는 파일(프로그램) 이름, x
,y
,z
는 각각 이 echo
라는 파일을 실행하는데 전달 되어야 할 인자(Argument)가 될 것이다.
우리는 이렇게 커맨드 라인 명령어가 들어왔을 때, User Stack에 올리기 전에 이 명령어를 각각의 토큰으로 Parsing 해주는 작업을 해주어야 한다.
이러한 Tokenizing 작업은 아래와 같이 strtok_r()
함수를 통해 진행할 수 있다.
process_create_initd()
함수에서 커맨드 라인 명령을 parsing 한 후 맨 앞 토큰(명령어에 해당)을 thread_create()
로 전달한다./* userprog/process.c */
tid_t
process_create_initd (const char *file_name) {
char *fn_copy;
tid_t tid;
/* Make a copy of FILE_NAME.
* Otherwise there's a race between the caller and load(). */
fn_copy = palloc_get_page (0);
if (fn_copy == NULL)
return TID_ERROR;
strlcpy (fn_copy, file_name, PGSIZE);
/* file_name을 parsing해서 맨 앞 토큰을 thread_create()에 인자 전달 */
char *save_ptr;
strtok_r(file_name," ",&save_ptr);
/* Create a new thread to execute FILE_NAME. */
tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
if (tid == TID_ERROR)
palloc_free_page (fn_copy);
return tid;
}
ex) process_create_initd()
에 인자로 전달 되어 들어온file_name
이 "my program -arg1 -arg2"
와 같다고 가정해보자.
fn_copy
는 file_name
의 원본을 복사한다. 즉, my program -arg1 -arg2
가 된다.
file_name
은 strtok_r(file_name, " ", &save+ptr);
를 거쳐 가장 첫 번째 토큰 "my_program"
만 남게 된다.
이 내용이 thread_create()
로 전달되어, thread의 이름이 이 값이 된다.
fn_copy
도 thread_create()
에 전달된다. thread_create()
로 실행되는 각 스레드는 initd()
를 실행하게 되는데, 이 때 이 fn_copy
가 initd()
안의 process_exec()
으로 전달되어 사용된다.
위의 process_create_initd()
를 통해 스레드를 생성하고, 이 스레드 안에서 실행되는 initd()
를 거쳐 process_exec()
으로 들어온다.
앞의 과정에서 사용자 프로그램을 적재할 스레드를 생성하고, 스케줄링 한 뒤 사실상 사용자 프로그램은 이제 이 스레드에서 process_exec()
을 통해 실행되는 것이다.
입력받은 커맨드 라인이 인자 void *f_name
으로 들어올 것이다.
우리는 이제 이 커맨드 라인을 토큰 단위로 parsing해서 명령어와 각 인자들을 User Stack으로 올려주어야 한다.
우선, 커맨드 라인을 parsing 하는 부분은 다음과 같이 작성할 수 있다.
strtok_r()
함수를 반복적으로 호출해서 parse[]
라는 배열 안에 file_name
을 token
으로 분할해 추가한다.
/* userprog/process.c */
int
process_exec (void *f_name) { // 커맨드 라인을 f_name으로 전달
char *file_name = f_name; //f_name은 void* 이므로 이를 char*로 수정
bool success;
...
/* We first kill the current context */
process_cleanup ();
/* 인자 parsing */
char *parse[64];
char *token, *save_ptr;
int count = 0;
for (token = strtok_r(file_name," ",&save_ptr); token != NULL; token = strtok_r(NULL, " ", &save_ptr))
parse[count++] = token;
/* And then load the binary */
success = load (file_name, &_if);
...
}
커맨드 라인의 parsing 작업을 끝냈다면, 이제 User stack에 해당 내용들을 올려줄 차례다.
User stack에 커맨드 라인 토큰들을 올릴 때는 다음과 같이 커맨드 라인의 오른쪽 내용부터 추가하고, 스택 포인터를 내리는 식으로 진행한다.
* User Stack에 관련된 내용은 다음 글에서 다뤄보자!
parse[]
배열을 전달해서 내용을 적재하는 함수를 아래와 같이 작성하자./* userprog/process.c */
void
argument_stack(char **parse, int count, void **rsp){
for (int i = count - 1; i > -1; i--)
{
for (int j = strlen(parse[i]); j>-1; j--)
{
(*rsp)--;
**(char **)rsp = parse[i][j];
}
parse[i] = *(char **)rsp;
}
int padding = (int)*rsp % 8;
for (int i = 0; i < padding; i++)
{
(*rsp)--;
**(uint8_t **)rsp = 0;
}
(*rsp) -= 8;
**(char ***)rsp = 0;
for (int i = count-1; i>-1; i--)
{
(*rsp) -= 8;
**(char ***)rsp = parse[i];
}
(*rsp) -= 8;
**(void ***)rsp = 0;
}
process_exec()
에서 호출해준다.int
process_exec (void *f_name) { // 커맨드 라인을 f_name으로 전달
char *file_name = f_name; //f_name은 void* 이므로 이를 char*로 수정
bool success;
...
/* We first kill the current context */
process_cleanup ();
/* 인자 parsing */
char *parse[64];
char *token, *save_ptr;
int count = 0;
for (token = strtok_r(file_name," ",&save_ptr); token != NULL; token = strtok_r(NULL, " ", &save_ptr))
parse[count++] = token;
/* And then load the binary */
success = load (file_name, &_if);
/* User Stack에 명령어와 인자 적재 */
argument_stack(parse, count, &_if.rsp);
_if.R.rdi = count;
_if.R.rsi = (char *)_if.rsp + 8;
/* user stack을 16진수로 프린트 */
hex_dump(_if.rsp, _if.rsp, USER_STACK - (uint64_t)_if.rsp, true);
/* If load failed, quit. */
palloc_free_page (file_name);
if (!success)
return -1;
/* Start switched process. */
do_iret (&_if);
NOT_REACHED ();
}
위에서 hex_dump
는 user_stack을 16진수로 출력해주는 함수이다. 이제 테스트 케이스를 돌려보면, 해당 작업을 수행했을 때 user stack의 상태를 16진수로 출력해줄 것이다.
process.h
에 프로토 타입 선언을 해주도록 하자./* userprog/process/h */
void argument_stack(char **parse, int count, void **rsp);
위처럼 Argument Parsing 작업도 수행하고, User Stack에 적재하는 작업까지 끝냈다면 이제 테스트 케이스를 돌려볼 생각을 했을 것이다.
이 때, 아직 해주어야 할 작업이 남아있다.
우리가 사용자 프로그램을 실행할 때, 이 사용자 프로그램은 threads/init.c
의 main()
에서 run_actions()
안의 run_task
에서 돌아가게 되는데,
init thread
(우리가 OS를 켜면 가장 먼저 실행되는, 가장 하단에서 기본적인 동작을 수행하는 스레드)는 해당 함수 안에서 process_create_initd()
함수를 호출한 뒤 process_wait()
를 호출해 사용자 프로그램이 끝날 때까지 기다린다.
/* threads/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);
}
process_wait
이 아래에서 보듯 이 모양 이꼴이기 때문에, 위의 그림처럼 사용자 프로그램이 종료되기 전에 그냥 끝나버린다.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. */
return -1;
}
thread_sleep()
을 이용해 임시로 process_wait()
을 작성 해보자.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. */
thread_sleep(100);
return -1;
}
✅ 2주차 User Program 부터는 테스트 케이스를 실행할 때, 다음과 같이 가상 디스크에 대한 명령어에 대해 이해하고 있으면 좋다.
make check
를 이용해 테스트 케이스 전부를 테스트하는 경우에는 아래 과정을 skip할 수 있도록 구성 되어있지만, 테스트 케이스를 하나씩 돌려보고 싶은 경우에는 파일 디스크의 생성과 포맷 정도는 알고 있어야 한다.pintos --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'
hex_dump
를 통해 출력된 User stack 내용과, 입력된 커맨드 라인이 잘 출력되고 있는 것을 확인할 수 있다!https://e-juhee.tistory.com/entry/Pintos-KAIST-Project-2-Argument-Passing-User-프로그램-인자-설정하기