На этой неделе я получила по почте новую книгу: Программный интерфейс Linux (The Linux Programming Interface). Моя замечательная коллега Аршия (Arshia) порекомендовала мне, и я купила ее! Она написана мейнтейнером проекта Linux man-pages Майклом Керриском (Michael Kerrisk). В ней рассказывается об программном интерфейсе Linux, начиная с ядра версии 2.6.x.
Вот обложка.
В руководстве по участию (вы можете вносить вклад в man-страницы linux!! это потрясающе) есть список отсутствующих man-страниц, которые было бы полезно внести. Там сказано:
Вы должны достаточно хорошо разбираться в теме или быть готовым уделить время (например, чтению исходного кода, написанию тестовых программ), чтобы добиться такого понимания. Написание тестовых программ очень важно: довольно много ошибок в ядре и glibc (GNU C Library (GNU библиотека)) было обнаружено при написании тестовых программ во время подготовки man-страниц.
Я подумала, что это отличное напоминание о том, как можно многому научиться, документируя что-либо и составляя небольшие тестовые программы!
Но сегодня мы поговорим о том, что я узнала из этой книги: о системных вызовах select, poll и epoll.
Глава 63: Альтернативные модели ввода-вывода
Эта книга огромна: 1400 страниц. Я начала ее с главы 63 ("Альтернативные модели ввода/вывода"), потому что уже давно хотела понять, что происходит с select
, poll
и epoll
. Когда я подробно описываю то, что узнаю, это помогает мне лучше разобраться!
Данная глава в основном о том, как мониторить множество файловых дескрипторов на предмет новых вводов/выводов. Кому нужно следить за большим количеством файловых дескрипторов одновременно? Серверам!
Например, если вы пишете веб-сервер в node.js на Linux, под капотом он фактически использует системный вызов epoll
Linux. Давайте поговорим о том, чем epoll
отличается от poll
и select
, и как он работает!
Серверы должны просматривать большое количество файловых дескрипторов
Предположим, вы веб-сервер. Каждый раз, когда вы принимаете соединение с помощью системного вызова accept (вот man-страница), то получаете новый файловый дескриптор, представляющий это соединение.
Если вы являетесь веб-сервером, у вас могут быть одновременно открыты тысячи соединений. Важно знать, когда люди посылают вам новые данные об этих подключениях, чтобы вы могли их обрабатывать и отвечать на них.
Вы можете создать цикл, который, по сути, делает следующее:
for x in open_connections:
if has_new_input(x):
process_input(x)
Проблема в том, что это может приводить к большим затратам процессорного времени. Вместо того чтобы постоянно запрашивать "есть ли обновления? а как насчет сейчас? что теперь?", мы лучше просто спросим ядро Linux "эй, вот 100 файловых дескрипторов. Скажи мне, когда один из них будет обновлен!".
Три системных вызова, которые позволяют вам попросить Linux отслеживать множество файловых дескрипторов, это poll
, epoll
и select
. Давайте начнем с poll
и select
, потому что именно с них началась эта глава.
Первый способ: select & poll
Эти два системных вызова доступны в любой Unix-системе, в то время как epoll
специфичен для Linux. Вот как они работают, в общих чертах:
Дайте им список дескрипторов файлов, о которых нужно получить информацию.
Они сообщают вам, в каких из них есть данные для чтения/записи.
Первая удивительная вещь, которую я узнала из этой главы, - это то, что poll
и select
в своей основе используют один и тот же код.
Я обратилась к определению poll
и select
в исходнике ядра Linux, чтобы подтвердить это, и оказалась права!
Они оба вызывают много одинаковых функций. В частности, в книге упоминается то, что poll
возвращает больший набор возможных результатов для таких дескрипторов файлов, как POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR
, в то время как select
просто сообщает вам "есть ввод / есть вывод / есть ошибка".
select преобразовывает из более подробных результатов poll
(например, POLLWRBAND
) в общие "вы можете написать". Вы можете посмотреть код, где это делается в Linux 4.10 здесь.
Следующее, что я узнала, это то, что poll
может работать лучше, чем select
, если у вас есть разреженный набор файловых дескрипторов.
Чтобы убедиться в этом, вы можете просто посмотреть на сигнатуры для poll и select!
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *tmo_p, const sigset_t
*sigmask)`
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
С помощью poll
вы говорите "вот файловые дескрипторы, которые я хочу отслеживать: 1, 3, 8, 19 и т.д." (это аргумент pollfd). При использовании select
сообщаете: "Я хочу отслеживать 19 файловых дескрипторов. Вот 3 битсета, которые нужно отслеживать на предмет чтения/записи/исключений". Таким образом, когда программа запускается, она перебирает от 0 до 19 файловых дескрипторов, даже если на самом деле вас интересовали только 4 из них.
В главе есть еще много специфических подробностей о том, чем отличаются poll
и select
, но это были 2 основные вещи, которые я узнала!
Почему бы нам не использовать poll и select?
Ладно, но в Linux мы сказали, что ваш сервер node.js не будет использовать poll
или select
, он будет использовать epoll. Почему?
Из книги:
При каждом вызове select()
или poll()
ядро должно проверять все указанные файловые дескрипторы на предмет их готовности. Когда осуществляется мониторинг большого количества файловых дескрипторов, находящихся в плотно упакованном диапазоне, время, необходимое для этой операции, намного превышает [все остальное, что они должны делать].
По сути: каждый раз, когда вы вызываете select
или poll
, ядру необходимо заново проверить, доступны ли ваши файловые дескрипторы для записи. Ядро не помнит список файловых дескрипторов, которые оно должно отслеживать!
Ввод-вывод управляемый сигналом (используют ли его люди?)
В книге описаны два способа попросить ядро запомнить список файловых дескрипторов, которые оно должно отслеживать: ввод-вывод с управлением по сигналу и epoll
. Управляемый сигналом ввод-вывод - это способ заставить ядро посылать вам сигнал, когда файловый дескриптор обновляется, вызывая fcntl
. Я никогда не слышала, чтобы кто-то использовал этот способ, и книга говорит о том, что epoll
будет лучше. Поэтому мы пока проигнорируем его и поговорим об epoll
.
срабатывание по уровню (level-triggered) в сравнении со срабатыванием по фронту (edge-triggered)
Прежде чем заговорить об epoll, необходимо обсудить "level-triggered" и "edge-triggered" уведомления о файловых дескрипторах. Я никогда раньше не слышала этой терминологии (думаю, она пришла из электротехники?). В принципе, есть 2 способа получения уведомлений
получать список всех интересующих вас файловых дескрипторов, которые доступны для чтения ("level-triggered")
получать уведомления каждый раз, когда дескриптор файла становится доступным для чтения ("edge-triggered").
Что такое epoll
?
Итак, мы готовы поговорить об epoll. Это очень интересно, потому что я часто встречала epoll_wait
при отслеживании (stracing) программ и часто ощущала какую-то неопределенность относительно того, что именно он означает.
Группа системных вызовов epoll (epoll_create
, epoll_ctl
, epoll_wait
) предоставляет ядру Linux список файловых дескрипторов для отслеживания и запрашивает обновления об активности на них.
Ниже описаны шаги для использования epoll
:
Вызовите
epoll_create
, чтобы сообщить ядру, что вы собираетесь использоватьepoll
. Оно вернет вам идентификатор.Вызовите
epoll_ctl
, чтобы сообщить ядру о файловых дескрипторах, обновления которых вас интересуют. Интересно, что вы можете передать ему множество различных типов файловых дескрипторов (конвейеры, FIFO (First In First Out — первый вошел - первый вышел), сокеты, очереди сообщений POSIX (portable operating system interface for Unix — переносимый интерфейс операционных систем Unix), экземпляры inotify, устройства и многое другое), но не обычные файлы. Я думаю, что это вполне логично - конвейеры и сокеты обладают довольно простым API (один процесс записал в конвейер, а другой - считал), поэтому вполне очевидно будет сказать: "В этом конвейере есть новые данные для чтения". Но с файлами все не так просто. Вы можете выполнить запись в середину файла. Так что на самом деле не имеет смысла говорить: “в этом файле есть новые данные, доступные для чтения”.Вызовите
epoll_wait
, чтобы дождаться обновления списка интересующих вас файлов.
Производительность: select и poll в сравнении с epoll
В книге есть таблица, в которой сравнивается производительность для 100 000 операций мониторинга:
# operations | poll | select | epoll
10 | 0.61 | 0.73 | 0.41
100 | 2.9 | 3.0 | 0.42
1000 | 35 | 35 | 0.53
10000 | 990 | 930 | 0.66
Применение epoll
значительно ускоряет работу, если у вас больше 10 или около того файловых дескрипторов для мониторинга.
Кто использует epoll?
Иногда я замечаю epoll_wait
, когда отслеживаю (strace) программу. Почему? Есть вроде бы очевидный, но бесполезный ответ "это мониторинг некоторых файловых дескрипторов", но мы можем сделать лучше!
Во-первых, если вы используете зеленые потоки (green threads) или цикл событий, вы, скорее всего, используете epoll
для выполнения всех сетевых и конвейерных операций ввода-вывода.
Например, вот программа на golang, которая использует epoll
в Linux.
package main
import "net/http"
import "io/ioutil"
func main() {
resp, err := http.Get("http://example.com/")
if err != nil {
// handle error
}
defer resp.Body.Close()
_, err = ioutil.ReadAll(resp.Body)
}
Здесь вы видите, как рантайм golang использует epoll
для поиска DNS:
16016 connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.1.1")}, 16 <unfinished ...>
16020 socket(PF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP
16016 epoll_create1(EPOLL_CLOEXEC <unfinished ...>
16016 epoll_ctl(5, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=334042824, u64=139818699396808}}
16020 connect(4, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.1.1")}, 16 <unfinished ...>
16020 epoll_ctl(5, EPOLL_CTL_ADD, 4, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=334042632, u64=139818699396616}}
В основном это делается для подключения 2 сокетов (в файловых дескрипторах 3 и 4) для выполнения DNS-запросов (к 127.0.1.1: 53), а затем с помощью epoll_ctl
попросить epoll
предоставить нам обновления о них.
Затем он делает 2 DNS-запроса для example.com (почему 2? nelhage предполагает, что один из них запрашивает запись A, а другой - запись AAAA!), и использует epoll_wait
для ожидания ответов
# these are DNS queries for example.com!
16016 write(3, "\3048\1\0\0\1\0\0\0\0\0\0\7example\3com\0\0\34\0\1", 29
16020 write(4, ";\251\1\0\0\1\0\0\0\0\0\0\7example\3com\0\0\1\0\1", 29
# here it tries to read a response but I guess there's no response
# available yet
16016 read(3, <unfinished ...>
16020 read(4, <unfinished ...>
16016 <... read resumed> 0xc8200f4000, 512) = -1 EAGAIN (Resource temporarily unavailable)
16020 <... read resumed> 0xc8200f6000, 512) = -1 EAGAIN (Resource temporarily unavailable)
# then it uses epoll to wait for responses
16016 epoll_wait(5, <unfinished ...>
16020 epoll_wait(5, <unfinished ...>
Итак, одна из причин, по которой ваша программа может использовать epoll
- "она написана на Go / node.js / Python с gevent и работает в сети".
Какие библиотеки применяются в go/node.js/Python для использования epoll
?
node.js использует libuv (которая была написана для проекта node.js)
сетевая библиотека gevent в Python использует libev/libevent
golang использует какой-то кастомный код, потому что это Go. Похоже, что это может быть реализация сетевого поллинга с помощью epoll в рантайме golang - здесь всего около 100 строк, что интересно. Общий интерфейс netpoll можно посмотреть здесь - на BSD он реализован с помощью kqueue.
Веб-серверы также имплементируют epoll
- например, вот код epoll в nginx.
Другие материалы по select и epoll
Мне понравились эти 3 поста Марека (Marek):
В частности, здесь говорится о том, что поддержка многопоточных программ в epoll изначально была не очень хорошей, хотя в Linux 4.5 были некоторые улучшения.
И это:
Ну все, хватит
Я узнала довольно много нового о select
и epoll
, написав этот пост! Сейчас у нас около 1800 слов, так что я думаю, этого достаточно. С нетерпением жду возможности прочитать больше из этой книги по программному интерфейсу Linux и сделать новые открытия.
Возможно, в этом посте есть какие-то неправильные вещи, дайте мне знать, что именно.
Одна мелочь, которая мне нравится в работе - то, что я могу тратить деньги на книги по программированию. Это здорово, потому мне приходится покупать и читать книги, которые учат меня тому, чему я, возможно, не научилась бы иначе. А купить книгу намного дешевле, чем поехать на конференцию!
Приглашаем всех желающих на открытое занятие «Паттерн Entity-Component-System в играх на C». На этом занятии мы познакомимся с часто применяемым в игровых приложениях архитектурным шаблоном Entity/Component/System и рассмотрим его реализацию на языке C на примере опенсорсной библиотеки flecs. Также мы изучим код несложной игры, использующей flecs на практике. Регистрация — по ссылке.
Также приходите на урок «Инструментарий UNIX-разработчика: исправляем утечку памяти в curl», который состоится уже сегодня в 20:00. Рассмотрим важные элементы инструментария разработчика под UNIX-подобными ОС и с их помощью продиагностируем и исправим утечку памяти в библиотеке для работы с HTTP/2 libcurl. Регистрация на урок