포스트

시스템 프로그래밍 4. 시그널 프로그래밍

시스템 프로그래밍 4. 시그널 프로그래밍

건국대학교 시스템 프로그래밍 진현욱 교수님의 수업을 정리한 내용입니다.

Signal

커널이 프로세스에게 보내는 이벤트 신호를 Signal이라고 한다.

운영체제에서 어떻게 구현되어 있을까? 각 프로세스마다 가지고 있는 Process Control Block (PCB)에는 Pending Bit VectorBlocked Bit Vector가 존재한다.

1
2
int64_t pendingSignal;
int64_t blockedSignal;

Pasted image 20240919205859.png

64개의 시그널을 받아야 하므로 64비트의 정수 자료형을 사용한다. 커널이 SIGILL 시그널을 어떤 프로세스에게 보내고 싶으면, 그 프로세스의 PCV에 접근해서 pendingSignalindex=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 a

Ctrl + 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의 특징은 다음과 같다.

  1. Handler는 여러개를 등록할 수 없고, 교체되는 방식이다.
  2. Parent Process가 Signal Handler를 바꾸고 fork를 하면, Child Process의 Signal Handler도 상속받는다.
  3. Handler 안에서 Blocking(Sleep)는 하지 마라.
    • Handler에서 Sleep 걸면 모든 프로세스가 멈춘다.
    • 그걸 원하는거면 상관 없지만, 다른 작업은 돌아가면서 시그널 내의 작업만 100초동안 멈추고 싶은거면 Blocking하면 안된다.
  4. SIGKILL의 Signal Handler는 바꿀 수 없다.
    • 이걸 바꿔버리면 죽지 않는 바이러스같은 프로그램을 만들 수 있으니까 막아둔 듯?

Standard Signal

다른 프로세스 또는 커널로부터 Signal을 받을 수 있다. 시그널을 받으면, Process Context에 들어있는 Bit Vector의 받은 시그널 부분을 1로 바꾼다. 시그널을 받은 Process에서는, Pending Bit Vector가 1로 바뀐걸 보고 해당 시그널의 Signal Handler를 실행한다.

따라서 Standard Signal은

  1. 여러 시그널이 동시에 들어와도 한번밖에 처리할 수 없다. 시그널이 들어 왔다, 안들어왔다 수준으로 구분하기 때문에
  2. 시그널에 정보를 담아서 보낼 수 없다.

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

시그널설명
SIGHUP1제어 터미널의 종료 또는 제어 프로세스의 종료 감지.
SIGINT2키보드에서의 인터럽트(Ctrl+C).
SIGQUIT3키보드에서의 종료(Ctrl+).
SIGILL4잘못된 명령어(잘못된 기계 코드 또는 연산 코드).
SIGABRT6abort() 함수에 의해 보내진 중단 시그널.
SIGFPE8부동소수점 예외(예: 0으로 나누기).
SIGKILL9강제 종료 시그널. 프로세스를 강제로 종료시키며, 잡을 수 없고, 차단할 수 없으며, 무시할 수 없음.
SIGSEGV11잘못된 메모리 참조(세그멘테이션 오류).
SIGPIPE13깨진 파이프: 읽는 프로세스가 없는 파이프에 쓰기 시 발생.
SIGALRM14alarm() 함수에서 발생한 타이머 시그널.
SIGTERM15종료 시그널(기본적으로 kill 명령에서 보내는 시그널).
SIGUSR110사용자 정의 시그널 1.
SIGUSR212사용자 정의 시그널 2.
시그널설명
SIGCHLD17자식 프로세스가 종료되거나 중단될 때 부모 프로세스에 전달됨.
SIGCONT18중단된 프로세스의 실행을 재개.
SIGSTOP19프로세스를 중단시킴(잡거나 무시할 수 없음).
SIGTSTP20터미널에서 중단(Ctrl+Z).
SIGTTIN21백그라운드 프로세스가 터미널에서 읽기를 시도할 때 발생.
SIGTTOU22백그라운드 프로세스가 터미널에 쓰기를 시도할 때 발생.

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이다.

  1. SIgnal Queueing이 지원된다. (같은 시그널을 여러개 받을 수 있음.)
  2. 시그널에 데이터를 담아 보낼 수 있다.

다만 단점도 존재한다.

  • 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 이렇게.

[!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)하며 자식 프로세스를 관리하는 예제 코드이다. 위 코드는 두가지 문제가 존재한다.

  1. 메인에서 자료구조에 접근하여 사용 도중 시그널 핸들러가 자료구조를 변경하고 다시 메인으로 돌아왔을 때, 이상한 동작을 할 수 있음.
  2. Fork하는 도중에 Child가 종료되어 deletejob가 먼저 실행되고 addjob이 실행되는 경우.
  3. 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가 간섭이 충분히 가능하다.