크래프톤 정글 (컴퓨터 시스템: CSAPP)/8장 예외적 제어 흐름

컴퓨터 시스템 : CSAPP 8장 정리 - 8.4 Process Control

고웅 2025. 4. 19. 10:12

8.4 Process Control

유닉스 시스템에서 프로세스를 생성, 종료, 관리하는 시스템 콜들을 소개한다. 프로세스를 제어하기 위한 함수들이 어떻게 동작하는지, 어떤 시점에 어떤 함수가 사용되는지를 예제와 함께 설명한다.

주요 기능:

  • 프로세스 ID 확인
  • 프로세스 생성 (fork)
  • 프로세스 종료 (exit)
  • 자식 프로세스 회수 (wait)
  • 프로그램 실행 (exec)
  • 일시 중단 및 재개

8.4.1 Obtaining Process IDs (프로세스 ID 얻기)

각 프로세스는 시스템 내에서 고유한 양의 정수인 PID(Process ID)를 갖는다.

관련 함수들

#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);     // 현재 프로세스의 PID 반환
pid_t getppid(void);    // 부모 프로세스의 PID 반환

 

  • getpid()는 현재 프로세스의 PID를 반환한다.
  • getppid()는 이 프로세스를 만든 부모의 PID를 반환한다.
  • 두 함수 모두 pid_t 타입(보통 int)을 반환하며, 에러 없이 항상 동작한다.

예제:

printf("My PID is %d\n", getpid());
printf("My Parent's PID is %d\n", getppid());

 

이 함수들은 디버깅, 프로세스 추적, 자식-부모 관계 파악 등에 유용하다​.


8.4.2 Creating and Terminating Processes (프로세스 생성과 종료)

프로세스를 생성(fork)하고 종료(exit)하는 기본적인 시스템 콜을 설명한다. 유닉스 시스템에서 프로세스를 제어하는 핵심 도구들이다.

프로세스 생성: fork

#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);
  • fork()는 현재 프로세스를 복제하여 새로운 프로세스를 생성한다.
  • 반환값:
    • 0 → 자식 프로세스에서 반환
    • > 0 (자식 PID) → 부모 프로세스에서 반환
    • < 0 → 실패 (리소스 부족 등)

중요한 특징:

  • 부모와 자식은 거의 완전히 동일한 메모리 상태에서 시작
  • 단, 주소 공간은 분리되어 있음 → 한쪽이 바꿔도 다른 쪽에 영향 없음
  • 보통 자식은 exec 호출로 새 프로그램을 실행하고, 부모는 wait로 자식 종료 대기

예제:

pid_t pid = fork();
if (pid == 0) {
    printf("Child process\n");
} else {
    printf("Parent process, child pid: %d\n", pid);
}

프로세스 종료: exit

#include <stdlib.h>

void exit(int status);
  • 프로세스를 종료하며, 종료 상태(status)를 부모에게 전달
  • 일반적으로 0은 정상 종료, 1 이상의 값은 오류

비교: return vs exit

  • main 함수에서 return은 암묵적으로 exit 호출과 같음.
  • 하지만 exit은 어디서든 명시적으로 프로세스를 종료할 수 있음.

8.4.3 Reaping Child Processes (좀비 프로세스 수거하기)

프로세스 종료 후 처리 과정과 이를 제대로 처리하지 않을 경우 발생하는 좀비(zombie) 프로세스의 개념 및 해결 방법을 설명한다.

좀비 프로세스란?

  • 프로세스가 종료되면 커널은 즉시 그 프로세스를 제거하지 않는다.
  • 대신, 부모가 종료 상태를 수거(reap)할 때까지 '종료 상태'로 유지된다.
  • 이 상태의 프로세스를 좀비(zombie)라고 부른다.
    • 살아있지는 않지만 아직 완전히 제거되지 않은 상태

수거(reaping)의 메커니즘

부모 프로세스는 자식의 종료 상태를 수거하기 위해 waitpid 함수를 사용한다:

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *statusp, int options);

 

  • 반환값:
    • 성공 시 종료된 자식의 PID
    • WNOHANG 사용 시 종료된 자식 없으면 0
    • 실패 시 -1 반환
  • 기본 동작 (options = 0): 자식 프로세스가 종료될 때까지 대기(blocking)

좀비 방지의 중요성

  • 좀비는 실행되지 않지만 시스템 메모리 등의 자원을 차지한다.
  • 부모가 종료되어 좀비를 수거하지 못하면, init 프로세스(PID 1)가 대신 수거한다.
  • 하지만 서버나 셸 같은 장기 실행 프로그램은 직접 수거해야 자원 누수가 없다.

waitpid의 다양한 옵션

  • pid 인자:
    • > 0: 특정 PID를 가진 자식만 대기
    • = -1: 모든 자식 프로세스 대기
  • options 인자:
    • WNOHANG: 자식이 종료되지 않았으면 바로 반환
    • WUNTRACED: 일시 중단된 자식도 감지

정리

개념 설명
좀비 프로세스 종료됐지만 수거되지 않은 프로세스
waitpid 자식의 종료 상태를 수거하는 함수
WNOHANG 옵션 대기하지 않고 즉시 반환
init 프로세스 부모가 종료되면 자식을 대신 수거

8.4.4 Putting Processes to Sleep (프로세스를 잠재우기)

프로세스를 일시적으로 중단(sleep)시키는 두 가지 주요 함수, sleep과 pause에 대해 설명한다.

sleep 함수

#include <unistd.h>
unsigned int sleep(unsigned int secs);

 

  • 기능: 프로세스를 secs초 동안 중단시킴
  • 반환값:
    • 요청한 시간 동안 모두 잠자면 → 0
    • 중간에 시그널로 인해 깨면 → 남은 시간(초) 반환

예: 5초를 요청했지만 3초 후 시그널이 와서 중단되면 sleep은 2를 반환

→ 시그널에 의한 중단 처리는 다음 절 8.5 "Signals"에서 다룬다.

pause 함수

#include <unistd.h>
int pause(void);
  • 기능: 시그널이 도착할 때까지 현재 프로세스를 중단시킴
  • 항상 -1 반환 (즉, 다시 실행되면 반드시 시그널이 원인)

→ 시그널을 기다리는 구조를 만들 때 유용하게 사용됨

실습 문제 (예시)

unsigned int wakeup(unsigned int secs) {
    unsigned int rc = sleep(secs);
    printf("Woke up at %d secs.\n", secs - rc + 1);
    return rc;
}

이 함수는 sleep과 동일하지만, 몇 초 후에 깨어났는지 메시지를 출력함​.


8.4.5 Loading and Running Programs (프로그램 로딩 및 실행)

현재 프로세스의 문맥(context)에서 새로운 프로그램을 로딩하고 실행하는 시스템 콜 execve에 대해 설명한다.
이는 fork로 프로세스를 복제한 다음, 자식 프로세스에서 execve를 호출해 기존 프로그램을 완전히 대체하는 전형적인 패턴이다.

execve 함수

#include <unistd.h>

int execve(const char *filename, const char *argv[], const char *envp[]);
  • filename: 실행할 실행 파일의 경로
  • argv[]: 인자 문자열 배열 (예: argv[0]은 프로그램 이름)
  • envp[]: 환경 변수 배열 (예: PATH=/bin 형태)

동작:

  • execve는 실행에 성공하면 절대 반환하지 않음 → 새 프로그램으로 현재 프로세스를 완전히 대체
  • 실패 시에만 -1을 반환하고, 에러는 errno에 저장됨

인자 구조

argv 배열:

  • argv는 널 종료된 포인터 배열로 각 인자는 문자열 포인터

예시:

argv[0] = "ls";
argv[1] = "-lt";
argv[2] = "/usr/include";
argv[3] = NULL;

envp 배열:

  • 환경 변수도 name=value 형식 문자열의 널 종료 배열

예시:

envp[0] = "USER=droh";
envp[1] = "PRINTER=iron";
envp[2] = NULL;

스택 초기 구성

  • execve가 성공하면, OS는 새로운 프로그램의 스택을 다음과 같이 구성함:
    1. 문자열 영역 (인자 및 환경 변수 문자열)
    2. envp[] 포인터 배열
    3. argv[] 포인터 배열
    4. argc 값 (인자 개수)
    5. startup 루틴 (__libc_start_main)으로 점프 → main 호출

이 과정은 Section 7.9에서 설명된 시작 코드(start-up code)를 통해 진행됨​.


8.4.6 Using fork and execve to Run Programs (프로그램 실행을 위한 fork와 execve 사용)

셸(shell)과 같은 프로그램이 fork와 execve를 결합해 외부 프로그램을 실행하는 방식을 설명한다.

개요

  • 셸(shell)은 사용자 명령어를 읽고, 분석(parse)한 후, 사용자가 요청한 프로그램을 실행하는 인터페이스 프로그램이다.
  • 실행은 일반적으로 다음 순서로 이루어진다:
    1. 명령어 입력 받기
    2. 명령어 해석 (파싱)
    3. fork로 자식 프로세스 생성
    4. 자식에서 execve로 프로그램 실행
    5. (필요시) 부모가 waitpid로 자식 종료 대기

기본 셸 루프 구조

while (1) {
    printf("> ");
    Fgets(cmdline, MAXLINE, stdin);  // 명령어 입력
    eval(cmdline);                   // 명령어 평가 및 실행
}

eval 함수 동작

  1. 명령어를 **parseline**을 통해 공백으로 분리 → argv[] 배열 구성
  2. 첫 번째 인자가 내부 명령어(quit, &)면 즉시 처리
  3. 내부 명령어가 아니면 fork 호출 → 자식 프로세스에서 execve 실행
  4. 백그라운드(&) 실행이면 부모는 waitpid 하지 않고 바로 다음 명령어 대기
  5. 포그라운드 실행이면 부모는 waitpid로 자식 종료 대기

내부 명령어 vs 외부 명령어

  • 내부 명령어 (quit, &): builtin_command()에서 처리
  • 외부 명령어: execve(argv[0], argv, environ) 호출로 프로그램 실행

프로그램 vs 프로세스

  • 프로그램: 실행 가능한 코드, 데이터 파일 (디스크 위 객체)
  • 프로세스: 실행 중인 프로그램 인스턴스 (메모리와 컨텍스트 포함)
  • fork: 현재 프로세스를 복제
  • execve: 현재 프로세스에 새 프로그램 로드 (PID는 유지됨)