>  기사  >  웹 프론트엔드  >  libuv란 무엇입니까? libuv의 이벤트 폴링에 대한 간략한 분석입니다(노드 코어 종속성).

libuv란 무엇입니까? libuv의 이벤트 폴링에 대한 간략한 분석입니다(노드 코어 종속성).

青灯夜游
青灯夜游앞으로
2022-03-22 19:58:553741검색

이 기사는 libuv에 대한 Node의 핵심 종속성을 이해하고 libuv가 무엇인지 소개하며 libuv의 이벤트 폴링이 모든 사람에게 도움이 되기를 바랍니다.

libuv란 무엇입니까? libuv의 이벤트 폴링에 대한 간략한 분석입니다(노드 코어 종속성).

Node.js를 언급하면 ​​대부분의 프론트엔드 엔지니어는 이를 기반으로 서버를 개발할 것이라고 생각하지만 풀스택 엔지니어가 되려면 JavaScript를 언어로 마스터하면 됩니다. Node.js의 의미는 여기에만 있는 것이 아닙니다.

많은 고급 언어의 경우 실행 권한이 운영 체제에 도달할 수 있지만 브라우저 측에서 실행되는 JavaScript는 예외입니다. 브라우저가 만든 샌드박스 환경은 프런트 엔드 엔지니어를 프로그래밍 세계의 상아탑에 봉인합니다. . 하지만 Node.js의 등장으로 이런 단점이 보완됐고, 프론트엔드 엔지니어도 컴퓨터 세계의 밑바닥까지 갈 수 있게 됐다.

따라서 프론트엔드 엔지니어에게 Nodejs의 의미는 풀스택 개발 기능을 제공하는 것뿐만 아니라 더 중요한 것은 프론트엔드 엔지니어에게 컴퓨터의 기본 세계로의 문을 열어준다는 것입니다. 이 기사는 Node.js의 구현 원칙을 분석하여 그 문을 엽니다.

Node.js 소스 코드 구조

Node.js 소스 코드 웨어하우스에는 C 언어로 작성된 모듈(예: libuv, V8)과 JavaScript 언어로 작성된 모듈(예: acorn, acorn -plugins), 아래 그림과 같습니다.

libuv란 무엇입니까? libuv의 이벤트 폴링에 대한 간략한 분석입니다(노드 코어 종속성).

  • acorn: JavaScript로 작성된 경량 JavaScript 파서입니다.
  • acorn-plugins: acorn의 확장 모듈로, acorn이 클래스 선언과 같은 ES6 기능 구문 분석을 지원할 수 있습니다.
  • brotli: C 언어로 작성된 Brotli 압축 알고리즘.
  • cares: 비동기 DNS 요청을 처리하기 위해 C 언어로 작성된 "c-ares"로 작성되어야 합니다.
  • 히스토그램: 히스토그램 생성 기능을 구현하기 위해 C 언어로 작성되었습니다.
  • icu-small: C 언어로 작성되고 Node.js용으로 맞춤화된 ICU(International Components for Unicode) 라이브러리(유니코드 운영을 위한 일부 기능 포함).
  • llhttp: C 언어로 작성되었으며 경량 http 파서입니다.
  • nghttp2/nghttp3/ngtcp2: HTTP/2, HTTP/3, TCP/2 프로토콜을 처리합니다.
  • node-inspect: Node.js 프로그램이 CLI 디버그 디버깅 모드를 지원하도록 허용합니다.
  • npm: JavaScript로 작성된 Node.js 모듈 관리자.
  • openssl: C 언어로 작성되었으며 암호화 관련 모듈이며 tls 및 crypto 모듈 모두에 사용됩니다.
  • uv: 비차단 I/O 작업을 사용하여 C 언어로 작성되어 Node.js에 시스템 리소스에 액세스할 수 있는 기능을 제공합니다.
  • uvwasi: C 언어로 작성되었으며 WASI 시스템 호출 API를 구현합니다.
  • v8: C 언어, JavaScript 엔진으로 작성되었습니다.
  • zlib: 빠른 압축을 위해 Node.js는 zlib를 사용하여 동기식, 비동기식 및 데이터 스트림 압축 및 압축 해제 인터페이스를 만듭니다.

가장 중요한 것은 v8 및 uv 디렉토리에 해당하는 모듈입니다. V8 자체에는 비동기식으로 실행되는 기능이 없지만 브라우저의 다른 스레드의 도움으로 구현됩니다. 이것이 바로 js가 단일 스레드라고 말하는 이유입니다. 파싱 엔진은 코드의 동기 파싱만 지원하기 때문입니다. 하지만 Node.js에서는 비동기 구현이 주로 libuv에 의존합니다. libuv의 구현 원리를 집중적으로 분석해 보겠습니다.

libuv란 무엇입니까

libuv는 여러 플랫폼을 지원하는 C로 작성된 비동기 I/O 라이브러리로 주로 차단을 쉽게 일으키는 I/O 작업 문제를 해결합니다. 원래 Node.js와 함께 사용하기 위해 특별히 개발되었지만 나중에 Luvit, Julia, pyuv 등과 같은 다른 모듈에서도 사용되었습니다. 다음 그림은 libuv의 구조도이다.

libuv란 무엇입니까? libuv의 이벤트 폴링에 대한 간략한 분석입니다(노드 코어 종속성).

libuv에는 두 가지 비동기 구현 방법이 있는데, 위 그림의 왼쪽과 오른쪽에 있는 노란색 상자가 선택한 두 부분입니다.

왼쪽 부분은 네트워크 I/O 모듈로, 서로 다른 플랫폼에서 서로 다른 구현 메커니즘을 가지고 있습니다. 이는 Linux 시스템에서 epoll을 통해 구현되며, OSX 및 기타 BSD 시스템은 KQueue를 사용하고, SunOS 시스템은 이벤트 포트를 사용하며, Windows 시스템은 IOCP를 사용합니다. . 운영체제의 기본 API를 포함하기 때문에 상대적으로 이해하기 복잡하므로 여기서는 자세히 소개하지 않겠습니다.

오른쪽 부분에는 스레드 풀을 통해 비동기 작업을 구현하는 파일 I/O 모듈, DNS 모듈 및 사용자 코드가 포함됩니다. 파일 I/O는 네트워크 I/O와 다릅니다. libuv는 시스템의 기본 API에 의존하지 않고 전역 스레드 풀에서 파일 I/O 작업을 차단합니다.

libuv의 이벤트 폴링

다음 그림은 libuv 공식 웹사이트에서 제공하는 이벤트 폴링 워크플로 다이어그램을 코드와 함께 분석해 보겠습니다.

libuv란 무엇입니까? libuv의 이벤트 폴링에 대한 간략한 분석입니다(노드 코어 종속성).

libuv 이벤트 루프의 핵심 코드는 uv_run() 함수에 구현되어 있습니다. 다음은 Unix 시스템의 핵심 코드 중 일부입니다. C언어로 작성되었지만 자바스크립트와 같은 고급 언어이므로 이해하는데 크게 어렵지는 않습니다. 가장 큰 차이점은 별표와 화살표일 수 있습니다. 별표는 무시해도 됩니다. 예를 들어, 함수 매개변수의 uv_loop_t* 루프는 uv_loop_t 유형의 변수 루프로 이해될 수 있습니다. 화살표 "→"는 마침표 "."로 이해될 수 있습니다. 예를 들어 loop→stop_flag는 loop.stop_flag로 이해될 수 있습니다.

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  ... 
r = uv__loop_alive(loop);
if (!r) uv__update_time(loop);
while (r != 0 && loop - >stop_flag == 0) {
    uv__update_time(loop);
    uv__run_timers(loop);
    ran_pending = uv__run_pending(loop);
    uv__run_idle(loop);
    uv__run_prepare(loop);...uv__io_poll(loop, timeout);
    uv__run_check(loop);
    uv__run_closing_handles(loop);...
}...
}

uv__loop_alive

이 함수는 이벤트 폴링을 계속할지 여부를 결정하는 데 사용됩니다. 루프 개체에 활성 작업이 없으면 0을 반환하고 루프를 종료합니다.

C 언어에서 이 "작업"은 작업을 가리키는 변수로 이해될 수 있는 "핸들"이라는 전문적인 이름을 갖습니다. 핸들은 요청과 핸들이라는 두 가지 범주로 나눌 수 있으며 각각 짧은 수명 주기 핸들과 긴 수명 주기 핸들을 나타냅니다. 구체적인 코드는 다음과 같습니다.

static int uv__loop_alive(const uv_loop_t * loop) {
    return uv__has_active_handles(loop) || uv__has_active_reqs(loop) || loop - >closing_handles != NULL;
}

uv__update_time

시간 관련 시스템 호출 수를 줄이기 위해 이 함수는 현재 시스템 시간을 캐시하는 데 사용되며 정확도가 매우 높으며 나노초 수준이지만 단위는 여전히 밀리초입니다.

구체적인 소스코드는 다음과 같습니다.

UV_UNUSED(static void uv__update_time(uv_loop_t * loop)) {
    loop - >time = uv__hrtime(UV_CLOCK_FAST) / 1000000;
}

uv__run_timers

setTimeout()과 setInterval()에서 시간 임계값에 도달하는 콜백 함수를 실행합니다. 이 실행 프로세스는 아래 코드에서 볼 수 있듯이 최소 힙 구조의 데이터에 저장되며 최소 힙이 비어 있거나 시간 임계값에 도달하지 않은 경우 종료됩니다. .

타이머 콜백 기능을 실행하기 전에 타이머를 제거하세요. 반복이 설정된 경우 최소 힙에 다시 추가한 후 타이머 콜백이 실행됩니다.

구체적인 코드는 다음과 같습니다.

void uv__run_timers(uv_loop_t * loop) {
    struct heap_node * heap_node;
    uv_timer_t * handle;
    for (;;) {
        heap_node = heap_min(timer_heap(loop));
        if (heap_node == NULL) break;
        handle = container_of(heap_node, uv_timer_t, heap_node);
        if (handle - >timeout > loop - >time) break;
        uv_timer_stop(handle);
        uv_timer_again(handle);
        handle - >timer_cb(handle);
    }
}

uv__run_pending

pending_queue에 저장된 모든 I/O 콜백 함수를 탐색하고, 보류 중인_queue가 비어 있으면 0을 반환하고, 그렇지 않으면 보류 중인 콜백 함수를 실행한 후 1을 반환합니다.

코드는 다음과 같습니다.

static int uv__run_pending(uv_loop_t * loop) {
    QUEUE * q;
    QUEUE pq;
    uv__io_t * w;
    if (QUEUE_EMPTY( & loop - >pending_queue)) return 0;
    QUEUE_MOVE( & loop - >pending_queue, &pq);
    while (!QUEUE_EMPTY( & pq)) {
        q = QUEUE_HEAD( & pq);
        QUEUE_REMOVE(q);
        QUEUE_INIT(q);
        w = QUEUE_DATA(q, uv__io_t, pending_queue);
        w - >cb(loop, w, POLLOUT);
    }
    return 1;
}

uvrun_idle / uvrun_prepare / uv__run_check

이 세 가지 함수는 매크로 함수 UV_LOOP_WATCHER_DEFINE을 통해 정의됩니다. 매크로 함수는 코드 템플릿 또는 정의하는 데 사용되는 함수로 이해할 수 있습니다. 기능. 매크로 함수가 3번 호출되고 이름 매개변수 값인 prepare, check, idle이 각각 전달됩니다. 동시에 uvrun_idle, uvrun_prepare, uv__run_check 세 가지 함수가 정의됩니다.

따라서 이들의 실행 논리는 모두 선입 선출 원칙에 따라 대기열 loop->name##_handles에서 객체를 반복하고 꺼낸 다음 해당 콜백 함수를 실행합니다.

#define UV_LOOP_WATCHER_DEFINE(name, type)
void uv__run_##name(uv_loop_t* loop) {
  uv_##name##_t* h;
  QUEUE queue;
  QUEUE* q;
  QUEUE_MOVE(&loop->name##_handles, &queue);
  while (!QUEUE_EMPTY(&queue)) {
    q = QUEUE_HEAD(&queue);
    h = QUEUE_DATA(q, uv_##name##_t, queue);
    QUEUE_REMOVE(q);
    QUEUE_INSERT_TAIL(&loop->name##_handles, q);
    h->name##_cb(h);
  }
}
UV_LOOP_WATCHER_DEFINE(prepare, PREPARE) 
UV_LOOP_WATCHER_DEFINE(check, CHECK) 
UV_LOOP_WATCHER_DEFINE(idle, IDLE)

uv__io_poll

uv__io_poll은 주로 I/O 작업 폴링에 사용됩니다. 구체적인 구현은 운영 체제에 따라 달라집니다. 분석을 위해 Linux 시스템을 예로 들어 보겠습니다.

uv__io_poll 함수의 핵심은 두 개의 루프 코드입니다. 코드의 일부는 다음과 같습니다.

void uv__io_poll(uv_loop_t * loop, int timeout) {
    while (!QUEUE_EMPTY( & loop - >watcher_queue)) {
        q = QUEUE_HEAD( & loop - >watcher_queue);
        QUEUE_REMOVE(q);
        QUEUE_INIT(q);
        w = QUEUE_DATA(q, uv__io_t, watcher_queue);
        e.events = w - >pevents;
        e.data.fd = w - >fd;
        if (w - >events == 0) op = EPOLL_CTL_ADD;
        else op = EPOLL_CTL_MOD;
        if (epoll_ctl(loop - >backend_fd, op, w - >fd, &e)) {
            if (errno != EEXIST) abort();
            if (epoll_ctl(loop - >backend_fd, EPOLL_CTL_MOD, w - >fd, &e)) abort();
        }
        w - >events = w - >pevents;
    }
    for (;;) {
        for (i = 0; i < nfds; i++) {
            pe = events + i;
            fd = pe - >data.fd;
            w = loop - >watchers[fd];
            pe - >events &= w - >pevents | POLLERR | POLLHUP;
            if (pe - >events == POLLERR || pe - >events == POLLHUP) pe - >events |= w - >pevents & (POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI);
            if (pe - >events != 0) {
                if (w == &loop - >signal_io_watcher) have_signals = 1;
                else w - >cb(loop, w, pe - >events);
                nevents++;
            }
        }
        if (have_signals != 0) loop - >signal_io_watcher.cb(loop, &loop - >signal_io_watcher, POLLIN);
    }...
}

while 루프에서 관찰자 큐 watcher_queue를 탐색하고 이벤트 및 파일 설명자를 꺼냅니다. 이를 이벤트 객체 e에 할당한 다음 epoll_ctl 함수를 호출하여 epoll 이벤트를 등록하거나 수정합니다.

for 루프에서는 epoll에 대기 중인 파일 설명자를 먼저 꺼내 nfds에 할당한 다음 nfds를 순회하여 콜백 함수를 실행합니다.

uv__run_closing_handles

닫기를 기다리는 대기열을 순회하고 stream, tcp, udp 등과 같은 핸들을 닫은 다음 핸들에 해당하는 close_cb를 호출합니다. 코드는 다음과 같습니다.

static void uv__run_closing_handles(uv_loop_t * loop) {
    uv_handle_t * p;
    uv_handle_t * q;
    p = loop - >closing_handles;
    loop - >closing_handles = NULL;
    while (p) {
        q = p - >next_closing;
        uv__finish_close(p);
        p = q;
    }
}

process.nextTick 및 Promise

process.nextTick 및 Promise는 모두 비동기 API이지만 이벤트 폴링의 각 단계에 자체 작업 대기열이 없습니다. 완성. 따라서 이 두 가지 비동기 API를 사용할 때는 주의가 필요합니다. 들어오는 콜백 함수에서 긴 작업이나 재귀가 수행되면 이벤트 폴링이 차단되어 I/O 작업이 "고갈"됩니다.

다음 코드는 prcoess.nextTick을 재귀적으로 호출하여 fs.readFile의 콜백 함수가 실행되지 못하게 하는 예입니다.

fs.readFile(&#39;config.json&#39;, (err, data) = >{...
}) const traverse = () = >{
    process.nextTick(traverse)
}

이 문제를 해결하려면 setImmediate를 대신 사용할 수 있습니다. 왜냐하면 setImmediate가 이벤트 루프에서 콜백 함수 대기열을 실행하기 때문입니다. process.nextTick 작업 대기열은 Promise 작업 대기열보다 우선순위가 높습니다. 구체적인 이유는 다음 코드를 참조하세요.

function processTicksAndRejections() {
    let tock;
    do {
        while (tock = queue.shift()) {
            const asyncId = tock[async_id_symbol];
            emitBefore(asyncId, tock[trigger_async_id_symbol], tock);
            try {
                const callback = tock.callback;
                if (tock.args === undefined) {
                    callback();
                } else {
                    const args = tock.args;
                    switch (args.length) {
                    case 1:
                        callback(args[0]);
                        break;
                    case 2:
                        callback(args[0], args[1]);
                        break;
                    case 3:
                        callback(args[0], args[1], args[2]);
                        break;
                    case 4:
                        callback(args[0], args[1], args[2], args[3]);
                        break;
                    default:
                        callback(...args);
                    }
                }
            } finally {
                if (destroyHooksExist()) emitDestroy(asyncId);
            }
            emitAfter(asyncId);
        }
        runMicrotasks();
    } while (! queue . isEmpty () || processPromiseRejections());
    setHasTickScheduled(false);
    setHasRejectionToWarn(false);
}

processTicksAndRejections() 함수에서 볼 수 있듯이 먼저 대기열의 콜백 함수는 다음과 같습니다. while 루프를 통해 꺼내고 이 큐에 있는 콜백 함수는 process.nextTick을 통해 추가됩니다. while 루프가 끝나면 runMicrotasks() 함수가 호출되어 Promise 콜백 함수를 실행합니다.

요약

libuv에 의존하는 Node.js의 핵심 구조는 두 부분으로 나눌 수 있습니다. 한 부분은 네트워크 I/O이며, 다른 부분은 다른 운영 체제에 따라 다른 시스템 API에 의존합니다. 파일 I/O, DNS, 사용자 코드, 이 부분은 스레드 풀에서 처리됩니다.

비동기 작업을 처리하기 위한 libuv의 핵심 메커니즘은 이벤트 폴링입니다. 이벤트 폴링은 여러 단계로 구분됩니다. 일반적인 작업은 대기열에서 콜백 함수를 탐색하고 실행하는 것입니다.

마지막으로 비동기 API process.nextTick과 Promise는 이벤트 폴링에 속하지 않는다는 점을 언급했습니다. 부적절한 사용으로 인해 이벤트 폴링이 차단될 수 있습니다. 대신 setImmediate를 사용하는 것입니다.

노드 관련 지식을 더 보려면 nodejs 튜토리얼을 방문하세요!

위 내용은 libuv란 무엇입니까? libuv의 이벤트 폴링에 대한 간략한 분석입니다(노드 코어 종속성).의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제