프로그래밍/C++

시그널의 모든것 (All about Linux signals)

보니아빠 2014. 3. 14. 18:41
퍼온글 번역본 입니다.
원본 링크


시그널의 모든것 (All about Linux signals)

프로그램 안에서 시그널 처리를 하려고 할 때 대부분의 경우 간단히 아래 구문을 이용해서 처리한다:

void handler (int sig)

그리고 프로세스에 시그널을 전달할 때는 시스템 펑션 signal(2) 을 사용한다. 이것은 매우매우 단순한 경우이고, 시그널은 그것보다는 더 흥미롭다. 이 문서는 데몬을 만들 때 또는 현재 동작 또는 전체 프로그램을 인터럽팅  하지 않도록 프로그램 안에서 인터럽트를 처리해야 할 때 유용한 정보를 기술하고 있다.


여기서 다루는 내용은?

이 문서는 리눅스 안에서 어떻게 시그널이 동작되는 지 그리고 시그널을 처리하기 위해 어떻게 POSIX API를 사용하는 지에 대해 기술한다. 모든 최신 리눅스 시스템 상에서 동작하는 펑션을 다룰 것이고, 특별히 명시적으로 언급하는 한 대부분의 POSIX 시스템에도 적용될 것이다. 오래된 펑션들은 다루지 않는다. 이 문서를 이해하려면 시그널에 대한 기반 지식은 필요하다는 점도 먼저 말해 둔다.


What is signaled in Linux


프로세스는 다음과 같은 경우에 시그널을 받는다:
  • 사용자 공간에서 다른 프로세스가 kill(2) 펑션을 호출했을 때.
  • 프로세스 자체에서 abort(3) 펑션을 사용해서 시그널을 보냈을 때.
  • 자식 프로세스가 종료 될 때 OS(운영체제) 가 SIGCHLD 시그널을 보냄.
  • 제어터미널 상에서 부모 프로세스가 죽거나 멈춘 게(hangup) 감지되면 SIGHUP 시그널을 보냄.
  • 사용자가 키보드에서 SIGINT 를 보냄.
  • 프로그램이 잘못된 행위를 하면 SIGILL, SIGFPE, SIGSEGV 중의 하나를 받음.
  • 프로그램이 mmap(2) 를 사용해서 매핑한 메모리에 액세스할 때 이용 불가능일 경우(예를 들면 다른 프로세스에 의해 truncate 되었을 때) - mmap()을 사용해서 파일에 액세스 할 때 정말 끝장인 상황임. 이 경우에는 처리를 위해 더 좋은 방법이 없다.
  • gprof 같은 프로파일러가 사용되는 경우 프로그램은 때때로 SIGPROF를 받는다. 이것은 read(2) 같은 시스템 펑션 인터럽트에 대한 적절한 처리를 깜빡했을 때 때때로 문제가 된다(errno == EINTR).
  • write(2) 또는 비슷한 데이터 전송 펑션을 사용할 때 받을 상대방이 없을 경우 SIGPIPE 가 발생된다. 이것은 매우 흔한 상황으로 이러한 펑션이 단순히 errno 를 설정하고 종료할 뿐만 아니라 SIGPIPE 를 발생시키는 요인이 된다는 것을 꼭 기억해야 한다. 이 경우에 대한 간단한 예를 위해 표준 출력에 내용을 쓰고 사용자가 이 출력을 파이프라인을 통해 다른 프로그램으로 리디렉션할 때를 가정하자. 만약 데이터를 보내는 도중에 다른 프로그램이 종료된다면 SIGPIPE 가 발생된다. 또한 시그널은 이러한 이벤트가 비동기이며 어느 정도 데이터가 성공적으로 리턴했는 지 알 수 없기 때문에 펑션 에러와 함께 리턴되도록 사용된다. 이것은 소켓에 데이터를 보낼 때도 발생된다. 이것은 데이터가 버퍼되어 있으며 와이어를 타고 전송되어서 상대편에 즉시 전송되지 않기 때문에 OS가 전송 펑션이 종료된 시점에 바로 알 수 없기 때문이다. (원본글: A signal is used in addition to the normal function return with error because this event is asynchronous and you can't actually tell how much data has been successfully sent. This can also happen when you are sending data to a socket. This is because data are buffered and/or send over a wire so are not delivered to the target immediately and the OS can realize that can't be delivered after the sending function exits.)

시스널의 전체 리스트는 signal(7) man 페이지를 참조하라.


시그널 핸들러

전통적인 signal() 은 사지 않는다.


signal(2) 펑션은 시그널 핸들러를 구축하는 오래되고 가장 간단한 방법이지만 더 이상 사용되지 않는다. 사용되지 않는 이유는 몇가지 있지만 가장 중요한 이유는 오리지널 Unix구현이 시그널을 받은 후에 디폴트 값으로 핸들러를 리셋해 버리기 때문이다. 만약 죽는 프로세스를 잡기 위해 SIGCHLD 각각을 별도로 핸들링 할 필요가 있을 경우 여기는 경쟁이 필요하다. 이걸 위해서는 시그널 핸들러 안에서 다시 시그널 핸들러를 설정할 필요가 있고 signal(2) 펑션을 호출 하기 전에 다른 시그널이 도착하게 될 것이다. 이 행동은 시스템에 따라서 다르다. 또한 signal(2) 펑션은 sigaction(2) 는 제공해 주는 때때로 필요로 하는 기능들을 지원해 주지 못한다.


시그널 설정을 위해 권장하는 방법은: sigaction 

sigaction(2) 펑션은 시그널을 설정하는 가장 좋은 방법이다. 프로토타입은 다음과 같다:

  1. int sigaction (int signum, const struct sigaction *act, struct sigaction *oldact);

위에 보이는 것처럼 시그널 핸들러 펑션에의 포인터를 직접 넘길수 없고 대신에 struct sigaction 에 대한 포인터를 넘긴다. struct sigaction 은 다음과 같이 정의되어 있다:

  1. struct sigaction {
  2. void (*sa_handler)(int);
  3. void (*sa_sigaction)(int, siginfo_t *, void *);
  4. sigset_t sa_mask;
  5. int sa_flags;
  6. void (*sa_restorer)(void);
  7. };

이 구조체 필드에 대한 자세한 정의는 sigaction(2) man 페이지를 참고하라. 중요한 필드들은 다음과 같다:

  • sa_handler - 핸들러 펑션에 대한 포인터이며, signal(2)을 위한 핸들러와 같은 프로토타입을 가진다.
  • sa_sigaction - 시그널 핸들러를 실행하는 다른 방법이다. 이 펑션은 시그널 번호 외에 2개의 추가적인 인수를 가지는 데 여기서 thesiginfo_t *가 더 흥미롭다. 이것은 받은 시그널에 대한 더 많은 정보를 제공하며 나중에 자세히 설명할 것이다.
  • sa_mask 핸들러 실행 중에 시그널이 블로킹 될 지를 명시적으로 설정할 수 있도록 허용한다. 추가적으로 만약 SA_NODEFER flag를 사용하지 않으면 트리거된 시그널은 블록될 수 있다.
  • sa_flags 는 시그널 핸들링 프로세스의 동작 변경을 허용한다. 이 필드에 대한 자세한 설명은 man 페이지를 보라. sa_sigaction 핸들러를 사용하기 위해서는 여기에 SA_SIGINFO flag 를 사용해야 한다.

만약 sigaction(2) 의 추가적인 기능을 사용하지 않는다면 두 펑션의 차이는 무엇일까? 그 답은 확율과 경쟁조건이 없다는 것이다. sigaction(2) 의 기본동작은 시그널 핸들러를 재설정하지 않고 실행되는 동안 시그널이 블록되므로, 호출된 후 시그널 핸들러가 리셋되는 이슈는 sigaction(2) 에는 영향을 주지 않는다. 그래서 경쟁이 없으며 이 동작이 POSIX 규격으로 기술되었다. 다른 차이점은 signal(2) 의 경우 일부 시스템  콜은 자동적으로 재시작 하는 데 반해 경우에는 sigaction(2) 의 경우 디폴트로는 그렇지 않다는 점이다.


sigaction() 예제


다른 인수와 함께 sigaction()을 사용해서 시그널 핸들러를 설정하는 예제이다.

이 예제에서 SIGTERM 을 위한 3개 인수버전의 핸들러를 사용한다. SA_SIGINFO flag 의 설정 없이 전통적인 1개의 인수만 사용하는 시그널 핸들러 버전 사용하고 sa_handler 필드를 통해 넘겨 준다. 이것은 signal(2) 의 단순 대체이다. 이 프로그램을 실행시키고 kill PID를 해서 어떤 일이 발생되는 지 확인해 봐라.

시그널 핸들러에서 siginfo_t *siginfo 필드를 통해 송신 쪽의 PID 와 UID를 알 수 있다 . 이 구조체는 더 많은 필드를 가지고 있지만 이에 대해서는 후에 다룬다.

시그널이 도착하고 멈추고 나서 다시 호출되어야 하기 때문에 sleep(3) 펑션이 루프 안에서 사용된다.


SA_SIGINFO handler


이전 예제에서 시그널 핸들러에 더 많은 정보를 넘기기 위해 SA_SIGINFO 가 사용된다. 위에서는 시그널을 전달한 프로세스의 UID와 PID를 알려 주는 siginfo_t구조체의  si_pid 와 si_uid 필드를 봤지만 이외에도 몇개의 필드가 더 있다. 이에 대해서는 sigaction(2) man 페이지에 설명되어 있다. 리눅스에서는 오직 si_signo (signal number) 와 si_code (signal code) 필드만 모든 시그널에 대해 이용가능하다. 다른 필드의 존재여부는 시그널 타입에 의존적이다. 일부 다른 필드들은:

  • si_code - 시그널이 보내진 이유. kill(2) 또는 raise(3) 에 의해 보내졌다면 SI_USER 일 것이다. 만약 커널이 보냈다면 SI_KERNEL 일 것이다. 잘못된 어드레싱 모드로 인해 보내지는 SIGILL 은 ILL_ILLADR 같은 특별한 값을 들어 있다.
  • SIGCHLD 인 경우 필드 si_status, si_utime, si_stime 은 종료 상태 또는 죽은 프로세스의 시그널, 사용자/시스템 시간이 채워진다.
  • SIGILL, SIGFPE, SIGSEGV, SIGBUS 의 경우 si_addr 문제가 발생된 메모리 어드레스를 가진다.

추후 siginfo_t 에 대한 다른 예제를 볼 것이다.

Compiler optimization and data in signal handler


다음 예를 보자:
  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <signal.h>
  4. #include <string.h>
  5.  
  6. static int exit_flag = 0;
  7.  
  8. static void hdl (int sig)
  9. {
  10. exit_flag = 1;
  11. }
  12.  
  13. int main (int argc, char *argv[])
  14. {
  15. struct sigaction act;
  16.  
  17. memset (&act, '\0', sizeof(act));
  18. act.sa_handler = &hdl;
  19. if (sigaction(SIGTERM, &act, NULL) < 0) {
  20. perror ("sigaction");
  21. return 1;
  22. }
  23.  
  24. while (!exit_flag)
  25. ;
  26.  
  27. return 0;
  28. }

이게 뭘까? 컴파일러의 최적화 설정에 따라 다르다. 최적화가 없다면 SIGTERM 또는 프로세스를 끝내는 다른 시그널을 받을  때까지 루프를 돌다가 종료한다. -O3 gcc flag 로 컴파일 하면 SIGTERM 을 받아도 종료되지 않는다. 왜 그럴까? while 루프가  -O3 로 최적화되면 exit_flag 변수가 일단 프로세서 레지스터 안으로 로드되고 나면 루프안에서는 메모리로 부터 읽어들이지 않는다. 컴파일러는 루프를 실행할 때 루프가 프로그램에서 변수를 액세스하는 유일한 장소라는 것을 인식하지 않는다. (프로그램의 다른 부분에서 액세스 되는 시그널 핸들러안의 변수를 수정하는) 그러한 경우 컴파일러에게 이 변수는 읽기 쓰기를 위해 메모리에서 액세스 해야 한다는 것을 알려야 한다. 이를 위해 변수 선언시에 volatile 키워드를 사용한다:

  1. static volatile int exit_flag = 0;


위와 같이 수정하면 모든 것이 기대대로 동작할 것이다.

Atomic Type


시그널 핸들러 안에서 원자적으로 읽기와 쓰기가 보증되는 데이터 타입으로 it:sig_atomic_t 타입이 정의되어 있다. 이 타입의 크기는 정의되어 있지 않으며, 정수형 타입이다. 이론적으로 이것은 시그널 핸들러 안에서 안전하게 읽기와 쓰기를 할 수 있는 유일한 타입이다. 다음을 명심하자:

  • 이것은 뮤텍스처럼 동작하지 않는다: 이것은 이 타입의 읽기 또는 쓰기가 인터럽터블 하지 않는 것을 보장하는 것이지 다음과 같은 코드가 안전한 것은 아니다:

  1. sig_atomic_t i = 0;
  2.  
  3. void sig_handler (int sig)
  4. {
  5. if (i++ == 5) {
  6. // ...
  7. }
  8. }

: if 동작 안에 읽기와 쓰기 동작이 있으며, 오직 단일 읽기와 단일 쓰기만이 원자적이다.

  • 이 타입을 mutex 없이 사용될 수 있는 멀티 쓰레드 프로그램에 사용하지 말아라. 이것은 단지 시그널 핸들러를 위한 것이고 뮤텍스와는 아무 상관이 없다!
  • 만약 시그널 핸들러 안에서 수정되거나 읽어들인 데이터가 또다시 수정되거나 읽히는 경우 만약 그것이 시그널이 블록된 부분에서 발생하는 경우 걱정할 필요가 없다. 하지만 여전히 volatile 키워드를 사용해야 한다.


Signal-safe functions


당신은 시그널 핸들러 안에서 아무것도 할 수 없다. 프로그램이 인터럽트 되었을 때 어떤 포인트인지 모르며 어떤 데이터가 수정되는 중간 시점에 있는 지 알수 없다는 것을 기억하라. 그것은 당신의 코드 뿐 아니라 당신이 사용하는 라이브러리 또는 표준 C 라이브러리 일 수 있다. signal(7) 안의 시그널 핸들러로 부터 안전하게 호출할 수 있는 펑션의 목록은 사실 몇개 되지 않는다. 예를 들면  open(2) 로 파일을 열 수 있고 unlink(2) 로 삭제하고, _exit(2) (exit(3) 가 아님!) 를 호출할 수 있다. 실제로 이 목록은 제한되어 있어서 할 수 있는 최선의 방법은 종료 같은 어떤 것을 프로세스에게 알려 주는 글로벌 flag 를 설정하는 것이다. wait(2) 와 waitpid(2) 펑션은 사용될 수 있고 unlink(2) 를 이용할 수 있어서 SIGCHLD 안에서 죽은 프로세스를 정리할 수 있고 pid 파일을 삭제할 수 있다. 


시그널을 처리하는 다른 방법: signalfd()


signalfd(2) 는 커널 2.6.22 부터 이용할 수 있는 꽤나 새로운 리눅스 특유의 시스템콜로 파일 기술자를 사용해서 시그널을 받을 수 있다. 이것은 핸들러 펑션을 제공하지 않고, 동기 방식으로 시그널을 처리할 수 있다. signalfd() 사용예 를 보라.

먼저 우리는 signalfd(2) 를 사용해서 sigprocmask(2) 로 처리할 시그널을 블록해야 된다. 이 펑션은 다음에 설명한다. 다음 우리는 들어오는 시그널을 읽기 위해 사용될 파일 기술자를 생성하기 위해 signalfd(2) 를 호출한다. 이 시점에 SIGTERM 또는 SIGINT 이 프로그램에 도착하면 인터럽트되지  않고 핸들러가 호출되지 않는다. 그것은 큐에 쌓이고 sfd 기술자에서 그것에 대한 정보를 읽을 수 있다. 이전에 설명했던 siginfo_t 와 비슷한 정보가 struct signalfd_siginfo  개체에 채워지며 이 개체를 읽을 때는 반드시 충분히 큰 버퍼를 제공해야 한다. 이 두 구조체는 약간 다른 필드명(si_signo 대신에 ssi_signo)을 가진다. 흥미로운 점은 sfd 기술자가 다른 파일 기술자에 대해서 할 수 있는 것처럼 동일하게 할 수 있다는 것이고 특히 당신이 다음의 것을 할 수 있다는 점이다.

  • select(2)poll(2) 그리고 다른 펑션들을 사용할 수 있다.
  • 넌 블로킹으로 만들수 있다.
  • 많은 기술자를 생성하고 각각 다른 시그널에 대해 select(2) 를 사용해서 다른 기술자를 리턴하게 해서 다른 처리를 할 수 있다.
  • fork() 후에도 파일 기술자는 닫히지 않으므로, 자식 프로세스가 부모 프로세스에 전달된 시그널을 읽을 수 있다.

이것은 많은 연결을 처리하기 위해 poll(2) 같은 펑션을 실행하는 메인루프를 가진 싱글 프로세스 서버에 사용되기에 완벽하다. 이것은 다른 비동기 처리 없이 시그널 기술자를 다른 것들과 같이 폴 되는 기술자 배열에 추가하고 처리함으로서 시그널 처리를 단순화 한다. 당신은 당신의 프로그램이 인터럽트 되지 않으므로 준비 되었을 때 시그널을 처리한다.


SIGCHLD 처리


프로그램에서 새로운 프로세스를 생성하고, 실제로 그들이  끝나기 기다리기를 원하지 않고 자식 프로세스의 종료 상태에 관심이 없고 단순히 좀비 프로세스만은 청소하기를 바란다면, SIGCHLD 핸들러는 생성한 프로세스에 대해 잊게 만들어 줄 것이다. 이 핸들러는 아래와 같이 될 수 있다.


Fragment of example: SIGCHLD handler

  1. static void sigchld_hdl (int sig)
  2. {
  3. /* Wait for all dead processes.
  4. * We use a non-blocking call to be sure this signal handler will not
  5. * block if a child was cleaned up in another part of the program. */
  6. while (waitpid(-1, NULL, WNOHANG) > 0) {
  7. }
  8. }

이 방법은 자식이 종료 될 때마다 청소되지만 프로세스가 왜 죽었는 지, 상태가 어땠는 지에 대한 정보는 잊어 버린다. 좀 더 똑똑한 핸들러도 만들수 있지만 싱글-세이프로 리스트된 어떤 펑션도 사용할 수 없다.

만약 자식 프로세스를 만든다면 SIGCHLD 는 핸들러를 가져야 한다는 걸 기억하자. 이 시그널을 무시하는 것은 정의되지 않았으므로 아무것도 하지 않는 핸들러라도 필요하다.


Handling SIGBUS


SIGBUS 시그널은 파일에 대응되지 않은 (mmap(2)로) 매핑된 메모리를 액세스할 때 프로세스에 보내진다. 일반적인 예는 매핑된 파일을 (아마도 다른 프로그램에 의해) 트렁케이트 된 후 현재의 끝을 지나 읽을 경우이다. 이러한 방식으로 파일을 액세스하는 것은 마치 그것이 힙 또는 스택 과 같은 메모리로 부터 읽어 들이는 것처럼 에러를 리턴하는 시스템 펑션을 요구하지 않습니다. 파일읽기 에러 후에 프로그램을 종료하기를 원치 않을 경우 이것은 실제로아주 나쁜 상황이다. 불행히도 SIGBUS 를 처리하는 것은 단순하고 깔끔하지는 않지만 가능하기는 하다. 만약 프로그램이 계속되기를 바란다면 longjmp(3) 를 사용해야만 한다. 이것은 goto 와 비슷하지만 더 나쁘다. 우리는 SIGBUS 를 받으면 mmap()된 메모리가 액세스되지 않는 프로그램 안에서 다른 장소로 점프 해야만 한다. 만약 이 시그널을 위한 빈 핸들러를 둘 경우 읽기 오류의 경우 프로그램이 중단된다. 시그널 핸들러는 실행 되고 제어는 에러를 발생시킨 같은 장소로 리턴한다. 그래서 우리는 시그널 핸들러로부터 다른 장소로 점프할 필요가 있다. 이것은 저 수준 으로 들리지만 표준 POSIX 펑션을 이용해서 가능하다.

예제 보기: SIGBUS handling

시그널 세이프 펑션의 리스트를 기억해야만 한다. 이 예제에서 우리는 시그널 핸들러로부터 실제로 리턴하지 않는다. 스택은 깨끗해지고 프로그램은 완전히 다른 장소에서 재시작 된다. 그래서 만약 다음과 같은 뮤텍스 잠금 중에 발생된다면:

  1. pthread_mutex_lock (&m);
  2. for (l = 0; l < 1000; l++)
  3. if (mem[l] == 'd') // BANG! SIGBUS here!
  4. j++;
  5. pthread_mutex_unlock (&m);

longjmp(3) 후에도 뮤텍스는 다른 모든 상황은 뮤텍스가 풀려야 함에도 여전히 홀딩되어 있다.

그래서 SIGBUS 처리는 가능하지만 매우 까다롭고 디버깅하기 어렵다. 프로그램 코드 또한 걸레처럼 되 버린다.


SIGSEGV 처리


SIGSEGV (segmentation fault) 시그널 처리도 가능하다. 시그널 핸들러에서 리턴되는  대부분의 경우 Seg falut 문제를 발생시킨 지점에서 다시 시작되기 때문에 아무런 의미가 없다. 그래서 만약 충돌 지점에서 프로그램을 계속할 수 있도록 프로그램의 상태를 해결할 방법이 없다면 프로그램을 그대로 종료해야만 한다. 프로그램을 재시작 할 수 있는 한가지 예는 mmap(2)를 사용하여 획득한 메모리가 읽기 전용일 때, Seg fault 의 원인이 이 메모리에 쓰기를 실행한 경우인지 체크하고 이 메모리의 보호를 변경하기 위해 mprotect(2) 를 사용하도록 시그널 핸들러를 작성하는 것이다. 얼마나 실용적인가? 나도 잘 모른다.

스택 공간을 소비하는 것은 Seg fault 의 한가지 원인이다. 이 경우 시그널 핸들러는 스택 상의 공간을 요구하기 때문에 실행 시킬 수 없다. 그러한 경우에 SIGSEGV 를 처리 하기 위해서는 sigaltstack(2) 펑션을 이용해서 시그널 핸들러를 위한 별도의 공간을 설정할 수 있다.


Handling SIGABRT


이 시그널을 처리할 때 abort(3) 펑션이 어떻게 동작하는 지를 명심해야 된다. : 이것은 시그널을 2번 발생시키지만, 두번째는 SIGABRT 핸들러가 디폴트 상태로 복원되므로, 지정된 핸들러를 가지고 있어도 프로그램은 종료된다. 그래서 프로그램이 종료 되기 전에 뭔가 할 수 있는 기회를 가진다. 앞에서 언급한 longjmp(3) 를 사용하는 대신 시그널 핸들러에서 빠져나감으로써 프로그램이 종료되지 않는 것이 가능하다.


시그널을 받았을 때 프로세스에는 어떤일이 생기나?


Default actions


시그널은 특별한 시그널 핸들러를 제공하지 않고 시그널을 블록하지 않았을 경우 각각의 시그널에 따라 정의된 디폴트 동작이 있다. 그것은 다음과 같다:

  • 프로세스를 종료한다. 이것은 SIGTERM 또는 SIGQUIT 뿐만 아니라  SIGPIPE, SIGUSR1, SIGUSR2 및 다른 시그널들 대부분 공통 동작이다. 
  • 코어 덤프와 함께 종료한다. 이것은 SIGSEGV, SIGILL, SIGABRT 류 시그널의 공통 동작이다.
  • SIGCHLD 와 같은 몇몇 시그널은 무시된다.
  • SIGSTOP (및 비슷한 시그널) 프로그램이 대기상태로 들어가도록 하고 SIGCOND 로 계속하게 한다. 쉘에서 CTRL-Z 를 누르면 발생되는 상황이 대표적이다.

디폴트 동작에 대한 자세한 내용은 signal(7) man 페이지 참조.


Interrupting system calls


만약 프로그램 안에 시그널 핸들러를 설정한다면 어떤 시스템 콜이 시그널에 의해 인터럽트되는 경우를 준비해야 한다. 어떤 시그널 핸들러를 설정하지 않았더라도, 프로그램으로 시그널이 전달될 수 있고 그래서 그것을 준비하는 것이 최선입니다. 예를 들자면 프로그램을 컴파일 할 때 gcc 옵션 중 -pg (프로파일링 활성화)를 주었다면 프로그램 실행 중에 미리 알지 못한 상태에서 때때로 SIGPROF 를 받을 때 시스템 콜이 인터럽트되는 요인이 된다.


인터럽트 되는 게 뭐야? (What is interrupted?)


시스템 콜에 사용되는 모든 시스템 또는 표준 라이브러리 펑션은 잠재적으로 인터럽트 될 수 있으며 정확히는 man 페이지를 참고해야 한다. (I/O 동작완료를 위해 기다리거나 슬립하지 않고) 바로 리턴되는 일반적인 펑션은 인터럽트 될 수 없다. 예를 들면 socket(2) 펑션은 소켓을 생성하고 아무것도 기다리지 않고 바로 리턴된다.

반면에 (네트웍 전송, 파이프 읽기, 특정한 슬립 등) 어떤 것을 기다리는 펑션들은 인터럽트 된다. 예를 들면 select(2)read(2)connect(2) 펑션은 인터럽트 된다. 이러한 펑션들이 완료를 위해 기다리는 동안 시그널이 도착해서 무슨 일이 정확히 발생되는 지는 각각의 man 페이지에 기술되어 있다.


signal 을 고려한 간단한 code


가장 단순한 경우는 nanosleep(2)을 이용해서 구현된 sleep(3) 이다. 시그널에 의해 인터럽트 되면 슬립에 들어가 있던 초를 리턴한다. 만약 시그널에 관계 없이 10초동안 슬립에 빠져 있게 하려면 아래처럼 하면 된다:

  1. #include <unistd.h>
  2. #include <signal.h>
  3.  
  4. static void hdl (int sig)
  5. {
  6. }
  7.  
  8. void my_sleep (int seconds)
  9. {
  10. while (seconds > 0)
  11. seconds = sleep (seconds);
  12. }
  13.  
  14. int main (int argc, char *argv[])
  15. {
  16. signal (SIGTERM, hdl);
  17.  
  18. my_sleep (10);
  19.  
  20. return 0;
  21. }

이 예제는 동작한다. 하지만 슬립에 빠져 있는 동안 몇개의 시그널을 보내면 다른 시간 동안 슬립에 빠지게 된다. 그 이유는 sleep(3) 인수를 받아들이고 인터럽트 후 얼마 동안 잠 들었었는 지 알려 줄 수 있도록 1초단위 값을 리턴하기 때문이다.


데이터 전송과 시그널(Data transferring and signals)


데몬 프로그램에서 매우 중요한 것은 시스템 펑션의 인터럽트 처리이다. 문제의 한 부분은 recv(2)write(2) 와 select(2) 같은 데이터를 전송하는 펑션이 시그널에 의해 인터럽트 되어서 시그널 처리 후에 하던 처리를 계속해야 한다는 점이다. 우리는 sleep(3)의 경우에 어떻게 처리하는 지를 보았다.

See an example of how to handle interruption of system calls.

This program reads from it's standard input and copies the data to the standard output. Additionally, when SIGUSR1 is received it prints tostderr how many bytes has been already read and written. It installs a signal handler which sets a global flag to 1 if called. Whatever the program does at the moment it receives the signal, the numbers are immediately printed. It works because read(2) and write(2) functions are interrupted by signals even during operation. In case of those functions two things might happen:

  • When read(2) waits for data or write(2) waits for stdout to put some data and no data were yet transfered in the call and SIGUSR1 arrives those functions exit with return value of -1. You can distinguish this situation from other errors by reading the value of the errnovariable. If it's EINTR it means that the function was interrupted without any data transfered and we can call the function again with the same parameters.
  • Another case is that some data were transfered but the function was interrupted before it finished. In this case the functions don't return an error but a value less that the supplied data size (or buffer size). Neither the return value nor the errno variable tells us that the function was interrupted by a signal, if we want to distinguish this case we need to set some flag in the signal handler (as we do in this example). To continue after interruption we need to call the function again keeping in mind that some data were consumed or read adn we must restart from the right point. In our example only the write(2) must be properly restarted, we use the written variable to track how many bytes were actually written and properly call write(2) again if there are data left in the buffer.

Remember that not all system calls behave exactly the same way, consult their manual page to make sure.

Reading the sigaction(2) manual page you can think that setting the SA_RESTART flag is simpler that handling system call interruption. The documentation says that setting it will make certain system calls automatically restartable across signals. It's not specified which calls are restarted. This flag is mainly used for compatibility with older systems, don't use it.

Blocking signals


How to block signals


There is sometime a need to block receiving some signals, not handling them. Traditional way is to use the deprecated signal(2) function with SIG_IGN constant as a signal handler. There is also newer, recommended function to do that: sigprocmask(2). It has a bit more complex usage, let's see an example of signal blocking with sigprocmask().

This program will sleep for 10 seconds and will ignore the SIGTERM signal during the sleep. It works this way because we've block the signal with sigprocmask(2). The signal is not ignored, it's blocked, it means that are queued by the kernel and delivered when we unblock the signal. This is different than ignoring the signal with signal(2). First sigprocmask(2) is more complicated, it operates in a set of signals represented by sigset_t, not on one signal. The SIG_BLOCK parameter tells that the the signals in set are to be blocked (in addition to the already blocked signals). The SIG_SETMASK tells that the signals in set are to be blocked, and signals that are not present in the set are to be unblocked. The third parameter, if not NULL, is written with the current signal mask. This allows to restore the mask after modifying the process' signal mask. We do it in this example. The first sleep(3) function is executed with SIGTERM blocked, if the signal arrives at this moment, it's queued. When we restore the original signal mask, we unblock SIGTERM and it's delivered, the signal handler is called.

See the sigprocmask(2) manual on how to use this function and sigsetops(3) on how to manipulate signal sets.

Preventing race conditions.


In the previous example nothing really useful was presented, such use of sigprocmask(2) isn't very interesting. Here is a bit more complex example of code that really needs sigprocmask(2):

Fragment of example: Signal race with select() and accept()

  1. while (!exit_request) {
  2. fd_set fds;
  3. int res;
  4.  
  5. /* BANG! we can get SIGTERM at this point. */
  6.  
  7. FD_ZERO (&fds);
  8. FD_SET (lfd, &fds);
  9.  
  10. res = select (lfd + 1, &fds, NULL, NULL, NULL);
  11.  
  12. /* accept connection if listening socket lfd is ready */
  13. }

Let's say it's an example of a network daemon that accepts connections using select(2) and accept(2). It can use select(2) because it listens on multiple interfaces or waits also for some events other than incoming connections. We want to be able to cleanly shut it down with a signal like SIGTERM (remove the PID file, wait for pending connections to finish etc.). To do this we have a handler for the signal defined which sets global flag and relay on the fact that select(2) will be interrupted when the signal arrives at the moment we are just waiting for some events. If the main loop in the program looks similarly as the above code everything works... almost. There is a specific case in which the signal will not interrupt the program even if it does nothing at all at the moment. When it arrives between checking the while condition and executing select(2). The select(2) function will not be interrupted (because signal was handled) and will sleep until some file descriptor it monitors will be ready.

This is where the sigprocmask(2) and other "new" functions are useful. Let's see an improved version:

Fragment of example: Using pselect() to avoid a signal race

  1. sigemptyset (&mask);
  2. sigaddset (&mask, SIGTERM);
  3.  
  4. if (sigprocmask(SIG_BLOCK, &mask, &orig_mask) < 0) {
  5. perror ("sigprocmask");
  6. return 1;
  7. }
  8.  
  9. while (!exit_request) {
  10.  
  11. /* BANG! we can get SIGTERM at this point, but it will be
  12. * delivered while we are in pselect(), because now
  13. * we block SIGTERM.
  14. */
  15.  
  16. FD_ZERO (&fds);
  17. FD_SET (lfd, &fds);
  18.  
  19. res = pselect (lfd + 1, &fds, NULL, NULL, NULL, &orig_mask);
  20.  
  21. /* accept connection if listening socket lfd is ready */
  22. }

What's the difference between select(2) and pselect(2)? The most important one is that the later takes an additional argument of typesigset_t with set of signals that are unblocked during the execution of the system call. The idea is that the signals are blocked, then global variables/flags that are changed in signal handlers are read and then pselect(2) runs. There is no race because pselect(2) unblocks the signals atomically. See the example: the exit_request flag is checked while the signal is blocked, so there is no race here that would lead to executing pselect(2) just after the signal arrives. In fact, in this example we block the signal all the time and the only place where it can be delivered to the program is the pselect(2) execution. In real world you may block the signals only for the part of the program that contains the flag check and the pselect(2) call to allow interruption in other places in the program.
Another difference not related to the signals is that select(2)'s timeout parameter is of type struct timeval * and pselect(2)'s is const structtimespec *. See the pselect(2) manual page for more information.

If you like poll(2) there is analogous ppoll(2) functions, but in contrast of pselect(2) ppoll(2) is not a standard POSIX function.

Waiting for a signal


Suppose we want to execute an external command and wait until it exits. We don't want to wait forever, we want to set some timeout after which we will kill the child process. How to do this? To run a command we use fork(2) and execve(2). To wait for a specific process to exit we can use the waitpid(2) function, but it has no timeout parameter. We can also create a loop in which we call sleep(3) with the timeout as an argument and use the fact that sleep(3) will be interrupted by the SIGCHLD signal. This solution will work... almost. It would contain a race condition: if the process exits immediately, before we call sleep(3) we will wait until the timeout expires. It's a race similar to the one described previously.

The proper solution is to use a dedicated function to wait for a signal: see an example of using sigtimedwait().

This program creates a child process that sleeps few seconds (in a real world application this process would do something like execve(2)) and waits for it to finish. We want to implement a timeout after which the process is killed. The waitpid(2) function does not have a timeout parameter, but we use the SIGCHLD signal that is sent when the child process exits. One solution would be to have a handler for this signal and a loop with sleep(3) in it. The sleep(3) will be interrupted by the SIGCHLD signal or will sleep for the whole time which means the timeout occurred. Such a loop would have a race because the signal could arrive not in the sleep(3), but somewhere else like just before thesleep(3). To solve this we use the sigtimedwait(2) function that allows us to wait for a signal without any race. We can do this because we block the SIGCHLD signal before fork(2) and then call sigtimedwait(2) which atomically unblock the signal and wait for it. If the signal arrives it block it again and returns. It can also take a timeout parameter so it will not sleep forever. So without any trick we can wait for the signal safely.

One drawback is that if sigtimedwait(2) is interrupted by another signal it returns with an error and doesn't tell us how much time elapsed, so we don't know how to properly restart it. The proper solution is to wait for all signals we expect at this point in hte program or block other signals. There is another small bug i the program: when we kill the process, SIGCHLD is sent and we don't handle it anywhere. We should unblock the signal before waitpid(2) and have a handler for it.

Other functions to wait for a signal


There are also other functions that can be used to wait for a signal:

  • sigsuspend(2) - waits for any signal. It takes a signal mask of signals that are atomically unblocked, co it doesn't introduce race conditions.
  • sigwaitinfo(2) - like sigtimedwait(2), but without the timeout parameter.
  • pause(2) - simple function taking no argument. Just waits for any signal. Don't use it, you will introduce a race condition similar to the described previously, use sigsuspend(2).

Sending signals


Sending signal from keyboard


There are two special key combinations that can be used in a terminal to send a signal to the running application:
  • CTRL-C - sends SIGINT which default action is to terminate the application.
  • CTRL-\ - sends SIGQUIT which default action is to terminate the application dumping core.
  • CTRL-Z - sends SIGSTOP that suspends the program.

kill()


The simplest way to send a signal to the process is to use kill(2). It takes two arguments: pid (PID of the process) and sig (the signal to send). Although the function has a simple interface it's worth to read the manual page because there are few more things we can do than just sending a signal to a process:
  • The pid can be 0, the signal will be sent to all processes in the process group.
  • The pid can be -1, the signal is sent to every process you have permission to send signals except init and system processes (you won't kill system threads).
  • The pid can be less than -1 to send signal to all processes in the process group whose ID is -pid.
  • You can check is a process exists sending signal 0. Nothing is really sent, but the kill(2) return value will be as if it sent a signal, so if it's OK it means that the process exists.

Sending signals to yourself


There are two standard function that will help you to send signals to yourself:
  • raise(3) - Just send the specified signal to yourself, but if it's a multithreaded program it sends the signal to the thread, not the process.
  • abort(3) - Sends SIGABRT, but before that it will unblock this signal, so this function works always, you don't need to bother about unblocking this signal. It will also terminates you program even if you have handler for SIGABRT by restoring the default signal handler and sending the signal again. You can prevent it as was mentioned in signal handling chapter.

Sending data along with signal - sigqueue()


The sigqueue(2) function works very similar to kill(2) but is has a third argument of type const union sigval which can be used to send an integer value or a pointer that can be read in the signal handler if it reads the siginfo_t argument. If you use this function instead of 32) the handler can distinguish this with the si_code field because it will have SI_QUEUE value.

Real-time signals


POSIX 표준은 리얼타임 시그널이라 불리는 것을 정의하고 리눅스는 그것을 지원한다. 이것은 프로그래머에 의해 사용되고 의미가 미리 정의되어 있지는 않다. 시그널의 범위를 알기 위해 SIGRTMIN 과 SIGRTMAX 2개의 매크로를 이용할 수있다. 이것을 SIGRTMIN+n(n = 임의의 번호) 과 같은 식으로 사용할 수 있다. 리얼타임 시그널은 쓰레드 라이브러리 (리눅스 쓰레드와 NPTL(Native Posix Thread Library) 양쪽 다) 에서 사용되고 있으므로 실행시에 SIGRTMIN 을 조정하게 된다. 따라서 결코 시그널 숫자를 하드코딩해서는 안된다.

RT 시그널과 표준 시그널을 뭐가 다를까? 다음 2가지가 다르다:

  • 누군가 그것을 보내는 동안 시그널이 블록된다면 2개 이상의  RT 시그널이 큐에 쌓일 수 있다. 표준 시그널에서는 오직 1개만이 큐잉되고 나머지는 무시된다.
  • RT 시그널의 전송 순서는 보낸 순서와 같은 것이 보장된다.
  • 전송 프로세스의 PID 와 UID 는 siginfo_t 의 si_pid 와 si_uid 필드에 저장된다. 더 자세한 정보는 signal(7) man 페이지의 RT시그널 부분을 참고하라.


Signals and fork()


What happens with signals and signal-related settings after fork(2)? A new child starts with the signal queue empty even if some signals were queued for the parent at the time fork(2) was invoked. Signal handers and blocked signal state is inherited by the child. Attributes of file descriptors associated with signals are also inherited. In conclusion: no unexpected behavior here, you don't need to set up any signal handlers or mask in the child.

Signals and threads


단일 쓰레드 프로그램과 멀티 쓰레드 프로그램 사이의 시그널 처리는 차이가 있다. 

멀티 쓰레드 프로그램 POSIX 사양에 따르면 하나의 PID를 가지는 하나의 프로세스는 시그널이 도착했을 때 어떤 쓰레드가 인터럽트 될까? 

만약 지원되지 않는 구식 리눅스쓰레드 구현을 가지고 있다면 답은 간단하다. 모든 쓰레드는 분리된 PID를 가진다. 따라서 시그널은 kill(2)에 의해 지정된 PID를 가지는 쓰레드로 전달되며 이 경우에 모든 쓰레드는 분리된 프로세스로 처리된다. 이러한 사실은 그다지 중요하지 않다. 현대의 모든 리눅스 배포판은 더 이상 이 구현을 사용하지 않기 때문이다.

네이티브 POSIX 쓰레드 라이브러리를 사용하는 경우는 보다 흥미롭다. 여기서 설명하는 것은 POSIX 호환 구현에 대해 설명하는 것이므로 다른 POSIX 시스템에도 적용된다.


어떤 쓰레드가 시그널을 받을까?


이것은 대단히 흥미로운 질문이다. 다음 두가지 경우가 있다:

  • (kill(2) 같은 펑션을 사용해서 PID 에 보내는)프로세스 지향 시그널 : 쓰레드는 sigprocmask(2) 와 비슷한 pthread_sigmask(2)를 이용해서 분리된 시그널 마스크를 가진다. 따라서 그러한 시그널은 블록된 쓰레드에 전달되지 않는다. 그것은 시그널이 블록되지 않은 쓰레드 중 하나에 전달된다. 어떤 쓰레드가 받는 지는 정의되지 않았다. 만약 모든 쓰레드가 시그널이 블록되어 있다면 그것은 per-process 큐에 쌓인다. 만약 시그널을 위해 정의된 핸들러가 없고 디폴트 액션이 프로세스를 종료하는 거라면 전체 프로세스는 종료된다.
  • 쓰레드 지향 시그널 : 특정한 쓰레드에 시그널을 보내는 특별한 펑션 pthread_kill(2) 이 있다. 이것은 어떤 쓰레드에서 다른 쓰레드(또는 그 자신에게)로 시그널을 보내는 데 사용될 수 있다. 이 방법으로 쓰레드는 특정한 쓰레드에 보내지거나 큐잉 될 수 있다. SIGSEGV 같은 OS(운영체제)에 의해 발생된 쓰레드 지향 시그널도 있다. 만약 시그널을 위해 지정된 핸들러가 없고 디폴트 액션이 프로세스를 종료하는 것이라면 전체 프로세스가 종료된다.

위에서 본 것처럼 프로세스 범위의 큐와 쓰레드 단위의 큐가 있다.


Signal handlers


시그널 액션은 전체 프로세스를 위해 설정 된다. 멀티 쓰레드를 위한 signal(2) 의 동작은 정의되지 않았고 sigaction(2) 을 사용해야 한다. signal(7) 안의 싱글 세이프로 기술된 pthread  관련 펑션은 존재하지 않는다는 것을 명심해야 한다. 특히 시그널 핸들러 안에서 뮤텍스의 사용은 아주 나쁜 생각이다.


sigwaitinfo()/sigtimedwait() and process-directed signals


프로세스 지향 시그널을 위한 sigwaitinfo(2) 와 sigtimedwait(2) 펑션을 신뢰성 있는 동작을 얻으려면, 기다리는 모든 시그널은 모든 쓰레드에 대해 블록되어야 한다. 특히 프로세스 지향 시그널을 위해 pause()를 사용하는 것은 나쁜 생각이 될 수 있다.

Real-time signals


이전에도 말한 것처럼, 양쪽 쓰레드 구현 (LinuxThreads 와 NPTL) 은 내부적으로 일정 개수의 리얼타임 시그널을 사용한다. 그래서 그러한 시그널을 참조할 때는 SIGRTMIN+n 표현을 사용하는 것이 좋은 방법이다.

Other uses of signals


Here I'll present non-traditional uses of signals, but mainly for historical reasons. We have better mechanisms to do the same things now, but it might be interesting that signals may be used this way.

It's possible to be notified of I/O availability by a signal. It's an alternative to functions like select(2). It's done by setting the O_ASYNC flag on the file descriptor. If you do so and if I/O is available (as select(2) would consider it) a signal is sent to the process. By default it's SIGIO, but using Real-time signals is more practical and you can set up the file descriptor using fcntl(2) so that you get more information in siginfo_tstructure. See the links at the bottom of this article for more information. There is now a better way to do it on Linux: epoll(7) and similar mechanisms are available on other systems.

The dnotify mechanism uses similar technique: you are notified about file system actions using signals related to file descriptors of monitored directories or files. The recommended way of monitoring files is now inotify.

Related websites


Here are some places worth visiting that describe some topics in more details.

http://www.visolve.com/squid/whitepapers/squidrtsignal.php - Describes Squid's modifications to use RT signals to polling sockets.
http://www.linuxjournal.com/article/3985 - Old (2000), but still interesting article on how signals are implemented in Linux kernel.

It's not really everything...


The title of this article is misleading. UNIX/Linux signals is a big topic. When I was writing it I found many aspects of signals I had not known about. I'm also not a big expert, so as always: there could be bugs, not all important things may be mentioned. Comments are welcome!