signal은 이벤트가 일어났을 때 발생했다는 사실을 프로세스에게 전달해주는 software notification다.
명령어 kill을 주면 목적지로 시그널을 준다.
- -l option은 available symbolic signal names의 list들을 준다.
프로세스는 도착한 시그널에 대해서 처리를 해줘야한다.
→ 시그널이 왔을 때 프로세스가 취해줘야 되는 default action이 있다.
터미널에서 ctrl+c → interrupt signal이 발생해서 지금 돌고 있는 프로세스에게 전달이 된다. 그러면 프로세스는 하던 작업을 멈추고 default action을 수행한다.(여기서는 강제 종료)
Generating Signals
#include <signal.h>
int kill(pid_t pid, int sig);
마지막 형태의 kill은 signal 0에 대해 0, SIGHUP에 대해 1, SIGINT에 대해 2, SIGQUIT에 대해 3, SIGABRT에 대해 6, SIGKILL에 대해 9, SIGALRM에 대해 14, SIGTERM에 대해 15의 signal_number 값을 지원합니다.
예시: kill -9 3423
pid parameter
- target process ID
- 0
- -1
return value
- 0 → successful, -1 → unsucessful
시그널을 받았을 때 무엇을 해야하는가?
프로세스는 시그널이 언제 도착할지 모른다.
- 시그널이 언제 도착하든 프로세스가 하던 작업에 방해가 되지 않고 시그널을 받을 수 있게 대비를 해야한다.
시그널이 오는 것을 막고 싶다 : signal mask
벽 안에 막고 싶은 signal들을 집어넣을 수 있다.
나에게 오는 시그널을 도착하도록 허용하든가, 못 오게 막을 수 있다.
sigfrommask 함수를 활용
sigaction 함수를 통해서 시그널이 왔을 때 어떻게 해야 할지 정할 수 있음
signal을 파라미터로 넘길 때 여러 signal을 설정해야 될 때 signal sets를 활용한다.
#include <signal.h> int sigaddset(sigset_t* set, int signo);
→ sigset 안에 signal들을 넣을 수 있다.
int sigdelset(sigset_t* set, int signo);
→ sigset 안에 signal을 뺄 수 있다.
int sigemptyset(sigset_t* set);
→ sigset 초기화
int sigfillset(sigset_t* set); int sigismember(const sigset_t* set, int signo);
Signal masks
#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
함수 sigprocmask로 시그널을 넣을 수도 뺄 수도 있다.
- 첫 번재 파라미터 how로 판단할 수 있다.
- SIG_BLOCK : signal mask에 두번째 파라미터에 있는 sigset을 추가하자.
- 만약에 같은 숫자의 시그널이 프로세스에 접근하면 막힌다.(막힌 시그널 → pending signal, pending signal list에 들어가서 대기하게 된다.)
- SIG_UNBLOCK: signal mask에 두번째 파라미터에 있는 sigset을 빼자.
- SIG_SETMASK: signal mask에 있는 signal들을 전부 빼고 두번째 파라미터에 있는 signal들을 집어넣자.(교체)
- 마지막 파라미터 oset의 용도: output 파라미터, 시그널 마스크가 변경되기 전 상태를 세번째 oset에 담아서 output으로 전달한다.)
- 다시 원래대로 돌릴려면 sigprocmask를 다시 실행해서 oset을 두번째 파라미터로 주면 된다.
Catching and ignoring signals
시그널이 왔을 때 default action이 아닌 다른 action을 하고싶다면?
#include <signal.h>
int sigaction(int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);
→ 시그널이 왔을 때 무슨 action을 취할지 등록하는 함수(시그널 핸들러 함수)
세번째 파라미터 oact는 oset과 같이 원래 상태를 담는다.
시그널 핸들러 함수는 아무 이름으로 써는데 파라미터와 리턴 타입은 정의가 되있다.
파라미터: int
리턴 타입: void
그 외에도 다른 옵션들을 정의할 수 있게 구조체가 제공이 된다.
struct sigaction{
void (*sa_handler)(int); /* SIG_DFL, SIG_IGN or pointer to function */
sigset_t sa_mask; /* additional signals to be blocked during execution of handler*/
int sa_flags; /* special flags and options */
void (*sa_sigaction) (int, siginfo_t *, void *); /* realtime handler */
}
void (*sa_handler)(int) → 시그널 핸들러 등록하는 자리
**SIG_DFL**은 해당 시그널에 대해 기본 동작을 수행하도록 지정한다.
**SIG_IGN**은 해당 시그널을 무시하도록 지정한다.
프로세스는 메인 함수에 어느 부분을 수행하다가 시그널이 올지 알 수 없다.
→ 시행하던 작업을 멈추고 signal handler 함수를 부른다. 보통의 경우에는 문제가 잘 안생기지만, 문제가 생기는 경우가 있다.
예시)
doneflag에 접근하는 코드는 critical section(임계 영역)이다. 왜냐하면 시그널 핸들러가 해당 값을 수정할 수 있고, 메인 함수에서도 그 값을 조사하기 때문이다.
→ 다중 스레드가 하나의 변수를 동시에 접근할 때 발생한 문제가 나타날 수 있다.
메인 함수에서 건드리고 있었던 변수를 시그널 핸들러에서 건드리지 못하게 해야한다.
- doneflag는 int인데, doneflag를 sig_atomic_t로 선언한다. 그러면 커널 레벨에서 원자적인 operation을 보장한다.(이 operation을 더 이상 쪼갤 수 없다. → 방해받지 않고 끝까지 수행할 수 있다.)
(volatile 한정자는 컴파일러에게 해당 변수가 프로그램 실행과 비동기적으로 변경될 수 있음을 알려준다.)
다른 예시)
이번에는 int가 아니라 배열이다. → sig_atomic_t 사용 불가능.
메인 함수에서도, 시그널 핸들러에서도 배열에 접근한다.
→ signal mask를 사용해서 시그널이 접근하지 못하게 제어하면 된다.
Waiting for signals
사전에 필요한 일을 다했고, 이제 시그널만 받으면 될 때, 시그널을 기다리는 기능은 어떻게 구현해야 할까?
시그널을 기다리는 함수들:
pause(), sigsuspend(), sigwait()
(pause()는 문제가 발생할 여지가 있음)
#include <unistd.h>
int pause(void);
pause()는 아무 시그널이나 와도 return이 된다. 그래서 while문으로 감싸줘서 내가 기다리는 시그널인지 판별해준다.
static volatile sig_atomic_t sigreceived = 0;
while( sigreceived == 0 )
pause();
sigsuspend()
#include <signal.h>
int sigsuspend(const sigset_t* sigmask);
시그널 마스크를 sigmask가 가리키는 마스크로 설정하고, 프로세스가 시그널을 잡을 때까지 프로세스를 중단시킨다. sigmask 매개변수는 프로그램이 찾고 있는 시그널을 언블록하는 데 사용될 수 있다. 반환되면 시그널 마스크는 sigsuspend가 호출되기 전의 값으로 재설정된다.
sigwait()
#include <signal.h>
int sigwait(const sigset_t *restrict sigmask, int *restrict signo);
- set: 기다릴 시그널 집합을 나타내는 sigset_t 구조체의 포인터
- sig: 발생한 시그널 번호가 저장될 포인터
작동 방식:
- sigwait 함수는 전달된 시그널 집합 **set**에 포함된 시그널 중 하나가 발생할 때까지 블록된다.
- 시그널이 발생하면 해당 시그널 번호가 **sig**에 저장되고, 함수는 반환된다.
- 시그널은 발생해도 디폴트 동작이 수행되지 않고 블록되어 있는 **sigwait**에서 처리된다.
suspend()와의 차이점
- sigmask 매개변수는 기다릴 시그널 집합을 유지하므로 집합에 있는 시그널만이 sigwait를 반환할 수 있다.
- 프로세스 시그널 마스크를 변경하지 않는다.
- sigwait를 호출하기 전에 sigmask에 있는 시그널은 차단되어 있어야 한다.
- 사용 방식:
- sigsuspend: 현재의 시그널 마스크를 변경하고 특정 시그널이 발생할 때까지 대기. 대기하는 동안에는 시그널이 블록되어 처리되지 않습니다. 대기가 끝나면 이전의 시그널 마스크로 복원된다.
- sigwait: 지정된 시그널 집합에 포함된 시그널 중 하나가 발생할 때까지 대기한다. 시그널이 발생하면 해당 시그널 번호를 반환하고, 시그널은 블록되지 않고 디폴트 동작이 수행되지 않는다.
- 인자 및 반환값:
- sigsuspend: 시그널 마스크를 변경하므로 **sigsuspend**는 따로 시그널 집합을 명시적으로 지정하지 않는다. 반환값은 항상 -1이며, 시그널이 처리되면 중단되는 경우가 많다.
- sigwait: 기다리는 동안에는 시그널이 블록되지 않기 때문에, **sigwait**은 시그널 집합을 명시적으로 지정해야 합니다. 반환값은 시그널이 발생할 때 해당 시그널의 번호가 된다.
- 대기 동작:
- sigsuspend: 특정 시그널이 발생할 때까지 대기하고, 대기 중에 다른 시그널은 블록되어 처리되지 않는다.
- sigwait: 지정된 시그널 집합에 포함된 시그널 중 하나가 발생할 때까지 대기하고, 다른 시그널은 블록되지 않는다.
- 활용:
- sigsuspend: 시그널 마스크를 임시로 변경하여 특정 시그널을 처리하고자 할 때 사용된다.
- sigwait: 특정 스레드에서 특정 시그널을 처리하도록 지정할 때, 멀티스레드 환경에서 편리하게 사용된다.
Async-signal safe 함수: signal에 안전한 함수(많지 않다)
printf도 Async-signal safe 함수가 아니다.
'Study > 시스템프로그래밍' 카테고리의 다른 글
[시스템프로그래밍] 스레드 (0) | 2023.12.04 |
---|---|
[시스템프로그래밍] 시간과 타이머 (0) | 2023.12.03 |
[시스템프로그래밍] pipe에 관하여 (0) | 2023.11.02 |
[시스템프로그래밍] 디렉토리 엔트리, hard/symbolic link, stat에 관하여 (0) | 2023.11.02 |
[시스템프로그래밍] 프로그램, 프로세스, 스레드에 관하여 (2) | 2023.10.26 |