Студопедия

Главная страница Случайная страница

Разделы сайта

АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторикаСоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника






Мультиплексирование ввода-вывода






Мультиплексирование ввода/вывода и асинхронный ввод/вывод

Мы ждали его слишком долго

Что может быть глупее, чем ждать?

Б. Гребенщиков

Обзор

В ходе этой лекции вы изучите:

  • Опрос нескольких устройств ввода-вывода при помощи системных вызовов select и poll
  • Использование select/poll для ожидания ввода с тайм-аутом
  • Стандартные средства асинхронного ввода/вывода

Мультиплексирование ввода-вывода

Если ваша программа главным образом занимается операциями ввода/вывода, вы можете получить наиболее важные из преимуществ многопоточности в однопоточной программе, используя системный вызов select(3C). В большинстве Unix-систем select является системным вызовом, или, во всяком случае, описывается в секции системного руководства 2 (системные вызовы), т.е. ссылка на него должна была бы выглядеть как select(2), но в Solaris 10 соответствующая страница системного руководства размещена в секции 3C (стандартная библиотека языка С).

Устройства ввода/вывода обычно работают гораздо медленнее центрального процессора, поэтому при выполнении операций с ними процессор обычно оказывается вынужден ждать их. Поэтому во всех ОС системные вызовы синхронного ввода/вывода представляют собой блокирующиеся операции.

Это относится и к сетевым коммуникациям – взаимодействие через Интернет сопряжено с большими задержками и, как правило, происходит через не очень широкий и/или перегруженный канал связи.

Если ваша программа работает с несколькими устройствами ввода/вывода и/или сетевыми соединениями, ей невыгодно блокироваться на операции, связанной с одним из этих устройств, ведь в таком состоянии она может пропустить возможность совершить ввод/вывод с другого устройства без блокировки. Эту проблему можно решать при помощи создания нитей, работающих с различными устройствами. В предыдущих лекциях мы изучили все необходимое для разработки таких программ. Однако для решения этой проблемы есть и другие средства.

Системный вызов select(3C)

В большинстве Unix-систем, select(3C) представляет собой системный вызов, но в Solaris 10 он реализован как библиотечная функция, использующая системный вызов poll(2), поэтому в Solaris руководство по select находится в секции руководства 3C.

select(3C) позволяет ожидать готовности нескольких устройств или сетевых соединений (в действительности, готовности объектов большинства типов, которые могут быть идентифицированы файловым дескриптором). Когда один или несколько из дескрипторов оказываются готовы передать данные, select(3C) возвращает управление программе и передает списки готовых дескрипторов в выходных параметрах.

В качестве параметров select(3C) использует множества (наборы) дескрипторов. В старых Unix-системах множества были реализованы в виде 1024-разрядных битовых масок. В современных Unix-системах и в других ОС, реализующих select, множества реализованы в виде непрозрачного типа fd_set, над которым определены некоторые теоретико-множественные операции, а именно – очистка множества, включение дескриптора в множество, исключение дескриптора из множества и проверка наличия дескриптора в множестве. Препроцессорные директивы для выполнения этих операций описаны на странице руководства select(3C).

В 32-разрядных версиях Unix SVR4, в том числе в Solaris, fd_set по прежнему представляет собой 1024-битовую маску; в 64-разрядных версиях SVR4 это маска разрядности 65536 бит. Размер маски определяет не только максимальное количество файловых дескрипторов в наборе, но и максимальный номер файлового дескриптора в наборе. Размер маски в вашей версии системы можно определить во время компиляции по значению препроцессорного символа FD_SETSIZE. Нумерация файловых дескрипторов в Unix начинается с 0, поэтому максимальный номер дескриптора равен FD_SETSIZE-1.

Таким образом, если вы используете select(3C), вам необходимо установить ограничения на количество дескрипторов вашего процесса. Это может быть сделано шелловской командой ulimit(1) перед запуском процесса или системным вызовом setrlimit(2) уже во время исполнения вашего процесса. Разумеется, setrlimit(2) необходимо вызвать до того, как вы начнете создавать файловые дескрипторы.

Если вам необходимо использовать более 1024 дескрипторов в 32-битной программе, Solaris 10 предоставляет переходный API. Для его использования необходимо определить

препроцессорный символ FD_SETSIZE с числовым значением, превышающим 1024, перед включением файла < sys/time.h>. При этом в файле < sys/select.h> сработают необходимые препроцессорные директивы и тип fd_set будет определен как большая битовая маска, а select и другие системные вызовы этого семейства будут переопределены для использования масок такого размера.

В некоторых реализациях fd_set реализован другими средствами, без использования битовых масок. Например, Win32 предоставляет select в составе так называемого Winsock API. В Win32 fd_set реализован как динамический массив, содержащий значения файловых дескрипторов. Поэтому вам не следует полагаться на знание внутренней структуры типа fd_set.

Так или иначе, изменения размера битовой маски fd_set или внутреннего представления этого типа требуют перекомпиляции всех программ, использующих select(3C). В будущем, когда архитектурный лимит в 65536 дескрипторов на процесс будет повышен, может потребоваться новая версия реализации fd_set и select и новая перекомпиляция программ. Чтобы избежать этого и упростить переход на новую версию ABI, компания Sun Microsystems рекомендует отказываться от использования select(3C) и использовать вместо него системный вызов poll(2). Системный вызов poll(2) рассматривается далее на этой лекции.

Select(3C)

Системный вызов select(3C) имеет пять параметров.

int nfds – число, на единицу большее, чем максимальный номер файлового дескриптора во всех множествах, переданных как параметры.

fd_set *readfds – Входной параметр, множество дескрипторов, которые следует проверять на готовность к чтению. Конец файла или закрытие сокета считается частным случаем готовности к чтению. Регулярные файлы всегда считаются готовыми к чтению. Также, если вы хотите проверить слушающий сокет TCP на готовность к выполнению accept(3SOCKET), его следует включить в это множество. Также, выходной параметр, множество дескрипторов, готовых к чтению.

fd_set *writefds – Входной параметр, множество дескрипторов, которые следует проверять на готовность к записи. Ошибка при отложенной записи считается частным случаем готовности к записи. Регулярные файлы всегда готовы к записи. Также, если вы хотите проверить завершение операции асинхронного connect(3SOCKET), сокет следует включить в это множество. Также, выходной параметр, множество дескрипторов, готовых к записи.

fd_set *errorfds – Входной параметр, множество дескрипторов, которые следует проверять на наличие исключительных состояний. Определение исключительного состояния зависит от типа файлового дескриптора. Для сокетов TCP исключительное состояние возникает при приходе внеполосных данных. Регулярные файлы всегда считаются находящимися в исключительном состоянии. Также, выходной параметр, множество дескрипторов, на которых возникли исключительные состояния.

struct timeval * timeout – тайм-аут, временной интервал, задаваемый с точностью до микросекунд. Если этот параметр равен NULL, то select(3C) будет ожидать неограниченное время; если в структуре задан нулевой интервал времени, select(3C) работает в режиме опроса, то есть возвращает управление немедленно, возможно с пустыми наборами дескрипторов.

Вместо любого из параметров типа fd_set * можно передать нулевой указатель. Это означает, что соответствующий класс событий нас не интересует. select(3C) возвращает общее количество готовых дескрипторов во всех множествах при нормальном завершении (в том числе при завершении по тайм-ауту), и -1 при ошибке.

Использование select(3C)

В примере 1 приводится использование select(3C) для копирования данных из сетевого соединения на терминал, а с терминала – в сетевое соединение. Эта программа упрощенная, она предполагает, что запись на терминал и в сетевое соединение никогда не будет заблокирована. Поскольку и терминал, и сетевое соединение имеют внутренние буферы, при небольших потоках данных это обычно так и есть.

Пример 1. Двустороннее копирование данных между терминалом и сетевым соединением. Пример взят из книги У.Р. Стивенс, Unix: разработка сетевых приложений. Вместо стандартных системных вызовов используются «обертки», описанные в файле “unp.h”

 

Использование select

#include " unp.h"

void str_cli(FILE *fp, int sockfd) {

int maxfdp1, stdineof;

fd_set rset;

char sendline[MAXLINE], recvline[MAXLINE];

 

stdineof = 0;

FD_ZERO(& rset);

for (;;) {

if (stdineof == 0) FD_SET(fileno(fp), & rset);

FD_SET(sockfd, & rset);

maxfdp1 = max(fileno(fp), sockfd) + 1;

Select(maxfdp1, & rset, NULL, NULL, NULL);

if (FD_ISSET(sockfd, & rset)) { /* socket is readable */

if (Readline(sockfd, recvline, MAXLINE) == 0) {

if (stdineof == 1) return; /* normal termination */

else err_quit(" str_cli: server terminated prematurely");

}

 

Fputs(recvline, stdout);

}

if (FD_ISSET(fileno(fp), & rset)) { /* input is readable */

if (Fgets(sendline, MAXLINE, fp) == NULL) {

stdineof = 1;

Shutdown(sockfd, SHUT_WR); /* send FIN */

FD_CLR(fileno(fp), & rset);

continue;

}

Writen(sockfd, sendline, strlen(sendline));

}

}

}

Мультиплексирование ввода при помощи 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 (см. пример 2).

Кроме POLLIN и POLLOUT, в revents также могут появляться биты POLLERR, POLLHUP и POLLNVAL. В events эти биты игнорируются, а в revents могут быть установлены при следующих условиях:

POLLERR – на устройстве возникла ошибка

POLLHUP – сокет, труба или терминальное устройство закрыты на другом конце

POLLNVAL – значение fd не соответствует валидному файловому дескриптору (скорее всего, дескриптор был закрыт на нашем конце).


Использование poll(2) (фрагмент программы)

 

#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);

Сравнение poll(2) и select(3C)

Преимущества poll(2) перед select(3C) достаточно очевидны:

  1. интерфейс poll не накладывает ограничений на пространство номеров дескрипторов, во всяком случае пока эти номера входят в диапазон представления int.
  2. при большом пространстве номеров дескрипторов (65536 в данном контексте следует считать большим пространством), poll часто требует передачи между пользовательским процессом и ядром меньшего объема данных, чем select.
  3. poll сообщает больше информации о происшедших с дескриптором событиях, чем может сообщить select
  4. У poll входные и выходные значения разнесены по разным полям структуры, так что не требуется полностью пересоздавать массив fds после каждого вызова.





© 2023 :: MyLektsii.ru :: Мои Лекции
Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав.
Копирование текстов разрешено только с указанием индексируемой ссылки на источник.