시스템 프로그래밍 4. 시그널 프로그래밍
건국대학교 시스템 프로그래밍 진현욱 교수님의 수업을 정리한 내용입니다.
Signal
커널이 프로세스에게 보내는 이벤트 신호를 Signal이라고 한다.
운영체제에서 어떻게 구현되어 있을까? 각 프로세스마다 가지고 있는 Process Control Block (PCB)에는 Pending Bit Vector와 Blocked Bit Vector가 존재한다.
1
2
int64_t pendingSignal;
int64_t blockedSignal;
64개의 시그널을 받아야 하므로 64비트의 정수 자료형을 사용한다. 커널이 SIGILL 시그널을 어떤 프로세스에게 보내고 싶으면, 그 프로세스의 PCV에 접근해서 pendingSignal
의 index=3
비트를 1로 변경한다.
프로세스에서 주기적으로 pendingSignal
을 확인해서, 어떤 시그널이 들어왔는지 체크하고, 들어온 시그널이 있다면 그 시그널과 연결된 시그널 핸들러를 실행한다.
이런 구조 때문에, 커널에서 시그널을 보내도 시그널 핸들러가 실행될 때까지 약간의 텀이 생기며, 시그널을 여러개 보내도 그저 1로 바꾸기 때문에 다 씹히고 시그널 핸들러가 한번만 실행된다.
시그널이 들어와도 시그널 핸들러를 실행하고 싶지 않을 수 있다. 이럴떈 Block Bit Vector에 무시할 시그널 ID 위치의 비트를 1로 바꾸면 된다. 계산할땐 아래 계산식을 사용한다.
1
2
3
4
5
6
7
8
9
10
pnd = pending & ~blocked; // pending의 비트가 1이면서 blocked의 비트가 0인 비트만 뽑아낸다.
/*
10101101 // pend
00000001 // blocked
11111110 // ~blocked
pending & ~blocked =>
10101100 // 처리할 signals
*/
Shell에서 Signal을 보내는 법
1
2
3
4
5
6
7
// Signal List를 볼 수 있습니다.
kill -l
// 프로세스 또는 프로세스 그룹에게 시그널을 보냅니다.
// [Signal Id]를 생략하면, 자동으로 SIGKILL 시그널을 보냅니다.
// 앞에 -를 붙이면 프로세스 그룹에게 시그널을 보내고, 붙이지 않으면 프로세스에게 시그널을 보냅니다.
kill [Signal Id] [PID or -PGID]
[!example]- Example 1{title}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 linux> ./forks 16 linux> Child1: pid=24818 pgrp=24817 Child2: pid=24819 pgrp=24817 linux> ps PID TTY TIME CMD 24788 pts/2 00:00:00 tcsh 24818 pts/2 00:00:02 forks 24819 pts/2 00:00:02 forks 24820 pts/2 00:00:00 ps linux> kill -9 -24817 linux> ps PID TTY TIME CMD 24788 pts/2 00:00:00 tcsh 24823 pts/2 00:00:00 ps
[!example]- Example 2{title}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 linux> ./forks 17 Child: pid=24868 pgrp=24867 Parent: pid=24867 pgrp=24867 <ctrl+z 입력> Suspended linux> ps a PID TTY STAT TIME COMMAND 24788 pts/2 S 0:00 -usr/local/bin/tcsh -i 24867 pts/2 T 0:01 ./forks 17 24868 pts/2 T 0:01 ./forks 17 24869 pts/2 R 0:00 ps a bash> fg ./forks 17 <ctrl+c 입력> linux> ps a PID TTY STAT TIME COMMAND 24788 pts/2 S 0:00 -usr/local/bin/tcsh -i 24870 pts/2 R 0:00 ps aCtrl + Z는 프로세스 일시정지 커맨드다.
Signal Handler
sighandler_t signal(int signum, sighandler_t handler)
sighandler_t
: void (int) 함수 포인터를 typedef로 이름만 바꿔놓은 것
프로세스에서 Signal을 받으면, 등록된 Signal Handler를 실행시킨다. 이 Signal Handler는 내가 임의의 함수를 등록할 수 있다. 따로 등록하지 않으면, Default Signal Handler를 실행한다. 예를들어 SIGKILL는 기본 시그널 핸들러로 현재 프로세스를 종료하는 내용을 담은 시그널 핸들러가 연결되어있다.
Signal Handler의 특징은 다음과 같다.
- Handler는 여러개를 등록할 수 없고, 교체되는 방식이다.
- Parent Process가 Signal Handler를 바꾸고 fork를 하면, Child Process의 Signal Handler도 상속받는다.
- Handler 안에서 Blocking(Sleep)는 하지 마라.
- Handler에서 Sleep 걸면 모든 프로세스가 멈춘다.
- 그걸 원하는거면 상관 없지만, 다른 작업은 돌아가면서 시그널 내의 작업만 100초동안 멈추고 싶은거면 Blocking하면 안된다.
- SIGKILL의 Signal Handler는 바꿀 수 없다.
이걸 바꿔버리면 죽지 않는 바이러스같은 프로그램을 만들 수 있으니까 막아둔 듯?
Standard Signal
다른 프로세스 또는 커널로부터 Signal을 받을 수 있다. 시그널을 받으면, Process Context에 들어있는 Bit Vector의 받은 시그널 부분을 1로 바꾼다. 시그널을 받은 Process에서는, Pending Bit Vector가 1로 바뀐걸 보고 해당 시그널의 Signal Handler를 실행한다.
따라서 Standard Signal은
- 여러 시그널이 동시에 들어와도 한번밖에 처리할 수 없다.
시그널이 들어 왔다, 안들어왔다 수준으로 구분하기 때문에
- 시그널에 정보를 담아서 보낼 수 없다.
Send Signal
int kill(pid_t pid, int sig)
pid 프로세스에게 sig 시그널을 보낸다. 보내기에 성공하면 0, 실패하면 -1를 반환한다. 실패한 경우 errno에 오류 코드가 설정된다.
[!example]- 예제 코드{title}
1 2 3 4 5 6 if (kill(pid, SIGUSR1) == 0) { printf("Signal SIGUSR1 sent to process %d\n", pid); } else { perror("Failed to send signal"); exit(EXIT_FAILURE); }
Signal List
시그널 | 값 | 설명 |
---|---|---|
SIGHUP | 1 | 제어 터미널의 종료 또는 제어 프로세스의 종료 감지. |
SIGINT | 2 | 키보드에서의 인터럽트(Ctrl+C). |
SIGQUIT | 3 | 키보드에서의 종료(Ctrl+). |
SIGILL | 4 | 잘못된 명령어(잘못된 기계 코드 또는 연산 코드). |
SIGABRT | 6 | abort() 함수에 의해 보내진 중단 시그널. |
SIGFPE | 8 | 부동소수점 예외(예: 0으로 나누기). |
SIGKILL | 9 | 강제 종료 시그널. 프로세스를 강제로 종료시키며, 잡을 수 없고, 차단할 수 없으며, 무시할 수 없음. |
SIGSEGV | 11 | 잘못된 메모리 참조(세그멘테이션 오류). |
SIGPIPE | 13 | 깨진 파이프: 읽는 프로세스가 없는 파이프에 쓰기 시 발생. |
SIGALRM | 14 | alarm() 함수에서 발생한 타이머 시그널. |
SIGTERM | 15 | 종료 시그널(기본적으로 kill 명령에서 보내는 시그널). |
SIGUSR1 | 10 | 사용자 정의 시그널 1. |
SIGUSR2 | 12 | 사용자 정의 시그널 2. |
시그널 | 값 | 설명 |
---|---|---|
SIGCHLD | 17 | 자식 프로세스가 종료되거나 중단될 때 부모 프로세스에 전달됨. |
SIGCONT | 18 | 중단된 프로세스의 실행을 재개. |
SIGSTOP | 19 | 프로세스를 중단시킴(잡거나 무시할 수 없음). |
SIGTSTP | 20 | 터미널에서 중단(Ctrl+Z). |
SIGTTIN | 21 | 백그라운드 프로세스가 터미널에서 읽기를 시도할 때 발생. |
SIGTTOU | 22 | 백그라운드 프로세스가 터미널에 쓰기를 시도할 때 발생. |
Change Signal Handler
시그널 핸들러를 등록하기 위해선 먼저 void (int) 형식의 함수를 만들어야 한다.
1
2
3
4
5
void example_handler(int sig)
{
printf("My Sig Number: %d\n", sig);
_exit(0); // Sighandler를 끝내려면 이걸 쓰면 됨. exit(0)을 쓰면 프로세스가 다 종료되버린다
}
이후 signal()
함수에 시그널과 handler를 넣어서 시그널 핸들러를 변경하면 된다. 교체 후, 전에 있던 핸들러를 반환한다. 교체를 실패하면 오류를 반환한다. 각 시그널에는 Default Signal Handler가 존재한다.
[!tip] Handler Constant{title}
- SIG_IGN : ignore(무시) 함수
- SIG_DFL : 이 시그널의 Default 함수로 바꿔달라. ex) signal(25, SIG_DFL)
사용하고 싶다면, handler 인자에 상수 코드를 써서 넘기면 된다.
Signal Handler를 사용할 땐 항상 Race Condition를 신경써줘야 한다.
[!example]- Child Process가 종료되면 자동으로 Reaping 해주는 예제{title}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 int main() { signal(SIGCHLD, handler); // Signal Handler Change sigset_t mask_all; sigset_t mask_one, prev_one; sigfillset(&mask_all); sigemptyset(&mask_one); sigaddset(&mask_one, SIGCHLD); while (1) { sigprocmask(SIG_BLOCK, &mask_one, &prev_one); if ((pid = fork()) == 0) { /* Child */ sigprocmask(SIG_SETMASK, &prev_one, NULL); execve("/bin/date", argv, NULL); } sigprocmask(SIG_BLOCK, &mask_all, NULL); addjob(pid); sigprocmask(SIG_SETMASK, &prev_one, NULL); } } void handler(int sig) { int olderrno = errno; pid_t pid; sigset_t mask, prev_mask; sigfillset(&mask); while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) { sigprocmask(SIG_BLOCK, &mask, &prev_mask); deletejob(pid); sigprocmask(SIG_SETMASK, &prev_mask, NULL); } if (errno != ECHILD) sio_error("waitpid error"); errno = olderrno; }
Real-Time Signal
Standard Signal의 단점을 보완하고자 만든 Signal이다.
- SIgnal Queueing이 지원된다.
(같은 시그널을 여러개 받을 수 있음.)
- 시그널에 데이터를 담아 보낼 수 있다.
다만 단점도 존재한다.
- Standard Signal은 모든 OS에서 다 갖고 있기 때문에 portable (범용적으로)하게 사용할 수 있다.
- Realtime Signal은 간혹 지원해주지 않는 OS가 있어서 범용성은 살짝 떨어진다.
Send Signal
int sigqueue(pid_t pid, int sig, const union sigval value)
pid
: 시그널을 받을 프로세스의 PID.pid > 0
: 해당 PID의 프로세스에 시그널을 보낸다.pid == 0
: 시그널을 보내는 프로세스와 같은 프로세스 그룹에 시그널을 보낸다.pid == -1
: 시스템 내의 모든 프로세스에 시그널을 보낸다.pid < -1
: 절대값을 PGID로 인식하고, 해당 그룹에 시그널을 보낸다.
sig
: 시그널 번호value
: 시그널과 함께 보낼 데이터.
1
2
3
4
union sigval {
int sival_int;
void *sival_ptr;
}
구조체 안에 정수형 데이터를 담아서 보내거나, 포인터 데이터를 받아서 보낼 수 있다.
[!example]- 정수형 데이터 보내는 예제 코드{title}
1 2 3 4 5 6 7 8 9 pid_t target_pid = 12345; union sigval value; value.sival_int = 42; // 전달할 정수 값 if (sigqueue(target_pid, SIGUSR1, value) == 0) { printf("SIGUSR1 시그널과 함께 값 %d를 성공적으로 보냈습니다.\n", value.sival_int); } else { perror("sigqueue 실패"); }
[!example]- 임의의 데이터 포인터로 보내는 예제 코드{title}
1 2 3 4 5 6 7 8 9 10 11 12 13 union sigval value; // 메시지를 포인터로 설정 char *message = "Hello, this is a pointer message!"; value.sival_ptr = (void *)message; // SIGUSR1 시그널과 함께 포인터를 전송 if (sigqueue(pid, SIGUSR1, value) == 0) { printf("SIGUSR1 signal with message sent to process %d\n", pid); } else { perror("sigqueue failed"); exit(EXIT_FAILURE); }
프로세스끼리 ‘SIGUSR1는 char* 타입을 주고받는 시그널이다’ 라는 약속이 있어야 한다.
시그널을 받는 쪽에서는 SIGUSR1이 들어오면, 시그널 핸들러에서 데이터 값을 char*
로 해석해야 데이터를 받을 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void handle_signal(int sig, siginfo_t *siginfo, void *context) {
printf("Received signal %d\n", sig);
// 전달받은 포인터 값을 처리
if (siginfo->si_code == SI_QUEUE) {
char *message = (char *)siginfo->si_value.sival_ptr;
printf("Received message: %s\n", message);
}
}
int main() {
struct sigaction act;
act.sa_sigaction = &handle_signal; // 확장된 핸들러 설정
act.sa_flags = SA_SIGINFO; // 확장된 시그널 핸들러 사용 설정
// SIGUSR1 시그널에 대해 sigaction 설정
if (sigaction(SIGUSR1, &act, NULL) < 0) {
perror("sigaction");
return 1;
}
printf("Waiting for SIGUSR1 signal (PID: %d)...\n", getpid());
// 무한 대기
while (1) {
pause(); // 시그널을 받을 때까지 대기
}
return 0;
}
시그널 종류
Realtime Signal Number는 SIGRTMIN과 SIGRTMAX 사이의 범위에서 사용된다. Linux에서는 SIGRTMIN은 34번, SIGRTMAX는 64번이다. Standard Signal과 다르게, 각 시그널에는 미리 정의된 의미가 없으므로 34번 ~ 64번 Signal Number는 프로그래머가 마음대로 가져다가 쓸 수 있다.
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main() {
printf("SIGRTMIN: %d\n", SIGRTMIN);
printf("SIGRTMAX: %d\n", SIGRTMAX);
return 0;
}
- Standard Signal도 Realtime Signal 방법으로 보낼 수 있다.
Change Signal Handler
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
signum
: 시그널 번호act
: realtime signal handler 정보가 담겨있는 구조체.oldact
: 이전의 siganl handler는 oldact에 담겨서 반환된다. 필요 없으면 NULL.
1
2
3
4
5
6
struct sigaction {
void (*sa_handler)(int); // 시그널 처리 함수 포인터 또는 SIG_IGN, SIG_DFL
void (*sa_sigaction)(int, siginfo_t *, void *); // 확장된 시그널 처리 함수 (siginfo 사용)
sigset_t sa_mask; // 시그널 처리 중 블록할 시그널 집합
int sa_flags; // 시그널 처리 옵션
};
등록에 성공하면 0, 실패하면 -1을 반환한다. 실패시 errno에 코드가 기록된다.
sigaction.sa_handler 또는 sicaction.sa_sigaction 둘중 하나에 시그널 핸들러를 넣으면 된다.
- sa_handler : 기존과 같은
void (int)
타입의 시그널 핸들러. 데이터를 받을 필요가 없으면 이걸 사용한다. - sa_sigaction : 보내진 데이터를 처리할 수 있는 시그널 핸들러.
void (int, siginfo_t*, void*)
타입의 함수 포인터다. - sa_mask : 이 시그널 핸들러가 실행되는 동안 블락하고 싶은 시그널 Mask.
- sa_flags
- SA_NOCLDSTOP :
SIGCHLD
전용 Flag. 자식 프로세스가 중지(Pause)되었을 때는 SIGCHLD를 보내지 않도록 한다. 죽었을 때만 보내도록 한다. - SA_NOCLDWAIT :
SIGCHLD
전용 Flag. 자식 프로세스가 종료된 후 좀비 프로세스 상태로 남지 않게 한다. 부모 프로세스는 wait(), waitpid()로 Reaping할 필요가 없다. - SA_SIGINFO : 이 Flag가 설정되면, sa_handler 대신 sa_sigaction을 사용하게 됨. 즉 시그널 핸들러에서 추가적인 데이터를 받을 수 있게 한다.
- SA_ONESHOT / SA_RESETHAND : 시그널 핸들러를 1회용으로 설정한다. 즉 한번 처리한 후 Default Signal Handler로 돌아간다.
- SA_RESTART : 기존에 실행중인 System Call이 시그널로 인해 중단되었다면, 기존의 동작중인 System Call을 자동으로 다시 시작하도록 한다.
- SA_NODEFER : 시그널 핸들러가 실행되는 동안에도 동일한 시그널이 발생될 수 있게 한다.
기본값은, 시그널 핸들러가 실행중이면 동일한 시그널은 차단
- 여러 플래그를 동시에 적용하고 싶다면,
SA_SIGINFO | SA_RESTART
이렇게.
- SA_NOCLDSTOP :
[!example]- 시그널 핸들러 설정하는 예제 코드{title}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> void handle_signal(int sig, siginfo_t *siginfo, void *context) { printf("Received signal %d from process %d\n", sig, siginfo->si_pid); } int main() { struct sigaction act; act.sa_sigaction = &handle_signal; // 확장된 핸들러 설정 act.sa_flags = SA_SIGINFO | SA_RESTART; // SIGUSR1 시그널에 대해 sigaction 설정 if (sigaction(SIGUSR1, &act, NULL) < 0) { perror("sigaction"); return 1; } printf("Waiting for SIGUSR1 signal...\n"); // 무한 대기 while (1) { pause(); // 시그널을 받을 때까지 대기 } return 0; }
[!example]- SA_NOCLDWAIT로 자동 Reaping 설정하는 예제 코드{title}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> static volatile sig_atomic_t childCount = 0; void sigchld_handler(int sig) { sigset_t mask, prev_mask; sigfillset(&mask); sigprocmask(SIG_BLOCK, &mask, &prev_mask); printf("Child Count : %d\n", --childCount); sigprocmask(SIG_SETMASK, &prev_mask, NULL); } int main() { sigset_t mask_all; sigset_t mask_one, prev_one; sigfillset(&mask_all); sigemptyset(&mask_one); sigaddset(&mask_one, SIGCHLD); struct sigaction act; act.sa_handler = sigchld_handler; act.sa_flags = SA_NOCLDWAIT | SA_RESTART; if (sigaction(SIGCHLD, &act, NULL) < 0) { perror("sigaction"); exit(1); } sigprocmask(SIG_BLOCK, &mask_one, &prev_one); if (fork() == 0) { /* Child */ sigprocmask(SIG_SETMASK, &prev_one, NULL); // 자식에선 굳이 막아둘 필요가 없기 떄문에 바로 해제함. printf("Child process running\n"); sleep(2); printf("Child process exiting\n"); exit(0); } else { /* Parent */ sigprocmask(SIG_BLOCK, &mask_all, NULL); childCount++; sigprocmask(SIG_SETMASK, &prev_one, NULL); printf("Parent process waiting for child to exit...\n"); sleep(5); printf("Parent process exiting\n"); } return 0; }
[!tip] Tip{title}
1 sigaction(__SIGNAL__, &new_act, &old_act);
__SIGNAL__
을 define으로 정의하고, 컴파일할 때__SIGNAL__
을 어떤 시그널로 할건지 인자값으로 넣을 수 있다.
1 gcc -o rt_sig.out signal_example.c -D__SIGNAL__=SIGRTMIN+12
Race Condition
공유된 자원에 동시에 접근할 때 발생하는 문제를 통칭한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void my_sighandler(int sig)
{
...
*i = *i + value;
...
}
int main(void)
{
...
for(k=0; k<N; k++)
{
*i = *i + value;
}
}
예를 들어, C Code를 Assembly Language 수준에서 살펴보자.
my_sighandler:
...
movq (%rax), %ecx
addl %eax, %ecx
movq %ecx, (%rax)
...
main:
...
movq (%rax), %edx
addl %eax, %edx
movq %edx, (%rax)
...
메모리에 있는 i의 값을 연산을 위해 register에 가져와야 하고, local variable로 갖고있는 value 값을 더하는 코드이다. 이 Code의 문제점은, 두 함수에서 \(i\) 변수를 공유해서 사용하고 있다는 것!
[!error] Race Condition Simulation{title}
- i = (%rax) = 1
- my_sighandler value = %eax = 1
- main value = %eax = 1 라고 해보자.
main 함수에서
addl %eax, %edx
까지 실행했는데, 이 타이밍에 Signal이 발생하여my_sighandler()
함수가 실행되면 문제가 발생한다.메인 함수에서 레지스터에 value를 더하는 것까지 했지만,
i
에 덮어쓰지 않았기 때문에i
의 값은 1이다.이 상태에서,
my_sighandler
에서도i
에 1을 더하는 연산을 수행하고 다시 main 함수로 돌아온다. 현재i
는 2가 들어있다.main 함수의
movq %edx, (%rax)
가 실행되면 전에 갖고있던%edx
의 값은 2이므로 이걸i
에 덮어쓴다.원래 의도한 것은 main에서
i
에 1을 더하고 시그널 함수에서도 1을 더해서 총i
가 3이 되어야하지만, Race Condition이 발생하여i
의 값이 2가 되는 버그가 발생한다.
Race Condition은 정말 특별한 경우에만 발생한다. 그래서 평소에 버그없다고 생각하고 넘어갈 수 있는데, 갑자기 버그가 터진다. 그렇다고 이 버그 상황을 재현하는것도 너무 힘들다.
Shared Resource에 접근하는 코드 부분을 Critical Section라고 한다. Race Condition을 해결하기 위해, 공유 자원을 사용중일 땐 다른 곳에서 접근을 못하게 막는 작업을 Mutal Exclusion 라고 한다. Mutal Exclusion를 보장하면, 코드가 Atomicity하게 실행된다고 한다.
안전하게 Signal Handler 사용하는 방법
1. async-signal-safe한 함수만 사용하자.
exit, write, wait, waitpid, sleep, kill. … 등 local variable만 사용하는 함수가 async-signal-safe한 함수이다. man 7 signal
명령어를 통해 async-signal-safe한 함수 리스트를 확인할 수 있다.
[!example] non-safe한 함수{title} printf, sprintf, malloc, exit, …
예를들어 pause()
대신 sigsuspend(&mask)
를 사용하는 것이다. 이 함수는 기본적으로 pause()
와 같다. 그냥 pause()를 쓰게 되면, pause() 되는 도중에 Sighandler가 실행되서 pid값을 바꿔버리면 영영 pause() 상태에서 해제되지 않는다. 따라서, pause를 Signal Handler에 대해 Atomic함을 보장해주는 sigsuspend()
를 사용하면 된다.
2. errno 값을 처음에 저장해뒀다가 끝날때 쯤 덮어쓰자.
1
2
3
4
5
6
7
8
9
10
void handler(int sig)
{
int olderrno = errno;
/*
...
*/
errno = olderrno;
}
errno를 딱 사용하려고 할 때 시그널 핸들러가 호출되어서 시그널 핸들러 내용이 실행되는 도중 다른 errno 값에 의해 덮어씌워지게 되면, 내가 원하던 동작이 일어나지 않을 수 있다.
3. Signal Handler와 Main 함수 간 공유하는 자료구조가 있다면..
Race Condition을 신경써야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int main(int argc, char **argv)
{
int pid;
signal(SIGCHLD, handler);
initjobs();
while (1)
{
if ((pid = fork()) == 0) {
/* Child */
execve("/bin/date", argv, NULL);
}
/* Parent */
addjob(pid); /* Add the child to the job list */
}
exit(0);
}
void handler(int sig)
{
int olderrno = errno;
pid_t pid;
while ((pid = waitpid(-1, NULL, WNOHANG)) > 0)
{
deletejob(pid); /* Delete the child from the job list */
}
if (errno != ECHILD)
sio_error("waitpid error");
errno= olderrno;
}
위 코드는, Child Process List를 하나 만들어 리스트에서 추가(addjob)/삭제(deletejob)하며 자식 프로세스를 관리하는 예제 코드이다. 위 코드는 두가지 문제가 존재한다.
- 메인에서 자료구조에 접근하여 사용 도중 시그널 핸들러가 자료구조를 변경하고 다시 메인으로 돌아왔을 때, 이상한 동작을 할 수 있음.
- Fork하는 도중에 Child가 종료되어 deletejob가 먼저 실행되고 addjob이 실행되는 경우.
- addjob(pid)가 실행되기도 전에 Child가 종료되어 Handler가 실행되는 경우.
이 경우 이미 종료된 프로세스가 리스트에 계속 남아있는 문제가 발생한다.
[!success] 메인에서 자료구조를 쓰고있는 동안에는, Signal이 들어와도 Handler가 실행되지 않도록 Block해두면 된다.{title}
1
2
3
4
5
6
7
8
9
10
11
12
sigset_t mask, prev_mask;
sigemptyset(&mask); // Signal 집합 'mask'을 비웁니다.
sigfillset(&mask); // Signal 집합 'mask'에 모든 신호를 추가합니다.
sigaddset(&mask, SIGINT); // SIGINT 신호를 Signal 집합에 추가합니다.
sigdelset(&mask, SIGINT); // SIGINT 신호를 Signal 집합에서 제거합니다.
// mask에 등록된 Signal를 차단하고 이전 Signal 마스크를 prev_mask에 저장합니다.
sigprocmask(SIG_BLOCK, &mask, &prev_mask);
// 이전 Signal 마스크로 복원하여 차단을 해제합니다.
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
위 함수들을 사용하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
sigset_t mask, prev_mask;
sigfillset(&mask); // Signal 집합 'mask'에 모든 신호를 추가합니다.
// ...
sigprocmask(SIG_BLOCK, &mask, &prev_mask);
addjob(pid);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
// ...
sigprocmask(SIG_BLOCK, &mask, &prev_mask);
deletejob(pid);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
이렇게 사용하면 한쪽에서 자료구조를 쓰고 있는 동안에 Signal Handler가 실행되어 간섭될 여지는 제거했다.
[!success] Fork가 다 될때까지는 SIGCHLD를 무시해서 순서를 보장받자.{title}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sigset_t mask_all;
sigset_t mask_one, prev_one;
sigfillset(&mask_all);
sigemptyset(&mask_one);
sigaddset(&mask_one, SIGCHLD);
//...
while (1)
{
sigprocmask(SIG_BLOCK, &mask_one, &prev_one);
if ((pid = fork()) == 0) { /* Child */
sigprocmask(SIG_SETMASK, &prev_one, NULL);
execve("/bin/date", argv, NULL);
}
sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(pid); /* Add the child to the job list */
sigprocmask(SIG_SETMASK, &prev_one, NULL);
}
만약 fork 도중에 SIGCHLD가 실행되더라도, BLOCK되어 있으므로 SIGCHLD의 Signal Handler는 실행되지 않는다. 그대로 Perent Process로 넘어가 addjob(pid)를 실행하고 Block했던걸 해제한다. 이때 SIGCHLD도 같이 해제되며, deletejob(pid)가 이때 실행된다.
4. Signal Handler && Main에서 동시에 사용하는 전역 변수는 volatile sig_atomic_t 타입으로 선언하라.
1
volatile sig_atomic_t flag = 0;
[!question] What is volatile?{title} 변수의 값을 매번 메모리에서 읽고, 한번 계산하고 다시 메모리에 덮어쓰고, 다시 읽고, 한번 계산하고 다시 메모리에 덮어쓰고, … 이 과정은 비효율적이다.
따라서 한번 메모리에서 불러온 뒤, 레지스터에서 값을 이것저것 업데이트하고 최종적으로 메모리에 덮어쓰면 효율적이다.
다만 이 과정중에 다른곳에서 개입해서 메모리에 있는 값을 수정하면, 나중에 레지스터에서 메모리에 덮어쓸 때 수정된 값이 날아가버린다.
volatile 타입으로 선언하면, 이 최적화 과정을 생략하고 항상 메모리에서 읽고 한번 계산하고 다시 덮어쓰는 비효율적인 방식을 사용한다.
[!question] What is sig_atomic_t{title} 변수가 Flag 변수라면,
sig_atomic_t
를 사용해주는게 좋다.변수를 read, write에 대해 atomic함이 보장되는 데이터 타입으로 사용하겠다. 보통 int가 사용된다.
++
과 같은 연산은sig_atomic_t
타입으로 선언해도 atomic함이 보장받지 못한다. 변수값이 저장된 메모리에서 값을 레지스터로 부르고, 레지스터에 값을 1 증가하고, 다시 메모리에 써야하는데, 단계가 3단계나 있다.이 사이에 얼마든지 시그널 핸들러같은게 비집고 들어올 수 있다.
[!question] 어차피 읽고 쓰는건 mov 하나로 다 되는거 아닌가? 굳이 쓰는 이유가 뭘까?{title} 그렇지 않은 자료형도 있기 때문이다.
long long같은 자료형은 너무 크기 때문에 두번 mov를 하게되는데, 그 사이에 Signal Handler가 간섭이 충분히 가능하다.