시그널(앞으로 신호라 해석하지 않고 시그널이라고 하겠습니다. 그것이 더 좋을 것 같아서. . . )은 프로세스에게 배달된 소프트웨어 인터럽트이다. 운영체제는 실행하고 있는 프로그램에 예외적인 상황을 보고하기 위해서 시그널을 사용한다. 어떤 시그널들은 유용하지 않은 메모리 주소를 참조하는것과 같은 에러를 보고하고; 다른 것은 전화선의 단절과 같은, 비동기적 사건을 보고한다.
GNU C 라이브러리는 각각의 특정한 사건들의 종류에 따라, 다양한 시그널의 형태를 정의한다. 사건들의 어떤 종류들은 보통 프로그램의 계속된 진행을권장할 수 없거나 불가능하게 하고, 그에 해당하는 시그널들은 보통 그 프로그램을 중지시킨다. , 유해하지 않은 사건들을 보고한 다른 종류의 시그널들은 보통 무시된다.
만일 당신이 시그널이 발생한 사건을 예상한다면, 당신은 시그널 처리 함수를 정의할 수 있고 특정한 형태의 시그널이 도착했을 때 운영체제가 그 시그널 처리함수를 실행하게 할 수 있다. 최종적으로, 하나의 프로세스는 다른 프로세스에게 한 개의 신호를 보낸다; 이것은 부모 프로세스가 자식 프로세스를 중지시키는 것을 허용하거나, 또는 두 개의 연관된 프로세스가 통신하거나 동기하도록 하는 것을 허용한다.
어떻게 시그널들이 발생되고, 시그널이 도착된 이후에 무슨 일이 발생할 것이며, 어떻게 프로그램이 시그널을 처리할 수 있는지에 대한 기본 원칙들을 설명한다.
신호는 예외적인 사건의 발생을 보고한다. 다음은 시그널을 발생시키는 어떤 예외적인 사건들이다.
프로그램이 0으로 나누는 일을 하거나, 또는 유용한 범위를 넘어서는 주소를 억세스하려는것과 같은 에러.
사용자가 프로그램을 인터럽트 또는 중지하도록 요청한다. 대부분의 환경들은 사용자가 C-z를 타이핑하면 일시 중지하거나, C-c를 타이핑하면 종료를 허용하도록 만들어졌다. 키 시퀀스( key sequence)에 무엇이 사용되었던지, 운영체제는 프로세스를 인터럽트 하기 위하여 적당한 시그널을 보낸다.
이들 사건들(죽이거나 발생하기 위해 명백하게 호출한 것을 제외하고 )의 각각은 자신만의 특정한 종류의 신호를 발생시킨다. 다양한 종류의 시그널들은 21. 2절 [Standard Signals] 에 상세하게 설명되었다.
일반적으로, 시그널을 발생시키는 사건들은 세 가지로 나눌 수 있다: 에러들. 외부의 사건들과 명백한 요청.
에러는 프로그램이 무언가 유용하지 않을 일을 하고 실행을 계속할 수 없는 것을 의미한다. 그러나 에러들의 모든 종류가 시그널을 발생시키지는 않는다_실제로 그들의 대부분은 시그널을 발생시키지만. . . 예를 들어, 존재하지않는 파일을 개방하기와 같은 것은 에러이지만, 그것은 시그널을 발생시키지 않고; 대신에 open은 -1을 반환한다. 일반적으로, 에러들은 에러를 지적하는 값을 반환함으로써 보고되는 어떤 라이브러리 함수들과 연관되어있다. 시그널들을 발생시킨 에러들은 단지 라이브러리 호출뿐만 아니라 프로그램의 어디서든 발생할 수 있다. 그들에는 0으로 나누기를 하고 유용하지 않은 메모리 주소의 참조가 포함된다.
외부에서 발생한 사건은 입/출력이나 다른 프로세스들과 함께 하는 것에서 나온다. 그들에는 입력의 도착, 타이머의 경과, 자식 프로세스의 종료등이 포함된다.
명백한 요청은 kill처럼 특별하게 시그널을 발생하도록 어떤 목적을 가진 라이브러리 함수의 사용을 의미한다.
시그널들은 동기적으로 또는 비동기적으로 발생되어진다. 동기적 시그널 은 프로그램의 어느 정해진 동작과 관계하고, 그 동작을 하는 동안(블록된 것이 아니라면)에 배달 되어진다. . 에러들은 동기적으로 시그널을 발생하고, 프로세스가 같은 프로세스를 위하여 시그널을 발생하도록 함으로써 명백하게 요청한다.
비동기적 시그널들은 시그널을 받은 프로세스의 제어의 밖에서 발생한 사건에 의해 발생되어진다. 그들 시그널들은 실행동안에 예측할 수 없는 시간에 도착된다. 외부의 사건들은 비동기적으로 시그널들을 발생하고, 어떤 다른 프로세스에 적용하도록 명백하게 요청한다.
시그널의 주어진 형태는 전형적으로 동기적이거나 또는 비동기적중에 하나가 된다. 예를 들어, 에러를 위한 시그널은 에러가 동기적으로 신호를 발생했다면 전형적으로 동기적이다. 그러나 어느 시그널의 형태는 명백한 요청으로는 동기적, 또는 비동기적으로 발생되어질 수 있다.
시그널이 발생되어졌을 때, 그때는 아직 미해결인 상태가 된다. 일반적으로 아주 짧은 시간동안만 미해결인채로 남아있고 그 다음에는 신호가 프로세스에게 배달 되어진다. 그렇지만 만일 시그널의 종류가 블록되어졌다면, 그것은 막연히 미해결인체로 남아있게 될 것이다_그 시그널의 블록이 해제될때까지. 일단 블록이 해제되면, 그것은 즉시 배달되어질 것이다. 21. 7절 [Blocking Signals] 참조.
시그널이 배달되었을 때, 즉시 또는 긴 지연후에 그 시그널을 위하여 정해진 행동을 하게 된다. SIGKILL 과 SIGSTOP 와 같은 어떤 시그널들은 그 행동이 정해져있지만, 대부분의 시그널들은 프로그램이 선택하게 된다: 시그널을 무시하거나, 처리 함수를 지정하거나, 또는 시그널의 종류 에 따라 디폴트 동작을 하거나. 프로그램은 signal이나 sigaction과 같은 함수들을 사용해서 선택을 하게 된다(21. 3절 [Signal Actions] 참조. ).
우리는 때때로 핸들러가 시그널을 잡았다고 얘기를 한다. 핸들러가 실행되고 있는 동안, 특정한 시그널은 일반적으로 블록되어진다. 만일 한 종류의 시그널을 위한 정해진 동작이 시그널을 무시하는것이라면, 발생되어진 시그널은 즉시 버려진다 이것은 심지어 시그널이 동시에 블록 되어질지라도 발생한다. 이렇게 버려진 시그널은 비록 프로그램이 연속적으로 그 종류의 시그널을 위하여 다른 동작을 지정하고 블록을 해제하지 않았을지라도 결코 배달되지 않을 것이다.
프로그램에서 처리되지도 않고 무시하지도 않는 신호가 발생하면, 그것의
디폴트 동작이 일어난다. 시그널의 각 종류들은 밑에 설명된 자신의 디폴트 동작을 갖는다(21. 2절 [Standard Signals] 참조. ). 대부분의 시그널에서 디폴트 동작은 프로세스를 종료하는 것이다. "유해하지 않은" 사건들에서 발생한 어떤 종류의 시그널들의 디폴트 동작은 아무 것도 하지않는 것이다.
한 시그널이 프로세스를 종료할 때, 그것의 부모 프로세스는 wait 또는 waitpid 함수들에 의해 보고된 종료 상황 코드를 조사함으로써 종료가 발생한 원인을 알아낼 수 있다. (이것에 대해서는 23. 6절 [Process Completion] 에 좀더 자세하게 나와있다. ) 시그널의 원인이 된 종료의 요소, 그리고 시그널의 종료를 포함해서 정보를 얻을 수 있다. 만일 쉘로부터 당신이 실행하고 있는 어떤 프로그램이 시그널에 의해 종료가 된다면, 그 쉘은 그것에 해당하는 에러메세지를 프린트 할 것이다. 프로그램의 에러를 표현하는 시그널들은 특별한 속성을 갖는다: 시그널 중의 하나가 프로세스를 종료할 때, 종료한 시간에 프로세스의 상황에 대한 기록을 코어 덤프 파일에 기록한다. 당신은 무슨 에러가 발생했는지 조사 하기 위해서 디버거를 사용해서 코어 덤프 파일을 조사할 수 있다.
만일 당신의 명백한 요청에 의해서 프로세스를 종료하고 "프로그램 에러" 시그널이 발생하면, 직접적으로 에러에 기인하는 시그널로써 코어 덤프 파일을 만든다.
이 절은 다양한 표준 시그널들의 이름과 그것이 어떤 사건을 의미하고 있는지를 설명하고 있다. 각각의 시그널의 이름은 시그널의 종류를 위한 시그널번호로써 양의 정수로 나타낸 매크로이다. 당신은 여기에서 정의한 이름대신에 당신 마음대로 시그널의 번호코드를 결코 가정할 수 없다. 이것은 시그널의 종류에 부여된 번호가 시스템에서 시스템으로는 바꿀수 있지만, 그 이름들의 의미는 표준화되어있고 완전히 단일화 되어있기 때문이다.
시그널의 이름들은 헤더파일 `signal. h'에 정의되어 있다.
매크로 : int NSIG
다음의 시그널들은 심각한 프로그램의 에러가 운영체제나 컴퓨터 자체에 의해 검출되었을 때 발생 된다. 일반적으로, 이들 시그널 모두는 당신의 프로그램이 심각하게 깨져있고, 에러가 포함된 그 실행을 계속할 아무런 방법이 없음을 지적한다.
어떤 프로그램들은 프로그램의 에러 시그널로 인해서 종료되기전에 그들을 깨끗하게 처리한다. 예를 들어, 터미널 입력의 반향을 끈(tnun off) 프로그램들은 다시 반향을 켤 목적으로 프로그램 에러 시그널들을 처리할 것이다. 핸들러는 시그널을 위한 디폴트 동작을 정하고 그 동작을 함으로써 끝날 것이다; 만일 프로그램이 시그널 핸들러를 가지지 않았다면, 프로그램은 그 시그널로 인해서 종료될 것이다. ( 21. 4. 2절 [Termination in Handler] 참조. )
종료는 대부분의 프로그램에 에서 에러에 대응한 이해 가능한 최종적인 결과이다. 그렇지만, Lisp과 같은 프로그래밍시스템들은 사용자 프로그램에 에러가 발생했을지라도 컴파일된 사용자 프로그램을 실행시켜야할 필요가 있다면 로드(load)시킬 수 있다. 이들 프로그램은 커멘드 레벨(command level)로 제어를 반환하는 longjmp를 사용한 핸들러를 갖는다. 모든 시그널의 디폴트 동작은 프로세스를 종료하는 것이다. 만일 당신이 그 시그널들을 블록하거나 무시하거나 시그널을 위한 핸들러를 만든다면, 당신의 프로그램은 아마도 그와같은 시그널들이 발생했을 때, 그들이 실제 에러대신에 raise나 kill에 의해 발생된 것이 아니라면, 심각하게 파괴될 것이다.
그들 프로그램 에러 시그널중의 하나가 프로세스를 종료할 때, 종료와 같은 시간에 프로세스의 상황기록을 코어덤프 파일에 출력한다. 코어덤프 파일은 `core'라고 이름지어졌고 프로세스가 현재 존재하고 있는 디렉토리 에 존재한다. ( GNU 시스템에서, 당신은 환경변수 COREFILE를 통해서 코어 파일의 이름을 지정할 수 있다. ) 코아덤프파일의 존재 목적은 무슨 에러가 발생했는지 조사 하기 위함으로써, 디버거를 사용해서 그들을 시험할 수 있다.
매크로 : int SIGFPE
플로팅-포인트 예외상황에 대한 것은 아주 민감하게 다른 의미를 지닌 예외상황의 여러종류들이 있기 때문에 아주 복잡한 주제이고, SIGFPE 시그널은 그들을 구분하지 않는다. 이진 플로팅-포인트 연산을 위한 IEEE 표준(ANSI/IEEE Std 754-1985)은 다양한 플로팅-포인트 예외상황에 대해서 정의하고 있고 컴퓨터 시스템이 예외상황의 발생을 보고할 때 따르도록 요구한다. 그렇지만, 이 표준은 그 예외상황이 어떻게 보고되는 지에 대해서는 지정하지 않았고, 또한 운영체제가 제어와 처리의 어떤 종류를 프로그래머에게 제공할 수 있는지를 지정하지 않았다.
BSD 시스템들은 예외상황의 다양한 원인을 구별하는 특별한 인수를 가진 SIGFPE 핸들러를 제공한다. 이 인수를 억세스 하기 위해서, 당신은 두 개의 인수를 받아들이는 핸들러를 정의해야만 한다. GNU 라이브러리는 이 특별한 인수를 제공하지만, 그 값은 BSD 시스템과 GNU 시스템에서만 오직 의미가 있다.
역자주 : trap(트랩) : 하나의 명령어가 실행될 따마다 자동적으로 발생되는 인터럽트. 이러한 인터럽트는 중앙처리 장치에 의하여 하드웨어 적으로 발생하게 되는데 프로그램에서 하나의 명령어가 실행될 때마다 자동적으로 미리 정의된 트랩 처리 루틴으로 실행의 제어권이 넘어온다. 하드웨어 장치와 밀접한 관련이 있는 시스템 소프트웨어에서 오류를 찾아내기 위한 수단으로 이용된다.FPE_INTOVF_TRAP
FPE_INTDIV_TRAP
FPE_SUBRNG_TRAP
FPE_FLTOVF_TRAP
FPE_FLTDIV_TRAP
FPE_FLTUND_TRAP
FPE_DECOVF_TRAP
매크로 : int SIGILL
매크로 : int SIGSEGV
매크로 : int SIGBUS
매크로 : int SIGABRT
이들 시그널들은 이런 저런 방법으로 프로세스를 종료함을 알리기위해 사용된다. 그들은 완전히 다른 목적을 위해 사용되기 때문에 다른 이름을 가졌고, 프로그램은 그들은 다르게 취급하기를 원할 것이다.
이들 시그널들은 처리하기 위한 이유는 보통 당신의 프로그램이 실제로 종료되기전에 적당하게 처리할 수 있도록 하기 위한 것이다. 예를 들어, 당신은 상황정보를 저장하고, 임시 파일들을 지우고, 이전의 터미널 모드를 반환하기를 원할수도 있다. 그와 같이 핸들러(handler)는 발생된 시그널을 위한 디폴트 동작을 지정하고 그리고 그 시그널을 다시 발생시킴으로써 종료할 것이다. 이것은 만일 프로그램이 핸들러를 가지지 않았더라도, 그 시그널로 인해서 프로그램이 종료될 것이다. ( 21. 4. 2절 [Termination in Handler] 참조. )
이 시그널들을 위한 (명백한) 디폴트 동작은 프로세스가 종료되도록 하는 것이다.
매크로 : int SIGHUP
매크로 : int SIGINT
매크로 : int SIGQUIT
매크로 : int SIGTERM
매크로 : int SIGKILL
그들 시그널은 타이머의 경과를 지적하는데 사용되어진다. 이들 시그널을 보내는 함수에 대한 정보는 17. 3절 [Settin an Alarm] 참조. 그들 시그널을 위한 디폴트 동작은 프로그램을 종료를 일으키는 것이다. 이 디폴트 동작은 거의 유용하지 않다; 그들 시그널을 사용하는 대부분의 방법은 어느 경우에 맞는 핸들러 함수들을 요구하는 것이다.
매크로 : int SIGALRM
매크로 : int SIGVTALRM
매크로 : int SIGPROF
이 절에 설명된 시그널들은 비동기 입/출력 도구들과 함께 사용되어진다. 당신은 어떤 특정한 파일 기술자가 그들 시그널을 발생시키도록 하기 위해서 fcntl을 호출함으로써 명백한 동작을 취하도록 해야한다( 8. 12절[Interrupt Input] 참조. ) 그들 시그널을 위한 디폴트 동작은 그들을 무시하는 것이다.
매크로 : int SIGIO
매크로 : int SIGURG
이들 시그널은 작업 제어를 지원하기 위해서 사용되어진다. 만일 당신의 시스템이 작업 제어를 지원하지 않는다면 시그널들은 발생되어지거나, 처리될 수는 없지만 매크로들은 정의되어있다. 당신이 실제로 작업이 어떻게 제어되는지를 이해할 수 없다면 그들 시그널을 그대로 방치할 것이다. 24장 [Job Control] 참조.
매크로 : int SIGCHLD
매크로 : int SIGCONT
매크로 : int SIGSTOP
매크로 : int SIGTSTP
매크로 : int SIGTTIN
매크로 : int SIGTTOU
그들 시그널은 다양한 다른 상활들을 보고하기 위해서 사용되어진다. 이들의 디폴트 동작은 프로세스가 종료되도록 하는 것이다.
매크로 : int SIGPIPE
매크로 : int SIGUSR1
매크로 : int SIGUSR22
특정한 운영체제는 위에 설명되지 않은 부가적인 시그널들을 지원한다. ANSI C 표준은 시그널들의 명칭을 `SIG'로 시작하는 대문자로 예약하였다. 당신은 당신의 특정한 운영체제를 위한 헤더파일이나 그 운영체제가 지원하고 있는 시그널을 발견하는 프로세서의 타입 등에 대한 것에 조언을 구할 수 있다. 예를 들어, 어떤 시스템은 하드웨어 트랩에 해당하는 여분의 시그널들을 제공한다. 보통 지원되는 어떤 다른 종류의 시그널들은 CPU 시간과 파일 시스템 사용에 대한 제한을 가하거나, 터미널 구성을 비동기적으로 변경하는 것과 같은 것을 위해 사용되어진다. 시스템들은 또한 표준 시그널 이름의 별칭(aliases)이 되는 시그널 이름들을 정의하고 있다.
당신은 당신이 이해하고 있는 정의된 시그널들을 위한 디폴트 동작을 (또는 쉘에 의해 작동하는 동작) 가정할 수 있고, 당신은 그들에 대해서는 걱정하지 않는다. 당신이 그 시그널의 의미를 알지 못하는 것에 대해서 핸들러를 만들려 시도하거나 알지 못하는 시그널을 무시하거나 블록하는 것은 좋지 못한 생각이다. 여기에 일반적으로 운영체제에서 사용되고 있는 약간의 다른 시그널에 대한 것이 있다.
SIGCLD
SIGTRAP
SIGIOT
SIGEMT
SIGSYS
SIGPOLL
SIGXCPU
SIGXFSZ
SIGWINCH
우리는 자식 프로세스를 종료한 시그널을 설명하는 메시지를 쉘이 프린트하는 것에 대해서는 위에서 잠깐 언급했다. 시그널을 설명하는 메시지를 프린트하는 깨끗한 방법은 strsignal 과 psignal 함수들을 사용하는 것이다. 그들 함수들은 설명하려는 시그널의 종류를 지정하기 위해서 시그널 번호를 사용한다. 시그널번호는 자식 프로세스의 종료상황으로부터(23. 6절 [Process Compltion] 참조)오거나 또는 같은 프로세스 안에 있는 시그널 핸들러로부터 올 것이다.
함수 : char * strsignal (int signum)
함수 : void psignal (int signum, const char *message)
시그널을 위한 동작을 변경하기 위한 가장 간단한 방법은 signal함수를 사용하는 것이다. 당신은 내장된(built-in) 동작을 지정하거나, 핸들러를 만들 수 있다. GNU 라이브러리는 또한 좀더 다양한 기능을 가진 sigaction 도구를 사용한다. 이 절은 두 개의 도구들에 대한 설명과 언제 이것을 사용할 지에 대한 제안을 한다.
signal 함수는 특정한 시그널을 위한 동작을 만들기 위한 간단한 인터페이스를 제공한다. 그 함수와 연관된 매크로들은 헤더파일 `signal. h'에 선언되어 있다.
데이터 타입 : sighandler__t
void handler (int signum) { . . . }
함수 : sighandler_t signal (int signum, sighandler_t action)
SIG_DFL
SIG_IGN
handler
EINVAL
주어진 시그널이 무시되도록 미리 설정되어졌다면, 이 코드는 그 설정을 바꾸는 것을 피함을 기억하라. 이것은 비-작업-제어 쉘들이 자식 프로세스가 시작될 때 어떤 시그널들을 종종 무시하기 때문이고, 그리고 이것을 고려하는 것은 자식 프로세스에게는 중요하다. 우리는 이 예제 프로그램이 디버깅을 위해서(코어 덤프 파일) 정보를 제공하도록 만들어졌기 때문에 프로그램 에러 시그널이나 SIGQUIT를 처리하지 않고, 임시 파일들은 유용한 정보를 가질 것이다.
함수 : sighandler_t ssignal (int signum, sighandler_t action)
매크로: sighandler_t SIG__ERR
sigaction 함수는 signal과 같은 기본 효과를 갖는다: 한 시그널이 어떻게 프로세스에 의해 처리될 것인지를 정하는. sigaction은 조금은 복잡하지만, 더 많은 제어를 제공한다. 특별하게, sigacion은 시그널이 언제 발생되고 어떻게 그 핸들러가 호출될 것인지에 대해 제어를 할 수 있는 부가적인 플래그를 지정하도록 허용한다. sigaction 함수는 `signal. h'에 선언되어 있다.
데이터 타입 : struct sigaction
sighandler_t sa_handler
sigset_t sa_mask
int sa_flags
함수 : int sigaction(int signum, const struct sigaction *action, struct sigaction *old_action)
EINVAL
한 단일한 프로그램 안에서 signal 과 sigaction 함수들을 모두 사용하는 것이 가능하지만, 그들은 완전히 다른 방법들로 서로 영향을 미칠 수 있기 때문에 주의해야만 한다.
sigaction 함수는 signal 함수보다는 좀더 많은 정보를 지정하기 때문에, signal로부터의 반환값은 sigaction이 표현할 수 있는 범위를 표현할 수 없다. 그렇지만, 만일 당신이 어느 동작을 저장하고 나중에 다시 그 동작을 재건하기 위해서 signal을 사용한다면, sigaction을 통해서 만들어졌던 핸들러는 적당하게 재건되어질 수 없을 것이다.
이러한 문제를 피하기 위해서, 핸들러가 전혀 sigaction을 사용하지 않은 프로그램이라고 할 지하도 핸들러는 저장하고 반환하기 위해서는 항상 sigaction을 사용하라. sigaction이 좀더 일반적이기 때문에, 원래 siganl 또는 sigaction을 가지고 만들었는지에 상관없이, 어느 동작을 저장하고 재건하기 위해서 sigaction을 사용하는 것이 더 적당할 수 있다.
만일 signal을 사용해서 어떤 동작을 만들었고 그 다음 sigaction을 사용해서 그것을 시험한다면, 핸들러 주소는 당신이 signal에서 지정했던 것과 같지 않은 것을 얻게 될 것이다. 그것은 심지어 signal에 action인수로써 사용하기에도 적당하지 않게 될 것이다. 그러나 당신은 sigaction의 인수로써 그것을 사용하고 적용할 수 있다. 그래서, 단일한 프로그램 안에서는 시종일관 한가지 또는 다른 한가지의 메커니즘을 고수하는 것이 한결 더 낫다.
이식성 노트 : 기본 signal 함수는 ANSI C에서 지원되고, sigaction함수는 POSIX. 1 표준에서 지원하고 있다. 만일 당신이 비-POSIX 계열의 시스템과의 호환성에 관심이 있다면, 당신은 대신에 signal을 사용하라.
21. 3. 1절 [Basic Signal Handling] 에서, signal을 사용해서 종료 시그널들을 위한 간단한 핸들러를 만드는 예제를 보여주었다. 다음은 sigaction을 사용하는 같은 예제이다.
이 프로그램은 요구된 파라미터로 단지 new_action을 로드하고 sigaction을 호출할 때 그것을 인수로써 사용한다. sigemptyset의 사용에 대한 것은 나중에 설명되었다. 21. 7절 [Blocking Signals] 참조. signal을 사용하는 예제에서, 미리 무시되도록 설정된 시그널들을 처리하는 것을 피했다. 여기서 새로운 동작을 지정하지 않고 현재의 동작을 시험하는 것을 허용하는 sigaction을 사용해서, 순간적으로 시그널핸들러가 변경되는 것을 피할 수 있다.
다음은 다른 예제이다. 이것은 동작을 변경하지 않고 SIGINT를 위해서 현재의 동작에 대한 정보를 구한다.
sigaction 구조체의 멤버 sa_flags는 특별한 기능을 위한 것이다. 대부분의 경우에, SA_RESTART가 이 필드에서 사용하기에 가장 좋은 값이다. sa_flags의 값은 비트마스크로써 해석되어진다. 그래서, 당신은 당신이 원하는 플래그를 설정하거나 선택할 수 있고, sigaction 구조체의 sa_flags 멤버 안에 그 결과를 저장한다. 각각의 시그널 번호는 자신만의 플래그 설정을 갖는다. sigaction을 호출하는 것은 특정한 시그널 번호의 영향을 받고, 당신이 지정한 플래그들은 오직 특정한 시그널에만 적용된다.
GNU C 라이브러리에서, signal로 핸들러는 만드는 것은 당신이 siginterrupt로 만들었던 설정에 의존한 값을 갖는 SA_RESTART를 제외하고는 모두 0으로 플래그를 설정한다. 21. 5절 [Interrupter Primitives] 를 참조하라. 이들 매크로들은 헤더파일 `signal. h'에 정의되어 있다.
매크로 : int SA__NOCLDSTOP
매크로 : int SA__ONSTACK
매크로 : int SA__RESTART
새로운 프로세스가 만들어질 때(23. 4절 [Creating a Process] 참조. ), 부모 프로세스로 부터 시그널들의 처리를 상속받는다. 그렇지만, 당신이 exec 함수(23. 5절 [Executing a File] 참조. )를 사용해서 새로운 프로세스 이미지를 로드할 때, 어느 시그널들은 그들의 원래의 동작으로 환원하도록 SIG_DFL을 사용해서 당신 자신의 핸들러를 정의해야만 한다. (만일 당신이 이것에 대해서 조금만 생각한다면, 이것을 이해할 수 있다; 원래의 프로그램으로부터 온 처리 함수들은 그 프로그램을 위한 정의이고, 새로운 프로그램 이미지의 주소 공간에는 심지어 존재하지도 않는다. ) 물론, 새로운 프로그램은 자신의 핸들러를 만들 수 있다.
어떤 프로그램이 쉘에 의해 실행될 때, 쉘은 보통 적당한 SIG_DFL 또는 SIG_IGN을 사용해서 자식 프로세스를 위한 초기동작을 설정한다. 그것은 당신이 새로운 시그널 핸들러를 만들기 전에 쉘이 SIG_IGN으로 초기동작을 준비하지 않은 것이 확실한지 체크하는 것은 좋은 생각이다. 다음은 현재 무시되지 않고 있는 SIGHUP 시그널을 위한 핸들러를 어떻게 만드는지에 대한 예제이다.
이 절은 signal 이나 sigaction 함수들을 사용해서 만들 수 있는 시그널 핸들러 함수를 어떻게 쓸것인지에 대해서 설명하고 있다. 시그널 핸들러는 프로그램의 다른 부분과 함께 컴파일하는 함수일 뿐이다. 직접적으로 그 함수를 불러내는 대신에, 시그널이 도착했을 때 그 핸들러를 호출하도록 운영체제에게 알리는 signal 이나 sigaction 함수를 사용한다. 이것이 핸들러를 만드는 것이다. 21. 3절 [Signal Actions] 참조. 다음은 당신이 시그널 핸들러 함수에서 사용할 수 있는 두 개의 기본 범주들이다.
어떤 전역 데이터 구조체에 의해서 도착된 시그널을 기록해두고 그 다음 정상적인 반환을 하는 핸들러 함수를 가질 수 있다.
프로그램을 종료시키거나 그 시그널의 원인이 된 상황으로부터 회복할 수 있는 지점으로 제어를 옮기는 핸들러 함수를 가질 수 있다.
당신이 핸들러 함수를 작성할 때 핸들러 함수는 비동기적으로 호출되어질 수 있기 때문에 각별한 주의가 필요하다. 즉, 핸들러함수는 예측할 수 없이, 프로그램의 어느 지점에서든지 호출되어질 것이다. 만일 매우 짧은 간격을 두고 두 개의 시그널이 도착한다면, 한 개의 핸들러는 다른 핸들러 안에서 실행할 수 있다. 이 절은 당신의 핸들러는 무엇을 하고, 무엇을 피해야 하는지를 설명한다.
정상적인 반환을 하는 핸들러는 SIGALRM 과 입/출력과 시그널을 사용한 프로세스간 통신과 같은 시그널에 보통 사용되어진다. 그러나 SIGINT를 위한 핸들러는 가능한 시간에 프로그램이 분기하도록 플래그를 설정한 후에 정상적으로 반환한다.
프로그램 에러 시그널을 위한 핸들러로부터 정상적인 반환을 하도록 하는 것은 안전하지 않다, 왜냐하면 핸들러 함수가 반환할 때 프로그램의 동작은 프로그램 에러 후에 무엇을 할 것인지 정의되지 않았기 때문이다. 21. 2. 1절 [Program Erroe Signals] 참조.
정상적으로 반환하는 핸들러들은 어떤 전역 변수를 갱신해야만 한다. 전형적으로, 그 변수는 정상적으로 작동하는 동안에 프로그램에 의해 주기적으로 시험되는 것이다. 데이터타입은 21. 4. 7절 [Atomic Data Access] 에서 설명된 sig_atomic_t가 된다.
다음은 한 개의 프로그램과 같은 간단한 예제이다. 이 프로그램은 SIGALRM 시그널이 도착한 것을 발견할 때까지 루프의 몸체를 실행한다. 이 기술은 시그널이 도착하면 그 루프를 분기하기 전에 어떤 동작을 완성하도록 진행과정에서 상호작용을 허용하기 때문에 유용하다.
프로세스를 종료시키는 핸들러 함수들은 전형적으로 소거 명령이나 프로그램 에러 시그널로부터의 복구와 인터럽트가 원인이 된 곳에 사용된다. 프로세스를 종료하도록 하는 핸들러를 만드는 가장 깨끗한 방법은 핸들러 실행의 첫 번째에서 같은 시그널이 발생되도록 하는 것이다. 다음과 같이.
당신은 setjmp 와 longjmp 기능을 사용해서 시그널 핸들러의 외부로 제어의 비지역 이동을 할 수 있다( 20장 [Non-Local Exits] 참조. ) 핸들러가 비지역 제어 이동을 할 때, 실행 중에 있던 프로그램의 그 부분은 계속되지 않을 것이다. 만일 프로그램의 그 부분이 중요한 데이터 구조체를 갱신 중에 있었다면, 그 데이터 구조체는 여전히 완벽하게 처리되지 못한 상태로 남게될 것이다. 프로그램이 종료되지 않는다면, 위와 같은 문제는 나중에 발견될지도 모른다.
이러한 문제를 피하기 위한 두 개의 방법이 존재한다. 한가지는 중요한 데이터를 갱신하는 프로그램 부분을 위해서는 시그널을 블록하는 것이다. 블록된 시그널은 그 블록이 해제된 후에 배달되어지고, 그때는 이미 중요한 데이터 갱신은 끝난 상태가 된다. 21. 7절 [Blocking Signals] 참조. 다른 방법은 시그널 핸들러 안에서 중요한 데이터의 구조체들을 재-초기화하거나, 그들의 값을 모순이 없도록 만드는 것이다. 다음은 한 개의 전역변수의 재초기화를 보여주는 개략적인 예제이다.
시그널 핸들러 함수가 실행되고 있을 때 도착한 다른 시그널이 있다면 무슨 일이 발생할까? 한 특정한 시그널을 위한 핸들러가 호출되었을 때, 핸들러가 반환할 때까지 그 시그널은 보통 블록된다. 만일 같은 종류의 두 개의 시그널이 서로 가까운 시간에 도착한다면, 두 번째 것은 첫 번째 것이 처리될 때까지 그냥 보유하고 있을 것이다. ( 만일 당신이 이러한 형태의 더 많은 시그널이 도착하도록 허용하기를 원한다면, 핸들러는 sigprocmask를 사용해서 시그널을 명백하게 블록을 해제할 수 있다; 21. 7. 3절 [Process Signal Mask] 참조. )
그렇지만, 당신의 핸들러는 다른 종류의 시그널의 배달에 의해서는 여전히 인터럽트 되어질 수 있다. 이것을 피하기 위해서, 당신은 sigaction에 인수로써 사용하는 action 구조체의 sa_mask 멤버를 사용해서 핸들러가 실행되는 동안 블록되어질 시그널을 명백하게 지정할 수 있다. 그들 시그널은 호출 된 핸들러를 위한 시그널에 더해져 있고, 다른 시그널들은 보통 프로세스에 의해서 블록되어진다. 21. 7. 5절 [Blocking for Handler] 참조.
이식성 노트 : 만일 당신의 프로그램이 완전히 System V Unix상에서 작업하기를 원할 때, 비동기적인 발생이 예상되는 시그널을 위한 핸들러를 만들려면 항상 sigaction을 사용하라. 다른 시스템에서는, 핸들러에서 하는 시그널의 처리는 SIG_DFL로 시그널이 가진 원래의 동작으로 되돌려지도록 만들어져있고, 핸들러는 실행될 때마다 그 자체를 다시 만들어야만 한다. 이 것은 실제로 시그널이 연속적으로 도착할 수 없을 때 작업하기는 불편하다. 하지만 다른 시그널이 즉시 도착할 수 있다면, 그것은 다시 핸들러를 다시 정하지 않아도 된다. 그러면 두 번째 시그널은 프로세스를 종료시키는, 디폴트 처리로 받게될 것이다.
만일 당신의 시그널 핸들러가 전혀 호출될만한 기회도 갖기 전에, 당신의 프로세스에 같은 종류의 시그널이 여러 개 배달되었다면, 그 핸들러는 오직 한 개의 시그널이 도착한 것처럼 호출되어질 것이다. 실제로, 그 시그널들은 한 개로 합병한다. 이 상황은 시그널이 블록되었을 때나, 또는 멀티프로세싱 환경에서 시그널이 도착했는데 시스템이 다른 프로세스의 실행 때문에 바쁠 때 발생할 수 있다. 이것이 의미하는 것은, 예를 들어, 당신은 발생한 시그널의 개수를 세는 시그널 핸들러의 사용을 신뢰할 수 없다. 오로지 당신이 구분할 수 있는 것은 과거의 주어진 시간동안에 적어도 한 개의 시그널이 도착했는지, 또는 도착하지 않았는지를 구분하는 것만을 신뢰할 수 있다. 다음은 자식 프로세스가 발생시킨 SIGCHLD의 개수와는 같지 않을지도 모르는 SIGCHLD 시그널의 개수를 실제처럼 대치하는 핸들러의 예제이다. 그것은 프로그램이 다음처럼 구조체를 연결하여 자식 프로세스의 모두를 추적하고 있다고 가정한다.
리스트를 시험하기 전에 플래그를 소거하는 것은 치명적이다; 그렇지 않고, 만일 플래그가 소거되기 전과, 프로세스 리스트의 적당한 요소가 체크된 후에 시그널이 배달된다면, 다음 도착한 시그널이 다시 그 플래그를 설정하기 전까지는 그 상황변화를 알아차릴 수 없다. 당신은 물론 그 리스트를 조사하는 동안 시그널을 블록함으로써 이러한 문제를 피할 수는 있지만, 올바른 순서로 일들을 처리하는 것이 정확함을 보증하기에는 좀더 좋은 방법이다.
프로세스 상황을 체크하는 루프가 그 상황이 유용하게 저장되어졌음이 확인될 때까지 p->status를 조사하는 것을 피한다. 이것은 status가 억세스 되고 있는 도중에 변화될 수 없음을 확실하게 한다. 일단 p->have_status가 설정되면, 그것은 자식 프로세스가 멈추거나 종료했음을 의미하고, 그 어느 경우에도, 프로그램이 주목하고 있는 동안에 다시 멈추거나 종료할 수 없다. 변수를 억세스하고 있는 동안에 인터럽션(interruptions)을 모방하기에 대한 상세한 정보는 21. 4. 7. 3절 [Atomic Usage] 를 참조하라.
다음은 당신이 체크했던 마지막 시간이후 핸들러가 실행되었는지를 시험 할 수 있는 다른 방법이다. 이 기술은 핸들러의 외부에서 결코 변화되지 않을 카운터로 사용한다. 빈도수(count)를 소거하는 대신에, 프로그램은 전의 값을 기억하고 있다가 그 이후에 그 값이 변화되었는지를 보여준다. 이 방법이 유리한 점은 프로그램의 다른 부분들을 독립적으로 체크할 수 있다는 것으로, 각각의 부분은 그 부분을 마지막으로 체크한 이후에 시그널이 있었는지를 체크한다.
핸들러 함수들은 보통 많은 일을 하지는 않는다. 핸들러 함수에게는 프로그램이 정기적으로 체크하는 외부변수를 설정하는 일 이외에는 아무 것도 하지 않게 하고, 다른 중요한 일들은 프로그램에게 맡기는 것이 좋다. 이것은 핸들러가 예측할 수 없는 시간에_시스템 호출도중, 또는 다중 명령을 요구하는 C연산자의 시작과 끝 사이에_비동기적으로 호출될 수 있기 때문이다.
데이터 구조체가 처리되고 있는 동안 핸들러 함수가 호출되면 데이터 구조체의 상황은 불일치하게 될 것이다. 심지어 한 개의 int 형 변수에서 다른 변수로 값을 복사하는 것조차도 대부분의 기계에서 두 개의 명령어를 취할 수도 있다. 이것은 당신이 시그널 핸들러에서 무언가를 할 때 많은 주의를 해야만 한다는 것을 의미한다.
만일 당신의 핸들러가 어느 전역변수를 억세스할 필요가 있다면, 그 변수들을 휘발성으로 선언하라. 이것은 변수들의 값이 비동기적으로 변화할 것이라고 컴파일러에게 알리고, 그와같은 갱신에 의해 무효로 만들게 될 어떤 최적화를 금한다.
만일 핸들러 안의 어떤 함수를 호출한다면, 그것이 시그널들에 대해서는 재진입성이 있음을 확실히 하거나, 시그널이 함수와 연관된 호출에는 인터럽트 할 수 없음을 확실히 하라.
스택 (stack)상에 존재하지않는 메모리를 사용하는 함수는 비-재진입성이 될 수 없다.
만일 어떤 함수가 정적 변수나 전역변수, 또는 동적으로 할당된 오브젝트를 사용한다면, 그것은 비-재진입성이고, 그 함수를 두 번 호출하면 서로 충돌하게 될 수 있다.
예를 들어, 시그널 핸들러가 gethostbyname을 사용한다고 가정하자. 이 함수는 정적 오브젝트에 그 값을 반환하고, 매번 같은 오브젝트를 다시 사용한다. 만일 gethostbyname이 호출된 동안, 또는 심지어 그 후(하지만 여전히 그 값은 프로그램에서 사용하고 있는 중이다. )라도 그 시그널이 도착하는 일이 발생한다면, 그것은 프로그램이 요청한 그 값을 지워버릴 것이다. 그렇지만, 만일 그 프로그램이 gethostbyname이나 같은 오브젝트에 정보를 반환하는 함수를 사용하지 않거나, 또는 만일 그와같은 것을 사용한다고 해도 그것을 사용할 때 시그널들을 블록한다면, 당신은 안전하다. 라이브러리 함수들의 대부분은 한 고정 오브젝트에 값들을 반환하고 항상 같은 오브젝트를 재 사용하기 때문에 그들은 같은 문제를 발생시킬 가능성이 있다. 이 매뉴얼 안에 있는 함수들에 대한 설명에는 이러한 것들을 항상 언급할 것이다.
만일 어떤 함수가 당신이 공급한 오브젝트를 사용하고 갱신한다면, 그것은 잠재적으로 비-진입성이다. 같은 오브젝트를 사용하고 있는 두 개의 호출은 충돌할 수 있다.
이와 같은 경우는 당신이 스트림을 사용해서 입/출력을 할 때 발생한다. 시그널 핸들러가 fprintf를 사용해서 메시지를 출력한다고 가정하자. 그리고 그 프로그램이 fpintf를 처리하고 있는 도중에 같은 오브젝트를 사용하는 시그널이 배달되었다고 가정하자. 이때 두 개의 호출은 같은 데이터 구조체_스트림 자체_에서 동작하기 때문에 핸들러의 메시지와 프로그램의 데이터는 모두 변조될 것이다. 그렇지만, 만일 핸들러에서 사용하는 스트림이 시그널이 도착하여 동시에 그 스트림이 프로그램에 의해 사용되어질 가능성이 없다는 것을 당신이 알고 있다면, 아무런 문제가 없다. 그리고 만일 프로그램이 다른 스트림을 사용한다면 아무런 문제가 없다.
대부분의 시스템에서, malloc 과 free는 무슨 메모리 블록들이 해제상태에 있는지를 기록하고 있는 정적 데이터 구조체를 사용하기 때문에, 재진입성이 없다. 그렇기 때문에 메모리를 할당하고 해제하는 라이브러리 함수중에 재진입성이 있는 것은 아무 것도 없다. 핸들러에서 메모리를 할당할 필요를 피하기 위한 가장 좋은 방법은 시그널 핸들러에서 사용할 공간을 미리 할당받는 것이다.
핸들러에서 메모리를 해제하는 것을 피하는 가장 좋은 방법은 해제할 오브젝트를 플래그로 표시하거나 기록해두고, 어느 것이 해제되기를 기다리고 있는지를 나중에 프로그램에서 체크하는 것이다. 그러나 이것은 오브젝트들이 개별적으로 존재하는 것이 아니라 서로 연결되어 있고, 같은 일을 하는 다른 시그널 핸들러에 의해서 그것이 인터럽트 되어졌다면, 당신은 오브젝트들 중 하나를 "잃어"버릴 수 있기 때문에 주의를 해야만 한다. GNU 시스템에서, malloc 과 free는 시그널들을 블록하기 때문에 시그널 핸들러에서 사용하는 것은 안전하다. 그렇기 때문에, 시그널 핸들러에서 결과를 위해서 공간을 할당하는 것은 또한 안전하다. obstack 할당 함수들도 당신이 시그널 핸들러의 외부와 내부양쪽에서 같은 obstack을 사용하지 않는다면 안전하다. 재배치(relocating) 할당 함수들( 3. 6절 [Relocating Allocator] 참조. )을 시그널 핸들러 안에서 사용하는 것은 안전하지 않음이 확실하다.
errno를 갱신하는 어떤 함수들은 비-진입성이지만, 당신은 이것을 진입성으로 만들 수 있다: 핸들러에서, errno의 원래 값을 저장하고 정상적으로 반환하기 전에 그것을 반환한다. 이것은 시그널 핸들러 내부에서 발생된 에러들이, 핸들러가 실행되도록 프로그램이 인터럽트 된 순간에 시스템 호출로부터 발생한 에러와 혼동되는 것을 막는다.
이 기술은 일반적으로 응용 가능하다; 만일 당신이 핸들러의 내부에서 메모리의 특정한 오브젝트를 갱신하는 함수를 호출하기 원한다면, 당신은 그 오브젝트를 저장하고 다시 반환함을 통해서 안전하게 구현할 수 있다.
메모리 오브젝트로부터 읽기는 시그널이 배달되어질 때라도 오브젝트에 나타날 수 있는 어떤 값들을 취급할 수 있도록 안전하게 제공되었다. 어떤 데이터 타입에 배정(assignment)할 때, 그 데이터 타입이 원자단위가 아닌 변수에 배정(assignment)하는 "도중에" 핸들러가 실행될 수 있다면 그 배정에는 많은 명령(instruction)이 요구됨을 명심하라.
메모리 오브젝트에 기록하기는 핸들러가 실행되고 있는 순간일지라도 안전하고, 어느 것도 방해되지 않을 것이다.
당신의 어플리케이션에서 데이터가 원자와 관계가 있던지, 또는 단순한 텍스트이던지, 당신은 원자화가 필요 없는 단일한 데이터를 억세스 하는 요소에 대해서 주의를 해야만 한다. 이것은 단일한 오브젝트를 읽거나 쓰기 위해서는 여러 개의 명령이 필요할 수 있다는 것을 의미한다. 그와같은 경우에, 시그널 핸들러는 오브젝트의 읽기나 쓰기 중간에 실행될 수 있다.
이러한 문제를 커버할 수 있는 세 가지 방법이 있다. 당신은 항상 원자 단위로 억세스되는 데이터 타입을 사용할 수 있다; 억세스를 인터럽트 하여 아무런 부적당한 일이 일어나지 않게 하거나, 또는 인터럽트보다는 좋지는 않지만 억세스동안에 모든 시그널들을 블록하는 등 당신은 주의 깊은 조정을 할 수가 있다.
다음은 변수를 갱신하는 도중에 시그널 핸들러를 실행하면 무슨 일이 발생하는지를 보여주는 예제이다. (변수 읽기를 인터럽트 하는 것도 역설적인 결과에 이르게 할 수 있지만, 여기서 우리는 쓰기를 보여준다. )
이 프로그램은 계속 번갈아 가면서 0, 1, 0, 1 로 메모리를 채운다; 그 동안, 일초마다, 알람 시그널 핸들러는 현재의 내용을 프린트한다. ( 핸들러 안에서 printf의 호출은 시그널이 발생했을 때 핸들러외부에서 printf가 확실히 호출되어지지 않을 것이므로 이 프로그램은 안전하다. ) 분명히, 이 프로그램은 0 한 쌍과 1 한 쌍을 프린트 할 수 있다. 하지만 그것이 그 프로그램이 할 수 있는 전부가 아니다! 대부분의 기계에서, 메모리에 새로운 값을 저장하기 위해서는 여러 개의 명령을 취하고, 그 값은 동시에 한 워드(word)에 저장된다. 만일 시그널이 그 명령들 사이에 배달된다면, 핸들러는 memory. a는 0이고 memory. b는 1인걸 발견할지 모른다(또는 그의 반대).
한 개의 명령으로 메모리 안에 한 개의 새로운 값을 저장할 수 있는 어떤 기계에서는 인터럽트 될 수 없다. 그 기계들에서, 핸들러는 항상 두 개의 0과 두 개의 1을 프린트 할 것이다.
변수를 억세스할 때 인터럽트 하는 것에 대한 불확실성을 피하기 위해서, 당신은 항상 원자단위로 억세스를 하는 특별한 데이터 타입을 사용할 수 있다: sig_atomic_t. 이 데이터타입을 읽기와 쓰기는 단일한 명령으로 발생한다는 것이 보증되므로 핸들러가 억세스의 "중간에" 실행될 방법이 없는 것이다.
sig_atomic_t 타입은 항상 정수 데이터 타입이지만, 그 데이터 타입이 몇 개의 비트로 구성되어있는지는 한가지로 정해진 것이 아니라 각각의 기계마다 다양하다.
데이터 타입 : sig__atomic__t
억세스의 어떤 형태는 억세스가 인터럽트 되는 것과 같은 문제들을 피한다. 예를 들어, 핸들러에 의해 설정되고, 때때로 메인 프로그램에 의해서 소거되고 테스트되는 어떤 플래그를 억세스 하는데 실제로 두 개의 명령(instructions)이 필요하다고 할지라도 항상 안전하다. 이것이 그렇게 보이도록 하기 위해서, 우리는 인터럽트 되어질 수 있는 모든 억세스를 고려해야만 하고, 인터럽트 되면 아무런 문제가 없음을 보여야 한다. 플래그를 테스트하는 도중에 발생한 인터럽트는 아무런 문제가 없는 정확한 값인 경우에, 0이 아닌 값으로 인식이 되거나 또는 테스트된 다음에 0이 아닌 값으로 되어질 것이기 때문에 아무런 문제가 없다.
플래그를 소거하는 도중에 인터럽트도 아무런 문제가 없는데, 플래그가 소거되기 전에 시그널이 발생한 것은, 그 값이 0으로 끝나거나, 아니면 0이 아닌 값으로 끝나고, 플래그가 소거된 후에 시그널이 발생한 것처럼 연속적인 사건들이 발생한다. 그 두 개의 경우 모두 코드가 처리되기만 하면, 플래그를 소거하는 도중에 발생한 시그널 또한 처리 할 수 있다. ( 이것은 비-원소단위의 사용이 언제 안전할 수 있는지를 당신에게 설명하기 위한 예제이다. )
때때로 당신은 다른 오브젝트를 사용해서 어떤 오브젝트의 사용을 막음으로써 그 오브젝트에 인터럽트 되지 않는 억세스를 보증할 수 있다, 그것의 형태는 원소단위가 확실할 것이다. 21. 4. 5절 [Merged Signals] 에서 예제참조.
open 이나 read가 입/출력 디바이스에서 기다리는 것과 같은 입/출력 기본동작 동안에 시그널이 발생할 수도 있고 처리될 수도 있다. 만일 시그널 핸들러가 반환하면, 그 시스템은 의문을 갖는다: 다음에 무슨 일이 발생하지?
POSIX는 한가지 접근법을 정한다: 즉시 그 기본동작을 실패로 만든다. 이러한 종류의 실패를 위한 에러코드는 EINTR이다. 이것은 유연하지만, 보통은 불편하다. 전형적으로, POSIX 어플리케이션은 그 호출을 다시 할 목적으로 라이브러리 함수가 반환했을 때 EINTR인지 체크하는 시그널 핸들러를 사용한다. 종종 프로그래머들은 체크하는 것을 잊는다.
GNU 라이브러리는 매크로 TEMP_FAILURE_RETRY를 사용해서, 임시적인 실패 후에 다시 호출을 시도하도록 하는 편리한 방법을 제공한다.
매크로 : TEMP__FAILURE__RETRY (expression)
BSD는 완전히 EINTR을 피하고 좀더 편리한 접근법을 제공한다: 그것을 실패로 만드는 대신에 인터럽트된 기본동작을 다시 시작한다. 만일 당신이 이 접근법을 선택한다면, 당신은 EINTR에 관심을 가질 필요가 없다.
GNU 라이브러리에서는 접근법을 선택할 수 있다. 만일 당신이 시그널 핸들러를 만드는 sigaction을 사용한다면, 당신은 핸들러가 어떻게 동작할지를 정할 수 있다. 만일 당신이 SA_RESTART 플래그를 지정하면, 핸들러부터의 반환은 어떤 기본동작을 다시 시작할 것이다; 그렇지 않으면, 핸들러로부터의 반환은 EINTR을 발생할 것이다. 21. 3. 5절 [Flags for Sigaction] 참조. 다른 방법은 siginterrupt 함수를 사용하는 것이다. 21. 9. 1절 [POSIX vs BSD] 참조.
당신이 한 특정한 핸들러에서 sigaction 이나 siginterrupt로 할 일을 정하지 않을 때, 그것은 디폴트 선택을 사용한다. GNU 라이브러리에서 디폴트 선택은 당신이 정의한 테스트 매크로에 의존한다. 만일 시그널이 발생되기 전에 _BSD_SOURCE 또는 _GNU_SOURCE로 정의하면, 디폴트는 기본동작을 다시 시작하는 것이다; 그렇지 않다면, 디폴트는 EINTR로 그들을 실패하게 만드는 것이다. ( 라이브러리는 signal 함수의 다양한 변형을 포함하고 있고, 당신이 사용한 테스트 매크로에 따라서 실제로 호출될 signal 함수가 결정된다. ) 1. 3. 4절 [Feature Test Macros] 참조. 위와 같은 문제에 영향을 받는 기본동작들은 close, fcntl(operation F_SETLK), open, read, recv, recvfrom, select, send, sendto, tcdrain, waitpid, wait, 그리고 write가 있다.
결코 재개(resumption)가 발생되지 않는 한가지 상황이 있다: read 와 write 와 같은 데이터-참조 함수가 데이터의 일부분만을 참조한 후에 시그널에 의해서 인터럽트 되었을 때. 이 경우, 그 함수는 부분적인 성공을 지적하기 위해서, 이미 참조된 바이트의 개수를 반환한다.
레코드-지향 디바이스 상에서는 두 개의 레코드를 read 하거나 write하려는 것에서 한 개로 read, write를 분리해버리는 것 과 같은 이상한 동작의 원인이 될 수도 있다. (데이터그램 소켓을 포함; 11. 9절 [Datagrams] 참조. ). 실제로는, 그와같은 디바이스 상에서는 데이터를 참조 중에 인터럽션이 발생할 수 없기 때문에 아무런 문제가 없다; 그와같은 디바이스들은 일단 데이터 참조가 시작되면 아무런 기다림이 없이, 한 버스트(burst)에 전체 레코드를 항상 참조한다.
**역자주 : 버스트(burst) : 중간에 어떤 이유들로 인해서 중단이 발생하지 않고, 한 묶음의 데이터를 한꺼번에 전달하는 방법을 의미함.하드웨어 트랩이나 인터럽트의 결과로서 발생되는 시그널을 제외하고, 당신의 프로그램에서 프로세스, 또는 다른 프로세스에게 명시적으로 시그널을 보낼 수 있다.
프로세스는 raise 함수를 통해서 시그널을 스스로에게 보낼 수 있다. 이 함수는 `signal. h'에 선언되어 있다.
함수 : int raise (int signum)
함수 : int gsignal (int signum)
raise 사용으로 한가지 편리한 점은 당신이 트랩 했던 시그널의 디폴트 동작을 재생할 수 있다는 것이다. 이를테면, 당신의 프로그램을 사용하는 사용자가 stop 시그널(SIGTSTP)을 보내기 위해서 SUSP 문자를 타이핑할 때, 당신은 멈추기 전에 어떤 내부적 데이터 버퍼들을 소거하기를 원한다고 가정하자. 당신은 다음처럼 이것을 설정할 수 있을 것이다.
kill 함수는 다른 프로세스에게 시그널을 보내기 위해서 사용될 수 있다. 함수의 명칭이 악의적이지만, 그것은 다른 프로세스를 종료시키는데 사용하기보다는 더 많은 것들을 위해서 사용될 수 있다. 다음의 경우, 당신이 프로세스들 사이에 시그널들을 보내기 원할 때 사용할 수 있다.
부모 프로세스가 작업을 수행하기 위해서 자식 프로세스를 시작한다. 아마도 자식 프로세스는 한정된 루프를 돌 것이고_자식 프로세스가 작업에서 더 이상 필요치 않을 때 종료한다.
한 프로세스가 그룹(group)의 일부로써 실행될 때, 에러나 다른 사건이 발생하면 그룹에 있는 다른 프로세스에게 신고하거나 종료할 필요가 있다.
두 개의 프로세스가 서로 작업하는 동안 동기(synchronize)할 필요가 있다.
이 절은 당신이 프로세스가 어떻게 작업하는지에 대해서 조금이나마 알 것이라고 가정한다. 이 주제에 대한 자세한 정보는 23장 [Child Process] 에 나와있다. kill 함수는 `signal. h'에 선언되어 있다.
함수 : int kill (pid_t pid, int signum)
pid > 0
pid == 0
pid < -1
pid == -1
다음의 errno는 이 함수를 위해 정의된 에러상황이다.
EINVAL
EPERM
ESCRH pid
함수 : int killpg (int pgid, int signum)
어느 임의의 프로세스에게 시그널을 보내기 위해서 kill을 사용하는 것을 방지하기 위한 제한이 있다. 그것은 다른 사용자에게 소속되어 있는 프로세스를 제멋대로 죽이는 것과 같은 반사회적인 행동을 방지하기 위한 의도가 있다. kill을 자식과 부모 프로세스사이에 시그널을 주고 받기 위해 사용하는것과 같은 상황에서는, 보통 당신은 시그널을 보내기 위한 허가권을 갖고 있다. 그러나 자식 프로세스에서 setuid 프로그램이 실행될 때는 유일하게 제외된다; 만일 프로그램이 실제 UID를 유효 UID로 변경한다면, 당신은 시그널을 보내기 위한 허가권을 가지지 않을 수도 있다. su 프로그램을 이런 일을 한다.
프로세스가 다른 프로세스에게 시그널을 보내기 위한 허가권을 가지고 있는지 없는지의 여부는 두 개의 프로세스의 사용자 ID들에 의해 결정된다. 이 원칙은 25. 2절 [Process Personal] 에 자세하게 논의되고 있다.
일반적으로, 어떤 프로세스가 다른 프로세스에게 시그널을 보낼 수 있기 위해서는, 시그널을 보내는 프로세스가 특권이 부여된 사용자(`root'처럼)이거나 시그널을 보내는 프로세스의 실제 또는 유효 사용자 ID가 시그널을 받는 프로세스의 실제 또는 유효 사용자 ID와 매치되어야만 한다. 만일 시그널을 받는 프로세스가 프로세스 이미지 파일에서 set-user-ID 모드를 통해 유효 사용자 ID를 변경했다면, 프로세스 이미지 파일의 소유자가 현재 유효 사용자 ID 대신에 사용된다. 어떤 경우에, 만일 사용자 ID들이 매치되지 않는다 할지라도 부모 프로세스가 자식 프로세스에게 시그널을 보내는 것이 가능하고, 다른 경우에는 다른 제한들이 강요 될 것이다. SIGCONT 시그널은 특별한 경우이다. 그것은 만일 시그널을 보내는 쪽이 시그널을 받는 쪽과 같은 세션에 있다면, 사용자 ID들에 상관없이 시그널을 보낼 수 있다.
다음은 프로세스간 통신을 위해서 어떻게 시그널들을 사용할 수 있는지 보여주는 조금 긴 예제 프로그램이다. SIGUSR1 과 SIGUSR2가 프로세스 간 통신을 지원하지 위하여 제공된 것이다. 그 시그널들은 기본적으로 치명적이기 때문에, 그 시그널들을 받을 것으로 가정되는 프로세스는 signal 이나 sigaction을 통해서 그들을 트랩 해야만 한다.
다음의 예제는, 부모 프로세스가 fork로 자식 프로세스를 생성한 다음 자식 프로세스가 초기화를 수행할 때까지 기다린다. 자식 프로세스는 준비가 되었음을 알리기위해서, kill 함수를 사용해서 SIGUSR1 시그널을 보낸다.
위의 예제는 busy wait(적당한 말이 없어서. . )을 사용하는데, 그것은 다른 프로그램에서 사용할 수 있도록 CPU 사이클을 기다려야하기 때문에 좋지 않다. 시그널이 도착할 때까지 기다리도록 시스템에게 부탁하는 것이 더 좋다. 21. 8절 [Waiting for a signal] 에 있는 예제를 참조하라.
시그널 블록하기는 운영체제에게 그 시그널을 붙잡아서 나중에 배달하도록 알리는 것을 의미한다. 일반적으로, 프로그램에서는 SIG_IGN을 사용해서, 시그널의 동작을 무시하는 것으로 설정할 망정, 불명확하게 시그널들을 블록하지 않는다. 하지만 시그널 블록킹(blocking)은 민감한 오퍼레이션들이 인터럽트 되는 것을 막기 위해서 시그널들을 블록하는데 사용된다.
시그널들 때문에 핸들러에 의해 수정되었던 전역 변수들을 갱신하는 동안 시그널들을 블록하기 위해서 sigprocmask 함수를 사용 할 수 있다.
특정한 핸들러가 실행되는 동안 어떤 시그널들을 블록하도록 sigaction 함수호출에서 sa_mask를 설정할 수 있다. 이 방법으로, 시그널 핸들러는 시그널들에 의해서 그 자체가 인터럽트 됨이 없이 실행될 수 있다.
sigprocmask을 사용해서 임시적으로 시그널 블록하기는 당신의 프로그램에서 임계부분(critical parts)이 실행되는 동안에 발생할지도 모를 인터럽트를 막기 위한 방법으로 제공된다. 만일 시그널들이 프로그램의 그 부분(critical parts)에 도달한다면, 당신이 그들의 블록을 해제한 후에, 나중에 배달 되어진다. 이것의 유용한 사용예는 프로그램의 나머지와 시그널 핸들러 사이에 데이터를 분배하는데 사용하는 것이다. 만일 데이터의 타입이 sig_atomic_t( 21. 4. 7절 [Atomic Data Access] 참조. )가 아니라면 시그널 핸들러는 프로그램의 나머지가 데이터의 읽기와 쓰기를 완전히 끝냈을 때 실행될 수 있다. 이것은 혼란된 결과를 초래할 것이다.
신뢰 가능한 프로그램을 만들기 위해서, 프로그램의 나머지가 데이터를 시험하거나 갱신하는 동안에 시그널 핸들러가 실행되는 것을 막을 수 있다_프로그램의 나머지가 실행되는 동안 발생할 여지가 있으며, 그 데이터를 건드릴 위험이 있는 적당한 시그널들을 블록함으로 해서. 만일 어떤 시그널이 도착하지 않았을 때, 당신이 어떤 동작을 수행하기를 바란다면 시그널 블록킹은 필요하다. 그 시그널을 위한 핸들러가 sig_atomic_t 타입의 플래그를 설정한다고 가정하다; 당신은 그 플래그를 시험하고 만일 그 플래그가 설정되지 않았다면 어떤 동작을 수행하도록 하고 싶어한다. 하지만 이것은 신뢰할 수 없다. 만일 그 시그널이 아직 중요한 동작은 수행하기 전이고, 플래그는 테스트 한 직후에 시그널이 배달된다고 가정하면, 그 프로그램은 시그널이 도착할지라도 그 동작을 수행 할 것이다.
어떤 시그널이 도착했는지의 여부를 확인하는 유일한 신뢰 가능한 방법은 시그널이 블록되어 있을 동안 테스트하는 것이다.
시그널을 블록 킹하는 함수들 모두는 무슨 시그널들이 영향을 받게되는지를 정하는 데이터 구조체를 사용한다. 그리고, 두 개의 단계, 즉, 시그널을 만들기와 시그널을 라이브러리 함수에 인수로써 사용하기를 포함한다.
그들은 헤더파일 `signal. h'에 선언되어 있다.
데이터 타입 : sigset__t
시그널 셋(set)을 초기화하기 위한 두 가지 방법이 있다. 하나는 처음에 sigemptyset을 사용하여 비어있게 해놓은 다음, 개별적으로 시그널을 하나씩 더한다. 아니면, sigfillset을 사용하여 완전히 채운다음, 개별적으로 정해진 시그널들을 하나씩 지운다.
당신이 어떤 식으로든 그것을 사용하기 전에 그 두 개의 함수중 하나로써 시그널 셋(set)을 초기화해야만 한다. 모든 시그널들을 명시적으로 설정하려 시도하지 말아라, 왜냐하면, sigset_t 오브젝트는 초기화될 필요가 있는 어떤 다른 정보(버전 필드와 같은)를 포함하고 있을 것이기 때문이다. (더하자면, 당신이 알고 있는 것 외의 시그널은, 시스템이 발생시키지 않을 것이라는 가정을 당신의 프로그램 안에서 하는 것은 현명하지 못하다. )
함수 : int sigemptyset (sigset_t *set)
함수 : int sigfillset (sigset_t *set)
함수 : int sigaddset (sigset_t *set, int signum)
EINVAL : signum 인수로 무효한 시그널을 지정하였다.
함수 : int sigdelset (sigset_t *set, int signum)
마지막으로, 시그널 셋(set)안에 어떤 시그널들이 있는지를 테스트하기 위한 함수의 설명이다.
함수 : int sigismember (const sigset_t *set, int signum)
현재 블록되어 있는 시그널들의 모음(collection)을 시그널 마스크라고 부른다. 각 프로세스는 자신 소유의 시그널 마스크를 갖고 있다. 당신이 새로운 프로세스를 만들 때(23. 4절 [Creating a Process] 참조) 그것은 부모의 마스크를 상속받는다. 당신은 시그널 마스크를 갱신하여 유연성 있게 시그널들을 블록하거나 해제할 수 있다.
sigprocmask 함수의 프로토타입은 `signal. h'에 있다.
함수 : int sigprocmask (int how, const sigset_t *set, sigset_t *oldset)
당신은 SIGKILL 과 SIGSTOP 시그널들을 블록할 수 없지만, 만일 시그널 셋이 그들을 포함한다면, sigprocmask는 에러 상황을 보고하는 대신에 그들을 단지 무시한다. 기억하라, SIGFPE와 같은 프로그램 에러 시그널들을 블록하는 것은 실제 프로그램 에러에 의해 발생된 시그널로 인해 바람직하지 못한 결과를 초래한다. (raise 나 kill에 의해 만들어진 시그널들은 제외하고) 이것은 시그널이 다시 블록이 해제되었을 때, 그 지점에서 실행을 계속하지 못할 정도로 프로그램이 파괴되었기 때문이다. 21. 2. 1절 [Program Erroe Signals] 참조.
다음은 간단한 예제이다. SIGALRM 시그널이 도착할 때마다 플래그를 설정하는 핸들러를 만들고, 메인 프로그램에서는 시간마다 이 플래그를 체크하고 그것을 재설정한다고 가정하자. 당신은 sigprocmask를 호출해서 코드의 임계부분을 보호함으로써 그 동안 도착한 부가적인 SIGALRM 시그널을 막을 수 있다.
/* 이 변수는 SIGALRM 시그널 핸들러에 의해 설정된다. */
시그널 핸들러가 호출되었을 때, 당신은 보통 그 시그널 핸들러가 다른 시그널에 의해 블록됨이 없이 끝나기를 원한다. 그 핸들러가 시작된 순간부터 끝나는 순간까지, 당신은 핸들러의 데이터를 오염시키거나 혼란시킬지도 모르는 시그널을 블록해야만 한다.
한 시그널에 의해 핸들러 함수가 호출되었을 때, 핸들러가 실행되는 동안 그 시그널은 자동적으로 블록된다 ( 다른 시그널과 함께 그 시그널은 이미 프로세스의 시그널 마스크에 존재하게된다. ) 만일 예를 들어 당신이 SIGTSTP를 위한 핸들러를 준비했을 때, 그 시그널이 도착하면 핸들러는 핸들러가 실행되는 동안 기다리도록 하여 나중에 SIGTSTP 시그널을 다시 발생시킨다.
그렇지만, 디폴트로, 다른 종류의 시그널들은 블록되지 않았다; 그들은 핸들러가 실행되는 동안 발생할 수도 있다. 핸들러가 실행되는 동안 다른 종류의 시그널을 블록하기 위한 좋은 방법은 sigaction 구조체의 sa_mask 멤버를 사용하는 것이다. 다음은 그에 대한 예제이다.
핸들러 코드 안에서 명시적으로 다른 시그널들을 블록하는 것보다는 더 신뢰 가능하다. 만일 당신이 핸들러 안에서 명시적으로 시그널들을 블록한다면, 아직 당신이 그들을 블록하지 않았을, 핸들러 시작 초기의 짧은 간격동안에 발생된 시그널로 인한 문제는 피할 수가 없다.
이 메커니즘을 사용하여 프로세스의 현재 마스크로부터 시그널들을 제거 할 수 없다. 그렇지만, 핸들러 함수에서 sigprocmask를 호출하여, 당신이 원하는 시그널을 블록하거나 해제하도록 만들 수 있다. 어쨌든, 핸들러 함수가 반환할 때, 시스템은 핸들러 함수가 진입하기 전으로 마스크를 반환한다.
당신은 sigpending 을 호출하여 어느 시점에서 미해결 상태인 시그널들을 발견해낼 수 있다. 이 함수는 `signal. h'에 선언되어 있다.
함수 : int sigpending (sigset_t *set)
시그널이 미해결 상태인지를 테스트하는 것은 자주 유용하지는 않다. 시그널이 블록되지 않았을 때 테스트하는 것은 좋지 않다. 다음의 예제를 살펴보자.
당신의 프로세스를 위하여 미해결상태인 특정한 시그널이 있다면 그 동안에 도착한 같은 종류의 부가적인 시그널들은 버려질 것임을 기억하라. 예를 들어, 만일 SIGINT 시그널이 미해결 상태일 때 다른 SIGINT 시그널이 도착하면, 당신의 프로그램은 이 시그널의 블록을 해제할 때 오직 한 개의 SIGINT 시그널로 처리할 것이다.
이식성 노트 : sigpending 함수는 POSIX. 1에 새로이 추가되었다. 오래된 시스템들은 이와 동등한 함수가 없다.
라이브러리 함수를 사용해서 시그널을 블록하는 대신에, 당신은 당신이 블록을 "해제"할 때, 나중에 테스트 되도록 플래그를 설정하는 핸들러를 만들어서 거의 같은 결과를 얻을 수 있다.
다음의 예제를 살펴보라.
도착한 특정한 시그널이_미해결인_어떻게 signal에 저장되었는지에 주목하라. 그와같은 방법으로, 우리는 아직 해결할 형편이 되지 않은 시그널의 다양한 종류를 처리할 수 있다.
우리는 defer_signal을 증가시키고 감소시켜서 중첩된 임계 구역(critical sections)을 적당히 작업하게 한다; 그래서, 만일 signal_pending 과 함께 호출되었던 update_mumble 의 값이 이미 0이 아니라면, 시그널들은 update_mumble안에서는 연기되지 않고, 오직 caller 내부에서만 연기된다. 이것은 defer_signal 이 여전히 0이 아닐 때, 왜 signal_pending을 체크하지 않는지에 대한 이유가 된다.
defer_signal 의 증가와 감소는 한 개의 명령보다는 많은 명령이 요구된다; 그러므로 중간에 시그널이 발생하는 것이 가능하다. 그러나 이것은 아무런 문제도 야기하지 않는다. 만일 증가나 감소를 시작하기 전에 발생했던 시그널과 동등한 그 시그널이 증가나 감소 전에 그 값을 보기 위해서 충분히 많이 발생된 것이라면, 이 경우 아무런 문제없이 작업한다.
signal_pending 을 테스트하기 전에 defer_signal을 증가시키는 것은 굉장히 중요하다, 왜냐하면 이것은 민감한 버그를 피하게 하기 때문이다. 만일 우리가 그와같은 일들을 다른 순서로 한다면 이것은 다음과 같다.
위의 경우 if 구문과 감소사이에 도착된 시그널은 불명확한 시간동안은 잃어버리게 된다. 핸들러는 완전하게 defer_signal을 설정하였지만, 프로그램은 이미 이 변수를 테스트해버렸고, 다시는 변수를 테스트하지 않을 것이다.
그와같은 버그들을 타이밍 에러라고 부른다. 그들은 희귀하게 발생하고 재생시키는데는 굉장히 중요하기 때문에 아주 나쁜 버그이다. 당신은 재생 가능한 버그를 발견하는 것처럼 디버거로 그들을 발견할거라고 예상하지 마라. 그렇기 때문에 그러한 버그를 피하기 위해서는 특별히 주의할 가치가 있다.
( 당신은 이러한 순서로 코드를 기록하고 싶은 유혹을 받지 말아라, defer_signal 이 카운터(counter)로써 사용된다면 signal_pending 과 함께 테스트되어야만 한다. 후에, 0에 대한 테스트는 1에 대한 테스트보다는 깨끗하다. 그러나 만일 당신이 defer_signal을 카운터로써 사용하지 않고, 0과 1의 값만 그것에 주어진다면, 순서는 간단하게 보여질 것이다. 이것은 defer_signal을 카운터로써 사용하는 것보다 더한 이득을 갖는다: 그것은 당신이 잘못된 순서로 코드를 기록하고 민감한 버그를 만들어낼 가능성을 감소시킬 것이다. )
당신의 프로그램이 외부 사건에 의해서 조종되거나, 동기화를 위해서 시그널을 사용한다면, 그때 그 프로그램은 시그널이 도착할 때까지 기다릴 수밖에 없다.
시그널이 도착할 때까지 기다리기 위한 간단한 방법은 pause 를 호출하는 것이다. 당신이 그것을 사용하기 전에 다음절에 있는, 그것을 사용함으로 써 얻게되는 불리한 점을 보아라.
함수 : int pause ()
pause의 간단함은 프로그램을 이상하게 중단(hang) 시킬 수도 있는 심각한 타이밍 에러들을 숨길수도 있다. 만일 당신의 프로그램에서, 실제 작업이 시그널 핸들러에 의해서 수행되고, "메일 프로그램"은 pause는 호출하지만 아무런 일을 하지 않을 때는 안전하다. 시그널이 배달될 때마다, 핸들러는 해야할 작업을 하고, 다음에 반환한다, 그래서 프로그램의 메인 루프는 다시 pause를 호출 할 수 있다.
한 개 이상의 시그널이 도착하기를 기다렸다가 실제작업을 재개하기 위해서 pause를 사용하는 것은 안전할 수 없다. 당신이 플래그를 설정하는 것으로 시그널 핸들러를 조정한다고 할지라도, 당신은 여전히 pause함수를 믿을 수 없다. 다음에 이 문제에 대한 예제가 있다.
이것은 버그를 갖고 있다: 변수 usr_interrupt가 체크된 후, 하지만 pause가 호출되기 전에 시그널이 도착할 수 있다. 만일 앞으로 아무런 시그널이 도착하지 않으면, 프로세스는 결코 다시는 재개될 수 없다. puase를 사용하는 대신에 루프 안에서 sleep를 사용해서 오랜 기다림에 상위(upper) 제한을 가할 수 있다. (sleep에 대한 상세한 정보는 17. 4절 [Sleeping] 를 참조하라. ) 다음의 예제를 보자.
어떤 목적으로도, 이것은 사용하기에 충분하다. 조금 더 복잡하기는 하지만, sigsuspend를 사용해서도 특정한 핸들러가 실행되는 동안 확실하게 기다릴 수 있다.
시그널이 도착하기를 기다리는 깨끗하고 신뢰 가능하다하다 방법은 그것을 블록하고 sigsuspend를 사용하는 것이다. 루프 안에서 사용된 sigsuspend는, 다른 종류의 시그널들이 그들의 핸들러에 의해 처리되는 동안, 어떤 종류의 시그널을 위해서 기다릴 수 있다.
함수 : int sigsuspend (const sigset_t *set)
코드의 마지막 부분은 약간 교묘하다. 이것의 핵심은 sigsuspend가 반환 할 때, 프로세스가 원래 가졌던 시그널 마스크의 값으로 재설정하는 것이다. 이 경우, SIGUSR1 시그널이 다시 블록되어진다. sigprocmask의 두 번째 호출은 이 시그널의 블록을 명백하게 해제할 필요가 있다.
다른 포인트 : 오직 하나의 SIGUSR1 시그널을 기다리는 그 프로그램에서 왜 while 루프가 필요한지 의아해 할지 모른다. 그 대답은, sigsuspend에 주어지는 마스크가, 예를 들어, 작업 제어 시그널처럼 다른 종류의 시그널이 배달됨으로 인해서 구동되어질 프로세스를 허가한다는 것이다. 만일 usr_interrput를 설정하지 않은 시그널에 의해 프로세스가 재개된다면, 그것은 단지 "올바른" 종류의 시그널이 발생할 때까지 다시 중지된다. 이 테크닉은 준비작업에 더 낳은 라인이 필요하지만, 당신이 시그널에 대한 정확한 기다림을 위해서는 필요하다. 실제로 기다림을 위한 코드는 단지 4줄뿐이다.
이 절은 BSD 유닉스에서 온 시그널 핸들링 함수들에 대해서 설명한다. 이들 도구들은 그들의 시대에서는 진보적이였지만; 오늘날은 굉장히 시대에 뒤떨어진 것이고, 오직 BSD와의 호환성을 위해서 제공되고 있다.
POSIX 시그널 처리 기능들은 BSD 기능들로부터 나온 것이기 때문에 BSD와 POSIX 시그널 처리 기능들 사이에는 많은 유사성이 있다. 충돌을 피하기 위해서 모든 함수들이 서로 다른 이름을 갖고 있다는 것을 제외하고, 둘 사이에는 주요한 차이들이 있다.
BSD 유닉스는 sigset_t 오브젝트로 시그널 마스크를 나타내는 것이 아니라 int 비트마스크로써 시그널 마스크를 표현한다.
BSD 기능들은 인터럽트된 기본동작(primitive)을 실패하게 할 것인지 재개할 것인지의 여부에 대해서 다른 디폴트를 사용한다. POSIX 기능은 당신이 그들을 재개하도록 정할지라도 시스템 호출이 실패하게 만들고, BSD 기능들은, 당신이 그들을 실패하도록 정했을지라도 시스템 호출은 그것을 재개하도록 만드는 것이다. 21. 5절 [Interrupted Primitives] 참조.
BSD 유닉스는 시그널 스택의 구상을 갖는다. 이것은 보통의 실행 스택대신에, 시그널 핸들러 함수들의 실행동안에 사용되는 대체스택 (alternate stack)이다.
BSD 기능들은 `signal. h'에 선언되어 있다.
데이터타입 : struct sigvec
sighandler_t sv_handler
int sv_mask
int sv_flags
그들 기호 상수들은 sigvec 구조체의 sv_flags 를 위해 제공되는 값들로 사용될 수 있다. 이 필드는 비트마스크 값으로써, 당신이 관심을 갖는 플래그들을 비트별-OR를 사용해서 결합할 수 있다.
매크로 : int SV__ONSTACK
매크로 : int SV__INTERRUPT
매크로 : int SV__RESETHAND
함수 : int sigvec (int signum, const struct sigvec *action, struct sigvec *old_action)
함수 : int siginterrupt (int signum, int failflag)
매크로 : int sigmask (int signum)
함수 : int sigblock (int mask)
함수 : int sigsetmask (int mask)
함수 : int sigpause (int mask)
시그널 스택은 시그널 핸들러가 실행되는 동안 실행 스택으로써 사용되는 메모리의 특별한 영역이다. 오버플로우가 일어날 위험을 피하기 위해서는, 꽤 커야한다; 매크로 SIGSTKSZ는 시그널 스택을 위한 정규 크기로 정의되었다. 당신은 mallocac 을 사용해서 스택을 위한 공간을 할당할 수 있다. 그리고 나서 sigaltstack 이나 stgstack를 호출하여 시그널 스택을 사용하도록 시스템에게 알린다.
당신이 시그널 스택을 사용하기 위해서 시그널 핸들러를 달리 만들 필요는 없다. 다른 것에서 스택으로의 변경은 자동적으로 발생한다. 그렇지만, 어떤 기계 상에 존재하는 어떤 디버거는 시그널 스택을 사용하는 핸들러가 실행되는 동안 스택 트래이스(trace)를 하면 혼란스럽게 될지도 모른다.
분리된 시그널 스택을 사용하도록 시스템에게 알리기 위한 두 개의 인터페이스가 있다. sigstack은 오래된 인터페이스로써 4. 2 BSD 로부터 왔다. sigaltstack은 새로운 인터페이스로써 4. 4 BSD 로부터 왔다. sigaltstack 인터페이스는 스택의 성장 방향을 알리도록 당신의 프로그램에게 요구하지 않고, 정해진 기계와 운영체제에 의존한다는 편리점을 갖는다.
데이터 타입 : struct sigaltstack
void *ss_sp
size_t ss_size
SIGSTKSZ
MINSIGSTKSZ
int ss_flags
SA_DISABLE
SA_ONSTACK
함수 : int sigaltstack (const struct sigaltstack *stack, struct sigaltstack *oldstack)
EINVAL
ENOMEM
다음은 오래된 sigstack 인터페이스이다. 당신은 sigaltstack 대신에 사용할 수 있다.
데이터 타입 : struct sigstack
void *ss_sp
int ss_onstack
함수 : int sigstack (const struct sigstack *stack, struct sigstack *oldstack)
목차 이전 : 20. 비-지역 탈출 다음 : 22. 프로세스의 시동과 종료