Действительно ли в Linux нет асинхронного блочного ввода-вывода?
Рассмотрим приложение, которое связано с центральным процессором, но также имеет высокопроизводительные требования к вводу / выводу.
Я сравниваю файловый ввод / вывод Linux с Windows, и я не вижу, как epoll вообще поможет программе Linux. Ядро скажет мне, что файловый дескриптор "готов к чтению", но мне все еще нужно вызвать блокировку read(), чтобы получить мои данные, и если я хочу прочитать мегабайты, то довольно ясно, что это заблокирует.
В Windows я могу создать дескриптор файла с установленным значением OVERLAPPED, а затем использовать неблокирующий ввод-вывод, получать уведомления о завершении ввода-вывода и использовать данные из этой функции завершения. Мне не нужно тратить время на ожидание данных на уровне приложения, а это означает, что я могу точно настроить количество потоков на количество ядер и получить 100% эффективное использование процессора.
Если мне нужно эмулировать асинхронный ввод-вывод в Linux, то для этого мне нужно выделить некоторое количество потоков, и эти потоки будут тратить немного времени на выполнение задач процессора и много времени на блокировку ввода-вывода, Кроме того, в обмене сообщениями с этими потоками будут накладные расходы. Таким образом, я буду либо переподписываться, либо недоиспользовать свои ядра процессора.
Я смотрел на mmap() + madvise() (WILLNEED) как на "асинхронный ввод-вывод бедного человека", но он все еще не дошел до конца, потому что я не могу получить уведомление, когда это будет сделано - у меня есть "угадать" и, если я угадал "неправильно", я в конечном итоге заблокирую доступ к памяти, ожидая поступления данных с диска.
Похоже, что в Linux есть запуск асинхронного ввода-вывода в io_submit, и, похоже, также есть реализация POSIX aio в пользовательском пространстве, но так было некоторое время, и я не знаю никого, кто бы поручился за эти системы за критическую высокопроизводительные приложения.
Модель Windows работает примерно так:
- Выполните асинхронную операцию.
- Свяжите асинхронную операцию с конкретным портом завершения ввода / вывода.
- Дождитесь завершения операций на этом порту
- Когда ввод-вывод завершен, поток, ожидающий на порте, разблокируется и возвращает ссылку на ожидающую операцию ввода-вывода.
Шаги 1/2 обычно выполняются как одно целое. Шаги 3/4 обычно выполняются с пулом рабочих потоков, а не (обязательно) того же потока, который выдает ввод / вывод. Эта модель в некоторой степени похожа на модель, предоставляемую boost::asio, за исключением того, что boost:: asio фактически не обеспечивает асинхронный блочный (дисковый) ввод-вывод.
Разница с epoll в Linux заключается в том, что на шаге 4 еще не произошло ввода-вывода - он поднимает шаг 1 за шагом 4, который является "задом наперед", если вы точно знаете, что вам уже нужно.
Запрограммировав большое количество встроенных, настольных и серверных операционных систем, я могу сказать, что эта модель асинхронного ввода-вывода очень естественна для определенных видов программ. Это также очень высокая пропускная способность и низкие накладные расходы. Я думаю, что это один из оставшихся реальных недостатков модели ввода-вывода Linux на уровне API.
3 ответа
Реальный ответ, на который косвенно указал Питер Теох, основан на io_setup() и io_submit(). В частности, функции "aio_", указанные Питером, являются частью эмуляции уровня пользователя glibc, основанной на потоках, что не является эффективной реализацией. Реальный ответ в:
io_submit(2)
io_setup(2)
io_cancel(2)
io_destroy(2)
io_getevents(2)
Обратите внимание, что на странице руководства от 2012-08 написано, что эта реализация еще не достигла такой степени, что может заменить эмуляцию пространства пользователя glibc:
http://man7.org/linux/man-pages/man7/aio.7.html
эта реализация еще не достигла такой степени, что реализация POSIX AIO может быть полностью переопределена с помощью системных вызовов ядра.
Итак, согласно последней документации по ядру, которую я могу найти, у Linux еще нет зрелой модели асинхронного ввода-вывода, основанной на ядре. И, если я предполагаю, что документированная модель действительно является зрелой, она все еще не поддерживает частичный ввод-вывод в смысле recv() против read().
(2020) Если вы используете ядро 5.1 или выше, вы можете использовать io_uring
интерфейс для файлового ввода-вывода и получить отличную асинхронную работу.
По сравнению с существующими libaio
/KAIO интерфейс, io_uring
имеет следующие преимущества:
- Сохраняет асинхронное поведение при выполнении буферизованного ввода-вывода (а не только при выполнении прямого ввода-вывода)
- Легче использовать (особенно при использовании
liburing
вспомогательная библиотека) - При желании может работать в режиме опроса (но для включения этого режима вам потребуются более высокие привилегии)
- Меньше накладных расходов на бухгалтерию на ввод / вывод
- Снижение нагрузки на ЦП из-за меньшего количества переключений контекста пользовательского пространства / системного вызова ядра (что в наши дни имеет большое значение из-за воздействия средств защиты от Spectre / Meltdown)
- Дескрипторы файлов и буферы могут быть предварительно зарегистрированы для экономии времени сопоставления / отмены сопоставления.
- Быстрее (можно достичь более высокой совокупной пропускной способности, операции ввода-вывода имеют меньшую задержку)
- "Связанный режим", который можно использовать для выражения зависимостей между группами операций ввода-вывода (ядро>=5.3)
- Быстро улучшенная поддержка ввода-вывода на основе сокетов (
recvmsg()
/sendmsg()
поддерживаются начиная с>=5.3, см. сообщения, в которых упоминается слово support, в истории git io_uring.c) - Не блокируется каждый раз, когда звезды не совсем выровнены
По сравнению с POSIX AIO от glibc, io_uring
имеет следующие преимущества:
- Намного быстрее и эффективнее (меньшие накладные расходы, указанные выше, применимы и здесь)
- Интерфейс поддерживается ядром и НЕ использует пул потоков пользовательского пространства
- Нулевое копирование между пользовательским пространством и ядром, даже при выполнении буферизованного ввода-вывода
- POSIX AIO от Glibc не может иметь более одного ввода-вывода для одного файлового дескриптора, тогда как
io_uring
конечно может!
Документ "Эффективный ввод-вывод с io_uring" периодически обновляется и содержит гораздо более подробные сведения оio_uring
Преимущества и использование. В документе "Что нового в io_uring" описаны новые функции, добавленные вio_uring
с момента его создания, а в статье "Быстрый рост io_uring" LWN описывается, какие функции были доступны в каждом из ядер 5.1–5,5, с предварительным обзором того, что будет в версии 5.6 (на момент написания статьи). Также есть видео-презентация ( слайды) "Ускорение ввода-вывода с помощью io_uring" с конца 2019 г.io_uring
автор Йенс Аксбо.
Re "поддерживает частичный ввод / вывод в смысле recv()
против read()
": в ядро 5.3 добавлен патч, который автоматически повторяет попыткуio_uring
короткие чтения и дальнейшая фиксация вошли в ядро 5.4, которое настраивает поведение, чтобы автоматически заботиться только о коротких чтениях при работе с "обычными" файлами по запросам, которые не установилиREQ_F_NOWAIT
флаг (похоже, вы можете запроситьREQ_F_NOWAIT
через IOCB_NOWAIT
или открыв файл с помощью O_NONBLOCK
). Таким образом вы можете получитьrecv()
style- "короткое" поведение ввода-вывода от io_uring
слишком.
Программное обеспечение с использованием io_uring
Хотя интерфейс все еще новый (его первое воплощение появилось в мае 2019 года), некоторые программы с открытым исходным кодом используют io_uring
"в дикой природе":
- fio (который также является автором Jens Axboe) имеет бэкэнд io_uring ioengine (на самом деле он был представлен еще в fio-3.13 с февраля 2019 года!). В презентации " Повышение производительности хранилища с использованием нового интерфейса ввода-вывода ядра Linux " ( слайды) двух инженеров Intel говорится, что они смогли получить вдвое больше операций ввода-вывода в секунду для одной рабочей нагрузки и менее половины средней задержки при глубине очереди 1 на другая рабочая нагрузка при сравнении
io_uring
двигатель кlibaio
двигатель на устройстве Optane. - В проекте SPDK добавлена поддержка использования io_uring (!) Для доступа к блочным устройствам в версии v19.04 (но, очевидно, это не та серверная часть, которую вы обычно используете для других целей, кроме тестирования производительности). Совсем недавно они, кажется, также добавили поддержку его использования с сокетами в версии 20.04...
- В декабре 2019 года Ceph реализовал бэкэнд io_uring, который был частью его выпуска 15.1.0. Автор коммита опубликовал комментарий на github, показывающий, что какой- то бэкэнд io_uring имеет некоторые преимущества и недостатки по сравнению с бэкэндом libaio (с точки зрения IOPS, пропускной способности и задержки) в зависимости от рабочей нагрузки.
- RocksDB совершил
io_uring
backend для MultiRead в декабре 2019 года и был частью его выпуска 6.7.3. Йенс заявляетio_uring
помогло резко сократить задержку. - libev выпустила 4.31 с начальным
io_uring
backend в декабре 2019 года, но автор libev ожидает ядра версии 5.6+, прежде чем улучшать его ( примечания автора libev звучат так, будто все проблемы / проблемы с ядром будут решены в версии 5.7) - QEMU совершил бэкэнд io_uring в январе 2020 года и был частью выпуска QEMU 5.0. В PDF-презентации " io_uring в QEMU: высокопроизводительный дисковый ввод-вывод для Linux " Юлия Суворова показывает
io_uring
бэкэнд превосходитthreads
а такжеaio
бэкенды на одной рабочей нагрузке из случайных блоков размером 16К. - Samba объединила
io_uring
Бэкэнд VFS в феврале 2020 года (и он был частью выпуска Samba 4.12). В "Linux io_uring VFS backend".В ветке списка рассылки Samba Стефан Метцмахер (автор коммита) говорит, чтоio_uring
модуль смог увеличить пропускную способность примерно на 19% (по сравнению с некоторыми неуказанными бэкэндами) в синтетическом тесте. Вы также можете прочитать PDF-презентацию "Async VFS Future" от Стефана, чтобы узнать о мотивах этих изменений. - Экспериментальный C++ libunifex от Facebook использует его (но вам также понадобится ядро 5.6+)
- Люди ржавчины писали обертки, чтобы сделать
io_uring
более доступный для чистой ржавчины. rio - одна из библиотек, о которой немного говорили, и автор говорит, что они достигли более высокой пропускной способности по сравнению с использованием вызовов синхронизации, заключенных в потоки. Автор представил свою базу данных и библиотеку на FOSDEM 2020, в которой был раздел, в котором восхвалялись достоинстваio_uring
.
Программное обеспечение, исследующее использование io_uring
- Разработчик PostgreSQL Андрес Фройнд был одной из движущих сил
io_uring
улучшения (например, обходной путь для уменьшения конфликтов inode файловой системы). Есть презентация "Асинхронный ввод-вывод для PostgreSQL" (имейте в виду, что видео прерывается до 5-минутной отметки) ( PDF), мотивирующая необходимость изменений PostgreSQL и демонстрирующая некоторые экспериментальные результаты. Он выразил надежду на получение необязательногоio_uring
поддержка в PostgreSQL 14 и, кажется, прекрасно понимает, что работает, а что нет, даже на уровне ядра. - .NET, похоже, рассматривает / экспериментирует с использованием
io_uring
(хотя они утверждают, что натолкнулись на узкое место) - libuv активно проверяет пулреквест против добавления io_uring
- В проекте Netty учащийся добавляет
io_uring
поддержка через проект GsoC - Проект Seastar проверял исправления для интеграции
io_uring
но из-за нехватки времени его разработка приостановлена. Разработчик ScyllaDB Глаубер Коста упоминает первые результаты использования Seastario_uring
бэкэнд показал более быструю производительность (чемlibaio
backend) с очень специфической рабочей нагрузкой, но в другой статье он отмечает "[при] более внимательном рассмотрении, который ясно дал понять, что это потому, что наша реализация linux-aio была не так хороша, как могла бы" и исправление их внедрения linux-aio заставили разницу исчезнуть.
Дистрибутивы Linux поставляют достаточно новое ядро
- Последнее ядро включения HWE в Ubuntu 18.04 - 5.4. Этот дистрибутив не упаковывает
liburing
вспомогательная библиотека, но вы можете легко создать ее для себя. - Исходное ядро Ubuntu 20.04 - 5.4. Как и выше, дистрибутив не предварительно упаковывает
liburing
. - Исходное ядро Fedora 32 - 5.6. У него есть упакованный
liburing
. - Похоже, кто-то попросил Red Hat выполнить резервное копирование
io_uring
на RHEL 8 (ссылка находится за платным доступом). В примечаниях к выпуску бета-версии RHEL 8.3 упоминается "Добавлена поддержка системных вызовов, связанных с io-uring [sic]" в рамках новых функций.
С надеждой io_uring
откроет лучшую историю асинхронного файлового ввода-вывода для Linux.
Как объяснено в:
http://code.google.com/p/kernel/wiki/AIOUserGuide
и здесь:
http://www.ibm.com/developerworks/library/l-async/
Linux обеспечивает асинхронный блочный ввод-вывод на уровне ядра, API-интерфейсы выглядят следующим образом:
aio_read Request an asynchronous read operation
aio_error Check the status of an asynchronous request
aio_return Get the return status of a completed asynchronous request
aio_write Request an asynchronous operation
aio_suspend Suspend the calling process until one or more asynchronous requests have completed (or failed)
aio_cancel Cancel an asynchronous I/O request
lio_listio Initiate a list of I/O operations
И если вы спросите, кто является пользователями этих API, то это само ядро - здесь показано только небольшое подмножество:
./drivers/net/tun.c (for network tunnelling):
static ssize_t tun_chr_aio_read(struct kiocb *iocb, const struct iovec *iv,
./drivers/usb/gadget/inode.c:
ep_aio_read(struct kiocb *iocb, const struct iovec *iov,
./net/socket.c (general socket programming):
static ssize_t sock_aio_read(struct kiocb *iocb, const struct iovec *iov,
./mm/filemap.c (mmap of files):
generic_file_aio_read(struct kiocb *iocb, const struct iovec *iov,
./mm/shmem.c:
static ssize_t shmem_file_aio_read(struct kiocb *iocb,
и т.п.
На уровне пользовательского пространства также есть API io_submit () и т. Д. (Из glibc), но в следующей статье предлагается альтернатива использованию glibc:
http://www.fsl.cs.sunysb.edu/~vass/linux-aio.txt
Он напрямую реализует API для таких функций, как io_setup () как прямой системный вызов (минуя зависимости glibc), должно существовать отображение ядра с помощью той же сигнатуры "__NR_io_setup". После поиска источника ядра по адресу:
http://lxr.free-electrons.com/source/include/linux/syscalls.h#L474 (URL-адрес применим для последней версии 3.13), вы приветствуете прямую реализацию этих API io_*() в ядре:
474 asmlinkage long sys_io_setup(unsigned nr_reqs, aio_context_t __user *ctx);
475 asmlinkage long sys_io_destroy(aio_context_t ctx);
476 asmlinkage long sys_io_getevents(aio_context_t ctx_id,
481 asmlinkage long sys_io_submit(aio_context_t, long,
483 asmlinkage long sys_io_cancel(aio_context_t ctx_id, struct iocb __user *iocb,
Более поздняя версия glibc должна сделать ненужным использование "syscall()" для вызова sys_io_setup(), но без последней версии glibc вы всегда можете сделать это сами, если вы используете более позднее ядро с этими возможностями "sys_io_setup". ()".
Конечно, есть другие опции для пользовательского пространства для асинхронного ввода / вывода (например, с использованием сигналов?):
http://personal.denison.edu/~bressoud/cs375-s13/supplements/linux_altIO.pdf
или perhap:
Каково состояние асинхронного ввода / вывода POSIX (AIO)?
"io_submit" и друзья по-прежнему недоступны в glibc (см. страницы руководства io_submit), что я проверял в своей Ubuntu 14.04, но этот API зависит от linux.
Другие, такие как libuv, libev и libevent, также являются асинхронными API:
http://nikhilm.github.io/uvbook/filesystem.html
http://software.schmorp.de/pkg/libev.html
Все эти API предназначены для переносимости между BSD, Linux, MacOSX и даже Windows.
С точки зрения производительности я не видел никаких цифр, но подозреваю, что libuv может быть самым быстрым из-за его легковесности?
Для сетевого сокета ввода-вывода, когда он "готов", он не блокируется. Вот что O_NONBLOCK
и "готов" означает.
Для дискового ввода-вывода у нас есть posix aio, linux aio, sendfile и друзья.