Введение

Форма обработки ввода/вывода

В информатике асинхронный ввод/вывод (также непоследовательный ввод/вывод) — это форма обработки ввода/вывода, которая позволяет другим процессам продолжать работу до завершения операции ввода/вывода. В Windows API для асинхронного ввода/вывода используется термин «перекрывающийся ввод/вывод». Операции ввода и вывода (I/O) на компьютере могут быть значительно медленнее обработки данных. Устройство ввода/вывода может включать механические компоненты, требующие физического перемещения, например, жесткий диск, ищущий дорожку для чтения или записи; это часто на порядки медленнее, чем переключение электрического тока. Например, во время дисковой операции, занимающей десять миллисекунд, процессор с тактовой частотой один гигагерц мог бы выполнить десять миллионов циклов обработки инструкций. Простой подход к I/O заключается в запуске доступа и ожидании его завершения. Однако такой подход, называемый синхронным вводом/выводом или блокирующим вводом/выводом, блокирует выполнение программы на время обмена данными, оставляя системные ресурсы неиспользуемыми. Если программа выполняет множество операций ввода/вывода (например, программа, в основном или в значительной степени зависящая от пользовательского ввода), процессор может большую часть времени простаивать в ожидании завершения этих операций. В качестве альтернативы можно запустить обмен данными и затем выполнять обработку, не требующую завершения ввода/вывода. Этот подход называется асинхронным вводом/выводом. Любая задача, зависящая от завершения ввода/вывода (включая использование входных значений и критические операции, подтверждающие завершение операции записи), все равно должна ждать завершения операции ввода/вывода и, следовательно, остается заблокированной, но другая обработка, не зависящая от операции ввода/вывода, может продолжаться. Существует множество функций операционной системы для реализации асинхронного ввода/вывода на различных уровнях. Фактически, одна из основных функций большинства операционных систем, за исключением самых простых, — выполнение хотя бы базового асинхронного ввода/вывода, хотя это может быть не очевидно для пользователя или программиста. В простейшем программном решении состояние аппаратного устройства периодически опрашивается для определения его готовности к следующей операции. (Например, операционная система CP/M была построена таким образом. Её семантика системных вызовов не требовала более сложной структуры ввода/вывода, хотя большинство реализаций были более сложными и, следовательно, более эффективными.) Прямой доступ к памяти (DMA) может значительно повысить эффективность системы, основанной на опросе, а аппаратные прерывания могут полностью устранить необходимость в опросе. Многозадачные операционные системы могут использовать функциональность, предоставляемую аппаратными прерываниями, скрывая сложность обработки прерываний от пользователя. Спулинг был одной из первых форм многозадачности, разработанной для использования асинхронного ввода/вывода. Наконец, многопоточность и явные асинхронные API в пользовательских процессах могут еще больше использовать асинхронный ввод/вывод, но за счет увеличения сложности программного обеспечения. Асинхронный ввод/вывод используется для повышения энергоэффективности и, в некоторых случаях, пропускной способности. Однако в некоторых случаях он может негативно влиять на задержку и пропускную способность.

Процесс

Доступен в ранних версиях Unix. В многозадачной операционной системе обработка может быть распределена между различными процессами, которые выполняются независимо, имеют собственную память и обрабатывают собственные потоки ввода-вывода; эти потоки обычно соединяются в конвейеры. Создание и поддержка процессов достаточно затратно, поэтому это решение эффективно только при небольшом и относительно стабильном наборе процессов. Оно также предполагает, что отдельные процессы могут работать независимо, за исключением обработки ввода-вывода друг друга; если им требуется взаимодействие другими способами, их координация может оказаться сложной. Расширением этого подхода является программирование потоками данных, позволяющее создавать более сложные сети, чем просто цепочки, поддерживаемые конвейерами.

Выберите (/ опрос) петли

Доступен в BSD Unix и практически в любой другой системе с пакетом протоколов TCP/IP, который либо использует, либо основан на реализации BSD. Являясь разновидностью механизма опроса, цикл `select` использует системный вызов `select` для ожидания, пока не произойдет событие на файловом дескрипторе (например, когда появятся данные для чтения), не истечет время ожидания или не будет получен сигнал (например, при завершении дочернего процесса). Анализируя возвращаемые параметры вызова `select`, цикл определяет, какой файловый дескриптор изменился, и выполняет соответствующий код. Часто, для удобства, цикл `select` реализуется как цикл обработки событий, возможно, с использованием функций обратного вызова; такая ситуация особенно хорошо подходит для событийного программирования. Хотя этот метод надежен и относительно эффективен, он сильно зависит от парадигмы Unix, согласно которой "все есть файл"; любой блокирующий ввод-вывод, не связанный с файловым дескриптором, заблокирует процесс. Цикл `select` также требует возможности включить весь ввод-вывод в центральный вызов `select`; библиотеки, выполняющие собственный ввод-вывод, представляют в этом отношении особую проблему. Дополнительная потенциальная проблема заключается в том, что `select` и операции ввода-вывода все еще достаточно слабо связаны, поэтому результат `select` может оказаться неверным: если два процесса читают из одного файлового дескриптора (что, возможно, является плохим дизайном), `select` может указать на наличие данных для чтения, которые исчезли к моменту выполнения операции чтения, что приведет к блокировке; если два процесса записывают в один файловый дескриптор (что не редкость), `select` может указать на возможность немедленной записи, но запись все равно может быть заблокирована, поскольку буфер мог быть заполнен другим процессом, или из-за того, что запись слишком велика для доступного буфера, или по другим причинам не подходит для получателя. Цикл `select` не достигает максимальной эффективности системы, возможной, например, при использовании метода очередей завершения, поскольку семантика вызова `select`, позволяющая настраивать допустимый набор событий для каждого вызова, требует некоторого времени на перебор массива выбора при каждом вызове. Это создает небольшую нагрузку для пользовательских приложений, которым может потребоваться один файловый дескриптор для оконной системы и несколько для открытых файлов, но становится более серьезной проблемой по мере увеличения числа потенциальных источников событий и может затруднить разработку многих клиент-серверных приложений, как в случае проблемы C10k; другие асинхронные методы могут быть заметно более эффективными в таких случаях. Некоторые Unix-системы предоставляют системные вызовы с лучшей масштабируемостью, например, `epoll` в Linux (который заполняет возвращаемый массив выбора только теми источниками событий, на которых произошло событие), `kqueue` в FreeBSD и порты событий (и `/dev/poll`) в Solaris. SVR3 Unix предоставлял системный вызов `poll`. Возможно, более удачное название, чем `select`, но для целей данного обсуждения они по сути одинаковы. SVR4 Unix (и, следовательно, POSIX) предлагают оба вызова.

Сигналы (перерывы)

Доступен в BSD и POSIX Unix. Ввод/вывод выполняется асинхронно, и по завершении генерируется сигнал (прерывание). Как и при программировании ядра низкого уровня, возможности, доступные для безопасного использования в обработчике сигналов, ограничены, и основной поток процесса мог быть прерван практически в любой момент, что приводит к несогласованности структур данных, видимых обработчику сигналов. Обработчик сигналов обычно не может самостоятельно инициировать дальнейший асинхронный ввод/вывод. Подход с использованием сигналов, хотя и относительно прост в реализации в операционной системе, накладывает на прикладную программу нежелательные сложности, связанные с разработкой системы прерываний ядра ОС. Его наиболее серьезный недостаток заключается в том, что любой блокирующий (синхронный) системный вызов потенциально может быть прерван; программисту обычно требуется добавлять код повтора для каждого вызова.

Функции обратного вызова

Доступен в классических Mac OS, VMS и Windows. Обладает многими характеристиками сигнального метода, поскольку по сути является тем же самым, хотя это и редко признается. Разница заключается в том, что каждый запрос ввода/вывода обычно может иметь свою собственную функцию завершения, в то время как сигнальная система использует единственный механизм обратного вызова. С другой стороны, потенциальная проблема использования обратных вызовов заключается в том, что глубина стека может стать неуправляемой, поскольку очень часто после завершения одного запроса ввода/вывода планируется следующий. Если это требование должно быть выполнено немедленно, первый обратный вызов не завершается (не "разворачивается" из стека) до того, как будет вызван следующий. Механизмы предотвращения этого (например, промежуточное планирование новых задач) усложняют систему и снижают производительность. Однако на практике это обычно не является проблемой, поскольку новый запрос ввода/вывода обычно возвращает управление сразу после запуска, позволяя стеку "развернуться". Эту проблему также можно избежать, предотвращая дальнейшие обратные вызовы с помощью очереди, пока не вернется первый обратный вызов.

Легкие процессы или нити

Легкие процессы (LWPs) или потоки доступны в большинстве современных операционных систем. Подобно использованию процессов, но с меньшими накладными расходами и без изоляции данных, которая затрудняет координацию потоков. Каждый LWP или поток сам по себе использует традиционный блокирующий синхронный ввод/вывод, что упрощает логику программирования; это распространенная парадигма, используемая во многих языках программирования, включая Java и Rust. Многопоточность требует использования механизмов синхронизации, предоставляемых ядром, и потокобезопасных библиотек. Этот метод не является оптимальным для приложений очень большого масштаба, таких как веб-серверы, из-за большого количества необходимых потоков. Этот подход также используется в среде выполнения языка программирования Erlang. Виртуальная машина Erlang использует асинхронный ввод/вывод, используя небольшой пул из нескольких потоков или иногда только один процесс, для обработки ввода/вывода от миллионов процессов Erlang. Обработка ввода/вывода в каждом процессе в основном осуществляется с использованием блокирующего синхронного ввода/вывода. Таким образом, высокая производительность асинхронного ввода/вывода сочетается с простотой обычного ввода/вывода (ср. модель Actor). Многие задачи ввода/вывода в Erlang сводятся к передаче сообщений, которые можно легко обрабатывать с помощью встроенного селективного приема. Волокна / Корутины можно рассматривать как аналогичный легковесный подход к реализации асинхронного ввода/вывода вне среды выполнения Erlang, хотя они не предоставляют тех же гарантий, что и процессы Erlang.

Очереди на заполнение/порт

Доступен в Microsoft Windows, Solaris, AmigaOS, DNIX и Linux (с использованием io uring, начиная с версии 5.1). Запросы ввода/вывода выполняются асинхронно, однако уведомления о завершении предоставляются посредством синхронизирующего механизма очереди в порядке их завершения. Как правило, используется в структуре основного процесса, основанной на конечном автомате (событийно-ориентированное программирование), что может существенно отличаться от процесса, не использующего асинхронный ввод/вывод или использующего другие подходы, что затрудняет повторное использование кода. Не требует дополнительных специальных механизмов синхронизации или потокобезопасных библиотек, а также не разделяет логический (код) и временной (события) потоки выполнения.

Флаги события

Доступен в VMS и AmigaOS (часто используется в сочетании с портом завершения). Обладает многими характеристиками метода очереди завершения, так как по сути представляет собой очередь завершения глубиной один. Для имитации эффекта глубины очереди требуется дополнительный флаг события для каждого потенциально необработанного (но завершенного) события, иначе информация о событии может быть утеряна. Ожидание следующего доступного события в таком наборе требует механизмов синхронизации, которые могут плохо масштабироваться при большом количестве потенциально параллельных событий.

Канал ввода/вывода

Доступен на мейнфреймах IBM, Groupe Bull и Unisys. Канал ввода-вывода разработан для максимизации загрузки центрального процессора и увеличения пропускной способности за счет перекладывания большей части операций ввода-вывода на сопроцессор. Сопроцессор оснащен встроенным DMA, обрабатывает аппаратные прерывания, управляется основным процессором и прерывает его только при крайней необходимости. Эта архитектура также поддерживает так называемые канальные программы, выполняемые на канальном процессоре для обработки ресурсоемких задач ввода-вывода и протоколов.

Зарегистрированные В/В

Доступен в Windows Server 2012 и Windows 8. Оптимизирован для приложений, обрабатывающих большое количество небольших сообщений, чтобы добиться более высокой скорости операций ввода-вывода в секунду при снижении колебаний и задержки.

Реализация

Подавляющее большинство вычислительного оборудования общего назначения полностью полагается на два метода реализации асинхронного ввода-вывода: опрос и прерывания. Обычно оба метода используются совместно, а их соотношение сильно зависит от конструкции оборудования и требуемых характеристик производительности. (DMA само по себе не является независимым методом, а лишь способом выполнения большего объема работы за один опрос или прерывание.) Чистые системы опроса вполне возможны; небольшие микроконтроллеры (например, системы на базе PIC) часто строятся именно так. Системы CP/M также могли быть построены таким образом (хотя это случалось редко), с использованием DMA или без него. Кроме того, когда максимальная производительность необходима лишь для нескольких задач в ущерб любым другим потенциальным задачам, опрос может быть предпочтительным, поскольку накладные расходы на обработку прерываний могут быть нежелательны. (Обслуживание прерывания требует времени [и памяти] для сохранения как минимум части состояния процессора, а также времени, необходимого для возобновления прерванной задачи.) Большинство вычислительных систем общего назначения в значительной степени полагаются на прерывания. Чистая система прерываний возможна, хотя обычно требуется и компонент опроса, поскольку часто несколько потенциальных источников прерываний используют общую линию сигнала прерывания, в этом случае опрос используется в драйвере устройства для определения фактического источника. (Время определения также вносит вклад в снижение производительности системы прерываний. За прошедшие годы было проделано много работы для минимизации накладных расходов, связанных с обслуживанием прерываний. Современные системы прерываний довольно неторопливы по сравнению с некоторыми хорошо настроенными старыми системами, но общее повышение производительности оборудования значительно смягчило эту проблему.) Возможны и гибридные подходы, когда прерывание запускает начало серии асинхронных операций ввода-вывода, а опрос используется внутри этой серии. Эта техника распространена в высокоскоростных драйверах устройств, таких как сетевые или дисковые, где время, потерянное на возврат к задаче, прерванной прерыванием, больше, чем время до следующего необходимого обслуживания. (Современное оборудование ввода-вывода в значительной степени полагается на DMA и большие буферы данных для компенсации относительно неэффективной системы прерываний. Они используют опрос внутри циклов драйверов и могут демонстрировать огромную пропускную способность. В идеале, опросы по каждому элементу данных всегда успешны или, в худшем случае, повторяются лишь несколько раз.) В свое время этот гибридный подход был распространен в дисковых и сетевых драйверах, где не было DMA или значительной буферизации. Поскольку желаемые скорости передачи данных были выше, чем мог выдержать минимальный цикл из четырех операций на элемент данных (проверка бита, условный переход к самому себе, выборка и сохранение), аппаратное обеспечение часто строилось с автоматической генерацией состояний ожидания на устройстве ввода-вывода, что переносило проверку готовности данных из программного обеспечения на аппаратное обеспечение выборки или сохранения процессора и сокращало запрограммированный цикл до двух операций. (По сути, используя сам процессор в качестве DMA-контроллера.) Процессор 6502 предлагал необычный способ реализации цикла из трех элементов на элемент данных, поскольку он имел аппаратный вывод, который при активации напрямую устанавливал бит переполнения (Overflow) процессора. (Очевидно, при проектировании аппаратного обеспечения необходимо было проявлять большую осторожность, чтобы избежать перезаписи бита переполнения вне драйвера устройства!)

Синтез

Используя только эти два инструмента (опрос и прерывания), все остальные формы асинхронного ввода-вывода, описанные выше, могут быть (и фактически являются) синтезированы. В среде, такой как виртуальная машина Java (JVM), асинхронный ввод-вывод может быть синтезирован, даже если среда, в которой работает JVM, вообще не предоставляет его. Это обусловлено интерпретируемым характером JVM. JVM может периодически выполнять опрос (или обрабатывать прерывание), чтобы инициировать внутреннее изменение потока управления, создавая видимость множества одновременных процессов, по крайней мере некоторые из которых, предположительно, существуют для выполнения асинхронного ввода-вывода. (Разумеется, на микроскопическом уровне параллелизм может быть довольно грубым и демонстрировать некоторые неидеальные характеристики, но внешне он будет выглядеть желаемым образом.) Именно в этом, на самом деле, заключается проблема использования опроса в любой форме для синтеза другого вида асинхронного ввода-вывода. Каждый цикл процессора, затраченный на опрос, пропадает впустую и уходит на накладные расходы, вместо выполнения полезной работы. Каждый цикл процессора, не затраченный на опрос, приводит к увеличению задержки реакции на ожидающие операции ввода-вывода. Найти приемлемый баланс между этими двумя противоположными факторами сложно. (Именно поэтому изначально были изобретены аппаратные системы прерываний.) Ключ к максимальной эффективности – минимизировать объем работы, который необходимо выполнить при получении прерывания для пробуждения соответствующего приложения. Второстепенно (но не менее важно) – метод, который само приложение использует для определения необходимых действий. Особенно проблематичны для эффективности приложения методы явного опроса, включая механизмы select/poll. Хотя базовые события ввода-вывода, которые их интересуют, скорее всего, генерируются прерываниями, взаимодействие с этими механизмами осуществляется посредством опроса и может занимать значительное время. Это особенно актуально для потенциально масштабного опроса, возможного с помощью select (и poll). Прерывания хорошо сочетаются с сигналами, функциями обратного вызова, очередями завершения и флагами событий, и такие системы могут быть очень эффективными.