CS/운영체제

[OSTEP 가상화(virtualization)]Chapter 5 프로세스 API

빙응이 2025. 1. 5. 19:11

막간
이번 절에서는 운영체제 API와 그의 사용법을 포함하여 시스템의 실제적인 측면을 다룹니다.

 

이번 절에서는 유닉스 시스템의 프로세스 생성의 관해 논의합니다. 유닉스 기반 운영체제는 프로세스 생성을 위해 fork()와 exec() 시스템 콜을 사용합니다. wait()은 프로세스가 자신이 생성한 프로세스가 종료되기를 기다리기 원할 때 사용합니다.

 


핵심 질문
프로세스를 생성하고 제어하려면 운영체제가 어떤 인터페이스를 제공해야 하는가?
유용하고 편하게 사용하기 위해서 이 인터페이스를 어떻게 설계되어야 하는가?

 

📝8.1 fork() 시스템 콜

프로세스 생성에 fork() 시스템 콜이 사용됩니다. 

##include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int
main(int argc, char *argv[])
{
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc < 0) { //fork() 실패 종료
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) { //자식 프로세스 번호
        printf("hello, I am child (pid:%d)\n", (int) getpid());
	sleep(1);
    } else { //부모 프로세스
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
	       rc, wc, (int) getpid());
    }
    return 0;
}

 

prompt> ./p1
hello world (pid:29146)
hello, I am parent of 29147 (pid : 29146)
hello, I am child(pid : 29147)

 

해당 프로그램에서 어떤 일들이 벌어지는지 알아봅시다.

  1. 실행이 시작될 때 hello world가 호출됩니다. 이 메시지에는 PID로 알려진 프로세스 식별자가 포함됩니다. 이 프로세스는 29146이라는 PID를 가집니다.
  2. 프로세스는 fork() 시스템 콜을 호출합니다. 이때 생성된 프로세스는 호출한 프로세스의 복사본입니다. 이대 생성된 프로세스는 main() 함수 첫 부분부터 시작하지 않고 fork()부터 시작됩니다. 
  3. 자식 프로세스는 부모와 완전히 동일하지 않으며 자신만의 주소 공간, 레지스터, PC, fork 리턴  값을 갖습니다.

위의 코드는 출력값이 항상 같지 않습니다. 

prompt> ./p1
hello world (pid:29146)
hello, I am child(pid : 29147)
hello, I am parent of 29147 (pid : 29146)

 

CPU 스케줄러는 실행할 프로세스를 선택합니다. 스케줄러의 동작은 일반적으로 상당히 복잡하며 상황에 따라 다릅니다.

이런 비결정성으로 인해 멀티 쓰레드 프로그램 실행 시에 다양한 문제가 발생합니다.

 

📝8.2 Wait() 시스템 콜

이제까지 메시지 출력하는 자식 프로세스를 만들어 보았습니다. 운영체제에서 부모 프로세스가 자식 프로세스의 종료를 대기해야 하는 경우가 많이 발생합니다. wait() 시스템 콜은 이러한 기능을 구현합니다.(더 많은 기능을 가진 waitpid()도 있습니다)

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

int
main(int argc, char *argv[])
{
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc < 0) {// fork 실패 종료
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) { // 자식 프로세스
        printf("hello, I am child (pid:%d)\n", (int) getpid());
	sleep(1);
    } else { // 부모 프로세스는 이 경로로 실행
        int wc = wait(NULL); //자식이 종료될 때까지 대기 
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
	       rc, wc, (int) getpid());
    }
    return 0;
}

 

이 예제에서 부모 프로세스는 wait() 시스템 콜을 호출하여 자식 프로세스 종료 시점까지 자신의 실행을 중단합니다. 자식 프로세스가 종료되면 wait()은 리턴합니다.  wait()을 추가하면 자식과 부모의 순서를 조절할 수 있어 항상 동일한 결과를 호출합니다.

 

prompt> ./p1
hello world (pid:29146)
hello, I am child(pid : 29147)
hello, I am parent of 29147 (wc:29147) (pid : 29146)

 

wait()은 자식 프로세스 중 하나가 끝날 때까지 기다리며 끝난 자식 프로세스의 종료 코드를 프로세스 상태에 저장합니다. 또한 끝난 자식 프로세스의 번호를 리턴하며 기다릴 자식이 없으면 -1를 리턴합니다.

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

    if (pid == 0) {
        // 자식 프로세스
        printf("Child process exiting...\n");
        exit(42);  // 자식 프로세스 종료, 상태 코드 42로 종료
    } else {
        // 부모 프로세스
        int status;
        wait(&status);  // 자식 프로세스 종료 대기

        // 종료 상태 코드 처리
        if (WIFEXITED(status)) {  // 정상 종료 여부 확인
            printf("Child exited with status %d\n", WEXITSTATUS(status));
        } else {
            printf("Child did not exit normally\n");
        }
    }
    return 0;
}

 

 

📝8.3 exec() 시스템 콜

프로세스 생성 관련 API 중에서 마지막으로 중요한 시스템 콜은 exec() 시스템 콜입니다. 

이 시스템 콜은 자기 자신이 아닌 다른 프로그램을 실행해야 할 때 사용됩니다. 전의 예제는 fork()로 자신의 복사본을 실행했다면 exec()는 다른 프로그램을 실행합니다.  exec는 다양한 변형이 존재하며 궁금하면 찾아보길 바랍니다.

 

exec()는 다음과 같은 과정을 수행됩니다.

  1. 실행 파일의 이름과 약간의 인자가 주어지면 해당 실행 파일의 코드와 정적 데이터를 읽어 들여 현재 실행 중인 프로세스의 코드 세그먼트와 정적 데이터 부분을 덮어 씁니다.
  2. 힙과 스택 및 프로그램 다른 주소 공간들로 새로운 프로그램을 실행을 위해 다시 초기화 합니다. 그런 다음 실행 시킵니다.
  3. 해당 프로그램 종료 후 원래의 프로그램으로 돌아가지 않습니다. 완전히 덮어씁니다.

 

🧷정리

이번 장에서는  유닉스에서 사용하는 프로세스 제어에 대해 알아봤습니다. 이 과정을 알아야 더 다양한 가상화를 알 수 있다 생각합니다.

운영체제의 이러한 기능을 활용 것은 멀티프로세싱, 멀티스레딩, 시스템 프로그램에서 매우 중요한 개념입니다.