5 minute read

Computer Systems : A Programmer’s Perspective 8장

형태

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

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

사용 목적

부모 프로세스는 fork()를 통해 자식 프로세스를 생성할 수 있다.
자식 프로세스가 먼저 종료되면(terminated), 죽은 프로세스는 메모리에서 바로 사라지지 않고 좀비 프로세스로 남아있게 된다.
운영체제 커널이 이를 이렇게 방치하는 이유는 종료된 자식 프로세스의 정보(어떻게 종료됐는지, 왜 종료됐는지 등등)를 부모 프로세스에게 알려줘야 하기 때문이다.

자식 프로세스가 종료되면 커널은 부모 프로세스에게 SIGCHLD 시그널을 보낸다. SIGCHLD 시그널에 대한 프로세스의 디폴트 반응은 ‘무시’ 이다. 즉, 자식의 부고를 듣고 아무것도 하지 않기 때문에 종료됐던 자식 프로세스는 좀비로 쭉 남아 있게 된다.

위 코드는 부모 프로세스가 5개의 자식 프로세스를 fork하고 무한 대기하는 함수이다.

image

이렇게 백그라운드에서 실행시켜 놓고 ps로 프로세스 현황을 보면 <defunct>로 표시된 5개의 좀비 프로세스를 확인할 수 있다.
좀비 프로세스는 계속 메모리를 잡아먹고 있기 때문에 문제가 될 수 있다.

좀비 프로세스가 청소되는 것을 reap이라고 부르는데, 좀비 프로세스가 reap되는 방법은 두 가지가 있다.

  1. 부모 프로세스의 종료
    부모 프로세스가 자식 프로세스를 남기고 먼저 종료되면 부모를 잃은 고아 프로세스들은 init이라는 프로세스의 자식 프로세스로 입양된다. 나중에 자식 프로세스들이 종료되면 init 프로세스가 대신 이들을 reap해준다.
  2. 부모 프로세스의 대응
    앞 문단에서 SIGCHLD 시그널에 대한 프로세스의 디폴트 대응은 ‘무시’ 라고 했는데, 대응 방법을 설정해서 reap해주도록 하면 좀비 프로세스를 reap할 수 있다. 그리고 그 방법이 바로 waitpid 함수이다.

사용 방법

1. Signal handler 설치하기

waitpid 함수를 사용학기 전에 먼저 SIGCHLD 시그널에 대한 프로세스의 대응 방법을 디폴트로부터 바꿔 주어야 한다.
sigaction 함수 또는 signal 함수를 사용할 수 있는데, 보통 sigaction 함수를 사용한다. 그 이유는 이곳에서 알 수 있다. 보통 sigaction을 날것으로 쓰진 않고 다음과 같은 wrapper function을 사용한다.

이 함수를 사용하려면 ‘install` 해줘야 한다. main 함수에서 단순하게

Signal(SIGCHLD, handler1);

이렇게 해 주면, 이 프로세스에서 SIGCHLD 시그널에 대응하는 방법이 함수 handler1로 바뀌게 된다. 그러면 이제 handler1 함수를 만들어 보자. 본격적으로 waitpid를 사용할 때다.

2. waitpid 사용하기

waitpid 함수는 이름에서도 알 수 있듯, ‘PID를 기다린다’ 라는 의미이다. 3개의 파라미터를 하나씩 살펴 보자.

  1. pid_t pid: 기다릴 프로세스를 wait set에 넣고 wait set의 원소를 기다리게 되는데, wait set을 뭘로 설정할지 결정하는 파라미터이다.

    (1) pid = 0 : 현재 프로세스의 프로세스 그룹 아이디(pgid)가 같은 자식 프로세스들이 wait set의 원소이다.

    (2) pid = -1 : 아무 자식 프로세스가 wait set의 원소이다.

    (3) pid < 0 : |pid|의 pgid를 가지는 프로세스 그룹의 모든 프로세스가 wait set의 원소이다.

    (4) pid > 0: 해당 PID를 갖는 자식 프로세스만이 wait set의 원소이다.

  2. int *statusp: NULL이 아니면, waitpid 함수의 return을 일으킨 원인을 여기에 저장해 준다. 저장된 값은 아래의 매크로들로 사용 가능하다.

    (1) WIFEXITED: 지금 waitpid의 return을 만든 프로세스가 정상적으로 평범하게 종료된 것(return했다던가, exit(0) 했다던가 등등)인지를 판별한다. WIFEXITED(status)가 참이면 그렇고, 거짓이면 아니다.

    (2) WEXITSTATUS: 정상적으로 종료된, 즉 WIFEXITED(status)가 참인 경우에 해당 자식 프로세스의 exit status를 반환해 준다.

    (3) WIFSIGNALED: 지금 waitpid의 return을 야기한 프로세스가 시그널에 의해 종료되었으면 WIFSIGNALED(status)가 참이다.

    (4) WIFSTOPPED: 지금 waitpid의 return을 야기한 프로세스가 중지된 상태이면 WIFSTOPPED(status)는 참을 반환한다.

    이 외에도 몇 가지 더 있다. 나머지 부분은 책을 참고하기 바란다.

  3. int option: 자식 프로세스가 어떻게 되기를 기다리는지 결정한다. 여러 옵션이 있다.

    (1) 디폴트(0): wait set의 프로세스 중에서 종료되는 게 생길 때까지 기다린다. 하나라도 종료되면 그 프로세스의 PID를 반환한다.

    (2) WNOHANG: wait set의 프로세스 중 종료된 게 있으면 PID를 반환하고, 아직 종료된 게 없어도 기다리지 않고 바로 0을 반환한다.

    (3) WUNTRACED: wait set의 프로세스 중에서 종료 or 중지되는 게 생길 때까지 기다린다. 하나라도 종료 or 중지되면 그 PID를 반환한다.

    (4) WCONTINUED: wait set의 프로세스 중에서 종료되는 게 생기거나, 중지됐던 게 재시작되는 게 생길 때까지 기다린다. 그 프로세스의 PID를 반환한다.

    특이하게 이 옵션들은 OR 기호 |를 사용해서 중첩시킬 수 있다. 예를 들어, WNOHANG|WUNTRACED를 넣으면 종료되거나 중지된 게 있으면 PID를 반환하고 그런 것이 없으면 기다리지 않고 0을 반환한다.


그러면 waitpid를 사용해서 handler 함수를 만드는 방법을 실제 예시를 통해 살펴보자. 코드를 한번 자세히 읽어보길 바란다.

파라미터가 pids[i]이므로 생성된 자식 순서대로 기다린다. 옵션이 0이니 그것이 종료되는 것을 기다린다.
실행 결과는 다음과 같다.

image

0번부터 4번까지 각 자식 프로세스를 각각 ‘끝날 때까지’ 기다렸으므로, 5초동안 아무것도 출력되지 않다가 0번이 제일 나중(5초 뒤)에 종료되면 waitpid가 그 PID를 반환하고, 밀려 있던 나머지들이 즉시 출력된다.
이는 절대 1~4번 프로세스의 종료로 인한 시그널이 쌓여 있던 게 아니다! 시그널은 queue되지 않는다. 다른 곳에 시그널이 아닌 다른 정보로 queue되어 있다. 쓰레드와 관련된 부분 같다.(잘 모름)

waitpid의 옵션을 WNOHANG 으로 바꾸면 결과가 달라진다.

image

1초 간격으로 4, 3, 2, 1, 0으로 종료된 순서대로 출력된다.
WNOHANG 옵션에 의해 이제 0번째 프로세스가 아직 종료되지 않았다 하더라도, waitpid(pids[0])는 그것이 종료될때까지 기다리지 않고 바로 -1을 반환한다. 그렇기 때문에 1초 시점의 for문에서 i=0부터 i=3까지는 아무 소득 없이 지나가고 i=4 차례가 돌아와서 문구가 출력, 즉 부모 프로세스가 자식을 reap한다.
for문은 계속해서 돌고 시간이 2초, 3초, 이렇게 지나면서 종료된 프로세스가 생기면 waitpid는 그 프로세스의 PID를 반환하기 때문에 종료된 순서대로 출력되게 된다.

출력했다는 것은, 즉 waitpid가 PID를 반환했다는 것은 종료된 자식 프로세스가 reap되었다는 것을 의미한다.
handler1 함수의 for문에서 i+=1를 i+=2로 바꾸고 이 프로그램을 백그라운드에서 돌려 보자.

image

0번, 2번, 4번째 자식은 reap됐지만 1, 3번은 시간이 아무리 지나도 defunct, 즉 좀비로 남아있는 것을 확인할 수 있다. fg를 입력하고 ctrl+c를 쳐서 이 프로그램(=부모 프로세스)를 종료시키면 그제서야 좀비가 청소된다. 물론 앞에서 말했던 대로 init이 reaping수행했을 것이다.

지금은 waitpid의 반환값이 i번째 자식의 PID와 일치할 때만 출력하는데, 저 조건을 없애면 어떻게 될까?

void handler1(int sig){
    for(int i=0 ; i<NUM ; i+=1) {                               /* 0번부터 NUM-1번까지 */
        pid_t reaped = waitpid(pids[i], NULL, WNOHANG);         /* pids[0]부터 pids[NUM-1]까지 waitpid한다 */                        
        printf("return %d (num %d)\n", reaped, i);              /* 모든 waitpid의 반환에 대해 메시지 출력 */
        //end++;
        fflush(stdout);
    }
}

이렇게 바꾼 것의 행 결과는 다음과 같다.

return 0 (num 0)
return 0 (num 1)
return 0 (num 2)
return 0 (num 3)
return 8877 (num 4)

return 0 (num 0)
return 0 (num 1)
return 0 (num 2)
return 8876 (num 3)
return -1 (num 4)

return 0 (num 0)
return 0 (num 1)
return 8875 (num 2)
return -1 (num 3)
return -1 (num 4)

return 0 (num 0)
return 8874 (num 1)
return -1 (num 2)
return -1 (num 3)
return -1 (num 4)

return 8873 (num 0)
return -1 (num 1)
return -1 (num 2)
return -1 (num 3)
return -1 (num 4)

원래는 중간에 줄바꿈이 없지만 가독성을 위해 넣었다.
첫 번째 handler1의 호출은 1초 시점에 4번째 자식이 종료되어 커널이 부모에게 보낸 SIGCHLD 시그널이 원인이다. 이때 5개의 PID에 대해 각각 waitpid를 하면 0~3번째 프로세스는 종료되지 않았으므로 WNOHANG 옵션에 의해 즉시 0이 반환된다. 4번째 프로세스는 종료된 게 맞으므로 PID를 반환했다.
그 다음 루프는 2초 시점에 3번째 자식이 종료되어 커널이 부모에게 보낸 SIGCHLD 시그널이 원인이다. 0~2번째 프로세스는 종료되지 않았으므로 0이, 3번째 프로세스는 종료됐으므로 PID가, 그리고 4번째 프로세스의 PID로 waitpid를 부르면 없는 프로세스에 대해 호출했으므로 에러 리턴값 -1이 반환된다.
3초, 4초, 5초 시점도 동일한 로직으로 진행되어서 위와 같은 출력 결과가 나오게 된다.

정리

  • waitpid를 부르면 부모 프로세스는 자식 프로세스가 종료될 때까지 기다렸다가 reap한다.
  • SIGCHLD 시그널에 대한 디폴트 대응(action)은 무시이므로, Signal 함수를 이용한 handler install로 대응 방법을 설정한다.
  • 첫 번째 파라미터를 바꿔서 기다릴 대상을 지정할 수 있다. (특정 PID를 지정하거나, 아무 자식 프로세스를 기다리거나, 특정 프로세스 그룹을 기다리거나 등등)
  • 두 번째 파라미터에는 return을 야기한 프로세스의 종료 원인 등의 정보가 저장된다.
  • 세 번째 파라미터를 바꿔서 프로세스를 기다릴 방법을 지정할 수 있다. (종료를 기다리거나, 중지를 기다리거나, 한 번 확인 후 리턴하거나 등등)
  • 반환값은 return을 야기한 프로세스의 PID, 또는 0 (WNOHANG 옵션), 또는 -1 (에러)

Tags:

Categories:

Updated: