21. 시그널 처리
시그널(앞으로 신호라 해석하지 않고 시그널이라고 하겠습니다. 그것이 더 좋을 것 같아서)은 프로세스에게 배달된 소프트웨어 인터럽트이다. 운영체제는 실행하고 있는 프로그램에 예외적인 상황을 보고하기 위해서 시그널을 사용한다. 어떤 시그널들은 유용하지 않은 메모리 주소를 참조하는것과 같은 에러를 보고하고; 다른 것은 전화선의 단절과 같은, 비동기적 사건을 보고한다.
GNU C 라이브러리는 각각의 특정한 사건들의 종류에 따라, 다양한 시그널의 형태를 정의한다. 사건들의 어떤 종류들은 보통 프로그램의 계속된 진행을권장할 수 없거나 불가능하게 하고, 그에 해당하는 시그널들은 보통 그 프로그램을 중지시킨다. , 유해하지 않은 사건들을 보고한 다른 종류의 시그널들은 보통 무시된다.
만일 당신이 시그널이 발생한 사건을 예상한다면, 당신은 시그널 처리 함수를 정의할 수 있고 특정한 형태의 시그널이 도착했을 때 운영체제가 그 시그널 처리함수를 실행하게 할 수 있다. 최종적으로, 하나의 프로세스는 다른 프로세스에게 한 개의 신호를 보낸다; 이것은 부모 프로세스가 자식 프로세스를 중지시키는 것을 허용하거나, 또는 두 개의 연관된 프로세스가 통신하거나 동기하도록 하는 것을 허용한다.
21. 1 시그널들의 기본 원칙
어떻게 시그널들이 발생되고, 시그널이 도착된 이후에 무슨 일이 발생할 것이며, 어떻게 프로그램이 시그널을 처리할 수 있는지에 대한 기본 원칙들을 설명한다.
21. 1. 1 시그널들의 종류
신호는 예외적인 사건의 발생을 보고한다. 다음은 시그널을 발생시키는 어떤 예외적인 사건들이다.
프로그램이 0으로 나누는 일을 하거나, 또는 유용한 범위를 넘어서는 주소를 억세스하려는것과 같은 에러.
사용자가 프로그램을 인터럽트 또는 중지하도록 요청한다. 대부분의 환경들은 사용자가 C-z를 타이핑하면 일시 중지하거나, C-c를 타이핑하면 종료를 허용하도록 만들어졌다. 키 시퀀스( key sequence)에 무엇이 사용되었던지, 운영체제는 프로세스를 인터럽트 하기 위하여 적당한 시그널을 보낸다.
- 자식 프로세스의 종료.
- 타이머나 알람의 경과.
- 같은 프로세스에 의해 죽이거나 발생한 호출.
- 다른 프로세스로 부터 죽이기 위한 호출. 시그널들은 프로세스사이의 통신에 유용한 형식이지만 제한을 갖는다.
- 타이머나 알람의 경과.
이들 사건들(죽이거나 발생하기 위해 명백하게 호출한 것을 제외하고 )의 각각은 자신만의 특정한 종류의 신호를 발생시킨다. 다양한 종류의 시그널들은 21. 2절 [Standard Signals] 에 상세하게 설명되었다.
21. 1. 2 시그널 발생의 원칙들
일반적으로, 시그널을 발생시키는 사건들은 세 가지로 나눌 수 있다: 에러들. 외부의 사건들과 명백한 요청.
에러는 프로그램이 무언가 유용하지 않을 일을 하고 실행을 계속할 수 없는 것을 의미한다. 그러나 에러들의 모든 종류가 시그널을 발생시키지는 않는다_실제로 그들의 대부분은 시그널을 발생시키지만. . . 예를 들어, 존재하지않는 파일을 개방하기와 같은 것은 에러이지만, 그것은 시그널을 발생시키지 않고; 대신에 open은 -1을 반환한다. 일반적으로, 에러들은 에러를 지적하는 값을 반환함으로써 보고되는 어떤 라이브러리 함수들과 연관되어있다. 시그널들을 발생시킨 에러들은 단지 라이브러리 호출뿐만 아니라 프로그램의 어디서든 발생할 수 있다. 그들에는 0으로 나누기를 하고 유용하지 않은 메모리 주소의 참조가 포함된다.
외부에서 발생한 사건은 입/출력이나 다른 프로세스들과 함께 하는 것에서 나온다. 그들에는 입력의 도착, 타이머의 경과, 자식 프로세스의 종료등이 포함된다.
명백한 요청은 kill처럼 특별하게 시그널을 발생하도록 어떤 목적을 가진 라이브러리 함수의 사용을 의미한다.
시그널들은 동기적으로 또는 비동기적으로 발생되어진다. 동기적 시그널 은 프로그램의 어느 정해진 동작과 관계하고, 그 동작을 하는 동안(블록된 것이 아니라면)에 배달 되어진다. . 에러들은 동기적으로 시그널을 발생하고, 프로세스가 같은 프로세스를 위하여 시그널을 발생하도록 함으로써 명백하게 요청한다.
비동기적 시그널들은 시그널을 받은 프로세스의 제어의 밖에서 발생한 사건에 의해 발생되어진다. 그들 시그널들은 실행동안에 예측할 수 없는 시간에 도착된다. 외부의 사건들은 비동기적으로 시그널들을 발생하고, 어떤 다른 프로세스에 적용하도록 명백하게 요청한다.
시그널의 주어진 형태는 전형적으로 동기적이거나 또는 비동기적중에 하나가 된다. 예를 들어, 에러를 위한 시그널은 에러가 동기적으로 신호를 발생했다면 전형적으로 동기적이다. 그러나 어느 시그널의 형태는 명백한 요청으로는 동기적, 또는 비동기적으로 발생되어질 수 있다.
21. 1. 3 어떻게 신호들이 배달되는가
시그널이 발생되어졌을 때, 그때는 아직 미해결인 상태가 된다. 일반적으로 아주 짧은 시간동안만 미해결인채로 남아있고 그 다음에는 신호가 프로세스에게 배달 되어진다. 그렇지만 만일 시그널의 종류가 블록되어졌다면, 그것은 막연히 미해결인체로 남아있게 될 것이다_그 시그널의 블록이 해제될때까지. 일단 블록이 해제되면, 그것은 즉시 배달되어질 것이다. 21. 7절 [Blocking Signals] 참조.
시그널이 배달되었을 때, 즉시 또는 긴 지연후에 그 시그널을 위하여 정해진 행동을 하게 된다. SIGKILL 과 SIGSTOP 와 같은 어떤 시그널들은 그 행동이 정해져있지만, 대부분의 시그널들은 프로그램이 선택하게 된다: 시그널을 무시하거나, 처리 함수를 지정하거나, 또는 시그널의 종류 에 따라 디폴트 동작을 하거나. 프로그램은 signal이나 sigaction과 같은 함수들을 사용해서 선택을 하게 된다(21. 3절 [Signal Actions] 참조. ).
우리는 때때로 핸들러가 시그널을 잡았다고 얘기를 한다. 핸들러가 실행되고 있는 동안, 특정한 시그널은 일반적으로 블록되어진다. 만일 한 종류의 시그널을 위한 정해진 동작이 시그널을 무시하는것이라면, 발생되어진 시그널은 즉시 버려진다 이것은 심지어 시그널이 동시에 블록 되어질지라도 발생한다. 이렇게 버려진 시그널은 비록 프로그램이 연속적으로 그 종류의 시그널을 위하여 다른 동작을 지정하고 블록을 해제하지 않았을지라도 결코 배달되지 않을 것이다.
프로그램에서 처리되지도 않고 무시하지도 않는 신호가 발생하면, 그것의
디폴트 동작이 일어난다. 시그널의 각 종류들은 밑에 설명된 자신의 디폴트 동작을 갖는다(21. 2절 [Standard Signals] 참조. ). 대부분의 시그널에서 디폴트 동작은 프로세스를 종료하는 것이다. "유해하지 않은" 사건들에서 발생한 어떤 종류의 시그널들의 디폴트 동작은 아무 것도 하지않는 것이다.
한 시그널이 프로세스를 종료할 때, 그것의 부모 프로세스는 wait 또는 waitpid 함수들에 의해 보고된 종료 상황 코드를 조사함으로써 종료가 발생한 원인을 알아낼 수 있다. (이것에 대해서는 23. 6절 [Process Completion] 에 좀더 자세하게 나와있다. ) 시그널의 원인이 된 종료의 요소, 그리고 시그널의 종료를 포함해서 정보를 얻을 수 있다. 만일 쉘로부터 당신이 실행하고 있는 어떤 프로그램이 시그널에 의해 종료가 된다면, 그 쉘은 그것에 해당하는 에러메세지를 프린트 할 것이다. 프로그램의 에러를 표현하는 시그널들은 특별한 속성을 갖는다: 시그널 중의 하나가 프로세스를 종료할 때, 종료한 시간에 프로세스의 상황에 대한 기록을 코어 덤프 파일에 기록한다. 당신은 무슨 에러가 발생했는지 조사 하기 위해서 디버거를 사용해서 코어 덤프 파일을 조사할 수 있다.
만일 당신의 명백한 요청에 의해서 프로세스를 종료하고 "프로그램 에러" 시그널이 발생하면, 직접적으로 에러에 기인하는 시그널로써 코어 덤프 파일을 만든다.
21. 2 표준 시그널들
이 절은 다양한 표준 시그널들의 이름과 그것이 어떤 사건을 의미하고 있는지를 설명하고 있다. 각각의 시그널의 이름은 시그널의 종류를 위한 시그널번호로써 양의 정수로 나타낸 매크로이다. 당신은 여기에서 정의한 이름대신에 당신 마음대로 시그널의 번호코드를 결코 가정할 수 없다. 이것은 시그널의 종류에 부여된 번호가 시스템에서 시스템으로는 바꿀수 있지만, 그 이름들의 의미는 표준화되어있고 완전히 단일화 되어있기 때문이다.
시그널의 이름들은 헤더파일 `signal. h'에 정의되어 있다.
매크로 : int NSIG
- 이 심볼 상수의 값은 정의된 시그널의 총 개수이다. 시그널의 번호들은 연속적으로 할당되어 있기 때문에 NSIG는 정의된 시그널의 번호중에서 가장 큰 번호보다 하나가 크다.
21. 2. 1 프로그램 에러 시그널들
다음의 시그널들은 심각한 프로그램의 에러가 운영체제나 컴퓨터 자체에 의해 검출되었을 때 발생 된다. 일반적으로, 이들 시그널 모두는 당신의 프로그램이 심각하게 깨져있고, 에러가 포함된 그 실행을 계속할 아무런 방법이 없음을 지적한다.
어떤 프로그램들은 프로그램의 에러 시그널로 인해서 종료되기전에 그들을 깨끗하게 처리한다. 예를 들어, 터미널 입력의 반향을 끈(tnun off) 프로그램들은 다시 반향을 켤 목적으로 프로그램 에러 시그널들을 처리할 것이다. 핸들러는 시그널을 위한 디폴트 동작을 정하고 그 동작을 함으로써 끝날 것이다; 만일 프로그램이 시그널 핸들러를 가지지 않았다면, 프로그램은 그 시그널로 인해서 종료될 것이다. ( 21. 4. 2절 [Termination in Handler] 참조. )
종료는 대부분의 프로그램에 에서 에러에 대응한 이해 가능한 최종적인 결과이다. 그렇지만, Lisp과 같은 프로그래밍시스템들은 사용자 프로그램에 에러가 발생했을지라도 컴파일된 사용자 프로그램을 실행시켜야할 필요가 있다면 로드(load)시킬 수 있다. 이들 프로그램은 커멘드 레벨(command level)로 제어를 반환하는 longjmp를 사용한 핸들러를 갖는다. 모든 시그널의 디폴트 동작은 프로세스를 종료하는 것이다. 만일 당신이 그 시그널들을 블록하거나 무시하거나 시그널을 위한 핸들러를 만든다면, 당신의 프로그램은 아마도 그와같은 시그널들이 발생했을 때, 그들이 실제 에러대신에 raise나 kill에 의해 발생된 것이 아니라면, 심각하게 파괴될 것이다.
그들 프로그램 에러 시그널중의 하나가 프로세스를 종료할 때, 종료와 같은 시간에 프로세스의 상황기록을 코어덤프 파일에 출력한다. 코어덤프 파일은 `core'라고 이름지어졌고 프로세스가 현재 존재하고 있는 디렉토리 에 존재한다. ( GNU 시스템에서, 당신은 환경변수 COREFILE를 통해서 코어 파일의 이름을 지정할 수 있다. ) 코아덤프파일의 존재 목적은 무슨 에러가 발생했는지 조사 하기 위함으로써, 디버거를 사용해서 그들을 시험할 수 있다.
매크로 : int SIGFPE
- SIGFPE 시그널은 심각한 산술적 에러를 보고한다. 그 이름이 "floating-point exception"에서 유래된것이라 할지라도, 이 시그널은 실제로는 모든 산술적 에러들에 작용한다. 만일 어떤 프로그램이 어떤 위치에 정수 데이터를 저장하고 그 데이터에 플로팅-포인트 명령을 사용한다면, 이것은 그 프로세서가 데이터를 플로팅-포인트 수로써 인식할 수 없기 때문에 종종 "유용하지 않은 연산"의 원인이 된다.
플로팅-포인트 예외상황에 대한 것은 아주 민감하게 다른 의미를 지닌 예외상황의 여러종류들이 있기 때문에 아주 복잡한 주제이고, SIGFPE 시그널은 그들을 구분하지 않는다. 이진 플로팅-포인트 연산을 위한 IEEE 표준(ANSI/IEEE Std 754-1985)은 다양한 플로팅-포인트 예외상황에 대해서 정의하고 있고 컴퓨터 시스템이 예외상황의 발생을 보고할 때 따르도록 요구한다. 그렇지만, 이 표준은 그 예외상황이 어떻게 보고되는 지에 대해서는 지정하지 않았고, 또한 운영체제가 제어와 처리의 어떤 종류를 프로그래머에게 제공할 수 있는지를 지정하지 않았다.
BSD 시스템들은 예외상황의 다양한 원인을 구별하는 특별한 인수를 가진 SIGFPE 핸들러를 제공한다. 이 인수를 억세스 하기 위해서, 당신은 두 개의 인수를 받아들이는 핸들러를 정의해야만 한다. GNU 라이브러리는 이 특별한 인수를 제공하지만, 그 값은 BSD 시스템과 GNU 시스템에서만 오직 의미가 있다.
역자주 : trap(트랩) : 하나의 명령어가 실행될 따마다 자동적으로 발생되는 인터럽트. 이러한 인터럽트는 중앙처리 장치에 의하여 하드웨어 적으로 발생하게 되는데 프로그램에서 하나의 명령어가 실행될 때마다 자동적으로 미리 정의된 트랩 처리 루틴으로 실행의 제어권이 넘어온다. 하드웨어 장치와 밀접한 관련이 있는 시스템 소프트웨어에서 오류를 찾아내기 위한 수단으로 이용된다.FPE_INTOVF_TRAP
- 정수 오버플로우 ( 당신이 하드웨어의 정해진 사양에 따라서 오버플로우의 트랩이 가능하지 않으면 C 프로그램에서는 불가능하다. )
FPE_INTDIV_TRAP
- 정수를 0으로 나누기.
FPE_SUBRNG_TRAP
- 아래에 기입한-범위 ( 어떤 C 프로그램은 결코 체크하지 않는다. )
FPE_FLTOVF_TRAP
- 플로팅 오버플로우 트랩.
FPE_FLTDIV_TRAP
- 플로팅/정수를 0으로 나눔.
FPE_FLTUND_TRAP
- 플로팅 언더플로우. 트랩 ( 플로팅 포인트에서 트랩 하는 것은 보통 가능하지 않다. )
FPE_DECOVF_TRAP
- 십진수 오버플로우 트랩. ( 오직 몇 개의 기계에서만 십진수 연산을 갖고 있고 C에서는 결코 그것을 사용하지 않는다. )
매크로 : int SIGILL
- 이 시그널의 이름은 "illegal instruction : 비합법적인 명령"에서 유래되었다; 그것은 쓸모없거나 특권이 부여된 명령어를 실행하려 했다는 의미이다. 오직 유용한 명령어만이 발생된 C 컴파일러에서, SIGILL은 전형적으로 실행 가능 파일이 훼손되었거나, 당신이 데이터를 실행하려 시도했다는 것을 지적한다. 후자의 상황이 발생되는 일반적 상황으로는 함수를 위한 포인터가 있을 것이라고 예상된 곳에서 유용하지 않은 오브젝트를 파싱하거나, 자동 배열의 끝을 넘어서 기록을 하고( 또는 자동 변수를 위한 포인터와 유사한 문제들) 스택 프레임의 반환 어드레스 처럼 스택에서 다른 데이터의 훼손과 같은 문제들이 있다.
매크로 : int SIGSEGV
- 이 시그널은 할당된 메모리의 범위를 벗어나는곳에서 읽거나, 쓰기를 시도할 때 발생 된다. ( 실제로, 그 시그널들은 프로그램이 충분한 영역을 할당받지 못할 때 시스템 메모리 보호 메커니즘에 의해서 발생한다. ) 그 이름은 "segmentation violation"의 약자이다. SIGSEGV 상황이 발생되는 가장 일반적인 방법은 비참조 되는 널( defeferencing a null) 이나 초기화되지 않은 포인터에 의한 것이다. 널 포인터는 주소 0으로 참조되고, 대부분의 운영체제는 이 주소가 정확하게 유용하지 않음을 확실히 하기 때문에 비참조 널 포인터는 SIGSEGV가 발생될 것이다. (어떤 운영체제는 주소가 0인 메모리도 유용하고, 비참조 널 포인터는 그들 시스템상에서는 시그널을 발생하지 않는다. ) 비초기화된 포인터에서는, 유용하지 않거나, 유용하더라도 임의의 주소들을 갖게된다. SIGSEGV 상황이 얻어지는 다른 일반적 방법은 배열에 포인터를 사용했을 때 그 배열의 끝을 체크하기를 실패했을 때이다.
매크로 : int SIGBUS
- 이 시그널은 유용하지 않은 포인터가 비참조되었을 때 발생 된다. SIGSEGV 처럼, 이 시그널은 초기화되지 않은 포인터를 비참조 한 것의 결과이다. 두 시그널의 차이점은 SIGSEGV는 유용한 메모리에서 유용하지못한 억세스를 지적하고, SIGBUS는 유용하지못한 주소를 억세스 하는 것을 지적한다. 특별하게, SIGBUS 시그널은 4개로 나누어지지 않은 주소에 4-단어 정수로 참조하는것처럼, 부적당한 포인터가 비참조 됨으로써 발생한다. (각종 시스템은 주소 정렬은 위한 자신만의 필요조건을 갖는다. ) 이 시그널의 이름은 "bus error"의 약자이다.
매크로 : int SIGABRT
- 이 시그널은 프로그램 그 자체와 abort가 호출되었음을 보고함으로써 발생되는 에러를 지적한다. 22. 3. 4절 [Aborting a Program] 참조.
21. 2. 2 종료 시그널
이들 시그널들은 이런 저런 방법으로 프로세스를 종료함을 알리기위해 사용된다. 그들은 완전히 다른 목적을 위해 사용되기 때문에 다른 이름을 가졌고, 프로그램은 그들은 다르게 취급하기를 원할 것이다.
이들 시그널들은 처리하기 위한 이유는 보통 당신의 프로그램이 실제로 종료되기전에 적당하게 처리할 수 있도록 하기 위한 것이다. 예를 들어, 당신은 상황정보를 저장하고, 임시 파일들을 지우고, 이전의 터미널 모드를 반환하기를 원할수도 있다. 그와 같이 핸들러(handler)는 발생된 시그널을 위한 디폴트 동작을 지정하고 그리고 그 시그널을 다시 발생시킴으로써 종료할 것이다. 이것은 만일 프로그램이 핸들러를 가지지 않았더라도, 그 시그널로 인해서 프로그램이 종료될 것이다. ( 21. 4. 2절 [Termination in Handler] 참조. )
이 시그널들을 위한 (명백한) 디폴트 동작은 프로세스가 종료되도록 하는 것이다.
매크로 : int SIGHUP
- SIGHUP ("hang-up") 시그널은 사용자 터미널의 단절을 보고하기 위해 사용되어지는데, 아마도 네트웍이나 전화선 연결이 끊어졌기 때문이다. 이것에 대한 상세한 정보는 12. 4. 6절 [Control Modes] 참조. 이 시그널은 또한 그 세션과 연관된 작업을 위해서 터미널에서 제어하고 있는 프로세스의 종료를 보고하기 위해 사용되어진다; 이 종료는 제어중인 터미널로부터 그 세션안에 있는 모든 프로세스를 효과적으로 단절한다. 더 상세한 정보는 22. 3. 5절 [Termination Internals] 참조.
매크로 : int SIGINT
- SIGINT("program interrupt") 시그널은 사용자가 INTR 문자를 (보통 C-c)을 입력했을 때 보내어진다. 터미널 드라이버가 C-c 를 지원하는지에 대한 정보는 12. 4. 9절 [Special Characters] 참조.
매크로 : int SIGQUIT
- SIGQUIT 시그널은 다른 키_QUIT 문자, 보통 C-\_에 의해서 제어된다는 것을 제외하고는 SIGINT와 유사하고, 그 프로세스가 종료 될 때 프로그램 에러 시그널처럼 코어 파일을 작성한다. 당신은 사용자에 의해 "검출된" 프로그램 에러 상황으로 이들을 생각할 수 있다. 코어 덤프 파일에 대한 정보는 21. 2. 1절 [Program Erroe Signals] 참조. 지원하는 터미널 드라이버에 대한 정보는 12. 4. 9절 [Special Characters] 참조. 소거의 어떤 종류들은 SIGQUIT를 처리하는 동안에 생략되어지는 것이 좋다. 예를 들어, 만일 프로그램이 임시파일을 만든다면, 그것은 임시파일을 지움으로써 다른 종료 요청을 처리할 것이다. 하지만 사용자가 코어 덤프 파일을 시험할수 있게 하기 위하여, 그들을 지우지 않는 것이 SIGQUIT를 위해서 더 좋다.
매크로 : int SIGTERM
- SIGTERM 시그널은 프로그램을 종료하는데 사용하는 포괄적인 시그널이다. SIGKILL과 달리, 이 신호는 블록되어진고, 처리되어지고 무시되어질 수 있다. 쉘 코맨드 kill은 디폴트로 SIGTERM을 발생시킨다.
매크로 : int SIGKILL
- SIGKILL 시그널은 즉각적인 프로그램 종료를 일으키기 위해서 사용되어진다. 이 시그널은 처리되거나, 무시되거나 할 수 없고, 그 결과는 항상 치명적이 된다. 이 시그널은 블록하는것도 불가능하다.
- 이 시그널은 오직 명백한 요청에의해 발생되어진다. 그것이 처리되어질 수 없다면, 당신은 일단C-c 또는 SIGTERM과 같은 덜 격렬한 방법을 시도한 후에, 나중에 마지막 방법으로 오직 그것을 발생시킬 것이다. 만일 프로세스가 어느 다른 종료 시그널들에 반응하지 않는다면, SIGKILL시그널을 보내면 거의 항상 그 프로세스가 종료될 것이다. 실제로, SIGKILL이 프로세스를 종료하는데 실패한다면, 운영체제의 버그 때문이다.
12. 2. 3 알람 시그널
그들 시그널은 타이머의 경과를 지적하는데 사용되어진다. 이들 시그널을 보내는 함수에 대한 정보는 17. 3절 [Settin an Alarm] 참조. 그들 시그널을 위한 디폴트 동작은 프로그램을 종료를 일으키는 것이다. 이 디폴트 동작은 거의 유용하지 않다; 그들 시그널을 사용하는 대부분의 방법은 어느 경우에 맞는 핸들러 함수들을 요구하는 것이다.
매크로 : int SIGALRM
- 이 시그널은 전형적으로 실제또는 클럭 시간을 계산한 타이머의 경과를 지적한다. 예를 들어 alarm 함수에의해 사용되어진다.
매크로 : int SIGVTALRM
- 이 시그널은 전형적으로 현재 프로세스에 의해 사용된 CPU시간을 계산하는 타이머의 경과를 지적한다. 그 이름은 "virtual time alarm"의 약자이다.
매크로 : int SIGPROF
- 이 시그널은 현재의 프로세스에 의해 사용된 CPU 시간과, 프로세스를 대신하여 시스템에의해 사용된 CPU시간의 둘을 계산한 타이머의 경과를 지적하는데 사용된다. 타이머가 자원의 프로파일링을 위한 도구로써 사용되어지므로, 시그널의 이름이 SIGPROF이다.
21. 2. 4 비동기 입/출력 시그널
이 절에 설명된 시그널들은 비동기 입/출력 도구들과 함께 사용되어진다. 당신은 어떤 특정한 파일 기술자가 그들 시그널을 발생시키도록 하기 위해서 fcntl을 호출함으로써 명백한 동작을 취하도록 해야한다( 8. 12절[Interrupt Input] 참조. ) 그들 시그널을 위한 디폴트 동작은 그들을 무시하는 것이다.
매크로 : int SIGIO
- 이 시그널은 파일기술자가 입력 또는 출력을 수행할 준비가 되어있을 때 보내어진다. 대부분의 운영체제에서, 터미널과 소켓만이 SIGIO를 발생시킬 수 있다; 보통의 파일들을 포함한 다른 종류들은 당신이 그들에게 요청했을지라도 SIGIO신호를 발생시키지 않는다.
매크로 : int SIGURG
- 이 시그널은 소켓에 도착한 데이터가 "긴급"하거나 범위를 벗어 났을 때 보내어진다. 11. 8. 8절 [Out-of-Band Date] 참조.
21. 2. 5 작업 제어 시그널
이들 시그널은 작업 제어를 지원하기 위해서 사용되어진다. 만일 당신의 시스템이 작업 제어를 지원하지 않는다면 시그널들은 발생되어지거나, 처리될 수는 없지만 매크로들은 정의되어있다. 당신이 실제로 작업이 어떻게 제어되는지를 이해할 수 없다면 그들 시그널을 그대로 방치할 것이다. 24장 [Job Control] 참조.
매크로 : int SIGCHLD
- 이 시그널은 자식 프로세스들중의 하나라도 종료되거나 멈출 때마다 부모 프로세스에게 보내어진다. 이 시그널을 위한 디폴트 동작은 그것을 무시하는 것이다. 만일 당신이 wait 또는 waitpid를거쳐 (23. 6절 [Process Completion] 참조. ) 그들의 상황이 보고되지 않았지만, 종료된 자식 프로세스에서 발생한 시그널을 위한 핸들러를 만든다면, 당신의 새로운 핸들러가 그들 프로세스에 적용이 되던지 또는 특정한 운영체제에 달려있다.
매크로 : int SIGCONT
- 당신은 프로세스가 계속되도록 하기 위해서 SIGCONT 신호를 보낼 것이다. 이 시그널을 위한 디폴트 동작은 만일 그 프로세스가 멈추었다면 그 프로세스를 계속하도록 만드는 것이고 그렇지 않다면 그것을 무시하는 것이다. 대부분의 프로그램에서는 SIGCONT를 처리할 아무런 이유가 없다; 그들은 전에 멈추었었음을 인식함이 없이 계속 실행되고 있다고 가정한다.
- 당신은 어떤 특정한 동작을 하는 프로그램을 멈추거나 계속하도록 만들기 위해서 SIGCONT 시그널을 위한 핸들러를 사용할 수 있다_예를 들어, 입력을 기다리기 위해서 잠시 멈추었을 때 프롬프트를 다시 프린트 하는것과 같은.
매크로 : int SIGSTOP
- SIGSTOP 시그널은 프로세스를 멈춘다. 그것은 처리되거나, 무시되거나 블록될 수 없다.
매크로 : int SIGTSTP
- SIGTSTP 시그널은 상호 작용하는 멈춤 신호이다. SIGSTOP와는 달리 이 신호는 처리되거나 무시되어질 수 있다. 당신의 프로그램에서 프로세스가 멈추었을 때 파일이나 시스템 테이블을 안전한 상황으로 만들어놓을 특별한 필요가 있다면 이 신호를 처리할 수 있다.
- 예를 들어, 반향이 꺼진 프로그램에서는 멈추기 전에 다시 반향을 켜도록 SIGTSTP 시그널을 처리할 것이다. 이 시그널은 사용자가 SUSP 문자(보통 C-z)를 입력하다 때 발생 된다. 지원하는 터미널 드라이버에 대한 자세한 정보는 12. 4. 9절 [Special Characters] 참조.
매크로 : int SIGTTIN
- 한 프로세스가 배경 작업으로써 실행되고 있는 동안 사용자의 터미널로부터 읽을 수 없다. 배경 작업에 속한 어느 프로세스가 터미널로부터 읽으려 시도할 때, 그 작업에 속한 모든 프로세스는 SIGTTIN 신호를 받는다. 이 시그널을 위한 디폴트 동작은 그 프로세스를 멈추는 것이다. 어떻게 터미널 드라이버와 상호작용 하는지에 대한 자세한 정보는 24. 4절 [Access to the Terminal] 참조.
매크로 : int SIGTTOU
- SIGTTIN과 유사하지만, 배경 작업에 속한 프로세스가 터미널에 출력하려 시도하거나 그 터미널 모드를 설정하려 시도할 때 발생 된다. 다시 말하면 디폴트 동작은 그 프로세스를 멈추는 것이다.
- 프로세스가 멈추어있을 동안, SIGKILL 시그널과 SIGCONT시그널을 제외하고는 어느 다른 시그널들은 배달되어질 수 없다.
- SIGKILL 시그널은 항상 프로세스의 종료를 유발하고 블록되거나 무시될 수 없다. 당신이 SIGCONT 시그널을 무시하거나 블록할 수 있지만, 그것은 만일 그 프로세스가 멈추어져있다면 프로세스가 계속되도록 한다. 프로세스에게 보낸 SIGCONT 시그널은 아직 미해결인채로 남아있는 멈춤 시그널을 프로세스가 버리도록 한다. 이와 비슷하게, 어떤 프로세스에서 아직 미해결인채로 남아있는 SIGCONT 시그널은 멈춤 시그널이 도착했을 때 버려진다.
- 고아가 되어버린 프로세스 그룹에 있는 한 프로세스에게 SIGTSTP, SIGTTIN, 또는 SIGTTOU 시그널을 보내면 그것은 처리되지도 않고, 그 프로세스는 멈추어 지지도 않는다. 그것을 계속할 아무런 방법이 없는 부당하게 되어버린 프로세스를 멈추게 하라. 운영체제에 의존하지 말고당신이 무언가를 사용해서 멈추게 하라. 어떤 시스템은 아무런 일도 하지 않을 것이다. 다른 시스템들은 대신에 SIGKILL 또는 SIGHUP와 같은 시그널들을 배달할 것이다.
- 프로세스가 멈추어있을 동안, SIGKILL 시그널과 SIGCONT시그널을 제외하고는 어느 다른 시그널들은 배달되어질 수 없다.
21. 2. 6 잡다한 시그널
그들 시그널은 다양한 다른 상활들을 보고하기 위해서 사용되어진다. 이들의 디폴트 동작은 프로세스가 종료되도록 하는 것이다.
매크로 : int SIGPIPE
- 만일 당신이 파이프나 FIFO들을 사용한다면, 당신의 어플리케이션에서 다른 것이 출력을 시작하기 전에 한 프로세스가 읽기를 위해서 파이프를 개방하도록 만들어야 한다. 만일 읽기 프로세스가 결코 시작되지 않거나, 급작스럽게 종료된다면 파이프나 FIFO에 출력하기는 SIGPIPE 시그널을 발생시킨다.
- 만일 SIGPIPE 가 블록되거나, 처리되어지거나, 무시되어지면, 그 손상된 호출은 대신에 EPIPE로 실패한다. 파이프와 FIFO 특별한 파일들은 10장 [Pipes and FIFOs] 에서 좀더 자세하게 논의되었다. SIGPIPE가 발생하는 다른 원인은 당신이 연결되지 않은 소켓에 출력을 시도했을 때 발생한다. 11. 8. 5. 1절 [Sending Data] 참조.
매크로 : int SIGUSR1
매크로 : int SIGUSR22
- SIGUSR1 과 SIGUSR2 시그널들은 당신이 원하는 어떤 방법을 사용하지 못하도록 한다. 그들은 프로세스간 통신을 위해서 유용하다. 그들 시그널을 보통 심각하기 때문에 당신은 그 시그널을 받은 프로그램에서 그들은 위한 시그널 처리를 해야할 것이다. SIGUSR1 과 SIGUSR2에 대한 사용예는 21. 6. 2절 [Signaling Another Process] 참조.
21. 2. 7 비표준 시그널
특정한 운영체제는 위에 설명되지 않은 부가적인 시그널들을 지원한다. ANSI C 표준은 시그널들의 명칭을 `SIG'로 시작하는 대문자로 예약하였다. 당신은 당신의 특정한 운영체제를 위한 헤더파일이나 그 운영체제가 지원하고 있는 시그널을 발견하는 프로세서의 타입 등에 대한 것에 조언을 구할 수 있다. 예를 들어, 어떤 시스템은 하드웨어 트랩에 해당하는 여분의 시그널들을 제공한다. 보통 지원되는 어떤 다른 종류의 시그널들은 CPU 시간과 파일 시스템 사용에 대한 제한을 가하거나, 터미널 구성을 비동기적으로 변경하는 것과 같은 것을 위해 사용되어진다. 시스템들은 또한 표준 시그널 이름의 별칭(aliases)이 되는 시그널 이름들을 정의하고 있다.
당신은 당신이 이해하고 있는 정의된 시그널들을 위한 디폴트 동작을 (또는 쉘에 의해 작동하는 동작) 가정할 수 있고, 당신은 그들에 대해서는 걱정하지 않는다. 당신이 그 시그널의 의미를 알지 못하는 것에 대해서 핸들러를 만들려 시도하거나 알지 못하는 시그널을 무시하거나 블록하는 것은 좋지 못한 생각이다. 여기에 일반적으로 운영체제에서 사용되고 있는 약간의 다른 시그널에 대한 것이 있다.
SIGCLD
- SIGCHLD의 오래된 명칭.
SIGTRAP
- 기계상의 중단점 명령에 의해 발생 된다. 디버거에 의해 사용된다. 디폴트 동작은 코어를 덤프하는 것이다.
SIGIOT
- PDP-II "iot" 명령에 의해 발생 된다; SIGABRT와 동등하다. 디폴트 동작은 코어 덤프하는 것이다.
SIGEMT
- 에뮬레이터를 트랩한다; 어떤 충족되지 못한 명령으로부터의 결과이다. 그것은 프로그램 에러 시그널이다.
SIGSYS
- 좋지 못한 시스템 호출; 실행된 운영체제를 트랩하기 위한 명령이지만, 그것을 수행하도록 하는 시스템 호출을 위한 코드 번호가 유용하지 않다. 이것은 프로그램 에러이다.
SIGPOLL
- 이것은 SIGIO와 많이 또는 덜 유사한, 시스템 V 시그널 명칭이다.
SIGXCPU
- CPU 시간 제한이 초과되었다. 이것은 배치 프로세싱을 위해서 사용되어진다. 디폴트 동작은 프로그램 종료이다.
SIGXFSZ
- 파일 크기 제한이 초과되었다. 이것은 배치 프로세싱을 위해서 사용되어진다. 디폴트 동작은 프로그램 종료이다.
SIGWINCH
- 윈도우 크기를 변경한다. 이것은 스크린에서 현재 윈도우의 크기가 변경되었을 때 어떤 시스템에서 발생되어진다. 디폴트 동작은 그것을 무시하는 것이다.
21. 2. 8 시그널 메시지
우리는 자식 프로세스를 종료한 시그널을 설명하는 메시지를 쉘이 프린트하는 것에 대해서는 위에서 잠깐 언급했다. 시그널을 설명하는 메시지를 프린트하는 깨끗한 방법은 strsignal 과 psignal 함수들을 사용하는 것이다. 그들 함수들은 설명하려는 시그널의 종류를 지정하기 위해서 시그널 번호를 사용한다. 시그널번호는 자식 프로세스의 종료상황으로부터(23. 6절 [Process Compltion] 참조)오거나 또는 같은 프로세스 안에 있는 시그널 핸들러로부터 올 것이다.
함수 : char * strsignal (int signum)
- 이 함수는 시그널 signum을 설명하고 있는 메시지를 포함하고 있는 정적으로 할당된 문자열에 대한 포인터를 반환한다. 당신은 이 문자열의 내용을 갱신할 수 없다. 그리고 그것은 연속된 호출에 의해서 덧씌워질 수 있으므로, 당신은 만일 그것을 나중에 참조 할 필요가 있다면 따로 저장해야할 것이다. 이 함수는 GNU확장으로 헤더파일`string. h'에 선언되어 있다.
함수 : void psignal (int signum, const char *message)
- 이 함수는 표준 에러 출력 스트림 stderr에 시그널 signum을 설명하는 메시지를 프린트한다. 7. 2절 [Standard Streams] 참조. 만일 당신이 널 포인터이거나 또는 빈 문자열인 message를 가지고 psignal을 호출하면, psignal은 단지 새줄을 하나 덧붙여서 signum에 해당하는 메시지를 프린트한다.
- 만일 당신이 널이 아닌 message 인수를 공급하면, psignal은 이 문자열로 그 출력의 앞에 놓는다. 그것은 signum에 해당하는 문자열로부터 메시지를 분리하기 위한 공백이나 콜론을 더한다. 이 함수는 BSD를 위한 것이고, 헤더파일 `stdio. h'에 선언되어 있다. 또한 다양한 시그널 코드들을 위한 메시지를 포함하고 있는 배열 sys_siglist가 있다. 이 배열은 strsiganl과 달리 BSD 시스템 상에 존재한다
21. 3 시그널 동작 정하기
시그널을 위한 동작을 변경하기 위한 가장 간단한 방법은 signal함수를 사용하는 것이다. 당신은 내장된(built-in) 동작을 지정하거나, 핸들러를 만들 수 있다. GNU 라이브러리는 또한 좀더 다양한 기능을 가진 sigaction 도구를 사용한다. 이 절은 두 개의 도구들에 대한 설명과 언제 이것을 사용할 지에 대한 제안을 한다.
21. 3. 1 기본 시그널 처리
signal 함수는 특정한 시그널을 위한 동작을 만들기 위한 간단한 인터페이스를 제공한다. 그 함수와 연관된 매크로들은 헤더파일 `signal. h'에 선언되어 있다.
데이터 타입 : sighandler__t
- 이것은 시그널 핸들러 함수들의 타입이다. 시그널 핸들러들은 시그널 번호를 지정하기 위해서 정수인수를 하나 취하고, 반환 타입으로는 void 형을 가진다. 그래서 당신은 다음과 같이 핸들러 함수를 정의할 수 있다.
void handler (int signum) { . . . }
- 이 데이터 타입을 위한 sinhandler_t는 GNU 확장이다.
함수 : sighandler_t signal (int signum, sighandler_t action)
- signal 함수는 시그널 signam을 위한 동작을 action으로 만든다. 첫 번째 인수, signum은 당신이 제어하기 원하는 행동을 가진 시그널을 시그널 번호로써 지정한다. 시그널 번호를 지정하기 위한 적당한 방법은 21. 2절 [Standard Signals] 에서 설명된 심볼릭 시그널 이름들 중 하나를 사용하는 것이다_주어진 시그널의 종류를 위한 숫자 코드들은 서로 다른 운영체제에서는 변화할 수 있기 때문에 명백한 숫자를 사용하는 것을 삼가라.
- 두 번째 인수, action은 시그널 signum을 위해 사용하는 동작을 지정한다. 다음중의 하나가 사용될 수 있다.
SIG_DFL
- SIG_DFL 은 특정한 시그널을 위한 디폴트 동작을 지정한다. 다양한 종류의 시그널들을 위한 디폴트 동작들은 21. 2절 [Standard Signals] 에 나와있다.
SIG_IGN
- SIG_IGN은 시그널이 무시되도록 정한다. 당신의 프로그램은 심각한 사건들을 표현하거나, 또는 종료를 요청하는데 사용되는 시그널들은 보통무시하지 않을 것이다. 당신은 전혀 SIGKILL 또는 SIGSTOP신호를 무시할 수 없다. 당신은 SIGSEGV와 같은 프로그램 에러 시그널들을 무시할 수 있지만, 에러를 무시하는 것이 실행을 계속하도록 프로그램을 가능하게 만드는 것은 아니다. SIGINT, SIGQUIT 그리고 SIGTSTP와 같은 사용자의 요청을 무시하는 것은 사용자를 불쾌하게 만든다. 당신이 프로그램의 어떤 부분이 실행되는 동안에 시그널이 배달되는 것을 원하지 않을 때, 그들을 블록하기 위해서 사용하는 것이지 그들을 무시하는 것이 아니다. 21. 7절 [Blocking Signals] 참조.
handler
- 어떤 시그널이 배달되었을 때 이 핸들러가 작동하도록 하기 위해서 당신의 프로그램에 핸들러 함수의 주소를 공급한다. 시그널 핸들러 함수를 정의하기에 대한 상세한 정보는 21. 4절 [Defining Handlers] 참조. 만일 당신이 SIG_IGN으로 시그널을 위한 동작을 설정했거나, 또는 SIG_DFL로 설정하고 디폴트 동작이 시그널을 무시하는 것이라면 어느 미해결된 시그널들은 버려진다( 심지어 그들이 블록되었을 지라도 ). 미해결인 시그널들을 버리는 것은, 심지어 당신이 그 시그널을 위한 다른 동작을 지정하고 블록을 해제하도록 연속적으로 정할지라도 그들이 결코 배달되어지지 않을 것임을 의미한다.
- signal 함수는 정해진 signum 시그널을 위해서 효력이 있었던 동작을 반환한다. 당신은 이 값을 저장할 수 있고 나중에 다시 signal을 호출함으로써 그것을 다시 반환한다. 만일 signal이 요청을 받아들일 수 없다면, 그것은 대신에 SIG_ERR을 반환한다.
- 다음의 errno는 이 함수를 위해 정의된 에러 상황이다.
EINVAL
- 당신은 유용하지 않은 signum을 지정하였거나; 또는 SIGKILL 이나 SIGSTOP를 위한 핸들러를 제공하거나 무시하려 시도했다.
- 어떤 심각한 시그널이 발생했을 때 임시파일들을 지우기 위한 핸들러를 설정하는 간단한 예제이다.
- #include <signal. h>
- void
- termination_handler (int signum)
- {
- struct temp_file *p;
- for (p = temp_file_list; p; p = p->next)
- unlink (p->name);
- }
- int
- main (void)
- {
- . . .
- if (signal (SIGINT, termination_handler) == SIG_IGN)
- signal (SIGINT, SIG_IGN);
- if (signal (SIGHUP, termination_handler) == SIG_IGN)
- signal (SIGHUP, SIG_IGN);
- if (signal (SIGTERM, termination_handler) == SIG_IGN)
- signal (SIGTERM, SIG_IGN);
- . . .
- if (signal (SIGINT, termination_handler) == SIG_IGN)
- }
주어진 시그널이 무시되도록 미리 설정되어졌다면, 이 코드는 그 설정을 바꾸는 것을 피함을 기억하라. 이것은 비-작업-제어 쉘들이 자식 프로세스가 시작될 때 어떤 시그널들을 종종 무시하기 때문이고, 그리고 이것을 고려하는 것은 자식 프로세스에게는 중요하다. 우리는 이 예제 프로그램이 디버깅을 위해서(코어 덤프 파일) 정보를 제공하도록 만들어졌기 때문에 프로그램 에러 시그널이나 SIGQUIT를 처리하지 않고, 임시 파일들은 유용한 정보를 가질 것이다.
함수 : sighandler_t ssignal (int signum, sighandler_t action)
- ssignal 함수는 signal과 같은 일을 한다; 이것은 오직 SVID와의 호환성을 위해서 제공되어졌다.
매크로: sighandler_t SIG__ERR
- 이 매크로의 값은 에러를 지적하는 signal함수로부터의 반환값으로써 사용되어진다.
21. 3. 2 진보된 시그널 처리
sigaction 함수는 signal과 같은 기본 효과를 갖는다: 한 시그널이 어떻게 프로세스에 의해 처리될 것인지를 정하는. sigaction은 조금은 복잡하지만, 더 많은 제어를 제공한다. 특별하게, sigacion은 시그널이 언제 발생되고 어떻게 그 핸들러가 호출될 것인지에 대해 제어를 할 수 있는 부가적인 플래그를 지정하도록 허용한다. sigaction 함수는 `signal. h'에 선언되어 있다.
데이터 타입 : struct sigaction
- struct sigacion 타입의 구조체들은 어떻게 특정한 시그널을 처리 할 것인지에 대한 모든 정보를 지정하기 위해서 sigaction 함수에서 사용된다. 이 구조체는 적어도 다음의 멤버들을 갖고 있다.
sighandler_t sa_handler
- 이것은 signal 함수의 action인수와 같은 방법으로 사용되어진다. 그 값은 SIG_DFL, SIG_IGN 또는 함수 포인터가 될 수 있다. 21. 3. 1절 [Basic Signal Handling] 참조.
sigset_t sa_mask
- 이것은 핸들러가 작동되고 있는 동안 블록될 시그널의 집합을 설정한다. 블록킹에 대한 것은 21. 7. 5절 [Blocking for Handler] 에 설명되어 있다. 시그널이 배달되었을 때 핸들러가 작동되기 전에 디폴트로써 자동적으로 블록됨을 알아둬라; 이것은 sa_mask에 있는 값에 상관하지 않는다. 만일 당신이 어떤 시그널이 핸들러 안에서 블록되어지지 않기를 원한다면, 당신은 핸들러 안에 있는 코드에 그것을 블록하지 않을 것임을 적어야 한다.
int sa_flags
- 이것은 시그널의 동작에 영향을 미칠 수 있는 다양한 플래그들을 지정한다. 이들에 대한 것은 21. 3. 5절 [Flags for Sigaction] 에 좀더 자세하게 설명되어 있다.
함수 : int sigaction(int signum, const struct sigaction *action, struct sigaction *old_action)
- action 인수는 시그널 signum을 위한 새로운 동작을 준비하기 위해서 사용되고, old_action 인수는 이 심볼과 연관된 이전의 동작에 대한 정보를 반환하기 위해 사용된다. (즉, old_action은 signal 함수의 반환값과 같은 목적을 갖는다.
- 당신은 그 시그널에 영향을 미쳤던 이전의 동작이 무엇이었는지를 알 수 있을 뿐만 아니라 나중에 만일 당신이 원한다면 그 동작을 다시 반환할 수도 있다. ) action 또는 old_action 중의 하나는 널 포인터가 될 수 있다. 만일 old_action 이 널 포인터라면, 이전동작(old action)에 대한 정보를 반환하는 것이 생략된다. 만일 action이 널 포인터라면, 시그널 signum과 연관된 동작이 변경되지 않는다; 이것은 원래의 시그널이 가진 동작은 변경함이 없이 시그널을 처리할 수 있음을 허용한다.
- sigaction으로부터의 반환값은 만일 성공하면 0이고, 실패하면 -1을 반환한다.
- 다음의 errno는 이 함수를 위해 정의된 에러상황이다.
EINVAL
- signum인수가 유용하지 않거나, 당신이 SIGKILL 또는 SIGSTOP 시그널을 무시하거나 트랩 하려고 시도하였다.
21. 3. 3 signal 과 sigaction 의 상호작용
한 단일한 프로그램 안에서 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. 4 sigaction 함수 예제
21. 3. 1절 [Basic Signal Handling] 에서, signal을 사용해서 종료 시그널들을 위한 간단한 핸들러를 만드는 예제를 보여주었다. 다음은 sigaction을 사용하는 같은 예제이다.
- #include <signal. h>
- void
- termination_handler (int signum)
- {
- struct temp_file *p;
- for (p = temp_file_list; p; p = p->next)
- unlink (p->name);
- }
- int
- main (void)
- {
- . . .
- struct sigaction new_action, old_action;
- /* 새로운 동작을 지정할 구조체를 준비하라. */
- new_action. sa_handler = termination_handler;
- sigemptyset (&new_action. sa_mask);
- new_action. sa_flags = 0;
- sigaction (SIGINT, NULL, &old_action);
- if (old_action. sa_handler != SIG_IGN)
- sigaction (SIGINT, &new_action, NULL);
- sigaction (SIGHUP, NULL, &old_action);
- if (old_action. sa_handler != SIG_IGN)
- sigaction (SIGHUP, &new_action, NULL);
- sigaction (SIGTERM, NULL, &old_action);
- if (old_action. sa_handler != SIG_IGN)
- sigaction (SIGTERM, &new_action, NULL);
- . . .
- struct sigaction new_action, old_action;
- }
이 프로그램은 요구된 파라미터로 단지 new_action을 로드하고 sigaction을 호출할 때 그것을 인수로써 사용한다. sigemptyset의 사용에 대한 것은 나중에 설명되었다. 21. 7절 [Blocking Signals] 참조. signal을 사용하는 예제에서, 미리 무시되도록 설정된 시그널들을 처리하는 것을 피했다. 여기서 새로운 동작을 지정하지 않고 현재의 동작을 시험하는 것을 허용하는 sigaction을 사용해서, 순간적으로 시그널핸들러가 변경되는 것을 피할 수 있다.
다음은 다른 예제이다. 이것은 동작을 변경하지 않고 SIGINT를 위해서 현재의 동작에 대한 정보를 구한다.
- struct sigaction query_action;
- if (sigaction (SIGINT, NULL, &query_action) < 0)
- /* 에러가 발생하면 sigaction은 -1을 반환한다. */
- else if (query_action. sa_handler == SIG_DFL)
- /* SIGINT는 원래 자신이 가지고 있던 디폴트 동작으로 처리되었다. */
- else if (query_action. sa_handler == SIG_IGN)
- /* SIGINT는 무시되었다. */
- else
- /* 프로그래머가 만든 핸들러가 효력을 발휘하였다. */
21. 3. 5 sigaction을 위한 플래그
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
- 이 플래그는 SIGCHLD 시그널에서만 유용하다. 그 플래그가 설정되었을 때, 종료된 자식 프로세스를 위한 시그널은 배달하지만, 멈추어있는 자식프로세스를 위한 시그널은 배달하지 않는다. 원래 SIGCHLD를 위한 디폴트 설정은 종료된 자식 프로세스와 멈춘 자식 프로세스 둘다를 위한 시그널을 배달하는 것이다. 이 플래그가 설정되면 SIGCHLD를 제외한 다른 시그널에는 아무런 영향을 주지 않는다.
매크로 : int SA__ONSTACK
- 만일 어떤 특정한 시그널 번호를 위해서 이 플래그가 설정되면, 시스템은 그런 종류의 시그널이 배달되었을 때 시그널 스택을 사용한다. 21. 9절 [BSD Signal Handling] 참조
매크로 : int SA__RESTART
- 이 플래그는 어떤 기본동작 (open, read, 또는 write와 같은)이 진행되고있는 동안 배달된 시그널을 어떻게 할 것인지를 제어하고, 시그널 핸들러는 정상적으로 반환한다. 두 개의 선택권을 가진다. 라이브러리 함수가 계속될 수 있거나, 또는 에러코드 EINTR을 사용해서 실패를 반환할 수 있다. 그러한 선택은 배달된 특정한 종류의 시그널에 따라서 SA_RESTART에 의해 제어된다. 만일 그 플래그가 설정되면, 핸들러로부터의 반환은 라이브러리 함수를 다시 계속한다. 만일 그 플래그의 설정이 해제되면, 핸들러로부터의 반환은 그 함수가 실패하도록 만든다. 21. 5절 [Interrupted Primitives] 참조.
21. 3. 6 초기 시그널 동작들
새로운 프로세스가 만들어질 때(23. 4절 [Creating a Process] 참조. ), 부모 프로세스로 부터 시그널들의 처리를 상속받는다. 그렇지만, 당신이 exec 함수(23. 5절 [Executing a File] 참조. )를 사용해서 새로운 프로세스 이미지를 로드할 때, 어느 시그널들은 그들의 원래의 동작으로 환원하도록 SIG_DFL을 사용해서 당신 자신의 핸들러를 정의해야만 한다. (만일 당신이 이것에 대해서 조금만 생각한다면, 이것을 이해할 수 있다; 원래의 프로그램으로부터 온 처리 함수들은 그 프로그램을 위한 정의이고, 새로운 프로그램 이미지의 주소 공간에는 심지어 존재하지도 않는다. ) 물론, 새로운 프로그램은 자신의 핸들러를 만들 수 있다.
어떤 프로그램이 쉘에 의해 실행될 때, 쉘은 보통 적당한 SIG_DFL 또는 SIG_IGN을 사용해서 자식 프로세스를 위한 초기동작을 설정한다. 그것은 당신이 새로운 시그널 핸들러를 만들기 전에 쉘이 SIG_IGN으로 초기동작을 준비하지 않은 것이 확실한지 체크하는 것은 좋은 생각이다. 다음은 현재 무시되지 않고 있는 SIGHUP 시그널을 위한 핸들러를 어떻게 만드는지에 대한 예제이다.
- . . .
- struct sigaction temp;
- sigaction (SIGHUP, NULL, &temp);
- if (temp. sa_handler != SIG_IGN)
- {
- temp. sa_handler = handle_sighup;
- sigemptyset (&temp. sa_mask);
- sigaction (SIGHUP, &temp, NULL);
- sigemptyset (&temp. sa_mask);
- }
- struct sigaction temp;
21. 4 시그널 핸들러 정의하기
이 절은 signal 이나 sigaction 함수들을 사용해서 만들 수 있는 시그널 핸들러 함수를 어떻게 쓸것인지에 대해서 설명하고 있다. 시그널 핸들러는 프로그램의 다른 부분과 함께 컴파일하는 함수일 뿐이다. 직접적으로 그 함수를 불러내는 대신에, 시그널이 도착했을 때 그 핸들러를 호출하도록 운영체제에게 알리는 signal 이나 sigaction 함수를 사용한다. 이것이 핸들러를 만드는 것이다. 21. 3절 [Signal Actions] 참조. 다음은 당신이 시그널 핸들러 함수에서 사용할 수 있는 두 개의 기본 범주들이다.
어떤 전역 데이터 구조체에 의해서 도착된 시그널을 기록해두고 그 다음 정상적인 반환을 하는 핸들러 함수를 가질 수 있다.
프로그램을 종료시키거나 그 시그널의 원인이 된 상황으로부터 회복할 수 있는 지점으로 제어를 옮기는 핸들러 함수를 가질 수 있다.
당신이 핸들러 함수를 작성할 때 핸들러 함수는 비동기적으로 호출되어질 수 있기 때문에 각별한 주의가 필요하다. 즉, 핸들러함수는 예측할 수 없이, 프로그램의 어느 지점에서든지 호출되어질 것이다. 만일 매우 짧은 간격을 두고 두 개의 시그널이 도착한다면, 한 개의 핸들러는 다른 핸들러 안에서 실행할 수 있다. 이 절은 당신의 핸들러는 무엇을 하고, 무엇을 피해야 하는지를 설명한다.
21. 4. 1 반환하는 시그널 핸들러
정상적인 반환을 하는 핸들러는 SIGALRM 과 입/출력과 시그널을 사용한 프로세스간 통신과 같은 시그널에 보통 사용되어진다. 그러나 SIGINT를 위한 핸들러는 가능한 시간에 프로그램이 분기하도록 플래그를 설정한 후에 정상적으로 반환한다.
프로그램 에러 시그널을 위한 핸들러로부터 정상적인 반환을 하도록 하는 것은 안전하지 않다, 왜냐하면 핸들러 함수가 반환할 때 프로그램의 동작은 프로그램 에러 후에 무엇을 할 것인지 정의되지 않았기 때문이다. 21. 2. 1절 [Program Erroe Signals] 참조.
정상적으로 반환하는 핸들러들은 어떤 전역 변수를 갱신해야만 한다. 전형적으로, 그 변수는 정상적으로 작동하는 동안에 프로그램에 의해 주기적으로 시험되는 것이다. 데이터타입은 21. 4. 7절 [Atomic Data Access] 에서 설명된 sig_atomic_t가 된다.
다음은 한 개의 프로그램과 같은 간단한 예제이다. 이 프로그램은 SIGALRM 시그널이 도착한 것을 발견할 때까지 루프의 몸체를 실행한다. 이 기술은 시그널이 도착하면 그 루프를 분기하기 전에 어떤 동작을 완성하도록 진행과정에서 상호작용을 허용하기 때문에 유용하다.
- #include <signal. h>
- #include <stdio. h>
- #include <stdlib. h>
- /* 이 플래그는 메인 루프의 종료를 제어한다. */
- volatile sig_atomic_t keep_going = 1;
- /* 시그널 핸들러는 단지 그 플래그를 소거하고 그 자체를 다시 가능하게 한다. */
- void
- catch_alarm (int sig)
- {
- keep_going = 0;
- signal (sig, catch_alarm);
- }
- void
- do_stuff (void)
- {
- puts ("Doing stuff while waiting for alarm. . . . ");
- }
- int
- main (void)
- {
- /* SIGALRM 시그널을 위한 핸들러를 만든다. */
- signal (SIGALRM, catch_alarm);
- /* 잠시동안 알람이 멈추도록 설정한다. */
- alarm (2);
- /* 종료할 때를 알기 위해서 while에서 플래그를 체크한다. */
- while (keep_going)
- do_stuff ();
- return EXIT_SUCCESS;
- signal (SIGALRM, catch_alarm);
- }
- #include <stdio. h>
21. 4. 2 프로세스를 종료시키는 핸들러
프로세스를 종료시키는 핸들러 함수들은 전형적으로 소거 명령이나 프로그램 에러 시그널로부터의 복구와 인터럽트가 원인이 된 곳에 사용된다. 프로세스를 종료하도록 하는 핸들러를 만드는 가장 깨끗한 방법은 핸들러 실행의 첫 번째에서 같은 시그널이 발생되도록 하는 것이다. 다음과 같이.
- volatile sig_atomic_t fatal_error_in_progress = 0;
- void
- fatal_error_signal (int sig)
- {
-
- /* 이 핸들러는 여러 개의 시그널에 대하여 처리하는 것이므로, 어떤 종류의 시그널이 배달될 때마다 반복적으로 핸들러가 불려질 것이다. 그것을 기억하도록 정적변수를 사용하라. */
- if (fatal_error_in_progress)
- raise (sig);
- fatal_error_in_progress = 1;
- /* 이제 동작들을 정리하자 :
- - 터미널 모드를 재설정한다.
- - 자식 프로세스를 kill한다. (어~~ 쌀벌해. . )
- - 록 파일들을 제거하자. */
- . . .
- /* 이제 시그널을 다시 일으키자. 그 동안 시그널은 블록되어져 있었고, 이제 디폴트 처리로써, 프로세스를 종료하도록 하는 그 시그널을 받을 것이다. 우리는 단지 exit 나 abort만을 호출할 수도 있지만, 시그널을 재 발생시키면 프로세스의 정확한 상황으로 반환을 설정한다. */
- raise (sig);
- /* 이 핸들러는 여러 개의 시그널에 대하여 처리하는 것이므로, 어떤 종류의 시그널이 배달될 때마다 반복적으로 핸들러가 불려질 것이다. 그것을 기억하도록 정적변수를 사용하라. */
- }
21. 4. 3 핸들러 안에서 비지역 제어 이동
당신은 setjmp 와 longjmp 기능을 사용해서 시그널 핸들러의 외부로 제어의 비지역 이동을 할 수 있다( 20장 [Non-Local Exits] 참조. ) 핸들러가 비지역 제어 이동을 할 때, 실행 중에 있던 프로그램의 그 부분은 계속되지 않을 것이다. 만일 프로그램의 그 부분이 중요한 데이터 구조체를 갱신 중에 있었다면, 그 데이터 구조체는 여전히 완벽하게 처리되지 못한 상태로 남게될 것이다. 프로그램이 종료되지 않는다면, 위와 같은 문제는 나중에 발견될지도 모른다.
이러한 문제를 피하기 위한 두 개의 방법이 존재한다. 한가지는 중요한 데이터를 갱신하는 프로그램 부분을 위해서는 시그널을 블록하는 것이다. 블록된 시그널은 그 블록이 해제된 후에 배달되어지고, 그때는 이미 중요한 데이터 갱신은 끝난 상태가 된다. 21. 7절 [Blocking Signals] 참조. 다른 방법은 시그널 핸들러 안에서 중요한 데이터의 구조체들을 재-초기화하거나, 그들의 값을 모순이 없도록 만드는 것이다. 다음은 한 개의 전역변수의 재초기화를 보여주는 개략적인 예제이다.
- #include <signal. h>
- #include <setjmp. h>
- jmp_buf return_to_top_level;
- volatile sig_atomic_t waiting_for_input;
- void
- handle_sigint (int signum)
- {
- /* 우리는 시그널이 도착했을 때는 입력을 받기 위해서 기다리겠지만, 제어를 옮길 때는 더 이상 기다리지 않는다. */
- waiting_for_input = 0;
- longjmp (return_to_top_level, 1);
- waiting_for_input = 0;
- }
- int
- main (void)
- {
- . . .
- signal (SIGINT, sigint_handler);
- . . .
- while (1) {
- prepare_for_command ();
- if (setjmp (return_to_top_level) == 0)
- read_and_execute_command();
- if (setjmp (return_to_top_level) == 0)
- }
- signal (SIGINT, sigint_handler);
- }
- /* 이것이 여러 명령문에서 사용되는 서브루틴이라고 생각하자. */
- char *
- read_data ()
- {
- if (input_from_terminal) {
- waiting_for_input = 1;
- . . .
- waiting_for_input = 0;
- . . .
- } else {
- . . .
- }
- waiting_for_input = 1;
- }
- #include <setjmp. h>
21. 4. 4 핸들러가 실행되고 있는 동안 도착한 시그널들
시그널 핸들러 함수가 실행되고 있을 때 도착한 다른 시그널이 있다면 무슨 일이 발생할까? 한 특정한 시그널을 위한 핸들러가 호출되었을 때, 핸들러가 반환할 때까지 그 시그널은 보통 블록된다. 만일 같은 종류의 두 개의 시그널이 서로 가까운 시간에 도착한다면, 두 번째 것은 첫 번째 것이 처리될 때까지 그냥 보유하고 있을 것이다. ( 만일 당신이 이러한 형태의 더 많은 시그널이 도착하도록 허용하기를 원한다면, 핸들러는 sigprocmask를 사용해서 시그널을 명백하게 블록을 해제할 수 있다; 21. 7. 3절 [Process Signal Mask] 참조. )
그렇지만, 당신의 핸들러는 다른 종류의 시그널의 배달에 의해서는 여전히 인터럽트 되어질 수 있다. 이것을 피하기 위해서, 당신은 sigaction에 인수로써 사용하는 action 구조체의 sa_mask 멤버를 사용해서 핸들러가 실행되는 동안 블록되어질 시그널을 명백하게 지정할 수 있다. 그들 시그널은 호출 된 핸들러를 위한 시그널에 더해져 있고, 다른 시그널들은 보통 프로세스에 의해서 블록되어진다. 21. 7. 5절 [Blocking for Handler] 참조.
이식성 노트 : 만일 당신의 프로그램이 완전히 System V Unix상에서 작업하기를 원할 때, 비동기적인 발생이 예상되는 시그널을 위한 핸들러를 만들려면 항상 sigaction을 사용하라. 다른 시스템에서는, 핸들러에서 하는 시그널의 처리는 SIG_DFL로 시그널이 가진 원래의 동작으로 되돌려지도록 만들어져있고, 핸들러는 실행될 때마다 그 자체를 다시 만들어야만 한다. 이 것은 실제로 시그널이 연속적으로 도착할 수 없을 때 작업하기는 불편하다. 하지만 다른 시그널이 즉시 도착할 수 있다면, 그것은 다시 핸들러를 다시 정하지 않아도 된다. 그러면 두 번째 시그널은 프로세스를 종료시키는, 디폴트 처리로 받게될 것이다.
21. 4. 5 한가지로 합병한 서로 밀접한 시그널들
만일 당신의 시그널 핸들러가 전혀 호출될만한 기회도 갖기 전에, 당신의 프로세스에 같은 종류의 시그널이 여러 개 배달되었다면, 그 핸들러는 오직 한 개의 시그널이 도착한 것처럼 호출되어질 것이다. 실제로, 그 시그널들은 한 개로 합병한다. 이 상황은 시그널이 블록되었을 때나, 또는 멀티프로세싱 환경에서 시그널이 도착했는데 시스템이 다른 프로세스의 실행 때문에 바쁠 때 발생할 수 있다. 이것이 의미하는 것은, 예를 들어, 당신은 발생한 시그널의 개수를 세는 시그널 핸들러의 사용을 신뢰할 수 없다. 오로지 당신이 구분할 수 있는 것은 과거의 주어진 시간동안에 적어도 한 개의 시그널이 도착했는지, 또는 도착하지 않았는지를 구분하는 것만을 신뢰할 수 있다. 다음은 자식 프로세스가 발생시킨 SIGCHLD의 개수와는 같지 않을지도 모르는 SIGCHLD 시그널의 개수를 실제처럼 대치하는 핸들러의 예제이다. 그것은 프로그램이 다음처럼 구조체를 연결하여 자식 프로세스의 모두를 추적하고 있다고 가정한다.
- structprocess
- {
- struct process *next;
- /* 자식 프로세스의 프로세스 ID */
- int pid;
- /* 자식 프로세스가 출력을 하는 파이프나 가상 터미널의 기술자 */
- int input_descriptor;
- /* 만일 이 프로세스가 멈추거나 종료된다면 0이 아니다. */
- sig_atomic_t have_status;
- /* 자식 프로세스의 상황; 실행중이면 0, 그렇지 않으면 그 status는 waitpid로부터의 값이다. */
- int status;
- /* 자식 프로세스의 프로세스 ID */
- };
- struct process *process_list;
- 다음 예제는 과거의 어떤 시간동안에 시그널이 도착했는지를 지적하는 플래그를 사용한다_그때마다 프로그램은 마지막에 그 플래그를 0으로 소거한다.
- /* 0이 아닌 값은 process_list의 항목에서 상황이 변환된 자식 프로세스를 발견했음을 의미한다. */
- int process_status_change;
- 다음은 핸들러 자체이다.
- void sigchld_handler (int signo)
- {
- int old_errno = errno;
- while (1) {
- register int pid;
- int w;
- struct process *p;
- int w;
- /* 정해진 결과를 얻을 때까지 상황을 물어보아라 */
- do{
- errno = 0;
- pid = waitpid (WAIT_ANY, &w, WNOHANG |
- do{
- WUNTRACED);
- }while (pid <= 0 && errno == EINTR);
- if (pid <= 0 ) {
- /* 실패의 실제 의미는 더 이상 멈추거나 종료될 자식프로세스가 없음을 의미하고, 그래서 반환한다. */
- errno = old_errno;
- return;
- }
- /* 우리에게 신호를 보냈던 프로세스를 찾아서, 그 상황을 기록하라 */
- for (p = process_list; p; p = p->next)
- if (p->pid == pid) {
- p->status = w;
- /* 보려하는 데이터를 가진 상황 필드를 지적하라. 우리는 그것이 저장된 후에 이것을 한다. */
- p->have_status = 1;
- p->status = w;
- /* 만일 프로세스가 종료되었다면, 출력을 위한 기다림을 멈추어라 */
- if (WIFSIGNALED (w) || WIFEXITED (w)) {
- if (p->input_descriptor)
- FD_CLR (p->input_descriptor, &input_wait_mask);
- /* 프로그램은 process_list 안에 어떤 새로운 것이 있는지 알아보기 위해서 주어진 시간동안에 이 플래그를 체크할 것이다. */
- ++process_status_change;
- }
- if (WIFSIGNALED (w) || WIFEXITED (w)) {
- }
- /* 우리에게 말할 무언가를 가진 모든 프로세스들을 처리하도록 루프를 돌려라 */
- }
- }
- 다음은 process_status_change 플래그를 체크하기 위한 적당한 방법이다.
- if (process_status_change) {
- struct process *p;
- process_status_change = 0;
- for (p = process_list; p; p = p->next)
- if (p->have_status) {
- . . . Examine p->status . . .
- }
- if (p->have_status) {
- }
- {
리스트를 시험하기 전에 플래그를 소거하는 것은 치명적이다; 그렇지 않고, 만일 플래그가 소거되기 전과, 프로세스 리스트의 적당한 요소가 체크된 후에 시그널이 배달된다면, 다음 도착한 시그널이 다시 그 플래그를 설정하기 전까지는 그 상황변화를 알아차릴 수 없다. 당신은 물론 그 리스트를 조사하는 동안 시그널을 블록함으로써 이러한 문제를 피할 수는 있지만, 올바른 순서로 일들을 처리하는 것이 정확함을 보증하기에는 좀더 좋은 방법이다.
프로세스 상황을 체크하는 루프가 그 상황이 유용하게 저장되어졌음이 확인될 때까지 p->status를 조사하는 것을 피한다. 이것은 status가 억세스 되고 있는 도중에 변화될 수 없음을 확실하게 한다. 일단 p->have_status가 설정되면, 그것은 자식 프로세스가 멈추거나 종료했음을 의미하고, 그 어느 경우에도, 프로그램이 주목하고 있는 동안에 다시 멈추거나 종료할 수 없다. 변수를 억세스하고 있는 동안에 인터럽션(interruptions)을 모방하기에 대한 상세한 정보는 21. 4. 7. 3절 [Atomic Usage] 를 참조하라.
다음은 당신이 체크했던 마지막 시간이후 핸들러가 실행되었는지를 시험 할 수 있는 다른 방법이다. 이 기술은 핸들러의 외부에서 결코 변화되지 않을 카운터로 사용한다. 빈도수(count)를 소거하는 대신에, 프로그램은 전의 값을 기억하고 있다가 그 이후에 그 값이 변화되었는지를 보여준다. 이 방법이 유리한 점은 프로그램의 다른 부분들을 독립적으로 체크할 수 있다는 것으로, 각각의 부분은 그 부분을 마지막으로 체크한 이후에 시그널이 있었는지를 체크한다.
- sig_atomic_t process_status_change;
- sig_atomic_t last_process_status_change;
- . . .
- {
- sig_atomic_t prev = last_process_status_change;
- last_process_status_change = process_status_change;
- if (last_process_status_change != prev) {
- struct process *p;
- for (p = process_list; p; p = p->next)
- if (p->have_status) {
- . . . Examine p->status . . .
- }
- for (p = process_list; p; p = p->next)
- }
- last_process_status_change = process_status_change;
- }
21. 4. 6 시그널 핸들링 과 재진입 불가 함수들
** 역자주 : 재진입(reentrant) : 어떤 모듈을 재진입이 가능하다라고 할 때, 그것은 동시에 두 개 이상의 프로그램에 의하여 공유될 수 있는 모듈을 말한다. 이러한 모듈은 실행 중에 자신의 코드 또는 데이터 영역을 변경시키지 않아야 합니다.핸들러 함수들은 보통 많은 일을 하지는 않는다. 핸들러 함수에게는 프로그램이 정기적으로 체크하는 외부변수를 설정하는 일 이외에는 아무 것도 하지 않게 하고, 다른 중요한 일들은 프로그램에게 맡기는 것이 좋다. 이것은 핸들러가 예측할 수 없는 시간에_시스템 호출도중, 또는 다중 명령을 요구하는 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)이 요구됨을 명심하라.
메모리 오브젝트에 기록하기는 핸들러가 실행되고 있는 순간일지라도 안전하고, 어느 것도 방해되지 않을 것이다.
21. 4. 7 원소 데이터 억세스와 시그널 핸들링
당신의 어플리케이션에서 데이터가 원자와 관계가 있던지, 또는 단순한 텍스트이던지, 당신은 원자화가 필요 없는 단일한 데이터를 억세스 하는 요소에 대해서 주의를 해야만 한다. 이것은 단일한 오브젝트를 읽거나 쓰기 위해서는 여러 개의 명령이 필요할 수 있다는 것을 의미한다. 그와같은 경우에, 시그널 핸들러는 오브젝트의 읽기나 쓰기 중간에 실행될 수 있다.
이러한 문제를 커버할 수 있는 세 가지 방법이 있다. 당신은 항상 원자 단위로 억세스되는 데이터 타입을 사용할 수 있다; 억세스를 인터럽트 하여 아무런 부적당한 일이 일어나지 않게 하거나, 또는 인터럽트보다는 좋지는 않지만 억세스동안에 모든 시그널들을 블록하는 등 당신은 주의 깊은 조정을 할 수가 있다.
21. 4. 7. 1 비-원자 억세스가 갖는 문제점
다음은 변수를 갱신하는 도중에 시그널 핸들러를 실행하면 무슨 일이 발생하는지를 보여주는 예제이다. (변수 읽기를 인터럽트 하는 것도 역설적인 결과에 이르게 할 수 있지만, 여기서 우리는 쓰기를 보여준다. )
- #include <signal. h>
- #include <stdio. h>
- struct two_words { int a, b; } memory;
- void
- handler(int signum)
- {
- printf ("%d, %d\n", memory. a, memory. b);
- alarm (1);
- }
- int
- main (void)
- {
- static struct two_words zeros = { 0, 0 }, ones = { 1, 1 };
- signal (SIGALRM, handler);
- memory = zeros;
- alarm (1);
- while (1)
- {
- memory = zeros;
- memory = ones;
- }
- signal (SIGALRM, handler);
- }
- #include <stdio. h>
이 프로그램은 계속 번갈아 가면서 0, 1, 0, 1 로 메모리를 채운다; 그 동안, 일초마다, 알람 시그널 핸들러는 현재의 내용을 프린트한다. ( 핸들러 안에서 printf의 호출은 시그널이 발생했을 때 핸들러외부에서 printf가 확실히 호출되어지지 않을 것이므로 이 프로그램은 안전하다. ) 분명히, 이 프로그램은 0 한 쌍과 1 한 쌍을 프린트 할 수 있다. 하지만 그것이 그 프로그램이 할 수 있는 전부가 아니다! 대부분의 기계에서, 메모리에 새로운 값을 저장하기 위해서는 여러 개의 명령을 취하고, 그 값은 동시에 한 워드(word)에 저장된다. 만일 시그널이 그 명령들 사이에 배달된다면, 핸들러는 memory. a는 0이고 memory. b는 1인걸 발견할지 모른다(또는 그의 반대).
한 개의 명령으로 메모리 안에 한 개의 새로운 값을 저장할 수 있는 어떤 기계에서는 인터럽트 될 수 없다. 그 기계들에서, 핸들러는 항상 두 개의 0과 두 개의 1을 프린트 할 것이다.
21. 4. 7. 2 원자 형
변수를 억세스할 때 인터럽트 하는 것에 대한 불확실성을 피하기 위해서, 당신은 항상 원자단위로 억세스를 하는 특별한 데이터 타입을 사용할 수 있다: sig_atomic_t. 이 데이터타입을 읽기와 쓰기는 단일한 명령으로 발생한다는 것이 보증되므로 핸들러가 억세스의 "중간에" 실행될 방법이 없는 것이다.
sig_atomic_t 타입은 항상 정수 데이터 타입이지만, 그 데이터 타입이 몇 개의 비트로 구성되어있는지는 한가지로 정해진 것이 아니라 각각의 기계마다 다양하다.
데이터 타입 : sig__atomic__t
- 이것은 정수 데이터 타입이다. 이 타입의 오브젝트는 항상 자동적으로 억세스 된다.
- 실제로, 당신은 int와 int보다는 길지 않은 다른 정수형을 원소단위라고 가정할 수 있다. 당신은 또한 포인터형을 원소단위로 가정할 수 있다; 그것은 매우 편리하다. GNU C 라이브러리를 지원하는 모든 기계와 모든 POSIX 시스템상에서 그 두 개의 가정은 모두 사실이다.
21. 4. 7. 3 원소단위 사용 형태.
억세스의 어떤 형태는 억세스가 인터럽트 되는 것과 같은 문제들을 피한다. 예를 들어, 핸들러에 의해 설정되고, 때때로 메인 프로그램에 의해서 소거되고 테스트되는 어떤 플래그를 억세스 하는데 실제로 두 개의 명령(instructions)이 필요하다고 할지라도 항상 안전하다. 이것이 그렇게 보이도록 하기 위해서, 우리는 인터럽트 되어질 수 있는 모든 억세스를 고려해야만 하고, 인터럽트 되면 아무런 문제가 없음을 보여야 한다. 플래그를 테스트하는 도중에 발생한 인터럽트는 아무런 문제가 없는 정확한 값인 경우에, 0이 아닌 값으로 인식이 되거나 또는 테스트된 다음에 0이 아닌 값으로 되어질 것이기 때문에 아무런 문제가 없다.
플래그를 소거하는 도중에 인터럽트도 아무런 문제가 없는데, 플래그가 소거되기 전에 시그널이 발생한 것은, 그 값이 0으로 끝나거나, 아니면 0이 아닌 값으로 끝나고, 플래그가 소거된 후에 시그널이 발생한 것처럼 연속적인 사건들이 발생한다. 그 두 개의 경우 모두 코드가 처리되기만 하면, 플래그를 소거하는 도중에 발생한 시그널 또한 처리 할 수 있다. ( 이것은 비-원소단위의 사용이 언제 안전할 수 있는지를 당신에게 설명하기 위한 예제이다. )
때때로 당신은 다른 오브젝트를 사용해서 어떤 오브젝트의 사용을 막음으로써 그 오브젝트에 인터럽트 되지 않는 억세스를 보증할 수 있다, 그것의 형태는 원소단위가 확실할 것이다. 21. 4. 5절 [Merged Signals] 에서 예제참조.
21. 5 시그널에 의해 인터럽트된 기본동작 ( Primitives )
open 이나 read가 입/출력 디바이스에서 기다리는 것과 같은 입/출력 기본동작 동안에 시그널이 발생할 수도 있고 처리될 수도 있다. 만일 시그널 핸들러가 반환하면, 그 시스템은 의문을 갖는다: 다음에 무슨 일이 발생하지?
POSIX는 한가지 접근법을 정한다: 즉시 그 기본동작을 실패로 만든다. 이러한 종류의 실패를 위한 에러코드는 EINTR이다. 이것은 유연하지만, 보통은 불편하다. 전형적으로, POSIX 어플리케이션은 그 호출을 다시 할 목적으로 라이브러리 함수가 반환했을 때 EINTR인지 체크하는 시그널 핸들러를 사용한다. 종종 프로그래머들은 체크하는 것을 잊는다.
GNU 라이브러리는 매크로 TEMP_FAILURE_RETRY를 사용해서, 임시적인 실패 후에 다시 호출을 시도하도록 하는 편리한 방법을 제공한다.
매크로 : TEMP__FAILURE__RETRY (expression)
- 이 매크로는 일단 expression을 평가한다. 만일 그것이 실패이면 에러코드 EINTR을 보고하고, 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) : 중간에 어떤 이유들로 인해서 중단이 발생하지 않고, 한 묶음의 데이터를 한꺼번에 전달하는 방법을 의미함.
21. 6 시그널 발생시키기
하드웨어 트랩이나 인터럽트의 결과로서 발생되는 시그널을 제외하고, 당신의 프로그램에서 프로세스, 또는 다른 프로세스에게 명시적으로 시그널을 보낼 수 있다.
21. 6. 1 스스로에게 신호 보내기
프로세스는 raise 함수를 통해서 시그널을 스스로에게 보낼 수 있다. 이 함수는 `signal. h'에 선언되어 있다.
함수 : int raise (int signum)
- raise 함수는 호출한 프로세스에게 시그널 signum을 보낸다. 성공하면 0을 반환하고 실패하면 0이 아닌 값을 반환한다. 실패하게 되는 유일한 이유는 signum의 값이 무효한 경우이다.
함수 : int gsignal (int signum)
- gsignal 함수는 raise와 같은 일을 하지만 SVID와의 호환성을 위해서 제공된다.
raise 사용으로 한가지 편리한 점은 당신이 트랩 했던 시그널의 디폴트 동작을 재생할 수 있다는 것이다. 이를테면, 당신의 프로그램을 사용하는 사용자가 stop 시그널(SIGTSTP)을 보내기 위해서 SUSP 문자를 타이핑할 때, 당신은 멈추기 전에 어떤 내부적 데이터 버퍼들을 소거하기를 원한다고 가정하자. 당신은 다음처럼 이것을 설정할 수 있을 것이다.
- #include <signal. h>
- /* stop 시그널이 도착했을 때, 원래의 디폴트로 동작을 설정하고 소거 동작을 한 후에 그 시그널을 다시 보낸다. */
- void
- tstp_handler (int sig)
- {
- signal (SIGTSTP, SIG_DFL);
- /* 여기서 소거 동작을 하라 */
- . . .
- raise (SIGTSTP);
- /* 여기서 소거 동작을 하라 */
- }
- /* 프로세스가 다시 계속될 때, 시그널 핸들러를 반환하라. */
- void
- cont_handler (int sig)
- {
- signal (SIGCONT, cont_handler);
- signal (SIGTSTP, tstp_handler);
- }
- /* 프로그램을 초기화하는 동안에 양(both) 핸들러를 가능하게 하라 */
- int
- main (void)
- {
- signal (SIGCONT, cont_handler);
- signal (SIGTSTP, tstp_handler);
- . . .
- signal (SIGTSTP, tstp_handler);
- }
21. 6. 2 다른 프로세스에게 시그널 보내기
kill 함수는 다른 프로세스에게 시그널을 보내기 위해서 사용될 수 있다. 함수의 명칭이 악의적이지만, 그것은 다른 프로세스를 종료시키는데 사용하기보다는 더 많은 것들을 위해서 사용될 수 있다. 다음의 경우, 당신이 프로세스들 사이에 시그널들을 보내기 원할 때 사용할 수 있다.
부모 프로세스가 작업을 수행하기 위해서 자식 프로세스를 시작한다. 아마도 자식 프로세스는 한정된 루프를 돌 것이고_자식 프로세스가 작업에서 더 이상 필요치 않을 때 종료한다.
한 프로세스가 그룹(group)의 일부로써 실행될 때, 에러나 다른 사건이 발생하면 그룹에 있는 다른 프로세스에게 신고하거나 종료할 필요가 있다.
두 개의 프로세스가 서로 작업하는 동안 동기(synchronize)할 필요가 있다.
이 절은 당신이 프로세스가 어떻게 작업하는지에 대해서 조금이나마 알 것이라고 가정한다. 이 주제에 대한 자세한 정보는 23장 [Child Process] 에 나와있다. kill 함수는 `signal. h'에 선언되어 있다.
함수 : int kill (pid_t pid, int signum)
- kill 함수는 pid로 정해진 프로세스나 프로세스 그룹에게 시그널 signum을 보낸다. 21. 2절 [Standard Signals] 에 나와있는 시그널들뿐만 아니라, signum은 pid의 유효성을 체크하기 위해서 0의 값을 가질 수 있다. pid는 시그널을 받기 위한 프로세스나 프로세스 그룹을 지정한다.
pid > 0
- pid는 프로세스이다.
pid == 0
- 같은 프로세스 그룹에 있는 모든 프로세스에게 시그널을 보내지만, 시그널을 보내는 프로세스 자체는 그 시그널을 받지 않는다.
pid < -1
- -pid는 프로세스 그룹이다.
pid == -1
- 만일 그 프로세스에게 특권이 부여되어 있다면, 어떤 특별한 시스템 프로세스들을 제외한 모든 프로세스들에게 시그널을 보낸다. 그렇지 않다면, 같은 사용자 ID를 가진 모든 프로세스에게 시그널을 보낸다.
- kill (getpid(), signum) 과 같은 호출로 프로세스는 자신에게 시그널 signum을 보낼 수 있다. 만일 kill이 자신에게 시그널을 보내도록 어떤 프로세스에 의해 사용되고, 그 시그널이 블록되지 않는다면, kill은 반환하기 전에 프로세스에게 적어도 한 개의 시그널( 시그널 signum 대신에 아직 미해결 상태로 남아있는 블록되지 않은 다른 시그널이 될 수도 있다. )을 배달한다.
- kill로 부터의 반환값은, 만일 성공적으로 시그널이 보내질 수 있다면 0을 반환한다. 그렇지 않고, 아무런 시그널도 보낼 수 없다면 -1을 반환한다. 만일 pid를 여러 개의 프로세스에게 시그널을 보내도록 지정한다면, 그때 만일 kill이 그들에게 적어도 한 개의 시그널을 보낼 수 있다면 성공한 것이다. 당신이 그들 모두에게 시그널이 갔는지 또는 어떤 프로세스가 시그널을 얻었는지를 알 수 있는 방법은 없다.
다음의 errno는 이 함수를 위해 정의된 에러상황이다.
EINVAL
- signum 인수가 무효하거나 그 숫자를 지원하지 않는 시그널 번호를 사용했다.
EPERM
- 당신은 pid에 의해 지정된 프로세스나 프로세스 그룹 안의 어떤 프로세스들에게 시그널을 보내기 위한 특권을 갖고 있지 않다.
ESCRH pid
- 인수가 현존하고 있는 프로세스나 그룹으로 지정되지 않았다.
함수 : int killpg (int pgid, int signum)
- 이것은 kill과 유사하지만, 프로세스 그룹에게는 시그널을 보낼 수 없다. 이 함수는 BSD와의 호환성을 위해서 제공되었다; 이식성을 위해서는 kill을 사용하라.
- kill 사용의 간단한 예로서, kill(getpid(), sig)은 raise(sig)와 같은 효과를 갖는다.
21. 6. 3 kill을 사용하기 위한 허가
어느 임의의 프로세스에게 시그널을 보내기 위해서 kill을 사용하는 것을 방지하기 위한 제한이 있다. 그것은 다른 사용자에게 소속되어 있는 프로세스를 제멋대로 죽이는 것과 같은 반사회적인 행동을 방지하기 위한 의도가 있다. kill을 자식과 부모 프로세스사이에 시그널을 주고 받기 위해 사용하는것과 같은 상황에서는, 보통 당신은 시그널을 보내기 위한 허가권을 갖고 있다. 그러나 자식 프로세스에서 setuid 프로그램이 실행될 때는 유일하게 제외된다; 만일 프로그램이 실제 UID를 유효 UID로 변경한다면, 당신은 시그널을 보내기 위한 허가권을 가지지 않을 수도 있다. su 프로그램을 이런 일을 한다.
프로세스가 다른 프로세스에게 시그널을 보내기 위한 허가권을 가지고 있는지 없는지의 여부는 두 개의 프로세스의 사용자 ID들에 의해 결정된다. 이 원칙은 25. 2절 [Process Personal] 에 자세하게 논의되고 있다.
일반적으로, 어떤 프로세스가 다른 프로세스에게 시그널을 보낼 수 있기 위해서는, 시그널을 보내는 프로세스가 특권이 부여된 사용자(`root'처럼)이거나 시그널을 보내는 프로세스의 실제 또는 유효 사용자 ID가 시그널을 받는 프로세스의 실제 또는 유효 사용자 ID와 매치되어야만 한다. 만일 시그널을 받는 프로세스가 프로세스 이미지 파일에서 set-user-ID 모드를 통해 유효 사용자 ID를 변경했다면, 프로세스 이미지 파일의 소유자가 현재 유효 사용자 ID 대신에 사용된다. 어떤 경우에, 만일 사용자 ID들이 매치되지 않는다 할지라도 부모 프로세스가 자식 프로세스에게 시그널을 보내는 것이 가능하고, 다른 경우에는 다른 제한들이 강요 될 것이다. SIGCONT 시그널은 특별한 경우이다. 그것은 만일 시그널을 보내는 쪽이 시그널을 받는 쪽과 같은 세션에 있다면, 사용자 ID들에 상관없이 시그널을 보낼 수 있다.
21. 6. 4 통신을 위해서 kill을 사용하기
다음은 프로세스간 통신을 위해서 어떻게 시그널들을 사용할 수 있는지 보여주는 조금 긴 예제 프로그램이다. SIGUSR1 과 SIGUSR2가 프로세스 간 통신을 지원하지 위하여 제공된 것이다. 그 시그널들은 기본적으로 치명적이기 때문에, 그 시그널들을 받을 것으로 가정되는 프로세스는 signal 이나 sigaction을 통해서 그들을 트랩 해야만 한다.
다음의 예제는, 부모 프로세스가 fork로 자식 프로세스를 생성한 다음 자식 프로세스가 초기화를 수행할 때까지 기다린다. 자식 프로세스는 준비가 되었음을 알리기위해서, kill 함수를 사용해서 SIGUSR1 시그널을 보낸다.
- #include <signal. h>
- #include <stdio. h>
- #include <sys/types. h>
- #include <unistd. h>
- /* SIGUSR1 시그널이 도착할 때, 이 변수를 설정하라 */
- volatile sig_atomic_t usr_interrupt = 0;
- void
- synch_signal (int sig)
- {
- usr_interrupt = 1;
- }
- /* 자식 프로세스가 이 함수를 실행한다. */
- void
- child_function (void)
- {
- /* 초기화를 수행하라 */
- printf ("I'm here!!! My pid is %d. \n", (int) getpid ());
- /* 초기화를 수행했음을 부모에게 알리자 */
- kill (getppid (), SIGUSR1);
- /* 실행을 계속한다 */
- puts ("Bye, now. . . . ");
- exit (0);
- printf ("I'm here!!! My pid is %d. \n", (int) getpid ());
- }
- int
- main (void)
- {
- struct sigaction usr_action;
- sigset_t block_mask;
- pid_t child_id;
- /* 시그널 핸들러를 만들어라. */
- sigfillset (&block_mask);
- usr_action. sa_handler = synch_signal;
- usr_action. sa_mask = block_mask;
- usr_action. sa_flags = 0;
- sigaction (SIGUSR1, &usr_action, NULL);
- /* 자식 프로세스를 만들어라 */
- child_id = fork ();
- if (child_id == 0)
- child_function (); /* 반환하지 말아라. */
- /* 자식 프로세스가 시그널을 보낼 때까지 기다리자. */
- while (!usr_interrupt)
- ;
- /* 이제 실행을 계속한다. */
- puts ("That's all, folks!");
- return 0;
- sigset_t block_mask;
- }
- #include <stdio. h>
위의 예제는 busy wait(적당한 말이 없어서. . )을 사용하는데, 그것은 다른 프로그램에서 사용할 수 있도록 CPU 사이클을 기다려야하기 때문에 좋지 않다. 시그널이 도착할 때까지 기다리도록 시스템에게 부탁하는 것이 더 좋다. 21. 8절 [Waiting for a signal] 에 있는 예제를 참조하라.
21. 7 시그널 블록하기
시그널 블록하기는 운영체제에게 그 시그널을 붙잡아서 나중에 배달하도록 알리는 것을 의미한다. 일반적으로, 프로그램에서는 SIG_IGN을 사용해서, 시그널의 동작을 무시하는 것으로 설정할 망정, 불명확하게 시그널들을 블록하지 않는다. 하지만 시그널 블록킹(blocking)은 민감한 오퍼레이션들이 인터럽트 되는 것을 막기 위해서 시그널들을 블록하는데 사용된다.
시그널들 때문에 핸들러에 의해 수정되었던 전역 변수들을 갱신하는 동안 시그널들을 블록하기 위해서 sigprocmask 함수를 사용 할 수 있다.
특정한 핸들러가 실행되는 동안 어떤 시그널들을 블록하도록 sigaction 함수호출에서 sa_mask를 설정할 수 있다. 이 방법으로, 시그널 핸들러는 시그널들에 의해서 그 자체가 인터럽트 됨이 없이 실행될 수 있다.
21. 7. 1 왜 시그널 블록킹 ( Blocking ) 이 유용한가
sigprocmask을 사용해서 임시적으로 시그널 블록하기는 당신의 프로그램에서 임계부분(critical parts)이 실행되는 동안에 발생할지도 모를 인터럽트를 막기 위한 방법으로 제공된다. 만일 시그널들이 프로그램의 그 부분(critical parts)에 도달한다면, 당신이 그들의 블록을 해제한 후에, 나중에 배달 되어진다. 이것의 유용한 사용예는 프로그램의 나머지와 시그널 핸들러 사이에 데이터를 분배하는데 사용하는 것이다. 만일 데이터의 타입이 sig_atomic_t( 21. 4. 7절 [Atomic Data Access] 참조. )가 아니라면 시그널 핸들러는 프로그램의 나머지가 데이터의 읽기와 쓰기를 완전히 끝냈을 때 실행될 수 있다. 이것은 혼란된 결과를 초래할 것이다.
신뢰 가능한 프로그램을 만들기 위해서, 프로그램의 나머지가 데이터를 시험하거나 갱신하는 동안에 시그널 핸들러가 실행되는 것을 막을 수 있다_프로그램의 나머지가 실행되는 동안 발생할 여지가 있으며, 그 데이터를 건드릴 위험이 있는 적당한 시그널들을 블록함으로 해서. 만일 어떤 시그널이 도착하지 않았을 때, 당신이 어떤 동작을 수행하기를 바란다면 시그널 블록킹은 필요하다. 그 시그널을 위한 핸들러가 sig_atomic_t 타입의 플래그를 설정한다고 가정하다; 당신은 그 플래그를 시험하고 만일 그 플래그가 설정되지 않았다면 어떤 동작을 수행하도록 하고 싶어한다. 하지만 이것은 신뢰할 수 없다. 만일 그 시그널이 아직 중요한 동작은 수행하기 전이고, 플래그는 테스트 한 직후에 시그널이 배달된다고 가정하면, 그 프로그램은 시그널이 도착할지라도 그 동작을 수행 할 것이다.
어떤 시그널이 도착했는지의 여부를 확인하는 유일한 신뢰 가능한 방법은 시그널이 블록되어 있을 동안 테스트하는 것이다.
21. 7. 2 시그널 설정
시그널을 블록 킹하는 함수들 모두는 무슨 시그널들이 영향을 받게되는지를 정하는 데이터 구조체를 사용한다. 그리고, 두 개의 단계, 즉, 시그널을 만들기와 시그널을 라이브러리 함수에 인수로써 사용하기를 포함한다.
그들은 헤더파일 `signal. h'에 선언되어 있다.
데이터 타입 : sigset__t
- sigset_t 데이터 타입은 시그널 셋(set)을 표현하기 위해서 사용된다. 내부적으로, 정수나 구조체형으로 이행될 것이다. 이식성을 위해서, sigset_t 오브젝트에서 정보를 초기화하고, 변경하고, 추출하는 역할을 하는, 이 절에서 설명된 함수들을 사용하라_그들을 직접적으로 다루려고 시도하지 마라.
시그널 셋(set)을 초기화하기 위한 두 가지 방법이 있다. 하나는 처음에 sigemptyset을 사용하여 비어있게 해놓은 다음, 개별적으로 시그널을 하나씩 더한다. 아니면, sigfillset을 사용하여 완전히 채운다음, 개별적으로 정해진 시그널들을 하나씩 지운다.
당신이 어떤 식으로든 그것을 사용하기 전에 그 두 개의 함수중 하나로써 시그널 셋(set)을 초기화해야만 한다. 모든 시그널들을 명시적으로 설정하려 시도하지 말아라, 왜냐하면, sigset_t 오브젝트는 초기화될 필요가 있는 어떤 다른 정보(버전 필드와 같은)를 포함하고 있을 것이기 때문이다. (더하자면, 당신이 알고 있는 것 외의 시그널은, 시스템이 발생시키지 않을 것이라는 가정을 당신의 프로그램 안에서 하는 것은 현명하지 못하다. )
함수 : int sigemptyset (sigset_t *set)
- 이 함수는 정의된 모든 시그널을 포함하도록 시그널 셋(set)을 set으로 초기화한다. 이 함수는 항상 0을 반환한다.
함수 : int sigfillset (sigset_t *set)
- 이 함수는 정의된 모든 시그널을 포함하도록 set으로 시그널 셋을 정한다. 반환값은 0이다.
함수 : int sigaddset (sigset_t *set, int signum)
- 이 함수는 시그널 셋에 시그널 signum을 더한다. sigaddset이 하는 모든 것은 셋(set)을 갱신하는 것이다; 어느 시그널을 블록하지 않거나 또는 블록을 해제하지 않거나 한다. 반환값은 성공하면 0이고 실패하면 -1이다.
- 다음의 errno는 이 함수를 위해 정의된 에러 상황이다.
EINVAL : signum 인수로 무효한 시그널을 지정하였다.
- 다음의 errno는 이 함수를 위해 정의된 에러 상황이다.
함수 : int sigdelset (sigset_t *set, int signum)
- 이 함수는 시그널 셋 set으로부터 시그널 signum을 제거한다. sigdelset이 하는 모든 것은 셋(set)을 갱신하는 것이다; 그것은 시그널을 블록하지 않거나 블록을 해제하지 않거나 한다. 반환값과 에러상황은 sigaddset과 같다.
마지막으로, 시그널 셋(set)안에 어떤 시그널들이 있는지를 테스트하기 위한 함수의 설명이다.
함수 : int sigismember (const sigset_t *set, int signum)
- sigismember 함수는 시그널 signum이 시그널 집합 set의 멤버인지의 여부를 테스트하는 함수이다. 만일 그 시그널이 집합 안에 있으면 1을 반환하고, 그렇지 않으면 0을 반환하고, 에러가 발생하면 -1을 반환한다. 다음의 errno는 이 함수를 위해 정의된 에러상황이다.
- EINVAL signum 인수에 무효한 시그널이 지정되었다.
21. 7. 3 프로세스 시그널 마스크
현재 블록되어 있는 시그널들의 모음(collection)을 시그널 마스크라고 부른다. 각 프로세스는 자신 소유의 시그널 마스크를 갖고 있다. 당신이 새로운 프로세스를 만들 때(23. 4절 [Creating a Process] 참조) 그것은 부모의 마스크를 상속받는다. 당신은 시그널 마스크를 갱신하여 유연성 있게 시그널들을 블록하거나 해제할 수 있다.
sigprocmask 함수의 프로토타입은 `signal. h'에 있다.
함수 : int sigprocmask (int how, const sigset_t *set, sigset_t *oldset)
- sigprocmask 함수는 호출된 프로세스의 시그널 마스크를 시험하거나 갱신하는데 사용된다. how 인수는 시그널 마스크를 어떻게 변경할지 정하는 인수로써, 다음 값들 중에 하나를 사용해야만 한다.
- SIG_BLOCK set에 있는 시그널들을 블록하라. 현존하는 마스크에 그들을 더하라. 즉, 새로운 마스크는 현존하는 마스크와 set의 합집합이다.
- SIG_UNBLOCK set에 있는 시그널들의 블록을 해제하라. 현존하는 마스크에서 그들을 제거하라.
- SIG_SETMASK 마스크로 set을 사용하라; 마스크의 전의 값은 무시하라.
- 마지막 인수 oldset은 예전 프로세스 시그널 마스크에 대한 정보를 반환하는데 사용된다. 만일 당신이 예전 프로세스 시그널 마스크에 대한 정보를 살펴보지 않고 마스크를 변경하기 원한다면, oldset 인수에 널 포인터를 사용하면 된다. 유사하게, 만일 당신이 현존하는 마스크를 변경하지 않고, 단지 마스크 안에 무엇이 있는지 알기를 원한다면, set 인수에 널 포인터를 사용하면 된다. ( 이 경우, how 인수는 아무런 의미가 없다. )
- oldset 인수는 나중에 예전 프로세스 시그널 마스크를 반환하기 위해 그 시그널 마스크를 기억하는데 종종 사용된다. ( fork 와 exec의 호출로 시그널 마스크가 상속된 후, 당신은 당신의 프로그램의 실행을 시작할 때 그 안에 무슨 내용이 있는지 예언할 수 없다. )
- 호출된 sigprocmask가 어느 미해결인 상태의 시그널의 블록을 해제하게 된다면, 그들 시그널들 중 적어도 하나는 sigprocmask가 반환하기 전에 프로세스에게 배달된다. 배달된 미해결 시그널의 순서는 정해지지 않았지만, 당신이 한꺼번에 여러 개의 시그널들의 블록을 해제하도록 다중의 sigprocmask을 사용함으로써 명시적으로 순서를 제어할 수 있다.
- sigprocmask함수는 성공하면 0을 반환하고, 실패하면 -1을 반환한다.
- 다음의 errno는 이 함수를 위해 정의된 에러상황이다.
- EINVAL : how 인수가 무효하다.
당신은 SIGKILL 과 SIGSTOP 시그널들을 블록할 수 없지만, 만일 시그널 셋이 그들을 포함한다면, sigprocmask는 에러 상황을 보고하는 대신에 그들을 단지 무시한다. 기억하라, SIGFPE와 같은 프로그램 에러 시그널들을 블록하는 것은 실제 프로그램 에러에 의해 발생된 시그널로 인해 바람직하지 못한 결과를 초래한다. (raise 나 kill에 의해 만들어진 시그널들은 제외하고) 이것은 시그널이 다시 블록이 해제되었을 때, 그 지점에서 실행을 계속하지 못할 정도로 프로그램이 파괴되었기 때문이다. 21. 2. 1절 [Program Erroe Signals] 참조.
21. 7. 4 시그널의 배달 여부를 테스트하기 위한 블럭킹
다음은 간단한 예제이다. SIGALRM 시그널이 도착할 때마다 플래그를 설정하는 핸들러를 만들고, 메인 프로그램에서는 시간마다 이 플래그를 체크하고 그것을 재설정한다고 가정하자. 당신은 sigprocmask를 호출해서 코드의 임계부분을 보호함으로써 그 동안 도착한 부가적인 SIGALRM 시그널을 막을 수 있다.
/* 이 변수는 SIGALRM 시그널 핸들러에 의해 설정된다. */
- int
- main (void)
- {
- sigset_t block_alarm;
- . . .
- /* 시그널 마스크를 초기화한다. */
- sigemptyset (&block_alarm);
- sigaddset (&block_alarm, SIGALRM);
- while (1) {
- /* 시그널이 도착했는지를 체크하라; 만일 도착했다면, 플래그를 재설정하라. */
- sigprocmask (SIG_BLOCK, &block_alarm, NULL);
- if (flag) {
- actions-if-not-arrived
- flag = 0;
- }
- sigprocmask (SIG_UNBLOCK, &block_alarm, NULL);
- . . .
- sigprocmask (SIG_BLOCK, &block_alarm, NULL);
- }
- . . .
- }
- main (void)
21. 7. 5 핸들러를 위하여 블록된 시그널
시그널 핸들러가 호출되었을 때, 당신은 보통 그 시그널 핸들러가 다른 시그널에 의해 블록됨이 없이 끝나기를 원한다. 그 핸들러가 시작된 순간부터 끝나는 순간까지, 당신은 핸들러의 데이터를 오염시키거나 혼란시킬지도 모르는 시그널을 블록해야만 한다.
한 시그널에 의해 핸들러 함수가 호출되었을 때, 핸들러가 실행되는 동안 그 시그널은 자동적으로 블록된다 ( 다른 시그널과 함께 그 시그널은 이미 프로세스의 시그널 마스크에 존재하게된다. ) 만일 예를 들어 당신이 SIGTSTP를 위한 핸들러를 준비했을 때, 그 시그널이 도착하면 핸들러는 핸들러가 실행되는 동안 기다리도록 하여 나중에 SIGTSTP 시그널을 다시 발생시킨다.
그렇지만, 디폴트로, 다른 종류의 시그널들은 블록되지 않았다; 그들은 핸들러가 실행되는 동안 발생할 수도 있다. 핸들러가 실행되는 동안 다른 종류의 시그널을 블록하기 위한 좋은 방법은 sigaction 구조체의 sa_mask 멤버를 사용하는 것이다. 다음은 그에 대한 예제이다.
- #include <signal. h>
- #include <stddef. h>
- void catch_stop ();
- void
- install_handler (void)
- {
- struct sigaction setup_action;
- sigset_t block_mask;
- sigemptyset (&block_mask);
- /* 핸들러가 실행되는 동안 다른 터미널-발생 시그널들을 블록하라. */
- sigaddset (&block_mask, SIGINT);
- sigaddset (&block_mask, SIGQUIT);
- setup_action. sa_handler = catch_stop;
- setup_action. sa_mask = block_mask;
- setup_action. sa_flags = 0;
- sigaction (SIGTSTP, &setup_action, NULL);
- sigset_t block_mask;
- }
- #include <stddef. h>
핸들러 코드 안에서 명시적으로 다른 시그널들을 블록하는 것보다는 더 신뢰 가능하다. 만일 당신이 핸들러 안에서 명시적으로 시그널들을 블록한다면, 아직 당신이 그들을 블록하지 않았을, 핸들러 시작 초기의 짧은 간격동안에 발생된 시그널로 인한 문제는 피할 수가 없다.
이 메커니즘을 사용하여 프로세스의 현재 마스크로부터 시그널들을 제거 할 수 없다. 그렇지만, 핸들러 함수에서 sigprocmask를 호출하여, 당신이 원하는 시그널을 블록하거나 해제하도록 만들 수 있다. 어쨌든, 핸들러 함수가 반환할 때, 시스템은 핸들러 함수가 진입하기 전으로 마스크를 반환한다.
21. 7. 6 미해결 시그널 체크하기
당신은 sigpending 을 호출하여 어느 시점에서 미해결 상태인 시그널들을 발견해낼 수 있다. 이 함수는 `signal. h'에 선언되어 있다.
함수 : int sigpending (sigset_t *set)
- sigpending 함수는 set에 미해결인 시그널에 대한 정보를 저장한다. 만일 미해결 상태의 시그널이 있다면, 그 시그널은 반환된 set의 멤버이다. (당신은 sigismember 을 사용해서 특정한 시그널이 이 set의 멤버인지 테스트 할 수 있다; 21. 7. 2절 [Signal Sets] 참조. ) 성공하면 반환값은 0이고 실패하면 -1이다.
시그널이 미해결 상태인지를 테스트하는 것은 자주 유용하지는 않다. 시그널이 블록되지 않았을 때 테스트하는 것은 좋지 않다. 다음의 예제를 살펴보자.
- #include <signal. h>
- #include <stddef. h>
- sigset_t base_mask, waiting_mask;
- sigemptyset (&base_mask);
- sigaddset (&base_mask, SIGINT);
- sigaddset (&base_mask, SIGTSTP);
- /* 다른 프로세싱을 하는 동안에 사용자 인터럽트를 블록하라. */
- sigprocmask (SIG_SETMASK, &base_mask, NULL);
- . . .
- /* 그 후에, 어느 시그널이 미해결인지를 체크하라. */
- sigpending (&waiting_mask);
- if (sigismember (&waiting_mask, SIGINT)) {
- /* 사용자가 프로세스를 죽이기기를 시도한다 */
- } else if (sigismember (&waiting_mask, SIGTSTP)) {
- /* 사용자가 프로세스를 멈추기를 시도한다 */
- }
- #include <stddef. h>
당신의 프로세스를 위하여 미해결상태인 특정한 시그널이 있다면 그 동안에 도착한 같은 종류의 부가적인 시그널들은 버려질 것임을 기억하라. 예를 들어, 만일 SIGINT 시그널이 미해결 상태일 때 다른 SIGINT 시그널이 도착하면, 당신의 프로그램은 이 시그널의 블록을 해제할 때 오직 한 개의 SIGINT 시그널로 처리할 것이다.
이식성 노트 : sigpending 함수는 POSIX. 1에 새로이 추가되었다. 오래된 시스템들은 이와 동등한 함수가 없다.
21. 7. 7 나중에 동작하도록 시그널을 기억하기
라이브러리 함수를 사용해서 시그널을 블록하는 대신에, 당신은 당신이 블록을 "해제"할 때, 나중에 테스트 되도록 플래그를 설정하는 핸들러를 만들어서 거의 같은 결과를 얻을 수 있다.
다음의 예제를 살펴보라.
- /* 만일 이 플래그가 0이 아니라면, 즉시 그 시그널을 처리하지 말아라. */
- volatile sig_atomic_t signal_pending;
- /* 이것은 시그널이 도착했고 처리되지 않았으면 0이 아니다. */
- volatilesig_atomic_t defer_signal;
- void
- handler (int signum)
- {
- if (defer_signal)
- signal_pending = signum;
- else
- . . . /* "실제로" 그 시그널을 처리한다. */
- }
- . . .
- void
- update_mumble (int frob)
- {
- /* 시그널이 즉각적인 효력을 발휘하는 것을 막아라. */
- defer_signal++;
- /* 인터럽션에 대한 걱정 없이, 이제 mumble을 갱신한다. */
- mumble. a = 1;
- mumble. b = hack ();
- mumble. c = frob;
- /* 우리는 mumble을 갱신하였다. 들어와 있는 어느 시그널을 처리하라. */
- defer_signal--;
- if (defer_signal == 0 && signal_pending != 0)
- raise (signal_pending);
- defer_signal++;
- }
- volatile sig_atomic_t signal_pending;
도착한 특정한 시그널이_미해결인_어떻게 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 == 1 && signal_pending != 0)
- raise (signal_pending);
- defer_singal--;
위의 경우 if 구문과 감소사이에 도착된 시그널은 불명확한 시간동안은 잃어버리게 된다. 핸들러는 완전하게 defer_signal을 설정하였지만, 프로그램은 이미 이 변수를 테스트해버렸고, 다시는 변수를 테스트하지 않을 것이다.
그와같은 버그들을 타이밍 에러라고 부른다. 그들은 희귀하게 발생하고 재생시키는데는 굉장히 중요하기 때문에 아주 나쁜 버그이다. 당신은 재생 가능한 버그를 발견하는 것처럼 디버거로 그들을 발견할거라고 예상하지 마라. 그렇기 때문에 그러한 버그를 피하기 위해서는 특별히 주의할 가치가 있다.
( 당신은 이러한 순서로 코드를 기록하고 싶은 유혹을 받지 말아라, defer_signal 이 카운터(counter)로써 사용된다면 signal_pending 과 함께 테스트되어야만 한다. 후에, 0에 대한 테스트는 1에 대한 테스트보다는 깨끗하다. 그러나 만일 당신이 defer_signal을 카운터로써 사용하지 않고, 0과 1의 값만 그것에 주어진다면, 순서는 간단하게 보여질 것이다. 이것은 defer_signal을 카운터로써 사용하는 것보다 더한 이득을 갖는다: 그것은 당신이 잘못된 순서로 코드를 기록하고 민감한 버그를 만들어낼 가능성을 감소시킬 것이다. )
21. 8 시그널을 위한 기다림
당신의 프로그램이 외부 사건에 의해서 조종되거나, 동기화를 위해서 시그널을 사용한다면, 그때 그 프로그램은 시그널이 도착할 때까지 기다릴 수밖에 없다.
21. 8. 1 pause 사용하기
시그널이 도착할 때까지 기다리기 위한 간단한 방법은 pause 를 호출하는 것이다. 당신이 그것을 사용하기 전에 다음절에 있는, 그것을 사용함으로 써 얻게되는 불리한 점을 보아라.
함수 : int pause ()
- pause 함수는 핸들러 함수를 실행하거나, 또는 프로세스를 종료시키는 시그널이 발생할 때까지 프로그램의 실행을 잠시 멈추는 역할을 하는 함수이다.
- 만일 그 시그널이 핸들러 함수가 실행되도록 하는 원인이 된다면, pause는 반환한다. 이것은 비성공적인 반환으로 간주한다. (왜냐면, "성공적"인 행동은 영원히 프로그램을 멈추도록 할 것이기 때문이다. ) 그렇기 때문에 -1을 반환한다. 심지어 당신이 시스템 핸들러가 반환할 때 다른 기본동작(primitives)을 재 시작하도록 정한다고 해도 ( 21. 5절 [Interrupted Primitives] 참조. ), 이것은 pause에 아무런 영향을 미치지 않는다; 그것은 시그널이 처리될 때 항상 실패한다.
- 다음의 errno는 이 함수를 위해 정의된 에러 상황이다.
- EINTR : 그 함수가 시그널의 배달로 인해서 인터럽트 되었다.
- 만일 그 시그널의 프로그램 종료의 원인이 된다면, pause는 반환하지 않는다(명백하게). pause 함수는 `unistd. h'에 선언되어 있다.
21. 8. 2 pause 사용의 문제들
pause의 간단함은 프로그램을 이상하게 중단(hang) 시킬 수도 있는 심각한 타이밍 에러들을 숨길수도 있다. 만일 당신의 프로그램에서, 실제 작업이 시그널 핸들러에 의해서 수행되고, "메일 프로그램"은 pause는 호출하지만 아무런 일을 하지 않을 때는 안전하다. 시그널이 배달될 때마다, 핸들러는 해야할 작업을 하고, 다음에 반환한다, 그래서 프로그램의 메인 루프는 다시 pause를 호출 할 수 있다.
한 개 이상의 시그널이 도착하기를 기다렸다가 실제작업을 재개하기 위해서 pause를 사용하는 것은 안전할 수 없다. 당신이 플래그를 설정하는 것으로 시그널 핸들러를 조정한다고 할지라도, 당신은 여전히 pause함수를 믿을 수 없다. 다음에 이 문제에 대한 예제가 있다.
- /* usr_interrupt는 시그널 핸들러에 의해 설정된다. */
- if (!usr_interrupt)
- pause ();
- /* 시그널이 도착했을 때 작업하라. */
- . . .
- if (!usr_interrupt)
이것은 버그를 갖고 있다: 변수 usr_interrupt가 체크된 후, 하지만 pause가 호출되기 전에 시그널이 도착할 수 있다. 만일 앞으로 아무런 시그널이 도착하지 않으면, 프로세스는 결코 다시는 재개될 수 없다. puase를 사용하는 대신에 루프 안에서 sleep를 사용해서 오랜 기다림에 상위(upper) 제한을 가할 수 있다. (sleep에 대한 상세한 정보는 17. 4절 [Sleeping] 를 참조하라. ) 다음의 예제를 보자.
- /* usr_interrupt는 시그널 핸들러에 의해 설정된다. */
- while (!usr_interrupt)
- sleep (1);
- /* 시그널이 도착할 때 작업하라. */
- . . .
- while (!usr_interrupt)
어떤 목적으로도, 이것은 사용하기에 충분하다. 조금 더 복잡하기는 하지만, sigsuspend를 사용해서도 특정한 핸들러가 실행되는 동안 확실하게 기다릴 수 있다.
21. 8. 3 sigsuspend 사용하기
시그널이 도착하기를 기다리는 깨끗하고 신뢰 가능하다하다 방법은 그것을 블록하고 sigsuspend를 사용하는 것이다. 루프 안에서 사용된 sigsuspend는, 다른 종류의 시그널들이 그들의 핸들러에 의해 처리되는 동안, 어떤 종류의 시그널을 위해서 기다릴 수 있다.
함수 : int sigsuspend (const sigset_t *set)
- 이 함수는 set으로 프로세스의 시그널 마스크를 대체하고, 프로세스를 종료시키거나, 시그널 처리 함수를 호출하는 동작을 하는 시그널이 배달될 때까지 프로세스를 중지시킨다. 즉, 프로그램은 set의 멤버가 아닌 시그널중의 하나가 도착할 때까지 중지된다.
- 만일 어떤 프로세스가 핸들러 함수를 호출하는 시그널의 배달로 인해서 재개되면, 그 핸들러 함수는 반환하고, sigsuspend 또한 반환한다. 마스크는 sigsuspend가 기다리고 있는 동안만 set으로 유지된다. sigsuspend 함수는 반환할 때 항상 전의 시그널 마스크를 반환한다. 반환값과 에러상황은 pause와 같다.
- sigsuspend를 사용해서, 앞절에서 나온 pause와 sleep를 완전하게 대체할 수 있다.
- sigset_t mask, oldmask;
- . . .
- /* 일시적으로 블록할 시그널들의 마스크를 준비하라. */
- sigemptyset (&mask);
- sigaddset (&mask, SIGUSR1);
- . . .
- /* 시그널이 도착하기를 기다려라. */
- sigprocmask (SIG_BLOCK, &mask, &oldmask);
- while (!usr_interrupt)
- sigsuspend (&oldmask);
- sigprocmask (SIG_UNBLOCK, &mask, NULL);
코드의 마지막 부분은 약간 교묘하다. 이것의 핵심은 sigsuspend가 반환 할 때, 프로세스가 원래 가졌던 시그널 마스크의 값으로 재설정하는 것이다. 이 경우, SIGUSR1 시그널이 다시 블록되어진다. sigprocmask의 두 번째 호출은 이 시그널의 블록을 명백하게 해제할 필요가 있다.
다른 포인트 : 오직 하나의 SIGUSR1 시그널을 기다리는 그 프로그램에서 왜 while 루프가 필요한지 의아해 할지 모른다. 그 대답은, sigsuspend에 주어지는 마스크가, 예를 들어, 작업 제어 시그널처럼 다른 종류의 시그널이 배달됨으로 인해서 구동되어질 프로세스를 허가한다는 것이다. 만일 usr_interrput를 설정하지 않은 시그널에 의해 프로세스가 재개된다면, 그것은 단지 "올바른" 종류의 시그널이 발생할 때까지 다시 중지된다. 이 테크닉은 준비작업에 더 낳은 라인이 필요하지만, 당신이 시그널에 대한 정확한 기다림을 위해서는 필요하다. 실제로 기다림을 위한 코드는 단지 4줄뿐이다.
21. 9 BSD 시그널 핸들링
이 절은 BSD 유닉스에서 온 시그널 핸들링 함수들에 대해서 설명한다. 이들 도구들은 그들의 시대에서는 진보적이였지만; 오늘날은 굉장히 시대에 뒤떨어진 것이고, 오직 BSD와의 호환성을 위해서 제공되고 있다.
21. 9. 1 POSIX 와 BSD 시그널 기능들
POSIX 시그널 처리 기능들은 BSD 기능들로부터 나온 것이기 때문에 BSD와 POSIX 시그널 처리 기능들 사이에는 많은 유사성이 있다. 충돌을 피하기 위해서 모든 함수들이 서로 다른 이름을 갖고 있다는 것을 제외하고, 둘 사이에는 주요한 차이들이 있다.
BSD 유닉스는 sigset_t 오브젝트로 시그널 마스크를 나타내는 것이 아니라 int 비트마스크로써 시그널 마스크를 표현한다.
BSD 기능들은 인터럽트된 기본동작(primitive)을 실패하게 할 것인지 재개할 것인지의 여부에 대해서 다른 디폴트를 사용한다. POSIX 기능은 당신이 그들을 재개하도록 정할지라도 시스템 호출이 실패하게 만들고, BSD 기능들은, 당신이 그들을 실패하도록 정했을지라도 시스템 호출은 그것을 재개하도록 만드는 것이다. 21. 5절 [Interrupted Primitives] 참조.
BSD 유닉스는 시그널 스택의 구상을 갖는다. 이것은 보통의 실행 스택대신에, 시그널 핸들러 함수들의 실행동안에 사용되는 대체스택 (alternate stack)이다.
BSD 기능들은 `signal. h'에 선언되어 있다.
21. 10 핸들러 함수를 만들기 위한 BSD 함수
데이터타입 : struct sigvec
- 이 데이터 타입은 struct sigaction과 동등한 것이다( 21. 3. 2절 [Advanced Signal Handling] 참조); 이것은 sigvec함수에서 시그널 동작을 지정하기 위해서 사용된다. 그것은 다음과 같은 멤버들을 포함하고 있다.
sighandler_t sv_handler
- 이것은 핸들러 함수이다.
int sv_mask
- 이것은 핸들러 함수가 호출되어있을 동안에 블록될 부가적인 시그널들의 마스크이다.
int sv_flags
- 이것은 시그널의 동작에 영향을 미치는 다양한 플래그를 정하는데 사용되는 비트마스크이다. 당신은 sv_onstack로서 이 필드를 참조할 수 있다.
그들 기호 상수들은 sigvec 구조체의 sv_flags 를 위해 제공되는 값들로 사용될 수 있다. 이 필드는 비트마스크 값으로써, 당신이 관심을 갖는 플래그들을 비트별-OR를 사용해서 결합할 수 있다.
매크로 : int SV__ONSTACK
- 만일 이 비트가 구조체 sigvec의 sv_flags에서 설정되면, 그것은 시그널이 배달되었을 때 시그널 스택을 사용하는 것을 의미한다.
매크로 : int SV__INTERRUPT
- 만일 이 비트가 sigvec 구조체의 sv_flags안에서 설정되면, 이러한 종류의 시그널에 의해 인터럽트된 시스템 호출은 핸들러가 반환해도 재시작 되지 않을 것임을 의미한다; 대신에 시스템 호출은 EINTR 에러 상황으로 반환할 것이다. 21. 5절 [Interrupted Primitives] 참조.
매크로 : int SV__RESETHAND
- 만일 이 비트가 sigvec 구조체의 sv_flags에서 설정되면, 시그널을 받았을 때, SIG_DFL로 원래의 시그널을 위한 동작으로 재설정하는 것을 의미한다.
함수 : int sigvec (int signum, const struct sigvec *action, struct sigvec *old_action)
- 이 함수는 sigaction (21. 3. 2절 [Acvanced Signal Handling] 참조. )과 같다; 그것은 시그널 signum에 대한 동작을 action으로 하고, old_action에 그 시그널의 예전 동작에 대한 정보를 반환한다.
함수 : int siginterrupt (int signum, int failflag)
- 이 함수는 어떤 기본동작이 시그널 signum에 의해 인터럽트 되었을 때, 사용할 접근법을 지정한다. 만일 failflag가 false이면, 시그널 signum은 기본동작을 다시 시작한다. 만일 failflag가 true이면, 처리되는 signum은 에러코드 EINTR로 그들 기본동작을 실패하게 한다. 21. 5절 [Interrupted Primitives] 참조.
21. 10. 1 블록된 시그널을 위한 BSD 함수들
매크로 : int sigmask (int signum)
- 이 매크로는 시그널 signum을 위한 비트를 가진 시그널 마스크를 반환한다. 당신은 여러 개의 시그널을 정해주기 위해서 비트별-OR 연산자를 사용할 수 있다. 다음처럼.
- (sigmask (SIGTSTP) | sigmask (SIGSTOP)
- | sigmask (SIGTTIN) | sigmask (SIGTTOU))
- 이것은 모든 작업-제어 시그널 중 stop 시그널들을 포함하는 마스크를 지정한다.
- (sigmask (SIGTSTP) | sigmask (SIGSTOP)
함수 : int sigblock (int mask)
- 이 함수는 how 인수를 SIG_BLOCK로 가진, sigprocmask( 21. 7. 3절 [Process Signal Mask] 참조)과 같다: 그것은 호출된 프로세스의 블록된 시그널의 집합에, mask에 의해 정해진 시그널들을 더한다. 반환값은 전의 블록된 시그널들의 집합이다.
함수 : int sigsetmask (int mask)
- 이 함수는 how 인수가 SIG_SETMASK 인, sigprocmask와 (21. 7. 3절 [Process Signal Mask] 참조. ) 같다: 그것은 호출된 프로세스의 시그널 마스크를 set으로 정한다. 반환값은 전의 블록된 시그널들이 집합이다.
함수 : int sigpause (int mask)
- 이 함수는 sigsuspend(21. 8 [Waiting for a Signal] 참조. ) 과 같다: 호출된 프로세스의 시그널 마스크를 mask로 설정하고, 시그널이 도착하기를 기다린다. 반환할 때 전의 블록된 시그널의 집합은 다시 반환된다.
21. 10. 2 분리된 시그널 스택 사용하기
시그널 스택은 시그널 핸들러가 실행되는 동안 실행 스택으로써 사용되는 메모리의 특별한 영역이다. 오버플로우가 일어날 위험을 피하기 위해서는, 꽤 커야한다; 매크로 SIGSTKSZ는 시그널 스택을 위한 정규 크기로 정의되었다. 당신은 mallocac 을 사용해서 스택을 위한 공간을 할당할 수 있다. 그리고 나서 sigaltstack 이나 stgstack를 호출하여 시그널 스택을 사용하도록 시스템에게 알린다.
당신이 시그널 스택을 사용하기 위해서 시그널 핸들러를 달리 만들 필요는 없다. 다른 것에서 스택으로의 변경은 자동적으로 발생한다. 그렇지만, 어떤 기계 상에 존재하는 어떤 디버거는 시그널 스택을 사용하는 핸들러가 실행되는 동안 스택 트래이스(trace)를 하면 혼란스럽게 될지도 모른다.
분리된 시그널 스택을 사용하도록 시스템에게 알리기 위한 두 개의 인터페이스가 있다. sigstack은 오래된 인터페이스로써 4. 2 BSD 로부터 왔다. sigaltstack은 새로운 인터페이스로써 4. 4 BSD 로부터 왔다. sigaltstack 인터페이스는 스택의 성장 방향을 알리도록 당신의 프로그램에게 요구하지 않고, 정해진 기계와 운영체제에 의존한다는 편리점을 갖는다.
데이터 타입 : struct sigaltstack
- 이 구조체는 시그널 스택을 설명한다. 그것은 다음의 멤버들을 포함하고 있다:
void *ss_sp
- 이것은 시그널 스택의 기준(base)을 가리킨다.
size_t ss_size
- `ss_sp'가 가리키는 시그널 스택의 크기(바이트)이다. 당신이 스택을 위한 공간을 얼마나 많이 할 것인지를 정한다. 다음 두 개의 매크로는 `signal. h'에 정의되어 있고, 당신은 이것은 계산된 크기로써 사용할 것이다.
SIGSTKSZ
- 이것은 시그널 스택을 위한 정규 크기이다. 이것은 보통 사용하기 위해서 규격화된 용량을 갖는다.
MINSIGSTKSZ
- 이것은 단지 운영체제가 시그널 배달을 수행하기 위해서 필요한 시그널 스택 공간의 양이다. 시그널 스택은 적어도 이것보다는 커야만 한다.
- 대부분의 경우, SIGSTKSZ를 사용한다. 하지만, 당신이 당신의 프로그램 시그널 핸들러가 얼마나 많은 스택 공간을 필요로 하는지 안다면, 당신은 다른 크기를 사용하기를 원할 것이다. 이 경우, 우리는 시그널 스택을 MINSIGSTKSZ로 할당하고, ss_size를 증가시킨다.
int ss_flags
- 이 필드는 다음의 플래그나 그들의 조합을 포함한다.
SA_DISABLE
- 이것은 시그널 스택을 사용하지 않을 것임을 시스템에게 알린다.
SA_ONSTACK
- 이것은 시스템에의해 설정되고, 현재 사용중인 시그널 스택을 가리킨다. 만일 이 비트가 설정되지 않으면, 시그널은 보통 사용자 스택 상에 배달될 것이다.
함수 : int sigaltstack (const struct sigaltstack *stack, struct sigaltstack *oldstack)
- sigaltstack 함수는 시그널 핸들링 중에 사용할 대체 스택을 정한다. 시그널이 프로세스에 의해 받아들여지고 그것이 시그널 스택을 사용하도록 지정할 때, 시스템은 시그널 핸들러가 실행될 동안 사용되도록 현재 인스톨된 시그널 스택으로 변경한다.
- 만일 oldstack 이 널 포인터가 아니라면, 현재 인스톨된 시그널 스택에 대한 정보가 그곳으로 반환된다. 만일 stack이 널 포인터가 아니라면, 이것은 시그널 핸들러에 의해 사용되도록 새로운 스택으로 인스톨된다.
- 성공하면 반환값은 0이고 실패하면 -1이다. 만일 sigaltstack이 실패하면, 다음 값들 중 하나로 errno를 설정한다.
EINVAL
- 당신은 실제로 사용중이였던 불가능한 스택으로 시도하였다.
ENOMEM
- 대체 스택의 크기가 너무 작다. 그것은 적어도 MINSIGSTKSZ 보다는 커야만 한다.
다음은 오래된 sigstack 인터페이스이다. 당신은 sigaltstack 대신에 사용할 수 있다.
데이터 타입 : struct sigstack
- 이 구조체는 시그널 스택을 표현한다. 그것은 다음의 멤버들을 포함한다.
void *ss_sp
- 이것은 스택 포인터이다. 만일 당신의 기계에서 스택이 밑쪽으로 성장한다면, 이것은 당신이 할당한곳의 위를 가리킨다. 만일 그 스택이 위를 향해 성장한다면, 그것은 밑을 가리킨다.
int ss_onstack
- 이 필드는 만일 프로세스에서 현재 이 스택을 사용하고 있다면 참이다.
함수 : int sigstack (const struct sigstack *stack, struct sigstack *oldstack)
- sigstack 함수는 시그널 핸들링 중에 사용할 대체 스택을 정한다. 시그널이 프로세스에 의해 받아들여지고 그 동작이 시그널 스택을 사용하도록 정해진다면, 시스템은 시그널 핸들러가 실행되는 동안 사용할 현재 인스톨된 시그널 스택으로 변경한다. 만일 oldstack 이 널 포인터가 아니라면, 현재 인스톨된 시그널 스택에 대한 정보가 그것이 가리키는 곳으로 반환된다. 만일 stack이 널 포인터가 아니라면, 이것은 시그널 핸들러에 의해 사용되도록 새로운 스택으로 인스톨된다. 성공하면 0을 반환하고 실패하면 -1을 반환한다.
출처 : The GNU C Library Reference Manual
'Programming > C, C++' 카테고리의 다른 글
메모리 누수없는 코드 만들기 C, C++ (0) | 2011.06.23 |
---|---|
파일 비교 부터 텍스트 비교까지 / 개발자를 위한 머지 툴 (0) | 2011.04.27 |
callback (콜백) 의 개념 (0) | 2011.01.26 |
와치독? 워치독??? WatchDog!! (0) | 2011.01.26 |
socket 함수 정의 (2) | 2011.01.24 |
댓글