Эта статья начинается с железа и разбирается в процессе передачи данных от рута.Это неплохо, так что держите.
Занимаясь разработкой на стороне сервера, необходимо иметь представление о сетевом программировании. epoll необходим как обязательная технология для высокопроизводительных сетевых серверов под Linux.Эта технология мультиплексирования используется nginx, Redis, Skynet и большинством игровых серверов.
epoll важен, но в чем разница между epoll и select? Что делает epoll эффективным?
Хотя в Интернете много статей, объясняющих epoll, они либо слишком просты, либо цепляются за анализ исходного кода, и лишь немногие из них легко понять. Автор решил написать эту статью, чтобы читатели, которым не хватает профессиональных знаний, могли понять принцип epoll.
Основная идея статьи — дать читателям четкое представление о том, почему epoll работает хорошо.
Эта статья начнется с процесса получения данных от сетевой карты и последовательно соединит знания о прерываниях ЦП и планировании процессов операционной системы; затем проанализирует процесс эволюции блокировки полученных данных, шаг за шагом выберет epoll; и, наконец, исследует детали реализации epoll.
1. Начните с получения данных с сетевой карты
Ниже приведена типичная схема структуры компьютера.Компьютер состоит из процессора, памяти (памяти) и сетевого интерфейса.Первым шагом для понимания природы epoll является рассмотрение того, как компьютер получает сетевые данные с точки зрения аппаратного обеспечения.
На следующем рисунке показан процесс приема данных сетевой картой.
- На этапе ① сетевая карта получает данные от сетевого кабеля;
- После передачи аппаратной схемы на этапе ②;
- Заключительный ③ этап записывает данные по адресу в памяти.
Этот процесс включает в себя знания, связанные с аппаратным обеспечением, такие как передача DMA и выбор канала ввода-вывода, но нам нужно знать только следующее:Сетевая карта запишет полученные данные в память.
Процесс получения данных с сетевой карты
При аппаратной передаче данные, полученные сетевой картой, сохраняются в памяти, и операционная система может их прочитать.
2. Как узнать, что данные получены?
Второй шаг в понимании природы epoll — посмотреть на прием данных с точки зрения ЦП. Чтобы понять эту проблему, мы должны сначала понять понятие - прерывание.
Когда компьютер выполняет программу, необходим приоритет. Например, когда компьютер получает сигнал о сбое питания, он должен немедленно сохранить данные, а программа, сохраняющая данные, имеет более высокий приоритет (конденсатор может сохранять небольшое количество энергии для работы ЦП в течение короткого периода времени). время).
Вообще говоря, сигнал, генерируемый аппаратным обеспечением, требует немедленного ответа от ЦП, иначе данные могут быть потеряны, поэтому он имеет высокий приоритет. ЦП должен прервать выполняемую программу, чтобы ответить; когда ЦП завершит ответ аппаратному обеспечению, он повторно выполнит пользовательскую программу. Процесс прерывания показан на рисунке ниже, он аналогичен вызову функции, за исключением того, что вызов функции находится в заданной позиции, а позиция прерывания определяется «сигналом».
прерывание обычного вызова
Возьмем клавиатуру в качестве примера: когда пользователь нажимает клавишу на клавиатуре, клавиатура посылает высокий уровень на вывод прерывания ЦП, и ЦП может перехватить этот сигнал и затем выполнить программу прерывания клавиатуры. На приведенной ниже диаграмме показано, как различное оборудование взаимодействует с ЦП посредством прерываний.
теперь можно ответить"Как я узнаю, что данные были получены?«Вот в чем проблема: когда сетевая карта записывает данные в память, сетевая карта отправляет сигнал прерывания на ЦП, и операционная система может знать, что приходят новые данные, и затем обрабатывать данные через сетевую карту программа прерывания.3. Почему блокировка процессов не занимает ресурсы процессора?
Третий шаг в понимании природы epoll — это рассмотрение приема данных с точки зрения планирования процессов операционной системы. Блокировка является ключевой частью планирования процессов, которая относится к состоянию ожидания процесса перед тем, как произойдет событие (например, получение сетевых данных).Recv, select и epoll — все методы блокировки. Давайте проанализируем, почему блокировка процесса не занимает ресурсов процессора?
Для простоты начнем разбор с обычного приема recv, сначала посмотрим на следующий код:
//创建socket
int s = socket(AF_INET, SOCK_STREAM, 0);
//绑定
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据
recv(c, ...);
//将数据打印出来
printf(...)
Это самый простой код сетевого программирования.Сначала создайте новый объект сокета, вызовите bind, последовательно прослушайте и примите и, наконец, вызовите recv для получения данных. recv — это метод блокировки.Когда программа запускается в recv, она будет ждать, пока не получит данные, прежде чем продолжить.
Итак, каков принцип блокировки?
рабочая очередь
Для поддержки многозадачности операционная система реализует функцию планирования процессов и разделяет процесс на несколько состояний, таких как «выполнение» и «ожидание». Состояние выполнения — это состояние, в котором процесс получил право использовать ЦП и выполняет код, состояние ожидания — состояние блокировки.Например, когда вышеприведенная программа запускается в recv, программа переходит из состояния выполнения в состояние ожидания и вернуться в рабочее состояние после получения данных. Операционная система будет своевременно выполнять процессы каждого рабочего состояния, и из-за скорости создается впечатление, что она выполняет несколько задач одновременно.
На приведенном ниже рисунке на компьютере запущены три процесса A, B и C. Процесс A выполняет описанную выше базовую сетевую программу.В начале все эти три процесса упоминаются в рабочей очереди операционной системы, выполняются и будет с разделением времени.
очередь ожидания
Когда процесс A выполняет оператор для создания сокета, операционная система создает объект сокета, управляемый файловой системой (как показано на рисунке ниже). Этот объект сокета содержит элементы, такие как буфер отправки, буфер приема и очередь ожидания. Очередь ожидания — очень важная структура, она указывает на все процессы, которым необходимо дождаться события сокета.
Когда программа выполняется для recv, операционная система перемещает процесс A из рабочей очереди в очередь ожидания сокета (как показано на рисунке ниже). Поскольку в рабочей очереди остались только процессы B и C, согласно планированию процессов, CPU будет выполнять программы этих двух процессов по очереди, а не выполнять программу процесса A. Таким образом, процесс A заблокирован, не будет выполнять код и не будет занимать ресурсы процессора. Примечание. Добавление очереди ожидания операционной системой просто добавляет ссылку на этот «ожидающий» процесс, чтобы получить объект процесса и разбудить его при получении данных, вместо того, чтобы напрямую управлять процессом. Для удобства пояснения на приведенном выше рисунке процесс непосредственно подвешивается под очередью ожидания.процесс пробуждения
Когда сокет получает данные, операционная система помещает процесс из очереди ожидания сокета обратно в рабочую очередь, и процесс становится запущенным и продолжает выполнять код. В то же время, поскольку в приемном буфере сокета уже есть данные, recv может вернуть полученные данные.
В-четвертых, весь процесс получения ядром сетевых данных
Этот шаг, благодаря знанию сетевой карты, прерываний и планирования процессов, описывает весь процесс получения ядром данных при блокировке recv.
Как показано на рисунке ниже, когда процесс блокируется recv, компьютер получает данные, отправленные пиром (шаг 1), данные отправляются в память через сетевую карту (шаг 2), а затем сетевая карта информирует ЦП о поступлении данных посредством сигнала прерывания, и ЦП выполняет программу прерывания (шаг ③).
Программа прерывания здесь имеет две основные функции: сначала записать сетевые данные в приемный буфер соответствующего сокета (шаг ④), затем разбудить процесс А (шаг ⑤) и снова поставить процесс А в рабочую очередь.
Весь процесс получения ядром данных
Процесс пробуждения процесса показан на следующем рисунке:
процесс пробуждения
Выше показан весь процесс получения данных ядром Здесь мы можем подумать над двумя вопросами:
- Во-первых, как операционная система узнает, какому сокету соответствуют сетевые данные?
- Во-вторых, как отслеживать данные нескольких сокетов одновременно? Первый вопрос: поскольку сокет соответствует номеру порта, а пакет сетевых данных содержит информацию об ip и порте, ядро может найти соответствующий сокет по номеру порта. Конечно, чтобы повысить скорость обработки, операционная система будет поддерживать индексную структуру номера порта для сокета для быстрого чтения.
Второй вопрос является высшим приоритетом мультиплексирования и находится в центре внимания второй половины этой статьи.
5. Простой способ одновременного мониторинга нескольких сокетов
Серверу нужно управлять несколькими клиентскими подключениями, а recv может мониторить только один сокет, из-за этого противоречия люди стали искать способы мониторить несколько сокетов.Цель epoll — эффективно отслеживать несколько сокетов..
С точки зрения исторического развития, сначала должен быть менее эффективный метод, и люди будут его улучшать, точно так же, как select — это epoll.
Чтобы сначала понять неэффективный выбор, мы можем лучше понять суть epoll.
Если список сокетов может быть передан заранее, если сокеты в списке не имеют данных, процесс будет приостановлен до тех пор, пока сокет не получит данные, и процесс будет разбужен. Этот метод очень прост и также является дизайнерской идеей select.
Для простоты понимания давайте сначала рассмотрим использование select. В следующем коде сначала подготовьте массив fds, пусть fds хранит все сокеты, которые необходимо отслеживать. Затем вызовите select.Если все сокеты в fds не имеют данных, select будет блокироваться до тех пор, пока сокет не получит данные, select не вернется и не разбудит процесс. Пользователи могут просматривать fds, определять, какой сокет получает данные через FD_ISSET, а затем обрабатывать их.
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...);
int fds[] = 存放需要监听的socket;
while(1){
int n = select(..., fds, ...)
for(int i=0; i < fds.count; i++){
if(FD_ISSET(fds[i], ...)){
//fds[i]的数据处理
}
}}
Процесс выбора
Идея реализации select очень проста: если программа одновременно отслеживает три сокета sock1, sock2 и sock3, как показано на рисунке ниже, после вызова select операционная система добавляет процесс A в ожидающие очереди этих сокетов. три розетки соответственно.
Операционная система добавляет процесс A в очереди ожидания трех сокетов соответственно.
Когда какой-либо сокет получает данные, процедура прерывания пробуждает процесс. На следующем рисунке показан поток обработки данных, полученных sock2:
Примечание. Обратные вызовы прерываний recv и select могут иметь разное содержимое.
sock2 получает данные, программа прерывания пробуждает процесс A
Так называемый процесс пробуждения заключается в удалении процесса из всех ожидающих очередей и добавлении его в рабочую очередь, как показано на следующем рисунке:
Удалить процесс A из всех ожидающих очередей и добавить его в рабочую очередь
После этих шагов, когда процесс А просыпается, он знает, что по крайней мере один сокет получил данные. Программе нужно только один раз просмотреть список сокетов, чтобы получить готовый сокет.
Этот простой подход хорошо работает и имеет соответствующие реализации почти во всех операционных системах.
Но простые методы часто имеют недостатки, в основном:
Во-первых, каждый вызов select должен добавлять процесс в очереди ожидания всех сокетов мониторинга, а каждое пробуждение должно удаляться из каждой очереди. Здесь задействованы два обхода, и каждый раз весь список fds передается ядру, что имеет определенные накладные расходы. Именно из-за высоких накладных расходов на операцию обхода для эффективности указано максимальное число контролируемых операций select: по умолчанию можно отслеживать только 1024 сокета.
Во-вторых, после пробуждения процесса программа не знает, какие сокеты получают данные, и ей нужно пройти один раз.
Так,Есть ли способ уменьшить обход? Есть ли способ сохранить готовый сокет? Эти две проблемы решает технология epoll.
Дополнительное примечание: в этом разделе объясняется только один случай выбора. Когда программа вызывает select, ядро сначала проходит через сокет.Если существует более одного сокета, принимающего буфер с данными, тогда select вернется напрямую без блокировки. Это одна из причин, по которой возвращаемое значение select может быть больше 1. Если нет сокета с данными, процесс заблокируется.
Шесть, идеи дизайна epoll
epoll был придуман спустя много лет после появления select, это расширенная версия select и poll (poll и select в основном одинаковы, с некоторыми улучшениями). epoll повышает эффективность за счет:
Мера 1: Разделение функций
Одной из причин неэффективности select является объединение шагов «обслуживания очереди ожидания» и «блокировки процесса» в один. Как показано на рисунке ниже, эти два шага требуются каждый раз при вызове select.Однако в большинстве сценариев приложений отслеживаемые сокеты относительно фиксированы и не требуют изменения каждый раз. epoll разделяет эти две операции: сначала epoll_ctl используется для обслуживания очереди ожидания, а затем вызывается epoll_wait для блокировки процесса. Очевидно, эффективность можно повысить.
По сравнению с select, epoll разделяет функцииЧтобы облегчить понимание последующего контента, давайте сначала разберемся с использованием epoll. В следующем коде сначала используйте epoll_create для создания объекта epoll epfd, затем используйте epoll_ctl для добавления отслеживаемого сокета в epfd и, наконец, вызовите epoll_wait для ожидания данных:
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...)
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
while(1){
int n = epoll_wait(...)
for(接收到数据的socket){
//处理
}
}
Разделение функций позволяет оптимизировать epoll.
###Мера два: готовый список
Другая причина неэффективности select заключается в том, что программа не знает, какие сокеты получают данные, и может проходить их только один за другим. Обхода можно избежать, если ядро поддерживает «готовый список», ссылающийся на сокет, который получил данные. Как показано на рисунке ниже, компьютер имеет три сокета, а на sock2 и sock3, принимающие данные, ссылается готовый список rdlist. Когда процесс пробуждается, пока получено содержимое rdlist, он может знать, какие сокеты получают данные.
Диаграмма готового списка
7. Принцип и рабочий процесс epoll
В этом разделе объясняется принцип и рабочий процесс epoll с примерами и диаграммами.
Создать объект epoll
Как показано на рисунке ниже, когда процесс вызывает метод epoll_create, ядро создает объект eventpoll (то есть объект, представленный epfd в программе). Объект eventpoll также является членом файловой системы и, как и сокет, имеет очередь ожидания.
Ядро создает объект eventpoll
Необходимо создать объект eventpoll, представляющий epoll, потому что ядру необходимо поддерживать такие данные, как «список готовности», а «список готовности» можно использовать как член eventpoll.
Список наблюдения за техническим обслуживанием
После создания объекта epoll вы можете использовать epoll_ctl для добавления или удаления сокета для прослушивания. Возьмем в качестве примера добавление сокета, как показано на рисунке ниже, если мониторинг sock1, sock2 и sock3 добавлен через epoll_ctl, ядро добавит eventpoll в очередь ожидания этих трех сокетов.
Добавьте сокет для прослушивания
Когда сокет получает данные, программа прерывания будет работать с объектом опроса событий, а не непосредственно с процессом.
Получить данные
Когда сокет получает данные, процедура прерывания добавит ссылку на сокет в «готовый список» опроса событий. На следующем рисунке показано, что после того, как sock2 и sock3 получили данные, программа прерывания заставляет rdlist ссылаться на эти два сокета.
Добавить ссылку в список готовых
Объект eventpoll эквивалентен посреднику между сокетом и процессом.Прием данных сокета напрямую не влияет на процесс, но изменяет состояние процесса путем изменения списка готовности eventpoll.
Когда программа выполняется до epoll_wait, если rdlist уже ссылается на сокет, то epoll_wait возвращается напрямую.Если rdlist пуст, процесс блокируется.
Блокировать и пробуждать процессы
Предположим, что на компьютере запущены процессы A и B, и в какой-то момент процесс A переходит к оператору epoll_wait. Как показано на рисунке ниже, ядро поместит процесс А в очередь ожидания опроса событий, блокируя процесс.
epoll_wait блокирует процесс
Когда сокет получает данные, программа прерывания модифицирует rdlist, с одной стороны, и пробуждает процесс в очереди ожидания eventpoll, с другой стороны, и процесс A снова входит в состояние выполнения (как показано на рисунке ниже). Также из-за существования rdlist процесс A может знать, какие сокеты изменились.
epoll запускает процесс
Восемь, детали реализации epoll
Пока что я полагаю, что у читателей есть определенное понимание сути epoll. Но нам также нужно знать, как выглядит структура данных eventpoll?
Кроме того, какую структуру данных должна использовать очередь готовности? Какую структуру данных должен использовать eventpoll для управления добавлением или удалением сокетов через epoll_ctl?
Как показано на рисунке ниже, eventpoll содержит такие элементы, как lock, mtx, wq (очередь ожидания) и rdlist, из которых нас интересуют rdlist и rbr.
Схематическая диаграмма принципа epoll, источник изображения: «Углубленное понимание Nginx: разработка модулей и анализ архитектуры (второе издание)», Тао Хуэй.
Структура данных готового списка
Готовый список относится к готовым сокетам, поэтому он должен иметь возможность быстро вставлять данные.
Программа может вызвать epoll_ctl в любое время, чтобы добавить сокеты мониторинга или удалить их в любое время. При удалении, если сокет уже есть в списке готовых, его тоже следует удалить. Таким образом, готовый список должен представлять собой структуру данных, которую можно быстро вставлять и удалять.
Двусвязный список является такой структурой данных, epoll использует двусвязный список для реализации очереди готовности (соответствует rdllist на рисунке выше).
структура индекса
Поскольку epoll разделяет «очередь мониторинга обслуживания» и «блокировку процессов», это также означает, что должна быть структура данных для хранения отслеживаемых сокетов, по крайней мере, чтобы ее можно было легко добавлять и удалять, а также легко искать, чтобы избежать повторных добавлений. Красно-черное дерево — это самобалансирующееся бинарное дерево поиска.Временная сложность поиска, вставки и удаления составляет O(log(N)), что является более эффективным.epoll использует красно-черное дерево в качестве индексной структуры (соответствующей к rbr на рисунке выше).
Примечание. Поскольку операционной системе необходимо учитывать несколько функций и сохранять больше данных, rdlist не ссылается напрямую на сокеты, а косвенно ссылается на эпитемы, а узлы красно-черного дерева также являются объектами эпитемов. Точно так же файловая система не ссылается напрямую на сокет. Для простоты понимания в этой статье опущены некоторые косвенные конструкции.
9. Резюме
Основанный на выборке и опросе, epoll вводит eventpoll в качестве промежуточного уровня, использует расширенные структуры данных и представляет собой эффективную технологию мультиплексирования. Вот также простое сравнение select, poll и epoll в табличной форме, чтобы закончить эту статью. Надеюсь, читатели смогут чему-нибудь научиться.