предисловие
В последние годы в разработке серверных приложений широко упоминаются понятия «асинхронность» и «неблокировка». Часто всем нравится описывать их вместе, что может привести к путанице в понимании этих двух слов. В этой статье делается попытка начать с LinuxI/Oс точки зрения обид и обид между ними двумя.
В этой статье рассматриваются следующие вопросы:
-
LinuxизI/Oбазовые знания; -
I/OЗначение модели и существующие категории: - блокировать
I/O; - многопоточная блокировка
I/O; - неблокирующий
I/O; -
I/Oмультиплексирование:select/poll/epoll; - асинхронный
I/O -
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, могут возникнуть две проблемы.
- Какое число возвращает открытый метод?
- Операция чтения будет считывать ресурсы с жесткого диска, а код после чтения нужно ждать, как это сделать (вроде бы отличается от Node.js).
С сомнения начинаем следующее знание.
файловый оператор
мы знаемLinuxЕсть предложениеsloganЭто называется "все есть файл". Очень важным моментом, отражающим эту особенность, является механизм дескриптора файла.
Подытожим общееI/O операций, в том числе:
-
TCP/UDP - стандартный ввод и вывод
- файл читать и писать
DNS- Трубы (технологическая связь)
LinuxМеханизм файловых операторов используется для обеспечения согласованности интерфейса, в том числе:
- представлятьфайловый оператор(
file descriptor, именуемый в дальнейшемfd); - единая пара
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Операционные требования также могут вызвать проблемы:
- если
whileЦиклический опрос операций, ожидающих выполнения, может вызвать ненужныеCPUнапрасная операция, потому что в это времяI/Oоперация не завершена,readФункция не может получить результат; - При использовании
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Проблема мультиплексирования, но есть и недочеты:
- Максимальное количество fds, разрешенное структурой fd_set, равно 1024. Если оно превышает это число, для его решения все равно может потребоваться использование многопоточности;
- накладные расходы на производительность
-
selectКаждый раз, когда функция выполняется, она существуетfd_setКопировать из состояния использования в состояние ядра; - Ядро должно опросить
fd_setсерединаfdположение дел; - Возвращаемое значение также необходимо опросить в пользовательском режиме, чтобы определить, какие из них
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Одноэтапная операция делится на три этапа:
-
epoll_create,Создавать
epoll_fd, для 3-этапного мониторинга; -
epoll_ctl, привяжите fd, который хотите прослушать
epoll_fdнад; -
epoll_wait, входящий
epoll_fd, процесс переходит в состояние блокировки, и любойfdПроцесс разблокируется после изменения.
Давайте взглянемepollКак решить упомянутые выше накладные расходы на производительность:
-
fdПривязка находится вepoll_ctlэтап пройден,epoll_waitПросто пройти вepoll_fd, нет необходимости повторно передаватьfdсобирать; -
epoll_ctlвходящийfdКрасно-черное дерево будет поддерживаться в состоянии ядра, когдаI/OКогда операция завершена, она находится в O(LogN) по красно-черному дереву.fd, чтобы избежать опроса; - вернуться в пользовательский режим
fdМассив фактически вводится в состояние, доступное для чтения и записи.fdКоллекция, больше не требует, чтобы пользователь опрашивал все fds.
Похоже на то,epollСхема является лучшей схемой для схемы мультиплексирования. это имеетepollПример построения веб-сервера вы можетепосмотри.
ноepollНет ли недостатков? ответ отрицательный:
-
epollВ настоящее время поддерживается толькоpipe, созданный такими операциями, как сетьfd, в настоящее время не поддерживает сгенерированную файловую системуfd.
Асинхронный ввод-вывод
описано выше, блокирует лиI/Oили неблокирующийI/OещеI/OМультиплексирование, все синхронноI/O. требовать от пользователя ожиданияI/OОперация завершается, и возвращенное содержимое получено. Сама операционная система также обеспечивает асинхронныйI/OПрограммы соответствуют разным операционным системам:
- Linux
- aio, который в настоящее время подвергается критике, самый большой недостаток в том, что он поддерживает только
Direct I/O(файловая операция) - io_uring, новое дополнение к ядру Linux в версии 5.1, считается асинхронным в Linux.
I/Oновый дом - windows
- iocp, как решение для асинхронной обработки для libuv в Windows. (Автор
windowsНе так много исследований, не так много введения. )
Пока что некоторые общиеI/OМодель.
и в настоящее время вLinuxРекомендуемое решение вышеepollМеханизмы. ноepollФайлы мониторинга не поддерживаютсяfdпроблема, нам еще нужно включить мозги, давайте посмотримlibuvкак это решить.
решение libuv
libuvиспользоватьepollстроитьevent-loop, куда:
-
socket,pipeждать, чтобы пройтиepollспособ контролироватьfdтип, черезepoll_waitспособ наблюдения; - Обработка файлов/разрешение DNS/распаковка, сжатие и другие операции обрабатываются с помощью рабочих потоков, а запрос и результат соединяются через две очереди, а
pipeобщаться с основным потоком,epollконтролироватьfdспособ определить, когда читать очередь.
На этом статья закончится. Подведем краткий итог:
Сначала мы представили концепцию файловых дескрипторов, а затемLinuxПереключатель основного состояния процесса. затем ввести блокировкуI/Oконцепция и недостатки, которые вводят многопоточную блокировкуI/O, неблокирующийI/O,так же какI/OМультиплексирование и асинхронностьI/OКонцепция чего-либо. Наконец, в сочетании с вышеизложенным знанием, краткое введение во внутреннюю либувуI/Oрабочий механизм.
Наконец, сделайте «небольшое объявление».ByteDance искренне приглашает выдающихся фронтенд-инженеров иNode.jsИнженеры могут присоединиться к нам, чтобы делать интересные вещи.
Ладно, увидимся в следующий раз.