태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.
블로그 이미지

haRu™'s Thinks

느리게 걷기, 천천히 생각하기, 그리고 한 발 뒤에서 바라 보기… by haruroh


'리눅스'에 해당되는 글 29건

  1. 2007.12.24 20. Non-Local Exits
  2. 2007.12.22 부록 C : Maintenance
  3. 2007.12.22 부록 A : Language Features
  4. 2007.12.22 27. System Configuration
  5. 2007.12.22 26. System Information
  6. 2007.12.22 25. 사용자와 그룹
  7. 2007.12.22 24. Job Control(작업 제어)
  8. 2007.12.22 23. Processes
  9. 2007.12.22 22. 프로세스의 시동과 종료(Process StartUp)
  10. 2007.12.22 21. 신호처리(Signal)

20. Non-Local Exits

목차 이전 : 19. 지역과 세계화 다음 : 21. 시그널 처리 


20 비-지역 탈출

때때로 당신의 프로그램이 호출한 함수의 깊게 내포된 곳에서 평소와는 다른 상황을 검출할 때, 당신은 제어수준의 밖으로 즉시 반환이 가능하게

되는 것이 좋을 것이다. 이 절은 그와같은 비-지역 탈출의 상황에서

어떻게 setjmp 와 longjmp를 사용할 수 있는지를 설명한다.


20. 1 비-지역 탈출들에 대한 안내

비-지역 탈출이 유용할 수 있는 상황에 대한 예제로서, 프롬프트 상에서 명령문으로 실행하는 "메인 루프"를 가진, 즉 상호 작용하는 프로그램이 있다고 가정하자. 그리고 "read"라는 명령문으로는 파일로부터 입력을 읽어서, 그것이 처리되는 동안 입력의 어휘와 구문을 분석한다고 가정하자. 그때 만일 저-수준 입력 에러가 검출되면, 어휘와 구문분석 각각을 만들고, 내포된 호출에 의해 검출된 에러상황을 명백하게 취급하는 모든 처리상황을 만들기보다는 "메인 루프"로 즉시 반환 가능하도록 하는 것이 유용할 것이다. ( 다른 한편으로, 만일 파일들을 닫거나, 버퍼나 다른 데이터 구조체를 해제하는 등의 그와같은 exit의 상황일 때 그들 상황에서 그들에게 할당된 실제적인 양을 소거한다면_보통 반환을 하고 그 자체가 소거되는 것이 더 적당할 수 있다, 왜냐하면 비-지역 탈출은 방해되는 상황들을 무시하고 그들에 연관된 전체 코드를 소거하기 때문이다. 선택적으로 당신은 "메인 루프"의 뒤나 또는 앞에서 명백하게 소거를 할 수 있다. )

어떤 방법에서, 비-지역 탈출은 함수로부터 반환을 하는 `return' 구문을 사용하는 것과 유사하다. 그러나 `return'은 오직 단일하게 호출된 함수로부터 빠져나와서 그것이 호출되었던 지점의 뒤로 호출을 넘기지만, 비-지역 탈출은 내포된 함수 호출의 여러 단계를 빠져나올 수 있다.

당신은 setjmp 함수를 호출해서 비-지역 탈출을 위한 반환지점을 확인하라. 이 함수는 object의 타입 jmp_buf안에 실행환경에 대한 정보를 저장한다. setjmp의 호출 후에도 프로그램의 실행은 정상적으로 계속되지만, 만일 한 exit가 그것에 부합되는 jmp_buf 오브젝트를 인수로 하여 longjmp를 호출함으로써 이 반환 지점보다 나중에 만들어졌다면 제어는 setjmp가 호출되었던 그 지점의 뒤로 넘겨진다. setjmp로 부터의 반환값은, 보통의 반환과 longjmp를 호출함으로 해서 만들어진 반환과의 구별하기 위해 사용되므로, setjmp는 보통 `if'문안에 나타난다.

위에 설명된 예제 프로그램이 어떻게 만들어지는지를 보여준다.

#include <setjmp. h>
#include <stdlib. h>
#include <stdio. h>
 
jmp_buf main_loop;
 
void
abort_to_main_loop (int status)
{
longjmp (main_loop, status);
}
 
int
main (void)
{
while (1) {
if (setjmp (main_loop))
puts("Back at main loop. . . . ");
else
do_command();
}
}
 
void
do_command(void)
{
char buffer[128];
if (fgets (buffer, 128, stdin) == NULL)
abort_to_main_loop (-1);
else
exit (EXIT_SUCCESS);
}
 
함수 abort_to_main_loop는 그것이 어디로부터 호출되었는지에 상관없이, 프로그램의 메인 루프로 제어를 즉시 돌려준다.
 
메인 함수 안의 제어의 흐름은 처음에 조금 이상한일이 발생할지도 모르지만, 그러나 그것은 실제로 setjmp에서 사용하는 보통의 관용이다. setjmp는 보통 0을 반환하므로, 그래서 조건절 "else"가 실행되어진다. 만일 abort_to_main_loop가 do_command의 실행 중에 어느 곳에서 호출되어지면, 그것은 메인에서 setjmp를 호출하는 것처럼 나타난다. 그래서 setjmp를 사용하는 일반적 양식은 다음과 같다:
 
if (setjmp (buffer))
/* 때이른 반환 후에 소거하려는 코드 */
. . .
else
/* 반환 지점을 설정한 후에 일반적으로 실행되어지는 코드 */
. . .


20. 2 비-지역 분기의 상세한 설명

여기에서는 비-지역 분기를 수행하기 위해 사용되는 함수들과 데이터 구조체에 대해서 설명한다. 이들 도구들은 `setjmp. h'에 선언되어 있다.

데이터 타입 : jmp__buf

오브젝트의 타입 jmp_buf는 비-지역 분기에 의해 되돌려질 상황 정보를 저장한다. jum_buf의 내용들은 반환하기 위한 지정된 장소를 알린다.

매크로 : int setjmp (jmp_buf state)

보통 호출되었을 때, setjmp는 state안에 현재 실행중인 프로그램의 상황에 대한 정보를 저장하고 0을 반환한다. 만일 longjmp가 이 state로 비-지역 분기를 수행하기 위해 나중에 사용된다면, setjmp 는 0이 아닌 값을 반환한다.

함수 : void longjmp (jmp_buf state, int value)

이 함수는 state에 저장된 상황으로 현재 실행을 되돌리고, 반환 지점을 만들었던 setjmp를 호출한 지점부터 실행을 계속한다. longjmp에 의한 setjmp로부터의 반환은 longjmp에 주어졌던 0이 아닌 인수값을 반환한다. (그러나 만일 값이 0으로 주어지면, setjmp는 0을 반환한다. )

setjmp와 longjmp의 사용에는 많이 모호하지만 중요한 제한들이 있다. 이들 제한의 대부분은 비-지역 분기가 C 컴파일 상에서 상당한 양의 신비한 힘(?)을 요구했고 이상한 방법으로 언어의 다른 부분들과 함께 영향을 끼칠 수 있기 때문에 발생한다.

setjmp함수는 함수정의가 없는 매크로이다, 그래서 당신은 `#undef를 사용하거나 또는 그것의 주소를 취하려 할 수가 없다. 그것에 더하여, setjmp를 호출하는 것은 오직 다음과 같은 경우에만 안전하다.

  • 선택이나 반복구문을 시험하기 위한 표현으로(`if' 또는 `while'와 같은)
  • 선택이나 반복구문을 시험하기 위한 표현으로 사용한 동등 또는 비교 연산자에서 사용된 피연산자의 하나로써. 그때 다른 피연산자는 정수 상수 표현이어야만 한다.
  • 선택이나 반복구문을 시험하기 위한 표현으로 사용한 단항연산자`!'의 피연산자로 사용할 때.
  • 표현식으로 자체로 사용할 때.

반환지점은 그들을 만들기 위해서 setjmp를 호출했던 그 함수가 계속 작동하고 있을때만 유용하다. 만일 당신이 이미 반환되어진 함수에서 만들었던 반환지점으로 longjmp를 호출하면, 예측할 수 없고 불행한 일들이 발생될 것이다.

당신은 longjmp의 인수로써 0이 아닌 값을 사용수도 있다. longjmp는 setjmp로부터의 반환값인 0 인수를 되돌려주기를 거부하므로, 갑자기 발생하는 실수들에 대항한 안전망으로써의 역할을 할 수도 있지만 좋은 프로그래밍 스타일은 아니다.

당신이 비-지역 분기를 수행할 때, 검색 가능한 오브젝트들은 longjmp가 호출되었을 때 그들이 가졌던 값이 무엇이든지 일반적으로 계속 유지한다. 그러나 자동 지역변수의 경우에는 setjmp가 호출된 이후에 변경된다. 즉 그들을 휘발성으로 선언하지만 않으면 그들은 변경되지 않는다.


20. 3 비-지역 분기와 신호

BSD 유닉스 시스템에서, setjmp와 longjmp는 블록된 신호들을 저장하고 반환한다; 21. 7절 [Blocking Signals] 참조. 그렇지만, POSIX. 1 표준은 setjmp와 longjmp가 블록된 신호들을 변경하지 않을 것을 요구하고, BSD처럼 행동하기 위한 함수들(sigsetjmp와siglongjmp) 두 개를 부가적으로 제공한다.

GNU 라이브러리에서 setjmp와 longjmp의 행동은 테스트 매크로에 의해 제어된다; 1. 3. 4절 [Feature Test Macros] 참조. GNU 시스템에서 디폴트로 사용하는 것은 BSD가 아니라 POSIX. 1 이다. 이 절에 있는 도구들은 헤더파일 `setjmp. h'에 선언되어 있다.

데이터 타입 : sigjmp__buf

이것은 블록된 신호들에 대한 상황 정보를 저장할 수 있다는 것을 제외하고는 jmp_buf와 유사하다.

함수 : int sigsetjmp (sigjmp_buf state, int savesigs)

이것은 setjmp와 유사하다. 만일 savesigs가 0이 아니면, 블록된 신호들의 집합은 state에 저장되고 만일 siglongjmp가 나중에 이 state를 가지고 수행되면 반환되어질 것이다

함수 : void siglingjmp (sigjmp_buf state, int value)

이것은 state인수의 타입이 다르다는 것을 제외하고는 longjmp와 유사하다. 만일 0이 아닌 savesigs 플래그를 사용했던 state를 설정하고 sigsetjmp를 호출하면, siglongjmp는 또한 블록된 신호들의 집합을 반환한다.


목차 이전 : 19. 지역과 세계화 다음 : 21. 시그널 처리 

'Programming > Unix Programing' 카테고리의 다른 글

20. Non-Local Exits  (0) 2007.12.24
부록 D : GENERAL PUBLIC LICENSE 2  (0) 2007.12.22
부록 C : Maintenance  (0) 2007.12.22
부록 B : Library Summary  (0) 2007.12.22
부록 A : Language Features  (0) 2007.12.22
27. System Configuration  (0) 2007.12.22
Comment 0 Trackback 0
Top

부록 C : Maintenance

목차 이전 :  부록 B 라이브러리 기능들의 요약 다음 : 부록 D. GPL2


죄송합니다. 제가 가진 문서에서 이 장이 손실 되었습니ㄷ.(ㅜ.ㅜ)

3장으로 카피가 되이 있습니다. 혹시 가지고 계신 분 공유 좀 부탁합니다.

'Programming > Unix Programing' 카테고리의 다른 글

20. Non-Local Exits  (0) 2007.12.24
부록 D : GENERAL PUBLIC LICENSE 2  (0) 2007.12.22
부록 C : Maintenance  (0) 2007.12.22
부록 B : Library Summary  (0) 2007.12.22
부록 A : Language Features  (0) 2007.12.22
27. System Configuration  (0) 2007.12.22
Comment 0 Trackback 0
Top

부록 A : Language Features

목차 이전 : 27. 시스템 구성 파라미터 다음 : B. 라이브러리 기능들의 요약


부록 A 라이브러리에 있는 C 언어 기능들

C 라이브러리에 의해서 실행되는 기능들 중 어떤 것은 C 언어 그 자체의 일부처럼 생각될 수 있다. 그 기능들은 라이브러리 매뉴얼이 아니라, C 언어 매뉴얼에서 문서화되어야 하지만, 우리는 아직 그 C 언어 매뉴얼을 만들지 않았기 때문에, 그 기능들을 이곳에서 설명할 것이다.


A. 1 내부적 일관성을 명백하게 테스트하기

당신이 프로그램을 만들 때, "불가능한" 에러나 기본 가설의 위반에 대해서 전략적인 위치에서 그들을 체크하는 것은 좋은 생각이다. 그 체크는 프로그램의 다른 부분들 사이에서 불화합으로 일어나는 문제들을 디버깅하는데 유익하다.

헤더파일 `assert. h'에 정의된, assert 매크로는 프로그램의 에러가 검출된 곳에서 에러메세지를 출력하는 동안 프로그램을 중지시키기에 편리한 방법을 제공한다. 당신이 당신의 프로그램을 디버깅하려할 때, 당신은 정의된 매크로 NDEBUG를 사용해서 재컴파일하면 assert 매크로에 의해서 수행됐던 에러 체크는 디버그하지 않을 수 있다. 이것은 기능무효(disable)로 체크된 프로그램 소스 코드는 변경하지 않는다는 것을 의미한다.

그러나 그 체크를 무효화(disabling)하는 것은 그 프로그램을 확연하게 느리게 만들지 않는 한 바람직하지 않다. 또한 누가 그 프로그램을 실행시키던지 발생할 수 있는 가능한 에러에 대해서 대비하도록 좀더 많이 에러를 체크해보는 것이 좋다. 똑똑한 사용자는 어떤 것 이 잘못되었을 때 그것을 지적하지 않고 무의미한 반환값을 갖는 것보다는 차라리 프로그램이 파손되는 것을 원할 것이다.

매크로 : void assert (int expression)

프로그램 안에서 그 expression이 그 순간 0이 아닌 값이 되는지를 검증한다. 만일 NDEBUG 가 정의되지 않으면, assert는 expression의 값을 테스트하는데, 그 값이 0이면, assert는 다음 형식처럼 메시지를 프린트한 후에 프로그램을 중지시킨다 ( 22. 3. 4절 [Aborting a Program] 참조)
 
`file': linenum: Assertion `expression' failed.
표준 에러 스트림 stderr에 메시지를 프린트한다 ( 7. 2절 [Standard Streams] 참조. ). 파일 이름과 라인 번호는 C 전처리기 매크로 __FILE__ 과 __LINE__ 어로부터 가져오고 그 파일이름과 라인번호는 assert가 쓰여져서 호출된 곳으로 정한다. 만일 전처리기 매크로 NDEBUG가 `assert. h'가 인클루드된 지점에서 정의되었다면, assert 매크로는 완전히 아무 것도 하지 않도록 정의된 것이 된다.
 
주의 : 만일 NDEUG가 영향력을 발휘하는 곳에서는 인수 expression 표현식도 평가되지 않는다. 그러므로 부작용이 있을 법한 인수를 넣고 assert를 사용하지 말아라. 예를 들어, assert(++i>0); 은 만일 NDEBUG가 정의된 곳에서는 i변수가 증가되지 않기 때문에 좋지 않다.
 
사용법 노트: assert 기능은 내부적 모순을 검출하기 위해서 설계되었다; 그러므로 사용자에 의한 부적당한 사용이나 무효한 입력을 보고하는데는 적당하지 않다.

매크로 assert에 의해 프린트된 진단 메시지에 있는 정보는, 당신의 프로그램을 사용하는 사용자에게 왜 입력이 무효한지 또는 왜 명령이 실행되지 않았는지를 알리는데는 유용하지 않지만, 프로그래머나 당신이, 프로그램에서 나타난 버그를 추적하는데 도움이 된다. 그러므로 당신은 예측할 수 없는 사건에 대한 에러메세지를 출력하는데 assert를 사용할 수 없다.

더 말하자면, assert에 무효한 입력이 주어진다면 당신의 프로그램은 중지된다_그것은 에러메세지를 출력한 후에 0이 아닌 상황(22. 3. 2절 [Exit Status] 참조. )으로 종료되거나, 또는 다른 명령을 읽거나 다음 입력 파일로 옮긴다. 프로그램에 나타나지 않은 버그 문제에 에러메세지를 출력하는 것에 대한 정보는 2. 3절 [Error Messages] 참조.


A. 2 가변인자 함수들

ANSI C 는 인수의 타입이나 개수를 다양하게 취할 수 있는 함수를 선언하기 위한 구문을 정의한다. ( 그와같은 함수들은 varargs 함수 또는 variadic 함수라고 부른다. ) 그렇지만, 언어 그 자체는 그와같은 함수들을 위한 메커니즘을 제공하지 않는다; 대신에, `stdarg. h'에 정의된 가변 인수 매크로들을 사용하는 것이다. 이 절은 어떻게 가변인자 함수들을 선언하고, 어떻게 사용하며, 호출할 것인지에 대해서 설명한다.

 
이식성 노트 : 많은 오래된 C 방언들은 `varargs. h'를 사용해서 다양한 개수의 변수를 정의하는 함수 메커니즘으로 유사한 것을 제공하지만, 호환성이 없다.

A. 2. 1 왜 가변인자 함수들이 사용되는가?

원래 C 함수들은 고정된 개수의 인수들을 취한다. 당신이 함수를 정의할 때, 당신은 각 인수의 데이터 타입을 정한다. 함수가 호출될때마다 그전에 정해진 예상된 개수의 인수들이 공급되는데, 그 인수들의 타입은 정해진 것으로 변경될 수 있는 것이다. 그래서, 만일 함수 `foo'가 foo(int, char *); 로 선언된다면, 하나는 숫자 다른 하나는 문자열 포인터인 두 개의 인수를 가지고 foo 함수를 호출해야만 한다.

그러나 어떤 함수들은 정해지지 않은 개수의 인수를 받아들일 수 있는 동작을 수행한다. 어떤 경우, 함수는 한 블록에 그들의 모두를 처리함으로써 여러 개수의 값들을 처리할 수 있다. 예를 들어, 정해진 값들의 집합을 저장하기 위해서 malloc으로 일차원 배열을 할당하는 함수를 고려해보자. 이 연산은 숫자에 해당하는 배열의 길이로써 어떤 개수의 값이 있다고 이해한다. 가변 인수 기능이 없다면, 당신은 가능한 배열 크기를 얻어내는 또 다른 한 개의 함수를 정의해야만 한다.

라이브러리 함수 printf ( 7. 9절 [Formatted Output] 참조. )는 가변인수가 유용하게 쓰이는 다른 부류의 함수에 대한 예제가 된다. 이 함수는 규정된 템플리트 문자열의 제어 하에 인수들( 개수뿐만 아니라 다양한 형을 가질 수 있다)을 프린트한다. 가변인자 함수는 많은 인수들을 처리할 수 있다는 점에서 가변인수 함수를 정의하는 이유가 된다.

open과 같은 함수들은 고정된 개수의 인수들을 취하지만, 때때로 마지막 몇 개는 무시된다. ANSI C 는 그 함수를 가변으로 정의하도록 요구하지만; GNU C 컴파일러와 대부분 다른 C 컴파일러들을 고정된 인수를 취하는 함수처럼 정의하도록 허용하고 선언할 때만 가변으로써 그 함수를 선언한다 (또는 그 인수들을 전혀 선언하지 않는다. ).


A. 2. 2 어떻게 가변인자 함수를 정의하고 사용하는가?

가변인자 함수를 정의하고 사용하는 세 가지 단계이다.

인수리스트안에 생략표시 (`. . . ')를 사용하고, 가변 인수들을 억세스 하도록 특별한 매크로들을 사용하여서, 가변인수 함수를 정의하라. A. 2. 2. 2절 [Receving Arguments] 참조.

그것을 호출하는 모든 파일에서, 생략표시 (`. . . ')와 함께 프로토타입을 사용해서 가변으로써 함수를 선언하라. A. 2. 2. 1절 [Varidic Prototypes] 참조.

고정된 인수들 뒤에 가변인수들이 뒤에 나오도록 해서 함수를 호출하라. A. 2. 2. 4 [Calling Variadics] 참조.


A. 2. 2. 1 가변 인수들을 위한 구문

가변 인수를 받아들이는 함수는 올바른 프로토타입으로 선언되어야만 한다. 당신은 보통 고정된 인수들을 사용하고 가변인수들의 가능성을 지적하기 위해서 `. . . '을 취한다. ANSI C 구문은 `. . . '가 나오기 전에 적어도 한 개의 고정 인수를 필요로 한다. 예를 들어,

int
func (const char *a, int b, . . . )
{
. . .
}

고정된 두 개의 인수로써, const char * 와 int 인수를 취하고 int형의 값을 반환하는 func 함수의 정의이다. 그 두 개의 고정인수 다음에 알려지지 않은 인수들이 몇 개가 따르게 된다.

 
이식성 노트 : 어떤 C 컴파일러에서, 함수 정의에서 가변인수는 형을 선언하여 등록될 수 없다. 좀더 자세히 말하면, 이 인수들의 타입은 자체-진행(self-promoting)이 되어야만 한다: 즉, 디폴트 진행은 그 타입들을 변경하지 않아야 한다. 이것은, float , char(부호가 있던지 없던지), 그리고 short int (부호가 있거나 없거나) 뿐만 아니라 배열과 함수들의 타입을 무시한다.

A. 2. 2. 2 인수 값들을 받기

보통 고정된 인수들은 개별적인 이름을 갖고, 당신은 그들의 값을 억세스하기 위해서 그들의 이름을 사용할 수 있다. 그러나 가변 인수들은 아무런 이름을 갖지 않는다. 어떻게 당신이 그들을 억세스 할 것인가? 그들을 억세스하기 위한 유일한 방법은 그들이 기록된 순서대로, 순차적으로 억세스하고 다음 세 가지 단계에서 있는 헤더파일 `stdarg. h'에 선언된 특별한 매크로들을 사용해야만 한다.

1. va_start를 사용해서 va_list 형의 포인터 변수를 인수로써 초기화한다. 초기화된 인수 포인터는 첫 번째 가변인수를 가리킨다.
2. va_arg를 호출함으로써 가변인수들을 억세스 한다. va_arg를 첫 번째 호출하면, 첫 번째 인수를 반환하게 되고, 다음 호출은 두 번째 인수를 반환하고. . . 그렇게 진행이 된다.

당신이 만일 남겨진 가변인수를 무시하기를 원한다면 언제든지 멈출 수 있다. 호출로 공급된 인수들보다는 소수의 인수들을 억세스 하는 함수를 위해서 아주 좋지만, 만일 당신이 너무 많은 인수들을 억세스 하려 시도한다면 당신은 쓰레기 값을 얻게 될 것이다.

3. va_end를 호출해서 포인터 변수인 인수를 끝냈음을 알려라.

(실제로, 대부분 C 컴파일러에서, va_end의 호출은 아무 일도 하지 않고 당신은 그것을 실제로 호출할 필요가 없다. 이것은 GNU C 컴파일러에서는 항상 참이다. 그러나 누군가 당신의 프로그램을 독특한 컴파일러에서 컴파일하는 경우라면 va_end를 호출해야만 할 것이다.

va_start, va_arg 그리고 va_end에 대한 완전한 정의는 A. 2. 2. 5절 [Argument Macros] 를 참조하라. 단계 1과 3은 가변 인수를 받아들이는 함수에서 반드시 수행되어야만 한다. 그렇지만, 당신은 다른 함수에 인수로써 va_list 변수를 줄 수 있고 전부 또는 단계 2를 수행할 수 있다.

당신은 단일한 함수 호출에서 여러 번 세 단계의 전부를 반복해서 수행 할 수 있다. 만일 당신이 가변 인수를 무시하기를 원한다면, 세단계를 하지 않을 수 있다. 만일 당신이 원한다면 포인터 변수인 한 개의 인수보다 더 많은 것을 가질 수 있다. 당신은 당신이 원할 때 va_start로 각 변수를 초기화 할 수 있고, 그러고 나면 당신이 원하는 각각의 포인터 인수를 추출할 수 있다. 각 포인터 변수인 인수는 인수 값들의 같은 집합을 통해서 진행되지만, 그것은 자신만의 페이스(pace)를 갖는다.

 
이식성 노트: 어떤 컴파일러로, 당신이 서브루틴(subroutine)에 인수로써 포인터 변수를 사용한다면, 당신은 서브루틴이 반환한 후에 같은 포인터 변수인 인수를 사용해서 기록하지 않아야만 한다. 완벽한 이식성을 위해서, 당신은 va_end에 그것을 주어야한다. 이것은 실제로 ANSI C의 권장사항이지만, 대부분 ANSI C 컴파일러는 다행이 상관없이 작업한다.

A. 2. 2. 3 어떻게 많은 인수들이 공급되는가?

가변 인수들의 타입과 개수를 알 수 있는 일반적인 방법은 없다. 그래서 누구든 그 가변인수가 얼마나 많은 인수들을 가졌고, 그것이 무슨 종류인지를 알아낼 수 있는 특별한 방법의 함수를 고안해야한다. 그것은 가변인수 함수의 호출 관습에 적당하게 정의되어야 하고, 그것에 근거하여 프로그램에서 가변 인수 함수를 호출해야 한다.

호출관습의 한가지는 한 개의 고정된 인수를 사용해서 가변인수의 개수를 공급하는 것이다. 이 방법은 공급된 가변 인수들이 모두 같은 형일 경우에 가능한 방법이다. 그와 유사한 방법으로는 가변인수가 공급될 가능성에 대한 정보를 한 비트에 담은, 비트 마스크가될 고정인수를 인수중에 하나로 갖는 것이다. 당신은 미리 선언된 시퀀스 안에 있는 비트들을 테스트할 수 있다; 만일 그 비트가 설정되면, 다음 인수의 값을 추출하는 것이고, 그렇지 않다면, 디폴트값을 사용하는 것이다. 고정된 인수는 가변 인수들의 개수와 타입, 이 둘을 지정하는 패턴으로써 사용될 수 있다. printf에서 형식화된 문자열 인수는 이것의 한 예가 된다. (7. 9. 7절 [Formatted Output Functions] 참조.)

다른 가능성은 마지막 가변 인수로써 "끝 표시"값을 사용하는 것이다. 예를 들어, 예측할 수 없는 포인터 인수들의 개수를 처리하는 함수가 있다면, 널 포인터는 인수 리스트의 끝을 지적할 것이다. (이것은 널 포인터가 함수에게 의미 있는 값이 아니라고 가정한다. ) execl 함수는 이 방법으로 작업한다; 23. 5절 [Executing a File] 참조.


A. 2. 2. 4 가변인수 함수들을 호출하기

당신이 가변인수 함수를 호출할 때 특정한 어떤 것을 써서는 안된다. 단지 괄호안에 보통, 콤마에 의해 분리된 인수들(가변으로써, 요청된 인수)만 사용하라. 그러나 당신은 프로토타입으로 그 함수를 선언함으로써 준비하고, 그 인수의 값들이 어떻게 변환되는지를 알아야만 한다.

원칙적으로, 가변으로써 정의된 함수들은 당신이 그들을 호출할 때마다 함수 프로토타입을 사용해서 가변이 되도록 선언되어야한다. (A. 2. 2. 1 [Variadic Prototypes] 참조. ) 이것은 함수가 가변 인수 또는 고정된 인수를 취하는지의 여부에 의존하여 함수에 인수 값들을 부여하는 다른 호출 관습을 가진 C 컴파일러 때문이다.

실제로, GNU C 컴파일러는 항상 당신이 가변인수 또는 요청된 인수를 사용하는지에 상관없이 같은 방법으로 인수형의 주어진 집합을 부여한다. 그래서, 인수들의 타입이 자체-진행인 동안, 당신은 그들의 선언을 안전하게 생략할 수 있다. 보통 가변함수를 위해서 인수의 형을 선언하는 것은 좋은 방법이고, 모든 함수들을 위해서는 물론 당연한 것이다. 그런데 몇 개의 함수는 그렇지 않은 경우가 있다_예를 들어, open과 printf

함수의 프로토타입이 가변인수들의 타입을 정하지 않았을 때, 가변인수 함수를 호출하면, 함수의 가변 인수 값들은 디폴트 인수 승급이 수행된다. 디폴트 인수 승급이란 char 또는 short int (부호가 있던지 없던지)의 형을 가진 오브젝트들은 int 나 unisgned int로 승급되고; float의 형을 가진 오브젝트들은 double로 승급되는 것을 말한다. 그래서, 가변인수에 char형의 값을 넣으면, 그것은 int로 승급되고, 그 함수는 va_arg(ap, int)과 함께 그것을 얻을 것이다.

고정 인수들은 보통 함수의 원형을 통해서 제어된다: 인수 표현식은 마치 그형의 변수로 할당되었던 것 선언된 인수의 형으로 변환된다.


A. 2. 2. 5 인수 억세스 매크로들

다음은 가변 인수들을 가져오기 위해서 사용되는 매크로에 대한 기술이다. 그 매크로들은 헤더파일 `stdarg. h'에 정의되어 있다.

데이터 타입 : va__list

va_list는 포인터 변수들인 인수를 위해서 사용된다.

매크로 : void va__start (va_list ap, last_required)

이 매크로는 현재 함수의 가변 인수들의 첫 번째를 가리키는 포인터 변수 ap를 초기화한다; lastrequired는 함수에 있는 마지막 고정인수가 되어야 한다. `varargs. h'에 있는 va_start의 정의를 변경하려면 A. 2. 3. 1 [Old Varargs] 를 참조하라.

매크로 : type va__arg (va_list ap, type)

va_arg 매크로는 다음 가변 인수의 값을 반환하고, 다음 인수를 가리키도록 ap의 값을 갱신한다. 그래서, va_arg의 성공적인 사용은 가변 인수들을 성공적으로 반환한다. va_arg에 의해 반환된 값의 타입은 호출에서 정했던 타입이다. type은 반드시 실제 인수의 타입과 매치되는 자체-승급 타입 (char나 short int 나 float가 아닌)이 되어야 한다.

매크로 : void va__end (va_list ap)

이것은 ap의 사용을 끝낸다. va_end 호출 후에, 다음에 같은 ap를 사용해서 va_arg를 호출하면 작업하지 않을 것이다. 당신은 같은 ap 인수를 사용하는 va_start를 호출했던 함수를 반환하기 전에 va_end를 호출해야만 한다. GNU C 라이브러리에서, va_end는 아무 일도 하지 않기 때문에 이식성의 이유가 아니라면 va_end를 호출할 필요가 없다.

A. 2. 3 가변인수 함수의 예제

다음은 인수들을 가변적인 개수로 받아들이는 함수에 대한 예이다. 함수의 첫 번째 인수는 반환된 결과와 합산된, 남겨진 인수들의 개수이다. 이 함수는 가변 인수 기능을 어떻게 사용하는지 설명하는데 충분하다.

#include <stdarg. h>
#include <stdio. h>
 
int
add_em_up (int count, . . . )
{
va_list ap;
int i, sum;
 
va_start (ap, count);
/* 인수 목록을 초기화하라. */
sum = 0;
for (i = 0; i < count; i++)
sum += va_arg (ap, int);
/* 다음 인수값을 얻어라. */
va_end (ap);
/* 정리하라. */
return sum;
}
 
int
main (void)
{
/* 이 호출은 16을 출력한다. */
printf ("%d\n", add_em_up (3, 5, 5, 6));
 
/* 이 호출은 55를 출력한다. */
printf ("%d\n", add_em_up (10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
return 0;
}

A. 2. 3. 1 오래된-형태의 가변인수 함수들

ANSI C 이전에, 프로그래머들은 가변함수들을 쓰기 위해서 완전히 다른 기능을 사용했었다. GNU C 컴파일러는 여전히 그것을 지원한다; 현재, 그것은 ANSI C가 여전히 일반적이지 않기 때문에, ANSI C 기능보다는 더 이식성이 있다. 오래된-형태의 가변인수 함수를 정의하고 있는 헤더파일은 `varargs. h'라고 불린다.

`varargs. h'를 사용하는 것은 `stdarg. h'를 사용하는것과 거의 같다. 가변인수 함수를 어떻게 호출하는지에 대한 것은 거의 다름이 없다; A. 2. 2. 4절 [Calling Variadics] 참조. 오직 유일한 차이는 그들을 정의하는 방법이다. 무엇보다도, 당신은 오래된 형태의 비- 프로토타입 구문을 사용해야만 한다. 다음처럼:

tree
build (va_alist)
va_dcl
{
두 번째로, va_start에 오직 한 개의 인수만을 넣어야한다. 다음처럼:
va_list p;
va_start (p);

오래된 형태의 가변인수 함수들을 정의하기 위해서는 특정한 매크로가 사용된다:

매크로 : va__alist

이 매크로는 가변인수 함수에 있는 고정 인수이름 목록을 나타낸다.

매크로 : va__decl

이 매크로는 가변인수 함수를 위한 인수들이나 또는 암묵적인 인수를 선언한다.

매크로 : void va__start (va_list ap)

`varargs. h'에 선언된 이 매크로는 현재 함수의 첫 번째 인수를 가리키는 포인터로 포인터 변수인 인수를 초기화한다.

다른 인수 매크로, va_arg 와 va_end는 `varargs. h' 와 `stdarg. h'의 것이 서로 같다; A. 2. 2. 5절 [Argument Macros] 참조. 동일한 컴파일 단위에서 `varargs. h'와 `stdarg. h'가 둘다 인클루드 되어서는 안된다; va_start가 서로 충돌하게 된다.


A. 3 널 포인터 상수

널 포인터 상수는 어느 실제 오브젝트를 가리키고 있는 것이 아니라는 것을 말한다. 당신은 void * 형을 가진 포인터 변수로 그것을 할당할 수 있다. 널 포인터 상수를 사용하기 위한 좋은 방법은 NULL을 사용하는 것이다.

매크로 : void * NULL

이것은 널 포인터 상수이다.
널 포인터 상수로써 0 또는 (void *)0을 사용할 수 있지만, NULL을 사용하는 것이 상수를 사용하는 목적으로 좀더 분명하게 만들어졌기 때문에 더 깨끗하다. 만일 당신이 함수 인수로써 널 포인터 상수를 사용한다면, 그 함수가 가진 프로토타입 선언과 완전히 맞도록 널 포인터 상수를 사용해야한다. 그렇지 않다면, 만일 목표 머쉰(machin)이 두 개의 다른 포인터 표현을 가진다면, 컴파일러는 그 인수를 위해 사용할 표현이 무엇인지 알지 못한다.
당신은 적당한 포인터 타입으로 상수를 캐스트(cast)함으로써 그 문제를 피할 수 있지만, 우리는 그보다는 당신이 호출할 함수를 위해서 프로토타입에 더할 것을 권장한다.

A. 4 중요한 데이터 타입들

C에서 두 개의 포인터를 뺀 결과는 항상 정수이지만, 정밀한 데이터 타입은 C 컴파일러에 따라 다르다. . 그처럼 데이터 타입에 따라서, sizeof의 결과 또한 컴파일러에 따라서 다르다. ANSI 는 그 두 개의 데이터 타입을 위해서 표준 이름을 정의하기 때문에, 당신은 이식성을 위해서 그 데이터 타입을 사용할 수 있다. 그들은 헤더파일 `stddef. h'에 정의되어 있다.

데이터 타입 : ptrdiff__t

이것은 두 개 포인터를 빼서 나온 결과의 부호화된 정수 타입이다. 예를 들어, char *p1, *p2; 이렇게 선언되었다면; 표현식 p2 - p1은 ptrdiff_t 형의 결과값을 갖는다. 이것은 아마도 표준 부호형 정수 타입들(short int, int 또는 long int)중의 하나가 될 것이지만, 표준 부호형 정수 타입들이 이러한 목적으로 존재한다면 비표준 타입이 되어버릴 것이다.

데이터 타입 : size__t

이것은 오브젝트(objects)의 크기를 나타내는데 사용하는 비부호형 정수 타입이다. sizeof 연산의 결과가 이 타입을 갖고, malloc( 3. 3절 [Unconstrained Allocation] 참조. ) 그리고 memcpy(5. 4절 [Copying and Concatenation] 참조. )와 같은 함수들은 오브젝트의 크기를 정하는데 이 타입의 인수를 받아들인다.
사용법 노트: size_t는 오브젝트의 크기를 저장하는 인수나 변수를 선언하는데 좋은 방법을 제공한다.

GNU 시스템에서 size_t는 unsigned int 또는 unsigned long int 와 동일하다. 그 타입들은 GNU 시스템상에서 동일한 특성을 갖고 있고, 그들을 사용할 때 대부분은 그들 사이를 서로 변경시키지 않고도 사용할 수 있다. 그렇지만, 그들은 어떤 구문들에서는 차이를 갖기 때문에 다른 데이터타입으로 구분된 것이다.

예를 들어, 함수의 원형으로 함수 인수의 타입을 정할 때, 당신이 사용하는 것은 차이가 있다. 만일 시스템 헤더파일이 size_t 타입의 인수를 갖는 malloc 함수를 선언하고, 당신이 unisgned int의 타입을 갖는 malloc를 선언했을 때, 만일 size_t가 당신의 시스템에서 unisgned long int형으로 발생한다면, 당신은 컴파일 에러를 얻을 것이다. 이러한 문제의 가능성을 피하기 위해서, 함수의 인수나 값은 다른 방법으로 그 형을 선언하기보다는 size_t의 타입을 갖도록 선언하라.

 
호환성 노트 : ANSI C가 나타나기 전에 C는 포인터 뺄셈의 결과를 나타내기 위해서 int를 사용하고 오브젝트의 크기를 표현하기 위해서 unsigned int를 사용했었다. 그들은 size_t나 ptrdiff_t를 정의할 필요가 없었다. 유닉스 시스템들은 `sys/types. h'에 size_t를 정의해놓았지만, 그 정의는 보통 signed 형을 말한다.

A. 5 데이터 타입 측정

당신이 당신의 프로그램에서 사용되는 오브젝트의 적당한 C 데이터 타입을 선정할 때 대부분 그것이 얼마나 많은 비트들을 사용하고 그 오브젝트가 어떻게 표현되는지에 관심을 가질 필요가 없다. 당신이 그와같은 정보를 필요로 할 때, C 언어 자체는 그것을 얻을 수 있는 방법을 제공하지 않는다.

헤더파일 `limits. h'와 `float. h'에 포함된 매크로들은 당신에게 그것에 관한 세심한 정보를 줄 것이다.


A. 5. 1 정수 데이터 타입의 너비 계산하기

정수 타입이 얼마나 많은 비트로 구성되었는지 알 필요가 있는 프로그램은 비트 벡터(bit vector)로써 lont int의 배열을 사용하는 경우가 일반적이다.

당신은 vector[n / LONGBITS] & (1 << (n % LONGBITS)) 로 인덱스 N을 구성하는 비트를 억세스할 수 있다. LONGBITS는 long int를 구성하는 비트의 개수로 당신이 정의해서 공급하라.

C 언어에서 정수 데이터 타입에 있는 비트의 수에 대한 정보를 당신에게 줄 수 있는 연산자는 없다. 그렇지만 헤더파일 `limits. h'에 정의된 매크로 CHAR_BIT를 사용해서 그것을 계산할 수 있다.

CHAR_BIT

이것은 한 개의 char안에 있는 비트의 개수를 말하는데, 대부분의 시스템에서 그 값은 8이다. 그 값은 int형을 갖는다. 당신은 다음처럼 어느 데이터 타입 type안의 비트의 개수를 계산할 수 있다

A. 5. 2 정수 타입의 범위

당신이 0에서 일 백만 사이에 있는 정수의 값을 저장할 필요가 있다고 가정해보자. 당신이 사용할 수 있는 가장 작은 타입은 무엇인가? 그것을 정하는데 일반적인 규칙은 없다; 그것은 C 컴파일러와 목표 머쉰(machine)에 의존한다. 당신은 타입을 결정하기 위해서 `limits. h'에 있는 매크로 `MIN'과 `MAX'를 사용할 수 있다.

각각의 부호가 있는 정수 타입은 그것이 저장할 수 있는 가장 작은 값과 가장 큰 값을 나타내는 한 쌍의 매크로를 갖는다. 부호가 없는 정수 타입은 최대의 값을 나타내는 한 개의 매크로를 갖는다; 최소값은 물론 0이다. 그 매크로들의 값은 모두 정수 상수 표현이다. 다른 타입들을 위한 `MAX' 와 `MIN' 매크로들은 매크로에 의해 설명된 같은 타입의 값을 갖는다_그래서 ULONG_MAX는 unisgned long int 의 타입을 갖는다.

SCHAR_MIN

signed char에 의해 표현될 수 있는 최소값.

SCHAR_MAX, UCHAR_MAX

signed char 와 unsigned char에 의해 표현될 수 있는 최댓값.

CHAR_MIN

char 에 의해 표현될 수 있는 최소값. 여기서 만일 char에 부호가 있는 것이라면 SCHAR_MIN과 같고, 그렇지 않다면 0이다.

CHAR_MAX

char 에 의해 표현될 수 있는 최댓값. char가 부호가 있는 것이라면 SCHAR_MAX와 같고, 그렇지 않다면 UCHAR_MAX와 같다.

SHRT_MIN

signed short int에 의해 표현될 수 있는 최소값. GNU C 라이브러리가 실행되는 대부분의 컴퓨터에서, short int 는 16-비트로 동일하다.

SHRT_MAX, USHRT_MAX

signed short int 와 unsigned short int에 의해 표현될 수 있는 최댓값.

INT_MIN

signed int 에 의해 표현될 수 있는 최소값. GNU C 시스템이 실행되는 대부분의 컴퓨터에서, int 32-비트로 동일하다.

INT_MAX, UINT_MAX

signed int 와 unsigned int 에 의해 각각 표현될 수 있는 최댓값들.

LONG_MIN

signed long int 에 의해 표현될 수 있는 최댓값. GNU C 시스템이 실행중인 대부분의 컴퓨터에서, long 정수들은 int와 동일한 크기를 갖고 32-비트로 동일하다.

LONG_MAX, ULONG_MAX

signed long int 와 unsigned long int에 의해 표현될 수 있는 최댓값.

LONG_LONG_MIN

signed long int에 의해서 표현될 수 있는 최소값. GNU C 시스템이 실행되는 대부분의 컴퓨터에서, long long 정수들은 64-비트로 동일하다.

LONG_LONG_MAX, ULONG_LONG_MAX

signed long long int 와 unsigned long long int에 의해 표현될 수 있는 최댓값.

WCHAR_MAX

wchar_t에 의해 표현될 수 있는 최댓값. 18. 4절 [Wide Char Intro] 참조.

헤더파일 `limits. h'는 또한 다양한 운영체제와 파일 시스템 제한들을 파라미터 화한 부가적인 상수들을 정의하고 있다. 그 상수들은 27장[System Configuration] 에 설명되어 있다.


A. 5. 3 부동형 매크로들

플로팅 포인트 수의 구체적인 표현 방법은 기계마다 다르다. 플로팅 포인트 수들은 내부적으로 근사치로써 표현되기 때문에, 플로팅 포인트 데이터를 다루기 위한 알고리즘은, 때때로 기계의 플로팅 포인트 표현에 대한 자세한 정밀도를 참작하는데 사용된다.

C 라이브러리에서 어떤 함수들은 이러한 정보를 필요로 한다; 예를 들어, 플로팅 포인트 숫자들을 읽거나 출력하는 알고리즘 (7장 [I/O on Stream] 참조) 과 삼각함수를 계산하기 위한 알고리즘 그리고 무리수 함수들을 위한 알고리즘들은 정확성의 상실이나 반올림-에러를 피하기 위해서 그 정보를 사용한다. 수학적인 분석 기술을 다루는 프로그램들은 에러 경계를 계산하거나 최소화하기 위해서 이 정보를 필요로 한다. 헤더파일 `float. h'는 당신의 기계에 의해서 사용되는 형식을 설명한다.


A. 5. 3. 1 플로팅 포인트 표기 개념

이 절은 플로팅 포인트 표현법을 설명하기 위한 용어들을 설명한다. 당신은 플로팅 포인트 숫자들을 표현하는 과학적인 표기 또는 지수적인 표기의 개념에 대해서 이미 친숙할 것이다. 예를 들어, 숫자 123456. 0은 가수가 1. 23456이고 베이스가 10으로 5승임을 가리키는, 1. 234546e+05의 지수적 표기로써 표현될 수 있다. 더 형식적으로, 플로팅 포인트 수의 내부적 표현은 다음의 파라미터로써 특징을 나타낼 수 있다.

부호는 -1 또는 1 이다.

지수를 위한 베이스(base) 또는 기수(radix)는 1보다 큰 정수이다. 이것은 특정 표기에 따라서 다른 상수이다.

베이스에 몇 승인지를 나타내는 수가 지수이다. 지수의 상한과 하한은 특정한 표기에 따라서 다른 상수이다.

때때로, 플로팅 포인트 수를 표현하고 있는 실제 비트들에서, 지수를 항상 unsigned로 표현되도록 만들기 위해서 그것에 상수를 더하여 부호의 의미를 부여한다. 이것은 만일 당신이 직접 플로팅 포인트 수를 구성하고 있는 비트 영역들의 어떤 부분을 사용할 필요가 있는 경우에만 중요하다. GNU 라이브러리는 이러한 것을 지원하지 않는다. 그러므로 다음부터는 이것에 대한 논의는 무시될 것이다.

가수부(mantissa) 또는 유효수(significand)는 플로팅 포인트 숫자를 이루는 한 부분으로써 부호가 없는 정수이다.

가수부의 정밀도. 만일 어떤 플로팅 표현에서 베이스(base)가 b라고 했을 때, 정밀도는 베이스-b를 기반으로 가수부 안에 들어가 있는 숫자들의 개수이다. (즉. . 가수부가 몇 개의 비트로써 표현되느냐를 정밀도라고 한다. ) 이것은 특정한 표현에 따라 다른 상수이다.

많은 플로팅 포인트 표현들은 가수부 안에 암묵적으로 숨겨진 비트를 가지고 있다. 이것은 가수부 안에서 실질적으로는 표현되지만 그 값이 항상 1로 되어있기 때문에 메모리에는 저장되지 않는다. 정밀도 형태는(위를 보라) 숨겨진 비트들도 표함 한다. 다시, GNU 라이브러리는 플로팅 포인트 표현을 위해서 그와같은 저수준의 관점을 다루는 기능을 제공하지 않는다.

플로팅 포인트 수의 가수부는 지수의 몇 승을 가진 함축적인 소수부로써 표현된다. 그래서 가장 크게 표현할 수 있는 가수부가 이 정밀도보다 적은 것이면, 소수의 값은 항상 1보다 적다. 플로팅 포인트 수의 수학적인 값은 소수, 부호 그리고 베이스의 몇 승임을 나타내는 지수로써 만들어진다.

b가 베이스라고 했을 때, 소수가 적어도 1/b라면, 플로팅 포인트 수가 일반화되었다고 말한다. 바꾸어 말하면, 가수부에 지수승이 곱해지면 맞추기에 너무 크게 될 것이다. 비-일반화된 수들은 디노멀(denomal)이라고 부른다; 그들은 플로팅 포인트 수가 일반적으로 저장될 수 있는 정밀도 보다도 작은 정밀도를 갖고 있다.

만일 그 수가 일반화되지 않았다면, 가수부를 base로 나눈 다음 나온 지수로부터 1을 뺄 수 있고, 그러면 같은값을 가진 다른 표기형식의 플로팅 포인트를 얻게 된다. 그 수가 일반화될 때까지 반복적으로 위와 같은 일을 하면 일반화된 플로팅 포인트 수가 나오게 된다. 두 개의 다른 일반화된 플로팅 포인트 수들은 값이 같을 수 없다.

( 이 규칙에는 예외가 있다: 만일 가수부가 0이라면, 그것은 일반화된 것으로 간주된다. 특정한 기계에서 발생할 수 있는 예외상황이란, 지수부가 그 표기법으로 저장할 수 있기에는 너무 작은 경우이다. 그러면 지수부로부터 1을 빼는 것이 불가능하기 때문에 , 소수가 1/b보다 적은 소수부라면 일반화될 수 있을 것이다. )


A. 5. 3. 2 플로팅 포인트 파라미터들

다음 매크로 정의들은 헤더파일 `float. h'에 있다. `FLT_'로 시작하는 매크로들은 float 타입에 관한 것이고, `DBL_'로 시작되는 매크로들은 double 타입에 와 `LDBL_'로 시작되는 매크로들은 long double 타입에 관한 것이다. (현재 GCC 는 분리된 데이터형으로써 long double를 지원하지 않기 때문에, `LDBL_'상수들을 위한 값들은 double형을 위한 상수에 해당되는 값과 같다. )

그 매크로들 중에서, 오직 FLT_RADIX는 상수 표현식이 되도록 보증된다. 이곳에 설명된 다른 매크로들은 상수 표현식, `#if'와 같은 전처리 지시자 또는 정적 배열 안의 차원을 요구하는 곳에서 사용될 수 없다.

ANSI C 표준이 대부분의 파라미터들을 위한 최소값과 최댓값 정했다고 하더라도, GNU C는 목표 기계의 플로팅 포인트 표현에 따른 값을 사용한다. 그래서 GNU C는 목표 기계가 안정적이라면 ANSI C 요구를 만족시키게 되는 것이다. 실제로, 현재 지원되는 모든 기계들은 안정적이다.

FLT_ROUNDS

이 값은 반올림 형식을 지정하는 값이다. 다음 값은 표준 반올림 모드를 나타낸다.
-1 반올림하지 않는다.
0 소수점 뒤를 0으로 만든다.
1 가장 가까운 수로 반올림한다.
2 무한대로 양의 값을 향한다.
3 무한대로 음의 값을 향한다.
이 이외의 값은 기계_의존적인 비표준 반올림 모드를 나타낸다. 대부분의 기계에서, 그 값은 IEEE 표준에 따라서 1로 되어있다.
다음은 FLT_ROUNDS의 값에 따라서 값들이 어떻게 변하는지를 보여주는 테이블이다,
0 1 2 3
1. 00000003 1. 0 1. 0 1. 00000012 1. 0
1. 00000007 1. 0 1. 00000012 1. 00000012 1. 0
-1. 00000003 -1. 0 -1. 0 -1. 0 -1. 00000012
-1. 00000007 -1. 0 -1. 00000012 -1. 0 -1. 00000012

FLT_RADIX

이것은 지수부의 베이스(base) 또는 기수(radix)의 값이다. 이것은 이 절에 설명된 다른 매크로와는 달리 상수 표현식임이 보장된다. IBM 360과 그곳에서 파생된 제품을 제외하고는 모든 기계에서 2로 되어있다.

FLT_MANT_DIG

float형에서 가수부를 표현하는데 사용되는 비트수. 다음 표현식은 가수부 숫자들의 제한된 수 때문에 1. 0이 나온다(수학적으로는 그것이 될 수 없을 지라도):
float radix = FLT_RADIX;
1. 0f + 1. 0f / radix / radix / . . . / radix
여기서 radix는 FLT_MANT_DIG 번 나타난다.

DBL_MANT_DIG

LDBL_MANT_DIG

이것은 각각 double 과 long double형 각각이 가수부를 표현하는데 사용되는 비트수이다.

FLT_DIG

This is the number of decimal digits of precision for the float data type. Technically, if p and b are the precision and base (respectively) for the representation, then the decimal precision q is the maximum number of decimal digits such that any floating point number with q base 10 digits can be rounded to a floating point number with p base b digits and back again, without change to the q decimal digits.
float형에서 유효숫자의 최소개수.
이 매크로의 값은 ANSI C에서, 적어도 6으로 지원되고 있다.

DBL_DIG, LDBL_DIG

FLT_DIG와 유사하지만, double와 long double형을 위한 것이다. 그 매크로의 값은 적어도 10이 되도록 지원된다.

FLT_MIN_EXP

이것은 float형을 위해서 가능한 지수값으로 가장 작은 값이다. 더 자세하게는, FLT_RADIX에서 1을 뺀 값이 float형으로써 일반화된 플로팅 포인트 수로써 표현될 수 있는 최소 음의 정수이다.

DBL_MIN_EXP, LDBL_MIN_EXP

FLT_MIN_EXP와 유사하지만, double 와 long double를 위한 것이다.

FLT_MIN_10_EXP

This is the minimum negative integer such that 10 raised to this power minus 1 can be representedas a normalized floating point number of type float. This is supposed to be -37 or even less.
지수부의 최소범위. -37이거나 그보다 적다.

DBL_MIN_10_EXP, LDBL_MIN_10_EXP

double와 long double형 각각을 위한 것으로, 지수가 나타낼 수 있는 최소범위.

FLT_MAX_EXP

This is the largest possible exponent value for type float. More precisely, this is the maximum positive integer such that value FLT_RADIX raised to this power minus 1 can be represented as a floating point number of type float.
float이 표현할 수 있는 지수의 최댓값.

DBL_MAX_EXP, LDBL_MAX_EXP

double 와 long double형이 각각 표현할 수 있는 지수의 최댓값.

FLT_MAX_10_EXP

This is the maximum positive integer such that 10 raised to this power minus 1 can be represented as a normalized floating point number of type float. This is supposed to be at least 37.
float형에서 베이스가 10일 때 표현할 수 있는 지수의 최댓값. 이것은 적어도 37이다.

DBL_MAX_10_EXP, LDBL_MAX_10_EXP

double 와 long double형엣 베이스가 10일 때 각각이 표현할 수 있는 지수의 최댓값.

FLT_MAX

이 매크로의 값은 float형이 표현할 수 있는 최대 수를 의미한다. 이것은 적어도 1E+37이 된다. 값 자체도 float형을 갖는다. 표현 가능한 가장 작은 수는 -FLT_MAX가 된다.

DBL_MAX, LDBL_MAX

doble 와 long double 형 각각이 표현할 수 있는 최대 수를 의미한다. 이 매크로 값이 가지는 형은 그것을 설명하는 형과 동일하다.

FLT_MIN

이 매크로의 값은 float형이 표현할 수 있는 표준화된 양의 플로팅 포인트 수의 최소값. 그 값은 1E-37보다 크지 않다.

DBL_MIN, LDBL_MIN

double 과 long double형 각각을 위한 것으로 표준화된 양의 플로팅 포인트수의 최소값. 매크로의 값 자체가 가지는 형은 그것이 설명하는 형과 동일하다.

FLT_EPSILON

1. 0 + FLT_EPSILON != 1. 0이 참인 float형의 플로팅 포인트 수의 최소 양의 수이다. 1E-5보다 크지 않다.

DBL_EPSILON, LDBL_EPSILON

double 와 long double를 위한 것으로 의미는 FLT_EPSILON과 같다. 이 매크로 값 자체가 가지는 형은 그것이 설명하는 형과 동일하다. 그 값은 1E-9보다 크지 않다.

A. 5. 3. 3 IEEE 플로팅 포인트

다음은 이진 플로팅 포인트 연산을 위해서 IEEE 표준에서(ANSI/IEEE Std 754-1985) 정한, 대부분의 일반 플로팅 포인트 표기에서 산출된 float형의 대부분의 매크로 값을 보여주고 있다. 1980년대 이후에 디자인된 대부분의 컴퓨터는 이 형식을 사용한다.

IEEE 단정도(single-precision) float 표기법은 베이스로 2를 사용한다. 그것은 23비트에 한 개의 숨겨진 비트를 더해서(그래서 총 정밀도는 베이스를 2로 했을 때 24가 된다. ) 부호 비트와 가수부를 나타내고, 8-비트 지수부는 -125에서 128까지의 범위에 있는 값을 표현할 수 있다. 다음은, float형 데이터를 이 표기법을 사용할 경우, 그것에 연관된 파라미터의 적당한 값을 나타내고 있다.

FLT_RADIX 2
FLT_MANT_DIG 24
FLT_DIG 6
FLT_MIN_EXP -125
FLT_MIN_10_EXP -37
FLT_MAX_EXP 128
FLT_MAX_10_EXP +38
FLT_MIN 1. 17549435E-38F
FLT_MAX 3. 40282347E+38F
FLT_EPSILON 1. 19209290E-07F

다음은 double 데이터 타입을 위한 값들이다.

DBL_MANT_DIG 53
DBL_DIG 15
DBL_MIN_EXP -1021
DBL_MIN_10_EXP -307
DBL_MAX_EXP 1024
DBL_MAX_10_EXP 308
DBL_MAX 1. 7976931348623157E+308
DBL_MIN 2. 2250738585072014E-308
DBL_EPSILON 2. 2204460492503131E-016

A. 5. 4 구조체 필드 옵셋 ( offset ) 측정

구조체 형안에서 특정한 구조체멤버의 위치를 계산하기 위해서는 offsetof를 사용할 수 있다.

매크로 : size_t offsetof (type, member)

이것은 구조체형을 가진 type안에 있는 member라는 이름을 가진 구조체 멤버의 옵셋(offset)을 구하는 정수 상수 표현식이다. 예를 들어, offsetof(struct s, elem)은 struct s라는 구조체 안에 있는 멤버 elem의 오프셋(offset)이 된다. 이 매크로는 만일 멤버가 비트 필드가 아니면 작업하지 않는다; 당신은 그 경우에 컴파일러로부터 에러를 얻게 될 것이다.

목차 이전 : 27. 시스템 구성 파라미터 다음 : B. 라이브러리 기능들의 요약

'Programming > Unix Programing' 카테고리의 다른 글

부록 C : Maintenance  (0) 2007.12.22
부록 B : Library Summary  (0) 2007.12.22
부록 A : Language Features  (0) 2007.12.22
27. System Configuration  (0) 2007.12.22
26. System Information  (0) 2007.12.22
25. 사용자와 그룹  (0) 2007.12.22
Comment 0 Trackback 0
Top

27. System Configuration

목차 이전 : 26. 시스템 정보 다음 : A 라이브러리에 있는 C 언어 기능들


27 시스템 구성 파라미터

이 장에 설명된 함수들과 매크로들은 운영체제의 구성 파라미터에 대한 정보를 제공한다_시스템 구성 파라미터는, 예를 들면, 용량제한, 선택적인 POSIX 특징의 존재, 실행 파일들을 위한 디폴트 경로(path) ( 27. 12절[String Parameters] 참조. )


27. 1 일반적인 용량 제한

POSIX. 1 과 POSIX. 2 표준은 시스템의 용량 제한들을 나타내는 파라미터의 개수를 정한다. 그 제한들은 주어진 운영체제에서 고정된 상수이거나, 아니면 기계마다 다양할 수 있다. 예를 들면, 어떤 제한된 값은 실행할 때나, 커널을 다시 재건할 때, 시스템 관리자에 의해서 구성될 수 있고, 이것은 응용 프로그램들의 재 컴파일이 요구되지 않는다.

다음의 제한 파라미터들은 파라미터에 단일한 제한값을 갖는, 고정된 시스템일 경우에만 'limits. h'에 정의되어 있다. 만일 시스템이 다른 파일시스템이나 혹은 다른 제한을 갖는 파일들을 허용한다면, 그 매크로는 정의되지 않는다; 특정한 시스템에서 특정한 시간에 적용되는 제한을 찾아내려면 sysconf를 사용하라. 27. 4절 [Sysconf] 참조.

그 파라미터 각각은 다른 이름의 매크로를 갖는데, 그것은 어떤 POSIX시스템에서 갖도록 허용되는, 최소한의 제한 값이 주어지는, `_POSIX'로 시작하는 문자열이다. 27. 5절 [Minimums] 참조.

매크로 : int ARG__MAX

만일 정의되면, exec부류 함수들에게 인수로 줄 수 있는 argv와 환경변수들의 결합된 최대길이를 나타냄. 그 최대길이는 불변이다.

매크로 : int CHILD__MAX

만일 정의되면, 어떤 시간에 같은 실제 사용자 ID로 존재할 수 있는 프로세스의 최대개수를 말함. 그 최대 개수는 불변이다.

매크로 : int OPEN__MAX

만일 정의되면, 단일한 프로세스가 동시에 개방할 수 있는 파일들의 최대개수를 말한다. 그 최대개수는 불변이다.

매크로 : int STREAM__MAX

만일 정의되면, 단일한 프로세스가 동시에 개방할 수 있는 스트림의 최대개수를 말한다. 그 최대개수는 불변이다. 7. 3절 [Opening Streams] 참조.

매크로 : int TZNAME__MAX

만일 정의되면, 시간대(time zone)의 불변하는 최대길이를 말한다. 17. 2. 6 [Time Zone Functions] 참조.

 

다음 제한 매크로들은 `limits. h'에 항상 정의되어 있다.

매크로 : int NGROUPS__MAX

한 프로세스가 가질 수 있는 부가적인 그룹 ID들의 최대개수. 이 매크로의 값은 실제로는 최댓값의 하한 경계이다. 그것은 당신이 얼마나 많은 부가적인 그룹 ID들을 가질 수 있는지를 알 수 있게 하지만, 어떤 특정한 기계는 그것보다 더 많은 것을 허용하기도 한다. 당신은 어떤 특정한 기계가 당신에게 더 많은 ID들을 허용할 수 있는지에 대해서 알아보기 위해서 sysconf 를 사용할 수 있다. ( 27. 4절 [Sysconf] 참조. )

매크로 : int SSIZE__MAX

타입 ssize_t 의 오브젝트 안에 넣을 수 있는 가장 큰 값. 효율적이게, 이것은 단일한 명령에서 읽거나 쓸 수 있는 바이트의 개수에 대한 제한이 된다. 이 매크로는 제한이 불변이기 때문에 모든 POSIX 시스템들에서 정의되어 있다.

매크로 : int RE__DUP__MAX

정규표현식의 `\{min, max\}'의 구성 안에 허용되는 반복의 최대개수. 이 매크로의 값은 실제로는 최댓값 하향 경계선이다. 그것은 당신이 얼마나 많은 반복을 할 수 있는지를 알리지만, 어떤 특정한 기계는 그것보다 더 많은 반복을 허용한다. 어떤 특정한 기계가 더 많은 반복을 허용하는지를 알기 위해서는 sysconf를 사용할 수 있다. (27. 4절 [Sysconf] 참조. ) sysconf의 값은 단지 하향 경계의 값을 말해주므로_더 큰 값으로 작업할 수 있다. POSIX. 2 시스템들에서는 그들에게 어떤 정해진 제한 값이 없다고 하더라도 항상 정의되어야 하기 때문에 모든 POSIX. 2에서 이 매크로는 항상 정의되어 있다.


27. 2 포괄적인 시스템 옵션들

POSIX 는 모든 POSIX 시스템들이 지원하지 않는 시스템-명세 옵션을 정의한다. 그 옵션들은 라이브러리가 아니라 커널에 제공되고 있기 때문에, 간단히 GNU C 라이브러리를 사용해서는 그 기능들이 지원됨을 보증할 수 없다; 당신이 사용중인 시스템에 달려있다.

이 절에 있는 매크로들과 sysconf 함수를 함께 사용해서 주어진 옵션의 유용성을 테스트할 수 있다. 매크로들은 `unistd. h'에 정의되어 있다. 다음 매크로들은, 만일 그 매크로들이 `unistd. h'에 정의되어있다면, 옵션들이 지원되는 것이고, 그렇지 않다면 옵션이 될 수도 있고 되지 않을 수도 있다. 알아내려면 sysconf를 사용하라. 27. 4절 [Sysconf]참조.

매크로 : int __POSIX__JOB__CONTROL

만일 이 심볼이 정의되면, 시스템이 작업제어를 지원함을 알린다. 그렇지 않다면, 한 세션에 있는 모든 프로세스들은 단일한 프로세스 그룹에 소속되어있다. 24장 [Job Control] 참조.

매크로 : int __POSIX__SAVED__IDS

만일 이 심볼이 정의되면, 실행파일이 set-user-ID 또는 set-group-ID 비트들을 설정하여 실행되기 전에 프로세스의 유효 사용자와 그룹 ID들을 시스템이 기억했다가 나중에 그 값들을 다시 반환함을 의미한다. 이 옵션이 정의되지 않으면, 만일 특권을 가지지 않은 프로세스가 그 유효 사용자와 그룹 ID 를 프로세스의 실제 사용자 또는 그룹 ID로 변경했을 때 그것을 다시 되돌릴 수 없다. 25. 8절 [Enable/Disable Setuid] 참조.

다음 매크로에서, 만일 그 매크로들이 `unistd. h'에 정의되면, 그 값은 그 옵션이 지원되는지의 여부를 알린다. -1의 값의 의미는 없음을, 제외한 다른 값이 있음을 의미한다. 만일 그 매크로가 정의되지 않았다면, 그 옵션은 지원될 수도 있고 안될 수도 있다; 알아내려면 sysconf를 사용하라. 27. 4절 [Sysconf] 참조.

매크로 : int __POSIX2__C__DEV

만일 이 심볼이 정의되면, 그것은 그 시스템이POSIX. 2 C 컴파일러 명령, c89를 가지고 있음을 알린다. GNU C 라이브러리는 만일 당신이 C 컴파일러를 가지지 않았다면 당신이 그것을 인스톨하지 않았다는 가정으로, 항상 그 값을 1로써 정의한다.

매크로 : int __POSIX2__FORT__DEV

만일 이 심볼이 정의되면, 시스템이 POSIX. 2 포트란 컴파일러 명령 fort77을 가지고 있음을 알린다. GNU C 라이브러리에서 이 값은 결코 정의되지 않는다.

매크로 : int __POSIX2__FORT__RUN

만일 이 심볼이 정의되면, 시스템이 포트란 캐리지 제어를 해석하기 위한 명령으로 POSIX. 2 asa 명령을 갖고 있음을 지적한다. GNU C 라이브러리는 그 시스템이 무엇을 가졌는지 알지 못하기 때문에 이 매크로는 결코 정의하지 않는다.

매크로 : int __POSIX2__LOCALEDEF

이 심볼이 정의되면, 시스템이 POSIX. 2 localedef 명령을 갖고 있음을 알린다. GNU C라이브러리는 그 시스템이 무엇을 가졌는지 알지 못하기 때문에 이 매크로는 결코 정의하지 않는다.

매크로 : int __POSIX2__SW__DEV

이 심볼이 정의되면, 시스템이 POSIX. 2 명령들 ar, make 그리고 strip을 가지고 있음을 알린다. GNU C라이브러리에서, ar과 make는 라이브러리를 인스톨시키기 위해서는 당신이 꼭 갖고 있어야만하고 strip이 없음은 가망 없는 일이므로 이 매크로 값을 항상 1로써 정의한다.


27. 3 지원되는 POSIX의 버전은?

매크로 : long int __POSIX__VERSION

이 상수는 POSIX. 1 표준의 버전을 나타낸다. 1990 POSIX. 1은 199009L의 값이다. _POSIX_VERSION은 항상 어느 POSIX 시스템에서든지 `unistd. h'에 정의되어 있다.
사용법 노트: `unistd. h' 를 인클루드 함으로써 시스템이 POSIX를 지원하는지의 여부를 테스트하지 말고 _POSIX_VERSION이 정의되었는지를 통해서 그 여부를 테스트하라. 비-POSIX 시스템은, `unistd. h'가 없기 때문에 아마도 실패할 것이다. 컴파일 시간에 POSIX가 지원되는지, 또는 `unistd. h'가 존재하는지의 여부를 당신이 알 수 있는 방법은 없다.

GNU C 컴파일러는 만일 목표 시스템이 POSIX 시스템이라면 심볼 __POSIX__를 미리 정의한다. 정의된 __POSIX__를 테스트해서, 당신이 POSIX 시스템상에서 사용하지 않는 것을 제공했는지, 그러한 것을 신뢰성 있게 검출할 것이다.

매크로 : long int __POSIX2__C__VERSION

이 상수는 라이브러리와 시스템 커널이 제공하는 POSIX. 2 표준의 버전을 나타낸다. 우리는 POSIX. 2 표준의 첫 번째 버전이 어떤 값인지 알지 못하기 때문에 그 값은 그 표준이 공식적으로 채택된 연도와 달을 기준으로 한다.
이 심볼의 값은 시스템에 인스톨된 유틸리티에 대한 것은 아무 것도 말하지 않는다.
 
사용법 노트: 당신은 POSIX. 1 시스템 라이브러리가 POSIX. 2를 지원하는지의 여부를 알기 위해서 이 매크로를 사용할 수 있다. 어떤 POSIX. 1 시스템이 `unistd. h'를 갖고있다면, 그 파일을 인클루드하고 정의된 (_POSIX2_C_VERSION) 을 테스트하라.


27. 4 sysconf 사용하기

당신의 시스템이 시스템 제한을 구성할 수 있을 때, 당신은 어느 특정한 기계에 적용되는 그 값을 알아내기 위해서 sysconf 함수를 사용할 수 있다. 그 함수와 연관된 파라미터 상수들은 헤더파일 `unistd. h' 에 선언되어 있다.

27. 4. 1 sysconf의 정의

함수 : long int sysconf (int parameter)

이 함수는 실행시간 시스템 파라미터에 대해서 질의하기 위해서 사용된다. 그 parameter 인수는 밑에 설명된 심볼들 `_SC_'중 하나가 되어야 한다. sysconf의 보통의 반환값은 당신이 요청한 값이고 만일 그 제한이 허용되지 않거나 에러가 발생한 경우에는 -1의 값을 반환한다. 다음의 errno는 이 함수를 위해 정의된 에러상황이다.
EINVAL parameter의 값이 유용하지 않다.

27. 4. 2 sysconf의 parameter를 위한 상수들

다음은 sysconf에 parameter 인수로써 사용하기 위한 기호상수들이다. 그 값들은 모두 정수 상수들이다 (좀더 특별하게 말하면, 열거형 값이다. )

    _SC_ARG_MAX : ARG_MAX 에 해당하는 파라미터에 대해 질의한다.

    _SC_CHILD_MAX : CHILD_MAX 에 해당하는 파라미터에 대해 질의한다.

    _SC_OPEN_MAX : OPEN_MAX에 해당하는 파라미터에 대해 질의한다.

    _SC_STREAM_MAX : STREAM_MAX에 해당하는 파라미터에 대해 질의한다.

    _SC_TZNAME_MAX : TZNAME_MAX에 해당하는 파라미터에 대해 질의한다.

    _SC_NGROUPS_MAX : NGROUPS_MAX 에 해당하는 파라미터에 대해 질의한다.

    _SC_JOB_CONTROL : _POSIX_JOB_CONTROL에 해당하는 파라미터에 대해 질의한다.

    _SC_SAVED_IDS : _POSIX_SAVED_IDS 에 해당하는 파라미터에 대해 질의한다.

    _SC_VERSION : _POSIX_VERSION에 해당하는 파라미터에 대해 질의한다.

    _SC_CLK_TCK

    CLOCKS_PER_SEC에 해당하는 파라미터에 대해 질의한다; 17. 1. 1 [Basic CPU Time] 참조.

    _SC_2_C_DEV : POSIX. 2 C 컴파일러 명령, c89를 시스템이 가졌는지에 대해서 질의한다.

    _SC_2_FORT_DEV : 시스템이 POSIX. 2 포트란 컴파일러 명령, fort77을 가졌는지 여부를 질의한다.

    _SC_2_FORT_RUN

    시스템이 포트란 캐리지 제어를 해석하기 위한 POSIX. 2 asa 명령을 가졌는지 여부를 질의한다.

    _SC_2_LOCALEDEF : 시스템이 POSIX. 2 localedef 명령을 가졌는지 질의한다.

    _SC_2_SW_DEV : 시스템이 POSIX. 2 명령들 ar, make 그리고 strip를 가졌는지 질의한다.

    _SC_BC_BASE_MAX : bc 유틸리티에 있는 obase의 최댓값에 대해서 질의한다.

    _SC_BC_DIM_MAX : bc 유틸리티에 있는 배열의 최대 크기에 대해서 질의한다.

    _SC_BC_SCALE_MAX : bc 유틸리티에 있는 스케일의 최대 값에 대해서 질의한다.

    _SC_BC_STRING_MAX : bc 유틸리티에 있는 문자열 상수의 최대크기에 대해서 질의한다.

    _SC_COLL_WEIGHTS_MAX

    지역(locale)에서 대조문자열을 정의하는데 사용될 수 있는 가중치의 최대수에 대해서 질의한다.

    _SC_EXPR_NEST_MAX

    expr 유틸리티를 사용할 때 괄호안에 중첩된 표현식의 최대개수에 대해서 질의한다.

    _SC_LINE_MAX

    POSIX. 2 텍스트 유틸리티들이 처리할 수 있는 텍스트 라인의 최대크기에 대해서 질의한다.

    _SC_EQUIV_CLASS_MAX

    지역(locale) 정의에 있는 LC_COLLATE 범주 `order'에 배정될 수 있는 가중치의 최대수에 대해서 질의한다. GNU C 라이브러리는 곧 지역(locale) 정의를 지원하지 않는다.

    _SC_VERSION : 라이브러리와 커널이 지원하는 POSIX. 1의 버전 번호에 대해서 질의한다.

    _SC_2_VERSION : 시스템 유틸리티들이 지원하는 POSIX. 2 의 버전번호에 대해서 질의한다.

    _SC_PAGESIZE

    기계의 가상적인 메모리 크기에 대해서 질의한다. getpagesize도 같은값을 반환한다.

 

27. 4. 3 sysconf의 예제

우리는 일단 당신이 관심을 갖고있는 파라미터에 대한 매크로 정의가 있는지의 여부를 테스트 해보고 없다면 그 다음 sysconf를 사용하기를 권장한다. 예를 들어, 작업제어가 지원되는지의 여부를 어떻게 테스트하는지에 대한 예제가 다음에 있다:

int
have_job_control (void)
{
#ifdef _POSIX_JOB_CONTROL
return 1;
#else
int value = sysconf (_SC_JOB_CONTROL);
if (value < 0)
/* 만일 시스템이 고정되어 있다면, 시도를 계속할 수 없다. */
fatal (strerror (errno));
return value;
#endif
}
 
다음은 수치적 제한의 값을 얻기 위한 방법이다.
int
get_child_max ()
{
#ifdef CHILD_MAX
return CHILD_MAX;
#else
int value = sysconf (_SC_CHILD_MAX);
if (value < 0)
fatal (strerror (errno));
return value;
#endif
}


27. 5 총괄적인 용량 제한들을 위한 최소값

다음은 시스템 제한 파라미터들을 위한 POSIX 최소 하향 한계들을 위한 이름들이다. 그 값들의 유의값(significance)은 당신이 사용하고 있는 시스템이 무엇이든지 상관없이 그 제한을 안전하게 사용할 수 있는 값들이다.

_POSIX_ARG_MAX

이 매크로의 값은 exec 함수에 사용할 수 있는 argv와 환경변수들의 결합된 최대길이를 위한 POSIX에 의해 허용되고 있는 대부분의 제한 값이다. 그 값은 4096이다.

_POSIX_CHILD_MAX

이 매크로의 값은 실제 사용자 ID마다 동시에 가질 수 있는 프로세스들의 최대 개수로써 POSIX에 의해 허용되고 있는 대부분의 제한 값이. 그 값은 6이다.

_POSIX_NGROUPS_MAX

이 매크로의 값은 프로세스당 가질 수 있는 부가적인 그룹 ID들의 최대 개수로써 POSIX에 의해 대부분 허용되고 있는 제한 값. 그 값은 0이다.

_POSIX_OPEN_MAX

이 매크로의 값은 단일한 프로세스에서 동시에 개방할 수 있는 파일들의 최대개수로써 POSIX에 의해 대부분 허용되고 있는 제한 값. 그 값은 16이다.

_POSIX_SSIZE_MAX

ssize_t 의 오브젝트에 저장될 수 있는 최댓값으로써 POSIX에 의해 대부분 허용되고 있는 제한 값. 그 값은 32767이다.

_POSIX_STREAM_MAX

이 매크로의 값은 단일한 프로세스가 동시에 개방할 수 있는 스트림의 최대개수로써 POSIX에 의해 대부분 허용되고 있는 제한 값. 그 값은 8이다.

_POSIX_TZNAME_MAX

이 매크로의 값은 시간대의 최대 길이로써 POSIX에 의해 허용되고 있는 대부분의 제한 값. 그 값은 3이다.

_POSIX2_RE_DUP_MAX

이 매크로의 값은 정규표현식의 `\{min, max\}'에서 사용되고 있는 숫자로써 POSIX에 의해 허용되고 있는 대부분의 제한 값. 그 값은 255이다.


27. 6 파일 시스템 용량의 제한

POSIX. 1 표준은 파일 시스템의 제한들을 나타내는 파라미터들의 개수를 한정하고 있다. 이것은 보통의 경우는 아니지만, 한 파라미터에 단일한 값을 갖는 고정된 시스템에서 가능한 것이다. 대부분의 시스템에서, 다른 최대 제한을 갖는 다른 파일 시스템 ( 그리고, 다른 파라미터들이나, 심지어 다른 파일들 )이 가능하다. 예를 들어, 다른 기계들로부터 파일 시스템들의 어떤 것을 마운트(mount) 하기 위해서 NFS를 사용한다면 매우 가능성 있는 일이다.

다음 매크로들 각각은 만일 그 파라미터들이 고정된 값을 갖는 시스템이라면 `limits. h'에 정의되어 있다. 만일 그 시스템이 다른 제한들을 갖는 파일시스템이나 파일들을 허용한다면, 그 매크로는 정의되지 않는다; 특정한 파일에 적용되는 제한을 알아내려면 pathconf 나 fpathconf를 사용하라. 27. 9절 [Patcconf] 참조. 각 파라미터들은 다른 매크로들을 갖는데, 그 매크로는 POSIX 시스템에서 갖도록 허용되는 제한의 최소값을 갖는 `_POSIX'로 시작되는 이름이다. 27. 8절 [File Minimums] 참조.

매크로 : int LINK__MAX

불변의 시스템은 주어진 파일을 위한 이름들의 개수를 제한한다. 9. 3절 [Hard Links] 참조.

매크로 : int MAX__CANON

불변의 시스템은 입력 편집이 가능할 때 한 라인의 텍스트의 양을 제한한다. 12. 3절 [Canonical or Not] 참조.

매크로 : int MAX__INPUT

불변의 시스템은 입력(input)으로써 미리 입력(typed)된 문자들의 총 개수를 제한한다. 12. 2 [I/O Queses] 참조.

매크로 : int NAME__MAX

불변의 시스템은 파일 이름 구성요소의 길이에 대해서 제한한다.

매크로 : int PATH__MAX

불변의 시스템은 전체 파일이름의 길이에 대해서 제한한다 (그것은 open과 같은 시스템 호출에 주어진 인수이다).

매크로 : int PIPE__BUF

불변의 시스템은 파이프에 자동적으로 쓰여질 수 있는 바이트의 개수에 대해서 제한한다. 만일 다중 프로세스가 동시에 같은 파이프에 기록하고 있다면, 다른 프로세스들로부터의 출력은 이 크기의 청크(chunks) 에 삽입될 것이다. 10장 [Pipes and FIFOs] 참조.

 

다음은 같은 역할을 하는 매크로의 다른 이름이다.

매크로 : int MAXNAMLEN

이것은 NAME_MAX 를 위한 BSD 이름이다. `dirent. h'에 정의되어 있다.

매크로 : int FILENAME__MAX

이 매크로의 값은 파일 이름 문자열의 최대 길이를 나타내는 정수 상수 표현이다. 이것은 `stdio. h'에 정의되어 있다. PATH_MAX와 달리, 이 매크로는 실제로 제한된 값이 없더라도 정의되어 있다. 그 경우, 그 값은 전형적으로 매우 큰 값으로 존재한다. GNU 시스템에서는 항상 그 경우이다.
사용법 노트: 파일 이름을 저장하기 위해서 배열의 크기를 FILENAME_MAX를 사용하지 말아라! 당신이 그렇게 배열을 크게 만드는 것은 가능하지 않다! 대신에 동적 할당을 사용하라( 3장 [Memory Allocation] 참조. )


27. 7 파일에 지원되는 선택적 기능들

POSIX는 시스템에서 파일들을 운영하기 위해서 호출하는 시스템-설정 옵션들을 정의한다. 어떤 시스템들은 그 옵션들을 지원하고 다른 것은 지원하지 않는다. 그 옵션들이 라이브러리가 아니라 커널에서 제공되기 때문에, GNU C 라이브러리를 사용해서 그 기능들 중 어느 것이 지원되는지 간단히 확인할 수 없다. 그들은 파일 시스템과 기계 사이에서 굉장히 다양할 수 있다.

이 절은 당신의 기계가 특별한 옵션을 지원하는지에 대한 여부를 테스트 할 수 있는 매크로를 설명한다. 만일 주어진 매크로가 `unistd. h'에 정의되어 있다면, 그 값을 통해서 해당하는 기능들을 제공하는지에 대해서 알 수 있다. (-1의 값은 없음을 지적하고; 그 외 다른 값은 있음을 지적한다. ) 만일 그 매크로가 정의되어 있지 않으면, 그것은 특별한 파일들이 그 기능을 지원할 수도 있고 지원하지 않을 수도 있음을 의미한다.

GNU C 라이브러리를 지원하는 모든 기계에서는 또한 NFS를 지원하기 때문에, 모든 파일 시스템들이 _POSIX_CHOWN_RESTRICTED 와 _POSIX_NO_TRUNC 기능들을 지원하는지에 대해서 일반적인 구문을 전혀 만들 수 없다. 그래서 GNU C 라이브러리에서 그 매크로들은 결코 정의되지 않는다.

매크로 : int __POSIX__CHOWN__RESTRICTED

이 옵션이 가능하다면, chown 함수는 특권이 없는 프로세스들이 그 프로세스의 유효 사용자 ID나 부가적인 그룹 ID중의 하나로 파일의 그룹 소유자를 변경하는 것만을 허용하도록 제한한다. 9. 8. 4절 [File Owner] 참조.

매크로 : int __POSIX__NO__TRUNC

만일 이 옵션이 효과가 있다면, NAME_MAX 보다 긴 파일 이름 구성요소들은 ENAMETOOLONG 에러를 발생한다. 그렇지 않다면, 너무 긴 파일이름 구성요소는 잘리고 만다.

매크로 : unsigned char __POSIX__VDISABLE

이 옵션은 터미널 디바이스인 파일에만 오직 유효하다. 만일 이 옵션이 효력을 갖는다면, 특정한 제어문자를 처리할 때 개별적으로 효력이 없게 할 수 있다. 12. 4. 9절 [Special Characters] 참조.

만일 그 매크로들 중 하나가 정의되어 있지 않다면, 그것은 그 옵션이 어떤 파일들에는 효과가 있고 어떤 파일들에는 효과가 없음을 의미한다. 특정한 파일에 대해서 문의하려면, pathconf 또는 fpathconf를 호출하라. 27. 9절 [Pathconf] 참조.


27. 8 파일 시스템 제한을 위한 최소값들

다음은 위에 설명된 파라미터들에 대한 POSIX 최소 하향 경계들을 나타내는 이름이다. 그 값들은 당신이 사용하고 있는 특정한 시스템에 상관없이 제한들을 안전하게 사용할 수 있다.

_POSIX_LINK_MAX

POSIX에 의해 대부분 허가되고 있는 제한 값 파일의 링크 개수의 최댓값이다. 이 상수 값은 8이다. 그래서, 당신은 시스템 제한을 시험하지 않고도 한 개의 파일을 위해서 항상 8개의 이름을 만들 수 있다.

_POSIX_MAX_CANON

POSIX에 의해 대부분 허가되고 있는 제한 값, 터미널 디바이스로부터 한 개의 정규 입력 라인에서 최대한 받아들일 수 있는 바이트의 개수이다. 이 상수의 값은 255.

_POSIX_MAX_INPUT

POSIX에 의해 대부분 허가되고 있는 제한 값, 터미널 디바이스 입력 큐(또는 선행입력 버퍼)에 들어갈 수 있는 최대 바이트의 개수이다 12. 4. 4절 [Input Modes] 참조. 이 상수의 값은 255이다.

_POSIX_NAME_MAX

POSIX에 의해 대부분 허가되고 있는 제한 값, 파일 이름 구성에 있는 바이트의 최대 개수이다. 이 상수의 값은 14이다.

_POSIX_PATH_MAX

POSIX에 의해 대부분 허가되고 있는 제한 값 파일 이름의 최대 바이트 개수이다. 이 상수의 값은 255이다.

_POSIX_PIPE_BUF

POSIX에 의해 대부분 제한되고 있는 제한 값, 한 파이프에 자동적으로 쓰여질 수 있는 바이트의 최대 개수이다. 이 상수의 값은 512이다.


27. 9 pathconf 사용하기

당신의 기계가 한 개의 파일 시스템 파라미터에 대해서 여러 개의 값을 갖는 다른 파일들을 허용할 때, 당신이 어느 특정한 파일에 적용되는 값을 찾아내기 위해서는 이 절에 설명된 파일들을 사용해야만 한다. 그 함수들과 그와 연관된 파라미터 인수를 위한 상수들은 헤더파일 `unistd. h'에 선언되어 있다.

함수 : long int pathconf (const char *filename, int parameter)

이 함수는 filename이라는 이름을 가진 파일에 적용되는 제한에 대해서 질의하는데 사용된다. parameter 인수는 밑에 설명된 `_PC_' 상수중 한 개가 된다.
 
pathconf로부터의 보통 반환되는 값은 당신이 요청한 값이다. 어떤 제한으로 강요할 수 없거나 에러가 발생한 경우에는 -1의 값을 반환한다. 전자의 경우에는 errno가 설정되지 않았고, 후자의 경우에는 문제가 발생된 원인을 지적하기 위해서 errno가 설정되었다. 그래서 이 함수를 안정적으로 사용하기 위한 유일한 방법은 그것을 호출하기 전에 errno를 0으로 저장해놓는 것이다.
 
파일 이름 구문 에러들 외에도( 6. 2. 3절 [File Name Errors] 참조. ), 다음의 errno는 이 함수를 위해서 정의되었다.

EINVAL

parameter값이 유효하지 않거나, 그 실행이 지정된 파일을 위해서 그 parameter를 지원하지 않는다.

함수 : long int fpathconf (int filedes, int parameter)

이것은 정보를 요청하기 위한 파일을 정함에 있어서, 파일이름을 사용하는 것이 아니라 개방된 파일 기술자를 사용한다는 점을 제외하고는 pathconf와 같다. 다음의 errno는 이 함수를 위해 정의된 에러상황이다.

EBADF : filedes 인수가 유효한 파일 기술자가 아니다.

EINVAL

parameter 의 값이 유효하지 않거나, 그 실행은 지정된 파일을 위해서 그 parameter를 지원하지 않는다.

다음은 pathconf 와 fpathconf에 parameter 인수로써 사용할 수 있는 기호 상수들이다. 그 값들은 모두 정수 상수들이다.

    _PC_LINK_MAX

    LINK_MAX 의 값에 대해서 질의한다.

    _PC_MAX_CANON

    MAX_CANON 의 값에 대해서 질의한다.

    _PC_MAX_INPUT

    MAX_INPUT의 값에 대해서 질의한다.

    _PC_NAME_MAX

    NAME_MAX의 값에 대해서 질의한다.

    _PC_PATH_MAX

    PATH_MAX의 값에 대해서 질의한다.

    _PC_PIPE_BUF

    PIPE_BUF의 값에 대해서 질의한다.

    _PC_CHOWN_RESTRICTED

    _POSIX_CHOWN_RESTRICTED의 값에 대해서 질의한다.

    _PC_NO_TRUNC

    _POSIX_NO_TRUNC의 값에 대해서 질의한다.

    _PC_VDISABLE

    _POSIX_VDISABLE의 값에 대해서 질의한다.


27. 10 유틸리티 프로그램 용량 제한들

POSIX. 2 표준은 유틸리티 동작에 적용되는 시스템 제한을 정해놓았고, 당신은 sysconf를 통해서 그 제한을 억세스할 수 있다. GNU C 라이브러리는 그 제한들을 위해서 매크로를 정의하고, 만일 당신이 그 값을 질의하면 sysconf는 그들의 값을 반환한다; 하지만 그 값들은 아무런 의미 없는 정보이다. 그들은 POSIX. 2가 허가하는 가장 작은 값들이다.

매크로 : int BC__BASE__MAX

bc 유틸리티를 지원하도록 보증되는 obase의 최댓값.

매크로 : int BC__SCALE__MAX

bc 유틸리티를 지원하도록 보증되는 scale의 최댓값

매크로 : int BC__DIM__MAX

bc 유틸리티를 지원하도록 보증되는 한 개의 배열 안에 들어갈 수 있는 요소들의 최대개수.

매크로 : int BC__STRING__MAX

bc 유틸리티를 지원하도록 보증되는 한 개의 문자열 안에 들어갈 수 있는 문자들의 최대개수.

매크로 : int BC__DIM__MAX

bc 유틸리티를 지원하도록 보증되는 한 개의 배열 안에 들어갈 수 있는 요소들의 최대 개수.

매크로 : int COLL__WEIGHTS__MAX

로케일(locale)을 위해서 대조문자열을 정의하는데 사용될 수 있는 가중치의 최댓값.

매크로 : int EXPR__NEST__MAX

expr 유틸리티에 의해서 괄호안에 중첩될 수 있는 표현식의 최대 개수.

매크로 : int LINE__MAX

텍스트-지향 POSIX. 2 유틸리티들이 지원할 수 있는 최대 텍스트 라인. (만일 당신이 그 유틸리티의 GNU 버전을 사용하고 있다면, 가상 메모리를 사용함으로써 강요된 것을 제외하고는 실제로 아무런 제한이 없지만, 그 라이브러리가 당신에게 이것에 대해서 알릴 수 있는 아무런 방법이 없다. )

매크로 : int EQUIV__CLASS__MAX

로케일(locale) 정의에서 LC_COLLATE 범주 `order' 키워드의 엔트리에 배정될 수 있는 가중치의 최댓값. GNU C 라이브러리는 곧 로케일 정의들을 지원하지 않는다.


27. 11 유틸리티 제한들을 위한 최댓값

_POSIX2_BC_BASE_MAX

POSIX. 2에 의해 허용되고 있는 대부분의 제한으로써, bc 유틸리티 안에 있는 obase의 최댓값. 그 값은 99.

_POSIX2_BC_DIM_MAX

bc 유틸리티 안에 있는 배열의 최대크기를 위해서 POSIX. 2에 의해서 대부분 허용되고 있는 제한 값. 그 값은 2048.

_POSIX2_BC_SCALE_MAX

bc 유틸리티 안에 있는 scale의 최대크기를 위해서 POSIX. 2에 의해서 대부분 허용되고 있는 제한 값. 그 값은 99.

_POSIX2_BC_STRING_MAX

bc 유틸리티 안에 있는 문자열 상수의 최대크기를 위해서 POSIX. 2에 의해서 대부분 허용되고 있는 제한 값. 그 값은 1000이다.

_POSIX2_COLL_WEIGHTS_MAX

로케일 을 위해서 대조 문자열을 정의하는데 사용될 수 있는 가중치의 최댓값으로 POSIX. 2에 의해서 대부분 허용되고 있는 제한 값. 그 값은 2이다.

_POSIX2_EXPR_NEST_MAX

expr 유틸리티를 사용할 때 괄호안에 중첩된 표현식의 최대개수로써 POSIX. 2에 의해서 대부분 허용되고 있는 제한 값. 그 값은 32이다.

_POSIX2_LINE_MAX

텍스트 유틸리티들이 처리할 수 있는 텍스트 라인의 최대 크기로써 POSIX. 2에 의해서 대부분 허용되고 있는 제한 값. 그 값은 2048이다.

_POSIX2_EQUIV_CLASS_MAX

로케일 정의에서 LC_COLLATE 범주 `order' 키워드 엔트리에 배정할 수 있는 가중치의 최댓값으로써, POSIX. 2에 의해서 대부분 허용되고 있는 제한 값. 그 값은 2. GNU C 라이브러리는 곧 로케일 정의를 지원하지 않는다.


27. 12 문자열-평가 파라미터들

POSIX. 2는 confstr 함수를 사용해서 운영체제로부터 문자열-평가 파라미터들을 얻는 방법을 정의한다.

함수 : size_t confstr (int parameter, char *buf, size_t len)

이 함수는 buf에서 시작되는 메모리 공간에 len길이의 바이트를 가진 문자열을 저장하여, 문자열-평가 시스템 파라미터를 읽는다. parameter 인수는 밑에 설명된 `_CS_'중의 하나가 될 것이다.
 
confstr로부터 보통 반환되는 값은 당신이 요청했던 문자열 값의 길이다. 만일 당신이 buf를 널 포인터로 하여 그 함수를 호출한다면, confstr은 그 문자열을 저장하려 시도하지 않는다; 그것은 단지 그 길이를 반환한다. 에러를 지적하는 값은 0이다.
 
만일 당신이 요청했던 문자열이 버퍼보다 길다면(그것은, len-1 보다 길다), confstr은 단지 그 만큼( 널 종료 문자를 위한 공간은 남기고. )만 저장한다. 당신은 confsr이 len보다 크거나 같은값을 반환했을 때 그와같은 일이 발생했다는 것을 알 수 있다.
 
다음의 errno는 이 함수를 위해 정의된 에러상황이다.
EINVAL : parameter 값이 유효하지 않다.

다음은 confstr로 읽을 수 있는 단지 한 개의 파라미터이다.

_CS_PATH

이 파라미터의 값은 실행파일을 찾을 때 디폴트 경로에서 찾도록 한다. 이것은 로그인한 후에 사용자가 디폴트로 갖는 경로가 된다.
문자열 크기에 대한 제한 없이 confstr을 사용하는 방법은 그것을 두 번 호출하는 것이다. 첫 번째 호출은 길이를 얻고, 그것에 근거하여 버퍼를 할당하고 다시 confstr을 호출해서 버퍼를 채워라. 다음과 같이.
char *
get_default_path (void)
{
size_t len = confstr (_CS_PATH, NULL, 0);
char *buffer = (char *) xmalloc (len);
 
if (confstr (_CS_PATH, buf, len + 1) == 0)
{
free (buffer);
return NULL;
}
 
return buffer;
}


목차 이전 : 26. 시스템 정보 다음 : A 라이브러리에 있는 C 언어 기능들

'Programming > Unix Programing' 카테고리의 다른 글

부록 B : Library Summary  (0) 2007.12.22
부록 A : Language Features  (0) 2007.12.22
27. System Configuration  (0) 2007.12.22
26. System Information  (0) 2007.12.22
25. 사용자와 그룹  (0) 2007.12.22
24. Job Control(작업 제어)  (0) 2007.12.22
Comment 0 Trackback 0
Top

26. System Information

목차 이전 : 25. 사용자와 그룹 다음 : 27. 시스템 구성 파라미터


26 시스템 정보

이 장은 사용하고 있는 특정한 기계_즉 하드웨어의 타입, 소프트웨어의 타입, 그리고 개별적인 기계들의 이름_에 대한 정보를 반환하는 함수를 설명한다.


26. 1 호스트 확인

이 절은 당신의 프로그램이 실행되고 있는 특정한 기계(machine)를 어떻게 확인할 것인지를 설명하고 있다. 기계의 구분은 인터넷 호스트 이름(Internet host name) 과 인터넷 주소(Internet address)로 한다; 그 함수들에 대한 프로토타입은 `unistd. h'에 나와있다. 쉘 명령어 hostname과 hostid는 그들을 호출하여 작업을 수행한다.

함수 : int gethostname (char *name, size_t size)

이 함수는 배열 name에 호스트 기계의 이름을 반환한다. size인수는 이 배열의 크기를 바이트 단위로 지정한다.
 
이 함수는 성공하면 0을 반환하고, 실패하면 -1을 반환한다. GNU C 라이브러리에서, gethostname은 만일 size 인수가 충분히 크지 않다면 실패하기 때문에 그 경우에는 그 배열을 크게해서 다시 그 함수를 호출하라.
다음의 errno는 이 함수를 위해 정의된 에러상황이다.

ENAMETOOLONG

size 인수가 호스트 이름에다 1을 더한 크기보다 작다. 어떤 시스템에서는, 최대 가능한 호스트 이름의 길이가 심볼 MAXHOSTNAMELEN 어로 정해져있다. 그것은 `sys/param. h'에 정의되어 있다. 그것에 맞지 않는다면 실패로 처리하고 다시 시도하라.

gethostname은 만일 호스트 이름의 길이가 길어서 짤린다고 하더라도 호스트이름을 name에 저장하고 그것을 가리키는 포인터를 반환한다. 어떤 때는 잘려진 호스트이름으로도 충분할 때가 있다. 만일 그렇다면, 그때 발생한 에러코드는 무시할 수 있다.

함수 : int sethostname (const char *name, size_t length)

sethostname 함수는 length의 길이를 가진 문자열 name으로 호스트 기계의 이름을 설정한다. 오직 특권을 가진 프로세스만이 이 일을 하도록 허락된다. 보통 시스템이 부팅할때 딱 한 번 발생한다. 성공하면 반환값은 0이고 실패하면 -1이다.
 
다음의 errno는 이 함수를 위해 정의된 에러상황이다.
EPERM : 이 프로세스는 특권이 없기 때문에 호스트이름을 설정할 수 없다.

함수 : long int gethostid (void)

이 함수는 프로그램이 실행되고 있는 기계의 "호스트 ID"를 반환한다. 관례적으로, 반환되는 호스트 ID는 보통 기계의 주요한(primary) 인터넷 주소로써, long int로 변환된 것이다. 그러나 어떤 기계에서는 각각의 기계에 불변인(hard-coded) 단일한 번호로 의미가 없다.

함수 : int sethostid (long int id)

sethostid 함수는 id로 호스트 기계의 "호스트 ID"를 설정한다. 오직 특권을 가진 프로세스만이 이러한 일을 하도록 허용된다. 보통 그것은 시스템이 부팅될 때, 딱 한 번 발생한다.
 
성공하면 반환값은 0이고 실패하면 -1이다. 다음의 errno는 이 함수를 위해 정의된 에러상황이다.

EPERM

이 프로세스는 특권이 없기 때문에 호스트 이름을 설정할 수 없다.

ENOSYS

운영체제가 호스트 ID를 설정하는 것을 지원하지 않는다. 어떤 시스템에서, 호스트 ID는 불변의 단일한 숫자이기 때문에 의미가 없다.


26. 2 하드웨어/소프트웨어 타입 확인하기

당신은 당신의 프로그램이 실행되고 있는 컴퓨터의 타입에 대한 정보를 얻기 위하여 uname 함수를 사용할 수 있다. 이 함수와 그것과 연관된 데이터타입은 헤더파일 `sys/utsname. h'에 선언되어 있다.

데이터타입 : struct utsname

utsname 구조체는 uname 함수에의해 반환된 정보를 저장하는데 사용된다. 다음과 같은 멤버들을 갖는다.

char sysname[] : 사용중인 운영체제의 이름이다.

char nodename[]

이것은 특정한 컴퓨터의 네트웍 이름이다. GNU라이브러리에서, 이 값은 gethostname에 의해 반환된 것과 같은 값이다; 26. 1절 [Host Identification] 참조.

char release[] : 이것은 운영체제의 현재 개정판 번호이다.

char version[] : 이것은 운영체제의 현재 버전(version) 이다.

char machine[]

이것은 사용중인 하드웨어 타입에 대한 명세이다. 어떤 시스템은 이 정보를 직접적으로 커널로부터 얻을 수 있는 메커니즘을 제공한다. 이러한 메커니즘이 없는 시스템에서, GNU C 라이브러리는 그 라이브러리가 만들어지고 인스톨 될 때 정해진 형태에 기초하여 이 필드를 채운다. GNU 는 시스템 구성을 설명하기 위해서 세-부분을 사용한다; 세부분은 중앙처리장치, 제조회사, 그리고 시스템-타입이고, 그들은 대쉬로 구분된다. 어떤 경우에 그 세부분의 조합된 정보는 유용하지만, 대부분은 실제적으로 유용하지 않고 필요가 없다.
 
기계에서 그 값이 단지 하드웨어에 대한 명세만을 지원한다면, 그것은 두 개의 부분으로 형태의 이름이 구성된다.
예를 들면 다음과 같다:
"sparc-sun", "i386-anything ", "m68k-hp", "m68k-sony", "m68k-sun", "mips-dec"

함수 : int uname (struct utsname *info)

uname 함수는 info가 가리키고 있는 구조체를 운영체제와 호스트 기계에 대한 정보로 채운다. 음이 아닌 값은 성공적으로 정보가 저장되었음을 얘기하고 -1은 실패할 경우 반환된다. 오직 가능한 에러는 EFAULT 밖에 없는데, 그 에러는 항상 발생가능성이 있는 것으로써 간단히 얘기할 수는 없다.


목차 이전 : 25. 사용자와 그룹 다음 : 27. 시스템 구성 파라미터

'Programming > Unix Programing' 카테고리의 다른 글

부록 A : Language Features  (0) 2007.12.22
27. System Configuration  (0) 2007.12.22
26. System Information  (0) 2007.12.22
25. 사용자와 그룹  (0) 2007.12.22
24. Job Control(작업 제어)  (0) 2007.12.22
23. Processes  (0) 2007.12.22
Comment 0 Trackback 0
Top

25. 사용자와 그룹

목차 이전 : 24. 작업 제어 다음 26. 시스템 정보


25 사용자와 그룹

시스템에서 로그인하고 있는 모든 사용자는 사용자 ID(user ID)라고 불리는 한 개의 단일한 숫자에 의해서 구분된다. 각 프로세스는 그 프로세스를 가진 사용자의 억세스 권한을 알리는 유효 사용자 ID를 갖는다. 사용자들은 억세스 제어를 목적으로 하여 그룹으로 분류된다. 각 프로세스는 그 프로세스가 파일들을 억세스하기 위해서 사용할 수 있는 어떤 그룹들이 있는지를 알리는 한 개 이상의 그룹 ID(group ID)가 있다. 프로세스의 유효 사용자 ID 와 유효 그룹 ID들은 persona의 집합적인 형식이다. 이것은 그 프로세스가 어떤 파일을 억세스 할 수 있는지를 결정한다. 일반적으로, 프로세스는 부모 프로세스로 부터 persona를 상속받지만, 특별한 상황하에서는 그 persona를 변경해서 억세스 권한을 변경할 수 있다.

시스템에 있는 각 파일은 또한 사용자 ID 와 그룹 ID를 갖는다. 억세스제어(access control)는, 실행되고 있는 프로세스의 사용자 ID 와 그룹 ID를 파일의 사용자 ID 와 그룹 ID를 비교함으로써 수행된다. 그 시스템은 등록된 사용자들을 모두 데이터베이스에 저장하고, 다른 데이터베이스에 정의된 모든 그룹들을 저장한다. 그 데이터베이스를 시험하기 위해 사용할 수 있는 라이브러리 함수들이 있다.


25. 1 사용자와 그룹 ID들

컴퓨터 시스템에 등록된 각 사용자들은 사용자 이름(user name) 과 사용자ID(user ID) 에 의해 구분된다. 일반적으로, 각 사용자 이름은 한 개의 단일한 사용자 ID를 갖지만, 같은 사용자 ID에 여러개의 로그인 이름들을 가질 수 있다. 사용자 이름과 그에 해당하는 사용자 ID는 25. 12절 [User Database] 에 설명된, 당신이 억세스 할 수 있는 데이터베이스에 저장되어 있다.

사용자는 그룹 안에 분류된다. 각 사용자 이름은 디폴트로는 한 개의 그룹이지만, 한 개 이상의 그룹에 소속된다. 같은 그룹의 멤버인 사용자들은 그 그룹의 멤버가 아닌 사용자가 억세스할 수 없는 자원들(파일과 같은)을 공유한다. 각 그룹은 한 개의 그룹 이름과 그룹 ID를 갖는다. 25. 13절[Group Database] 에, 그룹 ID 또는 그룹 이름에 대한 정보를 어떻게 찾는지 나와있다.


25. 2 프로세스의 persona

어느 시간에, 각 프로세스는 프로세스의 권한을 결정하는 한 개가 단일한 사용자 ID와 한 개의 그룹 ID를 갖는다. 그들은 프로세스의 집합적인 persona라고 불리는데, 왜냐하면 그들은 억세스 제어의 목적을 위하여 "그것은 누구이다"를 결정하기 때문이다. 그 ID들은 프로세스의 유효 사용자 ID(effective user ID) 와 유효 그룹 ID(effective group ID) 라고 불린다. 당신의 로그인 쉘은 당신의 사용자 ID 와 당신의 디폴트 그룹 ID로 구성되는 persona로 시작한다. 일반적인 환경에서, 모든 당신의 다른 프로세스들은 그들의 값들을 상속받는다.

프로세스는 그 프로세스를 만들었던 사용자를 확인할 수 있는 실제사용자 ID(real user ID)와 사용자의 디폴트 그룹을 확인하는 실제 그룹 ID(real group ID)를 갖는다. 그들의 값들은 억세스 제어의 역할을 하지 않기 때문에, 우리는 그들을 persona의 부분으로 간주하지 않는다. 하지만 그들은 중요하다. 실제 와 유효 사용자 ID 둘은 모두 프로세스의 생존기간 동안에 변경될 수 있다. 25. 3절 [Why Change Persona] 참조. 부가적으로, 사용자는 여러 개의 그룹에 포함될 수 있기 때문에, persona에는 억세스 권한에 기여하는 부가적인 그룹 ID들이 포함된다. 어떻게 프로세스의 유효 사용자 ID와 그룹 ID들이 파일을 접근하는 권한에 영향을 미치는지에 대한 상세한 정보들은, 9. 8. 6절 [Access Permission] 참조.

프로세스의 사용자 ID는 kill 함수를 사용해서 시그널들을 보낼 수 있는 권한 또한 제어한다. 21. 6. 2절 [Signaling Another Process] 참조.


25. 3 왜 프로세스의 persona를 변경하는가

프로세스를 위하여 사용자 그리고/또는 그룹 ID들을 변경할 필요가 있는 가장 명백한 상황이 있는 곳은 로그인 프로그램이다. 로그인이 실행을 시작할 때, 그 사용자 ID는 루트이다. 작업은 로그인하고 있는 사용자의 사용자 ID와 그룹 ID들을 가진 쉘을 시작하는 것이다. (이것을 완전히 수행하기 위해서, 로그인은 persona 뿐만 아니라 실제 사용자와 그룹 ID들을 설정해야만 한다. 하지만 이것은 특별한 경우이다. ) persona를 변경하는 더 일반적인 경우는 보통의 사용자 프로그램에서 실제로 그 프로그램을 실행시키고 있는사용자에게는 억세스가 가능하지 않는 자원을 사용자가 억세스할 필요가 있을 때이다.

예를 들어, 당신이 당신의 프로그램에 의해 제어되지만 다른 사용자에 의해서 직접적으로 읽거나 갱신될 수 없는 어떤 파일이 있다고 했을 때, 다른 사용자에게는 개방되지 않은 어떤 종류의 프로토콜의 기능을 원하거나, 또는 그것이 가지고 있는 정보의 비밀이나 완전을 보장하기 원할 때, persona를 변경하는 것이 필요하다. 이 제한된 억세스의 종류는 원하는 자원과 매치되도록 유효 사용자와 그룹 ID를 프로그램이 변경함으로써 가능하다.

그러면, 파일안에 점수를 저장하는 게임 프로그램을 상상해보자. 게임프로그램 자체는 누가 그것을 실행시키는지에 상관없이 이 파일을 갱신할 수 있도록 할 필요가 있지만, 만일 사용자가 그 게임을 실행시키지 않고도 그 점수파일에 기록이 가능하다면, 그들은 그들이 좋아하는 어느 점수로 그 점수파일에 그들 점수를 기록할 수 있다. 어떤 사람은 이것을 상상할 수도 없거나, 아니면 비난할 것이다. 이러한 것은 점수 파일을 소유하는 새로운 사용자 ID와 로그인 이름을 만들고 이 사용자에 의해서만 오직 기록 가능하도록 그 파일을 만들면 방지할 수 있다. 그러면, 게임 프로그램이 이 파일을 갱신하기 원할 때, 그것은 그 게임의 유효 사용자 ID를 변경할 수 있다. 실제로, 프로그램은 점수파일에 기록할 수 있는 게임의 persona를 받아 들여야만 한다.


25. 4 어떻게 응용프로그램이 persona를 변경할 수 있는가

프로세스의 persoan을 변경하기 위한 능력은 무심한 비밀의 침해나, 또는 고의적인 남용의 소스(source)가 될 수 있다. persona를 변경하는, 프로그램에 포함된 문제는 특별한 상황들에 제한이 있기 때문이다.

당신은 제멋대로 당신이 원한다고 당신의 사용자 ID 나 그룹 ID를 변경 할 수는 없다; 오직 특권이 부여된 프로세스들만이 그것을 할 수 있다. 대신에, 프로그램을 위하여 persona를 변경하기 위한 일반적인 방법은 미리 특별한 사용자나 그룹으로 변경하여 프로그램이 시작되는 것이다. 이것은 파일의 억세스 모드의 setuid 와 setgid 비트들의 함수이다. 9. 8. 5절 [Permission Bits] 참조.

억세스 가능한 파일의 setuid 비트가 설정될 때, 그 파일은 자동적으로 그 파일을 소유한 사용자로 유효 사용자 ID를 변경하여 실행된다. 그와 같이, setgid 비트가 설정된 파일은 파일의 그룹으로 유효사용자 ID를 변경하여 실행된다. 23. 5절 [Executing a File] 참조. 특별한 사용자나 그룹 ID로 변경하여 만들어진 파일은 사용자나 그룹 ID에게 완전한 억세스를 보장한다. 파일 모드와 억세스가능성에 대한 일반적인 내용은 9. 8절 [File Attributes] 참조하라.

프로세스는 유효 사용자(또는 그룹) ID는 실제 ID로 언제든지 다시 변경할 수 있다. 프로그램을 좀더 견고한 프로그램으로 만들기 위해서, 그들이 필요치 않을 때는 그들에 대한 특별한 권한을 없애야(turn off) 한다.


25. 5 프로세스의 persona 읽기

다음은 실제와 유효 사용자와 그룹 ID들을 읽기 위한 함수들의 상세한 설명이다. 그 기능들을 사용하기 위해서는 당신은 헤더파일 `sys/types. h'와 `unistd. h'를 포함시켜야만 한다.

데이터타입 : uid__t

이것은 사용자 ID들을 나타내는데 사용되는 정수 데이터 타입이다. GNU 라이브러리에서, 이것은 unsigned int 와 같다.

데이터타입 : gid__t

이것은 그룹 ID들을 나타내기 위해서 사용되는 정수 데이터타입이다. GNU 라이브러리에서, 이것은 unsigned int 와 같다.

함수 : uid_t getuid (void)

getuid 함수는 프로세스의 실제 사용자 ID를 반환한다.

함수 : gid_t getgid (void)

getgid 함수는 프로세스의 실제 그룹 ID들을 반환한다.

함수 : uid_t geteuid (void)

geteuid 함수는 프로세스의 유효 사용자 ID를 반환한다.

함수 : gid_t getegid (void)

getegid 함수는 프로세스의 유효 사용자 ID 를 반환한다.

함수 : int getgroups (int count, gid_t *groups)

getgroups 함수는 프로세스의 부가적인 그룹 ID들에 대해서 묻는데 사용된다. 그들 그룹 ID들의 개수까지도 배열 groups에 저장된다; 함수로부터의 반환값은 실제로 저장된 그룹 ID들의 개수이다. 만일 count가 부가적인 그룹 ID들의 전체 개수보다 작다면, getgroups는 -1의 값을 반환하고 errno를 EINVAL로 설정한다. 만일 count 가 0이면, getgroups는 단지 부가적인(supplementary) 그룹 ID들의 전체 개수를 반환한다. 부가적인 그룹들을 지원하지 않는 시스템에서는, 이것은 항상 0이 될 것이다.
 
다음은 getgroups가 어떻게 모든 부가적인 그룹 ID들을 읽어서 사용하는지에 대한 것이다.
gid_t *read_all_groups (void)
{
int ngroups = getgroups (NULL, 0);
gid_t *groups = (gid_t *) xmalloc (ngroups * sizeof (gid_t));
int val = getgroups (ngroups, groups);
if (val < 0) {
free (groups);
return NULL;
}
return groups;
}


25. 6 사용자 ID 설정하기

이 절은 프로세스의 사용자 ID(실제 그리고/또는 유효)를 변경하기 위한 함수들을 설명하고 있다. 그 기능들을 사용하기 위해서, 당신은 헤더파일 `sys/types. h'와 `unistd. h'를 포함해야만 한다.

함수 : int setuid (uid_t newuid)

이 함수는 적당한 특권을 가지고 제공된 프로세스에서, 그 프로세스의 실제 사용자 ID 와 유효사용자 ID를 newuid로 설정한다. 만일 프로세스가 특권이 없다면, newuid는 실제 사용자 ID또는 저장된 사용자 ID(만일 시스템이 _POSIX_SAVED_IDS 를 지원한다면)중의 하나가 되어야만 한다. 이 경우, setuid는 오직 유효 사용자 ID만을 설정하고 실제 사용자 ID는 설정하지 않는다.
 
setuid 함수는 성공하면 0의 값을 반환하고 에러가 발생하면 -1의 값을 반환한다.  
다음의 errno는 이 함수를 위해 정의된 에러상황이다.

EINVAL : newuid 인수의 값이 유용하지 않다.

EPERM

그 프로세스가 적당한 특권을 가지지 않는다; 당신은 정해진 ID로 변경하기 위한 권한을 가지지 않았다.

함수 : int setreuid (uid_t ruid, uid_t euid)

이 함수는 ruid로 프로세스의 실제 사용자 ID를 설정하고 euid로 유효 사용자 ID를 설정한다. 만일 ruid가 -1이면, 그것은 실제 사용자 ID를 변경하지 않음을 의미한다; 그처럼 만일 euid가 -1이면 유효사용자 ID를 변경하지 않음을 의미한다.
 
setreuid 함수는 저장된 ID(saved IDs)들을 지원하지 않는 4. 3 BSD 유닉스와의 호환성을 위해서 존재한다. 당신은 프로세스의 유효와 실제 사용자 ID들을 교환하기 위해서 이 함수를 사용할 수 있다. (특권을 가진 프로세스는 이 특별한 사용에 대해서 제한이 없다. ) 만일 저장된 ID들이 지원된다면, 당신은 이 함수대신에 25. 8절 [Enable/Disable Setuid] 에 있는 함수들을 사용하는 것이 좋다.
 
반환값은 성공하면 0이고 실패하면 -1이다.
다음의 errno는 이 함수를 위해 정의된 에러상황이다.

EPERM

그 프로세스가 적당한 특권을 가지지 않았다; 당신은 정해진 ID로 변경하기 위한 권한이 없다.


25. 7 그룹 ID들을 설정하기

이 절은 프로세스의 그룹 ID들(실제 와 유효)을 변경하기 위한 함수들을 설명하고 있다. 이 기능들을 사용하기 위해서, 헤더파일 `sys/types. h'와 `unistd. h'를 포함해야만 한다.

함수 : int setgid (gid_t newgid)

이 함수는 적당한 특권을 가지고 제공된 프로세스의 실제와 유효 그룹 ID를 newgid로 설정한다. 만일 그 프로세스가 특권이 없다면, newgid는 반드시 실제 그룹 ID 또는 저장된 그룹 ID중의 하나와 동일해야만 한다. 이 경우, setgid는 오직 유효 그룹 ID만 설정하고 실제 그룹 ID는 설정하지 않는다. setgid를 위한 반환값과 에러 상황들은 setuid와 같다.

함수 : int setregid (gid_t rgid, fid_t egid)

이 함수는 rgid로 프로세스의 실제 그룹 ID 를 설정하고 egid로 유효 그룹 ID를 설정한다. 만일 rgid가 -1이면, 그것은 실제 그룹 ID를 변경하지 않음을 의미한다; 그와 같이 만일 egid가 -1이면, 유효 그룹 ID를 변경하지 않음을 의미한다.
 
setregid 함수는 저장된 ID들을 지원하지 않는, 4. 3 BSD 유닉스와의 호환성을 위해서 제공되었다. 당신은 프로세스의 유효와 실제 그룹 ID들을 교환하기 위해서 이 함수를 사용할 수 있다( 특권을 가진 프로세스들은 이 사용에 대해서 제한이 없다. ). 만일 저장된 ID들이 지원된다면, 이 함수를 사용하는 대신에 25. 8절 [Enable/Disable Setuid] 에 있는 함수들을 사용하라. setregid 를 위한 반환값과 에러상황들은 setreuid와 같다. GNU 시스템은 특권을 가진 프로세스가 그들의 부가적인 그룹 ID들을 변경하는 것을 허용한다. setgroups 나 initgroups를 사용하기 위해서, 당신은 헤더파일 `grp. h'를 포함해야만 한다.

함수 : int setgroups (size_t count, gid_t *groups)

이 함수는 프로세스의 부가적인 그룹 ID들을 설정한다. 그것은 오직 특권을 가진 프로세스로 부터 호출될 수 있다. count 인수는 배열 groups안에 있는 그룹 ID들의 개수를 지정한다.
 
이 함수는 성공하면 0을 반환하고 에러가 발생하면 -1을 반환한다. 다음의 errno 는 이 함수를 위해 정의된 에러상황이다.
EPERM : 호출한 프로세스가 특권이 없다.

함수 : int initgroups (const char *user, gid_t gid)

initgroups 함수는 사용자 이름 user를 위해서, 디폴트가 되도록 프로세스의 부가적인 그룹 ID들을 설정하기 위해서 setgroups를 호출한다. 그룹 ID gid 또한 포함된다.


25. 8 Setuid 억세스를 가능하게 하거나 불가능하게 하기

전형적인 setuid를 사용하는 프로그램에서 항상 특별한 억세스가 필요한 것은 아니다. 그래서 필요하지 않을 때 이 억세스를 꺼버리면, 무의식적인 억세스에 대한 빌미를 제공하지 않게된다. 만일 시스템이 저장된 사용자 ID 기능을 지원하지 않는다면, 당신은 setuid로 이것을 수행할 수 있다. 게임 프로그램이 시작할 때, 그것의 실제 사용자 ID는 jdoe이고, 유효 사용자 ID는 games이고, 그것의 저장된 사용자 ID 또한 games이다. 그 프로그램은 다음과 같이, 시작할 때 일단 사용자 ID값들을 기록하게 된다.

    user_user_id = getuid ();

    game_user_id = geteuid ();

그러면 setuid (user_user_id); 로 게임 파일 억세스를 끌 수 있고 setuid (game_user_id); 로 다시 켤 수 있다. 이 프로세스를 통해서, 실제 사용자 ID를 jdoe로 남고 저장된 사용자 ID는 games로 저장되기 때문에, 프로그램은 항상 다른 것으로 유효 사용자 ID를 설정할 수 있다.

저장된 사용자 ID 기능을 지원하지 않는 다른 시스템에서, 당신은 프로세스의 실제 사용자 ID 와 유효 사용자 ID를 교환하기 위해서는 setreuid를 사용해서 setuid 억세스를 켜고 끌 수 있다. 다음처럼;

    setreuid (geteuid (), getuid ());

이것은 실패하지 않고_항상 허용되는 특별한 경우이다.

왜 이것은 setuid 억세스를 토글링하는 효과를 갖는가? 게임 프로그램이 단지 시작되어 있다고 가정하고, 유효 사용자 ID가 games 일 동안 실제 사용자 ID는 jdoe라고 가정하자. 이 경우, 게임은 점수 파일을 기록할 수 있다. 만일 두 개의 유효 사용자 ID 와 실제 사용자 ID를 교환한다면, 실제 사용자 ID는 games가 되고 유효 사용자 ID는 jdoe가 된다; 이제 프로그램은 오직 jdoe만 억세스를 갖는다. 다른 교환은 유효 사용자 ID를 다시 games로 되돌리고 점수 파일에 억세스를 저장한다.

시스템의 두 종류를 다루기 위해서는, 다음처럼 프리프로세서를 사용해서 저장된 사용자 ID 기능을 지원하는지 테스트해 보아야 한다.

#ifdef _POSIX_SAVED_IDS
setuid (user_user_id);
#else
setreuid (geteuid (), getuid ());
#endif


25. 9 setuid 프로그램 예제

다음은 유효 사용자 ID를 변경하는 프로그램을 어떻게 준비하는지 보여주는 예제이다. 이것은 caber-toss라고 불리는 게임 프로그램의 일부분으로써 그 게임 프로그램은 오직 게임 프로그램 그 자체에 의해서만 점수 파일을 다룰 수 있도록 되어있다. 그 게임 프로그램은 실행파일이 set-user-ID 비트로 인스톨될 것이고 실행파일이 점수파일과 같은 사용자에 의해 소유된다고 가정한다. 전형적으로, 시스템 관리자는 이 목적을 위해서 게임과 같은 계정을 준비할 것이다.

실행가능파일은 모드 4755가 주어지고, 그래서 `ls -l'을 하면 다음과 같은 출력을 얻게된다.

-rwsr-xr-x 1 games 184422 Jul 30 15: 17 caber-toss

set-user-ID 비트는 파일 모드에서 `s'로 나타난다. 점수 파일을 모드 644로 주어지고, `ls -l'하면 다음과 같은 결과를 얻는다.

다음은 어떻게 변경된 사용자 ID를 준비할 것인지를 보여주는 프로그램의 일부분이다. 이 프로그램은 만일 시스템이 저장된 ID 기능을 지원한다면 그것을 사용하고, 그렇지 않다면 setreuid를 사용해서 유효 사용자 ID와 실제 사용자 ID를 교환한다.

#include <stdio. h>
#include <sys/types. h>
#include <unistd. h>
#include <stdlib. h>
 
/* 유효와 실제 사용자 ID들을 저장하라. */
 
static uid_t euid, ruid;
 
/* 원래의 값으로 유효 사용자 ID를 반환하라. */
 
void
do_setuid (void)
{
int status;
 
#ifdef _POSIX_SAVED_IDS
status = setuid (euid);
#else
status = setreuid (ruid, euid);
#endif
if (status < 0) {
fprintf (stderr, "Couldn't set uid. \n");
exit (status);
}
}
 
/* 실제 사용자 ID로 유효 사용자 ID를 설정하라. */
 
void
undo_setuid (void)
{
int status;
 
#ifdef _POSIX_SAVED_IDS
status = setuid (ruid);
#else
status = setreuid (euid, ruid);
#endif
if (status < 0) {
fprintf (stderr, "Couldn't set uid. \n");
exit (status);
}
}
 
/* 메인 프로그램 */
 
int
main (void)
{
/* 실제와 유효 사용자 ID들을 저장하라. */
ruid = getuid ();
euid = geteuid ();
undo_setuid ();
 
/* 게임을 하고 점수를 기록하라. */
. . .
}
 
메인 함수에서 첫째로 어떻게 실제 사용자 ID로 유효 사용자 ID를 되돌려 설정하는지에 대해서 주목하라. 이것은 사용자가 게임을 실행하고 있을 동안 수행되는 파일 억세스들로써 권한을 결정하기 위해서 실제 사용자 ID를 사용한다. 오직 프로그램이 점수 파일을 개방할 필요만 있을 때 다음처럼, 원래의 유효 사용자 ID로 되바꾼다.
 
/* 점수를 기록하라. */
int
record_score (int score)
{
FILE *stream;
char *myname;
 
/* 점수파일을 개방하라. */
do_setuid ();
stream = fopen (SCORES_FILE, "a");
undo_setuid ();
 
/* 파일에 그 점수를 기록하라. */
if (stream)
{
myname = cuserid (NULL);
if (score < 0)
fprintf(stream, "%10s: Couldn't lift the caber. \n", myname);
else
fprintf (stream, "%10s: %d feet. \n", myname, score);
fclose (stream);
return 0;
} else
return -1;
}


25. 10 setuid 프로그램을 만들기 위한 팁

setuid 프로그램은 원래 의도하지 않았던 사용자 억세스를 부여하는 오류를 범하기 쉽다_실제로, 만일 당신이 이러한 문제를 피하기를 원한다면, 각별한 주의를 해야만 한다. 다음은 의도되지 않은 억세스를 막고, 만일 그것일 발생했을 때 그 영향력을 최소화하기 위한 안내지침이다.

절대적인 필요가 없는 한 root 와 같은 특권을 가진 사용자 ID들은 setuid 프로그램을 가져서는 안된다. 만일 자원(resource)이 당신의 특정한 프로그램에게 특정한 것이라면, 특권을 가지지 않은 사용자 ID 또는 그룹 ID가 단지 그 자원을 다루기만 하도록, 새롭게 정의하는 것이 좋다.

유효 사용자 ID를 변경하는것과 함께 system 함수와 exec 함수를 사용하는 것에 대해서는 주의를 해야만 한다. 당신 프로그램의 사용자가 변경된 사용자 ID를 사용하여 프로그램을 실행시키는 것을 허가하지 말아라. execlp 와 execvp 함수들도 위험을 내포하고 있다 (그들은 사용자의 PATH 환경변수에 의존하여 실행되기 때문이다. ) 만일 변경된 ID로 다른 프로그램을 실행해야만 한다면, 실행가능 파일을 절대 파일 이름( 6. 2. 2절 [File Name Resolution] 참조. ) 어로 지정하고, 실행가능 파일과 원래의 users 와 같은 모든 포함된 디렉토리들이 다른 프로그램과 함께 그것의 위치를 변경하지 못하도록 보호를 해야만 한다.

실제로 자원을 사용하는 프로그램의 일부분에서 자원을 제어하고 있는 사용자 ID만을 사용하라. 당신이 그 작업을 마쳤을 때, 실제 사용자의 사용자 ID로 유효 사용자 ID를 반환하라. 25. 8절 [Enable/Disable Setuid] 참조.

만일 당신의 프로그램에서 setuid 부분이 제어할 수 있는 자원이외에 다른 파일을 억세스할 필요가 있다면, 그와같은 파일을 억세스할 수 있는 권한을 가진 실제 사용자를 조회해야만 한다. 당신은 그와같은 일을 위해서 access 함수 ( 9. 8. 6절 [Access Permission] 참조. )를 사용할 수 있다; 그것은 유효 ID들보다는 실제 사용자와 그룹 ID들을 사용한다.


25. 11 누가 로그인 했는지 확인하기

이 절에 설명된 함수들을 사용해서 프로세스를 실행시키고 있는 사용자의 이름과 현재 세션에 로그인 된 사용자의 이름을 알아낼 수 있다. getuid와 friends 함수( 25. 5절 [Reading Persona] 참조. )를 또한 참조하라. getlogin 함수는 `unistd. h'에 선언되어 있고 cuserid 와 L_cuserid는 `stdio. h'에 선언되어 있다.

함수 : char *getlogin (void)

getlogin 함수는 프로세스가 제어하고 있는 터미널에 로그인 되어 있는 사용자의 이름이 포함된 문자열을 가리키는 포인터를 반환하거나, 만일 그 정보를 결정할 수 없다면 널 포인터를 반환한다. 문자열은 정적으로 할당되고 이 함수나 cuserid 함수의 연속적인 호출에 의해서 덧씌워진다.

함수 : char * cuserid (char *string)

cuserid 함수는 프로세스의 유효 ID와 연관된 사용자 이름이 포함된 string을 가리키는 포인터를 반환한다. 만일 string이 널 포인터가 아니라면, 그 string은 적어도 L_cuserid개의 문자를 저장할 수 있는 배열이 된다; 사용자의 이름이 포함된 문자열을 이 string에 반환된다. 그렇지 않다면, 정적인 지역에 있는 문자열을 가리키는 포인터가 반환된다. 이 string는 정적으로 할당되고 이 함수나 getlogin 함수의 연속적인 호출에 의해서 덧씌워질 것이다.

매크로 : int L__cuserid

사용자의 이름을 저장하기에 필요한 배열이 얼마나 길어야 하는지를 지적하는 정수 상수이다. 그 함수들은 이 세션에 로그인 된 사용자나 프로세스를 실행시키고 있는 사용자를 명확하게 확인하도록 허용한다. (그들은 setuid 프로그램이 포함되었을 때는 다를 수 있다; 25. 2절 [Process Persona] 참조. ) 사용자는 그 함수들을 속일 수 있는 방법이 아무 것도 없다.

대부분의 경우에, 사용자를 찾아내는 환경변수 LOGNAME를 사용하는 것이 더욱 유용하다. 이것은 사용자가 제멋대로 LOGNAME 을 설정할 수 있기 때문에 정밀함에 있어서는 좀더 유연성이 있다. 22. 2. 2절 [Standard Environment] 참조.


25. 12 사용자 데이터베이스

이 절은 등록된 사용자의 데이터베이스를 검색하고 찾기 위한 모든 것에 대해서 설명한다. 데이터베이스 자체는 거의 대부분의 시스템에서 `/etc/passwd'에 보존되어 있지만, 어떤 시스템에서는 특별한 네트웍 서버에게 그것을 억세스할 수 있도록 한다.

 

25. 12. 1 사용자의 정보를 담고 있는 데이터 구조체

시스템 사용자 데이터베이스를 억세스 하기 위하여 함수와 데이터 구조체들은 헤더파일 `pwd. h'에 선언되어 있다.

데이터타입 : struct passwd

passwd 데이터구조체는 시스템 사용자 데이터베이스 안에 엔트리들에 대한 정보를 저장하는데 사용된다. 그것은 적어도 다음과 같은 멤버들을 갖고 있다.

char *pw_name : 사용자 로그인 이름.

char *pw_passwd : 암호화된 비밀번호(password) 문자열.

uid_t pw_uid : 사용자 ID 번호.

gid_t pw_gid : 사용자의 디폴트 그룹 ID 번호.

char *pw_gecos

이 문자열은 전형적으로 사용자의 실제 이름을 포함하고, 가능하면 전화번호와 같은 다른 정보도 포함된다.

char *pw_dir

사용자의 홈 디렉토리(home directory), 또는 초기작업디렉토리. 이것에 대한 해석이 시스템-의존적인 경우에는 널 포인터가 되어야한다.

char *pw_shell

사용자의 디폴트 쉘, 이나 사용자가 로그인할 때 실행되는 초기 프로그램. 시스템에서 디폴트로 지정된 것이 사용되는 경우에는, 널 포인터가 되어야한다.

 

25. 12. 2 한 명의 사용자에 대해 자세히 알아보기

특정한 사용자에 대한 정보를 알아내기 위해서는, getpwuid 또는 getpwnam을 사용해서 시스템 사용자 데이터베이스를 검색할 수 있다. 그 함수들은 헤더파일 `pwd. h'에 선언되어 있다.

함수 : struct passwd * getpwuid (uid_t uid)

이 함수는 사용자 ID가 uid인 사용자에 대한 정보를 담고 있는 정적으로 할당된 구조체를 가리키는 포인터를 반환한다. 이 구조체는 getpwuid의 연속적인 호출에 의해서 덧씌워진다. 널 포인터 값은 데이터베이스에 사용자 ID가 uid인 사용자가 없다는 말이다.

함수 : struct passwd *getpwnam (const char *name)

이 함수는 사용자 이름이 name인 사용자에 대한 정보를 담고 있는 정적으로 할당된 구조체를 가리키는 포인터를 반환한다. 이 구조체는 getpwnam의 연속적인 호출로 덧씌워질 것이다. 널 포인터 값은 사용자의 이름이 name인 사용자가 데이터베이스에 없다는 말이다.

 

25. 12. 3 모든 사용자 리스트를 검색하기

이 절은 한 번에 한 사용자씩, 시스템에 있는 모든 사용자들의 리스트를 읽을 수 있기 위해서는 프로그램에서 어떻게 해야하는지를 설명한다. 이곳에 설명된 함수는 헤더파일 `pwd. h'에 선언되어 있다. 당신은 특정한 파일로부터 사용자 엔트리들을 읽기 위해서 fgetpwent 함수를 사용할 수 있다.

함수 : struct passwd * fgetpwent (FILE *stream)

이 함수는 stream 어로부터 다음 사용자 엔트리를 읽고 그 엔트리를 가리키는 포인터를 반환한다. 그 구조체는 정적으로 할당되었고, fgetpwent의 연속적인 호출로 덧씌워진다. 그러므로, 만일 당신이 그 정보를 저장하길 원한다면 구조체의 내용을 다른 곳에 복사해 놓아야만 한다. stream은 표준 password 데이터베이스 파일과 같은 형식의 파일이어야만 한다. 이 함수는 시스템 V 로부터 왔다.

사용자 데이터베이스의 모든 엔트리들을 검색하는 방법에는 setpwent, getpwent, 그리고 endpwent 가 있다.

함수 : void setpwent (void)

이 함수는 getpwent 가 사용자 데이터베이스를 읽기 위해서 사용하는 스트림을 초기화한다.

함수 : struct passwd * getpwent (void)

getpwent 함수는 setpwent 에 의해 초기화된 stream으로부터, 다음 엔트리를 읽고 엔트리를 가리키는 포인터를 반환한다. 그 구조체는 정적으로 할당되었고 getpwent의 연속적인 호출에 의해서 덧씌워진다. 그래서 만일 당신이 그 정보를 저장하기를 원한다면 구조체의 내용을 다른 곳으로 복사해놓아야만 한다.

함수 : void endpwent (void)

이 함수는 getpwent에 의해 사용된 내부 스트림을 닫는다.

 

25. 12. 4 사용자 엔트리를 기록하기

함수 : int putpwent (const struct passwd *p, FILE *stream)

이 함수는 표준 사용자 데이터베이스 파일에서 사용된 형식으로, 스트림 stream에 사용자 엔트리 *p를 기록한다. 성공하면 반환값은 0이고 실패하면 0이 아닌 값이다. SVID 와의 호환성을 위해서 이 함수는 존재한다. 우리는 구조체 struct passwd 가 위에 설명된 표준적인 것을 제외하고는 부가적으로 아무런 멤버들을 가지지 않았다고 예상이 되는 경우에만 이 함수를 사용하고, 그렇지 않은 경우들에는 이 함수를 사용하지 말 것은 권장한다; 전형적인 유닉스 데이터베이스와 더불어 사용자에 대한 다른 확장된 정보를 합병하는 시스템에서, 이 함수를 사용해서 엔트리를 더하는 것은 중요한 정보를 생략하게 되는 것이다.

함수 putpwent 는 `pwd. h'에 선언되어 있다.


25. 13 그룹 데이터 베이스

이 절은 등록된 그룹들의 데이터베이스를 어떻게 검색하고 찾을 것인지에 대한 모든 것이 설명되어 있다. 대부분의 시스템에서 데이터베이스 그 자체는 `/etc/group'에 보존되고 있는데, 어떤 시스템에서 특정한 네트웍 서비스는 그것에 대한 억세스를 제공한다.

 

25. 13. 1 그룹을 위한 데이터 구조체

시스템 그룹 데이터베이스를 억세스하기 위한 함수와 데이터구조체는 헤더파일 `grp. h'에 선언되어 있다.

데이터 타입 : struct group

group 구조체는 시스템 그룹 데이터베이스 안에 엔트리에 대한 정보를 저장하기 위해서 사용된다. 그 구조체는 적어도 다음과 같은 멤버들을 갖고 있다.

char *gr_name 그룹의 이름.

gid_t gr_gid 그룹의 그룹 ID

char **gr_mem

그룹에 있는 사용자들의 이름을 가리키는 포인터의 벡터. 각 사용자 이름은 널문자로 끝나는 문자열이고, 벡터 자체는 끝이 널 포인터로 끝난다.

 

25. 13. 2 한 개의 그룹에 대한 상세한 정보를 알아내기

getgrgid 나 getgrnam 을 사용해서 정해진 그룹에 대한 정보를 그룹 데이터베이스에서 찾아낼 수 있다. 그 함수들은 `grp. h'에 선언되어 있다.

함수 : struct group * getgrgid (gid_t gid)

이 함수는 그룹 ID 로 gid를 갖고 있는 그룹에 대한 정보를 갖고 있는 정적으로 할당된 구조체를 가리키는 포인터를 반환한다. 이 구조체는 getgrgid 의 연속적인 호출에 의해서 덧씌워질 것이다. 널 포인터는 그룹 ID로 gid 를 가진 그룹이 없다는 말이다.

함수 : struct group * getgrnam (const char *name)

이 함수는 그룹 이름으로 name을 가진 그룹에 대한 정보를 담고 있는 정적으로 할당된 구조체에 대한 포인터를 반환한다. 이 구조체는 getgrnam의 연속적인 호출에 의해서 덧씌워진다. 널 포인터는 그룹이름으로 name을 가진 그룹이 없다는 말이다.

 

25. 13. 3 모든 그룹에 대한 리스트를 검색하기

이 절은 프로그램에서 한 번에 한 그룹씩, 시스템에 있는 모든 그룹에 대한 리스트를 어떻게 읽을 수 있는지를 설명한다. 이곳에 설명된 함수들은 헤더파일 `grp. h'에 설명되어 있다. 특정한 파일로부터 그룹 엔트리들을 읽기 위해서 fgetgrent 함수를 사용할 수 있다.

함수 : struct group * fgetgrent (FILE *stream)

fgetgrent 함수는 stream으로부터 다음 엔트리를 읽고, 그 엔트리에 대한 포인터를 반환한다. 그 구조체는 정적으로 할당되었고 fgetgrent 의 연속적인 호출에 의해 덧씌워지기 때문에 만일 당신이 그 정보를 저장하기를 원한다면 그 구조체의 내용을 다른 곳으로 복사해놓아야만 한다. stream은 표준 그룹 데이터파일과 같은 형식을 가진 파일이어야만 한다.

그룹 데이터베이스에 있는 모든 엔트리들을 검색하는 방법은 setgrent, getgrent, 그리고 endgrent 가 있다.

함수 : void setgrent (void)

이 함수는 그룹 데이터베이스로부터 읽기 위한 스트림을 초기화한다. getgrent 함수의 호출에서 이 스트림이 사용된다.

함수 : struct group * getgrent (void)

getgrent 함수는 setgrent 에 의해 초기화된 스트림으로부터 다음 엔트리를 읽고 그 엔트리에 대한 포인터를 반환한다. 그 구조체는 정적으로 할당되었고 getgrent의 연속적인 호출에 의해서 덧씌워진다. 그래서 만일 당신이 그 정보를 저장하기를 원한다면 그 구조체의 내용을 다른 곳으로 복사해놓아야만 한다.

함수 : void entgrent (void)

이 함수는 getgrent 에 의해 사용된 내부스트림을 닫는다.


25. 14 사용자와 그룹 데이터베이스에 대한 예제

다음은 시스템 데이터베이스 질의 함수들을 사용하는 예를 보여주는 예제 프로그램이다. 다음 프로그램은 프로그램을 실행시키고 있는 사용자에 대한 어떤 정보를 출력한다.

#include <grp. h>
#include <pwd. h>
#include <sys/types. h>
#include <unistd. h>
#include <stdlib. h>
 
int
main (void)
{
uid_t me;
struct passwd *my_passwd;
struct group *my_group;
char **members;
 
/* 사용자 ID에 대한 정보를 얻어라. */
me = getuid ();
my_passwd = getpwuid (me);
if (!my_passwd) {
printf ("Couldn't find out about user %d. \n", (int) me);
exit (EXIT_FAILURE);
}
 
/* 정보를 출력하라. */
printf ("I am %s. \n", my_passwd->pw_gecos);
printf ("My login name is %s. \n", my_passwd->pw_name);
printf ("My uid is %d. \n", (int) (my_passwd->pw_uid));
printf ("My home directory is %s. \n", my_passwd->pw_dir);
printf ("My default shell is %s. \n", my_passwd->pw_shell);
 
/* 디폴트 그룹 ID에 대한 정보를 얻어라. */
my_group = getgrgid (my_passwd->pw_gid);
if (!my_group) {
printf ("Couldn't find out about group %d. \n",
(int) my_passwd->pw_gid);
exit (EXIT_FAILURE);
}
 
/* 정보를 출력하라. */
printf ("My default group is %s (%d). \n",
my_group->gr_name, (int) (my_passwd->pw_gid));
printf ("The members of this group are: \n");
members = my_group->gr_mem;
while (*members)
{
printf (" %s\n", *(members));
members++;
}
 
return EXIT_SUCCESS;
}
 
다음은 이 프로그램을 통한 출력이다.
I am Throckmorton Snurd.
My login name is snurd.
My uid is 31093.
My home directory is /home/fsg/snurd.
My default shell is /bin/sh.
My default group is guest (12).
The members of this group are:
friedman
tami


목차 이전 : 24. 작업 제어 다음 26. 시스템 정보

'Programming > Unix Programing' 카테고리의 다른 글

27. System Configuration  (0) 2007.12.22
26. System Information  (0) 2007.12.22
25. 사용자와 그룹  (0) 2007.12.22
24. Job Control(작업 제어)  (0) 2007.12.22
23. Processes  (0) 2007.12.22
22. 프로세스의 시동과 종료(Process StartUp)  (0) 2007.12.22
Comment 0 Trackback 0
Top

24. Job Control(작업 제어)

목차 이전 : 23. 프로세스 다음 : 25. 사용자와 그룹


24 작업 제어

작업제어는 단일한 로그인 세션에서 사용자가 여러개의 프로세스 그룹들(또는 작업들) 사이에서 제어를 옮기는 것을 허용하는 프로토콜을 적용한다. 작업제어 기능은 대부분의 프로그램에서 자동적으로 적당하게 수행되고, 작업제어에 대한 특별한 어떤 것을 할 필요가 없다. 그래서 당신이 쉘이나 로그인 프로그램을 만들지 않는다면 이 장에 있는 것들을 무시할 수 있다.

당신이 이 장에 나와있는 것들을 이해하기 위해서는 시그널 핸들링(21장 [Signal Handling] 참조)과 프로세스 생성(23.2절 [Process Creation Concepts] 참조) 과 연관된 개념에 친숙해질 필요가 있다.


24.1 작업제어의 개념

대화형(interactive) 쉘의 기본적인 목적은 사용자 터미널로부터 명령문을 읽고 그 명령문에의해 지정된 프로그램을 실행하도록 프로세스들을 만드는 것이다. 그것은 fork( 23.4절 [Creadting a Process] 참조.)와 exec ( 23.5절 [Executing a File] 참조.)함수를 사용해서 할 수 있다.

단일한 명령문은 단지 한 개의 프로세스를 실행시키지만_종종 한 개의 명령문도 여러개의 프로세스들을 사용한다. 만일 당신이 쉘 커맨드에서 `|'을 사용한다면 당신은 명시적으로 그들 프로세스안에 여러개의 프로그램들을 요청한것이다. 그러나 만일 당신이 한 개의 프로그램을 실행시킬지라도, 그것이 내부적으로는 여러개의 프로세스들을 사용할 수 있다. 예를들어, `cc -c foo.c'와 같은 단일한 컴파일 명령문은, 전형적으로 네 개의 프로세스들을 사용한다. 만일 당신이 make를 실행한다면, 그 작업은 분리된 프로세스 안에서 다른 프로그램을 실행하는 것이다.

단일한 명령문에 소속되어 있는 프로세스들은 프로세스 그룹이나 작업(job)이라고 불린다. 당신은 동시에 그들 모든 프로세스들을 작동시킬 수 있다. 예를들어, C-c를 타이핑하면 전면 프로세스 그룹에 있는 모든 프로세스가 종료되도록 시그널 SIGINT를 보낸다. 세션은 프로세스들의 좀 큰 그룹이라고 할 수 있다. 일반적으로 단일한 로그인에 있는 모든 프로세스들은 같은 세션에 속한다.

모든 프로세스는 프로세스 그룹에 속한다. 어떤 프로세스가 만들어질 때, 그것은 부모 프로세스와 같은 프로세스 그룹과 세션의 멤버가 된다. 당신은 setpid 함수를 사용해서 다른 프로세스 그룹으로 소속을 바꿀 수 있는데, 그때 그 프로세스 그룹은 같은 세션에 소속된 프로세스 그룹이다.

다른 세션에 프로세스를 소속시키는 유일한 방법은 setsid 함수를 사용해서, 새로운 세션이나, 또는 세션 리더의 처음 프로세스를 만드는 것이다. 새로운 프로세스 그룹에 세션리더를 소속시키면, 당신은 다시 프로세스 그룹의 외부로 그것을 다시 옮길 수 없다.

보통, 새로운 세션들은 시스템 로그인 프로그램에 의해 만들어지고 세션 리더는 사용자의 로그인 쉘을 실행시키는 프로세스이다. 작업제어를 지원하는 쉘은 어떤 시간에 어떤 작업이 터미널을 사용할 수 있도록 제어를 조정해야만 한다. 그렇지않다면 여러개의 작업들이 동시에 터미널로부터 입력을 받아들이려 시도할것이고, 어떤 프로세스가 사용자가 타입한 입력을 받을 것인지가 혼란하게 될 수도 있다. 이것을 방지하기 위해서, 쉘은 이 장에 설명된 프로토콜을 사용해서 터미널 드라이버와 협력해야만 한다.

쉘은 한 번에 오직 한 프로세스 그룹에게만 터미널을 제어하는 제한없는 권한을 줄 수 있다. 이처럼 터미널을 제어하고 있는 프로세스 그룹을 전면 작업(foreground job)이라고 부른다. 터미널을 억세스 하지 않고 실행중인, 쉘에 의해 처리되고 있는 다른 프로세스 그룹들은 배경작업(background job)이라고 불린다.

만일 배경 작업이 터미널을 제어하여 터미널로부터 읽거나 쓰거나 할 필요가 있다면, 그것은 터미널 드라이버에 의해 멈추어진다. 사용자는 SUSP문자( 12.4.9절 [Special Characters] 참조)를 입력함으로써 전면 작업을 멈출 수 있고 프로그램은 SIGSTOP시그널을 보냄으로써 어떤 작업을 멈출 수 있다. 작업들이 멈추었을 때 쉘은 그들에 대하여 사용자에게 알릴 책임이 있고, 멈추어진 작업에 대해서 계속 진행할 것인지에 대한 여부를 사용자에게 선택하도록 허용하기 위한 메카니즘의 제공과 전면 작업과 배경 작업사이의 작업전환도 쉘에게 책임이 있다. 제어중인 터미널에 입/출력 하는것에 대한 자세한 정보는 24.4절 [Access to the Terminal] 참조.


24.2 작업제어는 선택적이다

모든 운영체제가 작업제어를 지원하는 것은 아니다. GNU 시스템은 작업제어를 지원하지만, 만일 다른 시스템에서 GNU 라이브러리를 사용한다고해도 그 시스템에서 작업제어를 지원하지 않을 수 도 있다. 당신은 시스템이 작업제어를 지원하는지에 대한 여부를 시험하기 위해서 컴파일할 때 _POSIX_JOB_CONTROL 매크로를 사용할 수 있다. 27.2절[System Options] 참조.

만일 작업제어가 지원되지 않으면, 한 세션만다 오직 한 개의 프로세스 그룹만 존재하고 그 프로세스 그룹은 항상 전면 작업으로써 행동한다. 부가적인 프로세스 그룹을 만드는 함수에는 에러코드 ENOSYS를 사용해서 간단히 실패했음을 알린다.

다양한 작업제어 시그널(21.2.5절 [Job Control Signals] 참조.)들을 대표하고 있는 매크로들은 작업제어가 지원되지 않는다고 할지라도 정의되어 있다. 그렇지만, 작업제어를 지원하지 않는 시스템에서 그 시그널들을 결코 발생되지 않고, 에러를 보고하거나 아무것도 하지않는 등의 시그널에 대한 행동을 정하거나 시험하거나 보내려고 시도하지 않는다.


24.3 프로세스의 터미널 제어하기

프로세스의 속성들중 하나는 그 프로세스가 제어중인 터미널이다. fork를 사용해서 만들어진 자식 프로세스는 그들의 부모 프로세스로부터 제어중인 터미널을 상속받는다. 이와같은 방법으로, 한 세션안에 있는 모든 프로세스들은 세션 리더(session leader)로부터 제어중인 터미널을 상속받는다. 터미널의 제어권을 가진 세션리더를 터미널의제어중인 프로세스(controlling process)라고 불린다.

당신이 로그인할 때 시스템이 당신을 위해서 그 일을 하기 때문에, 당신은 세션에 터미널 제어권을 할당하는 정확한 메카니즘에 대해서는 걱정할 필요가 없다. 개별적인 프로세스가 새로운 세션의 리더가 되기위해서 setsid를 호출할 때, 그 프로세스는 제어중인 터미널로부터 단절된다.


24.4 제어중인 터미널 억세스

터미널을 제어하고 있는 전면작업안의 프로세스들은 터미널에 대한 제한 없는 억세스 권한을 가진다; 배경프로세스들은 권한을 가지지 않는다. 이 절에서는 배경작업에 있는 프로세스가 터미널을 제어하려 시도할 때 어떤 일이 발생하는지에 대해서 자세히 설명하고 있다.

배경작업에 있는 프로세스가 전면작업이 제어하고 있는 터미널로부터 읽으려 시도할 때, 프로세스 그룹은 SIGTTIN 시그널을 받는다. 이것은 그룹안에 있는 모든 프로세스들의 동작을 멈추게 하는 원인이 된다(그 시그널이 처리되지 않거나 그들 스스로 멈추게 하지 않는다면). 그렇지만 읽기를 시도한 프로세스가 이 시그널을 무시하거나 블록한다면, 대신에 EIO 에러를 얻게 될 것이다.

유사하게, 배경작업에 있는 프로세스가 전면작업에서 제어하고 있는 터미널에 출력을 시도한다면, 그것에 대응된 디폴트 동작은 프로세스 그룹에게 SIGTTOU 시그널을 보내는 것이다. 그렇지만, 그 동작은 지역 모드 플래그(local modes flags)(12.4.7절 [Local Modes] 참조.)의 TOSTOP 비트에 의해서 갱신된다. 만일 이 비트가 설정되지 않는다면(이것이 디폴트이다.), 전면 작업에 의해 제어중인 터미널에 배경작업이 출력을 시도하는 것에는 시그널을 보내지 않고 항상 허가된다. 또한 출력을 시도하는 프로세스에 의해 SIGTTOU 시그널이 무시되거나 블록된다면 이때도 출력은 허가된다.

프로그램에서 할 수 있는 터미널 명령들의 대부분은 입력이나 출력을 위한 것이다. 기본적인 입력과 출력 함수에 대한 정보는 8.2절 [I/O Primitives] 참조.


24.5 고아가된 프로세스 그룹들

터미널 제어중이던 프로세스가 종료되면, 그 터미널은 해제되고 새로운 세션에 의해서 다시 제어될 수 있다.(실제로, 다른 사용자가 터미널 상에서 로그인 할 수 있다.) 이것은 만일 예전의 세션에 있는 어떤 프로세스가 그 터미널을 계속 사용하려고 시도한다면 문제가 발생할 수 있다.

그러한 문제들을 막기위해서, 세션리더가 종료된 후에도 계속 실행되고 있는 프로세스 그룹들은 고아가된 프로세스 그룹으로써 표시된다. 고아 프로세스 그룹(orphand process group) 안에 소속된 프로세스들은 터미널에 읽거나 쓰기를 할 수 없다. 만일 그렇게 하려 시도한다면 EIO 에러를 얻게 될 것이다.

어떤 프로세스 그룹이 고아가 되었을 때, 그 프로세스들은 SIGHUP 시그널을 받는다. 보통, 이것은 프로세스를 종료시키는 원인이 된다. 그렇지만, 만일 프로그램이 이 시그널을 무시하거나 시그널을 위한 핸들러를 만들었다면(21장 [Signal Handling] 참조.), 터미널을 제어중이던 프로세스가 종료되었다고 하더라도 실행중이던 고아 프로세스들은 계속 그 작업을 수행할 수 있다; 그렇지만 더 이상은 터미널의 억세스를 시도할 수 없다.


24.6 작업제어 쉘 실행시키기

이 절은 쉘이 작업 제어를 수행하기 위해서는 무엇을 해야만 하는지, 그 개념이 포함된 간단한 예제 프로그램을 통해서 설명하고 있다.

24.6.1절 [Data Structures]

이곳에서는 그 예제를 소개하고, 주요한 데이터 구조체를 소개하고 있다.

24.6.2절 [Initializing the Shell]

쉘이 작업제어를 준비하기 위해서 수행해야만 하는 동작들에 대해서 논의하고 있다.

24.6.3절 [Launching Jobs]

명령문을 실행하기 위해서 어떻게 작업들을 만들것이지에 대한 정보가 포함되어 있다.

24.6.4절 [Foreground and Background]

배경작업에 대립하는것으로써 전면으로 작업을 진출시킬 때 쉘이 무엇을 다르게 해야 만하는지를 설명하고 있다.

24.6.5절 [Stoppend and Terminated Jobs]

쉘에게 작업상황을 보고하기에 대해서 설명한다.

24.6.6 [Continuing Stopped Jobs]

작업이 멈추어졌을 때 다시 계속하기 위해서 어떻게 해야하는지를 설명한다.

24.6.7 [Missing Pieces] : 쉘의 다른 부분들에 대해서 설명하고 있다.

 

24.6.1 쉘을 위한 데이터 구조체들

이 장에 포함된 모든 예제 프로그램들은 간단한 쉘 프로그램의 일부분이다. 이 절은 이 예제에서 사용되고 있는 데이터 구조체와 유틸리티 함수들을 설명하고 있다.

간단한 쉘은 주로 두 개의 데이터 구조체를 다룬다. job 타입(type)은 파이프(pipes)로 서로 연결된 서브프로세스들의 집합인 작업에 대한 정보를 포함한다. process 타입(type)은 단일한 서브프로세스에 대한 정보를 저장하고 있다. 다음은 적절한 데이터 구조체 선언들이다.

/* process는 한 개의 단일한 프로세스이다. */
typedef struct process
{
struct process *next; /* 파이프라인에 있는 다음 프로세스 */
char **argv; /* exec을 위한 */
pid_tpid; /* 프로세스 ID */
char completed; /* 프로세스가 수행되었으면 참이다. */
char stopped; /* 만일 프로세스가 멈추었으면 참이다. */
int status; /* 보고된 상황들의 값 */
} process;
 
/* job 은 프로세스들의 파이프라인이다. */
typedef struct job
{
struct job *next; /* 다음 활동할 작업 */
char *command; /* 메시지를 위해서 사용되는, 커맨드 라인 */
process *first_process; /* 작업안에 있는 프로세스의 리스트 */
pid_t pgid; /* 프로세스 그룹 ID */
char notified; /* 사용자가 멈춘 작업에 대해서 말했으면 참이다. */
struct termios tmodes; /* 저장된 터미널 모드들 */
int stdin, stdout, stderr; /* 표준 입/출력 채널들 */
} job;
 
/* 활성화된 작업들은 리스트안에 연결되어 있고 이것이 그 헤드(head)이다.*/
job *first_job = NULL;
 
다음은 작업 오브젝트들은 운영하기 위해서 사용되는 유틸리티 함수들에 대한 것이다.
 
/* pgid를 통해서 활성 작업을 찾아라. */
job *
find_job (pid_t pgid)
{
job *j;
 
for (j = first_job; j; j = j->next) {
if (j->pgid == pgid)
return j;
}
return NULL;
}
 
/* 만일 job 안에 있는 모든 프로세스들이 완료되었거나 멈추어있다면 참을 리턴한다 */
int
job_is_stopped (job *j)
{
process *p;
 
for (p = j->first_process; p; p = p->next) {
if (!p->completed && !p->stopped)
return 0;
}
return 1;
}
 
/* 만일 job 안에 있는 모든 프로세스들이 완료되었다면 참을 리턴 */
int
job_is_completed (job *j)
{
process *p;
 
for (p = j->first_process; p; p = p->next) {
if (!p->completed)
return 0;
}
return 1;
}

 

24.6.2 쉘 초기화하기

일반적으로 작업제어를 수행하는 쉘 프로그램이 시작될 때, 그 쉘이 이미 자신의 작업제어를 수행하고 있는 다른 쉘로부터 호출되었을 경우에는 주의를 해야만 한다. 서브쉘(subshell)에서 작업제어가 가능하게 되기전에 우선 부모 쉘에 의해서 전면에 놓여져야만 한다. 그것은 getpgrp로 초기 프로세스 그룹 ID를 얻고, 현재 터미널을 제어하고 있는 전면작업의 프로세스 그룹 ID와 그것을 비교해서 한다( tcgetpgrp함수를 사용해서 추출될 수 있다. )

만일 그 서브쉘이 전면 작업으로써 실행되고 있지 않다면, 그것은 그 자신의 프로세스 그룹에 SIGTTIN 시그널을 보냄으로써 스스로 멈춰야만 한다. 그것은 전면에 그 자신을 제멋대로 놓을수 없다; 사용자가 부모 쉘에게 이것을 시키도록 기다려야만 한다. 만일 그 서브쉘이 계속된다면, 검사를 반복하고 만일 그것이 여전히 전면에 있지 않다면 다시 스스로 멈춘다.

일단 서브쉘이 부모 쉘에 의해서 전면에 놓이게되면, 서브쉘은 자신의 작업 제어가 가능하게 된다. 자신소유의 프로세스 그룹안에 그 자신이 놓이도록 setpgid를 호출하고, 그다음 전면에 이 프로세스 그룹이 놓이도록 tcsetpgrp를 호출함으로써 이러한 일을 한다.

쉘이 작업제어가 가능하게 되었을 때, 작업제어를 멈추도록 하는 모든 시그널에 대해서 무시하도록 그 자체에서 설정하기 때문에 그 자체는 우연히 멈추게 되지 않는다. 당신은 모든 멈춤 시그널들을 SIG_IGN 으로 설정하여 이러한 일을 할 수 있다.

비-대화식으로 실행되는 서브쉘은 작업제어가 지원되지도 않고 지원될 수도 없다. 그것은 그자체가 쉘인 프로세스 그룹안에 만든 모든 프로세스가 존재해야만 한다; 이것은 비-대화실 쉘과 부모 쉘에 의해 단일한 작업으로 취급되는 자식 프로세스를 허용한다. 이것은 작업제어를 사용하지 않지만 그것을 하는 쉘을 만들기 위해서는 기억해야만 한다.

다음은 이러한 모든 것을 어떻게 하는지 보여주는 예제 쉘을 위한 초기 코드이다.

/* 쉘의 속성을 기억하라. */
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
 
pid_t shell_pgid;
struct termios shell_tmodes;
int shell_terminal;
int shell_is_interactive;
 
/* 진행하기 전에 쉘이 전면작업으로써 대화식으로 실행되고 있는지를 확인하라. */
void
init_shell ()
{
 
/* 만일 대화식으로 실행된다면 보아라. */
shell_terminal = STDIN_FILENO;
shell_is_interactive = isatty (shell_terminal);
 
if (shell_is_interactive)
{
/* 전면에 놓일때까지 루프를 돈다. */
while (tcgetpgrp (shell_terminal) != (shell_pgid = getpgrp()))
kill (- shell_pgid, SIGTTIN);
 
/* 대화와 작업-제어 시그널들을 무시하라. */
signal (SIGINT, SIG_IGN);
signal (SIGQUIT, SIG_IGN);
signal (SIGTSTP, SIG_IGN);
signal (SIGTTIN, SIG_IGN);
signal (SIGTTOU, SIG_IGN);
signal (SIGCHLD, SIG_IGN);
 
/* 우리소유의 프로세스 그룹에 우리자신을 넣어라. */
shell_pgid = getpid ();
if (setpgid (shell_pgid, shell_pgid) < 0) {
perror("Couldn't put the shell in its own process group");
exit (1);
}
 
/* 터미널의 제어를 잡아라 */
tcsetpgrp (shell_terminal, shell_pgid);
 
/* 쉘을 위한 디폴트 터미널 속성을 저장하라. */
tcgetattr (shell_terminal, &shell_tmodes);
}
}

 

24.6.3 작업들을 개시하기

일단 쉘이 터미널을 제어하여서 작업제어를 수행하는 책임을 갖게 되면, 쉘은 사용자에의해 입력된 명령문에 응답하여 작업을 개시할 수 있다. 프로세스 그룹에서 프로세스들을 만들기 위해서는, 23.2절 [Process Creation Concepts] 에 설명된 fork 와 exec를 사용한다. 그곳에는 여러개의 자식 프로세스들이 이미 존재하고 있기 때문에, 그 일을 하기는 좀 많이 복잡하고 당신은 올바른 순서로 프로세스를 만들도록 주의를 해야만 한다. 그렇지 않다면, 고약한 경쟁상황(race condition)이 발생될 것이다.

당신은 프로세스들 사이의 부모-자식 관계를 어떻게 구성할것인지에 대해서 두가지를 선택할 수 있다. 프로세스 그룹안에 있는 모든 프로세스들을 쉘 프로세스의 자식 프로세스로 만들거나, 또는 그룹에 있는 한 개의 프로세스를 그 그룹에 있는 다른 모든 프로세스들의 조상이 되도록 만들수 있다. 이 장에 설명된 예제 쉘 프로그램은 첫 번째가 좀 간단하게 때문에 첫 번째를 사용한다.

만들어진 각각의 프로세스는, setpid를 호출하여 새로운 프로세스 그룹에 그 자신을 넣어야만 한다; 24.7.2절 [Process Group Functions] 참조. 새로운 그룹에 있는 첫 번째 프로세스는 그 프로세스 그룹의 리더가 되고 그 프로세스 ID가 그 그룹을 위한 프로세스 그룹 ID가 된다.

쉘은 새로운 프로세스 그룹으로 각각의 자식 프로세스들을 넣기 위해서 setpgid를 호출한다. 이것은 타이밍 문제가 내포되어 있다; 각각의 자식 프로세스는 새로운 프로그램이 실행되기 전에 프로세스 그룹에 들어가야만 하고, 쉘은 실행을 계속하기 전에 그룹이 가지고 있는 모든 자식 프로세스에 의존한다. 만일 자식 프로세스와 쉘이 모두 setpgid를 호출한다면, 이것은 프로세스가 먼저 그 일을 처리하도록 해서 아무런 문제가 발생되지 않을 것이다.

만일 전면 작업으로 어떤 작업이 진행되고 있다면, 새로운 프로세스 그룹을 tcsetpgrp를 사용해서 터미널을 제어하고 있는 전면으로 넣을 필요가 있다. 다시, 이것은 경쟁 상황을 피하기 위해서 각각의 자식 프로세스 뿐만아니라, 쉘에 의해서도 수행된다. 각 자식 프로세스의 다음 일은 시그널동작을 재설정하는 일이다.

초기화 하는동안, 쉘 프로세스는 작업제어 시그널들을 무시하도록 그 자신을 설정한다; 24.6.2절 [Initializing the Shell] 참조. 그 결과로, 그것이 만든 어떤 자식 프로세스도 또한 상속으로 인해서 그러한 시그널들을 무시하게 된다. 이것에 만족하지 못하면, 각각의 자식 프로세스는 그것이 만들어진 후에 SIG-DFL을 사용해서 그 시그널의 원래 동작으로 되돌려 설정할 수 있다.

쉘이 이러한 관습을 따르기 때문에, 응용 프로그램은 그들이 부모 프로세스로부터 시그널의 처리에 대한 것을 정확히 상속받는다고 가정 할 수 있다. 그러나 모든 응용프로그램은 멈춤 시그널들의 처리에 간섭할 수 없다. SUSP 문자의 일반적인 해석이 불가능하게 만든 응용프로그램은 사용자가 작업을 멈추게 하기 위해서 다른 메카니즘을 제공한다. 사용자가 이 메카니즘을 호출할 때, 그 프로그램은 단지 프로세스 그 자체가 아니라, 그 프로세스의 프로세스 그룹에게 SIGTSTP 시그널을 보낸다. 21.6.2절 [Signaling Another Process] 참조.

마침내, 각각의 자식 프로세스는 보통의 방법으로 exec를 호출하여야 한다. 이것은 표준 입/출력 채널들의 리다이렉션이 처리되는 지점이다. 8.8절 [Duplicating Descriptors] 를 참조하여 이것을 어떻게 하는지 보아라. 다음은 프로그램을 개시하기 위한, 대화식 쉘 프로그램에 있는 함수이다. 그 함수는 쉘에 의해서 자식 프로세스가 만들어진 후에 즉시 자식 프로세스에 의해서 실행되고 결코 리턴하지 않는다.

void
launch_process (process *p, pid_t pgid, int infile, int outfile, int errfile, int foreground)
{
pid_t pid;
 
if (shell_is_interactive)
{
/* 프로세스 그룹에 그 프로세스를 넣고 터미널을 프로세스 그룹에게 주어라, 적당하면, 이것은 쉘과 개별적인 자식 프로세스에 의해서 모두 실행되기 때문에 경쟁상황을 포함하고 있다. */
pid = getpid ();
if (pgid == 0) pgid = pid;
setpgid (pid, pgid);
if (foreground)
tcsetpgrp (shell_terminal, pgid);
 
/* 작업제어 시그널위한 처리를 디폴트로되돌려 설정하라. */
signal (SIGINT, SIG_DFL);
signal (SIGQUIT, SIG_DFL);
signal (SIGTSTP, SIG_DFL);
signal (SIGTTIN, SIG_DFL);
signal (SIGTTOU, SIG_DFL);
signal (SIGCHLD, SIG_DFL);
}
 
/* 새로운 프로세스를 위한 표준 입/출력 채널들을 설정하라. */
if (infile != STDIN_FILENO)
{
dup2 (infile, STDIN_FILENO);
close (infile);
}
if (outfile != STDOUT_FILENO)
{
dup2 (outfile, STDOUT_FILENO);
close (outfile);
}
if (errfile != STDERR_FILENO)
{
dup2 (errfile, STDERR_FILENO);
close (errfile);
}
 
/* 새로운 프로세스를 실행하라. exit를 확인하라. */
execvp (p->argv[0], p->argv);
perror ("execvp");
exit (1);
}

만일 쉘이 대화식으로 실행되지 않으면, 이 함수는 프로세스 그룹이나 시그널로 아무일도 하지 않는다. 작업제어를 수행하지 않는 쉘은 그 자체가 쉘인 프로세스 그룹안에 모든 서브프로세스들이 존재해야만 한다.

다음은 실제로 완전한 작업을 시작하는 함수이다. 자식 프로세스가 만들어진 후에, 이 함수는 전면이나 배경으로 새로이 만들어진 작업을 넣기위해서 어떤 다른 함수를 호출한다; 그것은 24.6.4절 [Foreground and Background] 에 설명되어 있다.

void
launch_job (job *j, int foreground)
{
process *p;
pid_t pid;
int mypipe[2], infile, outfile;
 
infile = j->stdin;
for (p = j->first_process; p; p = p->next)
{
/* 만일 필요하면 pipe들을 준비하라. */
if (p->next) {
if (pipe (mypipe) < 0) {
perror ("pipe");
exit (1);
}
outfile = mypipe[1];
} else {
outfile = j->stdout;
}
 
/* 자식 프로세스들을 만들어라 */
pid = fork ();
if (pid == 0) { /* 이것은 자식 프로세스이다. */
launch_process (p, j->pgid, infile, outfile, j->stderr, foreground);
} else if (pid < 0) {
/* fork 가 실패했다. */
perror ("fork");
exit (1);
} else { /* 이것은 부모 프로세스이다. */
p->pid = pid;
if (shell_is_interactive) {
if (!j->pgid)
j->pgid = pid;
setpgid (pid, j->pgid);
}
}
 
/* pipe들을 정리하라. */
if (infile != j->stdin)
close (infile);
if (outfile != j->stdout)
close (outfile);
infile = mypipe[0];
}
 
format_job_info (j, "launched");
 
if (!shell_is_interactive)
wait_for_job (j);
else if (foreground)
put_job_in_foreground (j, 0);
else
put_job_in_background (j, 0);
}

 

24.6.4 전면 과 배경

이제 전면(foreground)에 있는 작업을 시작할 때 쉘에 의해서 행해져야만 하는 동작은 무엇이며, 배경작업이 시작될 때 해야만 되는 것과 무엇이 어떻게 다른지 알아보자. 전면작업이 시작될 때, 쉘은 첫 번째로 tcsetpgrp를 호출해서 그 전면작업에 터미널 제어권을 줘야만 한다. 그리고나서, 쉘은 프로세스 그룹안에 있는 프로세스들이 종료되거나 멈출때까지 기다려야한다. 이것에 대한 상세한 내용은 24.6.5절 [Stopped and Terminated Jobs] 를 참조하라.

그룹안에 있는 모든 프로세스들이 수행됐거나 멈추었을 때, 쉘은 다시 tcsetpgrp를 호출해서 자신의 프로세스 그룹을 위해서 터미널 제어권을 되찾는다. 배경작업으로부터의 입/출력이나 사용자에의해 입력된 SUSP 문자가 원인이 된 멈춤 시그널이 프로세스 그룹에게 보내어지면, 보통, 작업안에 있는 모든 프로세스들이 함께 멈춘다.

전면작업이 터미널을 이상한 상황으로 만들었을지도 모르므로, 쉘은 계속하기전에 자신이 저장해놓았던 터미널 모드들을 반환하여야한다. 작업이 완전히 멈춘경우에, 쉘은 일단 현재 터미널 모드들을 저장하고, 그래서 만일 그 작업이 나중에 다시 계속된다면 그들을 반환할 수 있다. 터미널 모드들을 다루는 함수들은 tcgetattr 과 tcsetattr이 있다; 그들은 12.4절 [Terminal Modes] 를 참조하라. 다음은 위에 설명된 일들을 하는 예제 쉘을 함수이다.

/* 작업 j를 전면에 놓는다. 만일 cont가 0이 아니면, 저장된 터미널 모드들을 반환하고 우리가 블록하기 전에 그것이 계속되도록 SIGCONT 시그널을 프로세스 그룹에게 보낸다. */
void
put_job_in_foreground (job *j, int cont)
{
/* 전면으로 작업을 놓는다. */
tcsetpgrp (shell_terminal, j->pgid);
 
/* 만일 필요하다면 작업을 계속하도록 시그널을 보낸다. */
if (cont) {
tcsetattr (shell_terminal, TCSADRAIN, &j->tmodes);
if (kill (- j->pgid, SIGCONT) < 0)
perror ("kill (SIGCONT)");
}
 
/* 보고하도록 기다린다. */
wait_for_job (j);
 
/* 전면에 쉘을 다시 놓아라. */
tcsetpgrp (shell_terminal, shell_pgid);
 
/* 쉘의 터미널 모드들을 반환한다. */
tcgetattr (shell_terminal, &j->tmodes);
tcsetattr (shell_terminal, TCSADRAIN, &shell_tmodes);
}
만일 프로세스 그룹이 배경작업으로써 시작된다면, 쉘은 전면에 자신을 놓고 터미널로부터 명령문 읽기를 계속한다. 예제 쉘에서는, 배경에 작업을 놓아야 될 필요가 그다지 없다. 다음은 그것을 사용하는 함수이다.
/* 배경에 작업을 놓는다. 만일 cont 인수가 참이면, 그것이 계속되도록 SIGCONT 시그널을 프로세스 그룹에게 보낸다. */
void
put_job_in_background (job *j, int cont)
{
/* 만일 필요하면, 작업을 계속하도록 시그널을 보낸다. */
if (cont)
if (kill (-j->pgid, SIGCONT) < 0)
perror ("kill (SIGCONT)");
}

 

24.6.5 멈추고 종료된 작업들

전면 프로세스가 시작될 때, 쉘은 작업에 있는 모든 프로세스들이 멈추거나 종료될때까지 블록해야만 한다. 그것은 waitpid 함수를 호출함으로써 이루어진다; 23.6절 [Process Completion] 참조. 프로세스가 종료된것뿐만 아니라 멈춤 또한 보고하도록 WUNTRACED 옵션을 사용하라. 쉘은 배경작업이 멈추었거나 종료된 작업들을 사용자에게 보고하도록 배경작업들의 상황을 체크해야만 한다; 이것은 WNOHANG 옵션을 사용해서 waitpid 함수를 호출함으로써할 수 있다. 작업이 멈추었거나 종료되었음을 체크하는것과 같은 코드문을 넣는 좋은 위치는 새로운 명령문을 읽으려(prompting) 하기 전이다.

쉘은 SIGCHLD 시그널을 처리하는 핸들러를 만듦으로써 자식 프로세스를 위한 유용한 상황정보를 비동기적으로 통지받을 수 있다. 21장 [Signal Handling] 참조.

예제 쉘 프로그램의 경우에는, SIGCHLD 시그널은 보통 무시된다. 쉘이 조작하고 있는 전역 데이터 구조체를 재진입하는 문제를 피하기 위함이다. 그러나 쉘이 그들 데이터 구조체를 사용하지 않는 어떤 시간동안에는 _예를들어, 터미널에서 입력을 기다리는 때와 같은_SIGCHLD 시그널을 처리하는 핸들러는 가능하다. 동기적으로 상황들을 체크하는데 사용되는 같은 함수는 (이 경우, do_job_notification) 이 핸들러로부터 호출될 수 있다.

다음은 작업의 상황들을 체크하고 사용자에게 정보를 보고하는 것을 취급하는 예제 쉘 프로그램의 일부분이다.

/* waitpid에 의해 리턴된 프로세스 pid의 상황을 저장하라. 모두 잘 됐으면 0을 리턴하고, 아니면 0이 아닌값을 리턴하라. */
 
int
mark_process_status (pid_t pid, int status)
{
job *j;
process *p;
 
if (pid > 0) {
/* 프로세스를 위해서 기록을 갱신하라. */
for (j = first_job; j; j = j->next) {
for (p = j->first_process; p; p = p->next) {
if (p->pid == pid) {
p->status = status;
if (WIFSTOPPED (status)) {
p->stopped = 1;
} else {
p->completed = 1;
if (WIFSIGNALED (status))
fprintf(stderr, "%d: Terminated by signal %d.\n", (int)pid, WTERMSIG(p->status));
}
return 0;
}
fprintf (stderr, "No child process %d.\n", pid);
return -1;
} /* for 의 끝 */
} /* for 의 끝 */
} else if (pid == 0 || errno == ECHILD) {
/* 보고하기위해 준비된 프로세스가 없다. */
return -1;
} else { /* Other weird errors. */
perror ("waitpid");
return -1;
}
}
 
/* 블록하지지 않고, 유용한 상황정보를 가진 프로세스들을 체크하라. */
 
void
update_status (void)
{
int status;
pid_t pid;
 
 
do {
pid = waitpid(WAIT_ANY, &status, WUNTRACED|WNOHANG);
} while (!mark_process_status (pid, status));
}
 
/* 주어진 작업안에 있는 모든 프로세스들이 보고되었을때까지 블록하여, 유용한 상황정보를 가진 프로세스들을 체크하라. */
 
void
wait_for_job (job *j)
{
int status;
pid_t pid;
 
do {
pid = waitpid (WAIT_ANY, &status, WUNTRACED);
} while (((!mark_process_status (pid, status))
&& (!job_is_stopped (j))
&& (!job_is_completed (j)));
}
 
/* 사용자에게 보이기위한 작업상황에 대한 정보를 형식화하라. */
 
void
format_job_info (job *j, const char *status)
{
fprintf(stderr, "%ld (%s): %s\n", (long)j->pgid, status, j->command);
}
 
/* 멈추었거나 종료된 작업들에 대해서 사용자에게 보고하라. 활성화된 작업 리스트로부터 종료된 작업들을 지워라. */
 
void
do_job_notification (void)
{
job *j, *jlast, *jnext;
process *p;
 
/* 자식 프로세스를 위한 상황정보들을 갱신하라. */
update_status ();
 
jlast = NULL;
for (j = first_job; j; j = jnext)
{
jnext = j->next;
 
/* 만일 모든 프로세스들이 완수되었다면, 작업들이 완수되었음을 사용자에게 알리고 활성 작업 리스트에서 그것을 지워라. */
if (job_is_completed (j)) {
format_job_info (j, "completed");
if (jlast)
jlast->next = jnext;
else
first_job = jnext;
free_job (j);
} else if (job_is_stopped (j) && !j->notified) {
/* 더 이상 진행할 수 없음을 표시하여, 멈추어진 작업에 대하여 사용자에게 통지하라. */
format_job_info (j, "stopped");
j->notified = 1;
jlast = j;
} else /* 여전히 실행되고 있는 작업들에 대해서는 아무것도 말하지 말아라. */
jlast = j;
}
}

 

24.6.6 멈추어있는 작업들을계속 실행시키기

쉘은 프로세스 그룹에게 SIGCONT 시그널을 보냄으로써 멈추어진 작업을 계속하게 할 수 있다. 만일 작업이 전면에서 계속 실행되어야하면, 쉘은 일단 tcsetpgrp 를 호출해서 터미널 제어권을 그 작업에게 부여하고, 저장된 터미널 설정을 반환한다. 전면에서 작업이 재개된 후에, 쉘은 그 작업이 마치 전면에서 시작된것처럼, 그 작업이 멈추거나 종료되기를 기다린다.

예제 쉘 프로그램에서 새로이 만들어진 작업과 재개된 작업들에 대한 처리는 put_job_in_foreground 와 put_job_in_background 한쌍의 함수로 이루어진다. 그들 함수에 대한 정의는 24.6.4절 [Foreground and Background] 에 나와있다. 멈추었던 작업을 재개할 때, cont 인수에 주어진 0이 아닌값은 SIGCONT 시그널을 보내고 적당한 터미널 모드를 반환함을 확실하게 한다.

다음은 재개되어 실행되고 있는 작업에 대해서 쉘이 내부적으로 보유하고 있던 정보를 갱신하는 함수이다.

/* 멈추어있던 작업 J를 다시 실행되고 있는 작업으로써 표시하라. */
 
void
mark_job_as_running (job *j)
{
Process *p;
 
for (p = j->first_process; p; p = p->next)
p->stopped = 0;
j->notified = 0;
}
 
/* 작업 J를 계속하라. */
 
void
continue_job (job *j, int foreground)
{
mark_job_as_running (j);
if (foreground)
put_job_in_foreground (j, 1);
else
put_job_in_background (j, 1);
}

 

24.6.7 이 장에서 설명되지 않은 부분들

이장에 포함된 예제 쉘을 위한 여분의 코드는 전체 쉘 프로그램을 위한 일부분이다. 하지만, 어떻게 작업과 프로그램 데이터 구조체들이 할당되고 초기화되는지에 대해서는 전혀 얘기하지 않았다. 대부분의 실제 쉘은 명령문; 변수들; 약어, 대입, 그리고 파일이름의 패턴 매칭등을 위한 것을 제공하는, 복잡한 사용자 인터페이스로 되어있다. 이곳에 설명된 이 모든 것은 복잡함과는 거리가 멀다! 대신에, 우리는 어떻게 코아 프로세스를 생성을 하는지를 보여주고 그와 같은 쉘에서 어떤 작업제어 함수들이 호출될 수 있는지에 대해서 관심을 두었다. 다음은 우리가 표현했던 주요 엔트리 포인트를 요약한 테이블이다.

void init_shell (void)

쉘의 내부 상황을 초기화한다. 24.6.2절 [Initializing the Shell] 참조.

void init_sh

void launch_job (job *j, int foreground)

전면 또는 배경에서 작업 j를 시작한다. 24.6.3절 [Launching Jobs] 참조.

void do_job_notification (void)

종료되거나 멈추어진 어떤 작업들이 있는지 체크하고 보고한다. SIGCHLD 시그널을 위한 핸들러 안에서 또는 동기적으로 호출될 수 있다. 24.6.5절 [Stopped and Terminated Jobs] 참조.

void continue_job (job *j, int foreground)

작업 j를 재개한다. 24.6.6절 [Continuing Stopped Jobs] 참조.
물론, 실제 쉘은 작업을 처리하기 위해서 다른 함수들을 제공받기 원할지 모른다. 예를들어, 활성화된 모든 작업들의 리스트를 보여주거나, 작업에게 시그널(SIGKILL 과 같은)을 보내는 명령문은 유용할 것이다.


24.7 작업제어를 위한 함수들

이 절은 작업제어와 연관된 함수들을 자세하게 설명하고 있다.

 

24.7.1 제어중인 터미널 확인하기

당신은 제어중인 터미널을 개방하기 위해서 사용할 수 있는 파일이름을 얻기 위해서 ctermid 함수를 사용할 수 있다. GNU 라이브러리에서는, 항상 같은 문자열이 리턴된다: "/dev/tty". 그것은 현재 프로세스의 제어 중인 터미널(만일 그것이 한 개라면)을 참조하기위한 "이상한" 특별파일 이름이다. ctermid 함수는 헤더파일 `stdio.h'에 선언되어 있다.

함수 : char * ctermid (char *string)

ctermid 함수는 현재 프로세스를 위해서 제어중인 터미널의 파일이름이 포함된 문자열을 리턴한다. 만일 문자열이 널 포인터가 아니라면, 그것은 적어도 L_ctermid 문자들을 저장하고 있는 배열이 될 것이다. 문자열을 이 배열안에 리턴된다. 그렇지 않다면, 정적 지역안에 리턴된 문자열에 대한 포인터는 이 함수가 연속적으로 호출되어 덧씌워질 것이다.
만일 어떠한 이유로 파일이름이 결정될 수 없다면 빈 문자열(empty string)을 리턴한다. 심지어 파일이름이 리턴됐을지라도, 그 파일에 대한 억세스는 보증되지 않는다.

매크로 : int L_ctermid

이 매크로는 ctermid에 의해 리턴된 파일이름을 저장하기에 충분히 큰 문자열의 크기를 나타내는 정수 상수 표현식이다.

또한 12.1절 [Is It a Terminal] 에 있는 isatty 와 ttyname 함수를 참조하라.

 

24.7.2 프로세스 그룹 함수들

다음은 프로세스 그룹을 다루기 위한 함수를 설명하고 있다. 당신의 프로그램에서 그들 함수들을 사용하기 위해서는 헤더파일 `sys/types.h' 와 `unistd.h'를 포함해야만 한다.

함수 : pid_t setsid (void)

setsid 함수는 새로운 세션을 만든다. 호출한 프로세스는 세션리더가 되고, 그 프로세스의 프로세스 ID와 같은 ID를 가진 새로운 프로세스 그룹이 만들어지고 프로세스가 그 안에 소속된다. 새로운 프로세스 그룹에 다른 프로세스들이 없고 새로운 세션안에 다른 프로세스 그룹이 없이 초기화된다. 이 함수는 또한 호출한 프로세스가 제어중인 터미널을 갖지 않도록 만든다.
 
setsid 함수는 만일 성공하면 호출한 프로세스의 새로운 프로세스 그룹 ID를 리턴하고 실패하면 -1의 값을 리턴한다.  
다음은 이 함수를 위해 정의된 에러상황이다.

    EPERM

    호출한 프로세스가 이미 프로세스 그룹의 리더이거나, 같은 프로세스 그룹 ID를 가진 다른 프로세스 그룹이 있다.
 
getpgrp 함수는 두 개의 정의를 가진다; 한 개는 BSD 유닉스에서 왔고, 한 개는 POSIX.1 표준으로부터 온 것이다. 당신이 선택한 테스트 매크로(1.3.4절 [Feature Test Macros] 참조.)에 따라서 당신이 어떤 정의를 선택했는지가 결정된다. 즉, 당신이 _BSD_SOURCE를 정의하면 BSD 것이 선택되고; 그렇지않고, _POSIX_SOURCE 또는 _GNU_SOURCE를 정의한다면 POSIX의 것이 선택된 것이다. 오래된 BSD 시스템에서 만들어진 프로그램들은 _BSD_SOURCE 하에서 특별히 정의된 getgrp를 사용하도록 `unistd.h'를 인클루드 할 수없다. 당신은 BSD정의를 획득 하기 위해서 -lbsd-compact 로 그와 같은 프로그램들을 링크해야만 한다.

    POSIX.1 함수 : pid_t getpgrp (void)

    getpgrp의 POSIX.1 정의는 호출한 프로세스의 프로세스 그룹 ID를 리턴한다.

    BSD 함수 : pid_t getpgrp (pid_t pid)

    getpgrp 의 BSD 정의는 프로세스 pid의 프로세스 그룹 ID를 리턴한다. 당신이 호출한 프로세스에 대한 정보를 얻기위해서는 pid인수를 위해서 0의 값을 사용할 수 있다.

함수 : int setpgid (pid_t pid, pid_t pgid)

setpgid 함수는 프로세스 그룹 pgid에 프로세스 pid를 소속시킨다. 특별한 경우로, pid 또는 pgid는 호출한 프로세스의 프로세스 그룹을 지적하기 위해서 0으로 될 수 있다. 이 함수는 작업제어를 지원하지 않는 시스템에서는 실패한다. 24.2절 [Job Control is Optional] 참조하여 상세한 정보를 얻어라.
 
만일 명령이 성공하면, setpgid는 0을 리턴한다. 그렇지 않다면 -1을 리턴한다.  
다음의 errno는 이 함수를 위해 정의된 에러상황이다.

EACCES : pid라는 이름을 가진 자식 프로세스가 만들어진 후에 exec 함수를 통해서 실행되었다.

EINVAL : pgid의 값이 유용하지 않다.

ENOSYS : 시스템이 작업제어를 지원하지 않는다.

EPERM

pid 인수에 의해 지적된 프로세스가 세션리더이거나, 호출한 프로세스와 같은 세션에 없거나, pgid 인수의 값이 호출한 프로세스와 같은 세션안에 있는 프로세스 그룹 ID와 매치되는 것이 없다.

ESRCH

pid 인수에 의해 지적된 프로세스가 호출한 프로세스가 아니거나 호출한 프로세스의 자식 프로세스가 아니다.

함수 : int setpgrp(pid_t pid, pid_t pgid)

이것은 setpgid를 위한 BSD용이다. 두 함수는 같은 일을 한다.

 

24.7.3 제어권을 가진 터미널을 억세스하기 위한 함수들

다음은 터미널의 전면프로세스 그룹을 알아내거나 설정하기 위한 함수들이다. 당신은 그들 함수들을 당신의 어플리케이션에서 사용하기 위해서는 헤더파일 `sys/types.h'와 `unistd.h'를 인클루드해야만 한다. 그들 함수들이 터미널 디바이스를 정하도록 파일 기술자 인수를 정한다고 하더라도, 전면작업은 터미널 파일 그자체와 연관되어 있고 특별히 개방된 파일 기술자가 아니다.

함수 : pid_t tcgetpgrp(int filedes)

이 함수는 기술자 filedes로 개방한 터미널과 연관된 전면 프로세스 그룹의 프로세스 그룹 ID를 리턴한다. 만일 아무런 전면 프로세스 그룹이 없다면, 현존하는 프로세스 그룹의 프로세스 그룹 ID와 매치 되지 않는, 1보다 큰 수를 리턴한다. 그러한 경우는 전면작업으로써 수행되고 있던 작업안에 있는 모든 프로세스들이 종료되고 전면으로 옮겨질 작업들이 없을 때 발생할 수 있다
 
에러의 경우에 -1의 값이 리턴된다. 다음의 errno는 이 함수를 위해 정의된 에러상황이다.

EBADF : filedes 인수가 유용한 파일 기술자가 아니다.

ENOSYS : 시스템이 작업제어를 지원하지 않는다.

ENOTTY : filedes 인수와 연관된 터미널 파일이 호출한 프로세스가 제어중인 터미널이 아니다.

함수 : tcsetpgrp(int filedes, pid_t pgid)

이 함수는 터미널의 전면 프로세스 그룹 ID를 설정하는데 사용된다. 인수 filedes는 터미널을 지정하는 기술자이다; pgid 인수는 프로세스 그룹을 지정한다. 호출한 프로세스는 pgid와 같은 세션의 멤버가 되어야만 하고 같은 터미널을 제어하고 있어야만 한다.
 
터미널 억세스를 목적으로 하여, 이 함수는 출력으로 취급된다. 만일 배경 프로세스로부터 이 함수가 호출된다면, 보통 그 프로세스 그룹안에 있는 모든 프로세스들에게 SIGTTOU 시그널이 보내어진다. 만일 호출한 프로세스 자체가 SIGTTOU 시그널을 무시하는 예외의 경우라면, 그 명령은 수행되고 아무런 시그널도 보내어지지 않는다.
 
만일 성공하면, tcsetpgrp 는 0을 리턴한다. 에러가 발생한 경우에는 -1을 리턴한다.
다음의 errno는 이 함수를 위해 정의된 에러상황이다.

EBADF : filedes 인수가 유용한 파일 기술자가 아니다.

EINVAL : pgid 인수가 유용하지 않다.

ENOSYS : 시스템이 작업제어를 지원하지 않는다.

ENOTTY : filedes는 호출한 프로세스가 제어하고 있는 터미널이 아니다.

EPERM : pgid 는 호출한 프로세스와 같은 세션에 있는 프로세스 그룹이 아니다.


목차 이전 : 23. 프로세스 다음 : 25. 사용자와 그룹

'Programming > Unix Programing' 카테고리의 다른 글

26. System Information  (0) 2007.12.22
25. 사용자와 그룹  (0) 2007.12.22
24. Job Control(작업 제어)  (0) 2007.12.22
23. Processes  (0) 2007.12.22
22. 프로세스의 시동과 종료(Process StartUp)  (0) 2007.12.22
21. 신호처리(Signal)  (0) 2007.12.22
Comment 0 Trackback 0
Top

23. Processes

목차 이전 : 22. 프로세스의 시동과 종료 다음 : 24. 작업 제어


23 프로세스

프로세스들은 시스템의 자원들의 할당을 위한 기본적인 단위이다. 각 프로세스는 자신만의 주소공간과 (보통) 한 개의 제어 쓰레드를 갖는다. 프로세스는 프로그램을 실행한다; 당신은 같은 프로그램을 실행하는데 여러개의 프로세스를 가질 수 있지만, 각각의 프로세스는 자신의 주소공간에서 자신의 프로그램 복제본을 갖고 다른 프로그램 복사본과 독립적으로 실행된다.

프로세스들은 계층적으로 구성된다. 각 프로세스는 그것을 만들었던 부모 프로세스를 갖는다. 주어진 부모 프로세스에 의해 만들어진 프로세스는 자식 프로세스라고 불린다. 자식 프로세스는 부모 프로세스로 부터 그 속성의 대부분을 상속받는다.

이 장은 프로그램에서 어떤 자식 프로세스를 만들고, 종료하고, 제어하는지에 대해서 설명하고 있다. 실제로, 새로운 자식 프로세스를 만들로, 새로운 프로세스가 프로그램을 실행시키고, 원래의 프로그램과 자식 프로세스가 조화롭게 수행되도록 하는데에는 세 가지의 독림적인 명령이 있다.

system 함수는 다른 프로그램을 실행시키기 위한 간단하고, 이식성 있는 메커니즘을 제공한다. 그들은 자동적으로 세 개의 단계를 거친다. 만일 당신이 이러한 일들을 하는데 세심하게 제어할 필요가 있다면, 당신은 각 단계를 개별적으로 수행하는 기본 함수들을 사용할 수 있다.


23. 1 명령 실행시키기

다른 프로그램을 실행하기 위한 가장 쉬운 방법은 system 함수를 사용하는 것이다. 이 함수는 실행되는 서브프로그램의 모든 작업을 수행하지만, 당신이 세밀하게 제어를 할 수는 없다: 당신이 어떤 것을 할 수 있기 전에 그 서브 프로그램이 종료될 때까지 기다려야만 한다.

함수 : int system (const char *command)

이 함수는 쉘 커맨드로써 command를 실행한다. GNU C 라이브러리에서, 그것은 항상 커맨드를 실행하도록 디폴트 쉘 sh를 사용한다. 특별하게, 실행하기 위한 프로그램을 찾기 위해서 PATH에 있는 디렉토리들을 탐색한다. 반환값은 만일 쉘 프로세스를 만들기가 불가능하면 -1을 반환하고 그렇지 않다면 쉘 프로세스의 상황이 된다. 이 상황 코드가 어떻게 해석되어지는지에 대해서는 23. 6절 [Process Completion] 참조. system 함수는 헤더파일 `stdlib. h'에 선언되어 있다.
이식성 노트 : 어떤 C에서는 다른 프로그램을 실행할 수 있는 커맨드 프로세스의 개념을 가지지 않을 수도 있다. 당신은 커맨드 프로세스가 존재하는지에 대한 여부를 system(NULL)을 실행해봄으로써 알아낼 수 있다; 만일 그 반환값이 0이 아니면, 커맨드 프로세서는 유용하다.

popen 과 pclose 함수들은(10. 2절 [Pipe to a Subprocess] 참조) system 함수와 밀접하게 연관되어 있다. 그들은 실행되고 있는 커맨드의 표준 입/출력 채널과 통신하도록 부모 프로세스에게 허용한다.


23. 2 프로세스 만들기에 대한 원칙

이 절은 프로세스와 프로세스를 만드는데 포함되는 단계와 그것이 다른 프로그램을 실행하도록 만드는데 대한 개요가 있다. 각 프로세스는 프로세스 ID 번호에 의해 이름지어진다. 어떤 단일한 프로세스 ID는 그것이 만들어질 때 각 프로세스에게 할당되어진다. 프로세스의 수명이 다하면 부모 프로세스에게 보고된다; 그때, 그 프로세스 ID에 포함된 프로세스의 모든 자원들은 해제된다.

프로세스들은 fork 시스템 호출로 만들어진다( 그래서 새로운 프로세스를 만드는 동작을 forking a process라고 부르기도 한다. ). fork에 의해 만들어진 자식 프로세스는 오직 자신의 프로세스 ID를 제외하고는, 원래 부모 프로세스의 완전한 복제이다.

자식 프로세스를 만든 후에, 부모와 자식 프로세스 모두는 정상적으로 실행을 계속한다. 만일 당신이 당신의 프로그램에서 실행을 계속하기 전에 자식 프로세스가 실행을 끝내도록 기다리기 원한다면, fork 연산을 수행한 후에 wait 나 waitpid(23. 6절 [Process Completion] 참조)를 호출함으로써 명백하게 이일을 하도록 해야만 한다. 그들 함수들은 자식프로세스가 왜 종료되었는지에 대한 제한된 정보_예를 들면, exit 상황코드_를 준다.

새롭게 만들어진 자식 프로세스는 fork가 return을 호출한 지점에서 부모 프로세스와 같은 프로그램을 실행한다. 당신은 그 프로그램이 부모 프로세스 또는 자식 프로세스에서 실행되고 있는지에 대해서 알기 위해서 fork로부터의 반환값을 사용할 수 있다.

여러 개의 프로세스가 같은 프로그램을 실행하기는 때때로 유용하다. 그러나 자식 프로세스는 exec 함수들중의 하나를 사용해서 다른 프로그램을 실행할 수 있다; 23. 5절 [Executing a File] 참조. 프로세스가 실행시키고 있는 프로그램을 프로세스 이미지(process image)라고 부른다. 새로운 프로그램의 실행을 시작하는 것은 프로세스가 그 전의 프로세스 이미지에 대한 모든 것을 잊게 한다; 새로운 프로그램이 종료될 때, 그 전의 프로세스 이미지를 반환하지 않고, 프로그램처럼 종료한다.


23. 3 프로세스 식별

pid_t 데이터 타입은 프로세스 ID들을 나타낸다. 당신은 getpid를 호출함으로써 프로세스의 ID를 얻을 수 있다. getppid 함수는 현재 프로세스의 (이것은 또한 부모 프로세스의 ID로써 알려져 있다. ) 부모 프로세스 ID를 반환한다. 당신의 프로그램에서 그들 함수를 사용하기 위해서는 `unistd. h'와 `sys/types. h'의 헤더파일을 포함해야한다.

데이터 타입 : pid__t

pid_t 데이터 타입은 프로세스 ID를 표현하는 부호화 정수 타입이다. GNU 라이브러리에서, 이것은 int 이다.

함수 : pid_t getpid (void)

getpid 함수는 현재 프로세스의 프로세스 ID를 반환한다.

함수 : pid_t getppid (void)

getppid 함수는 현재 프로세스의 부모 프로세스 ID를 반환한다.


23. 4 프로세스 만들기

fork 함수는 프로세스를 만드는 기본동작(primitive)이다. 그것은 헤더파일 `unistd. h'에 선언되어 있다.

함수 : pid_t fork (void)

fork 함수는 새로운 프로세스를 만든다. 만일 그 명령이 성공하면, 그곳에는 부모와 자식 프로세스가 존재하고 그 둘은 fork의 반환값을 서로 다른 값으로 보게된다. 자식 프로세스 안에서는 0의 값을 반환하고 부모 프로세스 안에서는 자식 프로세스의 ID를 반환한다. 만일 프로세스 만들기가 실패하면, fork는 부모 프로세스에게 -1의 값을 반환한다.
 
다음의 errno는 fork를 위해 정의된 에러 상황이다.

EAGAIN

다른 프로세스를 만들만한 충분한 시스템 자원이 없거나, 사용자가 이미 너무 많은 프로세스들을 실행시키고 있다.

ENOMEM : 프로세스는 시스템이 공급할 수 있는 것보다 더 많은 공간을 필요로 한다.

다음은 부모 프로세스와는 다른, 자식 프로세스의 정해진 속성이다.

자식 프로세스는 자신만의 단일한 프로세스 ID를 갖는다.
 
자식 프로세스의 부모 프로세스 ID는 그 자신의 부모 프로세스의 ID이다.
 
자식 프로세스는 부모 프로세스가 개방한 파일 기술자의 자신 소유의 복사본을 가진다. 부모 프로세스 안에서 연속적으로 속성이 변화하는 파일 기술자들은 자식 프로세스의 파일 기술자에게 영향을 미치지 않고, 그리고 자식 프로세스에 속한 파일기술자의 속성이 변한다고 해도 그것또한 부모 프로세스의 기술자에게 영향을 미치지 못한다. 8. 7절 [Control Operations] 참조.
자식 프로세스를 위하여 경과된 프로세서 시간은 0으로 설정된다; 17. 1절 [Processor Time] 참조.
 
자식 프로세스는 부모 프로세스에 의해 설정된 파일 락(lock)들을 상속받지 않는다. 8. 7절 [Control Operations] 참조.
 
자식 프로세스는 부모 프로세스에 의해 설정된 알람(alarm)을 상속받지 않는다. 17. 3절 [Setting an Alarm] 참조.
 
자식 프로세스를 위해서 미처리중인 시그널의 설정(21. 1. 3절 [Delivery of Signal] 참조)은 소거된다. (자식 프로세스는 부모 프로세스로 부터 블록된 시그널의 마스크와 시그널 동작을 상속받는다. )

함수 : pid_t vfork (void)

vfork 함수는 fork와 유사하지만 더 효율적이다; 하지만, 그것을 안전하게 사용하기 위해서 따랴야만 하는 제한이 있다.
 
fork는 호출된 프로세스의 주소공산의 완전한 복제본을 만들고 부모와 자식 프로세스가 독립적으로 실행되도록 허용하지만, vfork는 이 복제본을 만들지 않는다. 대신에, vfork로 만들어진 자식 프로세스는 exec 함수들중의 하나가 호출될 때까지 부모의 주소공간을 분배받는다. 그것은 부모 프로세스가 일시적으로 실행을 멈추게 됨을 의미한다. 당신은 vfork로 만들어진 자식 프로세스가 부모로부터 분배받은 전역 데이터나 심지어 지역 변수들의 갱신을 허용하지 않도록 매우 주의해야만 한다. 게다가, 자식 프로세스는 호출된 vfork 함수로부터 반환할 수 없다(또는 vfork 함수의 밖으로 long jump하는 것). 이것은 부모 프로세스의 제어 정보를 매우 혼란스럽게 만든다. 만일 의심이 된다면, 대신에 fork를 사용하라. 어떤 운영체제는 실제로 vfork를 지원하지 않는다.
 
GNU C 라이브러리는 모든 시스템에서 vfork를 사용하도록 허용하지만, vfork가 유용하지 않다면 실제로는 fork를 실행한다. 만일 당신이 vfork를 사용하기 위해서 적당하게 예방조치를 취한다면, 당신의 프로그램은 시스템에서 대신에 fork를 사용해서라도 여전히 작업할 것이다.


23. 5 파일 실행시키기

이 절은 프로세스 이미지로써 파일을 실행시키는 exec부류의 함수들을 설명한다. 당신은 자식 프로세스가 만들어진 후에 자식 프로세스가 새로운 프로그램을 실행하도록 그들 함수들을 사용할 수 있다. 이 부류의 함수들은 같은 일을 하지만, 인수를 정하는 방법은 차이가 있다. 그들은 헤더파일 `unistd. h'에 선언되어 있다.

함수 : int execv (const char *filename, char *const argv[])

execv 함수는 새로운 프로세스 이미지로써 파일이름 filename을 실행한다. 널-종료 문자열의 배열인 argv 인수는 실행시키려는 프로그램의 메인 함수에 argv 인수로써 제공하기 위한 값으로 사용된다. 이 배열의 마지막 요소는 반드시 널 포인터가 되어야만 한다. 관례대로, 이 배열의 첫 번째 요소는 디렉토리 이름이 없는 프로그램의 파일 이름이다. 어떻게 프로그램이 그들 인수들을 억세스 하는지에 대한 자세한 것은 22. 1절 [Program Arguments] 참조. 새로운 프로세스 이미지를 위한 환경은 현재 프로세스 이미지의 environ변수로부터 획득된다; 환경에 대한 자세한 정보는 22. 2절 [Environment Variables] 참조.

함수 : int execl (const char *filename, const char *arg(), . . . )

이것은 execv와 유사하지만, argv 문자열은 배열이 아니라 개별적으로 정해져있다. 널 포인터가 마지막 인수로 주어져야만 한다.

함수 : int execve (const chat *filename, chat *const argv[], chat *const env[])

이것은 execv와 유사하지만, env 인수를 통해 새로운 프로그램을 위한 환경을 명시적으로 지정하도록 허용한다. 이것은 environ변수와 같은 형식으로써, 문자열의 배열이다; 22. 2. 1 [Environment Access] 참조.

함수 : int execle (const char *filename, const char *arg(), char *const env[], . . . )

이것은 execl과 유사하지만, 명시적으로 새로운 프로그램을 위한 환경을 정하도록 허용한다. 환경 인수는 마지막 argv 인수로써 표시된 널 포인터의 뒤에 따르고, environ 변수와 같은 형식으로 문자열의 배열이 된다.

함수 : int execvp (const char *filename, char *const argv[])

execvp 함수는 filename에 슬래쉬가 포함되어 있지 않으면 filename으로부터 완전한 파일 이름을 찾기 위해서, PATH 환경변수에 있는 디렉토리를 탐색한다는 점을 제외하고는, execv 와 유사하다 (22. 2. 2 [Standard Environment] 참조. ). 이 함수는 사용자가 선택한 곳에서 그들을 찾기 때문에, 시스템유틸리티 프로그램들을 실행하기에는 유용하다. 쉘들은 사용자가 타이핑한 커맨드를 실행하기 위해서 그것을 사용한다.

함수 : int execlp (const char *filename, const char *arg(), . . . )

이 함수는 execvp 함수처럼 filename과 같은 이름을 탐색한다는 점을 제외하고는, execl 함수와 같다. 서로 받아들인 인수 리스트와 환경 리스트의 크기는 ARG_MAX 바이트 보다 크지 않아야만 한다. 27. 1절 [General Limits] 참조. GNU시스템에서 크기(ARG_MAX와 비교하여)는 각 문자열에서, 문자열 안에 있는 문자들의 개수에 char *의 크기를 더하고, char *의 크기의 배수로 반올림된 하나를 더한다. 다른 시스템들은 크기를 셈하기 위한 다른 규칙을 갖는다.

그들 함수들은 일반적으로 반환하지 않는다. 실패가 발생하면 -1의 값을 반환한다. 보통의 파일 이름 구문 에러(6. 2. 3절 [File Name Errors] 참조. )들에 더해서, 다음의 errno는 이 그들 함수들을 위해서 정의된 에러상황이다.

    E2BIG

    새로운 프로그램의 인수 리스트와 환경 리스트의 결합된 크기가 ARG_MAX 바이트보다 크다. GNU 시스템은 인수 리스트의 크기에 아무런 제한을 두지 않기 때문에 이 에러가 발생하지는 않지만, 그 인수가 유용한 메모리의 크기보다 크다면 ENOMEN의 에러는 발생할 것이다.

    ENOEXEC : 정해진 파일이 올바른 형식이 아니기 때문에 실행될 수 없다.

    ENOMEM : 정해진 파일을 실행시키는데는 현재 유용한 것보다 더 많은 공간이 필요하다.

만일 새로운 파일의 실행이 성공하면, 그것은 마치 그 파일을 읽은 것 그 파일의 억세스 타임 필드(access time field)를 갱신한다. 9. 8. 9절[File Times] 를 참조하여, 파일의 억세스 타임에 대한 자세한 정보를 얻어라. 그 파일이 폐쇄된 지점에서 다시 어떤 것도 정해지지 않았다면, 그것은 프로세스가 종료되기전에나 다른 프로세스 이미지가 실행되기 전인 어떤 지점이다.

새로운 프로세스 이미지를 실행하는 것은 새로운 위치로 인수와 환경 문자열을 복사하고, 메모리의 내용을 완전히 바꾸는 것이다. 그러나 프로세스의 많은 다른 속성들은 변경되지 않는다.

만일 프로세스 이미지 파일의 set-user-ID 와 set-group-ID 모드 비트가 설정되면, 이것은 프로세스의 유효 사용자 ID와 유효 그룹 ID에게 영향을 미친다. 그러한 개념들은 25. 2절 [Process Persona] 에 상세하게 설명되어 있다.

현존하는 프로세스 이미지에서 무시되도록 설정된 시그널들은 새로운 프로세스 이미지에서도 또한 무시되도록 설정된다. 모든 다른 시그널들은 새로운 프로세스 이미지에서 디폴트 동작으로 설정된다. 시그널에 대한 더 많은 정보는, 21장 [Signal Handling] 참조하라.

실행중인 프로세스 이미지에서 개방한 파일 기술자들은, 그들이 FD_CLOEXEC (close-on-exec) 플래그를 설정하지 않는다면, 새로운 프로세스 이미지에서도 개방된 채로 남겨진다. 개방된 상태로 남겨진 파일들은 실행중인 프로세스 이미지로부터 파일 락들을 포함한, 개방 파일 기술의 모든 속성을 상속받는다. 파일 기술자는 8장 [Low-Level I/O] 에 상세하게 설명되어 있다.

파일과 비교하여 스트림은, exec 함수들을 통해서도 살아남지 못한다, 왜냐하면 그들은 프로세스 자체의 메모리 안에 위치하고 있기 때문이다. 새로운 프로세스 이미지는 그들이 새롭게 만든 스트림을 제외하고는 아무런 스트림도 갖지 않는다. pre-exec프로세스 이미지 안에 있는 스트림들의 각각은 그 내부에 기술자를 갖고있고, 그들 기술자는 exec을 통해서 살아남는다. (그들은 FD_CLOEXEC의 설정을 가지지 않고 제공된 것. 새로운 프로세스 이미지는 fdopen을 사용해서 새로운 스트림에 그들을 재연결 할 수 있다. (8. 4절 [Descriptors and Streams] 참조. )


23. 6 프로세스 종료

이 절에서 설명하고 있는 함수들은 자식 프로세스가 종료하거나 멈추도록 기다리는데 사용되고, 그러한 상황인지의 여부를 알아보는데 사용된다. 그들 함수들은 헤더파일 `sys/wait. h'에 선언되어 있다.

함수 : pid_t waitpid (pid_t pid, int *status_ptr, int options)

waitpid 함수는 프로세스 ID를 pid로 가진 자식 프로세스로 부터 상황 정보를 요청하는데 사용된다. 일반적으로, 호출된 프로세스는 자식 프로세스가 종료됨으로써 유용한 상황정보를 만들 때까지 잠시 중지되어 있다.

pid 인수를 위한 다른 값들은 특별한 뜻을 갖는다. -1의 값이나 WAIT_ANY는 어떤 자식 프로세스를 위한 상황정보를 요청한다; 0의 값이나 WAIT_MYPGRP는 호출된 프로세스와 같은 프로세스 그룹에 있는 어떤 자식 프로세스를 위한 정보를 요청한다; 그리고 다른 음수값 - pgid는 프로세스 그룹 ID로써 pgid를 가진 자식 프로세스를 위한 정보를 요청한다.

만일 자식 프로세스를 위한 상황정보가 즉시 유용한 상태라면, 이 함수는 기다림이 없이 즉시 반환한다. 만일 한 개의 적합한 자식 프로세스보다 많은 프로세스가 유용한 상황 정보를 갖고있다면, 그둘중의 하나가 임의로 선택되고, 그 상황은 즉시 반환된다. 다른 적합한 자식 프로세스로 부터 상황을 얻기 위하여, 당신은 waitpid를 다시 호출할 필요가 있다.

options 인수는 비트마스크이다. 그 값은 0이나 WNOHANG와 WUNTRACED 플래그들이 비트별 OR로 조합되어질 것이다. 당신은 부모 프로세스가 기다리지 않을 것임을 지적하기 위해서 WNOHANG 플래그를 사용할 수 있다; 그리고 WUNTRACED플래그는 종료된 프로세스 뿐만 아니라 멈추어진 프로세스들로 부터 상황 정보를 요청하기 위해서 사용되어진다.

자식 프로세스로 부터의 상황정보는 status_ptr이 널 포인터가 아니라면, status_ptr이 가리키고 있는 오브젝트 안에 저장된다. 반환값은 보통 보고된 상황을 가진 자식 프로세스의 프로세스 ID 가 된다.

만일 WNOHANG 옵션이 지정됐고 어떤 자식 프로세스도 기다리고 있지 않다면, 그 값은 0이된다. 에러가 발생한 경우에 -1을 반환한다. 다음의 errno는 이 함수를 위해서 정의된 에러상황이다.

    EINTR

    그 함수는 호출된 프로세스에게 배달된 시그널에 의해 인터럽트 되어졌다. 21. 5절 [Interrupted Primitives] 참조.

    ECHILD

    그곳에 기다리고 있는 자식 프로세스가 아무 것도 없거나, 정해진 pid가 호출된 프로세스의 자식프로세스가 아니다.

    EINVAL : 올바르지 못한 값이 options 인수로써 공급되었다.

다음 기호 상수들은 waitpid 함수에게 pid 인수를 위한 값으로써 정의되었다.

    WAIT_ANY

    이 상수 매크로는 waitpid가 어느 자식 프로세스에 대한 상황정보를 반환하도록 지정되었다. (값은 -1)

    WAIT_MYPGRP

    이 상수는(값은 0) 호출된 프로세스와 같은 프로세스 그룹 안에 있는 어떤 자식 프로세스에 대한 상황정보를 반환하도록 지정되었다.

다음의 기호 상수들은 waitpid 함수의 options 인수를 위한 플래그로써 정의되었다. 당신은 그들은 비트별-OR 연산을 통해서 조합할 수 있다.

    WNOHANG

    이 플래그는 만일그곳에 준비된 자식 프로세스가 없다면, waitpid가 기다리지 않고 즉시 반환하도록 지정되었다.

    WUNTRACED

    이 플래그는 종료된 것뿐만 아니라 멈추어진 자식 프로세스에 대한 상황을 보고하도록 지정되었다.

함수 : pid_t wait (int *status_ptr)

이 함수는 waitpid의 간소화된 변형이고, 어떤 한 개의 자식 프로세스가 종료될 때까지 기다리는데 사용된다. 호출은:
wait (&status)
는 정확히 다음과 동등하다:
waitpid (-1, &status, 0)

다음은 기다림이 없이, 종료된 모든 자식 프로세스에서 보내온 상황을 얻기 위해서는 waitpid를 어떻게 사용하는지에 대한 예제가 있다. 이 함수는 SIGCHLD 시그널을 위한 핸들러로써 만들어졌는데, 그 시그널은 적어도 한 개의 자식 프로세스가 종료되었음을 알리기위해서 발생 된다.

void
sigchld_handler (int signum)
{
int pid;
int status;
while (1)
{
pid = waitpid (WAIT_ANY, &status, WNOHANG);
if (pid < 0)
{
perror ("waitpid");
break;
}
if (pid == 0)
break;
notice_termination (pid, status);
}
}


23. 7 프로세스 종료 상황들

만일 자식 프로세스의 종료 상황 값(22. 3절 [Program Termination] 참조. )이 0이면, waitpid 또는 wait에 의해 보고된 상황 값 또한 0이다. 당신은 다음의 매크로를 사용해서 반환된 상황값안에 있는 암호화된 정보를 테스트할 수 있다.

매크로 : int WIFEXITED (int status)

이 매크로는 만일 자식 프로세스가 exit 또는 _exit로써 정상적으로 종료되면 0이 아닌 값을 지닌다.

매크로 : int WEXITSTATUS (int status)

만일 status가 참인 상태의 WIFEXITED이면, 이 매크로는 자식프로세스로부터의 종료 상황 값의 하위 8비트를 반환한다. 22. 3. 2절 [Exit Status] 참조.

매크로 : int WIFSIGNALED (int status)

이 매크로는 만일 자식 프로세스가 처리되지 않은 시그널을 받았기 때문에 종료되었다면 0이 아닌 값을 반환한다. 21장 [Signal Handling] 참조.

매크로 : int WTERMSIG (int status)

만일 status가 참인 WIFSEGNALED이면, 이 매크로는 자식 프로세스를 종료시켰던 시그널의 시그널 번호를 반환한다.

매크로 : int WCOREDUMP (int status)

이 매크로는 자식 프로세스가 종료되고 코어를 생성했다면, 0이 아닌 값을 반환한다.

매크로 : int WIFSTOPPED (int status)

이 매크로는 만일 자식 프로세스가 멈추어져있다면 0이 아닌 값을 반환한다.

매크로 : int WSTOPSIG (int status)

만일 status가 참인 WIFSTOPPED 이면, 이 매크로는 자식 프로세스를 멈추게 한 원인이 된 시그널의 시그널 번호를 반환한다.


23. 8 BSD 프로세스 Wait 함수들

GNU 라이브러리는 BSD 유닉스와의 호환성을 위해서 그들과 연관된 기능들을 제공한다. BSD는 int 와 다르게 상황 값을 표현하는 union wait 데이터 타입을 사용한다. 두 개의 표현은 실제로 상호간에 변경 가능하다; 그들은 동일한 비트 패턴을 표현하기 때문이다. WEXITSTATUS 와 같은 매크로를 정의하고 있는 GNU C 라이브러리는 오브젝트의 둘중 한 종류를 선택해서 작업할 것이고, wait 함수는 status_ptr 인수로써 포인터의 한 종류를 받아들이도록 정의된다. 그들 함수들은 `sys/wait. h'에 선언되어 있다.

데이터 타입 : union wait

이 데이터 타입은 프로그램 종료 상황 값들을 표현하고, 다음과 같은 멤버들을 갖는다.

int w_termsig

이 멤버의 값은 매크로 WTERMSIG의 결과와 같다.

int w_coredump

이 맴버의 값은 매크로 WCOREDUMP의 결과와 같다.

int w_retcode

이 맴버의 값은 매크로 WEXITSTATUS의 결과와 같다.

int w_stopsig

이 맴버의 값은 매크로 WSTOPSIG의 결과와 같다.

직접적으로 이 맴버들을 억세스 하는 대신에, 당신은 동등한 매크로 를 사용하도록 하라.

함수: pid_t wait3 (union wait *status_ptr, int options, struct rusage *usage)

usage가 널 포인터라면, wait3는 waitpid(-1, status`ptr, options)와 같다. 만일 usage가 널 포인터가 아니라면, wait3는 *rusage에 있는 자식 프로세스를 위한 사용 형태를 저장한다(그러나 오직 멈추어 있는 것이 아니라, 종료된 자식 프로세스라면. )17. 5절 [Resource Usage] 참조.

함수 : pid_t wait4 (pid_t pid, union wait *status_ptr, int options, struct rusage *usage)

만일 usage가 널 포인터라면, wait4는 waitpid (pid, status`ptr, options)와 같다. 만일 usage가 널 포인터가 아니라면, wait4는 *rusage에 있는 자식 프로세스를 위하여 사용 형태들을 저장한다. (그러나 오직 자식 프로세스는 멈추어 있는 것이 아니라, 종료된 것이다. ) 17. 5절 [Resource Usage] 참조.


23. 9 프로세스 만들기 예제

다음은 내장 시스템과 유사한 함수를 어떻게 만들 것인지를 보여주는 예제 프로그램이다. 그것은 `sh -c command'와 동등한 것을 사용하여 command 인수를 실행한다.

#include <stddef. h>
#include <stdlib. h>
#include <unistd. h>
#include <sys/types. h>
#include <sys/wait. h>
 
/* 쉘 프로그램을 사용해서 command 를 실행한다. */
#define SHELL "/bin/sh"
 
int
my_system (const char *command)
{
int status;
pid_t pid;
 
if ((pid = fork()) == 0) { /* 이것은 자식 프로세스로써 쉘 커맨드를 실행한다. */
execl (SHELL, SHELL, "-c", command, NULL);
_exit (EXIT_FAILURE);
} else if (pid < 0) { /* fork가 실패하였으므로 실패를 보고하라. */
status = -1;
} else { /* 이것은 부모 프로세스로써, 자식이 수행을 완료할 때까지 기다린다. */
if (waitpid (pid, &status, 0) != pid)
status = -1;
}
return status;
}

당신이 이 예제에서 주목해야 할 것이 두 가지 있다. 프로그램에 공급된 첫 번째 argv 인수는 실행시키려는 프로그램의 이름을 표현한다는 것을 기억하라. 그것은 execl의 호출에서, SHELL에게 일단 실행하려는 프로그램의 이름이 주어지고 두 번째로 argv[0]을 위한 값을 공급하는 이유이다.

자식 프로세스에서 execl 호출은 만일 그것이 성공하면 반환하지 않는다. 만일 그것이 실패하면, 당신은 자식 프로세스가 종료되도록 무엇인가를 해야만 한다. 단지 return을 통하여 나쁜 상황 코드만을 반환하는것은 원래의 프로그램을 실행시키던 두개의 프로세스를 그냥 남기게 된다. 대신에, 올바른 처리는 부모 프로세스에게 자식 프로세스가 실패를 보고하는 것이다.

그것을 수행하도록 -exit를 호출하라. exit 대신에 _exit를 사용하는 이유는 stdout와 같은 완전히 버퍼화된 스트림들을 플러쉬 하는 것을 피하기 위함이다. 아마도 데이터를 담고 있는 그들 스트림의 버퍼들은 fork에 의해 부모 프로세스로 부터 복사된 것이고, 데이터는 부모 프로세스에 의해 결국 출력될 것이다. 자식 프로세스에서 호출된 exit는 데이터를 두 번 출력할 것이다. 22. 3. 5절 [Termination Internals] 참조.


목차 이전 : 22. 프로세스의 시동과 종료 다음 : 24. 작업 제어

'Programming > Unix Programing' 카테고리의 다른 글

25. 사용자와 그룹  (0) 2007.12.22
24. Job Control(작업 제어)  (0) 2007.12.22
23. Processes  (0) 2007.12.22
22. 프로세스의 시동과 종료(Process StartUp)  (0) 2007.12.22
21. 신호처리(Signal)  (0) 2007.12.22
19. 지역과 세계화  (0) 2007.12.22
Comment 0 Trackback 0
Top

22. 프로세스의 시동과 종료(Process StartUp)

목차 이전 : 21. 시그널 처리 다음 : 23. 프로세스


22 프로세스의 시동과 종료

프로세스는 시스템 자원들의 할당을 위한 기본적인 동작 단위이다. 각 프로세스는 자신만의 주소 공간(address space)과 제어의 한 쓰레드를 갖는다. 한 프로세스는 한 프로그램을 실행한다; 당신이 같은 프로그램을 실행하는데 여러개의 프로세스를 가질 수 있지만, 각각의 프로세스는 자신의 주소공간에서 자신의 프로그램 복제본을 갖고 다른 프로그램 복사본과 독립적으로 실행된다.

 
** 역자주 : thread(쓰레드) : 프로세스 보다 더 작은 하나의 작업단위.

일반적으로 우리는 하나의 프로세스를 단위로 하여 독립적인 작업을 수행하는 것으로 알고 있지만, 컴퓨터 기술의 발전으로 프로세스라는 단위는 너무나 큰 단위가 되어버리고 이를 대신하여 쓰레드라는 단위가 사용되게 되었다. 쓰레드를 사용하는 운영체제에서는 작업의 스케줄링을 수행할 때 쓰레드를 단위로 하여 스케줄링을 한다.

이 장은 당신의 프로그램에서 프로세스를 시동하고, 그것을 종료시키고, 부모 프로세스로 부터 정보(인수들과 환경)를 받기 위하여 무엇을 해야만 하는지를 설명하고 있다.


22. 1 프로그램 인수들

시스템은 main 함수를 호출함으로써 C 프로그램을 시작한다. 그것은 당신이 프로그램을 만들 때 main 이라 이름지어진 함수를 꼭 만들어야 함을 의미하고_그것을 하지 않았을 때 에러없이 프로그램을 링크할 수 없을 것이다.

main 함수는 아무런 인수를 취하지 않거나, 커멘드 라인에서 받아들이는 인수를 나타내는 두 개의 인수를 취한다. 다음처럼.

    int main (int argc, char *argv[])

커멘드 라인 인수들은 공백으로 분리된 토큰으로써 프로그램을 호출하도록 쉘 커맨드에 주어진다; 그래서, `cat foo bar'라고 했을 때 인수는 `foo' 와 `bar'가 된다. 어떤 프로그램에서 커맨드 라인에 인수를 넣는 방법은 오직 main함수의 인수를 통해서 가능하다. 만일 main 함수가 인수를 취하지 않는다면, 당신은 커맨드 라인에서 인수를 취할 수 없다.

argc 인수는 커맨드 라인 인수들의 개수를 말하고, argv 인수는 C 문자열의 벡터이다; argv의 각 요소들은 공백으로 분리된 개별적인 커맨드 라인 인수 문자열이다. 실행시키려는 프로그램의 파일이름 또한 첫 번째 요소로써 벡터 안에 포함된다. argc의 값은 이 요소들의 개수이다. 널 포인터가 항상 마지막 요소가 된다: 즉, argv[argc]는 널 포인터이다. 커맨드 `cat foo bar' 에서, argc는 3이고, argv는 "cat", "foo", 그리고 "bar" 라는 세 개의 요소들을 갖는다.

만일 당신의 프로그램에서 커맨드 라인 인수들의 구분이 간단하다면, 당신은 직접 argv로부터 간단하게 하나씩 인수를 가져올 수 있다. 당신의 프로그램이 한정된 인수들의 개수를 갖지않거나, 모든 인수들을 한꺼번에 취하지 않는다면 같은 방법으로 인수들을 취할 수 있지만, 그렇지 않다면 그것을 구문분석 하기 위해서 getopt를 사용해서 인수들을 취해야 할 것이다.

 

22. 1. 1 프로그램 인수 구문 관례들

POSIX에서 커맨드 라인 인수들을 위해서 다음과 같은 관례들을 권장하고 있다. getopt (22. 1. 2 [Parsing Options] 참조)는 다음의 권장사항을 쉽게 충족시키도록 만든다.

  • 하이픈(hyphen (`_') ) 구획문자(delimiter)로 시작하는 인수들은 옵션이 있다.
  • 한 개의 토큰에서 한 개의 하이픈 구획문자 뒤에 따르는 것은, 만일 그 옵션들이 인수들을 취하지 않는다면 다중 옵션이 된다. 그래서, `-abc'는 `-a -b -c'와 동등하다.
  • 옵션 이름들은 단일한 알파벳 문자들이다( isalnum에 관하여; 4. 1절[Classification of Characters] 참조)
  • 어떤 옵션들을 인수를 필요로 한다. 예를 들어, ld 명령의 `-o' 옵션은 출력 파일 이름으로 하나의 인수를 필요로 한다.
  • 옵션과 옵션이 필요로 하는 인수들은 토큰으로 분리되어 나타날 수도 있고, 그렇지 않을 수도 있다. (즉, 그들을 분리하는 공백문자는 선택적이다. ) 그래서, `-o foo' 와 `-ofoo'는 동등한다.
  • 옵션들은 전형적으로 다른 비-옵션 인수들에 선행된다.

GNU C 라이브러리에 있는 getopt의 구현은 당신의 프로그램을 사용하는 사용자가 심지어 옵션과 비-옵션 인수들을 혼합했을지라도, 구문분석을 위해서 모든 비-옵션 인수들 전에 옵션 인수들이 나타나도록 만든다.

인수 `--'는 모든 옵션들을 종결시킨다; 다음에 따르는 인수들은 그들이 하이픈으로 시작할지라도, 비-옵션 인수들로 취급된다.

한 개의 하이픈 문자가 들어가 있는 토큰은 보통 비-옵션 인수로써 취급된다. 관례상, 그것은 기본 입력과 출력 스트림으로 입력이나 출력을 정하기 위해서 사용된다.

옵션들은 어떤 순서대로 공급되어지고, 또는 여러번 중복해서 나타나기도 할 것이다. 해석은 특정한 응용 프로그램이 맨 왼쪽에 있다고 해석한다.

GNU 는 그들의 관례에 따라서 긴 옵션을 더한다. 긴 옵션들은 `--'뒤에 따르는 이름으로 구성되는데 그 이름은 영숫자와 대쉬들로 구성된다. 옵션 이름들은 전형적으로 하나에서 세 개 단어의 길이를 갖고, 그들은 하이픈으로 단어들을 분리한다. 사용자들은 옵션을 대표하는 약자가 단일하다면 얼마든지 옵션이름들을 축약할 수 있다. 한 개의 긴 옵션을 위한 인수를 정하기 위해서는, `--name=value'라고 써라. 이 구문은 선택적인 인수를 받아들이는 긴 옵션을 가능하게 한다. GNU 시스템은 쉘에서 긴 옵션 이름을 지원할 것이다.

 

22. 1. 2 프로그램 옵션들을 구문 분석하기

이번에는 getopt 함수를 어떻게 호출하는지에 대해서 자세히 설명한다. 이 기능을 사용하기 위해서, 당신의 프로그램에 헤더파일 `unistd. h'를 포함시켜야만 한다.

변수 : int opterr

만일 이 변수의 값이 0이 아니면, getopt는 만일 알지 못하는 옵션 문자가 들어오거나 인수를 필요로 하는 옵션이 옵션을 갖고있지 않다면 표준 에러 스트림에 에러메세지를 프린트한다. 이것은 디폴트 동작이다. 만일 당신이 이 변수를 0으로 설정하면, getopt는 아무런 메시지도 프린트하지는 않지만, 에러를 지적하기 위해서 여전히 ? 문자를 여전히 반환한다.

변수 : int optopt

getopt가 알려지지 않은 옵션 문자와 만나거나 필요한 인수가 빠져있는 옵션일 때, getopt는 그 옵션 문자를 이 변수에 저장한다. 당신은 자신만의 진단 메시지를 제공하기 이해서 이 변수를 사용할 수 있다.

변수 : int optind

이 변수는 앞으로 분석할 배열 argv의 다음 요소에 대한 인덱스로 getopt에 의해 설정된다. 일단 getopt가 옵션 인수들 모두를 발견했다면, 당신은 나머지 비-옵션 인수들이 어디에서 시작하는지 결정하기 위해서 이 변수를 사용할 수 있다. 이 변수의 초기값은 1이다.

변수 : chat *optarg

이 변수는 인수들을 받아들이는 옵션들을 위해서, 옵션 인수들의 값을 가리키는 포인터로써 getopt에 의해 설정된다.

함수 : int getopt(int argc, char **argv, const char *options)

getopt 함수는 argv와 argc 인수들에 의해 지정된 인수 목록으로부터 다음 옵션 인수를 얻는다. 보통, 그들 값들은 main에 의해 받아들여진 인수들로부터 직접적으로 온다. 옵션 인수는 이 프로그램에서 유용하게 쓰일 옵션 문자들을 지정한 문자열이다. 문자열 안에 있는 옵션 문자는 인수가 필요함을 알리기위해서 콜론(`: ')의 다음에 나타난다.

만일 옵션 인수 문자열이 하이픈(`-')으로 시작한다면, 이것은 특별하게 취급된다. 그들은 옵션 문자가 `\0'인 것처럼 반환되어, 옵션이 없는 인수들을 허용한다. getopt 함수는 다음 커맨드 라인옵션의 옵션 문자를 반환한다. 더 이상 유용한 옵션 인수들이 없을 때, -1을 반환한다. 그곳은 이제 비-옵션 인수들이 있을 것이다; 당신은 이것을 체크하기 위해서 argc 파라미터에 대응하는 외부변수 optind를 비교해야만 한다.

만일 어떤 옵션이 인수를 가진다면, getopt 는 변수들 optarg안에 그것을 저장함으로써 인수를 반환한다. 당신이 덧씌워질지도 모르는 정적 변수가 아닌, 보통의 argv 배열을 가리키는 포인터를 라면, optarg 문자열을 복사할 필요가 없다.

만일 알려지지 않은 옵션문자를 사용하거나, 옵션 인수가 빠져있다면 getopt는 `?'를 반환하고 현재 옵션 문자로 외부변수 optopt를 설정한다. 만일 옵션의 첫 번째 문자가 콜론(`: ') 이라면, getopt는 옵션 인수가 빠져있음을 알리기위해서 `?' 대신에 `: '를 반환한다. 또한, 만일 외부변수 opterr이 0이 아니면(디폴트이다. ), getopt는 에러 메시지를 출력한다.

 

22. 1. 3 getopt를 사용해서 인수를 구문 분석하는 예제

다음은 getopt가 전형적으로 어떻게 사용되는지를 보여주는 예제이다. 핵심은 다음과 같다:

보통, getopt는 루프 안에서 호출된다. getopt가 더 이상 나타난 옵션이 없음을 알리기위해서 -1을 반환 하면, 루프는 종료한다.

switch 구문은 getopt로부터의 반환값에 따른 각각의 처리에 사용된다. 각각의 경우는 프로그램에서 나중에 사용될 변수를 설정하게 된다.

두 번째 루프는 남아있는 비-옵션 인수들을 처리하는데 사용된다.

#include <unistd. h>
#include <stdio. h>
 
int
main (int argc, char **argv)
{
int aflag = 0;
int bflag = 0;
char *cvalue = NULL;
int index;
int c;
 
opterr = 0;
 
while ((c = getopt (argc, argv, "abc: ")) != -1) {
switch (c)
{
case 'a':
aflag = 1;
break;
case 'b':
bflag = 1;
break;
case 'c':
cvalue = optarg;
break;
case '?':
if (isprint (optopt))
fprintf(stderr, "Unknown option `-%c'. \n", optopt);
else
fprintf (stderr,
"Unknown option character `\\x%x'. \n", optopt);
return 1;
default:
abort ();
}
}
printf("aflag = %d, bflag = %d, cvalue = %s\n", aflag, bflag, cvalue);
 
for (index = optind; index < argc; index++)
printf ("Non-option argument %s\n", argv[index]);
return 0;
}
 
다음은 위의 프로그램을 여러 가지 인수들의 조합을 사용했을 때 어떤 결과를 나타내는지에 대한 예이다.
 
% testopt
aflag = 0, bflag = 0, cvalue = (null)
 
% testopt -a -b
aflag = 1, bflag = 1, cvalue = (null)
 
% testopt -ab
aflag = 1, bflag = 1, cvalue = (null)
 
% testopt -c foo
aflag = 0, bflag = 0, cvalue = foo
 
% testopt -cfoo
aflag = 0, bflag = 0, cvalue = foo
 
% testopt arg1
aflag = 0, bflag = 0, cvalue = (null)
Non-option argument arg1
 
% testopt -a arg1
aflag = 1, bflag = 0, cvalue = (null)
Non-option argument arg1
 
% testopt -c foo arg1
aflag = 0, bflag = 0, cvalue = foo
Non-option argument arg1
 
% testopt -a -- -b
aflag = 1, bflag = 0, cvalue = (null)
Non-option argument -b
 
% testopt -a -
aflag = 1, bflag = 0, cvalue = (null)
Non-option argument -

 

22. 1. 4 긴 옵션들을 구문 분석하기

단일한-문자 옵션들로만 되어있는 GNU-스타일의 긴 옵션들을 받아들이기 위해서는 getopt 대신에 getopt_long를 사용한다. 당신은 이런 가외의 작은 작업을 통해서 모든 프로그램이 긴 옵션들을 받아들이게 만들 수 있고, 초보자들에게 어떻게 그 프로그램을 사용하는지 도울 수 있다.

데이터 타입 : struct option

이 구조체는 getopt_long을 위한 단일한 긴 옵션 이름을 설명한다. 긴 옵션을 구성하는 옵션의 하나인 인수 longopts는 그들 구조체의 배열이 되어야만 한다. 그 배열은 0으로 채워진 요소로써 끝난다. struct option 구조체는 다음과 같은 필드를 갖는다.

const char *name

이 필드는 옵션의 이름으로 문자열이다.

int has_arg

이 필드는 옵션이 인수를 취하는지 또는 취하지 않는지의 여부를 알린다. 그것은 정수이고 그곳에 세 개의 합리적인 값들이 있다: no_argument, required argument, 그리고 optional_argument.

int *flag

int val

이 필드들은 그 옵션이 발생할 때 옵션에 대하여 어떻게 보고를 하거나 동작할 것인지를 제어한다. 만일 flag가 널 포인터라면, val은 이 옵션을 대표하는 값이다. 종종 그 값들은 특정한 긴 옵션들의 단일함을 확인하기 위해서 선택되어진다. 만일 플래그가 널 포인터가 아니라면, 그것은 이 옵션을 위한 플래그인 int형 변수의 주소가 되어질 것이다. val안의 값은 나타난 옵션을 지적하도록 플래그 안에 저장하기위한 값이다.

함수 : int getopt__long (int argc, char **argv, const char *shortopts, struct option *longopts, int *indexptr)

벡터 argv ( 길이는 argc이다. )로부터 옵션들을 해독한다. 인수 shortopts는 getopt가 해독할 수 있는 만큼의 짧은 옵션을 나타내고, 인수 longopts는 받아들이기 위한 긴 옵션들을 나타낸다. getopt_long이 짧은 옵션을 만났을 때, 그것은 getopt가 하는것과 같은 일을 한다: 그것은 옵션을 위한 문자 코드를 반환하고, optarg에 옵션 인수들을(만일 그것이한 개를 가진다면) 저장한다.
getopt_long가 긴 옵션을 만났을 때, 그것은 그 옵션을 정의하는 flag 와 val 필드에 기초한 동작들을 취한다.
 
만일 flag가 널 포인터라면, getopt_long 은 발견한 옵션을 지적하도록 val의 내용을 반환한다. 당신이 다른 의미를 가진 옵션을 위해서 val 필드 안에 별개의 값들을 배열하면, 당신은 getopt_long이 반환한 후에 그들 값을 해독할 수 있다. 만일 긴 옵션이 짧은 옵션과 동일하다면, 당신은 짭은 옵션의 문자 코드를 사용할 수 있다.
 
만일 flag가 널 포인터가 아니라면, 단지 프로그램 안에서 flag가 설정된 이 옵션을 의미한다. flag는 당신이 정의한 int형의 변수이다. flag 필드 안에 그 플래그의 주소를 저장하라. flag에 저장하려는 옵션값을 val 필드 안에 저장하라. 이 경우, getopt_long는0을 반환한다.
 
어떤 긴 옵션에서, getopt_long는 배열 longopts안의 옵션 정의에 대한 인덱스를, *indexptr에 저장하여 알린다. 당신은 longopts[*indexptr]. name 어로 옵션의 이름을 얻을 수 있다. 그래서 당신은 그들의 val 필드 안에 있는 값이나, 그들의 인덱스에 의해서 긴 옵션들을 구분할 수 있다. 당신은 플래그를 설정한 긴 옵션들을 이 방법으로 구분할 수 있다.
 
어떤 긴 옵션이 인수를 가질 때, getopt_long는 반환하기 전에 변수 optarg안에 인수값을 저장한다. 어떤 옵션이 아무런 인수를 가지지 않을 때, optarg안의 값은 널 포인터이다. 이것은 공급된 인수가 옵션을 가졌는지 아닌지에 대한 여부를 당신에게 알리는 방법인 것이다. getopt_long는 더 이상 처리할 옵션들이 없을 때, -1을 반환하고, 다음 남아있는 인수의 argv 인덱스를 변수 optind 안에 남긴다.

 

22. 1. 5 긴 옵션들의 구분분석에 대한 예제

#include <stdio. h>
 
/* 플래그는 `--verbose' 에 의해 설정한다. */
static int verbose_flag;
 
int
main (argc, argv)
int argc, char **argv;
{
int c;
 
while (1) {
static struct option long_options[] = {
/* 그들 옵션들은 플래그를 설정한다. */
{"verbose", 0, &verbose_flag, 1},
{"brief", 0, &verbose_flag, 0},
/* 그들 옵션들은 플래그를 설정하지 않는다. 우리는 그들의 인덱스에 의해 그들을 구분한다. */
{"add", 1, 0, 0},
{"append", 0, 0, 0},
{"delete", 1, 0, 0},
{"create", 0, 0, 0},
{"file", 1, 0, 0},
{0, 0, 0, 0}
};
 
/* getopt_long는 이곳에 옵션 인덱스를 저장한다. */
int option_index = 0;
 
c = getopt_long(argc, argv, "abc: d: ", long_options, &option_index);
 
/* 옵션들의 끝을 검출한다. */
if (c == -1)
break;
 
switch (c) {
case 0:
/* 만일 이 옵션이 플래그를 설정하면, 지금 아무런 일도 하지 말아라. */
if (long_options[option_index]. flag != 0)
break;
printf("option %s", long_options[option_index]. name);
if (optarg)
printf(" with arg %s", optarg);
printf("\n");
break;
case 'a':
puts("option -a\n");
break;
case 'b':
puts("option -b\n");
break;
case 'c':
printf("option -c with value `%s'\n", optarg);
break;
case 'd':
printf("option -d with value `%s'\n", optarg);
break;
case '?':
/* getopt_long는 이미 에러 메시지를 프린트했다. */
break;
default:
abort ();
} /* switch 의 끝*/
} /* while 의 끝 */
 
/* 그들이 맞추었던 것으로 `--verbose'와 `--brief'를 보고하는 대신에, 우리는 그들로부터 나온 결과로 마지막 상황을 보고한다. */
if (verbose_flag)
puts ("verbose flag is set");
 
/* 남아있는 코맨드 라인 인수들을( 옵션이 없는) 출력하라. */
if (optind < argc) {
printf ("non-option ARGV-elements: ");
while (optind < argc)
printf ("%s ", argv[optind++]);
putchar ('\n');
}
 
exit (0);
}


22. 2 환경 변수들

프로그램이 실행될 때, 두 가지 방법으로, 실행될 문맥(context)에 대한 정보를 받는다. 첫 번째 방법은 argv와 argc 인수를 메인 함수에서 사용하는 것으로써 이것에 대한 것은 22. 1절 [Program Arguments] 에서 설명하였다. 두 번째 방법은 환경변수를 사용하는 것인데 지금부터 설명할 것이다. argv를 사용하는 방법은 실행될 특정한 프로그램에 지정된 커맨드 라인 인수를 주기 위해서 전형적으로 사용된다. 다른 방법으로, 환경은, 드물게 변경되고 덜 빈번하게 억세스 되는, 많은 프로그램에 의해 분배될 정보를 기억한다.

이 절에서 논의되고 있는 환경변수들은 당신이 쉘에게 커맨드를 보내고 사용될 것을 배정하는 환경변수와 같은 것이다. 쉘로부터 실행된 프로그램들은 쉘로부터 환경변수 모두를 상속받는다. 표준 환경 변수들은 사용자의 홈 디렉토리, 터미널 타입, 현재 지역 등등에 대한 정보를 위해서 사용된다; 당신은 다른 목적으로 부가적인 환경변수를 정의할 수 있다. 모든 환경변수들의 가진 값들의 집합은 집합적으로 환경으로써 알려진다.

환경변수들의 이름은 대소문자를 구분하고 문자 `='를 포함해서는 안된다. 시스템-정의 환경변수들은 항상 대문자이다. 환경변수들의 값은 문자열로써 표현될 수 있는 어떤 것도 가능하다. 값에는 널 문자가 포함 되서는 안된다, 왜냐하면 이것은 문자열의 끝으로 간주되기 때문이다.

 

22. 2. 1 환경 검색

환경변수의 값은 getenv 함수를 통해서 억세스될 수 있다. 이것은 헤더 파일 `stdlib. h'에 선언되어 있다.

함수 : char *getenv (const char name)

이 함수는 환경변수 name의 값인 문자열을 반환한다. 당신은 이 문자열을 갱신할 수 없다. 어떤 GNU 라이브러리를 사용하지 않는 비-유닉스 시스템들은 getenv 함수의 연속적인 호출에 의해서 덮어 쓰여질지 모른다(다른 라이브러리 함수에 의하지 않는다. ) 만일 환경변수 name이 정의되지 않는다면, 그 값은 널 포인터이다.

함수 : int putenv (const char *string)

putenv 함수는 환경으로부터 정의를 더하거나 제한다. 만일 string이 `name=value'의 형식이라면, 그 정의는 환경에 더해진다. 그렇지 않다면, 그 string은 환경변수의 이름으로써 해석되고, 환경 안에서 이 변수에 대한 정의가 제거된다. GNU 라이브러리는 SVID와의 호환성을 위해서 이 함수를 제공하고 있고; 다른 시스템에서 이 함수 는 유용하지 않다.
당신은 환경에 더 많은 변수들을 더하기 위해서 환경 오브젝트의 내재된 표현을 직접적으로 다룰 수 있다( 예로, 당신이 실행하려는 것에 대해서 다른 프로그램으로 통신하기 위해서; 23. 5절 [Executing a File] 참조).

변수 : chat **environ

환경은 문자열의 배열로써 표현된다. 각 문자열은 `name=value'의 형식을 갖는다. 환경 안에 나타난 문자열의 순서는 중요하지 않지만, 같은 이름이 한번 이상 나타나서는 결코 안된다. 배열의 마지막 요소는 널 포인터이다. 이 변수는 헤더파일 `unistd. h'에 선언되어 있다. 만일 당신이 환경변수의 값을 얻기 원한다면 getenv를 사용하라.

 

22. 2. 2 표준 환경 변수들

그들 환경변수들은 표준 의미를 갖는다. 이것은 그들이 항상 환경 안에서만 나타남을 의미하지는 않는다; 그러나 그 변수들이 어디에서든 나타난다고 해도 그들은 그들만의 의미를 갖기 때문에 당신이 다른 목적으로 환경변수 이름을 사용하려고 시도해서는 안된다.

HOME

이것은 사용자의 홈 디렉토리나, 초기 디폴트 작업 디렉토리를 나타내는 문자열이다. 사용자는 어떤 값으로 HOME를 설정할 수 있다. 만일 당신이 어떤 특정한 사용자를 위해서 적당한 홈 디렉토리를 부여하기를 원한다면, HOME을 사용할 수 없고; 대신에, 사용자 데이터 베이스에서 사용자의 이름을 찾아보라 ( 25. 12절 [User Database] 참조). 대부분의 목적에서는, HOME을 사용하는 것이 좋다, 왜냐하면 정확하게 사용자가 그 값을 정하도록 허용하기 때문이다.

LOGNAME

이것은 로그 인 하기 위해서 사용되는 사용자 이름이다. 환경에서 이 값은 제멋대로 조정될 수 있기 때문에, 이것은 프로세스를 실행시키고 있는 사용자를 구분하기 위한 신뢰 가능한 방법이 아니다; getlogin과 같은 함수(25. 11 [Who Logged In] 참조)는 그러한 목적을 위해서는 오히려 더 낫다. 대부분의 목적을 위해서는 LOGNAME이 사용자가 그 값을 지정하도록 하기 때문에 사용하기 더 좋다.

PATH

경로는 파일을 찾기 위해서 사용되는 디렉토리 이름들의 열(sequence) 이다. 변수 PATH는 실행시키려는 프로그램을 찾기 위해서 사용되는 경로는 저장하고 있다. execlp와 execvp 함수들은 (23. 5절 [Executing a File] 참조) 그들 함수들에 의하여 실행된 쉘과 다른 유틸리티에서, 이 환경변수를 사용한다. 경로의 구분은 콜론으로 분리된 디렉토리 이름들의 순차열(sequence) 이다. 디렉토리 이름 대신에 빈 문자열은 현재의 디렉토리를 나타낸다( 9. 1절 [Working Directory] 참조).
 
이 환경변수의 전형적인 값은 다음과 같은 문자열이 될 것이다.
: /bin: /etc: /usr/bin: /usr/new/X11: /usr/new: /usr/local/bin

TERM

이것은 프로그램 출력을 받아들이는 터미널의 종류를 지정한다. 어떤 프로그램들은 터미널의 특정한 종류에 의해 지원되는 특별한 이스케이프 시퀀스 또는 터미널모드들을 이용하기 위해 이 정보를 사용하게 만들 수 있다. termcap 라이브러리( Termcap 라이브러리 매뉴얼에서 "Finding a Terminal Description" 절을 참고)를 사용하는 많은 프로그램들은 TERM 환경변수를 사용한다.

TZ

이것은 시간대(time zone)를 지정한다. 이 문자열의 형식과 어떻게 그것을 사용하는지에 대한 자세한 정보는 17. 2. 5절 [TZ Variable] 참조.

LANG

속성 범주를 사용하도록 디폴트 지역을 지정한다.

LC_ALL

범주를 위한 환경변수가 아니면 설정한다. 지역에 대한 자세한 정보를 19장 [Locales] 참조.

LC_COLLATE

이것은 문자열 정렬을 위해서 어떤 지역을 지정한다.

LC_CTYPE

이것은 문자 집합(sets)과 문자 분류를 위해 사용하도록 어떤 지역을 지정한다.

LC_MONETARY

이것은 형식화된 통화량 값을 사용하기 위한 어떤 지역을 지정한다.

LC_NUMERIC

이것은 형식화된 숫자들을 사용하기 위한 어떤 지역을 지정한다.

LC_TIME

이것은 형식화된 날짜/시간값들을 위해서 어떤 지역을 지정한다.

_POSIX_OPTION_ORDER

만일 이 환경변수가 정의되었다면, 그것은 getopt에 의한 커맨드 라인 인수의 재정리를 금지한다. 22. 1. 1절 [Argument Syntax] 참조)


22. 3 프로그램 종료

프로그램을 종료시키는 평상적인 방법은 메인 함수가 반환하도록 하는 것이다. 메인 함수로부터 반환되는 분기(exit) 상황 값은 프로세스의 부모 프로세스나 쉘에게 정보를 보고하기 위해서 사용된다. 프로그램은 exit 함수를 호출함으로써 또한 종료할 수 있다. 또한, 프로그램은 시그널들에 의해서 종료될 수 있다; 이것은 21장 [Signal Handling] 에 자세하게 설명되어 있다. abort함수는 프로그램을 죽이는(kill) 시그널을 발생시킨다.

 

22. 3. 1 보통의 종료

프로세스는 프로그램이 exit를 호출할 때 보통 종료된다. main 함수로부터의 반환은 exit 를 호출하는것과 같고, main이 반환한 값은 exit에 인수로써 사용된다.

함수 : void exit (int status)

exit 함수는 상황 status로 프로세스를 종료한다. 이 함수는 반환하지 않는다.
보통의 종료는 다음과 같은 동작들의 원인이 된다:
 
1. atexit 나 on_exit 함수들에 등록된 함수들은 등록의 역순으로 호출되어진다. 이 메커니즘은 프로그램이 종료할 때 수행되어지는 자신만의 "cleanup" 동작들을 지정하도록 당신의 응용프로그램에게 허용한다. 전형적으로, 이것은 파일에 프로그램 상황 정보를 저장하거나, 공유 데이터베이스에서 락들을 해제하는것과 같은 일들을 하는데 사용된다.
 
2. 버퍼된 출력 데이터가 모두 기록되어, 개방된 모든 스트림이 폐쇄되었다. 7. 4절 [Closing Streams] 참조. 또한, tmpfile 함수에의해 개방되었던 임시 파일들은 제거된다; 9. 10절 [Temporary Files] 참조.
 
3. 프로그램을 종료시키도록 _exit가 호출되었다. 22. 3. 5절 [Termination Internals] 참조.

 

22. 3. 2 Exit 상황들

프로그램이 종료할 때, 그것은 분기 상황(exit status)을 사용해서, 종료의 원인에 대한 작은 양의 정보를 부모 프로세스에게 반환할 수 있다. 이것은 현존하는 프로세스가 exit에게 인수로써 주는 0에서 255사이의 값이다. 보통 당신은 성공이나 실패에 대한 매우 광범위한 정보를 보고하기 위해서 exit 상황을 사용할 것이다. 당신은 실패의 이유에 대해서 매우 자세하게는 정보를 줄 수는 없고, 대부분의 부모 프로세스도 더 이상 세밀한 정보를 원하지 않을 것이다.

어떤 프로그램이 반환하는 값은 어떤 부류의 상황 값에 속한다. 대부분의 관습은 성공하면 0이고 실패하면 1이다. 그러나 비교를 수행하는 프로그램은 다른 관습을 사용한다: 매치되지 않았음을 지적하기 위해서 1을 사용하고 비교 불가능임을 지적하기 위해서 2를 사용한다. 당신의 프로그램이 그것을 위한 종료관습을 이해하려면 종료관습을 따라야한다.

일반적인 관습은 상황 값으로 128개를 예약하고 특별한 목적을 위해서 확장한다. 특별하게, 128개의 값은 서브프로세스에서 다른 프로그램에게 실패를 지적하기 위해서 사용된다. 이 관습은 보편적으로 제공되지는 않지만, 당신의 프로그램이 그것을 따르도록 하는 것이 좋다.

 
주의 : 분기상황으로써 에러들의 개수를 사용하려고 시도하지 말아라.

이것은 실제로 매우 유용하지 않다; 부모 프로세스는 얼마나 많은 에러들이 발생했는지에 주의를 기울이지 않는다. 그것보다 더 심각한 경우로는, 상황 값이 8비트에서 짤리기 때문에 만일 프로그램에서 256개의 에러를 보고한다면, 부모 프로세스는 0개의 에러가 난 것으로 보고를 받을 것이고_결국 성공한 것으로 되어버린다. 같은 이유로, 255개를 초과할 수 있기 때문에 종료의 상황으로써 에러의 개수를 사용하여 작업하지 말아라.

 
이식성 노트: 어떤 비-POSIX 시스템들은 종료 상황 값들을 위해서 다른 관습을 사용한다. 좀더 좋은 이식성을 위해서는, 성공과 실패를 위한 상황 값으로 매크로 EXIT_SUCCESS 와 EXIT_FAILURE 를 사용할 수 있다. 그들은 헤더파일 `stdlib. h'에 선언되어 있다.

매크로 : int EXIT__SUCCESS

이 매크로는 성공적인 프로그램 수행을 알리기위해서 exit 함수와 함께 사용될 수 있다. POSIX 시스템에서, 이 매크로의 값은 0이고, 다른 시스템에서 이 값은 어떤 다른 정수 표현( 아마도 비-상수 )이 될 것이다.

매크로 : int EXIT__FAILURE

이 매크로는 비성공적인 프로그램 수행을 알리기위해서 exit 함수와 함께 사용될 수 있다. POSIX 시스템에서, 이 매크로의 값은 1이다. 다른 시스템들에서 그 값은 어떤 다른 정수 표현( 아마도 비-상수 )이 될 것이다. 다른 0이 아닌 상황 값들은 앞으로의 일을 알린다. 어떤 프로그램들은 특정한 종류의 "비-성공"을 알리기위해서 다른 0이 아닌 상황 값들을 사용한다. 예를 들어, diff는 파일들이 서로 다름을 의미할 때 상황 값 1을 사용하고, 파일을 개방하기가 어렵다면 2 또는 그 이상의 값을 사용한다.

 

22. 3. 3 종료시의 상황정리

당신의 프로그램에서 보통의 종료가 발생하면 그 자신만의 정리(cleanup) 함수를 실행하여 정돈할 수 있다. 만일 당신이 여러 개의 응용 프로그램에서 한 개의 라이브러리를 사용하고 있을 때, 모든 응용프로그램이 종료되기전에 라이브러리의 정리(cleanup) 함수들을 명시적으로 호출한다면 신뢰성이 없다. atexit 또는 on_exit를 사용해서 라이브러리 그 자체에서 정리 함수를 설정함으로써, 응용 프로그램에게는 보이지 않는 정리(cleanup)함수를 만드는 것이 더 신뢰성 있는 프로그램이 된다.

함수 : int atexit (void (*function) (void))

atexit 함수는 프로그램이 정상적으로 종료하면 호출되도록 함수 function을 등록한다. 그 function은 아무런 인수가 없이 호출된다. atexit로부터의 반환값은 성공하면 0이고 만일 그 함수가 등록될 수 없으면 0이 아닌 값을 반환한다.

함수 : int on__exit (void (*function)(int status, void *arg), void *arg)

이 함수는 atexit 보다는 좀더 다양함을 구사할 수 있는 것이다. 이 함수는 두 개의 인수로써 함수 function과 포인터 arg를 받아들인다. 정상적이 프로그램 종료시하다, 그 function은 두 개의 인수들, 즉 exit에 주었던 상황 값과 arg와 함께 호출된다. 이 함수는 SunOS와의 호환성을 위해 GNU C 라이브러리에 포함되었으며, 다른 시스템에서는 지원되지 않을 것이다.
 
다음은 exit 와 atexit 의사용을 설명하는 프로그램 예제이다.
 
#include <stdio. h>
#include <stdlib. h>
 
void bye (void)
{
puts ("Goodbye, cruel world. . . . ");
}
 
int main (void)
{
atexit (bye);
exit (EXIT_SUCCESS);
}
 
이 프로그램이 실행됐을 때, 단지 메시지를 프린트하고 종료한다.

 

22. 3. 4 프로그램 중지시키기

당신은 abort 함수를 사용하여 당신의 프로그램을 중지시킬 수 있다. 이 함수를 위한 프로토타입은 `stdlib. h'에 있다.

함수 : void abort (void)

abort 함수는 비정상적으로 프로그램을 종료시킨다. 이것은 atexit 또는 on_exit로 등록된 정리(cleanup)함수들을 실행하지 않는다. 이 함수는 SIGABRT 시그널을 발생시킴으로써 프로세스를 종료시키고, 당신의 프로그램은 이 시그널을 가로채서 처리하는 핸들러를 포함할 수 있다.

 

22. 3. 5 내부적 종료

_exit 함수는 exit에 의해서 프로세스를 종료하기 위해서 사용되는 기본 동작이다. 이것은 `unistd. h'에 선언되어 있다.

함수 : void __exit (int status)

_exit 함수는 상황 status로 프로세스가 종료되도록 하는 기본 동작(primitives)이다. 이 함수를 호출하는 것은 atexit 또는 on_exit에 등록된 정리(cleanup) 함수들이 실행되지 않는다.

명백히 종료를 호출하거나, 또는 시그널의 결과에 의한 종료이거나_어떤 이유에 의해서 프로세스가 종료될 때, 다음과 같은 일이 발생한다.

프로세스에서 모든 개방된 파일 기술자들은 폐쇄된다. 8장 [Low-Level I/O] 참조.

종료 상황 코드의 하위 8비트는 wait 나 waitpid를거쳐서 부모 프로세스에게 보고되도록 저장된다; 23. 6절 [Process Completion] 참조.

종료된 프로세스의 어느 자식 프로세스들은 새로운 부모 프로세스가 할당된다. (이것은 프로세스 ID 1로서, 처음의 프로세스이다. )

SIGCHLD 시그널은 부로 프로세스에게 보내어진다.

만일 그 프로세스가 제어중인 터미널을 가진 세션 리더(session leader)라면, SIGHUP 시그널은 전면 작업에 있는 각각의 프로세스에게 보내어 지고, 제어중인 터미널은 세션으로부터 분열된다. 24장 [Job Control] 참조.

어떤 프로세스의 종료가 프로세스 그룹을 고아가 되도록 하고, 프로세스 그룹의 어떤 멤버를 멈추도록 하는 원인이 된다면, SIGHUP 시그널과 SIGCONT 시그널이 그룹에 있는 각각의 프로세스에게 보내어진다. 24장 [Job Control] 참조.


목차 이전 : 21. 시그널 처리 다음 : 23. 프로세스

'Programming > Unix Programing' 카테고리의 다른 글

24. Job Control(작업 제어)  (0) 2007.12.22
23. Processes  (0) 2007.12.22
22. 프로세스의 시동과 종료(Process StartUp)  (0) 2007.12.22
21. 신호처리(Signal)  (0) 2007.12.22
19. 지역과 세계화  (0) 2007.12.22
18. 확장된 문자  (0) 2007.12.22
Comment 0 Trackback 0
Top

21. 신호처리(Signal)

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


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. 프로세스의 시동과 종료

'Programming > Unix Programing' 카테고리의 다른 글

23. Processes  (0) 2007.12.22
22. 프로세스의 시동과 종료(Process StartUp)  (0) 2007.12.22
21. 신호처리(Signal)  (0) 2007.12.22
19. 지역과 세계화  (0) 2007.12.22
18. 확장된 문자  (0) 2007.12.22
17. 날짜와 시간  (0) 2007.12.22
Comment 0 Trackback 0
Top

prev 1 2 3 next