


Qu'est-ce que libuv, une brève analyse de l'interrogation d'événements dans libuv (dépendance du noyau du nœud)
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 !
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.
- 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.
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.
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('config.json', (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 delibuv 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!

Outils d'IA chauds

Undresser.AI Undress
Application basée sur l'IA pour créer des photos de nu réalistes

AI Clothes Remover
Outil d'IA en ligne pour supprimer les vêtements des photos.

Undress AI Tool
Images de déshabillage gratuites

Clothoff.io
Dissolvant de vêtements AI

Video Face Swap
Échangez les visages dans n'importe quelle vidéo sans effort grâce à notre outil d'échange de visage AI entièrement gratuit !

Article chaud

Outils chauds

Bloc-notes++7.3.1
Éditeur de code facile à utiliser et gratuit

SublimeText3 version chinoise
Version chinoise, très simple à utiliser

Envoyer Studio 13.0.1
Puissant environnement de développement intégré PHP

Dreamweaver CS6
Outils de développement Web visuel

SublimeText3 version Mac
Logiciel d'édition de code au niveau de Dieu (SublimeText3)

Sujets chauds





Cet article vous donnera une compréhension approfondie de la mémoire et du garbage collector (GC) du moteur NodeJS V8. J'espère qu'il vous sera utile !

Le service Node construit sur une base non bloquante et piloté par les événements présente l'avantage d'une faible consommation de mémoire et est très adapté à la gestion de requêtes réseau massives. Dans le contexte de demandes massives, les questions liées au « contrôle de la mémoire » doivent être prises en compte. 1. Le mécanisme de récupération de place du V8 et les limitations de mémoire Js sont contrôlés par la machine de récupération de place

Le choix d'une image Docker pour Node peut sembler trivial, mais la taille et les vulnérabilités potentielles de l'image peuvent avoir un impact significatif sur votre processus CI/CD et votre sécurité. Alors, comment choisir la meilleure image Docker Node.js ?

Le module de fichiers est une encapsulation des opérations de fichiers sous-jacentes, telles que l'ajout de lecture/écriture/ouverture/fermeture/suppression de fichiers, etc. La plus grande caractéristique du module de fichiers est que toutes les méthodes fournissent deux versions de **synchrone** et ** asynchrone**, with Les méthodes avec le suffixe sync sont toutes des méthodes de synchronisation, et celles qui n'en ont pas sont toutes des méthodes hétérogènes.

Node 19 est officiellement publié. Cet article vous donnera une explication détaillée des 6 fonctionnalités majeures de Node.js 19. J'espère qu'il vous sera utile !

Comment Node.js fait-il le GC (garbage collection) ? L’article suivant vous guidera à travers cela.

La boucle d'événements est un élément fondamental de Node.js et permet une programmation asynchrone en garantissant que le thread principal n'est pas bloqué. Comprendre la boucle d'événements est crucial pour créer des applications efficaces. L'article suivant vous donnera une compréhension approfondie de la boucle d'événements dans Node. J'espère qu'il vous sera utile !

Au début, JS ne fonctionnait que du côté du navigateur. Il était facile de traiter les chaînes codées en Unicode, mais il était difficile de traiter les chaînes binaires et non codées en Unicode. Et le binaire est le format de données le plus bas du package ordinateur, vidéo/audio/programme/réseau.
