불행히도, 원형은 같은 유혀의 두 개의 인수를 교환할 때는 호출 버그를 알려주지 않는다. 예를 들어, memchr 함수가 다음과 같은 원형을 갖는다면,

 

void * memchr(const void *pv, int ch, int size);

 

문자(char)와 크기(size) 인수를 교환할 수 있고 컴파일러는 경고를 내지 않을 것이다. 그러나 인터페이스와 원형에 보다 정확한 유형(type)을 사용함으로써, 원형이 제공하는 에러 검사 기능을 강화할 수 있다. 예를 들어, 아래와 같은 원형은 문자와 크기 인수를 바꿔 기입한다면, 버그에 대한 경고를 내보낼 것이다.

 

void * emmchr(const void *pv, unsigned char ch, size_t size);

 

보다 정확한 유형을 사용하는데 있어서의 단점은, 유형 불일치(type-mismatch) 경고가 나오지 않게 하기 위해 인수를 정확한 유형으로 자주 배정해야 한다는 것이다.

'c or linux' 카테고리의 다른 글

assert의 응용  (0) 2005.03.09
함수의 인수를 확인하기 위해 assertion을 사용한다  (0) 2005.03.09
배열 포인터  (0) 2005.03.09
void 형 포인터와 널 포인터  (0) 2005.03.09
포인터의 포인터  (0) 2005.03.09
Posted by '김용환'
,

배열 포인터

c or linux 2005. 3. 9. 01:34

배열 포인터란 배열의 주소를 가리키는 포인터. 배열 포인터는 함수의 인자가 메모리가 큰 배열일 경우 유용하게 사용될 수 있다. 배열 포인터는 다음과 같이 선언된다.

데이터 형 (*포인터명)[크기];

 

예를 들어서

int (*array_pointer)[2];

위의 예제가 의미하는 것은 크기가 2인 배열을 가리키는 포인터이다. 포인터의 연산에서 증가 연산을 하면 int형의 2배 크기(4byte)만큼 주소 값이 증가한다.

 

예제

 1  #include <stdio.h>
 2
 3  int main()
 4  {
 5      int i, j;
 6      int array[3][2] = {5,10,6,12,7,14};
 7      int (*pointer_array)[2];
 8
 9      for(j=0;j<3;j++)
10          for(i=0;i<2;i++)
11              printf("array[%d][%d]=%d\n",j,i,array[j][i]);
12
13      pointer_array=array;
14
15      for(j=0;j<3;j++){
16          for(i=0;i<2;i++)
17              printf("pointer_array[%d]=%d\n",i,(*pointer_array)[i]+1);
18          pointer_array++;
19      }
20
21      return 0;
22  }
23

결과

 


 
배열 포인터를 사용하는 방법은 여러 가지이지만 위의 예에서는 배열의 주소를 배열 포인터에 대입하였다. 배열은 3X2의 2차원 행렬이나 배열 포인터는 크기 2의 1차원 배열 형태이다. 포인터 연산자를 이용하여 배열 포인터를 증가함으로써 3*2의 각 요소를 접근할 수 있도록 되어있음을 알 수 있다.
Posted by '김용환'
,
void 형 포인터와 널 포인터 | c Note 2004/09/06 21:49
http://blog.naver.com/bravedog/100005562577

void 포인터란 데이터 형이 정해지지 않은 포인터를 말한다. 프로그램을 만드는 경우 필요에 따라서  여러 타입의 데이터형을 일괄적으로 처리할 일이 발생한다. 하지만 각각의 데이터 형마다 포인터를 만든다면 메모리의 낭비도 발생할 뿐만 아니라 소스가 엄청나게 길어진다. 이러한 문제를 해결하기 위한 포인터가 바로 void 포인터이다. void 포인터는 개게가 존재하는 위치와 타입만 알면 하나의 포인터로 여러 데이터 형을 처리할 수 있다.

 

void 포인터는 타입캐스팅이 없이는 참조할 수가 없다. 그 이유는 포인터가 가리키는 객체의 크기를 컴파일러가 결정할 수 없기 때문이다. 또한 void 포인터는 포인터 연산을 할수가 없다. 일반적으로 void 포인터는 메모리에 직접 접근할 대 사용한다.

 

다음 예제를 보고 void 포인터의 사용법을 알아보자.

 1  #include <stdio.h>
 2
 3  int main()
 4  {
 5      int x = 3;
 6      float f = 0.0f;
 7      void *p;            // void 형 포인터 선언
 8
 9      // 기존 값 출력
10      printf("x = %d, f = %f\n",x,f);
11
12      p = &x;             // x를 가리킨다
13      *(int*)p = 2;
14      p = &f;             // f를 가리킨다
15      *(float*)p=1.5f;
16
17      // 변경 값 출력
18      printf("x = %d, f = %f\n",x,f);
19
20      return 0;
21  }

 

결과화면

 

 

 

 

 

 

 

 

 

위의 예제에서 void 형 포인터 변수 p를 이용하여 각각의 변수에 값을 대입할 경우 타입캐스팅이 일어났음을 확인할 수 있다. 주의할 점은 void 형 포인터 변수 p도 포인터이기 때문에 타입캐스팅을 할 경우 데이터 형 뒤에 반드시 '*'을 붙이기 바란다.

 

 

한편, 포인터 중에는 아무 것도 가리키지 않는 NULL 포인터라는 것이 있다. NULL 포인터는 포인터의 초기화나 비교, 리턴값으로 많이 사용한다. 그럼 NULL포인터의 쓰임새를 잠시 살펴보자.

 

첫 번째로 NULL 포인터는 포인터의  초기화를 위해서 많이 사용된다. 예를 들어 나중에 사용할 포인터이지만 현재는 아무 것도 가리키지 않고 있다는 의미를 주기 위해서 NULL포인터로 초기화한다

 

int *pLater = NULL;

 

두 번재로 NULL 포인터는 현재 포인터가 가리키는 곳이 있는지 없는지를 확인하기 위해서 비교의 용도로 많이 사용된다. 따라서 어딘가 가리키는 곳이 있다면 그 포인터는 NULL이 될 수가 없겠지!?

 

if(pLater == NULL){

    // 가리키는 곳이 없으므로 포인터 사용불가

}

else {

    // 가리키는 곳이 있으므로 포인터 사용가능

}

 

이러한 비교식에서 C언어는 편리성을 위해 다음과 같이 간단한 표현식을 사용할 수 있다.

if(!pLater){}

위의 표현은 부정 연산자(!)를 사용한 것인데 '포인터 형 변수 pLater가 가리키는 곳이 없으면'라는 의미를 가진다. 'NULL 포인터와 같다'는 의미와 상통한다.

 

세 번째로 NULL 포인터는 메모리가 할당되지 않았음을 의미한다. 예를 들어 동적 메모리를 생성하는 malloc함수는 메모리 생성에 실패하면 NULL포인터를 리턴한다. 만약 NULL포인터를 리턴한다면 메모리가 생성되지 않았으므로 사용하면 안되겠지?

 

// 메모리 할당

pLater = (int*)malloc(10*sizeof(int));

if(pLater == NULL){

    // 메모리가 생성되지 않았으므로 처리

}

 

이렇게 NULL 포인터가 사용되는 용도는 크게 세 가지 정도로 정리할 수 있다.

 

그럼 좀 더 나아가 NULL포인터의 내부로 들어가 보자.

 

#define NULL 0

 

NULL 포인터가 0으로 정의되어 있지만 반드시 0이지는 않다. 환경에 따라서 NULL 포인터는 다른 값을 수도 있다. 그렇지만 공통적으로  NULL이란 매크로를 사용한다는 것을 기억해 두기 바란다. 개인적으로는 NULL이란 매크로를 사용할 것을 추천한다.

 

 

tip

포인터를 이용하여 프로그램을 하다보면 실행중에 다음과 같은 문장을 종종 발견할 수 있다.

null pointer assignment

이 메시지는 초기화하지 않은 포인터를 사용하여 잘못된 위치에 데이터를 쓰려고 할 때 발생한다. 이러한 경우는 초기화하지 않고 사용하는 포인터가 있는지 확인해 보기 바란다.

'c or linux' 카테고리의 다른 글

원형(prototype) 강화하기  (0) 2005.03.09
배열 포인터  (0) 2005.03.09
포인터의 포인터  (0) 2005.03.09
동적 메모리 - 메모리 할당  (0) 2005.03.09
동적 메모리 - 메모리 크기 변경  (0) 2005.03.09
Posted by '김용환'
,
포인터의 포인터 | c Note 2004/09/06 21:27
http://blog.naver.com/bravedog/100005561904

포인터의 포인터는 보통 이중 포인터라고 부른다. 이중 포인터는 현재 가리키는 주소가 변수의 주소가 아니라 다른 변수를 가리키는 포인터의 주소를 가리킨다. 이중 포인터를 주로 사용하는 경우는 2차원 구조를 가지는 메모리의 주소를 가리킬 대이다.

 

데이터 형 **포인터변수명;

 

이중 포인터를 사용하여 가리키는 주소의 값을 알고 싶은 경우에는 포인터 형 변수 앞에 '*'를, 가리키는 주소의 포인터가 다시 가리키는 주소의 값을 알고 싶은 경우에는 '**'를 사용한다. 이해를 돕기 위해서 프로그램을 통해 설명해 보도록 하겠다.

 

 1  #include <stdio.h>
 2
 3  int main()
 4  {
 5      int num =24;
 6      int *p;
 7      int **pp;
 8
 9      // 포인터 설정
10      p = &num;
11      pp =&p;
12
13      // 1차원 포인터
14      printf("*p = %d\n",*p);
15      printf("p = %p\n",p);
16      printf("&p = %p\n",&p);
17
18      // 2차원 포인터
19      printf("pp = %p\n",pp);
20      printf("*pp = %p\n",*pp);
21      printf("**pp = %d\n",**pp);
22
23      return 0;
24  }
25

 

결과 화면

 

 

 

 

 

 

 

 

 

 

 

 

실행 결과를 살펴보면 이중 포인터 형 변수 pp는 변수 num을 가리키는 포인터 형 변수 p의 주소를 가리키고 있음을 알 수 있다. 따라서 *pp는 p의 주소값이 되고, *p는 num변수의 주소가 된다. 또한 이중 포인터를 이용해 num변수를 바로 접근하기 위해서 **pp를 사용했음을 기억해 두자!

Posted by '김용환'
,
동적 메모리 - 메모리 할당 | c Note 2004/09/07 12:21
http://blog.naver.com/bravedog/100005577911

베열을 이용한 메모리 할당은 프로그램이 실행되기 전부터 이미 메모리의 크기가 정해져 있다. 하지만 필요에 따라 실시가능로 필요한 메모리의 크기를 설정하여 할당해야 할 경우가 있다. 이렇나 경우 피룡한 것이 동적 메모리 할당이다. 동적 메모리 할당은 메모리 할당 함수 malloc 또는 calloc을 이용하여 메모리를 할당하고 해당 메모리에 접근하기 위해서 포인터를 사용한다. 메모리의 크기를 변경하기 위해서는 realloc함수를 사용하고, 마지막으로 할당한 메모리를 제거하기 위해서는 free함수를 사용한다.

 

단 동적 메모리 할당 시에 주의할 점은 사용이 끝난 메모리는 반드시 free함수를 통하여 해제시켜 주어야 한다는 것이다. 그렇지 않으면 프로그램이 종료한 후에도 메모리가 해제되지 않아 사용 가능한 메모리 공간이 줄어들게 된다. 최악의 경우 메모리 부족으로 시스템 오류가 발생할 수도 있다.

 

메모리 할당

 

메모리 할당 함수 malloc과 calloc에 관해서 살펴보자. amlloc과 calloc은 alloc.h또는 stdlib.h에 선언되어 있다. 메모리를 할당한다는 측면에서 두 함수는 유사하나 malloc은 할당된 메모리의 초기화를 하지 않는 반면 calloc은 할당된 메모리를 0으로 초기화를 하는 차이점이 있다.

 

먼저 malloc을 살펴보자. malloc함수의 원형은 다음과 같다.

 

void *malloc(size_t size);

 

malloc은 메모리 힙으로부터 size 바이트 크기의 메모리를 할당하고 그 시작번지의 포인터를 리턴하는 함수이다. 메모리 할당에 성공하였을 경우 할당된 주소의 void 형 포인터가 리턴되며 실패한 경우에는 NULL포인터가 리턴된다.

 

이렇게 리턴된 void 형 포인터를 접근하기 위해서는 앞서 void 형 포인터에서 배운 바와 같이 타입캐스팅이 필요할 수도 있다. 실제로 에전의 C언어는 타입캐스팅이 없으면 경고 메시지를 보내었다. 하지만 현재 ANSI C표준에 의하면 타입캐스팅이 있던 없던 경고를 띄우지 않는다. 따라서 타입캐스팅이 없이 바로 사용할 수 있다.

 

 1  #include <stdio.h>
 2  #include <stdlib.h>         // alloc.h를 써도 무관
 3
 4  int main()
 5  {
 6      int i;
 7      size_t *pEven;
 8
 9      // int 형(2byte) * 10개 = 20 byte
10      pEven = (size_t*)malloc(100);
11
12      // 메모리 할당에 실패했을 경우 프로그램 종료
13      if(!pEven)
14          return EXIT_FAILURE;
15      for(i=0;i<10;i++)
16          pEven[i] = i+1;     // 자연수는 첨자보다 1크다
17
18      printf("짝수 : ");
19      for(i=0;i<10;i++)
20          // 짝수 체크
21          if(pEven[i]%2==0)
22              printf("%d ",pEven[i]);
23      printf("\n");
24
25      // 메모리 해제(반드시)
26      free(pEven);
27
28      return EXIT_SUCCESS;
29  }
30
31  /*---------------------------------------------------------
32   * malloc을 이용하여 메모리를 할당 받을 때에는 필요로 하는
33   * 메모리의 크기를 정확히 알아야 한다.
34   * 하지만 위의 예와 같이 특정 데이터 형을 특정 개수만큼
35   * 받는다면 데이터 형의 크기를 알 수 있는 sizeof연산자를
36   * 사용하여 다음과 같이 사용할 수도 있다.
37   *
38   * int형(2byte) * 10개 = 20byte
39   * pEven = (int*)malloc(10*sizeof(int));
40   *
41   * 동적 메모리 할당에 있어 주의해야 할 점이 또 하나 있다.
42   * 그것은 바로 메모리 할당이 실패하는 경우이다.
43   *
44   * 메모리 할당에 실패했을 경우 프로그램 종료
45   *  if(!pEven)
46   *          return EXIT_FAILURE;
47   *
48   *  만일 메모리 할당에 실패했을 경우 처리하는 구문이 없다면
49   *  프로그램은 마치 메모리가 할당된 것처럼 주소를 접근한다. 
50   *  이러한 경우 프로그램의 실행중 치명적인 오류가 발생한다.
51   *--------------------------------------------------------*/

 

결과화면

 

 

 

 

 

 

 

 

그러면 이번에는 calloc함수를 살펴 보자. 앞에서도 언급하였지만 calloc은 malloc과 달리 할당된 메모리를 0으로 초기화한다고 하였다. 먼저 calloc의 원형을 보도록 하자

 

void *calloc(size_t nitems, size_t size);

 

calloc는 nitems*size 바이트 크기의 메모리를 할당하고, 할당된 메모리를 모두 0으로 초기화하는 함수이다. 리턴값은 malloc과 마찬가지로 성공할 경우 할당된 메모리의 시작주소의 void 형 포인터를, 실패할 경우 NULL 포인터를 리턴한다. 할당 시 초기화가 가능하다는 점에서 malloc 보다 선호하는 경우도 많다.

 

다음 예제 프로그램을 통하여 calloc을 이용한 동젝 메모리 할당을 해보자. 20이하의 자연수 중에서 3의 배수를 출력하는 프로그램이다.

 1  #include <stdio.h>
 2  #include <stdlib.h>         // alloc.h를 써도 무관
 3
 4  int main()
 5  {
 6      int i;
 7      int *pEven;
 8
 9      // int 형(2byte) * 10개 = 20 byte
10      pEven = (int*)calloc(20,sizeof(int));
11
12      // 메모리 할당에 실패했을 경우 프로그램 종료
13      if(!pEven)
14          return EXIT_FAILURE;
15
16      // 초기값 출력
17      printf("초기값\n");
18      for(i=0;i<20;i++)
19          printf("%d ",pEven[i]);     // calloc은 0으로 초기화된다
20
21      for(i=0;i<20;i++)
22          pEven[i] = i+1;             // 자연수는 첨자보다 1크다
23
24      printf("\n20이하의 3의 배수 : \n");
25      for(i=0;i<20;i++)
26          // 3의 배수 체크
27          if(pEven[i]%3==0)
28              printf("%d\n",pEven[i]);
29
30      // 메모리 해제(반드시)
31      free(pEven);
32
33      return EXIT_SUCCESS;
34  }

 

결과 화면

Posted by '김용환'
,
동적 메모리 - 메모리 크기 변경 | c Note 2004/09/07 12:43
http://blog.naver.com/bravedog/100005578511

때로는 필요에 따라 포인터의 위치는 유지하면서 크기를 바꾸고 싶은 경우가 발생한다. 예를 들어 현재 생성되어있는 int형 메모리가 20개 정도인데 데이터가 30개가 들어온다고 가정하자. 이런 경우 기존의 20개 크기의 동적 메모리를 해제하고 다시 30개 크기의 동적 메모리를 생성해야 할까? 만약 그렇다면 매번 크기가 커질 때마다 메모리 해제와 생성을 반복해야 한다. 이런 때에 유용한 함수가 동적 메모리의 크기를 바꿔주는 realloc이다.

 

realloc의 원형은 다음과 같다.

 

void *realloc(void* block, size_t size);

 

여기서 void형 포인터 block에는 크기를 바꿀 메모리에 대한 포인터를, size는 바꿀 메모리의 크기를 말한다.

 

다음 예제 프로그램을 통해서 realloc에 대해 살펴보자. 인원이 총 5명인 한 학급에 대한 평균 성적을 가지고 있다고 가정해 보자. 이때 전학생이 3명 들어 왔을 경우 성적을 관리하기 위한 방법과 다시 4명이 전학간 경우 성적을 관리하기 위한 방법을 간단하게 프로그램으로 해보았다.

 1  #include <stdio.h>
 2  #include <stdlib.h>
 3
 4  void PrintGrade(int *p, int nPerson);               // 총원, 번호, 성적을 출력하는 함수
 5
 6  int main()
 7  {
 8      int i;
 9      int *pGrade = (int*)calloc(5,sizeof(int));      // 메모리 할당
10
11      // 메모리 할당에 실패했을 경우 프로그램 종료
12      if(!pGrade)
13          return EXIT_FAILURE;
14
15      // 성적 대입
16      pGrade[0]=95;
17      pGrade[1]=50;
18      pGrade[2]=65;
19      pGrade[3]=85;
20      pGrade[4]=100;
21
22      PrintGrade(pGrade,5);                           // 5명인 경우
23
24      // 전학생 3명을 포함한 성적 공간 재할당
25      pGrade = (int*)realloc(pGrade,8*sizeof(int));
26
27      pGrade[5]=25;
28      pGrade[6]=70;
29      pGrade[7]=90;
30
31      PrintGrade(pGrade,8);                           // 8명인 경우
32
33      // 전학생 4명을 제외한 성적 공간 재할당
34      pGrade = (int*)realloc(pGrade,4*sizeof(int));
35
36      PrintGrade(pGrade,4);                           // 4명인 경우
37
38      free(pGrade);
39
40      return EXIT_SUCCESS;
41  }
42
43  void PrintGrade(int *p, int nPerson)
44  {
45      int i;
46
47      printf("총원 %d\n",nPerson);
48      printf("==========================================\n");
49      printf("번호 : ");
50      for(i=0;i!=nPerson;++i)
51          printf("%3d ",i+1);
52      printf("\n성적 : ");
53      for(i=0;i!=nPerson;++i)
54          printf("%3d ",p[i]);
55      printf("\n==========================================\n");
56  }
위의 예제를 통해서 알수 있는 것은 realloc으로 동적 메모리의 크기를
변경하더라도 이전에 들어있던 데이터의 값은 변하지 않는다는 것이다.
실행 결과를 보면 5명에서 8명으로 인원이 변하여도 성적이 그대로
출력됨을 알 수 있다.

 

 

 

Posted by '김용환'
,

IBM Distributed Computing Environment for AIX, Version 2.2; (C) IBM Corporation

Application Development Guide --Core Components


Synchronization Objects

In a multithreaded program, you must use synchronization objects whenever there is a possibility of corruption of shared data or conflicting scheduling of threads that have mutual scheduling dependencies. The following subsections discuss two kinds of synchronization objects: mutexes and condition variables.

Mutexes

A mutex (mutual exclusion) is an object that multiple threads use to ensure the integrity of a shared resource that they access, most commonly shared data. A mutex has two states: locked and unlocked. For each piece of shared data, all threads accessing that data must use the same mutex; each thread locks the mutex before it accesses the shared data and unlocks the mutex when it is finished accessing that data. If the mutex is locked by another thread, the thread requesting the lock is blocked when it tries to lock the mutex if you call pthread_mutex_lock() (see Figure 9). The blocked thread continues and is not blocked if you call pthread_mutex_trylock().

Figure 9. Only One Thread Can Lock a Mutex



Figure a3u2j842 not displayed.

Each mutex must be initialized. (To initialize mutexes as part of the program's one-time initialization code, see "One-Time Initialization Routines".) To initialize a mutex, use the pthread_mutex_init() routine. This routine allows you to specify an attributes object, which allows you to specify the mutex type. The following are types of mutexes:

  • A fast mutex (the default) is locked only once by a thread. If the thread tries to lock the mutex again without first unlocking it, the thread waits for itself to release the first lock and deadlocks on itself.

    This type of mutex is called fast because it can be locked and unlocked more rapidly than a recursive mutex. It is the most efficient form of mutex.

  • A recursive mutex can be locked more than once by a given thread without causing a deadlock. The thread must call the pthread_mutex_unlock() routine the same number of times that it called the pthread_mutex_lock() routine before another thread can lock the mutex. Recursive mutexes have the notion of a mutex owner. When a thread successfully locks a recursive mutex, it owns that mutex and the lock count is set to 1. Any other thread attempting to lock the mutex blocks until the mutex becomes unlocked. If the owner of the mutex attempts to lock the mutex again, the lock count is incremented, and the thread continues running. When an owner unlocks a recursive mutex, the lock count is decremented. The mutex remains locked and owned until the count reaches 0 (zero). It is an error for any thread other than the owner to attempt to unlock the mutex.

    A recursive mutex is useful if a thread needs exclusive access to a piece of data, and it needs to call another routine (or itself) that needs exclusive access to the data. A recursive mutex allows nested attempts to lock the mutex to succeed rather than deadlock.

    This type of mutex requires more careful programming. Never use a recursive mutex with condition variables because the implicit unlock performed for a pthread_cond_wait() or pthread_cond_timedwait() may not actually release the mutex. In that case, no other thread can satisfy the condition of the predicate.

  • A nonrecursive mutex is locked only once by a thread, like a fast mutex. If the thread tries to lock the mutex again without first unlocking it, the thread receives an error. Thus, nonrecursive mutexes are more informative than fast mutexes because fast mutexes block in such a case, leaving it up to you to determine why the thread no longer executes. Also, if someone other than the owner tries to unlock a nonrecursive mutex, an error is returned.

To lock a mutex, use one of the following routines, depending on what you want to happen if the mutex is locked:

  • The pthread_mutex_lock() routine

    If the mutex is locked, the thread waits for the mutex to become available.

  • The pthread_mutex_trylock() routine

    If the mutex is locked, the thread continues without waiting for the mutex to become available. The thread immediately checks the return status to see if the lock was successful, and then takes whatever action is appropriate if it was not.

When a thread is finished accessing a piece of shared data, it unlocks the associated mutex by calling the pthread_mutex_unlock() routine.

If another thread is waiting on the mutex, its execution is unblocked. If more than one thread is waiting on the mutex, the scheduling policy and the thread scheduling priority determine which thread acquires the mutex. (See "Scheduling Priority Attribute" for additional information.)

You can delete a mutex and reclaim its storage by calling the pthread_mutex_destroy() routine. Use this routine only after the mutex is no longer needed by any thread. This routine may require serialization. Mutexes are automatically deleted when the program terminates.

Note: Never include DCE APIs (such as pthread_mutex_destroy()) in the termination routine of a DLL. Doing so can result in an error (such as return code 6 -- invalid handle) when termination occurs out of sequence.

Condition Variables

A condition variable allows a thread to block its own execution until some shared data reaches a particular state. Cooperating threads check the shared data and wait on the condition variable. For example, one thread in a program produces work-to-do packets and another thread consumes these packets (does the work). If the work queue is empty when the consumer thread checks it, that thread waits on a work-to-do condition variable. When the producer thread puts a packet on the queue, it signals the work-to-do condition variable.

A condition variable is used to wait for a shared resource to assume some specific state (a predicate). A mutex, on the other hand, is used to reserve some shared resource while the resource is being manipulated. For example, a thread A may need to wait for a thread B to finish a task X before thread A proceeds to execute a task Y. Thread B can tell thread A that it has finished task X by using a variable they both have access to, a condition variable called a predicate. When thread A is ready to execute task Y, it looks at the condition variable (predicate) to see if thread B is finished (see Figure 10).

Figure 10. Thread A Waits on Condition Ready, Then Wakes Up and Proceeds



Figure a3u2j373 not displayed.

First, thread A locks the mutex named mutex_ready that is associated with the condition variable. Then it reads the predicate associated with the condition variable named ready. If the predicate indicates that thread B has finished task X, then thread A can unlock the mutex and proceed with task Y. If the condition variable predicate indicated that thread B has not yet finished task X; however, then thread A waits for the condition variable to change. Thread A calls the pthreadwait primitive. Waiting on the condition variable automatically unlocks the mutex, allowing thread B to lock the mutex when it has finished task X. The lock is automatically reacquired before waking up thread A(see Figure 11).

Figure 11. Thread B Signals Condition Ready



Figure a3u2j579 not displayed.

Thread B updates the predicate named ready associated with the condition variable to the state thread A is waiting for. It also executes a signal on the condition variable while holding the mutex mutex_ready.

Thread A wakes up, verifies that the condition variable (predicate) is in the correct state, and proceeds to execute task Y (see Figure 10).

Note that, although the condition variable is used for explicit communications among threads, the communications are anonymous. Thread B does not necessarily know that thread A is waiting on the condition variable that thread B signals. And thread A does not know that it was thread B that woke it up from its wait on the condition variable.

Use the pthread_cond_init() routine to create a condition variable. To create condition variables as part of the program's one-time initialization code, see "One-Time Initialization Routines".

Use the pthread_cond_wait() routine to cause a thread to wait until the condition is signaled or broadcast. This routine specifies a condition variable and a mutex that you have locked. (If you have not locked the mutex, the results of pthread_cond_wait() are unpredictable.) This routine unlocks the mutex and causes the calling thread to wait on the condition variable until another thread calls one of the following routines:

  • The pthread_cond_signal() routine to wake one thread that is waiting on the condition variable

  • The pthread_cond_broadcast() routine to wake all threads that are waiting on a condition variable

If you want to limit the time that a thread waits for a condition to be signaled or broadcast, use the pthread_cond_timedwait() routine. This routine specifies the condition variable, mutex, and absolute time at which the wait should expire if the condition variable is not signaled or broadcast.

You can delete a condition variable and reclaim its storage by calling the pthread_cond_destroy() routine. Use this routine only after the condition variable is no longer needed by any thread. Condition variables are automatically deleted when the program terminates.

Other Synchronization Methods

There is another synchronization method that is not anonymous: the join primitive. This allows a thread to wait for another specific thread to complete its execution. When the second thread is finished, the first thread unblocks and continues its execution. Unlike mutexes and condition variables, the join primitive is not associated with any particular shared data.


[ Top of Page | Previous Page | Next Page | Table of Contents | Index ]

'c or linux' 카테고리의 다른 글

동적 메모리 - 메모리 할당  (0) 2005.03.09
동적 메모리 - 메모리 크기 변경  (0) 2005.03.09
POSIX 쓰레드로 멀티 쓰레드 프로그래밍하기  (0) 2005.02.18
함수 포인터  (0) 2005.02.16
ctags 활용  (0) 2005.02.15
Posted by '김용환'
,
v1.1.1

POSIX 쓰레드로 멀티 쓰레드 프로그래밍하기

옮긴이: 차현진(terminus@kldp.org)
원 본: http://users.actcom.co.il/~choo/lupg/tutorials/multi-thread/multi-thread.html

차례

  1. 시작하기 전에(Before We Start)...
  2. 쓰레드가 뭔데 그걸 쓰죠?(What Is a Thread? Why Use Threads)
  3. 쓰레드 만들고 없애기(Creating And Destroying Threads)
  4. 뮤텍스로 쓰레드 동기화하기(Synchronizing Threads With Mutexes)
    1. 뮤텍스가 뭐죠?(What Is A Mutex?)
    2. 뮤텍스 만들고 초기화하기(Creating And Initializing A Mutex)
    3. 뮤텍스 걸고 풀기(Locking And Unlocking A Mutex)
    4. 뮤텍스 없애기(Destroying A Mutex)
    5. 뮤텍스 사용법 - 완전한 예제(Using A Mutex - A Complete Example)
    6. 굶어죽기와 데드락(Starvation And Deadlock Situations)
  5. 세련된 동기화 - 조건 변수(Refined Synchronization - Condition Variables)
    1. 조건 변수가 뭐죠?(What Is A Condition Variable?)
    2. 조건 변수 만들고 초기화하기(Creating And Initializing A Condition Variable)
    3. 조건 변수에 시그널 보내기(Signaling A Condition Variable)
    4. 조건 변수 기다리기(Waiting On A Condition Variable)
    5. 조건 변수 없애기(Destroying A Condition Variable)
    6. 실제 상황의 조건 변수(A Real Condition For A Condition Variable)
    7. 조건 변수 사용법 - 완전한 예제(Using A Condition Variable - A Complete Example)
  6. "Private" thread data - Thread-Specific Data
    1. Overview Of Thread-Specific Data Support
    2. Allocating Thread-Specific Data Block
    3. Accessing Thread-Specific Data
    4. Deleting Thread-Specific Data Block
    5. A Complete Example
  7. 쓰레드 취소와 끝내기(Thread Cancellation And Termination)
    1. 쓰레드 취소하기(Canceling A Thread)
    2. 쓰레드 취소 상태 설정하기(Setting Thread Cancellation State)
    3. 취소 위치(Cancellation Points)
    4. 쓰레드 청소 함수 설정하기(Setting Thread Cleanup Functions)
    5. 쓰레드 끝내기 동기화(Synchronizing On Threads Exiting)
    6. 쓰레드 떼어내기(Detaching A Thread)
    7. 쓰레드 취소 - 완전한 예제(Threads Cancellation - A Complete Example)
  8. 쓰레드를 이용한 사용자 인터페이스 프로그래밍(Using Threads For Responsive User Interface Programming)
    1. 사용자 인터페이스 - 완전한 예제(User Interaction - A Complete Example)
  9. 멀티 쓰레드 어플리케이션에서 비시스템 라이브러리 쓰기(Using 3rd-Party Libraries In A Multi-Threaded Application)
  10. 쓰레드를 지원하는 디버거 쓰기(Using A Threads-Aware Debugger)


시작하기 전에(Before We Start)...

이 튜토리얼은 여러분에게 POSIX 쓰레드(pthread)를 이용한 멀티 쓰레드 프로그램에 익숙해지게 하고 쓰레드의 특징들이 실제 프로그램에서 어떻게 쓰이는지 보여줄 것입니다. 라이브러리가 정의해 놓은 여러가지 툴들을 설명하고 그것들을 어떻게 쓰는지, 또한 프로그래밍 문제를 해결하기위해 실제로 어떻게 적용시키는지를 보여줄 것입니다. 이 글을 읽으려면 병렬 프로그래밍(혹은 멀티 프로세스) 개념을 알고 있어야 합니다. 안 그러면 개념 잡기가 약간 힘들 것입니다. 각 튜토리얼은 "직렬" 프로그래밍에만 익숙한 독자들을 위해 이론적 배경 지식과 용어들을 설명 하면서 시작할 것입니다.

독자들이 X나 모티프 같은 비동기적인 프로그래밍 환경에 익숙하다고 가정을 하고 진행하겠습니다. 이런 환경에 익숙하다면 멀티 쓰레드 프로그래밍 개념을 이해하기 쉽습니다.

POSIX 쓰레드를 말할 때 항상 나오는 질문은 "어떤 POSIX 쓰레드 표준안을 써야 할 것인가?"입니다. 쓰레드 표준은 지난 몇 년간 계속 수정중이기 때문에 서로 다른 함수들, 서로 다른 디폴트 값, 서로 다른 뉘앙스의 여러 구현들이 있습니다. 본 튜토리얼은 리눅스 시스템의 커널 레벨 LinuxThreads 라이브러리 0.5 버전을 사용했기 때문에 다른 시스템, 다른 버전의 pthread를 쓰는 프로그래머들은 문제 발생시 해당 시스템의 매뉴얼을 참고해야 할 것입니다. 몇몇 예제들은 블러킹 시스템 콜을 쓰기 때문에 유저 레벨 쓰레드 라이브러리에서는 동작하지 않을 것입니다 (더 많은 정보를 보려면 우리 웹 사이트의 parallel programming theory tutorial을 참고하세요).
앞에서 얘기 했듯이 여기 나오는 예제들은 리눅스 이외의 다른 시스템에서도 동작하도록 노력을 했습니다(솔라리스 2.5).


쓰레드가 뭔데 그걸 쓰죠?(What Is a Thread? Why Use Threads)

쓰레드는 프로세스와 비슷합니다. 자신의 스택을 가지고 주어진 코드를 실행합니다. 하지만 진짜 프로세스와는 다르게 메모리를 다른 쓰레드와 공유합니다(프로세스는 자신만의 메모리 공간을 가지고 있습니다). 쓰레드 그룹은 한 프로세스 안에서 실행되는 모든 쓰레드를 나타내고, 메모리를 공유하기 때문에 전역 변수와 힙 메모리, 파일 디스크립터 등등을 공유합니다. 또한 같은 쓰레드 그룹의 쓰레드들은 병렬적으로 실행됩니다(즉, 시간을 잘라서 사용을 하는데 프로세서가 여러개라면 진짜 병렬로 동작합니다).

보통의 순차적인 프로그램 대신 쓰레드 그룹을 사용하면 몇 가지 일을 동시에 할 수 있는 장점이 있습니다. 따라서 어떤 이벤트에 대해 즉각적으로 반응을 할 수 있습니다 (예를 들면, 한 쓰레드는 사용자 인터페이스를 처리하고 다른 쓰레드는 데이타베이스 쿼리를 처리한다고 하면, 아주 엄청난 양의 쿼리가 들어와 바쁜 경우에도 사용자 입력에 대해 반응하고 처리할 수가 있습니다).

프로세스 그룹대신 쓰레드 그룹을 사용했을 때의 장점으로는 쓰레드간 컨택스트 스위치(context switching)가 프로세스간 컨택스트 스위치보다 훨씬 빠르다는 것입니다(컨택스트 스위칭이란 현재 돌고 있는 쓰레드나 프로세스에서 다른 쓰레드나 프로세스로 옮겨 가는 것을 말합니다). 또한, 보통 두 쓰레드간 통신을 두 프로세스간 통신보다 빠르고 쉽게 구현 할 수 있습니다.

다른 한 편으로는 한 그룹안의 모든 쓰레드들은 같은 메모리 영역을 사용하기 때문에 한 쓰레드가 메모리를 잘 못 건드리면 다른 쓰레드들에 영향이 미칠 수 있습니다. 프로세스에서는 운영체제가 프로세스를 다른 프로세스로부터 보호해 주기 때문에 쓰레드같은 영향은 없습니다. 프로세스의 다른 장점으로, 서로 다른 프로세스는 서로 다른 시스템(머신)에서 각각 돌 수 있다는 것입니다. 쓰레드는 보통 한 시스템에서 돌아야 합니다.


쓰레드 만들고 없애기(Creating And Destroying Threads)

멀티 쓰레드 프로그램이 실행을 시작하면 main()을 실행시키는 하나의 쓰레드만이 존재하게 됩니다. 이 완전한 쓰레드는 자신의 쓰레드 ID를 갖습니다. 새 쓰레드를 만들려면 pthread_create() 함수를 써야 됩니다. 어떻게 쓰는지 보시죠.



#include <stdio.h>       /* 표준 I/O 루틴 */
#include <pthread.h>     /* pthread 함수와 데이타 스트럭쳐 */

/* 새 쓰레드가 실행시킬 함수 */
void*
do_loop(void* data)
{
    int i;

    int i;			/* 숫자를 찍을 카운터 */
    int j;			/* 지연용 카운터      */
    int me = *((int*)data);     /* 쓰레드 구분 숫자 */

    for (i=0; i<10; i++) {
	for (j=0; j<500000; j++) /* 지연 루프 */
	    ;
        printf("'%d' - Got '%d'\n", me, i);
    }

    /* 쓰레드 없애기 */
    pthread_exit(NULL);
}

/* 보통의 C 프로그램처럼 main에서 시작합니다. */
int
main(int argc, char* argv[])
{
    int        thr_id;         /* 새 쓰레드용 쓰레드 ID */
    pthread_t  p_thread;       /* 쓰레드 구조체       */
    int        a         = 1;  /* 1번 쓰레드 구분 숫자  */
    int        b         = 2;  /* 2번 쓰레드 구분 숫자  */

    /* 'do_loop()를 실행시킬 새 쓰레드 만들기 */
    thr_id = pthread_create(&p_thread, NULL, do_loop, (void*)&a);
    /* main()함수에서도 'do_loop()' 실행시키기 */
    do_loop((void*)&b);
    
    /* NOT REACHED */
    return 0;
}

위 프로그램에서 몇 가지를 살펴보겠습니다.

  1. 메인 프로그램 자체도 쓰레드이기 때문에 do_loop()는 자신이 새로 실행시킨 쓰레드가 실행시킨 do_loop()와 병렬로 동작합니다.
  2. pthread_create()는 4개의 파라미터를 받습니다. 첫 번째는 쓰레드에 대한 정보를 제공하기 위해서 쓰입니다. 두 번째는 새 쓰레드에 속성을 주기 위해서 쓰이는데 우리는 NULL 포인터를 넘겨 줘서 기본값을 쓰게 했습니다. 세 번째 파라미터는 어떤 함수에서 쓰레드가 시작할 것인지를 알려주는 것이고 네 번째는 그 함수로 넘겨줄 아규먼트를 나타냅니다. 여기서 'void*'로 캐스팅 한 것은 이것이 비록 ANSI-C 문법에서는 불필요하지만 좀 더 명확하게 하기 위해서 쓰인 것입니다.
  3. 지연 루프는 병렬로 실행되는 쓰레드를 확실히 보여주기 위해서 쓰였습니다. CPU가 너무 빨라서 한 쓰레드가 모두 출력된 다음 다른 쓰레드의 출력이 나온다면 지연값을 증가시키기 바랍니다.
  4. pthread_exit()는 현재 쓰레드를 종료 시키고 자신이 갖고 있던 자신만의 쓰레드 리소스들을 놓아 줍니다. 쓰레드의 첫 함수 마지막에서 꼭 이 함수를 불러야 할 필요는 없습니다. 그 함수에서 리턴을 하게 되면 자동으로 종료가 됩니다. 쓰레드 중간에서 쓰레드를 종료하고 싶은 경우가 생길 때, 유용하게 쓰일 수 있습니다.

멀티 쓰레드 프로그램을 gcc로 컴파일 하려면 pthread 라이브러리 를 링크시켜줘야 합니다. 이미 여러분의 시스템에 이 라이브러리가 설치되어 있다고 가정하고 어떻게 컴파일 하는지를 보여 드리겠습니다.

gcc pthread_create.c -o pthread_create -lpthread

앞으로 나올 몇몇 프로그램들은 제대로 컴파일 하기 위해서 '-D_GNU_SOURCE' 를 줘서 컴파일 해야 할지도 모릅니다. 주의하세요.

이 프로그램의 소스 코드는 pthread_create.c를 보세요.


뮤텍스로 쓰레드 동기화하기

여러개의 쓰레드를 동시에 돌릴 때 발생하는 기본적인 문제점 중의 하나는 같은 메모리 영역을 쓰기 때문에 "서로의 상태에 신경 쓰도록" 하는 것입니다. 그래서 여기서는 두 개의 쓰레드가 동일한 데이타 구조에 접근할 때 생기는 문제점을 살펴 보도록 하겠습니다.

예를 들어서, 두 쓰레드가 두 변수를 업데이트 하려고 하는 상황을 생각해 봅시다. 한 쓰레드는 두 변수를 0으로 세트하려고 하고, 다른 쓰레드는 두 변수를 1로 세트 하려고 합니다. 만약에 두 쓰레드가 동시에 이 일을 하려고 한다면 한 변수는 1로, 다른 한 변수는 0으로 세트된 상황이 생길 수도 있습니다. 이런 일이 생기는 이유는 첫번째 쓰레드가 첫번째 변수를 0으로 만들고 나서 바로 컨택스트 스위치(context switching-이제 이게 뭔지 아시죠?)가 일어나고, 두번째 쓰레드가 두 변수를 1로 세트를 한 다음 다시 첫번째 쓰레드가 동작을 하면 두 번째 변수만을 0으로 만들기 때문에 결과적으로 첫 번째 변수는 1로, 두 번째 변수는 0으로 됩니다.


뮤텍스(mutex)가 뭐죠?

이 문제를 해결하기 위해서 pthread 라이브러리가 제공하는 기본 메카니즘을 뮤텍스라 부릅니다. 뮤텍스는 다음 세가지를 보장해주는 잠금 장치입니다. (역주: mutex - MUTual EXclusion - 상호 배타성)

  1. 원자성(Atomicity) - 뮤텍스를 걸었을 경우 다른 쓰레드가 동시에 뮤텍스가 걸린 영역으로 들어오지 못하게 보장해주는 원자적 동작입니다.
  2. 유일성(Singularity) - 한 쓰레드가 뮤텍스를 걸었을 경우 자신이 풀기 전에는 다른 쓰레드가 다시 뮤텍스를 걸지 못하게 해 줍니다.
  3. Non-Busy Wait - A라는 쓰레드가 이미 뮤텍스가 걸린 B 쓰레드를 걸려고 한다면 A 쓰레드는 B 쓰레드가 뮤텍스를 풀 때까지 서스펜드(suspend)됩니다 (CPU 리소스를 전혀 사용하지 않습니다). B가 뮤텍스를 풀면 A는 깨어나고 자신이 뮤텍스를 걸고 실행을 계속해 나갑니다.

이 세가지에서 볼 수 있듯이 어떻게 뮤텍스가 변수(혹은 코드의 임계 부분)에 대해서 배타적 접근을 확실하게 해 주는지 알 수 있습니다. 앞에서 설명했던 두 변수를 업데이트 해주는 가상 코드를 살펴 보죠. 다음은 첫 번째 쓰레드입니다.

'X1' 뮤텍스를 잠근다.
첫번째 변수를 '0'으로 세팅.
두번째 변수를 '0'으로 세팅.
'X1' 뮤텍스를 푼다.


두번째 쓰레드는 이렇게 되겠죠.

'X1' 뮤텍스를 잠근다.
첫번째 변수를 '1'로 세팅.
두번째 변수를 '1'로 세팅.
'X1' 뮤텍스를 푼다.


두 쓰레드가 같은 뮤텍스를 쓰고 동시에 돌았다고 하면 두 변수 모두 '0'으로 세트되어 있던지 '1'로 세트되어 있을 겁니다. 프로그래머가 주의할 일이 조금 있습니다. 만약에 세번째 쓰레드가 코드의 다른 부분에서 'X1' 뮤텍스 없이 이 두 변수에 접근을 한다면 역시나 변수 내용이 뒤죽박죽 될 가능성이 있습니다. 따라서 이 변수에 접근하는 모든 코드들을 조그만 함수로 만들어 놓고 이 변수들에 접근 할 때는 이 함수만 쓰도록 해야 합니다.


뮤텍스 만들고 초기화하기

뮤텍스를 만들려면 먼저 pthread_mutex_t 형의 변수를 선언하고 초기화 해야 합니다. 가장 간단한 방법은 PTHREAD_MUTEX_INITIALIZER 상수를 할당하는 것입니다. 따라서 다음같은 코드를 쓰면 되겠습니다.


pthread_mutex_t a_mutex = PTHREAD_MUTEX_INITIALIZER;


주의 할 점이 하나 있는데, 이런 형태의 초기화는 '빠른 뮤텍스(fast mutex)'라는 뮤텍스를 만들어 줍니다. 무슨 뜻이냐면, 만약에 쓰레드가 뮤텍스를 잠근 뒤에 또, 그 뮤텍스를 잠그려고 하면, 그냥 멈춰버릴 것입니다. - 데드락(deadlock)이 걸린다는 뜻입니다.

'재귀적 뮤텍스(recursive mutex)'란 다른 형태도 있는데 한 번 잠근 뒤에도 몇 번이고 더 잠글 수 있게 해주는 뮤텍스입니다. 이 뮤텍스는 위에서 말한 데드락 상황이 안 걸리게 해 줍니다(하지만 이 뮤텍스를 풀려고 하는 다른 쓰레드는 멈출 것입니다). 걸었던 뮤텍스를 풀 때, 걸었던 만큼 풀지 않는 한 뮤텍스가 계속 걸려 있을 겁니다. 이 방법은 현대적인 문잠금 장치에서 문을 잠글 때는 시계 방향으로 두 번 돌리고, 풀 때는 반시계 방향으로 두 번 돌리는 것과 비슷합니다. 이런 뮤텍스를 만들려면 PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP를 할당해 주면 됩니다.


뮤텍스 걸고 풀기(Locking And Unlocking A Mutex)

뮤텍스를 걸때는 pthread_mutex_lock() 함수를 씁니다. 뮤텍스를 걸려고 하는데, 이미 다른 쓰레드가 그 뮤텍스를 걸어놨다면 자신의 쓰레드를 멈추게 합니다. 이렇게 멈췄을 경우에는 뮤텍스를 걸었던 프로세스가 뮤텍스를 풀면 이 함수는 다시 뮤텍스를 걸고 리턴을 합니다. 미리 초기화 했다고 가정하고 어떻게 뮤텍스를 거는지 보여 드리죠.


int rc = pthread_mutex_lock(&a_mutex);
if (rc) { /* 에러 발생 */
    perror("pthread_mutex_lock");
    pthread_exit(NULL);
}
/* 뮤텍스가 걸렸습니다. 필요한 일을 하세요. */
.
.


쓰레드는 자신이 할 일(변수나 데이타 구조의 값을 바꾼다거나 파일을 처리하는등)을 하고 나면 다음처럼 pthread_mutex_unlock() 함수를 써서 뮤텍스를 풀어 줘야 합니다.


rc = pthread_mutex_unlock(&a_mutex);
if (rc) {
    perror("pthread_mutex_unlock");
    pthread_exit(NULL);
}


뮤텍스 없애기(Destroying A Mutex)

뮤텍스로 할 일을 다 했다면 이젠 없앨 차례입니다. 할 일을 다 했다는 얘기는 어떤 쓰레드도 그 뮤텍스가 필요없어졌다는 뜻입니다. 만약에 한 쓰레드만 뮤텍스로 할 일을 끝마쳤다면 이 때는 없애면 안 됩니다. 다른 쓰레드가 그 뮤텍스를 쓸지도 모르기 때문입니다. 모든 쓰레드가 확실히 뮤텍스를 쓸 일이 없다면 마지막 쓰레드가 pthread_mutex_destroy() 함수로 그 뮤텍스를 없앨 수 있습니다.


rc = pthread_mutex_destroy(&a_mutex);


이 함수를 부르고 나면 a_mutex 변수는 다시 초기화 되지 않는 한 더 이상 뮤텍스로 쓰일 수가 없습니다. 따라서 만약에 한 쓰레드가 너무 일찍 뮤텍스를 없앴을 경우에, 다른 쓰레드에서 잠그거나 풀려고 한다면 잠그고 푸는 함수는 EINVAL 에러 코드를 만나게 됩니다.


뮤텍스 사용법 - 완전한 예제(Using A Mutex - A Complete Example)

뮤텍스의 탄생부터 죽음까지 모두 알아봤기 때문에 이제는 예제를 살펴보겠습니다. 이 예제는 영광스러운 "종업원상"을 타기 위해 다투는 두 종업원을 시뮬레이션합니다. 빠르게 시뮬레이션하기 위해서 3개의 쓰레드를 쓰겠습니다. 하나는 Danny를 "종업원상"에 올리고 두번째 쓰레드는 Moshe를 올립니다. 세번째 쓰레드는 "종업원상"의 내용이 일치하는 지를 보여줍니다(즉, 정확하게 한 종업원의 데이타가 들어있음).
두 개의 프로그램이 있는데 하나는 뮤텍스를 쓰는 것이고 다른 하나는 쓰지 않는 것입니다. 둘 다 해보고 차이점을 알아본 다음, 멀티 쓰레드 환경에서 뮤텍스가 꼭 필요한 이유를 마음으로 느껴 보세요.

이 프로그램들은 파일 형태로 제공됩니다. 뮤텍스를 쓰는 것은 employee-with-mutex.c이고, 뮤텍스를 안 쓰는 것은 employee-without-mutex.c입니다. 소스에 있는 주석을 잘 읽어서 어떻게 동작하는지에 대해서 더 잘 이해하시기 바랍니다.


굶어죽기와 데드락 상황(Starvation And Deadlock Situations)

다시 기억을 되살려 보죠. pthread_mutex_lock()는 이미 잠겨 있는 뮤텍스에 대해서는 알 수 없는 시간 동안 멈춰 있을 수 있습니다. 만약에 그 잠김이 영원하다면 우리의 불쌍한 쓰레드는 "굶어(starved)" 죽습니다. 리소스를 얻으려 하지만 영원히 얻지 못하게 되는 것입니다. 이런 굶어 죽기( starvation)가 발생하지 않도록 하는 것은 프로그래머에게 달려 있습니다. pthread 라이브러리는 어떤 도움도 줄 수가 없습니다.

그렇지만, pthread 라이브러리는 "데드락(deadlock)"은 해결 할 수도 있습니다. 데드락이란 모두 같은 상태인 몇몇 쓰레드가 다른 쓰레드가 갖고 있는 리소스를 기다리는 상황입니다.(A deadlock is a situation in which a set of threads are all waiting for resources taken by other threads, all in the same set.) 당연히 모든 쓰레드가 뮤텍스를 기다리면서 멈춰있다면 아무도 다시 돌 수는 없을 것입니다. pthread 라이브러리는 이런 상황을 추적하다가 마지막 쓰레드가 pthread_mutex_lock()를 부르면 실패를 리턴하면서 EDEADLK 에러를 발생시킵니다. 프로그래머는 이런 값을 확인해서 데드락을 피할 방법을 찾아야 합니다.


세련된 동기화 - 조건 변수(Refined Synchronization - Condition Variables)

지금까지 살펴본 뮤텍스는 리소스에 대한 배타적 접근이라는 간단한 동기화를 제공합니다만, 가끔은 진짜 동기화가 필요할 경우가 있습니다.

  • 서버에서, 한 쓰레드는 클라이언트의 요청을 읽어들이고 그 요청을 해석해서 여러 쓰레드에게 처리를 넘깁니다. 이 처리 쓰레드들은 처리할 데이타가 생길 경우에 그 사실을 알아야 할 필요가 있습니다. 그렇지 않다면 CPU 시간을 쓰지 않으면서 기다려야 합니다.
  • GUI(Graphical User Interface) 어플리케이션에서 한 쓰레드는 사용자 입력을 읽어 들이고 한 쓰레드는 그래픽 출력을 담당하며, 한 쓰레드는 서버에 요청을 보내고 그 응답을 처리합니다. 서버쪽을 담당하는 쓰레드는 서버에서 응답이 왔을 때 그래픽을 담당하는 쓰레드에게 알려줄 수가 있어야 합니다. 그래야 사용자에게 즉시 보여줄 수 있기 때문입니다. 사용자 입력 담당 쓰레드는 예를 들면 서버 담당 쓰레드가 아주 긴 동작중이더라도 사용자가 그것을 취소 시킬 수 있게 해주는 상황처럼 사용자의 요청에 항상 빠르게 응답해야 할 필요가 있습니다.
이 상황들은 모두, 쓰레드는 서로 어떤 사건에 대해서 상대방에게 통보할 수 있는 능력이 필요합니다. 이것이 바로 조건 변수가 탄생한 이유입니다.


조건 변수가 뭐죠?(What Is A Condition Variable?)

조건 변수는 어떤 일이 발생할 때까지 CPU 사이클을 낭비하지 않고 기다릴 수 있도록 해 주는 메카니즘입니다. 몇개의 쓰레드가 조건 변수를 기다리고 있고, 다른 쓰레드가 그 조건 변수에 대해서 시그널을 날려주면(사건을 통지) 기다리던 쓰레드중의 하나가 깨어나서 그 사건에 대해 반응을 하게 됩니다. 또한 그 조건 변수를 기다리고 있던 모든 쓰레드를 깨울 수 있게 브로드캐스트 할 수 있는 방법도 있습니다.

주의할 점은 조건 변수는 잠금을 지원하지 않는다는 것입니다. 따라서 조건 변수에 접근을 하려면 뮤텍스와 같이 사용을 해야 합니다.


조건 변수 만들고 초기화하기(Creating And Initializing A Condition Variable)

조건 변수를 만들려면 pthread_cond_t 형의 변수를 선언하고 알맞게 초기화 시켜줘야 합니다. 초기화는 간단하게 PTHREAD_COND_INITIALIZER 라는 매크로를 쓰던지, pthread_cond_init() 함수를 쓰면 됩니다. 매크로를 쓰는 예제를 살펴 보겠습니다.

pthread_cond_t got_request = PTHREAD_COND_INITIALIZER;

'got_request'라는 조건 변수를 선언하고 초기화 합니다.

주의사항: PTHREAD_COND_INITIALIZER는 실제로 구조체이기 때문에 조건 변수가 선언 될 때에만 쓰일 수 있습니다. 실행 시간에 초기화를 해야 한다면 pthread_cond_init()함수를 쓰기 바랍니다.


조건 변수 시그널 날리기(Signaling A Condition Variable)

조건 변수에 시그널을 날리는 방법은 두 가지가 있습니다. 하나는 pthread_cond_signal() 함수를 부르는 것이고(이 변수를 기다리고 있는 하나의 쓰레드만을 깨울 때), 또 하나는 pthread_cond_broadcast() 함수를 부르는 것입니다(이 변수를 기다리고 있는 모든 쓰레드를 깨울 때). 'got_request'가 적당히 초기화 됐다고 가정하고 예제를 살펴보도록 하죠.

int rc = pthread_cond_signal(&got_request);

혹은 브로드캐스트 함수를 써서,

int rc = pthread_cond_broadcast(&got_request);

두 함수 모두 성공했을 때는 'rc'를 0으로, 실패했을 때는 0이 아닌 값으로 세팅합니다. 실패 했을 경우에는 리턴값은 에러 이유를 나타냅니다(파라미터가 조건 변수가 아닐 때는 EINVAL를, 시스템 메모리가 부족할 때는 ENOMEM를 나타냅니다).

주의 사항: 시그널이 성공했다고 해서 어떤 쓰레드가 깨어났다는 뜻은 아닙니다. 그 조건 변수를 기다리던 쓰레드가 하나도 없었다면 아무일도 아닌 것이죠(즉, 시그널을 잃어버리는 것입니다).
그리고 시그널을 저장해놨다가 쓸 수도 없습니다. 만약에 시그널 함수가 리턴한 다음에 어떤 쓰레드가 그 조건 변수를 기다리기 시작한다면 그 쓰레드는 다른 시그널이 발생해야 깨어날 수 있습니다.


조건 변수 기다리기(Waiting On A Condition Variable)

어떤 쓰레드가 조건 변수에 시그널을 날리길 다른 쓰레드가 기다리려고 한다면 다음 두 함수 중에 한 함수를 쓰면 됩니다. pthread_cond_wait(), pthread_cond_timedwait(). 각 함수는 조건 변수와 뮤텍스(기다리기 전에 뮤텍스를 걸지도 모르기 때문에)를 넘겨 받아서 뮤텍스를 푼 다음에 조건 변수에 시그널이 들어올 때까지 잠들어 버립니다. 앞에서 살펴 봤던 pthread_cond_signal()에 의해서 시그널이 발생해, 깨어 나게 된다면 뮤텍스는 자동으로 다시 잠기고 리턴하게 됩니다.

두 함수가 다른 점은 pthread_cond_timedwait()에 기다릴 시간을 알려준다는 것인데 ETIMEDOUT의 에러값을 갖고 리턴을 해서 조건 변수가 시그널을 받은 것이 아니라 시간이 지나서 리턴했다는 것을 알려준다는 것입니다. pthread_cond_wait() 는 시그널을 받기 전에는 영원히 기다릴 것입니다.

두 함수를 어떻게 쓰는지 보여드리죠. 'got_request'는 적당한 조건 변수로 초기화 됐고 역시 'request_mutex'도 적당한 뮤텍스로 초기화 됐다고 가정합니다. 먼저 pthread_cond_wait() 함수를 봅시다.


/* 뮤텍스를 먼저 걸고 */
int rc = pthread_mutex_lock(&a_mutex);
if (rc) { /* 에러 났음 */
    perror("pthread_mutex_lock");
    pthread_exit(NULL);
}
/* 이제 뮤텍스가 걸렸고, 조건 변수를 기다린다.            */
/* pthread_cond_wait이 실행되는 동안 뮤텍스는 풀립니다.   */
rc = pthread_cond_wait(&got_request, &request_mutex);
if (rc == 0) { /* 조건 변수가 시그널을 받아서 깨어났습니다. */
               /* pthread_cond_wait()가 뮤텍스를 다시 걸어 줍니다.      */
    /* 할 일을 하세요... */
    .
}
/* 끝으로 뮤텍스를 풀어 줍시다. */
pthread_mutex_unlock(&request_mutex);


다음은 pthread_cond_timedwait() 함수를 쓰는 예제입니다.


#include <sys/time.h>     /* struct timeval 정의           */
#include <unistd.h>       /* gettimeofday() 선언           */

struct timeval  now;            /* 기다리기 시작하는 시각        */
struct timespec timeout;        /* 대기 함수에서 쓸 타임아웃값   */
int             done;           /* 다 기다렸나요?                */

/* 뮤텍스를 먼저 걸고 */
int rc = pthread_mutex_lock(&a_mutex);
if (rc) { /* 에러 났음 */
    perror("pthread_mutex_lock");
    pthread_exit(NULL);
}
/* 이제 뮤텍스가 걸렸음. */

/* 지금 시각을 얻는다. */ 
gettimeofday(&now);
/* 타임아웃값을 세팅 */
timeout.tv_sec = now.tv_sec + 5
timeout.tv_nsec = now.tv_usec * 1000; /* timeval은 마이크로(micro)초를 씁니다.         */
                                      /* timespec은 나노(nano)초를 씁니다.         */
                                      /* 1 나노초 = 1000 마이크로초       */

/* 조건 변수를 기다림 */
/* 유닉스 시그널이 타임아웃 전에 대기 상태를 멈추게 할 수 있기 때문에 루프를 써서 피하겠습니다. */
done = 0;
while (!done) {
    /* pthread_cond_timedwait()은 함수 시작부분에서 뮤텍스를 푼다는 것을 기억하세요. */
    rc = pthread_cond_timedwait(&got_request, &request_mutex, &timeout);
    switch(rc) {
        case 0:  /* 조건 변수가 시그널을 받아서 깨어 났음 */
                 /* pthread_cond_timedwait가 뮤텍스를 다시 걸어줍니다. */
            /* 할 일을 하시고... */
            .
            .
            done = 0;
            break;
        case ETIMEDOUT: /* 시간이 다 됐네요 */
            done = 0;
            break;
        default:        /* 에러가 났습니다.(즉, 유닉스 시그널을 받았습니다.) */
            break;      /* swithc문을 빠져나가지만 다시 while 루프를 돕니다. */
    }
}
/* 자, 끝으로 뮤텍스를 풀어 줍시다. */
pthread_mutex_unlock(&request_mutex);


보는바와 같이 타임아웃을 쓰는 버전이 더 복잡합니다. 따라서 필요할 때마다 코드를 만들지 말고 래퍼 함수등을 쓰는게 훨씬 좋을 것입니다.

주의사항: 두 개 이상의 쓰레드가 기다리고 있는 조건 변수가 시그널을 아주 많이 받는다고 할 때, 기다리던 쓰레드 중의 하나는 영원히 깨어 나지 못 할 수도 있습니다. 조건 변수가 시그널을 받았을 때 기다리던 쓰레드중 어떤 쓰레드가 깨어날 지에 대해서 알 수가 없기 때문입니다. 방금 깨어난 쓰레드가 대기 상태로 다시 들어가자마자 시그널이 다시 발생해 그 쓰레드가 다시 깨어나는 식의 동작이 계속 될 수 있기 때문입니다. 이럴 경우에 계속 깨어나지 못하는 쓰레드를 가르켜 "굶어죽었다(starvation)"라고 부릅니다. 이렇게 원치 않는 동작이 일어날 가능성이 있는 상황을 피하는 것은 전적으로 프로그래머의 책임입니다. 하지만 앞에서 봤던 서버 예제에서는 요청이 아주 늦게 들어오고, 서비스 응답을 처리할 쓰레드는 많을 것이기 때문에 아주 바람직한 상황입니다. 즉, 이 경우에는 요청이 발생하자마자 바로바로 처리될 것이기 때문입니다.

주의사항 2: 뮤텍스가 pthread_cond_broadcast로 브로드캐스트를 받았을 때, 그 뮤텍스를 기다리던 모든 쓰레드가 동시에 실행되는것은 아닙니다. 기다리던 각각은 자신의 대기 함수가 리턴하기 전에 뮤텍스를 다시 걸려고 시도를 하기 때문에 하나씩 실행이 됩니다. 즉, 뮤텍스를 걸고, 자기 할 일을 하고, 뮤텍스를 풀고하는 식으로 차례차례 실행이 됩니다.


조건 변수 없애기(Destroying A Condition Variable)

조건 변수를 다 썼다면 없애야겠죠. 이래야 조건 변수가 갖고 있던 시스템 리소스를 반환할테니까요. pthread_cond_destroy()로 이 일을 합니다. 제대로 동작하려면 이 조건 변수를 기다리는 쓰레드가 하나도 없어야 합니다. 사용법을 보여드릴텐데, 역시 'got_request'가 이미 조건 변수로 초기화 되어 있었다고 가정합니다.


int rc = pthread_cond_destroy(&got_request);
if (rc == EBUSY) { /* 이 조건 변수를 기다리는 쓰레드가 있군요. */
    /* 잘 처리하세요... */
    .
    .
}


어떤 쓰레드가 여전히 조건 변수를 기다리고 있다면, 상황에 따라 다르겠지만, 이 조건 변수의 사용에 어떤 허점이 있었을 수도 있고 적당한 쓰레드 종료 코드가 빠졌을 수도 있습니다. 최소한 디버깅 단계에서는 이 상황을 프로그래머에게 알려주는게 좋습니다. 아무 것도 아닐 수도 있고 아주 중대한 결함일 수도 있으니까요.


실제 상황에서의 조건 변수(A Real Condition For A Condition Variable)

조건 변수에 대해서 하나 짚고 가야겠습니다. 이것과 관련된 실제 조건에 대한 확인들이 없다면 조건 변수는 거의 쓸모가 없습니다. 확실히 하기 위해서 앞에서 소개했던 서버 예제를 잠깐 살펴보도록 하죠. 'got_request' 조건 변수가 처리할 새 요청이 들어왔을 때 시그널을 받는다고 가정하고 사용을 했습니다. 이들은 또한 어떤 요청 큐에 들어 있을 것입니다. 그 조건 변수가 시그널을 받았을 때, 기다리던 쓰레드가 있다면 그 쓰레드는 깨어나고 응답을 처리할 것이라는 것을 확신할 수 있습니다.

하지만, 새 요청이 들어온 순간에 모든 쓰레드가 바로 전 응답을 처리하느라 바쁘다면 어떻게 될까요? 이 순간에는 모든 쓰레드는 조건 변수를 기다리고 있지 않고 자기 일을 하고 있었기 때문에 그 조건 변수가 받은 시그널은 무시될 겁니다. 또한 각 쓰레드가 자기 일을 마치고 조건 변수를 기다리는 상태가 됐을 경우, 그 무시됐던 시그널이 다시 발생하지도 않습니다(또다른 새 요청이 없다고 가정하면). 따라서, 모든 쓰레드가 시그널을 기다리느라 멈춰있는 동안 최소한 한 개의 요청이 처리되지 못 하고 남아 있게 됩니다.

이 문제를 해결하기 위해서 요청이 미처리 된 갯수를 정수 변수에 갖고 있겠습니다. 그리고 각 쓰레드는 조건 변수를 기다리기 전에 그 값을 확인해서 그 값이 양수이면 (미처리 된 요청이 있다), 멈추지 않고 그 응답을 처리할 겁니다. 또한, 요청을 처리한 쓰레드는 이 변수를 하나씩 감소시켜야 하는데 이렇게 해야 숫자가 정확해 질것입니다.
이런 고려 사항들이 위에서 봤던 코드를 어떻게 바꾸는지 봅시다.



/* 미처리된 요청, 0으로 초기화 */
int num_requests = 0;
.
.
/* 먼저, 뮤텍스를 잠급시다. */
int rc = pthread_mutex_lock(&a_mutex);
if (rc) { /* 에러 있음 */
    perror("pthread_mutex_lock");
    pthread_exit(NULL);
}
/* 이제 뮤텍스는 잠겼고, 조건 변수를 기다립니다. */
/* 처리할 요청이 없다면             */
rc = 0;
if (num_requests == 0)
    rc = pthread_cond_wait(&got_request, &request_mutex);
if (num_requests > 0 && rc == 0) { /* 미처리 요청이 있네용 */
        /* 할 일을 합시다. */
        .
        .
        /* 미처리 요청수를 하나 줄입니다. */
        num_requests--;
    }
}
/* 마지막으로, 뮤텍스를 풀어줘야죠 */
pthread_mutex_unlock(&request_mutex);


조건 변수 사용법 - 완전한 예제(Using A Condition Variable - A Complete Example)

조건 변수의 실질적인 사용법을 보여주기 위해서 앞에서 설명했던 서버를 시뮬레이션하는 프로그램을 소개하겠습니다. 한 쓰레드는 수신자로서, 클라이언트의 요청을 받아 들여서 링크드 리스트에 요청을 집어 넣습니다. 핸들러 쓰레드는 이 요청을 처리하게 됩니다. 간단하게 하기 위해서 수신자 쓰레드는 실제 클라이언트에서 요청을 받아들이지 않고 자신이 요청을 만들어 내게 할 것입니다.

소스는 thread-pool-server.c에서 볼 수 있습니다. 소스안에 아주 자세한 주석이 달려 있으니까 소스를 먼저 읽어 본 다음에 밑에 나오는 설명을 참고하세요.

  1. 'main' 함수는 먼저 핸들러 쓰레드를 만들고, 자신의 메인 루프를 통해 수신자 쓰레드의 역할을 짊어집니다.
  2. 한 개의 뮤텍스로, 조건 변수와 요청을 기다릴 링크드 리스트, 두 개를 보호하는데 씁니다. 이렇게 하면 전체 설계를 간단하게 할 수 있습니다. 연습문제 하나 내죠. 이 예제를 두 개의 뮤텍스를 쓰는 방식으로 바꿔보세요.
  3. 여기서 쓰이는 뮤텍스는 재귀적 뮤텍스"여야" 합니다. 왜 그런가는 소스 코드중, 'handle_requests_loop' 함수를 보세요. 보면, 먼저 뮤텍스를 걸고, 'get_request' 함수를 부르는데, 여기서도 뮤텍스를 또 거는군요. 만약에 재귀적 뮤텍스를 안 썼다면 이 'get_request' 함수에서 뮤텍스를 거는 순간 영원히 멈춰버릴 것입니다.
    'get_request' 함수에서 뮤텍스 거는 부분을 빼서 두 번 거는 문제를 풀 수 있지 않겠냐라고 할 지도 모르겠지만 이렇게 하면 결함이 있는 설계가 돼 버립니다. 아주 큰 프로그램에서 'get_request'를 다른 코드상에서 부를 수도 있기 때문입니다. 따라서 매번 쓸 때마다 뮤텍스가 적절하게 잠겼는지 확인할 필요가 있습니다.
  4. 일반적으로, 재귀적 뮤텍스를 쓸 때에는, 뮤텍스를 잠그고 푸는 것을 한 함수 안에서 하도록 해야 합니다. 안 그러면, 잠근 수만큼 풀기가 아주 어려워 지고 결국 데드락이 발생하게 될 겁니다.
  5. pthread_cond_wait() 함수가 내부적으로 뮤텍스를 풀었다 다시 거는게 처음에는 헷갈릴 수도 있습니다. 제일 좋은 방법은 코드상에 이런 동작에 대해 주석으로 달아서, 다른 사람이 쓸데없이 뮤텍스를 또 걸지 않게 해 줄 수 있습니다.


개인적인 쓰레드 데이타 - 쓰레드만의 데이타("Private" thread data - Thread-Specific Data)

보통의 쓰레드 하나짜리 프로그램에서 가끔 전역 변수를 써야 할 때가 있습니다. 맞습니다. 나이 드신 훌륭한 선생님께서는 전역 변수를 쓰는게 아주 나쁜 습관이라고 말씀하셨습니다. 하지만 가끔 이게 편할 때가 있습니다. 특히나 한 파일 안에서만 보이는 정적 변수라면 더욱 그렇죠.

멀티 쓰레드 프로그램에서도 이런 전역 변수를 써야 할 경우가 있습니다. 모든 쓰레드에서 접근 가능한 하나의 변수에 대해서는 약간의 오버헤드를 갖는 뮤텍스를 써서 보호해야 한다는 것에 주의하시기 바랍니다. 게다가, 특정한 쓰레드에서만 쓰일 "전역" 변수가 필요할 수도 있고, 똑같은 "전역" 변수이나 다른 쓰레드에서는 다른 값을 가져야 할 때도 있습니다. 예를 들어, 각 쓰레드에서 전역적으로 접근할 수 있는 하나의 연결 리스트(그러나 같지 않은)가 필요하다고 가정해 보죠. 더군다나, 모든 쓰레드가 실행할 코드는 동일해야 합니다. 이런 경우에, 리스트의 시작을 나타내는 전역 포인터는 각 쓰레드에서 서로 다른 위치를 가르키고 있어야 합니다.

이런 포인터를 가지려면 메모리상의 위치가 다른 동일한 전역 변수가 있어야 합니다. 이것이 바로 쓰레드만의 데이타(thread-specific data) 메카니즘이 필요한 이유입니다.


쓰레드만의 데이타 지원 개요(Overview Of Thread-Specific Data Support)

쓰레드만의 데이타(TSD) 메카니즘에서는 키와 값이라는 개념이 필요합니다. 각 키는 이름을 갖고 있고 어떤 메모리 영역을 가르킵니다. 두 개의 서로 다른 쓰레드에서 이름이 같은 키를 갖고 있다면 항상 서로 다른 메모리 위치를 나타냅니다. 이 키를 가지고 접근할 수 있는 메모리 블럭을 할당해 주는 라이브러리 함수들이 이것을 처리해 줍니다. 키를 만들어 주는 함수(전체 프로세스에서 한 키에 대해서 한 번만 실행), 메모리를 할당해 주는 함수(각 쓰레드에서 실행), 특정 쓰레드에서 이 메모리를 다시 반환해 주는 함수, 전체 프로세스에서 그 키를 없애주는 함수등이 있습니다. 또, 키가 가르키는 데이타에 접근하는 함수와 그 값을 세팅하거나 값을 알아내는 함수도 있습니다.


쓰레드만의 데이타 블럭 할당하기(Allocating Thread-Specific Data Block)

pthread_key_create() 함수는 새로운 키를 만들어 내려고 할 때 쓰입니다. 이 키는 전체 프로세스의 모든 쓰레드에서 유효합니다. 키가 생성 됐을 때, 기본으로 NULL을 가르키게 됩니다. 다음에 각 쓰레드들은 자신이 원하는 값으로 이 복사본을 변경하게 됩니다. 사용법을 보여드리죠.


/* rc 는 pthread 함수의 리턴값을 저장하는데 쓰입니다.       */
int rc;
/* 키를 갖고 있을 변수 정의.                                */
pthread_key_t list_key;
/* cleanup_list 는 데이타를 청소해 주는 함수입니다.         */
/* 이것은 우리 프로그램에서 만들어 주는 것이지 TSD 자체의 것이 아닙니다. */
extern void* cleanup_list(void*);

/* 삭제시 불릴 함수를 넘겨서 키를 만듭니다.                 */
rc = pthread_key_create(&list_key, cleanup_list);


몇 가지 주의사항:
  1. pthread_key_create() 가 리턴한 후에는 'list_key' 변수는 새롭게 생성된 키를 가르키게 됩니다.
  2. pthread_key_create()의 두번째 인자로 넘겨진 함수 포인터는 쓰레드 종료시, pthread 라이브러리에 의해서 키 값의 포인터를 인자로 받아서 불리게 됩니다. 함수 포인터에 NULL 포인터를 넘길 수도 있는데 이렇게 하면 종료시 해당 키에 대해서는 아무 함수도 실행 되지 않습니다. 주의할 점은, 이 키가 한 쓰레드에서 한 번만 생성됐다고 하더라도, 각 쓰레드가 종료할 때마다 실행된다는 것입니다.
    만약에 키를 여러개 생성했다면 키 생성 순서와는 상관없이 해당 종료 함수가 실행 될 것입니다.
  3. pthread_key_create() 함수는 성공시 0을, 실패시 에러 코드를 리턴합니다.
  4. PTHREAD_KEYS_MAX 만큼의 키 값 제한이 있습니다. PTHREAD_KEYS_MAX 가 넘어가게 되면 pthread_key_create() 함수에서 EAGAIN 에러 값을 받게 될 것입니다.


쓰레드만의 데이타에 접근하기(Accessing Thread-Specific Data)

키를 생성한 다음에는 pthread 함수를 써서 접근할 수 있습니다: pthread_getspecific()pthread_setspecific(). 첫번째 함수는 주어진 키에 대해서 그 값을 알아내는 데 쓰이고, 두번째 함수는 주어진 키에 데이타를 세트하는데 쓰입니다. 키 값은 간단하게 void 포인터(void *)이기 때문에, 아무것이나 저장할 수 있습니다. 사용법을 살펴보도록 하죠. 'a_key'는 pthread_key_t 타입으로서, 이미 적당히 초기화된 키 변수라고 가정합니다.



/* 이 변수는 pthread 함수의 리턴 코드값을 저장하는데 쓰입니다.    */
int rc;

/* 데이타를 저장할 변수를 정의합니다. 여기서는 integer 라고 하죠. */
int* p_num = (int*)malloc(sizeof(int));
if (!p_num) {
    fprintf(stderr, "malloc: out of memory\n";
    exit(1);
}
/* 변수를 아무 값으로 초기화 합니다.		      	*/
(*p_num) = 4;

/* 이제 이 값을 TSD 키에 저장합니다.				*/
/* 주의할 것은 'p_num' 을 저장하는게 아니라 		*/
/* p_num이 가르키는 값을 저장한다는 것입니다.  	*/
rc = pthread_setspecific(a_key, (void*)p_num);

.
.
/* 어쩌구 저쩌구...  */
.
.
/* 'a_key' 키의 값을 얻어서 출력. */
{
    int* p_keyval = (int*)pthread_getspecific(a_key);

    if (p_keyval != NULL) {
	printf("value of 'a_key' is: %d\n", *p_keyval);
    }
}

한 쓰레드에서 키 값을 세트한 후, 다른 쓰레드에서 그 값을 읽어보시기 바랍니다. NULL 을 얻게 될텐데 이 키 값은 쓰레드마다 서로 다르기 때문에 그렇습니다.

pthread_getspecific() 이 NULL을 리턴하는 두 가지 경우를 알아보겠습니다:

  1. 주어진 키가 유효하지 않다(즉, 키가 생성이 안 됐다).
  2. 키 값이 NULL이다. 이는 초기화가 안 됐거나 그 전에 pthread_setspecific()에 의해서 강제로 NULL로 세트됐을 경우중 하나이다.


쓰레드만의 데이타 블럭을 지우기(Deleting Thread-Specific Data Block)

pthread_key_delete() 함수는 키를 지울 때 쓰입니다만, 함수 이름 때문에 헷갈리지 말아야 할 것이 하나 있습니다. 이 함수는 해당 키가 갖고 있는 메모리를 지우지도 않고, 키 생성시 등록된 청소 함수를 부르지도 않습니다. 그러므로, 실행중에 이 메모리를 프리시켜야 한다면 직접 해 줘야 합니다. 하지만 보통, 전역 변수(쓰레드만의 데이타 역시)를 사용한다는 것은 쓰레드가 종료할 때까지 프리시킬 필요가 없을 테고, 이럴 경우에는 쓰레드 라이브러리가 종료함수를 불러줄 것입니다.

이 함수 사용법은 간단합니다. list_key를 알맞게 생성된 키를 가르키는 pthread_key_t 변수라고 가정하면 이런식으로 쓰면 됩니다:

int rc = pthread_key_delete(key);

성공시에는 0을 리턴하고, 주어진 변수가 유효한 TSD 키를 가르키지 않을 경우에는 EINVAL을 리턴합니다.


완전한 예제(A Complete Example)

아직 없습니다. 생각할 시간이 좀 필요하네요. 죄송합니다. 지금 당장 제가 생각할 수 있는 것은 '전역 변수는 아주 나쁘다'라는 것입니다. 앞으로 좋은 예제를 찾아보도록 하겠습니다. 혹시 좋은 예제가 있다면 제게 알려주시기 바랍니다.


쓰레드 취소와 끝내기

쓰레드를 만들었으니 끝내는것도 생각해 볼까요? 몇 가지를 살펴보죠. 쓰레드를 깨끗하게 끝낼 수 있어야 하겠죠. 그리고 아주 고약한 방법인 시그널을 사용하는 프로세스 포크(fork)와는 달리 pthread 라이브러리는 좀 더 신중하게 디자인 돼서 쓰레드를 취소한다든지 끝난 다음의 청소 작업등에 대한 완전한 시스템을 제공합니다. 한 번 살펴보죠.


Canceling A Thread

쓰레드를 끝내려고 할 때는 pthread_cancel를 쓰면 됩니다. 이 함수는 쓰레드 ID를 파라미터로 받아 그 쓰레드 ID로 취소 요청을 보냅니다. 이 요청에 대해 그 쓰레드가 어떻게 할 지는 그 쓰레드의 상태에 달려 있습니다. 즉시 취소될 수도 있고, 취소 위치(뒤에서 설명합니다)에 다다랐을때 취소될 수도 있고, 아예 무시해 버릴 수도 있습니다. 어떻게 쓰레드의 상태를 설정하며 취소 요청에 대해 어떻게 동작하는지에 대한 설정등에 대해서는 뒤에서 살펴보도록 하죠. 일단은 취소 함수를 어떻게 쓰는지 보겠습니다. 'thr_id'는 돌고 있는 쓰레드의 pthread_id 를 갖고 있는 변수라고 합시다.


pthread_cancel(thr_id);


pthread_cancel()은 0을 리턴하기 때문에 성공여부를 알 수가 없습니다.


쓰레드 취소 상태 설정하기

쓰레드의 취소 상태는 여러가지 방법으로 바꿀 수 있습니다. 첫번째는 pthread_setcancelstate() 함수를 쓰는 것입니다. 이 함수는 취소 요청을 받아 들일 것인지 아닌지를 결정합니다. 두 개의 파라미터가 필요한데, 하나는 새로운 취소 상태가 설정되어 있어야 하고 하나는 이전 취소 상태가 담겨질 변수입니다. 어떻게 쓰는지 보세요.


int old_cancel_state;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancel_state);


이 함수를 부른 쓰레드는 취소될 수가 없습니다. 취소될 수 있도록 하려면 다음처럼 하면 됩니다.


int old_cancel_state;
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_cancel_state);


두 번째 파라미터를 NULL로 넘겨주게 되면 예전 취소 상태에 대해서 알 수가 없습니다.

비슷하게 pthread_setcanceltype()이란 함수가 있는데 이 함수는 취소 요청에 대한 반응을 결정합니다. 이 때 이 쓰레드는 취소될 수 있다고 가정합니다. 가능한 반응으로는 취소 요청을 즉시(비동기적으로) 처리하는것과 취소 위치에 도착하기 전까지 취소를 미루는 것입니다. 다음은 비동기적 취소 방법 입니다.


int old_cancel_type;
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &old_cancel_type);


취소 위치까지 취소를 미루는 것은 다음처럼 하면 됩니다.


int old_cancel_type;
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &old_cancel_type);


두 번째 파라미터를 NULL로 이 함수를 부르면 예전 취소 상태에 대해 알 수 없습니다.

"취소 상태랑 타입을 설정 안 하면 어떻게 되나요?"라고 묻는다면 pthread_create()가 자동으로 PTHREAD_CANCEL_ENABLE (취소 요청 처리)과 PTHREAD_CANCEL_DEFERRED(취소 미룸)를 설정해 준다라고 대답해 드리죠.


취소 위치

지금까지 살펴본 것처럼, 쓰레드는 취소 요청을 즉시 처리하지 않을 수 있습니다. 대신 취소 위치에 도착할 때까지 그 요청을 미룰 수가 있습니다. 그럼 도대체 취소 위치란게 뭘까요?

보통, 쓰레드 실행을 오랫동안 정지시키는 함수는 취소 위치가 될 수 있습니다. 실제로는 특정 구현에 따라 달라지지고 얼마나 POSIX 표준을 따르냐(어느 버전의 표준)에 따라 달라집니다만 다음 함수들은 취소 위치입니다.

  • pthread_join()
  • pthread_cond_wait()
  • pthread_cond_timedwait()
  • pthread_testcancel()
  • sem_wait()
  • sigwait()
무슨 소리냐면 쓰레드가 이 중 한 함수를 실행중일 때, 뒤로 미룰 취소 요청이 있는지 확인하고 취소 요청이 들어와 있으면 자기가 끝난 다음에 취소 작업을 실행한 뒤 종료하게 됩니다. 이런 함수들이 실행중이 아니라면 방법은 한 가지 밖에 없는데, pthread_testcancel()를 쓰는 것입니다. 이 함수는 현재 쓰레드에서 대기중인 취소 요청이 있는지 확인해서 있다면 취소 작업을 실행하고, 없다면 그냥 리턴합니다. 보통 취소 상태로 들어가지 않고 긴 작업을 수행하는 쓰레드에서 쓰일 수 있습니다.

주의사항: 실제 pthread 표준에 일치하는 구현에서는, 프로세스를 블럭시키는 read(), select(), wait()등등의 시스템 콜들도 역시 취소 위치가 됩니다. 또한, 이 시스템 콜을 쓰는 표준 C 라이브러리들도 역시 마찬가지입니다(예를 들면 다양한 버전의 printf 함수들).


쓰레드 청소 함수 세팅하기(Setting Thread Cleanup Functions)

pthead 라이브러리가 제공해주는 기능중에, 자신이 종료하기 전에 자기 자신이 쓰던 리소스를 깨끗히 정리해주는 것이 있습니다. 이는 pthread 라이브러리에 의해서 자동으로 관련 함수가 불리거나 필요해 의해 스스로 부를 수 있기 때문에 가능해 집니다(즉, 자신이 pthread_exit()를 부르거나, 다른 쓰레드에 의해 취소될 때).

이를 위해 두 개의 함수가 제공됩니다. 하나는 pthread_cleanup_push() 함수로서 현재 쓰레드용 청소 함수 집합에 새로운 청소 함수를 추가해 줍니다. pthread_cleanup_pop() 함수는 pthread_cleanup_push()에 의해 추가된 마지막 함수를 제거해 줍니다. 쓰레드가 종료될 때는, 해당 청소 함수들은 등록됐던 반대 순서롤 불리게 됩니다. 즉, 마지막에 등록된 청소 함수가 제일 처음 불리게 됩니다.

pthread_cleanup_push() 함수의 두 번째 파라미터로 넘긴 변수가 청소 함수의 파라미터로 넘겨져서 불리게 됩니다. 이것들이 어떻게 쓰이는지를 살펴보도록 하죠. 여기 예제에서는 쓰레드가 시작할 때 할당받았던 메모리를 반환하는데 이 함수들을 적용시켜 보겠습니다.




/* 등록할 청소 함수        */
/* 할당된 메로리의 포인터를 받고 프리시켜 줌  */
void
cleanup_after_malloc(void* allocated_memory)
{
    if (allocated_memory)
        free(allocated_memory);
}

/* 쓰레드 함수      */
/* thread-pool 서버 예제에서 썼던 함수 그대로... */
void*
handle_requests_loop(void* data)
{
    .
    .
    /* 이 변수는 나중에 쓰일 겁니다. 그냥 읽어 나가세요..         */
    int old_cancel_type;

    /* 지금 이 쓰레드의 시작 시각을 기억하기 위해서 약간의 메모리를 할당 받습니다. */
    /* MAX_TIME_LEN 은 앞에서 미리 정의된 매크로라고 가정합니다.          */
    char* start_time = (char*)malloc(MAX_TIME_LEN);

    /* 청소 함수를 등록합니다. */
    pthread_cleanup_push(cleanup_after_malloc, (void*)start_time);
    .
    .
    /* 쓰레드의 메인 루프입니다. 어떤 일들을 하겠죠... */
    .
    .
    .

    /* 그리고 끝으로, 청소 핸들러를 제거할텐데 이 방법이 좀 이상해 보이겠지만   */
    /* 밑의 주석을 잘 읽어 보세요.     */

    /* 현재 쓰레드를 취소 미룸 상태에 둡니다.      */
    pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &old_cancel_type);

    /* '1'을 넘기면, 청소 핸들러 집합에서 지워버리기 전에 청소 핸들러를 실행 시킵니다. */
    /* '0'을 넘기면, 청소 핸들러를 실행하지 않습니다.   */
    pthread_cleanup_pop(1);

    /* 쓰레드를 이전 취소 상태로 다시 되돌려 놓습니다.   */
    pthread_setcanceltype(old_cancel_type, NULL);
}

여기서 볼 수 있듯이, 메모리를 약간 할당한 뒤, 이 메모리를 쓰레드 종료 시점에서 프리 시키도록 청소 핸들러를 등록시킵니다. 메인 루프가 다 돌고 나면 청소 핸들러를 제거시키게 되는데, 이 때 등록한 함수, 같은 블럭에서 제거 시켜야 합니다. 왜냐하면, pthread_cleanup_push()pthread_cleanup_pop() (역주 : 원문은 pthread_cleanup_pop()과 pthread_cleanup_pop(), 오타임)이 실제로는 '{'와 '}'를 나타내는 매크로이기 때문입니다.

청소 함수를 제거할 때 이렇게 복잡한 코드를 쓰는 이유는 청소 함수 내에서 쓰레드가 취소되지 않게 하기 위해서입니다. 쓰레드가 비동기 취소 상태에 있을 수도 있기 때문에, 확실히 하기 위해서 취소 미룸 상태로 바꾼 다음에, 청소 함수를 제거하고, 마지막으로 이전 취소 상태로 되돌려 놓는 것입니다. 주의 할 점은, 쓰레드가 pthread_cleanup_pop() 자체내에서 취소 되지 않는다는 것인데, pthread_cleanup_pop()가 취소 위치가 아니기 때문입니다.


쓰레드 종료 동기화 하기(Synchronizing On Threads Exiting)

가끔은 다른 쓰레드가 끝나길 기다려야 하는 경우가 있습니다. pthread_join() 함수로 이 일을 할 수 있습니다. 이 함수는 두 개의 파라미터가 필요한데, 조인(join)될 쓰레드를 나타내는 pthread_t 타입의 변수와 해당 쓰레드의 종료 코드값이 담길(취소 됐다면 PTHREAD_CANCELED) void *의 주소를 나타내는 변수입니다. pthread_join() 함수는 이 함수를 부르는 쓰레드를, 조인될 쓰레드가 끝날 때까지 중지시킵니다.

예를 들어 앞에서 살펴봤던 thread-pool 서버 예제를 생각해 보죠. 코드의 끝 부분을 보면, sleep()을 불러서 프로세스가 끝나길 기다리고 있는 것을 볼 수 있습니다. 이렇게 한 이유는 메인 쓰레드가 다른 쓰레드의 지연된 처리가 끝났는지 어떤지를 알 수 있는 방법이 없기 때문입니다. 해결 방법은 이것이 비록 바쁜 루프가 되긴 하겠지만 지연된 요청이 없을 때까지 메인 쓰레드가 루프를 돌게 하는 것입니다.

지금까지 살펴 본 것들을 깔끔하게 구현하려면 다음 세가지 변경사항을 추가시키면 됩니다.

  1. 요청이 다 만들어지면, 플래그를 이용해서 핸들러 쓰레드에게 알려줍니다.
  2. 요청 큐가 비어 있을 때마다 더 이상 만들 요청이 있는지 없는지를 확인하게 합니다. 더 만들어질 요청이 없다면 쓰레드를 종료시킵니다.
  3. 메인 쓰레드는 자신이 만든 쓰레드들이 끝날 때까지 기다립니다.

앞에 두 가지 변경사항은 좀 쉽습니다. 'done_creating_requests'란 전역 변수를 하나 만들고 '0'으로 초기화를 시킨 다음, 각 쓰레드들은 조건 변수를 기다리기 전에 (즉, 요청 큐가 비어있을 때), 이 전역 변수 값을 확인합니다.
메인 쓰레드는 자신이 모든 요청을 다 만들어 낸 다음, 이 변수를 '1'로 세팅합니다. 그리고, 조건 변수에 브로드캐스트를 날려 혹시 조건 변수를 기다리고 있는 쓰레드가 확실히 'done_creating_requests' 플래그를 다시 확인 할 수 있게 해줍니다.

마지막 세번째 변경사항은 pthread_join() 루프로 처리할 수 있습니다. 각 핸들러 쓰레드마다 한 번씩 pthread_join()를 불러주면 됩니다. 이렇게 하면 모든 핸들러 쓰레드가 종료된 다음에 이 루프가 끝나게 됩니다. 따라서 전체 프로세스를 안전하게 종료할 수 있습니다. 만약에 이 루프를 쓰지 않는다면 핸들러 쓰레드가 요청을 처리하고 있는 중간에 전체 프로세스를 끝낼 가능성이 있습니다.

변경된 프로그램은 thread-pool-server-with-join.c 에서 볼 수 있습니다. 세 가지 변경 사항은 소스에서 'CHANGE'(대문자)란 곳을 찾아 보면 됩니다.


쓰레드 떼어내기(Detaching A Thread)

지금까지 pthread_join() 함수를 써서 쓰레드가 어떻게 조인 되는지를 살펴 봤습니다. 사실 조인가능한(join-able) 상태에 있는 쓰레드는 꼭 다른 쓰레드에 의해서 조인되어야 합니다. 그렇지 않다면 그 쓰레드가 갖고 있던 메모리 리소스가 완전하게 제거되지 않을 것입니다. 이는, 부모 프로세스가 자식 프로세스를 거둬들이지 않는 상황과 비슷합니다('고아'나 '좀비'프로세스라고 부르죠).

만약에 어떤 쓰레드가 다른 쓰레드에 조인이 필요없이 아무때나 종료하고 싶다면, 그 쓰레드는 떨어진(detached) 상태에 있어야 합니다. 이렇게 하려면, pthread_create()에 적당한 플래그를 줘서 쓰레드를 만들어 내던지, pthread_detach() 함수를 쓰면 됩니다. 여기서는 두 번째 방법을 살펴 보겠습니다.

pthread_detach() 함수는 파라미터가 한 개 필요합니다. 파라미터는 pthread_t 형으로서 떨어진(detached) 상태로 놓을 쓰레드를 나타냅니다. 예를 들면, 다음 코드처럼 쓰레드를 만들자마자 바로 떨어지게(detach) 할 수 있습니다.


pthread_t a_thread;   /* 쓰레드 구조체를 담을 변수              */
int rc;               /* pthread 함수의 리턴값을 위한 변수           */
extern void* thread_loop(void*); /* 쓰레드의 메인 함수를 선언 */

/* 새 쓰레드를 만드는데... */
rc = pthread_create(&a_thread, NULL, thread_loop, NULL);

/* 성공이라면 새 쓰레드를 떼어낸다. */
if (rc == 0) {
    rc = pthread_detach(a_thread);
}


물론, 떨어진(detached) 상태의 쓰레드를 즉시 갖고 싶다면 첫번째 방법 (pthread_create()를 부를 때, 떨어진(detached) 상태를 세트해서 부름)을 쓰는 것이 더 효과적입니다.


쓰레드 취소 - 완전한 예제(Threads Cancellation - A Complete Example)

다음 예제는 지금까지 예제들보다 훨씬 큰 예제입니다. 이 예제는 C에서, 약간은 깔끔한 멀티 쓰레드 프로그램을 어떻게 만드는가를 보여줍니다. 앞에서 썼던 thread-pool 서버 예제를 사용하겠습니다. 이 예제는 두 가지 면에서 업그레이드 될 텐데, 하나는 요청의 부하에 따라 핸들러 쓰레드의 숫자를 조절하는 기능입니다. 요청 큐가 커지면 새 쓰레드가 만들어지고, 큐가 다시 줄어들면 필요없는 쓰레드는 취소 될 것입니다.

두 번째는, 더 이상 처리할 요청이 없을 때 서버의 종료 방법을 고칠 것입니다. 깔끔하지 못한 sleep()을 쓰는 대신, 각 핸들러 쓰레드들이 자신의 마지막 요청을 처리하고 종료할 때까지, 메인 쓰레드가 pthread_join()을 써서 기다리도록 할 것입니다.

다음처럼 4개의 파일로 나눠서 구현됐습니다.

  1. requests_queue.c - 이 파일에는 요청 큐를 처리하는 함수들이 있습니다. add_request()get_request() 함수를 여기에 넣었는데, 앞에서 전역 변수로 정의됐던 큐 헤드용 포인터와, 요청 카운터, 큐용 뮤텍스, 조건 변수를 하나의 구조체로 묶어서 같이 넣었습니다. 이렇게 해서, 데이타에 대한 모든 조작이 한 파일 안에서 일어나게 되고, 이 파일안에 있는 모든 함수는 'requests_queue'라는 구조체에 대한 포인터를 받습니다.
  2. handler_thread.c - 이 파일은 각 핸들러 쓰레드가 실행 시킬 메인 루프를 돌리는 함수들이 있습니다. (이 버전에서의 'handle_requests_loop()' 함수와, 밑에서 설명할 몇 가지 지역 함수들). 각 쓰레드간에 주고 받을 데이타들을 위한 구조체를 정의하고, pthread_create()에 파라미터로 그 구조체의 포인터를 넘깁니다. 이렇게 해서 세련되지 못한 전역변수의 사용을 대신합니다. 이 구조체에는 쓰레드 ID, 요청 큐 구조체에 대한 포인터, 뮤텍스, 조건 변수가 들어 있습니다.
  3. handler_threads_pool.c - 여기서 쓰레드 풀(pool)의 추상화를 정의합니다. 여기에는 쓰레드를 만드는 함수, 취소시키는 함수, 프로그램 종료시 모든 활성화된 쓰레드를 없애는 함수들이 들어 있습니다. 요청큐에서처럼 구조체를 정의해서 쓰겠습니다. 이것들에 대해서는 메인 쓰레드 혼자만 접근하기 때문에 뮤텍스로 이것들을 막을 필요가 없습니다. 이렇게 하면 뮤텍스에 의한 약간의 오버헤드를 줄일 수 있는데, 비록 이런 오버헤드가 작을지라도, 아주 바쁜 서버에서는 큰 영향을 미치기 때문입니다.
  4. main.c - 그리고 마지막으로, 이 모든 것들을 묶고, 관리하는 메인 함수입니다. 이 함수는 요청큐와 쓰레드 풀(pool), 핸들러 쓰레드들을 만들고, 요청을 발생시킵니다. 그 요청이 큐로 들어간 다음에는 큐 크기와 현재 활성화된 쓰레드의 숫자를 확인해서 큐 크기에 맞게 쓰레드 수를 조절합니다. 수위(water-mark) 알고리즘을 사용하는데, 코드를 보면 알겠지만, 좀 더 세련되고 복잡한 알고리즘으로 쉽게 바꿀 수가 있습니다. 여기서 쓰인 수위(water-mark) 알고리즘은 간단합니다. 수위가 높아지면 큐를 빨리 비우기 위해서 쓰레드들을 새로 만들어 내고, 수위가 낮아지면 원래 핸들러 쓰레드를 제외한 나머지 쓰레드들은 취소 시킵니다.

원래 프로그램을 좀 더 다루기 쉽게 고친 다음에 우리가 새로 배운 pthread 함수들을 다음과 같이 적용시켰습니다.

  1. 각 핸들러 쓰레드는 취소 미룸 상태로 만들어 집니다. 이렇게 하면 이 쓰레드들이 취소가 됐을때, 현재 처리중인 요청을 다 처리한 다음에 종료할 수 있게 됩니다.
  2. 각 핸들러 쓰레드는 또한 청소 함수를 등록하는데, 각 쓰레드가 종료시 뮤텍스를 풀고 종료토록 하기 위해서입니다. 이는 아마 거의 대부분이 취소 상태인 pthread_cond_wait()에서 취소 명령을 받을 것이기 때문에 정확히 동작 할 것입니다. 만약에 뮤텍스를 건 다음에 최소되거나 종료되면 다른 모든 쓰레드가 그 뮤텍스에 의해 '멈춰버릴' 것입니다. 따라서 청소 핸들러( pthread_cleanup_push() 함수로 등록함)에 뮤텍스를 풀어 주는 함수를 등록하는 것은 아주 확실한 해결책이 될 것입니다.
  3. 끝으로, 메인 쓰레드는 대충 종료하지 않고 아주 정확하게 종료되도록 세트됩니다. 종료할 시점이 되면, 'delete_handler_threads_pool()' 함수를 불러서 남아 있는 핸들러 쓰레드들을 기다리도록 pthread_join을 부릅니다. 이렇게 함으로써, 모든 핸들러 쓰레드가 자신의 마지막 요청을 다 처리하고 난 다음에 이 'delete_handler_threads_pool()' 함수가 리턴하게 됩니다.

자, 이제 소스 코드를 통해 모든 것을 살펴보시기 바랍니다. 헤더 파일을 먼저 읽으면 전체 디자인을 이해하기 쉽습니다. 컴파일하려면, thread-pool-server-changes 디렉토리로 들어가 'gmake'라고 치면 됩니다.

연습문제 1 : 마지막 예제 프로그램에는 종료 시점에 약간의 경쟁 상태(race condition)가 존재합니다. 이 경쟁이 뭐에 대한 건지 알 수 있겠습니까? 이 문제에 대해서 완전한 해결책을 제시할 수 있습니까?(힌트 - 'delete_handler_thread()'함수를 써서 쓰레드를 없애려고 할 때 무슨 일이 생기는지 생각해 보세요)

연습문제 2 : 우리가 사용한 수위(water-mark) 알고리즘은 새 쓰레드를 만들어 낼 때, 너무 느리게 동작하는 것 같습니다. 요청들이 처리 되기전에 큐에서 기다리는 평균 시간을 줄일 수 있는 다른 알고리즘을 생각해 보세요. 그리고 이 시간을 잴 수 있는 코드를 넣어 보세요. 여러분의 "최적화된 풀(pool) 알고리즘"을 찾을 때까지 계속 실험을 해 보세요. 주의 사항 - 시간을 재는 것은 getrusage, 시스템 콜로 할 수 있습니다. 정확한 측정값을 위해 각 알고리즘을 여러번 실행 시켜보시기 바랍니다.


쓰레드를 이용한 사용자 인터페이스 프로그래밍(Using Threads For Responsive User Interface Programming)

쓰레드가 아주 유용하게 쓰일 수 있는 분야로 유저 인터페이스(user interface)용 프로그램이 있습니다. 이런 프로그램들은 보통 한 곳에서 루프를 돌면서 사용자 입력을 읽고, 처리한 다음, 결과를 보여주는 식으로 되어 있습니다. 만약에 처리 부분이 시간을 아주 오래 잡아 먹고 있다면 사용자는 이 동작이 끝날 때까지 계속 기다려야 합니다. 이런 긴 처리 부분을 독립된 쓰레드로 돌리고, 다른 쓰레드에서는 사용자 입력을 받게 한다면, 그 프로그램은 좀 더 사용자의 반응에 민감하게 될 것입니다. 사용자는 그 긴 동작 중간에 취소를 시킬 수 있게 됩니다.

그래피컬한 프로그램에서는 이 문제가 더욱 심각해 집니다. 왜냐하면, 이런 프로그램은 자신의 윈도우를 다시 그리도록 윈도우 시스템에서 오는 메세지를 항상 기다리고 있어야 하기 때문입니다. 만약에 다른 일을 하느라고 너무 바쁘다면 자신의 윈도우는 텅 비어 있을 것입니다. 아주 안 좋아 보이죠. 이런 경우에, 한 쓰레드가 윈도우 시스템의 메세지를 처리하는 루프를 돌리면서, 다시 그리라는 요청에 항상 응답 할 수 있게 하는 것은 아주 좋은 방법입니다( 사용자 입력에 대해서도 마찬가지겠죠). 이렇게 오래 걸릴법한 동작이 필요하다 싶으면(최악의 경우에 0.2초 이상이라고 합시다), 독립된 쓰레드로 돌게 하십시요.

세번째 쓰레드를 쓰는 좀 더 좋은 방법이 있습니다. 이 세번째 쓰레드가 사용자 입력 쓰레드와 작업 수행 쓰레드의 제어와 동기화를 맏게 하는 것입니다. 사용자 입력 쓰레드가 사용자 입력을 받으면 제어 쓰레드에게 이 일을 처리하도록 요청하고, 작업 수행 쓰레드가 자신의 일 처리를 끝내면 결과를 사용자에게 보여주도록 제어 쓰레드에게 요청하게 하는 것입니다.


사용자 인터페이스 - 완전한 예제(User Interaction - A Complete Example)

사용자가 중간에 취소 시킬 수 있는, 파일에서 줄 수를 읽어들이는 간단한 문자 모드 프로그램을 작성해 보겠습니다.

메인 쓰레드는 줄 수를 세도록 쓰레드 하나를 만듭니다. 다음으로 사용자 입력을 확인하도록 두번째 쓰레드를 만듭니다. 그리고나서, 메인 쓰레드는 조건 변수를 기다립니다. 아무 쓰레드나 자신의 일을 마치면, 이 조건 변수에 시그널을 날려서 메인 쓰레드가 알게 합니다. 사용자의 취소 요청이 일어났는지 아닌지를 확인하기 위해서 전역 변수를 씁니다. '0'으로 초기화를 시키는데 만약에 사용자 쓰레드가 취소 요청을 받는다면(사용자가 'e'를 누른다면), 이 전역 변수를 '1'로 세팅하고 조건 변수에 시그널을 날리고 종료합니다. 줄 수를 세는 쓰레드는 자신의 계산이 다 끝났을 경우에만 조건 변수에 시그널을 날릴 것입니다.

프로그램을 읽기 전에 system() 함수의 사용법과 'stty' 유닉스 명령어에 대해서 설명드리겠습니다. system() 함수는 파라미터로 받아 들인 유닉스 명령어를 실행시킬 쉘을 하나 생성합니다. stty 유닉스 명령어는 터미널 모드 세팅을 바꾸는데 쓰입니다. 우리는 터미널을 라인 버퍼 모드에서 캐릭터 모드(raw 모드라고도 하죠)로 바꾸는데 썼습니다. 이렇게 하면, 사용자 입력 쓰레드에서 getchar() 를 부를 때, 사용자가 키를 누르자마자 즉시 리턴하도록 해줍니다. 만약에 이렇게 하지 않는다면, 사용자가 엔터(ENTER) 키를 누를 때까지 사용자의 입력을 버퍼에 저장해 놓을 것입니다. 끝으로, 이 캐릭터 모드는 별로 쓸모가 없기 때문에 프로그램이 종료하고 쉘 프롬프트를 다시 받으면, 사용자 입력 쓰레드는 원래의 터미널 모드(라인 버퍼 모드)로 돌아가도록 청소 함수를 등록시킵니다. 더 자세한 내용은 stty 매뉴얼을 참고하세요.

프로그램 소스는 line-count.c에서 받을 수 있습니다. 이 프로그램이 읽을 파일 이름은 'very_large_data_file'이라고 하드 코드 되어 있습니다. 이 이름의 파일을 하나 만드셔도 되고(충분한 시간동안 동작이 이뤄지도록 크게 만드세요), 저희가 제공하는 'very_large_data_file.Z' 파일을 받으셔서 압축을 풀어 사용하셔도 됩니다. 압축 푸는 명령어는 다음처럼 하시면 됩니다.

uncompress very_large_data_file.Z

압축이 풀리면 5메가(!) 짜리 'very_large_data_file'이 생기니까, 압축을 풀기 전에 디스크 용량이 충분한지 확인하시기 바랍니다.


멀티 쓰레드 어플리케이션에서 비시스템 라이브러리 쓰기(Using 3rd-Party Libraries In A Multi-Threaded Application)

멀티 쓰레드를 프로그램에 적용하려는 프로그래머에게 아주 중요한 것 하나만 더 말씀드리겠습니다. 멀티 쓰레드 프로그램은 동시에 똑같은 함수를 실행시킬 수도 있기 때문에, 한 쓰레드이상에서 동시에 실행 될지도 모르는 함수는 꼭 MT-safe(Multi-Thread Safe:멀티 쓰레드에 안전)해야 합니다. MT-safe한 함수 내부의 구조체나 다른 공유 리소스에 대한 접근이 뮤텍스로 보호된다는 뜻입니다.

멀티 쓰레드 프로그램에서 MT-safe하지 않는 라이브러리를 쓸 수 있는 가능한 방법은 두 가지가 있습니다.

  1. 오직 한 쓰레드에서만 이 라이브러리를 쓰기. 이 방법은 이 라이브러리 함수가 서로 다른 쓰레드에서 동시에 실행되지 않게 해줍니다. 하지만 이 방법은 문제가 있는데, 전체 설계에 제한 사항을 줄 수도 있다는 것입니다. 또한, 다른 쓰레드가 이 라이브러리의 함수를 쓰려고 할 가능성이 있기 때문에 쓰레드간 통신에 좀 더 신경을 써야 할 지도 모릅니다.
  2. 그 라이브러리 함수를 부를 때는 뮤텍스를 써서 보호할 것. 어느 쓰레드에서건 이 라이브러리의 함수를 부를 때는 하나의 뮤텍스를 쓰라는 뜻입니다. 뮤텍스를 걸고, 함수를 부르고, 뮤텍스를 푸는 순서로 사용하면 되겠습니다. 여기서 생길 수 있는 문제는 잠금이 그렇게 깔끔하게 이루어지지 않는다는 것입니다. 같은 라이브러리의 서로 다른 두 개의 함수가 서로 간섭하지 않는 독립된 함수임에도, 서로 다른 쓰레드에서 동시에 불릴 수 없을 수 있습니다. 두 번째 쓰레드는 첫번째 쓰레드가 함수 실행을 끝낼 때까지 뮤텍스에 걸려 있을 것입니다. 관련 없는 함수들에 대해서 서로 다른 두 개의 뮤텍스로 처리할 수도 있겠지만, 그 라이브러리가 실제로 어떻게 동작하는지 알 방법이 없기 때문에 어떤 함수끼리를 묶어야 하는지 알수가 없습니다. 거기다가, 혹시 안다고 할 지라도 새 버전의 라이브러리가 나왔을 때, 예전 버전과 다르게 동작할수도 있기 때문에 잠금 시스템 전체를 고쳐야 할 지도 모릅니다.
보시다시피, MT-safe하지 않은 라이브러리는 특별한 주의가 필요하기 때문에, 가능하면 비슷한 기능을 가진 MT-safe한 라이브러리를 찾아 쓰는 것이 제일 좋습니다.


쓰레드를 지원하는 디버거 쓰기(Using A Threads-Aware Debugger)

마지막 주의 사항입니다. 멀티 쓰레드 어플리케이션을 디버깅 할 때, 쓰레드를 "인식"하는 디버거가 필요합니다. 상용 개발 환경의 거의 대부분의 최신 디버거들은 모두 쓰레드를 처리할 수 있습니다. 리눅스에서, 거의 대부분의 배포판에 들어 있는 gdb는 쓰레드를 인식하지 못합니다. 'SmargGDB'라는 프로젝트가 있는데, gdb에 쓰레드 지원과, 그래픽 사용자 인터페이스(멀티 쓰레드 어플리케이션을 디버깅 할 때만 가능한)를 추가하는 프로젝트입니다. 어쨌든, 이것으로 다양한 사용자 레벨의 쓰레드 라이브러리를 쓰는 쓰레드 프로그램 에만 쓰이고, LinuxThreads를 디버깅하려면 커널 패치가 필요한데, 2.1.X 대 버전에서만 가능합니다. 더 자세히 알고 싶다면 http://hegel.ittc.ukans.edu/projects/smartgdb/를 찾아보시기 바랍니다. 또한 커널 2.0.32에 패치하는 법과 gdb 4.17을 쓰는 것에 대한 정보도 있는데, LinuxThreads homepage에서 찾아보시기 바랍니다.


Side-Notes

수위(water-mark) 알고리즘
버퍼나 큐를 처리할 때 주로 쓰이는 알고리즘입니다. 버퍼나 큐에 데이타를 채워넣다가, 크기가 상위 한계를 넘으면 큐에 넣는 것을 멈춥니다( 혹은 비우는 것을 좀 더 빠르게 합니다). 이 상태를 하위 한계 이하로 떨어질 때까지 계속 유지하다가 떨어지면, 큐에 채워넣기를 계속하게 됩니다( 혹은 비우는 속도를 원래 속도로 되돌려 놓습니다).


'c or linux' 카테고리의 다른 글

동적 메모리 - 메모리 크기 변경  (0) 2005.03.09
pthread 개념 - Application Development Guide --Core Components  (0) 2005.02.18
함수 포인터  (0) 2005.02.16
ctags 활용  (0) 2005.02.15
#ifdef __cplusplus  (1) 2005.02.12
Posted by '김용환'
,

함수 포인터

c or linux 2005. 2. 16. 05:47

오늘은 함수 포인터에 대해서 배워 보겠습니다.

함수 포인터는 말 그대로 함수를 가리키는 포인터 변수입니다. 여태껏 배워왔던 포인터와 똑같습니다.

여태껏 변수만을 가리키는 방법을 배워왔습니다.

하지만 포인터 변수는 함수도 가리킬수 있는 강력한 권한이 있습니다. 

저번에도 얘기했듯이 포인터 변수는 모두 4Byte입니다.

여지껏 배워온 포인터의 또다른 기법을 배워야 할것입니다.

포인터 변수에 함수의 시작번지를 저장시키고 저장된 시작번지의 주소를 가리키게 한후 실제 함수를

호출 하게 하면 끝입니다..

선언 하는 방법도 함수와 비슷하게 선언해줘야 합니다.

그래야 함수를 가리키는 포인터라고 인식을 하는거죠..

예를들어 다음과 같이 Start라는 함수가 있다고 봅시다.

이것을 포인터 변수가 가리키도록 해볼까요?

void Start()
{
   printf("안녕하세요");
}

void (*p)(); //함수를 가리키는 포인터 변수 4Byte짜리를 선언한다.

p = Start; // Start함수의 선두번지의 주소를 p포인터 변수가 가리킬수 있도록 p에 저장한다.

(*p)(); // p에 *를 붙이고 뒤에 ();를 붙여서 p가 가리키는 함수를 호출한다.

자 여기서 (*p)빼고는 Start와 리턴형과 파라미터가 없는것이 비슷합니다.

Start함수를 가리키기 위해서는 가리키는 함수와 똑같이 파라미터와 리턴값을 맞춰줘야 합니다.

void Start()
void (*p)()

자 (*p)와 이름만 틀릴뿐 비슷하죠?

자 여기서 *p가 붙어 함수를 가리키는 포인터 변수가 선언이 된것입니다.

함수를 가리키는 포인터 변수만 선언이 된것이지요.

포인터 변수에 Start함수의 시작주소를 저장시켜 볼까요?

p = Start;

Start 이름 자체는 함수의 시작번지를 가지고 있는 상수입니다!!!!!

그러므로 Start의 시작번지를 p에 저장하는 것입니다.

p가 가리키는 함수를 호출 해볼까요?

앞에 *를 붙여주면 자신이 가리키는 주소를 찾아가죠. 그리고 뒤에 ();를 붙여 함수를 찾아가라고

인식을 시켜줘야 합니다.

(*p)();를 호출하면 결국 Start();로 호출하는거와 똑같은 결과가 나타납니다.

결국 p가 가리키는 함수의 시작번지를 찾아가 Start()를 호출하는거죠.

자 다른 예제를 봅시다. 리턴값과 파라미터에 값을 전달하는 것입니다.

int Sum(int a, int b)
{
 printf("합은 [%d] 입니다",a+b);
 return a+b;
}

void main()
{
 int (*p)(int a, int b); //함수를 가리킬 수 있도록 포인터 변수 선언( 함수와 비슷하게 )

 p = Sum; //Sum함수의 선두번지를 p포인터에 저장

 int Value = (*p)(10,10); // p포인터가 가리키는 함수를 호출 리턴값을 Value에 저장

 printf("\r\n리턴값:%d", Value);
}

<결과>

합은 [20] 입니다
리턴값:20

다똑같죠 단지 (*p)이것만 해줬을 뿐이죠.

int Sum(int a, int b)를 가리키려면 반드시

int (*p)(int a, int b); <-- 이렇게 똑같은 형태로 변수를 선언해야 합니다.

(*p)이름을 변수처럼 자유롭게 사용할수 있죠..

포인터 변수라고 우선순위를 주기 위해서 괄호를 넣어 준것이구요.

아무튼 약간 이상해 보일지도 모르겠지만 이렇게 사용한다고 이해를 해두셔야 합니다.

마지막으로 알아야 할것은 함수를 가리키는 포인터 변수는 파라미터로 받을 수 있습니다.

당연히 변수니깐 파라미터로 받겠죠.

다음 예제를 봅시다.

void Hello()
{
  printf("Hello");
}

void Okay(void (*p)()) //파라미터를 함수를 가리키는 포인터로 파라미터를 받았습니다.
{
  (*p)(); //파라미터로 넘겨받은 선두번지를 찾아가 Hello함수를 실행합니다.
}

void main()
{
  Okay(Hello); // Hello함수의 선두번지를 파라미터로 넘겨준다.
}

이런식으로 아주 간단하게 넘길 수가 있습니다.

Okay파라미터에 void (*p)()를 그대로 넣어 줬습니다.

왜냐 함수를 가리키는 포인터는 변수이고 함수를 가리키기 위해서는 포인터 변수를 함수처럼

(*p)만 빼고 똑같이 선언해줘야 한다고 배웠습니다.

아무튼 리턴값과 파라미터는 항상 똑같이 해주어야 합니다 결코 잊지 마세요!

이상 함수 포인터를 마치겠습니다.

'c or linux' 카테고리의 다른 글

pthread 개념 - Application Development Guide --Core Components  (0) 2005.02.18
POSIX 쓰레드로 멀티 쓰레드 프로그래밍하기  (0) 2005.02.18
ctags 활용  (0) 2005.02.15
#ifdef __cplusplus  (1) 2005.02.12
Setjmp()  (0) 2005.02.11
Posted by '김용환'
,

ctags 활용

c or linux 2005. 2. 15. 01:35

출처 - JOINC WIKI

 

ctags 를 이용하면, 사용자 정의 함수가 나왔을때, 함수가 정의되어 있는 쏘스파일로 바로 점프할수 있으며, 또한 바로 원래의 쏘스파일로 되돌아올수 있다. 이렇게 함으로써 쏘스분석에 드는 시간을 상당히 줄일수 있다.
이번 문서에는 사용자가 vi 를 사용하고 있다는 가정하에 ctags 의 사용법을 설명하도록 하겠다.

ctags 는 공개 소프트웨어 임으로 쉽게 얻을수 있을것이다. 리눅스의 경우 웬만한 배포판은 기본으로 포함되어 있으니, 바로 사용하면 된다. 만약 설치되어 있지 않다면 www.rpmfind.org이나 www.freshmeat.net 등의 사이트에서 구해서 설치하면 된다.

일단 분석하고자 하는 쏘스의 디렉토리로 이동한다. 그리고 아래와 같은 방법으로 실행하면 된다.

[root@localhost user_admin_file]# ctags *
위에서 ctags 는 현재 디렉토리에 있는 모든 파일에 대해서 tags 정보를 작성하도록 했는데, 현재 디렉토리 뿐만 아니라 모든 하위디렉토리에 대해서 tags 정보를 작성하고자 한다면 "-R" 옵션을 사용하면 된다.
[root@localhost user_admin_file]# ctags -R
ctags 명령을 실행시키고 나면, ctags를 실행한 디렉토리에 tags 란 파일이 생길것이다. 여기에 각 함수가 어느파일에 설치되어 있는지에 대한 정보가 들어 있고, vi 를 실행 시키면 tags 파일을 참조하여 해당 함수가 정의되어 있는 파일로 자동으로 이동하게 된다.
tags 파일은 아래와 같이 구성되어 있다.
hello hello.c /^void hello(void)$/;" f

각필드는 "tab" 으로 구분된다. 첫번째 필드는 함수이름, 두번째 필드는 함수가 정의된 파일의 이름, 세번째 필드는(vi 를 좀 다룰줄 아는 유저 라면 익숙한 문장일 것이다), 해당 파일에서 vi가 함수명을 찾아가도록 하기위한 vi command 이다. 4번째 필드는 해당 함수의 타입이다. "f" 라면 일반 C 함수라는 뜻이며, "c" 는 클래스에 선언된 멤버함수, "d" 는 define 된 값이란 뜻이다.


이제 vi 로 분석하고자 하는 쏘스파일을 열어보자. 분석하는 중에 hello() 라는 알수없는 함수가 나와서 이 함수의 원형이 있는곳으로 이동하고 싶을 때는 hello() 함수에 커서를 위치시키고 "Ctrl + ]" 를 누르면 된다. 그러면 곧바로 hello() 함수의 원형으로 이동하게 된다.
hello() 함수에 대한 분석이 끝나서, 원래 분석하던 쏘스 파일로 돌아오고 싶다면 "Ctrl + t"키를 누르면 된다.

 

.vimrc에 set tags=d:\work\ttt\g4\base\cvm_hi\tags 이런식으로 vi가 쓸수있도록 set tags를 하느 것을 잊지 말아야 한다. = 용환 write.

 

-------------------------------------------------------------------------------------------

출처 - man page : ctags

NAME


       ctags - Generate tag files for source code

SYNOPSIS


       ctags [options] [file(s)]

       etags [options] [file(s)]

DESCRIPTION


       The  ctags and etags programs (hereinafter collectively referred to as
       ctags, except where distinguished) generate an index (or "tag") file
       for  a  variety of  language objects found in file(s). This tag file
       allows these items to be quickly and easily located by a text  editor
       or  other  utility.  A  "tag" signifies a language object for which an
       index entry is available (or, alternatively, the index  entry  created
       for that object).

       Alternatively,  ctags can generate a cross reference file which lists,
       in human readable form, information about the various  source  objects
       found in a set of language files.

       Tag  index  files  are  supported by numerous editors, which allow the
       user to locate the object associated with a name appearing in a source
       file and jump to the file and line which defines the name. Those known
       about at the time of this release are:

   Vi(1) and its derivatives (e.g. Elvis, Vim, Vile,  Lemmy),  CRiSP,
   Emacs,  FTE (Folding Text Editor), JED, jEdit, Mined, NEdit (Nir-
   vana Edit), TSE (The SemWare Editor),  UltraEdit,  WorkSpace,  X2,
   Zeus

       Ctags  is  capable  of  generating different kinds of tags for each of
       many different languages. For a complete list of supported  languages,
       the  names  by  which they are recognized, and the kinds of tags which
       are generated for each, see  the  --list-languages  and --list-kinds
       options.

SOURCE FILES


       Unless  the --language-force option is specified, the language of each
       source file is automatically selected based upon a  mapping  of file
       names  to  languages.  The mappings in effect for each language may be
       display using the --list-maps option and may  be  changed  using  the
       --langmap  option.   On platforms  which support it, if the name of a
       file is not mapped to a language and the file is executable, the first
       line  of the file is checked to see if the file is a "#!" script for a
       recognized language.

       By default, all other files names are ignored.  This  permits  running
       ctags  on  all files in either a single directory (e.g. "ctags *"), or
       on all files in an entire source directory  tree (e.g. "ctags -R"),
       since  only  those  files  whose names are mapped to languages will be
       scanned.

       [The reason that .h extensions are mapped to C++ files rather  than  C
       files is because it is common to use .h extensions in C++, and no harm
       results in treating them as C++ files.]

OPTIONS


       Despite the wealth of available options, defaults  are set  so that
       ctags  is  most commonly executed without any options (e.g. "ctags *",
       or "ctags -R"), which will create a tag file in the current  directory
       for  all recognized source files. The options described below are pro-
       vided merely to allow custom tailoring to meet special needs.

       Note that spaces separating  the  single-letter options  from their
       parameters are optional.

       Note  also that the boolean parameters to the long form options (those
       beginning with "--" and that take  a  "[=yes|no]"  parameter)  may  be
       omitted, in  which case "=yes" is implied. (e.g. --sort is equivalent
       to --sort=yes). Note further that "=1" and "=on" are  considered syn-
       onyms for "=yes", and that "=0" and "=off" are considered synonyms for
       "=no".

       Some options are either ignored or useful only when used while running
       in etags mode (see -e option). Such options will be noted.

       Most  options  may appear anywhere on the command line, affecting only
       those files which follow the option.  A few  options,  however, must
       appear before the first file name and will be noted as such.

       Options taking language names will accept those names in either upper
       or lower case. See the --list-languages option for a complete list  of
       the built-in language names.

       -a   Equivalent to --append.

       -B   Use backward  searching  patterns (e.g. ?pattern?). [Ignored in
    etags mode]

       -e   Enable etags mode, which will create a tag file for use with  the
    Emacs  editor.  Alternatively, if ctags is invoked by a name con-
    taining the string "etags" (either by  renaming,  or  creating  a
    link to, the executable), etags mode will be enabled. This option
    must appear before the first file name.

       -f tagfile
    Use the name specified by tagfile for the tag  file (default  is
    "tags",  or "TAGS" when  running in etags mode). If tagfile is
    specified as "-", then the tag file is written to standard output
    instead.  Ctags  will stubbornly refuse to take orders if tagfile
    exists and its first line contains something other than  a valid
    tags line. This will save your neck if you mistakenly type "ctags
    -f *.c", which would otherwise overwrite your first C  file with
    the tags  generated by the rest! It will also refuse to accept a
    multi character file name which begins with a '-' (dash)  charac-
    ter,  since this most likely means that you left out the tag file
    name and this option tried to grab the next option as  the file
    name.  If  you  really want to name your output tag file "-ugly",
    specify it as "./-ugly". This option must appear before the first
    file  name. If this option is specified more than once, only the
    last will apply.

       -F   Use forward  searching  patterns  (e.g.  /pattern/)   (default).
    [Ignored in etags mode]

       -h list
    Specifies  a list of file extensions, separated by periods, which
    are to be interpreted as include (or header) files. To  indicate
    files  having  no  extension, use a period not followed by a non-
    period character (e.g.  ".",  "..x",  ".x.").  This option only
    affects  how  the scoping of a particular kinds of tags is inter-
    preted (i.e. whether or not they are considered as globally visi-
    ble or  visible only within the file in which they are defined);
    it does not map the extension to any particular language. Any tag
    which  is  located in a non-include file and cannot be seen (e.g.
    linked to) from another file is considered to  have file-limited
    (e.g.  static) scope. No kind of tag appearing in an include file
    will be considered to have file-limited scope. If the first char-
    acter in the list is a plus sign, then the extensions in the list
    will be appended to the current list; otherwise,  the  list will
    replace the current list. See, also, the --file-scope option. The
    default list is  ".h.H.hh.hpp.hxx.h++.inc.def".  To restore  the
    default  list, specify -h default. Note that if an extension sup-
    plied to this option is not already mapped to a  particular lan-
    guage (see SOURCE FILES, above), you will also need to use either
    the --langmap or --language-force option.

       -I identifier-list
    Specifies a list of identifiers which are to be specially handled
    while parsing C and C++ source files. This option is specifically
    provided to handle special cases arising through the use of pre-
    processor  macros. When the identifiers listed are simple identi-
    fiers, these identifiers will be ignored during  parsing  of  the
    source  files. If an identifier is suffixed with a '+' character,
    ctags will also ignore  any parenthesis-enclosed  argument list
    which  may immediately follow the identifier in the source files.
    If two identifiers are separated  with  the '='  character,  the
    first identifiers is replaced by the second identifiers for pars-
    ing purposes. The list of identifiers may be supplied directly on
    the command  line or read in from a separate file. If the first
    character of identifier-list is '@', '.' or a pathname  separator
    ('/'  or '\'), or the first two characters specify a drive letter
    (e.g. "C:"), the parameter identifier-list will be interpreted as
    a  filename from  which  to  read a list of identifiers, one per
    input line. Otherwise, identifier-list is a list  of  identifiers
    (or identifier pairs) to be specially handled, each delimited by
    a either a comma or by white space (in which case the list should
    be quoted to keep the entire list as one command line argument).
    Multiple -I options may be supplied.  To clear the list of ignore
    identifiers, supply a single dash ("-") for identifier-list.

    This  feature is useful when preprocessor macros are used in such
    a way that they cause syntactic confusion due to their  presence.
    Indeed,  this is the best way of working around a number of prob-
    lems caused by the presence of syntax-busting  macros  in  source
    files  (see BUGS, below). Some  examples will illustrate this
    point.

       int foo ARGDECL4(void *, ptr, long int, nbytes)

    In the above example, the macro "ARGDECL4" would  be  mistakenly
    interpreted to be the name of the function instead of the correct
    name of "foo". Specifying -I  ARGDECL4  results  in the  correct
    behavior.

       /* creates an RCS version string in module */
       MODULE_VERSION("$Revision: 1.35 $")

    In the  above example the macro invocation looks too much like a
    function definition because it is not  followed  by a  semicolon
    (indeed,  it  could even be followed by a global variable defini-
    tion that would look much like a  K&R  style  function  parameter
    declaration).  In  fact,  this  seeming function definition could
    possibly even cause the rest of the file to be skipped over while
    trying  to complete the definition. Specifying -I MODULE_VERSION+
    would avoid such a problem.

       CLASS Example {
   // your content here
       };

    The example above uses "CLASS"  as a  preprocessor macro which
    expands  to something  different for each platform. For instance
    CLASS may be defined as "class  __declspec(dllexport)"  on Win32
    platforms  and  simply "class" on UNIX.  Normally, the absence of
    the C++ keyword "class" would cause the source file to be  incor-
    rectly  parsed. Correct behavior can be restored by specifying -I
    CLASS=class.

       -L file
    Read from file a list of file names for which tags should be gen-
    erated.   If  file is specified as "-", then file names are read
    from standard input. File names read using this option  are pro-
    cessed  following  file  names  appearing  on  the command line.
    Options all also accepted in this input. If this option is speci-
    fied more than once, only the last will apply. Note: file is read
    in line-oriented mode, where a new line is the only delimiter and
    spaces  are considered significant, in order that file names con-
    taining spaces may be supplied; this can affect how options  are
    parsed if included in the input.

       -n   Equivalent to --excmd=number.

       -N   Equivalent to --excmd=pattern.

       -o tagfile
    Equivalent to -f tagfile.

       -R   Equivalent to --recurse.

       -u   Equivalent to --sort=no (i.e. "unsorted").

       -V   Equivalent to --verbose.

       -w   This  option  is silently ignored for backward-compatibility with
    the ctags of SVR4 Unix.

       -x   Print a tabular, human-readable cross reference  (xref)  file  to
    standard output instead of generating a tag file. The information
    contained in the output includes: the tag name; the kind of tag;
    the line  number, file  name, and source line (with extra white
    space condensed) of the file which defines the tag. No  tag file
    is written and  all  options  affecting tag file output will be
    ignored. Example applications for this feature are generating  a
    listing  of all functions located in a source file (e.g. ctags -x
    --c-kinds=f file), or generating a list of all externally visible
    global  variables  located in  a source file (e.g. ctags -x --c-
    kinds=v --file-scope=no file). This option must appear before the
    first file name.

       --append[=yes|no]
    Indicates  whether tags generated from the specified files should
    be appended to those already present in the tag  file  or  should
    replace  them.  This  option  is off by default. This option must
    appear before the first file name.

       --etags-include=file
    Include a reference to file in the tag file. This option  may  be
    specified as many times as desired. This supports Emacs' capabil-
    ity to use a tag file which "includes" other tag  files.  [Avail-
    able only in etags mode]

       --exclude=[pattern]
    Add pattern  to  a list of excluded files and directories. This
    option may be specified as many times as desired. For  each file
    name  considered  by  ctags,  each pattern specified using this
    option will be compared against  both  the complete  path (e.g.
    some/path/base.ext) and  the  base name  (e.g. base.ext) of the
    file, thus allowing patterns which match a given file name irre-
    spective of its path, or match only a specific path. If appropri-
    ate support is available from the runtime library of your C com-
    piler,  then  pattern  may contain the usual shell wildcards (not
    regular expressions) common on Unix (be sure to quote the  option
    parameter  to  protect  the wildcards from being expanded by the
    shell before being passed to ctags; also be aware that  wildcards
    can match the slash character, '/'). You can determine if shell
    wildcards are available on your platform by examining the  output
    of the  --version option, which will include "+wildcards" in the
    compiled feature list; otherwise, pattern is matched against file
    names using a simple textual comparison.

    If pattern begins with the character '@', then the rest of the
    string is interpreted as a file name from which to read exclusion
    patterns, one per line. If pattern is empty, the list of excluded
    patterns is cleared.  Note that at program startup, the  default
    exclude  list  contains "EIFGEN", "SCCS", "RCS", and "CVS", which
    are names of directories for which it is generally not  desirable
    to descend while processing the --recurse option.

       --excmd=type
    Determines the  type  of  EX  command used to locate tags in the
    source file.  [Ignored in etags mode]

    The valid values for type (either the entire word  or  the first
    letter is accepted) are:

    number   Use only line numbers in the tag file for locating tags.
     This has four advantages:
     1. Significantly reduces the size of the resulting  tag
file.
     2. Eliminates  failures  to  find tags because the line
defining the tag has changed, causing the  pattern
match to fail (note that some editors, such as vim,
are able to recover in many such instances).
     3. Eliminates finding identical  matching,  but  incor-
rect, source lines (see BUGS, below).
     4. Retains  separate  entries in the tag file for lines
which are identical in content.  In  pattern mode,
duplicate  entries  are  dropped  because the search
patterns they generate are  identical,  making  the
duplicate entries useless.

     However,  this  option  has  one  significant  drawback:
     changes to the source files can cause the line  numbers
     recorded  in the tag file to no longer correspond to the
     lines in the source file, causing jumps to some tags  to
     miss  the target definition by one or more lines. Basi-
     cally, this option is best used when the source code  to
     which  it is applied is not subject to change. Selecting
     this option type causes  the  following  options  to  be
     ignored: -BF.

    pattern  Use  only search patterns for all tags, rather than the
     line numbers usually used for  macro  definitions. This
     has  the advantage of not referencing obsolete line num-
     bers when lines have been added or removed since the tag
     file was generated.

    mixed    In this  mode,  patterns are generally used with a few
     exceptions. For C, line numbers are used for macro defi-
     nition  tags.  This  was the default format generated by
     the original ctags and is, therefore,  retained  as  the
     default  for  this option. For Fortran, line numbers are
     used  for common blocks because their corresponding
     source  lines  are generally  identical, making pattern
     searches useless for finding all matches.

       --extra=[+|-]flags
    Specifies whether to include extra tag entries for certain kinds
    of information. The parameter flags is a set of one-letter flags,
    each representing one kind of extra tag entry to include  in  the
    tag file. If flags is preceded by by either the '+' or '-' char-
    acter, the effect of each flag is  added  to,  or  removed from,
    those  currently enabled; otherwise the flags replace any current
    settings. The meaning of each flag is as follows:

       f   Include an entry for the base file name  of every  source
   file (e.g. "example.c"), which addresses the first line
   of the file.

       q   Include an extra class-qualified tag entry  for  each  tag
   which is a member of a class (for languages for which this
   information is  extracted; currently  C++, Eiffel,  and
   Java).  The actual form of the qualified tag depends upon
   the language from which the tag was derived (using a form
   that is most natural for how qualified calls are specified
   in the language). For C++, it is in the form "class::mem-
   ber";  for  Eiffel and Java, it is in the form "class.mem-
   ber". This may allow easier location of  a  specific tags
   when multiple  occurrences of a tag name occur in the tag
   file. Note, however, that this could potentially more than
   double the size of the tag file.

       --fields=[+|-]flags
    Specifies the available extension fields which are to be included
    in the entries of the tag file (see TAG FILE FORMAT,  below,  for
    more  information). The  parameter flags is a set of one-letter
    flags, each representing one type of extension field to  include,
    with  the  following  meanings  (disabled by default unless indi-
    cated):

       a   Access (or export) of class members
       f   File-restricted scoping [enabled]
       i   Inheritance information
       k   Kind of tag as a single letter [enabled]
       K   Kind of tag as full name
       l   Language of source file containing tag
       m   Implementation information
       n   Line number of tag definition
       s   Scope of tag definition [enabled]
       S   Signature of routine (e.g. prototype or parameter list)
       z   Include the "kind:" key in kind field

    Each letter or group of letters may be preceded by either '+'  to
    add it  to the default set, or '-' to exclude it. In the absence
    of any preceding '+' or '-' sign,  only  those  kinds  explicitly
    listed  in flags will be included in the output (i.e. overriding
    the default set). This option is ignored if the option --format=1
    has been specified. The default value of this options is fks.

       --file-scope[=yes|no]
    Indicates  whether tags scoped only for a single file (i.e. tags
    which cannot be seen outside  of  the  file in  which  they  are
    defined, such as "static" tags) should be included in the output.
    See, also, the -h option. This option is enabled by default.

       --filter[=yes|no]
    Causes ctags to behave as a filter, reading  source  file names
    from standard input and printing their tags to standard output on
    a file-by-file basis. If --sorted is  enabled,  tags  are  sorted
    only within the source file in which they are defined. File names
    are read from standard output in line-oriented  input  mode (see
    note  for -L option) and only after file names listed on the com-
    mand line or from any file supplied using  the  -L option. When
    this  option  is  enabled, the  options -f, -o, and --totals are
    ignored. This  option  is  quite  esoteric and  is disabled  by
    default. This option must appear before the first file name.

       --filter-terminator=string
    Specifies a string to print to standard output following the tags
    for each file name parsed when the --filter option is  enabled.
    This  may  permit  an  application reading the output of ctags to
    determine when the output for each file is finished. Note that if
    the file name read is a directory and --recurse is enabled, this
    string will be printed only one once at the end of all tags found
    for by descending the directory. This string will always be sepa-
    rated from the last tag line for the file by its terminating new-
    line.   This  option  is  quite esoteric and is empty by default.
    This option must appear before the first file name.

       --format=level
    Change the format of the output  tag  file. Currently  the only
    valid values for level are 1 or 2. Level 1 specifies the original
    tag file format and level 2 specifies a new extended format con-
    taining extension fields (but in a manner which retains backward-
    compatibility with original vi(1) implementations). The  default
    level  is  2. This option must appear before the first file name.
    [Ignored in etags mode]

       --help
    Prints to standard output a detailed usage description  and then
    exits.

       --if0[=yes|no]
    Indicates  a  preference  as  to  whether  code within an "#if 0"
    branch of a preprocessor conditional should be examined for non-
    macro  tags (macro tags are always included). Because the intent
    of this construct is to disable code, the default value  of this
    options  is no.  Note  that this indicates a preference only and
    does not guarantee skipping code within an "#if 0" branch, since
    the fall-back  algorithm used to generate tags when preprocessor
    conditionals are too complex follows all  branches of  a  condi-
    tional. This option is disabled by default.

       --<LANG>-kinds=[+|-]kinds
    Specifies a list of language-specific kinds of tags (or kinds) to
    include in the output  file for  a particular  language, where
    <LANG>  is case-insensitive  and is one of the built-in language
    names (see the --list-languages option for a complete list).  The
    parameter  kinds is a group of one-letter flags designating kinds
    of tags (particular to the language) to either include or exclude
    from  the  output. The specific sets of flags recognized for each
    language, their meanings and  defaults  may be  list  using  the
    --list-kinds  option. Each letter or group of letters may be pre-
    ceded by either '+' to add it to, or '-' to remove it  from,  the
    default  set.  In  the  absence of any preceding '+' or '-' sign,
    only those kinds explicitly listed in kinds will be included  in
    the output (i.e. overriding the default for the specified lan-
    guage).

    As an example for the C language, in order to add prototypes  and
    external  variable declarations to the default set of tag kinds,
    but exclude macros, use --c-kinds=+px-d; to include only tags for
    functions, use --c-kinds=f.

       --langdef=name
    Defines a new user-defined language, name, to be parsed with reg-
    ular expressions. Once defined, name may be used in other options
    taking language names. The typical use of this option is to first
    define the language, then map file names to it  using  --langmap,
    then  specify  regular expressions using --regex-<LANG> to define
    how its tags are found.

       --langmap=map[,map[...]]
    Controls how file names are mapped to languages (see the  --list-
    maps  option).  Each comma-separated map consists of the language
    name (either a built-in or user-defined language), a colon, and a
    list  of file extensions and/or file name patterns. A file exten-
    sion is specified by preceding the extension with a period (e.g.
    ".c").  A file name pattern is specified by enclosing the pattern
    in parentheses (e.g. "([Mm]akefile)"). If appropriate support  is
    available  from  the runtime library of your C compiler, then the
    file name pattern may contain the usual shell wildcards common on
    Unix  (be  sure  to quote the  option  parameter to protect the
    wildcards from being expanded by the shell before being passed to
    ctags).  You  can  determine  if shell wildcards are available on
    your platform by examining the output of  the  --version  option,
    which  will include  "+wildcards" in the compiled feature list;
    otherwise, the file name patterns are matched against file names
    using a simple textual comparison.

    If the  first character in a map is a plus sign, then the exten-
    sions and file name patterns in that map will be appended to  the
    current  map  for  that language; otherwise, the map will replace
    the current map. For example, to specify  that  only  files with
    extensions of  .c and .x are to be treated as C language files,
    use "--langmap=c:.c.x"; to also add files with extensions  of  .j
    as Java  language files, specify "--langmap=c:.c.x,java:+.j". To
    map makefiles (.e.g files named either "Makefile", "makefile", or
    having the extension ".mak") to a language called "make", specify
    "--langmap=make:([Mm]akefile).mak".   To  map  files  having   no
    extension, specify a period not followed by a non-period charac-
    ter (e.g. ".", "..x", ".x."). To clear the mapping for a particu-
    lar language  (thus  inhibiting automatic generation of tags for
    that  language),  specify an   empty   extension list (e.g.
    "--langmap=fortran:").  To restore the default language mappings
    for all a particular language, supply the keyword  "default"  for
    the mapping.   To specify restore the default language mappings
    for all languages, specify "--langmap=default".  Note  that file
    extensions are  tested  before file name patterns when inferring
    the language of a file.

       --language-force=language
    By default, ctags automatically selects the language of a  source
    file,  ignoring  those  files whose language cannot be determined
    (see SOURCE FILES, above). This option forces the specified lan-
    guage  (case-insensitive;  either built-in or user-defined) to be
    used for every supplied file instead of  automatically  selecting
    the language  based upon its extension. In addition, the special
    value auto indicates that the language  should  be automatically
    selected (which effectively disables this option).

       --languages=[+|-]list
    Specifies the languages for which tag generation is enabled, with
    list containing a comma-separated list of language names  (case-
    insensitive;  either built-in or user-defined). If the first lan-
    guage of list is not preceded by either a '+' or '-', the current
    list  will be cleared before adding or removing the languages in
    list. Until a '-' is encountered, each language in the list will
    be added  to the current list. As either the '+' or removed from
    the current list, respectively.  Thus,  it  becomes  simple  to
    replace the current list with a new one, or to add or remove lan-
    guages from the current list. The actual list of files for which
    tags  will be generated depends upon the language extension map-
    ping in effect (see the --langmap option). Note  that  all lan-
    guages,  including user-defined  languages are  enabled  unless
    explicitly disabled using this option. Language names included in
    list  may be any built-in language or one previously defined with
    --langdef. The default is "all", which  is also  accepted as  a
    valid  argument.  See  the --list-languages option for a complete
    list of the built-in language names.

       --license
    Prints a summary of the software license to standard  output  and
    then exits.

       --line-directives[=yes|no]
    Specifies  whether "#line" directives should be recognized. These
    are present in the output of preprocessors and contain  the line
    number,  and  possibly  the file  name,  of  the original source
    file(s) from which the preprocessor output file  was  generated.
    When  enabled,  this  option  will cause  ctags  to generate tag
    entries marked with the file names and  line  numbers  of their
    locations  original source file(s), instead of their actual loca-
    tions in the preprocessor output. The actual  file names  placed
    into  the  tag file will have the same leading path components as
    the preprocessor output file, since it is assumed that the origi-
    nal source files are located relative to the preprocessor output
    file (unless, of course, the #line directive specifies  an abso-
    lute  path).  This option is off by default. Note: This option is
    generally only useful when used together with the  --excmd=number
    (-n)  option.  Also,  you may have to use either the --langmap or
    --language-force option if the extension of the preprocessor out-
    put file is not known to ctags.

       --links[=yes|no]
    Indicates  whether symbolic  links (if supported) should be fol-
    lowed. When disabled, symbolic links are ignored. This option  is
    on by default.

       --list-kinds[=language|all]
    Lists  the tag kinds recognized for either the specified language
    or all languages. Each kind of tag recorded in the tag  file  is
    represented by  a one-letter flag, which is also used to filter
    the tags placed into the output through use of the --<LANG>-kinds
    option.  Note  that some languages and/or tag kinds may be imple-
    mented using regular expressions and  may  not  be available  if
    regex  support is not compiled into ctags (see the --regex-<LANG>
    option). Each kind listed is enabled unless followed by  "[off]".

       --list-maps[=language|all]
    Lists  the file extensions and file name patterns which associate
    a file name with a language for either the specified language  or
    all languages. See the --langmap option, and SOURCE FILES, above.

       --list-languages
    Lists the names of the languages understood by ctags. These lan-
    guage  names  are  case insensitive and may be used in the --lan-
    guage-force,  --languages, --<LANG>-kinds, and   --regex-<LANG>
    options.

       --options=file
    Read  additional  options  from  file.  As a  special  case,  if
    --options=NONE is specified as the first option  on the  command
    line,  it will disable the automatic reading of any configuration
    options from either a file or the environment (see FILES).

       --recurse[=yes|no]
    Recurse into directories encountered  in  the  list of  supplied
    files. If the list of supplied files is empty and no file list is
    specified with the -L option, then the  current  directory (i.e.
    ".")  is  assumed. Symbolic links are followed. If you don't like
    these behaviors, either explicitly specify the files or pipe  the
    output  of find(1) into ctags -L- instead. Note: This option is
    not supported on all platforms at present. It  is available  if
    the output of the --help option includes this option.  See, also,
    the --exclude to limit recursion.

       --regex-<LANG>=/regexp/replacement/[kind-spec/][flags]
    The /regexp/replacement/  pair  define  a regular   expression
    replacement pattern,  similar  in style to sed substitution com-
    mands, with which to generate tags from source  files  mapped  to
    the named language, <LANG>, (case-insensitive; either a built-in
    or user-defined  language).  The  regular expression,   regexp,
    defines  an extended  regular  expression (roughly that used by
    egrep(1)), which is used to locate a single source line  contain-
    ing a tag and may specify tab characters using \t. When a match-
    ing line is found, a tag will be generated for the name  defined
    by replacement,  which  generally will contain the special back-
    references \1 through \9  to  refer to  matching  sub-expression
    groups  within  regexp. The '/' separator characters shown in the
    parameter to the option can actually be replaced by any  charac-
    ter. Note that whichever separator character is used will have to
    be escaped with a backslash ('\') character wherever it  is used
    in the parameter as something other than a separator. The regular
    expression defined by this option is added to the current list of
    regular expressions for the specified language unless the parame-
    ter is omitted, in which case the current list is cleared.

    Unless modified by flags, regexp is  interpreted as  a Posix
    extended  regular  expression.  The replacement should expand for
    all matching lines to a non-empty  string  of  characters, or  a
    warning  message will be reported. An optional kind specifier for
    tags matching regexp may follow replacement, which will determine
    what  kind of tag is reported in the "kind" extension field (see
    TAG FILE FORMAT, below). The full form of  kind-spec  is  in  the
    form  of  a single letter,  a comma, a name (without spaces), a
    comma, a description, followed by a separator, which specify  the
    short  and long forms of the kind value and its textual descrip-
    tion (displayed using --list-kinds). Either the kind name  and/or
    the description  may  be  omitted. If  kind-spec is omitted, it
    defaults to "r,regex". Finally, flags are one or more single-let-
    ter characters  having the following effect upon the interpreta-
    tion of regexp:

       b   The pattern is  interpreted as  a Posix  basic  regular
   expression.

       e   The pattern is  interpreted  as a Posix extended regular
   expression (default).

       i   The regular expression is to be applied in a case-insensi-
   tive manner.

    Note  that this  option  is available only if ctags was compiled
    with support for regular expressions,  which  depends  upon your
    platform. You can determine if support for regular expressions is
    compiled in by examining the  output  of  the  --version  option,
    which will include "+regex" in the compiled feature list.

    For more  information  on the regular expressions used by ctags,
    see either the regex(5,7) man page, or the GNU info documentation
    for regex (e.g. "info regex").

       --sort[=yes|no|foldcase]
    Indicates  whether the tag file should be sorted on the tag name
    (default is yes). Note that the original  vi(1)  required  sorted
    tags.   The foldcase  value specifies case insensitive (or case-
    folded) sorting.  Fast binary searches of tag files sorted with
    case-folding  will require special support from tools using tag
    files, such as that found in the ctags readtags library,  or  Vim
    version  6.2 or higher (using "set ignorecase"). This option must
    appear before the first file name. [Ignored in etags mode]

       --tag-relative[=yes|no]
    Indicates that the file paths recorded in the tag file should  be
    relative  to  the  directory containing the tag file, rather than
    relative to the current directory, unless the files supplied  on
    the command  line are specified with absolute paths. This option
    must appear before the first file name. The default is  yes when
    running in etags mode (see the -e option), no otherwise.

       --totals[=yes|no]
    Prints  statistics about  the source files read and the tag file
    written during the current invocation of ctags.  This  option  is
    off by  default. This option must appear before the first file
    name.

       --verbose[=yes|no]
    Enable verbose mode. This prints out information on option pro-
    cessing and a brief message describing what action is being taken
    for each file considered by ctags. Normally, ctags does not read
    command line arguments until after options are read from the con-
    figuration files (see FILES, below) and  the  CTAGS  environment
    variable.  However, if  this option is the first argument on the
    command line, it will take effect before  any  options  are read
    from these sources. The default is no.

       --version
    Prints a version identifier for ctags to standard output and then
    exits.  This is guaranteed to always contain the string  "Exuber-
    ant Ctags".

OPERATIONAL DETAILS


       As  ctags  considers each file name in turn, it tries to determine the
       language of the file by applying the following three tests  in  order:
       if  the file extension has been mapped to a language, if the file name
       matches a shell pattern mapped to a language, and finally if the file
       is  executable  and  its first line specifies an interpreter using the
       Unix-style "#!" specification (if supported on  the  platform). If  a
       language was  identified, the file is opened and then the appropriate
       language parser is called to operate on the currently open  file.  The
       parser  parses  through the file and adds an entry to the tag file for
       each language object it is written to handle.  See  TAG FILE  FORMAT,
       below, for details on these entries.

       This  implementation  of ctags imposes no formatting requirements on C
       code as do legacy  implementations.  Older  implementations  of ctags
       tended to rely upon certain formatting assumptions in order to help it
       resolve coding dilemmas caused by preprocessor conditionals.

       In general, ctags tries to be  smart  about  conditional preprocessor
       directives.  If a  preprocessor conditional  is encountered within a
       statement which defines a tag, ctags follows only the first branch  of
       that conditional (except in the special case of "#if 0", in which case
       it follows only the last branch). The reason for this is that  failing
       to  pursue  only one branch can result in ambiguous syntax, as in the
       following example:

      #ifdef TWO_ALTERNATIVES
      struct {
      #else
      union {
      #endif
  short a;
  long b;
      }

       Both branches cannot be followed,  or  braces  become  unbalanced  and
       ctags would be unable to make sense of the syntax.

       If  the application of this heuristic fails to properly parse a file,
       generally due to complicated and inconsistent pairing within the con-
       ditionals, ctags will retry the file using a different heuristic which
       does not selectively follow  conditional preprocessor  branches,  but
       instead falls  back to relying upon a closing brace ("}") in column 1
       as indicating the end of a block once any brace imbalance results from
       following a #if conditional branch.

       Ctags  will  also  try to specially handle arguments lists enclosed in
       double sets of parentheses in order to  accept  the  following  condi-
       tional construct:

      extern void foo __ARGS((int one, char two));

       Any  name immediately preceding the "((" will be automatically ignored
       and the previous name will be used.

       C++ operator definitions are specially handled. In order for  consis-
       tency  with  all types of operators (overloaded and conversion), the
       operator name in the tag file will always be preceded  by  the  string
       "operator  "  (i.e. even if the actual operator definition was written
       as "operator<<").

       After creating or appending to the tag file, it is sorted by  the  tag
       name, removing identical tag lines.

TAG FILE FORMAT


       When not running in etags mode, each entry in the tag file consists of
       a separate line, each looking like this in the most general case:

tag_name<TAB>file_name<TAB>ex_cmd;"<TAB>extension_fields

       The fields and separators of these lines are specified as follows:

   1.  tag name
   2.  single tab character
   3.  name of the file in which the object associated with  the  tag
       is located
   4.  single tab character
   5.  EX command used to locate the tag within the file; generally a
       search pattern (either /pattern/ or ?pattern?) or line  number
       (see  --excmd). Tag file format 2 (see --format) extends this
       EX command under certain circumstances to  include  a  set  of
       extension  fields  (described below) embedded in an EX comment
       immediately appended to the EX command, which leaves it back-
       ward-compatible with original vi(1) implementations.

       A  few  special tags  are written into the tag file for internal pur-
       poses. These tags are composed in such a way that they always sort  to
       the  top of  the  file. Therefore, the first two characters of these
       tags are used a magic number to detect a tag  file  for purposes  of
       determining  whether a valid tag file is being overwritten rather than
       a source file.

       Note that the name of each source file will be  recorded in  the  tag
       file exactly as it appears on the command line. Therefore, if the path
       you specified on the command line was relative to the  current  direc-
       tory,  then  it will be recorded in that same manner in the tag file.
       See, however, the --tag-relative option for how this behavior  can  be
       modified.

       Extension fields are tab-separated key-value pairs appended to the end
       of the EX command as a comment, as described above.  These  key value
       pairs  appear  in  the general form "key:value". Their presence in the
       lines of the tag file are controlled by the --fields option. The pos-
       sible keys and the meaning of their values are as follows:

       access    Indicates the visibility of this class member, where value
   is specific to the language.

       file    Indicates that the tag has file-limited  visibility. This
   key has no corresponding value.

       kind    Indicates  the  type, or kind, of tag. Its value is either
   one of the corresponding one-letter flags described under
   the various --<LANG>-kinds options above, or a full name.
   It is permitted (and is, in fact, the default) for the key
   portion  of this field to be omitted. The optional behav-
   iors are controlled with the --fields option.

       implementation
   When present,  this indicates  a  limited  implementation
   (abstract vs. concrete) of a routine or class, where value
   is specific to the language ("virtual" or  "pure  virtual"
   for C++; "abstract" for Java).

       inherits    When present, value. is a comma-separated list of classes
   from which this class is derived (i.e. inherits from).

       signature   When present, value. is a  language-dependent  representa-
   tion of the signature of a routine. A routine signature in
   its complete form specifies the return type of  a  routine
   and its  formal  argument  list.  This extension field is
   presently supported only for C-based languages  and does
   not include the return type.

       In  addition,  information  on  the scope of the tag definition may be
       available, with the key portion equal to some language-dependent con-
       struct  name and its value the name declared for that construct in the
       program. This scope entry indicates the scope in which the  tag  was
       found.  For  example,  a tag generated for a C structure member would
       have a scope looking like "struct:myStruct".

HOW TO USE WITH VI


       Vi will, by default, expect a tag file by the name "tags" in the cur-
       rent  directory. Once  the  tag file is built, the following commands
       exercise the tag indexing feature:

       vi -t tag   Start vi and position the cursor  at the  file  and line
   where "tag" is defined.

       :ta tag    Find a tag.

       Ctrl-]    Find the tag under the cursor.

       Ctrl-T    Return to previous location before jump to tag (not widely
   implemented).

HOW TO USE WITH GNU EMACS


       Emacs will, by default, expect a tag file by the name  "TAGS"  in  the
       current directory. Once the tag file is built, the following commands
       exercise the tag indexing feature:

       M-x visit-tags-table <RET> FILE <RET>
Select the tag file, "FILE", to use.

       M-. [TAG] <RET>
Find the first definition of TAG. The default tag  is  the
identifier under the cursor.

       M-* Pop back to where you previously invoked "M-.".

       C-u M-. Find the next definition for the last tag.

       For more commands, see the Tags topic in the Emacs info document.

HOW TO USE WITH NEDIT


       NEdit  version 5.1 and later can handle the new extended tag file for-
       mat (see --format). To make NEdit use the tag file, select "File->Load
       Tags  File".  To jump to the definition for a tag, highlight the word,
       the press Ctrl-D. NEdit 5.1 can can read multiple tag files from dif-
       ferent  directories.  Setting the X resource nedit.tagFile to the name
       of a tag file instructs NEdit to automatically load that tag  file  at
       startup time.

CAVEATS


       Because ctags is neither a preprocessor nor a compiler, use of prepro-
       cessor macros can fool ctags into either missing tags  or  improperly
       generating  inappropriate  tags. Although  ctags has been designed to
       handle certain common cases, this  is  the  single  biggest  cause  of
       reported problems.  In particular, the use of preprocessor constructs
       which alter the textual syntax of C  can fool  ctags.  You  can work
       around many such problems by using the -I option.

       White  space is treated as a separator for file names and options read
       from list files, specified using the -L option, and  in filter mode
       (specified  using the --filter option). Therefore, it is not currently
       possible to supply file names or other options containing  embedded
       white space (spaces, etc.) through these options.

       Note  that  when ctags generates uses patterns for locating tags (see
       the --excmd option), it is entirely possible that the wrong  line  may
       be  found  by your editor if there exists another source line which is
       identical to the line containing the tag. The following example demon-
       strates this condition:

      int variable;

      /* ... */
      void foo(variable)
      int variable;
      {
  /* ... */
      }

       Depending  upon which editor you use and where in the code you happen
       to be, it is possible that the search pattern  may  locate  the local
       parameter declaration in foo() before it finds the actual global vari-
       able definition, since the lines (and therefore their search  patterns
       are identical). This can be avoided by use of the --excmd=n option.

BUGS


       Ctags has more options than ls(1).

       When  parsing a C++ member function definition (e.g. "className::func-
       tion"), ctags cannot determine whether the scope specifier is a class
       name  or a namespace specifier and always lists it as a class name in
       the scope portion of the extension fields. Also, if a C++ function  is
       defined outside of the class declaration (the usual case), the access
       specification (i.e. public, protected, or private) and  implementation
       information  (e.g.  virtual,  pure  virtual) contained in the function
       declaration are not known when the tag is generated for the  function
       definition.   It will, however be  available for  prototypes (e.g
       --c++-kinds=+p).

       No qualified tags are generated for language objects inherited into  a
       class.

ENVIRONMENT VARIABLES


       CTAGS   If  this environment  variable exists, it will be expected to
       contain a set of default options which  are  read  when ctags
       starts, after the configuration files listed in FILES, below,
       are read, but  before  any  command  line  options  are read.
       Options appearing  on  the command line will override options
       specified in this variable. Only options will  be  read from
       this  variable. Note that all white space in this variable in
       considered a separator, making it impossible to pass an option
       parameter  containing an embedded space. If this is a problem,
       use a configuration file instead.

       ETAGS   Similar to the CTAGS variable above, this variable, if  found,
       will be read when etags starts. If this variable is not found,
       etags will try to use CTAGS instead.

       TMPDIR  On Unix-like hosts where mkstemp() is available, the value  of
       this variable specifies the directory in which to place tempo-
       rary files. This can be useful if the size of a temporary file
       becomes too large to fit on the partition holding the default
       temporary directory defined at compilation time.   ctags cre-
       ates  temporary files  only  if either (1) an emacs-style tag
       file is being generated, (2) the tag file  is  being  sent  to
       standard output,  or  (3)  the program was compiled to use an
       internal sort algorithm to sort the tag files instead  of  the
       the  sort utility of the operating system. If the sort utility
       of the operating system is  being  used,  it  will  generally
       observe this variable also. Note that if ctags is setuid, the
       value of TMPDIR will be ignored.

FILES


       /ctags.cnf (on MSDOS, MSWindows only)
       /etc/ctags.conf
       /usr/local/etc/ctags.conf
       $HOME/.ctags ($HOME/ctags.cnf on MSDOS, MSWindows)
       .ctags (ctags.cnf on MSDOS, MSWindows)
      If any  of  these configuration files  exist,  each  will  be
      expected to contain a set of default options which are read in
      the order listed when ctags starts, but before the CTAGS envi-
      ronment  variable is read or any command line options are read.
      This makes  it  possible to  set up  site-wide, personal  or
      project-level defaults. It is possible to compile ctags to read
      an additional configuration file before any  of those shown
      above,  which  will  be indicated if the output produced by the
      --version option lists the  "custom-conf"  feature.  Options
      appearing in  the CTAGS environment variable or on the command
      line will override  options  specified  in  these files. Only
      options  will  be read  from these files. Note that the option
      files are read in line-oriented mode in which spaces  are sig-
      nificant (since shell  quoting is not possible). Each line of
      the file is read as one command line parameter (as if  it were
      quoted  with  single quotes). Therefore, use new lines to indi-
      cate separate command-line arguments.

       tags   The default tag file created by ctags.

       TAGS   The default tag file created by etags.

SEE ALSO


       The official Exuberant Ctags web site at:

      http://ctags.sourceforge.net

       Also ex(1), vi(1), elvis, or, better yet, vim, the official editor  of
       ctags. For more information on vim, see the VIM Pages web site at:

      http://www.vim.org/

AUTHOR


       Darren Hiebert <dhiebert@users.sourceforge.net>
       http://DarrenHiebert.com/

MOTIVATION


       "Think  ye  at  all times of rendering some service to every member of
       the human race."

       "All effort and exertion put forth by man from  the  fullness  of  his
       heart  is  worship,  if it is prompted by the highest motives and the
       will to do service to humanity."

      -- From the Baha'i Writings

CREDITS


       This version of ctags was originally derived from and inspired by  the
       ctags  program  by  Steve  Kirkendall <kirkenda@cs.pdx.edu> that comes
       with the Elvis vi clone (though virtually none of  the  original code
       remains).

       Credit  is  also due Bram Moolenaar <Bram@vim.org>, the author of vim,
       who has devoted so much of his time and energy both to developing  the
       editor as a service to others, and to helping the orphans of Uganda.

       The  section  entitled  "HOW  TO USE  WITH GNU EMACS" was shamelessly
       stolen from the info page for GNU etags.

'c or linux' 카테고리의 다른 글

POSIX 쓰레드로 멀티 쓰레드 프로그래밍하기  (0) 2005.02.18
함수 포인터  (0) 2005.02.16
#ifdef __cplusplus  (1) 2005.02.12
Setjmp()  (0) 2005.02.11
C언어 함수  (0) 2005.02.05
Posted by '김용환'
,