User Programs Part1 구현하기 (프로세스 실행 및 인자 전달, 시스템 콜 및 사용자 포인터 검증, 프로세스 동기화 및 종료)
Pintos Project2 Part1의 핵심 목표는 사용자 프로그램을 안전하게 실행하고, system call을 통해 적절히 종료하며, 부모 프로세스가 자식 종료를 기다리고 상태를 회수할 수 있도록 하는 전체 흐름을 구현하는 것인데요. 곧 다음과 같은 사항들을 구현해야 합니다.
- 인자 파싱 및 전달 과정을 완성해 기본적인 유저 프로그램 실행을 할 수 있게 만들기
- 사용자 포인터를 검증하는 기본적인 시스템 콜 처리 (halt, exit, exec, wait)
- 프로세스 동기화 및 종료 처리
이번 글에서는 Part1에 대한 구현을 다뤄볼 예정인데요. 처음 사용자가 터미널에 run 명령을 땅 하고 쳤을 때, 최초의 유저 프로그램이 실행되는 흐름을 따라 구현해보고, 이를 시스템 콜과 연결하는 과정, 그리고 부모-자식 프로세스 구조와 wait, exit 동기화를 구현하는 전체 과정을 정리해봅니다.
기본적인 사용자 프로그램 실행 구현하기
process_create_initd 수정하기 (process.c)
process_create_initd
함수는 사용자 프로그램을 최초로 실행하는 initd 프로세스를 생성하는 함수입니다.
/*
* process_create_initd()
* PintOS에서 사용자 프로그램 실행을 처음 시작할 때 호출되는 함수입니다.
* 실행할 사용자 프로그램 이름(예: "initd arg1 arg2")을 받아서,
* 커널 스레드를 생성하고, 그 안에서 사용자 프로세스를 실행하게 만듭니다.
*/
tid_t process_create_initd(const char *file_name)
{
char *fn_copy, *fn_parse; // file_name의 복사본들
char *prog_name; // 프로그램 이름만 따로 저장
char *save_ptr; // strtok_r에서 내부 상태 추적용
tid_t tid; // 생성된 스레드의 ID (thread identifier)
/* file_name의 복사본 두 개를 만들기 위한 페이지 할당 */
fn_copy = palloc_get_page(0); // 자식에게 전달할 전체 인자 문자열 보관용
fn_parse = palloc_get_page(0); // strtok_r로 파일 이름만 파싱하기 위한 임시 용도
/* 메모리 할당 실패 시 오류 반환 (누수 방지용 해제 포함) */
if (fn_copy == NULL || fn_parse == NULL) {
palloc_free_page(fn_copy); // NULL이어도 안전하게 호출 가능
palloc_free_page(fn_parse);
return TID_ERROR;
}
/* file_name 문자열을 두 버퍼에 각각 복사 */
strlcpy(fn_copy, file_name, PGSIZE); // 자식 프로세스에 넘길 원본 인자 전체
strlcpy(fn_parse, file_name, PGSIZE); // strtok_r로 파싱해서 스레드 이름 추출용
/* fn_parse를 사용해서 첫 번째 단어(=실행 파일 이름)만 분리 */
// 예: "initd arg1 arg2" → prog_name = "initd"
prog_name = strtok_r(fn_parse, " ", &save_ptr);
/* 새 스레드를 생성
* - prog_name: 스레드 이름 (디버깅용으로 사용됨)
* - initd: 새 스레드에서 실행할 함수 (사용자 프로그램을 시작하는 함수)
* - fn_copy: 자식에게 전달할 전체 인자 문자열
*/
tid = thread_create(prog_name, PRI_DEFAULT, initd, fn_copy);
/* 스레드 생성 실패 시 fn_copy 메모리 회수 */
if (tid == TID_ERROR)
palloc_free_page(fn_copy);
/* 파싱용 메모리는 부모만 쓰기 때문에 항상 해제 */
palloc_free_page(fn_parse);
/* 생성된 스레드의 tid를 반환 */
return tid;
}
process_wait에서 임시로 대기시키기 (process.c)
이어서 process_create_initd
함수가 호출되기 전의 상황도 살펴봐야 합니다. init.c
에서는 process_create_initd
함수가 반환한 tid를 받아 process_wait
함수를 호출하게 되는데요. 곧 자식 스레드가 실행 중인 동안 부모 스레드를 기다리게 하는 역할을 합니다.
아직 구현된 게 별로 없는 초기에는 임시로 부모 프로세스를 대기하게 만들어 실행을 확인해 볼 수 있는데요. 이후 semaphore 같은 동기화 도구를 이용해 자식 스레드가 종료를 알려주기까지 대기하게 만들어야 합니다.
만약 이를 적용한 뒤 args 테스트 등에서 'Run didn't produce any output'가 나오면서 실패한다면, 자식 프로세스가 종료되기 전 부모 프로세스가 return 해버려서 아무런 출력도 표시하지 못하게 됐기 때문인데요. 이때에는 현재 4개인 for문을 더 늘려주면 됩니다. 반면 과도하게 늘릴 경우 TIMEOUT이 발생할 수 있으니 주의해야 합니다.
int process_wait(tid_t child_tid) {
int stat;
for (int i = 0; i < 100000000; i++) {
stat = 1;
}
for (int i = 0; i < 100000000; i++) {
stat = 1;
}
for (int i = 0; i < 100000000; i++) {
stat = 1;
}
for (int i = 0; i < 100000000; i++) {
stat = 1;
}
return stat;
}
process_exec 수정하기 (process.c)
thread_create
함수에서는 initd
를, 그리고 그 안에서는 최종적으로 process_exec
함수를 호출하는데요. 이 함수는 실행할 프로그램을 메모리에 로딩하고 실제 유저 프로그램의 main으로 이동해 실행을 시작하는 핵심 함수라고 할 수 있습니다.
다만 기본적으로 주어진 코드에서는 유저 프로그램 실행에 필요한 인자를 유저 스택에 전달하는 과정이 없기 때문에, 직접 명령 줄을 파싱한 다음 인자들을 유저 스택에 전달해주어야 합니다. 참고로 이 함수 내부에서 쓰이는 parse_args
, argument_stack
은 아래에 이어서 다룹니다.
#define MAX_ARGS 128 // 상단에 매크로 추가
int process_exec(void *f_name)
{
// 최대 MAX_ARGS 개수만큼의 인자들을 저장할 배열 선언
char *argv[MAX_ARGS];
// f_name은 "실행파일명 인자1 인자2 ..." 형태의 문자열임
// 이를 공백 기준으로 파싱하여 argv에 저장하고 argc에 개수를 저장
int argc = parse_args(f_name, argv);
bool success;
/* intr_frame 구조체는 유저 프로세스의 레지스터 정보를 저장
* 현재 스레드의 멤버를 사용할 수 없는 이유는,
* process_exec가 현재 실행 중인 스레드의 실행 컨텍스트를 완전히 새로 바꾸기 때문임.
* → _if는 임시로 스택에 선언된 intr_frame */
struct intr_frame _if;
_if.ds = _if.es = _if.ss = SEL_UDSEG;
_if.cs = SEL_UCSEG;
_if.eflags = FLAG_IF | FLAG_MBS;
/* 현재 프로세스에서 실행 중이던 프로그램과 자원들을 모두 정리
* - 열린 파일 닫기
* - 페이지 테이블 해제
* - 유저 스택 정리 등 */
process_cleanup();
/* 파일 이름 파싱 결과의 첫 번째 토큰은 실제 실행할 파일 이름임 */
ASSERT(argv[0] != NULL);
// 실행할 유저 프로그램을 메모리에 로드 (ELF 파일 분석 및 페이지 할당 포함)
success = load(argv[0], &_if);
/* 실행 파일 로드에 실패했으면 f_name 해제, -1 리턴 후 종료 */
if (!success) {
palloc_free_page(f_name);
return -1;
}
// load 성공 시, 유저 스택에 인자 전달
argument_stack(argv, argc, &_if);
// load 성공 시에도 f_name 해제
palloc_free_page(f_name);
/* 커널에서 유저 프로세스로 전환
* do_iret는 레지스터 값을 복원하고 유저 모드로 진입시키는 어셈블리 함수
* _if에 저장된 값들을 이용하여 유저 프로그램을 실행 */
do_iret(&_if);
/* do_iret는 유저 모드로 완전히 전환되기 때문에 이 아래 코드는 실행되지 않음 */
NOT_REACHED();
}
parse_args 구현하기 (process.c)
명령 줄 복사본을 받아 공백을 기준으로 파일명과 인자 각각을 분리해 인자 배열에 담고, 총 인자 개수를 반환하는데요. 이때 파일명도 인자에 포함됩니다.
// process.c 위쪽에 함수 선언
static int parse_args(char *, char *[]);
// 문자열 target을 공백(" ") 기준으로 잘라서 각 토큰(인자)을 argv 배열에 저장하고, 인자의 개수를 반환하는 함수
// 예: target = "echo hello world" → argv = ["echo", "hello", "world", NULL]
static int parse_args(char *target, char *argv[])
{
int argc = 0; // 인자의 개수를 세기 위한 변수
char *token;
char *save_ptr; // strtok_r에서 파싱 상태를 유지하기 위한 포인터 (reentrant-safe)
// 첫 번째 토큰 추출. strtok_r는 문자열을 공백을 기준으로 분리
for (token = strtok_r(target, " ", &save_ptr);
token != NULL;
token = strtok_r(NULL, " ", &save_ptr)) // 이후 토큰부터는 첫 인자에 NULL 전달
{
argv[argc++] = token; // 잘라낸 인자를 argv 배열에 저장하고 argc 증가
}
// argv는 마지막에 NULL 포인터로 끝나야 exec 계열 함수에서 제대로 처리됨 (C 언어 컨벤션)
argv[argc] = NULL;
// 최종적으로 인자의 개수를 반환
return argc;
}
argument_stack 구현하기 (process.c)
명령 줄 파싱을 통해 인자들의 배열과 인자 갯수를 구했다면, 그 다음에는 이를 해당 스레드의 유저 스택에 전달해야 하는데요. 그래야만 유저 프로그램이 정상적으로 실행될 수 있습니다.
참고: 메모리 레이아웃
아래의 도식은 /bin/ls -1 foo bar
명령 줄을 파싱해 유저 스택에 전달했을 때의 구조를 나타낸 것입니다.
0x1000: "bar\0" ← 4B
0x0ffc: "foo\0" ← 4B
0x0ff8: "-l\0" ← 3B
0x0ff5: "/bin/ls\0" ← 9B
0x0fec: [padding 12 bytes] ← 총 문자열 영역 = 32B로 정렬
----------------------------------------------------------
0x0fe0: argv[4] = 0
0x0fd8: argv[3] = 0x1000
0x0fd0: argv[2] = 0x0ffc
0x0fc8: argv[1] = 0x0ff8
0x0fc0: argv[0] = 0x0ff5
----------------------------------------------------------
0x0fb8: 0 (fake return addr) ← 8B
----------------------------------------------------------
**최종 rsp = 0x0fb8 (16바이트 정렬)**
먼저 인자 배열의 값 자체를 복사해 역순으로 푸시합니다. 그리고 16바이트 정렬을 맞추기 위해 패딩을 추가하고, 이어서 인자 값이 저장된 스택 주소를 역순으로 푸시합니다. 마지막으로 fake return 주소를 넣어주면 인자 전달이 완료됩니다.
유의사항으로는 x86-64같은 실제 OS와는 달리 argc와 argv를 스택에 push하지 않고 유저 프로그램 진입 직전에 레지스터로 전달한다는 점이 있습니다.
구현 코드
// process.c 위쪽에 함수 선언 추가
static void argument_stack(char *argv[], int argc, struct intr_frame *_if);
// 사용자 프로그램의 스택을 구성하여 인자들을 전달하는 함수
static void argument_stack(char *argv[], int argc, struct intr_frame *_if) {
uint64_t rsp_arr[argc]; // 각 인자 문자열의 시작 주소를 저장할 배열
// 문자열을 스택에 역순으로 복사
for (int i = argc - 1; i >= 0; i--) {
size_t len = strlen(argv[i]) + 1; // 문자열 길이 + 널 문자 포함
_if->rsp -= len; // 스택 아래로 공간 확보
rsp_arr[i] = _if->rsp; // 해당 문자열이 위치한 주소 저장
memcpy((void *)_if->rsp, argv[i], len); // 스택에 문자열 복사
}
// 16바이트 정렬 맞추기 (rsp를 16의 배수로 내림 정렬)
_if->rsp = _if->rsp & ~0xF; // 하위 4비트 0으로 마스킹 → 16의 배수
// NULL sentinel push (argv[argc] = NULL)
_if->rsp -= 8; // 포인터 크기만큼 스택 아래로
memset(_if->rsp, 0, sizeof(char **)); // 0으로 채움 (NULL)
// argv[i] 포인터들을 역순으로 push
for (int i = argc - 1; i >= 0; i--) {
_if->rsp -= 8; // 8바이트 공간 확보
memcpy(_if->rsp, &rsp_arr[i], sizeof(char **)); // 각 문자열의 주소를 복사
}
// fake return address
_if->rsp -= 8;
memset(_if->rsp, 0, sizeof(void *)); // 가짜 리턴 주소 = 0
// 사용자 프로그램 시작 시 인자 전달을 위한 레지스터 설정
_if->R.rdi = argc; // 첫 번째 인자: argc
_if->R.rsi = _if->rsp + 8; // 두 번째 인자: argv (가짜 리턴 주소 다음부터가 argv[0] 배열)
}
이 함수까지 구현되었다면 기본적인 유저 프로그램 실행이 가능한 상태가 됩니다.
기본 시스템 콜 처리 구현하기 (halt, exit, exec, wait)
다음 작업 순서는 기본적인 시스템 콜을 처리할 수 있게 만드는 것인데요. 여기에는 exec, halt, exit, wait가 있습니다.
먼저 유저 프로그램 실행 중 시스템 콜이 발생하면, 커널에 진입해 syscall.c
의 syscall_handler()
가 인터럽트 핸들러로 등록되어 호출됩니다. 이후 사용자 스택에서 syscall 번호를 꺼내 어떤 시스템 콜인지 판단하죠. 그리고 사용자 주소의 인자를 커널로 복사하고 유효성 검사까지 마쳤다면, 실제로 대응되는 커널 함수를 실행해 알맞은 기능을 수행하게 됩니다.
수행 이후 리턴값은 rax 레지스터에 저장한 뒤 사용자에게 전달됩니다. 마지막으로 사용자 프로그램으로 다시 복귀하면서 시스템 콜 처리가 끝납니다.
시스템 콜 핸들러 수정하기 (syscall.c)
기본으로 제공된 코드는 현재 어떠한 시스템 콜도 처리하지 못하는 상태인데요. 이를 switch문을 활용해서, 각 시스템 콜에 대응되는 번호에 맞는 처리 함수를 실행하게끔 바꿔줘야 합니다. 그리고 이후에 다른 시스템 콜들이 추가될 때마다 case를 추가하면 됩니다.
void syscall_handler(struct intr_frame *f UNUSED)
{
uint64_t syscall_num = f->R.rax;
uint64_t arg1 = f->R.rdi;
uint64_t arg2 = f->R.rsi;
uint64_t arg3 = f->R.rdx;
uint64_t arg4 = f->R.r10;
uint64_t arg5 = f->R.r8;
uint64_t arg6 = f->R.r9;
switch (syscall_num)
{
case SYS_HALT:
sys_halt();
break;
case SYS_EXIT:
sys_exit((int)arg1);
break;
case SYS_EXEC:
f->R.rax = sys_exec((const char *)arg1);
break;
case SYS_WAIT:
f->R.rax = sys_wait((int)arg1);
break;
default:
thread_exit();
break;
}
}
사용자 포인터 검증 함수 구현하기 (syscall.c)
유저 프로세스가 커널에 전달한 주소가 올바른지 확인하는 기능을 합니다. 시스템 콜 처리 시에, 유저가 넘긴 포인터가 커널에서 잘못 쓰이는 경우 커널 패닉이나 보안 취약점이 생길 수 있는데요. 이를 막기 위해 필수적인 방어 코드라고 할 수 있습니다.
// syscall.c 위쪽에 함수 선언 추가
void check_address(void *addr);
void check_address(void *addr)
{
// 널 포인터는 사용할 수 없으므로 바로 종료
if (addr == NULL)
sys_exit(-1);
// (!((uint64_t)((addr)) >= 0x8004000000))
// addr이 유저 영역이 아닌 커널 주소를 건드리려고 하면 보안상 위험 → 종료
if (!is_user_vaddr(addr))
sys_exit(-1);
// 현재 프로세스의 페이지 테이블(pml4)에 이 주소가 실제로 매핑되어 있는지 확인
// 즉, 유저가 요청한 주소가 현재 유효한 가상 주소인지 확인
if (pml4_get_page(thread_current()->pml4, addr) == NULL)
sys_exit(-1);
}
sys_halt 시스템 콜 구현하기 (syscall.c)
Pintos 시스템을 종료합니다. 운영체제 전체를 꺼버리는 역할로, 보통 테스트 종료 시에 사용됩니다.
// syscall.c 위쪽에 함수 선언 추가
static void sys_halt();
static void sys_halt() {
// Pintos 기본 제공 종료 함수
power_off();
}
sys_exit 시스템 콜 구현하기 (syscall.c)
현재 실행 중인 프로세스를 종료하고, 종료 상태(status)를 커널에 반환합니다.
thread 구조체에 필드 추가
int exit_status; // 종료 상태 값
추가한 필드 초기화
// thread.c의 init_thread 내부에 추가
t->exit_status = 0;
sys_exit 구현 코드
exit 시스템 콜은 다른 핸들러에서도 사용해야 하기 때문에 syscall.h
에 선언해줍니다.
// syscall.h에 함수 선언 추가
void sys_exit(int);
void sys_exit(int status)
{
// 현재 실행 중인 스레드(프로세스)를 가져옴
struct thread *cur = thread_current();
// 종료 상태(status)를 현재 스레드에 저장
// 부모 프로세스가 wait()로 이 값을 조회할 수 있도록 하기 위함
cur->exit_status = status;
// 종료 메시지 출력 (테스트 시 검증에 사용)
// 예: "echo: exit(0)"
printf("%s: exit(%d)\n", thread_name(), status);
// 현재 스레드를 종료하고 정리 → scheduler에 의해 다른 스레드로 전환됨
thread_exit();
}
sys_exec 시스템 콜 구현하기 (syscall.c)
인자로 받은 프로그램(cmd_line)을 현재 프로세스에서 실행합니다.
// syscall.c 위쪽에 함수 선언 추가
static tid_t sys_exec(const char *cmd_line);
static tid_t sys_exec(const char *cmd_line) {
// 유효한 주소인지 검사
check_address(cmd_line);
// 사용자로부터 받은 문자열(cmd_line)을 복사할 커널 영역의 페이지를 할당
// PAL_ZERO는 할당된 메모리를 0으로 초기화하라는 의미
char *cmd_line_copy = palloc_get_page(PAL_ZERO);
// 만약 메모리 할당에 실패했다면, exit 처리
if (cmd_line_copy == NULL) {
sys_exit(-1); // 시스템 콜 종료 코드로 -1을 반환
}
// 사용자 영역의 문자열을 커널 영역으로 안전하게 복사
// PGSIZE는 한 페이지의 크기(보통 4KB)를 의미
strlcpy(cmd_line_copy, cmd_line, PGSIZE);
// 실제로 새로운 프로그램을 현재 프로세스 위에 실행
// 실패하면 -1을 반환하므로, exit 처리
if (process_exec(cmd_line_copy) == -1) {
sys_exit(-1); // 실행 실패 시 종료
}
// 참고: 성공했다면 이 함수는 반환하지 않고, 새 프로그램으로 전환됨
}
palloc 모듈 참조 추가 (syscall.c)
PAL_ZERO 매크로가 있는 palloc.h에대한 참조를 추가합니다.
// syscall.c 맨 위에 추가
#include "threads/palloc.h"
sys_wait 시스템 콜 구현하기 (syscall.c)
특정 자식 프로세스의 종료를 기다리고, 그 종료 코드를 받아옵니다. 이 시점에서 process_wait
은 완전히 구현된 상태는 아니지만, 시스템 콜 핸들러에서 호출될 수 있도록 연결해 줍니다.
// syscall.c 위쪽에 함수 선언
int sys_wait(int pid);
int sys_wait(int pid)
{
// 실제 로직은 process_wait() 내부에 있음
return process_wait(pid);
}
Extra: sys_write 시스템 콜 임시로 구현하기 (syscall.c)
write 시스템 콜은 원래 Part2에서 파일 시스템 관련한 시스템 콜 구현에 할당된 부분입니다. 그런데 테스트 시에 유저 프로그램 내에서 msg 명령어로 write 시스템 콜을 호출해서 나온 출력이 정답 출력과 같은지 비교하기 때문에, 기본 출력에 대한 write 시스템 콜이 구현되어 있지 않으면 기본적인 인자 전달 테스트조차 통과할 수 없게 됩니다.
때문에 임시로 fd가 1일 때, 즉 stdout인 경우에만 제한적으로 콘솔에 출력할 수 있게끔 만들어주어야 기본적인 테스트 및 로그 출력을 통한 디버깅이 가능해집니다.
시스템 콜 핸들러에 SYS_WRITE 케이스 추가
case SYS_WRITE:
f->R.rax = sys_write((int)arg1, (const void *)arg2, (unsigned)arg3);
break;
구현 코드
// syscall.c 위쪽에 함수 선언
static int sys_write(int fd, const void *buffer, unsigned size);
static int sys_write(int fd, const void *buffer, unsigned size)
{
// 유저 포인터가 유효한지 검증 (전체 영역 검사 이전에 시작 주소 먼저)
check_address(buffer);
// buffer가 가리키는 전체 메모리 영역이 유저 공간에 있는지 확인
for (unsigned i = 0; i < size; i++) {
check_address((const uint8_t *)buffer + i);
}
// stdin(0), stderr(2)은 write 대상이 아님 → 에러 반환
if (fd == 0 || fd == 2) {
return -1;
}
// stdout (fd == 1): 콘솔 출력 → putbuf로 출력하고 size만큼 썼다고 리턴
if (fd == 1)
{
putbuf(buffer, size); // 콘솔에 buffer 내용을 출력
return size; // 실제 쓴 바이트 수 반환
}
}
프로세스 동기화 및 종료하기
운영체제에서 프로세스는 실행 중에 동적으로 생성되고 종료되며, 하나의 부모 프로세스는 여러 자식 프로세스를 가질 수 있습니다. 이때 동기화 처리가 제대로 되지 않으면, 자식이 생성되기도 전에 부모가 종료되거나, 자식이 종료되기 전에 부모가 먼저 종료되어 자식이 좀비 프로세스로 남는 문제가 발생할 수 있는데요. 때문에 프로세스 간 생성과 종료 시점을 적절히 조율하는 동기화가 꼭 필요하다고 할 수 있습니다.
thread 구조체에 부모-자식 동기화 관련 필드 추가하기 (thread.h)
참고로 이전에 sys_exit
을 구현하며 추가했던 exit_status 필드 또한 부모-자식 동기화 관련 필드입니다.
// struct thread 안의 필드로 추가
struct semaphore wait_sema; // 부모의 자식 종료 대기용 세마포어
struct semaphore exit_sema; // 자식 종료 시 자식의 부모 wait 마무리 대기용 세마포어
struct list children; // 자식 프로세스 리스트
struct list_elem child_elem; // 부모의 children 리스트에 들어갈 element
struct thread *parent; // 부모 프로세스 포인터
synch 모듈 참조 추가 (thread.h)
만약 해당 참조가 없는 경우, 추가해줍니다.
#include "threads/synch.h" // semaphore, lock 등 구조체 정보 정의
추가한 필드 초기화하기 (thread.c)
// init_thread 내부에서 초기화
sema_init(&t->wait_sema, 0);
sema_init(&t->exit_sema, 0);
list_init(&t->children);
스레드 생성 시 부모-자식 관계 반영하기 (thread.c)
기존 cur 스레드를 가져오는 부분과 if문 사이에 부모 설정 및 자식 리스트에 넣는 부분을 추가해 줍니다.
tid_t thread_create(const char *name, int priority,
thread_func *function, void *aux)
{
/* thread_unblock(t); 이전 코드 생략.. */
struct thread *cur = thread_current();
// 자식 스레드의 부모를 cur로 설정
t->parent = cur;
// 부모 스레드의 자식 리스트에 자식 스레드 추가
list_push_back(&cur->children, &t->child_elem);
/* priority 비교 코드 이후 생략.. */
}
process_wait 수정하기 (process.c)
기존에 임시 방편으로 무한 루프를 시켜놓았던 바로 그 함수인데요. 이제는 세마포어를 사용해 부모 프로세스가 자식 프로세스가 종료될 때까지 대기 상태로 들어가 CPU 자원을 양보한 채 기다리게 됩니다.
참고로 wait_sema
는 자식의 것을 참조하게 되는데요. 자식은 하나의 부모만 가지지만, 부모는 여러 자식을 가질 수 있기 때문에 자식의 세마포어를 사용합니다. 만약 부모의 세마포어를 사용할 경우 최초 자식에 대한 대기 이후에 제 기능을 하지 못하게 됩니다.
구현 코드
int process_wait(tid_t child_tid) {
// 인터럽트를 비활성화하여 동기화 문제를 방지하고 현재 스레드를 얻음
enum intr_level old_level = intr_disable();
struct thread *cur = thread_current();
// 현재 스레드(부모)의 자식 리스트에서 주어진 TID를 가진 자식을 탐색
struct thread *search_cur = get_child_by_tid(child_tid);
intr_set_level(old_level); // 인터럽트 다시 활성화
// 만약 해당 자식이 존재하지 않는다면 잘못된 접근이므로 -1 반환
if (search_cur == NULL)
return -1;
// 자식이 종료될 때까지 부모 프로세스를 대기 상태로 전환 (세마포어 다운)
sema_down(&search_cur->wait_sema);
// 이후 자식 종료 시 process_exit으로부터 대기를 마치고 깨어남 (세마포어 업)
// 자식의 종료 상태(exit_status)를 받아옴
int stat = search_cur->exit_status;
// 자식 리스트에서 해당 자식 정보를 제거
list_remove(&search_cur->child_elem);
// 자식이 완전히 종료될 수 있도록 process_exit의 자식을 깨워줌 (세마포어 업)
sema_up(&search_cur->exit_sema);
// 자식의 종료 상태를 부모에게 반환
return stat;
}
헬퍼 함수: get_child_by_tid 구현하기 (process.c)
자식 스레드 중에서 주어진 tid 값을 가진 스레드를 찾아 반환합니다.
// process.h에 함수 선언 추가
struct thread *get_child_by_tid(tid_t child_tid);
struct thread *get_child_by_tid(tid_t child_tid) {
struct thread *cur = thread_current(); // 현재 실행 중인 스레드(=부모 스레드)를 가져옴
struct thread *v = NULL; // 결과를 저장할 포인터
// 현재 스레드의 자식 리스트를 순회함
for (struct list_elem *i = list_begin(&cur->children);
i != list_end(&cur->children);
i = i->next) {
// 리스트 요소 i를 thread 구조체로 변환
struct thread *t = list_entry(i, struct thread, child_elem);
// 자식 스레드의 tid가 찾고자 하는 child_tid와 같다면
if (t->tid == child_tid) {
v = t; // 찾은 자식 스레드를 v에 저장
break; // 더 이상 탐색할 필요 없으므로 반복문 종료
}
}
return v; // 찾았으면 해당 스레드 포인터 반환, 못 찾았으면 NULL 반환
}
process_exit 수정하기 (process.c)
자식 프로세스 종료에 대한 처리를 담당하는데, process_wait
과 짝을 이뤄 동기화 및 작업을 수행합니다. 모든 동기화에서는 똑같이 자식의 세마포어를 참조하지만, wait_sema가 자식 종료를 기다리기 위한 부모용 세마포어라면, exit_sema는 부모의 자식 상태 회수를 기다리기 위한 자식용 세마포어라는 점이 다릅니다.
void process_exit(void) {
// 현재 종료 중인 프로세스(스레드)를 가져옴
struct thread *cur = thread_current();
// 프로세스 리소스 정리
process_cleanup();
// 부모 프로세스가 존재하는 경우 동기화 처리 진행
if (cur->parent != NULL) {
// process_wait에서 부모가 기다리고 있다면 이를 깨워줌 (세마포어 업)
sema_up(&cur->wait_sema);
// 부모가 자식의 상태를 회수할 때까지 대기 (세마포어 다운)
sema_down(&cur->exit_sema);
}
}
부모-자식 프로세스 종료 동기화 과정
- 부모 프로세스가 실행 중 자식 프로세스를 생성
- 이후 자식의 종료를 기다리기 위해
process_wait()
을 호출하여 wait_sema를 DOWN하고 대기
- 이후 자식의 종료를 기다리기 위해
- 자식 프로세스가 실행을 마친 뒤
process_exit()
을 호출하여 종료 상태를 부모에게 알림- wait_sema를 UP하여 부모를 깨워 종료 상태 회수 요청
- 자식은 자신의 상태가 부모에 의해 회수될 때까지 exit_sema를 DOWN하고 대기
- 부모가 자식의 종료 상태를 회수하면, exit_sema를 UP하여 자식을 깨움
- 최종적으로 자식 종료
마치면서
이러한 과정을 마치면 프로젝트에서 기본적인 유저 프로그램 실행과 일부 시스템 콜 처리, 그리고 동기화된 프로세스 대기 및 종료가 가능해지는데요. 이제까지의 고생을 자축하고 싶지만, 아직도 해결해야 할 문제들이 많이 남아 있습니다. 이어지는 Part2에서는 그러한 부분들에 대해 다뤄보고, 구현해보도록 하겠습니다. 감사합니다!
'크래프톤 정글 > Code 정글(C언어)' 카테고리의 다른 글
[Pintos] User Programs Part2 구현하기 (동기화 처리된 fork 구현, 파일 시스템 관련 시스템 콜 마무리) (0) | 2025.05.23 |
---|---|
[Pintos] User Programs Part 2 전체적인 큰 그림 그리기 (0) | 2025.05.21 |
[Pintos] Windows에서 make check가 안 될 때 해결 방법 (Error 127) (0) | 2025.05.19 |
[Pintos] 트러블슈팅: 기본 제공 함수에서 오류가 발생하는 문제 해결하기 (0) | 2025.05.17 |
[Pintos] User Programs Part1 전체적인 큰 그림 그리기 (0) | 2025.05.16 |