운영체제

[OS] 새로운 프로세스는 어떻게 만들어질까? fork(), exec(), wait()

샥쿠 2024. 5. 2. 15:29

일반적으로 새로운 프로세스를 만들기 위해서는 부모 프로세스로부터 자식 프로세스를 만들어야한다.

zsh, bash 같은 쉘 프로세스에서 쉘 명령어를 실행시키는 동작을 예시로 들어서 살펴보자.

쉘은 다양한 쉘 명령어를 실행시킬 수 있다. 예를 들어 ls 명령어를 사용하면 현재 디렉토리의 파일 목록을 출력할 수 있다.

명령을 입력 받은 쉘 프로세스는 ls 명령어를 실행시키기 위한 자식 프로세스를 만든다. 이는 다음과 같은 과정을 거쳐서 만들어진다.

  • ls 명령어가 실행되면 쉘은 fork() 시스템 콜을 통해 자식 프로세스를 만든다.
  • 자식 프로세스의 프로그램 코드는 exec() 시스템 콜을 통해 /bin/ls 위치에 있는 프로그램 코드로 교체된다.
  • 자식 프로세스는 ls 프로그램에 있는 명령어들을 실행하여 할일을 마치고 종료된다.

쉘 프로세스는 부모 프로세스로부터 생성된 자식 프로세스와 병행하여 계속 실행된다. 즉 자식 프로세스가 완료되기 이전에 새로운 명령어를 입력 받을 수 있다. 부모 프로세스는 wait() 시스템 콜을 통해 자식 프로세스가 종료되기를 기다리고, 새롭게 입력된 명령어는 먼저 실행했던 자식 프로세스가 끝난 다음 순차적으로 실행된다.

/Users/kimsj  > sleep 5 && echo "first"  # 5초 후에 "first" 출력하는 명령어
echo "second"                            # 5초가 지나기 전에 다음 명령어 입력
first                          # 5초 후에 "first"가 출력됨
/Users/kimsj  > echo "second"  # "first" 출력 후 다음 명령어가 실행됨
second

프로세스를 만들기 위한 시스템 콜

운영체제의 커널은 하드웨어를 제어하기 위한 API인 시스템 콜을 제공한다. 그 중 프로세스를 만들기 위해 다음과 같은 시스템 콜들을 사용할 수 있다.

  • fork()
    • 부모 프로세스의 메모리 공간을 복제하여 자식 프로세스를 만든다.
    • fork()의 반환값(pid)은 자식 프로세스인 경우 0, 부모 프로세스인 경우 자식 프로세스의 pid이다.
  • wait()
    • 부모 프로세스는 wait() 시스템 콜을 사용하여 자식 프로세스의 실행이 종료될 때까지 기다릴 수 있다.
    • wait()이 실행되면 부모 프로세스를 대기 큐(wait queue)로 보낸다.
  • exec()
    • 현재 프로세스의 메모리 공간을 새로운 프로그램으로 교체한다.
    • 자식 프로세스의 메모리 공간을 다른 프로그램으로 바꿀 때 사용한다.

자식 프로세스를 만드는 프로그램 예시(C 언어)

부모 프로세스로부터 자식 프로세스가 만들어지면 둘은 어떻게 구분할 수 있을까?

fork()를 호출하면 프로세스 아이디 값(pid)이 반환된다. 이 값이 0이면 자식 프로세스이고, 0보다 큰 값이면 부모 프로세스로 식별할 수 있다.

부모 프로세스가 반환 받는 아이디 값은 자식 프로세스의 pid이다.

// pid.c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid, parent_pid, child_pid;
    pid = fork();
    
    if (pid > 0) {
        wait(NULL); // 자식 프로세스 종료될 때까지 블록됨
        parent_pid = getpid(); // 현재 프로세스의 pid를 가져옴
        printf("pid: %d\\n", pid); // fork()에서 반환 받은 값은 자식 프로세스의 pid
        printf("parent pid: %d\\n", parent_pid);
        printf("I am parent process\\n");
    }
    else if (pid == 0) {
		    child_pid = getpid(); // 현재 프로세스의 pid를 가져옴
        printf("child pid: %d\\n", child_pid);
        printf("I am child process\\n");
    }
    
    return 0;
}

실행 결과는 다음과 같다.

> gcc pid.c  # pid.c 파일을 실행파일로 컴파일
> ./a.out    # a.out 실행파일 실행
child pid: 50550
I am child process
pid: 50550
parent pid: 50548

이번에는 fork()를 이용해 자식 프로세스를 만들고, exec()을 이용해 자식 프로세스를 다른 프로그램으로 대체하는 코드를 작성해보자. 여기서 execlp()는 exec()의 한 버전이다. execlp()는 첫번째 인자로 대체하고자 하는 프로그램이 저장된 경로를 받는다.

// exec_ls.c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid;
    pid = fork();
    
    if (pid == 0) {
        execlp("/bin/ls", "ls", NULL); // 자식 프로세스를 ls 프로그램으로 변경
        printf("여기는 실행되지 않음");
    }
    else if (pid > 0) {
        wait(NULL); // 자식 프로세스가 종료되기 전까지 기다린다.
        printf("부모 프로세스 완료\\n");
    }
    
    return 0;
}

실행 결과는 다음과 같다.

> gcc exec_ls.c
> ./a.out
a.out           exec_ls.c  # 자식 프로세스가 ls 프로그램을 실행한 결과
부모 프로세스 완료  # 부모 프로세스에서 출력된 것

기존에 있는 프로그램 말고도 내가 만든 프로그램으로 자식 프로세스를 교체해보았다. 먼저 다음과 같은 쉘 명령어 파일을 만든다. 주의할 점은 해당 파일을 실행할 수 있도록 실행 권한을 설정해야한다. chmod +x hello 명령어로 파일에 대한 실행 권한을 추가할 수 있다.

# hello

echo "Hello World"

execlp()에 인자로 넣는 경로를 hello 파일의 경로로 설정해서 자식 프로세스를 해당 쉘 명령어 프로그램으로 변경한다.

// exec_hello.c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid;
    pid = fork();
    
    if (pid == 0) {
        execlp("/Users/kimsj/Desktop/.../hello", "hello", NULL); // 자식 프로세스를 hello 프로그램으로 변경
        printf("여기는 실행되지 않음");
    }
    else if (pid > 0) {
        wait(NULL);
        printf("부모 프로세스 완료\\n");
    }
    
    return 0;
}

실행 결과는 다음과 같다.

> gcc exec_hello.c
> ./a.out      
Hello World  # hello 프로그램이 실행된 결과
부모 프로세스 완료

'운영체제' 카테고리의 다른 글

[OS] 가상 메모리 & 요구 페이징  (0) 2024.06.12
[OS] 경쟁 상태(Race Condition)  (0) 2024.05.14
쉘(Shell)과 커널(Kernel)  (0) 2023.09.18
[OS] 프로세스 스케줄링  (3) 2023.07.20
크론탭(crontab)을 이용한 스케줄링  (0) 2023.07.11