좀비 프로세스
좀비 프로세스
본 포스팅의 내용은 windows os에서 적용되지 않는 linux os의 내용임.
fork()
로 생성된 자식 프로세스의 소멸을 위해서는 부모 프로세스가 자식 프로세스의 전달 값을 요청해야 한다.
요청을 위한 구체적인 방법을 이 포스팅에서 설명한다.
좀비 프로세스의 소멸1: wait 함수의 사용
#include <sys/wait.h>
pid_t wait(int * statloc);
// 성공 시 종료된 자식 프로세스의 ID, 실패 시 -1 반환
위 함수 호출 시
- 이미 종료된 자식 프로세스가 있는 경우 : 자식 프로세스가 종료되면서 전달한 값이 매개변수로 전달된 주소의 변수에 저장됨
- 종료된 자식 프로세스가 없는 경우 : 자식 프로세스가 종료될때까지 블로킹(blocking)됨
블로킹(대기) 상태에서는 CPU 코어를 점유하지 않는다. 자식 프로세스가 종료되면 부모 프로세스는 운영체제로부터 해당 상태를 전달받아 블로킹이 해제된다.
statloc
에 저장되는 값은 다음과 같다.
WIFEXITED
: 자식 프로세스가 정상 종료한 경우 TRUE 반환WEXITSTATUS
: 자식 프로세스의 전달 값(return value)을 반환
⛔️
WEXITSTATUS
는 statloc 하위 8비트 값만 추출하기 때문에 0~255까지의 값만 가질 수 있다. (이것 때문에 삽질함)
wait 함수 호출 이후 코드 구성
int status;
wait(&status);
if(WIFEXITED(status))
{
puts("Normal termination!");
printf("Child pass num: %d", WEXITSTATUS(status));
}
좀비 프로세스의 소멸2: waitpid 함수의 사용
waitpid 함수는 wait 함수와 달리 블로킹되지 않는다.
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int * statloc, int options);
// 성공 시 종료된 자식 프로세스의 ID(또는 0), 실패 시 -1 반환
pid
: 종료를 확인하고자 하는 자식 프로세스의 ID 전달, 이를 대신해서 -1을 전달하면 wait 함수처럼 임의의 자식 프로세스의 종료를 기다린다.statloc
: wait 함수의 매개변수 statloc과 동일하게 사용된다.options
: 헤더파일 sys/wait.h에 선언된 상수 WNOHANG을 인자로 전달하면, 종료된 자식 프로세스가 존재하지 않아도 블로킹 상태에 있지 않고, 0을 반환하면서 함수를 빠져 나온다.
예제
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid;
int status;
pid = fork();
if(pid == 0)
{
sleep(10);
printf("Child process exit with 30\n");
return 30;
}
else
{
while(!waitpid(pid, &status, WNOHANG))
{
sleep(3);
printf("hello\n");
}
if(WIFEXITED(status))
{
printf("Child process return value: %d", WEXITSTATUS(status));
}
}
return 0;
}
시그널 핸들링
자식 프로세스 종료의 인식주체는 운영체제이다. 따라서 운영체제가 열심히 일하고 있는 부모 프로세스에게 다음과 같이 이야기해줄 수 있다면 효율적인 프로그램의 구현이 가능하다.
“어이, 부모 프로세스! 네가 생성한 자식 프로세스가 종료되었어!”
그러면 부모 프로세스는 하던 일을 잠시 멈추고, 자식 프로세스의 종료과 관련된 일을 처리하면 된다. 이상적이고도 멋진 시나리오 아닌가? 이러한 시나리오의 프로그램 구현을 위해서 ‘시그널 핸들링(Signal Handling)‘이라는 것이 존재한다. 여기서 ‘시그널’은 특정상황이 발생했음을 알리기 위해 운영체제가 프로세스에게 전달하는 메시지를 의미한다. 그리고 그 메시지에 반응해서 메시지와 연관된, 미리 정의된 작업이 진행되는 것을 가리켜 ‘핸들링’ 또는 ‘시그널 핸들링’이라 한다.
시그널과 signal 함수
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
// 시그널 발생시 호출되도록 이전에 등록된 함수의 포인터 반환
- 함수 이름 : signal
- 매개변수 선언 : int signo, void(*func)(int)
- 반환형 : 매개변수형이 int이고 반환형이 void인 함수 포인터
위 함수를 호출하면서 첫 번째 인자(int signo
)로 특정 상황에 대한 정보를, 두 번째 인자(void(*func)(int)
)로 특정 상황에서 호출될 함수의 주소 값(포인터)을 전달한다.
그러면 첫 번째 인자를 통해 명시된 상황 발생시, 두 번째 인자로 전달된 주소 값의 함수가 호출된다. 참고로 signal 함수를 통해서 등록 가능한 특정 상황과 그 상황에 할당된 상수 몇몇을 정리해보면 다음과 같다.
signo에 등록 가능한 상황(상수) 일부
SIGALRM
: alarm 함수호출을 통해서 등록된 시간이 된 상황SIGINT
: CTRL+C가 입력된 상황SIGCHLD
: 자식 프로세스가 종료된 상황
다음 요청에 해당하는 signal 함수의 호출문장을 만들어 보자.
“자식 프로세스가 종료되면 mychild 함수를 호출해 달라”
이때 mychild 함수는 매개변수형이 int이고 반환형이 void이어야 한다. 그래야 signal의 두 번째 전달인자가 될 수 있다.
그리고 자식 프로세스가 종료된 상황은 SIGCHLD
로 정의 되어야 한다.
signal(SIGCHLD, mychild);
“alarm 함수호출을 통해서 등록된 시간이 지나면 timeout 함수를 호출해 달라.”
signal(SIGALRM, timeout);
“CTRL+C가 입력되면 keycontrol 함수를 호출해 달라.”
signal(SIGINT, keycontrol);
참고로, alarm 함수는 다음과 같이 정의되어 있다.
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// 0 또는 SIGALRM 시그널이 발생하기까지 남아있는 시간을 초 단위로 반환
위 함수를 호출하면서 양의 정수를 인자로 전달하면 전달된 수에 해당하는 시간이 지나서 SIGALRM 시그널이 발생한다. 그리고 0을 인자로 전달하면 이전에 설정된 SIGALRM 시그널 발생의 예약이 취소된다.
signal과 블로킹
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
void timeout(int sig)
{
printf("TIME OUT!\n");
alarm(2);
}
int main(int argc, char *argv[])
{
signal(SIGALRM, timeout);
printf("start...\n");
alarm(2);
for(int i = 0 ; i < 10 ; i++)
{
sleep(10);
printf("loop\n");
}
return 0;
}
위 코드 실행시, for문 내부의 sleep(10)
이 제대로 동작하지 않고 2초마다 블로킹 상태가 해제되는데, 이는 시그널 핸들러의 호출을 위해서 블로킹 상태가 해제되어야하기 때문이다.
sigaction 함수를 이용한 시그널 핸들링
“signal 함수는 유닉스 계열의 운영체제 별로 동작방식에 있어서 약간의 차이를 보일 수 있지만, sigaction 함수는 차이를 보이지 않는다.”
#include <signal.h>
int sigaction(int signo, const struct sigaction * act, struct sigaction * oldact);
// 성공 시 0, 실패 시 -1 반환
signo
: signal 함수와 마찬가지로 시그널의 정보를 인자로 전달act
: 첫 번째 인자로 전달된 상수에 해당하는 시그널 발생시 호출될 함수(시그널 핸들러)의 정보 전달.oldact
: 이전에 등록되었던 시그널 핸들러의 함수 포인터를 얻는데 사용되는 인자, 필요 없다면 0 전달.
sigaction 구조체는 다음과 같이 구성되어 있다.
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
}
sa_handler
에 시그널 핸들러의 함수 포인터 값을 저장하면 된다.
sa_mask
는 모든 비트를 0으로, sa_flags
는 0으로 초기화한다. 이 두 멤버의 설명은 생략한다.
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig)
{
if(sig == SIGALRM)
puts("Time out!");
alarm(2);
}
int main(int argc, char *argv[])
{
int i;
struct sigaction act;
act.sa_handler = timeout;
sigemptyset(&act.sa_mask); // sa_mask의 모든 비트 0으로 초기화
act.sa_flags = 0; // sa_flags 0으로 초기화
sigaction(SIGALRM, &act, 0);// SIGALRM으로 act호출
alarm(2);
for(i = 0 ; i < 3 ; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}