- 시그널은 비동기 이벤트 처리를 위한 메커니즘을 제공하는 소프트웨어 인터럽트(시그널의 발생/처리 모두 비동기)
- 시그널은 초창기 부터 있었고 신뢰성 측면에서 향상을 이루었고(사라지거나 하지 않게) 사용자 데이터를 전달할 수 있도록 발전
- POSIX에서 시그널 처리를 표준화함
시그널 개념
- 시그널의 생명 주기 : 시그널 발생 -> 커널은 해당 시그널 전달 가능 시까지 쌓아둠 -> 커널은 가능 시점에 시그널 처리
- 커널은 프로세스 요청에 따라 세가지 중 한가지 동작 수행
- 시그널 무시 : 아무동작 하지 않음(SIGKILL, SIGSTOP은 무시 안됨)
- 시그널 처리 : 커널은 프로세스 현재 코드 실행 중지 후 등록된 시그널 핸들러 수행(SIGKILL, SIGSTOP은 잡을 수 없음)
- 기본동작 수행 : 기본 동작은 시그널에 따라 다름(대부분 프로세스 종료)
시그널 식별자
- 모든 시그널은 SIG라는 접두어로 시작하는 상징적 이름이 있으며 <signal.h>파일에 정의 됨(#define 양의 정수 형태)
- 시그널 번호는 1에서 시작, 대략 31개의 시그널이 있음(0은 NULL Signal, 아무런 행동도 정의되지 않음)
리눅스에서 지원하는 시그널
- 책에 리눅스에 지원하는 시그널 종류 및 자세한 설명
시그널 관리 기초
- 시그널 관리를 위한 가장 단순하고 오래된 인터페이스는 signal() 함수
|
|
- signal()은 호출이 성공하면 signo 시그널을 받았을 때 수행할 handler를 설정
- handler의 인자는 signo와 같은 시그널 식별자
- handler는 함수 포인터 외 SIG_DFL(동작을 기본값으로 설정)과 SIG_IGN(시그널 무시)도 설정 가능
- signal() 함수는 호출 성공 시 이전 handler 또는 SIG_DFL, SIG_IGN을 반환, 에러 발생 시 SIG_ERR 반환(errn 설정 없음)
모든 시그널 기다리기
|
|
- pause() 시스템 콜은 프로세스를 종료시키는 시그널을 받을 때까지 해당 프로세스를 잠재움(디버깅/테스트용 코드 작성 시 유용)
- pause()는 붙잡을 수 있는 시그널을 받았을 때만 반환, -1을 반환하고 errno를 EINTR로 설정(무시된 시그널을 받은 경우 안깨어남)
예제
- 책에 예제 있음
실행과 상속
- fork() 시스템 콜을 통해서 프로세스 생성 시 자식 프로세스는 부모 프로세스의 시그널에 대한 동작(무시, 기본동작, 핸들러)을 상속, 대기중인 시그널은 상속되지 않음
- exec 시스템 콜을 통해서 프로세스 생성 시 모든 시그널은 부모 프로세스가 무시하는 경우를 제외하고 기본동작으로 설정, 대기중인 시그널은 상속
- 쉘이 백그라운드에서 프로세스 실행 시 새로 실행되는 프로세스는 인터럽트 문자와 종료 문자를 무시해야 함 (SIGINT와 SIGQUIT가 SIG_IGN으로 설정해야함, 두시그널이 무시되지 않음을 확인하기 위해 시그널 동작을 설정해봐야 한다는 것은 singal() 인터페이스의 결점)
시그널 번호를 문자열에 맵핑하기
|
|
- sys_siglist는 시그템에서 지원하는 시그널 이름을 담고 있는 문자열 배열, 시그널 번호를 색인으로 이용
- 대안으로 BSD/linux에서 지원하는 psignal(), strsignal()이 있으나 보통 sys_siglist가 최선임
시그널 보내기
|
|
- 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)이면 호출은 시그널을 보내지 않지만 에러검사를 수행, 시그널을 보낼 수 있는 권한이 있는지 검사할 때 유용
예제
- 책에 시그널을 보내는 예제와 시그널을 보낼 수 있는 권한이 있는지 검사하는 예제 있음
자신에게 시그널 보내기
|
|
- raise()는 자기 자신에게 시그널을 보내는 함수
- 호출 성공시 0, 실패하면 0이 아닌 값을 반환, errno는 설정하지 않음
프로세스 그룹 전체에 시그널 보내기
|
|
- killpg()는 프로세스 그룹에 속한 모든 프로세스에 시그널을 보냄 (프로세스 그룹ID를 음수로 바꿔서 kill() 사용하는 대신)
- pgrp가 0인 경우 호출하는 프로세스의 그룹에 속한 모든 프로세스에 signo로 지정한 시그널을 보낸다.
- 호출이 성공하면 0을 반환, 실패시 -1반환 및 errno를 설정(EINVAL, EPERM, ESRCH)
재진입성
- 시그널은 소프트웨어 인터럽트이므로 시그널 핸들러에서 글로벌 데이터/공유데이터를 손대지 않는 것이 바람직(다음절에서 일시적으로 공유데이터를 안전하게 처리하는법(시그널 블록)을 설명)
- 일부 함수는 확실히 재진입이 가능하지 않으므로 주의
재진입이 가능한 함수
- 시그널 핸들러 작성시 중단된 프로세스가 재진입이 불가능한 함수를 수행하는 중이었다고 가정할 것
- 그래서 시그널 핸들러는 반드시 재진입이 가능한 함수만 이용해야 함
- 책에서 시그널 사용 시 안전하게 재진입이 가능한 함수 목록을 기술하고 있음
시그널 모음
- 시그널 집합 연산은 프로세스가 블록한 시그널 모음이나 프로세스에 대기 중인 시그널 모음을 관리
|
|
- 하기 함수는 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 호환이 중요한 프로그램에서는 사용하지 말 것)
|
|
- sigisemptyset()은 set으로 지정된 시그널 모음이 비어 있는 경우에는 1을, 아닌 경우 0을 반환
- sigorset()은 시그널 모음인 left와 right의 합집함(이진 OR)을 dest에 넣고 sigandset()은 교집함(이진 AND)을 dest에 넣음
- 두 함수 모두 성공하면 0을 반환, 에러 발생 시 -1 반환 및 errno를 설정(EIVAL)
시그널 블록
- 시그널 핸들러와 프로그램의 다른 부분이 데이터를 공유해야 할 때(크리티컬 섹션) 시그널 전달을 보류하여 영역을 보호(= 시그널 블록)
- 프로세스가 블록한 시그널 모음을 해당 프로세스의 시그널 마스크라 함
|
|
- 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 시그널은 시그널 블록이 해제되면 커널은 이른 프로세스에 넘겨 처리하게 함
|
|
- sigpending()은 호출이 성공하면 대기중인 시그널 모음을 set에 넣고 0을 반환, 실패 시 -1 반환 및 errno 설정(EFAULT)
여러 시그널 기다리기
|
|
- sigsuspend()는 프로세스가 자신의 시그널 마스크를 일시적으로 변경 후 시그널 발생시 까지 대기, 시그널이 프로세스를 종료시키는 경우 반환되지 않음
- 시그널이 발생해서 이를 처리한 경우 시그널 핸들러가 반환한 후에 sigsuspend()는 -1를 반환 및 errno를 EINTR로 설정
- sigsuspend()의 활용 방법은 프로그램이 크리티컬 섹션에 머물러 있을 때 도착해서 블록되었던 시그널 조회
- sigprosmask()를 호출 시 이전 마스크를 oldset에 저장, 크리티컬 섹션 빠져나온 후 oldset으로 sigsuspend() 호출
고급 시그널 관리
- signal() 함수는 매우 기초적이나 sigaction()이 더 많은 능력(POSIX 표준)
- 사용하면 핸들러가 동작하는 동안 지정한 시그널 블럭, 시그널 수신 시점의 시스템과 프로세스에 대한 넓은 데이터 조회 등
|
|
- 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 구조체는 시그널에서 훨씬 많은 기능 및 정보(시그널 원인 등)를 제공
|
|
- 상세한 설명은 책에 있음
- POSIX는 처음 세 필드많이 모든 시그널이 유효하다고 보증, 다른 필드는 적절한 시그널을 다룰 때만 접근할 것
si_code의 멋진 세계
- si_code 필드는 시그널을 일으킨 원인을 알려 줌(사용자가 보낸 시그널의 경우 시그널을 어떻게 보냈는지, 커널이 시그널을 보낼 경우 왜 시그널을 보냈는지 확인 가능)
- 상세 내용은 책에 있음
- si_code는 값을 담고 있는 필드이며 비트 필드가 아님
페이로드와 함께 시그널 보내기
- SA_SIGINFO 플래그와 함께 등록된 시그널 핸들러는 siginfo_t 인자를 전달하고 이의 si_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를 사용하자