Unix 시그널 처리

2008/03/18 15:18 / Resource

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이다.
역자주 : profiling: 프로파일링 : 시스템의 성능 및 병목현상을 방지하기 위한 도구라고 생각하시면 될 것 같네요. 정확하지가 않아서. .

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와 같은 시그널들을 배달할 것이다.

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);
. . .
}

주어진 시그널이 무시되도록 미리 설정되어졌다면, 이 코드는 그 설정을 바꾸는 것을 피함을 기억하라. 이것은 비-작업-제어 쉘들이 자식 프로세스가 시작될 때 어떤 시그널들을 종종 무시하기 때문이고, 그리고 이것을 고려하는 것은 자식 프로세스에게는 중요하다. 우리는 이 예제 프로그램이 디버깅을 위해서(코어 덤프 파일) 정보를 제공하도록 만들어졌기 때문에 프로그램 에러 시그널이나 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);
. . .
}

이 프로그램은 요구된 파라미터로 단지 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);
}

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;
}

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);
}
int
main (void)
{
. . .
signal (SIGINT, sigint_handler);
. . .
while (1) {
prepare_for_command ();
if (setjmp (return_to_top_level) == 0)
read_and_execute_command();
}
}
/* 이것이 여러 명령문에서 사용되는 서브루틴이라고 생각하자. */
char *
read_data ()
{
if (input_from_terminal) {
waiting_for_input = 1;
. . .
waiting_for_input = 0;
} else {
. . .
}
}

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;
};
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;
 
/* 정해진 결과를 얻을 때까지 상황을 물어보아라 */
do{
errno = 0;
pid = waitpid (WAIT_ANY, &w, WNOHANG |
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;
 
    /* 만일 프로세스가 종료되었다면, 출력을 위한 기다림을 멈추어라 */
    if (WIFSIGNALED (w) || WIFEXITED (w)) {
    if (p->input_descriptor)
    FD_CLR (p->input_descriptor, &input_wait_mask);
     
    /* 프로그램은 process_list 안에 어떤 새로운 것이 있는지 알아보기 위해서 주어진 시간동안에 이 플래그를 체크할 것이다. */
    ++process_status_change;
    }
}
/* 우리에게 말할 무언가를 가진 모든 프로세스들을 처리하도록 루프를 돌려라 */
}
}
다음은 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 . . .
}
}

리스트를 시험하기 전에 플래그를 소거하는 것은 치명적이다; 그렇지 않고, 만일 플래그가 소거되기 전과, 프로세스 리스트의 적당한 요소가 체크된 후에 시그널이 배달된다면, 다음 도착한 시그널이 다시 그 플래그를 설정하기 전까지는 그 상황변화를 알아차릴 수 없다. 당신은 물론 그 리스트를 조사하는 동안 시그널을 블록함으로써 이러한 문제를 피할 수는 있지만, 올바른 순서로 일들을 처리하는 것이 정확함을 보증하기에는 좀더 좋은 방법이다.

프로세스 상황을 체크하는 루프가 그 상황이 유용하게 저장되어졌음이 확인될 때까지 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 . . .
}
}
}

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;
}
}

이 프로그램은 계속 번갈아 가면서 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);
. . .
}
  이식성 노트: raise 는 ANSI C 위원회에 의해 만들어졌다. 오래된 시스템들은 그것을 지원하지 않을 것이기 때문에, 이식성을 위해서는 kill을 사용하라. 21. 6. 2절[Signaling Another Process] 참조.

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);
}
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;
}

위의 예제는 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 인수로 무효한 시그널을 지정하였다.

함수 : 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);
. . .
}
}

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);
}

핸들러 코드 안에서 명시적으로 다른 시그널들을 블록하는 것보다는 더 신뢰 가능하다. 만일 당신이 핸들러 안에서 명시적으로 시그널들을 블록한다면, 아직 당신이 그들을 블록하지 않았을, 핸들러 시작 초기의 짧은 간격동안에 발생된 시그널로 인한 문제는 피할 수가 없다.

이 메커니즘을 사용하여 프로세스의 현재 마스크로부터 시그널들을 제거 할 수 없다. 그렇지만, 핸들러 함수에서 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)) {
/* 사용자가 프로세스를 멈추기를 시도한다 */
}

당신의 프로세스를 위하여 미해결상태인 특정한 시그널이 있다면 그 동안에 도착한 같은 종류의 부가적인 시그널들은 버려질 것임을 기억하라. 예를 들어, 만일 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);
}

도착한 특정한 시그널이_미해결인_어떻게 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 ();
/* 시그널이 도착했을 때 작업하라. */
. . .

이것은 버그를 갖고 있다: 변수 usr_interrupt가 체크된 후, 하지만 pause가 호출되기 전에 시그널이 도착할 수 있다. 만일 앞으로 아무런 시그널이 도착하지 않으면, 프로세스는 결코 다시는 재개될 수 없다. puase를 사용하는 대신에 루프 안에서 sleep를 사용해서 오랜 기다림에 상위(upper) 제한을 가할 수 있다. (sleep에 대한 상세한 정보는 17. 4절 [Sleeping] 를 참조하라. ) 다음의 예제를 보자.

/* usr_interrupt는 시그널 핸들러에 의해 설정된다. */
while (!usr_interrupt)
sleep (1);
/* 시그널이 도착할 때 작업하라. */
. . .

어떤 목적으로도, 이것은 사용하기에 충분하다. 조금 더 복잡하기는 하지만, 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 시그널들을 포함하는 마스크를 지정한다.

함수 : 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을 반환한다.

목차 이전 : 20. 비-지역 탈출 다음 : 22. 프로세스의 시동과 종료

여덟시간 메모장 2007/11/26 16:16

[GCC] 파일 잠그기

목차
1 교정 과정
2 파일잠그기
2.1 파일잠금의 필요성
2.2 유닉스에서 제공하는 파일 잠금 도구
2.2.1 flock(2)
2.2.2 fcntl(2)
2.3 경쟁상태 문제에 대해서
2.3.1 커널 업그레이드
2.3.2 O_APPEND 모드로 열기
2.3.3 세마포어 응용
3 토론및 잡담
4 참고문헌


1 교정 과정 #
2003/12/16 위키 생성 - yundream

2 파일잠그기 #
2.1 파일잠금의 필요성 #
하나의 프로세스가 하나의 파일을 열어서 작업하는 경우라면 관계없겠지만 여러개의 프로세스나 쓰레드가 하나의 파일을 열어서 작업할 경우 다음과 같은 문제가 발생할 것을 예상할 수 있을 것이다.


하나의 프로세스가 파일을 쓰고 있는데 쓰기가 완전히 끝나지 않은 상태에서 다른 프로세스도 파일에 써버린다. 데이터가 뒤죽박죽 되어 버릴 것이다.
둘다 동시에 파일의 내용을 읽고 그 내용을 수정함으로써 발생하는 동시성 문제, 가장 간단한 예로 카운트를 늘리는 프로그램을 생각할 수 있다. 파일의 카운트가 1000이고 두개의 프로세스가 카운트를 증가 시키려고 하는데 거의 동시에 읽어서 둘다 1000을 읽었다면 카운트는 1001이 될것이다. 그러나 원하는 값은 1002가 되어야 한다.

이런 문제는 특히 다중 프로세스기반으로 작동하는 웹서버상에서의 프로그래밍시 자주 발생하는 문제다. 유저로그를 파일로 남긴다고 했을 때 파일에 동시에 여러명이 접근해서 쓰게 되면 분명 데이터가 꼬이는 문제가 발생할 것이다.


이런 문제를 해결하기 위한 가장 간단한 방법은 동시에 오직 하나의 프로세스만이 파일에 접근하도록 하는 것이다. 프로세스는 파일에 접근하기에 잠그고 모든 일이 끝났을 때 잠금을 풀도록 한다.


또한 덤으로 잠금을 위한 접근제어에서 발생할 수 있는 경쟁상태(race condition)문제에 대해서도 간단히 알아보도록 하겠다.


2.2 유닉스에서 제공하는 파일 잠금 도구 #
2.2.1 flock(2) #
flock함수는 다음과 같이 선언되어 있다.
#include <sys/file.h> int flock(int fd, int operation)


이 함수는 열려진 파일에 대해서 권고잠금을 적용하거나 제거하는 일을 수행할 수 있다. 권고잠금이란 말에 유의해야 한다. 이것은 파일에 접근하려는 프로세스들이 flock를 사용해서 잠금을 검사하도록 서로 약속되어 있어야 잠금을 보장할 수 있음을 뜻한다. 어떤 파일은 flock를 사용해서 잠금을 검사하고 어떤 파일은 검사하지 않는다면 잠금을 하나 마나가 된다.


첫번째 인자인 fd는 잠금을 적용혹은 해제하길 원하는 열려진 파일 지정자 이다.


두번째 인자는 fd에 대해서 행해 지는 연산으로 다음과 같은 종류가 있다.
LOCK_SH 공유 잠금. 으로 한개 이상의 프로세스들이 파일에 대한 공유잠금이 가능하게 한다.
LOCK_EX 배타(exclusive)잠금으로 한번에 하나의 파일만이 잠금을 얻을 수 있다.
LOCK_UN 잠금을 푼다.
LOCK_NB 잠금일 때 블럭하지 않고 리턴한다. 잠겨있음을 확인하고 다른 일을 할 때 유용하다. 이 연산은 다른 연산들과 |연산이 가능하다. 만약 파일이 잠겨있게 되면 errno에 EWOULBLOCK가 설정된다.

flock를 이용해서 한 파일에 대해서 공유 잠금과 배타 잠금을 동시에 할 수는 없다. 파일잠금은 inode에 대해서 이루어 지게 되므로 dup와 fork는 중복된 잠금을 만들지 않는다. 즉 dup와 fork를 통해서 새로운 프로세스가 만들어지고 거기에 파일 지정자를 공유하게 되면 하나의 잠금을 공유하게 된다. 다음은 간단한 flock을 이용한 파일잠금 테스트 코드다.
#include <unistd.h>

#include <stdlib.h>

#include <sys/file.h>

#include <sys/types.h>

#include <sys/stat.h>

int main(int argc, char **argv ) {

int fd; int id; int i;

char *file_name = "lock.file"; id = atoi(argv[1]);


fd = open(file_name, O_RDONLY);

if (flock(fd, LOCK_EX) != 0) { printf("flock error\n"); exit(0); }

for (i = 0; i < 5; i++) { printf("file lock %d : %d\n", id, i); sleep(1); }

if (flock(fd, LOCK_UN) != 0) { printf("filue un lock error\n"); } close(fd); }


위의 코드를 컴파일 한 후 2개 이상 띄워서 테스트 해보면 한번에 하나의 프로세스만 잠금을 얻을 수 있고 나머지 프로세스들은 잠금을 얻을 때까지 기다리는걸 확인할 수 있을 것이다.


자 그럼 재미있는? 테스트를 한번 해보도록 하자. 위의 컴파일된 코드를 각각 다른 id를 줘서 한번에 실행시키면 과연 어떤 순서로 잠금을 얻게 될까? 실행된 순서대로 잠금을 얻게 될지 아닐지 알아보도록 하자.
# ./flock 1& ./flock 2& ./flock 3& ./flock 4& ./flock 5& ./flock 6 ... OK File FD 1 : 0 OK File FD 1 : 1 OK File FD 1 : 2 OK File FD 1 : 3 OK File FD 1 : 4 OK File FD 4 : 0 OK File FD 4 : 1 OK File FD 4 : 2 OK File FD 4 : 3 OK File FD 4 : 4 OK File FD 2 : 0 OK File FD 2 : 1 OK File FD 2 : 2
가능하다면 터미널 2개에서 한꺼번에 테스트 해보도록 한다.


테스트 해보면 어느 프로세스가 먼저 잠금을 얻게 되는지는 순전히 운에 좌우됨을 알수 있다. 이것은 프로세스들이 경쟁적으로 잠금을 얻을려고하는 경쟁상태(race condition) 에 놓이게 되고 운이 나쁘면 상당히 오랜시간 혹은 영원히 잠금을 얻지 못하고 블럭될 수도 있음을 의미한다. 경우에 따라서는 상당히 심각하게 생각해야될 문제다.


이 문제에 대한 해결 방안은 다른 장에서 다루도록 하겠다.


이외에도 NFS에서의 경우 사용할 수 없다는 단점을 가지고 있다. 이문제를 해결하기 위해서는 다음에서 설명할 fcntl을 사용해야 한다.


2.2.2 fcntl(2) #
fcntl은 파일(file)을 제어(control)하기 위해서 사용되는 시스템 함수로 파일 잠그기는 fcntl이 제공하는 여러가지 제어 기능중 일부분이다. fcntl을 이용할 경우 파일 단위뿐만 아니라 레코드 단위로도 잠금이 가능한데, 이에 내용은 이미 fcntl을 이용한 레코드 잠금에서 다루고 있으므로 여기에서는 개략적으로만 설명 하도록 하겠다.


기본적으로 fcntl은 파일단위가 아닌 레코드 단위로의 잠금을 제공하지만 어차피 명시적으로 이루어지는 잠금이므로 파일 잠금용으로도 확장 시켜서 사용할 수 있다. 다음은 fcntl을 이용한 파일 잠금 예제다. flock의 fcntl버젼이라고 보면 된다.


#include <unistd.h>

#include <stdlib.h>

#include <unistd.h>

#include <fcntl.h>

// 파일이 잠겨 있는지 확인해서 잠금을 얻고 

// 잠겨 있을 경우 잠금이 풀릴 때까지 

// 기다린다. 

int fd_lock(int fd) {

  struct flock lock;

  lock.l_type = F_WRLCK; lock.l_start = 0; lock.l_whence = SEEK_SET;

   lock.l_len = 0; return fcntl(fd, F_SETLKW, &lock);

}

// 파일 잠금을 얻은후 모든 작업이 끝났다면 

// 이 함수를 호출해서 잠금을 되돌려준다.

int fd_unlock(int fd) {

    struct flock lock; lock.l_type = F_UNLCK;

    lock.l_start = 0; lock.l_whence = SEEK_SET;

    lock.l_len = 0; return fcntl(fd, F_SETLK, &lock);

}


int main(int argc, char **argv) {

  int fd; int id; int i; char *file_name = "lock.file"; id = atoi(argv[1]);

   if ((fd = open(file_name, O_RDWR)) < 0) { perror("open failure"); exit(0); }

  if (fd_lock(fd) == -1) { perror("fd lock error"); exit(0); }

   for (i = 0; i < 5; i++) { printf("file lock %d : %d\n", id, i); sleep(1); }

  if (fd_unlock(fd) == -1) { perror("fd unlock error"); exit(0); }

   close(fd);

}


위 코드를 컴파일 한다음에 flock에서와 같은 동일한 테스트를 해보면 flock와 마찬가지로 경쟁상태에 놓임을 알 수 있다. 다음 장에서 경쟁상태해결에 대한 논의를 하도록 하겠다.


2.3 경쟁상태 문제에 대해서 #
경쟁 상태는 비단 파일잠금에서 뿐만 아니라 다른 데이터를 공유하는 모든 부분에서 발생할 수 있는 문제점이다. 여러가지 IPC들 예를 들어 공유메모리로 여러개의 프로세스가 접근할 때 과연 접근을 시도한 프로세스의 순서대로 공유메모리 접근이 이루어 질것인가 ? 세마포어에서는 ?


물론 경쟁상태가 문제가 되는 경우는 그리 흔하지 않겠지만 한번 이런 문제가 발생하면 문제점을 찾기가 매우 어려워 질것이다. 그렇다면 이번기회에 경쟁상태에 대해서 좀 알아보고 넘어가도록 하겠다.


2.3.1 커널 업그레이드 #
위의 코드들을 보면 flock와 fcntl에서 경쟁상태 문제가 발생한다고 했는데, 반드시 위의 문제가 발생하는 건 아니다. 위의 경쟁상태 문제는 최근의 커널에서는 해결된 상태에서 제공되어 진다. 확인해 본 결과 2.4.20을 기준으로 이전에 나와 있던 커널에서는 경쟁상태 문제가 발생하고 > 2.4.20 버젼과 2.6.x에서는 위의 문제가 해결 되어있음을 확인했다.


여러분의 커널을 확인해서 의심된다 싶으면 파일잠금을 제공하기 전에 한번쯤 테스트 해보거나 배포판에서 제공된 커널을 사용하고 있다면 배포판 홈페이지 등에서 확인을 해보길 바란다. 위의 문제가 해결되어 있음을 확인할 수 있을 것이다.
2.3.2 O_APPEND 모드로 열기 #
open(2)를 보면 O_APPEND 모드가 존재한다. 다음은 O_APPEND모드에 대한 open 맨페이지의 내용이다.
O_APPEND The file is opened in append mode. Before each write, the file pointer is positioned at the end of the file, as if with lseek. O_APPEND may lead to corrupted files on NFS file systems if more than one process appends data to a file at once. This is because NFS does not support appending to a file, so the client kernel has to simulate it, which can諄 be done without a race condition.
메뉴얼에 기술된걸 보면 O_APPEND모드로 열경우 race condition을 피해갈 수 있다고 되어 있다.


2.3.3 세마포어 응용 #
위의 방법들의 단점들은 뭐냐 하면 커널 업그레이드의 경우 솔직히 경쟁상태 문제 하나를 위해서 커널 업그레이드를 단행한다는 것도 그렇거니와 다른 (경쟁상태를 유발할 수 있는)유닉스로 포팅을 해야 한다면 난감하게 될 것이다. 그렇다면 좀더 표준적인 다른 도구를 사용해야될 필요가 있다. 안타깝게도 fcntl과 flock등에서의 경쟁상태 회피는 표준사항이 아니기 때문이다.


대부분의 경우에는 경쟁상태를 고려할 필요가 없겠지만 그래도 고려해야 하고 다른 유닉스와의 호환성도 중요하다면 세마포어를 응용해 보도록 하자. system V IPC 설비중 하나인 세마포어는 기본 스펙에 경쟁상태의 회피가 포함되어 있으니 system V IPC 설비를 제공하는 유닉스라면 믿고 사용해도 된다.


세마포어 응용 코드는 여기에서 제시하지 않겠다. 다음의 URL을 참고하기 바란다.
http://www.joinc.co.kr/modules.php?name=News&file=article&sid=40

3 토론및 잡담 #
flock에서 LOCK_SH를 이용했을 때 정확히 어떻게 작동하는지..
다른 Unix운영체제에서 잠금에서 발생할 수 있는 문제는 ?
쓰레드에서의 공유 파일에 대한 파일잠금

크리에이티브 커먼즈 라이센스
Creative Commons License
이올린에 북마크하기
Posted by 소리나는연탄.

Leave your greetings here.

  
  
  
  
  
  
  
  
 

Unix Error code 설명

2008/02/24 23:14 / Resource
#define EPERM 1 /* Not super-user */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Arg list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No children */
#define EAGAIN 11 /* Resource temporarily unavailable */
#define ENOMEM 12 /* Not enough core */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Mount device busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Inappropriate ioctl for device */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math arg out of domain of func */
#define ERANGE 34 /* Math result not representable */
#define ENOMSG 35 /* No message of desired type */
#define EIDRM 36 /* Identifier removed */
#define ECHRNG 37 /* Channel number out of range */
#define EL2NSYNC 38 /* Level 2 not synchronized */
#define EL3HLT 39 /* Level 3 halted */
#define EL3RST 40 /* Level 3 reset */
#define ELNRNG 41 /* Link number out of range */
#define EUNATCH 42 /* Protocol driver not attached */
#define ENOCSI 43 /* No CSI structure available */
#define EL2HLT 44 /* Level 2 halted */
#define EDEADLK 45 /* Deadlock condition. */
#define ENOLCK 46 /* No record locks available. */
#define ECANCELED 47 /* Operation canceled */
#define ENOTSUP 48 /* Operation not supported */

/* Filesystem Quotas */
#define EDQUOT 49 /* Disc quota exceeded */

/* Convergent Error Returns */
#define EBADE 50 /* invalid exchange */
#define EBADR 51 /* invalid request descriptor */
#define EXFULL 52 /* exchange full */
#define ENOANO 53 /* no anode */
#define EBADRQC 54 /* invalid request code */
#define EBADSLT 55 /* invalid slot */
#define EDEADLOCK 56 /* file locking deadlock error */

#define EBFONT 57 /* bad font file fmt */

/* Interprocess Robust Locks */
#define EOWNERDEAD 58 /* process died with the lock */
#define ENOTRECOVERABLE 59 /* lock is not recoverable */

/* stream problems */
#define ENOSTR 60 /* Device not a stream */
#define ENODATA 61 /* no data (for no delay io) */
#define ETIME 62 /* timer expired */
#define ENOSR 63 /* out of streams resources */

#define ENONET 64 /* Machine is not on the network */
#define ENOPKG 65 /* Package not installed */
#define EREMOTE 66 /* The object is remote */
#define ENOLINK 67 /* the link has been severed */
#define EADV 68 /* advertise error */
#define ESRMNT 69 /* srmount error */

#define ECOMM 70 /* Communication error on send */
#define EPROTO 71 /* Protocol error */

/* Interprocess Robust Locks */
#define ELOCKUNMAPPED 72 /* locked lock was unmapped */

#define EMULTIHOP 74 /* multihop attempted */
#define EBADMSG 77 /* trying to read unreadable message */
#define ENAMETOOLONG 78 /* path name is too long */
#define EOVERFLOW 79 /* value too large to be stored in data type */
#define ENOTUNIQ 80 /* given log. name not unique */
#define EBADFD 81 /* f.d. invalid for this operation */
#define EREMCHG 82 /* Remote address changed */

/* shared library problems */
#define ELIBACC 83 /* Can't access a needed shared lib. */
#define ELIBBAD 84 /* Accessing a corrupted shared lib. */
#define ELIBSCN 85 /* .lib section in a.out corrupted. */
#define ELIBMAX 86 /* Attempting to link in too many libs. */
#define ELIBEXEC 87 /* Attempting to exec a shared library. */
#define EILSEQ 88 /* Illegal byte sequence. */
#define ENOSYS 89 /* Unsupported file system operation */
#define ELOOP 90 /* Symbolic link loop */
#define ERESTART 91 /* Restartable system call */
#define ESTRPIPE 92 /* if pipe/FIFO, don't sleep in stream head */
#define ENOTEMPTY 93 /* directory not empty */
#define EUSERS 94 /* Too many users (for UFS) */

/* BSD Networking Software */
/* argument errors */
#define ENOTSOCK 95 /* Socket operation on non-socket */
#define EDESTADDRREQ 96 /* Destination address required */
#define EMSGSIZE 97 /* Message too long */
#define EPROTOTYPE 98 /* Protocol wrong type for socket */
#define ENOPROTOOPT 99 /* Protocol not available */
#define EPROTONOSUPPORT 120 /* Protocol not supported */
#define ESOCKTNOSUPPORT 121 /* Socket type not supported */
#define EOPNOTSUPP 122 /* Operation not supported on socket */
#define EPFNOSUPPORT 123 /* Protocol family not supported */
#define EAFNOSUPPORT 124 /* Address family not supported by */
/* protocol family */
#define EADDRINUSE 125 /* Address already in use */
#define EADDRNOTAVAIL 126 /* Can't assign requested address */
/* operational errors */
#define ENETDOWN 127 /* Network is down */
#define ENETUNREACH 128 /* Network is unreachable */
#define ENETRESET 129 /* Network dropped connection because */
/* of reset */
#define ECONNABORTED 130 /* Software caused connection abort */
#define ECONNRESET 131 /* Connection reset by peer */
#define ENOBUFS 132 /* No buffer space available */
#define EISCONN 133 /* Socket is already connected */
#define ENOTCONN 134 /* Socket is not connected */
/* XENIX has 135 - 142 */
#define ESHUTDOWN 143 /* Can't send after socket shutdown */
#define ETOOMANYREFS 144 /* Too many references: can't splice */
#define ETIMEDOUT 145 /* Connection timed out */
#define ECONNREFUSED 146 /* Connection refused */
#define EHOSTDOWN 147 /* Host is down */
#define EHOSTUNREACH 148 /* No route to host */
#define EWOULDBLOCK EAGAIN
#define EALREADY 149 /* operation already in progress */
#define EINPROGRESS 150 /* operation now in progress */

/* SUN Network File System */
#define ESTALE 151 /* Stale NFS file handle */
크리에이티브 커먼즈 라이센스
Creative Commons License
이올린에 북마크하기
Posted by 소리나는연탄.

Leave your greetings here.

  
  
  
  
  
  
  
  
 

소개

확장성을 확보하기 위한 방법은 여러가지가 있을 것이다. 여기에서는 그중 PlugIn 방식을 이용한 확장성확보에 대한 내용을 다룰 것이다.

Agent&Manager 방식의 프로그램을 만든다고 가정해보자. SNMP 프로토콜을 응용한 Net SNMP가 가장 대표적인 경우가 될 것이다. 이왕 Net SNMP를 예로 들었으니, Agent&Manager 방식의 SMS을 만드는 것으로 가닥을 잡아보겠다.

이러한 시스템에서 Agent 프로그램을 만들려고 한다면, 설계단계에서 가장 중요하게 생각해야 할게 확장성의 확보가 될 것이다. 왜냐면 시스템 관리의 범위가 매우 넓은 관계로 필요에 따라 관리 요소가 계속 추가될 수 있기 때문이다. 당장 생각나는게, CPU, Memory, Disk 관리쯤이 될것이다. 물론 초기에 완전하게 관리요소를 몽땅 예상하고 설계를 하는 방법도 있겠지만, 그렇게 될 경우 설계에 지나치게 많은 시간을 소비해야 할 것이다. 막상 그렇게 만들었다고 해도, 중간쯤 만들다 보면 관리요소가 새로 추가될 수도 있다. 심지어는 모두 만들고 나서 관리요소가 추가될 수도 있을 것이다.

이러한 경우 PlugIn 방식으로 각각의 성능을 모듈화 시켜서 붙이는 방식으로 개발시간을 아낄 수 있다. 거기에 덤으로 유연하고 확장성 좋은 시스템을 만들 수도 있다. 대략 다음과 같은 시스템 구성을 가지게 된다.

agent.png

필요한 기술

이 문서를 읽기 위해서는 함수포인터, STL, 라이브러리를 제어하기 위한 기술들을 가지고 있어야 한다.

Dynamic Module Loading

동적으로 모듈을 로딩하는 PlugIn방식을 구현하기 위한 기본적인 기술요구 사항은 그리 복잡하지 않다. 동적라이브러리를 이용하면, 쉽게 구현할 수 있다. 이 경우 중요한 것은 모듈과 Agent와의 인터페이스를 통일하는게 될 것이다. 어떠한 기능이 모듈형태로 추가되더라도, Agent와 Manager의 소스코드 수정없이 모듈이 로딩될 수 있어야 하기 때문이다.

인터페이스 이름을 맞추는 것은 문제가 되지 않을 것이다. 문제는 인터페이스를 통해서 이동하는 데이터가 될것이다. 이는 각각의 성능마다 보여줘야 하는 정보가 다를 수 있기 때문인데, CPU의 경우라면 사용율을 Disk라면 장치명,마운트이름,사용율을 보내야 하기 때문이다. 그러므로 예상가능한 모든 종류의 데이터를 처리할 수 있는 방법이 준비되어야 한다. 이 문제는 세가지 정도의 방식으로 해결할 수 있다.
  1. 문자열 전송
    간단하게 문자열을 전송한다. 성능=값1,값2 정도로 보내면 될 것이다. 포맷은 대략 아래와 같을 것이다. 성능이름이 들어가는 이유는, 나중에 Manager로 정보가 전달되었을때, 성능이름을 Key로 해서, 해당되는 모듈을 Plugin 방식으로 로딩하기 위함이다.

    CPU=89
    DSK=/dev/sda1,/root,58
  2. 구조체전송
    구조체로도 전송이 가능하다. Agent는 데이터를 처리하지 않고, Manager로 보내기만 하면되므로, 구조체에 어떤 멤버변수들이 있는지는 알 필요가 없다. 단지 보내야 하는 구조체의 크기성능이름만 알고 있으면 된다. 구조체를 받은 Manager 측은 성능이름에 해당되는 Plugin 모듈을 로딩해서 구조체의 값을 처리하면 된다.

    struct Info
    {
    int size; // 구조체의 크기
    char id[4]; // 성능 이름 : DSK, CPU, MEM...
    ... // 나머지 정보들은 성능에 따라 달라질 수 있다.
    ...
    }
  3. XML 데이터 전송
    잘 정의한다면, 유연하게 사용할 수 있을 것이다. 데이터의 크기가 커진다는 점을 고려하지 않아도 된다면, 가장 좋은 방법이라고 생각된다.

여기에서는 문자열을 보내는 것을 기준으로 설명하도록 하겠다.

Module Config

이제 설정파일을 만들어야 한다. 이 설정파일은 Key라고 할 수 있는 모듈 ID와 호출해야할 라이브러리의 이름들을 가진다. 다음과 같은 구조를 가지도록 하겠다.
[plugin]
CPU=libmycpu.so
MEM=libmymem.so
설정파일을 읽을 수 있는 라이브러리가 필요할 것 같아서, 급조한 코드가 있다. 간단 설정파일 Reader를 참고하기 바란다. 이 코드를 그대로 사용할 것이다.

프로시져

  1. 실행
  2. plugin 을 로딩하기 위해서 설정파일을 읽어들인다.
  3. plugin 목록을 읽어들인다.
  4. plugin 목록의 갯수만큼 루프를 돌면서, 라이브러리를 동적으로 적재한다.
  5. while 루프를 돌면서, 공통 인터페이스를 호출한다.

공통 인터페이스

공통 인터페이스를 정의해보도록 하자. 최대한 간단하게 정의하도록 하겠다.
  1. Init : 플러그인 모듈을 초기화 한다.
  2. Read : 플러그인 모듈로 부터, 데이터를 요청한다. 데이터는 문자열로 Key.IndexNum=Value,Value 형식으로 전달된다. IndexNum은 데이터가 2개이상일때, 사용하는 인덱스 번호다. 예를 들어 CPU가 2개라면
    • CPU.1=87
    • CPU.2=21
  3. RowNum : 몇개의 데이터가 있는지를 알려준다.
    • CPU가 2개라면, 2를 출력한다.
  4. Close : 플러그인 모듈을 닫는다.

테스트용 플러그인 모듈

libmycpu 와 libmysms 를 위한 모듈을 작성할 것이다. 이들은 공유라이브러리 형태로 작성될 것이다. 작성된 이들 플러그인 모듈은 dlopen(2) 함수를 이용해서 동적으로 적재 된다.

여기에서는 단순히 문자열을 리턴하는 dummy 모듈을 작성할 것이다.

플러그인 기능을 지원하는 Agent 프로그램

다음은 Agent 프로그램이다.
#include <iostream>
#include <cstdlib>
#include <qosconfig.h>
#include <dlfcn.h>
#include <vector>

using namespace std;

typedef char *(*Function)();
int main(int argc, char *argv[])
{
Config *agentCfg;
int rtv;
char *key;
char *value;
void *handle;
agentCfg = new Config();
Function myFunc;
// 플러그인을 저장할 Vector로 함수 포인터를 원소로 가진다.
vector<Function> FuncPList;

// 설정파일을 Open 한다.
rtv = agentCfg->openCfg("config.cfg");
if (rtv == -1)
{
perror("Config Read Error");
}

// 플러그인 섹션에서 플러그인 목록을 읽어온다.
if (agentCfg->findSection("PLUGIN"))
{
while(key = agentCfg->NextItem())
{
printf("Loding Module %s:%sn", key, agentCfg->NextValue());
// 플러그인을 로드한다.
handle = dlopen(agentCfg->NextValue(), RTLD_NOW);
if (!handle)
{
fputs(dlerror(), stderr);
}
else
{
// 공통인터페이스인 Read 함수를 함수포인터로 얻어오고
// vector에 push 한다.
myFunc = (char *(*)())dlsym(handle, "Read");
FuncPList.push_back(myFunc);
}
}
}

// 1초 간격으로 로딩된 플러그인 모듈로 부터, 데이터를 읽어온다.
while(1)
{
for (int i = 0; i < FuncPList.size(); i++)
{
printf("%s",FuncPList());
}
printf("==============n");
sleep(1);
}
return EXIT_SUCCESS;
}

모듈 프로그램

이 프로그램은 dummy 프로그램으로, 공통 인터페이스 포멧에 맞는 문자열을 리턴한다.
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

char *rtvstr = NULL;
int count;
int Init()
{
rtvstr = (char *)malloc(80);
count = 0;
return 1;
}

char *Read()
{
sprintf(rtvstr, "%s=%dn", count);
count++;
}

int RowNum()
{
return 1;
}

int Close()
{
if (rtvstr != NULL)
free(rtvstr);
count = 0;
}
이 코드는 공유라이브러리로 컴파일을 한다. 라이브러리의 이름은 libdummy.so 로 하겠다. 공유라이브러리를 만드는 방법은 라이브러리 만들기 문서를 참고하기 바란다.

이제 아래와 같은 설정파일 만들고, 실행하면 된다.
[PLUGIN]
DUMMY=libdummy.so

결론

동적라이브러리를 통한 확장가능한 프로그램을 만드는 방법에 대해서 알아보았다. 이 경우에는 데이터를 단순하게 제한했기 때문에, 비교적 손쉽게 인터페이스를 설계할 수 있었지만 실제 프로젝트에 도입해서 사용할때는 XML을 이용하거나, 데이터 처리 함수를 만들어야 하는 등, 좀더 복잡하게 구현될 수 있을 것이다.
크리에이티브 커먼즈 라이센스
Creative Commons License
이올린에 북마크하기
Posted by 소리나는연탄.
TAGS ,

Leave your greetings here.

  
  
  
  
  
  
  
  
 

Unix Signal 처리

2008/02/03 00:31 / Resource
SIGPIPE signal 처리

* Scenario: server가 다른 작업을 수행중인 동안 client가 종료한 경우 client는 server로 FIN packet을 전송한다. server가 client가 종료한 것을 모르는 상태에서 socket에 write를 하는경우 client쪽 TCP는 RST packet을 전송한다. server가 RST packet을 수신한 상태에서 다시 socket에 write를 하는 경우 SIGPIPE signal을 받게된다.

* FIN packet을 수신한 상태에서 socket을 read 하는 경우: EOF(0)
* RST packet을 수신한 상태에서 socket을 read 하는 경우: error (ECONNRESET)

* FIN packet을 수신한 상태에서 socket에 write를 하는 경우는 error가 아니지만 RST packet을 수신한 상태에서 socket에 write를 하는 경우는 error (EPIPE)

* SIGPIPE signal 처리
- SIGPIPE signal을 ignore (SIG_IGN)한다.
- write에서 return 되는 error (EPIPE)를 처리한다.



SIGCHLD signal 처리
* fork를 사용하여 child process를 생성하는 경우 parent process는 child process를 wait하여 child process가 zombie가 되는 것을 막아야 한다. 일반적으로 SIGCHLD 처리 함수안에서 waitpid 함수를 사용하여 child process가 zombie가 되는 것을 막는다.

* waitpid함수의 pid value를 -1을 사용하면 종료된 첫번째 child process를 wait하며 WNOHANG option을 사용하면 종료된 child process가 없더라도 waitpid 함수에서 block되는 것을 막을 수 있다.

* accept, read, write, select, open 과 같은 함수를 호출하는 중간에 SIGCHLD signal을 받아 interrupt된 경우 EINTR errno를 받게되며 이러한 경우 다시 위의 함수를 호출하면 된다. 하지만, connect 함수를 호출하는 중간에 interrupt된 경우 socket함수부터 다시 시작해야 한다.

* accept 함수를 사용하는 경우 block되는 것을 방지하기 위해서는 listening socket을 nonblocking으로 setting하며 EWOULDBLOCK, ECONNABORTED, EPROTO, EINTR error는 무시한다.

* 출처: UNIX Network Programming V.1 by W. Richard Stevens




◈ SIGHUP
- 터미널 인터페이스에 의해 연결의 단절이 감지되면 해당 제어 터미널과 연결된 제어 프로세스(세션 리더)에게 전달
- 세션 리더가 종료했을 때도 발생하는데 이때는 foreground 프로세스 그룹내의 모든 프로세스들에게 전달
- SIGHUP 시그널은 원래 모뎀 연결 끊김 등의 시리얼 라인이 끊어지면 발생하는 시그널이다.
- 이름 있는 시스템 데몬들은 SIGHUP 시그널을 configure file을 다시 읽어들이는 등의 초기화 신호로 해석한다.
  . bootp(8), gated(8), inetd(8), mountd(8), named(8), nfsd(8), ypbind(8)
  . pppd(8) 처럼 SIGHUP을 원래의 의도에 충실하게 세션 종료의 뜻으로 받아들이는 사례도 간혹 있는데, 요새는 보편적으로 이 역할을 SIGTERM이 맡는다.
  . daemon은 제어 단말기 없이 돌기 때문에 kernel로부터 SIGHUP 신호를 수신하지 못한다.
    그러므로 많은 daemon은 이 신호를 daemon의 구성 파일이 변경되어 daemon이 그 파일을 새로 읽어야 된다는 것을 알려주는 관리자로부터의 통지로 사용한다.
  . daemon이 수신할 수 없는 다른 두 개의 신호로 SIGINT와 SIGWINCH가 있고 이들도 역시 어떤 변화를 daemon에게 통지하기 위해 사용될 수 있다.

◈ SIGINT
- 인터럽트 키 (DELETE 또는 Control-C)가 눌렸을 때 발생

◈ SIGQUIT
- Control-backslash 에 의해 발생

◈ SIGCHLD
- 프로세스가 종료하거나 정지하면, 부모 프로세스에게 전달된다.
- 부모 프로세스는 wait() 시스템 콜을 사용하여 무슨 일이 일어났는지 알아본다.
- 이 시그널에 대한 default 처리는 무시하는 것이다. 즉 프로세스가 이 신호를 받으려고 할 때만 전달된다.

◈ SIGSEGV
- 유효하지 않은 가상 메모리 주소를 참조하거나 사용 권한이 없는 메모리에 접근할 때 프로세스로 전달된다.

◈ SIGTERM
- kill 명령에 의해 기본적으로 발생

◈ SIGKILL
- "극단의 조치(extreme prejudice)"로 프로그램을 종료하는 데 사용된다.
- 시그널 catch 하거나 무시할 수 없다.

◈ SIGALRM
- alarm()이나 setitimer() 시스템 콜로 설정한 알람 시간이 초과 했을 때 프로세스로 전달된다.

◈ SIGTSTP
-
Control-Z 키에 의해 발생
- 기본 처리 방법은 SIGCONT 신호를 받을 때까지 프로세스를 중단한다.

◈ SIGCONT
-
정지한 프로세스를 계속 실행시키려 할 때 발생
- 이 신호는 받을 수 있지만 블록하거나 무시할 수 없다.
- 기본 처리 방법은 중단된 프로세스를 재시작하는 것이다. 그러나 프로세스가 신호를 받지 않는다면 신호를 버린다.
- vi 에디터를 사용할 때
. Control-Z 를 눌러 수행을 잠시 정지시키면 쉘이 키 입력을 처리하게 되는데
. 이때 fg 명령을 실행시키면 쉘은 vi 에게 SIGCONT 시그널을 전달하며
. vi는 이 시그널에 대한 처리로 화면을 다시 그리고 사용자 키 입력을 받는 상태로 돌아간다.

◈ SIGSTOP
-
SIGTSTP과 동일하나 catch 하거나 무시할 수 없다.
- 이 신호를 받으면 무조건 SIGCONT 신호를 받을 때까지 프로세스를 중단한다.

◈ SIGABRT
-
abort() 함수의 호출로 발생

◈ SIGBUS
-
하드웨어 결함으로 발생

◈ SIGEMT
-
하드웨어 결함으로 발생

SIGFPE
-
divide-by-0나 부동 소숫점 오버플로우와 같은 산술 연산 오류에서 발생

◈ SIGILL

◈ SIGINFO

◈ SIGIO

◈ SIGIOT

◈ SIGPIPE
-
pipe 통신에서 수신 프로세스가 종료했을 때 송신 프로세스가 파이프에 write 하면 발생
- 프로세스가 RST를 받은 소켓에 데이터를 쓰면, 커널은 그 프로세스에 ISGPIPE 신호를 보낸다.
- 이 신호의 기본 동작은 프로세스를 종료시키는 것이므로, 프로세스가 원하지 않는 종료를 피하기 위해서는 이 신호를 포착해야 한다.

◈ SIGPOLL

◈ SIGROF

◈ SIGPWR

◈ SIGSYS

◈ SIGTTIN
-
background에 있는 프로세스가 제어 터미널로부터의 읽기를 시도한다.

◈ SIGTTOU
-
background에 있는 프로세스가 제어 터미널로부터의 쓰기를 시도한다.

◈ SIGURG
-
SIGIO와 SIGURG 라는 두 개의 신호는 소켓이 F_SETOWN 명령으로 소유주에게 할당되었을 때만 소켓에 대해 발생한다.

◈ SIGUSR1

◈ SIGUSR2

◈ SIGVTALRM

◈ SIGWINCH

◈ SIGXCPU

◈ SIGXFSZ
크리에이티브 커먼즈 라이센스
Creative Commons License
이올린에 북마크하기
Posted by 소리나는연탄.
TAGS ,

Leave your greetings here.

  
  
  
  
  
  
  
  
 

Contents

1 메시지 큐란
2 메시지큐의 생성, 사용, 제어
3 메시지큐 생성
4 메시지큐에 데이타 쓰기
5 메시지큐의 데이타 가져오기
6 예제를 통해 알아본 메시지큐
7 메시지큐의 제어
8 정리
9 참고문서

1 메시지 큐란

메시지큐는 메시지를 queue 데이타 구조 형태로 관리한다. 는 선입선출(먼저 들어간게 먼저 나오는) 데이타 구조를 말하며, 보통의 은행창구 혹은 일반적인 줄서기를 생각하면 된다. 이것은 흔히 FIFO(First in First Out)라고 불리운다(IPC 의 FIFO 설비와 혼동하지 말자). 이것과 반대되는 데이타 구조를 stack 이라고 하며, 큐와 반대로 가장 나중에 들어온게(가장 최근데이타) 먼저 나오는 형태를 가진다.

일반적인 배열을 접근방법에 따라 특수하게 분류한것이라고 생각하기 바란다.

메시지큐는 커널에서 전역적으로 관리되며(이를테면 커널 전역변수형태로), 모든 프로세스에서 접근가능하도록 되어있으므로, 하나의 메시지큐 서버가 커널에 요청해서 메시지큐를 작성하게 되면, 메시지큐의 접근자(식별자)를 아는 모든 프로세서는 동일한 메시지큐에 접근함으로써, 데이타를 공유할수 있게 된다.

메시지큐의 IPC로써의 특징은 다른 공유방식에 비해서 사용방법이 매우 직관적이고 간단하다라는데 있다. 다른 코드의 수정없이 단지 몇줄만의 코드를 추가시킴으로써 간단하게 메시지큐에 접근할수 있다.

또한 뒤에서 자세히 다루겠지만 메시지의 type 에 의해서 여러종류의 메시지를 효과적으로 다룰수 있는 장점을 가지고 있다. 여러개의 프로세스가 하나의 메시지큐를 엑세스 할때, 각 메시지에 type 를 줌으로써 각 프로세스에게 필요로하는 메시지만을 가져가게 할수 있는 상당히 편리한 기능을 제공한다.

또한 구조체를 몽땅 넘길수 있고, 이는 데이타의 사용을 매우 편하게 만들어준다.

2 메시지큐의 생성, 사용, 제어

메시지큐를 생성하고, 이를 이용 및 조작하기 위해서 Unix 시스템은 다음과 같은 함수들을 제공한다. 앞으로의 설명은 아래의 함수들을 기준으로 이루어지게 될것이다.

#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/msg.h> 
 
int msgget (key_t key, int msgflg) 
int msgsnd (int msqid, struct msgbuf *msgp, size_t msgsz, int msgflg) 
ssize_t msgrcv (int msqid, struct  msgbuf  *msgp,  size_t msgsz,  
                long msgtyp, int msgflg)  
 
Unix 커널은 메시지큐 정보를 유지하기 위해서 msqid_ds 라는 별도의 구조체를 유지한다. msqid_ds 구조체는 /usr/include/bits/msq.h 에 선언되어 있으며 대충 다음과 같은 구조를 가진다

이건 linux os 기준이며, Unix 에 따라 약간씩 다를수 있다
struct msqid_ds 
{ 
struct ipc_perm msg_perm; /* structure describing operation permission */ __time_t msg_stime; /* time of last msgsnd command */ unsigned long int __unused1; __time_t msg_rtime; /* time of last msgrcv command */ unsigned long int __unused2; __time_t msg_ctime; /* time of last change */ unsigned long int __unused3; unsigned long int __msg_cbytes; /* current number of bytes on queue */ msgqnum_t msg_qnum; /* number of messages currently on queue */ msglen_t msg_qbytes; /* max number of bytes allowed on queue */ __pid_t msg_lspid; /* pid of last msgsnd() */ __pid_t msg_lrpid; /* pid of last msgrcv() */ unsigned long int __unused4; unsigned long int __unused5; };
최초 msgget 를 이용해서 커널에 메시지큐를 요청하면, 커널은 해당 메시지큐를 위해 메모리를 할당하고, 메모리 관리와 그밖의 정보 관리를 위해 위의 구조체를 세팅하게 된다.

3 메시지큐 생성

메시지큐의 생성과 기존에 있던 메시지큐의 참조는 msgget(2) 를 이용해서 이루어진다. 첫번째 아규먼트인 key 는 kernel 에서 유일한 메시지큐를 만들고 참조하기 위해서 사용하는 식별번호이며, msgflg 는 메시지큐를 어떻게 생성하고 참조할지 행동양 식을 정해주기 위한 아규먼트이다. key 는 적당하게 유일한 int 형의 숫자를 정해주면 된다. msgflg 에는 IPC_CREAT와 IPC_EXCL등의 시작동작을 정해줄수 있으며, 퍼미션을 지정해 줄수도 있다.
IPC_CREAT
메시지큐를 새로 생성하기 위해서 사용한다. 만약 기존에 key 로 생선된 메시지큐가 있다면 해당 메시지큐의 식별자를 되돌려준다.
IPC_EXCL
IPC_CREAT 와 함께 쓰이며, IPC_EXCL이 지정되어 있을경우 이미 key 로 존재하는 메시지큐가 있다면, -1 을 리턴하고 errno 를 세팅한다.

msgget 는 성공할경우 메시지큐에 접근할수 있는 int 형의 메시지큐 식별자를 되돌려주며, 이후로는 이 메시지큐 식별자를 통해서 필요한 작업을 하게 된다.

4 메시지큐에 데이타 쓰기

메시지를 보내기 위해서는 msgsnd(2) 를 사용한다. 첫번째 아규먼트는 msgget 를 통해서 얻어온 메시지큐 식별자이며, 2번째 아규먼트는 메시지큐에 넘기고자하는 구조체, 3번째 아규먼트는 2번째 아규먼트인 구조체의 크기, 마지막 아규먼트는 메시지전달 옵션으로 봉쇄할것인지 아니면 비봉쇄로 메시지를 결정하기 위해서 사용된다.

2번째 아규먼트가 메시지큐로 전달할 메시지라고 했는데, 이것은 구조체로 전달 되며, 다음과 같은 모습을 가지게 된다.
struct msgbuf 
{ 
    long mtype; 
    char mtext[255]; 
} 
 
위의 모습은 메시지 구조체의 매우 전형적인 모습으로 사실멤버변수는 필요에 따라서 얼마든지 변경될수 있다. 다만 long mtype 만이 필수요소이다.

mtype 는 메시지의 타입으로 반드시 0보다 더큰 정수이여야 한다. 우리는 이 mtype 를 각각 다르게 줌으로써, 특정 프로세스에서 특정 메시지를 참조할수 있도록 만들수 있다. 예를 들어 A 라는 프로세스가 A 라는 메시 타입을 참조해야 하고 B 는 B 로 참조하도록 만들어야 한다면, msgbuf 를 만들때, mtype 에 A 은 1 B 은 2 이런식으로 메시지 타입을 정의 하고 A 는 mtype 가 1인것을 B는 mtype 이 2인것을 가지고 가도록 만들면 된다.

Upload new Attachment "queue.png"

위의 그림에서 처럼 mtype 을 이용해서 자신이 원하는 메시지에만 선택 적으로 접근이 가능하다. 이특성을 잘 이용하면 매우 유용하게 사용할수 있을것이다.

msgsz 은 구조체의 크기이니 그냥 넘어가고, msgflg 에 대해서 설명하겠다. msgflg 에는 IPC_NOWAIT를 설정할수 있으며 이 값을 이용해서 봉쇄형으로 할것인지 비봉쇄형으로 할것인지 결정할수 있다. IPC_NOWAIT를 설정하면, 메시지가 성공적으로 보내지게 될때까지 해당영역에서 봉쇄(block)되며, 설정하지 않을경우 에는 바로 return 하게 된다.

5 메시지큐의 데이타 가져오기

데이타는 msgrcv(2) 함수를 이용해서 가져올수 있다. 1번째 아규먼트는 메시지큐 식별자이며, 2번째가 가져올 데이타가 저장될 구조체, 3번째는 구조체의 크기, 4번째는 가져올 메시지 타입, 5번째는 세부 조종 옵션이다.

다른것들은 굳이 설명할 필요가 없는 간단한 것들이고, 다만 4번째 메시지 타입인 msgtyp에 대해서 상세히 설명하고, msgflg 를 간단히 설명하는 정도로 넘어가도록 하겠다. 우리는 메시지를 보낼 구조체를 만들때 mtype 라는것을 정의 해서, 메시지를 분류해서 접근할수 있도록 한다는것을 알고 있다. 메시지를 가져올때는 바로 msgtyp 를 통해서 자기가 원하는 msgtyp 의 메시지 구조체에 접근할수 있게 된다.
msgtyp == 0
메시지 큐의 첫번째 데이타를 돌려준다.
msgtyp > 0
메시지의 mtype 가 msgtyp 와 같은 첫번째 데이타를 돌려준다.
msgtyp < 0
메시지의 mtype 이 msgtyp 의 절대값보다 작거나 같은 첫번째 데이타를 돌려준다.

msgflg 는 msgrcv 의 메시지 가져오는 형태를 봉쇄로 할것인지 비 봉쇄로 할것인지 지정하기 위해서 사용한다. IPC_NOWAIT 를 설정할경우 가져올 메시지가 없더라도 해당 영역에서 봉쇄되지 않고 바로 error 코드를 넘겨주고 리턴한다.


6 예제를 통해 알아본 메시지큐

여기에는 총 2개의 예제프로그램이 만들어진다. 하나는 메시지큐 생산자로써, 메시지큐를 생성하고 메시지를 만들어서 메시지큐에 보내는(msgsnd) 일을 하고 다른 하나는 소비자로써 메시지큐에 있는 데이타를 받아오는 일을 한다. 다음은 메시지큐 생산자 이다.
#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/msg.h>  
#include <sys/stat.h>  
 
struct msgbuf 
{ 
    long msgtype; 
    char mtext[256]; 
    char myname[16]; 
    int  seq; 
}; 
 
int main() 
{ 
    key_t key_id; 
    int i; 
    struct msgbuf mybuf, rcvbuf; 
 
    key_id = msgget((key_t)1234, IPC_CREAT|0666); 
    if (key_id == -1) 
    { 
        perror("msgget error : "); 
        exit(0); 
    } 
 
    printf("Key is %d\n", key_id); 
 
    memset(mybuf.mtext, 0x00, 256);  
    memset(mybuf.myname, 0x00, 16);  
    memcpy(mybuf.mtext, "hello world 4", 13); 
    memcpy(mybuf.myname, "yundream", 8); 
    mybuf.seq = 0; 
    i = 0; 
 
    while(1) 
    { 
        // 짝수일경우 메시지 타입이 4 
        // 홀수일경우에는 메시지 타입이 3 
        if (i % 2 == 0) 
            mybuf.msgtype = 4; 
        else  
            mybuf.msgtype = 3; 
        mybuf.seq = i; 
 
        // 메시지를 전송한다.  
        if (msgsnd( key_id, (void *)&mybuf, sizeof(struct msgbuf), IPC_NOWAIT) == -1) 
        { 
            perror("msgsnd error : "); 
            exit(0); 
        }  
        printf("send %d\n", i); 
        i++; 
        sleep(1); 
    } 
 
    printf("%d \n", rcvbuf.msgtype); 
    printf("%s \n", rcvbuf.mtext); 
    printf("%s \n", rcvbuf.myname); 
    exit(0); 
} 
 
프로그램은 간단하다. mybuf 란 구조체를 만들어서 메시지를 전송하는데, 이때 메시지 타입을 i % 2 가 0일경우 4로 그렇지 않을경우 3으로 해서 전송을 하도록 만들었다.

#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/msg.h>  
#include <sys/stat.h>  
struct msgbuf 
{ 
    long msgtype; 
    char mtext[256]; 
    char myname[16]; 
    int  seq; 
}; 
 
int main(int argc, char **argv) 
{ 
    key_t key_id; 
    struct msgbuf mybuf; 
    int msgtype; 
 
    // 아규먼트가 있을경우 msgtype 가 3인 메시지를 받아오고(홀수)  
    // 아규먼트가 없을경우 msgtype 가 4인 메시지를 받아온다(짝수)   
    if (argc == 2) 
        msgtype = 3; 
    else  
        msgtype = 4; 
 
    key_id = msgget(1234, IPC_CREAT|0666); 
    if (key_id < 0) 
    { 
        perror("msgget error : "); 
        exit(0); 
    } 
    while(1) 
    { 
        if (msgrcv( key_id, (void *)&mybuf, sizeof(struct msgbuf), msgtype, 0) == -1) 
        { 
            perror("msgrcv error : "); 
            exit(0);     
        } 
        printf("%d\n", mybuf.seq); 
    } 
    exit(0); 
} 
 
이 예제는 더 간단하다. 아규먼트가 있으면 메시지타입이 3인 메시지를 아규먼트가 없으면 메시지 타입이 4인 메시지를 가져오도록 한다.

프로그램을 컴파일후 테스트를 해보면 ./msgrcv 1을 (아규먼트를 주고 실행) 실행시키면 msgtype 가 4인 메시지를 받아오고 그렇지 않을경우 msgtype 가 3인 메시지를 받아옴을 알수 있을것이다. ./msgsnd, ./msgrcv, ./msgrcv 1 을 동시에 띄워서 테스트하면 된다.

7 메시지큐의 제어

msgctl(2)함수를 이용한다. 첫번째 아규먼트인 msqid 는 메시지 식별자이며, 2번째 아규먼트인 cmd 는 해당 작동명령, 그리고 마지막 아규먼트는 msqid_ds 구조체 이다. 우리는 cmd 를 통해서 해당 메시지식별자가 가르키는 메시지큐를 제어할수 있다. cmd 에는 아래와 같은 종류의 명령을 사용할수 있다.

IPC_STAT
메시지큐의 정보를 원할때 사용한다. 해당 메시지큐의 정보는 3번째 아규먼트인 msqid_ds 구조체를 통해 넘어오게 된다.

IPC_SET
msqid_ds 구조체 정보를 변경하고자 할때 사용한다. 주로 퍼미션 정보를 바꾸기 위해서 사용한다.
IPCRMID
현재 메시지큐를 제거한다.

8 정리


이상 메시지큐에 대해서 간단히 알아보았다. 지금까지 설명에서 처럼 메시지큐는 내부 프로세스간 통신을 위한 상당히 유연한 방법을 제공하고 있음을 알수 있다. 반면 단점이 있는데, 제어하기가 상당히 까다롭다는 점이다.

우선 메시지큐에 들어갈수 있는 데이타의 수가 고정되어 있는데, 메시지큐가 어떤 이유로 꽉찼을 경우 이를 알수 있는 방법이 애매하다. 위의 예제에서 ./msgrcv 와 ./msgrcv 1 이 메시지를 계속적으로 소비하도록 되어 있는데, 만약 둘중 하나가 이상작동을 해서 메시지를 받아오지 못할경우 결국 메시지큐가 꽉 차버리게 되고, 더이상 정상적인 작동을 못하게 될것이다. 또한 커널의 영향을 많이 받으며, 잘못된 메시지큐의 사용은 전체 시스템에 영향을 미칠수도 있게 만든다. 이는 전체 시스템에서 사용할수 있는 메시지큐의 수와 크기에 제한이 있기 때문으로, 메시지큐를 사용하기 위해서는 조심해서 사용해야될 필요성이 있다.

또한 커널은 몇개의 프로세스가 현재 메시지큐를 참조하는지를 알려주는 참조계수를 제공하지 않는다. 그러므로 프로세스에 어떤 문제가 생겼을때, 해당 프로세스에 정확하게 어떤 문제가 발생했는지 알아내는게 상당히 까다롭다.

그러므로 메시지큐를 짧은 시간에 다량의 정보를 전달하기 위한 목적으로 사용하는 데에는 적당치 않다. 그리 많지 않은 정보를 프로세스간 교환하기 위한 용도로 사용하기에 적당한 IPC 설비이다.

9 참고문서



출처 : 조인시 위키

크리에이티브 커먼즈 라이센스
Creative Commons License
이올린에 북마크하기
Posted by 소리나는연탄.

Leave your greetings here.