이번 글은 OSTEP 5장 Process API를 공부하며 정리한 내용입니다. 이전 장에서 프로세스가 무엇인지 봤다면, 이번 장에서는 사용자가 운영체제에 어떤 요청을 보내 프로세스를 만들고 제어하는지 살펴봅니다.
한 문장 요약
Unix는 fork(), exec(), wait()를 조합해 프로세스를 만들고 제어하며, 이 API가 분리되어 있기 때문에 셸은 실행 직전에 입출력 재지정이나 파이프 같은 환경 설정을 할 수 있습니다.
핵심 질문
| 질문 | 핵심 답변 |
|---|---|
| 운영체제는 프로세스 생성을 위해 어떤 인터페이스를 제공하는가? | Unix는 새 프로세스를 복제하는 fork(), 자식 종료를 기다리는 wait()/waitpid(), 현재 프로세스 이미지를 다른 프로그램으로 바꾸는 exec() 계열 호출을 제공합니다. |
fork()는 왜 특이한가? |
한 번 호출되지만 부모와 자식 두 프로세스에서 각각 반환됩니다. 부모는 자식 PID를 받고, 자식은 0을 받기 때문에 같은 코드에서 서로 다른 분기를 실행할 수 있습니다. |
| 부모가 자식을 기다리려면 어떻게 하는가? | wait() 또는 waitpid()를 호출해 자식이 종료될 때까지 대기합니다. 이 호출은 종료된 자식의 PID와 종료 상태를 회수하는 역할도 합니다. |
| 다른 프로그램을 실행하려면 어떻게 하는가? | 자식 프로세스에서 exec() 계열 함수를 호출합니다. 성공하면 현재 프로세스의 코드와 데이터가 새 프로그램으로 교체되며, 기존 프로그램 흐름으로 돌아오지 않습니다. |
왜 fork()와 exec()을 분리했는가? |
분리 덕분에 부모 또는 자식은 exec() 전에 파일 디스크립터, 환경 변수, 파이프, 표준 입출력 등을 설정할 수 있습니다. 이것이 Unix 셸의 명령 실행, 입출력 재지정, 파이프 구현의 기반입니다. |
| 실행 순서는 항상 같은가? | fork() 이후 부모와 자식 중 누가 먼저 실행될지는 스케줄러 결정에 따라 달라집니다. wait()처럼 명시적으로 순서를 강제하지 않으면 출력 순서도 비결정적일 수 있습니다. |
fork()
fork()는 현재 프로세스를 복사해 자식 프로세스를 만듭니다.
| 위치 | 반환값 | 의미 |
|---|---|---|
| 부모 프로세스 | 자식 PID | 부모는 어떤 자식을 만들었는지 알 수 있습니다. |
| 자식 프로세스 | 0 | 자식은 자신이 자식 경로를 실행해야 함을 알 수 있습니다. |
| 실패 | -1 | 새 프로세스를 만들지 못했습니다. |
fork() 직후 부모와 자식은 같은 코드 위치에서 실행을 이어가지만, 각자 독립된 주소 공간과 레지스터 상태를 가집니다. 따라서 같은 변수 이름을 사용해도 부모와 자식의 변경은 서로의 메모리에 직접 영향을 주지 않습니다.
|
1 2 3 4 5 6 7 8 9 |
pid_t rc = fork(); if (rc < 0) { perror("fork"); } else if (rc == 0) { printf("child\n"); } else { printf("parent: child pid=%ld\n", (long) rc); } |
실행 예제: code/ostep/process-api/p1_fork.c
비결정적 실행 순서
fork() 후에는 부모와 자식이 모두 실행 가능한 상태가 됩니다. 어느 쪽이 먼저 CPU를 받을지는 스케줄러가 결정합니다.
- 부모 프로세스가
fork()를 호출합니다. - 부모 경로에서는
fork()가 자식 PID를 반환합니다. - 자식 경로에서는
fork()가 0을 반환합니다. - 스케줄러가 부모와 자식 중 다음에 실행할 프로세스를 선택합니다.
- 따라서 부모가 먼저 실행될 수도 있고, 자식이 먼저 실행될 수도 있습니다.
wait()가 없으면 출력 순서는 실행할 때마다 달라질 수 있습니다. 운영체제 관점에서는 둘 다 준비 상태의 프로세스이며, 누가 먼저 실행되는지는 정책의 결과입니다.
wait()와 waitpid()
부모가 자식의 종료를 기다려야 한다면 wait() 또는 waitpid()를 사용합니다.
| 호출 | 용도 |
|---|---|
wait(&status) |
종료된 자식 하나를 기다립니다. |
waitpid(pid, &status, options) |
특정 자식이나 조건을 지정해 기다립니다. |
wait()는 단순히 순서를 맞추는 도구가 아닙니다. 자식의 종료 상태를 회수하고, 운영체제가 자식 프로세스에 남아 있던 자원을 정리할 수 있게 합니다.
|
1 2 3 4 5 6 7 8 9 10 11 |
pid_t child = fork(); if (child == 0) { printf("child runs first\n"); fflush(stdout); _exit(0); } int status = 0; pid_t done = waitpid(child, &status, 0); printf("parent collected child pid=%ld\n", (long) done); |
실행 예제: code/ostep/process-api/p2_wait.c
exec() 계열
exec()는 새 프로세스를 만드는 호출이 아닙니다. 현재 프로세스의 프로그램 이미지를 다른 프로그램으로 바꿉니다.
| 구분 | 설명 |
|---|---|
| 호출 전 | 현재 프로세스는 기존 프로그램 코드를 실행 중입니다. |
| 호출 성공 | 코드, 정적 데이터, 힙, 스택이 새 프로그램 기준으로 바뀝니다. |
| 호출 후 | 성공한 exec()는 기존 코드로 반환하지 않습니다. |
| 호출 실패 | -1을 반환하고 errno가 설정됩니다. |
|
1 2 3 |
char *args[] = {"wc", "-l", "p3_exec_wc.c", NULL}; execvp(args[0], args); perror("execvp"); // execvp가 실패했을 때만 실행됨 |
실행 예제: code/ostep/process-api/p3_exec_wc.c
셸이 명령을 실행하는 방식
Unix 셸은 대체로 다음 순서로 명령을 실행합니다.
- 셸이
fork()로 자식 프로세스를 만듭니다. - 자식 프로세스가 입출력 재지정, 파이프, 환경 변수 등을 설정합니다.
- 자식 프로세스가
exec()로 실제 프로그램을 실행합니다. - 부모인 셸은 필요하면
wait()또는waitpid()로 자식 종료를 기다립니다. - 프로그램이 종료되면 셸은 종료 상태를 회수하고 다음 명령을 받을 준비를 합니다.
fork()와 exec()이 분리되어 있기 때문에 자식은 새 프로그램으로 바뀌기 직전에 표준 출력, 표준 입력, 파이프 등을 원하는 형태로 바꿀 수 있습니다.
입출력 재지정
셸에서 다음 명령을 실행한다고 생각해 봅니다.
|
1 |
wc p3_exec_wc.c > line-count.txt |
핵심 아이디어는 자식 프로세스가 exec()를 호출하기 전에 표준 출력을 파일로 바꾸는 것입니다.
|
1 2 3 |
close(STDOUT_FILENO); open("line-count.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644); execvp("wc", args); |
이후 새 프로그램이 STDOUT_FILENO에 쓰는 모든 출력은 터미널이 아니라 파일로 갑니다.
실행 예제: code/ostep/process-api/p4_redirect.c
기타 프로세스 API
| 도구 | 역할 |
|---|---|
kill() |
프로세스에 시그널을 보냅니다. 이름과 달리 종료뿐 아니라 다양한 이벤트 전달에 사용됩니다. |
ps |
현재 실행 중인 프로세스 목록을 봅니다. |
top |
프로세스별 CPU, 메모리 사용량을 실시간으로 봅니다. |
man |
시스템 콜과 라이브러리 호출의 정확한 인자, 반환값, 오류 조건을 확인합니다. |
시스템 프로그래밍에서는 반환값과 오류 조건을 정확히 알아야 하므로 man fork, man execvp, man waitpid처럼 매뉴얼을 확인하는 습관이 중요합니다.
정리
fork()는 현재 프로세스를 복제하고, exec()는 현재 프로세스를 새 프로그램으로 바꾸며, wait()는 부모가 자식의 종료를 기다리고 정리하게 합니다.
세 호출을 분리한 설계는 처음에는 이상해 보이지만, 셸이 명령 실행 직전에 입출력 재지정, 파이프, 환경 설정을 할 수 있게 만드는 강력한 구조입니다. Unix 프로세스 API의 핵심은 이 조합을 이해하는 데 있습니다.