본문 바로가기

CS/유닉스프로그래밍

9. 유닉스 시그널 프로세싱 - 2

The sigaction(2) system call

#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);
// returns : 0 if ok, -1 on error

 

sigaction argument에는 signo, act, oact가 있음

act는 action을 어떻게 modify할지, oact는 오래된 버전 

 

struct sigaction 구조체 멤버에는 네 개가 있음

 

struct sigaction{
	void (*sa_handler) (int);
    	sigset_t sa_mask;
        int sa_flags;
        void(*sa_sigaction) (int, siginfo_t *, void *);
}

 

sa_handler : 앞에서 본 handler와 같음 signo가 인자로 필요함

sa_sigaction : 새로운 action, 인자 3개가 필요함 signo와 추가적인 정보

추가적인 정보에는 시그널을 보낸 사람, 언제 보냈는지 등 시그널을 보낸 상황에 대한 추가적인 정보

sa_mask : 시그널의 catch로 설정된 handler에 들어갈 때 똑같은 시그널을 차단하는 역할

추가적으로 block 시킬 시그널들을 setting 해줌, sig_emptyset, sigfillset, sigaddset, sigdelset 사용

sa_flags : 각종 옵션, 이 옵션에 따라서 다양한 액션 취할 수 있음 SA_RESTART, SA_SIGINFO, 등

 

sa_handler와 sa_sigaction 둘 다 handler인데, sa_handler는 인자 1개, sigaction은 인자 3개 필요

두 개를 동시에 사용할 수 없고, 둘 중에 하나만 사용해야 함

어떤 거 사용할지를 sa_flags 옵션에서 알 수 있음

SA_SIGINFO가 setting 되어 있으면 sa_sigaction을 사용하고, 아니면 sa_handler 사용

 

#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);
	// chatchint 함수가 하는 게 없음, signo 들어오면 출력해주는 함수
 printf("CATCHINT: returning\n\n");
}

 

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

 

struct sigaction 선언할 때 static을 안쓰면 이 변수가 초기 값이 setting이 안 되어 있음

act는 bss영역에 잡히는데, 시스템에 따라 다르겠지만 어떤 경우에는 초기 값이지 맘대로 메모리에 잡혀서 불안전

정확하려면 clear해야하는데, static 쓰면 변수가 initialized data segment에 들어감

 

SIG_TERM에 대해서 new action에 null을 넣으면 옛날 action 대신 null로 overwrite해서 default action

act.sa_handler에 SIG_IGN을 설정하고 &act해서 넣어주면 SIG_TERM이 들어왔을 때, 무시하는 거로 세팅됨

옛날 거로 복원시키고 싶으면 &oact로 overwrite

 

/* 프로그램으로부터 우아하게 퇴장(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);

 

^C해서 시그널이 들어오면 default는 terminate인데 abnormal exit 말고 좀 더 graceful 한 exit을 하기 위함

 

Signals and system calls

system call은 kernel level 에서 처리하는 프로세스, os level 임

os level 에서 system call을 처리하는 도 중에 signal이 들어온다면, 처리 중인 시스템콜 완료까지 효과가 없음 무시함

 

예외가 있음

만약 시스템콜 중 slow system call이 실행되고 있을 때, 프로세스가 signal을 catch한다면,

마치 정상적인 user process를 돌고 있던 것처럼 interrupt가 됨

user process에서 interrupt 되면 handler 실행 후 실행 시점에 정확하게 돌아옴

근데 시스템콜 실행 중 interrupt 되면, 다시 돌아올 때 정확하게 중단된 위치로 갈 수 없음. 

그래서 slow system call을 실행하던 중에 catch를 하면 정확하게 못 돌아가서 return error가 됨

errorno는 EINTER

 

slow system call은 영원히 block 될 수 있는 시스템콜, pipe, terminal device IO, network device IO, pause, IPC

disk IO는 아님. fast system call 임.

 

slow system call의 interrupt 문제는 커널 프로세스를 돌다가 인터럽트가 걸린 거라서

핸들러를 수행하고 다시 돌아와서 continue가 아닌, 시스템콜 실패가 됨 그럼 goto again함

read(fd, keyboard, sockt, buf, bufsize)뭐 이런 식인데 소켓 읽으려다가 못 읽었으면 다시 읽어야함

이런 문제를 계산하기 위해서 4.2 BSD에서 automatic restart를 도입함

slow system call 수행 중 인터럽트 발생하면, 인터럽트 서비스 루틴 수행하고, 시스템콜을 자동으로 재시작함

option을 setting 해줘야 자동적으로 재시작해주는 거임. ioctl, read, readv, write, writev, wait, waitpid 이런게 있음

 

slow system call에 대해서 시그널을 catch로 설정하면 그 시스템콜이 인터럽트되고 fail 한다함

그래서 코드로 restart가 아니라 automatic restart option을 제공한다고 했음

struct sigaction의 멤버를 보면 sa_flags에 SA_RESTART를 설정해주면, 그 때 특정 시그널에 대해 설정하는 거임

모든 시그널에 대해서 시스템콜이 다 자동으로 restart되는게 아님

 

Another signal handler of sigaction(2)

void handler (int signo, siginfo_t *info, void *context);

두번째 argument의 siginfo_t 구조체

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

 

 

#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 시그널을 날림 */
   }
}

 

signal handler 가 실행되기 전에 이 시작 위치에서 시그널 mask에 있는 걸 다 block 시킴

나가는 시점에서 다시 해제함, 해제하기 위해서 reset함

시그널 pause하는 중에 pending 되어 있는 거임

똑같은 signal이 들어오면 pending 되어 있는데, 여러 개 pending 되어 있어도 대기는 한 개만 되어있음

 

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

 

sigsetjmp는 현재 프로세스의 sigsetjmp 호출 위치와 이때 signal maskenv에 저장한다. 이때 리턴값은 직접적인 sigsetjmp 호출에 의한 리턴인지 아니면 siglongjmp에 의한 리턴인지에 따라 달라진다.

 1) 직접호출에 의한 리턴: 0을 리턴

 2) siglongjmp에 의한 리턴: siglongjmp의 val

siglongjmpsigsetjmp에서 저장했던 env를 이용해 jump한다.

 

프로그래밍을 하다보면 goto를 해야할 때가 있는데, goto 어떤 label 해서 read하는 경우가 있음

sigsetjmp는 label을 설정하는 거고, siglongjmp는 goto 하는 거

label을 setting 할 때, env가 label이 되는 거고, savemask는 그냥 특정 번호를 쓰면 됨

 

signal handler로 부터 branching 할 때 사용함 

savemask는 0은 아니고, 1로 하면 프로세스의 환경변수의 현재 signal mask를 save하면서 sigsetjmp하는거임

siglongjmp는 env로 goto 하는 거

 

 

 

 

Shell같은 경우 명령처리 시 에러가 발생하면 다시 입력모드로 돌아가야 한다.

이 때 에러가 발생한 명령의 처리는 더 이상 진행하지 말아야 한다.

 

setjump, longjump사용하여 더 이상 에러가 발생한 명령어를 실행하지 않고 입력모드로 점프할 수 있다.

만약 에러 발생에 의한 signal 처리 후 입력 모드로 가기 위해 longjump를 하게 되면

signal handler()로 처리 중인 signal에 대한 handler()처리는 할 수 없게 된다.

 

왜냐하면, signal_handler()진입 시 해당 시그널이 processblock리스트에 추가됨으로 longjump 이후 그 signalsignal_handler()로 처리할 수 없다.

 

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 */

 

canjump = 1;         /* now sigsetjmp() is OK */

 - sigsetjmp가 설정되었음을 의미

 - canjump를 설정하는 이유는 sigsetjump없이 siglongjmp호출을 방지하기 위해서임.

 

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 &

The sigprocmask(2) system call

#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
// returns : 0 if ok, -1 on error

how는 signal mask를 어떻게 변경시킬건지,

set은 무엇을 변경시킬 건지

oset은 old set을 save해 놓는 거임

SIG_BLOCK은 새로 block할 시그널을 추가하는 거

SIG_UNBLOCK은 반대로 기존 지정된 시그널에서 뭘 뺄지 지정함

SIG_SETMASK는 기존 거를 완전히 clear 하고 set에 지정된 시그널들로만 새로 setting 하라는 말

 

#include <signal.h>

main(){
 sigset_t set1, set2;

 /* 시그널 집합을 완전히 채운다. */
 sigfillset (&set1);

 sigfillset (&set2);
 sigdelset (&set2, SIGINT);
 sigdelset (&set2, SIGQUIT);

 /* 중대하지 않은 코드를 수행 ... */

 /* 봉쇄를 설정한다. */
 sigprocmask(SIG_SETMASK, &set1, NULL);

 /* 극도로 중대한 코드를 수행한다. */

 /* 하나의 봉쇄를 제거한다. */
 sigprocmask(SIG_UNBLOCK, &set2, NULL);

 /* 덜 중대한 코드를 수행한다 ... */

 /* 모든 시그널 봉쇄를 제거한다. */
 sigprocmask(SIG_UNBLOCK, &set1, NULL);
}

sigset_t set1;

.
.

/* 시그널 집합을 완전히 채운다. */
sigfillset(&set1);

/* 봉쇄(block)를 설정한다. */
sigprocmask(SIG_SETMASK, &set1, NULL);

/* 극도로 중요한 코드를 수행한다 ... */

/* 시그널 봉쇄를 제거한다 */
sigprocmask(SIG_UNBLOCK, &set1, NULL);

 


sending signals

시그널을 보내는 방법은 여러가지가 있음

^C를 하면 foreground에 SIG_INT를 보내 줌

유저 프로세스가 다른 프로세스에게 시그널을 보낼 수도 있음

cmd 레벨에서도 kill이라는 cmd로 pid를 지정해서 보낼 수 있음

kill(2) and raise(2) system call

#include <signal.h>

int kill(pid_t pid, int signo);
int raise(int signo);
//return : 0 if ok, -1 on error

 

raise는 자기 자신한테 보내는 거고, kill은 pid를 지정해서 보내는 거임

pid 값의 종류에 따라서 다양한 버전이 있음

 

pid가 0보다 크면 특정 프로세스를 가리키는거,

0 이면 보내는 프로세스와 같은 그룹에 있는 프로세스들 모두에게 보냄

-1이 아닌 음수면, 그 수의 절댓값과 같은 그룹 아이디를 가진 프로세스에게 보냄

-1인 경우는 send permission을 가진 모든 프로세스에게 보냄

 

sender 또는 receiver의 euid와 real uid가 같아야만 receiver에게 보낼 수 있음

raise는 자기 자신한테 보내는 거임, 그래서 kill(getpid(),)와 같음

 

#include <unistd.h>
#include <signal.h>

int ntimes = 0;

main(){
  pid_t pid, ppid;
  void p_action (int), c_action (int);
  static struct sigaction pact, cact;

  /* 부모를 위해 SIGUSR1 행동을 지정한다*/
  pact.sa_handler = p_action;
  sigaction (SIGUSR1, &pact, NULL);

  switch (pid = fork()){
    case -1;		/* 오류 */
      perror ("synchro");
      exit (1);
      
    case 0: 		/* 자식 */
      /* 자식을 위해 행동을 지정 */
      cact.sa_handler = c_action;
      sigaction (SIGUSR1, &cact, NULL);
      ppid = getppid();  /* 부모의 프로세스 식별번호를 얻음 */
      for (;;){
        sleep (1);
        kill (ppid, SIGUSR1);
        pause();
      } /* 결코 퇴장(exit) 않음. */
      
    default: 		/*  부모 */
      for(;;) {
        pause();
        sleep (1);
        kill (pid, SIGUSR1);
      } /* 결코 퇴장(exit) 않음 */
  }
}

void p_action (int sig){
  printf("Parent caught signal #%d\n",++ntimes);
}

void c_action (int sig){
  printf("Child caught signal #%d\n",++ntimes);
}

 

parent process가 있고, child process가 있는데, 각각 pause하는 동안에 기다리고 있다가
parent도 sigusr1이라는 시그널 보내고 child도 sigusr1을 번갈아 보내는 거임

child 가 먼저 보냈다가, parent가 pause에 들어가 있다가 child가 sigusr 을 보내서 parent가 깨어남
child가 pause 되어있다가 parent 가 sigusr 을 보내고 pause에 들어가 있고 child가 sigusr 보내고 이런게 번갈아감


main 이 있고 pid는 child의 pid이고 ppid는 parent pid임 p_action은 interrupt handler of parent
c_action은 child 의 interrupt handler임 
pact는 parent가 sigaction할때 두번째 argument로 들어가는 거임
pact.sa_handler에 p_action을 넣어줌 sigaction 해서 sigusr 을 보내면 p_action을 하는거임.

default가 ignore인데 catch해서 p_action을 하라는 소리임. 


case0은 child, default는 parent
child가 sigusr을 받으면 default는 ignore 였는데 catch 해서 c_action 함. 

 

NULL signal

signal number가 원래 1부터 있는데 0번은 특수목적으로 남겨놓음

그게 null signal 이고, signal 역할을 하지 않음

posix안에서 null signal을 0번으로 지정해놓음

특정 프로세스에게 kill로 보낼 수는 있는데, 목적이 대상이 된 프로세스가 지금 살아있는지 죽었는지를 확인하는 용도

죽어있고 이미 없으면 -1이 return 됨. ESRCH라는 errno가 setting 되어 있음.

그 프로세스가 살아있다면 0 이 return 됨.

프로세스의 existence를 test하기 위한 용도로 사용한 거인데 이 행동이 atomic 하지 않음

 

The alarm(2) system call

#include <unistd.h>
unsigned int alarm (unsigned int seconds);
// returns : 0 or number of seconds until previously set alarm

return 값은 0이거나 그전에 settng된 alarm의 시간이 됨.

default action이 terminate임. 

 

alarm은 10초 뒤에 받기로 했는데 1초가 경과되면 9초가 남았을 거임 여기서 또 alarm(5)을 또하면 

그 alarm의 return 이 9가 되고 이전의 값이 clear 되고 타이머가 5로 setting 됨 1초가 경과하고 alarm(6)이라고 하면
alarm(6)의 return 값이 4가 되고, 타이머는 6으로 setting 됨, 5는 cancel 됨


4초 경과 후 2초 남았을 때 alarm(0)하면 return 값은 2가 되고, 0이라고 곧 바로 시그널을 받지 않음
0은 0초뒤에 곧바로 시그널을 받는 다는 말이 아니고, alarm clock setting 된 시간을 cancel 하고, signal을 받지 않음

 

alarm(0)은 cancel 한다는 의미
alarm 하기 전에 handler를 install 해야함. 보통 default인 terminate를 하기 위해서 사용하지는 않기 때문임

 

#include <stdio.h>
#include <signal.h>

#define TIMEOUT    5 /* 초단위 */
#define MAXTRIES   5
#define LINESIZE   100
#define CTRL_G	'\007' /*벨*/
#define TRUE       1
#define FALSE      0

/*타임아웃이 발생했는지 알아보기 위해 사용*/
static int timed_out;

/* 입력줄을 보관한다. */
static char answer[LINESIZE];	

char *quickreply (char *prompt){
  void catch (int);
  int ntries;
  static struct sigaction act, oact;

  /* SIGALRM을 포착, 과거 행동을 저장*/
  act.sa_handler = catch;
  sigaction (SIGALRM, &act, &oact);
  for (ntries=0;ntries<MAXTRIES;ntries++)  {
    timed_out = FALSE;
    printf ("\n%s > ", prompt);

    alarm (TIMEOUT); /* 얼람 시계를 설정*/
    gets (answer); /* 입력줄을 가져온다*/
    alarm (0); /* 얼람을 끈다. */

    /* timed_out이 참이면,응답 없는 경우*/
    if (!timed_out) break;
  }

  /*과거행동 복원*/
  sigaction (SIGALRM, &oact, NULL);

  /* 적절한 값을 복귀*/
  return (ntries==MAXTRIES ? ((char *)0):answer);
}
 /* SIGALRM을 받으면 수행한다. */
void catch (int sig) {
  timed_out = TRUE; /*타임아웃 플래그 설정*/
  putchar (CTRL_G); /* 벨을 울린다. */
}

 

프로그램 실행하는데, prompt를 출력하고, user가 input하고 enter하면 받아가지고 일정한 timeout시간 동안
여기서 timeout을 5초로 지정했는데, 5초 이내에 입력을 해야함. 

5초 이내 입력을 하지 않으면, 여기서 control G (terminal 에서 삑소리나는거임) 

 

max try가 5번임. input이 다섯번 출력되고 마지막에 5초 이내 입력안하면 삑소리나고 끝남

main 에서 quickreply를 부름. 

try라는 변수있고, sigaction (act, oact) act의 handler가 catch임 timeout 변수를 true로 바꿈
putchar(CTRL_G) 삑 소리나는 거임. 

 

The pause(2) system call

#include <unistd.h>
int pause(void);
//returns : -1 with errno set to EINTR

아무것도 안하는 slow systemcall임 suspend 하는 거임

signal catch 해서 핸들러를 실행하고, 핸들러 돌아오면 실패되고 다음걸 실행함

근데 catch 안하면 무조건 죽는거임. 
실패하고 return 함.