Interview/OS

Linux OS fork

김 정출 2024. 9. 26. 11:25

fork

fork()LinuxUnix 기반 운영체제에서 프로세스를 생성하는 가장 기본적인 시스템 호출입니다. 이 호출은 현재 프로세스(부모 프로세스)를 복사하여 자식 프로세스를 생성하는데, 부모와 자식 프로세스는 거의 동일한 환경을 가지지만, 프로세스 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() 동작 과정 요약

  1. fork() 시스템 호출을 통해 부모 프로세스는 자식 프로세스를 생성합니다.
  2. 커널은 부모의 프로세스 제어 블록을 복사하여 새로운 PCB를 생성하고, 새로운 PID를 자식에게 할당합니다.
  3. 주소 공간Copy-on-Write 방식으로 복사됩니다.
  4. 파일 디스크립터는 공유되지만 파일 오프셋은 독립적으로 관리됩니다.
  5. fork()의 반환값을 이용해 부모와 자식은 서로 다른 작업을 수행할 수 있습니다.
  6. 부모와 자식은 스케줄러에 의해 병렬로 실행됩니다.

이러한 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;
}