본문 바로가기

서버운영 (TA, ADMIN)/네트워크

[네트워크] 블로킹과 논블로킹 메커니즘

블로킹 모드


어떤 시스템 콜을 호출했을때 네트워크 시스템 동작이 완료할때까지 그 시스템 콜에서 프로세스가 멈춥니다.

소켓 생성시 디폴트는 default 모드입니다.

listen(), connect(), accept(), recv(), send(), read(), write(), recvfrom(), sento(), close()

block 될 수 있는 소켓 시스템 콜입니다.

I/O시 처리될 때까지 기다려야하기 때문에 비동기적인 작업 수행 불가능하게 됩니다.

일대일 통신을 하거나 프로그램이 한가지 작업만 하면 되는 경우는 blocking 모드로 프로그램을 작성할 수 있습니다.



논블로킹 모드


소켓 관련 시스템 콜에 대하여 네트워크 시스템이 즉시 처리할 수 없는 경우라도 시스템콜이 바로 리턴되어 응용 프로그램이 block되지 않게 하는 소켓 모드입니다.

통신 상대가 여럿이거나 여러가지 작업을 병행하려면 nonblocking 또는 비동기 모드를 사용해야 합니다.


non-blcking 모드를 사용하는 경우에는 일반적으로 어떤 시스템 콜이 성공적으로 실행될때까지 계속 루프를 돌면서 확인하는 방법(폴링)을 사용합니다.





블로킹과 논블로킹 메커니즘의 비교


자바 TCP 서버/클라이언트 애플리케이션을 작성하기로 했다면 블로킹 애플리케이션으로 작성할지, 논블로킹 애플리케이션으로 작성할지를 반드시 고려해야 합니다. 이 결정에 따라 구현이 달라지고, 복잡도도 달라지므로 이는 매우 중요합니다.


블로킹 메커니즘의 주요 특징은 I/O가 가득 수신할 때까지(경우에 따라 다시 시간이 걸릴 수 있음) 주어진 스레드가 아무것도 하지 않는다고 가정하는 것입니다. 이 경우 메서드가 즉시 애플리케이션 플로우로 제어를 반환하지 않으므로 애플리케이션의 플로우가 블록됩니다. 반면에 논블로킹 메커니즘에서는 I/O 요청을 즉시 큐에 넣고 애플리케이션 플로우로 제어를 반환합니다(메서드는 즉시 반환한다). 요청은 나중에 커널에서 처리됩니다.


자바 개발자 관점에서 이들 메커니즘과 관련된 복잡도는 반드시 이해해야 합니다. 논블로킹 메커니즘은 블로킹 메커니즘보다 구현이 더 복잡하지만, 성능과 확장성에서 유리합니다.


논블로킹 메커니즘이 비동기 메커니즘과 같은 것이 아닙니다. 예를 들어 논블로킹 환경에서 응답이 빠르게 돌아오지 않는다면 API는 즉시 에러와 함께 복귀하고 다른 행동을 하지 않습니다. 반면에 비동기 환경에서는 API가 항상 증시 복귀하지만, 뒤에서는 요청을 제공하는 작업이 시작됩니다. 다시 말해 논블로킹 매커니즘에서는 함수가 스택에 있는 동안 기다리지 않으며, 비동기 메커니즘에서는 함수 호출을 스택에 남겨두고, 함수 호출을 대신해서 작업이 계속해서 작업이 계속됩니다. 비동기는 병렬(parallel)에 가깝고 논블로킹은 종종 폴링(polling)이라고 불립니다. 




I/O 멀티플렉싱이란


한개의 프로세서가 두 군데 이상의 클라이언트로부터 데이터를 읽어야 한다고 생각해보겠습니다. 프로세서는 두 개 이상의 소켓을 열고 데이터가 전송되기를 기다려야 합니다. 하지만 클라이언트로부터 데이터는 언제 전송되어 올지 모르기 때문에 한쪽 소켓만 데이터를 읽으려고 기다리면 다른쪽 소켓에 데이터가 전다로디어 오더라도 서버는 전혀 인지하지 못하는 수가 있습니다.


이런 상황을 극복하기 위한 방법이 바로 입출력 다중화인데 먼저 ioctlsocket을 사용하여 읽고자 하는 두 개 이상의 소켓을 비블록화(Nonblocking)하는 방법이 있습니다. 그러면 프로세서는 순차적으로 소켓을 읽고 일정시간을 폴링(polling)하게 됩니다. 소프트웨어는 매 소켓을 일정한 간격으로 폴링하고 읽어들일 데이터가 존재하지 않으면 sleep하는 방법입니다.


하지만 폴링의 방법은 너무나 많은 시스템 자원이 소비하는 방법으로 그리 좋은 방법은 아닙니다. 프로세서가 스레드를 생성하여 각 스레드로 하여금 소켓의 정보를 읽어 들이게 하는 방법입니다. 각 스레드는 소켓에서 데이터를 읽어 들일 때까지 블록(block)화 되어도 상관없으며 스레드는 데이터를 읽으면 프로세스간 통신(IPC)을 통해 부모 프로세서에게 읽어들인 데이터를 전달하는 방법입니다.


다른 하나의 방법은 select 함수를 이용하는 것입니다. 이 방법은 프로세서가 커널에게 여러 개의 이벤트를 기다리게 하고 그 중 원하는 이벤트가 도달되면 프로세서를 깨우게 하는 방법입니다. 이렇게 함으로써 프로세서는 검색하고자 하는 소켓에 데이터가 읽어 들여지면 전달된 데이터를 읽는 것입니다. 위의 방법 중 가장 일반적으로 사용되는 방법이 select() 함수를 이용하는 것입니다.


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


select() 함수에서 사용된 timeval 인자는 다음과 같은 구조체의 포인터입니다.

struct timeval {
    long tv_sec;    /* seconds */
    long tv_use;    /* and microseconds */
};


select() 함수를 이용하면 예를 들어 소켓 세트 {1,4,5}가 소켓으로부터 데이터를 읽어들일 준비가 되어 있고 소켓 세트 {2,7}은 소켓으로 데이터를 쓸(write) 준비가 되어 있고 소켓 세트 {1,4}는 현재 소켓이 펜딩(pending)되어 있다는 등의 상태를 알 수 있습니다.


Select 함수는 디스크립트(descriptor)를 체크한 후 바로 그 결과를 반환하는데, 이때 timeval 인자는 시간을 나타내는 포인터이고 timeout 값이 0이어야 합니다. 이러한 작업을 폴링이라고 합니다. 그리고 디스크립트(descriptor) 상의 어떤 인자라도 사용할 준비가 되어있으면 지정한 시간을 기다리지 않고 즉시 반환합니다. 이때 timeval 인자는 체크할 시간을 나타내는 포인터이고 0 이외의 값이 설정되어야 합니다.


다음은 select의 간단한 사용 예입니다.

struct timeval timeout;
timeout.tv_sec = atol(argv[1]);
timeout.tv_usec = atol(argv[2]);

if(select(0, (fd_set *)0, (fd_set *)0, &timeout) < 0) {
    printf("select error\r\n");
    return FALSE;
}
return TRUE;

위의 예에서는 select 함수의 두 번째, 세 번째, 네 번째 인자는 null로 설정을 했지만 만일 체크하고 싶은 파일 디스크립트가 있으면 파라미터(readfds, writefds, exceptfds) 속에 설정을 해야 합니다. 이때 네번째 인수인 exceptfds를 통해 알 수 있는 예외 정보는 다음과 같은 상황에서 사용할 수 있습니다.


  • 소켓을 통한 긴급 데이터의 도착을 알릴 때
  • 현재 컨트롤 상태 정보를 알고 싶을 때


시스템에서 사용가능한 최대 디스크립터의 개수는 FD_SETSIZE를 통해 설정이 됩니다. 예를 들어 다음과 같이 세팅이 된 경우에는 최대 개수가 64개로 제한이 됩니다. 그리고 각각의 비트는 디스크립터와 연결됩니다.

#define FD_SETSIZE      54
FD_set  fdvar
FD_ZERO(&fdvar);        // fd_set을 초기화
FD_SET(1, &fdvar);      // fd 1비트 검색
FD_SET(4, &fdvar);      // fd 4비트 검색
FD_SET(5, &fdvar);      // fd 5비트 검색

작업을 수행할 때 fd_set을 초기화하는 것은 대단히 중요합니다. 만일 초기화하지 않고 사용한다면 시스템은 알 수 없는 오류들을 일읔밀 수 있습니다. select 함수를 사용하면 그 결과를 readfds, writfds 그리고 exceptfds에 각각 반환하면 FD_ISSET 매크로를 사용하여 fd_set 구조체에 select 함수가 정상적으로 값을 반환하였는지를 알아보아야 합니다.


FD_ISSET 매크로는 select를 통하여 반환된 총합을 반환합니다. 만약 시간 초과로 반환되었다면 그것은 0일것이고 에러가 발생하였다면 그 값은 -1입니다. select 함수는 몇개의 소켓이 동시에 연결되는 프로그램에 사용되기도합니다.


예를 들면, 프로세서가 여러 개의 소켓을 열고 클라이언트의 연결을 기다리고 있다면 하나의 연결이 accept 될때까지 프로세서는 다른 소켓을 검사할 수 없지만 select 함수를 사용하여 일정시간 동안 차례로 소켓을 검사하게 함으로써 모든 소켓이 동시에 연결을 기다리게 할 수 있습니다.