Interview/OS
Linux OS fork
김 정출
2024. 9. 26. 11:25
fork
fork()
는 Linux 및 Unix 기반 운영체제에서 프로세스를 생성하는 가장 기본적인 시스템 호출입니다. 이 호출은 현재 프로세스(부모 프로세스)를 복사하여 자식 프로세스를 생성하는데, 부모와 자식 프로세스는 거의 동일한 환경을 가지지만, 프로세스 ID(PID)와 몇 가지 자원은 서로 다릅니다.
fork()
호출의 내부적인 동작 방식을 단계별로 설명해 보겠습니다.
1. 시스템 호출 fork()
발생
- 사용자 공간에서
fork()
를 호출하면, 커널 모드로 진입하여 커널이 새로운 프로세스를 생성하는 작업을 시작합니다. fork()
는 부모 프로세스의 주소 공간을 그대로 복사해서 자식 프로세스를 만들지만, 커널 내부에서 몇 가지 최적화를 적용합니다.
2. 프로세스 테이블 엔트리 생성
- 커널은 프로세스 관리를 위해 프로세스 제어 블록(PCB, Process Control Block)를 사용합니다. 여기에는 PID, 부모 프로세스 ID, 프로세스 상태, CPU 레지스터, 메모리 매핑 정보, 파일 디스크립터 정보 등이 포함됩니다.
fork()
호출 시, 커널은 새로운 프로세스를 위해 새로운 프로세스 테이블 엔트리를 생성합니다.- 이 엔트리는 부모 프로세스의 PCB를 기반으로 하지만, 새로운 PID와 상태 정보 등 몇 가지는 새롭게 설정됩니다.
3. 자식 프로세스의 주소 공간 할당
fork()
는 부모 프로세스의 주소 공간을 복사합니다. 하지만 실제로 모든 메모리 페이지를 복사하는 것은 성능상 비효율적일 수 있기 때문에, Linux는 일반적으로 Copy-on-Write(COW) 전략을 사용합니다.- Copy-on-Write (COW):
- 실제 메모리 페이지는 복사하지 않고, 부모와 자식이 같은 물리적 메모리 페이지를 공유합니다.
- 부모나 자식이 그 페이지를 변경하려고 할 때, 그제서야 변경할 페이지를 복사해서 각각의 프로세스에 할당합니다.
- 이 방법은 메모리 낭비를 줄이고
fork()
의 성능을 크게 향상시킵니다.
4. 파일 디스크립터 복사
- 부모 프로세스에서 열려 있는 파일 디스크립터(file descriptor)는 자식 프로세스에게 복사됩니다.
- 자식은 부모와 동일한 파일 디스크립터를 공유하지만, 각각 독립적으로 파일 오프셋을 유지할 수 있습니다. 즉, 한 프로세스에서 파일 오프셋을 변경해도 다른 프로세스에는 영향을 주지 않습니다.
5. 프로세스 ID(PID) 할당
- 자식 프로세스는 부모와 동일한 코드를 실행하지만, PID는 다릅니다.
- 커널은 자식 프로세스에게 새로운 PID를 할당하고, 이를 프로세스 테이블에 기록합니다. 자식 프로세스는 부모 프로세스의 PID를 부모 프로세스 ID(PPID)로 저장합니다.
6. 스케줄러에 자식 프로세스 등록
- 자식 프로세스는 이제 새로운 프로세스로 간주되어 커널의 프로세스 스케줄러에 등록됩니다.
- 스케줄러는 부모와 자식 프로세스 모두를 작업 큐에 넣어 CPU에서 실행될 수 있도록 준비합니다.
7. fork()의 반환 값
fork()
는 부모와 자식 프로세스에서 각각 다른 값을 반환합니다:- 부모 프로세스에게는 자식의 PID가 반환됩니다. 이를 통해 부모는 자식 프로세스를 관리하거나 추적할 수 있습니다.
- 자식 프로세스에게는 0이 반환됩니다. 이를 통해 자식은 자신이 자식 프로세스라는 것을 알 수 있습니다.
이 차이를 이용해, 부모와 자식은 서로 다른 코드 경로를 실행할 수 있게 됩니다.
8. 프로세스 분기
fork()
이후 부모와 자식 프로세스는 동일한 코드를 실행하게 되지만,fork()
의 반환 값을 이용하여 두 프로세스가 서로 다른 작업을 할 수 있습니다.
예시:
pid_t pid = fork();
if (pid == 0) {
// 자식 프로세스: fork()의 반환값이 0
printf("This is the child process!\\n");
} else if (pid > 0) {
// 부모 프로세스: fork()의 반환값이 자식 프로세스의 PID
printf("This is the parent process! Child PID: %d\\n", pid);
} else {
// fork 실패 시
printf("Fork failed!\\n");
}
9. 자원 분리
- 자식 프로세스는 부모 프로세스의 복사본이지만,
fork()
이후 독립적인 프로세스로 동작합니다. - 부모와 자식이 공유하지 않는 자원들:
- PID: 각각 고유한 프로세스 ID를 가집니다.
- 메모리: 부모와 자식은 서로 독립된 주소 공간을 가지며, COW 전략으로 복사된 페이지는 쓰기가 발생할 때 분리됩니다.
- 파일 디스크립터 상태: 파일 디스크립터는 공유되지만, 파일 오프셋과 관련된 정보는 독립적으로 관리됩니다.
10. 프로세스 스케줄링 및 실행
- 부모와 자식은 병렬 실행되며, 운영체제의 스케줄러가 이들을 관리합니다. 어떤 프로세스가 먼저 실행될지는 스케줄러의 정책에 따라 다릅니다.
- 부모가 자식이 종료할 때까지 기다리기를 원하면,
wait()
또는 *waitpid()
를 호출하여 자식의 종료를 기다릴 수 있습니다.
fork()
동작 과정 요약
fork()
시스템 호출을 통해 부모 프로세스는 자식 프로세스를 생성합니다.- 커널은 부모의 프로세스 제어 블록을 복사하여 새로운 PCB를 생성하고, 새로운 PID를 자식에게 할당합니다.
- 주소 공간은 Copy-on-Write 방식으로 복사됩니다.
- 파일 디스크립터는 공유되지만 파일 오프셋은 독립적으로 관리됩니다.
fork()
의 반환값을 이용해 부모와 자식은 서로 다른 작업을 수행할 수 있습니다.- 부모와 자식은 스케줄러에 의해 병렬로 실행됩니다.
이러한 fork()
의 동작은 멀티프로세스 환경을 만들거나, 새로운 프로그램을 실행하기 전에 프로세스를 복사하는 데 자주 사용됩니다. fork()
와 exec()
시스템 호출을 조합하면 새로운 프로그램을 자식 프로세스에서 실행할 수 있습니다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // fork, getpid, getppid에 필요한 헤더
int main() {
// fork 호출로 새로운 프로세스 생성
pid_t pid = fork();
// fork가 반환하는 값에 따라 부모와 자식 프로세스를 구분
if (pid < 0) {
// fork 실패
perror("Fork failed");
return 1;
} else if (pid == 0) {
// 자식 프로세스에서 실행되는 코드
printf("I am the child process. My PID is %d and my parent's PID is %d.\n", getpid(), getppid());
} else {
// 부모 프로세스에서 실행되는 코드
printf("I am the parent process. My PID is %d and I created a child with PID %d.\n", getpid(), pid);
// 부모 프로세스가 자식 프로세스의 종료를 기다리기
wait(NULL); // 자식 프로세스가 종료될 때까지 대기
printf("Child process has terminated.\n");
}
return 0;
}