Introduction


언어를 공부할 때 항상 보는 예제들이 있는데, 그 중 하나가 피보나치 수열 출력 예제이다. 로직이 간단하고 반복문을 활용하는 법을 익힐 수 있기 때문에 매번 등장하는 것 같다. 이번 포스트에선 fork() 함수와 wait() 함수에 대해 알아보고 그것들을 활용하여 간단하게 피보나치 수열 출력 예제를 풀어보겠다.

일단 피보나치 수열은 다음의 점화식으로 정의되는 수열이다.

이 피보나치 수열을 출력하는 로직은 아래에 있는 코드처럼 매우 간단하다. 하지만 이번 포스트에서의 중점은 피보나치 수열에 있는 것이 아니고 fork() 함수에 있다. fork() 함수를 이용하여 자식 프로세스를 분기로 따내고 그 프로세스에서 일련의 작업을 마친 후 다시 부모 프로세스로 넘어와 새로운 작업을 시작하는 방법을 알아 볼 것이다. 먼저 fork() 함수에 대해 알아보자.

for(register int i = 1; i < arg - 1; i++) {
    fib_n = fib_0 + fib_1;
    fib_0 = fib_1;
    fib_1 = fib_n;
    printf(", %lld", fib_n);
}

fork()


fork()는 현재 실행되는 프로세스의 복사본을 생성하는 함수이다. 참고로 프로세스는 Disk Storage(i.e., HDD, SSD.)에 저장되어 있던 프로그램이 메모리에 올려지고 OS의 스케쥴에 따라 실행되는 하나의 실행 단위이다.

Header unistd.h
Format pid_t fork(void);
Return 프로세스 생성에 실패하면 -1을, 성공 시 부모 프로세스엔 자식 프로세스 ID, 자식 프로세스엔 0을 반환

fork() 함수에 대한 설명은 Wikipedia에도 잘 나와있다. Wikipedia에 올라와있는 Hello World 예제를 한번 살펴보자.

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

   if (pid == -1) {
      perror("fork failed");
      exit(EXIT_FAILURE);
   } else if (pid == 0) {
      printf("Hello from the child process!\n");
      _exit(EXIT_SUCCESS);
   } else {
      int status;
      (void)waitpid(pid, &status, 0);
   }
   
   return EXIT_SUCCESS;
}

예제를 분석해보자.

 pid_t pid = fork();

main() 함수의 첫 구문은 fork system call을 호출하여 프로세스의 실행을 두 갈래로 나누고 있다. fork() 함수의 반환형은 POSIX type의 pid_t 구조체로 변수 pid에 그 값이 들어간다.

if (pid == -1) {
    perror("fork failed");
    exit(EXIT_FAILURE);
}

pid의 값이 -1이라는 것은 프로세스 생성에 실패했다는 뜻이다. 따라서 “fork failed”라는 오류 메시지를 출력하고 exit() 함수로 프로세스를 중단시키고 있다.

fork가 제대로 이루어져 프로세스가 성공적으로 생성되면, 프로세스가 두 갈래로 나누어짐과 동시에 pid값을 반환받은 지점부터 main() 함수가 다시 실행된다. 각각의 프로세스가 서로 다른 작업을 하길 원한다면, fork()의 반환값으로 프로그램에 분기점들을 만들어 주면 된다. 위쪽의 표에도 나와 있듯이, 프로세스 생성에 성공하면 fork()는 부모 프로세스엔 자식 프로세스 ID를, 자식 프로세스엔 0을 반환하기 때문이다. 지금 설명한 내용이 보통 아래와 같이 구현된다.

// child process
else if (pid == 0) {
    printf("Hello from the child process!\n");
    _exit(EXIT_SUCCESS);
}

// parent process
else {
    int status;
    (void)waitpid(pid, &status, 0);
}

fork()가 성공적으로 호출되면 실행되고 있는 프로세스가 그대로 복사된 후, 두 개의 프로세스에서 fork()가 호출된 시점부터 다시 실행이 시작되기 때문에, 간단한 분기로도 부모 프로세스와 자식 프로세스의 작업을 나눌 수 있는 것이다.

부모 프로세스의 waitpid() 함수는 자식 프로세스가 종료될 때까지 기다리는 함수이다. 첫번째 인자 pid는 기다릴 자식 프로세스의 ID이고 두번째 인자 status는 기다린 자식 프로세스의 종료 상태를 받을 정수형 포인터이다. 호출이 끝나면 종료된 자식 프로세스의 ID를 반환한다.

자식 프로세스는 주의해서 종료하자

자식 프로세스는 표준 C 라이브러리의 exit() 함수가 아니라 반드시 POSIX의 _exit() 함수로 종료해야 한다.


wait()


Wikipedia의 예제에서 본 waitpid() 함수와는 다른 함수지만, 물론 그 쓰임새는 동일하다. 피보나치 수열 예제에서는 wait() 함수를 이용할 것이기 때문에 잠시 알아보자.

wait() 함수는 부모 프로세스 안에서 쓰이며, 실행되고 있는 자식 프로세스가 종료할 때까지 기다리는 역할을 한다. 반환형은 pid_t 구조체로 종료된 자식 프로세스의 ID이고 waitpid() 함수와는 다르게 pid 인자가 없다. waitpid()에서는 기다릴 자식 프로세스의 ID를 명시적으로 알려주었지만 wait()에서는 암시적으로 자식 프로세스의 ID를 파악한다.

Header wait.h
Format pid_t wait(int *status);
Parameters int status: 종료된 자식 프로세스의 상태
Return pid_t: 종료된 자식 프로세스의 ID

피보나치 출력 예제


다음은 시스템 인자로부터 출력하고 싶은 피보나치 수의 개수를 입력받아 그 만큼의 피보나치 수열을 출력하는 예제이다. 설명은 코드의 주석을 참고.

1. 컴파일


$ gcc fibonacci.c -o fibonacci

2. 실행


$ ./fibonacci 5 # 최초 5개의 피보나치 수 출력

3. 실행 결과


# 실행 결과
Parent is waiting for the child to complete.
Child forked.
0, 1, 1, 2, 3
Child ended.
Ended child process id is 16393
Child process ended successfully. Status: 0