Operations on Process

4 minute read

 

프로세스 연산

 

시스템은 다음과 같은 대표적인 프로세스의 연산을 포함해야 한다.

  • 프로세스 생성
  • 프로세스 제거
  • 이후에 자세히 설명할 것들

이들은 주로 시스템 콜의 형태로 사용자 프로세스에게 제공이 된다.

이들은 응용 프로그램이나 시스템 콜을 호출하여 쓸 수 있는 연산이다.

 

 

프로세스 생성

 

기존 프로세스는 부모 프로세스라고 하고,

그 프로세스가 새로 생성한 프로세스를 자식 프로세스라고 한다.

자식 프로세스는 또 자신의 자식 프로세스를 생성할 수 있는데,

두 개 이상도 가능하다.

즉 이는 프로세스의 트리 구조를 형성하게 된다.

 

<img src=”https://user-images.githubusercontent.com/22045424/78584026-91721880-7872-11ea-9852-4140ee48c146.png” width=60%>

 

위 그림은 Linux 운영 체제의 일반적인 프로세스 트리 구조이다.

부모 프로세스가 자식을 생성하는 과정이 반복된다.

root 는 Linux에서 init 프로세스이다.

이 프로세스는 부모 프로세스가 없는 유일한 프로세스로,

부팅하면서 커널이 손수 직접 만든 프로세스이다.

이 프로세스의 생성 이후, 새로운 프로세스의 생성이 반복된다.

 

보통 프로세스는 process identifier 라고 불리는 pid 로 구분된다.

부모-자식 간에 자원을 공유할 수도 있고 못할 수도 있다.

전자의 경우가 많은데, 이 경우네 만일 부모가 파일을 열고 있었다면

자식은 생성되자마자 그 파일을 가지고 출발하게 된다.

 

또한 부모와 자식이 동시에 실행될 수도 있고,

부모가 자식의 종료를 기다릴 수도 있다.

 

자식 프로세스는 부모의 주소 공간을 그대로 복사한다.

많은 운영 체제에서는 부모 프로세스의 주소 공간을 그대로 복사하여

자식 프로세스가 그대로 사용한다.

즉 생성 직후 내용이 동일한 메모리 상의 이미지를 얻게 된다.

 

 

<img src=”https://user-images.githubusercontent.com/22045424/78584780-b450fc80-7873-11ea-9f49-f058ecf818e1.png” width=60%>

 

UNIX의 예로,

fork()를 호출한 프로세스의 주소 공간을 복사하여 새로운 프로세스를 만든다.

이때 부모와 동일한 이미지를 갖게 된 자식 프로세스는 전형적으로 생성 직후 exec()를 호출한다.

exec()은 이를 호출한 프로세스의 주소 공간을 시스템 콜에서 지정한 실행 파일의 내용으로 교체한다.

자식 프로세스는 exec()을 호출해 부모 것이 아닌 새로운 이미지를 얻어

부모의 것과 다른 자신의 프로그램을 실행하게 된다.

이후 프로세스가 종료되면 wait 상태에 있던 부모 프로세스와 합류 (자식의 소멸) 하게 된다.

 

 

Forking 으로 프로세스를 분리하는 C 프로그램

 

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

int main() {
    pid_t pid;  // 새 프로세스의 id
    
    /* fork a child process */
    pid = fork();
    /*
    fork()로 자식 프로세스 생성. 
    프로세스가 2개가 되는데, 따라서 부모와 자식들한테 각각 return 값을 준다.
    fork() 가 return 하는 시점부터 이미 자식 프로세스가 생성된 상태이다.
    일단 생성되면 부모와 똑같은 코드로 실행이 된다.
    구체적으로 실행이 되는 것은 fork() 의 return 직후이다.
    
    이때 다른 것은 fork()의 리턴값이다. 
    부모에겐 새로 만들어진 자식 프로세스의 PID가 리턴된다. 주로 양수이다.
    자식에겐 0이 리턴된다. 이는 부모의 리턴값과 구분하기 위함이다.
    
    fork() 가 실행이 되면 현재 프로세스 외에 똑같이 생긴 자식 프로세스가 만들어지고
    각 프로세스의 PID에는 각각 자식 프로세스의 PID와 0이 들어가게 된다.
    */
    
    /* fork()의 리턴값에 따라서 각기 다른 동작을 해야하기 때문에
    if 가 오게 되어 있다. */
    if (pid < 0) { /* error occurred */ 
        fprintf(stderr, "Fork Failed");
        // 새 프로그램의 생성이 이뤄지지 않은 경우이다.
        return 1;
    }
    else if (pid == 0) { /* child process */
        execlp("/bin/ls", "ls", NULL);
        /*
        ls 라는 프로세스를 실행한다.
        이때 이 코드를 실행하던 자식 프로세스는 사라지고 ls가 대체한다.
        exec은 프로세스 자체는 유지하되, 프로세스가 실행하고 있는 프로그램만 대체한다.
        exec() 을 통해 프로세스가 생기거나 하진 않고 실행하는 코드만 바뀐다.
        */
    }
    else { /* parent process */
        /* parent will wait for the child to complete */
        wait(NULL);
        // wait()로 자식 프로세스의 종료를 기다리고 있다.
        printf("Child Complete");
    }
}

 

<img src=”https://user-images.githubusercontent.com/22045424/78585720-4c031a80-7875-11ea-9cc0-f34486566ece.png” width=80%>

 

위는 Windows API 에서의 프로세스 분리 방법이다.

fork()exec() 이 합쳐진 채로 CreateProcess(...)를 쓸 수 있다.

wait()WaitForSingleObject()를 사용했다.

 

 

프로세스 제거

 

프로세스는 마지막 문장을 실행하고 exit() 시스템 콜을 호출하여

운영 체제에게 종료를 요청한다.

exit() 함수는 정수 parameter 한 개를 가진다. 이는 exit status 가 저장될 포인터이다.

wait() 함수를 통해 자식에서 부모로 상태 데이터가 반환된다.

프로세스가 종료되면 운영 체제는 프로세스가 가지고 있던 모든 자원이 회수한다.

 

exit() 은 종료하고자 하는 것이 본인인 경우이나,

abort()는 부모가 자식을 강제로 종료하기 위해 사용한다.

이는 비정상적인 종료이다.

abort()는 다음과 같은 이유로 호출된다.

  • 자식 프로세스가 할당된 자원을 초과했을 때

  • 자식에게 맡겨진 일이 더 이상 필요하지 않게 됐을 때

  • 부모가 일을 끝냈는데 자식이 남아있는 것을 운영 체제가 허용하지 않을 때

    (부모가 exit() 하면서 자식이 남아있는 것을 미허용)

 

몇몇 운영 체제는 부모가 제거되는데 자식이 남아있는 것을 허용하지 않는다.

프로세서가 제거되면 자식들도 다 제거되어야 한다.

한 프로세스가 종료하는 상황에서 자식이 남아있다면

운영 체제는 자식 프로세스 및 그들의 자식들까지도 다 강제 종료 시킬 수도 있는데

이를 cascading termination 이라고 한다.

 

프로세스가 exit() 하고 나면 상태가 terminated 로 바뀌지만

완전히 소멸된 상태는 아니다.

소멸은 자기를 부른 부모 프로세스에서 wait() 함수가 반환될 때 소멸된다.

 

pid = wait(&status)

wait()은 종료한 자식 프로세스의 id를 반환한다.

매개변수로 자식 프로세스가 exit()을 수행하면서 전달할 exit status 가 담겨 있다.

이를 통해 자식 프로세스의 정확한 종료 상태도 전달 받을 수 있다.

이는 자식 프로세스가 종료되었을 때 return 되는 함수이다.

따라서 이 함수는 자식 프로세스의 종료를 기다리는 함수이다.

 

부모가 자식을 기다리는 과정에서 아다리가 안 맞으면 다음과 같은 일이 벌어진다.

  1. 부모 프로세스에서 wait() 가 실행되지 않았을 때, 자식 프로세스는 zombie 가 된다.
  2. 부모 프로세스가 wait() 없이 종료되었을 때 자식 프로세스는 orphan 이 된다.

1번 케이스는 자식은 exit() 했는데 부모가 wait()을 안 한 경우이고

2번 케이스는 부모가 먼저 종료했는데 자식은 아직 동작 중인 경우이다.