유닉스/리눅스 시스템에서 새로운 프로세스를 생성하는 핵심 메커니즘인 fork() 시스템 콜
- fork()는 단순해 보이지만 매우 강력한 기능이며, 쉘, 웹서버, 데이터베이스 등 많은 시스템 소프트웨어의 핵심

프로세스란?
프로세스 = 실행 중인 프로그램
- 프로그램 파일(fork) ≠ 프로세스(실행 중인 상태)
- 각 프로세스는 고유한 PID(Process ID)를 가짐
- 메모리 공간, 레지스터, 파일 디스크립터 등을 독립적으로 소유
출처: linux-in-practice GitHub 위의 코드를 기반으로 공부한 내용을 정리하였습니다!
fork() 예제 코드 분석
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <err.h>
static void child()
{
printf("I'm child! my pid is %d.\n", getpid());
exit(EXIT_SUCCESS);
}
static void parent(pid_t pid_c)
{
printf("I'm parent! my pid is %d and the pid of my child is %d.\n",
getpid(), pid_c);
exit(EXIT_SUCCESS);
}
int main(void)
{
pid_t ret;
ret = fork();
if (ret == -1)
err(EXIT_FAILURE, "fork() failed");
if (ret == 0) {
// child process came here because fork() returns 0 for child process
child();
} else {
// parent process came here because fork() returns the pid of newly created child process (> 1)
parent(ret);
}
// shouldn't reach here
err(EXIT_FAILURE, "shouldn't reach here");
}
실행 결과
user@user-utm:~/linux-in-practice/03-process-management$ gcc fork.c -o fork
user@user-utm:~/linux-in-practice/03-process-management$ ./fork
I'm parent! my pid is 21869 and the pid of my child is 21870.
I'm child! my pid is 21870.
user@user-utm:~/linux-in-practice/03-process-management$ strace ./fork
execve("./fork", ["./fork"], 0x7ffe8df068b0 /* 34 vars */) = 0
brk(NULL) = 0x5fc4d79a4000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7a5f25c3f000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=40899, ...}) = 0
mmap(NULL, 40899, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7a5f25c35000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220\243\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
fstat(3, {st_mode=S_IFREG|0755, st_size=2125328, ...}) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
mmap(NULL, 2170256, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7a5f25a00000
mmap(0x7a5f25a28000, 1605632, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7a5f25a28000
mmap(0x7a5f25bb0000, 323584, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b0000) = 0x7a5f25bb0000
mmap(0x7a5f25bff000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1fe000) = 0x7a5f25bff000
mmap(0x7a5f25c05000, 52624, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7a5f25c05000
close(3) = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7a5f25c32000
arch_prctl(ARCH_SET_FS, 0x7a5f25c32740) = 0
set_tid_address(0x7a5f25c32a10) = 21888
set_robust_list(0x7a5f25c32a20, 24) = 0
rseq(0x7a5f25c33060, 0x20, 0, 0x53053053) = 0
mprotect(0x7a5f25bff000, 16384, PROT_READ) = 0
mprotect(0x5fc4c254f000, 4096, PROT_READ) = 0
mprotect(0x7a5f25c77000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x7a5f25c35000, 40899) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLDI'm child! my pid is 21889.
, child_tidptr=0x7a5f25c32a10) = 21889
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21889, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
getpid() = 21888
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0
getrandom("\x26\xcd\x3b\x75\xc0\x3a\x98\xff", 8, GRND_NONBLOCK) = 8
brk(NULL) = 0x5fc4d79a4000
brk(0x5fc4d79c5000) = 0x5fc4d79c5000
write(1, "I'm parent! my pid is 21888 and "..., 62I'm parent! my pid is 21888 and the pid of my child is 21889.
) = 62
exit_group(0) = ?
+++ exited with 0 +++
user@user-utm:~/linux-in-practice/03-process-management$ ./fork
I'm parent! my pid is 22301 and the pid of my child is 22302.
I'm child! my pid is 22302.
user@user-utm:~/linux-in-practice/03-process-management$ ./fork
I'm parent! my pid is 22305 and the pid of my child is 22306.
I'm child! my pid is 22306.
user@user-utm:~/linux-in-practice/03-process-management$ ./fork
I'm parent! my pid is 22309 and the pid of my child is 22310.
I'm child! my pid is 22310
// 여러번 돌려보았음,,^^
fork() 시스템 콜의 동작 과정
1단계: fork() 호출 전
부모 프로세스 (PID: 22301)
├── 코드: main() 함수 실행 중
├── 메모리: 변수들 저장
└── 실행 위치: ret = fork(); 라인
2단계: fork() 호출 순간
clone() 시스템 콜이 실행됨
→ 프로세스 복제 시작
3단계: fork() 완료 후
부모 프로세스 (PID: 22301) 자식 프로세스 (PID: 22302)
├── ret = 22302 (자식의 PID) ├── ret = 0 (항상 0)
├── if (ret == 0) → False ├── if (ret == 0) → True
└── parent(ret) 실행 └── child() 실행
실행 순서와 스케줄링
왜 출력 순서가 다를 수 있는가?
- 운영체제 스케줄러가 결정함
- 부모와 자식 중 누가 먼저 실행될지는 예측 불가
- CPU 상황, 시스템 부하에 따라 달라짐
실제 실행 흐름
1. fork() 호출 → 프로세스 2개로 분할
2. 스케줄러가 실행 순서 결정
- 경우1: 부모 먼저 → parent() → exit → 자식 → child() → exit
- 경우2: 자식 먼저 → child() → exit → 부모 → parent() → exit
3. 두 프로세스 모두 종료
exit()의 역할
exit(EXIT_SUCCESS); // 프로세스 완전 종료
- 해당 프로세스만 종료 (다른 프로세스에 영향 없음)
- 자원 정리 후 운영체제에 제어권 반환
핵심 포인트
- fork() 후 코드는 같지만 데이터(ret 값)가 다름
- 실행 순서는 운영체제가 결정
- 각 프로세스는 독립적으로 실행되고 종료됨
fork() 코드 실행 과정
1단계: fork() 호출 전 (프로세스 1개)
int main(void)
{
pid_t ret;
// 여기서 프로세스 1개만 존재
ret = fork(); // 이 줄 주목하기!
2단계: fork() 호출 후 (프로세스 2개)
fork() 실행 후 같은 코드가 2개의 프로세스에서 실행됨:
부모 프로세스에서:
pid_t ret = 22302; // 자식의 PID가 저장됨
if (ret == -1)
err(EXIT_FAILURE, "fork() failed"); // 실행 안됨
if (ret == 0) {
child(); // 실행 안됨 (ret이 0이 아니므로)
} else {
parent(ret); // 이것만 실행됨!
}
자식 프로세스에서:
pid_t ret = 0; // 항상 0이 저장됨
if (ret == -1)
err(EXIT_FAILURE, "fork() failed"); // 실행 안됨
if (ret == 0) {
child(); // 이것만 실행됨!
} else {
parent(ret); // 실행 안됨 (ret이 0이므로)
}
3단계: 각 함수 실행
child() 함수 (자식 프로세스):
static void child()
{
printf("I'm child! my pid is %d.\n", getpid()); // PID: 22302
exit(EXIT_SUCCESS); // 자식 프로세스 종료
}
parent() 함수 (부모 프로세스):
static void parent(pid_t pid_c) // pid_c = 22302
{
printf("I'm parent! my pid is %d and the pid of my child is %d.\n",
getpid(), // 부모 PID: 22301
pid_c); // 자식 PID: 22302
exit(EXIT_SUCCESS); // 부모 프로세스 종료
}
핵심 포인트
- fork() 후 2개의 독립적인 프로세스가 같은 코드를 실행
- ret 변수의 값만 다름 (부모: 자식PID, 자식: 0)
- if문 때문에 다른 함수가 실행됨
- 각자 exit()로 독립적으로 종료
실행 타임라인
시간 →
프로세스1: main() → fork() → [분할됨]
↓
부모(PID:22301): parent() → exit() → 종료
자식(PID:22302): child() → exit() → 종료
두 프로세스는 동시에, 독립적으로 실행됨!
마무리
fork() 시스템 콜은 단순해 보이지만 유닉스/리눅스 시스템의 핵심 메커니즘
이 개념을 완전히 이해하면 쉘, 데몬 프로세스, 멀티프로세싱 등 다양한 시스템 프로그래밍 개념을 훨씬 쉽게 이해할 수 있음.
'system' 카테고리의 다른 글
| [시스템 프로그래밍] Tiny Shell 프로젝트: 잡 컨트롤, 시그널, 레이스 컨디션 다루기 (1) | 2025.09.09 |
|---|---|
| [시스템 프로그래밍] execve() 시스템 콜 C vs 어셈블리 비교 분석 (0) | 2025.09.08 |
| [시스템 프로그래밍] execve() 시스템 콜로 이해하는 프로세스 교체 원리 (0) | 2025.09.08 |
| [시스템 보안] 버퍼 오버플로우 (0) | 2025.09.08 |
| [시스템 프로그래밍] fork()와 execve()로 이해하는 bash의 명령어 실행 원리 (0) | 2025.09.08 |