이전 포스팅에서 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] Week2~3: User Program 외전 - Linked List를 이용한 FD관리 (0) | 2025.05.26 |
---|---|
[pintos] Week2~3: User Program Part.7 - fork (0) | 2025.05.26 |
[pintos] Week2~3: User Program Part.6 - lock 적용 (1) | 2025.05.25 |
[pintos] Week2~3: User Program Part.5 - 파일 디스크립터 (0) | 2025.05.25 |
[pintos] Week2~3: User Program Part.4 - halt, exit, create, remove (0) | 2025.05.20 |