Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

СПО_1 / СПО / Sozdanie.setevyh.prilojenii.v.srede.Linux

.pdf
Скачиваний:
79
Добавлен:
11.04.2015
Размер:
2.94 Mб
Скачать

int ready=0, bytes, size=0, pos=0;

void sig_io(int sig)

{

if ( size > 0 )

{

bytes = send

(client, buffer, size+pos, 0);

if ( bytes <

0 )

{

 

 

 

pos += bytes;

 

 

 

 

 

}

ready = 1;

 

 

 

}

 

 

 

 

 

 

/* —

Разрешаем

асинхронный,

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

и — */

/* —

заявляем

о готовности

обрабатывать сигнал SIGIO

— */

while (

Idone )

 

 

 

if

(

/*** канал доступен

***/

)

 

 

/***

отправляем сообщение

***/

 

else

 

 

 

 

 

 

}

/***

помещаем сообщение в очередь ***/

 

 

 

 

 

 

 

 

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

Подключение по запросу

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

/*** Пример подключения по запросу: установление соединения

***/

/***

в обработчике сигналов. В основной программе в цикле

***/

/***

опрашиваются все сокеты. (Взято из файла demand accept.с

***/

/*** на Web узле.)

***/

int Connections[MAXCONNECTIONS]; int sd, NumConnections=0;

void sig_io(int sig)

{int client;

Глава 8. Механизмы ввода вывода

181

Ⱦɚɧɧɚɹ ɜɟɪɫɢɹ ɤɧɢɝɢ ɜɵɩɭɳɟɧɚ ɷɥɟɤɬɪɨɧɧɵɦ ɢɡɞɚɬɟɥɶɫɬɜɨɦ %RRNV VKRS Ɋɚɫɩɪɨɫɬɪɚɧɟɧɢɟ ɩɪɨɞɚɠɚ ɩɟɪɟɡɚɩɢɫɶ ɞɚɧɧɨɣ ɤɧɢɝɢ ɢɥɢ ɟɟ ɱɚɫɬɟɣ ɁȺɉɊȿɓȿɇɕ Ɉ ɜɫɟɯ ɧɚɪɭɲɟɧɢɹɯ ɩɪɨɫɶɛɚ ɫɨɨɛɳɚɬɶ ɩɨ ɚɞɪɟɫɭ piracy@books-shop.com

/*— прием запросов на подключение; если запросов —*/

/*—

слишком много, выдаем сообщение

об ошибке

—*/

/* —

и разрываем связь

 

—*/

if ( (client = acceptfsd, 0, 0) > 0 )

 

 

 

if ( NumConnections < MAXCONNECTIONS )

 

 

Connections [NumConnections++]

= client;

 

 

else

 

 

{

sendfclient, "Too many connections!\n, 22, 0"); close(client);

}

else

perror("Accept");

}

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

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

Устранение нежелательного

блокирования с помощью функций

роll() и select()

В Linux есть две функции, которые помогают работать с несколькими откры тыми каналами одновременно. Они обеспечивают более высокую эффективность (и более просты в работе), чем при самостоятельном опросе каналов. Идея этих функций заключается в том, что системный вызов блокируется до тех пор, пока не изменится состояние любого из каналов.

Под изменением состояния подразумеваются различные события, включая на личие данных для чтения, доступность канала для записи и возникновение ошибки. Если функция завершилась, значит, состояние одного или нескольких каналов изменилось. Обе функции возвращают число каналов, подвергшихся из менению.

Функция select () довольно сложна. С ней связан ряд дополнительных макро сов, управляющих списками дескрипторов.

#include <sys/time.h>

 

#include <sys/types.h>

 

linclude <unistd.h>

 

int select (int maxfd, fd_set

*to_read, fd_set *to write,

fd_set *except, struct timeval *timeout);

182

Часть II. Создание серверных приложений

www.books-shop.com

FD_CLR(int fd, fd_set *set);

/*. удаляет дескриптор из списка

*/

FD_ISSET(int fd, fd_set *set);

/* проверяет наличие

 

 

 

дескриптора

в списке

*/

FD_SET(int fd, fd_set *set);

/* добавляет дескриптор

в список

*/

FD_ZERO(fd_set *set);

/* инициализирует список дескрипторов

*/

Описание параметров функции select() приведено в табл. 8.1.

Таблица 8.1. Параметры функции select() и связанных с ней макросов

Параметр

Описание

maxfd

Число, на единицу большее номера самого старшего дескриптора в списке

to_read

Список дескрипторов каналов, из которых производится чтение данных

to_write

Список дескрипторов каналов, в которые осуществляется запись данных

except

Список дескрипторов каналов, предназначенных для чтения приоритетных со

 

общений

timeout

Число микросекунд, в течение которых необходимо ждать

f d

Дескриптор добавляемого, удаляемого или проверяемого канала

set

Список дескрипторов

Параметр maxfd равен порядковому номеру старшего дескриптора в списке плюс 1. Каждому заданию выделяется пул дескрипторов ввода вывода (обычно 1024), а каждому из его каналов назначается один дескриптор (первыми идут стандартные каналы: stdin — 0, stdout — 1, stderr — 2). Если в список to_read входят дескрипторы [3,6], в список to_write — дескрипторы [4—6], а в список except — дескрипторы [3,4], то параметр maxfd будет равен 6 (номер старшего де скриптора) плюс 1,т.е. 7.

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

NULL — функция ждет бесконечно долго;

положительное число — функция ждет указанное число микросекунд;

нуль — после проверки всех каналов функция немедленно завершается.

Предположим, в программе открыты три канала с дескрипторами [3,4,6], по которым посылаются данные:

/***************************************************************/

/***

Пример функции selectf)

***/

/***************************************************************/

int count;

 

 

fd_set set;

 

 

struct timeval timeout;

 

FD_ZERO(&set);

/* очищаем список */

 

FD_SET(3, &set); /* добавляем канал #3 */

 

FD_SET(4, &set); /* добавляем канал #4 */

 

FD_SET(5, &set); /* добавляем канал #5 */

 

timeout.tv_sec = 5; /* тайм аут длится 5,25 с */

 

Глава 8. Механизмы ввода вывода

183

www.books-shop.com

timeout.tv_usec = 250000;

 

/*— Ожидаем завершения функции

select() —*/

if ( (count = select(6+l, &set, 0,

0, &itimeout)) > 0 )

/*** Находим канал, состояние которого изменилось ***/

else if ( count == 0 )

 

fprintf(stderr, "Timed out!");

 

else

 

perror("Select");

 

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

Функция poll() проще, чем select(), и ею легче управлять. В ней использует ся массив структур, определяющих поведение функции:

struct pollfd

{

int fd;

/* проверяемый дескриптор */

short events;

/* интересующие нас события */

short revents;

/* события, которые произошли в канале */

}

В первом поле, fd, содержится дескриптор файла или сокета. Второе и третье поля, events и revents, являются битовыми масками, определяющими события, за которыми требуется следить.

POLLERR. Любая ошибка. Функция завершается, если в канале возникла ошибка.

POLLHUP. Отбой на другом конце канала. Функция завершается, если кли ент разрывает соединение.

POLLIN. Поступили данные. Функция завершается, если во входном бу фере имеются данные.

POLLINVAL. Канал fd не был открыт. Функция завершается, если канал не является открытым файлом или сокетом.

POLLPRI. Приоритетные сообщения. Функция завершается, если поступи ло приоритетное сообщение.

POLLOUT. Канал готов. Функция завершается, если вызов функции write () не будет блокирован.

Объявление функции poll() выглядит так:

linclude <sys/poll.h>

int poll(struct pollfd *list, unsigned int cnt, int timeout);

Программа заполняет массив структур pollfd, прежде чем вызвать функцию. Параметр cnt определяет число дескрипторов в массиве. (Учтите, что массив де скрипторов должен быть непрерывным. Если один из каналов закрывается, необ ходимо переупорядочить, т.е. сжать, массив. Не во всех системах функция poll()

184

Часть II. Создание серверных приложений

www.books-shop.com

может работать с пустыми структурами.) Параметр timeout аналогичен одноимен ному параметру функции select(), но выражается в миллисекундах.

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

Реализация тайм аутов

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

путем задания параметра timeout в функции select() или poll();

путем посылки программе сигнала SIGALRM по истечении заданного вре мени.

Поддержкатайм аутовдлясокетоввLinux

Среди атрибутов сокета есть значения тайм аутов для операций чтения (SO_RCVTIMEO) и записи (SQ_SNDTIMEO). Ксожалению, в настоящеевремя эти атрибуты нельзя модифицировать.

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

Подобная ситуация может представлять проблему. Предположим, что имеются три канала, по одному из которых связь очень плохая. Посредством этих функ ций невозможно определить, что произошло в проблемном канале: тайм аут или закрытие из за отсутствия связи. Можно самостоятельно определять время рабо ты функции и сравнивать его с параметром timeout, но это требует большого ко личества дополнительных действий.

В качестве альтернативы можно воспользоваться сигналом таймера SIGALRM. Таймер — это часы, которые запущены в ядре. Когда таймер срабатывает, ядро посылает заданию команду пробуждения в виде сигнала. Если задание в этот мо мент находится в ожидании завершения системного вызова, сигнал прерывает со ответствующую функцию, вызывается обработчик сигналов и генерируется ошиб ка EINTR.

Любое задание может послать сигнал самому себе, но оно должно знать, как его обрабатывать. В некоторых системах тайм аут задается с помощью функции alarm(), но предварительно следует включить обработку сигнала SIGALRM. Рассмот

рим пример:

 

/*** Пример реализации тайм аута с помощью функции

alarm(). ***/

/*** (Взято из файла echo timeout.с на Web узле.)

***/

/**************************************************************/

int sig_alarm(int sig)

 

{/*** ничего не делаем ***/}

 

Глава 8. Механизмы ввода вывода

185

www.books-shop.com

void

reader()

 

 

 

 

{

struct

sigaction act;

 

 

 

/* —

Инициализируем

«структуру

— */

 

bzero(&act, sizeof(act));

 

 

 

act.sa_handler = sig_alarm;

 

 

act.sa_flags = SA_ONESHOT;

 

 

 

/* —

Активизируем

обработчик сигналов,—*/

 

if ( sigaction (SIGALARM, &act, 0) != 0 )

 

else

perror( "Could not set up timeout");

 

/* —

Если

обработчик

активизирован, — */

 

 

 

 

/* —

запускаем

таймер

— */

 

 

 

alarm(TIMEOUT_SEC);

 

 

 

/* —

Вызываем

функцию, которая

может — */

 

/* —

завершиться по

тайм ауту

— */

 

if ( recv(sd, buffer, sizeof(buffer), 0) < 0 )

 

{

if (errno == EINTR)

 

 

 

 

 

 

 

}

 

perror ("Timed out!");

 

}

 

 

 

 

 

 

 

 

 

 

 

 

 

В этой программе активизируется обработчик сигналов, запускается таймер, после чего вызывается функция recv(). Если за указанный промежуток времени функция не прочитает данные, программа получит сигнал SIGALRM. Выполнение функции recv() будет прервано, а в библиотечную переменную errno запишется код ошибки EINTR.

Чтобы добиться этого, из поля sa_flags

структуры sigaction следует

удалить

флаг SA_RESTART. В главе 7, "Распределение

нагрузки: многозадачность",

говори

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

Но как

видно из примера, это не относится к режиму тайм аутов.

И последнее замечание: избегайте использовать функцию alarm() вместе с системным вызовом sleep(), так как это может привести к возникновению про блем. Следует либо обрабатывать сигнал SIGALRM, либо вызывать функцию sleep(), но не смешивать их.

Резюме: выбор методик ввода$ вывода

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

1 86 Часть II. Создание серверных приложений

www.books-shop.com

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

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

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

Глава 8. Механизмы ввода вывода

187

www.books-shop.com

Глава Повышение 9 производительности

В этой главе...

 

Подготовка к приему запросов на подключение

194

Расширение возможностей сервера с помощью

 

функции select()

200

Анализ возможностей сокета

206

Восстановление дескриптора сокета

212

Досрочная отправка: перекрытие сообщений

213

Проблемы файлового ввода вывода

213

Ввод вывод по запросу: рациональное

 

использование ресурсов процессора

214

Отправка приоритетных сообщений

215

Резюме

217

www.books-shop.com

Как добиться максимальной производительности сервера или клиента? В биб лиотеке Socket API имеется ряд средств, позволяющих решить эту задачу доста точно просто. Тем не менее следует рассмотреть проблему под разными углами, поскольку все ее аспекты тесно связаны друг с другом.

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

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

Подготовка к приему запросов на

подключение

К настоящему моменту мы узнали, как создать сервер, как сделать его много задачным и как управлять вводом выводом. В главе 7, "Распределение нагрузки: многозадачность", рассказывалось о том, как поручать задачу приема и обслужи вания запросов на подключение дочерним процессам, создавая их по мере необ ходимости. Однако в некоторых системах создавать каждый раз новое задание расточительно с точки зрения времени и ресурсов. Да и не всегда хочется писать отдельную программу, управляющую только сетевыми клиентами. В любом слу чае необходимо контролировать как размер серверной программы, так и число одновременных подключений, что требует дополнительного программирования.

Ограничение числа клиентских соединений

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

/**************************************************************/

/*** Стандартный алгоритм обслуживания клиентов ***/

/**************************************************************/

int sd;

struct sockaddr_in addr;

if ( (sd = socket(PF_INET, SOCK_STREAM, 0)) < 0 ) PANIC("socket() failed");

bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(MY_PORT);

Глава 9. Повышение производительности

189

www.books-shop.com

addr.sin_addr = INADDR_ANY;

if (bind(sd, &addr, sizeof(addr)) != 0 ) PANIC("bind() failed");

if (listen(sd, 20) != 0) PANIC ("listen() failed");

for (;;}

{int client, len=sizeof(addr); client = accept(sd, &addr, &len); if (client > 0)

{ int pid;

if ( (pid = fork()) == 0 )

{

close(sd);

Child(client); /* Обслуживаем нового клиента */

}

else if( pid > 0 ) close(client);

else

perror("fork() failed");

} }

Этот алгоритм приводился в главе 7, "Распределение нагрузки: многозадач ность". Он хорошо подходит для обслуживания длительных соединений, таких как процесс регистрации пользователя в системе, сеанс работы с базой данных и т.д. Однако у него есть один существенный недостаток: что если исчерпается список доступных идентификаторов процессов? Что произойдет, если ресурсов ОЗУ окажется недостаточно для обслуживания существующего числа процессов и программе придется выгружать часть данных в файл подкачки? Эта проблема ха рактерна для процессов "кроликов".

Разница между ОЗУ и виртуальной памятью

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

Можно осуществлять подсчет числа процессов: /***************************************************************/

/***

Алгоритм, в котором ограничено число потомков.

***/

/***

(Взято из файла capped servlets.c на Web узле.)

***/

/***************************************************************/ int ChildCount=0;

void sig_child(int sig)

{

wait(O);

ChildCount —;

}

190

Часть II. Создание серверных приложений

www.books-shop.com

Соседние файлы в папке СПО
  • #
    11.04.201527.19 Mб69Cpp4Unix.pdf
  • #
    11.04.201516.44 Mб52IP Arhitektura, protokoly, realizatsiya (vklyuchaya IP versii s IP Security).djvu
  • #
  • #
    11.04.201510.72 Mб51Стивенс. UNIX. Разработка сетевых приложений.djvu