fork 를 알아보자

Matthew Woo·2022년 1월 15일
2

Computer Science

목록 보기
5/12

필자가 포크를 처음 들어본 건 코인과 관련된 용어였다. 비트코인을 포크한다, 뭘 포크한다.
비트코인을 긁어와서 동일한데 다른걸 만든다 뭐 그런 얘기였던거 같은데..

이제 cs에서 말하는 fork()를 정리해보고 넘어가고자 한다. fork 를 처음 봤을 때 궁금했던 점이나 늘 궁금했었던 부분을 이번 기회에 알아보고 정리하고자 한다.

목표

필자가 처음 cs에서 접하여 이해하게 된 fork는,
fork한 부모프로세스를 복사하여 자식프로세스를 만든다. 였다.

그럼 여기서 드는 궁금한 점.

  • 그럼 fork 이후에 parent, child 중에 뭐부터 실행될까?

  • fork 이후에 parent, child 중에 무엇부터 종료될까?

  • parent, child process간에 동일한걸 복사하여도 PID를 비롯하여 다른 부분들이 있을거로 이로 인해 위 두 질문에도 답이 풀릴거같은데 뭐가 같고 뭐가 다른걸까?

  • 자식프로세스(B)를 생성하는 부모프로세스(A)가 fork를 하면, 그럼 그 자식 프로세스(B)도 자식프로세스인 (C)를 fork하고, 그럼 프로세스(C)도 fork하여 프로세스(D)를 만들고.. 끝 없는 fork에 빠지는거 아닌가? 당연히 아니겠지만 나는 지금 뭘 몰랐었기에 이런 생각을 하게 되었던거지?


fork란?

In computing, particularly in the context of the Unix operating system and its workalikes, fork is an operation whereby a process creates a copy of itself. - wikipedia

forkfork한 프로세스를 copy하여 프로세스를 생성하는 시스템콜 이다.

fork를 하고 나면 copy를 해왔기에 동일하지만 별개의 프로세스이기에 (마치 아메바 처럼..) 내용은 동일하지만 별개의 가상메모리를 갖고 있다.

재밌는 점은 동일한 내용의 별개의 가상메모리인데 실제 물리 메모리에서는 같은 frame(영역)을 참조하고 있다. 이는 가상메모리-물리메모리의 특성인지라 간단하게만 언급하자면, 여러 프로세스가 각자의 가상메모리에서 동일하게 보유한 부분은 물리메모리에서는 실제로 같은 부분을 참조하면서 각각의 가상메모리에 매핑되어 있다.

fork를 처음 공부하면 신기했던 점은 프로세스를 생성할거면 그냥 새로 만들면 되지 왜 굳이 있는걸 복사해와서 다른걸로 바꿔주지..? 라는 생각을 했었다. 헌데 프로세스를 fork하고 copy해 온들 실제 메모리 상에서는 새로 같은걸 복사되어 있는 것이아니다. 복사된 child 프로세스는 동일한 데이터들을 참조만 해주고 있기에.

코드 예시

int main(void)
{
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    }
    else if (pid == 0) { // child 프로세스가 실행할 부분
        printf("Hello from the child process!\n");
        _exit(EXIT_SUCCESS);
    }
    else { // parent 프로세스가 실행할 부분
        int status;
        (void)waitpid(pid, &status, 0);
    }
    return EXIT_SUCCESS;
}

fork 가 성공하면 child 프로세스를 생성하고 그 child process의 pid를 return 한다. 그리고 두 프로세스의 main 함수가 실행된다. 여기서 중요한 포인트이자 필자가 착각했던 부분이 있다. parent 프로세스가 copy되었고 각 parent, child 프로세스도 같은 copy된 main프로세스를 실행하게 되니, child도 fork를 또 수행하는 것 아닌가라는 생각을 했었는데 child process는 fork를 수행하지 않는다. 왜냐하면 child 프로세스는 parent부터 동일한 pc(program counter), 같은 CPU registers, same open files 들을 그대로 복사해왔기에 parent가 fork까지는 수행했으니 그 다음실행되어야하는 pc, 레지스터 값들을 수행하면 fork 이후부터 동일한 코드로 실행되는 것이다 두둥!! 그러므로 처음에 의문을 가졌던 fork 지옥에 빠지지 않는다. 두 프로세스간 차이는 parent 프로세스는 pid는 child 프로세스의 pid 값을 return 받고, 자식프로세스는 pid == 0이라는 차이가 있어 fork 이후의 가정 코드들에서 구분하여 코드를 실행할 수 있다.

또 다른 예시를 보자.


#include <stdio.h>
#include <sys/types.h>
int main()
{
    fork();
    fork();
    fork();
    printf("hello\n");
    return 0;
}

여기서 hello 는 몇 번 출력될까?

답은 여덟번!!

(위 필자의 그림이 허접하여 보다 정확한 그림도 하나 들고옴..)

또 다른 예시,

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
  
void forkexample()
{
    int x = 1;
  
    if (fork() == 0)
        printf("Child has x = %d\n", ++x);
    else
        printf("Parent has x = %d\n", --x);
}
int main()
{
    forkexample();
    return 0;
}

결과는 어떻게 될까?

output: 
Parent has x = 0
Child has x = 2
     (or)
Child has x = 2
Parent has x = 0

이 예시에서 포인트는 두 가지다. child 이후 동일한 pc, 레지스터 값들을 가져왔지만 이후는 별개의 프로세스이기에 x라는 변수는 동일한 값을 별개로 갖고 있기에 child, parent의 연산은 누가 먼저 실행되건 서로 영향을 주지 않는 다는 것이며

os가 child, parent 중 어떤 것을 먼저 스케쥴링하여 실행시킬지 순서가 보장되지 않다.

그렇기에 보통 부모 프로세스는 자식프로세스를 fork하면서 wait 을 통해 자식프로세스가 실행되고 종료될 때까지 기다리게한다.

자식프로세스는 실행해야할 execution들이 끝나고 나면 메인함수의 return 값 혹은 에러가 발생했다면 에러코드 등 exit status를 부모함수에게 전달하고자하는데 부모프로세스가 이를 받아 자식프로세스를 종료해주지 않는다면 자식프로세스는 이를 전달하기 위해 계속 남아있게 됩니다. 이를 좀비 프로세스라고함. kill system call로도 처리할 수 없기에 부모프로세스가 이 자식 프로세스를 reap 시켜주는 것이 중요함. 실행이 종료되었는데도 계속 메모리 자원을 차지하고 남아있어선 안되기에.

좀비 프로세스를 예방하는 방법

  1. wait 시스템콜 사용
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
 
int main()
{
    int i;
    int pid = fork();
    if (pid==0)
    {
        for (i=0; i<5; i++)
            printf("I am Child\n");
    }
    else
    {
        wait(NULL);
        printf("I am Parent\n");
        while(1){
        };
    }
}
output
I am Child
I am Child
I am Child
I am Child
I am Child
I am Parent

wait 시스템콜은 자식 프로세스가 execution들을 다 수행하여 exit status를 전달받고 reap한 뒤 이후 parent 본인의 이후 코드들을 수행한다.

  1. SIGCHLD signal
    SIGCHLD signal 방식은 child process가 종료될 때까지 parent process가 기다리지 않는다.
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
 
int main()
{
    int i;
    int pid = fork();
    if (pid == 0)
        for (i=0; i<20; i++)
            printf("I am Child\n");
    else
    {
        signal(SIGCHLD,SIG_IGN);
        printf("I am Parent\n");
        while(1);
    }
}

1번 wait 코드와는 달리 parent가 child를 기다리지 않음을 확인할 수 있다. 이 경우 child의 exit_stauts를 확인하지 않으며

signal(SIGCHLD,SIG_IGN); 의 코드에서
child가 종료되었다는 SIGCHLD 신호가 왔을 때 자식 프로세스를 reap 한 뒤 SIGCHLD 를 무시할지(SIG_IGN) 혹은 (func)을 인자로 전달하여 자식 프로세스의 종료시 신호가 왔을 때 특정 함수가 실행되도록 할 수 있다.


정리

  • fork 이후에 parent, child 중에 뭐부터 실행될까?

    os가 스케줄링에 따라 parent부터, 혹은 child부터 실행 될 수 있음

  • fork 이후에 parent, child 중에 무엇부터 종료될까?
    마찬가지로 os 스케줄링에 따라 달라질 수 있지만 보통은 child 프로세스가 zombie 프로세스가 되지 않기 위해 자식 프로세스가 종료될 때까지 부모프로세스가 기다렸다가 reap을 해줌

  • parent, child process간에 동일한걸 복사하여도 PID를 비롯하여 다른 부분들이 있을거로 이로 인해 위 두 질문에도 답이 풀릴거같은데 뭐가 같고 뭐가 다른걸까?
    pid 가 다르고 이로 인해 fork 이후 다른 parent, child 각각 다른 execution들을 처리할 수 있다.

  • 자식프로세스(B)를 생성하는 부모프로세스(A)가 fork를 하면, 그럼 그 자식 프로세스(B)도 자식프로세스인 (C)를 fork하고, 그럼 프로세스(C)도 fork하여 프로세스(D)를 만들고.. 끝 없는 fork에 빠지는거 아닌가? 당연히 아니겠지만 나는 지금 뭘 몰랐었기에 이런 생각을 하게 되었던거지?
    child가 parent를 복사했지만 parent가 실행했던 모든 것을 실행하는 것이 아니라 program counter, regiset 값도 복사하기에 fork 이후의 instruction 을 자식프로세스가 수행하기에 무한 fork되는 것이 아님!


행여 잘못된 부분은 댓글로 남겨주시면 정말 감사합니다.
(_ _)

reference
https://en.wikipedia.org/wiki/Fork_(system_call)
https://www.geeksforgeeks.org/fork-system-call/
https://en.wikipedia.org/wiki/Zombie_process
https://www.geeksforgeeks.org/difference-between-process-parent-process-and-child-process/
https://en.wikipedia.org/wiki/Fork%E2%80%93exec
https://en.wikipedia.org/wiki/Wait_(system_call)
https://www.geeksforgeeks.org/difference-between-process-parent-process-and-child-process/

profile
지속가능하고 안정적인 시스템을 만들고자 합니다.

0개의 댓글