[운체] 오늘의 삽질 - 0725

방법이있지·2025년 7월 25일
post-thumbnail

너무 어려워서 글이 다소 날림조입니다.
이 글보다는 훌륭한 정글 동기들과 소통하면서 집단지성을 활용하시는 게 좋을 듯해보입니다.
소스코드는 깃허브에 올라와 있습니다.

wait 시스템 콜

기존 구조체 / 함수 수정

[구현 8-1] wait에 필요한 구조체 멤버 추가

  • waitfork로 생성한 자식 프로세스만 기다릴 수 있게 동작해야 함
  • 따라서 각 프로세스의 자식들을 확인할 방법이 필요

struct child_info

  • 부모-자식 관계에 필요한 정보들을 저장한 struct child_info 구조체 선언
// include/threads/thread.h

struct child_info {
	tid_t tid;				            // 쓰레드 번호
	struct list_elem c_elem;	    // 리스트 삽입용
	int exit_code;			          // 삭제코드 저장
	struct semaphore w_sema;	    // wait 시스템콜을 위한 세마포어
	bool alive;				            // 자신의 생존 여부
	bool parent_alive;		        // 부모의 생존 여부
	bool waiting;			            // 부모가 자신을 대기하는 중인지
};

struct thread 수정

// include/threads/thread.h

struct thread {
  // 생략....
	// wait 시스템 콜에 필요
	struct list children;				/* 자식 프로세스의 리스트 */
	struct child_info *ci;      /* 자신의 child_info의 주소 저장*/
}
  • struct thread에 다음과 같은 멤버를 추가한다
  • (1) struct list children: 자식들을 연결 리스트로 관리. 편의상 앞으로 자식 리스트로 부르겠음
    • struct child_infostruct list_elem c_elem을 삽입
  • (2) struct child_info *ci: 본인의 child_info의 주소 저장

children / child_info 초기화

  • fork() 시스템 콜에서 자식 프로세스를 생성할 때마다 실행되는 process_init()에 아래 코드 추가
// userprog/process.c 내 process_init() 함수 수정

// [구현 8-1] children / child_info 초기화
list_init(&(current -> children));  // children list 초기화

// struct child_info 초기화
struct child_info *ci = palloc_get_page(PAL_ZERO);
ci -> parent_alive = true;          // 부모로부터 fork되어 생성된 거니, 부모는 당연히 살아 있음
ci -> tid = current -> tid;         // tid는 자신의 tid로
ci -> alive = true;                 // 쓰레드 자신은 살아 있음
ci -> waiting = false;              // 지금은 부모가 대기하고 있지 않음
sema_init(&(ci -> w_sema), 0);      // 세마포어 0으로 초기화
current -> ci = ci;                 // struct thread의 ci에 주소 저장

왜 굳이 struct child_info를 만드나요? struct thread에 저장하면 안 되나요?

  • struct threadthread_exit() 하면 스케줄러가 나중에 메모리 할당을 free하게끔 구현되어 있습니다. (threads/thread.cdo_schedule 함수의 palloc_free_page 참고하기)
  • 하지만 자식이 죽은 후에도, 부모는 자식의 exit code 등 정보에 접근할 수 있어야 합니다.
  • 따라서 자동으로 free되지 않는 struct child_info를 만들어 줍니다. 대신 할당 해제는 저희가 알아서 해 줘야 하는데, 그것도 이따가 구현해야 함 ^^
    • 일반적으로는 부모가 wait() 보내고 자식이 종료된 걸 확인하면, 그때 free해 줍니다.
    • 하지만 부모가 이미 죽은 경우, 자식이 exit()할 때 스스로 free해 줘야 합니다. (그래서 parent_alive 멤버가 있는겁니다...)

[구현 8-2] 부모의 자식 리스트에 쓰레드 삽입

  • fork() 시스템 콜에서 자식이 생성될 때마다, 부모의 struct list children에 생성된 자식을 삽입해야 함
  • 자식 프로세스에서 실행되는 함수 __do_fork()을 수정하기
// userprog/process.c 내 __do_fork() 함수 수정

/* Finally, switch to the newly created process. */
	if (succ){
		// [구현 8-2] fork 이후 부모/자식 관계 설정
		list_push_back(&(parent -> children), &(current -> ci -> c_elem));				// 부모의 children 리스트에 자식 삽입

		// [구현 7-6] 자식프로세스 처리 종료 후 세마포어 올리기
		sema_up(&(parent -> f_sema));
		do_iret (&if_);
	}
  • 복제가 끝나고 sema_up이랑 do_iret 하기 직전에 자식을 삽입

부모쪽 process_wait 수정

  • 이제 wait 시스템 콜에 사용될 process_wait 함수를 수정해야 함

[구현 8-3] 자식 리스트 순회하며, 기다릴 자식 찾기

  • process_wait은 기다릴 자식의 쓰레드 id child_tid를 전달받음
  • 자식 리스트를 순회하면서, tidchild_tid가 동일한 자식을 찾기
    • 단, (1) 자식프로세스가 하나도 없거나
    • (2) 자식 리스트에 기다리는 자식이 없거나
    • (3) 이미 동일한 자식을 대기 중인 경우 (struct child_infowaiting 멤버로 확인 가능)
    • 곧바로 -1을 반환해야 함
  • 자식을 찾은 경우, 자식의 struct child_infochild 변수에 저장
// userprog/process.c
int process_wait (tid_t child_tid UNUSED) {
	// [구현 8-3] 자식 리스트 순회하며, 기다릴 자식 찾기
	struct thread *curr = thread_current();

	struct child_info *child;
	struct list_elem *e;
	int flag = 0;             // 찾은 경우 1, 못 찾은 경우 0
	int wait_return;          // 추후 반환값 저장 용도

  // 리스트를 순회
	if (!list_empty(&(curr->children))){
		for (e = list_begin(&(curr -> children)); e != list_end(&(curr -> children)); e = list_next(e)){

      // 자식의 tid를 child_tid와 비교
			child = list_entry(e, struct child_info, c_elem);
			if (child -> tid == child_tid){
				flag = 1; // 찾으면 flag를 1로 바꾸고 break
				break;
			}
		}
	}

  // (1)(2) 자식프로세스가 없거나, 리스트에서 찾지 못한 경우
  // (3) 이미 동일 자식을 대기 중인 경우 --> -1을 반환.
	if (flag == 0 || child -> waiting){
		return -1;
	}

  // 계속
}

[구현 8-4] 자식의 종료 전까지 대기하기 위해, 세마포어 내리기

  • 대기할 자식을 찾은 경우, 자식의 종료 전까지 대기해야 함
  • (1) child -> waitingtrue로 설정 -> 동일한 자식을 다시 대기하지 못하게 함
    • 이후 동일 자식에 대한 wait 시스템 콜을 보내는 경우, [구현 8-3]의 조건문에서 -1을 반환하게 됨
  • (2) child -> w_sema` 세마포어에 sema_down 적용해 대기
    • 나중에 자식 종료될때 sema_up되도록 구현할 거임. 그 전까지 대기타야 함.
  • (3) 대기 종료 후, child -> waitingfalse로 설정
  • 단, 자식이 이미 죽은 경우(이미 exit()한 경우), 위 과정을 생략. 이미 죽은 자식이니 대기할 필요가 없음
    • child -> alivetrue(자식이 살아있는 경우)일 때만 실행되게 조건문을 만들어 두기
// userprog/process.c
// [구현 8-4] 자식의 종료 전까지 대기
	// 이미 자식이 죽은 경우, 대기할 필요가 없음
	if (child -> alive){
		// waiting을 true로 설정하고, 세마포어를 내리기.
		child -> waiting = true;
		sema_down(&(child -> w_sema));
    // 대기 종료 후엔 waiting을 false로 설정.
		child -> waiting = false;
	}

자식쪽 process_exit 수정

  • 모든 프로세스는 종료될 때 thread_exit() (threads/thread.c) 호출 -> 함수 내부에서 process_exit()를 호출
  • 여기서도 수정이 필요할 거임. 일단 자식이 죽기 전에 세마포어를 올려 줘야, 부모 쪽 코드도 마저 실행이 되겠지? 근데 세마포어 말고도 해줘야 할게 많음...
  • 우선 아래 코드는 자식이 사용자 프로세스일 때만 실행되어야 함. 아래 bool 변수를 만들고 참일 때만 실행되게 구현하자.
// 사용자 프로세스만 pml4 테이블을 가짐
bool user_process = curr -> pml4 != NULL;

if (user_process){
	// ....
}

[구현 8-5] 자식에게 부모의 죽음을 알리기

  • 자신의 자식들을 순회하면서, struct child_infoparent_alivefalse로 설정 -struct child_info의 할당 해제 시점은, parent_alive에서 확인할 수 있는 부모의 생존/사망 시점에 따라 달라짐
    • 일반적으로는 부모가 wait() 보내고 자식이 종료된 걸 확인하면, 그때 free해 줌
    • 하지만 부모가 이미 죽은 경우, 자식이 exit()할 때 스스로 free해 줘야 함
    • 이걸 [구현 8-6]에서 구현
// [구현 8-5] 자식에게 부모의 죽음 알리기
struct list_elem *e;
// 모든 자식의 parent_alive를 false처리
if (!list_empty(&(curr->children))){
	for (e = list_begin(&(curr -> children)); e != list_end(&(curr -> children)); e = list_next(e)){
		list_entry(e, struct child_info, c_elem) -> parent_alive = false;
	}
}

[구현 8-6] 부모가 죽은 경우, child_info free

  • 부모가 이미 죽은 경우, palloc_free_page로 프로세스의 child_info (ci 멤버)의 메모리 할당 해제
// [구현 8-6] 부모가 죽은 경우 child_info free
if (!(curr -> ci -> parent_alive)){
	// 부모가 죽은 경우, 바로 여기서 free
	palloc_free_page(curr -> ci);
}

[구현 8-7] 자신의 child_info 수정

  • 부모가 아직 살아있는 경우, 죽기 전에 프로세스 자기 자신의 struct child_info 멤버들을 수정해 줘야 함
    • struct threadci 멤버는 child_info의 주소를 저장하고 있음
// [구현 8-7] 자신의 child_info 수정
struct thread *curr = thread_current ();
curr -> ci -> alive = false;
curr -> ci -> exit_code = curr -> exit_code;
  • (A) alivefalse로 설정
    • 자신이 이미 죽었음을 표시. 새로운 wait() 시스템 콜이 주어지는 경우, [구현 8-4]에서 설정했던 조건문 덕분에, 부모가 굳이 대기하지 않고 바로 처리함.
  • (B) exit 시스템 콜을 수행하면, 자신의 struct threadexit_code 멤버에 exit의 매개변수로 보낸 exit status가 저장됨 (앞서 exit 구현할 때 하셨을 겁니다...)
  • 이를 자신의 child_infoexit_code에도 저장해 두기
    • 나중에 부모가 해당 값을 확인해, 반환할 수 있게 하기 위함.

[구현 8-8] 세마포어를 올려 부모의 대기 해제

// [구현 8-8] 세마포아를 올려 부모의 대기 해제
sema_up(&(curr -> ci -> w_sema));
  • 앞서 [구현 8-4]에서, 자식의 child_info에 있던 w_sema를 sema_down했었음.
  • 자식의 종료 전 준비가 완료됐으니, sema_up으로 올려주기.

[구현 8-5, 6, 7, 8] 정리

/* Exit the process. This function is called by thread_exit (). */
void
process_exit (void) {

	struct thread *curr = thread_current ();

	// 사용자 프로세스만 pml4 테이블을 가짐
	bool user_process = curr -> pml4 != NULL;

	process_cleanup ();

	// 사용자 프로세스일 때만 아래 코드를 실행.
	if(user_process){
		// [구현 5-2] exit 및 exit 메시지 띄우기 구현
		printf("%s: exit(%d)\n", curr -> name, curr -> exit_code);

		// [구현 8-5] 자식에게 부모의 죽음 알리기
		struct list_elem *e;
		// 모든 자식의 parent_alive를 false처리
		if (!list_empty(&(curr->children))){
			for (e = list_begin(&(curr -> children)); e != list_end(&(curr -> children)); e = list_next(e)){
				list_entry(e, struct child_info, c_elem) -> parent_alive = false;
			}
		}

		// [구현 8-6] 부모가 죽은 경우 child_info free
		if (!(curr -> ci -> parent_alive)){
			// 부모가 죽은 경우, 바로 여기서 free
			palloc_free_page(curr -> ci);
		}
		else {
		// [구현 8-7] 자신의 child_info 수정
		curr -> ci -> alive = false;
		curr -> ci -> exit_code = curr -> exit_code;

		// [구현 8-8] 세마포아를 올려 부모의 대기 해제
		sema_up(&(curr -> ci -> w_sema));
		}
	}
}

부모쪽 process_wait 다시 수정

[구현 8-9] 종료된 자식의 child_info free하기

// userprog/process.c process_wait() 내부

// [구현 8-9] 자식이 종료되면...
// 자식을 리스트에서 없애고, free하고, 자식의 exit 코드 반환.
wait_return = child -> exit_code;
list_remove(&(child -> c_elem));
palloc_free_page(child);

return wait_return;
  • wait_return[구현 8-7]에서 설정된 자식의 exit status. 이 값을 반환
  • 이후 여기서 자식을 연결 리스트에서 없애고, palloc_free_page로 메모리 할당 해제해 줌.

[구현 8-3, 4, 9] 정리

int
process_wait (tid_t child_tid UNUSED) {
	//[구현 8-3] children 리스트에서 child_tid를 가진 자식을 찾기
	struct thread *curr = thread_current();

	struct child_info *child;
	struct list_elem *e;
	int flag = 0;
	int wait_return;

	if (!list_empty(&(curr->children))){
		for (e = list_begin(&(curr -> children)); e != list_end(&(curr -> children)); e = list_next(e)){
			child = list_entry(e, struct child_info, c_elem);
			//printf("찾는 %d, 실제 %d\n", child_tid, child -> tid);
			if (child -> tid == child_tid){
				flag = 1;
				break;
			}
		}
	}

	if (flag == 0 || child -> waiting){
		// 찾지 못한 경우 / 리스트가 빈 경우 -1 반환
		// 동일 child를 대기하는 경우도 -1 반환
		return -1;
	}

	// [구현 8-4] 자식의 종료 전까지 대기
	// 이미 자식이 죽은 경우, 바로 exit code를 얻기
	if (child -> alive){
		// waiting을 true로 설정하고, 세마포어를 내리기.
		child -> waiting = true;
		sema_down(&(child -> w_sema));
		child -> waiting = false;
	}

	// [구현 8-9] 종료된 자식의 child_info free하기
	// 자식을 리스트에서 없애고, free하고, 자식의 exit 코드 반환.
	wait_return = child -> exit_code;
	//printf("자식 %d의 exit 코드: %d", child->tid, child->exit_code);
	list_remove(&(child -> c_elem));
	palloc_free_page(child);

	return wait_return;

}

OS와 첫 사용자 프로그램 간 부모/자식 관계 설정

  • 지금까지는 process_fork(부모) 및 __do_fork(자식)를 통해 생성되는 자식 프로세스만 부모와의 관계 설정이 이루어짐
  • 하지만 첫 사용자 프로그램을 만들 댄, process_create_initd(부모) 및 initd(자식)을 이용함
    • 여기서도 부모, 자식 관계를 설정해야 wait가 의도한 대로 동작함

[구현 8-10, 11] process_create_initd 수정

[구현 8-10]

  • 우선 children 등 멤버들 초기화를 위해, 부모도 process_init()을 해 줘야 함 (기존엔 없음). 함수 추가.
  • 부모의 children 리스트에 자식을 삽입할 수 있게, 기존의 fn_copy를 포함해 struct thread *parent도 인수로 보내줘야 함
  • 이를 위해 새로운 구조체 struct init_aux 선언 후, thread_createinitd에 묶어서 보내줌

[구현 8-11]

  • 부모자식 관계 성립 전에 함수가 반환되면, 이후 wait() 등 시스템 콜이 호출될 때 race condition 발생 가능하니, 부모의 f_sema를 내려주기
// [구현 8-10] 부모의 struct thread 전달 위한 구조체 선언
struct init_aux {
	struct thread *parent;
	char *fn_copy;
};

tid_t
process_create_initd (const char *file_name) {
	tid_t tid;

	// 버퍼 준비
	char buffer[128];
	char *file_token;
	strlcpy(buffer, file_name, 128);

	// [구현 8-10] 첫 프로세스에 부모의 struct thread 전달하기
	process_init();
	struct init_aux *ia = malloc(sizeof(struct init_aux));
	ia -> fn_copy = palloc_get_page(0);
	if (ia -> fn_copy == NULL){
		return TID_ERROR;
	}
	strlcpy(ia -> fn_copy, file_name, PGSIZE);
	ia -> parent = thread_current();

	char *save_ptr;
	// [구현 1-2] 현재 file_name은 "echo x y z"
    //  file_name은 "echo"만 전달해야 함
	file_token = strtok_r(buffer, " ", &save_ptr);	// 첫 번째 argument만 저장됨

	/* Create a new thread to execute FILE_NAME. */
	tid = thread_create (file_token, PRI_DEFAULT, initd, ia);

	if (tid == TID_ERROR){
		// palloc_free_page (fn_copy);
		palloc_free_page(ia -> fn_copy);
		free(ia);
		return tid;
	}

	// [구현 8-11] 첫 사용자 프로그램이 자식으로 설정될 때까지 세마포어로 대기.
	sema_down(&(thread_current() -> f_sema));
	return tid;
}

[구현 8-12, 13] initd 수정

[구현 8-12]

  • 부모에게서 전달받은 부모의 struct threadf_name 저장하고, 포인터 free해 주기

[구현 8-13]

  • 리스트에 자식 추가한 뒤, [구현 8-11]에서 내렸던 세마포어 올려주기
static void
initd (void *aux_){
#ifdef VM
	supplemental_page_table_init (&thread_current ()->spt);
#endif
	process_init ();

	// [구현 8-12] 부모에게서 전달받은 데이터 확인
	struct thread *parent = ((struct fork_aux *)(aux_)) -> parent;
	char *f_name = ((struct init_aux *) aux_) -> fn_copy;
	struct thread *curr = thread_current();
	free(aux_);

	// [구현 8-13] 리스트에 삽입 및 sema-up
	list_push_back(&(parent -> children), &(curr -> ci -> c_elem));
	sema_up(&(parent -> f_sema)); // 설정 완료 후 부모의 대기 해제

	if (process_exec (f_name) < 0)
		PANIC("Fail to launch initd\n");
	NOT_REACHED ();
}

[구현 8-14] syscall handler에 추가

  • 반환값을 rax 레지스터에 잘 저장하면 됨
case SYS_WAIT:
	/* Wait for a child process to die. */
	// [구현 8-14]
    f -> R.rax = process_wait(f -> R.rdi);
	break;
profile
뭔가 만드는 걸 좋아하는 개발자 지망생입니다. 프로야구단 LG 트윈스를 응원하고 있습니다.

0개의 댓글