Асинхронный ввод вывод linux

Асинхронный ввод вывод linux

Библиотека сайта rus-linux.net

На главную -> MyLDP -> Электронные книги по ОС Linux

Цилюрик О.И. Linux-инструменты для Windows-программистов
Назад Библиотеки API POSIX Вперед

Асинхронный ввод-вывод

Асинхронный ввод-вывод добавлен только в редакции стандарта POSIX.1g (1993г., одно из расширений реального времени). В вызове aio_read() даётся указание ядру начать операцию ввод-вывода, и указывается, каким сигналом уведомить процесс о завершении операции (включая копирование данных в пользовательский буфер). Вызывающий процесс не блокируется. Результат операции (например, полученная UDP дейтаграмма) может быть обработан, например, в обработчике сигнала. Разница с предыдущей моделью, управляемой сигналом, состоит в том, что в той модели сигнал уведомлял о возможности начала операции (вызове операции чтения), а в асинхронной модели сигнал уведомляет уже о завершении операции чтения в буфер пользователя.

Того же назначения блок для 64-битных операций:

И некоторые операции (в качестве примера):

int aio_read( struct aiocb *__aiocbp );

int aio_write( struct aiocb *__aiocbp );

Инициализация выполнения целой цепочки асинхронных операций (длиной __nent ):

Как и для потоков pthread_t , асинхронные операции значительно легче породить, чем позже остановить. для чего также потребовался отдельный API:

int aio_cancel( int __fildes, struct aiocb *__aiocbp );

Можно предположить, что каждая асинхронная операция выполняется как отдельный поток, у которого не циклическая функция потока.

Источник

Асинхронный ввод вывод linux

API для ввода-вывода в современных ОС позволяют обрабатывать сотни и тысячи одновременно открытых сетевых запросов либо файлов. Для этой цели не нужно создавать множество потоков — достаточно запустить специальный цикл событий на одном потоке, начав мультиплексирование ввода-вывода

Содержание

В этой статье мы покажем, что именно происходит, когда вы используете неблокирующий ввод-вывод. Мы рассмотрим:

  • Что означают понятия “неблокирующий”, “асинхронный”, “событийный” для ввода-вывода
  • Смысл добавления флага O_NONBLOCK для файловых дескрипторов через fcntl
  • Почему неблокирующий ввод-вывод часто сочетается с мультиплексированием через select , epoll и kqueue
  • Как неблокирующий режим ввода-вывода взаимодействует со средствам опроса дескрипторов, такими как epoll

Термины: неблокирующий, асинхронный, событийный

  • “Асинхронный” буквально означает “не синхронный”. Например, отправка email асинхронная, потому что отправитель не ожидает получить ответ сразу же. В программировании “асинхронным” называют код, в котором компоненты посылают друг другу сообщения, не ожидая немедленного ответа.
  • “Неблокирующий” — термин, чаще всего касающийся ввода-вывода. Он означает, что при вызове “неблокирующего” системного API управление сразу же будет возвращено программе, и она продолжит использовать свой квант процессорного времени. Обычные, простые в использовании системные вызовы блокирующие: они усыпляют вызывающий поток до тех пор, пока данные для ответа не будут готовы.
  • “Событийный” означает, что компонент программы обрабатывает очередь событий с помощью цикла, а тем временем кто-либо добавляет события в очередь, формируя входные данные компонента, и забирает у него выходные данные.

Термины часто пересекаются. Например, протокол HTTP сам по себе синхронный (пакеты отправляются синхронно), но при наличии неблокирующего ввода-вывода программа, работающая с HTTP, может оставаться асинхронной, то есть успевать совершить какую-либо полезную работу между отправкой HTTP-запроса и получением ответа на него.

Блокирующий режим

По умолчанию все файловые дескрипторы в Unix-системах создаются в “блокирующем” режиме. Это означает, что системные вызовы для ввода-вывода, такие как read , write или connect , могут заблокировать выполнение программы вплоть до готовности результата операции. Легче всего понять это на примере чтения данных из потока stdin в консольной программе. Как только вы вызываете read для stdin, выполнение программы блокируется, пока данные не будут введены пользователем с клавиатуры и затем прочитаны системой. То же самое происходит при вызове функций стандартной библиотеки, таких как fread , getchar , std::getline , поскольку все они в конечном счёте используют системный вызов read . Если говорить конкретнее, ядро погружает процесс в спящее состояние, пока данные не станут доступны в псевдо-файле stdin. То же самое происходит и для любых других файловых дескрипторов. Например, если вы пытаетесь читать из TCP-сокета, вызов read заблокирует выполнение, пока другая сторона TCP-соединения не пришлёт ответные данные.

Блокировки — это проблема для всех программ, требующих конкурентного выполнения, поскольку заблокированные потоки процесса засыпают и не получают процессорное время. Существует два различных, но взаимодополняющих способа устранить блокировки:

  • неблокирующий режим ввода-вывода
  • мультиплексирование с помощью системного API, такого как select либо epoll
Читайте также:  Что такое windows desktop sharing

Эти решения часто применяются совместно, но предоставляют разные стратегии решения проблемы. Скоро мы узнаем разницу и выясним, почему их часто совмещают.

Неблокирующий режим (O_NONBLOCK)

Файловый дескриптор помещают в “неблокирующий” режим, добавляя флаг O_NONBLOCK к существующему набору флагов дескриптора с помощью fcntl :

С момента установки флага дескриптор становится неблокирующим. Любые системные вызовы для ввода-вывода, такие как read и write , в случае неготовности данных в момент вызова ранее могли бы вызвать блокировку, а теперь будут возвращать -1 и глобальную переменную errno устанавливать в EWOULDBLOCK , Это интересное изменение поведения, но само по себе мало полезное: оно лишь является базовым примитивом для построения эффективной системы ввода-вывода для множества файловых дескрипторов.

Допустим, требуется параллельно прочитать целиком данные из двух файловых дескрипторов. Это может быть достигнуто с помощью цикла, который проверяет наличие данных в каждом дескрипторе, а затем засыпает ненадолго перед новой проверкой:

Такой подход работает, но имеет свои минусы:

  • Если данные приходят очень медленно, программа будет постоянно просыпаться и тратить впустую процессорное время
  • Когда данные приходят, программа, возможно, не прочитает их сразу, т.к. выполнение приостановлено из-за nanosleep
  • Увеличение интервала сна уменьшит бесполезные траты процессорного времени, но увеличит задержку обработки данных
  • Увеличение числа файловых дескрипторов с таким же подходом к их обработке увеличит долю расходов на проверки наличия данных

Для решения этих проблем операционная система предоставляет мультиплексирование ввода-вывода.

Мультиплексирование ввода-вывода (select, epoll, kqueue и т.д.)

Существует несколько мультиплексирующих системных вызовов:

  • Вызов select существует во всех POSIX-совместимых системах, включая Linux и MacOSX
  • Группа вызовов epoll_* существует только на Linux
  • Группа вызовов kqueue существует на FreeBSD и других *BSD

Все три варианта реализуют единый принцип: делегировать ядру задачу по отслеживанию прихода данных для операций чтения/записи над множеством файловых дескрипторов. Все варианты могут заблокировать выполнение, пока не произойдёт какого-либо события с одним из дескрипторов из указанного множества. Например, вы можете сообщить ядру ОС, что вас интересуют только события чтения для файлового дескриптора X, события чтения-записи для дескриптора Y, и только события записи — для Z.

Все мультиплексирующие системные вызовы, как правило, работают независимо от режима файлового дескриптора (блокирующего или неблокирующего). Программист может даже все файловые дескрипторы оставить блокирующими, и после select либо epoll возвращённые ими дескрипторы не будут блокировать выполнение при вызове read или write , потому что данные в них уже готовы. Есть важное исключение для epoll , о котором скажем далее.

Как O_NONBLOCK сочетается с мультиплексером select

Допустим, мы пишем простую программу-daemon, обслуживающее клиентские приложения через сокеты. Мы воспользуемся мультиплексером select и блокирующими файловыми дескрипторами. Для простоты предположим, что мы уже открыли файлы и добавили их в переменную read_fds , имеющую тип fd_set (то есть “набор файлов”). Ключевым элементом цикла событий, обрабатывающего файл, будет вызов select и дальнейшие вызовы read для каждого из дескрипторов в наборе.

Тип данных fd_set представляет просто массив файловых дескрипторов, где с каждым дескриптором связан ещё и флаг (0 или 1). Примерно так могло бы выглядеть объявление:

Функция select принимает несколько объектов fd_set . В простейшем случае мы передаём один fd_set с набором файлов для чтения, а select модифицирует их, проставляя флаг для тех дескрипторов, из которых можно читать данные. Также функция возвращает число готовых для обработки файлов. Далее с помощью макроса FD_ISSET(index, &set) можно проверить, установлен ли флаг, т.е. можно ли читать данные без блокировки.

Такой подход работает, но давайте предположим, что размер буфера buf очень маленький, а объём пакетов данных, читаемых из дескрипторов файлов, очень велик. Например, в примере выше размер buf всего 1024 байта, допустим что через сокеты приходят пакеты по 64КБ. Для обработки одного пакета потребуется 64 раза вызвать select , а затем 64 раза вызвать read . В итоге мы получим 128 системных вызовов, но каждый вызов приводит к одному переключению контекста между kernel и userspace, в итоге обработка пакета обходится дорого.

Можем ли мы уменьшить число вызовов select ? В идеале, для обработки одного пакета мы хотели бы вызвать select только один раз. Чтобы сделать это, потребуется перевести все файловые дескрипторы в неблокирующий режим. Ключевая идея — вызывать read в цикле до тех пор, пока вы не получите код ошибки EWOULDBLOCK , обозначающий отсутствие новых данных в момент вызова. Идея реализована в примере:

В этом примере при наличии буфера в 1024 байта и входящего пакета в 64КБ мы получим 66 системных вызовов: select будет вызван один раз, а read будет вызываться 64 раза без каких-либо ошибок, а 65-й раз вернёт ошибку EWOULDBLOCK .

Читайте также:  Как узнать название оперативной памяти компьютера windows

Мультиплексер epoll в режиме edge-triggered

Группа вызовов epoll является наиболее развитым мультиплексером в ядре Linux и способна работать в двух режимах:

  • level-triggered — похожий на select упрощённый режим, в котором файловый дескриптор возвращается, если остались непрочитанные данные
    • если приложение прочитало только часть доступных данных, вызов epoll вернёт ему недопрочитанные дескрипторы
  • edge-triggered — файловый дескриптор с событием возвращается только если с момента последнего возврата epoll произошли новые события (например, пришли новые данные)
    • если приложение прочитало только часть доступных данных, в данном режиме оно всё равно будет заблокировано до прихода каких-либо новых данных

Чтобы глубже понять происходящее, рассмотрим схему работы epoll с точки зрения ядра. Допустим, приложение с помощью epoll начало мониторинг поступления данных для чтения из какого-либо файла. Для этого приложение вызывает epoll_wait и засыпает на этом вызове. Ядро хранит связь между ожидающими данных потоками и файловым дескриптором, который один или несколько потоков (или процессов) отслеживают. В случае поступления порции данных ядро обходит список ожидающих потоков и разблокирует их, что для потока выглядит как возврат из функции epoll_wait .

  • В случае level-triggered режима вызов epoll_wait пройдёт по списку файловых дескрипторов и проверит, не соблюдается ли в данный момент условие, которое интересует приложение, что может привести к возврату из epoll_wait без какой-либо блокировки.
  • В случае edge-triggered режима ядро пропускает такую проверку и усыпляет поток, пока не обнаружит событие прихода данных на одном из файловых дескрипторов, за которыми следит поток. Такой режим превращает epoll в мультиплексер с алгоритмической сложностью O(1): после прихода новых данных из файла ядро сразу же знает, какой процесс надо пробудить.

Для использования edge-triggered режима нужно поместить файловые дескрипторы в неблокирующий режим, и на каждой итерации вызывать read либо write до тех пор, пока они не установят код ошибки EWOULDBLOCK . Вызов epoll_wait будет более эффективным благодаря мгновенному засыпанию потока, и это крайне важно для программ с огромным числом конкурирующих файловых дескрипторов, например, для HTTP-серверов.

Источник

дисковый асинхронный ввод-вывод

здравствуйте, обдумываю сейчас реализацию eventloop-а и для сетевых сокетов и для дискового I/O. я так понял, что epoll для, например, текстовых файлов не предназначен совсем. можно использовать либо posix aio либо aio уровня ядра.

1) posix aio реально используется в 2017 году? я думал что создавать каждый раз поток для каждого файла чисто для I/O это слишком накладно

2) aio уровня ядра, все ли нормально с ним в linux? толком так и не понял: то ли не доделали его, то ли косячный какой-то он

posix aio реально используется в 2017 году?

А какие существуют кроссплатформенные альтернативы? Пазикс наше всё.

я просто думал что поток на соединение это такое костыльное решение. типа на коленке набросанное

Глянь реализацию sendfile в FreeBSD. Там как раз асинхронный вывод из файла в сокет, может чего полезного накопаешь.

libuv на Linux для дискового IO использует thread-pool. Сетевые сокеты мониторятся epoll. В нем же мониторятся и сигналы от пула тредов (которые отправляются когда операция завершается)

обдумываю сейчас реализацию eventloop-а и для сетевых сокетов и для дискового I/O.

А что мешает заюзать готовые реализации?

Да, есть реализация AIO на уровне ядра Linux, в которой не требуется треды создавать, но оно не портабельно

тебе нужно не на каждый файл, а создавать примерно в районе 20-80 потоков на каждое устройство и пропускать запросы через них.

Никакого асинхронного дискового IO не существует нигде. Есть какие-то попытки сделать асинхронное чтение/запись, но это лишь часть большой задачи, потому что есть ещё open, stat, а по моему опыту забитый шпиндель может отдать ответ на stat где-нибудь через 30-40 секунд. Весь твой event loop летит к черту.

Короче, thread pool, потом потыкайся в libuv когда наиграешься, а затем бери эрланг и расслабься. Там всё есть и сделано гораздо лучше.

Весь твой event loop летит к черту.

потом потыкайся в libuv когда наиграешься

вот libuv и вдохновил свой цикл сделать чтоб разобраться с нуля. в libuv для сетевого ввода/вывода используется epoll, а для дискового posix aio + thread pool? если так, то почему все говорят что node.js все делает в один поток?

Читайте также:  Взлома вай фай через линукс

эту ссылку видел. т.е. нужно самому обертки писать до сих пор? просто думал может уже реализовали все и устаканили

потому что тредпул только для дисков. Всё остальное нода обречена делать в один поток

а сколько потоков в тредпуле там?

это очень сложный вопрос, потому что по идее надо иметь порядка 20-80 (it depends) на один девайс. Но рейд всё поменяет

libuv на Linux для дискового IO использует thread-pool. Сетевые сокеты мониторятся epoll. В нем же мониторятся и сигналы от пула тредов

нет сейчас возможности смотреть код libuv. я так понимаю пул тредов «пишет» в какой-то unix-сокет который тоже опрашивается в epoll? или как тогда

Нафиг тебе epoll? Чем обычный select не устраивает?

по идее надо иметь порядка 20-80 (it depends) на один девайс

пытаюсь понять: а зачем так много? вот вызвали read — один поток для этого read. вызвали write — 1 поток для write и т.д.

С какого перепугу? select — простая штука, а с epoll ты будешь вынужден сношаться.

в зависимости от конкретной модели диска до N одновременных запросов проходят за одинаковое время, т.е. диск выходит на свою максимальную скорость при наличии забитой очереди задач на чтение

спасибо, надо будет просветиться

тебе нужно не на каждый файл, а создавать примерно в районе 20-80 потоков на каждое устройство и пропускать запросы через них.

Я знаю пример с прошлой работы, когда эта замечательная идея выродилась в 18 тысяч тредов в одном-единственном процессе.

зачем так много?

В Linux нет асинхронного ввода/вывода для дисковой подсистемы, только для прямого (некешируемого) ввода/вывода (DIRECT IO). posix эмулирует его с помощью потоков. Если такая задача возникнет, то лучше всего это реализовать самому на пуле потоков (как это сделали nginx).

Глянь реализацию sendfile в FreeBSD.

Во FreeBSD асинхронные дисковые операции реализовали Netflix специально для себя (очень недавно). В Linux такого нет.

Ну я и написал же — посмотри как реализовано, может что-то полезное увидишь. Можно ещё в стрекозе покопаться на предмет идей полезных 🙂

Если такая задача возникнет, то лучше всего это реализовать самому на пуле потоков

в смысле реализовать на posix aio + пул потоков?

в смысле реализовать на posix aio + пул потоков?

Вообще без posix AIO. Очередь, в которую ставятся заявки на чтение/запись + пул потоков, который исполняет запросы и уведомляет заказчика. Более того, если это не библиотека для неопределенного круга пользователей, а решение для конкретной задачи, то с учетом того, что запись всегда делается в кеш ОС, ее имеет смысл делать асинхронной только для случая, когда есть шанс, что оперативки будет не хватать для кеширования. Однако, в этом случае все приложение целиком начнет тормозить, вместе с операционкой из-за нехватки памяти и забитого на 100% ввода/вывода (у компа есть три основных ресурса — CPU, размер RAM и производительность IO из них 2 будут забиты на 100%).

а чем это лучше чем posix AIO? тоже самое ж имхо

Начали 25 лет назад, когда нагрузка сильно меньше была и треды дешевле, а сейчас, без изменения архитектуры, бегемот вырос.

В Linux нет асинхронного ввода/вывода для дисковой подсистемы, только для прямого (некешируемого) ввода/вывода (DIRECT IO).

Подсистема, которая в каталоге block в исходниках ядра, та вся асинхронная. В файловой семантике POSIX асинхронности нет, да.

а кстати как параллельно на диске может осуществляться ввод/вывод если читающая головка одна?

Нет. И вообще, чтение не так примитивно.

Начали 25 лет назад, когда нагрузка сильно меньше была и треды дешевле

Это в каком плане треды были дешевле 25 лет назад?

Это в каком плане треды были дешевле 25 лет назад?

Ну потери на свитч стали дороже и в софте, и в самом железе. А тогда там вообще юниксоподобное ядро без юзерспейса было.

Это в каком плане треды были дешевле 25 лет назад?

Ну потери на свитч стали дороже и в софте, и в самом железе

В абсолютном времени они стали меньше и в железе, и в софте (по крайней мере, в ОС).

А тогда там вообще юниксоподобное ядро без юзерспейса было.

Хотя, если раньше всё было в едином адресном пространстве ядра, а потом перешло в Unix-процесс, тогда расходы могли и возрасти.

В абсолютном времени они стали меньше и в железе, и в софте (по крайней мере, в ОС).

Источник

Оцените статью