signal은 중첩이 될 수 있다.
현재 signal을 받고 이 프로세스가 다시 context switch가 되어서 다시 돌아와서 수행되는 시점에 커널이 이 프로세스에게 전달이 되어 있는 시그널이 있는지 확인한다. 그래서 내가 받아야 하는 시그널들이 와 있는지 확인하고 핸들러를 수행해준다.
User next를 수행하기 전에 Handler를 수행한다. 그런데 이 Handler가 길어서 수행하다가 돌아와서 보니 다른 signal이 와 있는 것을 확인할 수 있다. 그럼 이것도 수행해주고 다시 돌아온다.
그래서 이러한 방식으로 signal이 중첩이 될 수 있다. 이것을 보고 nested signal handler이라고 부른다.
만약에 이 코드 안에 global 전역변수가 있다면 handler 들이 다 사용하고 바꿀 수 있다. 그러면 handler가 의도하지 않게 전역변수를 바꾸어서 전역변수가 바뀐 것을 이전 handler는 인식할 수 없다. 그럼 여기에서 발생하는 문제는 concurrency문제이다.
그래서 개발자가 concurrency 문제를 해결해주지 않으면 큰일 날 수 있다.
커널이 pending signal들을 blocking 할 수 있다. 만약 SIGINT 핸들러는 다른 SIGINT signal을 받았을 때 수행하지 않도록 blocking할 수 있다. 이런것을 보고 implicit blocking mechanism이라고 부른다.
명시적으로 어떤 signal을 blocking, unblocking 하겠다고 하는 메커니즘이다. 이것을 위해 sigprocmask를 사용한다.
sigint signal을 받지 않겠다는 코드를 짜겠다고 하자
sigset_t mask, prev_mask;
Sigemptyset(&mask); // blocking하고자 하는 signal을 모두 masking
Sigaddset(&mask, SIGINT); //해당하는 signal을 mask해서 blocking 하겠다.
/* Block SIGINT and save previous blocked set */
Sigprocmask(SIG_BLOCK, &mask, &prev_mask); // Sigprocmask를 호출해서 mask를 blocking하는데 이 함수 이전에 있던 signal mask를 잠시 저장하고(prev_mask) mask로 setting
.
./* Code region that will not be interrupted by SIGINT */
. //이때부터는 SIGINT는 모두 blocking 가능
/* Restore previous blocked set, unblocking SIGINT */
Sigprocmask(SIG_SETMASK, &prev_mask, NULL);
//다시 예전것으로 복구해준다. 그리고 sigint는 unblocking
safe한 signal handling이란 main programe과 concurrent하게 수행되기 때문에 굉장히 tricky하다. 프로세스가 global 변수를 참조할 수 있기 때문에 전역변수는 바뀔 수 있고 다른 프로세스가 이것을 확인할 수 없다.
handler는 최대한 간단하게 작성하자
printf
, sprintf
, malloc
, and exit
are not safe!async-signal-safe한 함수만 호출하자
entry와 exit시 global 변수는 임시변수에 저장하고 나중에 복원을 하는 방법을 권장한다.
만약 공유자료구조를 접근한다면 다른 signal을 blocking해서 corruption을 방지
global 변수를 volatile로 생성 -> 컴파일러가 머신코드 생성시 변수들이 레지스터에 생성되는 것을 막는다. 레지스터가 아닌 메모리에 저장을 하게 된다.
글로벌 flag 선언시 sig_atomic_t 타입으로 선언해라. 그러면 이 타입으로 선언된 변수는 데이터가 읽혀지거나 쓰여질때 atomic하게 읽히거나 쓰여진다. 그래서 변수를 쓰는 중간에 쪼개질 수 없게 막는다.
함수가 async-signal-safety하다는 것은 시그널을 받았을 때 함수가 중간에 쪼개질 수 없다는 것을 이야기 한다.
_exit, write, wait, waitpid, sleep, kill
printf는 부모, 자식 프로세스안에서 사용시 lock을 가지고 있다. 만약 printf가 실행중에 context-switch가 발생하는 것이다. signal handler를 수행중에 printf를 수행하려고 하지만 이미 부모 프로세스가 lock을 하고 있어서 자식 프로세스가 실행하지 못하고 있어서 기다리게 된다. 이것을 보고 Deadlock이라고 부른다.
그래서 117개의 async-signal-safe함수를 사용해서 예기치 않은 버그가 발생하지 않도록 한다.
예를 들어서 write와 printf이다. write는 async-signal-safe하기 때문에 printf 함수를 async-signal-safe함수로 바꾸어 수행가능하다.
ssize_t sio_puts(char s[]) /* Put string * /
ssize_t sio_putl(long v) /* Put long * /
void sio_error(char s[]) /* Put msg & exit * /
void sigint_handler(int sig) /* Safe SIGINT handler */
{
Sio_puts("So you think you can stop the bomb with ctrl-c, do you?\n");
sleep(2);
Sio_puts("Well..."); //여기가 원래 printf였으면 async하게 바꾸었다.
sleep(1);
Sio_puts("OK. :-)\n"); //여기가 원래 printf였으면 async하게 바꾸었다.
_exit(0);
} sigintsafe.c
fork14는 5개의 프로세스를 띄우는 것이다.
Signal(SIGCHLD, child_handler)을 동록했다. child process가 종료되면 SIGCHLD를 보내게 되고 이 signal을 child_handler가 처리해준다.
child_handler
wait함수를 통해서 child process를 reaping해준다.
child_handler는 async-saft-handler가 출력해준다. 그리고 errno를 미리 olderrno에 저장해 놓아서 나중에 errno가 바뀌는 것을 방지해준다.
그런데 child processer은 단 두개만 reaping되고 나머지는 hanging하고 있다. 왜냐하면 이미 handling을 하고 있는 프로세스가 있다면 후에 중간에 오는 signal은 받지 못한다.
그래서 signal handling함수가 두번만 호출이 된 것이다.
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;
}
그래서 위의 문제를 해결하기 위해 handler에 while문을 추가한다. while문으로 좀비프로세스가 있는 만큼 wait해서 다른 좀비들을 여러번 체크해서 ccount를 declined한다.
old system과 같은 경우 signal을 받아 handler가 처리하고 default로 돌아가는 문제점이 있다.
어떤 시스템에서는 interrupt 처리중에 signal을 받아서 돌아올때 error을 발생하는 경우가 존재한다.
read 시스템 콜을 발생하다가 시스템이 그냥 abort해서 error을 발생하는 경우가 존재한다. 이때는 프로그래머가 시스템 콜을 다시 보내는 식으로 문제를 해결할 수 있다.
그래서 이러한 문제점을 해결하기 위해 sigaction을 사용하게 된다.
race error은 굉장히 이상한 synchronization error가 발생하는 경우가 생길 수 있다. 동기화 문제로 error가 발생하는 것이다.
main과 handler가 공유하는 자원이 job queue이다. fork를 할때 Execve를 통해 날짜를 화면에 찍어준다. fork를 하면서 띄어주면서 job queue에 집어넣어준다.
handler는 child process가 끝나게 되면 child process가 main에 메세지를 보낸다. 그럼 handler는 job queue에서 dequeue 한다.
여기에서 한가지 문제가 있는데 코드를 보면
addjob을 할때 signal을 받지 않기 위해 Sigfillset으로 모든 signal을 masking해서 아무것도 안받겠다고 했다.
이후 Sigprocmask(SIG_SETMASK, &prev_all, NULL)로 다시 signal을 받는다. 이게 handler에서도 동일하게 작성되어 있다.
그런데 이게 문제점이 있는것이 addjob과 deletejob중 어떤것이 먼저 실행될지는 아무도 알 수 없다. 그래서 어쩌다 보니 addjob이 deletejob 수행후 발생하며 프로세스가 죽었음에도 불구하고 job queue에는 addjob이 들어와 있을 수가 있다.
waitpid로 기다리다가 deletejob으로 자식 프로세스를 지운다.
그래서 위의 문제를 해결하기 위해서 Sigemptyset으로 마스크를 하나더 만들고 SIGCHLD만 처리한다.
fork이전에 Sigprocmask로 마스킹으로 SIGCHLD를 blocking한다.
그 이후에 addjob을 한다.
waitpid를 사용해서 명시적으로 pid라는 것을 volatile로 선언하고 return 값을 가지고 pid의 값을 확인하는 방법이 있다.
Explicitly Waiting for Signals
foreground로 실행되다가 끝난경우 while(!pid)이면 계속 기다리고 어떤 값이 넘어오면 그때가서 확인할 수 있게 하는 방법을 사용한다.
그런데 이렇게 하면 cpu cycle을 너무 많이 쓴다.
pause() 혹은 sleep(1)을 쓴다.
sigprocmask(SIG_BLOCK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);
시그널을 안받고 기다렸다가 마스크를 푸는 형태이다.
prev가 이전의 signal들에 대한 information을 복원해준다.
그래서 while(!pid) 부분 덕분에 이미 SIGCHLD를 받는 경우를 막을 수 있게 된다.