상세 컨텐츠

본문 제목

UNIX - sigaction system call, sigsetjmp(3) and siglongjmp(3)

Computer Science/UNIX

by 2021. 11. 4. 17:06

본문

반응형

sigaction(2)

#include <signal.h>

int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);

// Returns: 0 if OK, -1 on error

sigaction system call은 특정 signal에 대한 action을 확인하거나 편집하는 역할을 하며, signal function의 최신 버전이다. 

argument

  • signo : signal number
  • act : modifying action
  • oact : previous action

 

act와 oact의 자료형인 sigaction 구조체는 다음과 같다

struct sigaction {
   void (*sa_handler)(int);   /* addr of signal handler, */
                              /* or SIG_IGN, or SIG_DFL */
   sigset_t sa_mask;          /* additional signals to block */
   int sa_flags;              /* signal options, Figure 10.16 */

   /* alternate handler */
   void (*sa_sigaction)(int, siginfo_t *, void *);
};

 

sa_handler는 signal이 들어왔을 때 호출할 signal-catching function

sa_mask는 sigaction가 호출되기전 추가적으로 block할 signal들을 signal mask에 추가한다.

sa_flags는 signal handling을 위한 각종 options이다.

 

sigaction 자료형에는 sa_handler이외에도 추가로 사용할 수있는 handler sa_sigaction이 있는데, 

sa_sigaction의 매개변수에는 signal 전송에 대한 정보가 담긴 siginfo_t을 전달한다.

 

sa_handler와 sa_sigaction는 동시에 사용할 수 없고 이 둘 중 어떤 것을 사용할지를 sa_flags를 통해 설정한다. 

sa_flags를 SA_SIGINFO flag로 세팅할 경우 signal handler로 sa_sigaction을 사용하고,

그렇지 않으면 sa_handler를 사용한다. 

 

example

#include <signal.h>

main() {
   static struct sigaction act; /* 왜 static 키워드를 사용하는가 ? */

   void catchint (int); /* catchint를 선언한다. 후에 핸들러로 사용된다. */

   act.sa_handler = catchint; /* SIGINT를 수신했을 때 취해질 행동을 지정한다. */

   sigfillset(&(act.sa_mask)); /* 완전히 찬 마스크를 하나 생성한다. */

   /* sigaction 호출전에는, SIGINT가 프로세스를 종료시킨다(디폴트 행동) */
   sigaction(SIGINT, &act, NULL);

   /* SIGINT를 수신하면 제어가 catchint로 전달될 것이다 */
   printf ("sleep call #1\n");
   sleep (1);
   printf ("sleep call #2\n");
   sleep (1);
   printf ("sleep call #3\n");
   sleep (1);
   printf ("sleep call #4\n");
   sleep (1);

   printf ("Exiting\n");
   exit (0);
}

/* SIGINT를 처리하는 간단한 함수 */
void catchint (int signo)
{
 printf ("\nCATCHINT: signo=%d\n", signo);

 printf("CATCHINT: returning\n\n");
}

sigaction 구조체 act를 생성하고, handler로 catchint, signal mask를 모두 1로 하여 다른 모든 signal을 block 시키도록 설정한다.

sigaction을 통해 SIGINT signal이 전달되었을 때 act의 handler를 실행하도록 설정한다. 

이후 1초 간격의 sleep이 4번 반복되고 종료된다.

 

실행 결과는 다음과 같다.

$ sigex
sleep call #1
sleep call #2
sleep call #3
sleep call #4
Exiting

아무것도 하지 않으면 sleep이 모두 끝나고 그냥 종료된다.

$ sigex
sleep call #1
<interrupt>	(user process interrupt key)

CATCHINT: signo=2
CATCHINT: returing

sleep call #2
sleep call #3
sleep call #4
Exiting

sleep 도중에 ^C를 통해 SIGINT를 전달하면, catchint function이 실행된다. 

 

sigaction act를 static으로 선언한 이유에 대해서 알아보자

 static으로 선언하지 않으면, act는 bss 메모리 영역에 할당되므로 초기값을 직접 초기화 해야하지만,

static으로 선언하면 initialize data segment에 할당되어 프로그램이 컴파일되는 동시에 할당되므로 초기값이 설정되고 안전하다.

example

#include <signal.h>

static struct sigaction act, oact;

/* SIGTERM을 위한 과거의 행동을 남겨둔다. */
sigaction(SIGTERM, NULL, &oact);

/* SIGTERM을 위한 새로운 행동을 지정한다. */
act.sa_handler = SIG_IGN;
sigaction(SIGTERM, &act, NULL);

/* 여기서 무언가 작업을 수행한다 ... */

/* 이제 과거의 행동을 복원한다. */
sigaction(SIGTERM, &oact, NULL);

 첫 번째 sigaction을 통해서 이전의 sigaction을 oact에 저장하고, 새 action으로 NULL을 전달하여 SIGTERM에 대한 default action으로 설정하였다.

 act.sa_handler에 SIG_IGN을 통해 ignore handler를 설정하고, sigaction으로 SIGTERM에 대한 action을 SIG_IGN으로 설정한다. 

 이전에 설정됬었던 action으로 되돌리기 위해 sigaction에 act 매개변수로 oact를 전달하여 설정한다. 

 

example graceful exit

/* 프로그램으로부터 우아하게 퇴장(exit)한다. */
#include <stdio.h>
#include <stdlib.h>

void g_exit(int s)
{
   unlink ("tempfile");
   fprintf (stderr, "Interrupted -- exiting\n");
   exit (1);
}

이 루틴은 다음과 같이 특정 시그널과 연관될 수 있다:

extern void g_exit(int);
.
.
.
static struct sigaction act;
act.sa_handler = g_exit;
sigaction(SIGINT, &act, NULL);

sa_handler를 g_exit 함수로 설정하고 SIGINT을 전달했을 때 g_exit 함수를 통해 정상종료하도록 설정하는 위와 같은 방법을 graceful exit라고 한다. 

 

Signals and system calls

system call을 실행하는 도중에 프로세스가 signal을 받으면 해당 system call이 완료될 때 까지 signal은 효과가 없다. 

만약 slow system call로 인해서 process가 block되어 있을 때 signal을 catch하게 되면, system call이 interrupted된다.

 

user process에서 signal handler를 실행하게 되는 경우에는 handler 실행 후 중단되었던 위치로 되돌아오지만, slow system call을 실행하던 도중에 interrupted된 경우에는 중단되었던 kernel의 위치로 돌아갈 수 없다. 이때 system call은 error가 발생하고 errno는 EINTR로 설정된다. 

 

slow system call이란 영원히 프로세스가 block 될 수 있는 system call들로 다음과 같은 것들이 있다.

  • pipes, terminal devices(I/O), and network devices(I/O) 
  • pause(), wait()
  • Certain ioctl operations
  • Some of the interprocess communication functions  eg). IPC

disk I/O는 slow system call이 아니다.

 

slow system call에서의 interrupted와 관련한 문제가 있는데, kernel process에서 interrupt가 걸린 것이므로 중단되었던 위치로 돌아가지 못하고 error를 return한다. 

again:
if ((n = read(fd, buf, BUFFSIZE)) < 0) {
   if (errno == EINTR)
      goto again;     /* just an interrupted system call */
   /* handle other errors */
}

위 코드를 보면 read를 통해 장치의 내용을 읽는데 interrupted되었다면 -1이 반환되고, errno가 EINTR이므로 again들 다시 실행하는 loop에 빠지게 된다.

 

때문에 4.2BSD부터 slow system call이 interrupted 된 경우에 slow system call을 자동으로 restart하는 option을 제공한다. 

자동으로 restart되는 system call은 다음과 같다.

  • ioctl, read, readv, write, writev, wait, and waitpid.
  • 위 system all 중 앞의 5개는 slow device에서 작동할 때에만 interrupted 된다.
  • wait and waitpid은 signal이 caught될 때 항상 interrupted 된다.

 

slow system call이 interrupted되었을 때, 자동으로 restart 되기 위한 option설정은 struct sigaction의 sa_flags에 특정 signal에 대해 SA_RESTART 값으로 설정해주면 된다. 

이 경우 해당 signal에 대해서는 자동으로 restart되므로 errno는 설정되지 않는다. 

주의 할 것은 restart option설정은 해준 signal에 대해서만 자동으로 restart되고 그렇지 않은 signal들은 interrupted되었을 때 error를 반환한다는 점이다.

 

Another signal handler of sigaction(2)

siginfo_t의 내용을 통해 signal의 발생 원인을 상세히 알 수 있는데 구조는 다음과 같다.

struct siginfo {
  int    si_signo;  /* signal number */
  int    si_errno;  /* if nonzero, errno value from <errno.h> */
  int    si_code;   /* additional info (depends on signal) */
  pid_t  si_pid;    /* sending process ID */
  uid_t  si_uid;    /* sending process real user ID */
  void  *si_addr;   /* address that caused the fault */
  int    si_status; /* exit value or signal number */
  long   si_band;   /* band number for SIGPOLL */
  /* possibly other fields also */
};

si_pid로 signal을 보낸 process ID를 알 수 있다.

si_uid로 signal을 보낸 user ID를 알 수 있다.

 

#include <signal.h>
void sig_handler(int sig, siginfo_t *siginfo, void* param2){
   printf("[parent:%d] : RECEIVE a signal from child %d!!\n", getpid(), siginfo->si_pid);
}

int main(){
   pid_t pid;
   struct sigaction act;
   int i=0;
   memset(&act, 0, sizeof(act));
   /* act.sa_handler = sig_handler; */
   act.sa_sigaction = sig_handler;  /* 시그널 핸들러 등록(파라메터가 있을때) */
   act.sa_flags = SA_SIGINFO;
   sigfillset(&act.sa_mask);     /* 시그널 집합 초기화 */
   sigaction(SIGUSR1, &act, 0);  /* 사용자정의 시그널 */
   
   i=0;
   while(pid=fork()) {
      printf("[parent:%d] : CREATE child %d\n\n", getpid(), pid);
      if(++i==3) break;
   }

   if( pid> 0){      /* 부모 루틴 */
      getchar();     /* 아무키나 누르면 부모는 종료됨 */
   } else {          /* 자식 루틴 */
      kill(getppid(), SIGUSR1);  /* 부모에게 SIGUSR1 시그널을 날림 */
   }
}

위 코드에서 act는 static으로 선언되지 않았으므로 memset을 통해 초기화한다.

sig_handler를 sigaction의 handler로 설정할 때, sig_handler는 매개변수가 3개이므로 sa_handler가 아닌, sa_sigaction에 대입하여 설정한다. 

또한 sa_sigaction으로 sigaction의 handler를 사용하기 위해 sa_flags를 SA_SIGINFO로 설정한다. 

sigfillset을 통해 handler를 실행하는 도중 다른 어떤 signal도 전달되지 못하도록 sa_mask를 모두 1로 설정한 후

sigaction으로 SIGUSR1을 act로 설정한다. 

 

while문에서 pid가 부모일 경우 0보다 큰 값이 반환되므로 true, 자식일 경우 0이 반환되므로 false이다. 

child process는 kill 을 통해 parent process에게 SIGUSR1을 전달한다. 

parent process는 getchar system call에서 대기하면서 child가 보내는 signal을 받는데, restart option을 설정하지 않았으므로 한 번만 받고 끝난다.

 

Additional information about the signal

signal handler가 실행되기 전에 signal mask(sa_mask)에 있는 signal들을 block시키고, 실행이 끝나면 reset 시킨다. 

signal handler가 실행되는 동안 OS는 signal을 signal mask에 전달시켜 pending시킨다. (block 시킨다)

signal 여러개가 들어오더라도 한 개의 signal만 pending시킨다. 

 

 

sigsetjmp(3) and siglongjmp(3)

#include <setjmp.h>

int sigsetjmp(sigjmp_buf env, int savemask);
 
// Returns: 0 if called directly,
// nonzero if returning from a call to siglongjmp 
 
void siglongjmp(sigjmp_buf env, int val);

goto와 비슷한 개념으로 label을 설정한다.

env가 label이고 sigsetjmp로 env를 설정하고, siglongjmp로 설정한 env 위치로 이동한다. 

 

signal handler로부터 branching 할 때 사용된다. 

savemask가 0이 아니면, 현재 env(환경변수)의 signal mask를 저장하면서 sigsetjmp한다. 

 

main(){
	x func():
}

func(){
	goto x;
}

위와 같이 func에서 main으로 goto하는 경우 c에서는 컴파일이 불가능하다. 하지만 이러한 경우를 sigsetjmp/siglongjmp는 가능하게 한다.

main(){
	if(sigsetjmp(env,1) == 2){
    	...
    }
    else if(sigsetjmp(env,1) == 3){
    	...
    }
    else{ // sigsetjmp return = 0
    }
}

f(){
	siglongjmp(env,2);
}

g(){
	siglongjmp(env,3);
}

위 코드와 같이 siglongjmp를 다른 함수에서 실행해도 main함수에 있는 sigsetjmp의 env로 이동할 수 있다.

또한 sigsetjmp에서 어떤 siglongjmp에서 두 번째 매개변수 val의 값이 sigsetjmp의 반환값으로 전달되므로 위 코드와 같이 sigsetjmp의 return이 2이면 f함수로부터, 3이면 g함수로부터 왔음을 알 수 있다. 

 

example

static void                         sig_usr1(int), sig_alrm(int);
static sigjmp_buf                   jmpbuf;
static volatile sig_atomic_t        canjump;

int main(void){
    if (signal(SIGUSR1, sig_usr1) == SIG_ERR)
        fatal("signal(SIGUSR1) error");
    if (signal(SIGALRM, sig_alrm) == SIG_ERR)
        fatal("signal(SIGALRM) error");
    pr_mask("starting main: ");     /* ppt-p.9 */

    if (sigsetjmp(jmpbuf, 1)) {
        pr_mask("ending main: ");  exit(0);
    }
    canjump = 1;         /* now sigsetjmp() is OK */
    for ( ; ; )  pause();
}
static void sig_usr1(int signo){
    time_t  starttime;
    if (canjump == 0)   return;     /* unexpected signal, ignore */

    pr_mask("starting sig_usr1: ");
    alarm(3);               /* SIGALRM in 3 seconds */
    starttime = time(NULL);
    for ( ; ; )             /* busy wait for 5 seconds */
        if (time(NULL) > starttime + 5)
            break;
    pr_mask("finishing sig_usr1: ");
    canjump = 0;
    siglongjmp(jmpbuf, 1);  /* jump back to main, don't return */
}

main의 sigsetjmp를 처음 실행할 때는 return == 0이므로 if문에 들어가지 못한다. 

signal이 들어와 sig_usr1을 실행하면 마지막 siglongjmp로 인해 sigsetjmp위치로 가서 return == 1로 동작한다. 

void pr_mask(const char *str){
    sigset_t    sigset;
    int         errno_save;

    errno_save = errno;     /* we can be called by signal handlers */
    if (sigprocmask(0, NULL, &sigset) < 0) fatal("sigprocmask error");

    printf("%s", str);
    if (sigismember(&sigset, SIGINT))   printf("SIGINT ");
    if (sigismember(&sigset, SIGQUIT))  printf("SIGQUIT ");
    if (sigismember(&sigset, SIGUSR1))  printf("SIGUSR1 ");
    if (sigismember(&sigset, SIGALRM))  printf("SIGALRM ");

    /* remaining signals can go here */
    printf("\n");
    errno = errno_save;
}

static void sig_alrm(int signo){
    pr_mask("in sig_alrm: ");
}
$ ./a.out &                    start process in background
starting main:
[1]   531                      the job-control shell prints its process ID
$ kill -USR1 531               send the process SIGUSR1
starting sig_usr1: SIGUSR1
$ in sig_alrm: SIGUSR1 SIGALRM
finishing sig_usr1: SIGUSR1
ending main:
                               just press RETURN
[1] + Done          ./a.out &

 

반응형

관련글 더보기