7 분 소요


libuv 는 Node.js의 이벤트 기반 I/O 엔진입니다.

소개

Node.js를 개발하면서 이벤트 기반의 I/O 엔진이 필요했습니다.
Node.js는 libev 를 사용했지만, 성능향상, 윈도우즈 IOCP 지원,
Node.js 특화기능 지원등을 위해서 libuv 를 개발하게 되었습니다.

libevent 는 다양한 OS의 고성능 IO(epoll/kqueue/poll/iocp)를 추상화한 Cross-Platform 라이브러리입니다.
아래 그림은 libuv 의 event loop 입니다.



아래 그림은 libuv 를 사용한 Node.js 의 구조입니다.



Node.js는 다음 프로젝트들도 사용합니다.



시작하기

IDE 개발 환경에 대해서 현재 리서치가 진행 중입니다.(2016년 12월 20일 기준)
윈도우에서 코딩을 하고, 리눅스 환경에서 컴파일과 디버깅을 할 수 있는 IDE를 확인 중에 있습니다.
다음은 확인 중인 IDE 리스트입니다.

  • Visual Studio + Linux Development Extension
  • CLion
    CMake 기반의 IDE
  • NetBeans
    리눅스 원격 컴파일/디버깅 가능

libuv 샘플 만들기

  • libuv 프로젝트 git clone
    $ git clone https://github.com/libuv/libuv
    
  • libuv 빌드 후 Output 경로에 include/lib 저장
    CentOS에서 Output의 기본 경로는 /usr/local/ 입니다.
    $ cd libuv
    $ sh autogen.sh
    $ ./configure --prefix={Output 디렉토리}
    $ make
    $ make install
    
  • 소스코드(main.c) 작성
    #include <stdio.h>
    #include <uv.h>
    int main() {
        uv_loop_t loop;
        uv_loop_init(&loop);
    
        printf("Now quitting.\n");
        uv_run(&loop, UV_RUN_DEFAULT);
    
        return 0;
    }
    
  • {Output} 실행파일 빌드
    $ gcc -o {Output} main.c /home/vagrant/projects/uv/lib/libuv.a -I/home/vagrant/projects/uv/include -pthread -lrt
    
  • {Output} 실행파일 빌드
    $ gcc -o {Output} main.c /home/vagrant/projects/uv/lib/libuv.a -I/home/vagrant/projects/uv/include -pthread -lrt
    

    Makefile을 작성한다면,

    main: main.c
        gcc -o {Output File} main.c {Library Output Path}/lib/libuv.a -lrt
    clean:
        rm {Output File}
    
  • 문제해결 libuv를 shared library 로 link 하는 경우 LD_LIBRARY_PATH 환경변수가 추가되어 있어야 합니다.
    환경변수에 추가하기 위해서 ~/.bashrc 에 다음 내용을 추가합니다.
    $ export PATH=$PATH:{Library Path}
    $ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:{Library Path}
    



기초

libuv는 비동기(asynchronous), 이벤트기반(event-driven) 라이브러리입니다.
libuv는 이벤트 루프와 Callback 기반의 IO 통지 기능을 제공합니다.
추가로 타이머, Non-Blocking 네트워크 지원, 비동기 파일 시스템 접근, Child 프로세스와 같은 기능을 제공합니다.

Event loops

Event-driven 프로그래밍은, 응용프로그램은 관심있는 특정 이벤트를 등록하면, 해당 이벤트가 발생할 때 응답을 받습니다.
OS로부터의 이벤트를 수집하거나 다른 이벤트 소스를 모니터링은 libuv가 처리하기 때문에,
사용자는 이벤트가 발생할 때 호출할 콜백을 등록하면 됩니다.
이벤트 루프는 일반적으로 영원히 계속 실행됩니다. 의사 코드는:

while there are still events to process:
    e = get the next event
    if there is a callback associated with e:
        call the callback

이벤트의 일부를 예로 들면:

  • 파일이 쓰기 준비가 되었을 때
  • 소켓에 데이터를 읽을 준비가 되었을 때
  • 타이머 시간이 만료되었을 때


이 이벤트 루프는 libuv의 가장 중요한 함수인 uv_run() 로 캡슐화되었습니다.
시스템 프로그래밍에서는 대용량 계산 처리보다는 입출력을 다루는 것이 일반적입니다.
기존 입출력 함수(read, frpintf 등)의 문제점은 완료되기 전까지 실행을 대기하는 blocking 방식입니다.
고성능을 필요로 하는 프로그램에서 blocking은 가장 큰 장애물입니다.
일반적은 해결방법 중 하나는 Thread를 사용하는 것입니다.
각 Blocking IO 동작을 Thread(또는 Thread Pool)로 나누어 실행하고, Blocking 함수가 Thread에서 호출되면,
프로세서는 CPU가 필요한 다른 Thread가 실행되도록 스케줄링할 수 있습니다.
libuv 에서는 비동기, Non-Blocking 방식을 사용합니다. 대부분의 최신 OS들은 이벤트 통지 서브시스템을 제공합니다.
예를 들면, 응용프로그램이 OS에 소켓을 감시하여 이벤트 통지를 큐에 넣어달라고 요청할 수 있습니다.
응용프로그램은 이벤트를 쉽게 검사할 수 있고, 데이터를 가져올 수 있습니다.
응용프로그램이 한 지점에서 등록한 후, 다른 지점에서 통보를 받기 때문에 비동기(asynchronous) 방식이고,
응용프로그램 프로세스가 작업이 완료되기 전에 다른 작업을 진행할 수 있기 때문에 Non-blocking 방식입니다.
libuv 는 OS 이벤트가 또 다른 libuv 이벤트로 처리되기 때문에 이벤트 루프방식과 잘 맞습니다.



  • Hello World
    다음은 루프를 시작한 후 바로 종료하는 첫 번째 libuv 프로그램입니다.
    #include <stdio.h>
    #include <stdlib.h>
    #include <uv.h>
    int main() {
        uv_loop_t *loop = malloc(sizeof(uv_loop_t));
        uv_loop_init(loop);
    
        printf("Now quitting.\n");
        uv_run(loop, UV_RUN_DEFAULT);
    
        uv_loop_close(loop);
        free(loop);
        return 0;
    }
    

이 프로그램은 처리할 이벤트가 없기 때문에 즉시 종료됩니다.
libuv 이벤트 루프는 다양한 API 함수들을 사용하여 이벤트를 감시하도록 알려줘야 합니다.
libuv v1.0 부터 uv_loop_init (uv_loop_t *) 를 사용하여 루프를 초기화하기 전에 메모리를 할당해야합니다.
이렇게 하면 사용자 정의 메모리 관리를 연결할 수 있습니다.
루프를 해제(de-initialize)할 때에는 uv_loop_close (uv_loop_t *) 를 사용한 다음 메모리에서 삭제해야 합니다.

루프가 끝난 후 프로그램이 종료되고 시스템이 메모리를 회수하기 때문에 예제는 절대로 루프를 닫지 않습니다.
프로덕션급 프로젝트, 특히 장기 실행 시스템 프로그램,은 리소스를 올바르게 릴리스해야 합니다.



Default loop

default loop 는 libuv에서 제공하며 uv_default_loop() 를 사용하여 접근할 수 있습니다.
만약 단일 루프만을 원한다면 이 루프를 사용해야합니다.

node.js는 메인 루프로 default loop를 사용합니다.



Error Handling

실패할 수 있는 초기화 함수 및 동기 함수들은 에러가 발생하면 음수를 반환합니다.
실패할 수 있는 비동기 함수는 상태 파라미터를 callback에 전달합니다.
에러 메시지들은 UV_E* 상수로 정의되어 있습니다.
uv_strerror(int)uv_err_name(int) 함수를 사용하여 각각 에러 문자열 및 이름을 얻을 수 있습니다.

파일과 소켓에 대한 I/O read callback 에는 nread 파라미터가 전달됩니다. 에러가 있다면, nread 가 음수로 설정됩니다.
(UV_EOF 는 파일 끝이라는 에러이므로, 다른 방식으로 처리해야 할 수 있습니다.)



Handles and Requests

libuv는 특정 이벤트를 감시하도록 등록하여 동작합니다.
이는 일반적으로 I/O 장치, 타이머 또는 프로세스에 대한 handle을 생성하여 처리됩니다.
handle은 uv_TYPE_t 라는 이름의 불투명한 구조체입니다.
여기서 TYPE 은 handle 이 사용되는 대상을 나타냅니다.



libuv watchers

  /* Handle types. */
  typedef struct uv_loop_s uv_loop_t;
  typedef struct uv_handle_s uv_handle_t;
  typedef struct uv_stream_s uv_stream_t;
  typedef struct uv_tcp_s uv_tcp_t;
  typedef struct uv_udp_s uv_udp_t;
  typedef struct uv_pipe_s uv_pipe_t;
  typedef struct uv_tty_s uv_tty_t;
  typedef struct uv_poll_s uv_poll_t;
  typedef struct uv_timer_s uv_timer_t;
  typedef struct uv_prepare_s uv_prepare_t;
  typedef struct uv_check_s uv_check_t;
  typedef struct uv_idle_s uv_idle_t;
  typedef struct uv_async_s uv_async_t;
  typedef struct uv_process_s uv_process_t;
  typedef struct uv_fs_event_s uv_fs_event_t;
  typedef struct uv_fs_poll_s uv_fs_poll_t;
  typedef struct uv_signal_s uv_signal_t;

  /* Request types. */
  typedef struct uv_req_s uv_req_t;
  typedef struct uv_getaddrinfo_s uv_getaddrinfo_t;
  typedef struct uv_getnameinfo_s uv_getnameinfo_t;
  typedef struct uv_shutdown_s uv_shutdown_t;
  typedef struct uv_write_s uv_write_t;
  typedef struct uv_connect_s uv_connect_t;
  typedef struct uv_udp_send_s uv_udp_send_t;
  typedef struct uv_fs_s uv_fs_t;
  typedef struct uv_work_s uv_work_t;

  /* None of the above. */
  typedef struct uv_cpu_info_s uv_cpu_info_t;
  typedef struct uv_interface_address_s uv_interface_address_t;
  typedef struct uv_dirent_s uv_dirent_t;  

handle은 수명이 긴 객체를 나타냅니다.
이러한 handle에 대한 비동기 작업은 request를 사용하여 확인할 수 있습니다.
request는 수명이 짧고 (일반적으로 하나의 callback 에서만 사용) 일반적으로 handle에 대해 하나의 I/O 작업을 나타냅니다.
request는 개별 작업의 시작과 callback 사이에 context 를 유지하기 위해 사용됩니다.
예를 들면, UDP 소켓은 uv_udp_t 로 나타내지만 소켓에 대한 개별 write 작업은 write가 완료된 후에
callback에 전달되는 uv_udp_send_t 구조체를 사용합니다. handle은 다음 함수로 설정합니다:

  uv_TYPE_init(uv_loop_t *, uv_TYPE_t *)

callback은 감시자(watcher)가 관심 있는 이벤트가 발생할 때마다 libuv가 호출하는 함수입니다.
응용프로그램 로직은 일반적으로 callback으로 구현됩니다. 예를 들어 IO 감시자의 callback은 파일에서 읽은 데이터를 수신하고
타이머 callback은 시간 초과 시 호출됩니다.



Idling

다음은 idle handle을 사용하는 예입니다.
callback은 이벤트 루프가 돌 때마다 한 번씩 호출됩니다.
idle 감시자를 사용하여 감시자 수명 주기를 살펴보고 감시자가 있기 때문에 uv_run() 이 block 되는 것을 보겠습니다.
idle 감시자는 카운트가 만료되면 멈추고 활성화된 이벤트 감시자가 없기 때문에 uv_run() 이 종료됩니다.

idle-basic/main.c

  #include <stdio.h>
  #include <uv.h>

  int64_t counter = 0;

  void wait_for_a_while(uv_idle_t* handle) {
      counter++;

      if (counter >= 10e6)
          uv_idle_stop(handle);
  }

  int main() {
      uv_idle_t idler;

      uv_idle_init(uv_default_loop(), &idler);
      uv_idle_start(&idler, wait_for_a_while);

      printf("Idling...\n");
      uv_run(uv_default_loop(), UV_RUN_DEFAULT);

      uv_loop_close(uv_default_loop());
      return 0;
  }



Storing Context

콜백 기반 프로그래밍에서는 자주 호출 위치와 callback 사이에 응용프로그램 정보인 context를 전달이 필요할 때가 있습니다.
모든 handle과 request에는 context를 설정할 수 있는 void* 데이터 멤버가 있습니다.
이 패턴은 많은 C 라이브러리에서 사용하는 일반적인 방법입니다.
또, uv_loop_t 에도 이와 비슷한 데이터 멤버가 있습니다.



예제

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <uv.h>

uv_loop_t *loop;
struct sockaddr_in addr;

void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
    buf->base = (char*)malloc(suggested_size);
    buf->len = suggested_size;
}

void echo_write(uv_write_t *req, int status) {
    if (status) {
        fprintf(stderr, "Write error %s\n", uv_strerror(status));
    }
    free(req);
}

void echo_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {
    if (nread < 0) {
        if (nread != UV_EOF) {
            fprintf(stderr, "Read error %s\n", uv_err_name(nread));
            uv_close((uv_handle_t*) client, NULL);
        }
    } else if (nread > 0) {
        uv_write_t *req = (uv_write_t *) malloc(sizeof(uv_write_t));
        uv_buf_t wrbuf = uv_buf_init(buf->base, nread);
        uv_write(req, client, &wrbuf, 1, echo_write);
    }

    if (buf->base) {
        free(buf->base);
    }
}

void on_new_connection(uv_stream_t *server, int status) {
    if (status < 0) {
        fprintf(stderr, "New connection error %s\n", uv_strerror(status));
        return;
    }

    uv_tcp_t *client = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
    uv_tcp_init(loop, client);
    if (uv_accept(server, (uv_stream_t*) client) == 0) {
        uv_read_start((uv_stream_t*)client, alloc_buffer, echo_read);
    } else {
        uv_close((uv_handle_t*) client, NULL);
    }
}

int main() {
    loop = uv_default_loop();

    uv_tcp_t server;
    uv_tcp_init(loop, &server);

    uv_ip4_addr("0.0.0.0", 7000, &addr);

    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
    int r = uv_listen((uv_stream_t*)&server, 128, on_new_connection);
    if (r) {
        fprintf(stderr, "Listen error %s\n", uv_strerror(r));
        return 1;
    }
    return uv_run(loop, UV_RUN_DEFAULT);
}



기타 설정

CMake build for libuv

CMake build for libuv 저장소는 Autotools 또는 GYP를 사용하지 않고도 libuv를 빌드할 수있는 CMakeLists.txt를 포함합니다.
특히, CMake를 프로젝트 파일 형식으로 사용하는 CLion과 같은 IDE 사용자에게 유용합니다.
예를 들면, 일반적인 소켓 read는 데이터를 받을 때까지 block 되지만, 응용프로그램이 소켓을 감시하다가 OS에게 요청할 수 있습니다.

  • git submodule 추가

    $ git clone https://github.com/libuv/libuv
    $ cd libuv
    $ git submodule add https://github.com/jen20/libuv-cmake libuv-cmake
    

Visual Studio 솔루션 만들기

svn은 커맨드 라인으로 실행해야 하기 때문에 커맨드 라인으로 svn 명령어를 실행하지 못한다면 silk-subversion을 설치해야 합니다.
libuv를 VC++로 빌드하기 위해서는 먼저 svn과 python 2.7이 설치되어 있어야 합니다. libuv 소스 코드의 압축을 푼 후,

  1. vcbuild.bat를 실행합니다.
  2. build\gyp\tools 디렉토리에 있는 petty_sln.py를 실행합니다.
  3. libuv 폴더에 uv.sln 파일이 생성되었습니다.



참고자료

댓글남기기