프로젝트1에서 실행했던 alarm clock, scheduling 등 모든 코드들은 전부 커널의 일부였고, 테스트 코드 또한 커널에 직접 컴파일했었다. 이제부터는 유저 프로그램을 실행하여 운영체제를 테스트한다.
그러나 현재 pintos는 커맨드 라인에 명령어를 작성하면, 프로그램 이름과 인자를 구분하지 못 하고 적은 명령어 전체를 하나의 프로그램으로 인식하게 구현되어 있다. 즉 프로그램과 인자를 구분하여 파싱하고 패싱할 수 있도록 하는 것이 이번 목표다.
(e.g. ls -l 이라는 명령어를 적었을 때, ls 와 -l을 구분하지 못 하고 'ls -l'을 하나의 프로그램 명으로 인식)
pintos에 이미 구현되어 있는 코드를 보면서 먼저 프로그램의 실행이 어떻게 되는지 살펴보자.
1. 핀토스에서 프로그램 실행
main() 함수는 pintos를 실행시키는 함수다. 처음 실행할 때 스레드와 메모리, 페이지, 인터럽트 핸들러 등을 초기화한다. 그리고 run_actions(argv); 라는 함수를 볼 수 있는데, 이 때 응용 프로그램이 실행일 경우 run_task() 함수를 호출한다.
// ../threads/init.c
/* Pintos main program. */
int
main (void) {
...
/* Run actions specified on kernel command line. */
run_actions (argv);
...
}
/* Executes all of the actions specified in ARGV[] up to the null pointer sentinel. */
static void
run_actions (char **argv) {
/* An action. */
struct action {
char *name; /* Action name. */
int argc; /* # of args, including action name. */
void (*function) (char **argv); /* Function to execute action. */
};
/* Table of supported actions. */
static const struct action actions[] = {
{"run", 2, run_task},
...
}
유저 프로세스 생성되었다면 커널은 프로세스 종료를 대기
// ../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);
}
프로세스(스레드)생성 함수 호출하고 tid 리턴
// ../threads/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);
/* 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;
}
스레드 생성 후 run queue에 추가
// ../threads/process.c
tid_t thread_create (const char *name, int priority, thread_func *function, void *aux) {
struct thread *t;
tid_t tid;
ASSERT (function != NULL);
/* Allocate thread. */
t = palloc_get_page (PAL_ZERO);
if (t == NULL)
return TID_ERROR;
/* Initialize thread. */
init_thread (t, name, priority);
tid = t->tid = allocate_tid ();
/* Call the kernel_thread if it scheduled.
* Note) rdi is 1st argument, and rsi is 2nd argument. */
t->tf.rip = (uintptr_t) kernel_thread;
t->tf.R.rdi = (uint64_t) function;
t->tf.R.rsi = (uint64_t) aux;
t->tf.ds = SEL_KDSEG;
t->tf.es = SEL_KDSEG;
t->tf.ss = SEL_KDSEG;
t->tf.cs = SEL_KCSEG;
t->tf.eflags = FLAG_IF;
/* Add to run queue. */
thread_unblock (t);
// 우선순위에 따른 CPU 선점하는 함수 추가
test_max_priority();
return tid;
}
그 이후 자식 프로세스가 종료될 때까지 대기한다.
// ../threads/process.c
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;
}
이 과정에서 우리는 커맨드 라인에서 프로세스 이름을 확인하고, 커맨드 라인을 파싱하여 인자를 확인해야 하며 그 인자를 스택에 삽입하는 것을 구현해야 한다.
2. Implement Argument Parsing
위에서 말한 것 처럼, 우리는 커맨드 라인에서 실행파일과 인자들을 분리해야 한다. process.c에 있는 process_exec()함수를 보자.
/* 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)
return -1;
/* Start switched process. */
do_iret (&_if);
NOT_REACHED ();
}
코드를 보면 file_name 변수를 찾을 수 있는데, 이는 사용자가 커맨드 라인에 적은 f_name을 받은 변수이다. 그리고 이 변수는 인터럽트 프레임 구조체인 _if와 함께 load()함수로 보내진다. 이 때 process_exec 함수에서 파일명과 인자를 분리하는 코드를 작성한다.
// for argument parsing
char *argv[64]; // 인자 배열
int argc = 0; // 인자 개수
char *token; // 실제 리턴 받을 토큰
char *save_ptr; // 토큰 분리 후 문자열 중 남는 부분
token = strtok_r(file_name, " ", &save_ptr);
while (token != NULL) {
argv[argc] = token;
token = strtok_r(NULL, " ", &save_ptr);
argc++;
}
※ char* strtok_r (char *s, const char *delimiters, char **save_ptr)
s는 분리하고자 하는 문자열, delimiters는 구분자(무엇을 기준으로 분리할것인가). 여기에서 분리자를 공백으로 줘야 한다. save_ptr은 함수 내에서 토큰이 추출된 뒤 남은 녀석을 가리키기 위한 것이다. 즉 strtok_r의 리턴은 s의 가장 앞에 있는 녀석이고, 이후 두번째 녀석에 접근하고 싶다면 두 번째 strtok 호출 전 s = save_ptr 해줘야 한다.
load()를 한 후, 유저스택에 인자를 넣는 함수를 추가한다.
// 유저스택에 인자 넣기
void **rspp = &_if.rsp;
argument_stack(argv, argc, rspp);
_if.R.rdi = argc;
_if.R.rsi = (uint64_t)*rspp + sizeof(void *);
*Interrupt Frame?
실행 중인 프로세스와 레지스터 정보, 스택 포인터, Instruction Count를 저장하는 자료구조
- 커널 스택에 있음
- 인터럽트나 시스템 콜 호출 시 사용
- rsp: stack pointer
- rax: temp register; return value
- rdi: used to pass 1st argument to functions
- rsi: used to pass 2nd argument to functions
- rdx: used to pass 3rd argument to functions
- rcx: used to pass 4th argument to functions
argument_stack()함수는 다음과 같다.
void argument_stack(char **argv, int argc, void **rsp) {
// Save argument strings (character by character)
for (int i = argc - 1; i >= 0; i--) {
int argv_len = strlen(argv[i]);
for (int j = argv_len; j >= 0; j--) {
char argv_char = argv[i][j];
(*rsp)--;
**(char **)rsp = argv_char; // 1 byte
}
argv[i] = *(char **)rsp; // 리스트에 rsp 주소 넣기
}
// Word-align padding
int pad = (int)*rsp % 8;
for (int k = 0; k < pad; k++) {
(*rsp)--;
**(uint8_t **)rsp = 0;
}
// Pointers to the argument strings
(*rsp) -= 8;
**(char ***)rsp = 0;
for (int i = argc - 1; i >= 0; i--) {
(*rsp) -= 8;
**(char ***)rsp = argv[i];
}
// Return address
(*rsp) -= 8;
**(void ***)rsp = 0;
}
함수 호출 시 인자 값은 오른쪽에서 왼쪽 순으로 유저 스택에 저장된다. 이를 위해 우리는 스택 포인터(코드 내에 rsp)를 활용해야 한다.
유저 스택에 저장된 값들은 다음과 같은 모습을 보인다.
이렇게 되면 process_exec() 함수를 통해 thread가 해당 프로그램을 실행할 수 있게 된다. 이제 다음으로 시스템콜에 대해서 정리해보자.
'개발자 도전기 > [OS] pintOS' 카테고리의 다른 글
pintOS | Project 2 | 시스템 콜 (extra) (0) | 2021.10.14 |
---|---|
pintOS | Project 2 | 시스템콜 구현 (fork, wait, read, write, seek, tell, close) (3) | 2021.10.13 |
pintOS | Project 2 | 시스템콜 구현 (halt, exit, exec, create, remove, open, filesize) (0) | 2021.10.11 |
pintOS | list_entry(), doubly linked-list (0) | 2021.10.04 |
pintOS | Project1: Threads 구현 (0) | 2021.10.04 |
댓글