
fork)기존 프로세스(부모)를 복제하여 새로운 프로세스(자식)를 만들 때, 부모의 코드, 데이터, 힙, 스택을 복사합니다. (실제로는 COW : Copy On Write 기법으로 처음에는 공유만 하다가 수정 시 복사) 자식은 부모의 Pending Signal이나 File Lock은 상속받지 않습니다.
PID 반환 | 자식: 0 반환open된 파일을 부모/자식이 공유하게 됩니다 (lseek 위치 공유).fork()의 핵심 개념인 복제, 동시 실행, 메모리 독립
fork()의 리턴값으로 내가 부모인지 자식인지 판단한다.#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
// 전역 변수 (메모리 독립성 확인용)
int g_score = 0;
int main() {
printf("=== [Start] 경기를 시작합니다 (PID: %d) ===\n", getpid());
// 1. 여기서 프로세스가 복제됨 (세포 분열)
pid_t pid = fork();
// 2. 분기 처리 (복제된 직후)
if (pid < 0) {
perror("Fork 실패");
return 1;
}
if (pid == 0) {
// [자식 프로세스 영역]
// 자식은 pid 변수에 0을 받음
for (int i = 0; i < 5; i++) {
g_score += 10; // 점수 증가
printf("🐥 자식: 달리는 중... (점수: %d, 내PID: %d, 부모PID: %d)\n",
g_score, getpid(), getppid());
sleep(1); // 1초 쉼
}
} else {
// [부모 프로세스 영역]
// 부모는 pid 변수에 '자식의 PID'를 받음
for (int i = 0; i < 5; i++) {
g_score += 1; // 점수 증가 (자식과 다르게 증가)
printf("🦕 부모: 관전 중... (점수: %d, 내PID: %d, 자식PID: %d)\n",
g_score, getpid(), pid);
sleep(1); // 1초 쉼
}
}
return 0;
}
=== [Start] 경기를 시작합니다 (PID: 1000) ===
🦕 부모: 관전 중... (점수: 1, 내PID: 1000, 자식PID: 1001)
🐥 자식: 달리는 중... (점수: 10, 내PID: 1001, 부모PID: 1000)
🐥 자식: 달리는 중... (점수: 20, 내PID: 1001, 부모PID: 1000)
🦕 부모: 관전 중... (점수: 2, 내PID: 1000, 자식PID: 1001)
Start 메시지는 한 번만 출력되었는가?fork() 호출 전에 printf가 있었기 때문이다. fork 이후의 코드만 복제되어 실행fork 되는 순간 메모리 공간이 완벽히 분리(Copy-on-Write)되었기 때문이다. 자식의 g_score와 부모의 g_score는 이름만 같고 완전히 다른 변수이다.exec family)fork로 만든 자식 프로세스는 보통 exec를 호출하여 새로운 프로그램으로 변신합니다. 현재 프로세스의 메모리 공간(Text, Data, Stack 등)을 비우고, 새로운 실행 파일의 코드와 데이터로 덮어씁니다. PID는 변하지 않고 유지되며, exec 성공 시, 코드 자체가 바뀌었으므로 기존 코드로 되돌아오지 않습니다.
함수 접미사 규칙:
l (List): 인자를 나열 (execl)v (Vector): 인자를 배열로 전달 (execv)p (Path): 환경변수 PATH 자동 탐색 (execlp)e (Env): 환경변수 직접 설정 (execle)exit & wait)exit vs _exit)exit(int status): C 라이브러리 함수. atexit 핸들러 실행, stdio 버퍼(printf 등) 비움(flush) 후 종료._exit(int status): 시스템 콜. 커널 즉시 종료. 버퍼 정리 안 함. (주로 fork 직후 exec 실패 시 사용).자식이 죽었는데(exit), 부모가 wait로 상태 코드를 회수해가지 않은 상태.
wait family):wait(&status): 자식이 죽을 때까지 부모가 멈춥니다.(Block)waitpid(pid, &status, options): 특정 PID를 기다리거나, WNOHANG 옵션으로 멈추지 않고(Non-blocking) 상태만 확인할 수 있습니다.SIGCHLD 시그널 핸들러를 등록하여, 자식이 죽었을 때만 waitpid를 호출하는 방식이 효율적입니다.system vs popen)직접 fork-exec-wait를 구현하기 번거로울 때 사용하는 래퍼(Wrapper) 함수들입니다.
| 구분 | system() | popen() |
|---|---|---|
| 목적 | 단순히 쉘 명령 실행 | 명령 실행 후 결과값 읽기/쓰기 |
| 구조 | fork + exec(/bin/sh) + wait | fork + exec + pipe |
| 동기화 | 명령 끝날 때까지 대기 (동기) | 스트림(FILE*)을 통해 통신 |
| 종료 | 함수 리턴 시 명령 종료됨 | pclose() 호출 시 정리됨 |
popen으로 ps 명령어를 매번 실행하면 느리고 복잡합니다. 리눅스 커널이 정보를 두는 /proc 파일을 직접 읽는 것이 가장 쉽고 빠르다고 합니다.
/proc/[PID]/stat 파일에는 프로세스 정보가 한 줄로 들어있습니다.fscanf를 쓰면 문자열 파싱(strtok) 없이 숫자만 쏙 빼낼 수 있습니다.ancestry.c)#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// PID를 주면 PPID를 리턴하는 함수 (핵심)
int get_ppid(int pid) {
char path[64];
FILE *fp;
int ppid;
// 1. 해당 PID의 정보 파일 열기
sprintf(path, "/proc/%d/stat", pid);
fp = fopen(path, "r");
if (fp == NULL) return 0; // 프로세스가 없거나 읽기 실패
// 2. 포맷 파싱: (1)PID (2)이름 (3)상태 (4)PPID
// %*d, %*s 등은 읽기만 하고 변수에 저장 안 함(Skip)
fscanf(fp, "%*d %*s %*c %d", &ppid);
fclose(fp);
return ppid;
}
int main() {
int pid = getpid(); // 내 PID부터 시작
printf("=== 프로세스 족보 추적 ===\n");
printf("나(Me): %d", pid);
// PID가 0이나 1(init)이 될 때까지 반복
while (pid > 1) {
pid = get_ppid(pid); // 부모 찾기
if (pid == 0) break; // 에러 처리
printf(" -> %d", pid);
}
printf("\n=== 추적 완료 (Root 도달) ===\n");
return 0;
}
=== 프로세스 족보 추적 ===
나(Me): 756332 -> 756322 -> 756320 -> 742339 -> 742074 -> 742019 -> 742015 -> 741984 -> 741966 -> 741965 -> 741957 -> 86526 -> 1
=== 추적 완료 (Root 도달) ===
[1] + Done "/usr/bin/gdb" --interpreter=mi --tty=${DbgTerm} 0<"/tmp/Microsoft-MIEngine-In-bk0ipbx0.cxl" 1>"/tmp/Microsoft-MIEngine-Out-iehzgvak.fdq"