libuv
libuv는 이벤트 기반의 비동기 I/O 모델을 중심으로 설계된 라이브러리다.
- Node.js에서는 비동기 이벤트 기반 아키텍처를 지원하는 중요한 라이브러리 중 하나이다.
- uvloop는 libuv를 사용하는 Python의 asyncio 이벤트 루프 구현체이다. 요즘 uvicorn으로 FastAPI와 같은 웹 프레임워크와 함께 사용된다.
이 다이어그램은 각각의 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 \
uv_loop_t* loop
: 핸들이 속한 루프uv_handle_type type
: 핸들의 타입uv_close_cb close_cb
: 핸들이 닫힐 때 호출되는 콜백struct uv__queue handle_queue
: 핸들이 속한 큐union { int fd; void* reserved[4]; } u;
: 핸들의 파일 디스크립터 또는 예약된 공간- reserved[4]는 특정 플랫폼이나 핸들 유형에 따라 다르게 활용될 수 있는 여분의 공간
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 살펴보기
위의 다이어그램의 과정은 아래 처럼 설명할 수 있다.
- 루프의 ‘현재(now)’ 개념이 초기화된다.
UV_RUN_DEFAULT
모드에서 실행된 경우, 기한이 도래한 타이머들이 실행된다. 루프의 현재 시간 이전에 예약된 모든 활성 타이머의 콜백이 호출된다.UV_RUN_DEFAULT
: 더이상 활성화되거나 참조된 Handle 또는 Request가 없을 때까지 이벤트 루프를 실행한다.uv_stop()
이 호출되었지만 아직 활성 핸들이나 요청이 남아 있으면 non-zero를 반환한다.
- 루프가 살아 있다면 한 번의 반복(iteration)이 시작되며, 그렇지 않으면 즉시 종료된다. 루프는 활성화되고 참조된 핸들, 활성 요청, 또는 닫히는 중인 핸들이 있을 경우 살아 있다고 간주된다.
- 보류 중인 콜백이 호출된다. 대부분의 경우, 모든 I/O 콜백은 I/O 폴링 직후 호출된다. 그러나 특정 경우에는 다음 루프 반복에서 실행되도록 연기될 수 있다. 이전 반복에서 연기된 I/O 콜백이 있다면, 이 시점에서 실행된다.
- Idle(유휴) 핸들 콜백이 호출된다. 이름과 다르게, 활성화된 유휴 핸들은 루프의 각 반복마다 실행된다.
- Prepare(준비) 핸들 콜백이 호출된다. 준비 핸들은 루프가 I/O를 차단(blocking)하기 직전에 실행된다.
- 폴링(polling) 대기 시간(timeout)이 계산된다. I/O 차단 전에 루프는 대기 시간을 계산한다. 다음 규칙에 따라 결정된다:
UV_RUN_NOWAIT
플래그로 실행된 경우, 대기 시간은 0이다.- 루프가 정지될 예정(
uv_stop()
호출됨)이라면, 대기 시간은 0이다. - 활성 핸들 또는 요청이 없다면, 대기 시간은 0이다.
- 활성 유휴 핸들이 있다면, 대기 시간은 0이다.
- 닫혀야 할 핸들이 있다면, 대기 시간은 0이다.
- 위 조건이 모두 해당되지 않는 경우, 가장 가까운 타이머의 시간 또는 활성 타이머가 없으면 무한대(infinity)가 설정된다.
- 루프가 I/O를 위해 차단된다. 이전 단계에서 계산된 시간 동안 루프가 I/O를 차단한다. 읽기 또는 쓰기 작업을 모니터링하는 모든 I/O 관련 핸들은 이 시점에서 콜백이 호출된다.
- Check(검사) 핸들 콜백이 호출된다. 검사 핸들은 루프가 I/O 차단을 끝낸 직후 실행된다. 이는 준비(prepare) 핸들의 반대 역할을 한다.
- 닫기(close) 콜백이 호출된다.
uv_close()
로 닫힌 핸들은 이 시점에서 닫기 콜백이 실행된다. - 루프의 ‘현재(now)’ 개념이 업데이트된다.
- 기한이 도래한 타이머가 실행된다. 이 시점에서 ‘현재(now)’는 다시 업데이트되지 않는다. 따라서 다른 타이머를 처리하는 동안 기한이 도래한 타이머가 있어도, 다음 루프 반복이 시작될 때까지 실행되지 않는다.
- 반복이 종료된다.
UV_RUN_NOWAIT
또는UV_RUN_ONCE
모드에서 실행된 경우, 반복이 종료되고uv_run()
이 반환된다.UV_RUN_DEFAULT
모드에서는 루프가 살아 있다면 처음부터 다시 실행되고, 살아 있지 않으면 종료된다.
그 이유로는 의존할만한 수준의 플랫폼별 파일 I/O 기본 라이브러리가 없기 때문이라고 이야기한다. ↩︎