Artikel ini akan membawa anda melalui kebergantungan teras Node, libuv, dan memperkenalkan apa itu libuv dan tinjauan acara dalam libuv. Saya harap ia dapat membantu semua orang.
Menyebut Node.js, saya percaya kebanyakan jurutera hadapan akan berfikir untuk membangunkan pelayan berdasarkannya. Mereka hanya perlu menguasai JavaScript sebagai bahasa. Menjadi jurutera timbunan penuh, tetapi sebenarnya maksud Node.js bukan sekadar itu.
Bagi kebanyakan bahasa peringkat tinggi, kebenaran pelaksanaan boleh mencapai sistem pengendalian, tetapi JavaScript yang dijalankan pada bahagian penyemak imbas adalah pengecualian Persekitaran kotak pasir yang dicipta oleh penyemak imbas menutup bahagian hadapan jurutera dalam sebuah Di menara gading dunia pengaturcaraan. Walau bagaimanapun, kemunculan Node.js telah menggantikan kelemahan ini, dan jurutera hadapan juga boleh mencapai bahagian bawah dunia komputer.
Jadi kepentingan Nodejs untuk jurutera hadapan bukan sahaja untuk menyediakan keupayaan pembangunan timbunan penuh, tetapi yang lebih penting, ia membuka pintu kepada dunia asas komputer untuk bahagian hadapan- jurutera akhir. Artikel ini membuka pintu ini dengan menganalisis prinsip pelaksanaan Node.js.
Terdapat lebih daripada sedozen kebergantungan dalam direktori /deps gudang kod sumber Node.js, termasuk modul yang ditulis dalam bahasa C (seperti libuv, V8) dan bahasa JavaScript Modul bertulis (seperti acorn, acorn-plugin) adalah seperti yang ditunjukkan dalam rajah di bawah.
Yang paling penting ialah modul yang sepadan dengan direktori v8 dan uv. V8 sendiri tidak mempunyai keupayaan untuk berjalan secara tidak segerak, tetapi dilaksanakan dengan bantuan utas lain dalam penyemak imbas Inilah sebabnya mengapa kami sering mengatakan bahawa js adalah satu benang, kerana enjin penghuraiannya hanya menyokong penghuraian kod segerak. . Tetapi dalam Node.js, pelaksanaan tak segerak bergantung terutamanya pada libuv. Mari fokus pada menganalisis prinsip pelaksanaan libuv.
libuv ialah perpustakaan I/O tak segerak yang ditulis dalam C yang menyokong berbilang platform terutamanya menyelesaikan masalah yang mudah disekat oleh operasi I/O. Asalnya dibangunkan khusus untuk digunakan dengan Node.js, tetapi kemudiannya juga digunakan oleh modul lain seperti Luvit, Julia, pyuv, dll. Rajah berikut ialah rajah struktur libuv.
libuv mempunyai dua kaedah pelaksanaan tak segerak, iaitu dua bahagian yang dipilih oleh kotak kuning di sebelah kiri dan kanan gambar di atas.
Bahagian kiri ialah modul I/O rangkaian, yang mempunyai mekanisme pelaksanaan yang berbeza di bawah platform yang berbeza Ia dilaksanakan melalui epoll di bawah sistem Linux, OSX dan sistem BSD lain menggunakan KQueue, sistem SunOS menggunakan port Acara dan. Sistem Windows menggunakan Ia IOCP. Memandangkan ia melibatkan API asas sistem pengendalian, ia lebih rumit untuk difahami, jadi saya tidak akan memperkenalkannya di sini.
Bahagian kanan termasuk modul I/O fail, modul DNS dan kod pengguna, yang melaksanakan operasi tak segerak melalui kumpulan benang. Fail I/O berbeza daripada rangkaian I/O libuv tidak bergantung pada API asas sistem sebaliknya, ia menjalankan operasi menyekat fail I/O dalam kumpulan benang global.
Rajah berikut ialah rajah aliran kerja tinjauan acara yang diberikan oleh tapak web rasmi libuv. Mari kita analisanya bersama kod.
Kod teras gelung acara libuv dilaksanakan dalam fungsi uv_run() Berikut ialah sebahagian daripada kod teras di bawah sistem Unix. Walaupun ia ditulis dalam bahasa C, ia adalah bahasa peringkat tinggi seperti JavaScript, jadi ia tidak terlalu sukar untuk difahami. Perbezaan terbesar mungkin adalah asterisk dan anak panah Kita boleh mengabaikan asterisk. Contohnya, gelung uv_loop_t* dalam parameter fungsi boleh difahami sebagai gelung pembolehubah jenis uv_loop_t. Anak panah "→" boleh difahami sebagai titik ".", contohnya, gelung→stop_flag boleh difahami sebagai gelung.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
Fungsi ini digunakan untuk menentukan sama ada pengundian acara perlu diteruskan, jika tiada tugas aktif dalam objek gelung Kemudian kembalikan 0 dan keluar dari gelung.
Dalam bahasa C, "tugas" ini mempunyai nama profesional, iaitu "handle", yang boleh difahami sebagai pembolehubah yang menunjuk kepada tugas itu. Pemegang boleh dibahagikan kepada dua kategori: permintaan dan pemegang, yang masing-masing mewakili pemegang kitaran hayat pendek dan pemegang kitaran panjang. Kod khusus adalah seperti berikut:
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
Untuk mengurangkan bilangan panggilan sistem berkaitan masa, fungsi ini digunakan untuk cache sistem semasa Masa mempunyai ketepatan yang sangat tinggi, yang boleh mencapai tahap nanosaat, tetapi unit masih milisaat.
Kod sumber khusus adalah seperti berikut:
UV_UNUSED(static void uv__update_time(uv_loop_t * loop)) { loop - >time = uv__hrtime(UV_CLOCK_FAST) / 1000000; }
uv__run_timers
Laksanakan setTimeout() dan setInterval() untuk mencapai fungsi panggil balik ambang masa. Proses pelaksanaan ini dilaksanakan melalui traversal gelung Seperti yang anda lihat dari kod di bawah, panggilan balik pemasa disimpan dalam data struktur timbunan minimum Ia keluar apabila timbunan minimum kosong atau belum mencapai ambang masa .
Alih keluar pemasa sebelum melaksanakan fungsi panggil balik pemasa Jika ulangan ditetapkan, ia perlu ditambahkan pada timbunan minimum sekali lagi, dan kemudian panggil balik pemasa dilaksanakan.
Kod khusus adalah seperti berikut:
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
Lintas semua fungsi panggil balik I/O yang disimpan dalam pending_queue , mengembalikan 0 apabila pending_queue kosong; sebaliknya mengembalikan 1 selepas melaksanakan fungsi panggil balik dalam pending_queue
Kodnya adalah seperti berikut:
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
Ketiga-tiga fungsi ini telah selesai makro Fungsi UV_LOOP_WATCHER_DEFINE ditakrifkan Fungsi makro boleh difahami sebagai templat kod, atau fungsi yang digunakan untuk mentakrifkan fungsi. Fungsi makro dipanggil tiga kali dan nilai parameter nama sediakan, semak dan melahu masing-masing pada masa yang sama, tiga fungsi, uvrun_idle, uvrun_prepare, dan uv__run_check, ditakrifkan.
Jadi logik pelaksanaannya adalah konsisten. Mereka semua menggelung dan mengeluarkan objek dalam gelung baris gilir->name##_mengendalikan mengikut prinsip masuk dahulu, keluar dahulu, dan kemudian laksanakan yang sepadan. fungsi panggil balik.
#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 digunakan terutamanya untuk operasi I/O pengundian. Pelaksanaan khusus akan berbeza-beza bergantung pada sistem pengendalian Kami mengambil sistem Linux sebagai contoh untuk analisis.
Fungsi uv__io_poll mempunyai banyak kod sumber Intinya ialah dua keping kod gelung adalah seperti berikut:
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); }... }
Dalam gelung sementara, lalui baris gilir pemerhati. watcher_queue dan letakkan acara dan deskriptor fail Keluarkan dan tetapkan ia kepada objek acara e, dan kemudian panggil fungsi epoll_ctl untuk mendaftar atau mengubah suai acara epoll.
Dalam gelung for, deskriptor fail yang menunggu dalam epoll akan mula-mula dikeluarkan dan diberikan kepada nfds, dan kemudian nfds akan dilalui untuk melaksanakan fungsi panggil balik.
uv__run_closing_handles
Lintas barisan menunggu untuk ditutup, tutup pemegang seperti stream, tcp, udp, dsb., dan kemudian panggil close_cb sepadan dengan pemegang. Kodnya adalah seperti berikut:
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; } }
Walaupun process.nextTick dan Promise kedua-duanya adalah API tak segerak, mereka bukan sebahagian daripada tinjauan acara . Baris gilir, dilaksanakan selepas setiap langkah pengundian acara selesai. Oleh itu, apabila kami menggunakan kedua-dua API tak segerak ini, kami perlu memberi perhatian Jika tugasan panjang atau pengulangan dilakukan dalam fungsi panggil balik masuk, pengundian acara akan disekat, dengan itu "kebuluran" operasi I/O.
Kod berikut ialah contoh panggilan rekursif ke prcoess.nextTick yang menyebabkan fungsi panggil balik fs.readFile gagal dilaksanakan.
fs.readFile('config.json', (err, data) = >{... }) const traverse = () = >{ process.nextTick(traverse) }
Untuk menyelesaikan masalah ini, anda boleh menggunakan setImmediate sebaliknya, kerana setImmediate akan melaksanakan baris gilir fungsi panggil balik dalam gelung acara. Baris gilir tugas process.nextTick mempunyai keutamaan yang lebih tinggi daripada baris gilir tugas Promise Atas sebab tertentu, sila rujuk kod berikut:
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); }
Seperti yang boleh dilihat daripada fungsi processTicksAndRejections(), pertama sekali baris gilir dikeluarkan melalui gelung sementara, dan fungsi panggil balik dalam baris gilir ini ditambah melalui process.nextTick. Apabila gelung sementara tamat, fungsi runMicrotasks() dipanggil untuk melaksanakan fungsi panggil balik Promise.
Struktur teras Node.js yang bergantung pada libuv boleh dibahagikan kepada dua bahagian Satu bahagian ialah I/O rangkaian Pelaksanaan asas akan bergantung pada API sistem yang berbeza sistem pengendalian yang berbeza Bahagian lain ialah Fail I/O, DNS, dan kod pengguna dikendalikan oleh kumpulan benang.
Mekanisme teras libuv untuk mengendalikan operasi tak segerak ialah pengundian acara dibahagikan kepada beberapa langkah Operasi umum adalah untuk melintasi dan melaksanakan fungsi panggil balik dalam baris gilir.
Akhir sekali, dinyatakan bahawa proses API tak segerak.nextTick dan Promise tidak tergolong dalam tinjauan acara Penggunaan yang tidak betul akan menyebabkan tinjauan acara disekat.
Untuk lebih banyak pengetahuan berkaitan nod, sila lawati: tutorial nodejs!
Atas ialah kandungan terperinci Apakah itu libuv, analisis ringkas tinjauan acara dalam libuv (Kebergantungan teras nod). Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!