Привет всем, меня зовут «Юньшу Программирование». Сегодня мы поговорим о мультиплексировании ввода-вывода.
Статья была впервые опубликована на WeChat Официальный аккаунт:Юньшу Программирование Подпишитесь на официальный аккаунт, чтобы получить: 1. Обмен проектами от крупных производителей 2. Обмен различными техническими принципами 3. Рекомендация внутри отдела
через фронтиз Мы уже знаем статью「данные Бао КонгHTTPслой->TCPслой->IPслой->сетевая карта->Интернет->глазизлокальный сервер」а также「данные Как посылка идет от сетевого кабеля к процессу?,существоватьпо заявкеиспользовать」с участиемиз Знание。 В этой статье мы продолжим знакомить вас с различными деталями сетевого программирования и принципами мультиплексирования ввода-вывода.
Предположим, что приложение A хочет запросить у приложения B получение данных, тогда оно выполнит следующие шаги:
Теперь сосредоточимся на приложении А. После того, как приложение А отправит запрос, оно начнет вызывать системные вызовы для чтения данных из буфера чтения TCP. Поскольку невозможно узнать, когда приложение Б вернет данные ответа, то возникнут две ситуации:
Первый вариант — это то, что мы часто называем блокировкой ввода-вывода. Академическая точка зрения: когда пользовательский процесс инициирует вызов чтения, если данные ядра не готовы, операционная система завершит процесс и перейдет в состояние ожидания (без потребления ресурсов ЦП). Процесс не будет разбужен до тех пор, пока данные не будут готовы или не произойдет ошибка. Весь процесс показан на рисунке:
Второй вариант — это то, что мы часто называем неблокирующим вводом-выводом. Академическая точка зрения: когда пользовательский процесс инициирует вызов чтения, если данные ядра не готовы, операционная система вернет ошибку EAGAIN. На основании этой ошибки пользовательский процесс может решить, что данные не готовы, и может запросить. еще раз позже. Если ядро подготавливает данные во время опроса, пользовательский процесс может скопировать данные в пространство пользователя. Весь процесс показан на рисунке:
Блокирующий и неблокирующий ввод-вывод — наиболее распространенные ранние модели сетевого программирования, но у них есть фатальные недостатки. Рассмотрим следующий сценарий:
Однако блокировка ввода-вывода приведет к приостановке потока, а неблокирующий ввод-вывод приведет к тому, что поток останется в состоянии опроса. Обе ситуации предотвратят освобождение или повторное использование потока. По мере увеличения количества пользовательских запросов приложению A приходится создавать больше потоков. Однако для операционной системы существует верхний предел количества создаваемых потоков, и слишком большое количество потоков приведет к увеличению времени на переключение потоков. В серьезных случаях система может зависнуть и не сможет предоставлять внешние службы. Это также известная проблема C10K.
Чтобы решить эту проблему, люди придумали план: использовать один или несколько потоков для мониторинга нескольких сетевых запросов и позволить им завершить этап подготовки данных. Когда данные готовы, соответствующий поток назначается для чтения данных, поэтому небольшое количество потоков можно использовать для обслуживания большого количества сетевых запросов. Это мультиплексирование ввода-вывода.
❝ Повторное использование мультиплексирования ввода-вывода относится к повторному использованию потоков, а не соединений ввода-вывода; цель состоит в том, чтобы позволить небольшому количеству потоков обрабатывать несколько соединений ввода-вывода. ❞
Мультиплексирование ввода-вывода в основном реализуется с помощью следующих функций: select, poll и epoll.
select — самая ранняя функция в Linux, поддерживающая мультиплексирование ввода-вывода.
Мониторинг нескольких событий ввода-вывода можно выполнить с помощью функции выбора.
#define __FD_SETSIZE 1024
typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
//Объявление функции
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
Параметры функции:
Возвращаемое значение функции:
Как видно из приведенного выше объявления функции выбора, fd_set по сути является массивом. Чтобы облегчить работу с массивом, операционная система предоставляет следующие функции:
// Удалить файловый дескриптор fd из setсобирать
void FD_CLR(int fd, fd_set *set);
// Определите, находится ли дескриптор файла fdдасуществоватьsetсобирать в
int FD_ISSET(int fd, fd_set *set);
// Добавить дескриптор файла fd в setсобирать
void FD_SET(int fd, fd_set *set);
// вставь сетсобирать, Все файловые дескрипторы, соответствующие флагу из, установлены в 0.
void FD_ZERO(fd_set *set);
Poll — еще одна технология мультиплексирования ввода-вывода, появившаяся после выбора. По сравнению с select, он использует другой метод хранения файловых дескрипторов, а также устраняет ограничение на количество файловых дескрипторов.
struct pollfd {
int fd; /* file descriptor */
short events; /* events to look for */
short revents; /* events returned */
};
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
Параметры функции:
Возвращаемое значение функции:
В select количество файловых дескрипторов зафиксировано реализацией fd_set, и нет возможности его настроить; в функции poll мы можем свободно управлять размером массива структуры pollfd, прорываясь таким образом через файловое описание, с которым столкнулись; в select существует ограничение на количество символов.
Реализация poll очень похожа на select, за исключением того, что poll использует структуру pollfd, а select использует структуру fd_set. Poll решает проблему ограничения количества файловых дескрипторов, но ему также необходимо скопировать все fds из пользовательского режима в ядро. mode, а также необходимо линейно обходить всю коллекцию fd, поэтому он существенно не отличается от select.
epoll — это новая технология ввода-вывода, управляемая событиями, представленная после ядра Linux 2.6. Она устраняет недостатки производительности операций выбора и опроса и является текущим основным решением для мультиплексирования ввода-вывода. «Интерфейс программирования Linux» использует изображение, чтобы интуитивно показать производительность операций выбора, опроса и epoll при различном количестве файловых дескрипторов.
На приведенном выше рисунке ясно видно, что с увеличением количества файловых дескрипторов производительность epoll по-прежнему остается превосходной. Производительность операций выбора и опроса постепенно снижается по мере увеличения количества дескрипторов.
API epoll очень прост и состоит из 3-х системных функций:
int epoll_create(int size);
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
struct epoll_event {
__uint32_t events;
epoll_data_t data;
};
union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
};
typedef union epoll_data epoll_data_t;
Основным структурным объектом epoll является eventpoll, который в основном содержит несколько важных членов-атрибутов:
struct eventpoll {
/* Wait queue used by sys_epoll_wait() */
wait_queue_head_t wq;
/* List of ready file descriptors */
struct list_head rdllist;
/* RB tree root used to store monitored fd structs */
struct rb_root_cached rbr;
}
Его рабочий процесс в основном показан на рисунке:
В священной книге «Сетевое программирование UNIX» приведено сравнение пяти моделей ввода-вывода. Видно, что будь то синхронный или асинхронный ввод-вывод, получение данных делится на два этапа:
Определение того, является ли модель ввода-вывода синхронной или асинхронной, обычно зависит от того, будет ли пользовательский процесс заблокирован на втором этапе копирования данных. Исходя из этого принципа, из приведенного выше рисунка видно, что асинхронным является только асинхронный ввод-вывод. Остальные, включая select, poll и epoll, основанные на идее мультиплексирования ввода-вывода, являются синхронными вводами-выводами.
Основное внимание уделяется данным. Пока буфер чтения не пуст и буфер записи не заполнен, epoll_wait всегда будет возвращать готовность. Горизонтальный запуск является рабочим режимом epoll по умолчанию.
Основное внимание уделяется изменениям. Пока данные в буфере изменяются, epoll_wait возвращается в состояние готовности.
Изменение данных здесь не просто означает, что буфер меняется с данных на отсутствие данных или с отсутствия данных на данные, но также включает в себя больше или меньше данных. Другими словами, он будет срабатывать при изменении длины буфера.
Предположим, что epoll настроен на триггер по границе. Когда клиент записывает 10 символов, поскольку буфер изменяется с 0 на 10, сервер epoll_wait запускает готовность один раз, и сервер считывает 2 байта, а затем больше не читает. Если вы вызовете epoll_wait в это время, вы обнаружите, что он не будет готов. Готовность будет активирована только тогда, когда клиент снова запишет данные. Это означает, что если вы используете режим ET, вы должны убедиться, что «данные читаются/записываются все сразу», иначе данные не будут читаться/записываться в течение длительного времени. В режиме LT этой проблемы нет.
Как упоминалось ранее, если используется блокировка ввода-вывода, если дескриптор файла не читается в течение длительного времени, соответствующий поток будет заблокирован на долгое время, что приведет к пустой трате ресурсов.
Но когда используются select, poll и epoll, fd, возвращаемый вызовом функции, готов. В этом случае мне все равно нужно использовать неблокирующий ввод-вывод? Ответ — да, вам все равно необходимо использовать неблокирующий ввод-вывод по следующим причинам: 1. После того, как fd будет готов вернуться, перейдите в пользовательский поток для чтения. Существует интервал времени, в течение которого fd мог быть прочитан другими потоками (проблема грохота стада). Если вы прочитаете еще раз в это время, если ввод-вывод заблокирован, пользовательский поток будет заблокирован. 2. FD тоже может быть заброшен ядром. Если в это время прочитать еще раз, то если он блокирует IO, то пользовательский поток будет заблокирован. 3. Select, poll и epoll возвращают только читаемые события и не возвращают читаемые объемы данных. Таким образом, общий подход к использованию неблокирующего ввода-вывода заключается в многократном чтении, пока его невозможно будет прочитать. Но блокировка ввода-вывода отличается. Каждый раз, когда вы читаете данные, вам нужно снова вызвать epoll_wait, чтобы определить, доступны ли они для чтения. Вы не можете читать их несколько раз подряд. Потому что, если данные были прочитаны в последний раз, чтение напрямую без оценки приведет к блокировке пользовательского потока.
EPOLLIN | Указывает, что соответствующий дескриптор файла можно прочитать; |
---|---|
EPOLLOUT | Указывает, что соответствующий дескриптор файла может быть записан; |
EPOLLRDHUP | Указывает, что один конец сокета закрыт или полузакрыт. 1. Противоположный конец отправляет FIN (вызов закрытия или выключения (SHUT_WR)) 2. Локальный конец вызывает выключение (SHUT_RD), которое обычно не используется таким образом; Некоторые системы могут не поддерживать EPOLLRDHUP, вместо этого вы можете использовать «EPOLLIN + read return 0». |
EPOLLHUP | Указывает, что сокет закрыт для чтения и записи: 1. Локальный конец вызывает завершение работы (SHUT_RDWR) 2. И локальный конец, и противоположный конец вызывают завершение работы (SHUT_WR) 3. Противоположный конец вызывает закрытие |
EPOLLERR | Произошла ошибка |