[Project 2] User Program (1)

혀누·2022년 1월 10일
0

Pintos

목록 보기
7/11
post-thumbnail

OS 입장에서 User는 마치 아무것도 모르고 바닥을 기어다니는 아기와 같다. 아기는 아무것도 모르고 막 만질테니 위험할만한건 미리 미리 치워놔야한다.

Intro.

pintos 를 비롯한 linux 운영체제는 사용자 프로그램이 직접 시스템에 영향을 미칠 수 없도록 'protection'을 구현해놓았다.
핀토스 이번 주차에서는 이를 구현하는 한 방법으로써 'User Program'을 진행한다.

왜 구분해 놓아야 하는지는 위의 그림 한장으로 설명이 어느정도 될것 같으니 바로 어떤 식으로 구현해놓았는지 코드를 통해 알아보도록 하자.

what happens during "process exec user program"?

최초의 OS를 부팅할때 물리 메모리에 올라와 있는 데이터들은 다음과 같다.

주의할 점은 'OS'라고 부르는 것도 일종의 thread 라는 점이다. (그림의 initial kernel thread)

이 OS thread에서 user program을 실행하는 kernel thread 를 만들어낸다.

코드상으로는 init.c 파일의 main 함수를 실행하는 부분이다.

int
main (void) {
	uint64_t mem_end;
	char **argv;

	/* Clear BSS and get machine's RAM size. */
	bss_init ();

	/* Break command line into arguments and parse options. */
	argv = read_command_line ();
	argv = parse_options (argv);

	/* Initialize ourselves as a thread so we can use locks,
	   then enable console locking. */
	thread_init ();
	console_init ();

	/* Initialize memory system. */
	mem_end = palloc_init ();
	malloc_init ();
	paging_init (mem_end);

#ifdef USERPROG
	tss_init ();
	gdt_init ();
#endif

	/* Initialize interrupt handlers. */
	intr_init ();
	timer_init ();   //timer interrupt 시작.
	kbd_init ();
	input_init ();
#ifdef USERPROG
	exception_init ();
	syscall_init ();
#endif
	/* Start thread scheduler and enable interrupts. */
	thread_start ();
	serial_init_queue ();
	timer_calibrate ();

#ifdef FILESYS
	/* Initialize file system. */
	disk_init ();
	filesys_init (format_filesys);
#endif

#ifdef VM
	vm_init ();
#endif

	printf ("Boot complete.\n");

	/* Run actions specified on kernel command line. */
	run_actions (argv);

	/* Finish up. */
	if (power_off_when_done)
		power_off ();
	thread_exit ();
}

이중에서 run_actions 함수 부분이 실행하고자 하는 user program을 실행하는 부분이라 할 수 있다. 이 함수 안으로 들어가 보자.

process_create_initd 함수에서 무언가 일어나고 있는것 같으니 들어가보자!

thread_create 함수를 사용해, User process 를 실행시킬수 있는 'User' kernel thread를 만들어낸다. 그리고 해당 thread의 첫번째 실행함수는 initd 이다. ('User' kernel stack에 쌓일 예정)
위의 thread를 실행시키는데 필요한 code + data 와 kernel stack 데이터는 모두 앞서 설명했던 물리메모리의 Page pool 중에서 Kernel-pool 에 할당된다. (by palloc_get_page 함수)
'User' kernel thread 의 실행모습은 다음과 같다.

'User' kernel thread 에서 실행되는 함수는 kernel stack 에 쌓이고 있다. 함수가 리턴되면 스택영역에서 다시 빠진다.

그렇다면 initd 에서는 어떤 함수를 실행할까?

static void
initd (void *f_name) {
#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 ();
}

마침내 process_exec 함수를 통해 f_name이라는 이름을 가진 user process를 실행하려고 한다!

int process_exec(void *f_name) {
	char *file_name = f_name;
	bool success;
	struct thread *cur = thread_current();

	/* 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();

	// 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++;
	}

	/* And then load the binary */
	success = load(file_name, &_if);

	/* If load failed, quit. */
	if (!success) {
		palloc_free_page(file_name);
		return -1;
	}

	// 유저스택에 인자 넣기
	void **rspp = &_if.rsp;
	argument_stack(argv, argc, rspp);
	_if.R.rdi = argc;
	_if.R.rsi = (uint64_t)*rspp + sizeof(void *);

	// hex_dump(_if.rsp, _if.rsp, USER_STACK - (uint64_t)*rspp, true);

	palloc_free_page(file_name);

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

process_exec함수가 본격적으로 user process를 실행하는 부분이다. load 에서는 프로그램 실행에 필요한 데이터들을 디스크에서 꺼내서 메모리에 적재하는 과정을 거친다.

이후 argument stack함수를 통해, x86 calling convention에 맞춰서 user stack에 command line을 parsing하고 쌓는다. (user stack은 load안의 setup_stack함수에서 할당받아온 user pool메모리 영역에 있다.)

initd 가 실행되는 과정에서 불려진 process_exec 과 load가 계속해서 kernel stack에 쌓이는 모습을 확인할 수 있다.

이후 load 가 리턴되고 do_iret함수가 불려지며 stack에 쌓인다.

do_iret 함수는 parameter로 받은 interrupt frame 구조체에 맞추어 CPU 주변의 register값들을 바꿔주는 함수다.

즉, register를 바꾸므로 CPU가 실행하는 instruction line을 알려주는 rsp등이 바뀌며 User Process가 실행된다!!

그렇다면 User program을 실행하는 Virtual Memory 는 어떻게 생겼을까?

여기서 VM의 아주 독특한 특징이 나타난다. 바로 User space 와 Kernel space를 나누는 과정에서 kernel space 의 크기를 물리 메모리의 크기와 똑같이 1:1 mapping 해놓았기 때문이다.

즉, 모든 user process는 physical memory에 있는 정보를 모두 가진 kernel space를 머리에 지고 돌아간다.

이 특징은 이후에 system call handling에서 매우 중요한 역할을 한다.

Ref.

chicago university 강의자료

profile
개발자(물리)

0개의 댓글