目次
Node.js ソース コードの構造
libuv とは
ホームページ ウェブフロントエンド jsチュートリアル libuv とは何か、libuv でのイベント ポーリングの簡単な分析 (ノード コアの依存関係)

libuv とは何か、libuv でのイベント ポーリングの簡単な分析 (ノード コアの依存関係)

Mar 22, 2022 pm 07:58 PM
node.js

この記事では、Node の中心的な依存関係である libuv を理解し、libuv とは何か、および libuv でのイベント ポーリングについて紹介します。

libuv とは何か、libuv でのイベント ポーリングの簡単な分析 (ノード コアの依存関係)

Node.js のことになると、ほとんどのフロントエンド エンジニアはこれに基づいてサーバーを開発することを考えると思います。 JavaScriptを言語としてマスターする フルスタックエンジニアになる しかし、実はNode.jsの意味はそれだけではありません。

多くの高級言語では、実行権限がオペレーティング システムに到達しますが、ブラウザ側で実行される JavaScript は例外です。ブラウザによって作成されたサンドボックス環境は、フロントエンドを閉じます。プログラミングの世界の象牙の塔に住むエンジニア。しかし、Node.jsの登場によりこの欠点は補われ、フロントエンドエンジニアもコンピュータの世界の底辺に到達できるようになりました。

つまり Nodejs フロントエンド エンジニアにとっての重要性は、フルスタックの開発機能を提供することだけではなく、より重要なことに、フロントエンド用のコンピューターの基盤となる世界への扉を開くことです。エンジニア。この記事では、Node.js の実装原則を分析することでこの扉を開きます。

Node.js ソース コードの構造

Node.js ソース コード ウェアハウスの /deps ディレクトリには、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」として記述する必要があります。
  • histogram: ヒストグラム生成関数を実装するために C 言語で記述されています。
  • icu-small: C 言語で書かれ、Node.js 用にカスタマイズされた ICU (International Components for Unicode) ライブラリ。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 モジュールと暗号化モジュールの両方で使用されます。
  • uv: C 言語で書かれ、ノンブロッキング I/O 操作を使用して、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 には 2 つの非同期実装メソッドがあります。これらは、上の図の左右の黄色のボックスで選択された 2 つの部分です。

左側の部分はネットワーク I/O モジュールであり、プラットフォームごとに異なる実装メカニズムがあり、Linux システムでは epoll を通じて実装され、OSX およびその他の BSD システムでは KQueue が使用され、SunOS システムではイベント ポートが使用されます。 Windows システムは IOCP を使用します。これにはオペレーティング システムの基盤となる API が関係するため、理解するのがさらに複雑になるため、ここでは紹介しません。

右側の部分には、ファイル I/O モジュール、DNS モジュール、およびスレッド プールを介して非同期操作を実装するユーザー コードが含まれています。ファイル I/O はネットワーク I/O とは異なります。libuv はシステムの基礎となる API に依存せず、代わりに、グローバル スレッド プールでファイル I/O 操作のブロックを実行します。

libuv でのイベント ポーリング

次の図は libuv 公式 Web サイトに掲載されているイベント ポーリングのワークフロー図ですので、コードと合わせて分析してみましょう。

libuv とは何か、libuv でのイベント ポーリングの簡単な分析 (ノード コアの依存関係)

libuv イベント ループのコア コードは uv_run() 関数に実装されています。以下は Unix システムでのコア コードの一部です。 C言語で書かれていますが、JavaScriptと同じような高級言語なので、理解することはそれほど難しくありません。最大の違いはアスタリスクと矢印ですが、アスタリスクは単に無視して問題ありません。たとえば、関数パラメータの 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言語では、この「タスク」には「ハンドル」という専門的な名前が付けられており、タスクを指す変数として理解できます。ハンドルは、リクエストとハンドルの 2 つのカテゴリに分類でき、それぞれ短いライフ サイクルのハンドルと長いライフ サイクルのハンドルを表します。具体的なコードは次のとおりです。

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() 関数。この実行プロセスは for ループ トラバーサルによって実装されます。以下のコードからわかるように、タイマー コールバックは最小ヒープ構造のデータに格納されます。最小ヒープが空になるか、時間のしきい値に達しない場合に終了します。サイクル。

タイマー コールバック関数を実行する前にタイマーを削除してください。繰り返しが設定されている場合は、再度最小ヒープに追加する必要があり、その後タイマー コールバックが実行されます。

具体的なコードは次のとおりです。

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 コールバック関数を走査します。 when pending_queue が空の場合は 0 を返し、それ以外の場合は pending_queue でコールバック関数を実行した後に 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

これら 3 つの関数はすべて、マクロ関数 UV_LOOP_WATCHER_DEFINE 定義の場合、マクロ関数はコード テンプレート、または関数を定義するために使用される関数として理解できます。マクロ関数は3回呼び出され、それぞれprepare、check、idleという名前パラメータの値が渡され、同時にuvrun_idle、uvrun_prepare、uv__run_checkの3つの関数が定義されています。

したがって、それらの実行ロジックは一貫しています。すべてループして、先入れ先出しの原則に従ってキューの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 関数には多くのソース コードがあり、コアは 2 つのループ コードであり、コードの一部は次のとおりです:

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 が走査されてコールバック関数が実行されます。

__run_closed_handles

クローズされるのを待っているキューを走査し、ストリーム、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 ですが、イベント ポーリングの一部ではなく、独自のタスク キューを持っています。イベントループの各ステップが完了した後に実行されます。したがって、これら 2 つの非同期 API を使用する場合は、受信コールバック関数で長いタスクや再帰が実行されると、イベント ポーリングがブロックされ、I/O 操作が「枯渇」することに注意する必要があります。

次のコードは、fs.readFile のコールバック関数の実行に失敗する prcoess.nextTick への再帰呼び出しの例です。

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 ループでキューの queue を取り出し、process.nextTick でこのキュー内のコールバック関数を追加します。 while ループが終了すると、runMicrotasks() 関数が呼び出され、Promise コールバック関数が実行されます。

概要

libuv に依存する Node.js のコア構造は 2 つの部分に分けることができます。1 つの部分はネットワーク I/O です。基盤となる実装は、次のとおり異なるシステム API に依存します。他の部分は、ファイル I/O、DNS、およびユーザー コードがスレッド プールによって処理されることです。

libuv の非同期操作を処理するための中心的なメカニズムはイベント ポーリングです。イベント ポーリングはいくつかのステップに分かれています。一般的な操作は、キュー内のコールバック関数を走査して実行することです。

最後に、非同期 API process.nextTick と Promise はイベント ポーリングに属さないことに言及しました。不適切に使用すると、イベント ポーリングがブロックされます。解決策の 1 つは、代わりに setImmediate を使用することです。

ノード関連の知識の詳細については、nodejs チュートリアル を参照してください。

以上がlibuv とは何か、libuv でのイベント ポーリングの簡単な分析 (ノード コアの依存関係)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

AI Hentai Generator

AI Hentai Generator

AIヘンタイを無料で生成します。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

Node V8 エンジンのメモリと GC の詳細な図による説明 Node V8 エンジンのメモリと GC の詳細な図による説明 Mar 29, 2023 pm 06:02 PM

この記事では、NodeJS V8 エンジンのメモリとガベージ コレクター (GC) について詳しく説明します。

Nodeのメモリ制御に関する記事 Nodeのメモリ制御に関する記事 Apr 26, 2023 pm 05:37 PM

ノンブロッキングおよびイベント駆動に基づいて構築されたノード サービスには、メモリ消費量が少ないという利点があり、大量のネットワーク リクエストの処理に非常に適しています。大量のリクエストを前提として、「メモリ制御」に関する問題を考慮する必要があります。 1. V8 のガベージ コレクション メカニズムとメモリ制限 Js はガベージ コレクション マシンによって制御されます

最適な Node.js Docker イメージを選択する方法について話しましょう。 最適な Node.js Docker イメージを選択する方法について話しましょう。 Dec 13, 2022 pm 08:00 PM

ノード用の Docker イメージの選択は些細なことのように思えるかもしれませんが、イメージのサイズと潜在的な脆弱性は、CI/CD プロセスとセキュリティに大きな影響を与える可能性があります。では、最適な Node.js Docker イメージを選択するにはどうすればよいでしょうか?

Node の File モジュールについて詳しく説明しましょう Node の File モジュールについて詳しく説明しましょう Apr 24, 2023 pm 05:49 PM

ファイル モジュールは、ファイルの読み取り/書き込み/開く/閉じる/削除の追加など、基礎となるファイル操作をカプセル化したものです。ファイル モジュールの最大の特徴は、すべてのメソッドが **同期** と ** の 2 つのバージョンを提供することです。 asynchronous**、sync サフィックスが付いているメソッドはすべて同期メソッドであり、持たないメソッドはすべて異種メソッドです。

Node.js 19 が正式リリースされました。その 6 つの主要な機能についてお話しましょう。 Node.js 19 が正式リリースされました。その 6 つの主要な機能についてお話しましょう。 Nov 16, 2022 pm 08:34 PM

Node 19 が正式リリースされましたので、この記事では Node.js 19 の 6 つの主要な機能について詳しく説明します。

Node.js の GC (ガベージ コレクション) メカニズムについて話しましょう Node.js の GC (ガベージ コレクション) メカニズムについて話しましょう Nov 29, 2022 pm 08:44 PM

Node.js はどのように GC (ガベージ コレクション) を行うのでしょうか?次の記事で詳しく説明します。

Nodeのイベントループについて話しましょう Nodeのイベントループについて話しましょう Apr 11, 2023 pm 07:08 PM

イベント ループは Node.js の基本的な部分であり、メイン スレッドがブロックされていないことを確認することで非同期プログラミングが可能になります。イベント ループを理解することは、効率的なアプリケーションを構築するために重要です。次の記事では、Node のイベント ループについて詳しく説明します。お役に立てれば幸いです。

ノードがnpmコマンドを使用できない場合はどうすればよいですか? ノードがnpmコマンドを使用できない場合はどうすればよいですか? Feb 08, 2023 am 10:09 AM

ノードが npm コマンドを使用できない理由は、環境変数が正しく設定されていないためです。解決策は次のとおりです: 1. 「システムのプロパティ」を開きます; 2. 「環境変数」->「システム変数」を見つけて、環境を編集します。変数; 3.nodejs フォルダーの場所を見つけます; 4.「OK」をクリックします。

See all articles