상세 컨텐츠

본문 제목

UNIX - Process signal mask, signal system call, Signal block, Signal handling & exec, Signal Sets

Computer Science/UNIX

by 2021. 11. 3. 08:57

본문

반응형

Process signal mask

process를 종료시키기 위해 SIGTERM을 보내는데, SIGTERM으로 종료되지 않을 경우 kill -9 명령어를 이용하여 SIGKILL을 보내 프로세스를 종료시킨다. 

하지만 SIGKILL로도 프로세스가 종료되지 않는 경우가 있는데, 이는 signal을 block시키기 때문이다.

 

signal을 받을 때 특정 signal은 받지 않고 block 시킬 수 있다. 이를 process signal mask라고 한다.

 

signal이 생성되었을 때 action은 현재 signal handler 또는 process의 signal mask에 의해서 결정된다. signal mask은 block시킬 signal의 목록을 담고 있다.프로세스의 signal mask대로 signal을 block시키는데, signal mask을 설정하는 역할을 sigprocmask system call이 한다.fork나 exec를 통해 child process를 생성할 경우, signal mask는 상속된다. 

 

signal(2)

#include <signal.h>

void (*signal(int signo, void (*func)(int)))(int);
 
Returns: previous disposition of signal (see following) if OK, SIG_ERR on error

signal의 func 매개변수의 설정에 따라서 signal에 대해 IGNORE / default / SIGHANDLER 중 하나로 설정할 수 있다.성공하면 이전에 설정되어있던 signal func를 반환하고, error시 SIG_ERR을 반환한다.

 

signal의 새로운 버전인 sigaction을 사용하는 것이 좋다.

이전 버전인 signal을 사용한 코드들의 호환성 (backward compatibility)때문에 signal system call은 컴파일 가능하도록 남아있다. 

 

signal system call의 argument는 다음의 두 가지이다.

  • signo : signal 의 이름
  • *func : SIG_IGN, SIG_DFL, signal handler(signal-catching function)
    각각 signal이 왔을 때
    SIG_IGN이 전달되면 ignore
    SIG_DFL이 전달되면 defualt action
    signal handler는 유저가 정의한 function이 실행되도록 한다.
<signal.h>
#define SIG_ERR (void (*)()) -1
#define SIG_DFL (void (*)()) 0
#define SIG_IGN (void (*)()) 1

 

signal에는 void형 반환값을 가진 함수가 매개변수로 필요하다.

example

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

위 코드에서 sig_usr는 사용자 정의 signal handler이다. signal handler에는 매개변수로 signal number만이 필요하다. 

signal handler는 signo값이 SIGUSR1인가 SIGUSR2인가에 따라서 다른 동작을 하고 그 외의 signo를 전달받으면 그냥 종료하는 형태를 보인다.

 

main에서 pause는 signal을 받을 때까지 기다리는 함수이다. 

 

$ ./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

shell동작을 보면, 해당 프로세스의 pid에 kill을 통해 각각의 SIGUSR1, SIGUSR2 signal을 보낸 결과를 확인할 수 있다.

kill 명령어에 signal을 설정하지 않으면 SIGTERM이 전달되고, sig_usr handler의 default action인 exit를 수행한다. 

example

#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 handler는 sig_int이고, 어떤 signal이 들어오더라도 문자열을 출력만 한다.

pause를 통해 signal이 들어오기를 대기하고, ^C(Ctrl + C)를 통해 SIGINT signal을 보낸다. 

SIGINT가 전달되면 프로세스는 sig_int handler를 호출하므로 결과는 다음과 같다.

$ ./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()

 

Signal block

앞서 procsigmask를 통해서 특정 signal에 대해서 block처리를 할 수 있다고 하였다. process signal mask에는 block처리될 signal들의 list가 담겨있다. signal-catching function이 호출되기 전에 프로세스의 signal mask에 함수가 추가되고 function의 동작이 끝나면 signal mask에서 제거된다.

$ ./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);
   printf(" 1 pause()\n");
   pause();
   printf(" 2 pause()\n");
   pause();
}

위 코드와 shell을 보면, pause를 통해 signal을 기다리는 상태에서 ^C를 통해 SIGINT를 전달하여 sig_int handler가 실행된다. sig_int handler에서는 문자열 출력 후 5초간 sleep하게되는데, sleep하는 도중에 ^C를 여러번 호출하여 SIGINT를 여러번 보내게 되면 프로세스는 SIGINT를 받았으므로 sig_int handler가 이미 실행되고 있는 상황에서 또 sig_int를 실행시켜야 하는 상황인 것이다.

 

 때문에 위와 같은 상황을 방지하기 위해서 signal mask를 통해 실행되기 시작한 signal-catching function을 signal mask에 추가하고, 동작이 끝나면 signal mask에서 제외시킨다. 

sig_int을 실행하면서 signal mask에 추가되면 sig_int 동작이 끝날 때까지 이후에 전달되는 ^C로 인한 SIGINT signal은 block된다. 

완전히 모두 block되는 것 아니고 1개는 pending(대기)된다. signal이 여러번 들어왔을 때 최대 1개까지 pending된다. 

따라서 위 결과에서도 sig_int동작이 끝나자마자 한 번 더 SIGINT로 인해 sig_int가 호출되는 것을 확인할 수 있다.

 

Signal handling & exec

프로그램이 실행될 때, 모든 signal에 대한 action은 default 또는 ignore로 설정되어있다. 또는 사용자 정의 handler를 실행하도록 설정할 수도 있다.

 

exec를 통해 child process에서 다른 system call을 호출했을 때, 상속되는 signal handler(signal cathcing function)에 대한 정의를 exec로 실행된 새 프로그램에서는 찾을 수 없다. 따라서 exec는 signal handler를 초기화시킨다.

fork를 통해 생성된 child process는 부모의 메모리 이미지를 copy하고 코드가 같으므로 signal handler를 상속받았을 때 실행할 수 있다.

#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);
}
#include <unistd.h>
/* exprog */
int main()
{
   printf("executed program\n");
   while(1)
      pause();
}

signal_exec를 실행하여 argument로 exprog를 보내면 execl system call을 통해 child process가 exprog가 되어 동작한다.

signal_exec에서는 sig_int함수를 선언하고, signal을 통해 SIGINT에 대해 sig_int를 실행하도록 설정한다. 

# ./signal_exec exprog
^C
SIGINT!! 2
executed program
^C
#

pause에서 대기할 때 ^C를 통해 SIGINT를 보내면 sig_int가 정상적으로 호출된다.

다음 execl을 통해 exprog 프로세스를 실행한 상태에서 ^C를 통해 SIGINT를 전달하면 exprog 에서는 SIGINT에 할당한 sig_int를 찾을 수 없으므로 default 설정인 terminate를 실행하여 프로그램을 종료한다.

 

 

example

다음 코드를 보자

/* 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 : pause()\n");
   pause();
   
   execl(argv[1], argv[1], (char*)0);  
}

main함수를 보자

signal을 통해 SIGINT가 오면 SIG_IGN 즉 ignore하도록 설정하였고, SIGUSR1 signal이 오면 sig_usr1을 실행하도록 설정하였다.

pause를 통해 signal을 main 프로세스에서 받은 후, execl을 실행하여 exec 프로그램을 자식 프로세스가 실행한다.

 

/* 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();
}

exec.c에서는 signal에 대한 정의들이 없으므로 각 signal에 대해 default action이 실행된다.

 

$ gcc main.c –o main
$ gcc exec.c –o exec
$
$ ./main exec &
[1] 1828
BEFORE EXEC : pause()    ignore SIGINT 
$ kill –INT 1828
$ ps
 PID TTY          TIME CMD
1306 pts/1    00:00:00 bash
1828 pts/1    00:00:00 main
1829 pts/1    00:00:00 ps

main을 백그라운드로 실행하고 난 qause상태에서 kill 명령어를 통해 main 프로세스에게 SIGINT를 전달하면 main에서는 SIGINT에 대한 action을 SIG_IGN로 설정했으므로 signal을 ignore한다.

 

$ kill –USR1 1828
BEFORE EXEC : in SIGUSR handler
EXEC : START PROGRAM
EXEC : RUN pause()

$ kill -INT 1828
$ ps
…
$ kill –USR1 1828
$ ps
…

kill 명령어로 SIGUSR1를 전달하면 sig_usr1이 호출된 후 pause 다음 코드인 execl을 통해 exec 프로세스를 실행한다. 

 

exec 프로세스를 실행하여 pause인 상태에서 SIGINT를 전달할 경우 exec.c에는 SIGINT에 대한 설정이 main프로세스와는 달리 default값인 terminate이므로 종료할 것이다.

 

만약 SIGINT가 아니라 SIGUSR1을 전달할 경우, exec.c에는 SIGUSR1에 대한 signal handler가 설정되지 않았으므로 default action인 ignore를 실행하여 signal을 무시할 것이다.

 

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

// All four 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

sigemptyset은 set에 포함된 모든 sigset을 0으로 채운다. 

sigfillset은 set에 포함된 모든 sigset을 1로 채운다. 

sigaddset은 signo에 해당하는 signal을 1로 세팅한다.

sigdelset은 signo에 해당하는 signal을 0으로 세팅한다. 

위 라이브러리들을 이용하여 특정 signal을 set에 포함 혹은 제외시킬 수 있다.

 

sigismember를 통해서 signo에 해당하는 signal이 set에 포함되어있는지 확인할 수 있다. 

example

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

위 예시코드는 signal 라이브러리들의 사용예이다.

반응형

관련글 더보기