Heim  >  Artikel  >  Web-Frontend  >  Was ist libuv, eine kurze Analyse der Ereignisabfrage in libuv (Knotenkernabhängigkeit)

Was ist libuv, eine kurze Analyse der Ereignisabfrage in libuv (Knotenkernabhängigkeit)

青灯夜游
青灯夜游nach vorne
2022-03-22 19:58:553741Durchsuche

In diesem Artikel erfahren Sie mehr über die Kernabhängigkeit von Node von libuv, stellen vor, was libuv ist, und erläutern die Ereignisabfrage in libuv. Ich hoffe, dass er für alle hilfreich ist!

Was ist libuv, eine kurze Analyse der Ereignisabfrage in libuv (Knotenkernabhängigkeit)

Erwähnt Node.js, glaube ich, dass die meisten Front-End-Ingenieure darüber nachdenken werden, den darauf basierenden Server zu entwickeln. Sie müssen nur JavaScript als Sprache beherrschen, um ein Full-Stack-Ingenieur zu werden. Die Bedeutung von Node.js ist nicht nur Hier ist sie.

Bei vielen Hochsprachen können Ausführungsberechtigungen das Betriebssystem erreichen, aber JavaScript, das auf der Browserseite ausgeführt wird, ist eine Ausnahme. Die vom Browser erstellte Sandbox-Umgebung versiegelt Front-End-Ingenieure in einem Elfenbeinturm in der Programmierwelt . Das Aufkommen von Node.js hat dieses Manko jedoch wettgemacht, und Front-End-Ingenieure können auch bis in die Tiefen der Computerwelt vordringen.

Die Bedeutung von

Nodejs für Front-End-Ingenieure besteht also nicht nur darin, Full-Stack-Entwicklungsfunktionen bereitzustellen, sondern, was noch wichtiger ist, darin, Front-End-Ingenieuren eine Tür zur zugrunde liegenden Welt der Computer zu öffnen. Dieser Artikel öffnet diese Tür, indem er die Implementierungsprinzipien von Node.js analysiert.

Struktur des Quellcodes von Node.js

Das Quellcode-Warehouse von Node.js verfügt über mehr als ein Dutzend Abhängigkeiten im Verzeichnis /deps, darunter in C-Sprache geschriebene Module (z. B. libuv, V8) und in JavaScript-Sprache geschriebene Module (z. B acorn, acorn -plugins), wie in der Abbildung unten gezeigt.

Was ist libuv, eine kurze Analyse der Ereignisabfrage in libuv (Knotenkernabhängigkeit)

    acorn: Ein leichter JavaScript-Parser, der in JavaScript geschrieben ist.
  • acorn-plugins: Acorns Erweiterungsmodul, das es Acorn ermöglicht, ES6-Feature-Parsing, wie z. B. Klassendeklarationen, zu unterstützen.
  • brotli: Brotli-Komprimierungsalgorithmus, geschrieben in C-Sprache.
  • cares: sollte als „c-ares“ geschrieben werden, geschrieben in C-Sprache, um asynchrone DNS-Anfragen zu verarbeiten.
  • Histogramm: In C-Sprache geschrieben, um die Funktion zur Histogrammgenerierung zu implementieren.
  • icu-small: ICU-Bibliothek (International Components for Unicode), geschrieben in C-Sprache und angepasst für Node.js, einschließlich einiger Funktionen für den Betrieb von Unicode.
  • llhttp: in C-Sprache geschriebener, leichter http-Parser.
  • nghttp2/nghttp3/ngtcp2: Verarbeitet die Protokolle HTTP/2, HTTP/3, TCP/2.
  • node-inspect: Erlauben Sie Node.js-Programmen, den CLI-Debug-Debugging-Modus zu unterstützen.
  • npm: Node.js-Modulmanager, geschrieben in JavaScript.
  • openssl: geschrieben in C-Sprache, verschlüsselungsbezogenes Modul, wird sowohl in TLS- als auch in Kryptomodulen verwendet.
  • uv: In C-Sprache geschrieben, unter Verwendung nicht blockierender E/A-Operationen, wodurch Node.js die Möglichkeit erhält, auf Systemressourcen zuzugreifen.
  • uvwasi: in C-Sprache geschrieben, implementiert die WASI-Systemaufruf-API.
  • v8: geschrieben in C-Sprache, JavaScript-Engine.
  • zlib: Für eine schnelle Komprimierung verwendet Node.js zlib, um synchrone, asynchrone und Datenstrom-Komprimierungs- und Dekomprimierungsschnittstellen zu erstellen.
Die wichtigsten sind die Module, die den Verzeichnissen v8 und uv entsprechen.

V8 selbst kann nicht asynchron ausgeführt werden, sondern wird mit Hilfe anderer Threads im Browser implementiert. Aus diesem Grund sagen wir oft, dass js Single-Threaded ist, da seine Parsing-Engine nur synchrones Parsen von Code unterstützt. Aber in Node.js basiert die asynchrone Implementierung hauptsächlich auf libuv. Konzentrieren wir uns auf die Analyse des Implementierungsprinzips von libuv.

Was ist libuv? libuv ist eine in C geschriebene asynchrone E/A-Bibliothek, die mehrere Plattformen unterstützt. Sie löst hauptsächlich das Problem von E/A-Vorgängen, die leicht zu Blockierungen führen.

Ursprünglich speziell für die Verwendung mit Node.js entwickelt, später aber auch von anderen Modulen wie Luvit, Julia, Pyuv usw. verwendet. Die folgende Abbildung ist das Strukturdiagramm von libuv.

libuv verfügt über zwei asynchrone Implementierungsmethoden. Dies sind die beiden Teile, die durch das gelbe Feld links und rechts im Bild oben ausgewählt werden.

Was ist libuv, eine kurze Analyse der Ereignisabfrage in libuv (Knotenkernabhängigkeit)Der linke Teil ist das Netzwerk-E/A-Modul, das auf verschiedenen Plattformen über epoll implementiert wird. OSX und andere BSD-Systeme verwenden KQueue, SunOS-Systeme verwenden Ereignisports und Windows-Systeme verwenden IOCP . Da es sich um die zugrunde liegende API des Betriebssystems handelt, ist es relativ kompliziert zu verstehen, daher werde ich es hier nicht im Detail vorstellen.

Der rechte Teil umfasst das Datei-E/A-Modul, das DNS-Modul und den Benutzercode, der asynchrone Vorgänge über den Thread-Pool implementiert. Datei-E/A unterscheidet sich von Netzwerk-E/A. libuv ist nicht auf die zugrunde liegende API des Systems angewiesen, sondern führt blockierende Datei-E/A-Vorgänge im globalen Thread-Pool aus.

Ereignisabfrage in libuv

Das Bild unten ist das Workflow-Diagramm für die Ereignisabfrage auf der offiziellen libuv-Website. Lassen Sie es uns zusammen mit dem Code analysieren.

Der Kerncode der libuv-Ereignisschleife ist in der Funktion uv_run() implementiert. Das Folgende ist Teil des Kerncodes unter dem Unix-System. Obwohl es in der Sprache C geschrieben ist, handelt es sich um eine Hochsprache wie JavaScript, sodass es nicht allzu schwer zu verstehen ist. Der größte Unterschied dürften die Sternchen und Pfeile sein. Wir können die Sternchen einfach ignorieren. Beispielsweise kann die Schleife uv_loop_t* im Funktionsparameter als variable Schleife vom Typ uv_loop_t verstanden werden. Der Pfeil „→“ kann als Punkt „.“ verstanden werden, zum Beispiel kann loop→stop_flag als loop.stop_flag verstanden werden.

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

Mit dieser Funktion wird bestimmt, ob die Ereignisabfrage fortgesetzt werden soll. Wenn im Schleifenobjekt keine aktive Aufgabe vorhanden ist, wird 0 zurückgegeben und die Schleife verlassen.

In der C-Sprache hat diese „Aufgabe“ einen professionellen Namen, nämlich „Handle“, der als Variable verstanden werden kann, die auf die Aufgabe verweist. Handles können in zwei Kategorien unterteilt werden: Request und Handle, die Handles mit kurzem Lebenszyklus bzw. Handles mit langem Lebenszyklus darstellen. Der spezifische Code lautet wie folgt:

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

Um die Anzahl zeitbezogener Systemaufrufe zu reduzieren, wird diese Funktion zum Zwischenspeichern der aktuellen Systemzeit verwendet. Die Genauigkeit ist sehr hoch und kann erreicht werden Nanosekundenebene, aber die Einheit ist immer noch Millisekunden.

Der spezifische Quellcode lautet wie folgt:

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

uv__run_timers

Führen Sie die Rückruffunktion aus, die den Zeitschwellenwert in setTimeout () und setInterval () erreicht. Dieser Ausführungsprozess wird durch For-Schleifendurchlauf implementiert. Wie Sie dem folgenden Code entnehmen können, wird der Timer-Rückruf in den Daten einer minimalen Heap-Struktur gespeichert. Er wird beendet, wenn der minimale Heap-Speicher leer ist oder den Zeitschwellenwert nicht erreicht hat .

Entfernen Sie den Timer, bevor Sie die Timer-Rückruffunktion ausführen. Wenn die Wiederholung festgelegt ist, muss sie erneut zum minimalen Heap hinzugefügt werden, und dann wird der Timer-Rückruf ausgeführt.

Der spezifische Code lautet wie folgt:

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

Durchlaufen Sie alle in pending_queue gespeicherten E/A-Callback-Funktionen und geben Sie 0 zurück, wenn pending_queue leer ist. Andernfalls geben Sie 1 zurück, nachdem die Callback-Funktion in pending_queue ausgeführt wurde.

Der Code lautet wie folgt:

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

Diese drei Funktionen werden durch eine Makrofunktion UV_LOOP_WATCHER_DEFINE definiert. Die Makrofunktion kann als Codevorlage oder als Funktion zum Definieren verstanden werden Funktionen. Die Makrofunktion wird dreimal aufgerufen und die Namensparameterwerte „prepare“, „check“ und „idle“ werden übergeben. Gleichzeitig werden drei Funktionen definiert: uvrun_idle, uvrun_prepare und uv__run_check.

Ihre Ausführungslogik ist also konsistent. Sie durchlaufen alle Objekte in der Warteschlangenschleife und nehmen sie nach dem First-In-First-Out-Prinzip heraus und führen dann die entsprechende Rückruffunktion aus.

#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 wird hauptsächlich zum Abfragen von E/A-Vorgängen verwendet. Die konkrete Implementierung variiert je nach Betriebssystem. Wir nehmen das Linux-System als Beispiel für die Analyse.

uv__io_poll Funktion hat eine Menge Quellcode. Ein Teil des Codes ist wie folgt:

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);
    }...
}

Durchlaufen Sie in der while-Schleife die Beobachterwarteschlange watcher_queue, nehmen Sie den Ereignis- und Dateideskriptor heraus und Weisen Sie sie dem Ereignisobjekt e zu und rufen Sie dann die Funktion epoll_ctl auf, um das Epoll-Ereignis zu registrieren oder zu ändern.

In der for-Schleife wird der in epoll wartende Dateideskriptor zuerst herausgenommen und nfds zugewiesen, und dann wird nfds durchlaufen, um die Rückruffunktion auszuführen.

uv__run_closing_handles

Durchlaufen Sie die Warteschlange, die auf das Schließen wartet, schließen Sie Handles wie Stream, TCP, UDP usw. und rufen Sie dann die dem Handle entsprechende close_cb auf. Der Code lautet wie folgt:

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 und Promise

Obwohl process.nextTick und Promise beide asynchrone APIs sind, sind sie nicht Teil der Ereignisabfrage. Sie haben bei jedem Schritt der Ereignisabfrage ihre eigenen Aufgabenwarteschlangen Fertigstellung. Wenn wir diese beiden asynchronen APIs verwenden, müssen wir darauf achten, dass die Ereignisabfrage blockiert wird, wenn lange Aufgaben oder Rekursionen in der eingehenden Rückruffunktion ausgeführt werden, wodurch E/A-Vorgänge „verhungert“ werden.

Der folgende Code ist ein Beispiel für den rekursiven Aufruf von prcoess.nextTick, der dazu führt, dass die Rückruffunktion von fs.readFile nicht ausgeführt werden kann.

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

Um dieses Problem zu lösen, können Sie stattdessen setImmediate verwenden, da setImmediate die Rückruffunktionswarteschlange in der Ereignisschleife ausführt. Die Aufgabenwarteschlange „process.nextTick“ hat eine höhere Priorität als die Aufgabenwarteschlange „Promise“. Den genauen Grund entnehmen Sie bitte dem folgenden Code:

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);
}

Wie aus der Funktion „processTicksAndRejections()“ hervorgeht, ist zunächst die Rückruffunktion der Warteschlange Durch die while-Schleife herausgenommen und diese Warteschlange Die Rückruffunktion in der Warteschlange wird über process.nextTick hinzugefügt. Wenn die while-Schleife endet, wird die Funktion runMicrotasks() aufgerufen, um die Callback-Funktion Promise auszuführen.

Zusammenfassung

Die Kernstruktur von Node.js, die auf libuv basiert, kann in zwei Teile unterteilt werden: Netzwerk-E/A. Der andere Teil basiert auf unterschiedlichen System-APIs Datei-E/A, DNS und Benutzercode, dieser Teil wird vom Thread-Pool verarbeitet.

Der Kernmechanismus von

libuv für die Verarbeitung asynchroner Vorgänge ist die Ereignisabfrage. Die allgemeine Operation besteht darin, die Rückruffunktion in der Warteschlange zu durchlaufen.

Abschließend wird erwähnt, dass die asynchronen APIs „process.nextTick“ und „Promise“ nicht zur Ereignisabfrage gehören. Eine Lösung besteht darin, stattdessen setImmediate zu verwenden.

Weitere Informationen zu Knoten finden Sie unter: nodejs-Tutorial!

Das obige ist der detaillierte Inhalt vonWas ist libuv, eine kurze Analyse der Ereignisabfrage in libuv (Knotenkernabhängigkeit). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:juejin.cn. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen