libuv とは何か、libuv でのイベント ポーリングの簡単な分析 (ノード コアの依存関係)
この記事では、Node の中心的な依存関係である 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など)は下図のとおりです。
- #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 イベント ループのコア コードは 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); }... }
__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; } }
fs.readFile('config.json', (err, data) = >{... }) const traverse = () = >{ process.nextTick(traverse) }
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); }
libuv の非同期操作を処理するための中心的なメカニズムはイベント ポーリングです。イベント ポーリングはいくつかのステップに分かれています。一般的な操作は、キュー内のコールバック関数を走査して実行することです。
最後に、非同期 API process.nextTick と Promise はイベント ポーリングに属さないことに言及しました。不適切に使用すると、イベント ポーリングがブロックされます。解決策の 1 つは、代わりに setImmediate を使用することです。
ノード関連の知識の詳細については、nodejs チュートリアル を参照してください。
以上がlibuv とは何か、libuv でのイベント ポーリングの簡単な分析 (ノード コアの依存関係)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ホットAIツール

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

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

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

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

人気の記事

ホットツール

メモ帳++7.3.1
使いやすく無料のコードエディター

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

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

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

ホットトピック









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

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

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

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

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

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

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

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