상세 컨텐츠

본문 제목

UNIX - system call [exit, wait , waitpid], zombie process, orphan process

Computer Science/UNIX

by 2021. 10. 16. 02:42

본문

반응형

Terminating processes with the exit system call

프로세스 종료하는 8가지 방법은 크게 다음의 2가지로 분류된다.

Normal termination

main 프로그램에서 return을 통해 종료하는 경우

exit을 통해 종료하는 경우, _exit 또는 _Exit로 종료하는 경우 

시작 루틴에서 마지막 쓰레드가 return하는 경우

마지막 쓰레드에서 _exit가 호출되는 경우

등의 상황이 정상적인 종료이다.

Abnomal termination

abort system call을 통해 종료하는 경우 

signal을 받고 종료하는 경우

취소 request에 마지막 쓰레드가 응답하는 경우

등의 상황이 비정상적인 종료이다.

exit(3)

#include <stdlib.h>

void exit(int status);
void _Exit(int status);
#include <unistd.h>

void _exit(int status);

매개변수인 status하는 argument는 process exit status이다. 종료되는 상태를 status 변수에 저장한다. exit는 integer이므로 4byte인데, 그 중 low order 8bit는 parent에 전달된다. 

parent는 child 프로세스가 끝나기를 기다리고 있다. parent의 wait는 wait(&status)형태로 되어있는데 여기의 status에 exit의 status가 전달된다. 

종료가 정상적으로 수행되면, 보통 0를 parent에게 return한다. 문제가 있다면 0 이외의 값을 parent에게 반환한다. return으로 해도 wait의 status로 전달되고 exit로 해도 전달된다. 

 exit는 라이브러리로 종료하기 전에 메모리 등 cleanup 작업을 한 후 system call인 _exit를 호출한다. 

atexit(3)

#include <stdlib.h>

int atexit(void (*func)(void));
// Returns: 0 if OK, nonzero on error

atexit 라이브러리는 function을 전달하여, exit를 실행할 때 실행하는 cleanup에 exit handler를 등록시켜준다. function은 32개까지 등록할 수 있으며, 등록된 역순으로 실행된다.

 

위 그림의 관계와 같이 main function에서 exit를 실행하거나, main에서 user function을 오가고 exit를 실행하거나, start-up routine에서 시작하거나 여러 경우가 있고, exit을 실행하면 종료하게된다. 

 exit는 종료하기 전에 cleanup 작업이 있고, 등록된 exit handler가 있다. exit handler들을 실행하고 exit로 돌아오고 cleanup을 실행하고 돌아온 후 종료하는 과정이 있게된다.

 만약 exit라이브러리를 호출하는 것이 아니라 _exit이나 _Exit를 호출한다면 main이나 user function에서 바로 종료하게 된다. 

 

다음 예제 코드를 보자

void func1() { printf("print func1\n"); }
void func2() { printf("print func2\n"); }
void func3() { printf("print func3\n"); }
void func4() { printf("print func4\n"); }

int main(int argc, char** argv){
	pid_t pid;
	
	atexit(func1);	atexit(func2);
	atexit(func3);	atexit(func4);

	pid=fork();
	
	if(pid==0){
		printf("CHILD PROCESS : CALLED\n");
		printf("CHILD PROCESS : EXIT CALL\n");
		exit(0);
	}	
	if(pid < 0){
		perror("PARENT ERROR");
		exit(1);
	}
	wait(NULL);
	printf("PARENT PROCESS : EXIT CALL\n");
	exit(0);
}

실행 결과는 아래와 같다.


[root@localhost unix]# ./a.out

CHILD PROCESS : CALLED
CHILD PROCESS : EXIT CALL
print func4
print func3
print func2
print func1

PARENT PROCESS : EXIT CALL
print func4
print func3
print func2
print func1

func1~4를 atexit를 통해 exit handler로 등록한다. fork로 child process를 생성하면 printf문이 실행되고, exit를 실행하므로 exit handler의 func들의 printf문이 등록된 역순으로 출력되는 결과가 보인다.

 child process가 끝나고 parent의 wait 이후 코드가 실행되면, printf 출력 후 exit문에서 exit handler가 또 역순으로 실행되는 것을 확인할 수 있다.

 

Synchronizing process

Synchronizing with children

child 프로세스가 종료할 때 어떠한 일들이 일어나는지 보자.

 

child의 모든 open한 descriptor를 close하고, child가 사용하던 모든 메모리를 releases한다. 

kernel은 child가 종료한 프로세스의 proc entry에 pid, exit status, cpu time 등을 저장한다. 

parent process가 child process가 종료했다는 것을 확인할 때까지 proc entry에 대한 정보(status)는 남아있다. status 정보가 parent process에게 전달되면 비로소 clear된다.

종료되는 child process의 parent process는 wait system call을 이용하여 정보를 확인한다. 

즉 parent가 wait를 실행한 후에야, child process의 proc entry가 proc table에서 released 된다. 

wait (2)

#include <sys/wait.h>

pid_t wait(int *statloc);
// Return: child process ID if OK, 0 (see later), or -1 on error

wait는 child process의 종료를 대기하는 system call이다.

statloc은 child process의 exit status를 받기 위한 변수이다.

만약 종료하는 child process가 여러개일 경우, return 값은 가장 먼저 종료한 child의 pid이다. 

 

wait는 child process가 실행 중일 동안 parent process의 실행을 잠시 중단한다. 

child가 종료하고 나면, waiting 중이던 parent가 재시작된다. 

child가 한 개 이상 실행 중이라면, child 중 하나라도 종료하자마자 wait가 return한다. 

wait가 -1을 return하는 경우는, child가 존재하지 않는 경우로 error에 ECHILD가 저장된다. 

child가 여러 개일 경우 각각의 child process에 대해서 wait를 여러 번 실행 한다. 

 

다음 코드를 보자.

int stat;
for(int i=0; i<3; i++){
	if(fork()==0) {
		sleep(rand()%100);
		exit(i);
	}
}

 wait(&stat);
 wait(&stat);
 wait(&stat);
 
 // while(wait(NULL)!=-1);

위와 같은 상황에서는 child process 3개가 각각 일정 시간 sleep후 exit하는 상황이다. parent process는 child process 3개의 exit status를 받기위해 wait를 3번 실행하고 있다.

child process가 각자 exit하는 시간이 다르므로, 가장 먼저 exit되는 child process의 exit 값이 가장 먼저 parent가 호출한 wait의 stat으로 들어간다. 

또 다른 방법으로는 while(wait(NULL)!=-1) 을 이용하는 방법이 있다. wait(NULL)로 exit의 status를 받지않고, 종료되는 child process의 pid를 반환하므로, child process가 있는 3번은 pid를 반환하고 child process가 모두 종료되면 -1이 반환되어 while문이 끝난다.

 

예제 코드를 보자

int status;
pid_cpid;

cpid = fork();  /* create new process */
if(cpid ==0){
   /* child */
   /* do something ... */
} else {
   /* parent, so wait for child */
   cpid = wait(&status);
   printf(“The child %d is dead\n”, cpid);
}
------------------------------------------------------------------
pid_t pid;
int status;

pid = wait(NULL)      /* ignore status information */

pid = wait(&status);  /* status will contain status information */

fork로 반환된 cpid가 0인 경우 child process이고, 만약 0이 아닌 parent process인 경우 child의 종료를 wait하고, 종료된 child의 cpid를 출력한다.

wait(NULL)의 경우 child의 exit status를 받지 않고, wait(&status)일 경우 받는다. 

 

main(){	 /* status -- 자식의 퇴장(exit) 상태를 어떻게 알아내는지 보여준다 */
   pid_t pid;
   int status, exit_status;
   
   if((pid=fork()) < 0) fatal("fork failed");
   
   if(pid==0){	/*자식*/
      /* 이제 수행을 4초동안 중단시키기 위해 라이브러리 루틴 sleep을 호출 한다 */
      sleep(4);
      exit(5); /* 0이 아닌값을 가지고 퇴장 */
   }
   
   /* 여기까지 수행이 진전된 바 이것은 부모임. 자식을 기다린다. */
   if((pid=wait(&status)) == -1){
      perror("wait failed");
      exit(2);
   }

   /* 자식이 어떻게 죽었는지 알기 위해 테스트한다 */
   if (WIFEXITED(status)){
      exit_status = WEXITSTATUS(status);
      printf("Exit status from %d was %d\n", pid,exit_status);
   }
   exit(0);
}

fork로 구한  pid가 0으로 child process인 경우 4초 후에 exit(5)를 통해 종료한다.

parent는 child가 종료할 때 까지 wait하므로 4초간 wait한 후 child process의 pid가 반환된다. 

wait의 status에는 exit status도 들어가지만 비정상 종료에 대한 값도 들어가있다. WIFEXITED(status)를 통해 반환값이 true인 경우 정상적인 종료인지를 확인할 수 있다.

WEXITSTATUS(status)를 통해 exit status값만 추출할 수 있다.

 

waitpid (2)

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *statloc, int options); 
// Return: child process ID if OK, 0 (see later), or -1 on error

wait의 경우 가장 먼저 exit하는 child process에 의해 반환되지만, waitpid는 기다리는 child process를 지정하는 system call이다. 

pid 변수에 따라 경우가 다음과 같다.

pid ==-1 Waits for any child process. In this respect, waitpid is equivalent to wait
특정 pid를 지정하지 않음. wait와 동일
pid >  0 Waits for the child whose process ID equals pid.
특정 child의 pid를 기다림
pid == 0 Waits for any child whose process group ID equals that of the calling process.
parent가 속한 group id와 같은 child
pid <  0 Waits for any child whose process group ID equals the absolute value of pid.
parent와 group id가 다를때, pid의 절댓값은 child가 속한 group id, 해당 group에 속한 어떤 child를 기다림

option의 종류는 다음과 같다.

Constant Description
WCONTINUED

If the implementation supports job control, the status of any child specified by pid that has been continued after being stopped, but whose status has not yet been reported, is returned (XSI extension to POSIX.1).
WNOHANG
(wait = hang)
The waitpid function will not block if a child specified by pid is not immediately available. In this case, the return value is 0.
pid로 지정된 child에 대해서 즉시 exit status를 return한다. child process가 실행 중인 경우 0을 반환하고, exit한 경우 child process의 pid를 반환하여 child process의 exit여부를 알 수 있다. 
WUNTRACED

If the implementation supports job control, the status of any child specified by pid that has stopped, and whose status has not been reported since it has stopped, is returned. The WIFSTOPPED macro determines whether the return value corresponds to a stopped child process.

다음 예제를 보자

main(){/*status2--waitpid를 사용하여 자식의 퇴장상태를 어떻게 얻는지 본다*/
   pit_t pid;
   int status, exit_status;
   
   if((pid=fork()) < 0) fatal("fork failed");
   
   if(pid==0){  /* 자식 */
      /* 이제 4초동안 중단시키기 위해 라이브러리 루틴 sleep을 호출한다 */
      sleep(4);
      exit(5);  /* 0이 아닌 값을 가지고 퇴장한다 */
   }
   /* 여기까지 수행이 진전된 바 이것은 부모임. 따라서 자식이 퇴장했는지 확인한다.
      퇴장하지 않았으면, 1초 동안 수면한 후 다시 검사한다. */
   while(waitpid(pid, &status, WNOHANG) == 0){  
      printf("Still waiting... \n");
      sleep(1);
   }

   /* 자식이 어떻게 죽었는지 알기 위해 테스트한다 */
   if (WIFEXITED(status)){
      exit_status = WEXITSTATUS(status);
      printf("Exit status from %d was %d\n", pid,exit_status);
   }
   exit(0);
}

pid가 0인 경우 child process에서 sleep후 exit하고, parent의 경우 waitpid를 실행한다.

waitpid의 option이 WNOHANG이므로 return값이 0이면 child process가 실행중 이므로 1초 sleep하고, 그렇지 않으면 exit되었음을 확인하여 다음으로 넘어간다.  

 

WIFEXITED를 통해 child process가 정상적으로 exit하였는지 판단하고 WEXITSTATUS를 통해 exit status를 구하여 출력한다.

 

wait와 waitpid의 차이는 다음과 같다.

wait는 parent process를 무조건 block 시키지만, waitpid의 경우 WNOHANG 옵션을 통해 무조건적인 parent process blocking을 막을 수 있다.

 

다음은 exit status를 통해 상태를 확인하는 함수들이다.

Macro Description
WIFEXITED(status)
WEXITSTATUS(status)
evaluates to a nonzero value when the child terminates normally. If WIFEXITED evaluates to a nonzero value, then WEXITSTATUS evaluates to the low-order 8 bits returned by the child through _exit(), exit() or return from main.

WIFEXITED(status) : 정상종료 체크
WEXITSTATUS(status) : exit status return 
WIFSIGNALED(status)
WTERMSIG(status)

WCOREDUMP(status)
evaluates to a nonzero value when the child terminates because of an uncaught signal. If WIFSIGNALED evaluates to a nonzero value, then WTERMSIG evaluates to the number of the signal that caused the termination.

WIFSIGNALED(status) : signal을 받고 종료하였는지 체크
WTERMSIG(status) : signal 번호 return 
WCOREDUMP(status) : process 종료시 메모리 내용 dump 여부 확인
WIFSTOPPED(status)
WSTOPSIG(status)


evaluates to a nonzero value if a child is currently stopped. If WIFSTOPPED evaluates to a nonzero value, then WSTOPSIG evaluates to the number of the signal that caused the child process to stop.

WIFSTOPPED(status) : process가 일시중지되었는지를 체크 
WSTOPSIG(status) : stop signal return
WIFCONTINUED(status) evaluates to a nonzero value if a child is currently continued.

WIFCONTINUED(status) : process 재개 체크 

다음 예제 코드를 보자

#include <sys/wait.h>

void pr_exit(int status)
{
   if (WIFEXITED(status))
      printf("normal termination, exit status = %d\n",
         WEXITSTATUS(status));
   else if (WIFSIGNALED(status))
      printf("abnormal termination, signal number = %d%s\n",
         WTERMSIG(status),
#ifdef  WCOREDUMP
         WCOREDUMP(status) ? " (core file generated)" : "");
#else
         "");
#endif
   else if (WIFSTOPPED(status))
      printf("child stopped, signal number = %d\n",
         WSTOPSIG(status));
}

WIFEXITED(status)로 정상종료를 체크하고, WEXITSTATUS로 exit status를 확인한다.

WIFSIGNALED로 signal을 통해 종료하였는지 체크하고, WTERMSIG로 signal 종류를 확인한다. 

WCOREDUMP를 통해 메모리 dump 여부를 확인한다. 

WIFSTOPPPED를 통해 process의 stop여부를 확인하고, WSTOPSIG를 통해 stop signal 종류를 확인한다. 

 

Zombie and premature exits

좀비 프로세스는 프로세스가 죽었는데, 프로세스 목록에 나타나는 것을 zombie process라고 한다. 

 

좀비 프로세스가 왜 생기는지 알아보자.

child process가 exit하여 종료하면 parent의 wait에 exit status를 전달해야한다. 하지만 child가 exit하였는데도 parent가 wait하지 않으면 child process가 proc entry에서 clear되지 않는다. 이러한 상황에서 child process가 zombie process가 되어 프로세스 목록에 나타난다. 

zombie process는 parent가 wait할 때까지 기다린다. 

while(pid=fork()){
   if(++count==3) break;
}
    
if (pid == 0){
   sleep(5);
   printf("I will be back %d\n", getpid());
   exit(0);	/* 자식은 먼저 죽는다 */
}
else if(pid > 0){
   printf("Im parent %d\n", getpid());
   printf("Press any key\n");
   getchar();	/* 자식이 죽을 때까지 기다린다 */
}

while문으로 3번의 fork를 통해 child는 3번 만들어진다. child는 5초간 sleep후 exit하게되는데, parent가 wait하지 않고 getchar를 통해 입력을 기다리므로 child process는 zombie 프로세스가 된다.

 

child가 exit하면 sigchild 신호를 parent에게 보낸다. parent에서 wait를 실행하면 signchild 신호를 통해 exit status를 wait가 받고 정상적으로 child가 종료하지만, parent가 wait를 나중에 하게 되면 그 기간동안은 child는 zombie process가 된다. 

Orphan Process

zombie process와 달리 orphan process는 parent process가 child process보다 먼저 종료되는 경우에 발생한다.

init process가 주기적으로 orphan process를 찾아 orphan process의 parent가 된다. 

init process가 주기적으로 parent에 대해서 wait를 실행해주어 zombie process가 된 child의 parent가 되어준다. 

 

예제 코드를 보자

while(pid=fork()){
   if(++count==3) break;
}
    
if (pid == 0){
   printf("I will be back %d\n", getpid());
   sleep(500);
   exit(0);
}
else if(pid > 0){
   printf("Im parent %d\n", getpid());
   printf("Press any key\n");
   getchar();	/* 먼저 엔터를 치고 부모는 종료한다 */
}

fork를 통해 3개의 child를 생성한다. child는 500초 sleep 후 exit한다.

parent가 getchar에서 enter를 치고 child보다 먼저 종료한다면, child는 orphan process가 된다. 

init process가 wait를 통해 orphan process의 parent가 되어주고, 만약 child process가 exit한 후 init process가 wait할 때까지의 기간이 있다면 그 기간동안은 child process는 zombie process가 된다.

parent가 먼저 exit하여 child가 orphan이 된 경우 init process가 wait를 하면 orphan process의 parent가 되지만, 위 그림과 같이 child가 exit를 하고 나서 시간이 지난 후 init process의 wait가 있게되면 그 기간동안은 child는 zombie process가 된다. 

반응형

관련글 더보기