[Jungle] Week9. Pintos Project 2. System Calls

somi·2024년 5월 30일
1

[Krafton Jungle]

목록 보기
60/68

글을 쓰고 있는 현재 시점(24.5.30)에서 프로젝트 2 테스트를 모두 통과하지는 못했지만 그래도 큰 흐름과 개념을 잡는 것이 더 중요하기 때문에 간단하게라도 정리를 해본다!!

User Memory Access

시스템 콜 과제 구현에 앞서서 먼저 주소 유효성 검사를 하는 함수를 만들어야 한다.
시스템 콜을 구현할 때 사용자 가상 주소 공간에서 데이터를 쓰게끔 하는 것이 중요하기 때문
만약 유저 영역을 벗어난 경우 프로세스를 종료시켜야 한다. -> 유효하지 않은 포인터나 커널 메모리로 접근을 시도하는 경우 예외 처리를 하게끔 먼저 함수를 구현해야 한다!

void check_address(void *addr) {
	/*포인터가 가리키는 주소가 유저 영역의 주소인지 확인
	잘못된 접근일 경우 프로세스 종료*/

	struct thread *cur = thread_current();
	
	if (!(is_user_vaddr(addr)) || pml4_get_page(cur-> pml4, addr) == NULL || addr == NULL) {
		exit(-1);
	}

	/*is_user_vaddr(addr): 사용자 영역의 가상 메모리 주소인지 확인하는 함수 
	pml4_get_page(t->pml4, addr): 가상 메모리 주소의 유효성 판단 -> 해당 주소에 매핑된 페이지가 있는지 확인
		=> NULL 반환하면 가상 주소가 유효하지 않거나 접근할 수 없는 영역*/
}

시스템 콜 핸들러 구현

사용자 프로그램의 system call 호출 - 인터럽트를 발생시키는 명령 실행 -> 커널 모드로 진입
-> interrupt vector table에서 해당 인터럽트 번호에 매핑된 핸들러를 찾아서 실행
-> user stack에서 시스템 콜 번호와 해당 번호에 맞는 시스템 콜 함수 실행
-> 시스템 콜 처리
-> 각 시스템 콜 함수는 필요한 작업 수행후에 결과를 사용자 프로그램에 return

/* The main system call interface */
void
syscall_handler (struct intr_frame *f UNUSED) {
	// TODO: Your implementation goes here.
	/*User Stack에 저장되어 있는 시스템 콜 넘버를 이용해서 시스템 콜 핸들러 구현
	스택 포인터가 유저 영역인지 확인
	저장된 인자 값이 포인터일 경우 유저 영역의 주소인지 확인
	0: halt, 1: exit . . . */

	int syscall_num = f->R.rax; //rax: system call number

	/* 
	인자 들어오는 순서:
	1번째 인자: %rdi
	2번째 인자: %rsi
	3번째 인자: %rdx
	4번째 인자: %r10
	*/

	switch (syscall_num) {
		case SYS_HALT:
			halt();
			break;
		case SYS_EXIT:
			exit(f->R.rdi);
			break;
		case SYS_FORK:
			f->R.rax = fork(f->R.rdi, f);
			break;
		case SYS_EXEC:	
			if (exec(f->R.rdi)== -1) {
				exit(-1);
			}
			// f->R.rax = exec(f->R.rdi);
			break;
		case SYS_WAIT:
			f->R.rax = wait(f->R.rdi);
			break;
		case SYS_CREATE:
			f->R.rax = create(f->R.rdi, f->R.rsi);
			break;
		case SYS_REMOVE:
			f-> R.rax = remove(f->R.rdi);
			break;
		case SYS_OPEN:
			f->R.rax = open(f->R.rdi);
			break;
		case SYS_FILESIZE:
			f->R.rax = filesize(f->R.rdi);
			break;
		case SYS_READ:
			f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
			break;
		case SYS_WRITE:
			f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
			break;
		case SYS_SEEK:
			seek(f->R.rdi, f->R.rsi);
			break;
		case SYS_TELL:
			f->R.rax = tell(f->R.rdi);
			break;
		case SYS_CLOSE:
			close(f->R.rdi);
			break;
		default: 
			thread_exit();
			break; 
	}
}

시스템 콜 핸들러는 운영체제의 커널 내부에 구현되어 있다. 사용자 프로그램이 시스템 콜을 요청하면 운영체제는 인터럽트나 트랩을 통해 커널모드로 전환한다.
이때 시스템 콜 핸들러가 활성화되어서 user stack에서 시스템 콜 번호와 인자들을 읽어서 해당하는 시스템 콜 함수를 호출하고 이를 통해 사용자 프로그램은 안전하게 커널이 제공하는 서비스를 활용할 수 있게 된다.

  • 시스템 콜 핸들러에서 시스템 콜 번호에 해당하는 시스템 콜 호출
    : 시스템 콜 번호, 필요한 인자들이 user stack에 저장됨 -> 해당 번호에 맞는 시스템 콜 함수 호출
  • 시스템 콜 핸들러에서 유저 스택 포인터(rsp)의 주소와 인자가 가리키는 주소(포인터)가 유저 영역인지 확인
    => 유저 영역을 벗어난 경우 page fault 발생
  • 유저 스택에 있는 인자들을 커널에 저장
    : 시스템 콜 핸들러는 사용자 스택에 있는 인자들을 커널 메모리로 복사한다.
  • 시스템 콜의 함수의 리턴 값은 인터럽트 프레임의 rax에 저장

    intr_frame: 시스템 콜이 호출될 때 생성된 구조체 -> 시스템 콜이 완료된 후 사용자 프로그램으로 복귀할 때 필요한 정보를 담음!


프로세스 간의 부모와 자식 관계?


1. 새로운 프로세스를 생성하는 가장 일반적인 방법은 기존 프로세스가 fork와 같은 시스템 콜을 통해 자식 프로세스를 만드는 것이다.

왜?

fork()는 현재 실행 중인 프로세스를 복제해서 새로운 프로세스를 만든다. 초기 설정, 자원 할당 과정이 상대적으로 더 효율적이고 빠르다!

  • 부모 프로세스의 코드, 데이터, 힙, 스택 등의 메모리 영역을 모두 복제한다.
  • 하지만 자식 프로세스도 별도의 독립된 주소 공간을 가지기 때문에 부모와 자식 프로세스가 독립적으로 동작할 수 있다.
  • 자식 프로세스는 고유한 PID를 가짐

예시

  • 부모 프로세스 - 클라이언트 요청을 수신하고 새로운 연결이 들어오면 fork 통해서 자식 프로세스 생성
  • 새로운 자식 프로세스는 특정 클라이언트의 요청을 처리하고 요청 처리 끝나면 종료
  1. exec() 시스템 콜
    : 현재 프로세스의 메모리 공간을 새로운 프로그램으로 덮어쓰고 실행을 시작한다. 기존 프로세스의 주소 공간을 새로운 프로그램의 주소 공간으로 대체한다.
  • 현재 프로세스의 코드, 데이터, 힙, 스택 등을 새로운 프로그램의 내용으로 교체한다.
  • exec()은 새로운 프로그램, 새로운 코드를 실행하지만 기존 프로세스의 PID는 변하지 않고 그대로 유지된다.

핀토스에서의 우리의 과제)

  • 프로세스 간의 부모 자식 관계를 구현하고, 부모가 자식 프로세스의 종료를 대기하는 기능을 구현해야 한다.

지금의 핀토스는 프로세스 구조체에서 부모/자식 관계를 명시하는 정보가 없다.
그래서 자식의 시작/종료 전에 부모 프로세스가 종료되고 있다.



부모 프로세스와 자식 프로세스, 프로세스 간의 계층 구조를 우리는 어떻게 구현할 것인가?

: 부모와 자식 관계를 나타낼 수 있는 필드를 thread 구조체 내에 포함시킨다.

 struct intr_frame tf; /* Information for switching */
  unsigned magic;       /* Detects stack overflow. */

  struct intr_frame parent_if; /*부모 프로세스의 인터럽트 프레임*/
  struct list child_list; /*자식 프로세스 리스트*/
  struct list_elem child_elem; /*자식 프로세스 리스트의 element*/

  struct file *running; //현재 실행 중인 파일 
  int exit_status; //프로세스의 종료 유무 확인

  struct semaphore wait_sema; //자식 프로세스가 종료될 때까지 대기 - 종료 상태 저장
  struct semaphore free_sema; //자식 프로세스가 종료될 떄까지 
  struct semaphore fork_sema; //fork 완료 될 때 sema_up

참고)
핀토스에서는 프로세스는 1개의 쓰레드로 구성되어 있다. 그러니까 pid, tid를 여기서는 동일

fork()?

process_fork

현재 쓰레드를 복제하여 새로운 쓰레드 생성
-name:새로운 쓰레드(자식 프로세스) 이름
-if_: 인터럽트 프레임 주소, 현재 쓰레드의 상태를 자식 쓰레드에 복사하기 위해 사용한다.


tid_t
process_fork (const char *name, struct intr_frame *if_ UNUSED) {
	/* Clone current thread to new thread.*/
	struct thread *parent = thread_current(); //부모 쓰레드 - 현재 쓰레드 

	if (if_ != NULL) {
		//부모 쓰레드의 인터럽트 프레임 복사 
		memcpy(&parent->parent_if, if_, sizeof(struct intr_frame));
	} else {
		return TID_ERROR;
	}

	//새로운 쓰레드(자식 프로세스) 생성 
	tid_t tid = thread_create(name, PRI_DEFAULT, __do_fork, parent);

	//새로운 쓰레드 생성 실패 시 return TID_ERROR 
	if (tid == TID_ERROR) {
		return TID_ERROR;
	}

	//생성된 자식 쓰레드를 tid를 통해서 찾음
	struct thread *child = get_child_process(tid);
	 // 자식 쓰레드의 fork semaphore를 대기 상태로 만들어서 실행 일시 중지
	 //__do_fork 함수가 실행되어 로드가 완료될 때까지 부모는 대기한다. 
	
	if (child == NULL ){
		return TID_ERROR;
	}

	sema_down(&child->fork_sema);
	
	if (child->exit_status == TID_ERROR) {
		return TID_ERROR;
	} 
	return tid;
}

thread_current => 현재 쓰레드는 부모 쓰레드

memcpy(&parent->parent_if, if_, sizeof(struct intr_frame)); => 부모 쓰레드의 인터럽트 프레임을 새로운 자식 쓰레드에 복사한다.

intr_frame: cpu 레지스터, 스택 포인터, 프로그램 카운터 등 현재 쓰레드의 실행 상태를 포함함

=> 자식 쓰레드가 생성될 떄 부모 쓰레드의 실행 상태 그대로 가져갈 수 있게끔. 자식 쓰레드가 동일한 실행 흐름을 가질 수 있게.

그리고
sema_down(&child->fork_sema);
해당 자식 프로세스가 로드될 때까지 부모프로세스가 기다릴 수 있도록 semaphore를 사용한다. 세마포어의 값이 0이 될 때까지 부모 프로세스는 blocked 상태가 된다.

자식 프로세스가 __do_fork 함수 안에서 sema_up으로 신호를 보내면 부모 쓰레드는 깨어나서 Ready 상태로 전환된다.


thread_create

struct thread *cur = thread_current();
list_push_back(&cur->child_list, &new_t->child_elem);

thread_create 함수 안에서 자식 프로세스를 현재 쓰레드의 자식 리스트에 추가해준다.


duplicate_pte

: 현재 실행 중인 자식 쓰레드가 부모 쓰레드에서 페이지를 복제.
메모리 공간에서 부모 프로세스의 페이지를 자식 프로세스로 복제하는 함수이다.

-> Page Table Entry?
가상 메모리를 물리 메모리로 매핑하는 과정에서 사용되는 자료구조. 각 엔트리는 해당 페이지가 물리 메모리의 어느 위치에 있는지, 페이지가 읽기 전용인지 쓰기 가능한지 등의 정보가 포함되어 있음

  • 물리 메모리 주소
  • valid bit
  • 읽기/쓰기 권한
  • 캐싱 정책
  • 참조 비트, 수정 비트: 참조되거나 수정된 적이 있는지

=> 부모 프로세스의 페이지가 쓰기 가능한지 확인하고, 자식 프로세스에게 동일한 권한을 부여하기 위해 필요하다.

/* 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. */
	//만약 va가 커널 주소라면 즉시 return true 
	if (is_kernel_vaddr(va)) {
		return true;
	}

	/* 2. Resolve VA from the parent's page map level 4. */
	parent_page = pml4_get_page (parent->pml4, va); //부모의 pml4에서 va에 해당하는 페이지 가져옴
	//만약 부모 페이지가 null이면 return false
	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);//자식 프로세스를 위해 새로운 PAL_USER 페이지 할당 
	//만약 페이지 할당에 실패한다면 false return
	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); //부모의 페이지를 새 페이지로 복사

	writable = is_writable(pte); //페이지 테이블 엔트리(pte)를 통해서 부모 페이지가 쓰기 가능한지 확인
	// writable = (*pte & PTE_W) != 0;  //비트 연산(&)을 통해 페이지 쓰기가 가능한지 확인

	/* 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. */
		palloc_free_page(newpage); //할당한 페이지 해제
		return false;
	}
	return true;
}

__do_fork

부모 프로세스의 상태를 자식 프로세스에 복제하고 필요한 자원을 복제해서 자식 프로세스가 독립적으로 실행할 수 있도록

__do_fork(): 부모 프로세스로부터 자식 프로세스를 생성하는 작업 수행 - 인자 aux는 부모 쓰레드이다.

static void
__do_fork (void *aux) {
	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; //부모의 인터럽트 프레임을 가리킬 포인터 
	bool succ = true;

	parent_if = &parent->parent_if; //부모 프로세스의 intr_frame 포인터 

	/* 1. Read the cpu context to local stack. */
	/* 부모 프로세스의 CPU 컨텍스트를 자식 프로세스의 로컬 스택에 복사*/
	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->next_fd_idx == FDT_COUNT_LIMIT) //파일 디스크립터 테이블이 가득 찬 경우 에러 처리
		goto error;

	
	//표준 파일 디스크립터(0,1)을 제외하고 복제
	for (int fd = 2; fd < FDT_COUNT_LIMIT; fd++){
		struct file *file = parent->fdt[fd]; //부모의 파일 디스크립터를 가져옴

		if (file == NULL)
			continue;

		current->fdt[fd] = file_duplicate(file); //파일 복제 
	}

	current ->next_fd_idx = parent->next_fd_idx; // 다음 파일 디스크립터의 인덱스 설정
	sema_up(&current->fork_sema); //자식 프로세스가 준비되었음을 부모에게 알림
	if_.R.rax = 0; //자식 프로세스의 리턴값 0으로 설정
	process_init ();

	/* Finally, switch to the newly created process.
	 	새로 생성된 프로세스로 전환*/
	if (succ)
		do_iret (&if_);
error:
	// succ = false;
	sema_up(&parent->fork_sema); //에러 발생시 세마포어를 올려, 부모가 기다리지 않도록 한다. 
	exit(TID_ERROR); //에러 발생 시 자식 프로세스 종료시킴
	// thread_exit ();
}

자식 프로세스에 부모의 인터럽트 프레임을 복사해서 넣어주고, 부모 프로세스의 페이지 테이블을 자식 프로세스의 페이지 테이블로 복사해준다.

memcpy(newpage, parent_page, PGSIZE);

이후에 부모 프로세스에서 자식 프로세스의 복사가 다 완료되면
sema_up(&parent->fork_sema)을 통해서 부모 프로세스에게 알려준다.

이후 do_iret을 통해서 새로운 자식 프로세스의 인터럽트 프레임으로 전환되어 자식 프로세스가 실행된다.

부모 프로세스의 상태를 복사해서 독립적으로 자식이 실행흐름이 나아갈 수 있도록.


get_child_process

//현재 쓰레드의 자식 쓰레드 중에서(child_list) 주어진 pid와 일치하는 자식 쓰레드를 찾아서 return 
//int pid => 찾고자 하는 자식 프로세스 식별자 
// 주어진 자식 프로세스 식별자(pid)에 해당하는 쓰레드 구조체 검색 - 존재하지 않으면 NULL return
struct thread *get_child_process (int pid){
	struct thread *cur = thread_current();

	struct list *child_list = &cur->child_list; //현재 쓰레드의 자식 쓰레드 목록

	//자식 쓰레드 목록을 순회하면서 주어진 pid와 일치하는 자식 쓰레드가 있는지 찾는다.
	for (struct list_elem *e = list_begin(child_list); e != list_end(child_list); e = list_next(e)){
		struct thread *t = list_entry(e, struct thread, child_elem);
		
		//일치하는 자식 프로세스 반환
		if (t->tid == pid){ 
			return t;
		}
	}
	return NULL; //일치하는 자식 쓰레드가 없을 경우 return NULL

Wait()?

process_wait

: 자식 프로세스가 종료될 때까지 부모 프로세스가 기다리는 함수

대기하려는 자식의 tid를 인자로 받는다.

int
process_wait (tid_t child_tid UNUSED) {
	//자식 프로세스의 쓰레드 구조체 child 
	struct therad *cur = thread_current();
	int result = -1;

	struct thread *child = get_child_process(child_tid);

	//자식 프로세스가 존재하지 않는다면 return -1
	if (child == NULL) {
		return -1;
	}
	//자식 프로세스가 종료될 때까지 대기 
	sema_down(&child->wait_sema);
	result = child->exit_status;

	//자식 프로세스의 exit_status를 가져옴. 
	// int exit_status = child->exit_status;
	
	//자식 프로세스를 child_list에서 제거
	list_remove(&child->child_elem);

	//자식 프로세스를 해제할 수 있도록 신호를 보냄
	sema_up(&child->free_sema);

	//자식 프로세스의 exit_status를 return
	// return exit_status;
	return result;
}

자식 프로세스가 성공적으로 종료될 때까지 부모 프로세스는 대기한다.

자식 프로세스가 process_exit 함수를 통해 종료되고 wait_sema up이 되면 부모 프로세스는 ready 상태가 된다.
그리고 부모 프로세스는 child_list에서 해당 자식을 제거하고
free_sema down을 통해 자식 프로세스의 리소스를 완전히 해제할 수 있게 된다. -> 부모 프로세스는 자식 프로세스의 종료 상태를 확실하게 알 수 있다.

void
process_exit (void) {

	struct thread *cur = thread_current ();

	file_close(cur->running);
	cur->running = NULL;

	//프로세스에 열린 모든 파일을 닫음
	for (int i = 0; i < FDT_COUNT_LIMIT; i++){
		close(i);
	}

	palloc_free_multiple(cur->fdt, FDT_PAGES);

	process_cleanup ();

	//부모 프로세스가 프로세스가 종료되었음을 알 수 있게 wait_sema up
	sema_up(&cur->wait_sema);

	//부모 프로세스가 free_sema 자원을 해제할 때까지 대기 
	sema_down(&cur->free_sema);
	
}
profile
📝 It's been waiting for you

0개의 댓글

관련 채용 정보