Signal: 두 판 사이의 차이
| (같은 사용자의 중간 판 28개는 보이지 않습니다) | |||
| 1번째 줄: | 1번째 줄: | ||
상위 문서: [[ | 상위 문서: [[Exceptional Control Flow]] | ||
==개요== | ==개요== | ||
'''signal'''이란 시스템에서 어떤 종류의 이벤트가 발생했음을 프로세스에 알리는 작은 | '''signal'''이란 시스템에서 '''어떤 종류의 이벤트가 발생했음을 프로세스에 알리는 작은 메시지'''이다. signal은 이를 위해서 kernel에서 process로 보내지며, signal type은 1~30의 정수로 식별된다. 이때 어떤 signal에 저장된 정보는 해당 ID와 그 signal이 도착했다는 사실 뿐이다. 아래는 몇가지 signal ID와 그에 대응되는 signal들을 나타낸 표이다. | ||
{| class="wikitable" | {| class="wikitable" | ||
|+ | |+ | ||
| 68번째 줄: | 68번째 줄: | ||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
#include <unistd.h> | #include <unistd.h> | ||
pid_t getpgrp(void); | pid_t getpgrp(void); //반환값: 현재 프로세스의 PGID (Process Group ID) | ||
</syntaxhighlight> | </syntaxhighlight> | ||
기본적으로 child process는 parent process와 동일한 프로세스 그룹에 속한다. 이때 프로세스는 '''setpgid()''' 함수를 사용하여 자신이나 다른 프로세스의 프로세스 그룹을 변경할 수 있다. | 기본적으로 child process는 parent process와 동일한 프로세스 그룹에 속한다. 이때 프로세스는 '''setpgid()''' 함수를 사용하여 자신이나 다른 프로세스의 프로세스 그룹을 변경할 수 있다. | ||
| 75번째 줄: | 75번째 줄: | ||
int setpgid(pid_t pid, pid_t pgid); | int setpgid(pid_t pid, pid_t pgid); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
setpgid 함수는 프로세스 | setpgid 함수는 그룹 ID가 pgid인 프로세스 그룹이 존재하는지에 따라 그 실행 방식이 달라진다. | ||
* 그룹 ID가 pgid인 프로세스 그룹이 존재 | |||
** ID가 pid인 프로세스를 해당 프로세스 그룹에 속하도록 함 | |||
* 그룹 ID가 pgid인 프로세스 그룹이 존재 X | |||
** 만약 <code>pid == pgid</code>라면, 그룹 ID가 pgid인 프로세스 그룹을 생성하고, ID가 pid인 프로세스를 해당 프로세스 그룹에 추가한다. | |||
** 만약 <code>pid != pgid</code>라면, 오류가 발생한다. | |||
이때 pid가 0이면 현재 프로세스의 PID가 사용된다. pgid가 0이면, pid로 지정된 프로세스의 PID가 프로세스 그룹 ID로 사용된다. 다음은 PID가 15213인 프로세스에서 setpgid()함수를 호출한 예시이다. | |||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
setpgid(0, 0); | setpgid(0, 0); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
위 함수의 | 위 함수의 실행시 process group ID가 15213인 프로세스 그룹이 존재하지 않는다면 그러한 프로세스 그룹을 만들고 ID가 15213인 프로세스를 해당 그룹에 추가한다. 즉, 호출하는 프로세스가 자신의 PID를 프로세스 그룹 ID로 설정함을 의미하며, 이는 새로운 프로세스 그룹을 생성하는 효과를 갖는다. | ||
===/Bin/kill 프로그램을 사용해 signal 보내기=== | ===/Bin/kill 프로그램을 사용해 signal 보내기=== | ||
/bin/kill 프로그램은 다른 프로세스나 프로세스 그룹에 임의의 신호를 보낸다. | /bin/kill 프로그램은 다른 프로세스나 프로세스 그룹에 임의의 신호를 보낸다. 이는 아래와 같다: | ||
<syntaxhighlight lang="perl"> | <syntaxhighlight lang="perl"> | ||
linux> /bin/kill | linux> /bin/kill sig_ID PID | ||
</syntaxhighlight> | </syntaxhighlight> | ||
위 명령은 process | 위 명령은 process PID에 sig_ID에 해당하는 signal을 보낸다.<ref>/bin/kill 명령어에서 "kill"은 SIGKILL 시그널과 일말의 관련도 없다.</ref> 이때 PID로 음수를 사용하면 해당 process group 15213에 존재하는 모든 프로세스에 해당 signal을 보낸다. 예를 들어 | ||
<syntaxhighlight lang="perl"> | <syntaxhighlight lang="perl"> | ||
linux> /bin/kill - | linux> /bin/kill -2 -15213 | ||
</syntaxhighlight> | </syntaxhighlight> | ||
위 명령은 process group 15213에 있는 모든 프로세스에 | 위 명령은 process group 15213에 있는 모든 프로세스에 SIGINT 신호를 보낸다. | ||
===키보드로 signal 보내기=== | ===키보드로 signal 보내기=== | ||
| 99번째 줄: | 105번째 줄: | ||
이 명령은 Unix pipe<ref>두 개의 명령어를 연결하여, 첫 번째 명령어의 출력 결과를 두 번째 명령어의 입력으로 전달하는 메커니즘이다.</ref>를 통해 연결된 두 개의 process로 구성된 foreground job<ref>알파벳 순으로 정렬된 파일 목록을 얻는 job이다.</ref>을 만든다. 두 프로세스 중 하나는 ls 프로그램<ref>현재 디렉토리의 파일과 디렉토리 목록을 출력하는 명령어이다.</ref>을 실행하고, 다른 하나는 sort 프로그램<ref>sort는 정렬을 의미하는 명령어로, 텍스트 데이터를 정렬하는 데 사용한다.</ref>을 실행한다. Shell은 각 job마다 별도의 프로세스 그룹을 생성하며, 이때의 process group ID는 job 내의 parent PID 중 하나에서 따온다. 예를 들어 오른쪽의 그림은 하나의 foreground job과 두개의 background job을 가진 shell을 보여준다. foreground job의 부모 프로세스는 PID가 20이고, process group ID도 20이다. 부모 프로세스는 두 개의 자식 프로세스를 생성했으며, 이들 또한 프로세스 그룹 20에 속한다. | 이 명령은 Unix pipe<ref>두 개의 명령어를 연결하여, 첫 번째 명령어의 출력 결과를 두 번째 명령어의 입력으로 전달하는 메커니즘이다.</ref>를 통해 연결된 두 개의 process로 구성된 foreground job<ref>알파벳 순으로 정렬된 파일 목록을 얻는 job이다.</ref>을 만든다. 두 프로세스 중 하나는 ls 프로그램<ref>현재 디렉토리의 파일과 디렉토리 목록을 출력하는 명령어이다.</ref>을 실행하고, 다른 하나는 sort 프로그램<ref>sort는 정렬을 의미하는 명령어로, 텍스트 데이터를 정렬하는 데 사용한다.</ref>을 실행한다. Shell은 각 job마다 별도의 프로세스 그룹을 생성하며, 이때의 process group ID는 job 내의 parent PID 중 하나에서 따온다. 예를 들어 오른쪽의 그림은 하나의 foreground job과 두개의 background job을 가진 shell을 보여준다. foreground job의 부모 프로세스는 PID가 20이고, process group ID도 20이다. 부모 프로세스는 두 개의 자식 프로세스를 생성했으며, 이들 또한 프로세스 그룹 20에 속한다. | ||
키보드에서 '''Ctrl+C'''를 누르면 커널은 foreground group에 있는 모든 프로세스에 SIGINT<ref> | 키보드에서 '''Ctrl+C'''를 누르면 커널은 '''foreground group에 있는 모든 프로세스에 SIGINT<ref>*SIGINT: 프로세스가 종료를 거부하거나 신호를 처리할 수 있는 기회를 제공한다. 기본적으로 사용자가 인터럽트 키인 Ctrl+C를 눌렀을 때 발생한다. | ||
* SIGINT: 프로세스가 종료를 거부하거나 신호를 처리할 수 있는 기회를 제공한다. 기본적으로 사용자가 인터럽트 키인 Ctrl+C를 눌렀을 때 발생한다. | *SIGKILL: 프로세스를 즉시 강제 종료시키는 신호이다. 프로세스가 이를 거부하거나 처리할 수 없으며 무조건 종료된다. | ||
* SIGKILL: 프로세스를 즉시 강제 종료시키는 신호이다. 프로세스가 이를 거부하거나 처리할 수 없으며 무조건 종료된다. | </ref> 신호'''를 보낸다. 즉, foreground job을 종료(terminate)한다. 마찬가지로 '''Ctrl+Z'''를 누르면 커널은 '''foreground 프로세스 그룹에 있는 모든 프로세스에 SIGTSTP 신호'''를 보낸다. 즉, foreground job을 suspend한다. | ||
</ref> | |||
====키보드로 signal 보내기 예시==== | ====키보드로 signal 보내기 예시==== | ||
| 136번째 줄: | 141번째 줄: | ||
===kill 함수를 통해 signal 보내기=== | ===kill 함수를 통해 signal 보내기=== | ||
프로세스는 kill 함수를 호출하여 프로세스(자기 자신을 포함)에게 | 프로세스는 '''kill() 함수를 호출하여 프로세스(자기 자신을 포함)에게 signal'''을 보낼 수 있다. | ||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
#include <sys/types.h> | #include <sys/types.h> | ||
#include <signal.h> | #include <signal.h> | ||
int kill(pid_t pid, int sig); | int kill(pid_t pid, int sig); //성공적으로 전송시 0 반환, 오류 발생시 -1 반환 | ||
</syntaxhighlight> | </syntaxhighlight> | ||
* pid > 0: kill 함수는 signal number가 sig인 signal을 process pid에 보낸다. | * pid > 0: kill 함수는 signal number가 sig인 signal을 process pid에 보낸다. | ||
| 195번째 줄: | 200번째 줄: | ||
==Receiving Signals== | ==Receiving Signals== | ||
[[파일:ReceivingSignal.png|섬네일|300x300픽셀]] | [[파일:ReceivingSignal.png|섬네일|300x300픽셀]] | ||
커널이 프로세스 p를 kernel mode에서 user mode로 전환할 때<ref>system call에서 돌아오거나 context switch를 완료할 때 등</ref>, 커널은 p에 대해 unblocked이고 pending 상태인 signal의 집합을 | 커널이 프로세스 p를 kernel mode에서 user mode로 전환할 때<ref>system call에서 돌아오거나 context switch를 완료할 때 등</ref>, '''커널은 p에 대해 unblocked이고 pending 상태인 signal의 집합을 확인'''한다.<ref>pnb = pending & ~blocked</ref> 이 집합이 비어 있으면<ref>if (|pnb| == 0)</ref>, 커널은 p의 logical control flow에서 다음 instruction(I<sub>next</sub>)로 control을 넘긴다. 그러나 이 집합이 비어 있지 않으면, 커널은 집합에서 signal k를 하나 선택하고<ref>일반적으로 가장 작은 signal number k</ref>, p가 signal k를 수신하도록한다. 또한 이를 해당 집합에 있는 모든 signal에 대해 반복한다. | ||
signal을 수신하면 해당 프로세스는 특정 작업을 수행한다. 작업을 완료한 후, p의 logical control flow에서 다음 instruction(I<sub>next</sub>)로 control을 넘긴다. 각 signal 유형은 기본 동작이 있으며, 다음은 기본 동작을 모두 나열한 것이다. | signal을 수신하면 해당 프로세스는 특정 작업을 수행한다. 작업을 완료한 후, p의 logical control flow에서 다음 instruction(I<sub>next</sub>)로 control을 넘긴다. 각 signal 유형은 기본 동작이 있으며, 다음은 기본 동작을 모두 나열한 것이다. | ||
* 프로세스를 terminate / ex: SIGKILL | * 프로세스를 terminate / ex: SIGKILL | ||
| 208번째 줄: | 213번째 줄: | ||
handler_t *signal(int signum, handler_t *handler); | handler_t *signal(int signum, handler_t *handler); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
* signum: 처리하려는 | * signum: '''처리하려는 signal의 종류를 지정'''합니다. ex: SIGINT, SIGSEGV, SIGTERM 등... | ||
* handler: | * handler: '''signal를 받았을 때 실행할 핸들러 함수의 주소에 해당'''한다. 핸들러 함수는 신호에 대한 사용자 정의 동작을 포함하며, 이를 통해 signal을 처리할 방법을 정의한다. | ||
** handler가 SIG_IGN이면 해당 | ** handler가 SIG_IGN이면 해당 signal 신호는 무시된다. | ||
** handler가 SIG_DFL이면 해당 | ** handler가 SIG_DFL이면 해당 signal 신호에 대한 동작은 기본 동작으로 돌아간다. | ||
** 그 외의 경우에서는, handler는 user-level signal handler의 address이다. | ** 그 외의 경우에서는, handler는 user-level signal handler의 address이다. | ||
*** installing: 해당 handler가 프로세스가 signum 신호를 받을 때 호출되는 것이다. | *** installing: 해당 handler가 프로세스가 signum 신호를 받을 때 호출되는 것이다. | ||
| 251번째 줄: | 256번째 줄: | ||
===Nested Signal Handlers=== | ===Nested Signal Handlers=== | ||
[[파일:Nested handler.png|섬네일|300x300픽셀|Nested handler]] | [[파일:Nested handler.png|섬네일|300x300픽셀|Nested handler]] | ||
signal handler는 다른 handler 들에 의해 중단될 수 있다. 이 예제에서는 메인 프로그램이 signal S를 처리하고 있으며, 이를 통해 메인 프로그램을 중단시키고 signal S handler로 control flow를 넘긴다. 또한 S handler가 실행되는 동안 signal T<ref><math>\ne</math>s</ref>를 받아들여 signal T handler로 다시 control flow를 넘긴다. signal T handler가 return한 후 S handler는 중단된 지점에서 실행을 재개하고, signal S handler까지 다시 return한 이후 비로소 메인 프로그램이 재개된다. | '''signal handler는 다른 handler 들에 의해 중단될 수''' 있다. 이 예제에서는 메인 프로그램이 signal S를 처리하고 있으며, 이를 통해 메인 프로그램을 중단시키고 signal S handler로 control flow를 넘긴다. 또한 S handler가 실행되는 동안 signal T<ref><math>\ne</math>s</ref>를 받아들여 signal T handler로 다시 control flow를 넘긴다. signal T handler가 return한 후 S handler는 중단된 지점에서 실행을 재개하고, signal S handler까지 다시 return한 이후 비로소 메인 프로그램이 재개된다. | ||
===Signals Handlers as Concurrent Flows=== | ===Signals Handlers as Concurrent Flows=== | ||
[[파일:ConcurrentSignal.jpg|섬네일|300x300픽셀]] | [[파일:ConcurrentSignal.jpg|섬네일|300x300픽셀]] | ||
시그널 핸들러는 메인 프로그램과 같은 CPU에서 실행되지만 별도의 logical flow를 가진다. 이는 multithreading과 같은 완전한 병렬적인 실행은 아니나, 어떤 비동기적인 event가 발생해 signal이 전송되면 즉시 메인 프로그램의 흐름을 suspend하고 handler 코드로 jump해서 실행된다. | 시그널 핸들러는 메인 프로그램과 같은 CPU에서 실행되지만 별도의 logical flow를 가진다. 이는 multithreading과 같은 완전한 병렬적인 실행은 아니나, 어떤 비동기적인 event가 발생해 signal이 전송되면 즉시 메인 프로그램의 흐름을 suspend하고 handler 코드로 jump해서 실행된다. 이때 '''signal handler와 메인 프로그램의 logical flow가 겹치므로, signal handler는 메인 프로그램과 concurrently하게 실행'''된다. | ||
[[파일:SignalContextSwitching.jpg|프레임없음|300x300픽셀]] | [[파일:SignalContextSwitching.jpg|프레임없음|300x300픽셀]] | ||
위 그림은 signal이 전달되고 수신되는 과정에서 context switching이 발생함을 보여준다. Process A는 while (1) ; 같은 무한 루프를 도는 코드이다. 어떤 시점에 signal이 오면 handler() 함수가 실행되며, handler가 끝나면 다시 메인 루프(while)로 돌아간다. 이를 시간축으로 보면 signal handler가 main flow 중간에 개입한 것처럼 보인다. 이때 Process B는 전혀 별개의 프로세스로, signal 동작과는 무관한 | 위 그림은 signal이 전달되고 수신되는 과정에서 context switching이 발생함을 보여준다. Process A는 while (1) ; 같은 무한 루프를 도는 코드이다. 어떤 시점에 signal이 오면 handler() 함수가 실행되며, handler가 끝나면 다시 메인 루프(while)로 돌아간다. 이를 시간축으로 보면 signal handler가 main flow 중간에 개입한 것처럼 보인다. 이때 Process B는 전혀 별개의 프로세스로, signal 동작과는 무관한 흐름이다. | ||
이때 handler의 실행은 다음과 같은 중요한 특징을 지닌다. | 이때 handler의 실행은 다음과 같은 중요한 특징을 지닌다. | ||
# handler와 메인 프로그램은 같은 CPU 안에서 실행되므로 D램을 공유한다. 이를 통해 전역 변수, 스택, 힙 메모리 등을 모두 | # handler와 메인 프로그램은 '''같은 CPU 안에서 실행되므로 D램을 공유한다. 이를 통해 전역 변수, 스택, 힙 메모리 등을 모두 공유'''한다. 하지만 CPU 내의 저장공간은 부분적으로 공유하여 handler의 실행 전 OS가 상태를 저장하고, handler의 실행 종료 후 원래의 register로 복원하고 메인프로그램으로 복귀한다. | ||
# signal은 언제 발생할지 모르므로, handler는 예측 불가능한 시점에<ref>비동기적으로</ref> 실행된다. | # signal은 언제 발생할지 모르므로, handler는 예측 불가능한 시점에<ref>비동기적으로</ref> 실행된다. | ||
| 269번째 줄: | 274번째 줄: | ||
===Implicit blocking mechanism=== | ===Implicit blocking mechanism=== | ||
기본적으로 커널은 현재 handler에 의해서 처리되고 있는 signal과 동일한 종류의 pending signal들을 | 기본적으로 커널은 현재 handler에 의해서 '''처리되고 있는 signal과 동일한 종류의 pending signal들을 block'''한다. 예를 들어, 현재 signal s가 프로세스가 포착해 handler에서 실행되고 있다고 하자. 이때 signal s가 추가로 프로세스에 보내지면, 해당 signal은 pending signal이 되지만, handler가 실행 종료될 때까지는 프로세스에 수신되지 않는다. | ||
===Explicit blocking mechanism=== | ===Explicit blocking mechanism=== | ||
| 277번째 줄: | 282번째 줄: | ||
/* Returns: 0 if OK, −1 on error */ | /* Returns: 0 if OK, −1 on error */ | ||
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); | int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); | ||
int sigemptyset(sigset_t *set); | int sigemptyset(sigset_t *set); //set을 빈 set으로 초기화한다. | ||
int sigfillset(sigset_t *set); | int sigfillset(sigset_t *set); //모든 signum을 set에 추가한다. | ||
int sigaddset(sigset_t *set, int signum); | int sigaddset(sigset_t *set, int signum); //signum을 set에 추가한다. | ||
int sigdelset(sigset_t *set, int signum); | int sigdelset(sigset_t *set, int signum); //signum을 set에서 삭제한다. | ||
int sigismember(const sigset_t *set, int signum); //signum이 set의 멤버이면 1을 반환하고, 그렇지 않으면 0을 반환한다. | |||
/ | |||
int sigismember(const sigset_t *set, int signum); | |||
</syntaxhighlight> | </syntaxhighlight> | ||
sigpromask() 함수는 현재 차단된 signal들의 집합을 인자 how의 값에 따라 변경한다. 이때 how에 들어오는 인자는 다음과 같다. | sigpromask() 함수는 현재 차단된 signal들의 집합을 인자 how의 값에 따라 변경한다. 이때 how에 들어오는 인자는 다음과 같다. | ||
| 290번째 줄: | 293번째 줄: | ||
* SIG_SETMASK: blocked set을 set으로 설정한다. (blocked = set) | * SIG_SETMASK: blocked set을 set으로 설정한다. (blocked = set) | ||
oldset 인자가 NULL이 아니면, oldset bit vector에 이전 blocked set의 값이 저장된다. | oldset 인자가 NULL이 아니면, oldset bit vector에 이전 blocked set의 값이 저장된다. | ||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
#include <signal.h> | #include <signal.h> | ||
| 385번째 줄: | 382번째 줄: | ||
* Read/Write Lock: 공유 자원에 대한 read 작업은 동시적인 접근을 허용하나, write 작업에 대해서만 lock을 건다. | * Read/Write Lock: 공유 자원에 대한 read 작업은 동시적인 접근을 허용하나, write 작업에 대해서만 lock을 건다. | ||
이러한 lock은 때때로 치명적인 문제를 낳기도 한다. 그 대표적인 예시가 '''deadlock'''이다. Deadlock이란 '''여러 thread가 서로가 가진 공유 자원의 lock이 열리기를 기다리느라 영원히 빠져나오지 못하는 상태'''이다. 이는 다음의 발생 조건을 가지고 있다. | 이러한 lock은 때때로 치명적인 문제를 낳기도 한다. 그 대표적인 예시가 '''deadlock'''이다. Deadlock이란 '''여러 thread가 서로가 가진 공유 자원의 lock이 열리기를 기다리느라 영원히 빠져나오지 못하는 상태'''이다. 이는 다음의 발생 조건을 가지고 있다. | ||
# Mutual Extention: '''공유 자원에는 최대 하나의 thread'''만 배정된다. | # Mutual Extention: '''공유 자원에는 최대 하나의 thread'''만 배정된다. | ||
| 589번째 줄: | 585번째 줄: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
위 메인 프로그램에는 기존과는 작은 변화가 생겼다. 이는 아예 자식 프로세스를 생성하기 전에 모든 signal을 block하는 것이다. 이를 통해서 addjob()함수의 실행 전에는 deletejob()함수가 실행될 수 없도록 프로세스와 handler 간의 실행 순서를 강제하여 오류를 해결한다. | 위 메인 프로그램에는 기존과는 작은 변화가 생겼다. 이는 아예 자식 프로세스를 생성하기 전에 모든 signal을 block하는 것이다. 이를 통해서 addjob()함수의 실행 전에는 deletejob()함수가 실행될 수 없도록 프로세스와 handler 간의 실행 순서를 강제하여 오류를 해결한다. | ||
===Explicitly Waiting for Signals=== | |||
때때로 메인 프로그램은 특정 시그널 핸들러가 실행될 때까지 명시적으로 기달려야할 필요가 있다. 예를 들어서 리눅스 shell이 foregruond 작업을 실행하고 있을 때, 다음으로 실행할 명령을 입력받기 위해서는 현재 실행되고 있는 작업이 종료되고 SIGCHLD 핸들러에 의해 reap되기를 기다려야 한다. 아래는 이를 구현한 코드이다. | |||
<syntaxhighlight lang="cpp"> | |||
volatile sig_atomic_t pid; //시그널 핸들러 안팎에서 안전하게 접근할 수 있도록 보장된 자료형/pid를 통해 자식 프로세스의 종료 여부 확인 | |||
void sigchld_handler(int s) { //SIGCHLD 시그널 핸들러 | |||
int olderrno = errno; | |||
pid = Waitpid(-1, NULL, 0); //자식 프로세스를 reap하고 pid에 자식 프로세스 PID 저장 | |||
errno = olderrno; | |||
} | |||
void sigint_handler(int s) {} //SIGINT가 와도 아무 행동을 하지 않음 (Ctrl+C 무시 등을 위해 사용) | |||
int main(int argc, char **argv) { | |||
sigset_t mask, prev; | |||
Signal(SIGCHLD, sigchld_handler); //SIGCHLD에 대한 시그널 핸들러 등록 | |||
Signal(SIGINT, sigint_handler); //SIGINT에 대한 시그널 핸들러 등록 | |||
Sigemptyset(&mask); | |||
Sigaddset(&mask, SIGCHLD); //SIGCHLD 마스크 생성 | |||
while (1) { | |||
//Block하는 이유 >>> 부모와 자식 프로세스 사이의 race를 피하기 위해서. | |||
Sigprocmask(SIG_BLOCK, &mask, &prev); //SIGCHLD Block | |||
if (Fork() == 0) //자식 프로세스, 바로 종료 | |||
exit(0); | |||
//부모 프로세스 | |||
pid = 0; | |||
Sigprocmask(SIG_SETMASK, &prev, NULL); //SIGCHLD Unblock | |||
while (!pid); //pid의 값이 자식 프로세스의 PID로 바뀔 때 까지 무한 루프 >>> CPU 낭비!!! | |||
} | |||
exit(0); | |||
} | |||
</syntaxhighlight> | |||
위 코드의 부모 프로세스는 자식 프로세스를 실행한 다음 무한 루프로 진입한다. 이후 SIGCHLD를 수신하여 해당 핸들러를 통해 자식 프로세스를 reap하고, 자식 프로세스의 PID를 pid 값에 할당할 때하면 비로소 무한 루프에서 탈출한다. 즉 위 코드에서 무한 루프는 SIGCHLD를 통한 자식 프로세스의 reap을 명시적으로 기다리기 위해서 사용된다. 하지만 위 코드에서 무한 루프는 CPU 자원을 낭비한다. 따라서 위 코드는 아래 두 방식으로 개선될 수 있을 것이다. | |||
<syntaxhighlight lang="cpp"> | |||
while (!pid) //Race!!! | |||
pause(); | |||
</syntaxhighlight> | |||
<syntaxhighlight lang="cpp"> | |||
while (!pid) //Too slow!!! | |||
sleep(1); | |||
</syntaxhighlight> | |||
하지만 pause() 함수를 이용한 코드는 race condition<ref>둘 이상의 프로세스나 스레드의 실행 순서에 따라 그 실행 결과가 달라지는 상황이다.</ref>이 존재한다. pause() 함수는 UNIX/LINUX에서 시그널을 받을 때까지 무기한으로 정지시키는 함수이다. 위 코드에서의 race condition이 존재하는 상황은 다음과 같다. | |||
# <code>pid == 0</code>인 상태에서 <code>while (!pid)</code> 조건을 검사 → 조건 참 → 루프 진입 | |||
# <code>pause()</code>를 호출하려는 순간, 운 나쁘게도 그 직전에 SIGCHLD가 도착. | |||
# 시그널 핸들러가 호출되어 자식을 <code>waitpid()</code>로 수거하고, pid에 자식 PID를 설정하고 메인 루틴은 pause()를 호출 | |||
# 모든 시그널이 처리된 상태이므로, pause()는 더 이상 깨울 시그널 없이 무한 대기 상태에 빠짐 | |||
즉, while 문의 조건을 검사한 이후 <code>pause()</code> 함수 호출 직전에 SIGCHLD 시그널이 부모 프로세스에 도착하면 race가 발생한다. 이 경우 while 문이 없으면 race condition이 해결되는 것처럼 보일 수 있는데, while 문이 필요한 이유는 SIGCHLD 이외의 시그널들을 처리하기 위해서이다. 예를 들어 pause() 함수가 실행되던 중 SIGINT가 도착하였을 때 while 문이 없다면 그대로 부모 프로세스의 SIGCHLD에 대한 명시적인 기다림이 종료된다.<br> | |||
또한 <code>sleep(1)</code>을 이용한 코드는 적절하지만 너무 느리다. 따라서 해당 상황을 해결하기 위한 올바른 해결책은 '''sigsuspend 함수'''를 사용하는 것이다. | |||
<syntaxhighlight lang="cpp"> | |||
#include <signal.h> | |||
int sigsuspend(const sigset_t *mask); //return -1 | |||
</syntaxhighlight> | |||
<code>sigsuspend()</code> 함수는 현재의 블록된 시그널 집합을 mask로 일시적으로 교체한 다음, 시그널을 하나 받을 때까지 대기한다. 시그널을 받고 핸들러가 실행된 뒤, sigsuspend() 함수는 리턴되며, 시그널 마스크는 원래대로 복원된다. 즉 아래 코드의 atomic한(인터럽트되지 않는) 버전이다. | |||
<syntaxhighlight lang="cpp"> | |||
sigprocmask(SIG_BLOCK, &mask, &prev); | |||
pause(); | |||
sigprocmask(SIG_SETMASK, &prev, NULL); | |||
</syntaxhighlight> | |||
즉, <code>sigsuspend()</code> sigprocmask() 함수 호출과 pause() 함수 호출을 함께 실행해, sigprocmask() 함수 호출 이후 pause() 함수 호출 이전에 시그널이 도착하는 경쟁 조건을 제거한다. 이를 활용하면 메인 함수는 아래와 같이 작성된다. | |||
<syntaxhighlight lang="cpp"> | |||
int main(int argc, char **argv) { | |||
sigset_t mask, prev; | |||
Signal(SIGCHLD, sigchld_handler); //SIGCHLD에 대한 시그널 핸들러 등록 | |||
Signal(SIGINT, sigint_handler); //SIGINT에 대한 시그널 핸들러 등록 | |||
Sigemptyset(&mask); | |||
Sigaddset(&mask, SIGCHLD); //SIGCHLD 마스크 생성 | |||
while (1) { | |||
//Block하는 이유 >>> 부모와 자식 프로세스 사이의 race를 피하기 위해서. | |||
Sigprocmask(SIG_BLOCK, &mask, &prev); //SIGCHLD Block | |||
if (Fork() == 0) //자식 프로세스, 바로 종료 | |||
exit(0); | |||
//부모 프로세스 | |||
pid = 0; | |||
while(!pid) //조건식 판별 중에는 SIGCHLD는 Block됨 | |||
sigsuspend(&prev); //함수의 실행 중에만 SIGSHLD가 Unblock됨 | |||
Sigprocmask(SIG_SETMASK, &prev, NULL); //SIGCHLD Unblock | |||
} | |||
exit(0); | |||
} | |||
</syntaxhighlight> | |||
위 코드에서 <code>sigsuspend()</code> 함수를 호출하기 전에 SIGCHLD는 블록된다. 하지만 <code>sigsuspend()</code> 함수의 실행과 동시에 SIGCHLD는 일시적으로 언블록하고, 부모가 시그널을 받을 때까지 일시정지한다. 또한 <code>sigsuspend()</code> 함수가 종료된 후에는 다시 SIGCHLD는 다시 블록된다. 따라서 <code>pause()</code> 함수의 race condition이 사라진다. 그 이유는 <code>puase()</code> 함수를 통한 구현에서는 while 문의 조건 검사 직후 SIGCHLD가 도착할 수 있었지만(언블록되어 있었으므로) <code>sigsuspend()</code> 함수를 통한 구현에서는 <code>sigsuspend()</code> 함수 밖에서는 SIGCHLD가 도착할 수 없기 때문이다. 즉, <code>sigsuspend()</code> 함수가 실행되는 도중에만 SIGCHLD가 도착할 수 있고, 이 때문에 무한 대기 상태에 빠지지 않는다. | |||
==각주== | ==각주== | ||
[[분류:컴퓨터 네트워크]] | [[분류:컴퓨터 네트워크]] | ||
2025년 4월 21일 (월) 17:49 기준 최신판
상위 문서: Exceptional Control Flow
개요
signal이란 시스템에서 어떤 종류의 이벤트가 발생했음을 프로세스에 알리는 작은 메시지이다. signal은 이를 위해서 kernel에서 process로 보내지며, signal type은 1~30의 정수로 식별된다. 이때 어떤 signal에 저장된 정보는 해당 ID와 그 signal이 도착했다는 사실 뿐이다. 아래는 몇가지 signal ID와 그에 대응되는 signal들을 나타낸 표이다.
| ID | Name | Default Action | Corresponding Event |
|---|---|---|---|
| 2 | SIGINT | Terminate | User typed ctrl-c |
| 9 | SIGKILL | Terminate | Kill program(cannot override or ignore) |
| 11 | SIGSEGV | Terminate | Segmentation violation |
| 14 | SIGALRM | Terminate | Timer Signal |
| 17 | SIGCHLD | Ignore | Child stopped or terminated |
Signal Concepts

시그널이 대상 프로세스로 전달되는 과정은 다음 두 가지의 명확한 단계로 이루어진다.
- Sending a signal
- 커널은 destination 프로세스의 context 일부 상태를 업데이트해 해당 프로세스에 시그널을 보낸다. 다음은 signal이 전달되는 경우이다.
- 커널이 0으로 나누기 오류나 자식 프로세스의 종료와 같은 시스템 이벤트를 감지했을 때
- 한 프로세스가 kill 함수를 호출하여 명시적으로 커널에게 대상 프로세스에 signal을 보내도록 요청했을 때
- 프로세스는 자기 자신에게 시그널을 보낼 수도 있다.
- 커널은 destination 프로세스의 context 일부 상태를 업데이트해 해당 프로세스에 시그널을 보낸다. 다음은 signal이 전달되는 경우이다.
- Receiving a signal
- destination 프로세스는 커널이 해당 시그널의 전달에 반응하도록 강제할 때 시그널을 받는다. 프로세스는 시그널에 대해 다음 세 가지 중 하나의 반응을 할 수 있다:
- signal을 무시(Ignore)
- 프로세스를 종료(Terminate)
- 사용자 수준 함수인 signal handler을 실행하여 시그널을 catch하여 처리
- destination 프로세스는 커널이 해당 시그널의 전달에 반응하도록 강제할 때 시그널을 받는다. 프로세스는 시그널에 대해 다음 세 가지 중 하나의 반응을 할 수 있다:
- Pending and Blocked Signal
- Pending signal은 전송되었지만 수신이 완료되지 않은 signal을 의미한다.
- 특정한 유형의 pending signal은 최대 하나만 존재할 수 있다. 만약 프로세스가 특정 유형(k)의 pending signal을 가지고 있다면, 그 프로세스에 대해 이후에 보내지는 동일한 유형(k)의 signal는 대기열에 쌓이지 않고 단순히 버려진다.
- 프로세스는 특정한 유형의 signal 수신을 block할 수 있다.
- 이때 signal이 전달될 수는 있으나, 해당 signal은 pending signal이 되어 프로세스가 해당 유형의 signal을 unblock하기 전까지는 수신되지 않는다.
- 커널은 각 프로세스의 context에서 pending signal과 blocked signal에 대한 bit vector를 가지고 있다.
- pending: pending signal들의 집합을 나타내는 bit vector이다.
- 커널은 유형이 k인 signal이 전달되면 pending vector에 k bit를 설정한다.
- 커널은 유형이 k인 signal이 수신되면 pending vector에서 k bit를 지운다.
- blocked: block된 signal들의 집합을 나타내는 bit vector이다.
- signal mask라고도 불린다.
- sigprocmask 함수를 통해서 특정 유형의 signal을 block하거나 unblock할 수 있다.
- pending: pending signal들의 집합을 나타내는 bit vector이다.
- Pending signal은 전송되었지만 수신이 완료되지 않은 signal을 의미한다.
Sending Signal
Process Group

모든 프로세스는 단 하나의 process group(프로세스 그룹)에 속한다. 이 그룹은 양의 정수인 process group ID로 식별된다. 현재 프로세스의 process group ID를 알기 위해서는 이를 반환하는 getpgrp()함수를 사용할 수 있다.
#include <unistd.h>
pid_t getpgrp(void); //반환값: 현재 프로세스의 PGID (Process Group ID)
기본적으로 child process는 parent process와 동일한 프로세스 그룹에 속한다. 이때 프로세스는 setpgid() 함수를 사용하여 자신이나 다른 프로세스의 프로세스 그룹을 변경할 수 있다.
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
setpgid 함수는 그룹 ID가 pgid인 프로세스 그룹이 존재하는지에 따라 그 실행 방식이 달라진다.
- 그룹 ID가 pgid인 프로세스 그룹이 존재
- ID가 pid인 프로세스를 해당 프로세스 그룹에 속하도록 함
- 그룹 ID가 pgid인 프로세스 그룹이 존재 X
- 만약
pid == pgid라면, 그룹 ID가 pgid인 프로세스 그룹을 생성하고, ID가 pid인 프로세스를 해당 프로세스 그룹에 추가한다. - 만약
pid != pgid라면, 오류가 발생한다.
- 만약
이때 pid가 0이면 현재 프로세스의 PID가 사용된다. pgid가 0이면, pid로 지정된 프로세스의 PID가 프로세스 그룹 ID로 사용된다. 다음은 PID가 15213인 프로세스에서 setpgid()함수를 호출한 예시이다.
setpgid(0, 0);
위 함수의 실행시 process group ID가 15213인 프로세스 그룹이 존재하지 않는다면 그러한 프로세스 그룹을 만들고 ID가 15213인 프로세스를 해당 그룹에 추가한다. 즉, 호출하는 프로세스가 자신의 PID를 프로세스 그룹 ID로 설정함을 의미하며, 이는 새로운 프로세스 그룹을 생성하는 효과를 갖는다.
/Bin/kill 프로그램을 사용해 signal 보내기
/bin/kill 프로그램은 다른 프로세스나 프로세스 그룹에 임의의 신호를 보낸다. 이는 아래와 같다:
linux> /bin/kill sig_ID PID
위 명령은 process PID에 sig_ID에 해당하는 signal을 보낸다.[1] 이때 PID로 음수를 사용하면 해당 process group 15213에 존재하는 모든 프로세스에 해당 signal을 보낸다. 예를 들어
linux> /bin/kill -2 -15213
위 명령은 process group 15213에 있는 모든 프로세스에 SIGINT 신호를 보낸다.
키보드로 signal 보내기

Unix shell은 하나의 명령어를 evaluate해 생성된 process를 job이라는 abstraction으로 나타낸다. 이때 어떠한 시점에서든 하나의 foreground job과 여러 background job이 존재할 수 있다. 예를 들어,
linux> ls | sort
이 명령은 Unix pipe[2]를 통해 연결된 두 개의 process로 구성된 foreground job[3]을 만든다. 두 프로세스 중 하나는 ls 프로그램[4]을 실행하고, 다른 하나는 sort 프로그램[5]을 실행한다. Shell은 각 job마다 별도의 프로세스 그룹을 생성하며, 이때의 process group ID는 job 내의 parent PID 중 하나에서 따온다. 예를 들어 오른쪽의 그림은 하나의 foreground job과 두개의 background job을 가진 shell을 보여준다. foreground job의 부모 프로세스는 PID가 20이고, process group ID도 20이다. 부모 프로세스는 두 개의 자식 프로세스를 생성했으며, 이들 또한 프로세스 그룹 20에 속한다.
키보드에서 Ctrl+C를 누르면 커널은 foreground group에 있는 모든 프로세스에 SIGINT[6] 신호를 보낸다. 즉, foreground job을 종료(terminate)한다. 마찬가지로 Ctrl+Z를 누르면 커널은 foreground 프로세스 그룹에 있는 모든 프로세스에 SIGTSTP 신호를 보낸다. 즉, foreground job을 suspend한다.
키보드로 signal 보내기 예시
아래는 Ctrl+C와 Ctrl+Z의 예시이다.
bluefish> ./forks 17
Child: pid=28108 pgrp=28107
Parent: pid=28107 pgrp=28107
<types ctrl-z>
Suspended
bluefish> ps w
PID TTY STAT TIME COMMAND
27699 pts/8 Ss 0:00 -tcsh
28107 pts/8 T 0:01 ./forks 17
28108 pts/8 T 0:01 ./forks 17
28109 pts/8 R+ 0:00 ps w
bluefish> fg
./forks 17
<types ctrl-c>
bluefish> ps w
PID TTY STAT TIME COMMAND
27699 pts/8 Ss 0:00 -tcsh
28110 pts/8 R+ 0:00 ps w
위에서 STAT은 다음과 같이 표현된다.
- First letter:
- S: sleeping
- T: stopped
- R: running
- Second letter:
kill 함수를 통해 signal 보내기
프로세스는 kill() 함수를 호출하여 프로세스(자기 자신을 포함)에게 signal을 보낼 수 있다.
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig); //성공적으로 전송시 0 반환, 오류 발생시 -1 반환
- pid > 0: kill 함수는 signal number가 sig인 signal을 process pid에 보낸다.
- pid = 0: kill 함수는 호출 프로세스가 속한 프로세스 그룹에 있는 모든 프로세스(호출 프로세스 자신 포함)에게 signal number가 sig인 signal을 보낸다.
- pid < 0: kill 함수는 process group |pid|(pid의 절대값)에 속한 모든 프로세스에게 signal number가 sig인 signal을 보낸다.
void fork12()
{
pid_t pid[N]; //자식 프로세스의 PID를 저장하는 배열
int i;
int child_status;
//자식 프로세스 생성
for (i = 0; i < N; i++) {
if ((pid[i] = fork()) == 0) {
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)) //WIFEXITED(child_status)를 통해 정상적으로 종료되었는지 확인
//정상적으로 종료되었다면 자식 프로세스의 exit status를 출력한다.
printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminated abnormally\n", wpid);
}
}
> 14-ecf-procs ./forks 12
Killing process 24526
Killing process 24527
Killing process 24528
Killing process 24529
Killing process 24530
Child 24527 terminated abnormally
Child 24530 terminated abnormally
Child 24529 terminated abnormally
Child 24528 terminated abnormally
Child 24526 terminated abnormally
> 14-ecf-procs
위는 N을 5로 설정하였을 때[9] fork12() 함수를 실행한 예시 결과이다. 이때, signal의 기본 동작에 의한 종료는 비정상 종료에 해당된다. 출력결과 또한 이를 보여준다.
Receiving Signals

커널이 프로세스 p를 kernel mode에서 user mode로 전환할 때[10], 커널은 p에 대해 unblocked이고 pending 상태인 signal의 집합을 확인한다.[11] 이 집합이 비어 있으면[12], 커널은 p의 logical control flow에서 다음 instruction(Inext)로 control을 넘긴다. 그러나 이 집합이 비어 있지 않으면, 커널은 집합에서 signal k를 하나 선택하고[13], p가 signal k를 수신하도록한다. 또한 이를 해당 집합에 있는 모든 signal에 대해 반복한다. signal을 수신하면 해당 프로세스는 특정 작업을 수행한다. 작업을 완료한 후, p의 logical control flow에서 다음 instruction(Inext)로 control을 넘긴다. 각 signal 유형은 기본 동작이 있으며, 다음은 기본 동작을 모두 나열한 것이다.
- 프로세스를 terminate / ex: SIGKILL
- 프로세스를 terminate, core dump[14]라는 이름의 파일을 생성
- 프로세스를 SIGCONT signal에 의해 재개될 때까지 suspend(stop)
- 프로세스를 signal을 무시하도록 한다.[15] / ex: SIGCHLD[16]
이때 프로세스는 signal 함수를 사용하여 신호와 관련된 기본 동작을 수정할 수 있다. 단, SIGSTOP과 SIGKILL은 기본 동작을 변경할 수 없다.
Installing Signal Handlers
signal() 함수는 프로세스가 신호를 받았을 때 해당 신호에 대한 기본 동작을 수정하는 기능을 제공한다. 아래는 signal() 함수에 대한 기본 형식과 인자에 대한 설명이다.
handler_t *signal(int signum, handler_t *handler);
- signum: 처리하려는 signal의 종류를 지정합니다. ex: SIGINT, SIGSEGV, SIGTERM 등...
- handler: signal를 받았을 때 실행할 핸들러 함수의 주소에 해당한다. 핸들러 함수는 신호에 대한 사용자 정의 동작을 포함하며, 이를 통해 signal을 처리할 방법을 정의한다.
- handler가 SIG_IGN이면 해당 signal 신호는 무시된다.
- handler가 SIG_DFL이면 해당 signal 신호에 대한 동작은 기본 동작으로 돌아간다.
- 그 외의 경우에서는, handler는 user-level signal handler의 address이다.
- installing: 해당 handler가 프로세스가 signum 신호를 받을 때 호출되는 것이다.
- catching / handling: handler를 실행하는 것
- handler가 return문을 실행하면, 보통 control은 signal을 받았을 때 프로세스의 control flow에서 중단된 지점으로 돌아간다.
프로세스가 signal k를 받으면, signal k에 대해 설치된 handler가 k라는 하나의 인자를 전달받으며 호출된다. 이 인수는 동일한 handler 함수가 여러 신호 유형을 처리할 수 있도록 한다. 예를 들어,
void sigint_handler(int sig) /* SIGINT handler */ {
printf("So you think you can stop the bomb with ctrl-c, do you?\n");
sleep(2); //2초간 대기
printf("Well...");
fflush(stdout);
sleep(1); //1초간 대기
printf("OK. :-)\n");
exit(0); //프로그램을 정상적으로 종료
}
int main() {
// SIGINT handler를 install
if (signal(SIGINT, sigint_handler) == SIG_ERR)
unix_error("signal error");
//pause() 함수는 프로세스가 signal을 받을 때까지 멈추도록 한다.
pause();
return 0;
}
> ecf-signals ./sigint
^CSo you think you can stop the bomb with ctrl-c, do you?
Well...OK. :-)
> ecf-signals
위에서 sigint_handler()라는 이름의 handler에서 sig라는 이름의 정수형 인자는 등록되어 있지만 사용되지는 않았다. handler는 sig와 같은 정수형 인자를 가지고 있는데, 해당 인자에는 전송된 signal의 number가 전달된다. 이를 통해서 동일한 handler를 통해서 여러 signal에 대한 처리를 할 수 있다.
Nested Signal Handlers

signal handler는 다른 handler 들에 의해 중단될 수 있다. 이 예제에서는 메인 프로그램이 signal S를 처리하고 있으며, 이를 통해 메인 프로그램을 중단시키고 signal S handler로 control flow를 넘긴다. 또한 S handler가 실행되는 동안 signal T[17]를 받아들여 signal T handler로 다시 control flow를 넘긴다. signal T handler가 return한 후 S handler는 중단된 지점에서 실행을 재개하고, signal S handler까지 다시 return한 이후 비로소 메인 프로그램이 재개된다.
Signals Handlers as Concurrent Flows

시그널 핸들러는 메인 프로그램과 같은 CPU에서 실행되지만 별도의 logical flow를 가진다. 이는 multithreading과 같은 완전한 병렬적인 실행은 아니나, 어떤 비동기적인 event가 발생해 signal이 전송되면 즉시 메인 프로그램의 흐름을 suspend하고 handler 코드로 jump해서 실행된다. 이때 signal handler와 메인 프로그램의 logical flow가 겹치므로, signal handler는 메인 프로그램과 concurrently하게 실행된다.
위 그림은 signal이 전달되고 수신되는 과정에서 context switching이 발생함을 보여준다. Process A는 while (1) ; 같은 무한 루프를 도는 코드이다. 어떤 시점에 signal이 오면 handler() 함수가 실행되며, handler가 끝나면 다시 메인 루프(while)로 돌아간다. 이를 시간축으로 보면 signal handler가 main flow 중간에 개입한 것처럼 보인다. 이때 Process B는 전혀 별개의 프로세스로, signal 동작과는 무관한 흐름이다.
이때 handler의 실행은 다음과 같은 중요한 특징을 지닌다.
- handler와 메인 프로그램은 같은 CPU 안에서 실행되므로 D램을 공유한다. 이를 통해 전역 변수, 스택, 힙 메모리 등을 모두 공유한다. 하지만 CPU 내의 저장공간은 부분적으로 공유하여 handler의 실행 전 OS가 상태를 저장하고, handler의 실행 종료 후 원래의 register로 복원하고 메인프로그램으로 복귀한다.
- signal은 언제 발생할지 모르므로, handler는 예측 불가능한 시점에[18] 실행된다.
Blocking and Unblocking Signals
Linux(리눅스)는 signal을 block하는 implicit한, 혹은 explicit한 메커니즘을 제공한다.
Implicit blocking mechanism
기본적으로 커널은 현재 handler에 의해서 처리되고 있는 signal과 동일한 종류의 pending signal들을 block한다. 예를 들어, 현재 signal s가 프로세스가 포착해 handler에서 실행되고 있다고 하자. 이때 signal s가 추가로 프로세스에 보내지면, 해당 signal은 pending signal이 되지만, handler가 실행 종료될 때까지는 프로세스에 수신되지 않는다.
Explicit blocking mechanism
application은 sigpromask() 함수와 그 보조 함수들을 통해 특정 signal들을 explicit하게 block/unblock한다. 이때 sigpromask() 함수와 그 보조 함수들은 다음과 같다.
#include <signal.h>
/* Returns: 0 if OK, −1 on error */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigemptyset(sigset_t *set); //set을 빈 set으로 초기화한다.
int sigfillset(sigset_t *set); //모든 signum을 set에 추가한다.
int sigaddset(sigset_t *set, int signum); //signum을 set에 추가한다.
int sigdelset(sigset_t *set, int signum); //signum을 set에서 삭제한다.
int sigismember(const sigset_t *set, int signum); //signum이 set의 멤버이면 1을 반환하고, 그렇지 않으면 0을 반환한다.
sigpromask() 함수는 현재 차단된 signal들의 집합을 인자 how의 값에 따라 변경한다. 이때 how에 들어오는 인자는 다음과 같다.
- SIG_BLOCK: set에 있는 signal들을 blocked[19] set에 추가한다.(blocked = blocked | set).
- SIG_UNBLOCK: set에 있는 signal들을 blocked set에서 제거한다. (blocked = blocked & ~set)
- SIG_SETMASK: blocked set을 set으로 설정한다. (blocked = set)
oldset 인자가 NULL이 아니면, oldset bit vector에 이전 blocked set의 값이 저장된다.
#include <signal.h>
int main() {
sigset_t mask, prev_mask;
sigemptyset(&mask); // Initialize mask as empty
sigaddset(&mask, SIGINT); // Add SIGINT to the mask
// 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 (unblock SIGINT)
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
return 0;
}
위 코드는 SIGINT를 일시적으로 block한 다음, 특정 코드 실행 후, 다시 이전의 blocked set으로 되돌린다.
Writing signal handler
Signal handler는 리눅스 시스템 수준의 프로그래밍에서 가장 까다로운 것중 하나이다. 왜냐하면 handler가 다음과 같은 특성을 지니기 때문이다.
- Handler는 메인 프로그램과 concurrently하게 실행되며 동일한 global data structure들을 공유하므로, 메인 프로그램이나 다른 handler들과 간섭할 수 있다.[20]
- 신호가 어떻게, 그리고 언제 수신되는지에 대한 규칙은 종종 직관에 반한다.
- 서로 다른 시스템들은 서로 다른 신호 처리 의미론(semantics)을 가질 수 있다.
따라서 safe signal handler를 작성하는 것은 매우 중요하다고 볼 수 있다.
Data Race
Signal handler의 실행 중 일어날 수 있는 치명적인 오류 중 대표적인 예시가 data race이다. data race는 두개 이상의 스레드가 같은 메모리 주소에 동시에(concurrently) 접근하고, 이때 wirte 연산이 일어나며, 동기화나 lock 등의 장치가 되어있지 않으면 발생한다.
동기화나 lock등이 되어 있지 않다면 write 연산을 실행할 때 문제가 일어날 수 있다. 이를 이해하기 위해서는 동시성(concurrency)와 병렬성(parallelism)의 차이에 대해 이해해야 한다.
| 용어 | 설명 |
|---|---|
| 동시성(Concurrency) | CPU가 빠르게 스레드들을 왔다갔다 하며 처리하는 것 (실제로는 1개만 실행 중) |
| 병렬성(Parallelism) | 진짜로 여러 CPU 코어가 동시에 여러 스레드를 실행하는 것 |
즉, 동시성은 병렬성인 것처럼 보이는 상태라고 볼 수 있으며, 그 구현을 위해 CPU는 스레드를 굉장히 빠르게 전환하면서 실행한다. 그렇기 때문에 우리는 각 프로세스가 어떤 과정을 거쳐서 실행되는 지를 이해할 수는 있어도, 각 CPU 내에 있는 프로세스의 실제 실행 순서를 알 수는 없다. 이는 다음 예시를 통해 더욱 쉽게 이해할 수 있다.(동기화나 lock등이 사용되지 않았다고 가정하자.)
# 전역변수 x는 0으로 초기화 되어 있다. Thread A: printf(x) Thread B: x++; printf(x)
위의 A를 실행하면 일반적으로는 0이 출력된다고 생각하겠지만, 동기화가 되어 있지 않은 경우 실제로는 1이 출력될 수 있다. 그 이유는 A가 실행되기 전에 B의 x++ 이 먼저 실행될 수 있기 때문이다. 이러한 경우가 치명적인 문제점으로 작용하는 경우가 바로 data race이다. 위의 예시는 data race에 대한 모든 조건이 갖추어져 있다. 각 thread들이 동시적으로 작동하고, write 연산이 하나 이상 존재하며, 동기화나 lock등이 되어 있지 않기 때문이다.
다음 함수에 대해서도 생각해보자.
int counter = 0; //전역변수
void* increment(void* arg) {
for (int i = 0; i < 1000; ++i) {
counter++; // 읽고, 더하고, 쓰는 3단계
}
return NULL;
}
위의 코드를 두 thread가 동기화나 lock등이 없이 실행한다고 가정해보자. counter++는 사실 read->add->write의 세 단계로 이루어진다. 이때 한 thread가 read 단계를 마친 직후 동시적으로 다른 thread가 read 단계를 마친다면, 두 thread는 동일한 counter의 값을 읽는 것이다. 이후 두 thread가 add, write를 하더라도 counter의 값은 두 번 증가한 것이 아니라 한 번 증가한 것이 된다. 결과적으로 최종적인 counter의 값은 2000 미만이 된다.[21] 또한 더욱 최악인 점은 counter의 값은 두 thread를 실행시킬 때 마다 달라지며(non-determinstic) 이로 인해 디버깅이 어려워진다는 것이다.
Lock
Lock은 하나의 thread가 전역 변수 등의 공유 자원에 접근하는 동안 다른 thread가 해당 자원에 접근하지 못하도록 막는 동기화 메커니즘이며, 이를 통해서 data race를 막을 수 있다. 이는 다음과 같은 작동원리를 가진다.
- thread는 lock을 획득하기 위해서 시도한다.
- lock이 열려 있으면 즉시 획득하고 해당 자원에 접근한다.
- lock이 닫혀 있으면 대기한다.
- thread의 해당 자원에 대한 작업이 끝나면 lock을 해제한다.
- 해당 과정 후에 비로소 다른 thread가 lock을 얻을 수 있다.
아래는 data race의 두 번째 예시에 lock을 활용하여 해당 오류를 없앤 예시이다.
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int counter = 0; //전역 변수
void* increment(void* arg) {
pthread_mutex_lock(&lock); // lock 획득
counter++;
pthread_mutex_unlock(&lock); // lock 해제
return NULL;
}
위와 같이 lock을 활용하여 thread의 공유 자원에 대한 접근을 적절히 통제할 수 있다.
Lock에는 다음과 같은 종류가 있다.
- Mutex: 한 번에 하나의 thread만 공유 자원에 접근할 수 있도록 하며, 가장 흔학 lock의 형태이다.
- Spin lock: lock을 얻을 때까지 CPU를 쉬지 않고 계속 확인하는 것이다. 빠르지만, CPU를 낭비한다는 단점이 있다.
- Read/Write Lock: 공유 자원에 대한 read 작업은 동시적인 접근을 허용하나, write 작업에 대해서만 lock을 건다.
이러한 lock은 때때로 치명적인 문제를 낳기도 한다. 그 대표적인 예시가 deadlock이다. Deadlock이란 여러 thread가 서로가 가진 공유 자원의 lock이 열리기를 기다리느라 영원히 빠져나오지 못하는 상태이다. 이는 다음의 발생 조건을 가지고 있다.
- Mutual Extention: 공유 자원에는 최대 하나의 thread만 배정된다.
- Hold and Wait: thread가 공유 자원을 lock하고 다른 공유 자원의 unlock을 기다린다.
- No Preemtion: 공유 자원을 강제로 가져올 수 없다.
- Circular Wait: thread들이 circular하게 자원을 기다린다.
- 예시: A B C A
아래는 deadlock이 생길 수 있는 예시 코드이다.
pthread_mutex_t lockA, lockB;
void* thread1() {
pthread_mutex_lock(&lockA);
sleep(1);
pthread_mutex_lock(&lockB); // deadlock 발생 가능
...
}
void* thread2() {
pthread_mutex_lock(&lockB);
sleep(1);
pthread_mutex_lock(&lockA); // deadlock 발생 가능
...
}
thread1, 2를 동시적으로 실행할 경우에는 주석이 경고한 바와 같이 deadlock이 생길 수 있다.
Safe Signal Handling
다음은 safe signal handler를 작성하기 위한 기본적인 가이드라인이다.
- Handler를 최대한 단순하게 유지해야 한다.
- 예를 들어 handler는 단순히 global flag를 설정하고 즉시 반환하도록 만들 수 있다. signal 수신과 관련된 모든 처리를 메인 프로그램에 위임하는 것으로 구현 가능하다.[22]
- Handler에서는 async-signal-safe 함수만 호출해야 한다.
- async-signal-safe 함수: 신호 핸들러 내부에서 호출해도 안전한 함수를 의미한다.
- Handler의 시작과 종료시에 errno 변수를 저장하고 복원해야 한다.
- async-signal-safe 함수들은 에러가 발생하면 errono값을 설정하는데, 이를 handler에서 호출할 경우, errno값을 참조하고 있던 프로그램의 다른 부분들에 영향을 줄 수 있다.
- 따라서 이러한 문제를 해결하기 위해서는 handler 시작시 errno를 지역 변수에 저장하고 종료 직전에 원래의 errno 값으로 복원하여, handler가 errno의 값을 overwrite하지 못하도록 해야한다.[23]
- shared data structure를 접근할 때에는 모든 signal들을 잠시 block하여야 한다.
- shared data structure에 접근하는 instruction은 여러 개의 명령어로 구성된 sequence이므로, 해당 sequence가 handler에 의해서 중단되면 data structure가 비일관적이게 될 뿐 아니라, 해당 handler의 결과 또한 예측할 수 없게 된다.
- 전역 변수를 volatile로 선언해야 한다.
- 예를 들어 handler와 메인 프로그램이 전역 변수 g를 공유한다고 할때, handler가 g를 업데이트하고, 메인 프로그램은 g를 읽는다고 한다. 이때 컴파일러는 메인 프로그램에서 g가 바뀌지 않기 때문에 cache coherence가 유지되고 있다고 착각할 수 있다.
- 이러한 상황을 막기 위해서 g를 volatile을 이용하여 선언하는데,[24] 이는 컴파일러에게 해당 변수를 항상 메모리에서 읽도록 강제한다.
- flag 변수 sig_atomic_t 타입으로 선언해야 한다.
Async-signal-safe
Async-signal-safe 함수는 해당 함수가 signal handler 내부에서 호출해도 안전한 함수이다. 이는 다음 조건 중 하나라도 만족하면 된다.
- Reentrant: 지역 변수만을 사용하고 전역 변수를 사용하지 않는 프로그램이다.
- Non-interruptible by signals: 이는 함수 실행 중에 signal에 의해 interrupt되지 않는다는 것을 의미한다.
Poisx는 총 117개의 함수가 async-signal-safe하다고 보장하며 아래는 그 예시이다.
- _exit, write[28], wait, waitpid, sleep, kill...
다음은 널리 사용되지만 unsafe한 함수들이다.
- printf, sprintf, malloc, exit...
Signal handler에서 안전하게 출력하는 데에는 다음 csapp.c에서의 reentrant SIO[29]를 사용해야 한다.
ssize_t sio_puts(char s[]) // 문자열 출력
ssize_t sio_putl(long v) // long 타입 출력
void sio_error(char s[]) // 에러 메시지 출력 + 종료
Correcting Handler Example
아래는 부모 프로세스에서 N개의 자식 프로세스를 만들고 해당 프로세스가 종료될 때마다, handler를 통해 이를 reap하는 코드와 그 에시 결과이다.
/* 전역 변수 설정 */
int N = 3; //만들 자식 프로세스의 수
int ccount = 0; //남아있는 자식의 수를 추적하는 변수
void child_handler(int sig) { //SIGCHLD signal이 왔을 때, 즉 자식이 종료되었을 때 호출됨
int olderrno = errno; //errno의 save
pid_t pid; //자식 프로세스의 pid
if ((pid = wait(NULL)) < 0) //wait을 통해 자식 프로세스를 수거
Sio_error("wait error"); //reap 과정에서 오류 발생시 error 메시지 출력 후 종료
ccount--; //남아있는 자식의 수는 줄어든다.
Sio_puts("Handler reaped child "); //수거된 자식 프로세스 출력
Sio_putl((long)pid);
Sio_puts(" \n");
sleep(1); //충분한 시간을 두어 디버깅을 하도록 함
errno = olderrno; //errno의 restore
}
void fork14() {
pid_t pid[N]; //자식 프로세스의 pid를 담을 배열
ccount = N; //자식 수 초기화
Signal(SIGCHLD, child_handler); //명시적으로 wait을 통해 처리하지 않고 signal handler를 통해 자식을 reap
for (int i = 0; i < N; i++) { //부모 프로세스는 자식과는 별개로 실행되므로, 되었을 때는 N개의 자식 프로세스가 동시에 실행 중
if ((pid[i] = Fork()) == 0) { //자식 프로세스 호출
Sleep(1);
exit(0); /* Child exits */
}
}
while (ccount > 0); /* Parent spins, ccount == 0일 때까지 무한 루프 */
}
Handler reaped child 23240
Handler reaped child 23241
위 프로그램에서는 분명히 3개의 자식 프로세스를 호출하였지만, 실제로 reap된 자식 프로세스의 개수는 2개 뿐이다. 이는 pending signal은 추가로 queue되지 않기 때문이다. 즉, 위의 예시에서는 프로세스 23240에 대한 SIGCHLD handler가 실행되는 동안 23241에 대한 SIGCHLD handler가 추가로 deliver되어 pending 상태로 저장되었다. 하지만 이 상황에서 프로세스 23242에 대한 SIGCHLD handler는 이미 pending SIGCHLD가 존재하므로 pending signal이 되는 것이 아니라 아예 버려지게 된다. 따라서 실제로 처리되는 SIGCHLD의 개수는 단 2개인 것이다.
이는 signal을 통해서 어떤 event의 수를 count할 수 없음을 알려준다. 따라서 이를 피하여 child_handler를 다시 만들고 결과를 출력하면 아래와 같다.
void child_handler2(int sig)
{
int olderrno = errno;
pid_t pid;
while ((pid = wait(NULL)) > 0) { //wait() 함수는 더 이상 기다릴 child가 없다면 -1을 반환한다.
ccount--;
Sio_puts("Handler reaped child ");
Sio_putl((long)pid);
Sio_puts(" \n");
}
if (errno != ECHILD) //wait() 함수가 더 이상 기다릴 child가 없어서 종료하면 errno는 ECHILD가 된다.
Sio_error("wait error");
errno = olderrno;
}
Handler reaped child 23240
Handler reaped child 23241
Handler reaped child 23242
위 코드는 while 루프 안에서 더 이상 reap할 자식 프로세스가 존재하지 않을 때까지 좀비가 된 자식들을 reap한다. 이를 통해서 signal이 버려지더라도 자식 프로세스는 모두 reap될 수 있다. 하지만 해당 handler는 wait을 통한 메인 프로그램의 block이 while문을 통해 중첩될 수 있어서 메인 프로세스의 재개 시점이 미뤄질 수 있다는 단점이 있다.
Synchronizing Flows to Avoid Races
Write를 포함하는 concurrent flows들이 동일한 저장 공간에 접근할 때는 항상 data race를 유의하여야 한다. 일반적으로, flow들 사이에서 가능한 명령어 순서의 수는 지수적으로 증가한다. 이러한 명령어 순서들 중 일부는 정답을 만들어내고, 나머지는 오답을 만들어낸다. 이때 근본적인 문제는, 각 교차 순서가 모두 올바른 결과를 내도록 하면서, 가능한 많은 수의 명령어 순서를 허용하도록 concurrent flow들을 적절히 동기화 하는 것이다.
아래의 간단한 handler 예시를 살펴보자. 아래에서 부모 프로세스는 현재 실행 중인 자식 프로세스들을 전역으로 설정된 job list를 통해서 추적한다. 이때 addjob()함수는 리스트에 해당 자식 프로세스를 추가하고 deletejob()함수는 리스트에서 자식 프로세스를 제거한다.
int main(int argc, char **argv)
{
int pid;
sigset_t mask_all, prev_all;
Sigfillset(&mask_all); // 모든 signal을 포함하는 set을 만든다.
Signal(SIGCHLD, handler); // SIGCHLD에 대한 handler를 등록한다.
initjobs(); // job list를 초기화 한다.
while (1) { //무한 루프 내에서 자식 프로세스를 생성한다.
if ((pid = Fork()) == 0) {
Execve("/bin/date", argv, NULL); //자식은 date라고 하는 프로그램을 실행한다.
}
/* 부모는 job list에 접근하는 동안 어떤 signal이 와도 addjob()을 하기 전에는 handler가 실행되지 않도록 막는다.
이는 의도되지 않은 job list의 변경(data race)를 막기 위함이다. */
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
addjob(pid);
Sigprocmask(SIG_SETMASK, &prev_all, NULL); //원래대로 blocked set을 되돌린다.
}
exit(0);
}
void handler(int sig)
{
int olderrno = errno; // errno save
sigset_t mask_all, prev_all;
pid_t pid;
Sigfillset(&mask_all); // 모든 signal을 포함하는 set을 만든다.
while ((pid = waitpid(-1, NULL, 0)) > 0) { // 종료되어 있는 모든 자식 프로세스를 reap
/* 메인 프로그램과 마찬가지로, job list에 접근하는 동안
어떤 signal이 와도 addjob()을 하기 전에는 handler가 실행되지 않도록 막는다. */
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
deletejob(pid); /* Delete the child from the job list */
Sigprocmask(SIG_SETMASK, &prev_all, NULL); // addjob() 실행 이후 SIGCHLD를 unblock한다.(원래의 상태로 되돌림)
}
if (errno != ECHILD)
Sio_error("waitpid error");
errno = olderrno;
}
언뜻 보면 위 handler와 메인 프로그램은 매우 완성도가 높은 것 처럼 보인다. block과 unblock을 적절히 활용하여 서로 다른 두 thread가 동시에 같은 공유 자원에 접근하지 못하도록 막았기 때문이다. 하지만 이는 메인 프로그램과 handler가 의도대로 작동하지 않는 특정한 시나리오를 무시한 해석이다. fork() 함수를 통해서 자식프로세스가 만들어지고, block이 되기 전에 자식 프로세스가 그 사이에 종료되는 경우를 고려해보자.
이 경우, 아직 SIGCHLD가 block이 되지 않았기 때문에 signal handler가 동작한다. 이 경우, addjob()을 통해 job list에 해당 자식 프로세스가 추가되지 않았으므로, deletejob()은 error를 발생시키거나 error도 없이 실행을 종료할 수 있다. error가 발생한 경우는 향후 예외 처리를 통해 다룰 수 있지만,(물론 좋지 않다.) error도 발생하지 않은 경우는 문제가 심각해진다. 그 이유는 signal handler의 종료 후 부모는 존재하지도 않는 자식 프로세스를 job list에 저장하고, 이는 앞으로 삭제되지도 않는 항목이 되기 때문이다.
이는 고전적인 동기화 오류(synchronization error)이며, race condition이라고 부른다. 이는 매우 디버깅하기 어려우며, 해당 error가 간헐적으로 발생하기 때문이다. 이와 같은 오류는 아래와 같이 메인 프로그램을 변형하여 해결할 수 있다.
int main(int argc, char **argv)
{
int pid;
sigset_t mask_all, mask_one, prev_one;
Sigfillset(&mask_all); // 모든 signal을 포함하는 set을 만든다.
Sigemptyset(&mask_one); //비어있는 set을 만든다.
Sigaddset(&mask_one, SIGCHLD); //비어있는 set에 SIGCHLD를 등록한다.
Signal(SIGCHLD, handler); // SIGCHLD에 대한 handler를 등록한다.
initjobs(); //job list를 초기화한다.
while (1) {
// 자식이 빨리 종료되더라도 addjob()함수를 실행하기 전에는 SIGCHLD signal이 receive되지 못하도록한다.
Sigprocmask(SIG_BLOCK, &mask_one, &prev_one);
if ((pid = Fork()) == 0) { //자식 프로세스 생성
// 자식 프로세스는 signal block에 대한 제약 조건이 필요가 없으므로 이전으로 되돌린다.
Sigprocmask(SIG_SETMASK, &prev_one, NULL);
Execve("/bin/date", argv, NULL);
}
// 어떤 signal이 와도 addjob()을 하기 전에는 handler가 실행되지 않도록 막는다.
Sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(pid);
Sigprocmask(SIG_SETMASK, &prev_one, NULL); //원래대로 blocked set을 되돌린다.
}
exit(0);
}
위 메인 프로그램에는 기존과는 작은 변화가 생겼다. 이는 아예 자식 프로세스를 생성하기 전에 모든 signal을 block하는 것이다. 이를 통해서 addjob()함수의 실행 전에는 deletejob()함수가 실행될 수 없도록 프로세스와 handler 간의 실행 순서를 강제하여 오류를 해결한다.
Explicitly Waiting for Signals
때때로 메인 프로그램은 특정 시그널 핸들러가 실행될 때까지 명시적으로 기달려야할 필요가 있다. 예를 들어서 리눅스 shell이 foregruond 작업을 실행하고 있을 때, 다음으로 실행할 명령을 입력받기 위해서는 현재 실행되고 있는 작업이 종료되고 SIGCHLD 핸들러에 의해 reap되기를 기다려야 한다. 아래는 이를 구현한 코드이다.
volatile sig_atomic_t pid; //시그널 핸들러 안팎에서 안전하게 접근할 수 있도록 보장된 자료형/pid를 통해 자식 프로세스의 종료 여부 확인
void sigchld_handler(int s) { //SIGCHLD 시그널 핸들러
int olderrno = errno;
pid = Waitpid(-1, NULL, 0); //자식 프로세스를 reap하고 pid에 자식 프로세스 PID 저장
errno = olderrno;
}
void sigint_handler(int s) {} //SIGINT가 와도 아무 행동을 하지 않음 (Ctrl+C 무시 등을 위해 사용)
int main(int argc, char **argv) {
sigset_t mask, prev;
Signal(SIGCHLD, sigchld_handler); //SIGCHLD에 대한 시그널 핸들러 등록
Signal(SIGINT, sigint_handler); //SIGINT에 대한 시그널 핸들러 등록
Sigemptyset(&mask);
Sigaddset(&mask, SIGCHLD); //SIGCHLD 마스크 생성
while (1) {
//Block하는 이유 >>> 부모와 자식 프로세스 사이의 race를 피하기 위해서.
Sigprocmask(SIG_BLOCK, &mask, &prev); //SIGCHLD Block
if (Fork() == 0) //자식 프로세스, 바로 종료
exit(0);
//부모 프로세스
pid = 0;
Sigprocmask(SIG_SETMASK, &prev, NULL); //SIGCHLD Unblock
while (!pid); //pid의 값이 자식 프로세스의 PID로 바뀔 때 까지 무한 루프 >>> CPU 낭비!!!
}
exit(0);
}
위 코드의 부모 프로세스는 자식 프로세스를 실행한 다음 무한 루프로 진입한다. 이후 SIGCHLD를 수신하여 해당 핸들러를 통해 자식 프로세스를 reap하고, 자식 프로세스의 PID를 pid 값에 할당할 때하면 비로소 무한 루프에서 탈출한다. 즉 위 코드에서 무한 루프는 SIGCHLD를 통한 자식 프로세스의 reap을 명시적으로 기다리기 위해서 사용된다. 하지만 위 코드에서 무한 루프는 CPU 자원을 낭비한다. 따라서 위 코드는 아래 두 방식으로 개선될 수 있을 것이다.
while (!pid) //Race!!!
pause();
while (!pid) //Too slow!!!
sleep(1);
하지만 pause() 함수를 이용한 코드는 race condition[30]이 존재한다. pause() 함수는 UNIX/LINUX에서 시그널을 받을 때까지 무기한으로 정지시키는 함수이다. 위 코드에서의 race condition이 존재하는 상황은 다음과 같다.
pid == 0인 상태에서while (!pid)조건을 검사 → 조건 참 → 루프 진입pause()를 호출하려는 순간, 운 나쁘게도 그 직전에 SIGCHLD가 도착.- 시그널 핸들러가 호출되어 자식을
waitpid()로 수거하고, pid에 자식 PID를 설정하고 메인 루틴은 pause()를 호출 - 모든 시그널이 처리된 상태이므로, pause()는 더 이상 깨울 시그널 없이 무한 대기 상태에 빠짐
즉, while 문의 조건을 검사한 이후 pause() 함수 호출 직전에 SIGCHLD 시그널이 부모 프로세스에 도착하면 race가 발생한다. 이 경우 while 문이 없으면 race condition이 해결되는 것처럼 보일 수 있는데, while 문이 필요한 이유는 SIGCHLD 이외의 시그널들을 처리하기 위해서이다. 예를 들어 pause() 함수가 실행되던 중 SIGINT가 도착하였을 때 while 문이 없다면 그대로 부모 프로세스의 SIGCHLD에 대한 명시적인 기다림이 종료된다.
또한 sleep(1)을 이용한 코드는 적절하지만 너무 느리다. 따라서 해당 상황을 해결하기 위한 올바른 해결책은 sigsuspend 함수를 사용하는 것이다.
#include <signal.h>
int sigsuspend(const sigset_t *mask); //return -1
sigsuspend() 함수는 현재의 블록된 시그널 집합을 mask로 일시적으로 교체한 다음, 시그널을 하나 받을 때까지 대기한다. 시그널을 받고 핸들러가 실행된 뒤, sigsuspend() 함수는 리턴되며, 시그널 마스크는 원래대로 복원된다. 즉 아래 코드의 atomic한(인터럽트되지 않는) 버전이다.
sigprocmask(SIG_BLOCK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);
즉, sigsuspend() sigprocmask() 함수 호출과 pause() 함수 호출을 함께 실행해, sigprocmask() 함수 호출 이후 pause() 함수 호출 이전에 시그널이 도착하는 경쟁 조건을 제거한다. 이를 활용하면 메인 함수는 아래와 같이 작성된다.
int main(int argc, char **argv) {
sigset_t mask, prev;
Signal(SIGCHLD, sigchld_handler); //SIGCHLD에 대한 시그널 핸들러 등록
Signal(SIGINT, sigint_handler); //SIGINT에 대한 시그널 핸들러 등록
Sigemptyset(&mask);
Sigaddset(&mask, SIGCHLD); //SIGCHLD 마스크 생성
while (1) {
//Block하는 이유 >>> 부모와 자식 프로세스 사이의 race를 피하기 위해서.
Sigprocmask(SIG_BLOCK, &mask, &prev); //SIGCHLD Block
if (Fork() == 0) //자식 프로세스, 바로 종료
exit(0);
//부모 프로세스
pid = 0;
while(!pid) //조건식 판별 중에는 SIGCHLD는 Block됨
sigsuspend(&prev); //함수의 실행 중에만 SIGSHLD가 Unblock됨
Sigprocmask(SIG_SETMASK, &prev, NULL); //SIGCHLD Unblock
}
exit(0);
}
위 코드에서 sigsuspend() 함수를 호출하기 전에 SIGCHLD는 블록된다. 하지만 sigsuspend() 함수의 실행과 동시에 SIGCHLD는 일시적으로 언블록하고, 부모가 시그널을 받을 때까지 일시정지한다. 또한 sigsuspend() 함수가 종료된 후에는 다시 SIGCHLD는 다시 블록된다. 따라서 pause() 함수의 race condition이 사라진다. 그 이유는 puase() 함수를 통한 구현에서는 while 문의 조건 검사 직후 SIGCHLD가 도착할 수 있었지만(언블록되어 있었으므로) sigsuspend() 함수를 통한 구현에서는 sigsuspend() 함수 밖에서는 SIGCHLD가 도착할 수 없기 때문이다. 즉, sigsuspend() 함수가 실행되는 도중에만 SIGCHLD가 도착할 수 있고, 이 때문에 무한 대기 상태에 빠지지 않는다.
각주
- ↑ /bin/kill 명령어에서 "kill"은 SIGKILL 시그널과 일말의 관련도 없다.
- ↑ 두 개의 명령어를 연결하여, 첫 번째 명령어의 출력 결과를 두 번째 명령어의 입력으로 전달하는 메커니즘이다.
- ↑ 알파벳 순으로 정렬된 파일 목록을 얻는 job이다.
- ↑ 현재 디렉토리의 파일과 디렉토리 목록을 출력하는 명령어이다.
- ↑ sort는 정렬을 의미하는 명령어로, 텍스트 데이터를 정렬하는 데 사용한다.
- ↑ *SIGINT: 프로세스가 종료를 거부하거나 신호를 처리할 수 있는 기회를 제공한다. 기본적으로 사용자가 인터럽트 키인 Ctrl+C를 눌렀을 때 발생한다.
- SIGKILL: 프로세스를 즉시 강제 종료시키는 신호이다. 프로세스가 이를 거부하거나 처리할 수 없으며 무조건 종료된다.
- ↑ 사용자와 그 사용자에 의해 시작된 여러 프로세스를 관리하는 논리적인 단위이다.
- ↑ session을 생성하는 첫번째 프로세스이며, 해당 session 내 다른 프로세스들을 관리하는 역할을 한다.
- ↑ #define N = 5
- ↑ system call에서 돌아오거나 context switch를 완료할 때 등
- ↑ pnb = pending & ~blocked
- ↑ if (|pnb| == 0)
- ↑ 일반적으로 가장 작은 signal number k
- ↑ 프로세스가 종료될 때 해당 프로세스의 메모리, 레지스터 상태 등을 포함한 파일이다. 이 파일을 분석하면, 크래시가 발생한 지점을 추적하거나 오류를 진단할 수 있다.
- ↑ 프로세스가 해당 신호를 수신해도 아무 일도 일어나지 않게 처리된다.
- ↑ SIGCHLD 신호는 자식 프로세스의 종료나 상태 변경을 알리는 신호이며, 기본적으로 무시된다. 즉, 부모 프로세스는 자식 프로세스가 종료되었을 때 특별히 처리할 필요가 없다면 이 신호를 무시할 수 있다.
- ↑ s
- ↑ 비동기적으로
- ↑ block된 signal들의 집합을 나타내는 bit vector이다.
- ↑ 만약 handler와 메인 프로그램이 동시에 동일한 global data structure에 접근하면 종종 치명적인 오류를 초래한다.
- ↑ 동기화나 lock등이 되었었더라면 counter의 값은 정확히 2000이다.
- ↑ 메인 프로그램은 주기적으로 flag를 확인하고 (그리고 재설정)하면 된다.
- ↑ handler가 _exit 함수를 호출해 프로세스를 종ㄹㅅ하는 경우, 이러한 절차는 생략할 수 있다.
- ↑ 예를 들어 volatile int g;와 같이 설정한다.
- ↑ 예를 들어 volatile sig_atomic_t g;와 같이 설정한다.
- ↑ flag 변수는 오직 읽기, 쓰기에만 사용된다. flag = 1은 올바른 사용이지만, flag++은 복합 연산이므로 올바른 사용예시가 아니다.
- ↑ 중단없이, 완전히 실행되거나 아예 실행되지 않거나.
- ↑ write 함수만이 유일하게 안전한 output function이다.
- ↑ Safe I/O library
- ↑ 둘 이상의 프로세스나 스레드의 실행 순서에 따라 그 실행 결과가 달라지는 상황이다.