Меня спросили о разнице между select, poll и epoll, когда я проходил собеседование при приеме на работу перед выпуском, тогда я просто поискал в Интернете и запомнил ответ. Тоже не глубоко понял. Пользуясь временем, сейчас планирую хорошенько его рассмотреть.Следующая статья неплохая. Волна коллекций.
Что такое синхронный ввод-вывод и асинхронный ввод-вывод, блокирующий ввод-вывод и неблокирующий ввод-вывод и в чем разница? Разные люди дают разные ответы в разных контекстах. Итак, давайте сначала ограничим контекст этой статьи.
Предыстория, обсуждаемая в этой статье, — это сетевой ввод-вывод в среде Linux.
Концептуальная записка
Прежде чем объяснять, давайте сначала объясним несколько понятий:
- пространство пользователя и пространство ядра
- переключатель процесса
- блокировка процесса
- файловый дескриптор
- Кэшированный ввод-вывод
пространство пользователя и пространство ядра
Сейчас операционные системы используют виртуальную память, поэтому для 32-битной операционной системы ее адресное пространство (виртуальное пространство для хранения) равно 4G (2 в 32-й степени). Ядром операционной системы является ядро, которое не зависит от обычных приложений и имеет доступ к защищеннымпространство памяти, также имеет все права доступа к базовому аппаратному устройству. Чтобы гарантировать, что пользовательский процесс не может напрямую управлять ядром и обеспечить безопасность ядра, система беспокойства делит виртуальное пространство на две части: одна — пространство ядра, а другая — пространство пользователя. Для операционной системы Linux самые старшие байты 1G (от виртуального адреса 0xC0000000 до 0xFFFFFFFF) используются ядром, которое называется пространством ядра, а младшие байты 3G (от виртуального адреса 0x00000000 до 0xBFFFFFFF) используются для каждого использования процесса, называетсяпользовательское пространство.
переключатель процесса
Чтобы управлять выполнением процесса, ядро должно иметь возможность приостанавливать процесс, работающий на ЦП, и возобновлять выполнение ранее приостановленного процесса. Такое поведение называется переключением процессов. Поэтому можно сказать, что любой процесс работает при поддержке ядра операционной системы и тесно связан с ядром.
При переходе от запуска одного процесса к запуску другого процесса в этот процесс вносятся следующие изменения:
- Сохраняет контекст процессора, включая программный счетчик и другие регистры.
- Обновите информацию о печатной плате.
- Переместите PCB процесса в соответствующую очередь, например, готовность, блокировка по событию и т. д.
- Выберите другой процесс для выполнения и обновите его плату.
- Обновите структуры данных управления памятью.
- Восстановите контекст обработчика.
блокировка процесса
Выполняемый процесс, поскольку некоторые ожидаемые события не произошли, такие как отказ от запроса системных ресурсов, ожидание завершения определенных операций, еще не поступили новые данные или нет новой работы и т. д., система автоматически выполняет блокирующий примитив (Block), переходит из рабочего состояния в заблокированное. Видно, что блокировка процесса является активным поведением самого процесса, поэтому перевести его в состояние блокировки может только процесс в запущенном состоянии (получение ЦП). Когда процесс переходит в состояние блокировки, он не занимает ресурсы процессора.
файловый дескриптор fd
Дескриптор файла — это термин в информатике, абстракция, используемая для обозначения ссылки на файл.
Дескриптор файла формально представляет собой неотрицательное целое число. На самом деле это значение индекса, указывающее на таблицу записей открытых файлов процесса, поддерживаемую ядром для каждого процесса. Когда программа открывает существующий файл или создает новый файл, ядро возвращает процессу дескриптор файла. В программировании некоторые низкоуровневые программы часто вращаются вокруг файловых дескрипторов. Однако концепция файловых дескрипторов часто применима только к таким операционным системам, как UNIX и Linux.
Кэшированный ввод-вывод
Кэшированный ввод-вывод также известен как стандартный ввод-вывод, и операция ввода-вывода по умолчанию для большинства файловых систем — это кэшированный ввод-вывод. В кэшированном механизме ввода-вывода Linux операционная система кэширует данные ввода-вывода в кэше страниц файловой системы, т. е.Данные сначала будут скопированы в буфер ядра операционной системы, а затем будут скопированы из буфера ядра операционной системы в адресное пространство приложения..
Недостатки кэшированного ввода-вывода:
В процессе передачи данных требуется несколько операций копирования данных в адресном пространстве приложения и ядре, накладные расходы ЦП и памяти, вызванные этими операциями копирования данных, очень велики.
Два режима ввода-вывода
Как я только что сказал, для доступа IO (возьмем в качестве примера чтение) данные сначала будут скопированы в буфер ядра операционной системы.Затем он будет скопирован из буфера ядра операционной системы в адресное пространство приложения.. Итак, когда происходит операция чтения, она проходит две фазы:
- Ожидание готовности данных
- Копирование данных из ядра в процесс
Формально из-за этих двух этапов система Linux создала следующие пять сетевых режимов.
- Блокирующий ввод-вывод (блокирующий ввод-вывод)
- Неблокирующий ввод-вывод (неблокирующий ввод-вывод)
- Мультиплексирование ввода-вывода (мультиплексирование ввода-вывода)
- Ввод-вывод, управляемый сигналом (ввод-вывод, управляемый сигналом)
- Асинхронный ввод-вывод (асинхронный ввод-вывод)
Примечание. Поскольку ввод-вывод, управляемый сигналом, на практике обычно не используется, я упомяну только оставшиеся четыре модели ввода-вывода.
Блокирующий ввод-вывод (блокирующий ввод-вывод)
В linux все сокеты по умолчанию заблокированы.Типичный поток операции чтения выглядит так:
Когда пользовательский процесс вызывает системный вызов recvfrom, ядро запускает первый этап ввода-вывода: подготовка данных (для сетевого ввода-вывода много раз данные не поступали в начале. Например, полный UDP-пакет еще не получен. , В это время ядро будет ждать поступления достаточного количества данных). Этот процесс должен ждать, то есть требуется процесс для копирования данных в буфер ядра операционной системы. На стороне пользовательского процесса весь процесс будет заблокирован (конечно, сам процесс выбирает блокировку). Когда ядро ждет, пока данные будут готовы, оно скопирует данные из ядра в пользовательскую память, затем ядро возвращает результат, а пользовательский процесс освобождает состояние блока и снова начинает работать.Следовательно, характеристика блокирующего ввода-вывода заключается в том, что он блокируется на обоих этапах выполнения ввода-вывода.
Неблокирующий ввод-вывод (неблокирующий ввод-вывод)
В Linux вы можете сделать его неблокирующим, установив socket. При выполнении операции чтения на неблокирующем сокете поток выглядит следующим образом:
Когда пользовательский процесс выполняет операцию чтения, если данные в ядре не готовы, он не будет блокировать пользовательский процесс, а немедленно вернет ошибку. С точки зрения пользовательского процесса, после того, как он инициирует операцию чтения, ему не нужно ждать, а он немедленно получает результат. Когда пользовательский процесс считает, что результатом является ошибка, он знает, что данные не готовы, поэтому он может снова отправить операцию чтения. Как только данные в ядре готовы и снова получен системный вызов от пользовательского процесса, он немедленно копирует данные в пользовательскую память и возвращается.** Следовательно, особенность неблокирующего ввода-вывода заключается в том, что пользовательский процесс должен постоянно и активно запрашивать данные ядра. **
Мультиплексирование ввода-вывода (мультиплексирование ввода-вывода)
Мультиплексирование ввода-вывода — это то, что мы называем select, poll и epoll.В некоторых местах этот метод ввода-вывода также называется вводом-выводом, управляемым событиями. Преимущество select/epoll заключается в том, что один процесс может обрабатывать ввод-вывод нескольких сетевых подключений одновременно. Его основной принцип заключается в том, что функции select, poll и epoll будут непрерывно опрашивать все сокеты, за которые они отвечают, и когда в определенный сокет поступают данные, он уведомляет пользовательский процесс.
Когда пользовательский процесс вызывает select, весь процесс будет заблокирован, и в то же время ядро будет «мониторить» все сокеты, за которые отвечает select.Когда данные в каком-либо сокете будут готовы, select вернется. В это время пользовательский процесс снова вызывает операцию чтения, чтобы скопировать данные из ядра в пользовательский процесс.Таким образом, особенностью мультиплексирования ввода-вывода является то, что процесс может ожидать несколько файловых дескрипторов одновременно с помощью механизма, и любой из этих файловых дескрипторов (дескрипторов сокетов) переходит в состояние готовности к чтению, функция select() для вернуть.
Этот график мало чем отличается от графика блокирующего ввода-вывода, а даже хуже. Потому что здесь нужно использовать два системных вызова (select и recvfrom), а блокирующий ввод-вывод вызывает только один системный вызов (recvfrom). Однако преимущество использования select заключается в том, что он может обрабатывать несколько подключений одновременно.
Следовательно, если количество обработанных соединений не очень велико, веб-сервер, использующий select/epoll, не обязательно лучше, чем веб-сервер, использующий многопоточность + блокирующий ввод-вывод, и задержка может быть больше. Преимущество select/epoll не в том, что он может быстрее обрабатывать одно соединение, а в том, что он может обрабатывать больше соединений. )
На практике в модели мультиплексирования ввода-вывода каждый сокет обычно настроен как неблокирующий, однако, как показано на рисунке выше, весь пользовательский процесс на самом деле все время заблокирован. Просто процесс блокируется функцией select, а не сокетом IO.
Асинхронный ввод-вывод (асинхронный ввод-вывод)
Асинхронный ввод-вывод под Linux на самом деле редко используется. Давайте посмотрим на его процесс:
После того как пользовательский процесс инициирует операцию чтения, он может немедленно приступить к другим действиям. С другой стороны, с точки зрения ядра, когда оно получает асинхронное чтение, оно немедленно возвращается, поэтому оно не будет генерировать никакого блока для пользовательского процесса. Затем ядро будет ждать готовности данных, а затем скопирует данные в память пользователя.Когда все это будет сделано, ядро отправит сигнал пользовательскому процессу, сообщая ему, что операция чтения завершена.Суммировать
Разница между блокировкой и неблокировкой
Вызов блокирующего ввода-вывода заблокирует соответствующий процесс до завершения операции, в то время как неблокирующий ввод-вывод вернется немедленно, когда ядро все еще готовит данные.
Разница между синхронным вводом-выводом и асинхронным вводом-выводом
Прежде чем объяснять разницу между синхронным вводом-выводом и асинхронным вводом-выводом, нам нужно дать их определения. Определение POSIX выглядит так:
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- An asynchronous I/O operation does not cause the requesting process to be blocked;
Разница между ними заключается в том, что синхронный ввод-вывод блокирует процесс при выполнении «операции ввода-вывода». Согласно этому определению, вышеупомянутый блокирующий ввод-вывод, неблокирующий ввод-вывод и мультиплексирование ввода-вывода относятся к синхронному вводу-выводу.
Некоторые люди скажут, что неблокирующий ввод-вывод не блокируется. Здесь есть очень «хитрое» место: «операция ввода-вывода», упомянутая в определении, относится к реальной операции ввода-вывода, которой в примере является системный вызов recvfrom. Когда неблокирующий ввод-вывод выполняет системный вызов recvfrom, если данные ядра не готовы, процесс в это время не будет заблокирован. Однако, когда данные в ядре будут готовы, recvfrom скопирует данные из ядра в память пользователя.В это время процесс блокируется.В это время процесс блокируется.
Отличие асинхронного ввода-вывода заключается в том, что когда процесс инициирует операцию ввода-вывода, он напрямую возвращается и игнорирует ее до тех пор, пока ядро не отправит сигнал, сообщающий процессу, что ввод-вывод завершен. Во время всего этого процесса процесс вообще не блокируется.
Сравнение каждой модели ввода-вывода показано на рисунке:
Из приведенного выше рисунка видно, что разница между неблокирующим вводом-выводом и асинхронным вводом-выводом по-прежнему очевидна. В неблокирующем вводе-выводе, хотя процесс не будет заблокирован большую часть времени, он по-прежнему требует, чтобы процесс активно проверял, и когда подготовка данных завершена, процесс также должен активно вызывать recvfrom снова, чтобы скопировать данные в пользовательская память. А асинхронный ввод-вывод совсем другой. Это похоже на то, как пользовательский процесс передает всю операцию ввода-вывода кому-то другому (ядру) для завершения, а затем сигнализирует, когда другой человек закончит. В течение этого периода пользовательскому процессу не нужно ни проверять состояние операций ввода-вывода, ни активно копировать данные.
Подробное объяснение выбора, опроса и epoll мультиплексирования трех входов/выходов.
select, poll и epoll — все это механизмы мультиплексирования ввода-вывода. Мультиплексирование ввода-вывода — это механизм, с помощью которого процесс может отслеживать несколько дескрипторов.Когда дескриптор готов (обычно готов к чтению или записи), он может уведомить программу о выполнении соответствующих операций чтения и записи.Но select, poll и epoll по сути являются синхронным вводом-выводом, потому что все они должны отвечать за чтение и запись после того, как событие чтения-записи будет готово, что означает, что процесс чтения-записи заблокирован., а асинхронный ввод-вывод не должен сам отвечать за чтение и запись.Реализация асинхронного ввода-вывода будет отвечать за копирование данных из ядра в пространство пользователя. (здесь долго)
select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
Файловые дескрипторы, отслеживаемые функцией select, делятся на три категории: writefds, readfds и excludefds.Функция выбора будет заблокирована после вызова, пока описание не будет готово (данные доступны для чтения, записи или исключения) или пока не истечет время ожидания (время ожидания указывает время ожидания, если оно возвращается немедленно, его можно установить равным нулю), функция возвращает значение. Когда функция select возвращает значение, готовый дескриптор можно найти, просматривая файл fdset.
select в настоящее время поддерживается практически на всех платформах, и его хорошая кроссплатформенная поддержка также является одним из его преимуществ. Одним из недостатков метода select является то, что число файловых дескрипторов, которые может отслеживать один процесс,Существует максимальное количество, в линуксеОбычно 1024, вы можете улучшить это ограничение, изменив определение макроса или даже перекомпилировав ядро, но это также снизит эффективность.
poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
В отличие от того, как select использует три растровых изображения для представления трех fdset, poll использует указатель на pollfd.
struct pollfd { int fd; /* file descriptor / short events; / requested events to watch / short revents; /возвращенные события, засвидетельствованные */ }; Структура pollfd содержит событие, которое нужно отслеживать, и событие, которое происходит, и больше не использует метод выбора "параметр-значение". В то же время, pollfd неБез ограничения максимального количества(Но производительность также снизится, если число будет слишком большим). Как и в случае с функцией select, после возврата опроса вам необходимоопросчтобы получить готовый дескриптор.
Из вышеизложенного и select, и poll должны получить готовый сокет, пройдя файловый дескриптор после возврата. На самом деле большое количество одновременно подключенных клиентов может иметь несколько состояний готовности одновременно, поэтому эффективность снижается линейно по мере роста числа отслеживаемых дескрипторов.
epoll
epoll был предложен в ядре 2.6 и представляет собой расширенную версию предыдущих методов select и poll. По сравнению с select и poll, epoll является более гибким и не имеет ограничений по дескрипторам. epoll использует один дескриптор файла для управления несколькими дескрипторами и сохраняет события файловых дескрипторов, связанных с пользователем, в таблице событий ядра, так что копирование в пространство пользователя и пространство ядра необходимо выполнить только один раз.
Процесс работы epoll
Процесс работы epoll требует наличия трех интерфейсов, а именно:
int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
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);
1. int epoll_create(int size);
Создайте дескриптор epoll, размер используется, чтобы сообщить ядру, насколько велико количество мониторов.Этот параметр отличается от первого параметра в select(), который дает значение fd+1 для максимального монитора.Размер параметра не ограничено Максимальное количество дескрипторов, которые может отслеживать epoll, является лишь предложением для первоначального распределения ядром внутренних структур данных. Когда дескриптор epoll создан, он будет занимать значение fd.Если вы посмотрите на /proc/process id/fd/ под Linux, вы увидите fd, поэтому после использования epoll вы должны вызвать close(), чтобы закрыть его, в противном случае fd может быть исчерпан.
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
Функция заключается в выполнении операции op над указанным дескриптором fd.
- epfd: это возвращаемое значение epoll_create().
- op: представляет операцию op, представленную тремя макросами: добавить EPOLL_CTL_ADD, удалить EPOLL_CTL_DEL и изменить EPOLL_CTL_MOD. Добавьте, удалите и измените события прослушивания для fd соответственно.
- fd: это fd (файловый дескриптор), который необходимо отслеживать.
- epoll_event: сообщает ядру, что отслеживать.Структура struct epoll_event следующая:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
Дождитесь событий ввода-вывода на epfd, возвращая максимум событий maxevents. События параметра используются для получения набора событий от ядра, maxevents сообщает ядру, насколько велики события, значение этого maxevents не может быть больше, чем размер при создании epoll_create(), параметр timeout - это время ожидания ( миллисекунд, 0 вернется сразу, -1 будет Не уверен, есть еще поговорка, что он заблокирован навсегда). Эта функция возвращает количество событий, которые необходимо обработать.Если она возвращает 0, это означает, что время ожидания истекло.
Два режима работы
epoll работает с файловыми дескрипторами в двух режимах: LT (триггер уровня) и ET (триггер края). Режим LT является режимом по умолчанию Различия между режимом LT и режимом ET заключаются в следующем: LT-режим: когда epoll_wait обнаруживает возникновение события дескриптора и уведомляет приложение об этом событии, приложение может не обрабатывать событие немедленно. При следующем вызове epoll_wait приложение снова ответит и получит уведомление об этом событии. Режим ET: когда epoll_wait обнаруживает возникновение события дескриптора и уведомляет приложение об этом событии, приложение должно немедленно обработать событие. Если его не обработать, то при следующем вызове epoll_wait приложение больше не будет отвечать и будет уведомлено об этом событии.
1. Низкий режим
LT (триггер уровня) — это способ работы по умолчанию, который поддерживает как блочные, так и неблочные сокеты.При таком подходе ядро сообщает вам, готов ли файловый дескриптор, и затем вы можете выполнять операции ввода-вывода с готовым fd. Если вы ничего не сделаете, ядро будет продолжать уведомлять вас.
2. Режим ЕТ
ET (запуск по фронту) — это высокоскоростной метод работы, который поддерживает только неблокирующие сокеты. В этом режиме ядро сообщает вам через epoll, когда дескриптор становится готовым или никогда не готовым. Затем он будет считать, что вы знаете, что дескриптор файла готов, и не будет отправлять больше уведомлений о готовности для этого дескриптора файла, пока вы не сделаете что-то, что приведет к тому, что этот дескриптор файла больше не будет готов (например, вы вызвали ошибку EWOULDBLOCK при отправке, получении , или получение запроса, или отправка или получение менее определенного объема данных). Но обратите внимание, что если этот fd не был IOed (что приводит к тому, что он снова становится не готовым), ядро больше не будет отправлять уведомления (только один раз).
Режим ET значительно снижает количество повторных запусков события epoll, поэтому эффективность выше, чем у режима LT. Когда epoll работает в режиме ET, необходимо использовать неблокирующие сокеты, чтобы избежать голодания задачи обработки нескольких файловых дескрипторов из-за блокировки операций чтения/записи в дескрипторе файла.
3. Резюме
Если есть такой пример:
- Мы добавили дескриптор файла (RFD) в дескриптор epoll для чтения данных из пайпа.
- В это время 2 КБ данных записываются с другого конца канала.
- Вызовите epoll_wait(2), и он вернет RFD, указывающий, что он готов к операции чтения.
- Затем мы читаем 1 КБ данных
- вызовите epoll_wait(2)......
LT-режим: Если это режим LT, его все равно можно уведомить после вызова epoll_wait(2) на шаге 5.
Режим ЕТ: Если бы мы использовали флаг EPOLLET при добавлении RFD в дескриптор epoll на шаге 1, была бы вероятность зависания после вызова epoll_wait(2) на шаге 5, потому что оставшиеся данные все еще существуют во входном буфере файла, и отправитель данных все еще ожидает сообщения обратной связи для отправленных данных. Режим работы ET сообщает о событиях только тогда, когда событие происходит в отслеживаемом дескрипторе файла. Таким образом, на шаге 5 вызывающая сторона может отказаться от ожидания оставшихся данных в буфере ввода файла.
При работе с моделью ET epoll, когда генерируется событие EPOLLIN, При чтении данных необходимо учитывать, что когда размер, возвращаемый recv(), равен запрошенному размеру, весьма вероятно, что в буфере все еще есть данные, которые не были прочитаны, что означает, что событие произошло. не было обработано, поэтому это нужно сделать снова.Читать:
while(rs){
buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
if(buflen < 0){
// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读
// 在这里就当作是该次事件已处理处.
if(errno == EAGAIN){
break;
}
else{
return;
}
}
else if(buflen == 0){
// 这里表示对端的socket已正常关闭.
}
if(buflen == sizeof(buf){
rs = 1; // 需要再次读取
}
else{
rs = 0;
}
}
Значение EAGAIN в Linux
При разработке в среде Linux часто встречается множество ошибок (set errno), из которых EAGAIN является одной из наиболее распространенных ошибок (например, используется в неблокирующих операциях). Буквально, приглашение попробовать еще раз. Эта ошибка часто возникает, когда приложение выполняет некоторые неблокирующие операции (с файлами или сокетами).
Например, открытие файла/сокета/FIFO с флагом O_NONBLOCK, если вы выполняете непрерывные операции чтения и нет данных для чтения. В это время программа не будет блокироваться и ждать, пока данные будут готовы для возврата.Функция чтения вернет ошибку EAGAIN, указывающую, что у вашего приложения нет данных для чтения, повторите попытку позже. Другой пример: когда системный вызов (такой как fork) не выполняется из-за нехватки ресурсов (таких как виртуальная память), возвращается EAGAIN, чтобы предложить ему повторить вызов (возможно, в следующий раз он будет успешным).
Демонстрация трех кодов
Ниже приведен неполный код и неправильный формат, предназначенный для выражения описанного выше процесса, а некоторый код шаблона был удален.
#define IPADDRESS "127.0.0.1"
#define PORT 8787
#define MAXSIZE 1024
#define LISTENQ 5
#define FDSIZE 1000
#define EPOLLEVENTS 100
listenfd = socket_bind(IPADDRESS,PORT);
struct epoll_event events[EPOLLEVENTS];
//创建一个描述符
epollfd = epoll_create(FDSIZE);
//添加监听描述符事件
add_event(epollfd,listenfd,EPOLLIN);
//循环等待
for ( ; ; ){
//该函数返回已经准备好的描述符事件数目
ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
//处理接收到的连接
handle_events(epollfd,events,ret,listenfd,buf);
}
//事件处理函数
static void handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{
int i;
int fd;
//进行遍历;这里只要遍历已经准备好的io事件。num并不是当初epoll_create时的FDSIZE。
for (i = 0;i < num;i++)
{
fd = events[i].data.fd;
//根据描述符的类型和事件类型进行处理
if ((fd == listenfd) &&(events[i].events & EPOLLIN))
handle_accpet(epollfd,listenfd);
else if (events[i].events & EPOLLIN)
do_read(epollfd,fd,buf);
else if (events[i].events & EPOLLOUT)
do_write(epollfd,fd,buf);
}
}
//添加事件
static void add_event(int epollfd,int fd,int state){
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}
//处理接收到的连接
static void handle_accpet(int epollfd,int listenfd){
int clifd;
struct sockaddr_in cliaddr;
socklen_t cliaddrlen;
clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
if (clifd == -1)
perror("accpet error:");
else {
printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port); //添加一个客户描述符和事件
add_event(epollfd,clifd,EPOLLIN);
}
}
//读处理
static void do_read(int epollfd,int fd,char *buf){
int nread;
nread = read(fd,buf,MAXSIZE);
if (nread == -1) {
perror("read error:");
close(fd); //记住close fd
delete_event(epollfd,fd,EPOLLIN); //删除监听
}
else if (nread == 0) {
fprintf(stderr,"client close.\n");
close(fd); //记住close fd
delete_event(epollfd,fd,EPOLLIN); //删除监听
}
else {
printf("read message is : %s",buf);
//修改描述符对应的事件,由读改为写
modify_event(epollfd,fd,EPOLLOUT);
}
}
//写处理
static void do_write(int epollfd,int fd,char *buf) {
int nwrite;
nwrite = write(fd,buf,strlen(buf));
if (nwrite == -1){
perror("write error:");
close(fd); //记住close fd
delete_event(epollfd,fd,EPOLLOUT); //删除监听
}else{
modify_event(epollfd,fd,EPOLLIN);
}
memset(buf,0,MAXSIZE);
}
//删除事件
static void delete_event(int epollfd,int fd,int state) {
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}
//修改事件
static void modify_event(int epollfd,int fd,int state){
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}
//注:另外一端我就省了
Резюме четырех epoll
В select/poll ядро сканирует все отслеживаемые файловые дескрипторы только после того, как процесс вызовет определенный метод, а epoll заранее регистрирует файловый дескриптор через epoll_ctl(), как только он будет готов на основе файлового дескриптора, ядро будет использовать обратный вызов. -подобный механизм обратного вызова для быстрой активации файлового дескриптора и получения уведомлений, когда процесс вызывает epoll_wait(). (Механизм обхода файловых дескрипторов здесь удален, но механизм прослушивания обратных вызовов. В этом прелесть epoll.)
Преимущества epoll в основном заключаются в следующих аспектах:
- Количество отслеживаемых дескрипторов не ограничено.Верхний предел поддерживаемого FD — это максимальное количество открытых файлов.Это число, как правило, намного больше 2048. Например, на машине с 1 ГБ памяти оно составляет около 100 000. , Конкретный номер может быть cat Check /proc/sys/fs/file-max Вообще говоря, этот номер имеет много общего с системной памятью. Самым большим недостатком select является то, что количество fd, открытых процессом, ограничено. Этого просто недостаточно для сервера с большим количеством подключений. Хотя можно выбрать и многопроцессорное решение (так реализован Apache), хотя стоимость создания процесса в Linux относительно невелика, но все же не пренебрежимо мала, а синхронизация данных между процессами гораздо менее эффективна, чем синхронизация между потоками, так что это не идеальное решение.
Эффективность IO не снижается по мере роста количества отслеживаемых fd. В отличие от опроса select и poll, epoll реализуется через функцию обратного вызова, определяемую каждым fd. Только готовый fd будет выполнять функцию обратного вызова. Если нет большого количества холостых или мертвых соединений, эффективность epoll не будет намного выше, чем у select/poll, но при большом количестве холостых соединений вы обнаружите, что эффективность epoll намного выше, чем у select/poll.
Ссылаться на