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

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

와나진짜

,