PintOS P2 #6 : System Call : Fork, Exec, Wait

하던 일이 세상에게 이해 받지 못할 때가 있을지도 모른다. 다행스럽고 기대도 안했지만 세상은 원래부터 그렇게 친절하지 않다. 당신을 위한 세상은 당신이 만들어 나가야한다. 그런 당신의 편을 만들기 위해 쓰레드를 생성하는 법을 이제 알아야 할 것이다.


 

쓰레드 생성은 사실 당신의 편이 있냐 없냐와 상관이 없다. 그건 쓰레드랑 상관없이 주어진다. 안심하자.

2025.05.28 - [구현하기] - PintOS P2 #5 : System Call - File

 

PintOS P2 #5 : System Call - File

시스템 콜은 도움을 주는 영역이 아니고 사용자 영역에서 자원을 사용하기 위한 유일한 방법이다. 그런 만큼 군더더기 없이 기능면에선 시비 걸리지 않게 구현 할 수 있어야 한다. 파일 시스템

hyeonistic.tistory.com

이전 글에서 이어진다.

 

우리 조는 fork, exec, wait 시스템 콜 수행을 위해 굉장히 많은 고생을 했다. 결국 마지막 날 직전에 수행을 끝냈기 때문에 완전 마지막 퀘스트인 multi-out of memory는 따로 해결 할 시도를 못했다. 다 좋다. 꼭 완성만이 목표가 아니었던 걸 조금만 더 일찍 되새겼어야 했는데. 

 

Fork

시스템 콜 fork는 자식 프로세스를 생성한다. 처음엔 우왕좌왕 했는데 결국 계속 알아보니 process.c에 있는 process_fork로 바로 연결 시키는게 중요했다. 이 시점에서 부모의 레지스터의 몇몇 값들을 미리 빼놔야 했다.

 

미리 완성한 사람들은 이 레지스터를 빼두는 시점이 중요하다고 주장했다. 그래서 참 다양한 시도가 있었다. fork 시스템 콜이 시작 될 때 카피를 떠두기, process_fork가 시작될 때 떠두기..  나는 시스템 콜 분기점에서 복사하는 방식을 선택했는데, 그렇게 나쁜 선택은 아니었던 것 같다. 아래는 syscall.c에서 작성했던 나의 코드이다.

	case SYS_FORK:
		if(f->R.rdi != NULL)
		{
			if(is_user_vaddr(f->R.rdi))
			{
				memcpy(&thread_current()->backup_if, f, sizeof(struct intr_frame));
				f->R.rax = fork(f->R.rdi);
			}
		}
        [skip]
        
tid_t fork (const char *thread_name)
{
	return process_fork(thread_name, &thread_current()->backup_if);
}

참, 우린 이걸 구현하기 위해 쓰레드 구조체에 인터럽트 프레임을 하나 더 가리킬 수 있는 포인터 변수를 만들어놓았다. 뭔가 진짜 엄청 무지많이 추가했는데 어떤 계기였는지 전부 다 기억하는게 아니라서 우선 이 뭉치를 기록으로 남겨둔다. 앞으론 미루지 말고 글을 써야겠다라고 되새겨진다.

 

// thread.h의 thread 구조체 내용 중 일부 :
/* relations */
	int exit_status;
	struct thread *parent;
	struct intr_frame backup_if;
	struct list children;
	struct list_elem child_elem;

	/* semaphore */
	struct semaphore wait_sema;
	struct semaphore exit_sema;
	struct semaphore fork_sema;

	/* Page map level 4 */
	uint64_t *pml4;         
	struct file **FDT;				// File Descriptor Table
	int next_FD;					// 다음 사용 가능한 fd값
	struct file *running_file;		// 현재 프로세스에서 실행 중인 파일

어쨌든, fork는 process_fork를 실행한다 :

tid_t
process_fork (const char *name, struct intr_frame *if_) {

	struct thread *curr = thread_current();
	tid_t pid = thread_create(name, PRI_DEFAULT, __do_fork, curr);
	if (pid == TID_ERROR) return TID_ERROR;
	struct thread *child = get_child_thread(pid);
	if(child != NULL) sema_down(&curr->fork_sema);
	return pid;
}

thread_create() 에서 __do_fork를 실행하게 될 자식을 만든다. 그리고 만드는 과정에서 현재 주체의 자식으로 배정해준다. 즉, thread_create()는 당연하지만 이로 인해 만들어지는 쓰레드가 실행하는 것이 아니다. 태초의 쓰레드가 있었겠지만 그런건 당장 논할게 아니고, 어쨌든 해당 내용에서 현재 실행 주체의 자식으로 등록하는 내용이 필요하다.

// thread_create() 내용 중 일부 :
	t->parent = thread_current();
	list_push_back(&thread_current()->children, &t->child_elem);
	/** project1-Priority Scheduling */
	if(t->priority > thread_current()->priority)
		thread_yield();

 

 

해당 내용을 thread_create() return 직전에 추가해준다. 그러면 process_fork에서 리스트 중 자식을 찾고, 그대로 중지한다. 이것은 fork가 자식의 생성의 결과를 확실히 볼 수 있을 때까지 중단하면 안된다는 성질이 반영된 내용이다.

그렇게 부모 쓰레드가 중지되면 싱글쓰레드인 PintOS는 자식 쓰레드를 깨워서 실행시킨다. 이제 __do_fork가 실행된다. 

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->backup_if;
	bool succ = true;
	process_init ();

	// 내용을 지역변수에 담기
	memcpy (&if_, parent_if, sizeof (struct intr_frame));

	// 자식 프로세스의 페이지 테이블에게 복제한 값을 배치해야함
	current->pml4 = pml4_create();
	if (current->pml4 == NULL)
		goto error;

	process_activate (current);

	if (!pml4_for_each (parent->pml4, duplicate_pte, parent))
		goto error;

int fd_end = parent->next_FD;
for (int fd = 0; fd < fd_end; fd++) {
	if (fd <= 2)
		// stdin, stdout, stderr은 그대로 공유
		current->FDT[fd] = parent->FDT[fd];
	else {
		// 일반 파일은 다시 열어서 자식이 독립적으로 사용하게 함
		if (parent->FDT[fd] != NULL) 
			current->FDT[fd] = file_duplicate(parent->FDT[fd]);
	}
}
	current->next_FD = fd_end;

	// 자식 프로세스는 fork()의 반환값으로 0을 받아야 하므로 레지스터 설정
	if_.R.rax = 0;

	// 세그먼트 레지스터와 EFLAGS 설정 (유저 모드 전환 준비)
	if_.ds = if_.es = if_.ss = SEL_UDSEG;
	if_.cs = SEL_UCSEG;
	if_.eflags = FLAG_IF;
	

	sema_up(&current->fork_sema); 

	/* Finally, switch to the newly created process. */
	if (succ)
		do_iret (&if_);
error:
	current->exit_status = -1;
	sema_up(&current->fork_sema);
	thread_exit ();
}

기존의 구현된게 있다. 그리고 몇 안되는 부모 쓰레드와 자식 쓰레드가 매개변수와 실행주체로써 존재 할 수 있는 몇 안되는 메서드이기도 하다. 우리 조는 크게 나눠 3 부분을 추가했다 :

  • 인터럽트 프레임을 복사해서 자식에게 그대로 적용시키기
	struct intr_frame if_;
	struct thread *parent = (struct thread *) aux;
	struct thread *current = thread_current ();
	struct intr_frame *parent_if = &parent->backup_if;
	bool succ = true;
	process_init ();

	// 내용을 지역변수에 담기
	memcpy (&if_, parent_if, sizeof (struct intr_frame));

		(...)

	// 자식 프로세스는 fork()의 반환값으로 0을 받아야 하므로 레지스터 설정
	if_.R.rax = 0;

 

  • 파일 디스크립터 그대로 옮겨주기
int fd_end = parent->next_FD;
for (int fd = 0; fd < fd_end; fd++) {
	if (fd <= 2)
		// stdin, stdout, stderr은 그대로 공유
		current->FDT[fd] = parent->FDT[fd];
	else {
		// 일반 파일은 다시 열어서 자식이 독립적으로 사용하게 함
		if (parent->FDT[fd] != NULL) 
			current->FDT[fd] = file_duplicate(parent->FDT[fd]);
	}
}
	current->next_FD = fd_end;

 

  • 세마포어 조정을 통해 부모도 정상 수행을 할 수 있도록 복귀시키기
sema_up(&current->fork_sema);

할건 다 했다.

 

Wait

wait은 자식 프로세스를 기다리는데 의의가 있다. 자식 프로세스가 exit을 하면서 wait에 언급되어있는 세마포어에 조정을 주고 받으면서 서로에 대한 주 작동권을 양보하게 된다. 그래서인지 시스템 콜 자체는 무언가 들어있는 것이 없다.

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

pid로 어떤 프로세스의 종료를 기다릴건지를 등록 할 수 있다. 그럼 process_wait는 :

int process_wait (tid_t child_tid) {
	
	struct thread *child = get_child_thread(child_tid);
    if (child == NULL) return -1;

	sema_down (&child->wait_sema);

	int status = child->exit_status;
	list_remove(&child->child_elem);
    
    sema_up(&child->exit_sema);	
   	return status;
}

받아온 ID에 해당하는 자식을 찾아온다. 알고보면 이것은 완전 근본의 영역이었던 init.c에서 구현이 필요했던 영역이었다.

process_wait (process_create_initd (task));

하하 이제 작동 될 것이다! 어쨌든 wait는 자식의 exit을 기다리니 한번 보자.

 

void process_exit (void) {
	struct thread *curr = thread_current ();
    for(int i = 3; i < curr->next_FD; i++){
        // 만약 해당 FD 슬롯에 열린 파일이 있다면
        if (curr->FDT[i] != NULL)
            file_close(curr->FDT[i]); 	// 해당 파일 닫기
			curr->FDT[i] = NULL; 			// 슬롯을 NULL로 초기화
    }

	palloc_free_multiple(curr->FDT, FDT_PAGES);

	file_close(curr->running_file);
	if(curr->parent != NULL)
	{
		sema_up(&curr->wait_sema);
		sema_down(&curr->exit_sema); 
	}

	process_cleanup ();
}

부모 쓰레드의 존재는 세마포어의 조정을 동반하게 된다. 즉 이런 흐름이다. 수면의 선택은 본인이, 기상은 다른 쓰레드에서 시킴을 상기하기

부모 : 본인 수면

자식 : 부모 기상

자식 : 본인 수면

부모 : 자식 기상

이 4번의 세마포어 조정 과정에서 자식의 exit code 를 받아오게 된다. 이렇게 함으로써 완성 시킬 수 있었다.

 

Exec

int exec(const char *cmd_line)
{ 
	if(cmd_line == NULL) exit(-1);
	if (pml4_get_page(thread_current()->pml4, cmd_line) == NULL) exit(-1);
	
	char *package_cmd = palloc_get_page(PAL_ZERO);
	if (package_cmd == NULL) exit(-1);
	strlcpy(package_cmd, cmd_line, PGSIZE);

	int result = process_exec(package_cmd);
	if (result == -1) return result;
}

기존 내용을 정리하고 자기 자신의 실행 프로그램을 전환하는데 의의가 있다. 받아온 인자를 적당히 정돈해서 process_exec에 던진다.

process_exec은 특별히 구성되어있지도 않다. 스택을 설정하고 파일을 load 하는데, 그럼 exec의 행위 책임은 여기서 끝난다.

int
process_exec (void *f_name) {
	char *file_name = f_name;
	char *tokens;
	bool success;
	
	char *saveptr, *token;
	char *args_ptr[32] = {NULL};
	
	if(f_name == NULL)
		return TID_ERROR;

	int argc = 0;

	for(token = strtok_r(f_name, " \t\r\n", &saveptr); token && argc < 31; token = strtok_r(NULL, " \t\r\n", &saveptr))
	{
		args_ptr[argc] = token;
		argc++;
	}

	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 */
	success = load (file_name, &_if);

	/* 스택 포인터, karg->argc, karg->argv 가 이미 정의되어 있다고 가정 */
	char *argv_u[32];   /* argc 최대 32라 가정 */

	/* 1) 문자열을 역순으로 스택에 복사하고, 복사한 위치(=유저 스택 어드레스)를 argv_u에 저장 */

	// 페이지 경계에 맞닿는 상황을 제거해야한다. 그래서 rsp를 시작 전에 좀 더 내려서 시작하게끔 조정
	_if.rsp -= 8;

	for (int i = argc - 1; i >= 0; i--) {
		int len = strlen(args_ptr[i]) + 1;
		_if.rsp -= len;                         // 스택 포인터 내리고
		memcpy((void*)_if.rsp, args_ptr[i], len); 
		argv_u[i] = (char*)_if.rsp;            // 복사된 문자열의 주소 저장
	}

	/* 2) 스택 워드 정렬 (필요하다면) */
	_if.rsp = (uintptr_t)_if.rsp & ~0xF;

	/* 3) NULL sentinel */
	_if.rsp -= sizeof(char*);
	*(char**)_if.rsp = NULL;

	/* 4) argv_u[]에 모아둔 주소를 스택에 푸시 */
	for (int i = argc - 1; i >= 0; i--) {
		_if.rsp -= sizeof(char*);
		*(char**)_if.rsp = argv_u[i];
	}

	_if.R.rsi = _if.rsp;

	_if.rsp -= sizeof(void*);
	*(void**)_if.rsp = 0;   /* fake return address */

	_if.R.rdi = argc;

	palloc_free_page (file_name);

	if (!success)
	{
		palloc_free_page(f_name);
		return -1;
	}

	do_iret (&_if);
	NOT_REACHED ();
}

 

사실 트러블슈팅을 포함한 엄청 많은 고난을 겪어야했다. 완성된 내용에 실제 담아야 할 내용을 정제하고 나니 생각보다 별로 고생한것 같지 않게 작성된 모양인데, 진짜 미친 반복질을 해야 했다. 화이팅!

'구현하기' 카테고리의 다른 글

PintOS P3 #2 : Page/Frame 정리  (3) 2025.05.30
PintOS P3 #1 : Virtual Memory 서론  (0) 2025.05.29
PintOS P2 #5 : System Call - File  (1) 2025.05.28
PintOS P2 #4. System Call 서론  (0) 2025.05.28
PintOS P2 #3 : Argument Passing 본편  (0) 2025.05.21