클라이언트와 데이타를 주고 받는 전문서버로
오라클 10g를 사용하고, TMax Proframe을 이용하고 있는데
상담이력조회에서 조건 추가를 해달라는 현업부서요청에 따라
바인딩 변수를 하나 추가를 하였다..
분명 같은 쿼리 조건으로 오렌지상에서 실행시 플렌은 분명히 인덱스를
타는것을 확인 했으나, 실제 클라이언트를 통하여 전문서버에서 실행되었을때는
이상하게 속도가 늦게 나오는 현상발생, 인덱스힌트를 주고도 해보고, 이틀동안 삽질.
나와 비슷하게 고민한 글을 접하고 원인을 찾았음( 뭐 다 오라클을 제대로 알지 못해서 그런거 ㅡ.ㅡ;)
요지는 10g 바인딩 변수 사용시 데이타 양에따라 10g 내부적으로 사용되어지 인덱스가 달라진다
즉 바인드 변수에 NULL값 입력시 전체를 대상으로 조회하라는 부분이 문제였다.(바인드-피킹(bind-peeking) )
SELECT
...(중략)
WHREE
...(중략)
AND ( (:cntct_p_eno IS NULL) OR (S1.CNTCT_P_ENO=:cntct_p_eno) ) -- NULL입력시 전체대상
AND ( (:cs_no IS NULL) OR (S1.CS_NO=:cs_no) ) -- NULL입력시 전체대상
해결 특정인, 전체 대상의 두개의 조건으로 쿼리를 나누고 런타임시에 두 쿼리중 하나를 사용하도록 결정
참고: http://blog.naver.com/hypermin?Redirect=Log&logNo=70027026575
26 Articles, Search Results for 'Resource
21 시그널 처리
- 21.1 시그널들의 기본 원칙
- 21.2 표준 시그널들
- 21.3 시그널 동작 정하기
- 21.4 시그널 핸들러 정의하기
- 21.5 시그널에 의해 인터럽트된 기본동작 ( Primitives )
- 21.6 시그널 발생시키기
- 21.7 시그널 블록하기
- 21.8 시그널을 위한 기다림
- 21.9 BSD 시그널 핸들링
- 21.10 핸들러 함수를 만들기 위한 BSD 함수
시그널(앞으로 신호라 해석하지 않고 시그널이라고 하겠습니다. 그것이 더 좋을 것 같아서. . . )은 프로세스에게 배달된 소프트웨어 인터럽트이다. 운영체제는 실행하고 있는 프로그램에 예외적인 상황을 보고하기 위해서 시그널을 사용한다. 어떤 시그널들은 유용하지 않은 메모리 주소를 참조하는것과 같은 에러를 보고하고; 다른 것은 전화선의 단절과 같은, 비동기적 사건을 보고한다.
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
FPE_INTDIV_TRAP
FPE_SUBRNG_TRAP
FPE_FLTOVF_TRAP
FPE_FLTDIV_TRAP
FPE_FLTUND_TRAP
FPE_DECOVF_TRAP
매크로 : int SIGILL
매크로 : int SIGSEGV
매크로 : int SIGBUS
매크로 : int SIGABRT
이들 시그널들은 이런 저런 방법으로 프로세스를 종료함을 알리기위해 사용된다. 그들은 완전히 다른 목적을 위해 사용되기 때문에 다른 이름을 가졌고, 프로그램은 그들은 다르게 취급하기를 원할 것이다. 이들 시그널들은 처리하기 위한 이유는 보통 당신의 프로그램이 실제로 종료되기전에 적당하게 처리할 수 있도록 하기 위한 것이다. 예를 들어, 당신은 상황정보를 저장하고, 임시 파일들을 지우고, 이전의 터미널 모드를 반환하기를 원할수도 있다. 그와 같이 핸들러(handler)는 발생된 시그널을 위한 디폴트 동작을 지정하고 그리고 그 시그널을 다시 발생시킴으로써 종료할 것이다. 이것은 만일 프로그램이 핸들러를 가지지 않았더라도, 그 시그널로 인해서 프로그램이 종료될 것이다. ( 21. 4. 2절 [Termination in Handler] 참조. ) 이 시그널들을 위한 (명백한) 디폴트 동작은 프로세스가 종료되도록 하는 것이다. 매크로 : int SIGHUP
매크로 : int SIGINT
매크로 : int SIGQUIT
매크로 : int SIGTERM
매크로 : int SIGKILL
그들 시그널은 타이머의 경과를 지적하는데 사용되어진다. 이들 시그널을 보내는 함수에 대한 정보는 17. 3절 [Settin an Alarm] 참조. 그들 시그널을 위한 디폴트 동작은 프로그램을 종료를 일으키는 것이다. 이 디폴트 동작은 거의 유용하지 않다; 그들 시그널을 사용하는 대부분의 방법은 어느 경우에 맞는 핸들러 함수들을 요구하는 것이다. 매크로 : int SIGALRM
매크로 : int SIGVTALRM
매크로 : int SIGPROF
이 절에 설명된 시그널들은 비동기 입/출력 도구들과 함께 사용되어진다. 당신은 어떤 특정한 파일 기술자가 그들 시그널을 발생시키도록 하기 위해서 fcntl을 호출함으로써 명백한 동작을 취하도록 해야한다( 8. 12절[Interrupt Input] 참조. ) 그들 시그널을 위한 디폴트 동작은 그들을 무시하는 것이다. 매크로 : int SIGIO
매크로 : int SIGURG
이들 시그널은 작업 제어를 지원하기 위해서 사용되어진다. 만일 당신의 시스템이 작업 제어를 지원하지 않는다면 시그널들은 발생되어지거나, 처리될 수는 없지만 매크로들은 정의되어있다. 당신이 실제로 작업이 어떻게 제어되는지를 이해할 수 없다면 그들 시그널을 그대로 방치할 것이다. 24장 [Job Control] 참조. 매크로 : int SIGCHLD
매크로 : int SIGCONT
매크로 : int SIGSTOP
매크로 : int SIGTSTP
매크로 : int SIGTTIN
매크로 : int SIGTTOU
그들 시그널은 다양한 다른 상활들을 보고하기 위해서 사용되어진다. 이들의 디폴트 동작은 프로세스가 종료되도록 하는 것이다. 매크로 : int SIGPIPE
매크로 : int SIGUSR1 매크로 : int SIGUSR22
특정한 운영체제는 위에 설명되지 않은 부가적인 시그널들을 지원한다. ANSI C 표준은 시그널들의 명칭을 `SIG'로 시작하는 대문자로 예약하였다. 당신은 당신의 특정한 운영체제를 위한 헤더파일이나 그 운영체제가 지원하고 있는 시그널을 발견하는 프로세서의 타입 등에 대한 것에 조언을 구할 수 있다. 예를 들어, 어떤 시스템은 하드웨어 트랩에 해당하는 여분의 시그널들을 제공한다. 보통 지원되는 어떤 다른 종류의 시그널들은 CPU 시간과 파일 시스템 사용에 대한 제한을 가하거나, 터미널 구성을 비동기적으로 변경하는 것과 같은 것을 위해 사용되어진다. 시스템들은 또한 표준 시그널 이름의 별칭(aliases)이 되는 시그널 이름들을 정의하고 있다. 당신은 당신이 이해하고 있는 정의된 시그널들을 위한 디폴트 동작을 (또는 쉘에 의해 작동하는 동작) 가정할 수 있고, 당신은 그들에 대해서는 걱정하지 않는다. 당신이 그 시그널의 의미를 알지 못하는 것에 대해서 핸들러를 만들려 시도하거나 알지 못하는 시그널을 무시하거나 블록하는 것은 좋지 못한 생각이다. 여기에 일반적으로 운영체제에서 사용되고 있는 약간의 다른 시그널에 대한 것이 있다. SIGCLD
SIGTRAP
SIGIOT
SIGEMT
SIGSYS
SIGPOLL
SIGXCPU
SIGXFSZ
SIGWINCH
우리는 자식 프로세스를 종료한 시그널을 설명하는 메시지를 쉘이 프린트하는 것에 대해서는 위에서 잠깐 언급했다. 시그널을 설명하는 메시지를 프린트하는 깨끗한 방법은 strsignal 과 psignal 함수들을 사용하는 것이다. 그들 함수들은 설명하려는 시그널의 종류를 지정하기 위해서 시그널 번호를 사용한다. 시그널번호는 자식 프로세스의 종료상황으로부터(23. 6절 [Process Compltion] 참조)오거나 또는 같은 프로세스 안에 있는 시그널 핸들러로부터 올 것이다. 함수 : char * strsignal (int signum)
함수 : void psignal (int signum, const char *message)
시그널을 위한 동작을 변경하기 위한 가장 간단한 방법은 signal함수를 사용하는 것이다. 당신은 내장된(built-in) 동작을 지정하거나, 핸들러를 만들 수 있다. GNU 라이브러리는 또한 좀더 다양한 기능을 가진 sigaction 도구를 사용한다. 이 절은 두 개의 도구들에 대한 설명과 언제 이것을 사용할 지에 대한 제안을 한다. signal 함수는 특정한 시그널을 위한 동작을 만들기 위한 간단한 인터페이스를 제공한다. 그 함수와 연관된 매크로들은 헤더파일 `signal. h'에 선언되어 있다. 데이터 타입 : sighandler__t
void handler (int signum) { . . . }
함수 : sighandler_t signal (int signum, sighandler_t action)
SIG_DFL
SIG_IGN
handler
EINVAL
주어진 시그널이 무시되도록 미리 설정되어졌다면, 이 코드는 그 설정을 바꾸는 것을 피함을 기억하라. 이것은 비-작업-제어 쉘들이 자식 프로세스가 시작될 때 어떤 시그널들을 종종 무시하기 때문이고, 그리고 이것을 고려하는 것은 자식 프로세스에게는 중요하다. 우리는 이 예제 프로그램이 디버깅을 위해서(코어 덤프 파일) 정보를 제공하도록 만들어졌기 때문에 프로그램 에러 시그널이나 SIGQUIT를 처리하지 않고, 임시 파일들은 유용한 정보를 가질 것이다. 함수 : sighandler_t ssignal (int signum, sighandler_t action)
매크로: sighandler_t SIG__ERR
sigaction 함수는 signal과 같은 기본 효과를 갖는다: 한 시그널이 어떻게 프로세스에 의해 처리될 것인지를 정하는. sigaction은 조금은 복잡하지만, 더 많은 제어를 제공한다. 특별하게, sigacion은 시그널이 언제 발생되고 어떻게 그 핸들러가 호출될 것인지에 대해 제어를 할 수 있는 부가적인 플래그를 지정하도록 허용한다. sigaction 함수는 `signal. h'에 선언되어 있다. 데이터 타입 : struct sigaction
sighandler_t sa_handler
sigset_t sa_mask
int sa_flags
함수 : int sigaction(int signum, const struct sigaction *action, struct sigaction *old_action)
EINVAL
한 단일한 프로그램 안에서 signal 과 sigaction 함수들을 모두 사용하는 것이 가능하지만, 그들은 완전히 다른 방법들로 서로 영향을 미칠 수 있기 때문에 주의해야만 한다. sigaction 함수는 signal 함수보다는 좀더 많은 정보를 지정하기 때문에, signal로부터의 반환값은 sigaction이 표현할 수 있는 범위를 표현할 수 없다. 그렇지만, 만일 당신이 어느 동작을 저장하고 나중에 다시 그 동작을 재건하기 위해서 signal을 사용한다면, sigaction을 통해서 만들어졌던 핸들러는 적당하게 재건되어질 수 없을 것이다. 이러한 문제를 피하기 위해서, 핸들러가 전혀 sigaction을 사용하지 않은 프로그램이라고 할 지하도 핸들러는 저장하고 반환하기 위해서는 항상 sigaction을 사용하라. sigaction이 좀더 일반적이기 때문에, 원래 siganl 또는 sigaction을 가지고 만들었는지에 상관없이, 어느 동작을 저장하고 재건하기 위해서 sigaction을 사용하는 것이 더 적당할 수 있다. 만일 signal을 사용해서 어떤 동작을 만들었고 그 다음 sigaction을 사용해서 그것을 시험한다면, 핸들러 주소는 당신이 signal에서 지정했던 것과 같지 않은 것을 얻게 될 것이다. 그것은 심지어 signal에 action인수로써 사용하기에도 적당하지 않게 될 것이다. 그러나 당신은 sigaction의 인수로써 그것을 사용하고 적용할 수 있다. 그래서, 단일한 프로그램 안에서는 시종일관 한가지 또는 다른 한가지의 메커니즘을 고수하는 것이 한결 더 낫다. 이식성 노트 : 기본 signal 함수는 ANSI C에서 지원되고, sigaction함수는 POSIX. 1 표준에서 지원하고 있다. 만일 당신이 비-POSIX 계열의 시스템과의 호환성에 관심이 있다면, 당신은 대신에 signal을 사용하라.
21. 3. 1절 [Basic Signal Handling] 에서, signal을 사용해서 종료 시그널들을 위한 간단한 핸들러를 만드는 예제를 보여주었다. 다음은 sigaction을 사용하는 같은 예제이다.
이 프로그램은 요구된 파라미터로 단지 new_action을 로드하고 sigaction을 호출할 때 그것을 인수로써 사용한다. sigemptyset의 사용에 대한 것은 나중에 설명되었다. 21. 7절 [Blocking Signals] 참조. signal을 사용하는 예제에서, 미리 무시되도록 설정된 시그널들을 처리하는 것을 피했다. 여기서 새로운 동작을 지정하지 않고 현재의 동작을 시험하는 것을 허용하는 sigaction을 사용해서, 순간적으로 시그널핸들러가 변경되는 것을 피할 수 있다. 다음은 다른 예제이다. 이것은 동작을 변경하지 않고 SIGINT를 위해서 현재의 동작에 대한 정보를 구한다.
sigaction 구조체의 멤버 sa_flags는 특별한 기능을 위한 것이다. 대부분의 경우에, SA_RESTART가 이 필드에서 사용하기에 가장 좋은 값이다. sa_flags의 값은 비트마스크로써 해석되어진다. 그래서, 당신은 당신이 원하는 플래그를 설정하거나 선택할 수 있고, sigaction 구조체의 sa_flags 멤버 안에 그 결과를 저장한다. 각각의 시그널 번호는 자신만의 플래그 설정을 갖는다. sigaction을 호출하는 것은 특정한 시그널 번호의 영향을 받고, 당신이 지정한 플래그들은 오직 특정한 시그널에만 적용된다. GNU C 라이브러리에서, signal로 핸들러는 만드는 것은 당신이 siginterrupt로 만들었던 설정에 의존한 값을 갖는 SA_RESTART를 제외하고는 모두 0으로 플래그를 설정한다. 21. 5절 [Interrupter Primitives] 를 참조하라. 이들 매크로들은 헤더파일 `signal. h'에 정의되어 있다. 매크로 : int SA__NOCLDSTOP
매크로 : int SA__ONSTACK
매크로 : int SA__RESTART
새로운 프로세스가 만들어질 때(23. 4절 [Creating a Process] 참조. ), 부모 프로세스로 부터 시그널들의 처리를 상속받는다. 그렇지만, 당신이 exec 함수(23. 5절 [Executing a File] 참조. )를 사용해서 새로운 프로세스 이미지를 로드할 때, 어느 시그널들은 그들의 원래의 동작으로 환원하도록 SIG_DFL을 사용해서 당신 자신의 핸들러를 정의해야만 한다. (만일 당신이 이것에 대해서 조금만 생각한다면, 이것을 이해할 수 있다; 원래의 프로그램으로부터 온 처리 함수들은 그 프로그램을 위한 정의이고, 새로운 프로그램 이미지의 주소 공간에는 심지어 존재하지도 않는다. ) 물론, 새로운 프로그램은 자신의 핸들러를 만들 수 있다. 어떤 프로그램이 쉘에 의해 실행될 때, 쉘은 보통 적당한 SIG_DFL 또는 SIG_IGN을 사용해서 자식 프로세스를 위한 초기동작을 설정한다. 그것은 당신이 새로운 시그널 핸들러를 만들기 전에 쉘이 SIG_IGN으로 초기동작을 준비하지 않은 것이 확실한지 체크하는 것은 좋은 생각이다. 다음은 현재 무시되지 않고 있는 SIGHUP 시그널을 위한 핸들러를 어떻게 만드는지에 대한 예제이다.
이 절은 signal 이나 sigaction 함수들을 사용해서 만들 수 있는 시그널 핸들러 함수를 어떻게 쓸것인지에 대해서 설명하고 있다. 시그널 핸들러는 프로그램의 다른 부분과 함께 컴파일하는 함수일 뿐이다. 직접적으로 그 함수를 불러내는 대신에, 시그널이 도착했을 때 그 핸들러를 호출하도록 운영체제에게 알리는 signal 이나 sigaction 함수를 사용한다. 이것이 핸들러를 만드는 것이다. 21. 3절 [Signal Actions] 참조. 다음은 당신이 시그널 핸들러 함수에서 사용할 수 있는 두 개의 기본 범주들이다. 어떤 전역 데이터 구조체에 의해서 도착된 시그널을 기록해두고 그 다음 정상적인 반환을 하는 핸들러 함수를 가질 수 있다. 프로그램을 종료시키거나 그 시그널의 원인이 된 상황으로부터 회복할 수 있는 지점으로 제어를 옮기는 핸들러 함수를 가질 수 있다. 당신이 핸들러 함수를 작성할 때 핸들러 함수는 비동기적으로 호출되어질 수 있기 때문에 각별한 주의가 필요하다. 즉, 핸들러함수는 예측할 수 없이, 프로그램의 어느 지점에서든지 호출되어질 것이다. 만일 매우 짧은 간격을 두고 두 개의 시그널이 도착한다면, 한 개의 핸들러는 다른 핸들러 안에서 실행할 수 있다. 이 절은 당신의 핸들러는 무엇을 하고, 무엇을 피해야 하는지를 설명한다. 정상적인 반환을 하는 핸들러는 SIGALRM 과 입/출력과 시그널을 사용한 프로세스간 통신과 같은 시그널에 보통 사용되어진다. 그러나 SIGINT를 위한 핸들러는 가능한 시간에 프로그램이 분기하도록 플래그를 설정한 후에 정상적으로 반환한다. 프로그램 에러 시그널을 위한 핸들러로부터 정상적인 반환을 하도록 하는 것은 안전하지 않다, 왜냐하면 핸들러 함수가 반환할 때 프로그램의 동작은 프로그램 에러 후에 무엇을 할 것인지 정의되지 않았기 때문이다. 21. 2. 1절 [Program Erroe Signals] 참조. 정상적으로 반환하는 핸들러들은 어떤 전역 변수를 갱신해야만 한다. 전형적으로, 그 변수는 정상적으로 작동하는 동안에 프로그램에 의해 주기적으로 시험되는 것이다. 데이터타입은 21. 4. 7절 [Atomic Data Access] 에서 설명된 sig_atomic_t가 된다. 다음은 한 개의 프로그램과 같은 간단한 예제이다. 이 프로그램은 SIGALRM 시그널이 도착한 것을 발견할 때까지 루프의 몸체를 실행한다. 이 기술은 시그널이 도착하면 그 루프를 분기하기 전에 어떤 동작을 완성하도록 진행과정에서 상호작용을 허용하기 때문에 유용하다.
프로세스를 종료시키는 핸들러 함수들은 전형적으로 소거 명령이나 프로그램 에러 시그널로부터의 복구와 인터럽트가 원인이 된 곳에 사용된다. 프로세스를 종료하도록 하는 핸들러를 만드는 가장 깨끗한 방법은 핸들러 실행의 첫 번째에서 같은 시그널이 발생되도록 하는 것이다. 다음과 같이.
당신은 setjmp 와 longjmp 기능을 사용해서 시그널 핸들러의 외부로 제어의 비지역 이동을 할 수 있다( 20장 [Non-Local Exits] 참조. ) 핸들러가 비지역 제어 이동을 할 때, 실행 중에 있던 프로그램의 그 부분은 계속되지 않을 것이다. 만일 프로그램의 그 부분이 중요한 데이터 구조체를 갱신 중에 있었다면, 그 데이터 구조체는 여전히 완벽하게 처리되지 못한 상태로 남게될 것이다. 프로그램이 종료되지 않는다면, 위와 같은 문제는 나중에 발견될지도 모른다. 이러한 문제를 피하기 위한 두 개의 방법이 존재한다. 한가지는 중요한 데이터를 갱신하는 프로그램 부분을 위해서는 시그널을 블록하는 것이다. 블록된 시그널은 그 블록이 해제된 후에 배달되어지고, 그때는 이미 중요한 데이터 갱신은 끝난 상태가 된다. 21. 7절 [Blocking Signals] 참조. 다른 방법은 시그널 핸들러 안에서 중요한 데이터의 구조체들을 재-초기화하거나, 그들의 값을 모순이 없도록 만드는 것이다. 다음은 한 개의 전역변수의 재초기화를 보여주는 개략적인 예제이다.
시그널 핸들러 함수가 실행되고 있을 때 도착한 다른 시그널이 있다면 무슨 일이 발생할까? 한 특정한 시그널을 위한 핸들러가 호출되었을 때, 핸들러가 반환할 때까지 그 시그널은 보통 블록된다. 만일 같은 종류의 두 개의 시그널이 서로 가까운 시간에 도착한다면, 두 번째 것은 첫 번째 것이 처리될 때까지 그냥 보유하고 있을 것이다. ( 만일 당신이 이러한 형태의 더 많은 시그널이 도착하도록 허용하기를 원한다면, 핸들러는 sigprocmask를 사용해서 시그널을 명백하게 블록을 해제할 수 있다; 21. 7. 3절 [Process Signal Mask] 참조. ) 그렇지만, 당신의 핸들러는 다른 종류의 시그널의 배달에 의해서는 여전히 인터럽트 되어질 수 있다. 이것을 피하기 위해서, 당신은 sigaction에 인수로써 사용하는 action 구조체의 sa_mask 멤버를 사용해서 핸들러가 실행되는 동안 블록되어질 시그널을 명백하게 지정할 수 있다. 그들 시그널은 호출 된 핸들러를 위한 시그널에 더해져 있고, 다른 시그널들은 보통 프로세스에 의해서 블록되어진다. 21. 7. 5절 [Blocking for Handler] 참조. 이식성 노트 : 만일 당신의 프로그램이 완전히 System V Unix상에서 작업하기를 원할 때, 비동기적인 발생이 예상되는 시그널을 위한 핸들러를 만들려면 항상 sigaction을 사용하라. 다른 시스템에서는, 핸들러에서 하는 시그널의 처리는 SIG_DFL로 시그널이 가진 원래의 동작으로 되돌려지도록 만들어져있고, 핸들러는 실행될 때마다 그 자체를 다시 만들어야만 한다. 이 것은 실제로 시그널이 연속적으로 도착할 수 없을 때 작업하기는 불편하다. 하지만 다른 시그널이 즉시 도착할 수 있다면, 그것은 다시 핸들러를 다시 정하지 않아도 된다. 그러면 두 번째 시그널은 프로세스를 종료시키는, 디폴트 처리로 받게될 것이다.
만일 당신의 시그널 핸들러가 전혀 호출될만한 기회도 갖기 전에, 당신의 프로세스에 같은 종류의 시그널이 여러 개 배달되었다면, 그 핸들러는 오직 한 개의 시그널이 도착한 것처럼 호출되어질 것이다. 실제로, 그 시그널들은 한 개로 합병한다. 이 상황은 시그널이 블록되었을 때나, 또는 멀티프로세싱 환경에서 시그널이 도착했는데 시스템이 다른 프로세스의 실행 때문에 바쁠 때 발생할 수 있다. 이것이 의미하는 것은, 예를 들어, 당신은 발생한 시그널의 개수를 세는 시그널 핸들러의 사용을 신뢰할 수 없다. 오로지 당신이 구분할 수 있는 것은 과거의 주어진 시간동안에 적어도 한 개의 시그널이 도착했는지, 또는 도착하지 않았는지를 구분하는 것만을 신뢰할 수 있다. 다음은 자식 프로세스가 발생시킨 SIGCHLD의 개수와는 같지 않을지도 모르는 SIGCHLD 시그널의 개수를 실제처럼 대치하는 핸들러의 예제이다. 그것은 프로그램이 다음처럼 구조체를 연결하여 자식 프로세스의 모두를 추적하고 있다고 가정한다.
리스트를 시험하기 전에 플래그를 소거하는 것은 치명적이다; 그렇지 않고, 만일 플래그가 소거되기 전과, 프로세스 리스트의 적당한 요소가 체크된 후에 시그널이 배달된다면, 다음 도착한 시그널이 다시 그 플래그를 설정하기 전까지는 그 상황변화를 알아차릴 수 없다. 당신은 물론 그 리스트를 조사하는 동안 시그널을 블록함으로써 이러한 문제를 피할 수는 있지만, 올바른 순서로 일들을 처리하는 것이 정확함을 보증하기에는 좀더 좋은 방법이다. 프로세스 상황을 체크하는 루프가 그 상황이 유용하게 저장되어졌음이 확인될 때까지 p->status를 조사하는 것을 피한다. 이것은 status가 억세스 되고 있는 도중에 변화될 수 없음을 확실하게 한다. 일단 p->have_status가 설정되면, 그것은 자식 프로세스가 멈추거나 종료했음을 의미하고, 그 어느 경우에도, 프로그램이 주목하고 있는 동안에 다시 멈추거나 종료할 수 없다. 변수를 억세스하고 있는 동안에 인터럽션(interruptions)을 모방하기에 대한 상세한 정보는 21. 4. 7. 3절 [Atomic Usage] 를 참조하라. 다음은 당신이 체크했던 마지막 시간이후 핸들러가 실행되었는지를 시험 할 수 있는 다른 방법이다. 이 기술은 핸들러의 외부에서 결코 변화되지 않을 카운터로 사용한다. 빈도수(count)를 소거하는 대신에, 프로그램은 전의 값을 기억하고 있다가 그 이후에 그 값이 변화되었는지를 보여준다. 이 방법이 유리한 점은 프로그램의 다른 부분들을 독립적으로 체크할 수 있다는 것으로, 각각의 부분은 그 부분을 마지막으로 체크한 이후에 시그널이 있었는지를 체크한다.
핸들러 함수들은 보통 많은 일을 하지는 않는다. 핸들러 함수에게는 프로그램이 정기적으로 체크하는 외부변수를 설정하는 일 이외에는 아무 것도 하지 않게 하고, 다른 중요한 일들은 프로그램에게 맡기는 것이 좋다. 이것은 핸들러가 예측할 수 없는 시간에_시스템 호출도중, 또는 다중 명령을 요구하는 C연산자의 시작과 끝 사이에_비동기적으로 호출될 수 있기 때문이다. 데이터 구조체가 처리되고 있는 동안 핸들러 함수가 호출되면 데이터 구조체의 상황은 불일치하게 될 것이다. 심지어 한 개의 int 형 변수에서 다른 변수로 값을 복사하는 것조차도 대부분의 기계에서 두 개의 명령어를 취할 수도 있다. 이것은 당신이 시그널 핸들러에서 무언가를 할 때 많은 주의를 해야만 한다는 것을 의미한다. 만일 당신의 핸들러가 어느 전역변수를 억세스할 필요가 있다면, 그 변수들을 휘발성으로 선언하라. 이것은 변수들의 값이 비동기적으로 변화할 것이라고 컴파일러에게 알리고, 그와같은 갱신에 의해 무효로 만들게 될 어떤 최적화를 금한다. 만일 핸들러 안의 어떤 함수를 호출한다면, 그것이 시그널들에 대해서는 재진입성이 있음을 확실히 하거나, 시그널이 함수와 연관된 호출에는 인터럽트 할 수 없음을 확실히 하라. 스택 (stack)상에 존재하지않는 메모리를 사용하는 함수는 비-재진입성이 될 수 없다. 만일 어떤 함수가 정적 변수나 전역변수, 또는 동적으로 할당된 오브젝트를 사용한다면, 그것은 비-재진입성이고, 그 함수를 두 번 호출하면 서로 충돌하게 될 수 있다. 예를 들어, 시그널 핸들러가 gethostbyname을 사용한다고 가정하자. 이 함수는 정적 오브젝트에 그 값을 반환하고, 매번 같은 오브젝트를 다시 사용한다. 만일 gethostbyname이 호출된 동안, 또는 심지어 그 후(하지만 여전히 그 값은 프로그램에서 사용하고 있는 중이다. )라도 그 시그널이 도착하는 일이 발생한다면, 그것은 프로그램이 요청한 그 값을 지워버릴 것이다. 그렇지만, 만일 그 프로그램이 gethostbyname이나 같은 오브젝트에 정보를 반환하는 함수를 사용하지 않거나, 또는 만일 그와같은 것을 사용한다고 해도 그것을 사용할 때 시그널들을 블록한다면, 당신은 안전하다. 라이브러리 함수들의 대부분은 한 고정 오브젝트에 값들을 반환하고 항상 같은 오브젝트를 재 사용하기 때문에 그들은 같은 문제를 발생시킬 가능성이 있다. 이 매뉴얼 안에 있는 함수들에 대한 설명에는 이러한 것들을 항상 언급할 것이다. 만일 어떤 함수가 당신이 공급한 오브젝트를 사용하고 갱신한다면, 그것은 잠재적으로 비-진입성이다. 같은 오브젝트를 사용하고 있는 두 개의 호출은 충돌할 수 있다. 이와 같은 경우는 당신이 스트림을 사용해서 입/출력을 할 때 발생한다. 시그널 핸들러가 fprintf를 사용해서 메시지를 출력한다고 가정하자. 그리고 그 프로그램이 fpintf를 처리하고 있는 도중에 같은 오브젝트를 사용하는 시그널이 배달되었다고 가정하자. 이때 두 개의 호출은 같은 데이터 구조체_스트림 자체_에서 동작하기 때문에 핸들러의 메시지와 프로그램의 데이터는 모두 변조될 것이다. 그렇지만, 만일 핸들러에서 사용하는 스트림이 시그널이 도착하여 동시에 그 스트림이 프로그램에 의해 사용되어질 가능성이 없다는 것을 당신이 알고 있다면, 아무런 문제가 없다. 그리고 만일 프로그램이 다른 스트림을 사용한다면 아무런 문제가 없다. 대부분의 시스템에서, malloc 과 free는 무슨 메모리 블록들이 해제상태에 있는지를 기록하고 있는 정적 데이터 구조체를 사용하기 때문에, 재진입성이 없다. 그렇기 때문에 메모리를 할당하고 해제하는 라이브러리 함수중에 재진입성이 있는 것은 아무 것도 없다. 핸들러에서 메모리를 할당할 필요를 피하기 위한 가장 좋은 방법은 시그널 핸들러에서 사용할 공간을 미리 할당받는 것이다. 핸들러에서 메모리를 해제하는 것을 피하는 가장 좋은 방법은 해제할 오브젝트를 플래그로 표시하거나 기록해두고, 어느 것이 해제되기를 기다리고 있는지를 나중에 프로그램에서 체크하는 것이다. 그러나 이것은 오브젝트들이 개별적으로 존재하는 것이 아니라 서로 연결되어 있고, 같은 일을 하는 다른 시그널 핸들러에 의해서 그것이 인터럽트 되어졌다면, 당신은 오브젝트들 중 하나를 "잃어"버릴 수 있기 때문에 주의를 해야만 한다. GNU 시스템에서, malloc 과 free는 시그널들을 블록하기 때문에 시그널 핸들러에서 사용하는 것은 안전하다. 그렇기 때문에, 시그널 핸들러에서 결과를 위해서 공간을 할당하는 것은 또한 안전하다. obstack 할당 함수들도 당신이 시그널 핸들러의 외부와 내부양쪽에서 같은 obstack을 사용하지 않는다면 안전하다. 재배치(relocating) 할당 함수들( 3. 6절 [Relocating Allocator] 참조. )을 시그널 핸들러 안에서 사용하는 것은 안전하지 않음이 확실하다. errno를 갱신하는 어떤 함수들은 비-진입성이지만, 당신은 이것을 진입성으로 만들 수 있다: 핸들러에서, errno의 원래 값을 저장하고 정상적으로 반환하기 전에 그것을 반환한다. 이것은 시그널 핸들러 내부에서 발생된 에러들이, 핸들러가 실행되도록 프로그램이 인터럽트 된 순간에 시스템 호출로부터 발생한 에러와 혼동되는 것을 막는다. 이 기술은 일반적으로 응용 가능하다; 만일 당신이 핸들러의 내부에서 메모리의 특정한 오브젝트를 갱신하는 함수를 호출하기 원한다면, 당신은 그 오브젝트를 저장하고 다시 반환함을 통해서 안전하게 구현할 수 있다. 메모리 오브젝트로부터 읽기는 시그널이 배달되어질 때라도 오브젝트에 나타날 수 있는 어떤 값들을 취급할 수 있도록 안전하게 제공되었다. 어떤 데이터 타입에 배정(assignment)할 때, 그 데이터 타입이 원자단위가 아닌 변수에 배정(assignment)하는 "도중에" 핸들러가 실행될 수 있다면 그 배정에는 많은 명령(instruction)이 요구됨을 명심하라. 메모리 오브젝트에 기록하기는 핸들러가 실행되고 있는 순간일지라도 안전하고, 어느 것도 방해되지 않을 것이다. 당신의 어플리케이션에서 데이터가 원자와 관계가 있던지, 또는 단순한 텍스트이던지, 당신은 원자화가 필요 없는 단일한 데이터를 억세스 하는 요소에 대해서 주의를 해야만 한다. 이것은 단일한 오브젝트를 읽거나 쓰기 위해서는 여러 개의 명령이 필요할 수 있다는 것을 의미한다. 그와같은 경우에, 시그널 핸들러는 오브젝트의 읽기나 쓰기 중간에 실행될 수 있다. 이러한 문제를 커버할 수 있는 세 가지 방법이 있다. 당신은 항상 원자 단위로 억세스되는 데이터 타입을 사용할 수 있다; 억세스를 인터럽트 하여 아무런 부적당한 일이 일어나지 않게 하거나, 또는 인터럽트보다는 좋지는 않지만 억세스동안에 모든 시그널들을 블록하는 등 당신은 주의 깊은 조정을 할 수가 있다. 다음은 변수를 갱신하는 도중에 시그널 핸들러를 실행하면 무슨 일이 발생하는지를 보여주는 예제이다. (변수 읽기를 인터럽트 하는 것도 역설적인 결과에 이르게 할 수 있지만, 여기서 우리는 쓰기를 보여준다. )
이 프로그램은 계속 번갈아 가면서 0, 1, 0, 1 로 메모리를 채운다; 그 동안, 일초마다, 알람 시그널 핸들러는 현재의 내용을 프린트한다. ( 핸들러 안에서 printf의 호출은 시그널이 발생했을 때 핸들러외부에서 printf가 확실히 호출되어지지 않을 것이므로 이 프로그램은 안전하다. ) 분명히, 이 프로그램은 0 한 쌍과 1 한 쌍을 프린트 할 수 있다. 하지만 그것이 그 프로그램이 할 수 있는 전부가 아니다! 대부분의 기계에서, 메모리에 새로운 값을 저장하기 위해서는 여러 개의 명령을 취하고, 그 값은 동시에 한 워드(word)에 저장된다. 만일 시그널이 그 명령들 사이에 배달된다면, 핸들러는 memory. a는 0이고 memory. b는 1인걸 발견할지 모른다(또는 그의 반대). 한 개의 명령으로 메모리 안에 한 개의 새로운 값을 저장할 수 있는 어떤 기계에서는 인터럽트 될 수 없다. 그 기계들에서, 핸들러는 항상 두 개의 0과 두 개의 1을 프린트 할 것이다. 변수를 억세스할 때 인터럽트 하는 것에 대한 불확실성을 피하기 위해서, 당신은 항상 원자단위로 억세스를 하는 특별한 데이터 타입을 사용할 수 있다: sig_atomic_t. 이 데이터타입을 읽기와 쓰기는 단일한 명령으로 발생한다는 것이 보증되므로 핸들러가 억세스의 "중간에" 실행될 방법이 없는 것이다. sig_atomic_t 타입은 항상 정수 데이터 타입이지만, 그 데이터 타입이 몇 개의 비트로 구성되어있는지는 한가지로 정해진 것이 아니라 각각의 기계마다 다양하다. 데이터 타입 : sig__atomic__t
억세스의 어떤 형태는 억세스가 인터럽트 되는 것과 같은 문제들을 피한다. 예를 들어, 핸들러에 의해 설정되고, 때때로 메인 프로그램에 의해서 소거되고 테스트되는 어떤 플래그를 억세스 하는데 실제로 두 개의 명령(instructions)이 필요하다고 할지라도 항상 안전하다. 이것이 그렇게 보이도록 하기 위해서, 우리는 인터럽트 되어질 수 있는 모든 억세스를 고려해야만 하고, 인터럽트 되면 아무런 문제가 없음을 보여야 한다. 플래그를 테스트하는 도중에 발생한 인터럽트는 아무런 문제가 없는 정확한 값인 경우에, 0이 아닌 값으로 인식이 되거나 또는 테스트된 다음에 0이 아닌 값으로 되어질 것이기 때문에 아무런 문제가 없다. 플래그를 소거하는 도중에 인터럽트도 아무런 문제가 없는데, 플래그가 소거되기 전에 시그널이 발생한 것은, 그 값이 0으로 끝나거나, 아니면 0이 아닌 값으로 끝나고, 플래그가 소거된 후에 시그널이 발생한 것처럼 연속적인 사건들이 발생한다. 그 두 개의 경우 모두 코드가 처리되기만 하면, 플래그를 소거하는 도중에 발생한 시그널 또한 처리 할 수 있다. ( 이것은 비-원소단위의 사용이 언제 안전할 수 있는지를 당신에게 설명하기 위한 예제이다. ) 때때로 당신은 다른 오브젝트를 사용해서 어떤 오브젝트의 사용을 막음으로써 그 오브젝트에 인터럽트 되지 않는 억세스를 보증할 수 있다, 그것의 형태는 원소단위가 확실할 것이다. 21. 4. 5절 [Merged Signals] 에서 예제참조. open 이나 read가 입/출력 디바이스에서 기다리는 것과 같은 입/출력 기본동작 동안에 시그널이 발생할 수도 있고 처리될 수도 있다. 만일 시그널 핸들러가 반환하면, 그 시스템은 의문을 갖는다: 다음에 무슨 일이 발생하지? POSIX는 한가지 접근법을 정한다: 즉시 그 기본동작을 실패로 만든다. 이러한 종류의 실패를 위한 에러코드는 EINTR이다. 이것은 유연하지만, 보통은 불편하다. 전형적으로, POSIX 어플리케이션은 그 호출을 다시 할 목적으로 라이브러리 함수가 반환했을 때 EINTR인지 체크하는 시그널 핸들러를 사용한다. 종종 프로그래머들은 체크하는 것을 잊는다. GNU 라이브러리는 매크로 TEMP_FAILURE_RETRY를 사용해서, 임시적인 실패 후에 다시 호출을 시도하도록 하는 편리한 방법을 제공한다. 매크로 : TEMP__FAILURE__RETRY (expression)
BSD는 완전히 EINTR을 피하고 좀더 편리한 접근법을 제공한다: 그것을 실패로 만드는 대신에 인터럽트된 기본동작을 다시 시작한다. 만일 당신이 이 접근법을 선택한다면, 당신은 EINTR에 관심을 가질 필요가 없다. GNU 라이브러리에서는 접근법을 선택할 수 있다. 만일 당신이 시그널 핸들러를 만드는 sigaction을 사용한다면, 당신은 핸들러가 어떻게 동작할지를 정할 수 있다. 만일 당신이 SA_RESTART 플래그를 지정하면, 핸들러부터의 반환은 어떤 기본동작을 다시 시작할 것이다; 그렇지 않으면, 핸들러로부터의 반환은 EINTR을 발생할 것이다. 21. 3. 5절 [Flags for Sigaction] 참조. 다른 방법은 siginterrupt 함수를 사용하는 것이다. 21. 9. 1절 [POSIX vs BSD] 참조. 당신이 한 특정한 핸들러에서 sigaction 이나 siginterrupt로 할 일을 정하지 않을 때, 그것은 디폴트 선택을 사용한다. GNU 라이브러리에서 디폴트 선택은 당신이 정의한 테스트 매크로에 의존한다. 만일 시그널이 발생되기 전에 _BSD_SOURCE 또는 _GNU_SOURCE로 정의하면, 디폴트는 기본동작을 다시 시작하는 것이다; 그렇지 않다면, 디폴트는 EINTR로 그들을 실패하게 만드는 것이다. ( 라이브러리는 signal 함수의 다양한 변형을 포함하고 있고, 당신이 사용한 테스트 매크로에 따라서 실제로 호출될 signal 함수가 결정된다. ) 1. 3. 4절 [Feature Test Macros] 참조. 위와 같은 문제에 영향을 받는 기본동작들은 close, fcntl(operation F_SETLK), open, read, recv, recvfrom, select, send, sendto, tcdrain, waitpid, wait, 그리고 write가 있다. 결코 재개(resumption)가 발생되지 않는 한가지 상황이 있다: read 와 write 와 같은 데이터-참조 함수가 데이터의 일부분만을 참조한 후에 시그널에 의해서 인터럽트 되었을 때. 이 경우, 그 함수는 부분적인 성공을 지적하기 위해서, 이미 참조된 바이트의 개수를 반환한다. 레코드-지향 디바이스 상에서는 두 개의 레코드를 read 하거나 write하려는 것에서 한 개로 read, write를 분리해버리는 것 과 같은 이상한 동작의 원인이 될 수도 있다. (데이터그램 소켓을 포함; 11. 9절 [Datagrams] 참조. ). 실제로는, 그와같은 디바이스 상에서는 데이터를 참조 중에 인터럽션이 발생할 수 없기 때문에 아무런 문제가 없다; 그와같은 디바이스들은 일단 데이터 참조가 시작되면 아무런 기다림이 없이, 한 버스트(burst)에 전체 레코드를 항상 참조한다. **역자주 : 버스트(burst) : 중간에 어떤 이유들로 인해서 중단이 발생하지 않고, 한 묶음의 데이터를 한꺼번에 전달하는 방법을 의미함.
하드웨어 트랩이나 인터럽트의 결과로서 발생되는 시그널을 제외하고, 당신의 프로그램에서 프로세스, 또는 다른 프로세스에게 명시적으로 시그널을 보낼 수 있다. 프로세스는 raise 함수를 통해서 시그널을 스스로에게 보낼 수 있다. 이 함수는 `signal. h'에 선언되어 있다. 함수 : int raise (int signum)
함수 : int gsignal (int signum)
raise 사용으로 한가지 편리한 점은 당신이 트랩 했던 시그널의 디폴트 동작을 재생할 수 있다는 것이다. 이를테면, 당신의 프로그램을 사용하는 사용자가 stop 시그널(SIGTSTP)을 보내기 위해서 SUSP 문자를 타이핑할 때, 당신은 멈추기 전에 어떤 내부적 데이터 버퍼들을 소거하기를 원한다고 가정하자. 당신은 다음처럼 이것을 설정할 수 있을 것이다.
kill 함수는 다른 프로세스에게 시그널을 보내기 위해서 사용될 수 있다. 함수의 명칭이 악의적이지만, 그것은 다른 프로세스를 종료시키는데 사용하기보다는 더 많은 것들을 위해서 사용될 수 있다. 다음의 경우, 당신이 프로세스들 사이에 시그널들을 보내기 원할 때 사용할 수 있다. 부모 프로세스가 작업을 수행하기 위해서 자식 프로세스를 시작한다. 아마도 자식 프로세스는 한정된 루프를 돌 것이고_자식 프로세스가 작업에서 더 이상 필요치 않을 때 종료한다. 한 프로세스가 그룹(group)의 일부로써 실행될 때, 에러나 다른 사건이 발생하면 그룹에 있는 다른 프로세스에게 신고하거나 종료할 필요가 있다. 두 개의 프로세스가 서로 작업하는 동안 동기(synchronize)할 필요가 있다. 이 절은 당신이 프로세스가 어떻게 작업하는지에 대해서 조금이나마 알 것이라고 가정한다. 이 주제에 대한 자세한 정보는 23장 [Child Process] 에 나와있다. kill 함수는 `signal. h'에 선언되어 있다. 함수 : int kill (pid_t pid, int signum)
pid > 0
pid == 0
pid < -1
pid == -1
다음의 errno는 이 함수를 위해 정의된 에러상황이다. EINVAL
EPERM
ESCRH pid
함수 : int killpg (int pgid, int signum)
어느 임의의 프로세스에게 시그널을 보내기 위해서 kill을 사용하는 것을 방지하기 위한 제한이 있다. 그것은 다른 사용자에게 소속되어 있는 프로세스를 제멋대로 죽이는 것과 같은 반사회적인 행동을 방지하기 위한 의도가 있다. kill을 자식과 부모 프로세스사이에 시그널을 주고 받기 위해 사용하는것과 같은 상황에서는, 보통 당신은 시그널을 보내기 위한 허가권을 갖고 있다. 그러나 자식 프로세스에서 setuid 프로그램이 실행될 때는 유일하게 제외된다; 만일 프로그램이 실제 UID를 유효 UID로 변경한다면, 당신은 시그널을 보내기 위한 허가권을 가지지 않을 수도 있다. su 프로그램을 이런 일을 한다. 프로세스가 다른 프로세스에게 시그널을 보내기 위한 허가권을 가지고 있는지 없는지의 여부는 두 개의 프로세스의 사용자 ID들에 의해 결정된다. 이 원칙은 25. 2절 [Process Personal] 에 자세하게 논의되고 있다. 일반적으로, 어떤 프로세스가 다른 프로세스에게 시그널을 보낼 수 있기 위해서는, 시그널을 보내는 프로세스가 특권이 부여된 사용자(`root'처럼)이거나 시그널을 보내는 프로세스의 실제 또는 유효 사용자 ID가 시그널을 받는 프로세스의 실제 또는 유효 사용자 ID와 매치되어야만 한다. 만일 시그널을 받는 프로세스가 프로세스 이미지 파일에서 set-user-ID 모드를 통해 유효 사용자 ID를 변경했다면, 프로세스 이미지 파일의 소유자가 현재 유효 사용자 ID 대신에 사용된다. 어떤 경우에, 만일 사용자 ID들이 매치되지 않는다 할지라도 부모 프로세스가 자식 프로세스에게 시그널을 보내는 것이 가능하고, 다른 경우에는 다른 제한들이 강요 될 것이다. SIGCONT 시그널은 특별한 경우이다. 그것은 만일 시그널을 보내는 쪽이 시그널을 받는 쪽과 같은 세션에 있다면, 사용자 ID들에 상관없이 시그널을 보낼 수 있다. 다음은 프로세스간 통신을 위해서 어떻게 시그널들을 사용할 수 있는지 보여주는 조금 긴 예제 프로그램이다. SIGUSR1 과 SIGUSR2가 프로세스 간 통신을 지원하지 위하여 제공된 것이다. 그 시그널들은 기본적으로 치명적이기 때문에, 그 시그널들을 받을 것으로 가정되는 프로세스는 signal 이나 sigaction을 통해서 그들을 트랩 해야만 한다. 다음의 예제는, 부모 프로세스가 fork로 자식 프로세스를 생성한 다음 자식 프로세스가 초기화를 수행할 때까지 기다린다. 자식 프로세스는 준비가 되었음을 알리기위해서, kill 함수를 사용해서 SIGUSR1 시그널을 보낸다.
위의 예제는 busy wait(적당한 말이 없어서. . )을 사용하는데, 그것은 다른 프로그램에서 사용할 수 있도록 CPU 사이클을 기다려야하기 때문에 좋지 않다. 시그널이 도착할 때까지 기다리도록 시스템에게 부탁하는 것이 더 좋다. 21. 8절 [Waiting for a signal] 에 있는 예제를 참조하라. 시그널 블록하기는 운영체제에게 그 시그널을 붙잡아서 나중에 배달하도록 알리는 것을 의미한다. 일반적으로, 프로그램에서는 SIG_IGN을 사용해서, 시그널의 동작을 무시하는 것으로 설정할 망정, 불명확하게 시그널들을 블록하지 않는다. 하지만 시그널 블록킹(blocking)은 민감한 오퍼레이션들이 인터럽트 되는 것을 막기 위해서 시그널들을 블록하는데 사용된다. 시그널들 때문에 핸들러에 의해 수정되었던 전역 변수들을 갱신하는 동안 시그널들을 블록하기 위해서 sigprocmask 함수를 사용 할 수 있다. 특정한 핸들러가 실행되는 동안 어떤 시그널들을 블록하도록 sigaction 함수호출에서 sa_mask를 설정할 수 있다. 이 방법으로, 시그널 핸들러는 시그널들에 의해서 그 자체가 인터럽트 됨이 없이 실행될 수 있다. sigprocmask을 사용해서 임시적으로 시그널 블록하기는 당신의 프로그램에서 임계부분(critical parts)이 실행되는 동안에 발생할지도 모를 인터럽트를 막기 위한 방법으로 제공된다. 만일 시그널들이 프로그램의 그 부분(critical parts)에 도달한다면, 당신이 그들의 블록을 해제한 후에, 나중에 배달 되어진다. 이것의 유용한 사용예는 프로그램의 나머지와 시그널 핸들러 사이에 데이터를 분배하는데 사용하는 것이다. 만일 데이터의 타입이 sig_atomic_t( 21. 4. 7절 [Atomic Data Access] 참조. )가 아니라면 시그널 핸들러는 프로그램의 나머지가 데이터의 읽기와 쓰기를 완전히 끝냈을 때 실행될 수 있다. 이것은 혼란된 결과를 초래할 것이다. 신뢰 가능한 프로그램을 만들기 위해서, 프로그램의 나머지가 데이터를 시험하거나 갱신하는 동안에 시그널 핸들러가 실행되는 것을 막을 수 있다_프로그램의 나머지가 실행되는 동안 발생할 여지가 있으며, 그 데이터를 건드릴 위험이 있는 적당한 시그널들을 블록함으로 해서. 만일 어떤 시그널이 도착하지 않았을 때, 당신이 어떤 동작을 수행하기를 바란다면 시그널 블록킹은 필요하다. 그 시그널을 위한 핸들러가 sig_atomic_t 타입의 플래그를 설정한다고 가정하다; 당신은 그 플래그를 시험하고 만일 그 플래그가 설정되지 않았다면 어떤 동작을 수행하도록 하고 싶어한다. 하지만 이것은 신뢰할 수 없다. 만일 그 시그널이 아직 중요한 동작은 수행하기 전이고, 플래그는 테스트 한 직후에 시그널이 배달된다고 가정하면, 그 프로그램은 시그널이 도착할지라도 그 동작을 수행 할 것이다. 어떤 시그널이 도착했는지의 여부를 확인하는 유일한 신뢰 가능한 방법은 시그널이 블록되어 있을 동안 테스트하는 것이다. 시그널을 블록 킹하는 함수들 모두는 무슨 시그널들이 영향을 받게되는지를 정하는 데이터 구조체를 사용한다. 그리고, 두 개의 단계, 즉, 시그널을 만들기와 시그널을 라이브러리 함수에 인수로써 사용하기를 포함한다. 그들은 헤더파일 `signal. h'에 선언되어 있다. 데이터 타입 : sigset__t
시그널 셋(set)을 초기화하기 위한 두 가지 방법이 있다. 하나는 처음에 sigemptyset을 사용하여 비어있게 해놓은 다음, 개별적으로 시그널을 하나씩 더한다. 아니면, sigfillset을 사용하여 완전히 채운다음, 개별적으로 정해진 시그널들을 하나씩 지운다. 당신이 어떤 식으로든 그것을 사용하기 전에 그 두 개의 함수중 하나로써 시그널 셋(set)을 초기화해야만 한다. 모든 시그널들을 명시적으로 설정하려 시도하지 말아라, 왜냐하면, sigset_t 오브젝트는 초기화될 필요가 있는 어떤 다른 정보(버전 필드와 같은)를 포함하고 있을 것이기 때문이다. (더하자면, 당신이 알고 있는 것 외의 시그널은, 시스템이 발생시키지 않을 것이라는 가정을 당신의 프로그램 안에서 하는 것은 현명하지 못하다. ) 함수 : int sigemptyset (sigset_t *set)
함수 : int sigfillset (sigset_t *set)
함수 : int sigaddset (sigset_t *set, int signum)
EINVAL : signum 인수로 무효한 시그널을 지정하였다. 함수 : int sigdelset (sigset_t *set, int signum)
마지막으로, 시그널 셋(set)안에 어떤 시그널들이 있는지를 테스트하기 위한 함수의 설명이다. 함수 : int sigismember (const sigset_t *set, int signum)
현재 블록되어 있는 시그널들의 모음(collection)을 시그널 마스크라고 부른다. 각 프로세스는 자신 소유의 시그널 마스크를 갖고 있다. 당신이 새로운 프로세스를 만들 때(23. 4절 [Creating a Process] 참조) 그것은 부모의 마스크를 상속받는다. 당신은 시그널 마스크를 갱신하여 유연성 있게 시그널들을 블록하거나 해제할 수 있다. sigprocmask 함수의 프로토타입은 `signal. h'에 있다. 함수 : int sigprocmask (int how, const sigset_t *set, sigset_t *oldset)
당신은 SIGKILL 과 SIGSTOP 시그널들을 블록할 수 없지만, 만일 시그널 셋이 그들을 포함한다면, sigprocmask는 에러 상황을 보고하는 대신에 그들을 단지 무시한다. 기억하라, SIGFPE와 같은 프로그램 에러 시그널들을 블록하는 것은 실제 프로그램 에러에 의해 발생된 시그널로 인해 바람직하지 못한 결과를 초래한다. (raise 나 kill에 의해 만들어진 시그널들은 제외하고) 이것은 시그널이 다시 블록이 해제되었을 때, 그 지점에서 실행을 계속하지 못할 정도로 프로그램이 파괴되었기 때문이다. 21. 2. 1절 [Program Erroe Signals] 참조. 다음은 간단한 예제이다. SIGALRM 시그널이 도착할 때마다 플래그를 설정하는 핸들러를 만들고, 메인 프로그램에서는 시간마다 이 플래그를 체크하고 그것을 재설정한다고 가정하자. 당신은 sigprocmask를 호출해서 코드의 임계부분을 보호함으로써 그 동안 도착한 부가적인 SIGALRM 시그널을 막을 수 있다. /* 이 변수는 SIGALRM 시그널 핸들러에 의해 설정된다. */
시그널 핸들러가 호출되었을 때, 당신은 보통 그 시그널 핸들러가 다른 시그널에 의해 블록됨이 없이 끝나기를 원한다. 그 핸들러가 시작된 순간부터 끝나는 순간까지, 당신은 핸들러의 데이터를 오염시키거나 혼란시킬지도 모르는 시그널을 블록해야만 한다. 한 시그널에 의해 핸들러 함수가 호출되었을 때, 핸들러가 실행되는 동안 그 시그널은 자동적으로 블록된다 ( 다른 시그널과 함께 그 시그널은 이미 프로세스의 시그널 마스크에 존재하게된다. ) 만일 예를 들어 당신이 SIGTSTP를 위한 핸들러를 준비했을 때, 그 시그널이 도착하면 핸들러는 핸들러가 실행되는 동안 기다리도록 하여 나중에 SIGTSTP 시그널을 다시 발생시킨다. 그렇지만, 디폴트로, 다른 종류의 시그널들은 블록되지 않았다; 그들은 핸들러가 실행되는 동안 발생할 수도 있다. 핸들러가 실행되는 동안 다른 종류의 시그널을 블록하기 위한 좋은 방법은 sigaction 구조체의 sa_mask 멤버를 사용하는 것이다. 다음은 그에 대한 예제이다.
핸들러 코드 안에서 명시적으로 다른 시그널들을 블록하는 것보다는 더 신뢰 가능하다. 만일 당신이 핸들러 안에서 명시적으로 시그널들을 블록한다면, 아직 당신이 그들을 블록하지 않았을, 핸들러 시작 초기의 짧은 간격동안에 발생된 시그널로 인한 문제는 피할 수가 없다. 이 메커니즘을 사용하여 프로세스의 현재 마스크로부터 시그널들을 제거 할 수 없다. 그렇지만, 핸들러 함수에서 sigprocmask를 호출하여, 당신이 원하는 시그널을 블록하거나 해제하도록 만들 수 있다. 어쨌든, 핸들러 함수가 반환할 때, 시스템은 핸들러 함수가 진입하기 전으로 마스크를 반환한다. 당신은 sigpending 을 호출하여 어느 시점에서 미해결 상태인 시그널들을 발견해낼 수 있다. 이 함수는 `signal. h'에 선언되어 있다. 함수 : int sigpending (sigset_t *set)
시그널이 미해결 상태인지를 테스트하는 것은 자주 유용하지는 않다. 시그널이 블록되지 않았을 때 테스트하는 것은 좋지 않다. 다음의 예제를 살펴보자.
당신의 프로세스를 위하여 미해결상태인 특정한 시그널이 있다면 그 동안에 도착한 같은 종류의 부가적인 시그널들은 버려질 것임을 기억하라. 예를 들어, 만일 SIGINT 시그널이 미해결 상태일 때 다른 SIGINT 시그널이 도착하면, 당신의 프로그램은 이 시그널의 블록을 해제할 때 오직 한 개의 SIGINT 시그널로 처리할 것이다. 이식성 노트 : sigpending 함수는 POSIX. 1에 새로이 추가되었다. 오래된 시스템들은 이와 동등한 함수가 없다.
라이브러리 함수를 사용해서 시그널을 블록하는 대신에, 당신은 당신이 블록을 "해제"할 때, 나중에 테스트 되도록 플래그를 설정하는 핸들러를 만들어서 거의 같은 결과를 얻을 수 있다. 다음의 예제를 살펴보라.
도착한 특정한 시그널이_미해결인_어떻게 signal에 저장되었는지에 주목하라. 그와같은 방법으로, 우리는 아직 해결할 형편이 되지 않은 시그널의 다양한 종류를 처리할 수 있다. 우리는 defer_signal을 증가시키고 감소시켜서 중첩된 임계 구역(critical sections)을 적당히 작업하게 한다; 그래서, 만일 signal_pending 과 함께 호출되었던 update_mumble 의 값이 이미 0이 아니라면, 시그널들은 update_mumble안에서는 연기되지 않고, 오직 caller 내부에서만 연기된다. 이것은 defer_signal 이 여전히 0이 아닐 때, 왜 signal_pending을 체크하지 않는지에 대한 이유가 된다. defer_signal 의 증가와 감소는 한 개의 명령보다는 많은 명령이 요구된다; 그러므로 중간에 시그널이 발생하는 것이 가능하다. 그러나 이것은 아무런 문제도 야기하지 않는다. 만일 증가나 감소를 시작하기 전에 발생했던 시그널과 동등한 그 시그널이 증가나 감소 전에 그 값을 보기 위해서 충분히 많이 발생된 것이라면, 이 경우 아무런 문제없이 작업한다. signal_pending 을 테스트하기 전에 defer_signal을 증가시키는 것은 굉장히 중요하다, 왜냐하면 이것은 민감한 버그를 피하게 하기 때문이다. 만일 우리가 그와같은 일들을 다른 순서로 한다면 이것은 다음과 같다.
위의 경우 if 구문과 감소사이에 도착된 시그널은 불명확한 시간동안은 잃어버리게 된다. 핸들러는 완전하게 defer_signal을 설정하였지만, 프로그램은 이미 이 변수를 테스트해버렸고, 다시는 변수를 테스트하지 않을 것이다. 그와같은 버그들을 타이밍 에러라고 부른다. 그들은 희귀하게 발생하고 재생시키는데는 굉장히 중요하기 때문에 아주 나쁜 버그이다. 당신은 재생 가능한 버그를 발견하는 것처럼 디버거로 그들을 발견할거라고 예상하지 마라. 그렇기 때문에 그러한 버그를 피하기 위해서는 특별히 주의할 가치가 있다. ( 당신은 이러한 순서로 코드를 기록하고 싶은 유혹을 받지 말아라, defer_signal 이 카운터(counter)로써 사용된다면 signal_pending 과 함께 테스트되어야만 한다. 후에, 0에 대한 테스트는 1에 대한 테스트보다는 깨끗하다. 그러나 만일 당신이 defer_signal을 카운터로써 사용하지 않고, 0과 1의 값만 그것에 주어진다면, 순서는 간단하게 보여질 것이다. 이것은 defer_signal을 카운터로써 사용하는 것보다 더한 이득을 갖는다: 그것은 당신이 잘못된 순서로 코드를 기록하고 민감한 버그를 만들어낼 가능성을 감소시킬 것이다. ) 당신의 프로그램이 외부 사건에 의해서 조종되거나, 동기화를 위해서 시그널을 사용한다면, 그때 그 프로그램은 시그널이 도착할 때까지 기다릴 수밖에 없다. 시그널이 도착할 때까지 기다리기 위한 간단한 방법은 pause 를 호출하는 것이다. 당신이 그것을 사용하기 전에 다음절에 있는, 그것을 사용함으로 써 얻게되는 불리한 점을 보아라. 함수 : int pause ()
pause의 간단함은 프로그램을 이상하게 중단(hang) 시킬 수도 있는 심각한 타이밍 에러들을 숨길수도 있다. 만일 당신의 프로그램에서, 실제 작업이 시그널 핸들러에 의해서 수행되고, "메일 프로그램"은 pause는 호출하지만 아무런 일을 하지 않을 때는 안전하다. 시그널이 배달될 때마다, 핸들러는 해야할 작업을 하고, 다음에 반환한다, 그래서 프로그램의 메인 루프는 다시 pause를 호출 할 수 있다. 한 개 이상의 시그널이 도착하기를 기다렸다가 실제작업을 재개하기 위해서 pause를 사용하는 것은 안전할 수 없다. 당신이 플래그를 설정하는 것으로 시그널 핸들러를 조정한다고 할지라도, 당신은 여전히 pause함수를 믿을 수 없다. 다음에 이 문제에 대한 예제가 있다.
이것은 버그를 갖고 있다: 변수 usr_interrupt가 체크된 후, 하지만 pause가 호출되기 전에 시그널이 도착할 수 있다. 만일 앞으로 아무런 시그널이 도착하지 않으면, 프로세스는 결코 다시는 재개될 수 없다. puase를 사용하는 대신에 루프 안에서 sleep를 사용해서 오랜 기다림에 상위(upper) 제한을 가할 수 있다. (sleep에 대한 상세한 정보는 17. 4절 [Sleeping] 를 참조하라. ) 다음의 예제를 보자.
어떤 목적으로도, 이것은 사용하기에 충분하다. 조금 더 복잡하기는 하지만, sigsuspend를 사용해서도 특정한 핸들러가 실행되는 동안 확실하게 기다릴 수 있다. 시그널이 도착하기를 기다리는 깨끗하고 신뢰 가능하다하다 방법은 그것을 블록하고 sigsuspend를 사용하는 것이다. 루프 안에서 사용된 sigsuspend는, 다른 종류의 시그널들이 그들의 핸들러에 의해 처리되는 동안, 어떤 종류의 시그널을 위해서 기다릴 수 있다. 함수 : int sigsuspend (const sigset_t *set)
코드의 마지막 부분은 약간 교묘하다. 이것의 핵심은 sigsuspend가 반환 할 때, 프로세스가 원래 가졌던 시그널 마스크의 값으로 재설정하는 것이다. 이 경우, SIGUSR1 시그널이 다시 블록되어진다. sigprocmask의 두 번째 호출은 이 시그널의 블록을 명백하게 해제할 필요가 있다. 다른 포인트 : 오직 하나의 SIGUSR1 시그널을 기다리는 그 프로그램에서 왜 while 루프가 필요한지 의아해 할지 모른다. 그 대답은, sigsuspend에 주어지는 마스크가, 예를 들어, 작업 제어 시그널처럼 다른 종류의 시그널이 배달됨으로 인해서 구동되어질 프로세스를 허가한다는 것이다. 만일 usr_interrput를 설정하지 않은 시그널에 의해 프로세스가 재개된다면, 그것은 단지 "올바른" 종류의 시그널이 발생할 때까지 다시 중지된다. 이 테크닉은 준비작업에 더 낳은 라인이 필요하지만, 당신이 시그널에 대한 정확한 기다림을 위해서는 필요하다. 실제로 기다림을 위한 코드는 단지 4줄뿐이다. 이 절은 BSD 유닉스에서 온 시그널 핸들링 함수들에 대해서 설명한다. 이들 도구들은 그들의 시대에서는 진보적이였지만; 오늘날은 굉장히 시대에 뒤떨어진 것이고, 오직 BSD와의 호환성을 위해서 제공되고 있다. POSIX 시그널 처리 기능들은 BSD 기능들로부터 나온 것이기 때문에 BSD와 POSIX 시그널 처리 기능들 사이에는 많은 유사성이 있다. 충돌을 피하기 위해서 모든 함수들이 서로 다른 이름을 갖고 있다는 것을 제외하고, 둘 사이에는 주요한 차이들이 있다. BSD 유닉스는 sigset_t 오브젝트로 시그널 마스크를 나타내는 것이 아니라 int 비트마스크로써 시그널 마스크를 표현한다. BSD 기능들은 인터럽트된 기본동작(primitive)을 실패하게 할 것인지 재개할 것인지의 여부에 대해서 다른 디폴트를 사용한다. POSIX 기능은 당신이 그들을 재개하도록 정할지라도 시스템 호출이 실패하게 만들고, BSD 기능들은, 당신이 그들을 실패하도록 정했을지라도 시스템 호출은 그것을 재개하도록 만드는 것이다. 21. 5절 [Interrupted Primitives] 참조. BSD 유닉스는 시그널 스택의 구상을 갖는다. 이것은 보통의 실행 스택대신에, 시그널 핸들러 함수들의 실행동안에 사용되는 대체스택 (alternate stack)이다. BSD 기능들은 `signal. h'에 선언되어 있다. 데이터타입 : struct sigvec
sighandler_t sv_handler
int sv_mask
int sv_flags
그들 기호 상수들은 sigvec 구조체의 sv_flags 를 위해 제공되는 값들로 사용될 수 있다. 이 필드는 비트마스크 값으로써, 당신이 관심을 갖는 플래그들을 비트별-OR를 사용해서 결합할 수 있다. 매크로 : int SV__ONSTACK
매크로 : int SV__INTERRUPT
매크로 : int SV__RESETHAND
함수 : int sigvec (int signum, const struct sigvec *action, struct sigvec *old_action)
함수 : int siginterrupt (int signum, int failflag)
매크로 : int sigmask (int signum)
함수 : int sigblock (int mask)
함수 : int sigsetmask (int mask)
함수 : int sigpause (int mask)
시그널 스택은 시그널 핸들러가 실행되는 동안 실행 스택으로써 사용되는 메모리의 특별한 영역이다. 오버플로우가 일어날 위험을 피하기 위해서는, 꽤 커야한다; 매크로 SIGSTKSZ는 시그널 스택을 위한 정규 크기로 정의되었다. 당신은 mallocac 을 사용해서 스택을 위한 공간을 할당할 수 있다. 그리고 나서 sigaltstack 이나 stgstack를 호출하여 시그널 스택을 사용하도록 시스템에게 알린다. 당신이 시그널 스택을 사용하기 위해서 시그널 핸들러를 달리 만들 필요는 없다. 다른 것에서 스택으로의 변경은 자동적으로 발생한다. 그렇지만, 어떤 기계 상에 존재하는 어떤 디버거는 시그널 스택을 사용하는 핸들러가 실행되는 동안 스택 트래이스(trace)를 하면 혼란스럽게 될지도 모른다. 분리된 시그널 스택을 사용하도록 시스템에게 알리기 위한 두 개의 인터페이스가 있다. sigstack은 오래된 인터페이스로써 4. 2 BSD 로부터 왔다. sigaltstack은 새로운 인터페이스로써 4. 4 BSD 로부터 왔다. sigaltstack 인터페이스는 스택의 성장 방향을 알리도록 당신의 프로그램에게 요구하지 않고, 정해진 기계와 운영체제에 의존한다는 편리점을 갖는다. 데이터 타입 : struct sigaltstack
void *ss_sp
size_t ss_size
SIGSTKSZ
MINSIGSTKSZ
int ss_flags
SA_DISABLE
SA_ONSTACK
함수 : int sigaltstack (const struct sigaltstack *stack, struct sigaltstack *oldstack)
EINVAL
ENOMEM
다음은 오래된 sigstack 인터페이스이다. 당신은 sigaltstack 대신에 사용할 수 있다. 데이터 타입 : struct sigstack
void *ss_sp
int ss_onstack
함수 : int sigstack (const struct sigstack *stack, struct sigstack *oldstack)
목차 이전 : 20. 비-지역 탈출 다음 : 22. 프로세스의 시동과 종료
21. 2. 2 종료 시그널
12. 2. 3 알람 시그널
역자주 : profiling: 프로파일링 : 시스템의 성능 및 병목현상을 방지하기 위한 도구라고 생각하시면 될 것 같네요. 정확하지가 않아서. .
21. 2. 4 비동기 입/출력 시그널
21. 2. 5 작업 제어 시그널
21. 2. 6 잡다한 시그널
21. 2. 7 비표준 시그널
21. 2. 8 시그널 메시지
21. 3 시그널 동작 정하기
21. 3. 1 기본 시그널 처리
21. 3. 2 진보된 시그널 처리
21. 3. 3 signal 과 sigaction 의 상호작용
21. 3. 4 sigaction 함수 예제
21. 3. 5 sigaction을 위한 플래그
21. 3. 6 초기 시그널 동작들
21. 4 시그널 핸들러 정의하기
21. 4. 1 반환하는 시그널 핸들러
21. 4. 2 프로세스를 종료시키는 핸들러
21. 4. 3 핸들러 안에서 비지역 제어 이동
21. 4. 4 핸들러가 실행되고 있는 동안 도착한 시그널들
21. 4. 5 한가지로 합병한 서로 밀접한 시그널들
21. 4. 6 시그널 핸들링 과 재진입 불가 함수들
** 역자주 : 재진입(reentrant) : 어떤 모듈을 재진입이 가능하다라고 할 때, 그것은 동시에 두 개 이상의 프로그램에 의하여 공유될 수 있는 모듈을 말한다. 이러한 모듈은 실행 중에 자신의 코드 또는 데이터 영역을 변경시키지 않아야 합니다.
21. 4. 7 원소 데이터 억세스와 시그널 핸들링
21. 4. 7. 1 비-원자 억세스가 갖는 문제점
21. 4. 7. 2 원자 형
21. 4. 7. 3 원소단위 사용 형태.
21. 5 시그널에 의해 인터럽트된 기본동작 ( Primitives )
21. 6 시그널 발생시키기
21. 6. 1 스스로에게 신호 보내기
이식성 노트: raise 는 ANSI C 위원회에 의해 만들어졌다. 오래된 시스템들은 그것을 지원하지 않을 것이기 때문에, 이식성을 위해서는 kill을 사용하라. 21. 6. 2절[Signaling Another Process] 참조.
21. 6. 2 다른 프로세스에게 시그널 보내기
21. 6. 3 kill을 사용하기 위한 허가
21. 6. 4 통신을 위해서 kill을 사용하기
21. 7 시그널 블록하기
21. 7. 1 왜 시그널 블록킹 ( Blocking ) 이 유용한가
21. 7. 2 시그널 설정
21. 7. 3 프로세스 시그널 마스크
21. 7. 4 시그널의 배달 여부를 테스트하기 위한 블럭킹
21. 7. 5 핸들러를 위하여 블록된 시그널
21. 7. 6 미해결 시그널 체크하기
21. 7. 7 나중에 동작하도록 시그널을 기억하기
21. 8 시그널을 위한 기다림
21. 8. 1 pause 사용하기
21. 8. 2 pause 사용의 문제들
21. 8. 3 sigsuspend 사용하기
21. 9 BSD 시그널 핸들링
21. 9. 1 POSIX 와 BSD 시그널 기능들
21. 10 핸들러 함수를 만들기 위한 BSD 함수
21. 10. 1 블록된 시그널을 위한 BSD 함수들
21. 10. 2 분리된 시그널 스택 사용하기
댓글을 달아 주세요
여덟시간 메모장 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운영체제에서 잠금에서 발생할 수 있는 문제는 ?
쓰레드에서의 공유 파일에 대한 파일잠금
Leave your greetings here.
#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 */
Leave your greetings here.
TCP는 UDP와는 달리 connection-oriented protocol인 관계로, 연결을 맺거나 특히 끊을 때,
많은 확인 절차를 거치게 됩니다. 이로 인하여, server와 client 간에 약간의 overhead가 있을
수 있습니다. 특히, web server로 사용되는 system의 경우, 대부분의 client들이 그리 안정적
이지 않은 PC client이므로, connection을 맺던 client application이 crash되는 경우 web serve
r에는 다소간의 문제가 발생할 여지가 있습니다. 가장 대표적인 문제가 FIN_WAIT_2 상태입
니다. 이 상태가 무엇을 뜻하는지, 이로 인해 어떤 문제가 있을 수 있는지, 또 그에
대한 대책은 무엇인지 설명토록 하겠습니다.
TCP socket connection 상태의 종류
다음은 RFC 793 에 나와있는 그림으로, TCP connection을 맺고 있는 두 system이 정상적으로 connection을 끝내는 과정을 보여줍니다.
TCP A TCP B
1. ESTABLISHED ESTABLISHED
2. (Close)
FIN-WAIT-1 → <SEQ=100><ACK=300><CTL=FIN,ACK> → CLOSE-WAIT
3. FIN-WAIT-2 ← <SEQ=300><ACK=101><CTL=ACK> ← CLOSE-WAIT
4. (Close)
TIME-WAIT ← <SEQ=300><ACK=101><CTL=FIN,ACK> ← LAST-ACK
5. TIME-WAIT → <SEQ=101><ACK=301><CTL=ACK> → CLOSED
6. (2 MSL)
CLOSED
Normal Close Sequence
먼저, A가 connection을 close 하면서, 종료 신호인 FIN segment를 A가 B로 보내고, A는 FIN-
WAIT-1 상태로 들어갑니다. 이 상태에서는 A system의 user는 더 이상의 SEND는 사용할 수
없게 되지만, RECEIVE는 계속 가능합니다. B가 그 신호를 받으면 B는 CLOSE-WAIT에 들어가
면서 그에 대한 acknowledge를 보내옵니다. A는 이를 받으면 FIN-WAIT-2 상태가 되고, B는
connection을 close하면서 자신의 FIN을 보냅니다. 이 때 A는 TIME-WAIT 상태가 되어 최종적
으로 B와의 connection이 닫히는지 확인하게 됩니다. 즉, server가 client로부터 FIN에 대한
ACK을 받고나서, client의 FIN을 기다리는 상태가 FIN-WAIT-2 상태입니다. 문제는,
이 FIN-WAIT-2 상태에 대해서는 원래 time-out 값이 정해져 있지 않아서, client가 FIN을 보내
오기 전에 client의 application이나 client 자체가 crash되는 경우, 또는, client program이나
server program에 bug가 있어서 이를 제대로 처리 못하는 경우에는, server에 남아있을
FIN-WAIT-2 상태는 server가 rebooting하기 전에는 끝까지 남아 있게 된다는 것입니다.
이는 netstat -a 명령어로 확인해 볼 수 있는데, 이러한 상태의 connection이 많이 쌓이다보
면 문제를 일으킬 수도 있습니다.
FIN_WAIT_2 에 대한 대책
가장 좋은 해결책은 bug가 있는 client 또는 server program을 수정하는 것입니다. 대부분의
문제는 그 문제로부터 발생하기 때문입니다. Server에서 운영되는 가장 대표적인 application
은 Apache webserver일 것입니다. Apache는 이 문제에 대한 해결책으로 lingering_close() 함
수 대신 SO_LINGER 함수를 사용하도록 권하기도 합니다. (자세한 것은 아파치 홈페이지
www.apache.org를 참조하시기 바랍니다.) 다른 UNIX들은 FIN-WAIT-2에 대해, time-out 값
을 지정함으로써 이 문제를 회피하도록 해줄 수 있게 해주기도 합니다. Solaris, HP/UX, Linux
등 많은 OS가 이러한 기능을 제공합니다. 이것들은 RFC 793을 위반하는 것입니다만, 필요에
의해 각 vendor가 제공하는 것입니다. AIX는 (다른 UNIX와 마찬가지로) TCP keepalive timer
를 제공함으로써 이 문제를 해결합니다.
# no -a | grep tcp_keep
tcp_keepintvl = 150
tcp_keepidle = 14400
위의 값이 default 값이며, 이 경우, 150 * 500ms clock tick = 75 초, 그리고 14400 * 500ms
clock tick = 2 시간이 됩니다.
keepalive timer는 data segment를 보내면서 reset됩니다. 만약 주어진 tcp_keepidle tick
동안 아무 data segment도 보내지지 않았다면 먼저, 첫번째 keepalive probe packet이 보내
집니다.
이 packet에 대해 reply가 없을 경우, tcp_keepintvl에 정해진 tick만큼을 간격으로 하여, 추가
의 keepalive packet을 보내게 됩니다.
만약 이 keepalive에 대해 reply가 계속 없을 경우, 그 connection은 close됩니다. AIX 4.2에서
는 TCPTV_KEEPCNT (기본값 8)에 해당하는 개수의 probe에 대해 응답이 없을 경우 close되
는데, AIX 4.3에서는 이 no attribute가 없어졌습니다.
tcp_keepinit는 초기 연결시 응답이 없을 경우에 사용되는 값입니다. 따라서, 일단 connection
을 맺고 나서 발생하는 문제에 대해서는 적용되지 않습니다.
위에 따르면, FIN_WAIT_2가 발생하는 경우, default로 2시간 10분 후에야 그 connection이 c
lose되도록 되어 있습니다. 만약, 매우 busy한 web server인 경우에는 이 값들을 조절하는 것
이 필요할 수 있습니다.
참고로 첨부파일을 확인해 주시기 바랍니다.
Leave your greetings here.
소개
확장성을 확보하기 위한 방법은 여러가지가 있을 것이다. 여기에서는 그중 PlugIn 방식을 이용한 확장성확보에 대한 내용을 다룰 것이다.Agent&Manager 방식의 프로그램을 만든다고 가정해보자. SNMP 프로토콜을 응용한 Net SNMP가 가장 대표적인 경우가 될 것이다. 이왕 Net SNMP를 예로 들었으니, Agent&Manager 방식의 SMS을 만드는 것으로 가닥을 잡아보겠다.
이러한 시스템에서 Agent 프로그램을 만들려고 한다면, 설계단계에서 가장 중요하게 생각해야 할게 확장성의 확보가 될 것이다. 왜냐면 시스템 관리의 범위가 매우 넓은 관계로 필요에 따라 관리 요소가 계속 추가될 수 있기 때문이다. 당장 생각나는게, CPU, Memory, Disk 관리쯤이 될것이다. 물론 초기에 완전하게 관리요소를 몽땅 예상하고 설계를 하는 방법도 있겠지만, 그렇게 될 경우 설계에 지나치게 많은 시간을 소비해야 할 것이다. 막상 그렇게 만들었다고 해도, 중간쯤 만들다 보면 관리요소가 새로 추가될 수도 있다. 심지어는 모두 만들고 나서 관리요소가 추가될 수도 있을 것이다.
이러한 경우 PlugIn 방식으로 각각의 성능을 모듈화 시켜서 붙이는 방식으로 개발시간을 아낄 수 있다. 거기에 덤으로 유연하고 확장성 좋은 시스템을 만들 수도 있다. 대략 다음과 같은 시스템 구성을 가지게 된다.
필요한 기술
이 문서를 읽기 위해서는 함수포인터, STL, 라이브러리를 제어하기 위한 기술들을 가지고 있어야 한다.Dynamic Module Loading
동적으로 모듈을 로딩하는 PlugIn방식을 구현하기 위한 기본적인 기술요구 사항은 그리 복잡하지 않다. 동적라이브러리를 이용하면, 쉽게 구현할 수 있다. 이 경우 중요한 것은 모듈과 Agent와의 인터페이스를 통일하는게 될 것이다. 어떠한 기능이 모듈형태로 추가되더라도, Agent와 Manager의 소스코드 수정없이 모듈이 로딩될 수 있어야 하기 때문이다.인터페이스 이름을 맞추는 것은 문제가 되지 않을 것이다. 문제는 인터페이스를 통해서 이동하는 데이터가 될것이다. 이는 각각의 성능마다 보여줘야 하는 정보가 다를 수 있기 때문인데, CPU의 경우라면 사용율을 Disk라면 장치명,마운트이름,사용율을 보내야 하기 때문이다. 그러므로 예상가능한 모든 종류의 데이터를 처리할 수 있는 방법이 준비되어야 한다. 이 문제는 세가지 정도의 방식으로 해결할 수 있다.
- 문자열 전송
간단하게 문자열을 전송한다. 성능=값1,값2 정도로 보내면 될 것이다. 포맷은 대략 아래와 같을 것이다. 성능이름이 들어가는 이유는, 나중에 Manager로 정보가 전달되었을때, 성능이름을 Key로 해서, 해당되는 모듈을 Plugin 방식으로 로딩하기 위함이다.CPU=89
DSK=/dev/sda1,/root,58 - 구조체전송
구조체로도 전송이 가능하다. Agent는 데이터를 처리하지 않고, Manager로 보내기만 하면되므로, 구조체에 어떤 멤버변수들이 있는지는 알 필요가 없다. 단지 보내야 하는 구조체의 크기와 성능이름만 알고 있으면 된다. 구조체를 받은 Manager 측은 성능이름에 해당되는 Plugin 모듈을 로딩해서 구조체의 값을 처리하면 된다.struct Info
{
int size; // 구조체의 크기
char id[4]; // 성능 이름 : DSK, CPU, MEM...
... // 나머지 정보들은 성능에 따라 달라질 수 있다.
...
} - XML 데이터 전송
잘 정의한다면, 유연하게 사용할 수 있을 것이다. 데이터의 크기가 커진다는 점을 고려하지 않아도 된다면, 가장 좋은 방법이라고 생각된다.
Module Config
이제 설정파일을 만들어야 한다. 이 설정파일은 Key라고 할 수 있는 모듈 ID와 호출해야할 라이브러리의 이름들을 가진다. 다음과 같은 구조를 가지도록 하겠다.[plugin]설정파일을 읽을 수 있는 라이브러리가 필요할 것 같아서, 급조한 코드가 있다. 간단 설정파일 Reader를 참고하기 바란다. 이 코드를 그대로 사용할 것이다.
CPU=libmycpu.so
MEM=libmymem.so
프로시져
- 실행
- plugin 을 로딩하기 위해서 설정파일을 읽어들인다.
- plugin 목록을 읽어들인다.
- plugin 목록의 갯수만큼 루프를 돌면서, 라이브러리를 동적으로 적재한다.
- while 루프를 돌면서, 공통 인터페이스를 호출한다.
공통 인터페이스
공통 인터페이스를 정의해보도록 하자. 최대한 간단하게 정의하도록 하겠다.- Init : 플러그인 모듈을 초기화 한다.
- Read : 플러그인 모듈로 부터, 데이터를 요청한다. 데이터는 문자열로 Key.IndexNum=Value,Value 형식으로 전달된다. IndexNum은 데이터가 2개이상일때, 사용하는 인덱스 번호다. 예를 들어 CPU가 2개라면
- CPU.1=87
- CPU.2=21
- CPU.1=87
- RowNum : 몇개의 데이터가 있는지를 알려준다.
- CPU가 2개라면, 2를 출력한다.
- CPU가 2개라면, 2를 출력한다.
- 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>이 코드는 공유라이브러리로 컴파일을 한다. 라이브러리의 이름은 libdummy.so 로 하겠다. 공유라이브러리를 만드는 방법은 라이브러리 만들기 문서를 참고하기 바란다.
#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;
}
이제 아래와 같은 설정파일 만들고, 실행하면 된다.
[PLUGIN]
DUMMY=libdummy.so
Leave your greetings here.