-
[시스템프로그래밍] Signal Handling시스템프로그래밍 2023. 12. 17. 01:52
https://scwcart.tistory.com/entry/curses
curses
이번 글에서는 화면 공간을 프로그래밍하는 curses library에 대해서 알아볼 예정입니다. 초창기 컴퓨터 시절, 우주를 탐험하는 컨셉의 비디오 게임이 하나 나왔습니다. 이 게임을 구현하기 위해서
scwcart.tistory.com
https://scwcart.tistory.com/entry/Timer
Timer
https://scwcart.tistory.com/entry/curses curses 이번 글에서는 화면 공간을 프로그래밍하는 curses library에 대해서 알아볼 예정입니다. 초창기 컴퓨터 시절, 우주를 탐험하는 컨셉의 비디오 게임이 하나 나왔
scwcart.tistory.com
지난 글들에서 1) Space programming, 2) Time handling, 3) Signal handling, 4) IPC 중 앞 2개를 알아보았습니다.
이번 글에서는 3번째인 Signal handling에 대해 알아보겠습니다.
Signal
signal은 프로세스에 대한 알림입니다.
일단 프로세스가 signal을 받으면, 하던 수행을 멈추고 signal을 처리하러 가기 때문에 software interrupt라고 하기도 합니다.
신호를 처리하는 방법에는 2가지가 있는데요,
1) signal() : 신호의 처리를 설정하기 위한 원래 API로, sigaction()보다 더 간단한 인터페이스를 제공합니다.
2) sigaction(): signal()로는 사용할 수 없는 기능을 제공합니다. UNIX 구현에 따라 signal()의 동작에 차이가 있습니다.
먼저 signal()부터 볼까요?
커널은 다양한 이벤트에 응답하기 위해 프로세스에 신호를 보냅니다.
사용자의 키 입력, 프로세스의 비정상적인 행위, 타이머 경과 등이 모두 커널이 응답해야 하는 이벤트들에 해당할 수 있습니다.
지난 글에서도 한번 다루었었는데요, signal handling은 다음과 같이 이루어집니다.
1) Default(주로 종료): signal(SIGALRM, SIG_DFL)
2) Ignore signal: signal(SIGALRM, SIG_IGN)
3) Invoke function(user-defined): signal(SIGALRM, handler)
이에 대한 자세한 내용은 다음 글을 참고해 주시기 바랍니다.
https://scwcart.tistory.com/entry/Signals
Signals
https://scwcart.tistory.com/entry/User-Program User Program https://scwcart.tistory.com/entry/Termianal-Driver Termianal Driver 이번 글에서는 터미널 드라이버에 대해 알아보겠습니다. 터미널 드라이버를 다루기 전에, 간단
scwcart.tistory.com
signal()은 실제로 sigaction() 시스템 호출 위에 계층화된 라이브러리로서, glibc에서 구현됩니다.
glibc는 GNU C 라이브러리를 줄인 말로 유닉스 시스템에서 사용되는 주요 C 라이브러리 버전 중 하나인데 여기서 그렇게 중요한 내용은 아니니 그냥 이런 게 있다 정도만 알고 넘어가면 될 것 같습니다.
아무튼 위에서 보이는 것처럼 signal() 함수는 2개의 인자를 받습니다.
1) int sig: 변경하려는 신호가 무엇인지 식별합니다.
즉, signal() 함수가 받아야 하는 signal입니다. 위의 signal model에서는 SIGALRM을 받았습니다.
2) void (*handler) : signal을 받았을 때 호출해야 하는 함수의 주소입니다.
위의 signal model에서는 SIG_DFL, SIG_IGN, 사용자 정의 함수 등을 호출했습니다.
신호가 한 번에 하나만 온다면 위의 signal model들이 정상적으로 작동하겠지만,
만약 한 프로세스가 여러 개의 신호를 수신하면 어떻게 될까요? 이런 경우 여러 가지 의문점들이 생깁니다.
Q1. signal handler은 사용 후에 비활성화되나요?
Q2. 프로세스가 SIGX 핸들러에 있는 동안 SIGY가 도착하면 어떻게 되나요?
Q3. 프로세스가 아직 SIGX 핸들러에 있는 동안 두 번째 SIGX가 도착하면 어떻게 되나요? 세 번째 SIGX도 도착하면?
Q4. 프로세스가 getchar()이나 read()같은 입력을 차단하고 있을 때 신호가 도착하면 어떻게 되나요?
위 의문들을 해결하기 위해 프로세스가 신호를 여러 개를 받도록 프로그램을 짜서 실험해보겠습니다.
#include <stdio.h> #include <unistd.h> #include <string.h> #include <signal.h> #define INPUTLEN 100 void inthandler(int); void quithandler(int); int main(int ac, char *av[]) { void inthandler(int); void quithandler(int); char input[INPUTLEN]; int nchars; signal(SIGINT, inthandler); signal(SIGQUIT, quithandler); do { printf("\nType a message\n"); nchars = read(0, input, (INPUTLEN-1)); if (nchars == -1) perror("read returned an error"); else { input[nchars] = '\0'; printf("You typed: %s", input); } } while( strncmp(input, "quit", 4) != 0); return 0; } void inthandler(int s) { printf(" Received signal %d .. waiting\n", s); sleep(2); printf(" Leaving inthandler \n"); } void quithandler(int s) { printf(" Received signal %d .. waiting\n", s); sleep(3); printf(" Leaving quithandler \n"); }
사용자 입력을 받아 그대로 출력하는 프로그램인데,
사용자 입력으로 signal 문자가 들어오면 해당 핸들러로 이동하는 프로그램입니다.
그럼 실행시켜서 signal 문자를 한번 넣어보겠습니다.
Q1. signal handler은 사용 후에 비활성화되나요?
만약 inthandler 함수가 비활성화되었다면 2번째 Ctrl+C가 들어왔을 때 SIG_DFL로 가서 프로그램이 종료되었겠죠?
하지만 종료되지 않고 계속해서 inthandler를 호출하는 모습을 볼 수 있습니다.
즉, handler는 호출되고 나서 항상 reset합니다.
Q2. 프로세스가 SIGX 핸들러에 있는 동안 SIGY가 도착하면 어떻게 되나요?
프로세스가 quit 핸들러에 있는 동안 int가 도착하면 어떻게 되는지 보기 위해
Ctrl-\ 신호를 처리하고 있는 도중에 Ctrl-C를 입력해보았습니다.
quithandler로 갔다가, inthandler로 갔다가, 다시 quithandler로 돌아와서 마무리하는 것을 볼 수 있습니다.
inthandler의 처리 시간이 quithandler의 처리 시간보다 짧아서 그런 게 아닐까 하고 inthandler의 sleep시간을 5초로 주었는데,
동일하게 inthandler의 처리가 끝난 후 quithandler가 끝나는 결과를 볼 수 있었습니다.
Q3. 프로세스가 아직 SIGX 핸들러에 있는 동안 두 번째 SIGX가 도착하면 어떻게 되나요? 세 번째 SIGX도 도착하면?
재귀적으로 같은 핸들러 함수를 호출합니다.
사실 이상적인 방법으로는, 뒤에 오는 신호는 차단하는 것이 좋습니다.
Q4. 프로세스가 getchar()이나 read()같은 입력을 차단하고 있을 때 신호가 도착하면 어떻게 되나요?
다시 정상으로 돌아갑니다.
이러한 기존의 signal system에는 2가지의 약점이 있습니다.
W1) signal이 전송된 이유를 모르는 경우
signal handler에게 어떤 시그널이 호출되었는지(ex. inthandler - SIGINT, quithandler - SIGQUIT)는 알려주지만,
SIGINT, SIGQUIT signal이 왜 생성되었는지는 알려주지 않습니다.
W2) handler에 있는 동안에는 다른 signal을 안전하게 차단할 수 없습니다.
위에서 본 것처럼, quithandler를 처리하고 있는 동안 SIGINT가 오면 inthandler로 이동합니다.
처리 중 다른 함수로 이동을 방지하기 위해 inthandler를 수행하고 있는 동안 SIGQUIT를 차단하도록 코드를 바꾸어 보겠습니다.
void inthandler(int s) { int rv; void (*prev_qhandler) (); prev_qhandler = signal(SIGQUIT, SIG_IGN); ... signal(SIGQUIT, prev_qhandler); }
inthandler를 수행하는 동안 SIGQUIT가 오면 해당 signal은 무시합니다.
하지만 여기서도 문제가 생깁니다.
문제 1) SIGINT와 SIGQUIT 순차적으로 발생한다면 저 코드가 잘 실행되겠지만, 두 signal이 동시에 발생하면 어떻게 될까요?
문제 2) SIGQUIT를 무시하는 게 아니라, SIGINT가 처리될 때까지만 잠시 보류해두고 싶다면 어떻게 해야 할까요?
이런 문제들을 해결하기 위해서는 기존 signal() 보다 좀 더 확장된 기능을 제공하는 sigaction()이 필요합니다.
sigaction()
sigaction()은 signal을 대체하는 POSIX입니다.
POSIX란, Portable Operating System Interface로 운영체제 간의 호환성을 유지하기 위해 IEEE에서 지정한 표준 제품군입니다.
command line shell 및 utility Interface와 함께 유닉스/리눅스 및 기타 운영체제 변형과의 SW 호환성을 위한 API를 정의합니다.
다른 운영체제에서 실행되는 시스템 프로그램에서 발생할 수 있는 비호환성 문제를 최소화하기 위해 노력합니다.
무슨 말인지 모르겠죠?
그냥 운영체제 간 호환성을 지원하는 제품군 중 하나로 sigaction()이 포함된다 정도만 알면 될 것 같습니다.
중요한 것은 sigaction()은 처리할 '어떤' 신호와 해당 신호에 '어떻게' 응답할지 지정하는 방식으로 동작한다는 점입니다.
sigaction()은 사용자 정의 신호 처리를 지원하는 sigaction 구조체를 가집니다.
이 구조체는 old-style 혹은 new-style handler를 어떻게 호출할지를 명시합니다.
//old-style struct sigaction action; action.sa_handler = handler_old; //new-style struct sigaction action; action.sa_sigaction = handler_new;
그럼 커널에게 old-style 혹은 new-style handler를 사용하고 싶다고 어떻게 알려 줄 수 있을까요?
위 코드에 보이는 것처럼 이미 스타일별로 어떻게 호출해야 할지는 구조체 안에 인터페이스가 다 정의되어 있기 때문에,
프로그래머는 sigaction 구조체의 sa_flags에 SA_SIGINFO 비트를 설정해주기만 하면 됩니다.
sa_flags에 대해 좀 더 알아볼까요?
sa_flags는 4가지 질문에 대해 핸들러가 응답하는 방식을 제어하는 비트 집합입니다.
1) SA_RESETHAND: 핸들러 함수에 진입할 때 신호 처리 방식을 SIG_DFL(old-style)로 리셋합니다.
SA_SIGINFO(new-style) 플래그는 지웁니다.
2) SA_NODEFER: signal을 처리하고 있는 도중에 같은 신호가 발생됐을 때, 자동으로 차단하지 않습니다.
3) SA_RESTART: interrupt된 시스템 콜 호출이 자동으로 재시작됩니다.
4) SA_SIGINFO: 핸들러 함수는 기본적으로 sa_sigaction의 값을 사용하는데,
이 비트가 설정되어 있지 않으면 sa_handler의 값(old-style)을 사용합니다.
sa_sigaction 값을 사용하면 해당 핸들러 함수는 signal number뿐만 아니라 signal이 생성된 이유와 방법에 대한 정보를 포함하는 구조체 포인터도 함께 전달합니다.
sa_mask라는 데이터 손상 방지를 위한 필드도 있습니다.
sa_mask는 핸들러에 있는 동안 다른 신호를 차단할지 결정합니다(blocked로 설정하면 차단).
결정 여부와 함께 차단할 신호 집합도 함께 포함합니다.
그럼 sigaction을 활용한 프로그램을 하나 짜 보도록 하겠습니다.
#include <stdio.h> #include <unistd.h> #include <signal.h> #define INPUTLEN 100 void inthandler(int); int main() { struct sigaction newhandler; sigset_t blocked; //차단할 신호 집합 void inthandler(); char x[INPUTLEN]; //load these tew memberf first newhandler.sa_handler = inthandler; newhandler.sa_flags = SA_RESETHAND | SA_RESTART; sigemptyset(&blocked); sigaddset(&blocked, SIGQUIT); newhandler.sa_mask = blocked; if ( sigaction(SIGINT, &newhandler, NULL) == -1 ) perror("sigaction"); else while(1) { fgets(x, INPUTLEN, stdin); printf("input: %s", x); } return 0; } void inthandler(int s) { printf("Called with signal %d\n", s); sleep(s); printf("done handling signal %d\n", s); }
기존에 정의해 뒀던 inthandler 함수를 사용할 거라 sa_handler를 설정해 주었고, sa_flags도 SA_RESETHAND값을 주었네요.
sigaddset()으로 차단할 신호 집합에 SIGQUIT 을 넣어 준 것도 볼 수 있습니다.
sa_mask도 blocked로 설정해 핸들러에 있는 동안 다른 signal은 차단하도록 설정했습니다.
일반적인 입력에 대해서는 sigaction 함수로 처리해 입력값을 받아오고,
Ctrl-C가 들어오면 설정해뒀던 대로 sa_handler에 설정해둔 inthandler로 가서 signal을 처리하고,
Ctrl-\가 들어오면 차단할 신호 집합에 있기 때문에 default인 프로그램 종료로 이어집니다.
한 프로그램을 구성하는 코드에서, 이렇게 signal 처리 등의 원인으로 중단되었을 때
불완전하거나 손상된 데이터가 생성될 수 있는 경우 데이터 구조를 수정하는 코드 섹션을 Critical section이라고 합니다.
즉 sa_mask가 설정되면 이 플래그는 Critical section을 보호합니다.
Critical section이 항상 존재하는 것은 아니지만, 우리는 코드를 짤 때 Critical section이 될 수 있는 부분을 결정하고,
Signal 처리 등 데이터 손상을 발생시킬 수 있는 코드를 짤 때 Critical section 보호를 고려해서 코딩해야 합니다.
Critical section을 보호하는 가장 간단한 방법이 바로 process로 오는 signal을 무시하거나 차단하는 방법입니다.
그럼 차단할 신호 집합에 대해 좀 더 자세히 알아볼까요?
Blocking Signals: sigprocmask(), sigsetops()
우리는 signal을 차단할 때
1) signal-handler level에서 signal을 차단할 수 있고,
2) process level에서 signal을 차단할 수 있습니다.
첫 번째로, signal-handler 레벨에서 어떻게 signal을 차단할 수 있을까요?
지난 글에서 봤던 것처럼 sa_mask 멤버를 설정해서 signal을 차단할 수 있습니다.
두 번째로, proces level에서는 signal을 어떻게 차단할 수 있을까요?
프로세스에는 signal mask라고 하는 blocking signal 집합이 있습니다.
이 집합 내부 내용을 변경하기 위해서는(즉 차단할 signal 리스트를 변경하기 위해서는) sigpromask()를 사용할 수 있습니다.
sigprocmask() 함수는
1) 어떤 signal을 차단할지 signal set을 받고,
2) 그 set을 이용해 blocking signal set을 변경합니다.
blocking signal set을 변경할 때는 다른 프로세스에서 signal mask를 변경할 수 없도록 원자 연산을 사용합니다.
이를 이용해 차단할 signal을 추가할 수도 있고, 이미 차단된 signal을 다시 차단하지 않도록 제거할 수도 있습니다.
sigprocmask는 다음과 같이 정의됩니다.
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
첫 번째 자리인 how 자리에는 mask에 적용할 변경 사항을 알려줍니다.
sig_block, sig_unblock, sig_setmask 등의 인자가 들어갈 수 있습니다.
두 번째와 세 번째 자리에는 sigset_t의 포인터가 정의되어 있습니다.
이것들은 무엇을 의미할까요?
sigset_t는 signal을 추가하거나 제거하는 메서드가 있는 추상적 신호 집합입니다.
어떤 메서드들이 정의되어 있는지 볼까요?
- sigaddset(sigset_t* setp, int signum): setp가 가리키고 있는 set에 signum을 추가합니다.
- sigdelset(sigset_t* setp, int signum): setp가 가리키고 있는 set에서 signum을 제거합니다.
- sigfillset(sigset_t* setp): setp가 가리키고 있는 리스트에 모든 signal을 추가합니다.
- sigemptyset(sigset_t* setp): setp가 가리키고 있는 리스트에서 모든 signal을 삭제합니다.
전부 종합해서 한 번 사용해 볼까요?
sigset_t sigs, prevsigs; // signal set 정의 sigemptyset( &sigs ); // 설정할 signal set 초기화 sigaddset( &sigs, SIGINT ); // 무시할 signal로 SIGINT, SIGQUIT 추가 sigaddset( &sigs, SIGQUIT ); sigprocmask( SIGBLOCK, &sigs, &prevsigs); // sigprocmask로 설정할 signal set 전달 // .. modify data structure here .. sigprocmask( SIG_SET, &prevsigs, NULL); // 다시 prevsigs로 복원
sigset_t에 정의된 메서드를 이용해 sigset을 설정해서
sigprocmask로 전달합니다.
다음 글에서는 마지막 4번째인 IPC를 보도록 하겠습니다.
끝!
'시스템프로그래밍' 카테고리의 다른 글
[시스템프로그래밍] Inter-process communication (1) 2023.12.18 [시스템프로그래밍] Timer (1) 2023.12.16 [시스템프로그래밍] Curses (1) 2023.12.16 [시스템프로그래밍] Wait (0) 2023.12.14 [시스템프로그래밍] Process Management (0) 2023.12.13