355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Уильям Ричард Стивенс » UNIX: разработка сетевых приложений » Текст книги (страница 16)
UNIX: разработка сетевых приложений
  • Текст добавлен: 17 сентября 2016, 20:42

Текст книги "UNIX: разработка сетевых приложений"


Автор книги: Уильям Ричард Стивенс


Соавторы: Эндрю М. Рудофф,Билл Феннер

Жанр:

   

ОС и Сети


сообщить о нарушении

Текущая страница: 16 (всего у книги 88 страниц) [доступный отрывок для чтения: 32 страниц]

6.2. Модели ввода-вывода

Прежде чем начать описание функций selectи poll, мы должны вернуться назад и уяснить основные различия между пятью моделями ввода-вывода, доступными нам в Unix:

■ блокируемый ввод-вывод;

■ неблокируемый ввод-вывод;

■ мультиплексирование ввода-вывода (функции selectи poll);

■ ввод-вывод, управляемый сигналом (сигнал SIGIO);

■ асинхронный ввод-вывод (функции POSIX aio_).

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

Как вы увидите в примерах этого раздела, обычно различаются две фазы операции ввода:

1. Ожидание готовности данных.

2. Копирование данных от ядра процессу.

Первый шаг операции ввода на сокете обычно включает ожидание прихода данных по сети. Когда пакет приходит, он копируется в буфер внутри ядра. Второй шаг – копирование этих данных из буфера ядра в буфер приложения.

Модель блокируемого ввода-вывода

Наиболее распространенной моделью ввода-вывода является модель блокируемого ввода-вывода, которую мы использовали для всех предыдущих примеров. По умолчанию все сокеты являются блокируемыми. Используя в наших примерах сокет дейтаграмм, мы получаем сценарий, показанный на рис. 6.1.

Рис. 6.1. Модель блокируемого ввода-вывода

В этом примере вместо TCP мы используем UDP, поскольку в случае UDP признак готовности данных очень прост: получена вся дейтаграмма или нет. В случае TCP он становится сложнее, поскольку приходится учитывать дополнительные переменные, например минимальный объем данных в сокете (low water-mark).

В примерах этого раздела мы говорим о функции recvfromкак о системном вызове, поскольку делаем различие между нашим приложением и ядром. Вне зависимости от того, как реализована функция recvfrom(как системный вызов в ядре, происходящем от Беркли, или как функция, активизирующая системный вызов getmsgв ядре System V), она обычно выполняет переключение между работой в режиме приложения и работой в режиме ядра, за которым через определенный промежуток времени следует возвращение в режим приложения.

На рис. 6.1 процесс вызывает функцию recvfrom, и системный вызов не возвращает управление, пока дейтаграмма не придет и не будет скопирована в буфер приложения либо пока не произойдет ошибка. Наиболее типичная ошибка – это прерывание системного вызова сигналом, о чем рассказывалось в разделе 5.9. Процесс блокирован в течение всего времени с момента, когда он вызывает функцию recvfrom, до момента, когда эта функция завершается. Когда функция recvfromвыполняется нормально, наше приложение обрабатывает дейтаграмму.

Модель неблокируемого ввода-вывода

Когда мы определяем сокет как неблокируемый, мы тем самым сообщаем ядру следующее: «когда запрашиваемая нами операция ввода-вывода не может быть завершена без перевода процесса в состояние ожидания, следует не переводить процесс в состояние ожидания, а возвратить ошибку». Неблокируемый ввод-вывод мы описываем подробно в главе 16, а на рис. 6.2 лишь демонстрируем его свойства.

Рис. 6.2. Модель неблокируемого ввода-вывода

В первых трех случаях вызова функции recvfromданных для возвращения нет, поэтому ядро немедленно возвращает ошибку EWOULDBLOCK. Когда мы в четвертый раз вызываем функцию recvfrom, дейтаграмма готова, поэтому она копируется в буфер приложения и функция recvfromуспешно завершается. Затем мы обрабатываем данные.

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

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

В случае мультиплексирования ввода-выводамы вызываем функцию selectили poll, и блокирование происходит в одном из этих двух системных вызовов, а не в действительном системном вызове ввода-вывода. На рис. 6.3 обобщается модель мультиплексирования ввода-вывода.

Рис. 6.3. Модель мультиплексирования ввода-вывода

Процесс блокируется в вызове функции select, ожидая, когда дейтаграммный сокет будет готов для чтения. Когда функция selectвозвращает сообщение, что сокет готов для чтения, процесс вызывает функцию recvfrom, чтобы скопировать дейтаграмму в буфер приложения.

Сравнивая рис. 6.3 и 6.1, мы не найдем в модели мультиплексирования ввода– вывода каких-либо преимуществ, более того, она даже обладает незначительным недостатком, поскольку использование функции selectтребует двух системных вызовов вместо одного. Но преимущество использования функции select, которое мы увидим далее в этой главе, состоит в том, что мы сможем ожидать готовности не одного дескриптора, а нескольких.

ПРИМЕЧАНИЕ

Разновидностью данного способа мультиплексирования является многопоточное программирование с блокируемым вводом-выводом. Отличие состоит в том, что вместо вызова select с блокированием программа использует несколько потоков (по одному на каждый дескриптор), которые могут блокироваться в вызовах типа recvfrom.

Модель ввода-вывода, управляемого сигналом

Мы можем сообщить ядру, что необходимо уведомить процесс о готовности дескриптора с помощью сигнала SIGIO. Такая модель имеет название ввод-вывод, управляемый сигналом( signal-driven I/O). Она представлена в обобщенном виде на рис. 6.4.

Рис. 6.4. Модель управляемого сигналом ввода-вывода

Сначала мы включаем на сокете управляемый сигналом ввод-вывод (об этом рассказывается в разделе 22.2) и устанавливаем обработчик сигнала при помощи системного вызова sigaction. Возвращение из этого системного вызова происходит незамедлительно, и наш процесс продолжается (он не блокирован). Когда дейтаграмма готова для чтения, для нашего процесса генерируется сигнал SIGIO. Мы можем либо прочитать дейтаграмму из обработчика сигнала с помощью вызова функции recvfromи затем уведомить главный цикл о том, что данные готовы для обработки (см. раздел 22.3), либо уведомить основной цикл и позволить ему прочитать дейтаграмму.

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

Модель асинхронного ввода-вывода

Асинхронный ввод-выводбыл введен в редакции стандарта POSIX.1g 1993 г. (расширения реального времени). Мы сообщаем ядру, что нужно начать операцию и уведомить нас о том, когда вся операция (включая копирование данных из ядра в наш буфер) завершится. Мы не обсуждаем эту модель в этой книге, поскольку она еще не получила достаточного распространения. Ее основное отличие от модели ввода-вывода, управляемого сигналом, заключается в том, что при использовании сигналов ядро сообщает нам, когда операция ввода-вывода может быть инициирована, а в случае асинхронного ввода-вывода – когда операция завершается. Пример этой модели приведен на рис. 6.5.

Рис. 6.5. Модель асинхронного ввода-вывода

Мы вызываем функцию aio_read(функции асинхронного ввода-вывода POSIX начинаются с aio_или lio_) и передаем ядру дескриптор, указатель на буфер, размер буфера (те же три аргумента, что и для функции read), смещение файла (аналогично функции lseek), а также указываем, как уведомить нас, когда операция полностью завершится. Этот системный вызов завершается немедленно, и наш процесс не блокируется в ожидании завершения ввода-вывода. В этом примере предполагается, что мы указали ядру сгенерировать некий сигнал, когда операция завершится. Сигнал не генерируется до тех пор, пока данные не скопированы в наш буфер приложения, что отличает эту модель от модели ввода-вывода, управляемого сигналом.

ПРИМЕЧАНИЕ

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

Сравнение моделей ввода-вывода

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

Рис. 6.6. Сравнение моделей ввода-вывода


Сравнение синхронного и асинхронного ввода-вывода

POSIX дает следующие определения этих терминов:

■ Операция синхронного ввода-вывода блокирует запрашивающий процесс до тех пор, пока операция ввода-вывода не завершится.

■ Операция асинхронного ввода-вывода не вызывает блокирования запрашивающего процесса.

Используя эти определения, можно сказать, что первые четыре модели ввода– вывода – блокируемая, неблокируемая, модель мультиплексирования ввода-вывода и модель управляемого сигналом ввода-вывода – являются синхронными, поскольку фактическая операция ввода-вывода (функция recvfrom) блокирует процесс. Только модель асинхронного ввода-вывода соответствует определению асинхронного ввода-вывода.

6.3. Функция select

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

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

■ любой дескриптор из набора {1, 4, 5} готов для чтения;

■ любой дескриптор из набора {2, 7} готов для записи;

■ любой дескриптор из набора {1, 4} вызывает исключение, требующее обработки;

■ истекает 10,2 с.

Таким образом, мы сообщаем ядру, какие дескрипторы нас интересуют (готовые для чтения, готовые для записи или требующие обработки исключения) и как долго нужно ждать. Интересующие нас дескрипторы не ограничиваются сокетами: любой дескриптор можно проверить с помощью функции select.

ПРИМЕЧАНИЕ

Беркли-реализации всегда допускали мультиплексирование ввода-вывода с любыми дескрипторами. Система SVR3 ограничивала мультиплексирование ввода-вывода дескрипторами, которые являлись устройствами STREAMS (см. главу 31), но это ограничение было снято в SVR4.

#include

#include

int select(int maxfdp1, fd_set * readset, fd_set * writeset,

 fd_set * exceptset, const struct timeval * timeout);

Возвращает: положительное число – счетчик готовых дескрипторов, 0 в случае тайм-аута, -1 в случае ошибки

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

struct timeval {

 long tv_sec;  /* секунды */

 long tv_usec; /* микросекунды */

};

С помощью этого аргумента можно реализовать три сценария:

1. Ждать вечно: завершать работу, только когда один из заданных дескрипторов готов для ввода-вывода. Для этого нужно определить аргумент timeoutкак пустой указатель.

2. Ждать в течение определенного времени: завершение будет происходить, когда один из заданных дескрипторов готов для ввода-вывода, но период ожидания ограничивается количеством секунд и микросекунд, заданным в структуре timeval, на которую указывает аргумент timeout.

3. Не ждать вообще: завершение происходит сразу же после проверки дескрипторов. Это называется опросом( polling). Аргумент timeoutдолжен указывать на структуру timeval, а значение таймера (число секунд и микросекунд, заданных этой структурой) должно быть нулевым.

Ожидание в первых двух случаях обычно прерывается, когда процесс перехватывает сигнал и возвращается из обработчика сигнала.

ПРИМЕЧАНИЕ

Ядра реализаций, происходящих от Беркли, никогда автоматически не перезапускают функцию select [128, с. 527], в то время как ядра SVR4 перезапускают, если задан флаг SA_RESTART при установке обработчика сигнала. Это значит, что в целях переносимости мы должны быть готовы к тому, что функция select возвратит ошибку EINTR, если мы перехватываем сигналы.

Хотя структура timevalпозволяет нам задавать значение с точностью до микросекунд, реальная точность, поддерживаемая ядром, часто значительно ниже. Например, многие ядра Unix округляют значение тайм-аута до числа, кратного 10 мс. Присутствует также и некоторая скрытая задержка: между истечением времени таймера и моментом, когда ядро запустит данный процесс, проходит некоторое время.

ПРИМЕЧАНИЕ

В некоторых системах при задании поля tv_sec более 100 млн с функция select завершается с кодом ошибки EINVAL Это, конечно, достаточно большое число (более трех лет), но факт остается фактом: структура timeval может содержать значения, не поддерживаемые функцией select.

Спецификатор constаргумента timeoutозначает, что данный аргумент не изменяется функцией selectпри ее возвращении. Например, если мы зададим предел времени, равный 10 с, и функция selectвозвратит управление до истечения этого времени с одним или несколькими готовыми дескрипторами или ошибкой EINTR, то структура timevalне изменится, то есть при завершении функции значение тайм-аута не станет равно числу секунд, оставшихся от исходных 10. Чтобы узнать количество неизрасходованных секунд, следует определить системное время до вызова функции select, а когда она завершится, определить его еще раз и вычесть первое значение из второго. Устойчивая программа должна учитывать тот факт, что системное время может периодически корректироваться администратором или демоном типа ntpd.

ПРИМЕЧАНИЕ

В современных системах Linux структура timeval изменяема. Следовательно, в целях переносимости будем считать, что структура timeval по возвращении становится неопределенной, и будем инициализировать ее перед каждым вызовом функции select. В POSIX указывается спецификатор const.

Три средних аргумента, readset, writesetи exceptset, определяют дескрипторы, которые ядро должно проверить на возможность чтения и записи и на наличие исключений (exceptions). В настоящее время поддерживается только два исключения:

1. На сокет приходят внеполосные данные. Более подробно мы опишем этот случай в главе 24.

2. Присутствие информации об управлении состоянием (control status information), которая должна быть считана с управляющего (master side) псевдотерминала, помещенного в режим пакетной обработки. Псевдотерминалы в данном томе не рассматриваются.

Проблема в том, как задать одно или несколько значений дескрипторов для каждого из трех аргументов. Функция selectиспользует наборы дескрипторов, обычно это массив целых чисел, где каждый бит в каждом целом числе соответствует дескриптору. Например, при использовании 32-разрядных целых чисел первый элемент массива (целое число) соответствует дескрипторам от 0 до 31, второй элемент – дескрипторам от 32 до 63, и т.д. Детали реализации не влияют на приложение и скрыты в типе данных fd_setи следующих четырех макросах:

void FD_ZERO(fd_set * fdset); /* сбрасываем все биты в fdset*/

void FD_SET(int fd, fd_set * fdset); /* устанавливаем бит для fdв fdset*/

void FD_CLR(int fd, fd_set * fdset); /* сбрасываем бит для fdв fdset*/

int FD_ISSET(int fd, fd_set * fdset); /* установлен ли бит для fdв fdset? */

Мы размещаем в памяти набор дескрипторов типа fd_set, с помощью этих макросов устанавливаем и проверяем биты в наборе, а также можем присвоить его (как значение) другому набору дескрипторов с помощью оператора присваивания языка С.

ПРИМЕЧАНИЕ

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

В разделе 6.10 мы увидим, что функция poll использует совершенно другое представление: массив структур переменной длины, по одной структуре для каждого дескриптора.

Например, чтобы определить переменную типа fd_setи затем установить биты для дескрипторов 1, 4 и 5, мы пишем:

fd_set rset;

FD_ZERO(&rset); /* инициализируем набор все биты сброшены */

FD_SET(1, &rset); /* устанавливаем бит для fd 1 */

FD_SET(4, &rset); /* устанавливаем бит для fd 4 */

FD_SET(5, &rset); /* устанавливаем бит для fd 5 */

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

Любой из трех средних аргументов функции selectreadset, writesetили exceptset – может быть задан как пустой указатель, если нас не интересует определяемое им условие. На самом деле, если все три указателя пустые, мы просто получаем таймер большей точности, чем обычная функция Unix sleep(позволяющая задавать время с точностью до секунды). Функция pollобеспечивает аналогичную функциональность. На рис. С.9 и С.10 [110] показана функция sleep_us, реализованная с помощью функций selectи poll, которая позволяет устанавливать время ожидания с точностью до микросекунд.

Аргумент maxfdp1задает число проверяемых дескрипторов. Его значение на единицу больше максимального номера проверяемого дескриптора (поэтому мы назвали его maxfdp1). Проверяются дескрипторы 0, 1, 2 и далее до maxfdp1– 1 включительно.

Константа FD_SETSIZE, определяемая при подключении заголовочного файла , является максимальным числом дескрипторов для типа данных fd_set. Ее значение часто равно 1024, но такое количество дескрипторов используется очень немногими программами. Аргумент maxfdp1заставляет нас вычислять наибольший интересующий нас дескриптор и затем сообщать ядру его значение. Например, в предыдущем коде, который включает дескрипторы 1, 4 и 5, значение аргумента maxfdp1равно 6. Причина, по которой это 6, а не 5, в том, что мы задаем количество дескрипторов, а не наибольшее значение, а нумерация дескрипторов начинается с нуля.

ПРИМЕЧАНИЕ

Зачем нужно было включать этот аргумент и вычислять его значение? Причина в том, что он повышает эффективность работы ядра. Хотя каждый набор типа fd_set может содержать множество дескрипторов (обычно до 1024), реальное количество дескрипторов, используемое типичным процессом, значительно меньше. Эффективность возрастает за счет того, что не копируются ненужные части набора дескрипторов между ядром и процессом и не требуется проверять биты, которые всегда являются нулевыми (см. раздел 16.13 [128]).

Функция selectизменяет наборы дескрипторов, на которые указывают аргументы readset, writesetи exceptset. Эти три аргумента являются аргументами типа «значение-результат». Когда мы вызываем функцию, мы указываем интересующие нас дескрипторы, а по ее завершении результат показывает нам, какие дескрипторы готовы. Проверить определенный дескриптор из структуры fd_setпосле завершения вызова можно с помощью макроса FD_ISSET. Для дескриптора, не готового для чтения или записи, соответствующий бит в наборе дескрипторов будет сброшен. Поэтому мы устанавливаем все интересующие нас биты во всех наборах дескрипторов каждый раз, когда вызываем функцию select.

ПРИМЕЧАНИЕ

Две наиболее общих ошибки программирования при использовании функции select – это забыть добавить единицу к наибольшему номеру дескриптора и забыть, что наборы дескрипторов имеют тип «значение-результат». Вторая ошибка приводит к тому, что функция select вызывается с нулевым битом в наборе дескрипторов, когда мы думаем, что он установлен в единицу.

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

ПРИМЕЧАНИЕ

В ранних реализациях SVR4 функция select содержала ошибку: если один и тот же бит находился в нескольких наборах дескрипторов – допустим, дескриптор был готов и для чтения, и для записи, – он учитывался только один раз. В современных реализациях эта ошибка исправлена.


    Ваша оценка произведения:

Популярные книги за неделю