본문 바로가기

CS/유닉스프로그래밍

8. 유닉스 시그널 프로세싱 - 1

시그널은 소프트웨어 인터럽트임, 다른 프로세스에게 시그널을 보낼 수 있음

시그널을 받으면 그에 대한 액션을 하게 됨

대부분의 non trivial process는 시그널을 처리할 필요가 있음

비동기적 사건을 처리할 수 있는 방법을 시그널이 제시해줌

 

시그널 개념

$ cc verybigprog.c

아주 큰 프로그램이라서 오랫동안 돌리는 동안 에러가 많이 뜰 건데, 에러가 뜨면 계속 돌릴 필요 없음.

foreground로 돌려서 중지시키기 위해서 ^C를 하거나 인터럽트 키를 누르면 됨

에러를 고쳐서 다시 돌리면 되고, 여기서 시그널이 발생함

 

^C를 하면 커널이 응답을 해서 foreground에서 도는 모든 프로그램에게 SIGINT를 보냄

커널 프로세스가 foreground에 SIGINT를 보냈을 때

cc가 돌고 있는데, cc가 SIGINT를 받아서 그에 대한 action을 함

 

default action은 abnormal termination임

또는 유저가 정의한 함수(핸들러)를 실행하고 다시 돌아오면 interrupt 방식이 됨

언제 발생할지 모르기 때문에 asynchronous임

시그널은 asynchronous 한 event를 handling하는 방식을 제공

 

이런 시그널이 32개 정도 있음, signal.h에 들어 있음

여기서 positive integer 0부터 63까지 쓸 수 있음, 그 중 반은 쓰고 반은 안씀

 

시그널을 발생시키는 방법으로는 키보드에서 주는 방식이 있음 terminal generated signal

두번째는 hadware exception임. 보통 cpu나 주변 장치로부터 옴

cpu에서는 0으로 나누는 연산으로 exception error를 발생시킴

그리고 유저가 kill 시스템콜을 cmd에서 사용해서 시그널을 보낼 수 있음

 

$ kill 1947

pid가 1947인 프로세스에게 SIGTERM을 보내는 것임

SIGTERM이 보내지면 이 프로세스가 termination 함

어떤 프로세스는 SIGTERM을 보내도 안 죽을 수 있음 그러면 더 강력한 시그널을 보냄

SIGTERM 보다 더 강력한 시그널이 SIGKILL

 

$ KILL -9 1947

-9를 붙이면 거의 대부분의 프로세스가 죽음. 

그래도 안 죽는 프로세스가 있으면 전원을 껐다가 키면 됨

 

 

0번 시그널은 무시되는 특별한 의미를 가짐

63번까지 있는데 31번까지 씀, 33번부터는 안씀 , 32번은 없음

 

1번 SIGHUP은 SIG HANG UP임 

nohup nice 해서 long, big 프로그램을 실행할 때, background에서 실행하게 하면 $ exit을 해도 죽지 않음

$ exit을 하면 SIGHUP이 옴 

모든 프로그램에 SIGHUP이 날라가는데, nohup으로 차단되면 안 먹힘

 

2번 SIGINT는 키보드에서 ^C를 하면 날라가는 시그널

 

6번 SIGABRT는 abort 시스템콜을 사용하면 abort를 실행한 프로세스에게 보내는 거

자기가 스스로 죽고자하는 애임 SIGABRT를 받으면 default action이 termination임 

core는 프로세스가 실행하던 당시의 메모리를 말함, core 실행 당시 변수 값 등을 보고 

이 프로세스가 어떻게 작동했는지, 어떤 error가 발생했는지 등 디버깅 용도로 사용함

 

14번 SIGALRM은 alarm 시스템콜을 사용할 때 초 단위의 시간을 사용해서,

이 시간 경과 후 이 프로세스에게 SIGALRM을 보냄 마찬가지로 default action은 termination임

그 외에 유용한 handler를 사용해서 interrupt 방식을 이용하면 죽지 않음

 

17번 SIGCHLD는 그 child를 termination하는 것 

signal을 받아서 종료가 되는 경우 abnormal termination도 있고, stop signal을 받아서 잠시 중단일 수도 있음

이 때 부모 프로세스에게 SIGCHLD 시그널을 커널이 보내줌

부모가 wait하다가 SIGCHLD를 받으면 child에게 termination이나 stop이 됐다는 것을 알아차리고 맞는 action을  취함

 

18번 SIGCONT

stop signal에는 SIGSTOP과 SIGTSTP가 있는데, 이 시그널을 받으면 프로세스가 잠시 stop 함

그럼 언젠가 다시 continue해야하는데, 그런 목적으로 보내는 시그널임

stop된 프로세스에 보내서 suspend에서 실행

 

8번 SIGFPE는 floating point exception 계산 중 오류가 발생할 때 cpu에서 체크하고 커널에게 보내는 시그널임

 

4번 SIGILL도 하드웨어 시그널. machine instruction이 잘못됐을 때, 

cpu에서 이런 명령은 실행할 수 없다고 error 메시지를 보내면 커널이 SIGILL 시그널을 그 프로세스에 보냄

 

 

SIGINT는 core dump를 하지 않고 SIGQUIT은 core dump를 함

SIGTERM은 kill cmd나 kill system call을 통해서 signal을 지정하지 않으면 default로 가는 시그널이고,

SIGKILL은 SIGTERM을 보냈을 때 죽지 않으면 보냄. kill 시스템콜에 -9 붙이면 됨

 

시그널마다 번호가 있는데 SIGINT는 2번임

$ kill -2 pid
$ kill -INT pid

하면 해당 pid를 가진 프로세스에게 2번 SIGINT를 보내라는 거임

 

11번 SIGSEGV 메모리 참조 오류가 발생 했을 때 사용

c 프로그램에서 포인터를 잘못 사용하면 발생함

danggling reference 참조하는 곳에 할당된 변수가 없는 것임

 

19번 SIGSTOP20번 SIGTSTP는 비슷한데,

SIGTSTP를 보내면 받은 프로세스는 잠시 중단하고, ^Z로 보낼 수 있음

foreground에다가 돌리다가 prompt가 뜨지 않아서 답답해서 background로 돌리고 싶을 때,

^Z로 SIGTSTP를 보내고, 다시 &로 background에 돌리면 처음부터 안돌려도됨

$ bg명령어로 SIGCONT 보내서 background에 돌게 함

 

10번 SIGUSR1 12번 SIGUSR2는 user define하는 시그널임

 

대개가 그 시그널을 받으면 몇가지 제외하고 모두 default action이 termination임

예외는 SIGSTOP, SIGTSTP, SIGUSR, SIGCONT

 

 

Signal life cycle

프로세스에게 어떤 이벤트가 발생했다고 알려주는 SW notification

시그널은 생성, 전달, 도달해서 pending, 프로세스가 catch, install

전달 안되면(pending) 기다리고, 전달되면 프로세스가 반응

catch 하지 않으면 default action을 취하는데, 거의 다 termination임

시그널을 catch 해서 default action 말고 사전에 등록됨 signal handler를 interrupt 방식으로 실행할 수 있음

그래서 이 시그널 핸들러를 등록할 필요가 있음 그게 install, 그 때 사용하는게 sigaction

 

#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));
}

child process가 exit으로 종료하면 정상 종료이고, 시그널을 받거나 다른 이유면 abnormal termination임

parent가 child가 어떻게 죽었는지 상태를 파악할 수 있음

WIFEXITED 매크로를 이용하면 됨 wait(&status)으로 받은 status 정보를 넣어서 exit으로 정상종료인지 알 수 있음

 

비정상 종료이면 signal을 받고 종료했는지를 WIFSIGNALED에서 true가 나오면 그럼

stop 시그널을 받아서 잠시 종료돼도 parent가 깨어나는데, WIFSTOPPED가 true가 됨

WIFEXITED는 child가 exit하고 번호를 보내주고, status에 종료 이유에 대한 정보도 있음

WTERMSIG 어떤 시그널을 받았는지, 시그널 번호가 나옴

WCOREDUMP로 core dump 했는지 안했는지 알 수 있음

 

Signal Handling

시그널 핸들링은 세가지 방법이 있음 

1. ignore action : 아무 action을 하지 않음 

2. default action : 이미 setting된 default action을 수행, 대부분 default action이 termination임 예외 있음

3. user-defined action : 유저가 정해준 action을 취함

 

대부분 default action이 termination이지만 아닌 예외는 SIGSTOP, SIGTSTP, SIGUSR1, SIGUSR2, SIGCONT

SIGUSR의 default action은 ignore action임 

default action을 하지 않기 위해서 시그널을 catch해서 user defined action을 취하도록 하면됨 interrupt handler 방식

이 때도 예외가 있음. SIGKILL과 SIGSTOP은 ignore와 catch 둘다 못함. SIGTSTP는 ignore, catch 모두 가능

 

 

signal handler return 하면 interrupt가 걸린 위치에서 다시 시작함

signal handler에서는 프로세스가 실행한 곳이 어디인지 알수 없음 asynchronous

 

 

process signal mask

어떤 프로세스가 있는데, kill로 SIGTERM을 보내면 default action으로 termination됨

미리 handler를 정의해서 catch 하면 termination안 할 수 있음 그런 경우 $ kill -9 하면 죽음

SIGKILL은 catch를 못해서 무조건 죽어야하는데, 어떤 프로세스는 SIGKILL을 보내도 죽지 않음

 

그 시그널을 block 시킴. 시그널 자체를 선택적으로 block 시킬 수 있음

모든 시그널을 block 시킬 수도 있고, 특정 시그널을 block 할 수 있음 process signal mask 그라고 함

 

signal mask는 block 할 시그널의 목록을 갖고 있음

프로세스당 시그널 메시지가 attribute처럼 갖고 있는 거임

sigprocmask 시스템콜로 block할 signal을 선택적으로 setting하고 특정 signal을 변경시킬 수 있음

fork 해서 child를 만들면 child는 parent의 signal mask를 상속받음

 

The signal(2) system call

#include <signal.h>

void signal (* signal(int signo, void (*func)(int)))(int);
// returns : previous disposition of signal if ok, SIG_ERR on error

 

signal이 generate되고 deliver되면 ignore, default, user defined action을 하는데

SIGTERM을 ignore하려면 ignore하라고 setting해주면 됨

 

sigaction 인자로 signo와 sighandler가 들어감

disposition은 ignore거나 default거나 hanlder거나 모두 function으로 간주됨

이 시스템콜에서 지정된 function에 의해서 셋 중 하나를 선택할 건데, 그 전에 있던 거를 변경한다는 거임

변경된다는 것은, return 값이 그 전에 setting 되어 있던 function의 pointer임 그 시작 위치의 pointer

 

sigfunction은 ISO C에서 지정되어 있긴 하지만, 그 이후에 나온 sigaction이 또 있음

sigaction이 좀 더 자세한 버전이고, 옛날 버전을 위해서 signal을 남겨놓음

 

*func 함수 포인터에 대한 것은 SIG_IGN, SIG_DFL 이렇게 정의되어 있음 

아니면 handler는 실행할 함수의 포인터를 넣으면 됨

 

 

$ ./a.out &       start process in background
[1]      7216     Job-control shell prints job number and process ID

$ kill -USR1 7216    send it SIGUSR1
received SIGUSR1
$ kill -USR2 7216   
send it SIGUSR2
received SIGUSR2
$ kill 7216         
now send it SIGTERM
[1]+  Terminated    ./a.out

 

void fatal(char* str) {
  perror(str);
  exit(1);
}
int main(void) {
    if (signal(SIGUSR1, sig_usr) == SIG_ERR)
        fatal("can't catch SIGUSR1");
    if (signal(SIGUSR2, sig_usr) == SIG_ERR)
        fatal("can't catch SIGUSR2");
    for ( ; ; )
        pause();
}

static void sig_usr(int signo){ /* argument is signal number */
    if (signo == SIGUSR1)
        printf("received SIGUSR1\n");
    else if (signo == SIGUSR2)
        printf("received SIGUSR2\n");
    else {
        printf("received signal %d\n", signo);
        exit(1);
    }
}

 

$ ./a.out
step 1 ^C
in SIGINT handler()
step 2 ^C
in SIGINT handler()
step 3 ^C
in SIGINT handler()
step 4 ^C
in SIGINT handler()
#include <signal.h>
void sig_int(int signo){
   printf("in SIGINT handler()\n");
}

int main(){
   signal(SIGINT, sig_int);
   printf("step 1\n");
   pause();
   printf("step 2\n");
   pause();
   printf("step 3\n");
   pause();
   printf("step 4\n");
   pause();
}

 

signal block

시그널을 block 시킬 수 있는데 자동으로 시그널을 외부에서 procsigmask로 강제적으로 바꿀 수 있음

자동으로 block되는 signal을 setting하는 것도 있음

handler들어가기 전에 만들고 풀어주는 메커니즘이 있음

 

$ ./a.out

 1 pause() ^C

in SIGINT handler() ^C^C^C^C

in SIGINT handler()

 2 pause() ^C

in SIGINT handler() ^C^C^C

in SIGINT handler()

$
#include <signal.h>

void sig_int(int signo)
{
   printf("in SIGINT handler()\n");
   sleep(5);
}

int main()
{
   signal(SIGINT, sig_int);	//SIGINT로 sig_int 함수 등록함
   printf(" 1 pause()\n");
   pause();
   printf(" 2 pause()\n");
   pause();
}

 

 

시그널에 대해서 setting 되어 있던 status는 아무런 조치를 하지 않았으면 ignore나 default 임

ignore는 SIGUSR1, SIGUSR2의 특수한 경우가 있음

SIGSTOP, SIGTSTP, SIGCONT말고 signal handler를 실행하도록 setting할 수 있음

시그널에 대해 setting 된 정보는 child가 그대로 상속 받음

그 때 child 가 exec하면 실행하는 프로그램이 다 바뀌면서 parent의 handler 정보를 찾을 수 없음

 

# ./signal_exec exprog

^C

SIGINT!! 2

executed program

^C

#

#include <signal.h>	
#include <unistd.h>
/* signal_exec */
int main(int argc, char** argv)
{
   void sig_int(int signo);
   signal(SIGINT, sig_int);
   
   pause();
   execl(argv[1], argv[1], (char*)0);
 
   return 0;
}

void sig_int(int signo){
   printf("SIGINT!! %d\n", signo);
}



// child가 exit하면서 실행하는 프로그램
#include <unistd.h>
/* exprog */
int main()
{
   printf("executed program\n");
   while(1)
      pause();
}

 

 

/* main.c */

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

void sig_usr1(int signo)
{
   printf("BEFORE EXEC : in SIGUSR handler\n");
}
int main(int argc, char** argv)
{
   /*
   argv[1] : exec filename 
   */
   
   signal(SIGINT, SIG_IGN);
   signal(SIGUSR1, sig_usr1);
   printf("BEFORE EXEC : puase()\n");
   pause();
   
   execl(argv[1], argv[1], (char*)0);  
}

 

/* exec.c */
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

int main()
{
   printf("EXEC : START PROGRAM\n");
   printf("EXEC : RUN pause()\n");   
   pause();
}

 

 

Signal Sets

#include <signal.h>

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
// return : 0 if ok, -1 on error

int sigismember(const sigset_t *set, int signo);
// returns : 1 if true, 0 if false, -1 on error

 

sigset_t 는 signal set에 대한 변수, signal set이 64개가 있음

64bits가 있어야 하니, 8 bytes가 필요해서 unsigned long 이 필요할 거임

이런 변수는 pointer에 넣고 여기에 1이 setting이 되어있으면 포함이 됨

1번 위치에 1 setting 했으면 SIGHUP 시그널이 포함되는 거임

전부다 0으로 채우는 게 sigemptyset임 

sigfillset은 모두 1로 채움

sigaddset은 특정 위치에 set할 필요가 있을 때 사용

sigdelset은 특정 위치를 clear해줌 

sigismember는 해당 위치에 1이 setting이 되어있는지 알 수 있는 매크로임

 

 

#include     <signal.h>
#include     <errno.h>

/* <signal.h> usually defines NSIG to include signal number 0 */
#define SIGBAD(signo)   ((signo) <= 0 || (signo) >= NSIG)

int sigaddset(sigset_t *set, int signo){
    if (SIGBAD(signo)) { errno = EINVAL; return(-1); }

    *set |= 1 << (signo - 1);       /* turn bit on */
    return(0);
}

int sigdelset(sigset_t *set, int signo){
    if (SIGBAD(signo)) { errno = EINVAL; return(-1); }

    *set &= ~(1 << (signo - 1));    /* turn bit off */
    return(0);
}

int sigismember(const sigset_t *set, int signo){
     if (SIGBAD(signo)) { errno = EINVAL; return(-1); }

     return((*set & (1 << (signo - 1))) != 0);
}