[Linux_System_programming] 10 시그널

Linux System Programming (Robert Love) chapter 10 Signal

  • 시그널은 비동기 이벤트 처리를 위한 메커니즘을 제공하는 소프트웨어 인터럽트(시그널의 발생/처리 모두 비동기)
  • 시그널은 초창기 부터 있었고 신뢰성 측면에서 향상을 이루었고(사라지거나 하지 않게) 사용자 데이터를 전달할 수 있도록 발전
  • POSIX에서 시그널 처리를 표준화함

시그널 개념

  • 시그널의 생명 주기 : 시그널 발생 -> 커널은 해당 시그널 전달 가능 시까지 쌓아둠 -> 커널은 가능 시점에 시그널 처리
  • 커널은 프로세스 요청에 따라 세가지 중 한가지 동작 수행
    • 시그널 무시 : 아무동작 하지 않음(SIGKILL, SIGSTOP은 무시 안됨)
    • 시그널 처리 : 커널은 프로세스 현재 코드 실행 중지 후 등록된 시그널 핸들러 수행(SIGKILL, SIGSTOP은 잡을 수 없음)
    • 기본동작 수행 : 기본 동작은 시그널에 따라 다름(대부분 프로세스 종료)

시그널 식별자

  • 모든 시그널은 SIG라는 접두어로 시작하는 상징적 이름이 있으며 <signal.h>파일에 정의 됨(#define 양의 정수 형태)
  • 시그널 번호는 1에서 시작, 대략 31개의 시그널이 있음(0은 NULL Signal, 아무런 행동도 정의되지 않음)

리눅스에서 지원하는 시그널

  • 책에 리눅스에 지원하는 시그널 종류 및 자세한 설명

시그널 관리 기초

  • 시그널 관리를 위한 가장 단순하고 오래된 인터페이스는 signal() 함수
1
2
3
4
5
#include <signal.h>

typedef void (*sighandler_t)(int)

sighandler_t signal (int signo, sighandler_t handler);
  • signal()은 호출이 성공하면 signo 시그널을 받았을 때 수행할 handler를 설정
  • handler의 인자는 signo와 같은 시그널 식별자
  • handler는 함수 포인터 외 SIG_DFL(동작을 기본값으로 설정)과 SIG_IGN(시그널 무시)도 설정 가능
  • signal() 함수는 호출 성공 시 이전 handler 또는 SIG_DFL, SIG_IGN을 반환, 에러 발생 시 SIG_ERR 반환(errn 설정 없음)

모든 시그널 기다리기

1
2
3
#include <unistd.h>

int pause(void);
  • pause() 시스템 콜은 프로세스를 종료시키는 시그널을 받을 때까지 해당 프로세스를 잠재움(디버깅/테스트용 코드 작성 시 유용)
  • pause()는 붙잡을 수 있는 시그널을 받았을 때만 반환, -1을 반환하고 errno를 EINTR로 설정(무시된 시그널을 받은 경우 안깨어남)

예제

  • 책에 예제 있음

실행과 상속

  • fork() 시스템 콜을 통해서 프로세스 생성 시 자식 프로세스는 부모 프로세스의 시그널에 대한 동작(무시, 기본동작, 핸들러)을 상속, 대기중인 시그널은 상속되지 않음
  • exec 시스템 콜을 통해서 프로세스 생성 시 모든 시그널은 부모 프로세스가 무시하는 경우를 제외하고 기본동작으로 설정, 대기중인 시그널은 상속
  • 쉘이 백그라운드에서 프로세스 실행 시 새로 실행되는 프로세스는 인터럽트 문자와 종료 문자를 무시해야 함 (SIGINT와 SIGQUIT가 SIG_IGN으로 설정해야함, 두시그널이 무시되지 않음을 확인하기 위해 시그널 동작을 설정해봐야 한다는 것은 singal() 인터페이스의 결점)

시그널 번호를 문자열에 맵핑하기

1
extern const char* const sys_siglist[];
  • sys_siglist는 시그템에서 지원하는 시그널 이름을 담고 있는 문자열 배열, 시그널 번호를 색인으로 이용
  • 대안으로 BSD/linux에서 지원하는 psignal(), strsignal()이 있으나 보통 sys_siglist가 최선임

시그널 보내기

1
2
3
4
#include <sys/types.h>
#include <signal.h>

int kill (pid_t pid, int signo);
  • kill() 시스템콜은 특정 프로세스에서 다른 프로세스로 시그널 전송
    • pid > 0이면 큰 경우 pid가 가리키는 프로세스에 signo시그널 전송
    • pid = 0이면 호출한 프로세스의 프로세스 그룹에 속한 모든 프로세스에 signo 시그널 전송
    • pid = -1이면 호출한 프로세스가 시그널을 보낼 권한이 있는 모든 프로세스에 signo를 보냄(호출한 프로세스 자신과 init는 제외)
    • pid < -1이면 프로세스 그룹-pid에 signo 시그널 전송
  • 호출이 성공하면 0을 반환(시그널 하나라도 전송 시 성공으로 간주) 실패하면 -1을 반환 및 errno를 설정(EINVAL, EPERM, ESRCH)

권한

  • CAP_KILL 기능이 있는 프로세스(보통 root 프로세스)는 모든 프로세스에 시그널을 보낼 수 있음
  • 상기 기능이 없다면 사용자는 자신이 소유하고 있는 프로세스에만 시그널을 보낼 수 있음
    • 시그널을 보내는 프로세스의 유효 사용자 ID나 실제 사용자 ID는 시그널을 받는 프로세스의 실제 사용자 ID나 저장된 사용자 ID와 동일해야 함
  • signo가 0(NULL signal)이면 호출은 시그널을 보내지 않지만 에러검사를 수행, 시그널을 보낼 수 있는 권한이 있는지 검사할 때 유용

예제

  • 책에 시그널을 보내는 예제와 시그널을 보낼 수 있는 권한이 있는지 검사하는 예제 있음

자신에게 시그널 보내기

1
2
3
4
#include <signal.h>

int raise(int signo);
// kill( getpid(), signo) 와 같다
  • raise()는 자기 자신에게 시그널을 보내는 함수
  • 호출 성공시 0, 실패하면 0이 아닌 값을 반환, errno는 설정하지 않음

프로세스 그룹 전체에 시그널 보내기

1
2
3
4
#include <signal.h>

int killpg(int pgrp, int signo);
// kill(-pgrp, signo)와 같다
  • killpg()는 프로세스 그룹에 속한 모든 프로세스에 시그널을 보냄 (프로세스 그룹ID를 음수로 바꿔서 kill() 사용하는 대신)
  • pgrp가 0인 경우 호출하는 프로세스의 그룹에 속한 모든 프로세스에 signo로 지정한 시그널을 보낸다.
  • 호출이 성공하면 0을 반환, 실패시 -1반환 및 errno를 설정(EINVAL, EPERM, ESRCH)

재진입성

  • 시그널은 소프트웨어 인터럽트이므로 시그널 핸들러에서 글로벌 데이터/공유데이터를 손대지 않는 것이 바람직(다음절에서 일시적으로 공유데이터를 안전하게 처리하는법(시그널 블록)을 설명)
  • 일부 함수는 확실히 재진입이 가능하지 않으므로 주의

재진입이 가능한 함수

  • 시그널 핸들러 작성시 중단된 프로세스가 재진입이 불가능한 함수를 수행하는 중이었다고 가정할 것
  • 그래서 시그널 핸들러는 반드시 재진입이 가능한 함수만 이용해야 함
  • 책에서 시그널 사용 시 안전하게 재진입이 가능한 함수 목록을 기술하고 있음

시그널 모음

  • 시그널 집합 연산은 프로세스가 블록한 시그널 모음이나 프로세스에 대기 중인 시그널 모음을 관리
1
2
3
4
5
6
7
8
9
#include <signal.h>

int sigemptyset (sigset_t* set);
int sigfillset (sigset_t* set);

int sigaddset(sigset_t* set, int signo);
int sigdelset(sigset_t* set, int signo);

int sigismember(const sigset_t* set, int signo);
  • 하기 함수는 sigset 초기화 함수, 두 함수 모두 0 반환하며 시그널 모음을 사용하려면 두 함수 중 하나를 먼저 호출해야 함
    • sigemptyset()은 set으로 지정한 시그널 모음을 비어있다고 표시하여 초기화
    • sigfillset()은 set으로 지정한 시그널 모음을 가득 차 있다고 표시하여 초기화
  • 하기 함수는 sigset 추가 삭제 함수, 두 함수 모두 성공 시 0 반환, 실패 시 -1 반환 및 errno 설정(EINVAL)
    • sigaddset()은 set으로 지정한 시그널 모음에 signo를 추가
    • sigdelset()은 set으로 지정한 시그널 모음에 signo를 삭제
  • sigismember()는 set으로 지정한 시그널 모음에 signo가 있으면 1을 반환, 없으면 0반환, 에러시 -1 반환 및 errno 설정(EINVAL)

추가적인 시그널 모음 함수

  • 리눅스는 POSIX외 비표준 함수 제공 (POSIX 호환이 중요한 프로그램에서는 사용하지 말 것)
1
2
3
4
5
6
#define _GNU_SOURCE
#include <signal.h>

int sigisemptyset(sigset_t* set);
int sigorset(sigset_t* dest, sigset_t* left, sigset_t* right);
int sigandset(sigset_t* dest, sigset_t* left, sigset_t* right);
  • sigisemptyset()은 set으로 지정된 시그널 모음이 비어 있는 경우에는 1을, 아닌 경우 0을 반환
  • sigorset()은 시그널 모음인 left와 right의 합집함(이진 OR)을 dest에 넣고 sigandset()은 교집함(이진 AND)을 dest에 넣음
    • 두 함수 모두 성공하면 0을 반환, 에러 발생 시 -1 반환 및 errno를 설정(EIVAL)

시그널 블록

  • 시그널 핸들러와 프로그램의 다른 부분이 데이터를 공유해야 할 때(크리티컬 섹션) 시그널 전달을 보류하여 영역을 보호(= 시그널 블록)
  • 프로세스가 블록한 시그널 모음을 해당 프로세스의 시그널 마스크라 함
1
2
3
#include <signal.h>

int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
  • sigprocmask()는 how 값에 따라 다르게 동작
    • SIG_SETMASK : 호출한 프로세스의 시그널 마스크를 set으로 변경
    • SIG_BLOCK : 호출한 프로세스의 시그널 마스크에 set에 포함된 시그널 추가
    • SIG_UNBLOCK : 호출한 프로세스의 시그널 마스크에서 set에 포함된 시그널을 제거
  • oldset이 NULL이 아니면 이전 시그널 모음을 oldset에 넣음
  • set이 NULL인 경우, how를 무시하고 마스크를 변경하지 않으나 oldset에는 넣음
  • 호출이 성공하면 0을 반환, 실패하면 -1을 반환 및 errno를 설정(EINVAL, EFAULT)
  • SIGKILL이나 SIGSTOP은 블록할 수 없으며 sigprocmask()는 두 시그널을 추가하려는 시도는 무시함

대기 중인 시그널 조회하기

  • 커널에서 블록된 시그널이 발생할 경우, 이 시그널은 전달되지 않음, 이러한 시그널을 pending 시그널
  • pending 시그널은 시그널 블록이 해제되면 커널은 이른 프로세스에 넘겨 처리하게 함
1
2
3
#include <signal.h>

int sigpending (sigset_t* set);
  • sigpending()은 호출이 성공하면 대기중인 시그널 모음을 set에 넣고 0을 반환, 실패 시 -1 반환 및 errno 설정(EFAULT)

여러 시그널 기다리기

1
2
3
#include <signal.h>

int sigsuspend (const sigset_t* set);
  • sigsuspend()는 프로세스가 자신의 시그널 마스크를 일시적으로 변경 후 시그널 발생시 까지 대기, 시그널이 프로세스를 종료시키는 경우 반환되지 않음
  • 시그널이 발생해서 이를 처리한 경우 시그널 핸들러가 반환한 후에 sigsuspend()는 -1를 반환 및 errno를 EINTR로 설정
  • sigsuspend()의 활용 방법은 프로그램이 크리티컬 섹션에 머물러 있을 때 도착해서 블록되었던 시그널 조회
    • sigprosmask()를 호출 시 이전 마스크를 oldset에 저장, 크리티컬 섹션 빠져나온 후 oldset으로 sigsuspend() 호출

고급 시그널 관리

  • signal() 함수는 매우 기초적이나 sigaction()이 더 많은 능력(POSIX 표준)
    • 사용하면 핸들러가 동작하는 동안 지정한 시그널 블럭, 시그널 수신 시점의 시스템과 프로세스에 대한 넓은 데이터 조회 등
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <signal.h>

struct sigaction {
  void (*sa_handler)(int);   /* 시그널 핸들러 또는 동작 */
  void (*sa_sigaction)(int, siginfo_t*, void*)
  sigset_t sa_mask;          /* 블록할 시그널 */
  int sa_flags;              /* 플래그 */
  void (*sa_restorer)(void); /* 사용되지 않으며 POSIX표준이 아님 */ 
}

int sigaction(int signo, const struct sigaction* act, struct sigaction* oldact)
  • sigaction() 호출하면 signo로 지정한 시그널의 동작 방식을 변경(signo에는 SIGKILL/SIGSTOP 제외한 모든 시그널 설정 가능)
  • act가 NULL이 아닌 경우 시스템 콜은 해당 시그널의 현재 동작 방식을 act가 지정한 내용으로 변경
  • oldact가 NULL이 아닌 경우 해당 호출은 이전의 동작방식을 oldact에 저장
  • sigaction 구조체는 시그널을 세세히게 제어 가능
    • sa_handler 필드는 해당 시그널을 받았을 때 수행할 동작을 지정, sighandler_t와 동일
    • sa_flag에 SA_SIGINFO를 설정하면 sa_handler가 아니라 sa_sigaction이 시그널을 처리하는 함수를 명시(형식은 다름, 책을 보자)
    • sa_mask 필드는 시그널 핸들러를 실행하는 동안 시스템이 블록해야할 시그널 모음을 제공(SA_NODEFER 플래그 미설정 시 현재 처리중인 시그널도 블록됨)
    • sa_flags 필드는 플래그에 대한 비트 마스크, signo로 지정한 시그널의 처리를 변경(SA_NOCLDSTOP 등 책에 리스트/설명 있으므로 확인 할 것)
  • sigaction() 호출이 성공하면 0을 반환, 실패 시 -1을 반환 및 errno 설정(EFAULT, EINVAL)

siginfo_t 구조체

  • sa_sighandler 대신 sa_sigaction을 이용하는 경우 siginfo_t 구조체는 시그널에서 훨씬 많은 기능 및 정보(시그널 원인 등)를 제공
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
typedef struct siginfo_t{
  int si_signo;       /* 시그널 번호 */
  int si_errno;       /* errno 값 */
  int si_code;        /* 시그널 코드 */
  pid_t si_pid;       /* 보내는 프로세스의 pid */
  uid_t si_uid;       /* 보내는 프로세스의 실제 uid */
  int si_status;      /* 종료 값이나 시그널 */
  clock_t si_utime;   /* 소비된 사용자 시간 */
  clock_t si_stime;   /* 소비된 시스템 시간 */
  sigval_t si_value;  /* 시그널 페이로드 값 */
  int si_int;         /* POSIX.1b 시그널 */
  void* si_ptr;       /* POSIX.1b 시그널 */
  void* si_addr;      /* 장애가 발생한 메모리 위치 */
  int si_band;        /* 대역(band) 이벤트 */
  int si_fd;          /* 파일 디스크립터 */
}
  • 상세한 설명은 책에 있음
  • POSIX는 처음 세 필드많이 모든 시그널이 유효하다고 보증, 다른 필드는 적절한 시그널을 다룰 때만 접근할 것

si_code의 멋진 세계

  • si_code 필드는 시그널을 일으킨 원인을 알려 줌(사용자가 보낸 시그널의 경우 시그널을 어떻게 보냈는지, 커널이 시그널을 보낼 경우 왜 시그널을 보냈는지 확인 가능)
  • 상세 내용은 책에 있음
  • si_code는 값을 담고 있는 필드이며 비트 필드가 아님

페이로드와 함께 시그널 보내기

  • SA_SIGINFO 플래그와 함께 등록된 시그널 핸들러는 siginfo_t 인자를 전달하고 이의 si_value 필드를 통해 페이로드를 전달할 수 있다.
1
2
3
4
5
6
7
8
#include <signal.h>

union sigval {
  int sival_int;
  void* sival_ptr;
}

int sigqueue (pid_t pid, int signo, const union sigval value);
  • sigqueue()sms kill()과 유사하게 호출이 성공하면 signo 시그널은 pid 프로세스나 프로세스 그룹 큐에 들어가고 0을 반환
  • 호출이 실패하면 -1을 반환 및 errno를 설정(EAGAIN, EINVAL, EPERM, ESRCH)
  • kill()처럼 권한을 가지고 있는지 검사하기 위해 signo로 NULL 시그널을 전달 할 수 있다.

시그널 페이로드 예제

  • 책에 예제 있음
  • sigqueue() 시그널을 보내면 받는 프로세스에서 sa_sigaction 핸들러(SA_SIGINFO 용)으로 처리 시 siginfo_t의 si_int 또는 si_ptr 필드로 페이로드를 받고 si_code가 SI_QUEUE임을 확인한ㄷ.

시그널은 미운오리 새끼?

  • 시그널은 유닉스 프로그래머 사이에서 환영받지 못함(커널과 사용자 간 통신을 위한 구식 메커니즘, 멀티스레딩과 이벤트루프 세계에서 적절하지 않음)
  • 시그널은 커널에서 수많은 통지를 수신할 유일한 방법이며, 프로세스를 종료하고 부모/자식 프로세스 관계를 관리하는 방법이므로 이해하고 사용해야 함
  • 시그널이 평가절하되는 원인 중 하나는 재진입성에 대한 우려가 없는 시그널 핸들러 작성이 쉽지 않음 -> 시그널 핸들러를 간결하게, 재진입성이 보장된 함수만 사용
  • 사용할 거면 sigaction()과 sigqueue를 사용하자
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy