Jungle

[TIL] pintos : 유저 프로세스 생성 / 실행파일 인자 파싱

손가든 2023. 12. 7. 01:01

이번주부터는 pintos 운영체제 안에서 유저 프로그램을 실행시킬 수 있도록 해야 한다.

 

pintos는 1개의 프로세스에 1개의 스레드만 존재하는 단일 스레드 프로세서이기 때문에

 

유저 프로세스를 실행했을 때는 기본적으로 1개의 스레드가 프로세스의 job을 수행한다고 추상화하는 것으로 그림을 그려 나갈 예정이다.

 

하지만 부모 프로세스가 자식 프로세스를 실행시킨 뒤 wait하는 방식으로,

 

하나의 프로세스로부터 파생된 프로세스는 각각 1개의 스레드를 가진다는 것을 짚고 넘어가야 할 것 같다.

 


 

유저 프로그램 실행

 

일단 가장 먼저 사용자가 '디스크에 존재하는 프로그램의 이름'을 터미널에 호출하며 유저 프로그램을 실행시킬 때

 

어떤 일이 발생하는지 확인해보자.

 

 

그 전에 우리는 사용자가 찾을 데이터가 존재할 가상의 디스크를 생성해야 한다.

 

pintos-mkdisk filesys.dsk 10

 

이 명령어는 핀토스 운영체제에 가상의 디스크를 생성한다.

 

 

그 후 사용자는 (실 테스트를 실행하는 내가 사용자인 셈이다) 이 가상의 디스크에 실행할 파일을 복사해 넣은 뒤,

 

해당 파일을 실행시킨다.

 

pintos --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'

 

이 명령어를 통해 방금 만든 가상의 디스크로 실행할 파일을 복사 한 뒤

 

해당 파일의 이름과 인자를 run 명령어와 함께 실행시킨다.

 

 

그럼 이 명령어가 입력되고 pintos의 main문으로부터 시작하여 유저 프로그램이 생성되는 순서를 따라가보자.

 

Pintos 운영체제 - 프로세스 생성

 

운영체제의 main문을 보자.

 

run_actions을 수행하는 main문

 

main문에서는 모든 시스템의 init(부팅) 작업을 수행한 뒤 run_actions을 실행한다.

 

이후 run_actions의 실행 함수를 보자.

 

run 명령어를 통해 run_task(**argv)를 실행시키는 flow

 

밑에 코드가 더 있지만 매우 길기 때문에 생략하겠다.

 

요약하자면 run말고도 action의 종류가 있는데

 

사용자가 요청한 명령어에 맞춰 실행시키기 위해 for문으로 명령어를 action 명령어와 비교하며 수행할 함수를 찾는다.

 

참고로 유저 프로그램을 실행할 때는 run말고 다른 명령어는 없다.

 

 

아무튼 argv안에 사용자가 입력한 명령어 첫번째가 run이라는 것을 확인했기에 run_task 함수를 인자를 함께 실어 실행한다.

 

 

이제 run_task로 넘어가 보자.

 

USERPROG에서 process_wait를 안에 process_create_initd(task)를 실행

 

USERPROG로 설정되어 있기 때문에 빨간줄로 표시된 else문으로 진입하는데, 그전에 눈여겨 봐야할 인자의 변경점이 있다.

 

이 argv안의 인자는 run 시에 argv[2] = ["run", "파일이름 인자1 인자2"] 이런식으로 들어가 있다.

 

이는 디버깅을 통해 확인할 수 있다.

 

argv[0] = 'run'이었으며, argv[1] 대입을 통해 task가 'args-single test'로 초기화 된 상황

 

따라서 task라는 새로운 인자에서 run이라는 명령어는 걸러지고

 

'파일 이름과 전달 인자'만 process_create_initd()함수에 전달하고 있다.

 

그럼 process_create_initd()에선 무슨 작업을 수행하는지 살펴보자.

 

thread_create로 initd를 수행하는 스레드를 생성하고 있다.

 

process_create_initd() 함수에서는 가상 메모리에 실행 프로그램 이름과 함께

 

전달받은 인자들을 메모리에 로딩해주는 실행 흐름의 시작인 initd()를 수행하도록 thread를 생성하고 있다.

 

 

잘 보면 thread_create를 통해 스레드를 생성할 때,

 

3번째 인자는 해당 스레드가 실행하는 함수이고, 4번째 인자는 3번째 함수를 실행할 때 그 함수에 실어지는 인자이다.

 

따라서 file_name이라는 스레드는 생성되어 스케줄 된 뒤 initd(fn_copy)를 수행하게 된다.

 

 

 

그럼 다음 실행 흐름인 initd() 함수를 한번 살펴보자.

 

if문을 통해 process_exec() 함수를 수행하는 모습

 

initd()에서는 먼저 process_init() 함수 안에서 간단히 현재 이 실행 흐름이 thread를 통해 running되고 있는지 확인한다.

 

이후 if문 안에서 process_exec()을 실행한다.

 

 

 

이제 슬슬 중요해지는 process_exec() 함수를 들여다 보자.

 

나는 process_exec() 함수에서 해당 파일들을 실행시키도록 인자들을 파싱하였고,

 

파싱된 인자들이 올바르게 현재 스레드의 인터럽트 프레임 메모리에 로딩하는 작업을 수행하도록 코드를 이미 작성했다.

 

이 코드는 길지만 직접 작성했기 때문에 설명할 부분이 많아 사진이 아닌 코드로 보자.

 

 * Returns -1 on fail. */
int
process_exec (void *f_name) {
	char *file_name = f_name;
	char *buf;
	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 ();

	char *save_ptr;
	char *f_copy;
	f_copy = palloc_get_page(0);
	if (f_copy == NULL)
		return TID_ERROR;
	strlcpy (f_copy, file_name, PGSIZE);
	f_copy = strtok_r(f_copy," ",&save_ptr);
	/* And then load the binary */
	success = load (f_copy, &_if);
	parsing_file_input(file_name,&_if);
	hex_dump(_if.rsp,_if.rsp,USER_STACK-_if.rsp,true);
	/* If load failed, quit. */
	palloc_free_page (f_copy);
	palloc_free_page (file_name);
	if (!success)
		return -1;

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

 

이전 포스팅에서 인터럽트 프레임에 대해 설명했다.

 

현재 이 코드에서 아래 부분에 해당하는 코드는 인터럽트 프레임 플래그를 초기 설정하는 부분이다.

 

스레드 인터럽트 초기화

 

ds,es,ss는 각각 데이터 세그먼트 설정값이고,

 

cs는 코드 세그먼트 설정값이다.

 

모두 USER모드로 설정한 것을 알 수 있다.

 

 

또한 여기서 하나 인지해야 할 점은 FLAG_IF 플래그를 켜면서 인터럽트 플래그를 ON 시키고 있다.

 

이 코드가 현재 진행 상황까지는 어떤 문제와 대처를 해줘야 할 지 정확히 알수 없지만

 

인터럽트를 켰다는 것 자체는 이제 원자 단위로 수행해야 할 경우 인터럽트를 제어해야 할 지도 모르기에 짚고 넘어갔다.

 

 

이후 파싱하는 작업에 대한 설명으로 넘어가 보자.

 

 

깃북에 나와 있는 가상 메모리 구조이다.

 

현재는 체크된 곳에 초기화만 진행하였고,

로딩을 수행한 이후에는 user_stack하단으로 rsp를 내려가며 런타임 스택에 실행할 프로그램의 이름과 인자를 채워놓아야 한다.

 

 

 

 

아무튼 다시 설명했던 코드로 돌아오면 현재 file_name에는 "파일이름 인자1 인자2" 형태로, 각 인자 별로 파싱되어 있지 않다.

 

 

여기서 file_name은 실제로 건들이면 race_condition이 발생될 우려가 있기 때문에 const를 통해 해당 값을 변경시키지 않도록

 

선언 해둔 것 같다.

 

 

이 것 때문에 파싱 할 경우 문자열을 변환시키기 때문에 새로운 파일 이름 및 인자를 생성한 뒤 복사하는 작업을 수행해야 된다.

 

동그라미 친 부분

 

그래서 load 작업을 하기 전에 인자를 파싱한 뒤 파일 이름만 전달 해줄 때,

 

파일 이름 및 인자를 복사 하는 작업을 수행했다.

 

이때 중요한 점이 있는데, 새로운 파일 이름과 인자를 담을 변수를 palloc으로 할당한 뒤에 해당 변수를 다쓰고 나면

 

메모리 누수를 피하기 위해 free를 통해 반환을 꼭 수행해야 한다.

 

아무튼 load에는 '파일이름\0' 로 file_name을 전달했다.

 

 

load에서는 해당 코드가 한줄 한줄 어떤 작업을 하는지 정확하게 보진 않았다.

 

load 함수 안에서는 파싱하여 전달한 파일 이름을 통해 해당 파일을 열어 정보를 읽었고

 

setup_stack()함수를 통해 프로세스의 %rsp 런타임 스택의 값을 초기화했다.

 

 

이후 해당 파일을 실행시키기 위해 파일이름과 같이 받았던 argv 인자는 런타임 스택안에 아래의 사진과 같이 저장되어야 한다.

 

깃북에 친절하게도(?) 어떻게 인자들을 채워 넣어야 하는지에 대한 설명이 적혀 있다.

 

처음보면 진짜 괴상한 표

 

현재 인터럽트 프레임에 있는 rsp의 값을 하나씩 내려가며 위에서부터 아래를 순서대로 런타임 스택에 삽입하여 저장하는 작업이 필요하다.

 

이 작업을 수행 한 뒤에야 프로그램이 정상적으로 실행될 것이다.

 

나는 이 파싱 후 런타임 스택에 위의 표와 같이 삽입하는 함수를 생성했다.

 

 

static void parsing_file_input(char *file_name, struct intr_frame *if_){
	char *f_name;
    
	f_name = palloc_get_page(0);
	if (f_name == NULL)
		return TID_ERROR;
	strlcpy (f_name, file_name, PGSIZE);
    
	char *token, *save_ptr;
	int var_cnt = 0;
	uintptr_t *address[128];
	char *token_temp[128];
	int i;
    
	for (token = strtok_r(f_name, " ", &save_ptr); token != NULL;
			token = strtok_r (NULL, " ", &save_ptr)){
		token_temp[var_cnt++] = token;
	}
    
	printf("USER_STACK : %p\n",USER_STACK);
    
	for(i=var_cnt-1;i>=0;i--){
		if_->rsp -= strlen(token_temp[i])+1;
		printf("rsp address[%d] (파싱 인자 삽입 위해 down) : %p\n",i,if_->rsp);
		address[i] = (uintptr_t*)if_->rsp;
		strlcpy(if_->rsp,token_temp[i],strlen(token_temp[i])+1);
		printf("문자값 : %s\n", token_temp[i]);
	}
    
	/*word align*/
	uint8_t word_align = 0;
	size_t align = (if_->rsp % sizeof(uint8_t));
	if_->rsp -= (uint8_t)align;
	memcpy(if_->rsp,word_align,align);
    
	/*argv[n] = added align*/
	if_->rsp -= sizeof(char *);
	
	for(i=var_cnt-1;i>=0;i--){
		if_->rsp -= sizeof(uintptr_t);
		*((uintptr_t*)if_->rsp) = address[i];
	}
    
	if_->R.rsi = if_->rsp;
	if_->R.rdi = var_cnt;
	
    /* fake return address */
	if_->rsp -= sizeof(void *);
	palloc_free_page(f_name);
}

 

먼저 아까 해줬던 것 처럼 파싱을 위해 file_name을 복사하여 f_name에 저장했다.

 

이 f_name에는 그저 'filename argv[0] argv[1]' 이런식으로 저장되어 있다는 것을 이해하자.

 

이후 각각 저 사진에 맞게 해당 값들을 저장해주었다.

 

간단하게 로직을 얘기하면 f_name의 값들을 token화 하여 분리한 뒤 char *[] 배열에 저장하고

 

역으로 런타임 스택에 집어넣었다.

 

그리고 align을 8의 배수로 해주기 위한 로직을 추가한 뒤

 

마지막으로 가장 골머리를 썩혔던, string이 담겨있는 해당 값의 실제 메모리 주소값을 저장해야 하는데

 

맨 마지막 for문에서 형변환하여 수행했다.

 

마지막에 rsi와 rdi 레지스터에는 각각 런타임 스택의 argv[] 주소가 들어있는 곳의 시작 주소와, 인자들의 갯수를 넣고 마무리했다.

 

 

 

아무튼 다시 parsing한 함수 밖으로 돌아와서 hex_dump를 통해 런타임 스택에 올바르게 저장되었는지 확인했고,

 

파싱을 위해 복사한 palloc 동적 할당 메모리는 모두 반환해주었다.

 

hex_dump를 통해 인자 두개 (파일 이름과 'args') 가 각각 실제 값과 address로 잘 삽입 된 것을 확인.