8.4 Process Control

Park Choong Ho·2021년 2월 21일
0

8.4 Process Control

Unix는 C 프로그램 프로세스들을 다루기 위한 여러가지 system call들을 제공합니다.

8.4.1 Obtaining Process IDs

각 프로세스들은 unique한 양수 값인 Process ID(PID)를 가지게 됩니다. getpid 함수는 호출한 프로세스의 PID값을 반환하는 함수입니다. getppid 함수는 호출한 프로세스의 부모프로세스 PID를 반환합니다.

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

pid_t getpid(void);
pid_t getppid(void);

int main(){
    int pid = getpid();
    int ppid = getppid();

    printf("pid는 %d입니다.\n", pid);
    printf("ppid는 %d입니다.", ppid);
}

8.4.2 Creating and Terminating Processes

프로세스는 크게 3가지 상태중 한가지를 가지게 됩니다.

  1. Running: CPU에 의해 실행되고 있거나 언젠가 kernel에 의해서 결국 scheduled되기를 기다리는 상태.
  2. Stopped: 진행이 중지되고(suspended) scheduled 되지 않는 상태(미래에 scheduled 되지 않는다). 프로세스는 SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU signal를 받으면 멈추고 SIGCONT signal을 받기 전까지 그 상태가 유지됨.(SIGCONT를 받으면 다시 프로세스가 돌아갑니다.)
  3. Terminated: 영구적으로 멈춘 상태. 프로세스는 3가지 경우에만 terminate됩니다.
    1. 프로세스를 terminate하는 시그널을 받은 경우
    2. 메인 루틴으로 부터 return 받은 경우
    3. exit 함수를 호출한 경우

부모 프로세스는 fork 함수을 호출함으로써 새로운 running 자식 프로세스를 생성할 수 있습니다.

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

pid_t fork(void);

int main(){
    int pid =fork();
    if(pid ==0){
        printf("자식! \n");
    }
    if(pid > 0){
        printf("부모! \n");
    }
}

새로 생성된 자식 프로세스는 완전히는 아니지만 부모프로세스와 거의 동일합니다. 자식은 부모와 동일한 가상메모리 (data, code, heap, shared libraries, stack)와 file descriptor의 복사본을 받습니다. 이는 child가 부모가 오픈한 그 어떤 파일이든 읽고 쓸수 있다는 것을 의미합니다. 이 둘의 가장 두드러진 차이점은 PID가 다르다는 것입니다.

fork 함수는 호출은 한번 되고 return은 2번 하는 흥미로운 함수입니다.(return 한번은 부모 프로세스, 한번은 자식프로세스에서 합니다.) 부모에서 fork 함수는 자식의 PID를 자식에서는 integer 0을 반환합니다. 자식 PID는 항상 0이 아니기에, return 값을 통해서 우리는 프로그램이 자식을 실행중인지 부모를 실행중인지 명확히 알 수 있습니다.

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

pid_t fork(void);

int main(){
    pid_t pid;
    int x = 1;

    pid = fork();

    if(pid == 0){
        printf("child: x=%d\n", ++x);
        exit(0);
    }

    printf("parent: x=%d\n", --x);
    exit(0);
}
❯❯❯ ./fork
parent: x=0
child: x=2

위 예시를 한번 살펴보도록 하겠습니다.

  • Call once, Return twice
    • fork 함수는 부모 프로세스에서 호출되고, 자식 프로세스와 부모 프로세스에서 모두 반환합니다. 자식 프로세스는 0을 부모 프로세스는 자식의 PID를 반환하기에, 이를 바탕으로 해당 프로세스가 자식을 fork했는지 명확히 판단할 수 있습니다. 그러나 fork를 활용해 여러개의 프로세스를 생성한 경우 로직이 혼란스러울 수 있기에 심사숙고해서 fork를 사용해야합니다.
  • Concurrent Execution
    • 부모와 자식은 concurrent하게 동작하는 별도의 프로세스입니다. 이들 control flow의 instruction 사이사이에 kernel이 끼어들 수 있습니다. 어떤 시스템에서는 부모가 먼저 동작할 수 있고 다른 시스템에서는 자식이 선행할 수 있습니다. 우리는 일반적으로 어떤 프로세스가 먼저일 것이라고 가정할 수 없습니다.
  • Duplicate But Seperate Address Spaces
    • 만약 fork가 반환한 직후 프로세스를 잠시 멈춘다면, 각각의 메모리 주소가 동일한 것을 볼 수 있습니다. 부모, 자식은 동일한 user stack, 지역변수, heap, 전역변수, code를 가지고 있을 것입니다. 따라서 위 코드 예시 fork가 반환한 시점에서는 지역변수 x가 동일한 값인 1을 가지게 됩니다. 하지만 부모, 자식은 별도 프로세스이기에 자신의 주소공간을 가집니다. 따라서 그 이후 변화에 있어서는 이제 서로에게 영향을 주지 않습니다. 그 결과 x 값이 부모에서는 0, 자식에서는 2입니다.
  • Shared Files
    • 우리가 위 코드를 돌리면, 부모 자식 결과값이 같이 프린트됩니다. 그 이유는 자식이 부모가 열어놓은 파일들을 모두 상속받기 때문입니다. fork를 호출하면 자식이 부모가 열어놓은 stdout 파일을 상속받고 해당 파일이 screen에 연결됩니다.

8.4.3 Reaping Child Processes

프로세스가 terminate되면, kernel은 해당 프로세스를 즉시 시스템에서 제거하지 않습니다. 대신에 프로세스는 자기 부모 프로세스에게 reap될 때까지 terminate 상태를 유지합니다. 부모 가 자식을 reap하면, 커널은 자식 exit status를 부모에게 전달하고 terminate된 process를 제거합니다.(이 시점부터 자식 프로세스는 존재하지 않게됩니다.) 여기서 terminate 상태이며 아직 reap되지 못한 프로세스를 좀비라고 일컫습니다.

부모 프로세스가 terminate되면, init process가 자식 프로세스들의 부모 프로세스가 됩니다.

Init Process

Init Process는 3가지 특징을 가지는 프로세스입니다.

  1. PID가 항상 1입니다.
  2. 시스템 시작시점(컴퓨터 전원이 켜지는 시점)에서 kernel이 생성합니다.
  3. 시스템 종료전까지 terminate되지 않습니다.
  4. 모든 프로세스의 조상입니다.

부모 프로세스가 자식 좀비 프로세스들을 reap하지 않고 terminate 되면, kernel은 init 프로세스가 자식 프로세스들을 reap하게끔 합니다. 이렇게 kernel에서 좀비 프로세스를 관리해 주지만, 좀비 자식들을 부모에서 reap하는 것은 필요합니다. 좀비가 돌고있지는 않더라도 여전히 시스템 메모리 리소스를 잡아먹기 때문입니다.

#include<sys/types.h>
#include<sys/wait.h>

pid_t waitpid(pid_t pid, int *statusp, int options)

프로세스는 waitpid 함수 호출을 통해, 자식 프로세스가 terminate 또는 stop할 때까지 기다릴 수 있습니다. waitpid함수는 다소 복잡한데, 기본적으로 자식 프로세스가 terminate 될 때까지 호출한 부모 프로세스 진행을 suspend합니다.(options 인자로 0을 준 경우가 이에 해당) waitpid 함수는 terminate된 자식의 PID를 반환합니다.(자식이 waitpid 함수 호출전 terminate했어도 자식 PID를 반환합니다.) 이 시점에서 terminate된 자식프로세스는 reap되고 커널은 시스템으로부터 해당 프로세스의 모든 흔적을 제거합니다.

Determining the Members of the Wait Set

waitset 멤버들은 pid 인자에 의해 결정됩니다.

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *statusp, int options);
  • pid > 0: PID의 값이 pid인 프로세스
  • pid = -1: waitpid 호출한 프로세스의 모든 자식프로세스

waitpid 함수는 Unix process group을 포함한 여러 다른 형태의 waitset 또한 지원합니다.(하지만 여기서는 다루지 않습니다.)

Modifying the Default Behavior

waitpid 함수는 options 인자를 통해 default behavior를 바꿀 수 있습니다.

  • WNOHANG: waitset에 존재하는 그 어떤 자식 프로세스들도 아직 terminate되지 않은 경우 0을 반환합니다. default behavior는 child가 terminate될 때까지 호출한 프로세스를 suspend합니다. 기다리는 동안 다른 어떤 작업을 하기를 원할 경우, 유용하게 사용할 수 있는 options입니다.
  • WUNTRACED: waitset에 있는 프로세스가 terminate 또는 stop될 때까지 호출한 프로세스를 suspend합니다. 자식 프로세스의 PID를 반환합니다. default behavior는 terminate된 자식인 경우에만 반환합니다.
  • WCONTINUED waitset에 있는 프로세스가 terminate되거나 stop한 작식 프로세스가 SIGCONT 시그널을 받아 재개할 때까지 호출한 프로세스를 suspend합니다.

|(oring)을 통해 이 옵션들을 조합할 수 있습니다. 예를들어,

  • WNOHANG | WUNTRACED: waitset에 있는 그 어떤 프로세스들도 terminate되거나 stop되지 않은 경우 즉시 0을 리턴합니다. 또는 stopped되거나 terminated된 프로세스의 PID를 반환합니다.

Checking the Exit Status of a Reaped Child

만약 statusp 인자가 non-NULL인 경우, waitpid 함수가 자식 status 정보를 인코딩합니다. wait.h는 status 인자를 해석할 몇몇 매크로들을 정의한 파일을 포함하고 있습니다.

  • WIFEXITED(status): exit을 call하거나 반환함으로써 자식 프로세스가 terminate된 경우 true를 반환합니다.(normally terminate)
  • WEXITSTATUS(status): 일반적으로 terminate된 자식 프로세스 exit status를 반환합니다. 이 상태는 WIFEXITED()가 true를 반환한 경우에만 정의됩니다.
  • WIFSIGNALED(status): signal에 의해 자식 프로세스가 terminate된 경우 true를 반환합니다.
  • WTERMSIG(status): 자식 프로세스가 terminate되게끔 야기한 signal 숫자를 반환합니다. 이 상태는 WIFSINALED()가 true를 반환한 경우에만 정의됩니다.
  • WIFSTOPPED(status): child가 stop한 경우 true를 반환합니다.
  • WSTOPSIG(status): 자식 프로세스가 stop되게끔 야기한 signal 숫자를 반환합니다. 이 상태는 WIFSTOPPED()가 true를 반환한 경우에만 정의됩니다.
  • WIFCONTINUED(status): 자식 프로세스가 SIGCONT signal을 받아 재시작한 경우 true를 return합니다.

Error Conditions

만약 호출하는 프로세스가 자식이 없는 경우 waitpid는 -1을 반환합니다. 그리고 errno를 ECHILD로 설정합니다. 만약 waitpid 함수가 signal에 의해 interrupt된 경우 -1을 반환하고 errno을 EINTR로 설정합니다.

Examples of Using waitpid

waitpid는 상당히 복잡해서 몇개의 예시를 보는 것이 도움이 됩니다.

#include "csapp.h"
#define N 2

void unix_error(char *msg) /* Unix-style error */
{
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(0);
}

pid_t Fork(void)
{
    pid_t pid;

    if ((pid = fork()) < 0)
	unix_error("Fork error");
    return pid;
}

int main(){
    int status, i;
    pid_t pid;

    for(i = 0; i < N; i++){
        if((pid = Fork()) == 0){
            exit(100 + i);
        }
    }

    while((pid = waitpid(-1, &status, 0)) > 0){
        if(WIFEXITED(status)){
            printf("child %d terminated normally with exit status=%d\n", pid, WEXITSTATUS(status));
        }
        else{
            printf("child %d terminated abnormally\n", pid);
        }
    }

    if(errno != ECHILD){
        unix_error("waitpid error");
    }
    exit(0);
}

for 문에서 child process를 n개 생성하고 각각 process가 고유한 exit status와 함꼐 terminate합니다.

profile
백엔드 개발자 디디라고합니다.

0개의 댓글