- Асинхронный ввод вывод linux
- Асинхронный ввод-вывод
- Асинхронный ввод вывод linux
- Содержание
- Термины: неблокирующий, асинхронный, событийный
- Блокирующий режим
- Неблокирующий режим (O_NONBLOCK)
- Мультиплексирование ввода-вывода (select, epoll, kqueue и т.д.)
- Как O_NONBLOCK сочетается с мультиплексером select
- Мультиплексер epoll в режиме edge-triggered
- дисковый асинхронный ввод-вывод
Асинхронный ввод вывод 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
Эти решения часто применяются совместно, но предоставляют разные стратегии решения проблемы. Скоро мы узнаем разницу и выясним, почему их часто совмещают.
Неблокирующий режим (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 .
Мультиплексер 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-процесс, тогда расходы могли и возрасти.
В абсолютном времени они стали меньше и в железе, и в софте (по крайней мере, в ОС).
Источник