Maison  >  Article  >  interface Web  >  Qu'est-ce que libuv, une brève analyse de l'interrogation d'événements dans libuv (dépendance du noyau du nœud)

Qu'est-ce que libuv, une brève analyse de l'interrogation d'événements dans libuv (dépendance du noyau du nœud)

青灯夜游
青灯夜游avant
2022-03-22 19:58:553741parcourir

Cet article vous amènera à comprendre la dépendance fondamentale de Node à l'égard de libuv, à présenter ce qu'est libuv et les sondages d'événements dans libuv. J'espère que cela sera utile à tout le monde !

Qu'est-ce que libuv, une brève analyse de l'interrogation d'événements dans libuv (dépendance du noyau du nœud)

Mentionné Node.js, je pense que la plupart des ingénieurs front-end penseront à développer le serveur sur cette base. Il suffit de maîtriser JavaScript en tant que langage pour devenir un ingénieur full-stack, mais en fait, la signification de Node.js n’est pas seulement la voici.

Pour de nombreux langages de haut niveau, les autorisations d'exécution peuvent atteindre le système d'exploitation, mais JavaScript exécuté côté navigateur est une exception L'environnement sandbox créé par le navigateur enferme les ingénieurs front-end dans une tour d'ivoire dans le monde de la programmation. . Cependant, l'émergence de Node.js a comblé cette lacune et les ingénieurs front-end peuvent également atteindre le fond du monde informatique.

Ainsi, l'importance de Nodejs pour les ingénieurs front-end n'est pas seulement de fournir des capacités de développement full-stack, mais plus important encore, d'ouvrir une porte au monde sous-jacent des ordinateurs pour les ingénieurs front-end. Cet article ouvre cette porte en analysant les principes d'implémentation de Node.js.

Structure du code source de Node.js

L'entrepôt de code source de Node.js a plus d'une douzaine de dépendances dans le répertoire /deps, y compris des modules écrits en langage C (tels que libuv, V8) et des modules écrits en langage JavaScript (tels que gland, gland -plugins), comme le montre la figure ci-dessous.

Quest-ce que libuv, une brève analyse de linterrogation dévénements dans libuv (dépendance du noyau du nœud)

  • acorn : Un analyseur JavaScript léger écrit en JavaScript.
  • acorn-plugins : module d'extension d'acorn, permettant à acorn de prendre en charge l'analyse des fonctionnalités ES6, telles que les déclarations de classe.
  • brotli : algorithme de compression Brotli écrit en langage C.
  • cares : doit être écrit sous la forme "c-ares", écrit en langage C pour gérer les requêtes DNS asynchrones.
  • histogramme : écrit en langage C pour implémenter la fonction de génération d'histogramme.
  • icu-small : bibliothèque ICU (International Components for Unicode) écrite en langage C et personnalisée pour Node.js, comprenant certaines fonctions d'exploitation d'Unicode.
  • llhttp : écrit en langage C, analyseur http léger.
  • nghttp2/nghttp3/ngtcp2 : Gère les protocoles HTTP/2, HTTP/3, TCP/2.
  • node-inspect : autorise les programmes Node.js à prendre en charge le mode de débogage CLI debug.
  • npm : Gestionnaire de modules Node.js écrit en JavaScript.
  • openssl : écrit en langage C, module lié au chiffrement, utilisé à la fois dans les modules tls et crypto.
  • uv : Écrit en langage C, utilisant des opérations d'E/S non bloquantes, offrant à Node.js la possibilité d'accéder aux ressources système.
  • uvwasi : écrit en langage C, implémente l'API d'appel système WASI.
  • v8 : écrit en langage C, moteur JavaScript.
  • zlib : Pour une compression rapide, Node.js utilise zlib pour créer des interfaces de compression et de décompression de flux de données synchrones, asynchrones.

Les plus importants sont les modules correspondant aux répertoires v8 et uv. V8 lui-même n'a pas la capacité de s'exécuter de manière asynchrone, mais est implémenté à l'aide d'autres threads dans le navigateur. C'est pourquoi nous disons souvent que js est monothread, car son moteur d'analyse ne prend en charge que l'analyse synchrone du code. Mais dans Node.js, l'implémentation asynchrone repose principalement sur libuv. Concentrons-nous sur l'analyse du principe d'implémentation de libuv.

Qu'est-ce que libuv

libuv est une bibliothèque d'E/S asynchrone écrite en C qui prend en charge plusieurs plates-formes. Elle résout principalement le problème des opérations d'E/S qui provoquent facilement des blocages. Développé à l'origine spécifiquement pour être utilisé avec Node.js, mais plus tard également utilisé par d'autres modules tels que Luvit, Julia, pyuv, etc. La figure suivante est le diagramme de structure de libuv.

Quest-ce que libuv, une brève analyse de linterrogation dévénements dans libuv (dépendance du noyau du nœud)

libuv a deux méthodes d'implémentation asynchrones, qui sont les deux parties sélectionnées par la case jaune à gauche et à droite de l'image ci-dessus.

La partie gauche est le module d'E/S réseau, qui a différents mécanismes d'implémentation sous différentes plates-formes. Il est implémenté via epoll sous les systèmes Linux, OSX et d'autres systèmes BSD utilisent KQueue, les systèmes SunOS utilisent les ports d'événement et les systèmes Windows utilisent IOCP. . Puisqu’il s’agit de l’API sous-jacente du système d’exploitation, c’est relativement compliqué à comprendre, je ne le présenterai donc pas en détail ici.

La partie droite comprend le module d'E/S de fichier, le module DNS et le code utilisateur, qui implémente les opérations asynchrones via le pool de threads. Les E/S de fichiers sont différentes des E/S réseau. libuv ne s'appuie pas sur l'API sous-jacente du système, mais effectue des opérations de blocage d'E/S de fichiers dans le pool de threads global.

Interrogation d'événements dans libuv

L'image ci-dessous est le diagramme de flux de travail d'interrogation d'événements donné par le site officiel de libuv. Analysons-le avec le code.

Quest-ce que libuv, une brève analyse de linterrogation dévénements dans libuv (dépendance du noyau du nœud)

Le code principal de la boucle d'événements libuv est implémenté dans la fonction uv_run() Ce qui suit fait partie du code principal sous le système Unix. Bien qu’il soit écrit en langage C, il s’agit d’un langage de haut niveau comme JavaScript, il n’est donc pas trop difficile à comprendre. La plus grande différence réside peut-être dans les astérisques et les flèches. Nous pouvons simplement ignorer les astérisques. Par exemple, la boucle uv_loop_t* dans le paramètre de fonction peut être comprise comme une boucle variable de type uv_loop_t. La flèche "→" peut être comprise comme le point ".", par exemple, loop→stop_flag peut être compris comme 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

Cette fonction est utilisée pour déterminer si l'interrogation d'événements doit continuer S'il n'y a pas de tâche active dans l'objet de boucle, elle renverra 0 et quittera la boucle.

En langage C, cette "tâche" a un nom professionnel, qui est "handle", qui peut être compris comme une variable pointant vers la tâche. Les handles peuvent être divisés en deux catégories : request et handle, qui représentent respectivement les handles à cycle de vie court et les handles à cycle de vie longue. Le code spécifique est le suivant :

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

Afin de réduire le nombre d'appels système liés à l'heure, cette fonction est utilisée pour mettre en cache l'heure actuelle du système. La précision est très élevée et peut atteindre le. niveau nanoseconde, mais l'unité est toujours la milliseconde.

Le code source spécifique est le suivant :

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

uv__run_timers

Exécutez la fonction de rappel qui atteint le seuil de temps dans setTimeout() et setInterval(). Ce processus d'exécution est implémenté via un parcours de boucle for. Comme vous pouvez le voir dans le code ci-dessous, le rappel du minuteur est stocké dans les données d'une structure de tas minimum. Il se termine lorsque le tas minimum est vide ou n'a pas atteint le cycle de seuil. .

Supprimez la minuterie avant d'exécuter la fonction de rappel de la minuterie. Si la répétition est définie, elle doit être à nouveau ajoutée au tas minimum, puis le rappel de la minuterie est exécuté.

Le code spécifique est le suivant :

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_ending

Parcourt toutes les fonctions de rappel d'E/S stockées dans ending_queue et renvoie 0 lorsque ending_queue est vide ; sinon, renvoie 1 après avoir exécuté la fonction de rappel dans ending_queue.

Le code est le suivant :

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

Ces trois fonctions sont définies via une fonction macro UV_LOOP_WATCHER_DEFINE. fonctions. La fonction macro est appelée trois fois et les valeurs des paramètres de nom Prepare, Check et Idle sont transmises respectivement. En même temps, trois fonctions, uvrun_idle, uvrun_prepare et uv__run_check, sont définies.

Leur logique d'exécution est donc cohérente. Ils parcourent et retirent tous les objets de la file d'attente loop->name##_handles selon le principe du premier entré, premier sorti, puis exécutent la fonction de rappel correspondante.

#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 est principalement utilisé pour interroger les opérations d'E/S. L'implémentation spécifique variera en fonction du système d'exploitation. Nous prenons le système Linux comme exemple pour l'analyse.

La fonction uv__io_poll a beaucoup de code source. Le noyau est constitué de deux morceaux de code de boucle. Une partie du code est la suivante :

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

Dans la boucle while, parcourez la file d'attente des observateurs watcher_queue, supprimez l'événement et le descripteur de fichier et. affectez-les à l'objet événement e, puis appelez la fonction epoll_ctl pour enregistrer ou modifier l'événement epoll.

Dans la boucle for, le descripteur de fichier en attente dans epoll sera d'abord retiré et attribué à nfds, puis nfds sera parcouru pour exécuter la fonction de rappel.

uv__run_closing_handles

Parcourez la file d'attente en attente de fermeture, fermez les handles tels que stream, tcp, udp, etc., puis appelez le close_cb correspondant au handle. Le code est le suivant :

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

Bien que process.nextTick et Promise soient tous deux des API asynchrones, ils ne font pas partie de l'interrogation d'événements. Ils ont leurs propres files d'attente de tâches à chaque étape de l'interrogation d'événements. achèvement. Ainsi, lorsque nous utilisons ces deux API asynchrones, nous devons faire attention. Si de longues tâches ou récursions sont effectuées dans la fonction de rappel entrant, l'interrogation des événements sera bloquée, « affamant » ainsi les opérations d'E/S.

Le code suivant est un exemple d'appel récursif de prcoess.nextTick provoquant l'échec de l'exécution de la fonction de rappel de fs.readFile.

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

Pour résoudre ce problème, vous pouvez utiliser setImmediate à la place, car setImmediate exécutera la file d'attente des fonctions de rappel dans la boucle d'événements. La file d'attente des tâches process.nextTick a une priorité plus élevée que la file d'attente des tâches Promise. Pour la raison spécifique, veuillez vous référer au code suivant :

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

Comme le montre la fonction processTicksAndRejections(), d'abord la fonction de rappel de la file d'attente. est retiré via la boucle while, et cette file d'attente La fonction de rappel dans la file d'attente est ajoutée via process.nextTick. Lorsque la boucle while se termine, la fonction runMicrotasks() est appelée pour exécuter la fonction de rappel Promise.

Résumé

La structure de base de Node.js qui s'appuie sur libuv peut être divisée en deux parties. L'une est les E/S réseau. L'implémentation sous-jacente s'appuiera sur différentes API système en fonction des différents systèmes d'exploitation. E/S de fichier, DNS et code utilisateur, cette partie est traitée par le pool de threads.

Le mécanisme principal de

libuv pour gérer les opérations asynchrones est l'interrogation d'événements. L'interrogation d'événements est divisée en plusieurs étapes. L'opération générale consiste à parcourir et à exécuter la fonction de rappel dans la file d'attente.

Enfin, il est mentionné que le processus API asynchrone.nextTick et Promise n'appartiennent pas à l'interrogation d'événements. Une utilisation inappropriée entraînera le blocage de l'interrogation d'événements. Une solution consiste à utiliser setImmediate à la place.

Pour plus de connaissances sur les nœuds, veuillez visiter : tutoriel Nodejs !

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer