본문 바로가기
Review/SW Jungle

[WEEK10] Pintos _ Project2 User Program (2.2) System Calls

by jamiehun 2022. 11. 29.

0. 시작하기 전에

예전 직장에서 법이나 규정을 많이 보다보니
꽤나 오랜 시간을 단어나 용어의 정확한 정의나 의미 파악을 중점적으로 공부하고 일을 수행해왔다..
Abstraction으로 가득찬 프로그래밍 세계에서는 이런 공부방법이 좋지 못하다는 것을 시간이 지나면서 점점 깨닫고 있다. 🥲
물론 용어에 대한 정확한 파악도 중요하지만 개념파악 외에도 코드 구현 또한 매우 중요하다!

앞의 Argument Passing이나 File System 구현은 메뉴얼에서 시킨대로, 코드를 잘 짜면 구현이 가능하나 (물론 구현이 쉽지는 않다.)
fork, wait, exec부터는 각 코드들의 유기적 관계와 코드가 어디서 어떻게 움직이는지 알아야 이해력 높은 구현이 가능한 것 같다.
(Project를 마치는 지금 시점에도 헷갈리거나 정확히 모르는 개념들이 있어서 지속적으로 follow-up 해야할 것 같다.)

코드를 많이 읽고, 많이 쓰고, 개념은 deep하게 공부하자! (이해가 될 때까지)
코딩이 점점 더 재밌게 느껴진다. 아직은 부족하지만 꾸준히 채워나가다보면 성장하겠지!


1. 프로젝트 공부

연관지식

사용자영역과 커널영역

사용자 영역

  • 프로그램이 동작하기 위해 사용되는 메모리공간 (코드영역, 데이터 영역, 스택 및 힙 영역)을 가리켜 유저영역 (User 영역)
  • 응용프로그램이 실행되는 기본 모드로 물리적인 영역으로의 접근이 허용되지 않으며, 접근할 수 있는 메모리의 영역도 제한
  • 메모리 영역별 역할
    • 코드영역 (= text 영역) : 말 그대로 실행할 수 있는 코드, 기계어로 이루어진 명령어가 저장됨
      (CPU가 실행할 명령어가 담겨있기 때문에 쓰기 금지, 읽기전용 공간)
    • 데이터 영역 : 프로그램이 실행하는동안 유지할 데이터가 저장되는 공간으로 전역변수가 대표적
    • 여기서, 코드 영역과 데이터 영역은 바뀌지 않음 ⇒ 크기가 변하지 않고 프로그램이 실행되는 동안 유지됨 ⇒ 정적 할당 영역
    • 힙과 스택 영역은 동적 할당 영역
    • 힙 영역 : 프로그램을 만드는 사용자, 즉 프로그래머가 직접 할당할 수 있는 저장 공간
    • 스택 영역 : 데이터를 일시적으로 저장한느 공간 ⇒ 매개변수, 지역변수가 대표적
      • 높은 주소 → 낮은 주소로


커널 영역

  • 총 메모리 공간 중에서 유저 영역을 제외한 나머지 영역을 커널영역이라고 함
  • 운영체제가 실행되기 위해서는 운영체제도 메모리에 올라가야하고 일반 프로그램처럼 실행되는 과정에서
    변수선언 및 메모리 동적할당이 필요함
  • 운영체제라는 하나의 소프트웨어를 실행시키기 위해 필요한 메모리 공간을 커널 영역이라고 함

 

fork, exec, wait

- 해당 함수에서는 자식 프로세스를 생성하고, 부모가 자식을 기다리는 등의 일련의 과정을 다루는 시스템 콜을 다룬다.

fork

  • fork() 함수 실행시 운영체제는 시스템 콜을 제공함
    • 함수 실행 후 총 2개의 프로그램이 존재함
  • 자식 프로세스는 부모 프로세스와 완전히 동일하지는 않음
    • 자식 프로세스는 자신의 주소공간, 자신의 레지스터, 자신의 PC 값을 가짐
  • fork() 의 반환값이 다름
    • 부모 프로세스는 생성된 자식 프로세스의 PID를 반환
    • 자식 프로세스는 0을 반환
  • 주의할 점은 프로그램의 출력결과가 항상 동일하지 않다는점
    ⇒ 스케쥴러를 통해서 실행할 프로세스를 정해야하는 것 같음 (세마포어?)


fork 구현

1) 시작

// System Call 2 : Fork
	case SYS_FORK :
	{
		f->R.rax = fork(f->R.rdi, f);
		break;
	}


// ===================================

/* System Call 1 : Fork */

tid_t fork (const char *thread_name, struct intr_frame *if_){

	return process_fork(thread_name, if_);
}



2) process_fork

/* Clones the current process as `name`. Returns the new process's thread id, or
 * TID_ERROR if the thread cannot be created. */
tid_t
process_fork (const char *name, struct intr_frame *if_ UNUSED) {
	/* Clone current thread to new thread.*/
	struct thread *parent = thread_current();
	tid_t child_tid;
    
	/* parent_if에 유저스택 정보 담기*/
	memcpy(&parent->parent_if,if_,sizeof(struct intr_frame));//if_는 유저스택, 이 정보를(userland context)를 Parent_if에 넘겨준다
    
	/* 자식 스레드를 생성 */
	child_tid=thread_create (name,	// function함수를 실행하는 스레드 생성
			PRI_DEFAULT, __do_fork, thread_current ()); //부모스레드는 현재 실행중인 유저 스레드
            
	if (child_tid==TID_ERROR)
		return TID_ERROR;
        
	/* Project 2 fork()*/
	struct thread *child = get_child_process(child_tid);
	sema_down(&child->sema_fork);

    if (child->process_exit_status == -1)
    {
        return TID_ERROR;
    }
    
	return child_tid;
}

 

  • memcpy(&parent->parent_if,if_,sizeof(struct intr_frame)); ?
    • fork를 수행하는 Parent가 interrupt frame을 가지고 있지 않음
    • ⇒ fork시 인터럽트 프레임을 넘겨서 그대로 복사해줘야함!
    • 좀 더 자세한 내용은 해당 블로그를 참고하면 될 것 같다 (링크)
  • thread_create 실행시 주어진 이름, 우선순위, 함수, aux를 가지고 새로운 thread를 생성함
    • 이 때 주의 할 점은 위에서 넘겨준 func를 바로 실행하지는 않고 해당 스레드 구조체를 ready_list로 보내준다는 점이다!
      • init_thread를 통해서 thread 구조체를 blocked 상태로 하나 만듦
      • 그 후 allocate_tid()를 통해서 tid를 2부터 시작함
      • 해당 내용을 커널 스레드에 올려준 다음
      • thread_unblock을 통해서 ready_list로 삽입

__do_fork 구현

/* A thread function that copies parent's execution context.
 * Hint) parent->tf does not hold the userland context of the process.
 *       That is, you are required to pass second argument of process_fork to
 *       this function. */

static void
__do_fork (void *aux) {	//process_fork함수에서 thread_create()을 호출하면서 aux는 thread_current()를 들고옴
	struct intr_frame if_;
	struct thread *parent = (struct thread *) aux;
	struct thread *current = thread_current ();


	/* TODO: somehow pass the parent_if. (i.e. process_fork()'s if_) */
	
	struct intr_frame *parent_if; //부모 인터럽트 프레임
	parent_if = &parent->parent_if; // 넘어온 부모 인터럽트(userland context가 담긴)를 프레임을 다시 저장 

	bool succ = true;

	/* 1. Read the cpu context to local stack. */
	memcpy (&if_, parent_if, sizeof (struct intr_frame));

	/* 2. Duplicate 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.
	 * TODO: Hint) To duplicate the file object, use `file_duplicate`
	 * TODO:       in include/filesys/file.h. Note that parent should not return
	 * TODO:       from the fork() until this function successfully duplicates
	 * TODO:       the resources of parent.*/

	if (parent->fd_idx==FDCOUNT_LIMIT)
		goto error;
	for (int fd=0; fd<FDCOUNT_LIMIT;fd++){
		if (fd<=1){
			current->fdt[fd]=parent->fdt[fd];
		}
		else{
			if(parent->fdt[fd]!=NULL)
				current->fdt[fd]=file_duplicate(parent->fdt[fd]);
		}
	}
	current->fd_idx=parent->fd_idx;

	sema_up(&current->sema_fork);
    
	/* 자식 프로세스 0으로 반환 */
	if_.R.rax = 0;
	process_init ();



	/* Finally, switch to the newly created process. */
	if (succ)
		do_iret (&if_);


error:
	sema_up(&current->sema_fork);
	exit(TID_ERROR); // GitBook 참고
}

struct thread *parent = (struct thread *) aux;

  • do_fork를 저장할 당시에 thread_current()

struct thread *current = thread_current ();

  • 현재 do_fork를 실행할 당시의 thread_current() ⇒ 자식 스레드

parent_if = &parent->parent_if;

  • 가져온 부모의 인터럽트 프레임을 다시 저장함

FDT를 복제하는 과정이 있음

  • 파일 디스크립터는 파일을 접근하는데 사용됨


duplicate_pte 실행

#ifndef VM
/* Duplicate the parent's address space by passing this function to the
 * pml4_for_each. This is only for the project 2. */
static bool
duplicate_pte (uint64_t *pte, void *va, void *aux) {
	struct thread *current = thread_current ();   
	struct thread *parent = (struct thread *) aux; 
	void *parent_page;
	void *newpage;
	bool writable;

	/* 1. TODO: If the parent_page is kernel page, then return immediately. */
	if (is_kernel_vaddr(va)) return true; 
	// if (is_kernel_vaddr(va)) return false;
	
	/* 2. Resolve VA from the parent's page map level 4. */
	parent_page = pml4_get_page (parent->pml4, va); 
	if (parent_page == NULL){
		return false;
	}
	/* 3. TODO: Allocate new PAL_USER page for the child and set result to
	 *    TODO: NEWPAGE. */
	newpage = palloc_get_page(PAL_USER | PAL_ZERO);
	if(newpage == NULL) {
		return false;
	}

	/* 4. TODO: Duplicate parent's page to the new page and
	 *    TODO: check whether parent's page is writable or not (set WRITABLE
	 *    TODO: according to the result). */
	memcpy(newpage, parent_page, PGSIZE);
	// memcpy(newpage, parent_page, PGSIZE);
	writable=is_writable(pte);


	/* pml4_set_page로 가상메모리와 물리메모리를 맵핑함 (writable에 대한 정보를 가지고서) */
	/* 5. Add new page to child's page table at address VA with WRITABLE
	 *    permission. */
	if (!pml4_set_page (current->pml4, va, newpage, writable)) {
		/* 6. TODO: if fail to insert page, do error handling. */
		return false;
	}
	return true;
}
#endif


헷갈리는 sema up & down 정리

process_fork()
1. process_fork시에 thread_create를 통해 만들어진 스레드를 __do_fork와 함께 Ready_list에 올려줌
2. sema_down에서는 sema_down(&child->sema_fork); 를 통해 부모 기준의 child_elem을 block 후 sema_waiters에 올려줌

——————— 시간이 지난 후 ———————
3. 1번 과정에서 ready_list에 올라간 자식 스레드가 ready_list에서 나와 running이 됨

———————- do _ fork 실행 ——————
4. fork 하는 과정을 거친 후에 만들어진 fork된 자식 스레드가 sema_up으로 들어감
5. 자식스레드는 list_pop을 통해 sema_waiters에서 빠져 나오고
6. 완전체(?)된 상태로 ready_list에 들어가고
7. 자연스레 부모 기준의 자식 스레드는 process_fork에서 sema_down을 빠져나와 자식값을 반환함

exec

case SYS_EXEC :
	{
		f->R.rax = exec(f->R.rdi);
		break;
	}

 

/* System Call 3 : Exec */
int exec (const char *file){
	char *fn_copy;
	
	// 먼저 인자로 받은 file_name 주소의 유효성을 확인
	check_address(file);

	/* Make a copy of FILE_NAME.
	 * Otherwise there's a race between the caller and load(). */
	// palloc_get_page() 함수와 strlcpy() 함수를 이용하여 file_name을 fn_copy로 복사
	fn_copy = palloc_get_page(PAL_USER);
	if (fn_copy == NULL)
		return TID_ERROR;
	strlcpy(fn_copy, file, PGSIZE);


	if (process_exec(fn_copy)==-1) {
		exit(-1);
	}
}
  • fn_copy를 통해서 page를 새로 생성 (PAL_USER를 인자로)
  • fn_copy에 file을 저장시킴


process_exec

/* Switch the current execution context to the f_name.
 * Returns -1 on fail. */
int
process_exec (void *f_name) {	// f_name = 'args-single onearg'
	char *file_name = f_name;
	bool success;

	// Setup virtual address of the program: code, data, stack (user stack) (추측)
	/* 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 */
	// file_name : 프로그램(실행파일) 이름

	success = load (file_name, &_if);

	palloc_free_page (file_name);
	/* If load failed, quit. */
	if (!success)	//메모리 적재 실패시 -1 반환
		return -1;

	// hex_dump(_if.rsp,_if.rsp,USER_STACK-_if.rsp,true);
	
	/* Start switched process. */
	// 성공하면 유저 프로그램을 실행한다
	// do interrupt return

	do_iret (&_if);
	
	NOT_REACHED ();
}
  • process_exec 실행시
    • f_name으로 된 file_name을 생성하고
    • 유저영역과 데이터 영역에 대한 인터럽트프레임을 초기화함
    • 현재 실행중인 프로세스를 종료
  • load를 실행하여 success 값을 받음
  • 성공시 do_iret(&_if)을 수행 ⇒ 커널 → 사용자로 컨텍스트 스위칭

wait

/* System Call 4 : Wait */
int wait (tid_t pid)
{

	return process_wait(pid);


process_wait

/* Waits for thread TID to die and returns its exit status.  If
 * it was terminated by the kernel (i.e. killed due to an
 * exception), returns -1.  If TID is invalid or if it was not a
 * child of the calling process, or if process_wait() has already
 * been successfully called for the given TID, returns -1
 * immediately, without waiting.
 * 
 * This function will be implemented in problem 2-2.  For now, it
 * does nothing. */
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. */


	// struct thread *parent = thread_current();
	struct thread *child = get_child_process(child_tid);
	/* 1) TID가 잘못되었거나 2) TID가 호출 프로세스의 자식이 아니거나*/
	if (child==NULL)
		return -1;

	
	/* 3) 지정된 TID에 대해 process_wait()이 이미 성공적으로 호출된 경우 */
	if (child->is_waited_flag==true)
		return -1;

	child->is_waited_flag=true;

	/* 자식프로세스가 종료될 때 까지 부모프로세스 대기(세마포어이용) */
	sema_down(&child->sema_wait);
	int exit_status = child->process_exit_status;
    
	/* 자식프로세스 디스크립터 삭제*/
	remove_child_process(child);
	sema_up(&child->sema_free); // wake-up child in process_exit - proceed with thread_exit
	return exit_status;
}
  • 자식 tid를 기다리기 위한 함수를 선언하는데, 여기서 중요한 점은 각각의 예외 분기와 is_waited_flag를 바꿔주는 것임
  • 해당하는 점은 조건에 맞게 구현하면 문제가 없음
  • 다만 여기서 헷갈리는 점은 sema_down과 sema_up이 같이 쓰이는데
    • 첫번째 sema_down의 경우 일반적인 sema_wait으로 자식프로세스가 종료될때까지 부모 프로세스가 대기하는 것이고
    • 두번째 sema_up의 경우는 자식이 종료되기 전에 부모가 exit status를 받기 위해서 사용됨

 


2. 참고자료 

- CSAPP : 3장, 7장, 8장, 12장
- Operating Systems : Three Easy Pieces - File, Filesystem, fork, wait, exec
- 혼자공부하는 컴퓨터 구조 + 운영체제
- 각종 블로그 등

 

도움이 많이 된 블로그

- Woony's Growth Insight 

 

PintOS Project 2 - User Program (8) System Call(정글사관학교 71일차 TIL) - 프로세스 관련 system call 구현

대망의 마지막 시스템 콜 파트.. 하지만 구현이 전부가 아니다. 구현 100점 하면 뭐하나. 이해 못하면 말짱 꽝이다. 어차피 정답은 검색하면 다 나온다. 내가 해야 할 것은 이게 왜 맞고 틀린지, 뭐

woonys.tistory.com

- Pintos 개발일지

 

개발 노트

A new tool for teams & individuals that blends everyday work apps into one.

rohagru.notion.site

 

팀원들의 개발일지 공유

- Haydn389

 

PintOS Project

PintOS Project 1 - Thread

www.notion.so

- YoungwooKim09

 

[Pintos Project] User Programming - Argument Passing

Argument Passing PintOS의 process_exec() 함수의 인자를 file name과 argument로 pasing하여 전달해주도록 구현한다. 단순히 방법만을 따라서 구현하다 보니 parsing하여 전달된 인자들이 어떠한 의미를 가지는지

sappire-9.tistory.com