[pintos] Week2~3: User Program Part.8 - exec, wait

이전 포스팅에서 fork()를 구현했다. 이제 exec와 wait을 제대로 구현하여 테스트 통과를 확인하려고 한다.

2025.05.26 - [크래프톤 정글] - [pintos] Week2~3: User Program Part.7 - fork

 

[pintos] Week2~3: User Program Part.7 - fork

이전 시간까지 syscall에서 파일 디스크립터와 lock을 다루는 기능들을 구현했다.2025.05.25 - [크래프톤 정글] - [pintos] Week2~3: User Program Part.6 - lock 적용 [pintos] Week2~3: User Program Part.6 - lock 적용이 전 포

www.gowoong.com


exec 란?

exec 시스템 콜은 현재 프로세스를 새로운 프로그램으로 완전히 바꾸는 시스템 콜이다. 현재 실행 중인 프로세스를 종료하고, 지정된 새 프로그램으로 메모리, 코드, 스택 등을 교체하여 실행을 시작한다.

  • 자식은 fork() 후 exec()로 새로운 프로그램으로 바뀌고,
  • 부모는 자식이 종료될 때까지 wait()함

exec 동작 과정 (개념 흐름)

  • 현재 프로세스의 유저 영역 메모리(코드, 데이터, 스택 등)를 모두 해제
  • 주어진 파일(file_name)을 열어서 ELF 실행 파일로 파싱
  • 프로그램을 메모리에 로드하고, 새로운 스택과 진입점(RIP)을 설정
  • 새 프로그램의 main() 함수부터 실행
  • 기존 프로세스의 주소 공간은 완전히 사라짐
    PID는 유지되지만 내용은 완전히 바뀐다
항목 설명
PID 유지됨 (프로세스 ID는 바뀌지 않음)
코드/데이터 완전히 새로운 프로그램으로 교체
열린 파일 일반적으로 그대로 유지됨 (Pintos에서는 닫을 수도 있음)
이전 코드 절대 돌아가지 않음 (exec 이후 다음 코드 실행 안 됨)

열할 설명
프로그램 실행 cmd_line 문자열을 해석해 새로운 유저 프로그램을 실행
자식 프로세스 생성 fork() + exec()와 유사하게, 내부적으로 새로운 스레드/프로세스를 생성
인자 전달 "program arg1 arg2 ..."처럼 명령어와 인자를 함께 전달
pid 반환 실행된 자식 프로세스의 pid(tid)를 반환
실패 시 -1 반환 실행파일을 찾지 못하거나 로딩 실패하면 -1 반환
동기화 요구 부모는 자식이 load()를 완료할 때까지 기다려야 함 ← 중요 포인트!

  • 부모가 자식의 load() 완료를 기다려야 한다
  • 동기화 방식: 세마포어 사용


exec() 구현

  • userprog/syscall.c
int exec (const char *file_name){
	check_address(file_name);

    off_t size = strlen(file_name) + 1;
    char *cmd_copy = palloc_get_page(PAL_ZERO);

    if (cmd_copy == NULL)
        return -1;

    memcpy(cmd_copy, file_name, size);

    if (process_exec(cmd_copy) == -1)
        return -1;

    return 0;  // process_exec 성공시 리턴 값 없음 (do_iret)
}
  • exec(const char *file_name) 함수는 Pintos의 시스템 콜 핸들러에서 SYS_EXEC을 처리하기 위한 커널 함수로, 현재 프로세스를 file_name에 해당하는 새 실행파일로 완전히 덮어쓰기 위한 준비를 하는 함수이다.
  • 유저 영역에서 전달된 실행파일 이름(file_name)을 받아 커널에서 안전하게 복사한 뒤, process_exec()을 호출하여 현재 프로세스를 새로운 프로그램으로 완전히 대체한다.
off_t size = strlen(file_name) + 1;
  • 실행파일 이름 문자열의 길이 계산 (null 포함)
  • off_t는 파일 길이나 버퍼 크기를 표현하기 위한 타입 (보통 int)
char *cmd_copy = palloc_get_page(PAL_ZERO);
  • 한 페이지(4KB) 크기의 커널 메모리를 동적 할당
  • PAL_ZERO는 할당된 메모리를 0으로 초기화
  • 이후 process_exec()에서 유저 영역을 지워버리므로, 유저 주소인 file_name은 더 이상 접근할 수 없음
  • 따라서 실행파일 이름을 커널에서 안전하게 복사해서 넘겨야 함
if (cmd_copy == NULL)
	return -1;
  • 메모리 부족 등으로 palloc_get_page() 실패 시 -1 반환
memcpy(cmd_copy, file_name, size);
  • 유저가 넘긴 실행파일 이름 문자열을 커널 주소로 복사
  • 이제 cmd_copy는 안전하게 참조 가능한 실행파일 이름 문자열
if (process_exec(cmd_copy) == -1)
	return -1;
  • 실행파일 로딩, 주소공간 초기화, 유저 모드 전환

process_exec() 함수

process_exec() 함수는 기존에 구현했던 함수를 사용하지만 몇가지 로직 순서를 변경한다.

int
process_exec (void *f_name) {
	char *file_name = f_name;
	bool success;

	/* 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 ();

	char *ptr, *arg;
    int arg_cnt = 0;
    char *arg_list[64];

    for (arg = strtok_r(file_name, " ", &ptr); arg != NULL; arg = strtok_r(NULL, " ", &ptr))
        arg_list[arg_cnt++] = arg;

	/* And then load the binary */
	success = load (file_name, &_if);
	/* If load failed, quit. */
	if (!success)
		return -1;

	argument_stack(arg_list, arg_cnt, &_if);

	palloc_free_page (file_name);
	
	/* Start switched process. */
	do_iret (&_if);
	NOT_REACHED ();
}

wait 이란?

부모 프로세스가 자식 프로세스의 종료를 기다리고, 자식이 종료되면 해당 자식의 exit status를 받아오는 시스템 콜이다.

항목 설명
자식의 종료 대기 pid로 지정된 자식이 종료될 때까지 기다림
정상 종료 시 자식이 exit(status)로 종료하면 해당 status를 반환
비정상 종료 시 커널에 의해 종료된 경우 -1 반환
중복 호출 금지 같은 pid에 대해 여러 번 wait 호출 불가
자식 아님 만약 pid가 직접적인 자식이 아니면 -1 반환

  • 부모가 자식 리스트에서 child_tid에 해당하는 자식을 찾음
  • 해당 자식의 wait_sema를 통해 종료될 때까지 sema_down()
  • 자식이 exit()에서 sema_up()으로 부모를 깨움
  • 부모는 자식의 exit_status를 확인하고 리턴
  • 자식 정보를 리스트에서 제거 (자원 해제)

wait() 구현

  • userprog/syscall.c
int wait(pid_t tid) {
    return process_wait(tid);
}

process_wait() 함수 수정

  • userprog/process.c
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 *child = get_child_process(child_tid);
    if (child == NULL)
        return -1;

    sema_down(&child->wait_sema);  // 자식 프로세스가 종료될 때 까지 대기.

    int exit_status = child->exit_status;
    list_remove(&child->child_elem);

    sema_up(&child->exit_sema);  // 자식 프로세스가 죽을 수 있도록 signal

    return exit_status;
}

process_wait(tid_t child_tid) 함수는 부모 프로세스가 자식 프로세스의 종료를 기다리고, 자식의 종료 상태(exit status)를 받아오는 wait 시스템 콜의 핵심 커널 함수이다.

  • 부모가 wait(pid)를 호출하면 해당 자식 프로세스의 종료를 기다렸다가,
  • 자식의 exit_status를 받아서 반환하고, 자식의 자원도 정리하는 함수.
struct thread *child = get_child_process(child_tid);
  • 현재 스레드의 child_list에서 tid == child_tid인 자식을 찾아서 반환
  • 찾지 못하면 NULL을 반환하게 됨
if (child == NULL)
    return -1;
  • child_tid가 현재 스레드의 자식이 아니거나
  • 이미 wait한 자식이라면 (즉, 중복 wait)
  • → 시스템 콜 실패로 -1 반환
sema_down(&child->wait_sema);
  • 부모가 자식의 종료를 기다리는 지점
  • 자식은 exit()을 호출할 때 sema_up(&wait_sema)로 부모를 깨움
  • 자식이 종료될 때까지 부모는 block 상태로 대기
list_remove(&child->child_elem);
  • 부모의 child_list 에서 이 자식 항목을 제거
  • 더 이상 이 자식을 wait할 수 없도록 정리
sema_up(&child->exit_sema);
  • 자식은 process_exit()에서 자기 자신을 정리하지 않고 sema_down()으로 대기 중이다.
  • 부모가 이 sema_up()을 호출해야만 자식이 완전히 사라질 수 있다.
  • Pintos에서는 프로세스 종료 시점에 부모가 wait 하지 않으면 자식이 무한 block 될 수 있음
  • 따라서 exit_sema를 통해 부모가 자식을 최종적으로 정리해도 좋다는 신호를 줌
return exit_status;
  • 자식이 종료할 때 남긴 exit(-N)의 N값을 부모에게 전달
  • 유저 프로그램에서는 보통 이 값을 기준으로 다음 로직을 판단

이렇게 해서 userprogram 구현을 마쳤다. 마지막으로 ALL PASS되는지 확인하고 그렇지 않다면 어디가 문제인지 디버깅을 통해 해결해 나가면 될 것이다.


참고 자료

[Pintos-KAIST] Project 2 : Hierarchical Process Structure