Анализ механизма асинхронного неблокирующего ввода-вывода Node.js

Node.js

предисловие

В последние годы в разработке серверных приложений широко упоминаются понятия «асинхронность» и «неблокировка». Часто всем нравится описывать их вместе, что может привести к путанице в понимании этих двух слов. В этой статье делается попытка начать с LinuxI/Oс точки зрения обид и обид между ними двумя.

В этой статье рассматриваются следующие вопросы:

  1. LinuxизI/Oбазовые знания;
  2. I/OЗначение модели и существующие категории:
    1. блокироватьI/O;
    2. многопоточная блокировкаI/O;
    3. неблокирующийI/O;
    4. I/Oмультиплексирование:select/poll/ epoll;
    5. асинхронныйI/O
  3. libuvкак решитьI/OЭта проблема.

Кроме того, примеры, рассмотренные в этой статье, размещены наGITHUB, добро пожаловать на загрузку для пробной эксплуатации.

Основы ввода-вывода в Linux

Начните с примера чтения файла

Теперь есть требование читать файлы через скрипты оболочки. Я считаю, что большинство людей смогут завершить реализацию сразу:

$ cat say-hello.txt

А теперь мы просимCдля достижения той же функции, чтобы раскрыть больше деталей.

#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char const *argv[])
{
    char buffer[1024];
    int fd = open("say-hello.txt", O_RDONLY, 0666);
    int size;

    if (fd < 0) {
        perror("open");
        return 1;
    }

    while ((size = read(fd, buffer, sizeof(buffer))) > 0)
    {
        printf("%s\n", buffer);
    }

    if (size < 0) {
        perror("read");
    }

    close(fd);
    return 0;
}

передачаopenфункция принимает число, используя его дляwriteа такжеreadоперация, последний звонокclose, что является самым основнымLinux I/OОперационные процедуры.

Здесь часто пишут JavaScript, и у людей, не знакомых с c, могут возникнуть две проблемы.

  1. Какое число возвращает открытый метод?
  2. Операция чтения будет считывать ресурсы с жесткого диска, а код после чтения нужно ждать, как это сделать (вроде бы отличается от Node.js).

С сомнения начинаем следующее знание.

файловый оператор

мы знаемLinuxЕсть предложениеsloganЭто называется "все есть файл". Очень важным моментом, отражающим эту особенность, является механизм дескриптора файла.

Подытожим общееI/O операций, в том числе:

  • TCP / UDP
  • стандартный ввод и вывод
  • файл читать и писать
  • DNS
  • Трубы (технологическая связь)

LinuxМеханизм файловых операторов используется для обеспечения согласованности интерфейса, в том числе:

  1. представлятьфайловый оператор(file descriptor, именуемый в дальнейшемfd);
  2. единая параreadа такжеwriteМетод операций чтения и записи.

Пример, упомянутый вышеopenФункция возвращает число, являющееся дескриптором файла, который соответствует единственному файлу в текущем процессе.

LinuxВ эксплуатации будетБлок управления технологическим процессом (PCB)Используйте массив фиксированного размера, каждый элемент массива указывает наядродля каждогообработатьВедется запись файлов, открытых этим процессом.

struct task_struct {
    ...
    /* Open file information: */
    struct files_struct		*files;
    ...
}
/*
 * Open file table structure
 */
struct files_struct {
	spinlock_t file_lock ____cacheline_aligned_in_smp;
	unsigned int next_fd;
	unsigned long close_on_exec_init[1];
	unsigned long open_fds_init[1];
	unsigned long full_fds_bits_init[1];
	struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};

а такжеfdФактически это порядковый номер соответствующей файловой структуры в этом массиве, который такжеfdПричина, по которой он начнется с 0.

так вreadилиwriteпрошёл во время операцииfd,LinuxЕстественно, вы можете найти нужный файл для работы.

как восприниматьfdсуществует, вы можете использоватьlsofнапример, печать с помощью следующей командыChromeбраузер в данный момент открытfdситуации (если вы использовалиChromeбраузер для доступа к текущей странице).

$ lsof -p$(ps -ef |grep Chrome|awk 'NR==1{print $2}')

Вторая проблема, представленная вышеприведенным примером:

Операция чтения будет считывать ресурсы с жесткого диска, а код после чтения нужно ждать, как это сделать (вроде бы отличается от Node.js).

В работающем процессе Linux основной частью выполняемой программы является процесс или поток (до версии 2.4 ядро ​​Linux было процессом, а затем основной единицей планирования стал поток, а процесс стал контейнером потока). .

Процесс будет иметь базовое рабочее состояние во время выполнения процесса, как это

В сочетании с приведенным выше примером после выполнения функции чтения процесс переходит в состояние блокировки, иI/OПосле завершения система прерывает процесс, чтобы снова разблокировать процесс, переходит в состояние готовности и переходит в состояние выполнения после ожидания выделения кванта времени.

То есть наш процесс будет вI/OБлокировка во время операции, вот объяснение проблемы выше.

Модель ввода/вывода

этот описан вышеI/O 机制это мыI/O 模型середина阻塞 I/Oмеханизм.

блокировка ввода/вывода

блокироватьI/Oдаread/writeМеханизм выполнения функции по умолчанию переводит процесс в состояние блокировки при выполнении операций чтения и записи.После завершения ввода-вывода системное прерывание переводит его в состояние готовности, ожидание выделения кванта времени, и выполнить его.

но блокируетI/OПроблема с механизмом в том, что он не может выполняться одновременноI/Oоперации или вI/OОперация выполняется одновременно с вычислением ЦП. В сценарии веб-запрос/ответ, если один запрос блокирует чтение состояния, другие запросы не могут быть обработаны.

Нам нужно решить эту проблему.

Многопоточный блокирующий ввод-вывод

Первая идея заключается в использовании многопоточности.

Мы предварительно инициализируем пул потоков, используя семафорwaitПримитив переходит в состояние блокировки. подожди пока не будетI/OКогда операция нужна, через семафорsignalразбудить поток и выполнить соответствующийI/Oработать. Для подробной работы, пожалуйстасм. код.

Но многопоточный неблокирующийI/OНедостаток заключается в том, что когда количество соединений достигает большого уровня, переключение потоков также требует больших накладных расходов.

Итак, ожидайте, что сможете разрешить в потокеI/OОперация ожидания позволяет избежать накладных расходов на переключение контекста потока, вызванное открытием нескольких потоков. Есть ли такой способ, чтобы мы могли ввести неблокирующийI/Oрежим.

Неблокирующий ввод-вывод

Неблокирующий ввод-вывод — это механизм, который позволяет пользователюI/OПосле чтения и записи функции немедленно возвращайтесь, если буфер недоступен для чтения или записи, возвращайтесь напрямую-1. Вот неблокирующийI/OПример построения веб-сервера вы можетесм. код.

Ключевая функция такова:

fcntl(fd, F_SETFL, O_NONBLOCK);

Вы можете увидеть процесс здесьSTATEбольше не блокируется,I/OВо время операции другиеCPUоперация

неблокирующийI/OМожет помочь нам решить параллельное выполнение в одном потокеI/OОперационные требования также могут вызвать проблемы:

  1. еслиwhileЦиклический опрос операций, ожидающих выполнения, может вызвать ненужныеCPUнапрасная операция, потому что в это времяI/Oоперация не завершена,readФункция не может получить результат;
  2. При использованииsleep/usleepспособ заставить процесс спать в течение определенного периода времени, а затем вызватьI/OВозвращение операции было несвоевременным.

Итак, есть ли в системе механизм, позволяющий нам изначально ожидать несколькихI/OВыполнение операции. Ответ - да, нам нужно представить нашуI/OМультиплексирование.

Мультиплексирование ввода/вывода

Мультиплексирование ввода-вывода, поэтому название означает, что несколько операций ввода-вывода выполняются одновременно в рамках процесса. Также есть сам по себе эволюционный процесс, это select, poll, epoll (замена на macOS — kqueue). Мы представим их по очереди.

select

selectИспользование обычно состоит из трех функций (подробных,тыкать это):

void FD_SET(int fd, fd_set *fdset);

int select(int nfds, fd_set *restrict readfds,
fd_set *restrict writefds, fd_set *restrict errorfds,
struct timeval *restrict timeout);

int FD_ISSET(int fd, fd_set *fdset);

selectФункция заключается в мониторинге в пакетном режимеfd, когда входящийfd_setлюбой изfdКогда буфер переходит в состояние, доступное для чтения/записи, разблокировать и передатьFD_ISSETперейти к конкретномуfd.

fd_set tmpset = *fds;

struct timeval tv;
tv.tv_sec = 5;

int r = select(fd_size, &tmpset, NULL, NULL, &tv);

if (r < 0) do {
        perror("select()");
        break;
    } while(0);
else if (r) {
    printf("fd_size %d\n", r);
    QUEUE * q;
    QUEUE_FOREACH(q, &loop->wq->queue) {
        watcher_t * watcher = QUEUE_DATA(q, watcher_t, queue);
        int fd = watcher->fd;
        if (FD_ISSET(fd, &tmpset)) {
            int n = watcher->work(watcher);
        }
    }
}
else {
    printf("%d No data within five seconds.\n", r);
}

вот одинselectПример построения веб-сервера вы можетепосмотри.

selectфункция может решитьI/OПроблема мультиплексирования, но есть и недочеты:

  1. Максимальное количество fds, разрешенное структурой fd_set, равно 1024. Если оно превышает это число, для его решения все равно может потребоваться использование многопоточности;
  2. накладные расходы на производительность
    1. selectКаждый раз, когда функция выполняется, она существуетfd_setКопировать из состояния использования в состояние ядра;
    2. Ядро должно опроситьfd_setсерединаfdположение дел;
    3. Возвращаемое значение также необходимо опросить в пользовательском режиме, чтобы определить, какие из нихfdперешел в читаемое состояние;

poll

Первый вопрос задаетpollрешено,pollКоллекция fd, полученная функцией, заменяется на массив, и ограничение размера в 1024 больше не существует.pollФункция определяется следующим образом:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

конкретное использование иselectочень похожи:

struct pollfd * fds = poll_init_fd_set(loop, count);

int r = poll(fds, count, 5000);

if (r < 0) do {
        perror("select()");
        break;
    } while(0);
else if (r) {
    QUEUE * q;

    QUEUE_FOREACH(q, &loop->wq->queue) {
        watcher_t * watcher = QUEUE_DATA(q, watcher_t, queue);
        int fd = watcher->fd;

        if (watcher->fd_idx == -1) {
            continue;
        }

        if ((fds + watcher->fd_idx)->revents & POLLIN) {
            watcher->work(watcher);
        }
    }
}
else {
    printf("%d No data within five seconds.\n", r);
}

Вот пример создания опроса веб-сервера, который можетпосмотри.

но вышеselectС упомянутыми накладными расходами проблема остается. пока вepoll, проблема решена.

epoll

Подробности могут бытьСмотри сюда. просто говоря,epollБудуselect, pollОдноэтапная операция делится на три этапа:

  1. epoll_create,Создаватьepoll_fd, для 3-этапного мониторинга;
  2. epoll_ctl, привяжите fd, который хотите прослушатьepoll_fdнад;
  3. epoll_wait, входящийepoll_fd, процесс переходит в состояние блокировки, и любойfdПроцесс разблокируется после изменения.

Давайте взглянемepollКак решить упомянутые выше накладные расходы на производительность:

  1. fdПривязка находится вepoll_ctlэтап пройден,epoll_waitПросто пройти вepoll_fd, нет необходимости повторно передаватьfdсобирать;
  2. epoll_ctlвходящийfdКрасно-черное дерево будет поддерживаться в состоянии ядра, когдаI/OКогда операция завершена, она находится в O(LogN) по красно-черному дереву.fd, чтобы избежать опроса;
  3. вернуться в пользовательский режимfdМассив фактически вводится в состояние, доступное для чтения и записи.fdКоллекция, больше не требует, чтобы пользователь опрашивал все fds.

Похоже на то,epollСхема является лучшей схемой для схемы мультиплексирования. это имеетepollПример построения веб-сервера вы можетепосмотри.

ноepollНет ли недостатков? ответ отрицательный:

  1. epollВ настоящее время поддерживается толькоpipe, созданный такими операциями, как сетьfd, в настоящее время не поддерживает сгенерированную файловую системуfd.

Асинхронный ввод-вывод

описано выше, блокирует лиI/Oили неблокирующийI/OещеI/OМультиплексирование, все синхронноI/O. требовать от пользователя ожиданияI/OОперация завершается, и возвращенное содержимое получено. Сама операционная система также обеспечивает асинхронныйI/OПрограммы соответствуют разным операционным системам:

  1. Linux
    1. aio, который в настоящее время подвергается критике, самый большой недостаток в том, что он поддерживает толькоDirect I/O(файловая операция)
    2. io_uring, новое дополнение к ядру Linux в версии 5.1, считается асинхронным в Linux.I/Oновый дом
  2. windows
    1. iocp, как решение для асинхронной обработки для libuv в Windows. (АвторwindowsНе так много исследований, не так много введения. )

Пока что некоторые общиеI/OМодель.

и в настоящее время вLinuxРекомендуемое решение вышеepollМеханизмы. ноepollФайлы мониторинга не поддерживаютсяfdпроблема, нам еще нужно включить мозги, давайте посмотримlibuvкак это решить.

решение libuv

libuvиспользоватьepollстроитьevent-loop, куда:

  1. socket, pipeждать, чтобы пройтиepollспособ контролироватьfdтип, черезepoll_waitспособ наблюдения;
  2. Обработка файлов/разрешение DNS/распаковка, сжатие и другие операции обрабатываются с помощью рабочих потоков, а запрос и результат соединяются через две очереди, аpipeобщаться с основным потоком,epollконтролироватьfdспособ определить, когда читать очередь.

На этом статья закончится. Подведем краткий итог:

Сначала мы представили концепцию файловых дескрипторов, а затемLinuxПереключатель основного состояния процесса. затем ввести блокировкуI/Oконцепция и недостатки, которые вводят многопоточную блокировкуI/O, неблокирующийI/O,так же какI/OМультиплексирование и асинхронностьI/OКонцепция чего-либо. Наконец, в сочетании с вышеизложенным знанием, краткое введение во внутреннюю либувуI/Oрабочий механизм.

Наконец, сделайте «небольшое объявление».ByteDance искренне приглашает выдающихся фронтенд-инженеров иNode.jsИнженеры могут присоединиться к нам, чтобы делать интересные вещи.

Ладно, увидимся в следующий раз.