Действительно ли в Linux нет асинхронного блочного ввода-вывода?

Рассмотрим приложение, которое связано с центральным процессором, но также имеет высокопроизводительные требования к вводу / выводу.

Я сравниваю файловый ввод / вывод Linux с Windows, и я не вижу, как epoll вообще поможет программе Linux. Ядро скажет мне, что файловый дескриптор "готов к чтению", но мне все еще нужно вызвать блокировку read(), чтобы получить мои данные, и если я хочу прочитать мегабайты, то довольно ясно, что это заблокирует.

В Windows я могу создать дескриптор файла с установленным значением OVERLAPPED, а затем использовать неблокирующий ввод-вывод, получать уведомления о завершении ввода-вывода и использовать данные из этой функции завершения. Мне не нужно тратить время на ожидание данных на уровне приложения, а это означает, что я могу точно настроить количество потоков на количество ядер и получить 100% эффективное использование процессора.

Если мне нужно эмулировать асинхронный ввод-вывод в Linux, то для этого мне нужно выделить некоторое количество потоков, и эти потоки будут тратить немного времени на выполнение задач процессора и много времени на блокировку ввода-вывода, Кроме того, в обмене сообщениями с этими потоками будут накладные расходы. Таким образом, я буду либо переподписываться, либо недоиспользовать свои ядра процессора.

Я смотрел на mmap() + madvise() (WILLNEED) как на "асинхронный ввод-вывод бедного человека", но он все еще не дошел до конца, потому что я не могу получить уведомление, когда это будет сделано - у меня есть "угадать" и, если я угадал "неправильно", я в конечном итоге заблокирую доступ к памяти, ожидая поступления данных с диска.

Похоже, что в Linux есть запуск асинхронного ввода-вывода в io_submit, и, похоже, также есть реализация POSIX aio в пользовательском пространстве, но так было некоторое время, и я не знаю никого, кто бы поручился за эти системы за критическую высокопроизводительные приложения.

Модель Windows работает примерно так:

  1. Выполните асинхронную операцию.
  2. Свяжите асинхронную операцию с конкретным портом завершения ввода / вывода.
  3. Дождитесь завершения операций на этом порту
  4. Когда ввод-вывод завершен, поток, ожидающий на порте, разблокируется и возвращает ссылку на ожидающую операцию ввода-вывода.

Шаги 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 имеет следующие преимущества:

Документ "Эффективный ввод-вывод с 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 "в дикой природе":

Программное обеспечение, исследующее использование io_uring

Дистрибутивы 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

http://libevent.org/

Все эти API предназначены для переносимости между BSD, Linux, MacOSX и даже Windows.

С точки зрения производительности я не видел никаких цифр, но подозреваю, что libuv может быть самым быстрым из-за его легковесности?

https://ghc.haskell.org/trac/ghc/ticket/8400

Для сетевого сокета ввода-вывода, когда он "готов", он не блокируется. Вот что O_NONBLOCK и "готов" означает.

Для дискового ввода-вывода у нас есть posix aio, linux aio, sendfile и друзья.

Другие вопросы по тегам