본문 바로가기
  • AI (Artificial Intelligence)
Programming/C, C++

비동기 입출력 프로그래밍

by 로샤스 2014. 4. 16.

1 비 동기 입출력 프로그래밍

1.1 입출력 모델

소켓 응용 프로그램을 개발하다보면 종종 봉쇄(blocking) 소켓, 비 봉쇄(non-blocking) 소켓이란 말을 듣는다. 봉쇄 소켓 보다 비 봉쇄 소켓이 성능이 좋다느니, 이런 경우에는 비 봉쇄 소켓을 사용해야 한다느니 하는 것들이 그것이다. 특히 요즘에는 단일 프로세스 (단일 쓰레드)처리 방식이 선호되면서, 비 봉쇄 소켓에 대한 관심이 많아지고 있다.

봉쇄 모델과와 비 봉쇄모델은는 프로그램이 어떤 상태로 작동하는 지를 묘사한다. 함수호출을 한 영역에서 프로그램이 (반환 될 때까지)대기 하면, 봉쇄 모델 그렇지 않으면 비 봉쇄 모델라고 한다.

동기 / 비 동기는 데이터 상태와 관련된다. 데이터의 입출력 상태를 서로가 알면 동기, 그렇지 않으면 비 동기다. 데이터 상태와 관련되어 있다. 동기 입출력 모델에서는 A에서 데이터를 지금 쓰면, B에서도 지금 데이터를 읽을 것을 안다. B는 A가 데이터를 보내면, 이에 맞추어 데이터를 읽는다.

비 동기 입출력 모델은 입력과 출력의 시점을 알지 못하는 상태를 의미한다. A가 B로 데이터를 보낸다고 가정해 보자. 이 상태에서 B는 A가 언제 데이터를 보낼지 알 수 없으며, 데이터를 기다리지 않는다. 대신 "이벤트 통지"를 기다린다. 즉 다른 일을 하고 있다가 "A가 데이터를 전송했다"라는 비 동기적 신호를 받으면 그때, 데이터 읽기를 시작한다. 쓰는 시점과 읽는 시점이 일치 하지 않는다.

일반적으로 소켓은 봉쇄 소켓 으로 만들어진다. 데이터 입출력 상태로 보자면 동기 입출력 상태인 "동기 & 봉쇄 모델"이다. 봉쇄형 소켓이란 읽기와 쓰기의 과정이 완전히 끝날 때 까지, 머물러 있음을 의미한다. read함수를 예로 들어보자. 봉쇄형 소켓을 read할 경우, read함수는 데이터를 모두 읽을 때까지 대기한다. 봉쇄 소켓을 사용하면, 해당 영역에서 머무르기 때문에 다른 입출력 관련 작업을 할 수 없다는 문제가 생긴다. 예컨데, 두 개 이상의 입출력을 처리할 수 없다. 파일 지정 번호 4와 5를 가진 소켓이 있다. 현재 프로그램은 4번 소켓에서 읽기 위해서 기다리고 있다면, 4번 소켓에서의 읽기 작업이 끝나기 전에는 5번 소켓을 처리할 수 없다.

비 봉쇄인 경우 소켓 함수는 바로 반환한다. 반환 값을 에러를 나타내는 -1 을 가진다. 실제 에러가 아님에 주의해야 한다. 비 봉쇄 소켓을 사용할 경우에는 함수 반환 값이 아닌, errno값을 이용해서 함수 상태를 검사한다.

비 봉쇄는 두 가지 방법으로 구현한다.
  1. 입력이나 출력 함수를 호출하기 전에, 파일들로 부터 입력과 출력이 있는지를 검사한다.
    입출력 다중화epoll에서 사용하는 방법이다. 입출력 함수들은 여전히 봉쇄이지만, 이들을 호출하기전에 ( selectpoll함수로 ) 입출력을 검사해서 데이터가 있을 때만, 입출력 함수들을 검사하기 때문에 "비 봉쇄 인것처럼" 작동한다. 입출력 함수를 비 봉쇄로 해서 입출력 다중화를 구현하는 방법도 있다.
  2. 파일을 비 봉쇄로 한다. 그리고 비동기 통지 (asynchronous notification)을 기다린다.
    예를 들어 클라이언트에서 데이터를 전송하면, 이를 받은 서버의 소켓은 "read event"를 발생한다. 이벤트는 이벤트 정보를 포함하는 데이터에 대한 포인터도 함께 넘긴다. 이벤트 정보에는 이벤트가 발생한 소켓 지정 번호, 이벤트가 발생한 프로세스와 같은 정보들이 들어있다. 이 정보를 이용해서 데이터를 처리한다.
  3. 봉쇄 소켓 : 소켓 작업이 끝나기 전까지 해당 영역에서 대기한다.

입출력 모델은 "봉쇄/비봉쇄" 와 "동기/ 비 동기"의 조합에 따라 4가지 입출력 모델이 존재한다.

  1. 소켓은 기본적으로 봉쇄&동기 모드로 만들어 진다.
  2. O_NONBLOCK로 동기&비 봉쇄 모드로 만들 수 있다.
  3. 입출력 다중화 기술 자체는 비 동기 모델을 따른다. 소켓이 봉쇄면 "비 동기 & 봉쇄" 모델이 된다. epoll도 마찬가지다.
  4. 리얼 타임 시그널과 AIO는 "비 동기 & 비 봉쇄"모델을 따른다. epoll과 입출력 다중화는 소켓을 "비 봉쇄"로 할경우, "비 동기 & 비 봉쇄"모델이 된다.

입출력 다중화와 epoll을 "비 동기 & 비 봉쇄"모델로 해서 얻을 수 있는 이익이 있는지에 대해서는 회의적이다. 어차피 select나 epoll_wait에서 반환된 후에는 읽을 데이터가 있는게 분명하니, 봉쇄 모드로 작동해도 되기 때문이다. 다만 accept를 위한 "듣기 소켓"은 비동기로 했을 때 얻을 이익이 있을 것으로 생각된다. 연결 대기열에 있는 연결 요청을 한번에 꺼내올 수 있기 때문이다.

1.1.1 동기 봉쇄 모델

  1. read함수를 호출하면, 커널 모드로 요청이 가고 입력을 기다린다.
  2. 애플리케이션은 데이터 입력이 있기 까지 봉쇄된다.
  3. 데이터가 입력되면, 커널 모드에서 유저모드로 데이터가 복사된다.

1.1.2 동기 비 봉쇄 모델

  1. read함수는 바로 반환한다. 데이터가 준비되지 않았다면, errno는 EAGAIN으로 설정된다.
  2. 이를 반복한다.
  3. 만약 데이터가 준비되어 있다면, 데이터를 읽는다.
데이터가 준비되기 전까지 바쁘게 순환해야 하는 busy wait 상태에 놓일 수 있다.

1.1.3 비 동기 봉쇄 모델

  1. 동기 비 봉쇄 모델은 계속 read함수를 호출하기 때문에 busy wait 상태에 놓일 수 있다는 단점이 있다.
  2. 비 동기 봉쇄 모델은 입출력 함수 호출전에 입출력 데이터가 있는지를 검사하는 함수(select 혹은 poll)를 미리 배치한다.
  3. 입출력 데이터가 없을 때는 봉쇄된다.
  4. 입출력 데이터가 있으면, 비로서 입출력 함수를 호출한다. 입출력 함수는 봉쇄 모드로 작동한다.
비 동기 봉쇄 모델중 epoll에 관심을 간다. 현재 리눅스에서 가장 효율적인 네트워크 프로그래밍 도구로 알려져 있다.

1.1.4 비동기 비 봉쇄 모델

aio를 기준으로 설명
  1. aio_read함수를 호출한다.
  2. 다른 일을 한다.
  3. 읽을 데이터가 발생하면, 콜백 함수 혹은 시그널 핸들러로 데이터를 처리한다.


1.2 비 동기 입출력의 장점과 단점

비 동기 입출력을 이용하면 단일 프로세스&단일 쓰레드에서 여러 개의 소켓을 처리할 수 있다. (엄밀히 말해서 입출력 다중화는 비동기 입출력은 아니지만, 비 동기 입출력의 범주에 포함시켰다.) 비 동기 입출력이 가지는 장점과 단점에 대해서 알아보도록 하겠다. 여러 개의 소켓을 처리할 수 있기 때문에 멀티 쓰레드 방식과 많은 비교가 될 것이다.
  1. 쓰레드는 생각만큼 효율적이고 자유로운 도구가 아니다. 생각이상의 제약사항을 가지고 있다. 500개 이상의 클라이언트를 처리하기 위해서 500개의 쓰레드를 만든다고 생각해보자. (이 문제는 쓰레드 풀로 어느 정도 해결할 수 있긴 하다.) 비 동기 입출력은 단일 쓰레드처리 방식이므로 이러한 제한에서 자유롭다.
  2. 비 동기 입출력을 쓰레드와 단순비교하는 건 힘들다. 비 동기 입출력은 쓰레드와 같은 병렬처리 방법을 제공하진 않기 때문이다. 하지만 많은 경우 굳이 병렬로 처리하지 않아도 된다. 예를 들어 채팅 프로그램 같은 경우 굳이 멀티 쓰레드 방식을 사용할 필요는 없다.
  3. 멀티 쓰레드(멀티 프로세스)는 복잡한 프로그래밍 기술을 요구한다. 잠금, 동기화, IPC 등등등... 수많은 문제를 해결해야 한다.
  4. 디버깅이 어렵다는 단점이 있다. (그래도 멀티 쓰레드 프로그램보다는 쉽다)
  5. 데이터 연산이 긴 프로그램의 경우 문제가 될 수 있다. 병렬처리가 아니므로 다음 입출력이 오랜시간 기다릴 수 있기 때문이다. 이 문제는 데이터 처리를 위한 쓰레드 풀을 두는 것으로 해결할 수 있다.

최근의 네트워크 프로그래밍 트랜드는 "단일 쓰레드 기반 & 비동기 입출력"이다.

1.3 소켓을 비 봉쇄 입출력에 대응하도록 하기

소켓은 "봉쇄 & 동기"모드로 만들어진다. fcntl(2)함수로 비 봉쇄 모드로 만들 수 있다.
int fd; 
int flags; 
// 우선 F_GETFL로 파일 지정 번호 fd가 가리키는 파일의 flag값을 가져온다. 
if ((flags = fcntl(fd, F_GETFL, 0)) == -1) 
    flags = 0; 
 
// O_NONBLOCK로 비 봉쇄로 만든다. 
fcntl(fd, F_SETFL, flags | O_NONBLOCK); 
 

소켓을 비 봉쇄로 만들었다면, 해당 소켓을 매개 변수로 사용하는 함수들은 바로 반환한다. 데이터 입출력 관련 함수들인 accept(2), connect(2), read(2), write(2), recv(2), send(2)이다. 반환 값은 -1 이므로 반환 값만을 가지고는 함수가 실패했는지, 아니면 비 봉쇄라서 바로 반환한 것인지 확인할 수 없다. errno 값으로 확인해야 하는데, EAGAIN 혹은 EWOULDBLOCK이면 비 봉쇄 소켓에서 데이터가 준비되지 않아서 반환했음을 의미한다. EAGAIN과 EWOULDBLOCK는 POSIX.1 규격에서 동일한 값으로 사용된다.

간단한 비 봉쇄 소켓 프로그램 예제를 만들어 보았다. 설명은 주석으로 대신한다. 에러 처리는 신경쓰지 않았다.
#include <sys/socket.h> 
#include <sys/stat.h> 
#include <arpa/inet.h> 
#include <stdio.h> 
#include <string.h> 
 
#include <fcntl.h> 
#include <stdlib.h> 
#include <unistd.h> 
 
#include <errno.h> 
 
#define MAXBUF  256  
 
// 비 봉쇄 소켓으로 만들기 위한 함수 
int set_nonblock_socket(int fd) 
{ 
    int flags; 
    if((flags = fcntl(fd, F_GETFL,0)) == -1) 
    { 
        perror("fnctl error"); 
        flags = 0; 
    } 
    fcntl(fd, F_SETFL, flags | O_NONBLOCK); 
} 
 
int main(int argc, char **argv) 
{ 
    int server_sockfd, client_sockfd; 
    int client_len, n; 
    char buf[MAXBUF]; 
    struct sockaddr_in clientaddr, serveraddr; 
 
    client_len = sizeof(clientaddr); 
 
    if ((server_sockfd = socket (AF_INET, SOCK_STREAM, 0)) < 0) 
    { 
        perror("socket error : "); 
        exit(0); 
    } 
    bzero(&serveraddr, sizeof(serveraddr)); 
    serveraddr.sin_family = AF_INET; 
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); 
    serveraddr.sin_port = htons(atoi(argv[1])); 
 
    if(bind (server_sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 
    { 
        perror("Error"); 
    } 
    if(listen(server_sockfd, 5) == -1) 
    { 
        perror("Error"); 
    } 
    while(1) 
    { 
        memset(buf, 0x00, MAXBUF); 
        client_sockfd = accept(server_sockfd, (struct sockaddr *)&clientaddr, 
                            &client_len); 
 
       // 연결 소켓을 비 봉쇄 소켓으로 만든다. 
       set_nonblock_socket(client_sockfd); 
 
        while(1) 
        { 
            memset(buf, 0x00, sizeof(buf)); 
            if ((n = read(client_sockfd, buf, MAXBUF)) < 0) 
            { 
                // errno 값을 한번 더 계산해 줘야 한다. 
                if(errno == EAGAIN) 
                { 
                } 
                else 
                { 
                    printf("read Error %d\n", errno); 
                    close(client_sockfd); 
                    break; 
                } 
            } 
            else if(n == 0) 
            { 
                printf("close %d\n", errno); 
                close(client_sockfd); 
                break; 
            } 
            else 
            { 
                printf("Read Data %s", buf); 
            } 
        } 
    } 
} 
 
이 프로그램은 read함수에서 바로 반환해버리기 때문에 busy wait상태에 놓이게 된다는 문제점이 있다. 실제 이런 식으로 프로그램을 작성하지는 않는다. busy wait 문제 때문에 "동기 & 비 봉쇄"모델은 거의 사용하지 않는다. 일반적으로 입출력 동기화 혹은 epoll, 리얼 타임 시그널과 같은 비 동기 모델과 함께 사용한다. "이런 식으로 비 비봉쇄 소켓을 만들고 에러를 체크하는 구나"하는 정도만 이해하고 넘어가면 될 것 같다.

1.3.1 accept함수와 비 봉쇄 소켓

accept함수는 즉시 반환한다. accept함수에서 비 봉쇄 소켓은 주로 listen으로 만든 연결 대기열에 있는 연결을 한번게 가져오기 위해서 사용한다. 이 방식은 클라이언트의 연결 요청에 빠르게 반응할 수 있다.
  1. 연결 대기열에 3개의 연결이 있다.
  2. EAGIAN을 만날 때 까지 루프를 돌면서 accept함수를 호출한다.

1.3.2 read/write함수와 비 봉쇄 소켓

역시 즉시 반환한다. read함수와 write함수에서 직접 기다리면 busy wait 상태에 놓이므로 보통은 이들 앞에 상태 혹은 이벤트를 검사하기 위한 함수들을 놓는다. select, poll, sigwaitinfo등의 함수들이다. 특정 소켓에 이벤트가 발생해서 read함수가 호출되면, EAGAIN을 만날 때까지 루프를 돌면서 데이터를 읽는다.
while(1) 
{ 
    select(...); 
    while(1) 
    { 
        read(fd,...); 
        if(errno == EAGAIN) break; 
    } 
} 
 

1.3.3 connect함수와 비 봉쇄 소켓

비 봉쇄 소켓으로 connect함수를 호출하면, connect함수는 즉시 반환한다. 연결이 되었는지는 나중에 getsockopt함수로 확인을 한다. 주로 연결 타임 아웃을 검사하기 위한 목적으로 사용한다. 다음과 같은 과정을 거친다.
  1. 소켓을 비 봉쇄로 한다.
  2. connect함수롤 호출한다. 함수는 즉시 반환할 것이다. 연결 과정은 백그라운드에서 진행된다.
  3. select함수를 타임 아웃을 주고 호출한다.
  4. 타임 아웃 시간전에 연결이 성공하면 select함수는 성공적으로 반환할 것이다. 타임 아웃 시간을 초과해서 연결되지 않으면 에러를 반환한다.

비 봉쇄 소켓과 select를 이용한 연결 타임 아웃 검사 예제는 connect timeout문서를 참고한다.

아래 부분은 정리가 안된 내용들임

1.4 입출력 다중화의 입출력 모델

입출력 다중화를 이용하면 봉쇄 소켓 모드로 두 개 이상의 소켓을 처리할 수 있다. 소켓 함수는 봉쇄모드로 작동하지만 select함수가 이들 함수 앞에서 파일 상태를 체크해 주기 때문이다.

입출력 다중화는 select함수로 파일들에 데이터의 입출력이 있는지 확인해서 신호 (반환)하는 방식으로 작동한다. 그러므로 비 동기 입출력 기술이라고 할 수 있다. select함수에서 봉쇄되기 때문에 비 동기 봉쇄 모델을 따른다.

입출력 다중화에서 비 봉쇄 소켓을 사용할 수도 있다. (그래도 여전히 select에서 봉쇄 되므로 프로그램은 비동기 봉쇄 모델이다.)
  1. 클라이언트 연결을 한꺼번에 처리해서 클라이언트 연결 요청 대기 시간을 단축 시킬 수 있다.
    듣기 소켓에 이벤트가 발생해서 accept함수를 호출 하면, 루프를 돌면서 연결 대기열의 모든 클라이언트의 연결을 한꺼번에 가져올 수 있다. EAGAIN을 만날 때 까지 루프를 돌면 된다. 모아서 처리하기 때문에 그만큼 연결 요청 시간을 단축 시킬 수 있다.

관련 예제는 입출력 다중화 네트워크 프로그램 개발문서를 참고한다.

1.5 epoll의 입출력 모델

epoll도 봉쇄 소켓으로 두 개 이상의 소켓을 처리할 수 있다. 입출력 다중화와 동일하다. 입출력 이벤트를 기다리기 위해서 epoll_wait함수에서 봉쇄된다.

역시 비 봉쇄 소켓을 사용할 수도 있다. 얻을 수 있는 이익은 다음과 같다. (입출력 다중화와 동일한 이익을 얻을 수 있다.)
  1. 클라이언트 연결을 한꺼번에 처리해서 클라이언트 연결 요청 대기 시간을 단축 시킬 수 있다.
    듣기 소켓에 이벤트가 발생해서 accept함수를 호출 하면, 루프를 돌면서 연결 대기열의 모든 클라이언트의 연결을 한꺼번에 가져올 수 있다. EAGAIN을 만날 때 까지 루프를 돌면 된다. 모아서 처리하기 때문에 그만큼 연결 요청 시간을 단축 시킬 수 있다.

"비 동기 & 봉쇄 모델"을 따르는 epoll 프로그램의 예는 epoll문서를 참고하기 바란다.

1.6 리얼 타임 시그널의 입출력 모델

리얼 타임 시그널은 그 자체가 비 동기적 정보 통지 도구다. 관리하고자 하는 소켓에 데이터 입출력이 있으면 "시그널"을 발생시키는 방식으로 작동한다. 때문에 소켓을 비 동기, 비 봉쇄 상태로 만들어야 한다. 리얼 타임 시그널 관련 문서는 리얼 타임 시그널문서들을 참고하기 바란다.

2 개인적으로 선호하는 모델

비 동기 봉쇄 모델을 선호한다. 프로세스가 명확하기 때문이다.

물론 AIO와 같은 비 동기 비 봉쇄 모델의 경우, 입출력 작업과 별개로 다른 작업을 할 수 있다는 장점이 있다. 하지만 대부분의 프로그램이 데이터 입력 -> 처리 -> 데이터 출력의 진행 방식을 따르기 때문에, 딱히 "비 동기 비 봉쇄"가 가지는 장점이 필요 없는 경우가 많기 때문이다.

3 POSIX AIO

리눅스에서 제공하는 비 동기 입출력 매커니즘으로 비교적 최근에 (커널 2.6.x) 추가되었다.

3.1 AIO API

aio_suspend 파일 목록에서 이벤트의 발생을 기다린다.
aio_read 이벤트가 발생한 파일에서 데이터를 읽는다.
aio_write 데이터를 쓴다.
aio_return
aio_error
aio_cancel


4 기타 관련 기술들

  1. Twisted : Event driven framework
    • 게임 네트워크 라이브러리로 시작했음
    • python으로 만들어 졌음. 대략 200K라인
    • web, mail, ssh를 포함한 30개 이상의 프로토콜 지원
    • 실질적인 경쟁자로는 ACE가 있음. ACE를 참고해서 개발되었음.

  2. 하나의 프로세스로 여러 개의 소켓을 다룬다.
  3. 우선 BSD 소켓을 공부해야 한다.
    • 최초 만들어진 소켓은 봉쇄형이다. 이 소켓을 fcntl함수로 nonblocking 소켓으로 변경한다.
  4. 입출력 다중화를 먼저 공부하도록 하자.
  5. 최근 트랜드는 epoll, kqueue, Posix AIO, Twisted 이다.
  6. POSIX aio API

 

 

 

 

 

 

 

출처 : http://blog.naver.com/PostView.nhn?blogId=ahncs0728&logNo=150096223790

 

 

 

 

 

 

'Programming > C, C++' 카테고리의 다른 글

[C언어]strcmp함수  (1) 2014.07.18
다시쓰는 C언어 강좌] 079 - 열거형 - enum  (0) 2014.07.18
Chapter 1. C++ 시작하기 pdf 파일  (0) 2014.03.31
[C#] Queue, Thread, AutoReset  (0) 2014.03.31

댓글