다중 입출력 - select()

'다중 입출력'은 프로그램(단일 스레드)에서 여러 개의 파일을 작업하고자 할 때 사용할 수 있는 메커니즘입니다. 사실 '단일 스레드'에서 여러 개의 파일을 작업하고자 할 때 사용할 수 있는 다른 방법으로는 '논블록(Non-Block) 입출력'을 사용하는 방법도 있긴 합니다만, '논블록(Non-Block) 입출력'은 프로그래밍 작업이 까다롭습니다. 이제부터 설명할 'select'는 '블록/비동기적 입출력'에서의 '다중 입출력' 모델입니다.

 

A, B, C, D 4개의 파일을 다루는 작업을 하고싶다고 가정해볼때 다음과 같은 시나리오를 생각해볼 수 있습니다.

   1. A 파일에 대한 작업을 합니다.

   2. A 파일에 대한 작업을 마칩니다.

   3. B 파일에 대한 작업을 합니다.

   4. B 파일에 대한 작업을 하다가 프로그램이 '블록(Block)' 상태에 빠집니다.

 

파일에 대한 작업 중 '블록(Block)' 상태가 되는 이유야 다양하겠지만 대표적으로 read() 함수를 호출했는데 읽을 파일이 없다면 프로그램은 읽을 내용이 생길 때까지 '블록(Block)'됩니다. 그럼 위의 상황에서 '블록' 없이 어떻게 4개의 파일에 대한 작업을 정상적으로 실행할 수 있을까요? 답은 간단합니다. A, B, C, D 파일을 순서대로 작업할게 아니라 작업할 준비가된 파일에 대해서만 작업을 하면됩니다. 이러한 생각이 시초가 되어 최초로 탄생한 함수가 'select' 시스템콜입니다.

#include <sys/select.h>

int select(int n,
    fd_set* readfds,
    fd_set* writefds,
    fd_set* exceptfds,
    struct timeval* timeout);
    
FD_CLR(int fd, fd_set* set);
FD_ISSET(int fd, fd_set* set);
FD_SET(int fd, fd_set* set);
FD_ZERO(fd_set* set);

 

'select()'는 해당 파일 디스크립터가 입출력을 수행할 준비가 되거나 마지막 매개변수인 timeout 변수에 정해진 시간이 경과할 때까지만 '블록'됩니다. '감시 대상 파일 디스크립터'는 3가지 집합으로 나뉘어 각각 다른 '이벤트'를 기다립니다.

  readfds: '읽기'가 가능한지 감시합니다. (블록되지 않고 read() 작업이 가능한지)

  writefds: '쓰기'가 가능한지 감시합니다. (블록되지 않고 write() 작업이 가능한지)

  exceptfds: 예외가 발생했거나 대역을 넘어서는 데이터(소켓)가 존재하는지 감시합니다.

 

만약 select() 함수의 매개변수로 NULL을 넘기면 해당 이벤트는 감시하지 않습니다. select() 시스템콜의 예제를 살펴보겠습니다.

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#define TIMEOUT 5
#define BUF_LEN 1024

int main() {
    struct timeval tv;
    fd_set readfds;
    int ret;
    
    // 표준 입력에서 입력을 기다리기 위한 준비를 합니다.
    FD_ZERO(&readfds);
    FD_SET(STDIN_FILENO, &readfds);
    
    // select가 5초 동안 기다리도록 timeval 구조체를 설정합니다.
    tv.tv_sec = TIMEOUT;
    tv.tv_usec = 0;
    
    // select() 시스템콜을 이용해 입력을 기다립니다.
    ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv);
    
    if (ret == -1) {
        perror("select");
        return 1;
    }
    else if (!ret){
        printf("%d seconds elapsed.\n", TIMEOUT);
        return 0;
    }
    
    // select() 시스템콜이 양수를 반환했다면 '블록(block)'없이 즉시 읽기가 가능합니다.
    if (FD_ISSET(STDIN_FILENO, &readfds)) {
        char buf[BUF_LEN + 1];
        int len;
        
        // '블록(block)'없이 읽기가 가능합니다.
        len = read(STDIN_FILENO, buf, BUF_LEN);
        if (len == -1) return 1;
        if (len) {
            buf[len] = '\0';
            printf("read: %s\n", buf);
        }
        
        return 0;
    }
}