본문 바로가기
개발자 도전기/[OS] pintOS

pintOS | Project 2 | 명령어 실행 기능 구현(Command Line Parsing)

by 답수 2021. 10. 7.
728x90
SMALL

 

pintos project2 설명 보기

프로젝트1에서 실행했던 alarm clock, scheduling 등 모든 코드들은 전부 커널의 일부였고, 테스트 코드 또한 커널에 직접 컴파일했었다. 이제부터는 유저 프로그램을 실행하여 운영체제를 테스트한다.

 

그러나 현재 pintos는 커맨드 라인에 명령어를 작성하면, 프로그램 이름과 인자를 구분하지 못 하고 적은 명령어 전체를 하나의 프로그램으로 인식하게 구현되어 있다. 즉 프로그램과 인자를 구분하여 파싱하고 패싱할 수 있도록 하는 것이 이번 목표다. 

(e.g. ls -l 이라는 명령어를 적었을 때, ls 와 -l을 구분하지 못 하고 'ls -l'을 하나의 프로그램 명으로 인식)

 

저 miising 부분을 해결하자!

 

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)를 활용해야 한다.

 

유저 스택에 저장된 값들은 다음과 같은 모습을 보인다.

그림에서 esp를 rsp라고 생각하자

 

이렇게 되면 process_exec() 함수를 통해 thread가 해당 프로그램을 실행할 수 있게 된다. 이제 다음으로 시스템콜에 대해서 정리해보자.

728x90
LIST

댓글