시그널에 대한 더 많은 정보를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

시그널

의미전달에 사용되는 대표적인 방법은 메세지와 신호입니다. 메세지는 여러가지 의미를 갖을 수 있지만 복잡한 대신 , 신호는 1:1로 의미가 대응되기 때문에 간단합니다. 컴퓨터에서 신호, 즉 시그널은 소프트웨어적인 interrupt입니다. 컴퓨터 용어에서 인터럽트라는 것은 하던일 A를 중간이 잠시 멈추고 다른일 B를 하고 난 후 다시 A로 돌아와서 멈춘 부분부터 일을 하는 것이죠.  자, 이 의미를 명확히 아셔야할 필요가 있습니다. 신호가 왜 SW적인 인터럽트인가가 포스팅 아래에 코드와 함께 설명이 됩니다.

우선 컴퓨터에서 시그널의 종류에는 어떤 것들이 있을까요? 리눅스 상에서 kill -l 명령어를 입력하면 모든 시그널의 종류와 대응되는 번호를 알 수 있습니다.

 

#kill -l

 

 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP

 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1

11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM

16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP

21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ

26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR

31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3

38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8

43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13

48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12

53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7

58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2

63) SIGRTMAX-1  64) SIGRTMAX

 
이 시그널 중에 몇가지만 알아보도록 하겠습니다. 

 
1. SIGHUP : 터미널과 연결이 끊겼을 때 발생합니다. 기본적인 처리는 프로세스가 종료되는 것입니다.
2. SIGINT : 인터럽트가 발생했을때 발생합니다. 기본으로 프로세스가 종료됩니다.
9. SIGKILL : 프로세스를 무조건 종료합니다. 절대 무시할 수 없으며 제어될 수도 없습니다.
11. SIGSEGV : 프로세스가 잘못된 메모리를 참조했을 때 발생합니다. 기본 동작은 코어덤프를 남기고 종료합니다.
19 SIGSTOP : 프로세스를 중단시킵니다. 종료한 상태는 아닙니다. 이 신호 역시 제어될 수 없습니다.

 

프로세스가 시그널을 받게 되면

 

1. 시그널에 해당되는 기본 동작을 하거나
2. 그 시그널을 무시하거나
3. 사용자가 정의한 함수를 통해 동작 방식을 바꿀 수 있습니다.
 
시그널은 다음과 같은 성질이 있습니다.
 
● 비신뢰성
시그널을 보내면 그 시그널이 제대로 도착했는지, 잘 전달되었는지 확인하지 않습니다. 때문에 신뢰성이 낮습니다.
 
 대기하지 않음
만약 시그널 처리 함수를 시그널을 처리하고 있는데 그 사이에 다시 시그널을 주게 되면 그 시그널은 무시됩니다.
 
 
프로세스를 직접 생성해서 시그널을 한번 줘보도록 합시다.

 

#include <signal.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
        printf("give signal...\n");
        sleep(30);
        exit(0);
}

이후 컴파일을 하여 실행파일로 만들어줍니다. 이후 실행하면 30초간 프로그램이 실행될 겁니다.

 

이 상태에서 Ctrl+C를 누르게 되면 SIGINT 신호를 보내게 됩니다. 기본동작은 종료이므로 프로세스가 종료하게 됩니다.

 

또는 kill -시그널번호 프로세스 id 를 통해서 시그널을 보낼 수 있습니다. 그러니까 SIGINT를 보내려면 kill -2 pid 또는 kill -SIGINT pid를 통해서 SIGINT보낼 수 있습니다.

 

 

# ./a.out

give signal...

^C

#
 
우리는 이 시그널을 제어하고 싶습니다. SIGINT 신호 발생시 종료한다는 문자열을 출력하고 3초 이후에 종료하고 싶습니다.
 
그래서 리눅스는 signal핸들러를 제공합니다.
 
signal함수
원형은 다음과 같습니다.
 

void (*signal(int signum, void (*handler)(int)))(int);

 

signum은 시그널을 발생시키는 번호입니다. 아까 SIGINT는 2번이었죠?? 아니면 매크로를 쓸수도 있습니다. SIGINT 그대로가 매크로로 정의되어 있습니다.

 

두번째 인자로 handler라는 함수포인터가 보이네요. 여기에 함수를 인자를 주게 되면 시그널을 받았을때 그 함수가 호출됩니다.

 

직접 코드로 짜고 확인해보지요.

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

void interruptHandler(int sig){
        printf("this program will be exited in 3 seconds..\n");
        sleep(3);
        exit(0);
}

int main(){
        signal(SIGINT, interruptHandler);
        printf("input Ctrl+C\n");
        while(1);
}


컴파일하고 실행 후에 Ctrl+C를 입력하면 메시지와 함께 3초 후 프로그램이 종료됩니다.

# ./a.out 
input Ctrl+C
^Cthis program will be exited in 3 seconds..

이번엔 SIGTSTP이라는 시그널을 보내보도록 할게요. 위 코드에서 단지 SIGINT를 SIGTSTP이라고 바꿔주기만 하면 됩니다. (아, 물론 printf안에 문자열도 바꾸면 이쁘장하겠죠?)

#include <signal.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void stopHandler(int sig){
        printf("this program will stop in 3 seconds..\n");
        sleep(3);
        exit(0);
}

int main(){
        signal(SIGTSTP, stopHandler);
        printf("input Ctrl+Z\n");
        while(1);
}

그후 Ctrl+Z를 입력하여 SIGTSTP을 보내면 적절히 핸들링이 되는 것을 확인할 수 있습니다. 

# ./a.out 
input Ctrl+Z
^Zthis program will stop in 3 seconds..

 

그렇다면 SIGSTOP은 어떨까요? 아래의 코드는 SIGSTOP을 핸들링하기 위한 코드입니다. 

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

void stopHandler(int sig){
        printf("this program will stop in 3 seconds..\n");
        sleep(3);
        exit(0);
}

int main(){
        signal(SIGSTOP, stopHandler);
        printf("wait\n");
        pause();
}

a.outkill을 통해서 SIGSTOP을 보내기 위해서 잠시 백그라운드로 돌립니다. /proc/92089/status에는 92089에 대한 프로세스 정보들이 나와있습니다. 현재 StateSpeeping, , 우리가 pause를 했기 때문에 이러한 상태가 되는 것입니다. Kill 명령으로 SIGSTOP(19)를 그 PID로 보내게 되면 어떻게 될까요?

 

# ./a.out &
[2] 92089
# wait <-- a.out 프로그램에 의한 출력
# cat /proc/92089/status | head -5
Name:   a.out
Umask:  0022
State:  S (sleeping)
Tgid:   92089
Ngid:   0
# kill -19 92089
# cat /proc/92089/status | head -5
Name:   a.out
Umask:  0022
State:  T (stopped)
Tgid:   92089
Ngid:   0

[2]+  Stopped                 ./a.out

시그널을 보내고 다시 상태를 확인해보면StateTstop 상태인 것을 확인할 수 있습니다. 여기서 알 수 있는 것은 SIGSTOP은 제어할 수 없다는 것을 보여줍니다.

위에서 SIGSTOP 설명을 보고 오세요. 제어할 수 없다는 설명이 있습니다. SIGKILL과 SIGSTOP은 사용자가 절대 제어할 수 없다는 점을 알고 있으세요~! 그 이유는 어떤 이유로 인해 프로세스를 무조건 죽여야하는 경우가 있습니다. 만약 좀비프로세스를 계속해서 생성하는 프로세스가 있는데, 이 프로세스를 죽이지 못하면 안되겠죠. 그래서 2개는 핸들링을 하지 못합니다.

또한 핸들러에 전달인자 sig는 시그널의 종류를 나타냅니다. 그렇기 때문에 시그널의 종류에 따라 처리할 수 있죠.

 

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

void signalHandler(int sig){
        if(sig==SIGINT){
                printf("this program will stop in 3 seconds..\n");
                sleep(3);
                exit(0);
        }
        if(sig==SIGQUIT){
                printf("signal SIGQUIT\n");
        }
}

int main(){
        signal(SIGINT, signalHandler);
        signal(SIGQUIT, signalHandler);
        printf("input Ctrl+C or Ctrl+\\ \n");
        while(1);
}

 

프로그램을 실행하여 Ctrl+\를 입력해서 SIGQUIT신호를 보내도 종료하지 않고, Ctrl+C를 입력하여 SIGINT를 보냈을 때 3초안에 종료합니다.

# ./a.out

input Ctrl+C or Ctrl+\

^\signal SIGQUIT

^\signal SIGQUIT

^\signal SIGQUIT

^\signal SIGQUIT

^Cthis program will stop in 3 seconds..

 

 

SW적인 Interrupt

중간에 멈추고 다른일을 하고 다시 돌아와 아까 했던일을 처리하는것이 인터럽트라고 하였습니다. 아래의 코드가 있습니다. SIGINT와 SIGQUIT을 처리하는 함수 2개가 있죠.

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>
#include <stdlib.h>

void sig_int(int signo){
        volatile int k=0;
        int i,j;
        printf("sig_int start\n");

        for(i=0;i<300;i++)
                for(j=0;j<600000;j++)
                        k+=i*j;

        printf("sig_int end\n");
}

void sig_quit(int signo){
        volatile int k=0;
        int i,j;
        printf("sig_quit start\n");

        for(i=0;i<300;i++)
                for(j=0;j<600000;j++)
                        k+=i*j;

        printf("sig_quit end\n");
}
int main(){
        signal(SIGINT, sig_int);
        signal(SIGQUIT, sig_quit);
        //pause는 신호가 생기고 처리될때까지 대기하는 함수
        pause();
        printf("process end\n");
}

저는 SIGINT와 SIGQUIT 순서대로 시그널을 줄겁니다. for문을 도는 이유는 SIGINT가 끝나기 전에 SIGQUIT을 수행하기 위해서 시간 지연을 하는 역할을 합니다. 그럴때 결과를 여러분들이 예측해보세요.

아래와 같은 결과일까요? SIGINT를 처리하고 나서 SIGQUIT을 처리하니까요. 

^Csig_int start
^\sig_quit start
sig_int end
sig_quit end
process end

 

땡! 아래의 결과처럼 나오게 됩니다. 

^Csig_int start
^\sig_quit start
sig_quit end
sig_int end
process end

 

SIGINT신호가 발생되어 신호 처리부인 sig_int 수행합니다. for문을 도는 도중에 SIGQUIT 신호가 발생해서 sig_quit을 수행하러 갑니다. 그럼 sig_int는 sig_quit가 처리가 다 되고 나면 나중에 멈췄던 부분부터 다시 수행한다는 겁니다. 이런 사실을 망각하게 되면 아주 어려운 버그를 만날수가 있지요. 

느린 시스템 콜에서의 시그널

여기서부터는 라이브러리 함수가 아닌 시스템 콜과 시그널에 대해서 이야기합니다. 그 중에서도 반환이 느린 시스템 콜에 대해서 이야기합니다. 만약 read하는 동안 signal이 발생했다면 어떻게 될까요?

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void sig_int(int signo){
        printf("SIGINT 발생!\n");
}
int main(){
        char buf[32];
        int n;

        signal(SIGINT, sig_int);
        //읽는 중 SIGINT를 발생 시키면 오류? 아니면 성공?
        if((n = read(STDIN_FILENO, buf,32)) < 0){
                printf("read error\n");
        }else{
                buf[n]='\0';
                printf("%s\n",buf);
        }
}

 

위의 코드를 리눅스 상에서 실행을 시키고 난 후 아래와 같이 실행시켜보았습니다. 

# ./a.out
^CSIGINT 발생!
^CSIGINT 발생!
hello^CSIGINT 발생!
reakwon
reakwon

 

read를 하는 와중에 SIGINT를 발생시켰는데 오류가 발생하지 않고 처음부터 다시 read를 하는 것처럼 보이는데요. 이렇게 리눅스에서는 read와 같은 대기 시간이 길어가 수행시간이 긴 시스템 콜이 발생할때 자동으로 그 시스템 콜을 재시작하게 됩니다. 그러므로 hello는 출력이 되지 않고, reakwon만 출력이 되는거죠. 재시작한다는 것이 중요한 포인트입니다. 

read뿐만 아니라, 느린 호출에 해당되는 ioctl, read, readv, write, writev, wait, waitpid가 있습니다. 

이런 재시작 처리 방식은 시스템마다 다른데요. SVR2, SVR3, Solaris 등의 시스템은 자동 재시작하지 않고 errno를 EINTR로 설정한 후 반환시킵니다. 그래서 프로그래머는 아래와 같이 errno를 검사하며 프로그래밍해야됩니다. 하지만 우리는 리눅스를 만지는 중이니까 이런 처리를 할 필요는 없죠. 

retry :
        if ((n = read(fd, buf, BUF_SIZE)) < 0){
                if(errno == EINTR){
                        goto retry:
                }
        }else{
                printf("%s\n",buf);
        }

 

이처럼 리눅스 신호의 대한 개념과 시그널을 어떻게 잡아서 다룰 수 있는지에 대해서 알아보는 시간을 가졌습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

더 많은 정보를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

프로세스(process)

프로세스는 간단히 말하자면 실행 중인 프로그램을 의미합니다. 아마 여러분들은 컴퓨터를 하면서 아주 빈번하게 듣는 용어이기도 합니다.

실행 중인 프로그램이라??

컴퓨터에서 말하는 실행 중인 프로그램이라는 건 저장공간에 있는 실행 파일이 메모리를 할당받아 명령어를 수행한다는 것을 의미합니다.

우리가 예를 들어 한글이라는 프로그램을 설치했다고 가정합시다. 아직은 더블클릭으로 프로그램을 실행하지 않았으므로 프로세스가 아닙니다. 설치 후 보고서를 작성하기 위해 프로그램을 실행한다면 그 순한 한글이라는 프로세스가 생성되는 것입니다.

프로세스에서 프로세스를 생성할 수도 있습니다. 이 경우 생성한 프로세스는 부모 프로세스, 생성당한 프로세스를 자식프로세스라고 합니다.

 

 

 

코드 영역(혹은 텍스트 영역이라고도 함) : 프로세스의 명령어가 존재합니다. CPU는 이 영역에서 명령을 실행합니다. 위 그림에서는 부모 자식 프로세스간 별개로 텍스트 영역이 존재하는 것처럼 보이지만 실제로 코드영역은 프로 자식 프로세스간 공유가 됩니다.

 

데이터 영역 : 전역 변수와 정적 변수가 존재합니다. 프로그램이 실행하는 동시에 할당되고 종료시 소멸됩니다.

 

힙 영역 : 동적할당에 필요한 메모리입니다. 프로그램 종료시 해제되지 않고 직접 해제해 주어야 합니다. C언어에서는 free를 사용해서 해제하게 됩니다. 

 

스택 영역 : 함수 호출 시 매개변수, 지역변수가 이 곳에 자리잡게 됩니다. 함수가 종료하게 되면 해제됩니다.

 

자식 프로세스를 생성하는 시스템 콜이 fork라고 합니다. fork는 자기 자신을 복사합니다만 결국 다른 별도의 메모리를 갖게 됩니다. 그래서 자식 프로세스에서 변수의 값을 바꾸어도 부모 프로세스에 영향을 주지 않게 되죠.

 

자식 프로세스 생성(fork)

프로세스를 하나 생성하면서 특징을 같이 이야기 해봅시다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int g_var = 1; 
char buf[] = "a write to stdout\n"; 

int main(){ 
        int l_var; 
        pid_t pid; 
        l_var = 10; 

        if(write(STDOUT_FILENO,buf,sizeof(buf)-1) != sizeof(buf)-1){ 
                printf("write error\n"); exit(0); 
        } 

        printf("fork start!\n"); 

        //fflush(stdout); 
        if((pid = fork())<0){ 
                printf("fork error\n"); 
                exit(0); 
        } else if(pid == 0){ 
                //child 
                g_var++; 
                l_var++; 
        } else { 
                //parent sleep(3); 

        } 

        printf("pid = %ld, g_var = %d, l_var = %d\n",
                        (long)getpid(),g_var,l_var); 

        exit(0); 
}
 
이제 컴파일 후 실행시켜보도록 하지요.
 # gcc fork.c 
 # ./a.out
 a write to stdout
 fork start!
 pid = 83929, g_var = 2, l_var = 11
 pid = 83928, g_var = 1, l_var = 10

 

위 코드를 설명하자면 fork를 통해서 자식 프로세스를 생성하게 됩니다. fork가 정상적으로 호출이되면 자식 프로세스는 0이 반환되고 부모 프로세스에게는 자식의 프로세스 id가 반환됩니다. 

자식 프로세스는 변수들의 값을 변경시키고 있습니다. 그 결과는 위와 같습니다.

 

위의 실행 결과를 파일로 저장해볼까요?

 # ./a.out > fork.out
 # cat fork.out
 a write to stdout
 fork start!
 pid = 83960, glob = 2, var = 11
 fork start!
 pid = 83959, glob = 1, var = 10

자세히보면 fork_start!라는 문자열이 두번 출력되고 있네요. 이같은 결과가 우리에게 시사하는 바는 자식 프로세스는 부모 프로세스의 버퍼까지 복사한다는 사실입니다.

printf는 내부적으로 write 시스템 콜을 사용합니다. printf는 write를 최소한으로 그리고 효율적으로 사용하기 위해 내부적으로 버퍼를 사용하고 있습니다.

터미널 콘솔에 출력할때는 l줄 단위 버퍼링 방식을 사용합니다. 이 말은 줄이 바뀌면(엔터) 화면에 출력한다는 것입니다. 이와 다르게 파일에 기록할때는 full 버퍼링 방식을 사용합니다. 이 방식은 버퍼가 전부 채워져야 파일에 기록하는 버퍼링 방식입니다.

위의 테스트 결과에서 내용을 파일(fork.out)에 저장하는 것이므로 full buffering 방식을 사용하며 이때 버퍼가 다 차지 않았고 이 후에 자식 프로세스를 생성했습니다.

자식 프로세스는 부포 프로세스의 버퍼까지 전부 복제하였고 프로그램이 종료가 될때 버퍼를 비우며 종료가 됩니다.

 

위의 fflush(stdout)을 주석해제하고 다시 실행해보시기 바랍니다. fork전에 버퍼를 다 방출하기 때문에 fork_start!라는 문자열이 한번만 출력됩니다.
 

고아 프로세스

프로세스를 생성하기만 하면 다가 아닙니다. 왜 그런지 실행과 결과를 보면서 이야기 하도록 하지요.

#include <stdio.h> 
#include <unistd.h> 
#include <stdlib.h> 

int main(){ 
        int pid; 

        if((pid=fork()) == 0){ 
                sleep(1); 
                printf("\t [child process %d] created from %d\n", getpid(),getppid());
                exit(0); 
        } 

        printf("[parent process %d] create %d process, ppid:%d\n", getpid(),pid,getppid()); 

}

getpid : 현재 프로세스의 pid를 반환합니다. 

getppid : 부모의 프로세스 id를 반환하지요.

 

이 코드의 결과를 예상해보세요.

자식 프로세스에서는 부모의 프로세스 ID를 출력하니까 부모의 getpid와 같을 겁니다. 그럴까요??

 

[parent process 21162] create 21163 process, ppid:20819

[child process 21163] created from 1

 

????

우리의 예상과는 다른데요??

분명 부모프로세스의 pid는 21162인데 자식 프로세스의 부모프로세스 pid가 쌩뚱맞은 1입니다.

 

왜 그럴까요??

 

우선 이 코드는 문제가 좀 있는 코드입니다. 부모프로세스가 먼저 종료되었기 때문입니다.  자식이 먼저 종료된다는 사실을 확보하기 위해 1초를 기다리는 이유가 바로 이 사실을 확인하기 위해서 입니다.  자식 프로세스는 부모 프로세스를 잃어 다른 부모를 찾게 됩니다. 바로 init 프로세스라고 하는데요. init프로세스의 pid가 바로 1입니다.

그러니까 모든 프로세스의 선조가 init프로세스라는 이야기죠.

이런 프로세스를 부모를 잃었다고 하여 고아프로세스가 됩니다.

 

그렇기 때문에 모든 부모님이 그렇듯 자식을 기다려주어야합니다. 그렇기 때문에 wait이라는 시스템 콜이 있지요.

 

코드를 수정해서 다시 확인합시다.

 

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

int main(){         
        int pid;         
        int status;         
        int terminatedPid;         

        if((pid=fork()) == 0){  
                printf("\t [child process %d] created from %d\n", 
                                getpid(),getppid()); 

                sleep(1);       
                exit(0);     
        }      
        printf("[parent process %d] create %d process, ppid:%d\n",  
                        getpid(),pid,getppid()); 
        terminatedPid = wait(&status);      
        printf("[parent process ] process wait process %d, status %d\n",  
                        terminatedPid,status); 
}

wait : wait의 첫번째 인자는 상태를 나타냅니다. 종료 상태를 받아올 수 있는 거죠. 반환값은 종료된 자식의 pid를 반환합니다.

 

[parent process 21803] create 21804 process, ppid:20819

         [child process 21804] created from 21803

[parent process ] process wait process 21804, status 0

 
이제 우리가 원하는 결과가 나왔군요. 자식프로세스를 기다려야합니다.

 

 

좀비프로세스

하지만 반대로 부모프로세스는 종료하지 않았지만 자식 프로세스가 먼저 종료하게 되면 어떨까요? 단, wait 호출을 하지 않고 말입니다.

#include <stdio.h> 
#include <unistd.h> 
#include <stdlib.h> 

int main(){   
        if(fork()==0){    
                printf("\t child process created and exit\n");  
                exit(0);   
        }      
     
        printf("parent process sleep\n"); 

        sleep(10);  
        sleep(10);
        printf("process exit\n"); 
}
이 후 컴파일하고 실행합니다.
# gcc process.c
# ./a.out &
[1] 22111
parent process sleep
child process created and exit
process exit

# ps -ef | grep 22111

...

root     22112 22111  0 10:26 pts/2    00:00:00 [a.out] <defunct>

...

 

 
부모 프로세스는 총 20초간 수행합니다. pid가 22111인 부모프로세스의 자식 프로세스 22112가 defunct인것이 보이세요? 바로 좀비 프로세스입니다.
 
부모프로세스는 자식 프로세스를 기다리고 있지 않아 종료상태를 얻지 못하고 있습니다. 그게 좀비 프로세스가 된 이유인데요. 이 종료 상태는 의외로 중요한데 예를 들어 쉘스크립트를 통해 명령을 실행할때 종료상태에 따라 분기를 하기 위해서 사용되기도 합니다.
 
부모 프로세스가 종료상태를 얻어와야 커널에서 자식 프로세스의 정보를 갖고 있는 구조체를 해제할 수 있습니다. 그 구조체가 task_struct라는 구조체입니다.
 
부모 프로세스는 커널이 관리하는 task_struct 구조체에서 멤버 pid와 exit_code를 통해서 종료상태를 얻어올 수 있습니다. 비록 좀비프로세스는 직접적으로 CPU, 메모리를 소모하지는 않지만 task_struct를 유지해야하기 때문에 메모리 낭비가 있게 되지요.
 
task_struct에서 pid와 exit_code, 무엇이 생각나세요?
wait의 반환값과 인자값입니다. 그 때문에 wait을 통해 해결할 수 있습니다.

 

코드를 다음과 같이 수정해보세요. 

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

int main(){     
        if(fork() == 0){     
                printf("\tchild process created and exit\n");  
                exit(0);   
        }       
      
        printf("parent process sleep\n");   
  
        wait(NULL); 

        sleep(10);
        sleep(10);
        printf("process exit\n"); 
}


부모 프로세스에서 바로 wait 콜을 호출합니다. 그러니까 종료상태를 알게 되죠. 결과도 그럴까요?

# gcc process.c

# ./a.out &

[1] 23839

parent process sleep

        child process created and exit





# ps -ef | grep 23839

root     23839 20819  0 11:31 pts/2    00:00:00 ./a.out

root     23842 20819  0 11:31 pts/2    00:00:00 grep --color=auto 23839

process exit

 

오.. 좀비프로세스가 없어졌어요. 

또는 시그널을 이용한 방법도 있습니다.

 

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

void childSignal(int sig){        
        printf("signal:%d\n",sig); 
        wait(NULL);
} 

int main(){     
        signal(SIGCHLD,childSignal);     
        if(fork() == 0){ 
                printf("\tchild process created and exit\n"); 
                exit(0);   
        }    
   
        printf("parent process sleep\n"); 
        
        sleep(10);  
        sleep(10);

        printf("process exit\n"); 
}

(시그널을 알아야 코드를 이해하는 데 도움이 됩니다.)

그리고 컴파일하고 실행해보도록 합시다. 그리고 재빠르게 ps 명령어를 쳐서 확인해보세요.

# gcc process.c

# ./a.out &

[1] 23451

parent process sleep

        child process created and exit

signal:17



# ps -ef | grep 23451

root     23451 20819  0 11:09 pts/2    00:00:00 ./a.out

root     23461 20819  0 11:10 pts/2    00:00:00 grep --color=auto 23451

process exit
 

이제 좀비프로세스가 없어졌군요!

왜 그럴까요??

우선 부모프로세스는 시그널을 등록합니다. 핸들러는 childSignal입니다.

자식 프로세스가 종료가 되면 시그널 번호 SIGCHLD로 부모프로세스에 시그널을 보냅니다. 

그렇다면 그 순간 시그널 핸들러가 동작하죠. 그렇다면 부모프로세스는 childSignal함수안에 wait을 호출합니다.

그러므로 종료상태를 받을 수 있게 되고 자식 프로세스는 좀비프로세스가 되지 않습니다.

아차, sleep함수는 일정 시간 동안 시그널을 기다립니다. 그 시간안에 시그널을 받는 다면 sleep은 동작을 멈추게 됩니다. 

왜 2번 sleep을 호출하는지 알겠죠? 바로 ps를 입력할 시간을 확보하기 위해서 입니다.

 

자식 프로세스의 종료 상태 알아오기

이번에는 wait에 인자를 이용하여 자식 프로세스의 종지 상태를 알아보도록 합시다. 종지상태를 조사하는 매크로는 표에 나와있습니다.

 

 매크로  설명
 WIFEXITED(status)   자식프로세스가 정상적으로 종료되었으면 true를 반환합니다. 이때 자식이 exit, _exit, _Exit으로 넘겨준 인수의 하위 8비트를 얻을 수도 있습니다.
그럴때 WEXITSTATUS(status)를 사용하세요.
 WIFSIGNALED(status) 만일 자식프로세스가 신호를 받아 비정상적으로 종료되었다면 참을 반환합니다. 
이때 신호번호를 알려면 WTERMSIG(status)를 이용하여 알 수 있습니다. 
WCOREDUMP(status) 매크로를 지원하는 시스템은 코어덤프를 생성했는지도 알 수 있습니다. 
 WIFSTOPPED(status)  자식 프로세스가 정지된 상태라면 참을 반환합니다. 이때 정지를 유발한 신호는 WSTOPSIG(status)를 통해 알아낼 수 있습니다. 
 WIFCONTINUED(status) 만일 자식이 작업 제어 정지 이후 재개되었으면 참을 반환합니다. 

 

 

다음은 위의 매크로를 사용한 예제입니다.

 

#include <sys/wait.h> 
#include <unistd.h>
#include <stdio.h> 
#include <stdlib.h>  
#include <errno.h>
#include <string.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)); 
}  

int main(void){     
        pid_t pid;     
        int status;    

        if((pid = fork()) < 0){       
                printf("[1] fork error\n");       
                exit(0);      
        } else if(pid == 0) {
                // 자식프로세스 곧장 7 반환하며 종료
                exit(7);       
        }
        //부모 프로세스에서 자식 상태 점검
        if(wait(&status) != pid){      
                printf("[1] wait error\n");       
                exit(0);       
        }     
    
        //자식의 종료 상태 출력
        pr_exit(status);       


        if((pid = fork()) < 0){     
                printf("[2] fork error\n");       
                exit(0);      
        } else if(pid == 0) {
                //자식 abort signal 발생하며 종료, 신호번호 6   
                abort();
        }        

        //부모 프로세스에서 자식 상태 점검
        if(wait(&status) != pid){       
                printf("[2] wait error\n");      
                exit(0); 
        }        
       
        //자식의 종료 상태 출력
        pr_exit(status);      

        return 0;
}

 

컴파일 후 실행해보면 자식 프로세스가 어떻게 종료가 되었는지 알 수 있습니다.

 # ./a.out
normal termination, exit status = 7
abnormal termination, signal number =6 (core file generated)

 

자식 프로세스가 exit(7)로 종료되었음을 부모 프로세스는 WEXITSTATUS매크로로 알 수 가 있네요.

이제 fork와 wait을 사용하는 방법을 아시겠나요?

하지만 wait에는 문제점이 존재하는데요. 그 문제점을 보완하는 함수 waitpid가 존재합니다. 

아래의 포스팅을 참고하세요.

https://reakwon.tistory.com/99

 

반응형
블로그 이미지

REAKWON

와나진짜

,

구간트리(Segment Tree)


구간트리는 특정 구간에서 특정한 값을 뽑아올때 유용하게 사용됩니다. 세그먼트트리라고도 하지요. 한 가지 예를 들어보도록 할게요. 어떤 배열이 아래와 같이 있다고 치구요.


int arr[] = {7, 4, 5, 1, 9, 5, 2, 11, 10};


0번 요소부터 5번째 요소까지의 가장 작은 값은 얼마인가요? 1이네요. 그쵸?

그렇다면 2번 요소부터 7번째 요소까지 가장 큰 값은 얼마일까요? 11이 됩니다. 


어떻게 찾을 수 있죠? 구간트리를 배우지 않았다면 for루프를 통해서 가장 작은 값이나 가장 큰 값을 구할겁니다.




for (i = from; i <= to; i++) {
	minVal = min(arr[i], minVal);
}


이렇게 말이죠. 아주 쉽네요!! 시간 복잡도는 O(n)입니다. 


하지만 구간에서 가장 작은 값을 계속해서 뽑아내야하는 상황이라면 구간트리를 사용해야합니다. 구간트리를 사용한다면 최소, 최대값을 찾는데 O(log n)이면 충분합니다.


구간트리의 노드는 특정 구간에서 가장 작은 값을 가지고 있습니다. 아래 트리가 구간 트리를 보여줍니다. 위의 배열을 구간트리로 표현한 모습이죠.




파란색 원 안의 숫자는 노드의 번호, 사각형 안의 숫자는 배열의 범위를 나타냅니다. 우리는 트리를 배열로 표현하기 위해서 가장 첫번째(root)는 1번 인덱스를 갖습니다. 자식 노드의 번호는 2와 3이 됩니다.

그렇다면 어떤 노드 i의 왼쪽 자식은 i*2, 오른쪽 자식은 i*2+1이 되는 것이죠.


우리가 3번 요소부터 7번 요소까지 가장 작은 값을 갖는 값을 뽑아오려면 5번, 6번, 14번 노드를 통해서 구할 수 있습니다.


이제 본격적으로 구현해보도록 합시다. 



구현(C++)


구간 트리에서 특정 구간에서 최소값을 찾는 것을 구간 최소 트리(Range Minimum Query, RMQ) 라고 합니다. 그래서 이 구조체를 만드는 것에서부터 시작합니다.




struct RMQ {
	int size;
	vector<int> minValues;
	RMQ(int *arr,int arrSize) {
		size = arrSize;
		minValues.resize(size * 4);
		init(arr, 0, size - 1,1);
	}
}

size는 배열의 size를 의미합니다. minValues는 해당 노드에서 가장 작은 값을 저장하는 벡터입니다.


왜 minValues의 사이즈를 배열의 사이즈 * 4를 할까요? 위의 트리를 다시 보게 되면 배열의 크기보다 많은 노드를 볼 수 있습니다. 완전 이진 트리를 아신다면 마지막 leaf의 개수 * 2가 트리의 노드수를 의미한다는 것을 알겁니다.

하지만 귀찮으니 4를 곱하면 된다고 하네요.


이제 이 구조체를 초기화하는 함수 init을 불러서 구간트리의 모양을 잡아보도록 합시다.


init

잘 생각해보면 간단합니다. 왼쪽 자식, 오른쪽 자식의 값을 비교해서 가장 작은 값이 지금 이 노드의 값이 됩니다.

만약 leaf노드까지 도달했다면 그 값만을 반환해주면 되죠.

그리고 구간트리의 인덱스 node라는 값도 함께 넘겨주어 현재 노드에 가장 작은 값을 저장할 수 있게끔 하면 됩니다.




int init(int *arr, int left, int right,int node) { if (left == right) return minValues[node] = arr[left]; int mid = (left + right) / 2; int leftMinValue = init(arr, left, mid, node * 2); int rightMinValue = init(arr, mid + 1, right, node * 2 + 1); return minValues[node] = min(leftMinValue, rightMinValue); }



query

이 함수는 질의, 즉 물어보는 함수입니다. 특정 구간에 가장 작은 값을 반환하여라! 라고 질문을 던져 답을 받습니다. 이 함수도 역시 잘 생각해보면 별 어려움은 없습니다. 

질의하는 범위가 노드가 커버할 수 있는 범위를 완전히 포함한다면 그 값을 내주면 됩니다.


그것이 아니라면 아주 큰 값을 리턴하면 되지요.


만약 위의 배열에서 3-7 구간에 대해 질의를 한다면 5번, 6번, 14번 노드가 3-7구간에 완전히 포함되므로 그 세개의 노드만이 자신의 값을 반환합니다. 그 후 가장 작은 값이 답이 되겠죠?


헷갈릴 수 있습니다. 노드가 커버하는 범위가 질의하는 범위에 완전히! 속해있어야합니다. 




int query(int left, int right, int node, int nodeLeft, int nodeRight) {
	if (right < nodeLeft || nodeRight < left) return INF;
	if (left <= nodeLeft&&nodeRight <= right)
		return minValues[node];

	int mid = (nodeLeft + nodeRight) / 2;
	int leftMinValue = query(left, right, node * 2, nodeLeft, mid);
	int rightMinValue = query(left, right, node * 2 + 1, mid + 1, nodeRight);

	return min(leftMinValue, rightMinValue);
}


너무 함수가 섹시하지가 않군요. 인자가 너무 많습니다. C++에 지원되는 오버로딩을 사용하여 좀 더 간편하게 부를 수 있도록 하죠.


int query(int left, int right) {
	return query(left, right, 1, 0, size - 1);
}



update

구간 트리에서 값이 바뀌면 구간의 최소값도 바뀌어여합니다. 특정 index와 새로운 value를 받게되면 구간트리의 해당 노드의 값을 바꾸고 차례대로 값을 갱신해주어야합니다. 여기서 노드의 값이 바뀌는 순서는 해당 leaf노드부터 루트까지 올라오게 됩니다.


만약 5번 인덱스가 새로운 값으로 바뀌게 되었다면 해당하는 노드의 번호 12번노드부터 6번 노드, 3번 노드, 1번 노드가 갱신되어야 하죠.



int update(int index, int value, int node, int nodeLeft, int nodeRight) {
	if (index < nodeLeft || nodeRight < index) return minValues[node];

	if (nodeLeft == nodeRight) return minValues[node] = value;
	int mid = (nodeLeft + nodeRight) / 2;
	int leftMinValue = update(index, value, node * 2, nodeLeft, mid);
	int rightMinValue = update(index, value, node * 2 + 1, mid + 1, nodeRight);
	return minValues[node]=min(leftMinValue, rightMinValue);
}

그러니까 nodeLeft==nodeRight가 같은 경우, 즉 해당하는 leaf인 경우 그 노드의 값을 갱신합니다.index의 범위 밖이면 그냥 가지고 있는 값을 반환해주면 되고, index가 포함되어 있는 경우라면 왼쪽 자식 값, 오른쪽 자식 값을 비교해서 가장 작은 값을 갖게 해주면 됩니다.


int update(int index, int value) { return update(index, value, 1, 0, size - 1); }



깔끔하게 함수를 호출할 수 있도록 오버로딩했구요.


전체코드

#include <iostream>
#include <vector>
#include <algorithm>
#define INF 99999999
using namespace std;
struct RMQ {
	int size;
	vector<int> minValues;

	RMQ(int *arr,int arrSize) {
		size = arrSize;
		minValues.resize(size * 4);
		init(arr, 0, size - 1,1);
	}

	int init(int *arr, int left, int right,int node) {
		if (left == right) return minValues[node] = arr[left];

		int mid = (left + right) / 2;
		int leftMinValue = init(arr, left, mid, node * 2);
		int rightMinValue = init(arr, mid + 1, right, node * 2 + 1);

		return minValues[node] = min(leftMinValue, rightMinValue);
	}

	int query(int left, int right, int node, int nodeLeft, int nodeRight) {
		if (right < nodeLeft || nodeRight < left) return INF;
		if (left <= nodeLeft&&nodeRight <= right)
			return minValues[node];

		int mid = (nodeLeft + nodeRight) / 2;
		int leftMinValue = query(left, right, node * 2, nodeLeft, mid);
		int rightMinValue = query(left, right, node * 2 + 1, mid + 1, nodeRight);

		return min(leftMinValue, rightMinValue);
	}

	int query(int left, int right) {
		return query(left, right, 1, 0, size - 1);
	}

	int update(int index, int value, int node, int nodeLeft, int nodeRight) {
		if (index < nodeLeft || nodeRight < index) return minValues[node];

		if (nodeLeft == nodeRight) return minValues[node] = value;
		int mid = (nodeLeft + nodeRight) / 2;
		int leftMinValue = update(index, value, node * 2, nodeLeft, mid);
		int rightMinValue = update(index, value, node * 2 + 1, mid + 1, nodeRight);
		return minValues[node]=min(leftMinValue, rightMinValue);
	}

	int update(int index, int value) {
		return update(index, value, 1, 0, size - 1);
	}
};

int main() {

	int arr[] = { 7, 4, 5, 1, 9, 5, 2, 11, 10 };
	RMQ rmq(arr, sizeof(arr) / sizeof(int));

	printf("query(0-8) : %d\n", rmq.query(0, 8));
	printf("query(1-6) : %d\n", rmq.query(1, 6));
	printf("query(7-8) : %d\n", rmq.query(7, 8));
	printf("query(3-7) : %d\n", rmq.query(3, 7));
	printf("query(0-2) : %d\n", rmq.query(0, 2));
	printf("query(0-2) : %d\n", rmq.query(4, 8));
	printf("update(index 4, value 0)) : %d\n", rmq.update(4,0));
	
}



-- 내용과 코드는 구종만의 알고리즘 문제해결 전략을 참고했습니다 

반응형
블로그 이미지

REAKWON

와나진짜

,