pintos project 2: USER PROG - fork와 wait

Jifrozen·2024년 10월 8일

정글

목록 보기
3/4

fork 요구사항

  • 현재 프로세스를 복제하여 THREAD_NAME이라는 이름을 가진 새로운 프로세스를 생성해야 합니다.

→ thread_create 이름은 스레드를 생성하는 함수이지만, pintos와 같은 커널에서는 스레드와 프로세스의 구분이 명확하지 않아 thread_create함수를 사용하여 프로세스를 생성함

스레드의 경우 메모리 영역을 공유하지만

프로세스는 독립적인 주소 공간을 생성해야해서 thread 구조체에서 필드 추가가 필요함

  • 레지스터 값은 %RBX, %RSP, %RBP, %R12부터 %R15까지인 callee-saved register만 복제해야 합니다.→ 🚨 레지스터 값을 쟤네만 복제하라는 뜻이 아님!→ general purpose register 중에서는 callee-saved register 제외하고는 백업 안해도 된다.즉, %rip, %ds, %ss, %es, %cs 등 특수한 역할을 하는 레지스터들도 복사를 해줘야한다!

→ callee-saved 레지스터는 보존되어야 하는 레지스터들이기 때문에 부모에서 복제된 자식도 그대로 가져가야함 caller의 경우 바뀔수있기 때문에 완전한 백업은 필요없고 특수 레지스터들의 경우만 백업하면 된다.

  • 함수는 자식 프로세스의 pid를 반환해야 하며, 그렇지 않은 경우 유효한 pid가 될 수 없습니다.
  • 자식 프로세스에서는 반환 값이 0이어야 합니다.

→ fork 함수를 통해 부모가 자식을 생성하는데 생성된 자식도 똑같은 코드 위치를 가리키기 때문에 fork를 실행하게됨 그러면 fork는 커널상에서 부모한테 pid를 반환하고 자식한테는 0을 반환함

  • 자식 프로세스가 파일 디스크립터와 가상 메모리 공간을 포함한 리소스를 복제해야 한다는 의미
    • 파일 디스크립터
      • 자식 프로세스는 부모 프로세스가 열어둔 파일 디스크립터들을 그대로 복제해야힘
      • 부모 프로세스가 열어놓은 파일들을 자식프로세스도 동일하게 접근해야해서 파일 디스크립터도 동일하게 가짐
      • 엑세스 권한도 동일
    • 가상 메모리 공간 복제
      • 부모 프로세스의 메모리 공간을 자식 프로세스도 동일하게 복제해야함
      • 동일한 메모리 상태에서 실행
  • 부모 프로세스는 자식 프로세스가 성공적으로 복제되었는지 알 때까지 fork() 호출에서 반환되지 않아야 합니다.
    • 부모 프로세스는 자식 프로세스가 리소스 복제를 모두 완료하고 준비가 될 때까지 기다려야 합니다.
    • 반환 값으로 TID_ERROR를 반환하여 자식 생성 실패
  • 해당 템플릿은 threads/mmu.c 파일의 pml4_for_each() 함수를 사용하여 사용자 메모리 공간 전체와 해당 페이지 테이블 구조를 복사합니다. /
    • pte_for_each_func의 누락된 부분을 채워주어야 합니다.
    - pml4_for_each() 함수는 프로세스의 페이지 테이블에 있는 모든 페이지를 순회(탐색)하면서, 특정한 작업을 수행하는 함수
    - fork는 부모와 자식프로세스가 동일한 메모리 상태로 시작해야해서 페이지 테이블도 복사해야함
    - "누락된 부분을 채워주어야 한다"는 것은, pte_for_each_func에서각 페이지 항목에 대해 어떤 작업을 수행할지 정의해야 한다는 것을 의미합니다.
    - 예를 들어, 페이지를 복사할 때는 부모의 페이지를 읽어서 자식의 페이지 테이블에 새로운 페이지 항목을 추가해야 합니다.
    - 또는, 부모가 가지고 있는 가상 메모리 주소에 해당하는 물리 메모리 페이지를 복사해서 자식도 같은 내용을 가지도록 설정
    - pml4_for_each() 함수는 페이지 테이블을 순회하며 각 페이지 항목에 대해 작업을 수행하는 역할을 합니다.
    - pte_for_each_func는 누락된 부분을 채워주어야 한다는 것은, 각 페이지 테이블 항목을 어떻게 복사할지 또는 어떤 작업을 수행할지 정의해야 한다는 의미입니다. → 읽기 쓰기 권한같은 페이지 속성이나 매핑된 물리 주소를 복사

fork 구현

1) syscall_handler()

	switch (syscall_number)
	{
	case SYS_FORK:
		f->R.rax = fork(f->R.rdi, f);
		break;

R.rdi는 첫번째 인자를 가리킴 fork를 호출하는 사용자 영역의 process_fork의 함수를 보면 첫번째 인자가 이름임

2) fork()

/* userprog/syscall.c */

int fork(const char *thread_name, struct intr_frame *f); // 선언

int fork(const char *thread_name, struct intr_frame *f)
{
    return process_fork(thread_name, f);
}

3) process_fork()

→ 현재 프로세스를 복제하여 name이라는 이름의 프로세스를 만들고, 새로 만든 프로세스의 스레드 ID를 리턴한다.

tid_t process_fork(const char *name, struct intr_frame *if_ UNUSED)
{
    /* Clone current thread to new thread.*/
    return thread_create(name,
                         PRI_DEFAULT, __do_fork, thread_current());
}

3-1) if_를 parent_if에 복사

현재 스레드에서 인자로 전달받은 if_를 저장할 수 있도록 필드를 새로 만든다.

 parent->tf에는 userland context가 들어있지 않으니 process_fork의 두번째 인자를 이 함수에 전달할 수 있게 해야 한다고 말하고 있다.

parent->tf에는 userland context가 들어있지 않을까?

fork를 하면 자식은 부모의 메모리 영역 tf를 물려받아야하는데, 지금은 fork를 수행하면서 context switching이 일어났기 때문에 현재 부모의 tf에는 사용자 영역의 메모리가 아닌 커널이 작업하던 정보가 저장되어 있다.

자식에게는 부모의 user-level 부모 프로세스의 작업하던 메모리를 물려줘야하기 때문에

sys handler의 인자로 넘겨주는 f를 기반으로 복사해야한다.

→ 커널영역이 필요없는 이유는 사용자 모드에서 이어서 실행해야하니깐

struct thread

struct thread{

    struct intr_frame parent_if; // 부모의 메모리 영역을 저장할
    struct list child_list; // 자식 스레드 리스트
    struct list_elem child_elem; // 자식 리스트에 저장할 list_elem

		struct semaphore load_sema; // 부모가 자식을 기다리면서 재워야하는 세마포어
}

스레드 구조체에 부모 프로세스에서 가져온 if를 저장할 필드를 만든다.

process_fork

/* 현재 프로세스를 `name`으로 복제합니다. 새 프로세스의 스레드 ID를 반환하거나,
 * 스레드를 생성할 수 없으면 TID_ERROR를 반환합니다. */
tid_t process_fork(const char *name, struct intr_frame *if_ UNUSED)
{
	struct thread *cur = thread_current();
	// if 복사하기
	memcpy(&cur->parent_if, if_, sizeof(struct intr_frame));
	/* 현재 스레드를 새로운 스레드로 복제합니다. */
	// 스레드 생성하고 실행할 함수로 __do_fork를 지정
	// 인자로 부모가 될 스레드를 넣어준다.
	tid_t pid = thread_create(name, PRI_DEFAULT, __do_fork, cur);
	if (pid == TID_ERROR)
		return TID_ERROR;
	// 자식이 로드될 때까지 대기하기 위해서 방금 생성한 자식 스레드를 찾는다.
	struct thread *child = get_child_process(pid);

	sema_down(&child->load_sema);

	return pid;
}

thread.c

init_thread

static void
init_thread(struct thread *t, const char *name, int priority)
{
...

    list_init(&(t->child_list));
}

thread_create

list_push_back(&thread_current()->child_list, &t->child_elem);

스레드를 생성하면 현재 스레드의 자식이 생기는거니깐 저장함

thread_create가 실행되면 자식 프로세스가 생성되고 ready_list에 들어감

그리고 스케줄러를 통해 실행되면 thread_create 함수에 넣어준 dofork 함수가 호출되어 load가 진행됨

부모는 이 load가 완료될때까지 기다려야함 → 세마포어를 통해 재워야함

init_thread

/* threads/thread.c */
static void
init_thread(struct thread *t, const char *name, int priority)
{
...

    sema_init(&t->load_sema, 0);
}

세마포어 초기화

// 자식이 로드될 때까지 대기하기 위해서 방금 생성한 자식 스레드를 찾는다.
struct thread *child = get_child_process(pid);

sema_down(&child->load_sema);

자식이 로드될 때까지 대기하기 위해서 방금 생성한 자식 스레드를 찾는다.

현재 스레드는 생성만 완료된 상태이고 생성되어서 ready_list에 들어가고 실행될 때 __do_fork 함수가 실행된다.

__do_fork 함수가 실행되어 로드가 완료될 때까지 부모는 세마포어를 통해 대기함

get_child_process

pid 부모 프로세스를 전달받아서 자식 스레드를 반환하는 함수를 새로 선언해줌

// tid는 단순히 스레드의 ID일 뿐이고,
// 실제로 해당 스레드의 데이터(예: 세마포어,
// 스택 프레임 등)에 접근하려면 그 스레드의 구조체 포인터가 필요합니다.
// 그래서 get_child_process()를 통해 자식 스레드의 구조체를 찾는 과정
struct thread *get_child_process(int pid)
{
    /* 자식 리스트에 접근하여 프로세스 디스크립터 검색 */
    struct thread *cur = thread_current();
    struct list *child_list = &cur->child_list;
    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);
        /* 해당 pid가 존재하면 프로세스 디스크립터 반환 */
        if (t->tid == pid)
            return t;
    }
    /* 리스트에 존재하지 않으면 NULL 리턴 */
    return NULL;
}

__do_fork 함수

4-1) parent_if 할당

static void
__do_fork(void *aux)
{
	struct intr_frame if_;
	// do_fork 넘겨줄때 부모 스레드 넘겨줌
	struct thread *parent = (struct thread *)aux;
	// _do_fork가 실행된거면 자식스레드가 스케줄러를 통해 실행중인거임
	// 그러니깐 현재 스레드는 자식스레드인거임
	struct thread *current = thread_current();
	/* TODO: 어떻게든 parent_if를 전달합니다 (예: process_fork()의 if_) */
	// 부모 프로세스에 저장하고 있던 사용자 영역의 메모리를 넘겨줌
	struct intr_frame *parent_if = &parent->parent_if;
	bool succ = true;

	/* 1. CPU 컨텍스트를 로컬 스택으로 읽어옵니다. */
	// if_에 parent_if 넘겨줌
	memcpy(&if_, parent_if, sizeof(struct intr_frame));
	// 자식 프로세스의 리턴값은 0으로 지정한다.
	// 함수의 반환값을 저장하는 레지스터
	// 자식의 반환값을 지정
	// 자식은 fork 호출시 무조건 0을 반환받아야함
	// 그걸 여기서 설정해줌
	if_.R.rax = 0;

	/* 2. 페이지 테이블 복제 */
	// pml4_create() 함수는 새로운 페이지 맵 레벨 4 (PML4) 구조를 생성하는 역할
	// 각 프로세스마다 독립적인 주소 공간을 제공하기 위해 프로세스마다 고유한 PML4 생성
	current->pml4 = pml4_create();
	// 
	if (current->pml4 == NULL)
		goto error;
	// 새로운 스레드로의 컨텍스트 전환을 수행할 때, 그 스레드의 메모리 매핑과 CPU 상태
	// 프로세스 스레드간의 전환할때 CPU가 올바른 스택을 참조하도록 도와줌
	process_activate(current);
	
	//부모와 자식 프로세스가 동일한 메모리 구조를 가져야 하기 때문에 
	// 부모의 가상 메모리 매핑을 자식에게 복사해야함
	//보조 페이지 테이블(VM을 사용하는 경우)**와 
	// **페이지 맵 레벨 4(PML4)**를 복사하는 코드 둘중 하나
#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: 여기에 코드 작성
	 * TODO: 힌트) 파일 객체를 복제하려면 include/filesys/file.h의 `file_duplicate`를 사용하세요.
	 * TODO: 부모가 자원의 복제를 성공적으로 마칠 때까지 fork()에서 반환하면 안 됩니다. */

// 부모와 자식은 동일한 파일 디스크립터를 가져야함
//이를 통해 자식과 부모는 동일한 파일을 열고 동일한 상태를 이어서 실행할 수 있어야함
//FDT_COUNT_LIMIT**은 파일 디스크립터 테이블의 크기
	for (int i = 0; i < FDT_COUNT_LIMIT; i++)
	{
		struct file *file = parent->fdt[i];
		// 열려있는 파일만 복제하기 위함
		if (file == NULL)
			continue;
			// 표준 입력 출력 오류는 0 1 2를 가리킴 특별한 처리 필요 없으니깐 조건문 추가
		if (file > 2)
			file = file_duplicate(file);
		// 
		current->fdt[i] = file;
	}

	current->next_fd = parent->next_fd;

// 완전 초기화 과정
	process_init();
	// 자식 프로세스가 완전히 로드되었음 부모 깨움
	sema_up(&current->load_sema);

	/* 마지막으로 새로 생성된 프로세스로 전환합니다. */
	//  자식 프로세스의 준비가 성공적(succ == true)
	// do_iret(&if_)**를 호출하여 사용자 모드로 복귀
	if (succ)
		do_iret(&if_);
error:
	sema_up(&current->load_sema);
	thread_exit();
}
/* 다음 스레드에서 사용자 코드를 실행하기 위해 CPU를 설정합니다.
 * 이 함수는 매 컨텍스트 전환 시 호출됩니다. */
void process_activate(struct thread *next)
{
	/* 스레드의 페이지 테이블을 활성화합니다. */
	// 가상 메모리랑 물리 메모리 간의 매핑을 설정하는 것
	// 전환된 자식이 가상 주소 공간을 올바르게 사용 가능
	pml4_activate(next->pml4);

	/* 인터럽트 처리를 위해 스레드의 커널 스택을 설정합니다. */
	// tss는 
	tss_update(next);
}

TSS(Task State Segment)x86 아키텍처에서 사용되는 특수한 구조체로, 커널 스택 포인터와 같은 중요한 정보를 보관합니다. 이 정보를 사용하여 인터럽트가 발생했을 때 CPU가 참조해야 할 커널 스택의 위치를 알려줌

tss_update(next);는 현재 전환되는 스레드에 맞는 커널 스택 포인터를 설정하는 역할

시스템콜이나 인터럽트가 발생하면 cpu는 커널모드로 전환되어야하고 이때 올바른 커널 스택을 사용해야함 tss는 이런 상황에서 사용할 커널 스택을 CPU에 알려줌

#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

가상 메모리 사용 여부에 따른 차이

  • 가상 메모리를 사용하는 경우:
    • 메모리 부족 상황에서 페이지를 디스크로 스왑하거나, 필요할 때만 실제 메모리를 할당하는 등의 유연한 메모리 관리가 필요합니다. 이를 위해 보조 페이지 테이블(SPT)을 사용하여 페이지의 상태와 위치를 관리합니다.
    • 가상 메모리 시스템에서는 메모리의 실제 내용을 모두 복사하지 않고, 페이지의 메타데이터를 복사하여 효율적인 메모리 복사를 수행할 수 있습니다.
  • 가상 메모리를 사용하지 않는 경우:
    • 모든 페이지가 항상 메모리에 있기 때문에 페이지의 상태를 관리하는 보조 데이터 구조가 필요하지 않습니다.
    • 기본적인 페이지 테이블(PML4)을 복사하는 것으로 충분하며, 단순한 메모리 복사를 통해 자식 프로세스가 부모와 동일한 메모리 매핑을 가질 수 있도록 합니다.

duplicate_pte

부모 프로세스의 페이지 테이블 항목을 자식 프로세스에 복제하는 함수

부모 자식 프로세스가 동일한 메모리 공간을 공유하도록 보장함

가상 메모리 구조를 사용하지 않는 경우만 호출

부모 페이지 테이블을 순회하면서 각 페이지 항목을 자식의 페이지 테이블에 복사

static bool duplicate_pte(uint64_t pte, void va, void *aux)

  • uint64_t *pte: 부모의 페이지 테이블 항목을 가리키는 포인터입니다.
  • void *va: 가상 주소(VA)로, 현재 페이지 테이블 항목에 해당하는 가상 주소입니다.
  • void *aux: 부모 프로세스의 정보를 포함한 추가적인 인자(parent)입니다.
#ifndef VM
/* 부모의 주소 공간을 pml4_for_each 함수에 전달하여 복제합니다.
 * 이는 프로젝트 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: parent_page가 커널 페이지인 경우 즉시 반환합니다. */
	// 부모 페이지가 커널 페이지인지 확인 (is_kernel_vaddr(va))
	// 커널 페이지는 사용자 공간에서 접근할 수 없는 페이지로, 
	// 이 페이지를 자식 프로세스로 복제할 필요가 없음
	if (is_kernel_vaddr(va))
		return true;
	/* 2. 부모의 페이지 맵 레벨 4에서 VA를 해석합니다. */
	// 부모의 페이지 가져오기 (pml4_get_page())
	// 부모의 페이지 테이블(PML4)에서 **가상 주소 va**에 해당하는 물리 페이지 주소
	parent_page = pml4_get_page(parent->pml4, va);
	// parent_page 부모의 페이지 테이블에서 va를 매핑하는 실제 물리 메모리 주소
	if (parent_page == NULL)
		return false;
	/* 3. TODO: 자식에게 새로운 PAL_USER 페이지를 할당하고,
	 *    TODO: NEWPAGE에 결과를 설정합니다. */
	 // 자식에게 새로운 페이지 할당 (palloc_get_page())
	 // PAL_USER 사용자 공간에 할당할 페이지 임을 알려주고 PAL_ZERO 페이지를 0으로 초기화
	newpage = palloc_get_page(PAL_USER | PAL_ZERO);
	// newpage는 할당된 물리적 주소
	if (newpage == NULL)
		return false;
	/* 4. TODO: 부모의 페이지를 새로운 페이지로 복사하고,
	 *    TODO: 부모의 페이지가 쓰기 가능한지 확인합니다
	 *    TODO: (결과에 따라 WRITABLE을 설정합니다). */
	 // 부모의 페이지를 자식의 페이지로 PGSIZE만큼 복사
	memcpy(newpage, parent_page, PGSIZE);
	// 현재 페이지가 쓰기 가능한지 여부 저장 -> 자식에게 동일한 권한을 주기 위해서
	writable = is_writable(pte);
	/* 5. 자식의 페이지 테이블에 주소 VA로 WRITABLE 권한을 가진 새로운 페이지를 추가합니다. */
	// 자식 프로세스의 페이지 테이블에 가상 주소 va로 새로운 페이지(newpage)를 추가합니다.
	if (!pml4_set_page(current->pml4, va, newpage, writable))
	{
		/* 6. TODO: 페이지 삽입에 실패한 경우, 오류 처리를 수행합니다. */
		return false;
	}
	return true;
}
#endif
  • pml4_set_page
    bool pml4_set_page(uint64_t *pml4, void *upage, void *kpage, bool rw)
    • uint64_t *pml4: 페이지 맵 레벨 4(PML4) 포인터로, 프로세스의 최상위 페이지 테이블을 가리킵니다.

    • void *upage: 사용자 가상 주소(upage)로, 페이지를 매핑하려는 가상 주소입니다.

    • void *kpage: 물리 페이지 주소(kpage)로, 가상 주소가 매핑될 실제 물리 메모리 주소입니다.

    • bool rw: 페이지의 읽기/쓰기 권한을 설정하는 플래그입니다. true인 경우 쓰기 가능하고, false인 경우 읽기 전용입니다.

wait

부모 프로세스가 자식 프로세스의 종료를 기다리고, 그 자식이 종료될 때 종료상태를 얻기 위해 사용됨

그 자식이 종료될 때 종료 상태를 얻기 위해 사용

부모 프로세스는 자식의 성공적인 종료 여부를 알 수 있음, 자식이 작업을 완료하고 자원을 해제하도록 보장

  1. 자식 프로세스의 종료를 기다리고 종료 상태를 반환

wait() 함수는 부모가 자식 프로세스의 종료를 기다리고, 종료가 되면 그 종료 상태(exit status)를 반환 → 아직 종료안되어있으면부모 대기

자식이 종료되면 자식의 종료 상태 반환

  1. 예외로 종료된 자식에 대한 처리

자식 프로세스가 exit() 함수 호출하지 않고 예외로인해 종료된경우 -1반환

  1. 직접적인 자식 프로세스만 기다릴 수 있음
  • 부모 프로세스는 직접적인 자식 프로세스만 기다릴 수 있습니다.
  • 예를 들어, 프로세스 A가 자식 B를 생성하고, B가 자식 C를 생성한 경우, 프로세스 A는 프로세스 C를 기다릴 수 없습니다.
    • 이는 자식은 상속되지 않는다는 원칙 때문입니다.
    • B가 종료되더라도 A는 C를 기다릴 수 없으므로, wait(C) 호출은 실패해야 하며, 이 경우 1을 반환해야 합니다.
  1. 고아 프로세스 처리
  2. 중복 대기 방지

부모 프로세스가 특정 자식에 대해

한 번만 기다릴 수 있습니다

.

  • 만약 부모가 이미 한 번 wait()를 호출하여 자식의 종료 상태를 얻었다면, 다시 그 자식에 대해 wait()를 호출할 수 없습니다.
  • 이러한 중복 대기를 방지하여, 한 번만 기다릴 수 있게 구현
  1. 부모와 자식 관계
  • 부모 프로세스는 여러 자식 프로세스를 생성할 수 있으며, 이들을 어떤 순서로든 기다릴 수 있습니다.
  • 부모는 일부 또는 전체 자식에 대해 기다리지 않고 종료할 수도 있습니다.
  • 이 경우에도 자식 프로세스가 올바르게 종료되고 자원이 해제되도록 보장
  1. 모든 자원 해제 보장
  • struct thread를 포함한 프로세스의 모든 리소스는, 부모가 자식을 기다리는지 여부와 관계없이 올바르게 해제되어야 합니다.
  • 즉, 자식이 먼저 종료되었더라도 부모가 그 자식을 기다리지 않는 경우에도, 자식의 자원이 올바르게 해제되어야 합니다.
  • 이는 메모리 누수
  1. 시스템 종료 관리
  • Pintos가 종료되기 위해서는 initial process가 종료되어야 합니다.
  • main() 함수에서 process_wait()를 호출하여 initial process가 종료될 때까지 Pintos가 종료되지 않도록 보장합니다.
  • 이 부분은 시스템의 정상적인 종료

1) syscall_handler()

void syscall_handler(struct intr_frame *f UNUSED)
{
...

case SYS_WAIT:
        f->R.rax = wait(f->R.rdi);
        break;
}

2) wait()

int wait(int pid); // 선언

int wait(int pid)
{
    return process_wait(pid);
}

3) process_wait()

int process_wait(tid_t child_tid UNUSED)
{
	/* XXX: 힌트) Pintos가 process_wait(initd)일 때 종료됩니다.
	 * XXX: process_wait을 구현하기 전에 여기에 무한 루프를 추가하는 것을 추천합니다. */
	struct thread *child = get_child_process(child_tid);
	if (child == NULL) // 1) 자식이 아니면 -1을 반환한다.
        return -1;

    // 2) 자식이 종료될 때까지 대기한다. (process_exit에서 자식이 종료될 때 sema_up 해줄 것이다.)
    sema_down(&child->wait_sema);
    // 3) 자식이 종료됨을 알리는 `wait_sema` signal을 받으면 현재 스레드(부모)의 자식 리스트에서 제거한다.
    list_remove(&child->child_elem);
    // 4) 자식이 완전히 종료되고 스케줄링이 이어질 수 있도록 자식에게 signal을 보낸다.
    sema_up(&child->exit_sema);

    return child->exit_status; // 5) 자식의 exit_status를 반환한다.
}

4) process_exit()

1) FDT의 모든 파일을 닫고 메모리도 반환한다.

2) 현재 실행 중인 파일도 닫는다.

3) 자식이 종료되기를 기다리고 있는 (wait) 부모에게 sema_up으로 signal을 보낸다.

4) 부모가 wait을 마무리하고 나서 signal을 보내줄 때까지 대기한다.

  • 이 대기가 풀리고 나면 스케줄링이 이어진다.
  • thread_exit 함수에서 process_exit 이후에 do_schedule(THREADY_DYING)이 진행되는 것을 볼 수 있음

1. wait_sema

  • 역할:
    • 부모 프로세스가 자식 프로세스의 종료를 기다리는 것을 제어
    • 부모 프로세스가 자식의 종료를 기다릴 때, 자식이 종료될 때까지 부모 프로세스를 블록(대기) 상태로 유지하기 위해 사용됩니다.
  • 부모 프로세스가 wait() 호출:
    • 부모가 자식의 종료를 기다릴 때 sema_down()을 호출합니다. 이 호출은 wait_sema0이 될 때까지 부모 프로세스를 대기 상태로 전환합니다.
    • 이때 자식이 아직 종료되지 않았기 때문에 wait_sema는 기본적으로 0입니다. 따라서 부모는 블록 상태가 되어 자식의 종료를 기다립니다.
  • 자식 프로세스가 종료 시 sema_up() 호출:
    • 자식 프로세스가 process_exit()에서 종료 작업을 마친 후, 자식은 sema_up(&cur->wait_sema)를 호출하여 wait_sema의 값을 증가시킵니다.

2. exit_sema

  • 역할:
    • 자식 프로세스가 종료될 때, 자원의 해제를 기다리는 것을 제어
    • 자식 프로세스가 부모가 종료를 인지할 때까지 기다리도록 제어하여, 자식이 올바르게 자원을 해제할 수 있게 보장
  • 자식 프로세스가 종료될 때 부모가 종료를 인지하기까지 대기:
    • 자식 프로세스가 process_exit()에서 자원을 해제하고, 부모가 자식의 종료를 인지할 때까지 기다리는 동안 sema_down()을 호출합니다.
    • 이때 exit_sema는 기본적으로 0이므로, 자식은 부모가 자신의 종료를 인지하고 sema_up()을 호출하기 전까지 대기 상태가 됩니다.
  • 부모가 자식의 종료를 확인한 후 sema_up() 호출:
    • 부모가 wait()에서 자식의 종료를 확인하고 sema_up(&child->exit_sema)를 호출하여 exit_sema의 값을 증가시킵니다.
    • 이렇게 하면 자식 프로세스는 대기 상태에서 깨어나 스케줄러에 의해 완전히 종료될 수 있는 상태가 됩니다.

exec

exec() 함수는 현재 프로세스를 새로운 실행 파일로 변경하는 작업을 수행합니다. 이 함수는 프로그램을 실행하는 방식을 변경하고, 새로운 프로그램으로 현재 프로세스를 대체합니다. 이를 통해 프로세스가 새로운 프로그램을 실행

0개의 댓글