ホームページ > 記事 > ウェブフロントエンド > libuv とは何か、libuv でのイベント ポーリングの簡単な分析 (ノード コアの依存関係)
この記事では、Node の中心的な依存関係である libuv を理解し、libuv とは何か、および libuv でのイベント ポーリングについて紹介します。
Node.js のことになると、ほとんどのフロントエンド エンジニアはこれに基づいてサーバーを開発することを考えると思います。 JavaScriptを言語としてマスターする フルスタックエンジニアになる しかし、実はNode.jsの意味はそれだけではありません。
多くの高級言語では、実行権限がオペレーティング システムに到達しますが、ブラウザ側で実行される JavaScript は例外です。ブラウザによって作成されたサンドボックス環境は、フロントエンドを閉じます。プログラミングの世界の象牙の塔に住むエンジニア。しかし、Node.jsの登場によりこの欠点は補われ、フロントエンドエンジニアもコンピュータの世界の底辺に到達できるようになりました。
つまり Nodejs フロントエンド エンジニアにとっての重要性は、フルスタックの開発機能を提供することだけではなく、より重要なことに、フロントエンド用のコンピューターの基盤となる世界への扉を開くことです。エンジニア。この記事では、Node.js の実装原則を分析することでこの扉を開きます。
Node.js ソース コード ウェアハウスの /deps ディレクトリには、C 言語で書かれたモジュール (libuv、 V8)とJavaScript言語で記述したモジュール(acorn、acorn-pluginsなど)は下図のとおりです。
最も重要なものは、v8 ディレクトリと uv ディレクトリに対応するモジュールです。 V8 自体には非同期で実行する機能はありませんが、ブラウザー内の他のスレッドの助けを借りて実装されています。js がシングルスレッドであるとよく言われるのは、その解析エンジンが同期解析コードのみをサポートしているためです。 ただし、Node.js では、非同期実装は主に libuv に依存しているため、libuv の実装原理の分析に焦点を当てましょう。
#libuv は、複数のプラットフォームをサポートする C で書かれた非同期 I/O ライブラリであり、主に I/O 操作がブロックされやすい問題を解決します。 元々は Node.js で使用するために特別に開発されましたが、後に Luvit、Julia、pyuv などの他のモジュールでも使用されるようになりました。以下の図は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 イベント ループのコア コードは 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 と Promiseprocess.nextTick と Promise はどちらも非同期 API ですが、イベント ポーリングの一部ではなく、独自のタスク キューを持っています。イベントループの各ステップが完了した後に実行されます。したがって、これら 2 つの非同期 API を使用する場合は、受信コールバック関数で長いタスクや再帰が実行されると、イベント ポーリングがブロックされ、I/O 操作が「枯渇」することに注意する必要があります。 次のコードは、fs.readFile のコールバック関数の実行に失敗する prcoess.nextTick への再帰呼び出しの例です。
fs.readFile('config.json', (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 サイトの他の関連記事を参照してください。