이번 시간에는 시리얼 통신에서 자료를 수신하는 부분을 구현하려 합니다. 그러나 송신 보다 수신하는 부분은 생각할 점이 있습니다. 전송이야 이쪽에서 필요할 때 보내기만 하면 되기 때문에 “언제라는” 시간적인 문제가 없습니다. 그러나 수신은 자료가 언제 올지를 모르죠. 기다려야 한다는 것인데, 자료가 올 때까지 마냥 시리얼 포트만 쳐다 보고 있을 수 없습니다. 다른 일도 처리 해야죠. 해야할 일이 산더미처럼 쌓였는데, 마냥 포트만 쳐다 볼 수 없습니다. 

 

이럴 때 쉽게 생각할 수 있는 것이 일을 처리하는 중에 잠시잠시 포트를 확인하는 방법입니다. 가령 예를 들어서 아래와 같이 하는 것이죠.

while( 1)
{
  // 다른 업무를 실행
  if 0 < read( fd, buf, BUF_MAX_SIZE)
  {
    // 수신 자료를 처리
  }
}

물론 이와 같은 방법도 좋습니다만 ?자료 수신 이외의 이벤트 처리, 예로 통신에 에러가 발생하지 않았는지 등을 확인을 위해서는 또 다른 확인 루틴을 작성하고 if 절을 추가해야 합니다. 그러나 무엇보다도 read()함수가 block되 버리면 루틴 자체가 block되어 버리는 매우 큰 문제를 가지고 있습니다.

 

이럴 때 사용하는 것이 POLL입니다.

 

poll()

POLL은 확인하고 싶은 여러 가지 사건( 이하 event)를 미리 등록해 놓고 그 사건들이 발생했는지 확인할 수 있는 편리한 방법을 제공해 줍니다. POLL을 이용한 작업 진행 과정을 정리해 보면,

  1. 체크하고 싶은 여러 event를 등록
  2. poll() 함수를 호출하면
  3. event가 발생하면 poll() 함수 호출 후에 바로 복귀하지만,
  4. 발생된 event 가 없으면 하나 이상이 발생할 때 까지 time-out 시간 만큼 대기하게 됩니다.
  5. event가 발생면 해당 event의 배열 아이템의 값이 변동이 되는데,
    이 변동을 확인하여 어떤 event 가 발생했는지를 알 수 있습니다.

장황한 설명 보다는 먼저 간단한 예를 보여 드리고 그 후에 자세한 설명을 드리도록 하겠습니다. 예제는 이해를 돕기 위해 상수를 사용하지 않았고, 함수로 분리하지 않았습니다.

예제

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/poll.h>
#include <termios.h>                   // B115200, CS8 등 상수 정의
#include <fcntl.h>                     // O_RDWR , O_NOCTTY 등의 상수 정의

int main( void)
{        
   int    fd;
   int    ndx;
   int    cnt;
   char   buf[1024];
   struct termios    newtio;
   struct pollfd     poll_events;      // 체크할 event 정보를 갖는 struct
   int    poll_state;

   // 시리얼 포트를 open

   fd = open( "/dev/ttyS0", O_RDWR | O_NOCTTY | O_NONBLOCK );        // 디바이스를 open 한다.
   if ( 0 > fd)
   {        
      printf("open error\n");
      return -1;
   }

   // 시리얼 포트 통신 환경 설정

   memset( &newtio, 0, sizeof(newtio) );
   newtio.c_cflag       = B115200 | CS8 | CLOCAL | CREAD;
   newtio.c_oflag       = 0;
   newtio.c_lflag       = 0;
   newtio.c_cc[VTIME]   = 0;
   newtio.c_cc[VMIN]    = 1;
   
   tcflush  (fd, TCIFLUSH );
   tcsetattr(fd, TCSANOW, &newtio );
   fcntl(fd, F_SETFL, FNDELAY); 


   // poll 사용을 위한 준비
   
   poll_events.fd        = fd;
   poll_events.events    = POLLIN | POLLERR;          // 수신된 자료가 있는지, 에러가 있는지
   poll_events.revents   = 0;


   // 자료 송수신

   while ( 1)
   {
      poll_state = poll(                               // poll()을 호출하여 event 발생 여부 확인     
                         (struct pollfd*)&poll_events, // event 등록 변수
                                                   1,  // 체크할 pollfd 개수
                                                1000   // time out 시간
                       );

      if ( 0 < poll_state)                             // 발생한 event 가 있음
      {     
         if ( poll_events.revents & POLLIN)            // event 가 자료 수신?
         {
            cnt = read( fd, buf, 1024);
            write( fd, buf, cnt);
            printf( "data received - %d %s\n", cnt, buf);
         }
         if ( poll_events.revents & POLLERR)      // event 가 에러?
         {
            printf( "통신 라인에 에러가 발생, 프로그램 종료");
            break;
         }
      }
   }
   close( fd);
   return 0;
}

  예제를 보시면 struct pollfd를 사용했습니다. struct pollfd의 내용을 보면 아래와 같습니다.

struct pollfd
{
  int fd;             // 대상 파일 디스크립터
  short events;   // 발생된 이벤트
  short revents;   // 돌려받은 이벤트
};

  1. fd는 감시 대상인 디스크립터, 즉 핸들이 되겠습니다.
  2. events는 체크하고 싶은 event의 모음입니다. 여기서 체크가 가능한 것은 아래와 같습니다.
  • #define POLLIN 0x0001 // 읽을 데이터가 있다.
  • #define POLLPRI 0x0002 // 긴급한 읽을 데이타가 있다.
  • #define POLLOUT 0x0004 // 쓰기가 봉쇄(block)가 아니다.
  • #define POLLERR 0x0008 // 에러발생
  • #define POLLHUP 0x0010 // 연결이 끊겼음
  • #define POLLNVAL 0x0020 // 파일지시자가 열리지 않은 것 같은, Invalid request (잘못된 요청)
  1. revents 는 event 발생 여부를 bit 별로 갖는 값입니다.

 

  자, 이제 예제를 보겠습니다.

struct pollfd poll_events; // 체크할 event 정보를 갖는 struct

poll()을 사용하기 위한 변수를 선언했습니다. poll_events 에는 감시 대상인 디스크립터와 어떤 event를 감시할지 결정해서 bit 값으로 지정해 줄 것입니다.

int poll_state;

poll()이 수행한 결과값을 갖습니다. 이 값은 반드시 체크해야 하는데, 아래와 같은 반환값을 갖습니다.

 

poll() 수행 후 반환 값
반환 값 설명
음수 반환 값이 음수라면 치명적인 에러가 발생한 것입니다. 한번 이렇게 음수로 에러가 발생하면 이후 계속 음수값이 날라 옵니다. 거의 대부분 프로그램을 다시 실행해야 됩니다. 반드시 체크해야 겠지요.
0 지정한 대기시간, time-out이 지나도록 발생한 event가 없습니다.
양수 event 가 발생했습니다.

poll_events.fd = fd;

감시 대상인 디스크립터를 지정합니다.

poll_events.events = POLLIN | POLLERR; // 수신된 자료가 있는지, 에러가 있는지

체크하고 싶은 event에 대해 비트값으로 설정하여 지정해 줍니다.

poll_events.revents = 0;

revents를 0 으로 청소해 주고요.

poll_state = poll(                               // poll()을 호출하여 event 발생 여부 확인     
                  (struct pollfd*)&poll_events, // event 등록 변수
                                             1,  // 체크할 pollfd 개수
                                          1000   // time out 시간
                );

체크할 event 정보를 넘겨 줍니다. 1은 체크할 pollfd 개수입니다. 이 예제를 가지고는 pollfd의 개수가 왜 필요한지 이해가 안되시죠. 예제에는 감시하는 디스크립터가 한 개이지만 프로그램에 따라서는 한번에 여러 개의 디스크립터를 관리할 경우가 많습니다.

 

관리할 디스크립터가 많을 때, 각각을 변수로 처리하는 것 보다 배열로 처리하는 것이 편하겠죠. 이래서 poll() 이 편리합니다. poll()은 변수 하나 외에도 배열을 받을 수 있으며, 한번의 호출로 모든 event 발생 여부를 확인할 수 있어 매우 편리합니다.

 

참고

앞으로 강좌를 진행하면서 말씀 드리겠습니다만 poll()을 사용하는 또 다른 장점은 서로 다른 특징의 디스크립터를 사용한다 해도 프로그램 코드를 통일할 수 있습니다.

예로 rs232c 포트 10개와 tcp/ip 소켓 10개를 한번에 제어하는 프로그램을 작성했을 경우 역시 하나의 배열로 구성해서 poll()을 사용할 수 있기 때문에 rs232c 처리와 tcp/ip 처리하는 프로그램 코드를 통일할 수 있습니다.

 

1000은 time-out 시간으로 발생한 event 가 없을 경우 poll()은 event가 발생할 때 까지 time-out 시간 동안 대기하게 됩니다.

이 시간 값은 msec로 1000은 1초를 의미합니다. 0 이면? 바로 복귀합니다. -1 이면? evevnt 가 발생할 때 까지 기다리게 됩니다. 즉, block되 버리죠.

if ( 0 < poll_state)

poll() 함수 결과가 양수라면 event 가 발생한 것입니다.

if ( poll_events.revents & POLLIN)

발생한 event 중에 POLLIN 이 있는 지를 확인합니다. POLLIN에 해당되는 bit 가 1로 세트되어 있다면 POLLIN으로 AND한 값은 0 이 아닐 것입니다. 이렇게 비트값을 확인하여 event 발생 여부를 확인할 수 있습니다.

cnt = read( fd, buf, 1024);
write( fd, buf, cnt);
printf( "data received - %d %s\n", cnt, buf);

자료 수신 event 가 발생했으므로 fd로부터 자료 값을 읽어 들이고, 예제 테스트를 위해 다시 그 자료를 전송합니다. 또한 화면에도 수신한 자료를 뿌리고 있죠.

예제에 대한 설명은 여기 까지 드리겠습니다. 어떻게 자료 송신부터 수신까지 이해가 되시나요? 다음 시간에는 지금까지의 예제를 좀더 발전시켜서 한번에 여러 포트와 통신하는 방법을 말씀드리겠습니다. 조금씩 조금씩 발전 시켜 나가는 거죠.....^^

 

긴 글을 읽어 주셔서 감사합니다.