크래프톤정글8주차 - PintOS(Exit , Wait , Fork)

김태성·2024년 3월 24일
0
post-thumbnail

주의!

이 글은 pintos 프로젝트의 정답 코드 및 이론을 포함하고 있습니다.
pintos의 취지 특성 상 개인의 공부, 고민 과정이 정말 중요하다고 생각하며

프로젝트 풀이에 대한 스포일러를 받고싶지 않으신 분은 읽으시는걸 비추천 합니다.
































그냥 시간이 삭제되는 느낌이다... 아직 목요일인거 같은데 벌써 토요일이다. 진짜 바쁘게 프로잭트를 하고 있지만 운영체제라는 걸 뜯어보는게 재밌어서 힘들다는 생각은 안든다.

그래도 하루에 10몇시간씩 코드 보고 있으니 육체적으로는 힘들다.

exec

입력받은 context를 파싱 , register에 입력하는 역할을 한다.

int process_exec(void *f_name)
{
	char *file_name = f_name;
	bool success;

	/* 스레드 구조체에서는 intr_frame을 사용할 수 없다.
	 * 현재 스레드가 재스케줄 될 때 실행 정보를 멤버에 저장하기 때문이다.
	 */
	struct intr_frame _if;
	_if.ds = _if.es = _if.ss = SEL_UDSEG;
	_if.cs = SEL_UCSEG;
	_if.eflags = FLAG_IF | FLAG_MBS;

	/* 먼저 현재 컨텍스트를 죽인다. */
	process_cleanup();

	/* Project 2: Command to Word */
	char *argv[64];
	char *token, *save_ptr;
	int argc = 0;
	for (token = strtok_r(file_name, " ", &save_ptr); token != NULL; token = strtok_r(NULL, " ", &save_ptr))
		argv[argc++] = token;

	/* 그리고 바이너리를 불러온다. */
	success = load(file_name, &_if);

	/* Project 2: Argument Passing*/
	set_userstack(argv, argc, &_if);
	_if.R.rdi = argc;
	_if.R.rsi = _if.rsp + 8;
	// hex_dump(_if.rsp, _if.rsp, USER_STACK - (uint64_t)_if.rsp, true);

	/* 로드에 실패하면 종료한다. */
	palloc_free_page(file_name);
	if (!success)
		return -1;
	sema_up(&main_thread->duplicate_sema);
	/* 전환된 사용자 프로세스를 시작한다. */
	do_iret(&_if);
	NOT_REACHED();
}
  1. 현재 context를 청소

  2. token에 strtok_r을 사용해서 단어 단위로 파싱함(나누는 기준은 " ")

  3. 파일을 load함.(실패시 False 반환)

  4. userstack에 값 저장. -> argument pass에 나옴

5 . do_iret으로 스레드 시작

여기서 코드에 살짝 디테일이 들어가야 되는데 그건 바로 wait과 연관된 sema 관련된 내용이다.
exec의 설명을 보면 다음과 같다.

//process_exec - 현재 실행 컨텍스트를 f_name으로 전환한다.
//실패 시 -1을 반환한다.

즉 , exec 시스템콜을 사용했다는건 이미 kernel 영역으로 빠졌다는것이고 , 프로세스 실행에 관련된 내용이니 데이터 변조 방지를 위해서 sema를 미리 걸었으리라 예측할 수 있다. 그러니 exec가 끝나면 sema_up을 해서 sema를 풀어주고 , do_iret을 사용해서 해당 스레드를 시작한다.

wait

시간 순대로 하면 결과값이 튄다

/* 자식 프로세스가 종료될 때까지 대기하고 종료 상태를 반환하는 함수 */
int process_wait(tid_t child_tid)
{
	if (child_tid == TID_ERROR)
	{
		return -1;
	}

	struct thread *curr = thread_current();
	struct thread *child = get_child_by_tid(curr, child_tid);

	/* 해당 자식 프로세스가 존재하지 않는 경우 */
	if (child == NULL)
	{
		return -1;
	}

	/* 자식 프로세스가 종료될 때까지 대기 */
	sema_down(&child->child_wait_sema);

	/* 자식 프로세스를 부모의 자식 리스트에서 제거 */
	list_remove(&child->child_elem);

	/* 자식 프로세스의 메모리를 해제 */
	// palloc_free_page(child);

	// sema_up(&child->exit_sema);

	/* 자식 프로세스의 종료 상태를 반환 */
	return child->exit_status;
}

처음 봤을때 제일 이해도 안되고 감도 안잡혔던 부분인것 같다.
프로젝트를 시작할때 int process_wait 함수 안에 있는 코드는 while 무한반복문 돌리는거 하나만 있었다.

하지만 그렇게 코드를 작성하면 초반 테스트 케이스는 통과할지라도 , fork쯤 오면 프로세스를 새로 생성해서 스레드가 여러개 돌아가게 되는데 , 그러면 테스트 케이스 통과 자체가 불가능해진다.

우선 process_wait이 언제 쓰이는지부터 알아보자.

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);
}

위 코드는 run_task라는 함수인데 , 우리가 테스트 케이스를 돌리기 위해서 사용하게 될 '입력장치'라고 불러도 되겠다.

흐름은 다음과 같다.

  1. PintOS를 실행한다.

  2. run_task로 인자를 전달한다.

  3. process_create_initd로 인자를 파싱 , 해석해서 프로세스를 만든다.

  4. 이때 , process_exec함수를 실행하고 , process_create_initd에서 main thread에 sema_down을 건다.

  5. 위의 process_exec 함수가 끝나고 , main thread의 sema를 열어준다.(sema_up)

  6. 만약 , 이때 child thread가 발생한다면 , child thread의 진행상황이 끝나기까지 기다린다.

  7. 기다리는 이유는 wait 함수의 진행상황보다 child_thread의 진행상황이 훨씬 길기 때문이다.(스레드 생성 , 로딩 , 페이지 복사 등등 과정이 상당히 복잡하고 처리할것도 압도적으로 많다.)

  8. 이후 child_thread의 처리과정이 끝났으면 , 자식 프로세스를 부모의 자식 리스트에서 제거하고(독립된 스레드로 취급) 자식 스레드의 상태를 return 한다.

그러니까 wait 함수는 자식 프로세스와 부모 프로세스의 연산 양의 차이에 의한 템포 차이를 조절하는 역할을 하게 된다.
이러한 작업을 하는 이유는 , 컴퓨터 마다 cpu의 연산량의 차이가 극단적으로 나타나기도 하는데,(cpu의 성능 , 개발환경 등등... 일반적으로 윈도우+wsl 환경이 mac+docker보다 10배는 빠른것 같다.) 이러한 연산 속도의 차이 때문에 wait을 시간단위로 하게 되면 어떤 컴퓨터에서는 되고 어떤 컴퓨터에서는 안되고 하는 식의 문제가 발생하게 된다. 개인적인 생각이지만 , 컴퓨터와 vm의 아키텍쳐 구조상의 차이로 인해서 메모리가 캐싱되는 방식이 달라서 그렇지 않을까.. 하는 생각도 한다.

그러니까 sema를 사용해서 자식 프로세스의 작업이 끝났을때 부모 프로세스도 함께 실행시기게 되면 두 프로세스간의 작업 단계를 맞출 수 있는 것이다.

fork

fork는 다음 흐름을 이해해야 한다.

sema_down과 sema_up을 당하는 thread는 main thread이다!
놓치면 위의 그림을 전혀 이해하지 못하니까 주의하자.

  • main thread에서 do_fork를 가진 child thread를 만든다.

  • 이때 main thread에 sema_down을 걸어 child가 끝날때까지 기다린다.

  • chile thread가 모든 일을 마치고 rax 레지스터의 값을 0으로 마킹한다.(자식 스레드의 반환값은 0이기 때문)

  • 이후 sema_up을 한 후 do_iret을 실행하고 , 부모 프로세스는 sema_up이 되었기 때문에 자식 스레드의 tid를 반환한다.

위의 설명에서 꼭 알아야할 포인트는 몇개가 있는데,

  1. 부모 프로세스를 fork하면 자식 프로세스가 생기고 , 각각의 프로세스는 각각 스레드를 돌린다. 즉 fork를 하면 작동하는 스레드의 개수가 증가한다.

  2. 그렇기 때문에 부모 프로세스와 자식 프로세스는 독립적인 실행 환경으로 생각해야 하며 , 부모 프로세스에서 sema로 wait을 걸지 않고 시간초로 wait을 걸어버린다면 컴퓨터의 성능 차이로 인해서 결과값이 튀게 된다.

이 생각을 못해서 한 1주일 해맸던거 같다.

tid_t process_fork(const char *name, struct intr_frame *if_)
{
	struct thread *curr = thread_current();

	// 현재 스레드가 가지고 있는 부모의 intr_frame을 복사
	// (부모의 가상 주소 공간의 값을 복제하기 위함, 가상 주소 값은 다름)
	memcpy(&curr->parent_if, if_, sizeof(struct intr_frame));

	/* Clone current thread to new thread. */
	/* 현재 스레드를 새 스레드로 복제합니다. */
	tid_t tid = thread_create(name, PRI_DEFAULT, __do_fork, curr);

	if (tid == TID_ERROR)
	{
		return TID_ERROR;
	}

	/* 현재 스레드의 자식 프로세스로 새로 생성된 스레드를 추가 */
	struct thread *child = get_child_by_tid(curr, tid);
	// list_push_back(&curr->children, &child->child_elem);

	if (child == NULL || child->exit_status == TID_ERROR)
	{
		// sema_up(&child->exit_sema);
		return TID_ERROR;
	}

	/* 새로 생성된 스레드가 종료될 때까지 대기 */
	sema_down(&child->duplicate_sema);

	return tid;
}

코드의 실행 과정은 다음과 같다.

  1. process_fork로 do_fork를 만듬

  2. memcpy , pml4 생성 후 process를 활성화

  3. pml4확인 후 , fdt 초기화

  4. 예외 처리 해주고

  5. rax값 0으로 반환(자식은 0반환)

  6. 부모 프로세스 sema up , process init_

  7. sema가 풀린 부모 프로세스도 tid 반환(부모는 자식 tid 반환)

static void __do_fork(void *aux)
{
	struct intr_frame if_;
	struct thread *parent = (struct thread *)aux;
	struct thread *current = thread_current();
	struct intr_frame *parent_if = &parent->parent_if;
	bool succ = true;

	/* 1. CPU 컨텍스트를 로컬 스택으로 읽습니다. */
	memcpy(&if_, parent_if, sizeof(struct intr_frame));

	/* 2. PT 복제 */
	current->pml4 = pml4_create();

	if (current->pml4 == NULL)
	{
		goto error;
	}

	process_activate(current);

#ifdef VM
	supplemental_page_table_init(&current->spt);
	if (!supplemental_page_table_copy(&current->spt, &parent->spt))
		goto error;
#else
	if (!pml4_for_each(parent->pml4, duplicate_pte, parent))
		goto error;
#endif


	/* TODO: Your code goes here.
	 * 파일 객체를 복제하려면 include/filesys/file.h의 file_duplicate를 사용하십시오.
	 * 이 함수가 부모의 자원을 성공적으로 복제할 때까지
	 * 부모는 fork()에서 반환되지 않아야 한다.
	 */
	int idx = 2;

	// lock_acquire(&filesys_lock);

	current->fdt[0] = parent->fdt[0];
	current->fdt[1] = parent->fdt[1];

	while (idx < FDT_SIZE)
	{
		if (parent->fdt[idx] != NULL)
		{
			current->fdt[idx] = file_duplicate(parent->fdt[idx]);
		}

		idx++;
	}

	// lock_release(&filesys_lock);

	// 자식 프로세스 반환 값은 0
	if_.R.rax = 0;

	sema_up(&current->duplicate_sema);
	process_init();

	/* Finally, switch to the newly created process. */
	/* 마지막으로 새로 생성된 프로세스로 전환합니다. */
	if (succ)
	{
		do_iret(&if_);
	}

error:
	sema_up(&current->duplicate_sema);
	exit(-1);
}

do_fork 코드이다.

간단하게 말하면

  • 메모리 할당해주고

  • pml4 할당해주고

  • 잘 할당되었는지 확인해주고

  • fdt 설정해주고

  • 파일 복사해주고

  • rax = 0으로 반환해주고

  • 이후 sema_up으로 부모 프로세스를 풀어준다.

  • 자식프로세스는 do_iret을 한다.

간?단 하다.
이거 한다고 팀원분의 시간이 상당히 갈려버렸다.

do_iret

참 신기한 놈이다.
원래는 손댈 필요도 없는 코드고 보면 머리만 아픈 코드라고 하지만
스레드가 어떻게 작동하는지 가장 정확하게 이해를 할 수 있는 코드라고 생각한다.

/* Use iretq to launch the thread */
/* iretq를 사용하여 스레드를 시작합니다. */
void do_iret(struct intr_frame *tf)
{
	__asm __volatile(
		"movq %0, %%rsp\n"
		"movq 0(%%rsp),%%r15\n"
		"movq 8(%%rsp),%%r14\n"
		"movq 16(%%rsp),%%r13\n"
		"movq 24(%%rsp),%%r12\n"
		"movq 32(%%rsp),%%r11\n"
		"movq 40(%%rsp),%%r10\n"
		"movq 48(%%rsp),%%r9\n"
		"movq 56(%%rsp),%%r8\n"
		"movq 64(%%rsp),%%rsi\n"
		"movq 72(%%rsp),%%rdi\n"
		"movq 80(%%rsp),%%rbp\n"
		"movq 88(%%rsp),%%rdx\n"
		"movq 96(%%rsp),%%rcx\n"
		"movq 104(%%rsp),%%rbx\n"
		"movq 112(%%rsp),%%rax\n"
		"addq $120,%%rsp\n"
		"movw 8(%%rsp),%%ds\n"
		"movw (%%rsp),%%es\n"
		"addq $32, %%rsp\n"
		"iretq" // interrupt 처리 완료 후 이전에 수행하던 코드로 복원
		: : "g"((uint64_t)tf) : "memory");
}

movq와 addq로 유저 스택 위치를 왔다갔다 하면서 레지스터에 정보값을 넣어주는 코드이다.

volatile은 최적화 방지 함수라고 한다.
정말.. 공부하면서도 머리아프고 이해도 잘 되지 않았던 부분인데 정리하자면

  1. 원래 데이터의 저장 위치는 최적화를 함에 따라서 변경될 수 있음.

  2. 그래서 레지스터에 저장되는 값들도 변경가능

  3. 하지만 volatile을 선언해버리면 이러한 최적화 과정을 막아버림

  4. 내가 원하는 정보를 집어넣을수 있다!

가 된다.

그러니까 지금 쓰는 이 do_iret 함수는 레지스터에 내가 원하는 스레드의 데이터를 집어넣고 iretq로 이전에 수행하던 코드로 복원하기 위해 사용되는 것이다.

다시 정리하자면 , 프로세스가 kernel쪽으로 들어가서 어떠한 처리와 코드를 따라 실행하게 되면 레지스터에 저장된 값들이 바뀌게 되는데 , 이 과정때문에 kernel을 실행하기 직전의 상황과 kernel에서 나온 상황에서의 레지스터 값들이 변하게 되는것이다.
그래서 kernel을 실행하기 직전의 상황으로 초기화를 시켜 주기 위해서 위와같은 레지스터 최적화 방지 함수인 volatile을 선언해서 내가 데이터를 변경할 수 있게 해준 후 , 레지스터에 kernel으로 진입하기 직전의 데이터(User stack에 들어가 있는 데이터)로 초기화 해줌으로써 , atomic한 처리가 되는것이다!(어떠한 실행이 했는지 모름)

그리고 나중에 알았는데 iretq라는 어셈블리어 코드는 인터럽트 처리 후 이전에 수행하는 코드로 복원하는 코드라고 한다.

pass tests/threads/priority-donate-chain
pass tests/userprog/args-none
pass tests/userprog/args-single
pass tests/userprog/args-multiple
pass tests/userprog/args-many
pass tests/userprog/args-dbl-space
pass tests/userprog/halt
pass tests/userprog/exit
pass tests/userprog/create-normal
pass tests/userprog/create-empty
pass tests/userprog/create-null
pass tests/userprog/create-bad-ptr
pass tests/userprog/create-long
pass tests/userprog/create-exists
pass tests/userprog/create-bound
pass tests/userprog/open-normal
pass tests/userprog/open-missing
pass tests/userprog/open-boundary
pass tests/userprog/open-empty
pass tests/userprog/open-null
pass tests/userprog/open-bad-ptr
pass tests/userprog/open-twice
pass tests/userprog/close-normal
pass tests/userprog/close-twice
pass tests/userprog/close-bad-fd
pass tests/userprog/read-normal
pass tests/userprog/read-bad-ptr
pass tests/userprog/read-boundary
pass tests/userprog/read-zero
pass tests/userprog/read-stdout
pass tests/userprog/read-bad-fd
pass tests/userprog/write-normal
pass tests/userprog/write-bad-ptr
pass tests/userprog/write-boundary
pass tests/userprog/write-zero
pass tests/userprog/write-stdin
pass tests/userprog/write-bad-fd
pass tests/userprog/fork-once
pass tests/userprog/fork-multiple
pass tests/userprog/fork-recursive
pass tests/userprog/fork-read
pass tests/userprog/fork-close
pass tests/userprog/fork-boundary
pass tests/userprog/exec-once
pass tests/userprog/exec-arg
pass tests/userprog/exec-boundary
pass tests/userprog/exec-missing
pass tests/userprog/exec-bad-ptr
pass tests/userprog/exec-read
pass tests/userprog/wait-simple
pass tests/userprog/wait-twice
pass tests/userprog/wait-killed
pass tests/userprog/wait-bad-pid
pass tests/userprog/multi-recurse
pass tests/userprog/multi-child-fd
pass tests/userprog/rox-simple
pass tests/userprog/rox-child
pass tests/userprog/rox-multichild
pass tests/userprog/bad-read
pass tests/userprog/bad-write
pass tests/userprog/bad-read2
pass tests/userprog/bad-write2
pass tests/userprog/bad-jump
pass tests/userprog/bad-jump2
pass tests/filesys/base/lg-create
pass tests/filesys/base/lg-full
pass tests/filesys/base/lg-random
pass tests/filesys/base/lg-seq-block
pass tests/filesys/base/lg-seq-random
pass tests/filesys/base/sm-create
pass tests/filesys/base/sm-full
pass tests/filesys/base/sm-random
pass tests/filesys/base/sm-seq-block
pass tests/filesys/base/sm-seq-random
FAIL tests/filesys/base/syn-read
pass tests/filesys/base/syn-remove
FAIL tests/filesys/base/syn-write
FAIL tests/userprog/no-vm/multi-oom
pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
pass tests/threads/alarm-simultaneous
pass tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
pass tests/threads/priority-change
pass tests/threads/priority-donate-one
pass tests/threads/priority-donate-multiple
pass tests/threads/priority-donate-multiple2
pass tests/threads/priority-donate-nest
pass tests/threads/priority-donate-sema
pass tests/threads/priority-donate-lower
pass tests/threads/priority-fifo
pass tests/threads/priority-preempt
pass tests/threads/priority-sema
pass tests/threads/priority-condvar
pass tests/threads/priority-donate-chain
3 of 95 tests failed.

결국 3개 빼고 다 통과는 했다.









뭔가.. 뭔가 코드를 짜니까 돌아가긴 한다....
어떻게 굴러가는지 흐름은 파악한거 같아서 다행이긴 한데
다음 프로잭트는 훨씬 어렵다고 하니 한숨만 나온다.

이번에는 다리로 날아가지 않을까 싶다

profile
닭이 되고싶은 병아리

0개의 댓글

관련 채용 정보