Signal
Signal 이란 시스템에서 어떤 프로세스에 이벤트가 발생했다고 알려주는 메시지이다.
- Exception과 interrupt와 유사하다.
- 커널이 프로세스한테 보내준다.
- Signal type은 1~30으로 구분할 수 있다.
- Signal에 signal의 id와 도착했다는 것을 알려주는 정보들이 있다.
Signal Concepts :
커널은 대상 프로세스의 컨텍스트에서 일부 상태를 업데이트하여 대상 프로세스에 신호를 전달한다.
신호는 두 가지 이유 중 하나로 전달된다.
Sending a Signal
- 커널이 0으로 나누는 오류 또는 하위 프로세스의 종료와 같은 시스템 이벤트를 감지했을 때
- 프로세스가 kill 함수를 호출하여 커널이 대상 프로세스에 signal을 보내도록 명시적으로 요구했을 때
Receiving a Signal
대상 프로세스는 커널에 의해 signal 전달을 어떤 방식으로 반응하도록 강제되었을 때 signal을 받는다.
- delivered : 대상 프로세스에 메세지를 보냄.
- sent : 메세지에 대해서 프로세스가 어떤 액션을 취하지 않았을 때, 메시지가 delivered 되었을 때.
- received : 프로세스에 메세지가 deliver 되고 액션을 취했을 때.
Receive 했을 때 할 수 있는 행위는 3가지 정도 있다.
- Ignore the signal (do nothing)
- Terminate the process (with optional core dump)
- Catch the signal by executing a user-level function called signal handler
- asynchronous interrupt에 대한 응답으로 하드웨어 exception 핸들러가 호출되는 것과 유사하다.
Pending and Blocked Signals
Signal이 전송되었지만 아직 받지 못한 신호를 Pending Signal이라고 한다.
- 어떤 특정한 타입의 pending signal이 최대 1개 있다.
- 프로세스에 타입 k의 pending signal이 있을 때
그 프로세스의 뒤에 sent되는 k라는 신호는 queue 되지 않고 버려진다.
프로세스가 특정 signal을 block할 수 있다.
- 특정 signal이 전달됐을 때 signal이 unblocked 될 때까지 그 signal을 처리하지 않는다.
Pending and Blocked Bits
커널은 프로세스의 context에서 pending bit vector와 blocked bit vector를 유지한다.
즉, pending bit vector에 pending signal set, blocked bit vector에 blocked signal set을 유지한다.
pending
- k 타입의 signal이 delivered될 때 커널이 pending bit k를 set 한다.
- k 타입의 signal이 received 될 때 커널이 pending bit k를 clear 한다.
blocked
- sigprocmask라는 함수를 사용해서 blocked bit를 set 하고 clear 한다.
- 이러한 과정을 signal mask라고도 한다.
Sending Signals
Process Groups
각 프로세스는 정확히 하나의 프로세스 그룹에 속하며, 프로세스 그룹은 process group ID(pgid)에 의해 식별된다.
#include <unistd.h>
pid_t getpgrp(void);
Returns: process group ID of calling process
getpgrp 함수는 현재 프로세스의 pgid를 반환한다.
기본적으로 하위 프로세스는 상위 프로세스와 동일한 프로세스 그룹에 속한다.
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
Returns: 0 on success, −1 on error
프로세스는 setpgid 함수를 사용하여 자체 프로세스 그룹 또는 다른 프로세스의 프로세스 그룹을 변경할 수 있다.
Sending Signals with the /bin/kill Program
/bin/kill 프로그램은 다른 프로세스에 임의의 신호를 보낸다.
example
자식 프로세스 2개가 생성되었고 pid와 pgid를 확인할 수 있다.
다음에 ps 명령어를 실행하면 fork 된 두 개의 프로세스가 실행 중이다.
다음에 /bin/kill -9 24818 명령어를 실행하면 해당 pid를 가진 프로세스가 종료된다.
kill 명령어로 커널로 하여금 SIGKILL이라는 signal을 pid가 24818인 프로세스에게 보낸다.
내부적으로 pid가 24818인 프로세스의 context안에 있는 pending bit vector에서 SIGKILL에 해당하는 bit를 kernel이 set 해주는 것이다. 이 프로세스가 context switch 될 때 커널한테 SIGKILL이라는 signal이 오면 이 프로세스를 terminate 한다.
프로세스가 속한 프로세스 그룹을 모두 terminate 하려면 pid에 minus를 붙여서 실행한다.
---
명령어는 signal 9(SIGKILL)를 프로세스 24818로 전송한다. PID가 음수이면 pgid의 모든 프로세스로 신호가 전송된다.
Sending Signals from the Keyboard
쉘은 한 줄의 command line을 evaluate 한 결과로 생성된 프로세스를 나타낸다.
최대 1개의 foreground job과 0개 이상의 background job이 있다.
예를 들어 ls | sort라고 입력하면 Pipe에 의해 연결된 2개의 프로세스로 구성된 foreground job이 생성된다.
하나는 ls 프로그램을 실행하고 다른 하나는 sort 프로그램을 실행한다.
쉘은 각 작업에 대해 개별 프로세스 그룹을 생성한다.
일반적으로 pgid는 job의 상위 프로세스 중 하나에서 가져온다.
예로 들자면, 위의 그림은 하나의 foreground job과 두 개의 background job을 가진 쉘이다.
forground job의 상위 프로세스는 PID가 20이고 pgid는 20이다.
부모 프로세스에서는 2개의 자식 프로세스가 생성되어 각각 프로세스 그룹 20의 멤버이다.
Ctrl + C를 입력하면 커널은 foreground 프로세스 그룹의 모든 프로세스에 SIGINT signal을 보낸다.
이 결과, 기본적으로 foreground job은 종료된다.
마찬가지로 Ctrl + Z를 입력하면 커널은 foreground 프로세스 그룹의 모든 프로세스에 SIGTSTP 신호를 보낸다.
이 결과, 기본적으로 foreground job이 일시 정지된다.
- SIGINT - 각 프로세스를 terminate 하는 signal
- SIGTSTP - 각 프로세스를 일시 정지하는 signal
Sending Signals with kill Function
void fork12()
{
pid_t pid[N];
int i;
int child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
/* Child: Infinite Loop */
while(1)
;
}
for (i = 0; i < N; i++) {
printf("Killing process %d\n", pid[i]);
kill(pid[i], SIGINT);
}
for (i = 0; i < N; i++) {
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminated abnormally\n", wpid);
}
}
$ ./forks 12
Killing process 417757
Killing process 417758
Killing process 417759
Killing process 417760
Killing process 417761
Child 417761 terminated abnormally
Child 417760 terminated abnormally
Child 417759 terminated abnormally
Child 417757 terminated abnormally
Child 417758 terminated abnormally
Receiving Signals
커널이 프로세스 p를 커널 모드에서 사용자 모드로 전환할 때 (예를 들어 system call에서 복귀하거나 context switch를 완료하는 경우)는 block 되지 않은 pending signal set (pending & ~blokced)에서 p를 체크한다.
signal을 체크할 때 두 가지를 본다.
pnb(pending bit & ~blocked)의 결과가 1인 bit인 부분에 대해서 action을 취한다.
만약 pub의 결과가 다 0이었다면 다음 명령어를 실행한다.
그렇지 않다면
- pnb에서 0이 아닌 bit k를 선택하고 프로세스 p한테 signal k를 receive 하라고 명령한다.
- signal은 p의 action에 의해 set 된다.
- pnb의 모든 0이 아닌 k에 대해 반복한다.
- 처리가 완료되면 다음 명령어를 실행한다.
Default Actions
각 signal 타입은 다음 중 하나의 default action에 의해 미리 정의되어 있다.
- 프로세스 종료
- 프로세스가 SIGCONT signal에 의해 재시작될 때까지 정지.
- 프로세스가 signal을 무시.
Installing Signal Handlers
프로세스는 signal 함수를 사용하여 signal과 관련된 default action을 변경할 수 있다.
예외로 SIGSTOP과 SIGKILL은 변경할 수 없다.
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
Returns: pointer to previous handler if OK, SIG_ERR on error (does not set errno)
signal 함수는 다음 세 가지 방법 중 signal signum과 관련된 action을 변경할 수 있다.
- SIG_IGN : signum 타입의 signal은 무시된다.
- SIG_DFL : signum 타입의 signal에 대한 action은 default action으로 돌아간다.
- handler는 signal handler라고 불리는 사용자 정의 함수의 주소이다.
이 주소는 프로세스가 signal 타입의 signal을 receive 할 때마다 호출된다.
handler 주소를 signal 함수에 전달하여 default action을 변경하는 것을 hanlder installing라고 한다.
hanlder의 호출은 signal cathing이라고 한다.
handler의 실행을 handling the signal이라고 한다.
Signal Handling Example
#include "csapp.h"
void sigint_handler(int sig) /* SIGINT handler */
{
printf("So you think you can stop the bomb with ctrl-c, do you?\n");
sleep(2);
printf("Well...");
fflush(stdout);
sleep(1);
printf("OK. :-)\n");
exit(0);
}
int main()
{
/* Install the SIGINT handler */
if (signal(SIGINT, sigint_handler) == SIG_ERR)
unix_error("signal error");
/* Wait for the receipt of a signal */
pause();
return 0;
}
main 문에서 signal handler를 설치한다.
SIGINT는 Ctrl + C를 입력하면 전송된다. forground 프로세스에 SIGINT signal을 커널이 보낸다.
만약 이 SIGINT를 받으면 sigint_handler라는 함수를 실행한다는 의미이다.
pause() 하게 되면, foreground에서 command를 기다린다.
sigint_handler를 호출하면서 프로세스가 종료된다.
Signals Handlers as Concurrent Flows
signal handler는 main 프로그램과 동시에 실행되는 별도의 logical flow이다.
signal handler는 프로세스 A에서 핸들러 함수가 실행되지만, 핸들러가 동시에 실행되는 것처럼 보인다.
다른 프로세스를 실행하다가 돌아올 때 signal이 있다고 한다면 돌아온 부분의 다음을 실행하는 게 아니라 핸들러 코드를 실행한다. 그 다음으로 핸들러 코드가 끝난 부분 다음부터 실행한다.
Nested Signal Handlers
signal handler는 중첩이 되면 복잡해질 수 있다.
- 메인 프로그램이 실행되다가 현재 어떤 signal을 받고 프로세스가 다시 context switch 돼서 돌아왔을 때 실행하는 그 시점에 커널이 프로세스에게 전달이 된 signal이 있는지 확인한다. pnb(pending & ~blocked) set을 확인한다.
- 그다음 핸들러를 실행한다. 실행이 길면 context switch 되어 다른 프로세스를 실행하고 다시 돌아올 수 있다.
- 그 시점에 signal이 와있다.
- 현재 핸들러 코드가 아직 끝나지 않은 상태에서 또 다른 핸들러를 실행할 수 있다.
- 핸들러 코드의 실행이 끝나게 되면 S로 돌아온다.
- S의 핸들러가 끝나면 다시 메인으로 돌아온다.
- 다음 명령어를 실행한다.
이런 식으로 핸들러 코드가 중첩이 될 수 있다.
시그널 핸들러는 프로세스가 가지는 하나의 함수이다.
메인 프로그램에서 메인 코드가 있고 핸들러 코드는
개발자가 프로세스 안에서 함수 호출하듯이 정의해놓고 핸들러로 정의해놓았을 뿐이다.
프로그램에서 전역 변수가 있다면 전역 변수를 핸들러 S와 핸들러 T에서 접근할 수 있다.
그렇게 되면 핸들러 S가 전역 변수로 코드를 실행하다가
갑자기 핸들러 T로 넘어가서 전역 변수를 바꾸고
핸들러 S로 돌아오면 핸들러 S는 전역 변수가 바뀌었는지 알 방법이 없다.
전역 변수를 핸들러 S와 핸들러 T가 공유하기 때문에 발생한다. -> concurrency
개발자가 concurrency를 제어해주지 않으면 의도하지 않은 버그가 생길 수 있다.
이런 이유로 Signal을 Block, Unblock 해야 한다.
Blocking and Unblocking Signals
Linux는 signal을 block하기 위한 Implicit 암묵적, explicit 명시적 메카니즘을 제공한다.
Implicit blocking mechanism
기본적으로 커널은 핸들러에 의해 현재 처리되고 있는 타입의 pending signal을 block한다.
예를 들어, 현재 SIGINT signal을 받아서 SIGINT 핸들러가 수행되고 있을 때
또 다른 SIGINT signal에 의해 interrupt되지 않는다.
왜냐하면 signal은 queue가 안되기 때문에 현재 SIGINT signal이 수행되고 있을 때 pending signal을 가질 수 없다.
어떤 타입 s에 대한 signal을 수행하고 있을 때 동일한 타입 signal s가 오게 되면 block된다.
explicit blocking and unblocking mechanism
응용 프로그램은 sigprocmask 함수를 사용하여 어떤 신호를 명시적으로 block 또는 unblock 할 수 있다.
sigprocmask 함수는 현재 block된 signal set을 변경한다.
구체적인 동작은 아래와 같다.
- SIG_BLOCK : signal을 block set에 추가한다. (blocked = blocked | set)
- SIG_UNBLOCK : signal을 block set에서 제거한다. (blocked = blocked & ~set)
- SIG_SETMASK : blocked = set
oldset이 NULL이 아닌 경우 block된 bit vector의 이전 값이 oldset에 저장된다.
signal set은 다음 함수를 사용하여 동작된다.
- sigemptyset : signal의 집합을 배열로 표현하여 배열의 signal을 empty로 만든다.
- sigfillset : 모든 signal을 block한다.
- sigaddset : 전체 signal set 중에 특정한 signal에 해당하는 부분만 block한다.
- sigdelset : block 되어 있는 signal들을 unblock한다.
Temporarily Blocking Signals
blocked
- Sigprocmask라는 함수를 사용해서 blocked bit를 set 하고 clear 한다.
- 이러한 과정을 signal mask라고도 한다.
sigset_t mask, prev_mask;
Sigemptyset(&mask);
Sigaddset(&mask, SIGINT);
/* Block SIGINT and save previous blocked set */
Sigprocmask(SIG_BLOCK, &mask, &prev_mask);
/* Code region that will not be interrupted by SIGINT */
/* Restore previous blocked set, unblocking SIGINT */
Sigprocmask(SIG_SETMASK, &prev_mask, NULL);
임시적으로 signal을 blcok하는 예시이다.
Sigprocmask 가 시작되는 시점 부터 끝날 때까지 코드가 수행하는 동안에 signal을 block한다.
예로 들어, 기본적으로 프로세스에 SIGINT signal(Ctrl + C)이 receive되면 terminate된다.
Sigprocmask 사이에서 코드가 수행될 때 SIGINT signal이 와도 반응을 하지 않는 코드를 작성한다고 가정한다.
즉, SIGINT라는 signal을 받지 않겠다는 코드를 작성한다.
어떻게 구현할까?
sigset_t mask, prev_mask;
sigset_t 타입의 변수를 2개 선언한다.
Sigemptyset(&mask);
Sigaddset(&mask, SIGINT);
mask 변수로 block 하고 싶은 signal들을 모두 maksing한다.
처음에 blocking 하는 signal은 없는의미로 Sigemptyset을 사용해서 signal의 집합을 배열로 표현하여 배열의 signal을 empty로 만든다.
다음으로 Sigaddset(&mask, SIGINT)는 mask 변수에 SIGINT signal을 mask해서 block한다는 의미이다.
Sigprocmask(SIG_BLOCK, &mask, &prev_mask);
다음으로 Sigprocmask 함수를 호출하여 SIG_BLOCK을 이용해 signal을 block set에 추가한다.
masking하기 위한 mask 변수들을 인자로 받아서 이 함수를 호출하기 전에 있었던 signal을 masking하는 set을prev_mask에 저장하고 SIGINT를 block하는 mask 변수를 set한다. (SIGINT signal bit를 1로 set?)
그러면 다음 코드부터 SIGINT signal이 오면 blocking이 가능하다.
코드가 실행되고 끝날 때 prev_mask에 저장되어 있는 signal 들을 복구한다.
Writing Signal Handling
핸들러는 여러 가지 속성을 가지고 있어서 추론하기 어렵다.
- 핸들러는 메인 프로그램과 동시에 실행되며 전역 변수를 공유하기 때문에
메인 프로그램 및 다른 핸들러와 간섭할 수 있다. - signal을 receive하는 방법과 시기에 대한 규칙은 직관적이지 않은 경우가 많다.
- 시스템마다 signal-handling semantic이 다를 수 있다.
signal handling을 할 때 Async signal에 대해서 signal이 safe한지에 대해 다룬다.
signal이 safe한 것은 무엇인가?
Safe Signal Handling
signal 핸들러는 위 그림에서 보듯이 메인 프로그램과 동시에 실행할 수 있기 때문에 까다롭다.
핸들러와 메인 프로그램이 같은 global data 영역에 동시에 접근하는 경우, 결과는 예측할 수 없고, 치명적일 수 있다.
이 파트를 공부하는 목적은 동시에 실행해도 안전한 핸들러를 쓰기 위한 가이드라인을 제공하기 위함이다.
Guidelines for Writing Safe Handlers
- G0 : 핸들러를 가능한 단순하게 작성한다
ex) 전역 변수를 set하고 return - G1 : 핸들러의 async-signal-safe 함수만 호출한다.
printf, sprintf, malloc and exit are not safe! - G2 : entry와 exit할 때 errno를 저장하고 복구한다.
다른 핸들러가 errno을 덮어쓰지 않도록 하기 위함이다. - G3 : 일시적으로 모든 signal을 block함으로 써 공유 데이터 영역에 대한 접근을 보호한다.
데이터의 손상을 방지한다. - G4 : 전역 변수를 volatile로 선언한다.
컴파일러가 전역 변수를 레지스터에 저장하는 것을 방지한다. - G5 : global flag를 vloitile sig_atomic_t로 선언한다.
flag : 읽기 또는 쓰기 전용 변수
이러한 방식으로 선언된 flag는 다른 global처럼 보호될 필요가 없다.
Async-Signal-Safety
어떤 함수가 async-signal-safe 하다는 것은 signal에 의해 entry에 다시 접근하거나 interrupt될 수 없는 함수를 말한다.
signal을 받았을 때 함수가 중간에 쪼개질 수 없다는 것이다. 비동기적으로 signal이 발생했을 때도 안전하다.
즉, 공유 변수를 의도와 다르게 변경하지 않게 해주는 함수들을 async-signal-safe하다.
예를 들어서, printf 함수는 async-signal-safe하지 않다.
signal 핸들러안에서 printf 함수를 사용한다고 가정한다.
printf 함수는 내부적으로 lock을 가지고 있다.
lock을 잡은 뒤 실행하는 코드는 lock이 해제될 때까지 다른 프로세스가 이 코드에 접근할 수 없다.
메인 함수에서 printf를 호출하고 실행하면서 lock을 잡고 터미널에 출력하고 printf는 lock을 풀어준다.
문제는 printf가 async-signal-safe하지 않기 때문에 lock을 잡고 터미널에 출력하는 도중에
context switch가 일어나면 다른 프로세스를 실행하고 그 사이에 signal이 프로세스에 도착하고
printf를 실행하던 프로세스에 도착하면 signal handler를 수행하는 그 순간 printf 라이브러리 안에서
lock을 잡아야 하는데 메인 함수에서 lock을 잡고 있기 때문에 signal 핸들러 함수에서는 lock을 잡을 수 없다.
signal 핸들러 함수가 printf를 통해 화면에 출력할 수 없는 상황이다.
메인 함수가 lock을 풀어 주기를 기다려야 하고, 메인 함수 입장에서 핸들러가 끝나지 않았기 때문에
다음 코드를 실행할 수 없는 상황. 이것을 dead lock이라 한다.
교착 상태라고 일컫는 dead lock은 동시에 실행되고 있는 두개의 메인 함수와 signal 핸들러 함수가 교착 상태에 도달하여 서로 진행할 수 없는 상태이다. printf 함수는 async-signal-safe하다는 특성을 가지고 있기 때문에 사용하는 것을 권장하지 않는다.
이런 경우를 해결하기 위해 Posix에서 117개의 함수를 async-signal-safe 함수라고 정의하고 있다.
async-signal-safe 함수를 직접 사용하여 예상치 못한 상황이 발생하지 않도록 코드를 구현할 수 있다.
- async-signal-safe
- _exit, write, wait, waitpid, sleep, kill
- not async-signal-safe
- printf, sprintf, malloc, exit
write 함수가 async-signal-safe 함수이기 때문에 printf를 async-signal-safe로 바꿔 실행할 수 있다.
Safely Generating Formatted Output
ssize_t sio_puts(char s[]) /* Put string */
ssize_t sio_putl(long v) /* Put long */
void sio_error(char s[]) /* Put msg & exit */
/* Put string */
ssize_t sio_puts(char s[])
{
return write(STDOUT_FILENO, s, sio_strlen(s));
}
/* Put error message and exit */
void sio_error(char s[])
{
sio_puts(s);
_exit(1);
}
async-signal-safe 함수인 write 함수, _exit 함수를 이용하여
위와 같이 문자열을 출력하는 puts함수를 wrapper 함수로 만들어 사용하면 dead lock이 발생하지 않는다.
Correct Signal Handling
signal의 직관적이지 않은 측면 중 하나는 pending signal이 queue되지 않는다는 것이다.
왜냐하면 pending bit vector가 각 signal 타입 별로 한 개의 bit만 포함하기 때문에,
특정 타입의 pending signal을 최대 1개만 포함할 수 있다.
따라서 만약 대상 프로세스가 signal k에 대한 핸들러를 실행 중일 때 signal k는 block되는 동안,
대상 프로세스에 signal k 타입의 signal이 2개 sent되면 두번째 signal은 queue되지 않고 버려진다.
중요한 것은 pending인 signal의 존재는 적어도 1개의 signal이 도착했음을 나타낸다.
이것이 정확성에 어떻게 영향을 미치는지 알아보려면 쉘이나 웹 서버와 같은 실제 프로그램과 유사한 응용프로그램을 봐야 한다.
기본 구조는 부모 프로세스가 잠시 독립적으로 실행된 후 종료되는 자식 프로세스를 생성하는 것이다.
부모 프로세스는 좀비가 시스템에 남지 않도록 자식 프로세스를 reaping해야 한다.
하지만, 우리는 자식 프로세스가 실행되는 동안 부모 프로세스도 다른 작업을 하길 원한다.
그래서 명시적으로 자식 프로세스가 종료되기를 기다리는 대신 SIGCHLD 핸들러를 사용하여 자식 프로세스를 reaping하기로 결정한다.
flawed Ex
/* WARNING: This code is buggy! */
void handler1(int sig)
{
int olderrno = errno;
if ((waitpid(-1, NULL, 0)) < 0)
sio_error("waitpid error");
Sio_puts("Handler reaped child\n");
Sleep(1);
errno = olderrno;
}
int main()
{
int i, n;
char buf[MAXBUF];
if (signal(SIGCHLD, handler1) == SIG_ERR)
unix_error("signal error");
/* Parent creates children */
for (i = 0; i < 3; i++)
{
if (Fork() == 0)
{
printf("Hello from child %d\n", (int)getpid());
exit(0);
}
}
/* Parent waits for terminal input and then processes it */
if ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0)
unix_error("read");
printf("Parent processing input\n");
while (1)
;
exit(0);
}
부모 프로세스는 SIGCHLD 핸들러를 install하고 3개의 자식 프로세스를 생성한다.
한편, 부모 프로세스는 터미널로부터 입력을 기다렸다가 무한 루프를 처리한다.
각 자식 프로세스가 종료되면 커널은 SIGCHLD signal을 전송하여 부모 프로세스에게 알린다.
부모 프로세스는 SIGCHLD를 capture하여 1개의 자식 프로세스를 reap하고 추가로 reap하고 반환한다.
이 예시는 직관적이고 잘 작동하는 것처럼 보이지만 실제로 실행하면 다음과 같은 결과가 나올 수 있다.
linux> ./signal1
Hello from child 14073
Hello from child 14074
Hello from child 14075
Handler reaped child
Handler reaped child
결과에서 우리는 SIGCHLD signal 3개가 부모 프로세스에게 전송되었지만, 두개의 signal만 receive된 것을 확인할 수 있다. 따라서 부모 프로세스는 두개의 자식 프로세스만 reap한 것이다.
부모 프로세스를 일시 정지하면 실제로 자식 프로세스 14075는 reaping되지 않고 좀비상태로 남아있음을 알 수 있다.
Ctrl+Z
Suspended
linux> ps t
PID TTY STAT TIME COMMAND
.
.
.
14072 pts/3 T 0:02 ./signal1
14075 pts/3 Z 0:00 [signal1] <defunct>
14076 pts/3 R+ 0:00 ps t
무엇이 잘못된 것일까? 문제는 이 코드가 signal이 queue되지 않는다는 사실을 설명하지 못했다는 것이다.
첫 번째 signal은 부모 프로세스에 의해 receive되어 caputre된다. 핸들러가 첫 번째 signal을 처리하는 동안 두 번째 signal이 전달되어 pending인 signal set에 추가된다. 단, SIGCHLD signal은 SIGCHLD 핸들러에 의해 block되므로 두 번째 signal은 receive되지 않는다. 그 직후 핸들러가 첫 번째 signal을 처리하는 동안 세 번째 signal이 도착한다.
pending 중인 SIGCHLD가 이미 존재하기 때문에 세 번째 SIGCHLD signal은 버려진다. 잠시 후 핸들러가 복귀한 후 커널은 pending 중인 SIGCHLD signal이 있음을 알아차리고 부모에게 강제로 signal을 receive시킨다. 부모 프로세스는 signal을 caputre하고 핸들러를 두 번째로 실행한다. 두 번째 signal 처리가 완료되면 세 번째 SIGCHLD에 대한 모든 정보는 손실되었기 때문에 pending 중인 SIGCHLD signal은 더 이상 존재하지 않는다.
중요한 교훈은 signal을 사용하여 다른 프로세스의 이벤트 발생을 계산할 수 없다는 것이다.
Improved Ex
void handler2(int sig)
{
int olderrno = errno;
while (waitpid(-1, NULL, 0) > 0)
{
Sio_puts("Handler reaped child\n");
}
if (errno != ECHILD)
Sio_error("waitpid error");
Sleep(1);
errno = olderrno;
}
pending signal이 존재한다는 것은 프로세스가 해당 타입의 signal을 마지막으로 receive한 이후로 적어도 한 개의 signal이 전달되었다는 것이다.
따라서 SIGCHLD 핸들러가 호출될 때마다 가능한 많은 좀비 프로세스를 reaping하도록 SIGCHLD 핸들러를 변경해야 한다.
linux> ./signal2
Hello from child 15237
Hello from child 15238
Hello from child 15239
Handler reaped child
Handler reaped child
Handler reaped child
반복문으로 wait을 호출하여 모든 자식 프로세스가 종료되도록 한다.
Portable Signal Handling
어떤 버전의 Unix 시스템에서는 signal handling이 다르게 동작할 수 있다.
- 일부 오래된 시스템에서는 signal k가 핸들러에 의해 포착된 후 signal k의 액션을 기본값으로 되돌린다.
이러한 시스템에서는 핸들러가 실행될 때마다 signal을 호출하여 핸들러를 명시적으로 재설치해야한다. - system call이 중단될 수 있다.
read, wait, accept 등의 system call이 오랜 시간 동안 프로세스를 block할 가능성이 있는 경우 slow system call이라 한다. 일부 오래된 버전의 Unix에서는 핸들러가 signal을 감지했을 때 interrupt되는 slow system call은 signal 핸들러가 복귀했을 때 재개되지 않는다. 대신 에러 조건과 errno을 EINTR로 설정하여 사용자에게 즉시 반환한다.
이러한 시스템에서 프로그래머는 중단된 system call을 수동으로 재시작하는 코드를 포함해야 한다. - 어떤 시스템에선 handle되고 있는 타입의 signal을 blcok하지 않는다.
Sigaction
이러한 문제에 대처하기 위해 Posix 규격은 사용자가 핸들러를 설치할 때 원하는 signal을 처리하는 semaincs를 명확하게 지정할 수 있는 sigaction 함수를 정의한다.
handler_t *Signal(int signum, handler_t *handler)
{
struct sigaction action, old_action;
action.sa_handler = handler;
sigemptyset(&action.sa_mask); /* Block sigs of type being handled */
action.sa_flags = SA_RESTART; /* Restart syscalls if possible */
if (sigaction(signum, &action, &old_action) < 0)
unix_error("Signal error");
return (old_action.sa_handler);
}
#include <signal.h>
int sigaction(int signum, struct sigaction *act,
struct sigaction *oldact);
Returns: 0 if OK, −1 on error
sigaction 함수는 사용자가 복잡한 구조를 설정해야 하기 때문에 다루기 어렵다.
그래서 *Signal이라는 wrapper 함수를 정의하였다. *Signal 함수는 signal 함수와 같은 방식으로 동작한다.
*Signal wrapper 함수에는 다음 signal hadnling semantics를 가진 signal handler가 설치된다.
- 핸들러가 현재 처리 중인 유형의 signal만 blcok한다.
- 모든 signal 구현과 마찬가지로 signal은 queue되지 않는다.
- 중단된 system call은 가능한 자동으로 재시작된다.
- signal hadnler가 설치되면 SIG_IGN 또는 SIG_DFL 중 하나의 핸들러 인수를 사용하여 signal이 호출될 때 까지설치된 상태로 유지된다.
Synchronizing Flows to Avoid Races
race condition이란 동기화 문제가 발생하는 경우다. 부모와 자식 사이에 동기화 문제가 발생한다.
어떤 이유로 에러가 발생하는지 예제를 통해 알아본다.
/* WARNING: This code is buggy! */
void handler(int sig)
{
int olderrno = errno;
sigset_t mask_all, prev_all;
pid_t pid;
Sigfillset(&mask_all);
while ((pid = waitpid(-1, NULL, 0)) > 0)
{ /* Reap a zombie child */
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
deletejob(pid); /* Delete the child from the job list */
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
if (errno != ECHILD)
Sio_error("waitpid error");
errno = olderrno;
}
int main(int argc, char **argv)
{
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)
{ /* Child process */
Execve("/bin/date", argv, NULL);
}
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all); /* Parent process */
addjob(pid); /* Add the child to the job list */
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
exit(0);
}
핸들러는 자식 프로세스가 종료되면 SIGCHLD signal을 부모 프로세스가 받는 기능을 수행한다.
메인 함수와 핸들러 함수가 공유하고 있는 자료구조가 있다.
이 자료구조는 job queue로 main에서 fork하면서 해야하는 job은 data인데, 그 job을 fork하면서 job queue에 넣는다. 핸들러 함수는 자식 프로세스가 fork하고 execve하여 끝나게되면 자식 프로세스가 메인 함수한테 signal을 보낸다.
부모 프로세스가 fork하여 자식 프로세스가 생성되고 자식 프로세스가 끝나게되면 부모 프로세스한테 SIGCHLD를 보낸다. SIGCHLD를 받으면 메인 함수에서 핸들러를 실행한다. 이 핸들러 함수는 job queue에서 job을 delete한다.
핸들러가 delete(dequeue), 메인 함수가 insert하는 구조이다.
fork()를 하고 addjob(pid)로 queue에 자식 프로세스를 추가한다.
이 코드는 문제가 있다.
fork를 하고 addjob을 할 때 signal을 받으면 안된다.
그래서 mask_all 변수를 통해서 signal(SIGCHLD, handler) SIGCHLD를 등록하고 Sigfillset(&mask_all)을 호출하여 mask_all이라는 변수는 모든 signal을 masking하여 signal을 받지 않겠다고 Sigprocmask함수를 사용했다.
그 뒤에 핸들러에서 mas_all이라는 변수에 모든 signal을 masking하고 Sigprocmask함수를 사용했다.
자식 프로세스를 fork하여 실행하고 부모 프로세스를 실행하는데 둘 다 job queue를 공유하고 있는데
메인 함수는 addjob을 하고 핸들러는 deletejob을 한다.
결국 addjob과 deletejob이 누가 먼저 실행되는지 control할 수 없다.
메인 함수에서 fork를 하여 자식 프로세스가 먼저 실행되고 끝나서 Sigprocmask가 호출되기 직전에 signal을 받았다고 가정한다.
masking하기 이전에 signal을 받게되고 핸들러를 수행하게 되면 핸들러에서 delete할 job이 없다.
그래서 delete하지 않고 return하고 메인 함수가 실행되면 그때 자식 프로세스가 queue에 추가된다.
원래 job을 추가하고 삭제해야한다.
그런데 위 예시는 자식 프로세스가 terminate되었음에도 queue에 job이 아직 있는 상황이 발생한다.
이 문제를 해결하기 위해 아래와 같이 수정한다.
Corrected Shell Program without Race
void handler(int sig)
{
int olderrno = errno;
sigset_t mask_all, prev_all;
pid_t pid;
Sigfillset(&mask_all);
while ((pid = waitpid(-1, NULL, 0)) > 0)
{ /* Reap a zombie child */
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
deletejob(pid); /* Delete the child from the job list */
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
if (errno != ECHILD)
Sio_error("waitpid error");
errno = olderrno;
}
int main(int argc, char **argv)
{
int pid;
sigset_t mask_all, mask_one, prev_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); /* Block SIGCHLD */
if ((pid = Fork()) == 0)
{ /* Child process */
Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
Execve("/bin/date", argv, NULL);
}
Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* Parent process */
addjob(pid); /* Add the child to the job list */
Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
}
exit(0);
}
메인 함수에서 mask_one라는 변수를 하나 더 만들었다.
Sigaddset(&mask_one, SIGCHLD) -> SIGCHLD은 mask_one이 masking한다.
fork를 하기 전에 Sigprocmask 함수를 사용해서 SIGCHLD를 block한다.
다음에 fork하여 실행하고 실행이 끝나고 Sigprocmask함수가 호출되기 전에 signal을 수행하지 않는다.
이렇게 하면 job이 남아있는 상황을 방지할 수 있다.
Explicitly Waiting for Signals
쉘에서 foreground, background 모두 처리해야하는데 signal을 통해서 구현하다 보니까 명시적으로 프로세스가 terminate되었다는 것을 확인할 수 있는 방법이 있어야 한다.
volatile sig_atomic_t pid;
void sigchld_handler(int s)
{
int olderrno = errno;
pid = waitpid(-1, NULL, 0);
errno = olderrno;
}
pid를 atomic하게 선언하고 waitpid를 사용해서 pid를 확인할 수 있는 방법이 있다.
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) /* Child */
exit(0);
/* Parent */
pid = 0;
Sigprocmask(SIG_SETMASK, &prev, NULL); /* Unblock SIGCHLD */
/* Wait for SIGCHLD to be received (wasteful) */
while (!pid)
;
/* Do some work after receiving SIGCHLD */
printf(".");
}
exit(0);
}
fork하고 while loop에서 pid가 0이 아니라면 계속 기다리게 할 수 있다.
문제는 이런 식으로 while loop을 계속 실행하면 CPU cycle이 낭비된다.
해결 책은 pause(), sleep이다.
while (!pid) /* Race! */
pause();
while (!pid) /* Too slow! */
sleep(1);
pause는 프로세스가 sleep된 상태가 되지만, signal을 받으면 깨어난다.
signal을 받기 전까지 sleep하는 것이다.
sleep은 특정한 시간이 지나면 스스로 깨어난다.
pause()를 깨우게 하는 경우에는 Ctrl + C같은 SIGINT로 인해 깨어나는게 아니라 자식 프로세스가 끝나서 돌아왔을 때 깨어난다. 원래 의도와 상관없이 깨어날 수 있다. 또, race 상황이 발생할 수 있다. pause하기 직전에 SIGCHLD를 받았다고 하면 SIGCHLD 핸들러를 수행한다. 그 다음 pause를 하면 signal이 오지 않은 상황이 발생할 수 있기 때문에 프로세스를 계속 기다리는 상황이 발생할 수 있다.
sleep은 pid를 얼마나 자주 확인할 지에 대해 너무 느릴 수 있다.
Waiting for Signals with sigsuspend
#include <signal.h>
int sigsuspend(const sigset_t *mask);
Returns: −1
sigprocmask(SIG_BLOCK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);
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) /* Child */
exit(0);
/* Wait for SIGCHLD to be received (wasteful) */
pid = 0;
while (!pid)
Sigsuspend(&prev);
Sigprocmask(SIG_SETMASK, &prev, NULL); /* Unblock SIGCHLD */
/* Do some work after receiving SIGCHLD */
printf(".");
}
exit(0);
}
위와 같이 Sigsuspend 함수를 사용하면 SIGCHLD를 받지 않고도 실행할 수 있게 된다.