libuv

libuv는 이벤트 기반의 비동기 I/O 모델을 중심으로 설계된 라이브러리다.

libuv 이 다이어그램은 각각의 I/O가 어떤 디자인으로 구성된 시스템으로 요청을 처리하는지 간략하게 보여준다. I/O Multiplexing - select, poll, epoll 글에서 서술한 Linux의 epoll, 그리고 BSD에서의 epoll인 kqueue와 같은 I/O 멀티플렉싱 기법을 사용하여 네트워크 I/O를 위한 구현체를 구성한다.

libuv는 네트워크 I/O에 대해서는 각 loop의 스레드 안에서만 동작하는 것을 보장한다. 하지만, 파일 I/O에 대해서는 싱글 스레드 기반으로 동작하지 않고 스레드 풀1에서 동작한다.

Handle

활성 상태일 때 특정 작업을 수행할 수 있는 장기적인 객체(long-lived)이며 OS의 리소스(예: TCP 소켓, 타이머, 파일 디스크립터 등)를 추상화한다. 이벤트 루프에 등록되어 해당 리소스의 상태(읽기, 쓰기, 타이머 만료 등)를 지속적으로 감시하고, 이벤트가 발생하면 적절한 핸들링을 한다.

예를 들어 uv_tcp_t, uv_timer_t 등은 Handle에 해당하며, 이들은 네트워크 연결이나 타이머 등과 같이 지속적으로 관리되어야 하는 자원이다. 아래의 enum은 libuv에서 지원하는 핸들의 종류를 나타낸다.

typedef enum {
  UV_UNKNOWN_HANDLE = 0,
  UV_ASYNC,
  UV_CHECK,
  UV_FS_EVENT,
  UV_FS_POLL,
  UV_HANDLE,
  UV_IDLE,
  UV_NAMED_PIPE,
  UV_POLL,
  UV_PREPARE,
  UV_PROCESS,
  UV_STREAM,
  UV_TCP,
  UV_TIMER,
  UV_TTY,
  UV_UDP,
  UV_SIGNAL,
  UV_FILE,
  UV_HANDLE_TYPE_MAX
} uv_handle_type;

각 핸들의 구현은 아래의 UV_HANDLE_FIELDS를 포함한다.

#define UV_HANDLE_FIELDS                                                      \
  /* public */                                                                \
  void* data;                                                                 \
  /* read-only */                                                             \
  uv_loop_t* loop;                                                            \
  uv_handle_type type;                                                        \
  /* private */                                                               \
  uv_close_cb close_cb;                                                       \
  struct uv__queue handle_queue;                                              \
  union {                                                                     \
    int fd;                                                                   \
    void* reserved[4];                                                        \
  } u;                                                                        \
  UV_HANDLE_PRIVATE_FIELDS                                                    \

Request

일반적으로 단기적인 작업(short-lived)을 나타낸다. “나중에 수행될 작업"을 표현하는 객체로, 특정 비동기 작업(예: 연결, 쓰기, 파일 시스템 작업 등)을 나타낸다. 작업을 시작할 때 Request 객체를 생성하고 필요한 데이터를(예: 버퍼, 콜백 함수 등) 붙인 후 호출하면, libuv는 해당 작업을 적절한 방식(예를 들어, 이벤트 루프 내의 시스템 호출 또는 스레드 풀에서의 작업 실행)으로 처리한다. 작업이 완료되면, 등록된 콜백 함수가 호출되어 결과를 전달한다.

예시: uv_write()를 호출할 때 사용하는 Request 객체는 특정 시점에 데이터를 쓰기 위한 작업을 나타내며, 작업 완료 후 콜백을 통해 결과를 알린다.

typedef enum {
    UV_UNKNOWN_REQ = 0,
    UV_REQ,
    UV_CONNECT,
    UV_WRITE,
    UV_SHUTDOWN,
    UV_UDP_SEND,
    UV_FS,
    UV_WORK,
    UV_GETADDRINFO,
    UV_GETNAMEINFO,
    UV_REQ_TYPE_MAX,
} uv_req_type;

Loop Iteration 살펴보기

libuv-loop-iteration

위의 다이어그램의 과정은 아래 처럼 설명할 수 있다.

  1. 루프의 ‘현재(now)’ 개념이 초기화된다.
  2. UV_RUN_DEFAULT 모드에서 실행된 경우, 기한이 도래한 타이머들이 실행된다. 루프의 현재 시간 이전에 예약된 모든 활성 타이머의 콜백이 호출된다.
    • UV_RUN_DEFAULT: 더이상 활성화되거나 참조된 Handle 또는 Request가 없을 때까지 이벤트 루프를 실행한다. uv_stop()이 호출되었지만 아직 활성 핸들이나 요청이 남아 있으면 non-zero를 반환한다.
  3. 루프가 살아 있다면 한 번의 반복(iteration)이 시작되며, 그렇지 않으면 즉시 종료된다. 루프는 활성화되고 참조된 핸들, 활성 요청, 또는 닫히는 중인 핸들이 있을 경우 살아 있다고 간주된다.
  4. 보류 중인 콜백이 호출된다. 대부분의 경우, 모든 I/O 콜백은 I/O 폴링 직후 호출된다. 그러나 특정 경우에는 다음 루프 반복에서 실행되도록 연기될 수 있다. 이전 반복에서 연기된 I/O 콜백이 있다면, 이 시점에서 실행된다.
  5. Idle(유휴) 핸들 콜백이 호출된다. 이름과 다르게, 활성화된 유휴 핸들은 루프의 각 반복마다 실행된다.
  6. Prepare(준비) 핸들 콜백이 호출된다. 준비 핸들은 루프가 I/O를 차단(blocking)하기 직전에 실행된다.
  7. 폴링(polling) 대기 시간(timeout)이 계산된다. I/O 차단 전에 루프는 대기 시간을 계산한다. 다음 규칙에 따라 결정된다:
    • UV_RUN_NOWAIT 플래그로 실행된 경우, 대기 시간은 0이다.
    • 루프가 정지될 예정(uv_stop() 호출됨)이라면, 대기 시간은 0이다.
    • 활성 핸들 또는 요청이 없다면, 대기 시간은 0이다.
    • 활성 유휴 핸들이 있다면, 대기 시간은 0이다.
    • 닫혀야 할 핸들이 있다면, 대기 시간은 0이다.
    • 위 조건이 모두 해당되지 않는 경우, 가장 가까운 타이머의 시간 또는 활성 타이머가 없으면 무한대(infinity)가 설정된다.
  8. 루프가 I/O를 위해 차단된다. 이전 단계에서 계산된 시간 동안 루프가 I/O를 차단한다. 읽기 또는 쓰기 작업을 모니터링하는 모든 I/O 관련 핸들은 이 시점에서 콜백이 호출된다.
  9. Check(검사) 핸들 콜백이 호출된다. 검사 핸들은 루프가 I/O 차단을 끝낸 직후 실행된다. 이는 준비(prepare) 핸들의 반대 역할을 한다.
  10. 닫기(close) 콜백이 호출된다. uv_close()로 닫힌 핸들은 이 시점에서 닫기 콜백이 실행된다.
  11. 루프의 ‘현재(now)’ 개념이 업데이트된다.
  12. 기한이 도래한 타이머가 실행된다. 이 시점에서 ‘현재(now)’는 다시 업데이트되지 않는다. 따라서 다른 타이머를 처리하는 동안 기한이 도래한 타이머가 있어도, 다음 루프 반복이 시작될 때까지 실행되지 않는다.
  13. 반복이 종료된다. UV_RUN_NOWAIT 또는 UV_RUN_ONCE 모드에서 실행된 경우, 반복이 종료되고 uv_run()이 반환된다. UV_RUN_DEFAULT 모드에서는 루프가 살아 있다면 처음부터 다시 실행되고, 살아 있지 않으면 종료된다.

  1. 그 이유로는 의존할만한 수준의 플랫폼별 파일 I/O 기본 라이브러리가 없기 때문이라고 이야기한다. ↩︎