Don'y waste time waiting

일반적으로 read를 호출할 때, 데이터가 아직 사용할 수 없고 함수가 반환되기 전에 데이터가 준비될 때까지 기다려야 합니다. 디스크로부터 데이터를 읽을 때 딜레이는 길지 않지만, 느린 네트워크 연결에서 읽어오는 일은 데이터가 도착하는 데 오래 걸릴 수 있습니다.

 

POSIX는 file descriptor에 플래그를 설정하여 그 file descriptor에 read()를 호출했을 때 즉시 반환되도록 할 수 있습니다.

이 모드의 file descriptor에서 read()를 호출하면 read operation을 시작하고 다른 작업을 하는 동안 동작합니다. 이러한 호출을 read()가 블록되지 않으므로 nonblocking mode라고 합니다.

 

file descriptor를 nonblocking으로 세팅하기 위해서는 다음과 같이 설정합니다.

    // fd is my file descriptor
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);

소켓을 생성할 때 socket()의 두 번째 인자로 SOCK_NONBLOCK을 추가하여 nonblocking mode로 생성할 수 있습니다. 

  fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);

file이 nonblocking mode이고 read()를 호출했을 때, 이 호출은 사용할 수 있는 바이트만큼 즉시 반환합니다. 서버로부터 100바이트가 도착했고 소켓에서 read(fd, buf, 150)을 호출했다고 가정해봅시다. read는 즉시 100을 반환할 것입니다. 이 것은 150 바이트를 요청한 것 중에 100 바이트가 읽혔다는 것을 의미합니다. 나머지 데이터를 read(fd, buf+100, 50)을 호출해서 읽기를 시도했지만 나머지 50바이트가 아직 도착하지 않았다고 가정해봅시다. read()는 -1을 반환하고 global error variable인 errno를 EAGAIN 또는 EWOULDBLOCK으로 설정할 것입니다. 이것이 시스템이 데이터가 아직 준비되지 않았다고 알려주는 방법입니다.

 

write() 또한 nonblocking mode로 동작합니다. 소켓을 사용해 원격 서버에 40,000바이트를 전송하기를 원한다고 가정해봅시다. 시스템은 이 바이트를 한번에 보낼 수 있습니다. 일반적인 시스템은 약 23,000바이트만을 한 번에 보낼 수 있습니다. nonblocking mode에서 write(fd, buf, 40000)을 사용해 보내면 즉시 보낼 수 있는 바이트 또는 약 23,000만큼 리턴됩니다.  만약 write()를 바로 다시 호출하면 -1을 리턴하고 errno를 EAGAIN 또는 EWOULDBLOCK로 셋팅할 것입니다. 이 방법을 통해 시스템은 최근 데이터를 보내는 busy 상태여서 아직 데이터를 보낼 준비가 되지 않았음을 알려줍니다.

 

How do I check when the I/O has finished?

I/O가 끝났는지 확인하기 위해서는 몇 가지 방법이 있습니다. select와 epoll을 이용해 어떻게 이 동작을 하는지 알아보겠습니다.

select 

    int select(int nfds, 
               fd_set *readfds, 
               fd_set *writefds,
               fd_set *exceptfds, 
               struct timeval *timeout);

주어진 세 file descriptor 집합들을 사용해 select()는 이 file descriptor들 중에 어떤 것이 ready 될 때까지 기다립니다.

  • readfds - readfds에 있는 file descriptor들은 읽을 데이터가 있거나 EOF에 도달했을 때 준비됩니다.
  • writefds - writefds에 있는 file descruptor들은 write() 함수 호출이 성공적으로 수행되면 준비됩니다.
  • exceptfds - 시스템마다 특정되어 잘 정의되지 않았습니다. 이 경우 NULL을 사용합니다.

select()는 준비된 file descriptor의 총 숫자를 반환합니다. select가 반환된 다음에는 호출한 함수는 readfds나 writefds 안의 모든 file descriptor들을 돌면서 어떤 file descriptor들이 준비되었는지 확인해야 합니다. readfds와 writefds 모두 입력 파라미터와 출력 파라미터가 되므로 select가 준비된 file descriptor가 있음을 나타낼 때 이미 준비된 상태의 file descriptor들의 상태를 덮어씌울 것입니다. 그러므로 select를 단 한번만 호출하려는 의도가 아니라면, select를 호출하기 전에 readfds와 writefds의 복사본을 저장해 두는 것이 좋습니다.

    fd_set readfds, writefds;
    FD_ZERO(&readfds);
    FD_ZERO(&writefds);
    for (int i=0; i < read_fd_count; i++)
      FD_SET(my_read_fds[i], &readfds);
    for (int i=0; i < write_fd_count; i++)
      FD_SET(my_write_fds[i], &writefds);

    struct timeval timeout;
    timeout.tv_sec = 3;
    timeout.tv_usec = 0;

    int num_ready = select(FD_SETSIZE, &readfds, &writefds, NULL, &timeout);

    if (num_ready < 0) {
      perror("error in select()");
    } else if (num_ready == 0) {
      printf("timeout\n");
    } else {
      for (int i=0; i < read_fd_count; i++)
        if (FD_ISSET(my_read_fds[i], &readfds))
          printf("fd %d is ready for reading\n", my_read_fds[i]);
      for (int i=0; i < write_fd_count; i++)
        if (FD_ISSET(my_write_fds[i], &writefds))
          printf("fd %d is ready for writing\n", my_write_fds[i]);
    }

 

epoll

epoll은 posix의 일부분은 아니지만, Linux에서 지원합니다. epoll은 많은 file descriptor들을 대기하는데 더 효율적인 방법입니다. epoll은 정확히 어떤 file descriptor들이 준비되었는지 알려줍니다.  심지어 각각의 file descriptor에 배열 인덱스나 포인터와 같은 적은 양을 데이터를 저장하는 방법을 제공하여 그 파일 디스크립터에 관련된 데이터에 접근하는 것을 더 쉽게 만들어줍니다.

epoll을 사용하기 위해서는, 먼저 epoll_create()를 통해 특별한 file descriptor를 만들어주어야 합니다. 이 파일 디스크립터에는 read나 write가 불가능합니다. 단순히 다른 epoll_xxx함수를 넘겨주고 마지막에 close()를 호출합니다.

    epfd = epoll_create(1);

epoll을 사용해 관찰하고 싶은 각각의 file descriptor들은 epoll_ctl()을 EPOLL_CRL_ADD 옵션을 사용해 epoll 자료구조체에 추가해줄 필요가 있습니다. 어떠한 숫자의 file descritor든지 추가가 가능합니다.

    struct epoll_event event;
    event.events = EPOLLOUT;  // EPOLLIN==read, EPOLLOUT==write
    event.data.ptr = mypointer;
    epoll_ctl(epfd, EPOLL_CTL_ADD, mypointer->fd, &event)

어떤 file descriptor가 준비가 되었는지 기다리기 위해서 epoll_wait()을 사용합니다. epoll_event 구조체는 file descriptor를 추가했을 때 포함된 데이터를 event.data에 저장합니다. 이것을 통해 file descriptor에 관련된 데이터를 쉽게 찾을 수 있습니다.

    int num_ready = epoll_wait(epfd, &event, 1, timeout_milliseconds);
    if (num_ready > 0) {
      MyData *mypointer = (MyData*) event.data.ptr;
      printf("ready to write on %d\n", mypointer->fd);
    }

만약 현재 file descriptor에 데이터를 쓰기를 기다리고 있다고 가정합시다. 그러나 이제 그 file descriptor로부터 데이터를 읽어오는 것을 기다리고 싶습니다. 이럴 경우 단지 epoll_ctl에 EPOLL_CTL_MOD를 사용해 호출해주면 됩니다. 이 방법을 통해 관찰하고 싶은 명령을 바꿀 수 있습니다.

    event.events = EPOLLIN;
    event.data.ptr = mypointer;
    epoll_ctl(epfd, EPOLL_CTL_MOD, mypointer->fd, &event);

epoll instance를 종료하고 싶다면, file descriptor를 닫아줍니다.

    close(epfd);

nonblocking read()나 write() 호출 외에도 nonblocking 소켓에 connect를 호출하는 것도 nonblocking이 됩니다. 이러한 연결이 완료되는 것을 기다리기 위해 select나 epoll을 사용해 소켓이 쓸 수있는 상태가 되는 것을 기다릴 수 있습니다.

Posted by 몰랑&봉봉
,