시그널 관련 API

Soyun Park·2023년 11월 8일
0
post-thumbnail

1. 시그널

1-1. 시그널이란?

  • 시그널은 사용자나 커널이 프로세스에 무언가를 통지하는 목적으로 사용한다.

  • 매크로로 이름이 붙여 있지만 그 실체는 int 타입 정수이다.

  • 시그널 처리 방법은 다음과 같다.

    1. 시그널을 무시한다. 예를 들어 자식 프로세스가 종료한 경우 생성되는 SIGCHLD 는 무시된다.
    2. 프로세스를 종료한다. Ctrl+C 를 입력하면 생성되는 SIGINT 가 해당된다.
    3. 프로세스의 core 덤프를 생성하고 비정상 종료시킨다. SIGEGV 가 해당된다.
  • 자주 사용되는 시그널은 다음과 같다.

    시그널 명포착 가능디폴트 기능생성 원인과 용도
    SIGINTO종료주로 Ctrl+c 로 생성되며 프로그램을 중지하고 싶을 때 사용
    SIGHUPO종료사용자가 로그아웃할 때 생성. 데몬 프로세스에서는 설정 파일을 다시 읽어 들이는 경우에 많이 사용됨
    SIGPIPEO종료끊어진 파이프에 데이터를 쓰려고 시도하면 생성
    SIGTERMO종료프로세스를 종료시킬 때 사용. kill 명령어를 시그널 지정 없이 사용할 때 전달됨
    SIGKILLX종료프로세스를 확실하게 종료시키기 위해서 사용
    SIGCHLDO무시자식 프로세스가 정지 또는 종료되면 발생
    SIGSEGVO코어덤프액세스 금지된 메모리 영역에 접근하거나, 초기화하지 않은 포인터를 참조하거나, 버퍼 오버플로우가 발생한 경우에 생성됨. 프로그램에 버그에 의해 발생
    SIGBUSO코어덤프얼라인먼트 위반, 포인터 작업을 잘못한 경우 등 프로그램의 버그에 의해 발생
    SIGFPEO코어덤프산술 연산 에러. 0으로 나누거나 부동 소수점 오버플로우 등의 상황에서 발생


2. 시그널 포착하기

2-1. 시그널 포착

  • 위의 표에서 포착 가능 항목이 O 인 시그널은 그 시그널이 전달될 때의 동작을 변경할 수 있다.
  • 시그널 처리를 커널에 맡기지 않고 스스로 시그널을 처리하는 것을 시그널을 포착한다 또는 시그널을 트랩한다라고 한다.

2-2. signal(2)

#include <signal.h>
void (*signal(int sig, void (*fuunc)(int)))(int);
  • typedef를 사용해서 보기 쉽게 만들면 다음과 같다.

    typedef void (*sighandler_t)(int);
    sighandler_t signal(int sig, sighandler_t func);
  • sig의 시그널 번호를 받았을 때 func 함수를 호출하도록 동작을 변경한다.

  • 즉, 함수 포인터를 받고 함수 포인터를 반환하는 함수이다.

  • func에는 사용자가 정의한 함수 외에도 다음과 같이 특별한 값을 사용할 수 있다.

    상수의미
    SIG_DFL커널의 디폴트 액션을 사용
    SIG_IGN커널 레벨에서 시그널을 무시하도록 함

2-3. signal(2)의 문제점

  • 시그널은 프로세스의 상태를 고려하지 않고 실행 중에 함부로 끼어들기 때문에 다음의 문제가 발생한다.

    1. 핸들러 초기화
    • 프로그래머가 지정한 시그널 핸들러를 수행한 후 원래 설정으로 되돌리는 경우가 있다. 그러면 한번 시그널이 포착된 후 다시 핸들러를 등록하기 전까지 날아온 시그널은 포착하지 못한다.
    1. 시스템 콜 수행 중에 시그널 발생
    • 시그널은 read()나 write()와 같은 시스템 콜을 실행하는 중에 날아올 수 있다.
    • 운영체제는 시스템 콜 수행 중에 시그널이 날아오면 에러를 반환하고 종료하거나 시스템 콜 처리를 중재해서 프로그래머에게 문제가 보이지 않게 한다.
    1. 중복 호출해서는 안되는 함수를 중복 호출
    • 어떤 함수를 실행 중에 시그널 핸들러에 의해 다시 호출된다면 함수 내에서 전역 변수를 사용하고 있을 때 문제가 발생한다.
    1. 시그널 블록
    • 시그널 핸들러가 실행 중에는 같은 종류의 시그널 전달을 보류할 수 있게 한다. 이를 시그널을 블록한다 라고 한다.

2-4. sigaction(2)

#include <signal.h>

int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact)
struct sigaction{
    // sa_handler나 sa_sigaction 중 하나만 사용
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, sigifo_t*, void*);
    sigset_t sa_mask;
    int sa_flags;
};
  • sig에 해당하는 시그널에 대한 시그널 핸들러를 등록한다.

  • act에는 시그널 핸들러를 지정한다. 상수 SIG_IGN, SIG_DFL 또는 임의의 함수 포인터 중 하나이다.

  • oldset에는 sigaction()을 호출할 때 설정되어 있던 시그널 핸들러가 들어간다. 불필요할 경우 NULL을 지정한다.

  • sa_sigaction은 sa_handle과 똑같이 시그널 핸들러를 지정하는 멤버이다. sa_sigaction은 시그널을 수신했을 때 시그널 번호 이외의 자세한 정보를 얻을 수 있다.

  • signal의 문제점을 sigaction으로 다음과 같이 개선되었다.

    1. 핸들러 재설정
    • sigaction()은 운영체제와 관계없이 한번 설정한 시그널 핸들러가 계속 유지된다.
    1. 시스템 콜의 재기동
    • sa_flags 멤버에 SA_RESTART 플래그를 추가하면 시스템 콜을 재기동하고 그렇지 않은 경우에는 재기동하지 않는다.
    1. 시그널 블록
    • sa_mask 멤버에 블록할 시그널을 지정할 수 있다. sa_mask를 비우려면 후술하는 sigemptyset()을 사용한다.

2-5. sigaction의 사용 예

#include <signal.h>

typedef void(*sighandler_t)(int);

sighandler_t
trap_signal(int sig, sighandler_t handler){
	struct sigaction act, old;
	act.sa_handler=handler;
	sigemptyset(&act.sa_mask);
	act.sa_flags=SA_RESTART;
	if(sigaction(sig, &act, &old)<0)
		return NULL;

	return old.sa_handler;
}
  • 시그널 핸들러를 sa_handler에 설정한다. sa_handler와 sa_sigaction은 둘 중 하나만 사용할 수 있으므로 sa_sigaction은 무시한다.
  • sa_mask는 sigemptyset()으로 초기화해서 비운다.
  • 시스템 콜이 자동으로 재기동되도록 sa_flags에 SA_RESTART 를 설정한다.

2-6. sigset_t 조작 API

#include <signal.h>

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int sig);
int sigdelset(sigset_t *set, int sig);
int sigismember(const sigset_t *set, int sig);
  • sigemptyset()는 set을 빈 값으로 초기화한다.
  • sigfillset()은 set을 모든 시그널을 포함하는 상태로 한다.
  • sigaddset()은 시그널 sig를 set에 추가한다.
  • sigdelset()은 시그널 sig를 set에서 삭제한다.
  • sigismember()은 시그널 sig가 set에 포함되어 있으면 참을 반환한다.

2-7. 시그널 블록

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigpending(sigset_t *set);
int siguspend(const sigset_t *mask);
  • sigaction()의 sa_mask와 반대로 블록한 시그널을 다시 받기 위한 API는 위와 같다.

  • sigprocmask()은 현재 프로세스의 시그널 마스크를 설정한다. how에 다음의 플래그를 지정하여 설정한다.

    효과
    SIG_BLOCKset에 포함되는 시그널을 시그널 마스크에 추가한다
    SIG_UNBLOCKset에 포함되는 시그널을 시그널 마스크에서 삭제한다
    SIG_MASK시그널 마스크를 set으로 대체한다
  • sigpending()은 보류된 시그널을 set에 넣는다.

  • siguspend()는 시그널 마스크 mask를 설정하고 프로세스를 시그널 대기 상태로 만든다. 차단한 시그널을 해제하고 보류되어 있던 시그널을 처리할 때 사용한다.

  • siguspend()는 항상 시그널에 끼어들어 종료한다. 항상 -1을 반환한다.

2-8. SIGINT를 수신하면 메시지를 출력하는 프로그램 만들기

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

typedef void (*sighandler_t)(int); // 시그널 핸들러

sighandler_t trap_signal(int sig, sighandler_t handler){
  	// sig: 핸들링하는 시그널, handler: 시그널을 처리할 함수
  
    struct sigaction act, old;

    act.sa_handler = handler; // 실행할 핸들러를 저장
    sigemptyset(&act.sa_mask); // 핸들러가 실행하는 동안 블록할 시그널을 비움
    act.sa_flags = SA_RESTART; // 시스템  콜 재기동
    if (sigaction(sig, &act, &old) < 0){
		// sig 시그널에 실행할 핸들러 act를 지정
      	// 핸들러 지정 전 설정되어 있던 핸들러 old를 가져옴
      	return NULL; // sigaction()이 에러가 발생해 -1을 반환하면 NULL을 반환
    }

    return old.sa_handler; // act 핸들러를 지정 성공하면 이전 핸들러를 반환
}

void print_exit(int sig){
    printf("Got signal %d\n", sig);
    exit(0);
}

int main(int argc, char *argv[]){
    trap_signal(SIGINT, print_exit);
    pause(); // trap_signal의 실행을 대기
    exit(0);
}

2-9. 작성한 프로그램 실행 예

  • Ctrl+C를 눌러서 종료 시그널을 보냈다.
  • 2번 SIGINT에 해당하는 값을 결과값으로 출력한다.



3. 시그널 전송

3-1. kill(2)

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
  • kill()은 시그널을 전송하기 위해 사용한다. PID가 pid인 프로세스에 시그널 sig를 전송한다.
  • pid가 음수인 경우 PID가 -pid인 프로세스 그룹 전체에 시그널을 전송한다.
  • 프로세스 그룹에 시그널을 보내는 killpg()라는 전용 시스템 콜도 있다.



4. Ctrl+C

4-1. Ctrl+c란?

  • Ctrl+C 가 시그널로 변환되어 대상이 되는 프로세스에 전달되기까지의 구조는 다음과 같다.

  • 사용자가 키보드로 Ctrl+c 를 누르면 이것을 커널의 단말 드라이브가 파악한다.

  • 셸을 사용하는 경우 cooked 모드로 되어 있어 특수한 기능을 수행하는 키가 존재한다.

  • 어떤 키가 어떤 기능을 하는지는 stty -a 명령어를 실행하면 알 수 있다.

  • intr 는 interrupt를 말하며 ^DCtrl+C 를 의미한다. intr 로 설정된 Ctrl+C 가 눌렸기 때문에 이를 인터럽트라고 인식한다.

  • SIGINT 를 단말에서 동작 중인 프로세스에게 전송할 때 셸이 파이프로 연결된 프로세스 무리를 기동할 때 동작 중인 프로세스를 알 수 있다. 이를 위해 tcsetpgrp()라는 API가 사용된다.

  • 인터럽트는 파이프에 연결된 프로세스 전체를 중지해야 하므로 프로세스 그룹 전체에 신호를 전송한다. 그래서 SIGINT 가 전송되면 파이프 전체 프로세스가 종료된다.

  • 차례로 정리하면 다음과 같다.

    1. 셸이 파이프를 구성하는 프로세스를 fork()한다.
    2. 셸이 파이프의 PGID를 tcsetpgrp()로 단말에 통지한다.
    3. fork된 각 프로세스가 각각의 명령어를 exec한다.
    4. 사용자가 Ctrl+C 를 누른다.
    5. 커널 내의 단말 드라이브가 그것을 SIGINT 로 변환하여 동작 중인 프로세스 그룹에 발송한다.
    6. 프로세스 그룹이 시그널에 대한 기본 동작으로 종료된다.

0개의 댓글