Computer >> 컴퓨터 >  >> 프로그래밍 >> C 프로그래밍

C 언어에서 fork()와 exec() 시스템 콜의 차이점 완벽 이해하기

이번 글에서는 C 언어에서 fork()exec() 시스템 콜이 어떤 역할을 하는지, 그리고 두 함수의 핵심적인 차이점은 무엇인지 자세히 살펴보겠습니다.

fork()란 무엇인가?

fork()는 호출한 프로세스를 복제하여 새로운 프로세스를 생성하는 시스템 콜입니다. 새로 생성된 프로세스를 자식 프로세스(child process)라고 부르며, 원래의 프로세스는 부모 프로세스(parent process)가 됩니다. 자식 프로세스는 다음과 같은 특성을 가집니다.

  • 자식 프로세스는 고유한 프로세스 ID(PID)를 가집니다.
  • 자식 프로세스의 부모 프로세스 ID(PPID)는 호출한 프로세스(부모)의 PID와 동일합니다.
  • 자식 프로세스는 부모의 메모리 락(memory lock)과 세마포어(semaphore)를 상속받지 않습니다.

fork()의 반환값을 통해 부모와 자식 프로세스를 구분할 수 있습니다. 반환값이 0보다 크면 그 값은 자식 프로세스의 PID이며, 현재 코드는 부모 프로세스에서 실행 중임을 의미합니다. 반대로 반환값이 0이면 자식 프로세스에서 실행 중이라는 뜻입니다. 만약 음수가 반환되면 프로세스 생성에 실패한 것입니다.

exec()란 무엇인가?

exec() 시스템 콜은 현재 실행 중인 프로세스 이미지를 새로운 프로그램으로 완전히 교체하는 역할을 합니다. 즉, 새로운 프로그램을 현재 프로세스의 메모리 공간에 로드하고, 프로그램의 진입점(entry point)부터 실행을 시작합니다. exec()가 성공적으로 호출되면 기존 코드는 더 이상 실행되지 않으며, exec() 자체는 오류가 발생했을 때만 반환됩니다.

fork()와 exec()의 핵심 차이

두 함수의 가장 큰 차이점은 다음과 같습니다.

  • fork(): 새로운 프로세스를 생성하되, 원본 프로세스의 복사본을 만듭니다. 부모와 자식 프로세스는 동시에 병렬로 실행됩니다.
  • exec(): 새로운 프로세스를 만들지 않고, 현재 프로세스의 이미지를 완전히 다른 프로그램으로 교체합니다.

실무에서는 이 두 함수를 조합하여 사용하는 경우가 많습니다. 먼저 fork()로 자식 프로세스를 만든 뒤, 자식 프로세스에서 exec()를 호출하여 다른 프로그램을 실행하는 방식입니다.

예제 코드

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>

int main() {
    pid_t process_id;
    int return_val = 1;
    int state;

    process_id = fork();

    if (process_id == -1) { // 프로세스 ID가 음수면 fork 실패
        printf("can't fork, error occured\n");
        exit(EXIT_FAILURE);
    } else if (process_id == 0) { // 자식 프로세스 영역
        printf("The child process is (%u)\n", getpid());
        char * argv_list[] = {"ls", "-lart", "/home", NULL};
        execv("ls", argv_list); // execv()는 오류 발생 시에만 반환됨
        exit(0);
    } else { // 부모 프로세스 영역
        printf("The parent process is (%u)\n", getppid());
        if (waitpid(process_id, &state, 0) > 0) { // 자식 프로세스 상태 변화 대기
            if (WIFEXITED(state) && !WEXITSTATUS(state))
                printf("program is executed successfully\n");
            else if (WIFEXITED(state) && WEXITSTATUS(state)) {
                if (WEXITSTATUS(state) == 127) {
                    printf("Execution failed\n");
                } else
                    printf("program terminated with non-zero status\n");
            } else
                printf("program didn't terminate normally\n");
        }
        else {
            printf("waitpid() function failed\n");
        }
        exit(0);
    }
    return 0;
}

실행 결과

The parent process is (8627)
The child process is (8756)
program is executed successfully

코드 설명

위 예제의 동작 흐름은 다음과 같습니다.

  1. fork()를 호출하여 자식 프로세스를 생성합니다.
  2. fork()가 -1을 반환하면 프로세스 생성에 실패한 것이므로 오류 메시지를 출력하고 종료합니다.
  3. 반환값이 0이면 자식 프로세스이므로, 자신의 PID를 출력한 후 execv()를 통해 'ls' 명령어를 실행합니다.
  4. 반환값이 양수이면 부모 프로세스이므로, waitpid()를 호출하여 자식 프로세스가 종료될 때까지 기다립니다.
  5. WIFEXITED()와 WEXITSTATUS() 매크로를 사용해 자식 프로세스가 정상적으로 종료되었는지 확인하고 결과를 출력합니다.

이처럼 fork()와 exec()를 함께 사용하면 하나의 프로그램 안에서 다른 프로그램을 실행하면서도, 부모 프로세스는 자식의 종료 상태를 추적할 수 있습니다. 이는 셸(shell)이 외부 명령어를 실행하는 방식과 동일한 패턴입니다.