28. Модели (виды) ввода-вывода: блокирующий, неблокирующий, асинхронный, мультиплексированный ввод-вывод.

Модели ввода/вывода

Блокирующий I/O (blocking I/O)

По умолчанию весь ввод вывод выполняется в блокирующем стиле. В данном случае процесс делает системный вызов recvfrom. В результате процесс заблокируется (уйдет в сон) до тех пор пока не придут данные и системный вызов не запишет их в буфер приложения. После этого системный вызов заканчивается (return OK) и мы можем обрабатывать наши данные. Очевидно, что данный подход имеет очень большой недостаток — пока мы ждем данные (а они могут идти очень долго из-за качества коннекта и т.п.) процесс спит и не отвечает на запросы.

Неблокирующий I/O (nonblocking I/O)

Мы можем установить неблокирующий режим при работе с сокетами, фактически сказав ядру следующее: «Если ввод/вывод, который мы хотим осуществить, невозможен без погружения процесса в блокировку (сон), то верни мне ошибку что не можешь этого сделать без блокировки.»

Первые три раза, которые мы посылаем системный вызов на чтение не возвращают результат, т.к. ядро видит, что данных нет и просто возвращает нам ошибку EWOULDBLOCK. Последний системный вызов выполнится успешно, т.к. данные готовы для чтения. В результате ядро запишет данные в буфер процесса и они станут доступными для обработки. На этой основе можно создать цикл, который постоянно вызывает recvfrom (обращается за данными) для сокетов, открытых в неблокирующем режиме. Этот режим называется опросом (поллинг/polling) т.к. приложение все время опрашивает ядро системы на предмет наличия данных. Я принципиально не вижу ограничений чтоб опрашивать несколько сокетов последовательно и соответственно читать из первого, в котором есть данные. Такой подход приводит к большим накладным расходам (overhead) процессорного времени.

Мультиплексирование I/O (multiplexing I/O)

Вообще слово multiplexing переводится как «уплотнение». Мне кажется его удачно можно описать девизом тайм-менеджмента — «учись успевать больше». При мультиплексировании ввода/вывода мы обращаемся к одному из доступных в ОС системному вызову (мультиплексору например select, poll, pselect, dev/poll, epoll (рекомендуемый для Linux), kqueue (BSD)) и на нем блокируемся вместо того, чтобы блокироваться на фактическом I/O вызове. Схематично процесс мультиплексирования представлен на изображении

Приложение блокируется при вызове select’a ожидая когда сокет станет доступным для чтения. Затем ядро возвращает нам статус readable и можно получать данные помощью recvfrom. На первый взгляд — сплошное разочарование. Та же блокировка, ожидание, да и еще 2 системных вызова (select и recvfrom) — высокие накладные расходы. Но в отличии от блокирующего метода, select (и любой другой мультиплексор) позволяет ожидать данные не от одного, а от нескольких файловых дескрипторов. Надо сказать, что это наиболее разумный метод для обслуживания множества клиентов, особенно если ресурсы достаточно ограничены. Почему это так? Потому что мультиплексор снижает время простоя (сна). Попробую объяснить следующим изображением

Создается пул дескрипторов, соответствующих сокетам. Даже если при соединении нам пришел ответ EINPROGRESS это значит, что соединение устанавливается, что нам никак не мешает, т.к. мультиплексор в ходе проверки все равно возьмет тот, который первый освободился. А теперь внимание! Самое главное! Ответьте на вопрос: У какого события вероятность больше? У события А, что данные будут готовы у какого-то конкретного сокета или у события Б, что данные будут готовы хотя бы у одного сокета?. Ответ: Б В случае с мультиплексированием у нас в цикле проверяются ВСЕ сокеты и берется первый который готов. Пока мы с ним работаем, другие также могут подоспеть, тоесть мы снижаем время на простой (первый раз мы может ждем долго, но остальные разы — гораздо меньше). Если же решать проблему обычным способом (с блокировкой) то нам придется гадать, из какого коннекта прочитать первым вторым и т.п. т.е. мы 100% ошибемся и будем ждать, а хотя могли бы не тратить это время