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는 새로운 프로그램의 스택을 다음과 같이 구성함:
- 문자열 영역 (인자 및 환경 변수 문자열)
- envp[] 포인터 배열
- argv[] 포인터 배열
- argc 값 (인자 개수)
- 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)한 후, 사용자가 요청한 프로그램을 실행하는 인터페이스 프로그램이다.
- 실행은 일반적으로 다음 순서로 이루어진다:
- 명령어 입력 받기
- 명령어 해석 (파싱)
- fork로 자식 프로세스 생성
- 자식에서 execve로 프로그램 실행
- (필요시) 부모가 waitpid로 자식 종료 대기
기본 셸 루프 구조
while (1) {
printf("> ");
Fgets(cmdline, MAXLINE, stdin); // 명령어 입력
eval(cmdline); // 명령어 평가 및 실행
}
eval 함수 동작
- 명령어를 **parseline**을 통해 공백으로 분리 → argv[] 배열 구성
- 첫 번째 인자가 내부 명령어(quit, &)면 즉시 처리
- 내부 명령어가 아니면 fork 호출 → 자식 프로세스에서 execve 실행
- 백그라운드(&) 실행이면 부모는 waitpid 하지 않고 바로 다음 명령어 대기
- 포그라운드 실행이면 부모는 waitpid로 자식 종료 대기
내부 명령어 vs 외부 명령어
- 내부 명령어 (quit, &): builtin_command()에서 처리
- 외부 명령어: execve(argv[0], argv, environ) 호출로 프로그램 실행
프로그램 vs 프로세스
- 프로그램: 실행 가능한 코드, 데이터 파일 (디스크 위 객체)
- 프로세스: 실행 중인 프로그램 인스턴스 (메모리와 컨텍스트 포함)
- fork: 현재 프로세스를 복제
- execve: 현재 프로세스에 새 프로그램 로드 (PID는 유지됨)
'크래프톤 정글 (컴퓨터 시스템: CSAPP) > 8장 예외적 제어 흐름' 카테고리의 다른 글
컴퓨터 시스템 : CSAPP 8장 정리 - 8.5 Signals Part.2 8.5.7 까지 (1) | 2025.04.19 |
---|---|
컴퓨터 시스템 : CSAPP 8장 정리 - 8.5 Signals Part.1 8.5.4 까지 (0) | 2025.04.19 |
컴퓨터 시스템 : CSAPP 8장 정리 - 8.3 System Call Error Handling (1) | 2025.04.19 |
컴퓨터 시스템 : CSAPP 8장 정리 - 8.2 Processes (0) | 2025.04.19 |
컴퓨터 시스템 : CSAPP 8장 정리 - 8.1 Exceptions (2) | 2025.04.19 |