IT 그리고 정보보안/Knowledge base

리눅스 환경의 시그널(signal)

plummmm 2021. 4. 14. 14:04
반응형

시그널에 대해 알아보자. 이미 여러번 시그널에 대한 얘기를 했지만 이번에 이게 뭔지 제대로 알아 보자.

시그널은 소프트웨어(S/W) 인터럽트이다.

 

하드웨어 인터럽트도 있겠지? 그건 H/W 장치의 입출력에 대한 인터럽트이고,

시그널은 소프트웨어의 상황에 따른 인터럽트로, 비동기적인(asynchronous) 이벤트에 대한 핸들링을 하는 것이다.

예를 들어, 사용자가 프로그램을 멈추거나 종료시키면 발생하는 "메세지"정도로 보면 되겠다.

 

(실제로 windows 환경에서는 signal 을 message 라고 표현함)

 

비동기적 이벤트란, 예측이 불가능 한 이벤트를 말한다.

(동기적(synchronous) 이벤트는 scanf가 실행됐을 때 입력을 받아야  block이 풀리는 것 같이 예측 가능한 상황을 말함)

 

시그널의 이름들은 모두 SIG___ 으로 시작하며, 각자 시그널 넘버를 갖고 있다.

자주 사용되는 시그널들은 밑에서 알아보기로 하겠다.

 

먼저 시그널이 왔을 때 처리하는 방식이 기본적으로 3가지가 있다.

1. 신호를 무시함

- 말그대로 신호를 무시하는 것이다. 단, SIGKILL, SIGSTOP 이 두가지 신호는 무시할 수 없다.

   커널은 항상 프로세스를 죽일 수 있어야 하므로.

 

2. 신호를 캐치함.. 잡음

- 신호를 캐치하여 핸들링하는 것이다. 이 경우에는 해당 신호 발생 시에 호출될 함수를 커널에게 알려줘야 한다.

   signal 함수 등으로 설정 가능하다. 나중에 설명함.

 

3. default 설정이 실행되도록 함

- 모든 신호들은 각자 신호가 발생했을 시에 취하는 기본 액션이 있다. 그 설정 내용대로 처리하는 것.

 

 

그럼 자주 사용되는 시그널들에 대해 알아보자.

먼저 종료에 관련된 시그널들을 봅시다.

* SIGHUP  : 제어 터미널에서 연결 종료된 것이 발견되면 세션 리더에게 전달되는 시그널이다.

* SIGINT : ​[Ctrl + C] 쳤을 때 발생하는 신호, foreground 프로세스 그룹의 모든 프로세스를 종료시킨다.

* SIGQUIT : ​[Ctrl + \] 쳤을 때 발생하는 신호, SIGINT와 같지만 종료하면서 core덤프를 뜬다.

* SIGABRT : ​비정상 종료시에 발생하는 신호, abort() 함수에 의해 발생한다.

* SIGKILL : ​root가 어떤 프로세스라도 확실히 죽일 수 있도록 만든 신호이다. 무시 불가능 

* SIGTERM : ​kill(1) 명령이 기본적으로 전송하는 종료 신호

* SIGCHLD : ​프로세스가 종료되었을 때 부모 프로세스에게 전송되는 신호, 부모가 wait 계열 함수로 받는 신호.

 

다음은 서스펜딩하거나 재개시키는 시그널에 대해 알아보자.

(서스펜드(suspend)는 foreground에서 background로 옮길 때 건다.)

* SIGCONT : ​block된 프로세스에게 실행 재개를 알리는 신호, block상태가 아니라면 무시됨.

* SIGSTOP : ​프로세스를 block 시키는 신호, 무시할 수 없다.

* SIGTSTP : ​[Ctrl + z] 를 쳤을 때 발생하는 신호,  foreground 프로세스 그룹의 모든 프로세스를 일시중지 시킨다.

* SIGTTIN : ​background 프로세스가 읽기를 시도할 때 발생하는 신호, 일시중지 된다.

* SIGTTOU : background 프로세스가 쓰기를 시도할 때 발생하는 신호, 일시중지 된다.

 

 

세번째는 물리적 환경에서 유도된 시그널이다.

* SIGILL : ​잘못된 하드웨어 명령어에 의해 발생하는 신호, 종료된다.

* SIGTRAP : ​브레이크 포인트를 걸었을 때 발생하는 신호, 종료되며 코어덤프뜸.

* SIGBUS : ​bus 에러에 관련된 신호, 종료됨

* SIGFPE : ​부동 소수점 예외 발생시 생기는 오류다. 예를 들어, 0/1 같은 연산이 발생할 때 종료됨.

* SIGSEGV : ​Segment fault 가 발생하면 생기는 신호, 종료되며 코어덤프 뜸

 

 

그리고 유저 입맛대로 시그널을 만들 수도 있다. 만들어 쓰는 경우는 거의 없긴하지만,

SIGUSR1, SIGUSR2 이런 친구들을 이용해 사용자 정의 시그널도 사용가능하다. 종료된다.

 

마지막으로 파이프에 관련된 시그널이 있다.

* SIGPIPE : ​PIPE에서 ​출력 프로세스 쓰기를 시도하는데 입력 프로세스가 종료된 상태일 때 발생. 종료된다.

 

기본적인 시그널에 대한 핸들러 default 값이 있다고 앞에서 얘기했다.

이걸 우리가 마음대로 핸들링할 수는 없는 것인가?

왜 없겠는가. signal() 이라는 함수를 이용하면 가능하다.

 

인자로 signo와 *func라는 함수 포인터를 받아온다.

signo에는 어떠한 시그널을 핸들링할 것인가에 대한 내용이고,

*func에는 signo를 핸들링할 핸들러 즉, 핸들링 함수에 대한 인자이다.

 

리턴값은 희한하게 signal() 호출 이전에 핸들러가 저장된다.

예제코드를 보면 이해할 수 있다.

 

 

간단한 코드이다. myhandler 라는 핸들러 함수를 선언하여

SIGQUIT, SIGTSTP, SIGTERM, SIGUSR1 시그널이 발생했을 때

그냥 표준출력하고 종료하는 것이다.

실행해보면 아래와 같이 나온다.

 

 

이번엔 kill과 raise 함수에 대해 알아보자.

먼저 kill()을 알아보자. kill 함수는 해당 pid에 시그널을 보내는 함수이다.

kill 이라고해서 무작정 죽이는게 함수가 아니고,

signo의 시그널을 전달하는 것이다. kill 명령어와 구분해야 한다.

 

다음은 raise함수이다. 간단하게 설명해 rasie(signo) = kill(getpid(), signo) 랑 같은 말이다.

자기 자신에게 시그널을 보내는 함수가 rasie() 함수이다.

 

자기자신에게 시그널을 날림. 

rasie(signo) = kill(getpid(), signo)  ​이거만 좀 기억하면 될 듯.

 

다음...

여러가지 시그널들을 한 데 모아서 시그널 집합(signal set) 으로 관리하는 구조체가 있다.

네트워크 프로그래밍 포스팅 중 select 함수의 FD 구조체와 비슷하다고 볼 수 있다.

여기에도 signal set을 조작하는 여러가지 함수들이 존재한다.

signal sets을 조작하는 함수들은 위에 설명과 같고,

저렇게 sigset을 조작하여 그것을 인자로 사용하는 함수들이 있다. sigprocmask() 와 sigpending() 이다.

 

먼저 sigprocmask() 함수부터 알아보자.

 

sigprocmask() 함수는 현재 프로세스에 오는 signal을 BLOCK 시키는 함수이다.

how 인자에 셋팅하고 위의 표의 내용대로 비트를 컨트롤한다. set이 NULL이면 how가 무시되고 현재 비트 그대로 호출된다. sigpending()는 별로 중요한게 아니라 간단하게 넘어간다.

int sigpending(sigset_t *set); 

현재 프로세스의 막혀있는 신호들을 *set에 저장하는 함수이다.

 

 

이번에는 signal() 의 기능 확장 함수인 sigaction()에 대해 알아보자.

sigaction 함수는 인자로 sigaction 구조체 포인터를 쓴다.

그 말인 즉, sigaction 구조체가 존재한다는 말..

 

구조체 멤버들에 대해 알아보자.

* sa_handler : 시그널을 처리하기 위한 핸들러. SIG_DFL, SIG_IGN 또는 핸들러 함수

* sa_mask : 시그널이 처리될 동안 BLOCK 되어야하는 시그널 값

* sa_flags : 옵션값들

* sa_sigaction : 안씀

 

자 그럼 sigaction 함수에 대해 알아보자. 

리턴 값과 인자는 위와 같이 쓴다. 예제코드를 한번 보자.

 

catchint() 라는 SIGINT 핸들러 함수를 두고 sigaction 함수를 이용해 SIGINT가 발생하면

catchint가 작동되도록 한다. sigfilset()로 모든 비트에 마스킹을 건다.

실행하면 아래와 같이..

 

이번에는 SIGALRM 시그널을 생성하는 alarm() 함수를 알아보자.

 

쉽게 말해 alarm() 함수의 인자로 시간을 설정하면, 설정한 시간이 만료되면 SIGALRM 시그널이 발생한다.

반환값으로, 만약 이전에 셋팅된 알람시간이 alarm() 함수를 호출했을 때 만료되지 않았다면!

남은 시간이 반환되고 타이머는 새로 설정한 시간으로 셋팅된다.

 

더이상 설명할게 없다. 예제코드를 보자.

 

/* header file */

int alarm_flag = FALSE;

 

/* signal handler */

void setflag (int sig){

  alarm_flag = TRUE;

}

 

int main (int argc, char **argv) {

  int nsecs, j;

  pid_t pid;

  

  static struct sigaction act;

  if (argc <=2){

    fprintf (stderr, "Usage: ./a.out #seconds message\n");

    exit (1);

  }

 

  if ((nsecs = atoi(argv[1])) <= 0){

    fprintf(stderr, “invalid time\n");

    exit (2);

  }

  switch (pid = fork()){

    case 0: /* child */

      break;

    default: /* parent */

      printf ("child process id %d\n", pid);

    exit (0);

  }

 

  act.sa_handler = setflag;

  sigaction(SIGALRM, &act, NULL);

  alarm(nsecs);

  pause();

 

  if (alarm_flag == TRUE){

    printf ("Alarmed!\t");

    for (j = 2; j < argc; j++)

      printf ("%s", argv[j]);

      printf ("\n");

    }

  exit (0);

}

 

괜히 알람에 대한 기능만 보여주면 되는데 코드를 쓸데 없이 꼬아놓은 느낌;;

결국 하는일은 SIGARLM 시그널을 받으면 표준출력하는 프로그램이다.

argv[1]로 알람 타이머, argv[2]로 문자열을 받아 위와 같이 출력한다.

3번 라인에서 프롬프트부터 뜬 이유는 부모가 먼저 종료되었기 때문에.

 

 

마지막으로 시그널 관련 함수 3가지를 알아보자.

pause, abort, sleep 함수이다.

pause 함수는 신호가 전달될 때까지 무한정으로 sleep에 들어가는 함수이다.

alarm() 함수가 호출 되고 시그널 핸들러가 수행되기 전에 SIGALRM이 발생하면 프로세스가 종료되기 때문에

pause 함수로 일시정지 하는 것이다.

abort함수는 프로그램을 비정상적으로 종료시키는 함수이다.

호출한 프로세스에게 SIGABRT를 날린다.

리턴 값이 없는 함수이다.


인자로 지정한 시간만큼 프로세스를 잠재우는 함수이다.

지정한 시간만큼 잘 잤으면 0을 반환하고, 시그널을 받아 종료되면 남은 시간을 반환한다.

반응형