Мультиплексирование ввода/вывода и асинхронный ввод/вывод
Мультиплексирование ввода при помощи poll(2)
Системный вызов poll(2) выполняет приблизительно те же задачи, что и select(3C), но использует несколько более удобный способ передачи информации о том, какие дескрипторы его интересуют. poll(2) имеет три параметра:
- struct pollfd fds[] - массив описателей дескрипторов. Структура pollfd обсуждается далее в этом разделе
- nfds_t nfds - количество описателей в массиве fds
- int timeout - тайм-аут в миллисекундах. Если параметр timeout равен 0, poll работает в режиме опроса (возвращает управление немедленно). Если он равен -1, poll ждет готовности дескрипторов неограниченное время.
poll(2) возвращает количество дескрипторов, с которыми произошли какие-то события, запрошенные программой либо представляющие интерес для нее. Если poll(2) возвращает управление по тайм-ауту, код возврата будет равен 0. При ошибке poll(2) возвращает -1 и устанавливает errno.
Структура pollfd имеет следующие поля:
- int fd - дескриптор файла. Если это поле имеет отрицательное значение, запись игнорируется.
- short events - события, связанные с fd, которые нас интересуют.
- short revents - return events, события, связанные с fd, которые реально произошли.
При вызове poll пользователь должен заполнить поля fd и events ; поле revents заполняется системным вызовом.
Поля events и revents представляют собой битовые маски, биты которых соответствуют типам событий. Вместо битов рекомендуется использовать символьные константы, определенные в <poll.h>
Основные используемые типы событий - POLLIN (проверять готовность к чтению), и POLLOUT (проверять готовность к записи). В действительности, эти типы композитные и представляют собой сочетания разных типов событий. Так, для сокетов TCP можно указывать проверку поступления внеполосных данных, для устройств STREAMS - проверку поступления приоритетных данных и т.д. В revents устанавливаются биты, соответствующие реально происшедшему событию, т.е. если вы заказывали ожидание POLLIN, не обязательно в revents будут установлены все биты, входящие в маску POLLIN. Это необходимо иметь в виду при проверке revents (см. пример 8.2).
Кроме POLLIN и POLLOUT, в revents также могут появляться биты POLLERR, POLLHUP и POLLNVAL. В events эти биты игнорируются, а в revents могут быть установлены при следующих условиях:
- POLLERR - на устройстве возникла ошибка
- POLLHUP - сокет, труба или терминальное устройство закрыты на другом конце
- POLLNVAL - значение fd не соответствует валидному файловому дескриптору (скорее всего, дескриптор был закрыт на нашем конце).
#include <poll.h> struct pollfd fds[3]; int ifd1, ifd2, ofd, count; fds[0].fd = ifd1; fds[0].events = POLLNORM; fds[1].fd = ifd2; fds[1].events = POLLNORM; fds[2].fd = ofd; fds[2].events = POLLOUT; count = poll(fds, 3, 10000); if (count == -1) { perror("poll failed"); exit(1); } if (count==0) printf("No data for reading or writing\n"); if (fds[0].revents & POLLNORM) printf("There is data for reading fd %d\n", fds[0].fd); if (fds[1].revents & POLLNORM) printf("There is data for reading fd %d\n", fds[1].fd); if (fds[2].revents & POLLOUT) printf("There is room to write on fd %d\n", fds[2].fd);8.2. Использование poll(2) (фрагмент программы)
Преимущества poll(2) перед select(3C) достаточно очевидны:
- интерфейс poll не накладывает ограничений на пространство номеров дескрипторов, во всяком случае пока эти номера входят в диапазон представления int.
- при большом пространстве номеров дескрипторов ( 65536 в данном контексте следует считать большим пространством), poll часто требует передачи между пользовательским процессом и ядром меньшего объема данных, чем select.
- poll сообщает больше информации о происшедших с дескриптором событиях, чем может сообщить select
- У poll входные и выходные значения разнесены по разным полям структуры, так что не требуется полностью пересоздавать массив fds после каждого вызова.
При использовании poll(2) в многопоточной программе приложимы те же соображения, которые высказывались в конце предыдущего раздела.
Использование /dev/poll
Использование poll(2) с большим количеством файловых дескрипторов приводит к передаче больших объемов данных между пользовательским процессом и ядром. При этом, скорее всего, большая часть этих данных передается впустую - ведь если процесс действительно может работать с таким большим числом дескрипторов, это, скорее всего, означает, что большинство из них не готовы к работе.
В Solaris предоставляется нестандартный API, который может использоваться для решения этой проблемы. Этот API описывается на странице системного руководства poll(7D) и состоит в использовании специального псевдоустройства /dev/poll.
Это устройство открывается как обычный файл системным вызовом open(2). Затем в него следует записать одну или несколько структур pollfd (т.е. тех же самых структур, которые использует poll(2) ). Запись осуществляется системным вызовом write(2) и может осуществляться в несколько приемов. При этом, если вы несколько раз записываете структуры, соответствующие одному и тому же дескриптору, с разными значениями поля events, это будет означать расширение списка опрашиваемых событий для вашего дескриптора. Т.е. если вы сначала запишете pollfd с events==POLLIN, а затем с events==POLLOUT, дескриптор будет опрашиваться в режиме POLLIN | POLLOUT.
Если вы хотите исключить дескриптор из множества опрашиваемых, вам следует записать структуру pollfd, в которой поле events содержит бит POLLREMOVE.
Многократное открытие /dev/poll одним процессом приводит к созданию нескольких независимых наборов дескрипторов.
Сам опрос осуществляется вызовом ioctl(2) с командой DP_POLL. Этот ioctl использует в качестве параметра значение struct dvpoll *. Тип struct dvpoll описан в <sys/devpoll.h> и содержит следующие поля:
- struct pollfd* dp_fds - указатель на массив, в который следует положить описатели дескрипторов, с которым связаны события
- int dp_nfds - размер массива dp_fds. Также, максимальное количество описателей дескрипторов, которые следует получить
- int dp_timeout - тайм-аут в миллисекундах. Этот параметр соответствует параметру timeout poll(2).
Ioctl DP_POLL возвращает количество описателей файловых дескрипторов, записанных в dp_fds, 0 если ioctl был разблокирован по тайм-ауту и -1 в случае ошибки.
С практической точки зрения, важное отличие этого API от poll(2) состоит в том, что при использовании poll(2) описатели дескрипторов после опроса расположены на тех же местах в массиве, на которых вы сами их разместили. Напротив, ioctl DP_POLL возвращает вам массив, который включает только те дескрипторы, с которыми связаны события, причем эти дескрипторы лежат в массиве в том порядке, в котором их счел удобным разместить драйвер /dev/poll. Т.е. вы должны проcматривать dp_fds в порядке увеличения индекса, используя затем значение поля fd как ключ поиска. Скорее всего, вам придется завести массив (возможно, ассоциативный), связывающий значение файлового дескриптора с метаинформацией о том, что это за дескриптор и что вы с ним хотели делать. При работе с этим массивом вам следует иметь в виду, что Unix при нормальной работе переиспользует номера файловых дескрипторов, т.е. вам надо обновлять данные в вашем массиве каждом закрытии файла.
Корректная реализация всей требуемой функциональности (особенно в многопоточной программе) требует большого объемакода, причем кода, довольно сложного при отладке. Скорее всего, именно поэтому поддержка poll(7D) приложениями ограниченна и этот API не стал ни юридическим стандартом, ни даже стандартом де-факто. Большинство современных Unix-систем, за исключением Solaris, не поддерживают /dev/poll и не имеют планов реализации такой поддержки в обозримом будущем.
Однако с точки зрения производительности poll(7D) дает ощутимые преимущества даже при вполне реалистичных количествах файловых дескрипторов на процесс. По данным измерений, опубликованных на сайте http://developers.sun.com/solaris/articles/polling_efficient.html, при 4000 тысячах файловых дескрипторов, poll(7D) требует в 16 раз меньше процессорного времени на исполнение заданного количества циклов опроса-чтения-записи, чем poll(2)!