리눅스에서 쓰레드의 ID 확인시 주의할 점.

 리눅스에서 쓰레드를 생성하여 ID 값을 찍어볼때….

다음과 같이 마이너스 값이 나오는 경우가 있다.

사용자 삽입 이미지
 이와 같은 경우는 printf의 타입 값을 잘못 지정해서 생기는 현상이다.

 보통은 다음과 같이 입력했을 것이다.

(Language : c)
  1. pthread_t t_id;
  2.  
  3. state = pthread_create(&t_id, NULL, thread_function, NULL);
  4.  
  5. printf(“생성된 쓰레드의 ID : %d n, t_id);

 하지만 pthread_t 의 타입을 보면…
 
사용자 삽입 이미지
 즉…..%lu 로 지정해야 정상적인 값이 나온다.

(Language : c)
  1. pthread_t t_id;
  2.  
  3. state = pthread_create(&t_id, NULL, thread_function, NULL);
  4.  
  5. printf(“생성된 쓰레드의 ID : %lu n, t_id);

 정상적인 출력 값
 
사용자 삽입 이미지

파일 디스크립터와 파일 포인터 사용시 주의점

 네트워크 프로그래밍을 하더도중…한가지 풀리지 않는 의문점이 생겨 KLDP 게시판에 글을 올렸다.

 내용 보기 : http://kldp.org/node/90394

 글을 올린지 30분도 채 안되서 답변이 올라왔다.

 문제인즉…

 다음과 같은 코드를 실행했을때…
 

(Language : c)
  1. #include
  2. #include
  3. #include
  4. #include
  5. #include
  6.  
  7. void error_handling(char *message);
  8.  
  9. int main(void)
  10. {
  11.     int fildes;
  12.     FILE *fp;
  13.  
  14.     fildes = open(“data.dat”, O_WRONLY | O_CREAT | O_TRUNC);
  15.     if(fildes == -1)
  16.         error_handling(“open() error!”);
  17.    
  18.     /* 파일 디스크립터를 이용하여 파일 포인터 생성 */
  19.     fp = fdopen(fildes, “w”);
  20.     write(fileno(fp), “This is write funcion1!! nn, 27);
  21.     fputs(“This is fputs function!! nn, fp);
  22.     write(fileno(fp), “This is write funcion2!! nn, 27);
  23.    
  24.     fclose(fp);
  25.    
  26.     return 0;
  27. }
  28.  
  29. void error_handling(char *message)
  30. {
  31.     fputs(message, stderr);
  32.     fputc(n, stderr);
  33.     exit(1);
  34. }

 나타나는 결과 화면이 다음과 같다는데 있었다.

사용자 삽입 이미지
 즉 fgets() 함수의 호출 순서와 파일에 입력된 순서가 서로 맞지 않는다는 것이었다.

 이유는 간단했다.

 KLDP의 답글에 올라온 내용처럼 C표준 라이브러리의 입/출력 함수의 경우 자체 버퍼를 따로 가지고 있어서 이를 명시적으로 비워 주지 않으면 문제의 소지가 있다는 것이다.

 물론 명시적으로 해주지 않아도 표준 라이브러리만 사용한다면 문제는 없다. 하지만 지금처럼 시스템 함수와 표준 라이브러리를 병행해서 사용할 경우 위와 같은 문제가 생길 수 있기 때문에 혼용한다면 명시적으로 버퍼를 비워주는 작업이 필요하다.

 다음과 같이 말이다. 위의 코드는 아래의 코드를 추가한 결과 정상적으로 작동하였다.

(Language : c)
  1.     fputs(“This is fputs function!! nn, fp);
  2.     fflush(fp);

 원인을 안다면 해결책은 간단한 법이다.
 

리눅스(Unix)에서 fflush(stdin) 사용시 발생하는 문제점.

 scanf 로 문자열을 받고 나면 공백으로 구분되서 남은 문자들이 아직 버퍼에 남아 있기 때문에…

fflush(stdin);

을 해줌으로써 버퍼를 비워주는 프로그래밍 기법이 있다.

 하지만 이것은 엄밀히 말하면 틀린것이다. 더 정확히 이야기하자면 VC 에서만(아마도..) 된다.

 사용자들의 편의를 위해 VC에서 확장의 개념으로 만든것이라 생각하면 이해가 쉬울 것이다.

 그렇다면 왜 이것이 안되는 것일까?

C FAQ 12.26을 보면 다음과 같은 말이 나온다.


C 언어 표준에 따르면,
fflush()는 output stream에 대해서만 동작합니다.
“flush”라는 정의가 buffering된 문자들을 쓰는 것을
완료시킨다는
12.15 것이므로 문자를 취소시키는 것(discard)과는 아무런 관계가 없습니다.
따라서
입력 스트림의 입력을 무시하는 기능과는 전혀 상관이 없습니다.

아직 읽지 않은 문자(unread characters)들을 stdio input stream에서
무시하는(discard) 표준 방법은 존재하지 않습니다.
어떤 컴파일러 회사들은 fflush(stdin)이 읽지 않은 문자들을 취소할 수
있도록 라이브러리를 제공하기도 하지만, 이식성이 뛰어난 프로그램을
만들려면 절대로 써서는 안되는 기능입니다.
(어떤 stdio 라이브러리는 fpurgefabort를 같은 기능으로
제공하기도 하지만, 마찬가지로 표준은 아닙니다.)
또한 input buffer를 flush하는 것이 꼭 해결책이라고 할 수도 없습니다.
왜냐하면
일반적으로 아직 읽히지 않은 입력은 OS-level input buffer에
존재하기 때문입니다.

Input을 flush할 방법이 필요하다면 (질문
[*]
19.1과
[*]
19.2에 나온 것처럼)
시스템 의존적인 방법을 찾아야 할 것입니다.
게다가 사용자가 매우 빠른 속도로 입력하고 있고, 여러분의 프로그램이
그 입력을 무시해버릴 수 있다는 것을 꼭 염두에 두어야 합니다.

또는 n이 나오기 전까지 문자를 읽어서 무시하거나,
curses에서 제공하는 루틴인 flushinp()를 쓰면 됩니다.
덧붙여 질문
[*]
19.1,
[*]
19.2도 참고하시기 바랍니다.

 즉 원래 fflush() 함수는 output stream에 대해서만 동작하는 함수이기 때문에 input stream에는 사용할 수 없는 것이고, 또 그런 함수는 존재하지 않는다는 것이다.(물론 표준이야기이다.)

 그렇다면 어떻게 해야할까?

(Language : c)
  1. scanf(“%d”, &a);
  2.  
  3. getchar();

 이런 식으로 바로 아래 라인에 다시 입력을 받는 함수를 호출에 버퍼에 남아있는 문자들을 비워주는 것도 하나의 좋은 보기가 될 수 있다.

gethostbyaddr 함수 호출시 널(NULL)값 리턴 문제…

 참으로 골치아픈 문제였다. 하지만 답은 간단했다.

 문제의 시작점은 gethostbyaddr() 함수를 호출하면서 시작됐다.

 210.125.213.93 (본인 블로그의 IP)주소. 어라???안돼네??

 무슨 문제일까…..
 211.115.77.213 (daum 의 IP 주소). 이것도 안되네??

사용자 삽입 이미지

 무슨 문제가 있나보다….나는 책에 나온 예제를 다시 하나하나 비교를 하면서 혹시 있을지 모를 미스타이핑을 찾아보았다.

 그런데…없.었.다.

 도대체 무엇이 문제일까…

 한참 고민을 하다가 디버깅을 해보았다.

 역시나….gethostbyaddr()함수를 호출하는 부분에서 에러가 발생했다.
 
 이 함수의 실행결과값이 바로 NULL 값이었던 것이다.

 그렇다면 왜 NULL 을 반환할까…계속 고민해보았다.

 혹시나 내가 인자값의 자료형을 잘못 전달한건 아닐까…함수사용을 제대로 하지 못한것은 아닐까…

 몇시간을 고민한 끝에 겨우 답을 얻을 수 있었다….

 IP 주소를 도메인 이름으로 바꾸는 과정은 역방향으로 조회를 하는 것이다.

 그런데 이 역방향 도메인 주소를 조회하기 위해서는 ISP에서 따로이 등록을 해줘야 한다.

 다시 설명을 하자면….

 daum.net == 211.115.77.213 이것은 조회가 가능하지만…(네임서버를 통해서)

 211.115.77.213 == daum.net 이것은 조회가 불가능할 수도 있다는 이야기이다.

 혹시나 싶어…학교 네임서버의 IP를 넣어 보았다.

사용자 삽입 이미지

 정상적으로 작동하는 것이 보인다 : )

 아마도 나와 비슷한 경험을 하는 사람들이 많으리라 생각된다.

 하지만 자신의 프로그램이 잘못된 것은 아니니 걱정말고 자신의 DNS서버의 IP를 넣어보거나 gethostbyname 함수를 통해 알아낸 정보중에 ns.*****.*****로 시작하는 주소의 IP를 넣어보길 바란다.

 그러면 아주 잘 동작하는 자신의 프로그램을 볼 수 있을 것이다.

 다음은 본인이 사용한 프로그램의 소스이다. 참고가 되길 바란다 : )

gethostbyaddr.c (Language : c)
  1. /***************************************************************************
  2. *            gethostbyaddr.c
  3. *
  4. *  Sat Jan  5 22:18:59 2008
  5. *  Copyright  2008  pchero21
  6. *  pchero21@gmail.com
  7. ****************************************************************************/
  8.  
  9. /*
  10. *  This program is free software; you can redistribute it and/or modify
  11. *  it under the terms of the GNU General Public License as published by
  12. *  the Free Software Foundation; either version 2 of the License, or
  13. *  (at your option) any later version.
  14. *
  15. *  This program is distributed in the hope that it will be useful,
  16. *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  18. *  GNU General Public License for more details.
  19. *
  20. *  You should have received a copy of the GNU General Public License
  21. *  along with this program; if not, write to the Free Software
  22. *  Foundation, Inc., 59 Temple Place – Suite 330, Boston, MA 02111-1307, USA.
  23. */
  24.  
  25. #include <stdio.h>
  26. #include <stdlib.h>
  27. #include <string.h>
  28. #include <unistd.h>
  29. #include <arpa/inet.h>
  30. #include <netdb.h>
  31.  
  32.  
  33. void error_handling(char *message);
  34.  
  35. int main(int argc, char **argv)
  36. {
  37.     struct hostent *host;
  38.     struct sockaddr_in addr;
  39.     int i;
  40.    
  41.     if(argc != 2) {
  42.         printf(“Usage : %s <IP> n, argv[0]);
  43.         exit(1);
  44.     }
  45.    
  46.     memset(&addr, 0, sizeof(addr));
  47.     addr.sin_addr.s_addr = inet_addr(argv[1]);
  48.  
  49.     host = gethostbyaddr((char*)&(addr.sin_addr.s_addr), 4, AF_INET);
  50.     if(!host)
  51.         error_handling(“gethostbyaddr() error!”);
  52.    
  53.     printf(“Officially name : %s nn, host->h_name);
  54.    
  55.     puts(“Aliases———————-“);
  56.     for(i = 0; host->h_aliases[i]; i++) {
  57.         puts(host->h_aliases[i]);
  58.     }
  59.    
  60.     printf(“Address Type : %s n, host->h_addrtype == AF_INET ? “AF_INET” : “AF_INET6”);
  61.  
  62.     puts(“IP Address——————“);
  63.     for(i = 0; host->h_addr_list[i]; i++) {
  64.         puts(inet_ntoa(*(struct in_addr*)host->h_addr_list[i]));
  65.     }
  66.     return 0;
  67. }
  68.  
  69. void error_handling(char *message)
  70. {
  71.     fputs(message, stderr);
  72.     fputc(n, stderr);
  73.     exit(1);
  74. }
  75.  

이중 포인터..

이중 포인터란 포인터 변수를 가리키는 포인터라는 뜻이며 다른 말로 하면 포인터의 포인터라고 할 수 있다. 포인터 변수도 메모리를 차지하고 있으므로 이 변수도 당연히 번지가 있다. 따라서 이 번지를 가리키는 또 다른 포인터 변수를 선언할 수 있는 것이다. 이중 포인터 변수를 선언할 때는 * 구두점을 두 번 연속해서 쓴다.

 

int **ppi;

 

이 선언에서 ppi는 정수형 대상체를 가리키는 포인터
변수의 번지를 가리키는 포인터 변수로 선언되었다. 말이 조금 꼬이는 것 같고 복잡해 보이는데 어째서 저런 변수를 선언할 수
있는지 차근 차근히 풀어 보도록 하자. 다음 두 명제는 앞에서 이미 공부했던 것들인데 이중 포인터를 이해하기 위해 다시 정리해
보자.

 

T *형은 하나의 타입으로 인정된다.

T 변수를 선언할 있으면 T *형도 항상 선언할 있다.

 

정수형 포인터 변수는 다음과 같이 선언한다.

 

int *pi;

 

이 선언문에서 int *라는 표현이 “정수형 포인터”라는 뜻으로 그 자체가 하나의 타입이다. 따라서 다음과 같이 괄호로 묶으면 좀 더 읽기 쉬워지고 뜻이 분명해질 것이다.

 

(int *) pi;

 

두 번째 명제에 의해 T형에 대해 항상 T *형이 가능하므로 int *형에 대한 포인터형을 만들 수 있다. int *형 변수를 가리키는 변수 ppi를 선언하면 다음과 같아진다.

 

(int *) *ppi;

 

이 선언문에서 괄호를 제거하면 최초의 이중 포인터 선언문인
int **ppi;가 된다. 물론 여기서 사용한 괄호는 어디까지나 설명을 위해 쓴 것이지 실제로 타입에 괄호를 쓰면 컴파일
에러로 처리된다. 하지만 typedef문으로 int *형을 별도의 타입으로 정의한 후 이 타입의 변수를 선언할 수는 있다.

 

typedef int *PINT;

PINT pi;

 

보다시피 int *형을 PINT라는 사용자 정의 타입으로 정의하였고 이 타입의 변수 pi를 선언할 수 있다. 포인터 변수 pi를 가리킬 수 있는 포인터 변수를 선언하면 다음과 같다.

 

PINT *ppi;

 

이 선언문에서 PINT를 사용자 정의형으로 풀어 쓰면
int **ppi;가 되는 것이다. 같은 원리로 3중 포인터는 int ***pppi;와 같이 선언하면 되고 5중 포인터나 8중
포인터도 그 수만큼 *를 계속 붙이면 된다. 3중 포인터 이상은 현실적으로 거의 사용되지 않지만 이중 포인터와 원리는 동일하므로
이중 포인터만 이해하면 된다. 이중 포인터는 포인터 변수를 가리키는 변수이므로 이 변수에는 포인터의 포인터값을 대입해야 한다.
즉, 포인터 변수에 &연산자를 붙인 값이면 이중 포인터에 대입할 수 있다. 다음 예제는 이중 포인터의 동작 원리를
보여주는 가장 간단한 예제이다.

 

: dblPointer

#include <Turboc.h>

 

void main()

{

     int i;

     int *pi;

     int **ppi;

 

     i=1234;

     pi=&i;

     ppi=&pi;

 

     printf(“%dn”,**ppi);

}

 

정수형 변수 i에 1234값을 대입해 놓고 이중 포인터로
이 값을 읽는 시범을 보인다. 정수형 포인터 pi가 i의 번지를 가지고 정수형 이중 포인터 변수 ppi에는 pi의 번지를 대입해
놓고 **ppi값을 읽으면 결국 i의 값이 읽혀진다. 이 프로그램에서 각 변수들이 서로를 가리키는 모양은 다음과 같을 것이다.

i는 일반 변수이므로 메모리상에 4바이트를 차지하고
1234가 대입되었다. 정수형 포인터 pi도 일종의 변수이므로 메모리상에 할당되며 그 초기값으로 &i, 즉 i의 번지를
대입받았다. 이 상태에서 pi가 i를 가리키고 있으므로 *pi 연산문으로 i값을 읽을 수 있다. 여기까지는 앞에서 이미 살펴 본
것들이다.

포인터 변수 pi도 일종의 변수이므로 분명히 메모리에
할당될 것이고 따라서 번지를 가지고 있다. pi가 할당되어 있는 번지값인 &pi를 이중 포인터 변수 ppi에 대입했다.
그래서 ppi는 pi를 가리키고 pi는 다시 i를 가리키고 있는 것이다. 이 상태에서 **ppi 연산문으로 값을 읽으면
*(*ppi)=*(pi)=i가 되므로 결국 출력되는 값은 i인 것이다.

ppi가 가리키는 곳에 pi가 있으며 pi가 가리키는 곳에는 i가 있으므로 ppi에 대해 *연산자를 두 번 적용하면 결국 i값이 읽혀진다. 이 예제의 상황에서 다음 수식들은 모두 동등한 대상을 나타낸다.

 

i=*pi=**ppi

&i=pi=*ppi

*&i=*&*pi=*&**ppi

 

* 연산자와 & 연산자는 서로 반대되는 동작을
하는데 이 두 연산자에 의해 가리키고 끄집어 내오다 보면 동등한 수식이 여러 개 생길 수 있다. 그렇다고 해서
&&i=&pi=ppi라는 등식은 성립하지 않는데 &연산자를 두 번 쓰는 것은 적법하지 않다.
왜냐하면 &연산자의 피연산자는 메모리상의 실제 번지를 점유하고 있는 좌변값(lvalue)이어야 하는데 &i는 i가
저장된 번지를 나타내는 포인터 상수일 뿐 좌변값이 아니기 때문이다.

다음 예제는 이중 포인터의 전형적인 활용예인데 포인터를 참조 호출로 전달하여 함수가 포인터를 변경할 수 있도록 한다.
main에서 이름을 입력받는 함수를 호출하는데 이 이름의 길이가 DB나 사용자로부터 입력되어 실제 입력받아 보기 전에는
얼마일지를 알 수 없다고 하자. 이럴 경우 함수가 필요한만큼 메모리를 할당해서 할당한 번지를 리턴하도록 해야 한다.

 

: FuncAlloc

#include <Turboc.h>

 

void InputName(char **pName)

{

     *pName=(char *)malloc(12);

     strcpy(*pName,”Cabin”);

}

 

void main()

{

     char *Name;

 

     InputName(&Name);

     printf(“이름은 %s입니다n”,Name);

     free(Name);

}

 

main에서 char *형의 변수 Name을 선언하고 이
포인터 변수의 번지, 즉 char **형의 이중 포인터를 InputName 함수로 전달했으며 이 함수는 이중 포인터를 형식 인수
pName으로 대입받는다. Name은 함수 내부에서 값이 결정되는 출력용 인수이기 때문에 호출원에서 초기화하지 않아도 상관없다.
InputName 함수는 필요한만큼(예제에서는 12로 가정) 동적으로 메모리를 할당하여 할당된 번지를 pName이 가리키는
번지인 *pName에 대입했다. 여기서 *pName이라는 표현식은 곧 main에서 InputName으로 전달한 실인수 Name을
의미한다. 그리고 할당된 번지에 어떤 문자열을 복사했다.

결국 InputName 함수는 main의 Name 포인터
변수를 참조 호출로 전달받아 Name에 직접 메모리를 할당하고 이 번지에 scanf로 입력받은 이름까지 복사한 것이다.
InputName이 리턴되었을 때 Name은 12바이트 길이로 할당된 번지를 가리키며 이 안에는 입력된 이름까지 들어 있으므로
printf로 출력할 수 있고 다 사용한 후 free로 해제하면 된다. 이 문제를 잘못 생각하면 다음과 같이 InputName
함수가 char *형을 받도록 작성할 수도 있다.

 

: FuncAlloc2

#include <Turboc.h>

 

void InputName(char *pName)

{

     pName=(char *)malloc(12);

     strcpy(pName,”Cabin”);

}

 

void main()

{

     char *Name;

 

     InputName(Name);

     printf(“이름은 %s입니다n”,Name);

     free(Name);

}

 

언뜻 보기에는 이 코드가 맞는 것 같지만 실제로 컴파일해 보면 경고가 하나 발생하며 실행하면 제대로 동작하지도 않을 뿐더러 죽어 버리기까지 한다. 왜 그런지 다음 그림을 보자.

main에서 Name을 선언하고 초기화되지도 않은
Name의 값(비록 그 값이 번지라 하더라도 어쨌든 값이다.)을 InputName의 pName으로 전달했다. 이 함수는
pName에 메모리를 할당하고 이름 문자열을 복사해 넣지만 pName은 함수의 지역변수일 뿐이지 호출원의 실인수 Name과는
아무런 상관이 없다. 함수가 char *의 값을 전달받으면 이 번지가 가리키는 내용을 변경할 수는 있지만 포인터 자체를 변경해서
호출원으로 돌려줄 수는 없다.

이 코드대로라면 pName에 메모리가 할당되고 이름도
복사되지만 그 결과가 main함수의 Name까지는 전달되지 않는다. main의 Name은 여전히 쓰레기값을 가지고 있으며 이
번지를 잘못 읽거나 할당되지도 않은 영역을 해제하려고 시도하면 죽어 버릴 수도 있다. 뿐만 아니라 지역변수 pName은 함수가
리턴되기 전에 사라지므로 할당된 메모리의 진입점을 잃어 버려 더 이상 이 메모리를 읽을 수 없고 해제할 수도 없는 상태가 되어
버린다.

6장에서 배운 바에 의하면 인수 X의 값을 함수 내부에서
변경하려면 X의 포인터를 넘기는 참조 호출을 해야 한다. 그러므로 InputName에서 char *형의 인수 Name을 변경할
수 있도록 하려면 char *의 포인터인 char **형을 넘겨야 하고 InputName 함수에서는 *pName으로 실인수를
참조해야 하는 것이다. 만약 이 두 예제가 잘 이해가 되지 않는다면 6장의 값 호출, 참조 호출 예제로 돌아가 좀 더 단순한
타입인 정수형의 경우부터 복습한 후 다시 읽어 보아라. 그리고 메모리 안에서 어떤 일들이 일어나는지 잘 상상해 보아라.

 잘 이해가 안되는 부분이었는데 말끔하다.

 출처 : WIN32 API 연구 사이트