suminworld

system

[시스템 프로그래밍] fork() 시스템 콜로 이해하는 프로세스 복제 원리

숨usm 2025. 9. 8. 19:46

유닉스/리눅스 시스템에서 새로운 프로세스를 생성하는 핵심 메커니즘인 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);  // 프로세스 완전 종료
  • 해당 프로세스만 종료 (다른 프로세스에 영향 없음)
  • 자원 정리 후 운영체제에 제어권 반환

핵심 포인트

  1. fork() 후 코드는 같지만 데이터(ret 값)가 다름
  2. 실행 순서는 운영체제가 결정
  3. 각 프로세스는 독립적으로 실행되고 종료됨

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);  // 부모 프로세스 종료
}

핵심 포인트

  1. fork() 후 2개의 독립적인 프로세스가 같은 코드를 실행
  2. ret 변수의 값만 다름 (부모: 자식PID, 자식: 0)
  3. if문 때문에 다른 함수가 실행됨
  4. 각자 exit()로 독립적으로 종료

실행 타임라인

시간 →
프로세스1: main() → fork() → [분할됨]
                     ↓
부모(PID:22301): parent() → exit() → 종료
자식(PID:22302): child() → exit() → 종료

두 프로세스는 동시에, 독립적으로 실행됨!


마무리

fork() 시스템 콜은 단순해 보이지만 유닉스/리눅스 시스템의 핵심 메커니즘

이 개념을 완전히 이해하면 쉘, 데몬 프로세스, 멀티프로세싱 등 다양한 시스템 프로그래밍 개념을 훨씬 쉽게 이해할 수 있음.