이 글에서는 리눅스 환경에서 C 언어의 fork()와 exec() 함수를 사용해 프로세스를 생성·실행하거나 다른 프로그램으로 교체하는 방법을 알아봅니다. 두 함수의 개념과 문법, 호출 방식을 설명하고, 각각의 간단한 실습 예제도 함께 다룹니다. 마지막으로 fork()와 execv()를 조합하여 새로운 프로세스를 만든 뒤 이를 다른 프로그램으로 교체하는 방법까지 단계별로 살펴보겠습니다.
C 언어의 fork() 함수
fork() 함수는 호출한 프로세스(부모 프로세스)의 복제본을 생성합니다. 자식 프로세스는 부모 프로세스의 복제본이지만, 할당된 메모리 영역이나 PID 등 일부 속성은 서로 공유하지 않는다는 점에 유의해야 합니다.
fork() 함수는 부모 프로세스에서는 자식 프로세스의 PID를 반환하고, 자식 프로세스에서는 동일한 호출에 대해 0을 반환합니다. 이 메커니즘 덕분에 if 조건문의 조건을 fork()의 반환값으로 지정하면, 두 프로세스가 각각 다른 코드를 실행하도록 만들 수 있습니다. 기본적인 구조는 다음과 같습니다.
if (fork() == 0)
{
자식 프로세스 코드
}
else
{
부모 프로세스 코드
}
이렇게 작성하면 부모 프로세스는 else 문의 중괄호 안 코드를 실행하고, 자식 프로세스는 if 문 안의 코드를 실행합니다.
간단한 예제로 직접 확인해 보겠습니다. 다음 코드는 무한 루프 두 개로 구성되어 있습니다. 부모 프로세스는 else 문에 해당하는 루프에 진입해 "Write by parent process" 메시지를 출력하고, fork()로 생성된 자식 프로세스는 if 문의 루프에 진입해 "Write by child process" 메시지를 출력합니다.
#include <stdio.h>
#include <unistd.h>
void main(){
if (fork() == 0)
{
while (1){
printf ("Write by child process \n");
sleep(5);}
}
else
{
while (1){
printf ("Write by parent process \n");
sleep(2);}
}
}
다음 이미지는 이 코드의 컴파일 및 실행 화면입니다. 명령 콘솔에서 확인할 수 있듯이 각 프로세스는 서로 다른 코드를 실행합니다.

C 언어의 execXXX() 함수
execXXX() 계열 함수는 실행 중인 프로세스를 새로운 프로세스로 교체합니다. 새 프로세스의 이미지는 기존 프로세스에 할당된 메모리 영역에 복사되며, PID와 할당된 리소스 등은 그대로 유지됩니다.
"unistd.h" 헤더에 정의된 이 계열의 함수들은 입력 인자에 따라 호출 방식이 다르며, 가변 인자(variadic) 타입이므로 기존 프로세스에서 새 프로세스로 개수에 제한 없이 인자나 포인터 목록을 전달할 수 있습니다. 각 함수의 문법은 다음과 같습니다.
int execl(const char *path, const char *arg, ...(char *) NULL);
int execlp(const char *file, const char *arg, ... (char *) NULL);
int execle(const char *path, const char *arg, ..., (char *) NULL, char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
execl(), execle(), execv() 함수는 첫 번째 입력 인자로 새 프로세스의 실행 파일 절대 경로가 담긴 문자열 포인터를 사용합니다. 반면 execlp(), execvp(), execvpe() 함수는 현재 디렉터리 기준의 파일 이름을 사용합니다. 두 번째 입력은 새 프로세스에 전달할 인자로, const char *arg 문자열이거나 char *const argv[] 문자열 배열에 대한 포인터여야 합니다.
이제 execv() 함수를 사용해 프로세스를 교체하고, 한 프로그램에서 다른 프로그램으로 입력 인자를 전달하는 예제를 살펴보겠습니다.
이를 위해 매우 간단한 코드 두 개를 작성합니다. 하나는 execv() 함수를 호출해 자식 프로세스를 실행하는 부모 프로세스입니다. execv() 함수가 자식 프로세스를 시작할 때 문자열 형태의 입력 인자 두 개를 전달하고, 자식 프로세스는 이를 받아 셸에 출력합니다.
자식 프로세스
자식 프로세스는 "I am the child process" 메시지를 출력한 뒤, 부모 프로세스가 보낸 입력 인자를 받아 셸에 표시하는 간단한 코드입니다. 자식 프로세스 코드는 다음과 같습니다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
printf ("I am the child process\n\n");
printf ("Argument 1: %s\n", argv[1]);
printf ("Argument 2: %s\n", argv[2]);
}
이 코드를 컴파일하여 결과물을 "Documents" 폴더에 ".bin" 확장자로 "child"라는 이름으로 저장합니다.
~$ gcc Documents/child.c -o Documents/child.bin
이렇게 하면 자식 실행 파일이 "Documents" 폴더에 저장됩니다. 이 실행 파일의 경로가 부모 프로세스에서 execv()를 호출할 때 path 입력 인자로 사용됩니다.
부모 프로세스
부모 프로세스는 execv() 함수를 호출하여 스스로를 자식 프로세스로 교체하는 역할을 합니다. 이 코드에서는 execv() 함수가 실행할 프로세스의 입력 인자를 담는 문자열 포인터 배열을 정의합니다.
아래 예시는 문자열 포인터 배열을 올바르게 생성하는 방법을 보여줍니다. 이 예제에서는 네 개의 포인터로 구성된 "arg_Ptr[]" 배열을 사용합니다.
포인터 배열을 정의한 후에는 각 포인터에 자식 프로세스로 보낼 입력 인자가 담긴 문자열을 할당해야 합니다. execxx() 함수 사용 시 참고할 규칙은 다음과 같습니다. 첫 번째 인자는 실행 파일의 이름과 확장자가 담긴 문자열이어야 하고, 마지막 포인터는 반드시 NULL이어야 합니다.
따라서 각 포인터에 해당 인자를 문자열 형식으로 할당합니다.
arg_Ptr[0] = "child.bin";
arg_Ptr[1] = " Hello from ";
arg_Ptr[2] = " process 2 ";
arg_Ptr[3] = NULL;
다음 단계는 execv() 함수를 호출하는 것입니다. 첫 번째 인자로 실행 파일의 절대 경로가 담긴 문자열을, 두 번째 인자로 arg_Ptr[] 문자열 배열을 전달합니다. 부모 프로세스의 전체 코드는 다음과 같습니다.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main (){
printf ("I am the parent process");
char *arg_Ptr[4];
arg_Ptr[0] = " child.c";
arg_Ptr[1] = " Hello from ";
arg_Ptr[2] = " process 2 ";
arg_Ptr[3] = NULL;
execv("/home/linuxhint/Documents/child.bin", arg_Ptr);
}
".c" 파일의 경로와 출력 파일 이름을 지정하여 이 코드를 컴파일합니다.
~$ gcc Documents/parent.c -o pattern
그런 다음 컴파일된 결과물을 실행합니다.
부모 프로세스는 "I am the parent process" 메시지를 출력하고, 다음 프로세스로 전달할 각 입력 인자에 문자열을 할당해 배열을 구성한 뒤 execv() 함수를 호출합니다.
execv() 함수가 성공적으로 실행되면 "child.bin" 실행 파일이 부모 프로세스를 완전히 대체하며, PID와 할당된 메모리까지 인수받습니다. 따라서 이 작업은 되돌릴 수 없습니다.
자식 프로세스는 "I am the child process" 메시지를 출력하고, 부모 프로세스가 전달한 각 입력 인자를 받아 명령 콘솔에 표시합니다.

fork()와 execve()를 조합한 리눅스 새 프로세스 생성
지금까지 살펴본 것처럼 fork() 함수는 프로세스를 복제하고, execve() 함수는 프로세스를 교체합니다. 이번 예제에서는 두 함수를 조합하여 복제본으로부터 새 프로세스를 연 뒤, 이를 다른 프로그램으로 교체하는 방법을 알아봅니다. 이를 위해 앞서 살펴본 두 코드를 결합하여, fork()가 부모 프로세스를 복제하게 하고 execve()가 이를 이전 예제에서 사용한 것과 동일한 "child.bin" 실행 파일로 교체하도록 구성합니다.
먼저 ".c" 확장자의 빈 파일을 만들고 fork() 함수 예제에서 사용한 프로그램 코드를 삽입합니다.
그런 다음 자식 프로세스 부분의 코드만 수정하여 execv() 함수가 이를 "child.bin" 실행 파일로 교체하도록 합니다. 나머지 주요 로직은 fork() 함수 예제와 동일합니다.
구체적으로는 execv() 함수 예제의 main() 함수 내용을 복사하여 if 문 안의 내용을 해당 코드로 교체하면 됩니다. 완성된 프로그램은 다음과 같습니다.
#include <stdio.h>
#include <unistd.h>
void main(){
if (fork() == 0)
{
printf ("I am the child process\n");
char *arg_Ptr[5];
arg_Ptr[0] = " child.c";
arg_Ptr[1] = " Hello from ";
arg_Ptr[2] = " process 2 ";
arg_Ptr[3] = NULL;
execv("/home/linuxhint/Documents/child.bin", arg_Ptr);
}
else
{
while (1){
printf ("Write by parent process \n");
sleep(3);}
}
}
이렇게 하면 fork()가 복제를 통해 시스템에 새 프로세스를 생성하고, 이어서 execv() 함수가 해당 프로세스를 "child.bin" 실행 파일의 프로세스로 대체합니다. 다음은 이 코드의 컴파일 과정을 보여주는 화면입니다.

이미지에서 확인할 수 있듯이, fork() 함수가 프로세스를 복제하여 새 프로세스를 만들면 execv() 함수가 이를 "child.bin" 실행 파일로 교체합니다.
결론
이 글에서는 리눅스에서 fork()와 execv() 함수를 사용해 새 프로세스를 여는 방법을 알아보았습니다. 각 함수에 대한 개념 설명과 문법, 입력·출력 인자를 소개했으며, 이해를 돕기 위해 함수별 실습 예제를 통해 호출 방식과 수행하는 작업을 학습했습니다. 또한 두 함수를 개별적으로 살펴본 후, 이들을 조합하여 리눅스에서 C 언어가 제공하는 새로운 프로세스 생성 방식을 구현하는 방법까지 설명했습니다. 이 내용을 바탕으로 멀티프로세스 프로그래밍의 기초를 탄탄히 다져보시기 바랍니다.