Krafton Jungle/3. TIL
[WEEK08] 프로세스 컨트롤
munsik22
2025. 5. 7. 11:13
📚 강의자료 : CMU CSAPP 14장
🔹 프로세스란?
- 정의 : 실행 중인 프로그램의 인스턴스
- 프로세스는 각 프로그램에 두 가지 중요한 추상화를 제공한다.
- 논리적 제어 흐름
- 각 프로그램은 CPU를 독점적으로 사용하는 것처럼 보인다.
- 컨텍스트 스위칭이라고 불리는 커널의 메커니즘에 의해 제공된다.
- 사적 주소 공간
- 각 프로그램은 메인 메모리를 독점적으로 사용하는 것처럼 보인다.
- 가상 메모리라고 불리는 커널의 메커니즘에 의해 제공된다.
- 논리적 제어 흐름
🔹 시스템 콜 에러 핸들링
- 에러에 있어서 Linux 시스템 레벨 함수는 일반적으로 -1을 리턴하고 원인을 찾기 위해 전역 변수 erno를 셋한다.
- 모든 시스템 레벨 함수의 리턴 상태를 확인해야 한다 (void형 제외)
- 예:
if ((pid = fork()) < 0) {
fprintf(stderr, "fork error: %s\n", strerror(errno));
exit(0);
}
🔹 에러 리포팅 함수
void unix_error(char *msg) /* Unix-style error */
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(0);
}
if ((pid = fork()) < 0)
unix_error("fork error");
🔹 에러 핸들링 래퍼
Stevens-style 에러 핸들링 래퍼를 사용해 나타내는 코드를 더 단순화할 수 있다.
pid_t Fork(void)
{
pid_t pid;
if ((pid = fork()) < 0)
unix_error("Fork error");
return pid;
}
pid = Fork();
🔹 프로세스 ID 취득하기
pid_t getpid(void) // Returns PID of current process
pid_t getppid(void) // Returns PID of parent process
🔹 프로세스 생성하기/종료하기
- 개발자의 관점에서, 우리는 프로세스를 다음 세 가지 상태 중의 하나로 생각할 수 있다.
- Running: 프로세스는 실행 중이거나 커널에 의해 스케쥴되어 실행되기를 기다리는 중이다.
- Stopped: 프로세스 실행이 중지되어서 다음 시그널(SIGCONT)이 있을 때까지 스케쥴되지 않는다.
- Terminated: 프로세스가 영구적으로 멈춘다.
🔸 프로세스 종료하기
- 프로세스는 다음 세 가지 이유 중 하나로 인해 종료된다.
- 기본 동작이 종료하는 것인 시그널(SIGINT, SIGKILL 등)을 받음
- main 루틴으로부터 리턴함
- exit 함수 호출
- void exit(int status)
- exit status를 가지고 종료한다.
- 일반적인 리턴 상태는 0이며, 그 외는 error다.
- exit 상태를 명시적으로 설정하는 다른 방법은 main 루틴에서 정수값을 리턴하는 것이다.
- exit은 한 번 호출되지만 절대 리턴하지 않는다.
🔸 프로세스 생성하기
- 부모 프로세스는 fork 함수를 호출함으로써 새로운 running 자식 프로세스를 생성한다.
- int fort(void)
- 자식 프로세스에는 0을, 부모 프로세스에는 자식의 PID를 리턴한다.
- 자식은 부모와 거의 동일하다.
- 자식은 부모의 가상 주소 공간과 동일한 (그러나 분리된) 복사본을 가진다.
- 자식은 부모가 오픈한 파일 디스크립터와 동일한 복사본을 가진다 (부모가 오픈한 파일을 자식도 읽고 쓸 수 있다.)
- 자식은 부모와 다른 PID를 가진다.
- fork는 한 번 호출되지만 2번 리턴한다.
- fork 예시
int main() {
pid_t pid;
int x = 1;
pid = Fork();
if (pid == 0) { /* Child */
printf("child : x=%d\n", ++x);
exit(0);
}
/* Parent */
printf("parent: x=%d\n", --x);
exit(0);
}
$./fork
parent: x=0
child : x=2
위 코드에서 x의 변화는 독립적이다: 자식에서 1 증가한 x의 값이 부모의 x에는 영향을 주지 않았다.

🔹 자식 프로세스 정리하기
- 프로세스가 종료되어도 여전히 시스템 자원을 소비하는데, 이를 '좀비'라고 부른다.
- wait나 waitpid를 사용해 부모에 의해서 종료된 자식들이 정리(reap)된다.
- 부모에게는 exit 상태 정보가 주어진다.
- 이후 커널이 좀비 자식 프로세스를 제거한다.
- 부모가 reap 하지 않으면?
- 자식 프로세스를 정리하지 않고 부모 프로세스가 종료되면, 고아 프로세스들은 init 프로세스(pid:1)에 의해 reap된다.
- 따라서 쉘이나 서버 같이 장시간 실행하는 프로세스에서만 명시적으로 reaping하면 된다.
- 좀비 예시
void fork7() {
if (fork() == 0) {
/* Child */
printf("Terminating Child, PID = %d\n", getpid());
exit(0);
} else {
printf("Running Parent, PID = %d\n", getpid());
while (1)
; /* Infinite loop */
}
}
linux> ./forks 7 &
[1] 6639
Running Parent, PID = 6639
Terminating Child, PID = 6640
linux> ps
PID TTY TIME CMD
6585 ttyp9 00:00:00 tcsh
6639 ttyp9 00:00:03 forks
6640 ttyp9 00:00:00 forks <defunct> # ps shows child process as defunct → zombie
6641 ttyp9 00:00:00 ps
linux> kill 6639
[1] Terminated
linux> ps # Killing parent allows child to be reaped by init
PID TTY TIME CMD
6585 ttyp9 00:00:00 tcsh
6642 ttyp9 00:00:00 ps
- 종료되지 않은 자식 예시
void fork8()
{
if (fork() == 0) {
/* Child */
printf("Running Child, PID = %d\n",
getpid());
while (1)
; /* Infinite loop */
} else {
printf("Terminating Parent, PID = %d\n",
getpid());
exit(0);
}
}
linux> ./forks 8
Terminating Parent, PID = 6675
Running Child, PID = 6676
linux> ps
PID TTY TIME CMD
6585 ttyp9 00:00:00 tcsh
6676 ttyp9 00:00:06 forks # 자식 프로세스는 부모가 종료된 이후에도 여전히 active하다.
6677 ttyp9 00:00:00 ps
linux> kill 6676 # 명시적으로 자식 프로세스를 kill하지 않으면 무기한적으로 계속 실행된다.
linux> ps
PID TTY TIME CMD
6585 ttyp9 00:00:00 tcsh
6678 ttyp9 00:00:00 ps
🔹 자식과 동기화하기
- 부모는 wait 함수를 호출함으로써 자식을 정리한다.
- int wait(int *child_status)
- 현재 프로세스를 자식들 중 하나가 종료될 때까지 중단시킨다.
- 리턴값은 종료되는 자식 프로세스의 PID이다.
- child_status != NULL이라면, 그것이 지시하는 정수는 자식이 종료된 이유와 exit 상태의 값으로 설정된다.
- status 값을 확인하는 매크로들이 wait.h에 정의되어 있다. (참고)
- WIFEXITED(*status_ptr)
자식 프로세스가 정상적으로 종료되었는지 (즉, main() 함수에서 반환했거나, exit() 또는 _exit() 함수를 호출했는지) 확인하여, 정상 종료된 경우 0이 아닌 값을 반환한다. - WEXITSTATUS(*status_ptr)
WIFEXITED()가 0이 아닌 값을 반환했을 때, 이 매크로는 자식 프로세스가 exit() 또는 _exit() 함수에 전달했거나 main() 함수에서 반환한 상태 인자의 하위 8비트 값을 평가하여 반환한다. - WIFSIGNALED(*status_ptr)
자식 프로세스가 캐치되지 않은(처리되지 않은) 시그널 때문에 종료되었는지 확인하여, 시그널에 의해 종료된 경우 0이 아닌 값을 반환한다. - WTERMSIG(*status_ptr)
WIFSIGNALED()가 0이 아닌 값을 반환했을 때, 자식 프로세스를 종료시킨 시그널의 번호를 평가하여 반환한다. - WIFSTOPPED(*status_ptr)
자식 프로세스가 현재 중지(stopped) 상태인지 확인하여, 중지된 경우 0이 아닌 값을 반환한다. waitpid() 함수를 WUNTRACED 옵션과 함께 사용한 후에만 사용해야 한다. - WSTOPSIG(*status_ptr)
WIFSTOPPED()가 0이 아닌 값을 반환했을 때, 자식을 중지시킨 시그널의 번호를 평가하여 반환한다. - WIFCONTINUED(*status_ptr)
자식 프로세스가 작업 제어(job control) 중지 상태에서 다시 실행되었는지 확인하여, 다시 실행된 경우 0이 아닌 값을 반환한다. waitpid() 함수를 WCONTINUED 옵션과 함께 사용한 후에만 사용해야 한다.
- WIFEXITED(*status_ptr)
- wait 예시
void fork9() {
int child_status;
if (fork() == 0) {
printf("HC: hello from child\n");
exit(0);
} else {
printf("HP: hello from parent\n");
wait(&child_status);
printf("CT: child has terminated\n");
}
printf("Bye\n");
}

- 여러 개의 자식이 완료된다면 임의의 순서로 종료된다.
void fork10() {
pid_t pid[N];
int i, child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
exit(100+i); /* Child */
}
for (i = 0; i < N; i++) { /* Parent */
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminate abnormally\n", wpid);
}
}
🔹 특정한 프로세스 기다리기
- pid_t waitpid(pid_t pid, int &status, int options)
- 현재 프로세스를 특정한 프로세스가 종료될 때까지 중단시킨다.
- pid : 호출자가 기다리는 자식 프로세스를 특정한다.
- pid > 0 : 프로세스 ID가 pid와 같은 특정 자식의 종료를 기다린다.
- pid == 0 : 호출자의 프로세스 그룹 ID와 같은 프로세스 그룹 ID를 가진 어떤 자식의 종료를 기다린다.
- pid == -1 : 어떤 자식 프로세스든 종료되기를 기다린다.
- pid < -1 : pid의 절댓값과 같은 프로세스 그룹 ID를 가진 어떤 자식의 종료를 기다린다.
- options
- WCONTINUED
자식 프로세스가 (SIGCONT 시그널 등으로 인해) 다시 실행되었을 때 해당 상태를 보고한다. 단순히 종료된 프로세스뿐만 아니라 다시 실행된 프로세스의 상태도 확인할 수 있게 해준다. WIFCONTINUED라는 매크로를 사용하여 해당 상태 변화가 종료인지 다시 실행된 것인지 구분할 수 있다. - WNOHANG
자식 프로세스의 상태 변화(종료, 중지, 재개 등)에 대한 정보를 즉시 요구한다. 만약 waitpid가 호출되었을 때 기다리던 자식 프로세스에서 상태 정보가 즉시 확인되면 해당 정보를 반환한다. 하지만 만약 정보가 즉시 준비되지 않았다면, waitpid는 기다리지 않고 즉시 에러 코드를 반환하여 정보가 없음을 알린다. 즉, 호출하는 프로세스가 자식 프로세스를 기다리느라 멈추는 것(suspending) 없이 상태를 확인하게 해준다. - WUNTRACED
종료된 자식 프로세스뿐만 아니라 (SIGSTOP 등으로 인해) 중지된(stopped) 자식 프로세스의 상태도 보고한다. 프로세스가 종료된 것인지 아니면 단순히 중지된 것인지 구분하기 위해 WIFSTOPPED라는 매크로를 사용할 수 있다.
- WCONTINUED
void fork11() {
pid_t pid[N];
int i;
int child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0)
exit(100+i); /* Child */
for (i = N-1; i >= 0; i--) {
pid_t wpid = waitpid(pid[i], &child_status, 0);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminate abnormally\n", wpid);
}
}
🔹 프로그램을 로딩하고 실행하기
- int execve(char *filename, char *argv[], char *envp[])
- 현재 프로세스에서 로드하고 실행한다.
- 실행 가능 파일 filename : #!interpreter로 시작하는 목적 파일이나 스크립트 파일 (예: #!/bin/bash)
- 인자 리스트 argv : argv[0] == filename
- 환경 변수 리스트 envp : "name=value"의 문자열로, getenv, putenv, printenv 사용
- 코드, 데이터, 스택을 덮어쓰고 PID, 오픈 파일과 시그널 컨텍스트를 유지한다.
- 한 번 실행되고 (에러가 발생하지 않는 한) 리턴되지 않는다.

- execve 예시 : 자식 프로세스에서 /bin/ls -lt /usr/include를 실행했을 때
if ((pid = Fork()) == 0) { /* Child runs program */
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(1);
}
}
