출처: https://3months.tistory.com/307 [Deep Play]

4-1/시스템프로그래밍

Signal

코딩하는 랄뚜기 2022. 3. 28. 11:20

Signal

Signal은 Process가 받는 message이다.

 

 

 

Process는 kernel, 다른 process 그리고 자기 자신에게서 Signal을 받을 수 있다.

 

Signal 종류

 


 

Signal Concepts : Sending a Signal

Kernel은 Signal을 destination process로 context switch 할 때 전달해 준다.

 

Kernel이 signal을 보내는 이유

  1. Kernel이 divide-by-zero(SIGFPE) 또는 termination of a child process(SIGCHLD)를 해야 할 때
  2. Process가 다른 process를 kill 하고 싶을 때

 


 

Signal Concept : Receiving a Signal

Process가 Signal을 받았을 때 하는 행동은 3가지가 있다.

  1. Ignore the signal
  2. Terminate the process (with optional core dump)
  3. Cath the signal by executing a user-level function called signal handler

Signal handler가 작동하는 원리는 asynchronous exception이 발생했을 때와 유사하다.

 

 


 

Signal Concept : Pending and Blocked Signals

 

Signal에서 매우 중요한 개념인 pending과 block이다.

 

Signal이 보내졌지만 process에 도착하지 않은 경우를 pending이라고 한다.

만약 K라는 타입의 signal이 이미 pending 되었다면 다음에 오는 K 타입의 signal은 무시된다.

따라서 pending signal은 타입별로 최대 한 번만 받을 수 있다.

 

Process는 특정 signal을 block 할 수 있는데 block 된 signal은 unblock 될 때까지 process에게 보내질 수 없다.

 

Kernel은 각각의 process에게 pending 그리고 blocked bit vector를 제공한다.

 

Pending bit vector에 k의 signal이 왔다고 표시가 되었더라도 blocked bit vector에 k가 blocked 된 signal이라고 표시가 되어있으면 해당 signal은 receive 될 수 없다.( pnb = pending & ~blocked)

 

sigprocmask 함수를 이용해서 block, unblock을 할 수 있다.

 

 


 

Sending Signals : Process Groups

 

 

process의 id는 개별의 process id(pid)와 그룹 process id(pgid)가 있다.

어떤 process가 그룹의 root process라면 pid=gpid이다.

 

getpgrp() : 현재 process의 gpid를 return

setpgid() : 현재 process의 gpid를 바꾼다.

 


 

Signals Handler as Concurrent Flows

 

 

 

Process에는 각각 signal handler 가 있다.

Context swith가 일어난 다음 process가 실행될 때 process가 하려는 작업을 바로 하는 것이 아니라 signal handler가 먼저 실행되어 signal을 확인하고 처리를 해준 뒤에 작업을 실행한다.

 


 

Blocking and Unblocking Signals

 

 

Signal handler가 signal을 처리하는 도중에 interrupt 발생 후, 다시 process를 실행할 때 signal handler가 다시 실행되는 상황이 생긴다. 이런 경우 중복된 명령이 조건을 무시하고 실행될 수 있기 때문에 blocking과 unblocking을 사용하여 update 된 상황에서 signal이 처리될 수 있도록 해야 한다.

 

Implict block이 돼있는 signal들은 컴퓨터가 자체적으로 중복 signal이 실행되는 것을 막아준다.

 

Explicit blocking이란 사용자가 코드를 사용하여 직접 signal을 blocking 하는 것이다.

 

위 코드는 SIGINT에 대한 signal을 잠깐 block 했다가 SIGINT를 처리한 후에 signal을 unblock 해주는 코드이다.

이렇게 일시적으로 signal을 blocking해주면 해당 signal은 중복 실행되지 않으므로 update 된 상황에서 signal이 실행될 수 있다.

 

Explict blocking에 사용되는 function

  • sigprocmask - 사용자가 정의한 block signal을 적용
  • sigemptyset - empty set을 만든다.
  • sigfillset - 모든 signal을 set에 넣는다.
  • sigaddset - 특정한 signal을 set에 넣는다.
  • sigdelset - 특정한 signal을 set에서 지운다.

 

 


 

Safe Signal Handling

 

Guidelines for Writing Safe Handlers

  1. Keep your handlers as simple as possible
  2. Call only async-signal-safe function in your handlers
  3. Save and restore errno on entry and exit
  4. Protect accesses to shared data structures by temporarily blocking all signals
  5. Declare global variables as volatile
  6. Declare global flags as volatile sig_atomic_t

async-signal-safe fuction은 Hardware인 도움을 받아 non-interruptible하게 실행 가능한 fuction이다.

_exit, write, wait, waitpid, sleep, kill 등이 있다.

 

int ccount = 0;
int N=10;

void child_handler(int sig){
    int olderrno = errno;
    pid_t pid;
    if((pid=wait(NULL))<0) sio_error("wait error");
    ccount--;
    sio_puts("Handler reaped child");
    sio_putl((long)pid);
    sio_puts("\n");
    sleep(1);
    errno=olderrno;
}

int main(){
    pid_t pid[N];
    int i;
    ccount = N;
    signal(SIGCHLD,child_handler);
    for(int i=0;i<N;i++){
        if((pid[i]=fork())==0){
            sleep(1);
            exit(0);
        }
    }
    while(ccount>0) ;
    
	return 0;
}

 

위 코드는 child process가 종료될 때마다 ccount값을 빼고 parent process는 ccount==0이 될 때까지 while문을 도는 코드이다.

 

이 코드를 실행시키면 두 개의 child process만 종료되고 while문을 빠져나오지 못하게 된다.

이유는 pending 되는 signal이 한 개 밖에 없기 때문이다.

 

void child_handler2(int sig){
    int olderrno = errno;
    pid_t pid;
    while((pid=wait(NULL))>0){
        ccount--;
        sio_puts("Handler reaped child");
        sio_putl((long)pid);
        sio_puts("\n");
        
    }
    if(errno!=ECHILD) Sio_error("wait error");
    errno=olderrno;
}

코드를 위 처럼 바꿔주면 한 개의 signal이 와도 종료된 child process를 모두 kill 하기 때문에 잘 작동하게 된다.

 

 


 

Synchronizing Flows to Avoid Races

 

RaceCritical Section(임계 영역)을 한 번에 두 개 이상의 process가 접근하는 것이다.

Race가 발생하지 않게 Synchronizing을 해줘야 한다.

 

void handler(int sig){
    int dolerrno = errno;
    sigset_t mask_all, prev_all;
    pid_t pid;
    
    Sigfillset(&mask_all);
    while((pid=waitpid(-1,NULL,0))>0){
        Sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
        deletejob(pid);
        Sigprocmask(SIG_SETMASK,&prev_all,NULL);
    }
    if(errno!=ECHILD) Sio_error("waitpid error");
    errno = olderrno;
}


int main(){
    int pid;
    sigset_t mask_all, prev_all;
    
    Sigfillset(&mask_all);
    Signal(SIGCHLD,handler);
    initjobs(); /*Initialize the job list*/
    
    while(1){
        if((pid=fork())==0) execve("/bin/date","date",NULL); /*Child*/
        
        Sigprocmask(SIG_BLOCK,&mask_all,&prev_all); /*Parent*/
        addjob(pid); /*Add the child to the job list*/
        Sigprocmask(SIG_SETMASK,&prev_all,NULL);
    }
    exit(0);
    
    return 0;
}

 

job list 해야 할 일을 넣는 queue 형태의 list이다. job을 넣고 뺄 때 Race가 발생하면 안 된다.

이 코드는 job list에 값을 수정할 때 모든 signal을 block 하여 synchronizing을 하고 있지만 문제가 있다.

Parent process가 항상 child process보다 먼저 실행될 것이라 가정하고 있다.

실제로는 child process가 먼저 실행돼서 아무것도 없는 job list에서 값을 delete 하는 상황이 발생할 수 있다.

 

int main(int argc,char** argv){
    int pid;
    sigset_t mask_all, prev_all, mask_one;
    
    Sigfillset(&mask_all);
    Sigemptyset(&mask_one);
    Sigaddset(&mask_one,SIGCHLD);
    Signal(SIGCHLD,handler);
    initjobs(); /*Initialize the job list*/
    
    while(1){
        Sigprocmask(SIG_BLOCK,&mask_one,&prev_one);
        
        if((pid=fork())==0){
            Sigprocmask(SIG_SETMASK,&prev_one,NULL);
            execve("/bin/date",argv,NULL); /*Child*/
        }
        
        Sigprocmask(SIG_BLOCK,&mask_all,&prev_all); /*Parent*/
        addjob(pid); /*Add the child to the job list*/
        Sigprocmask(SIG_SETMASK,&prev_all,NULL);
    }
    exit(0);
    
    return 0;
}

위 코드에서는 SIGCHLD를 먼저 block 하여 child process가 먼저 실행되더라도 handler가 실행되지 않기 때문에 완벽하게 synchronizing 할 수 있다.

 


 

Explicitly Wating for Signals

 

volatile sig_atomic_t pid;

void sigchld_handler(int s){
    int olderrno = errno;
    pid = wait(-1,NULL,0);
    errno = olderrno;
}

void sigint_handler(int s){}


int main(int argc,char** argv){
    sigset_t mask, prev;
    
    Signal(SIGCHLD, sigchld_handler);
    Signal(SIGINT, sigint_handler);
    Sigemptyset(&mask);
    Sigaddset(&mask,SIGCHLD);
    
    while(1){
        Sigprocmask(SIG_BLOCK,&mask,&prev); /*Block SIGCHLD*/
        
        if(Fork()==0) exit(0); /*Child*/
        
        pid = 0;
        Sigprocmask(SIG_SETMASK, &prev, NULL);
        /*Wait for SIGCHLD to be received (wasteful!)*/
        while(!pid);
        /* Do some work after receiving SIGCHLD*/
        printf(".");
    }
    exit(0);
    
    return 0;
}

 

전역 변수 pid의 타입을 volatile로 하여 synchronizing을 하였다.

처음에 SIGCHLD signal을 block 하고 pid=0이 실행되기 전에 handler가 실행되어 pid 값이 바뀌는 것을 막았다.

Parent process는 while문을 돌며 pid 값이 바뀌면 나오게 된다.

과연 이 코드는 효율적이라고 할 수 있을까??

절대 아니다. Parent process가 while문을 돌며 할당된 시간 동안 쓸데없이 CPU를 사용하고 있기 때문이다.

 

 

CPU 사용을 줄이기 위해 pause()를 사용하게 되면 pause()가 실행되기 직전에 child process가 실행되어 signal을 보내버리는 경우에 signal이 보내진 상태에서 pause()에 들어가기 때문에 영원히 잠들어 버리게 된다.

sleep을 사용하게 되면 너무 느려지게 된다.

 

어떡하면 process가 효율적으로 wait 할 수 있을까?

 

sigsuspend를 사용하면된다.

sigsuspend는 위 코드를 atomic하게 처리해 주는 함수이다.

int main(int argc,char** argv){
    sigset_t mask, prev;
    
    Signal(SIGCHLD, sigchld_handler);
    Signal(SIGINT, sigint_handler);
    Sigemptyset(&mask);
    Sigaddset(&mask,SIGCHLD);
    
    while(1){
        Sigprocmask(SIG_BLOCK,&mask,&prev); /*Block SIGCHLD*/
        
        if(Fork()==0) exit(0); /*Child*/
        
        pid = 0;
        Sigprocmask(SIG_SETMASK, &prev, NULL);
        
        while(!pid)
            Sigsuspend(&prev);
        
        printf(".");
    }
    exit(0);
    
    return 0;
}

'4-1 > 시스템프로그래밍' 카테고리의 다른 글

Metadata, sharing and redirection  (0) 2022.04.04
Unix I/O, RIO package  (0) 2022.04.04
Shell  (0) 2022.03.22
Process Control  (0) 2022.03.15
Processes  (4) 2022.03.10