WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 | 2 || 4 | 5 |   ...   | 8 |

«Создание сетевых приложений в среде Linux Руководство разработчика Шон Уолтон Москва • Санкт Петербург • Киев 2001 ББК 32.973.26 018.2.75 УДК 681.3.07 Издательский дом "Вильяме" По общим вопросам ...»

-- [ Страница 3 ] --

Можно также узнать, кто устанавливает соединение с сервером, поскольку в функцию accept() передается информация о клиенте. Аналогичный процесс рас сматривался в главе 4, "Передача сообщений между одноранговыми компьютера ми", когда функция recvfrom() получала не только данные, но и указатель на ад рес отправителя.

#include #include int accept(int sd, sockaddr *addr, int *addr_size);

Как всегда, параметр sd является дескриптором сокета. Во втором параметре возвращается адрес клиента и номер порта, а в третьем — размер структуры sockaddr. В отличие от функции recvfrom(), последние два параметра являются необязательными. Если в программе не требуется знать адрес клиента, задайте эти параметры равными нулю.

Необходимо убедиться, что размер буфера адреса достаточен для размещения в нем полученной адресной структуры. Беспокоиться о повреждении данных из за переполнения буфера не стоит: функция задействует ровно столько байтов, сколько указано в третьем параметре. Параметр addr_size передается по ссылке, поэтому программа может легко узнать реальный размер полученной структуры (листинг 6.4).

Листинг 6.4. Пример функции accept () /****************************************************************/ /*** Пример функции accept(): ожидание и принятие запросов ***/ /*** на подключение от клиентов ***/ /****************************************************************/ int sd;

struct sockaddr_in addr;

/*** Создание сокета, привязка его к порту и перевод в режим прослушивания ***/ for (;

;

) /* цикл повторяется бесконечно */ { int clientsd;

/* новый дескриптор сокета */ int size = sizeof(addr);

/* вычисление размера структуры */ 122 Часть П. Создание серверных приложений www.books-shop.com clientsd = accept(sd, &addr, &size);

/* ожидание подключения */ if { clientsd > 0 ) /* ошибок нет */ { /*** Взаимодействие с клиентом ***/ close(clientsd);

/* очистка и отключение */ } else /* произошла ошибка */ perror("Accept");

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

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

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

Листинг 6.5. Пример функции accept( ) с регистрацией подключений /****************************************************************/ /*** Расширенный пример функции accept(): информация ***/ /*** о каждом новом подключении отображается на экране ***/ /****************************************************************/ /*** (Внутри цикла) ***/ client = accept(sd, saddr, &size);

if ( client > 0 ) { if ( addr.sin_family == AF_INET) printf( "Connection [%s]: %s:%d\n", /* регистрация */ ctime(time(0) ), /* метка времени */ ntoa(addr.sin_addr), ntohs(addr.sin_port));

/* — взаимодействие с клиентом — */ Если в процессе выполнения функции accept() происходит ошибка, функция возвращает отрицательное значение. В противном случае создается новый деск риптор сокета. Ниже перечислены коды возможных ошибок.

Глава 6. Пример сервера www.books-shop.com • EBADF. Указан неверный дескриптор сокета.

• EOPNOTSUPP. При вызове функции accept() сокет должен иметь тип SOCK_STREAM.

• EAGAIN. Сокет находится в режиме неблокируемого ввода вывода, а оче редь ожидания пуста. Функция accept() блокирует работу программы, если не включен данный режим.

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

Листинг 6.6. Пример эхо сервера /*** Пример эхо сервера: возврат полученного сообщения ***/ /*** до тех пор пока не поступит команда "bye" ***/ /*** (Внутри цикла после функции accept()) ***/ • • • if ( client > 0 ) { char buffer[1024];

int nbytes;

do { nbytes recv(client, buffer, sizeof(buffer), 0);

if ( nbytes > 0 ) /* если получены данные, возвращаем их */ send(client, buffer, nbytes, 0);

} while ( nbytes > 0 && strncmp("bye\r", buffer, 4) 1=0);

close(client);

} Заметьте, что признаком окончания сеанса является строка "bye\r", а не "bye\n". В общем случае это зависит от того, как выполняется обработка входного потока. Из соображений надежности следует проверять оба случая. Попробуйте протестировать данную программу, использовав в качестве клиента утилиту Telnet.

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

124 Часть //. Создание серверных приложений www.books-shop.com Какая программа должна начинать передачу первой?

БОЛЬШИНСТВО серверов первыми начинают сеанс. Но в некоторых системах с высоким уровнем безопасности предполагается, что клиент должен отправить первое сообщение. Сервер может заставить клиента идентифицировать себя (указать не только адрес узла и порт).

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

Нужно ли это клиенту? Не замедлит ли это работу?

Какая программа должна управлять диалогом?

Чаше всего диалогом с сервером управляют клиенты. Клиент подключается к серверу и посылает запросы. Сервер, в свою очередь, обрабатывает запросы и вы дает ответ.

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

Какой уровень сертификации требуется?

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

Процесс сертификации включает передачу имени пользователя и пароля.

Кроме того, может потребоваться наличие цифрового сертификата. В главе 16, "Безопасность сетевых приложений", описывается протокол SSL (Secure Sockets Layer — протокол защищенных сокетов), а также рассматриваются вопросы безо пасности.

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

Какой тип данных используется?

БОЛЬШИНСТВО серверов использует кодировку ASCII, а большинство Web страниц представлено в формате текст/HTML. Полезно задать себе вопросы:

"Является ли это наиболее эффективной формой представления данных?" и "Поддерживает ли клиент сжатие данных?" Подумайте, как можно уменьшить задержки на сервере, в сети и в клиентской системе.

Сжатие данных имеет существенные преимущества. Компрессированные ASCII потоки уменьшаются в размере на 50 80%.

Глава 6. Пример сервера www.books-shop.com Как следует обрабатывать двоичные данные?

Передавать двоичные данные — особенно в сжатом виде — намного эффек тивнее, чем работать с ASCII текстом. Но существует одна проблема: некоторые сети поддерживают только 7 битовую кодировку байта. Такие сети являются пе режитками прошлого, но их все еще слишком дорого демонтировать. К счастью, маршрутизаторы, подключенные к этим сетям, выявляют подобную несовмести мость и автоматически преобразуют данные в том или ином направлении. Это требует дополнительного времени и замедляет продвижение пакетов. Дополни тельная нагрузка ложится также на узел получатель, так как он должен восста навливать данные.

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

Как обнаружить взаимоблокировку?

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

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

Необходима ли синхронизация по таймеру?

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

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

Одно из решений проблемы синхронизации заключается в том, чтобы прово дить все транзакции на сервере (максимально снимая ответственность с клиента).

Когда сервер завершает транзакцию, он сообщает клиенту дату и время ее окон чания.

Как и когда переустанавливать соединение?

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

126 Часть II. Создание серверных приложений www.books-shop.com В TCP/IP существует понятие приоритетного сообщения, с помощью которого можно просигнализировать об отмене. Подробная информация о приоритетных сообщениях и передаче внеполосных данных приводится в главе 9, "Повышение производительности". Но отправка приоритетного сообщения — это только пол дела: как сервер, так и клиент должны вернуться к некой начальной точке, что представляет собой серьезную проблему в структурном программировании.

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

Когда завершать работу?

Итак, между клиентом и сервером установлено соединение и передаются па кеты. Пришло время прощаться. Определить конец сеанса может быть не так просто, как кажется. Например, при взаимодействии с HTTP сервером сеанс за вершается в момент получения двух символов новой строки. Но иногда запроса от клиента можно ждать бесконечно, если чтение данных осуществляется с по мощью функции read() или recv(), а буфер имеет недостаточный размер. В этом случае сервер превысит время ожидания и объявит о разрыве соединения.

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

Более сложный пример: сервер HTTP Эхо сервер представляет собой отличный отправной пункт для создания раз личных видов серверов. Одним из них является HTTP сервер. Полная его реали зация выходит за рамки данной книги, но можно создать уменьшенный вариант сервера, который отвечает на запросы любого броузера. Текст этого примера име ется на Web узле (файл html ls server.c).

Сервер генерирует HTML код динамически, а не загружает его из файла. Это упрощает программу (листинг 6.7).

Листинг 6.7. Пример простого НИР сервера /**************************************************************/ /*** Простой HTTP сервер ***/ /**************************************************************/ while(1) { int client;

int size = sizeof(addr);

client = accept(sd, &addr, &size);

if ( client > 0 ) { char buffer[1024];

/* Сообщение клиенту */ char *reply = "Hello!/n";

Глава 6. Пример сервера www.books-shop.com bzero(buffer, sizeof(buffer) );

/* очистка буфера */ recv(client, buffer, sizeof(buffer), 0);

/* получение сообщения */ send(client, reply, strlen(reply), 0);

/* ответ клиенту */ /* — отображение клиентского сообщения — */ fprintf(stderr, "%s", buffer);

close (client);

} else perror( "Accept");

Каждый раз, когда клиент подключается, он посылает запрос примерно такого вида:

GET /dir /document HTTP/ 1. (определение протокола) Первая строка представляет собой запрос. Все последующие сообщения ин формируют сервер о том, какого рода данные готов принимать клиент. В первую строку может входить конфигурационная информация, позволяющая серверу оп ределить, как следует взаимодействовать с клиентом. Метод GET принимает два параметра: собственно запрос и используемый протокол. Сервер может анализи ровать запрос, выделяя имя каталога и имя документа (естественно, запросы бы вают гораздо более сложными). Протокол HTTP 1.0 допускает наличие пробелов в путевом имени, поэтому запрос включает в себя все, что находится от началь ного символа косой черты до строки HTTP/.

При ответе сервер может дополнительно передавать MIME заголовок, указы вая на статус сообщения и тип возвращаемого документа:

НТТР/1.1 200 ОК Content Type: text/html (пустая строка) (пустая строка) Первая строка является статусной. Она информирует клиента о том, насколь ко успешно выполнен запрос. Именно здесь передается печально известное со общение об ошибке 404 ("Not Found"). Полный список кодов завершения HTTP 1.1 представлен в приложении А, "Информационные таблицы".

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

При написании HTTP сервера следует учитывать ряд моментов. В частности, сервер не знает заранее, насколько большим получится результирующий документ, поэтому дескриптор сокета необходимо привести к типу FILE* (листинг 6.8).

128 Часть II. Создание серверных приложений www.books-shop.com Листинг 6.8. Расширенный алгоритм HTTP сервера /**************************************************************/ /*** Пример сервера HTTP 1.0: устанавливаем соединение, ***/ /*** принимаем запрос, открываем каталог и создаем ***/ /*** для него список файлов в формате HTML ***/ /**************************************************************/ /*** Создание сокета, привязка его к порту и перевод в режим прослушивания ***/ for(;

;

) { int client;

int size = sizeof(addr);

client = accept(sd, &addr, &size);

/* ожидание запроса на подключение */ if ( client > 0 ) { char buf[1024];

FILE *clientfp;

bzero(buf, sizeof(buf));

/* очистка буфера */ recv(client, buf, sizeof(buf), 0) ;

/* получение сообщения */ clientfp = fdopen(client, "w");

/* приведение к типу FILE* */ if ( clientfp != NULL ) /* если преобразование прошло успешно */ { /**** Извлекаем путевое имя из сообщения ****/ /**** открываем каталог ****/ /**** для каждого файла... ****/ /**** Читаем имя файла ****/ /**** Генерируем HTML таблицу ****/ fclose(clientfp);

/* закрываем указатель на файл */ } else perror("Client FILE");

/* приведение к типу FILE* невозможно */ close(client);

/* закрываем клиентский сокет */ } else perror ("Accept") ;

/* ошибка в функции accept () */ Эту программу можно улучшить, сортируя список файлов по именам, распо знавая тип каждого файла, добавляя коды ошибок HTTP 1.1 и т.д.

Глава 6. Пример сервера www.books-shop.com Резюме: базовые компоненты сервера В этой главе рассматривались основы создания серверных приложений. Серве ры позволяют централизованно управлять данными и операциями, обслуживать множество клиентов и распределять нагрузку между ними.

В серверных приложениях используются три новые функции: bindf), listen() и accept() — помимо тех функций, которые обычно вызываются клиентами. С помощью этих функций осуществляется выбор номера порта (bind()), создание очереди подключений (listen()) и прием запросов на подключение (accept()).

Функция accept() создает новый сокет для каждого соединения, позволяя про грамме обслуживать несколько соединений по одному порту.

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

130 Часть II. Создание серверных приложений www.books-shop.com Глава Распределение нагрузки многозадачность В этой главе...

Понятие о многозадачности: процессы и потоки Обгоняя время: исключающие семафоры и гонки Управление дочерними заданиями и задания зомби Расширение существующих версий клиента и сервера Вызов внешних программ с помощью функций семейства ехес() Резюме piracy@books-shop.com Представим себе, что мы одновременно выполняем множество различных за дач. Работая в параллельном режиме, мы сосредоточиваем внимание на каждой конкретной задаче, не отвлекаясь на остальные. В действительности наш мозг способен делать это. Например, можно мыть пол, одновременно продумывая сложный программный.алгоритм, или решать кроссворд, слушая музыку. В про граммировании это называется многозадачностью.

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

А как насчет того, чтобы подключаться сразу к нескольким серверам?

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

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

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

Для ясности термин задание употребляется в данной главе по отношению к любому исполняемому системному объекту. Термины процесс и поток обозначают конкретные разновидности заданий.

Понятие о многозадачности:

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

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

Каждое задание хранит свою информацию в нескольких разделах памяти (страницах). Операционная система назначает заданию таблицу страниц — набор страниц, каждая из которых выполняет отдельную функцию. Работа со страница ми осуществляется через подсистему виртуальной памяти, которая реализуется в 132 Часть П. Создание серверных приложений www.books-shop.com виде таблицы, преобразующей программные адреса в физические. Когда опера ционная система начинает переключать задания, она сохраняет информацию о текущем задании — контекст — и загружает в виртуальную память таблицу стра ниц следующего задания.

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

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

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

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

На рис. 7.1 представлено, какие компоненты задания допускают совместное использование. В любом случае совместный доступ к стеку и контексту задания запрещен. В текущей реализации библиотеки потоковых функций потоку разре шается использовать все, кроме идентификатора процесса (PID — process ID).

При переключении заданий операционная система заменяет текущую таблицу страниц таблицей активизируемого задания. Это может потребовать нескольких циклов работы процессора. Обычно переключение занимает от 1 мкс до 0,1 мс, в зависимости от процессора и тактовой частоты. Задержка бывает достаточно большой, особенно если переключение осуществляется 100 раз в секунду (каждые 10 мс). Любое задание занимает долю процессорного времени, часть которого от водится на собственно переключение задания.

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

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

Если программа написана с учетом многозадачности, то в мультипроцессорной системе она получает дополнительное ускорение. (На момент написания книги в Linux могло одновременно поддерживаться максимум 16 процессоров.) Глава 7. Распределение нагрузки: многозадачность www.books-shop.com Рис. 7.1. Задания в Linux имеют несколько областей памяти Когда следует применять многозадачный режим Когда необходима многозадачность? В общем случае пользователь должен всегда контролировать выполнение программы. Иногда программа вынуждена ждать завершения других операций, и переход в многозадачный режим позволяет ей продолжить взаимодействие с пользователем во время простоя. Подобно тому как броузер Netscape позволяет вызывать команды меню в процессе загрузки Web страницы, родительская программа должна поручать все операции сетевого ввода вывода дочерним заданиям. Учитывая, что у различных серверов разное время ответа, можно эффективнее организовать использование сетевого канала, если с каждым сервером связать отдельный поток загрузки данных.

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

• делать другую работу — обрабатывать информацию или поручать зада ния другим программам;

• взаимодействовать с пользователем — принимать от него данные или отображать информацию о состоянии;

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

134 Часть //. Создание серверных приложений www.books-shop.com Смешение потоков и процессов в рамках одной программы может показаться непривычным. Однако так часто происходит в больших интерактивных приложе ниях. В броузерах, к примеру, каждое окно является отдельным процессом, а для каждого запроса, такого как загрузка страницы, создается несколько потоков.

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

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

Каждое задание использует память и другие ресурсы ввода вывода. Большин ство программ работает с информацией большего объема, чем может вместить контекст задания (16—32 регистра). Эта информация размещается в ОЗУ и файле подкачки.

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

• stdin — стандартный входной поток (только для чтения), обычно свя занный с клавиатурой;

• stdout — стандартный выходной поток (только для записи), обычно свя занный с экраном;

• stderr — стандартный поток ошибок (только для записи), обычно свя занный с экраном или журнальным файлом.

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

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

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

Глава 7. Распределение нагрузки: многозадачность www.books-shop.com Планирование заданий в Linux В многозадачных операционных системах применяются различные методики планирования зада ний. В Linux используется схема приоритетного кругового обслуживания. В этой схеме каждое задание по очереди получает свою долю процессорного времени. Задания с высоким приорите том перемещаются по списку быстрее, чем те, у которых низкий приоритет.

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

Процессы Потоки После успешного вызова функции fork() сущест Родительская программа указывает имя функции, которая будет выполняться в качестве дочернего вуют два процесса, выполняющихся параллельно потока Дочерний процесс должен быть явно завершен с Дочерний поток можно завершить явно либо неявно помощью системного вызова exit () с помощью функции pthread_exit(void* arg) или инструкции return Общих данных нет;

единственная информация, пе Потомок имеет доступ к данным предка, принимая редаваемая потомку, — это снимок данных роди от него параметры и возвращая значения тельского процесса Дочерний процесс всегда связан с родительским;

Дочерний поток может выполняться независимо от когда процесс потомок завершается, его предок родительского и завершиться без его вмешательст должен произвести очистку ва (если поток не является независимым, родитель ская программа также должна производить очистку после него) Поскольку данные процесса недоступны другим Все совместно используемые данные должны быть процессам, не происходит конфликтов при доступе идентифицированы и заблокированы, чтобы не про к ресурсам изошло их повреждение Независимая работа с файловой системой Потомок реагирует на все изменения текущего ка талога (команда chdir), корневого каталога (команда enroot) и стандартного режима доступа к файлам (команда umask) Таблицы дескрипторов открытых файлов не являют Совместное использование таблиц дескрипторов;

ся общими;

операционная система копирует табли если дочерний поток закрывает файл, родительский цы, поэтому если в двух процессах открыт один и поток теряет к нему доступ тот же файл, то закрытие его в одном процессе не приведет к изменению работы другого процесса Сигналы обрабатываются независимо Один поток может блокировать сигнал с помощью функции sigprocmask(), не влияя на работу дру гих потоков Создание процесса Многозадачность чаще всего реализуется с помощью процессов. Процесс представляет собой новый экземпляр программы, наследующий от нее копии де скрипторов открытых каналов ввода вывода и не обменивающийся никакими 136 Часть И. Создание серверных приложений www.books-shop.com другими данными. Для порождения нового процесса предназначен системный вызов fork():

#include pid_t fork(void);

Функция fork() проста и "немногословна": вы просто вызываете ее, и внезап но у вас появляются два идентичных процесса. Она возвращает значения в трех диапазонах:

• нуль — означает, что функция завершилась успешно и текущее задание является потомком;

чтобы получить идентификатор процесса потомка, вызовите функцию getpid();

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

значение, возвращаемое функцией, представляет собой идентификатор потомка;

• отрицательное число — произошла ошибка;

проверьте значение пере менной errno или вызовите функцию perror(), чтобы определить причи ну ошибки.

В большинстве программ функция fork() помещается в условную конструк цию (например, if). Результат проверки позволяет определить, кем стала про грамма — предком или потомком. В листингах 7.1 и 7.2 приведены два типичных примера использования функции.

Листинг 7.1. Пример разделения заданий /*********************************************************/ /*** Предок и потомок выполняются каждый по своему ***/ /*********************************************************/ int pchild;

if ( (pchild = fork()) == 0 ) { /* это процесс потомок */ /*— выполняем соответствующие действия —*/ exit(status);

/* Это важно! */ } else if ( pchild > 0 ) { /* это процесс предок */ int retval;

/*— выполняем соответствующие действия —*/ wait(&retval);

/* дожидаемся завершения потомка */ } else { /* произошла какая то ошибка */ perror("Tried to fork() a process");

Глава 7. Распределение нагрузки: многозадачность www.books-shop.com Листинг 7.2. Пример делегирования полномочий /**********************************************************/ /*** Предок (сервер) и потомок (обрабатывает задание) ***/ /**********************************************************/ int pchild;

for (;

;

) /* бесконечный цикл */ { /*— ожидаем получения запроса —*/ if ( (pchild = fork()) == 0 ) { /* это процесс потомок */ /*— обрабатываем запрос —*/ exit(status);

} else if ( pchild > 0 ) { /* предок выполняет очистку */ /* функция wait() НЕ НУЖНА */ /* используйте сигналы (см. далее) */ } else { /* произошла какая то ошибка */ perror("Can't process job request");

} В программе, представленной в листинге 7.1, процесс предок выполняет ка кую то работу, а затем дожидается завершения процесса потомка. В листинге 7. происходит распределение полномочий. Когда какая то внешняя программа по сылает запрос, предок создает потомка, который обрабатывает запрос. Большин ство серверов работает именно по такой схеме.

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

Листинг 7.3. Ветвление без дифференцирования /*************************************************************/ /*** Это пример дублирования. В подобном варианте вызова ***/ /*** функции fork() задания дублируют друг друга, что ***/ /*** приводит к бессмысленной трате ресурсов процессора. ***/ /*************************************************************/ /*— какие то действия —*/ fork();

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

138 Часть И. Создание серверных приложений www.books-shop.com Устойчивость к ошибкам за счет слияния Дублирование процессов может применяться при реализации отказоустойчивых систем. В отка зоустойчивой системе вычисления дублируются с целью повышения достоверности результатов.

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

Если при выполнении функции fork() произошла ошибка, то, очевидно, воз никли проблемы с таблицей страниц процесса или с ресурсами памяти. Одним из признаков перегруженности системы является отказ в предоставлении ресурсов.

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

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

Одной из наиболее известных реализаций многопотоковых функций является библиотека Pthreads. Она совместима со стандартом POSIX 1с. Программы, в ко торых используется эта библиотека, будут выполняться в других операционных системах, поддерживающих стандарт POSIX. В библиотеке Pthreads новый поток создается с помощью функции pthread_create ( ) :

#include int pthread_create(pthread_t* child, pthread_attr_t* attr, void* (*fn)(void*), void* arg);

Различия между библиотечными и системными вызовами Библиотечные и системные потоковые функции отличаются объемом выполняемой работы.

Функция fork() является интерфейсом к сервисам ядра. Вызов функции pthread_create() преобразуется в системный вызов _clone(). В этом случае для компиляции программы необ ходимо в качестве последнего аргумента командной строки компилятора cc указать переключа тель Ipthreads. Например, чтобы скомпилировать файл mythreads.c и подключить к нему библиотеку Pthreads, выполните такую команду:

cc mythreads.c о mythreads Ipthreads Как и в случае системного вызова fork(), после завершения функции pthread create() начинает выполняться второе задание. Однако создать поток сложнее, чем процесс, так как в общем случае требуется указать целый ряд пара метров (табл. 7.1).

Глава 7. Распределение нагрузки: многозадачность www.books-shop.com Таблица 7.1. Параметры функции pthread create () Параметр Описание child Дескриптор нового потока;

с помощью этого дескриптора можно управлять потоком после завершения функции attr Набор атрибутов, описывающих поведение нового потока и его взаимодействие с ро дительской программой (может быть равен NULL) f n Указатель на функцию, содержащую код потока;

в отличие от процессов, каждый поток выполняется в отдельной подпрограмме родительской программы, и когда эта подпро грамма завершается, система автоматически останавливает поток arg Параметр, передаваемый функции потока и позволяющий конфигурировать его на чальные установки;

необходимо, чтобы блок данных, на который осуществляется ссыл ка, был доступен потоку, т.е. нельзя ссылаться на стековую переменную (этот пара метр тоже может быть равен NULL) Как уже было сказано, после завершения функции существуют два потока: ро дительский и дочерний. Оба они совместно используют все программные данные, кроме стека. Родительская программа хранит дескриптор дочернего потока (child), который выполняется в рамках своей функции (fn) с конфигурационны ми параметрами (arg) и атрибутами (attr). Даже если параметры потока задать равными NULL, его поведение можно будет изменить впоследствии. Но до того времени он будет выполняться в соответствии с установками по умолчанию.

В итоге вызов функции сводится к указанию всего двух параметров. В сле дующих двух листингах можно сравнить алгоритмы создания процесса и потока (листинг 7.4 и 7.5).

Листинг 7.4. Пример создания процесса /*** В этом примере создается процесс ***/ void Child_Fn( void) { /* код потомка */ int main (void) { int pchild;

/*— Инициализация —*/ /*— Создание нового процесса —*/ if ( (pchild = fork(» < 0 ) perrorf ("Fork error");

else if ( pchild == 0 ) { /* это процесс потомок */ /* закрываем ненужные ресурсы ввода вывода */ Child_Fn();

exit(0);

140 Часть II. Создание серверных приложений www.books-shop.com else if ( pchild > 0 ) { /* это процесс предок */ /* закрываем ненужные ресурсы ввода вывода */ /* дожидаемся завершения потомка */ wait();

} return 0;

Листинг 7.5. Пример создания потока /*** В этом примере создается поток ***/ void *Child_Fn(void *arg) { struct argstruct *myarg = arg;

/* код потомка */ return NULL;

/* произвольное значение */ int main (void) { struct argstruct arg = {};

pthread_t tchild;

/* — Инициализация — */ /* — Создание нового потока — */ if ( pthread_create(&tchild, NULL, &Child_Fn, &arg) != 0 ) perror("Pthreads error");

/* ошибка */ /*** обратите внимание на то, что остальных проверок нет ***/ /* Мы по прежнему находимся в родительском модуле (неявно) */ /* выполняем другие действия */ /* дожидаемся завершения потомка */ pthread_join (tchild, NULL);

return 0;

} При создании процесса вызывается функция fork(), а затем проверяется, кем — предком или потомком — стала программа. При создании потока указыва ется функция, в которой он выполняется, атрибуты, задающие поведение потока, и инициализирующий параметр.

В первом случае требуется, чтобы дочерний процесс завершился явным вызо вом функции exit() во избежание дублирования. При работе с библиотекой Pthreads об этом можно не беспокоиться. Инструкция return (и даже просто дос тижение конца функции) приводит к неявному завершению потока.

Глава 7. Распределение нагрузки: многозадачность piracy@books-shop.com Системный вызов clone() В Linux имеется низкоуровневая функция _clone(), позволяющая гораздо силь нее управлять созданием процессов и потоков. С ее помощью можно задавать различных режимов совместного доступа к памяти. Если снова обратиться к диа грамме страниц виртуальной памяти (см. рис. 7.1), то окажется, что функция _clone() дает возможность указывать любую комбинацию общих областей памяти.

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

Синтаксис функции clone () таков:

#include int clone(int (*fn)(void*), void* stacktop, int flags, void* arg);

Подобно функции fork(), она возвращает идентификатор дочернего задания или 1 в случае ошибки. В табл. 7.2 описано назначение каждого параметра.

Таблица 7.2. Параметры функции clone() Параметр Описание fn Как и в функции pthread_create(), это указатель на функцию потока;

когда она за вершается (с помощью инструкции return или системного вызова exit()), поток ос танавливается stacktop Указатель на вершину стека дочернего задания;

в большинстве процессоров (за исклю чением HP/PA RISC) стек заполняется в направлении сверху вниз, поэтому необходимо задать указатель на первый байт стека (чтобы добиться совместимости, воспользуйтесь директивами условной компиляции) flags Набор флагов, определяющих, какие области памяти используются совместно (табл. 7.3) и какой сигнал посылать при завершении дочернего задания (по умолча нию SIGCHLD) arg Аналогично функции pthread_create(), передается в качестве параметра функции fn Работа со стеками Поскольку совместное использование аппаратного стека недопустимо, родительский процесс должен зарезервировать в программе дополнительную память для стека дочернего задания. Эта область памяти будет общедоступной.

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

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

142 Часть II. Создание серверных приложений www.books-shop.com Таблица 7.3. Флаги функции clone () Флаг Описание CLONE_VM Совместное использование области данных между заданиями;

если флаг указан, будут доступны все статические и предварительно инициализированные переменные, а так же блоки, выделенные в куче, в противном случае в дочернем задании будет создана копия области данных CLONE_FS Совместное использование информации о файловой системе: текущем каталоге, кор невом каталоге и стандартном режиме доступа к файлам (значение umask);

если флаг не указан, задания будут вести себя независимо друг от друга CLONE_FILES Совместное использование открытых файлов;

когда в одном задании перемещается указатель текущей позиции файла, в другом задании отразится это изменение, и если закрыть файл в одном задании, то и в другом он станет недоступным (если флаг не указан, в дочернем задании создаются новые ссылки на открытые индексные дескрип торы) CLONE_SIGHAND Совместное использование таблиц сигналов;

каждое задание может запретить обра ботку того или иного сигнала с помощью функции sigprocraaskf), и это не отразится на других заданиях (если флаг не указан, в дочернем задании создается копия табли цы сигналов) CLONE_PID Совместное использование идентификатора процесса;

применять данный флаг следует осторожно, так как он не всегда поддерживается (как это имеет место в случае биб лиотеки Pthreads);

если флаг не указан, в дочернем задании создается новый иденти фикатор процесса Функция _clone() является универсальным средством создания заданий. Если все области памяти доступны для совместного использования, создается поток, если ни одна из областей недоступна, создается процесс. Будучи правильно сконфигурированной, функция заменяет собой как системный вызов fork(), так и функцию pthread_create().

В листингах 7.6 и 7.7 сравнивается, как создавать процессы и потоки с помо щью функций fork() и pthread_create() с одной стороны, и функции clone() — с другой.

Листинг 7.6. Пример функций fork() и pthread_create() /******************************/ /*** Системный вызов fork() */ /******************************/ void Child(void) { /* код потомка */ exit(O);

} int main(void) { int pchild;

if ( (pchild = fork()) == 0 ) Child();

else if ( pchild > 0 ) Глава 7. Распределение нагрузки: многозадачность www.books-shop.com wait();

else perror( "Can't fork process");

/***********************************************/ /*** Библиотечная функция pthread_create () ***/ /***********************************************/ void* Child (void *arg) { /* код потомка */ return &Result;

} int main (void) { pthread_t tchild;

if ( pthread_create(Stchild, 0, &Child, 0) != perror ("Can 't create thread" ) ;

pthread_join(tchild, 0);

} Листинг 7.7. Эквивалентный пример функции clone () /*****************************************/ /*** Эквивалент системному вызову fork() */ /*****************************************/ void Child(void *arg) { /* код потомка */ exit(O);

} idefine STACK int main(void) { int cchild;

char *stack = mallocfSTACK);

if ((cchild = _clone(&Child, stack+STACK 1, SIGCHLD, 0)) == 0) {/** секция дочернего задания — недоступна **/} else if ( cchild > 0 ) wait();

else perror("Can't clone task");

} /********************************************/ /*** Эквивалент функции pthread_create() ***/ /********************** "~ void* Child(void *arg) { /* код потомка */ exit(O);

} 144 Часть П. Создание серверных приложений www.books-shop.com tdefine STACK int main(void) { int cchild;

char *stack = malloc(STACK);

if ((cchild = _clone(&Child, stack+STACK 1, CLONE_VM | CLONE FS | CLONE_FILES | CLONE_SIGHAND | SIGCHLD, 0 ) ) < 0) perror("Can't clone");

wait();

} Функцию _clone() можно использовать практически везде, где стоит вызов одной из двух эквивалентных ей функций. Следует, однако, отметить, что она не полностью совместима с библиотекой Pthreads. Например, клонированное зада ние может возвращать только целочисленное значение (как и процесс), тогда как в библиотеке Pthreads допускается возвращение произвольного значения из пото ка. Кроме того, отличается процедура завершения задания. Если создается про грамма, которая будет выполняться не только на компьютерах Linux, лучше вос пользоваться стандартными библиотечными функциями.

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

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

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

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

Процедура запуска процессов и потоков в целом похожа. В первую очередь необходимо определить совместно используемые области памяти, а затем создать новое задание. Потоки дополнительно могут принимать параметр типа void*. Тип данных void* определен в языке С для работы с абстрактными структурами. Бла годаря ему можно передавать в потоковую функцию любое значение, а уже при Глава 7. Распределение нагрузки: многозадачность www.books-shop.com нимающая сторона будет анализировать это значение. Получение данного пара метра подобно сдаче карт в покере.

Тип данных void Может показаться, что передача параметра типа void* — очень мощная возможность, но в дейст вительности необходимо внимательно следить за тем, какие именно данные передаются. Это дол жен быть либо блок памяти, выделенный с помощью функции malloc (), либо глобальная или ста тическая переменная. Другими словами, это не должны быть данные, помещаемые в стек (имеющие неявный спецификатор auto). Причина этого проста: при вызове/завершении различных функций содержимое стека меняется. Дочернее задание может не получить требуемые данные.

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

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

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

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

Проектируемое задание может взаимодействовать с другими процессами.

Обычно подобное взаимодействие организуется в форме каналов: сообщение на правляется от одного задания к другому через канал. Канал является однонаправ ленным, подобно стандартным потокам ввода вывода stdin и stdout, и создается с помощью системного вызова pipe():

linclude int pipe(int fd[2]);

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

• EMFILE. Уже открыто слишком много файлов.

• EFAULT. Параметр f d не указывает на массив из двух целых чисел.

146 Часть II. Создание серверных приложений www.books-shop.com Ниже представлен пример использования функции pipe():

/****************************************************************/ /*** Пример функции pipe() ***/ /****************************************************************/ int fd[2];

/* создание массива, содержащего два дескриптора */ if ( pipe(fd) != О ) /* создание канала */ perror("Pipe creation error");

read(fd[0], buffer, sizeof(buffer));

/* чтение данных из канала */ Индексация дескрипторов Каждому каналу ввода вывода назначается номер, который является индексом в таблице деск рипторов файлов. По умолчанию у каждого процесса, есть три канала ввода вывода: stdin(0), stdout (1) и stderr (2). Когда создается новый канал, ему назначаются две ближайшие пози ции в таблице (одна — для входного конца, другая — для выходного). Например, если у задания нет открытых файлов, то в результате вызова функции pipe() будет создан канал чтения (3) и канал записи (4).

Создавать канал в многозадачной среде довольно сложно. Но достаточно сде лать это несколько раз, и вы избавитесь от головной боли.

В листингах 7.8 и 7.9 демонстрируется, как создавать каналы в родительских процессах и потоках. Аналогичные операции в дочерних заданиях рассматрива ются чуть ниже.

Листинг 7.8. Создание канала в родительском процессе /****************************************************************/ /*** Родительский процесс ***/ /****************************************************************/ int FDs[2];

/* создание массива, содержащего два дескриптора */ pipe(FDs);

/* создание канала: FDs[0] чтение, FDs[l] запись */ char buffer[1024];

/*— Создание процесса с помощью функции fork() —*/ close(FDs[0]);

/* делаем канал доступным только для записи */ write(FDs[l], buffer, buffer_len);

/* передача сообщения дочернему заданию */ /*— дальнейшая обработка —*/ wait();

close(FDs[l]);

Листинг 7.9. Создание канала в родительском потоке /****************************************************************/ /*** Родительский поток ***/ /***************************************************************/ Глава 7. Распределение нагрузки: многозадачность www.books-shop.com int FDs[2);

/* создание массива, содержащего два дескриптора */ pipe(FDs);

/* создание канала: FDs[0] чтение, FDs[l] запись */ char buffer[1024];

/*— Создание потока с помощью функции pthread create() —*/ write(FDs[l], buffer, buffer_len);

/* передача сообщения дочернему заданию */ /*— закрытие канала предоставляется потомку —*/ pthread_join(pchild, arg);

Дочерние процессы и потоки решают другие задачи и программируются по разному. Сравните листинги 7.10 и 7.11.

Листинг 7.10. Создание канала в дочернем процессе /*** Дочерний процесс ***/ int FDs[2];

/* создание массива, содержащего два дескриптора */ pipe(FDs);

/* создание канала: FDs[0] чтение, FDs[l] запись */ char buffer[1024];

/*— Создание дочернего процесса —*/ dup(FDs[0], 0);

/* замещаем поток stdin */• close(FDs[0]);

/* больше не нужен */ close(FDs[l]);

/* передача данных родительскому процессу не производится */ read(0, buffer, sizeof(buffer));

/* чтение сообщения от родительского процесса */ /*— дальнейшая обработка —*/ printf("My report...");

exit(O);

Листинг7.11. Создание каналавдочернемпотоке /*********« /*** Дочерний поток ***/ int FDs[2];

/* создание массива, содержащего два дескриптора */ pipe(FDs);

/* создание канала: FDs[0] чтение, FDsfl] запись */ char buffer[1024];

/*_ создание дочернего потока —*/ 148 Часть П. Создание серверных приложений www.books-shop.com read(FDs[0], buffer, sizeof(buffer));

/* чтение сообщения от родительского процесса */ /*— дальнейшая обработка —*/ printf("My report...");

close(FDs[0]);

/* закрываем входной канал */ close(FDs[1]);

/* закрываем выходной канал */ pthread_exit(arg);

При создании канала между двумя заданиями пользуйтесь схемой, представ ленной на рис. 7.2, в качестве руководства. Ниже описана общая последователь ность действий.

1. Родительская программа объявляет массив дескрипторов файлов. Этот массив будет заполняться функцией pipe().

2. Родительская программа создает канал. При этом ядро создает очередь ввода вывода и помещает дескриптор канала чтения в элемент fd[0], a дескриптор канала записи — в элемент fd[l].

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

3. Родительская программа создает новое задание. При этом процесс по лучает точную копию родительских данных (включая канал). Канал ме жду предком и потомком является замкнутым.

4. Родительская программа взаимодействует с дочерним заданием благода ря имеющимся дескрипторам канала.

5. Дочерний процесс перенаправляет свой собственный входной поток (это необязательно) и получает данные от родительской программы че рез открытый канал. Выходной канал закрывается.

6. Родительской программе требуется только выходной конец канала для взаимодействия с потомком, поэтому она закрывает входной конец.

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

Глава 7. Распределение нагрузки: многозадачность www.books-shop.com Рис. 7.2. При создании канала между заданиями необходимо придержи ваться определенной последовательности действий по перенаправле нию и закрытию каналов На рис. 7.2 была представлена схема создания одностороннего соединения между предком и потомком. Для создания двунаправленного соединения необхо димо придерживаться схемы, изображенной на рис. 7.3.

150 Часть П. Создание серверных приложений www.books-shop.com Рис. 7.3. Если между предком и потомком необходимо организовать двунаправленное соединение, создайте дополнительный канал Глава 7. Распределение нагрузки: многозадачность piracy@books-shop.com Сигнализация о завершении В покере есть несколько прямых управляющих команд. В любой момент игрок может сказать "пас" (выйти из текущей игры). Если бы игра происходила на Ди ком Западе, обманывающий игрок мог подвергнуться внезапному исключению из игры. В любом случае брошенные карты забирает сдающий. Точно так же про цесс с помощью сигнала может уведомить программу о своем завершении.

Любое задание в программе должно обрабатывать все полезные сигналы. Всего существует около 30 ти сигналов (два сигнала определяются пользователем).

Большинство из них можно игнорировать или не перехватывать, поскольку веро ятность их возникновения ничтожно мала или их обработка не представляет осо бого смысла. Многие сигналы, будучи не перехваченными, приводят к заверше нию программы, а некоторые из этих сигналов важны в многозадачных програм мах. Например, операционная система уведомляет родительскую программу о завершении задания потомка, посылая сигнал SIGCHLD.

Сигнал напоминает запрос на аппаратное прерывание. О нем известно только то, что он произошел. Когда появляется сигнал, задание прекращает выполнять свои действия и переходит к специальной подпрограмме (называемой обработчи ком сигналов). При получении сигнала SIGCHLD можно вызвать функцию wait(), чтобы провести дополнительную очистку после завершения потомка. Можно пе рехватывать и другие сигналы, в частности, те, которые возникают в случае мате матических ошибок или после нажатия клавиш .

В Linux поддерживаются сигналы двух разновидностей: в стиле System V (однократный сигнал, который возвращается стандартному обработчику, когда система вызывает пользовательский обработчик) и в стиле BSD (посылается об работчику до тех пор, пока не будет явно остановлен). Если сигнал посылается с помощью системного вызова signal(), он будет однократным. Но на это можно не обращать внимания: следует ожидать, что сигнал будет перехватываться мно гократно.

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

Вместо функции signal() лучше пользоваться системным вызовом sigaction(), который позволяет лучше контролировать поведение сигнальной подсистемы.

Прототип этой функции таков:

iinclude int sigaction(int sig_num, const struct sigaction *action, const struct sigaction *old);

Первый параметр определяет номер перехватываемого сигнала. Второй пара метр задает способ обработки сигнала. Если последний параметр не равен NULL, будет запомнено последнее выполненное действие. Ниже приведено определение структуры sigaction:

152 Часть П. Создание серверных приложений www.books-shop.com struct sigaction { /* Указатель на функцию обработки */ void (*sa_handler)(int signum);

/* Специальная функция обратного вызова */ void (*sa_sigaction)(int, siginfo t *, void *);

/* Массив битовых флагов, указывающих, какие сигналы следует игнорировать, находясь в теле обработчика */ sigset_t sajnask;

/* Выполняемое действие */ int sa_flags;

/* (больше не используется должно быть равно 0) */ void (* sa_restorer)(void);

};

Для активизации третьего параметра необходимо, чтобы первое поле (sa_handler) отличалось от указателя на функцию во втором параметре. Если по местить в это поле константу SIG_IGN, программа проигнорирует указанный сиг нал, а если задать константу SIG_DFL, будет восстановлена стандартная процедура обработки.

Чтобы разрешить или запретить каскадирование сигналов (один сигнал пре рывает другой, вследствие чего возникает цепочка вызовов обработчиков), вос пользуйтесь третьим полем структуры, sa mask. Каждый бит (всего их 1024) обо значает разрешение (1) или запрет (0) обработки сигнала. По умолчанию обра ботчик сигнала игнорирует другие аналогичные сигналы. Например, если обрабатывается сигнал SIGCHLD и в это же время завершается другой процесс, по вторный сигнал будет проигнорирован. Подобный режим можно изменить с по мощью флага SA_NOMASK.

Поле sa_flags содержит набор флагов, определяющих поведение обработчика.

В большинстве случаев это поле можно задать равным нулю.

• SA_ONESHOT. Режим System V: сброс сигнала после того, как он перехва чен.

• SA_RESETHAND. To же, ЧТО И SA_ONESHOT.

• SA_RESTART. Повторный запуск некоторых системных функций, если сиг нал прервал их выполнение. Это позволяет восстанавливать работу таких функций, как, например, accept().

• SA_NOMASK. Разрешить обработку того же самого сигнала во время обра ботки более раннего сигнала.

• SA_NODEFER. То же, что И SA_NOMASK.

• SA_NOCLDSTOP. He уведомлять родительскую программу о прекращении работы дочернего задания (сигнал SIGSTOP, SIGTSTP, SIGTTIN или SIGTTOU).

Этот флаг важен для данной главы.

В листинге 7.12 демонстрируется, как перехватывать сигнал SIGFPE (исключительная ситуация в операции с плавающей запятой) и игнорировать сигнал SIGINT (запрос на прерывание от клавиатуры).

Глава 7. Распределение нагрузки: многозадачность www.books-shop.com Листинг 7.12. Обработчик сигналов SIGFPE и SIGINT /*** Перехват сигнала SIGFPE и игнорирование сигнала SIGINT ***/ #include /* Определение обработчика сигналов */ void sig_catcher(int sig) printf("I caught signal #%d\n", sig);

int main(void) { struct sigaction act;

bzero(&act, sizeof(act));

act.sa_handler = sig_catcher;

sigaction(SIGFPE, act, 0);

/* перехватываем ошибку в операции с плавающей запятой */ act.sa_handler = SIG_IGN;

/* игнорируем сигнал */ signal(SIGINT, &act, 0 ) ;

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

Серверы и клиенты могут принимать несколько различных сигналов. Чтобы сделать программу более отказоустойчивой, следует обрабатывать все сигналы, от которых потенциально зависит работа программы. (Некоторые сигналы, напри мер SIGFAULT, лучше всего не трогать. Данный сигнал свидетельствует о наличии ошибки в тексте программы или в ее данных. Такую ошибку нельзя исправить.) Уменьшение текста программы за счет совместного использования обработчиков сигналов Можно объединить несколько обработчиков сигналов в одной подпрограмме. Распределение обязанностей несложно организовать внутри подпрограммы, так как система передает ей номер сигнала.

Дочернему заданию можно послать любой сигнал. Из командной строки это можно сделать с помощью команды kill. В программе доступен системный вызов kill(). Его прототип выглядит следующим образом:

154 Часть II. Создание серверных приложений www.books-shop.com #include #include int kill(pid_t PID, int sig_num);

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

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

Некоторые игроки могут потребовать посмотреть карты победителя, прежде чем он заберет себе выигрыш. Точно так же родительская программа должна прове рять результаты завершения каждого из потомков.

По коду завершения программа может определить, успешно ли выполнилось дочернее задание. Когда процесс завершается, он всегда возвращает целое число со знаком. В то же время поток может вернуть абстрактный объект типа void*. В этом случае необходимо соблюдать осторожность, чтобы не произошло потери данных. Не следует возвращать объект, созданный на основании значений стеко вых переменных. Лучше передавать данные через кучу или глобальную перемен ную.

Обгоняя время: исключающие семафоры и гонки Сила, заключенная в потоках, очень привлекательна. Если правильно управ лять ими, можно заставить программу выполняться быстрее и реже "зависать".

Тем не менее есть один подводный камень — соперничество за право обладания ресурсами. Когда два потока одновременно обновляют одни и те же данные, они почти наверняка будут повреждены. Отлаживать такие потоки можно часами.

Ниже рассматриваются вопросы, связанные с одновременным доступом к ресур сам.

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

Листинг 7.13. Состояние гонки в потоке /****************************************************************/ /*** Пример гонки, в которой два потока соперничают ***/ /*** за право доступа к массиву queue ***/ Глава 7. Распределение нагрузки: многозадачность www.books-shop.com int queue[lO];

int in, out, empty;

/************** поток 1 **************/ /* Чтение данных из очереди */ if ( !empty ) /* избегаем чтения пустой очереди */ { int val = queue[out];

out++;

if ( out >= sizeof(queue) ) out = 0;

/* начинаем заново */ empty = (out == in);

} Листинг 7.14. Состояние гонки в потоке /*** Пример гонки, в которой два потока соперничают ***/ /*** за право доступа к массиву queue ***./ int queue[10] ;

int in, out, empty;

/************** поток 2 **************/ /* Запись данных в очередь */ if ( Jempty && out != in ) /* избегаем переполнения очереди */ { queue [in] = 50;

if ( in >= sizeof (queue) ) in = 0;

/* начинаем заново */ empty = (out == in);

} Обратите внимание на то, что приращение индексных переменных происходит после обращения к очереди. Все кажется правильным, если предположить, что потоки выполняются параллельно, строка за строкой. К сожалению, это происхо дит очень редко. Что если первый поток отстает от второго на несколько строк?

Поток 2 мог выполнить проверку переменной empty как раз перед тем, как по ток 1 сбросил значение переменной out. Возникнет проблема, так как перемен ная out никогда не станет равной переменной in.

Другая проблема возникнет в случае переключения между потоками (если они выполняются не строго параллельно, а в многозадачной среде). Переключение заданий может произойти сразу после завершения операции out++ в потоке 1.

При этом существует вероятность, что поток 2 получит неправильные значения переменных out и empty, так как не все проверки были завершены.

В этом примере две подпрограммы соперничают за четыре ресурса: перемен ные queue, in, out и empty.

Исключающий семафор Работать с критическими секциями можно, блокируя другие процессы при об ращении к ним. Подобный процесс называется взаимным исключением (кто пер 156 Часть Ц. Создание серверных приложений www.books-shop.com вый захватил ресурс, тот блокирует остальных) или сериализацией (разрешение одновременного доступа к ресурсу только для одного задания). Эта методика по зволяет предотвращать повреждение данных в критических секциях. Исключаю щий семафор — это флаг, разрешающий или запрещающий монопольный доступ к ресурсу. Если флаг сброшен (семафор опущен), поток может войти в критиче скую секцию. Если флаг установлен (семафор поднят), поток блокируется до тех пор, пока доступ не будет разрешен.

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

Точная блокировка применяется по отношению к ресурсам, а не заданиям.

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

В библиотеке Pthreads имеется множество средств управления потоками. Су ществуют также функции работы с исключающими семафорами. Использовать их очень просто (листинг 7.15).

Листинг 7.15. Пример исключающего семафора /****************************************************************/ /*** Создание глобального исключающего семафора ***/ /г***************************************************************/ pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

/* Начало критической секции */ pthread_mutex_lock(&mutex);

/*— Работа с критическими данными —*/ pthread_mutex_unlock(&mutex);

/* Конец критической секции */ Полный текст этого примера содержится на Web узле в файле thread mutex.c.

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

• Быстрый (по умолчанию) — PTHREAD_MUTEX INITIALIZER. Выполняется простая проверка наличия блокировки. Если один и тот же поток попы тается дважды заблокировать исключающий семафор, возникнет тупи ковая ситуация (взаимоблокировка).

• Рекурсивный — PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP. Проверяется, не блокирует ли владелец повторно тот же самый исключающий семафор.

Если это так, включается счетчик (считающий семафор), определяющий число блокировок. Исключающий семафор должен быть разблокирован столько раз, сколько было сделано блокировок.

Глава 7. Распределение нагрузки: многозадачность www.books-shop.com • С проверкой ошибок — PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP. Прове ряется, тот ли самый поток пытается разблокировать исключающий се мафор, что и поток, который заблокировал его. Если это другой поток, возвращается ошибка и блокировка не снимается.

В библиотеке имеется дополнительная функция pthread_mutex_trylock(), кото рая запрашивает блокировку семафора. Если она невозможна, возвращается ошибка EBUSY.

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

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

Во вторых, рассмотрим ситуацию, когда несколько потоков работают с боль шим массивом данных, например с таблицей. Ее ячейки не связаны друг с дру гом, и потоки работают в разных сегментах таблицы. Необходимо блокировать не всю таблицу, а лишь отдельные ее секции (зонная блокировка). Библиотека Pthreads не позволяет определить, в каком месте ресурса работает поток.

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

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

Создавая потоки, обязательно выявляйте критические секции и возможные конфликты ресурсов. Обнаружив критические данные, определите, кому и когда они могут понадобиться. Может оказаться, что два ресурса должны быть забло 158 Часть IL Создание серверных приложений www.books-shop.com кированы, прежде чем работа продолжится. Если проявить невнимательность, возникнет взаимоблокировка.

Рассмотрим следующий пример.

Поток 1 Поток 1. Блокирует семафор Funds_Mutex_1. 1. Блокирует семафор Funds_Mutex_2.

2. Блокирует семафор Funds_Mutex_2. 2. Блокирует семафор Funds_Mutex_1.

3. Используя семафор Funds_Mutex_2, изменяет 3. Используя семафор Funds_Mutex_1, изменяет семафор Funds_Mutex_1. семафор Funds_Mutex_2.

4. Разблокирует семафор Funds_Mutex_2. 4. Разблокирует семафор Funds Mutex_2.

5. Разблокирует семафор Funds_Mutex_1. 5. Разблокирует семафор Funds Mutex_1.

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

• наименование ресурсов по группам — идентифицируйте взаимосвязан ные группы ресурсов и присвойте соответствующим исключающим се мафорам сходные имена (например, Funds_Mutex_l и Funds_Mutex_2);

• правильный порядок блокировки — блокируйте ресурсы по номерам от наименьшего к наибольшему;

• правильный порядок разблокирования — разблокируйте ресурсы по но мерам от наибольшего к наименьшему.

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

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

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

В отличие от процессов, практически не имеющих контроля над своими до черними заданиями, для потоков можно изменять алгоритм планирования, а так Глава 7. Распределение нагрузки: многозадачность www.books-shop.com же отказываться от владения ими. В Linux применяется алгоритм приоритетного кругового обслуживания. В библиотеке Pthreads поддерживаются три алгоритма:

• обычный — аналогичен алгоритму планирования в Linux (принят по умолчанию);

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

• FIFO — планировщик помешает каждый поток в очередь и выполняет его до тех пор, пока он не завершится (этот алгоритм также применяет ся в системах реального времени).

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

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

Если вы любите риск и не знаете, что такое зомби, попробуйте создать его в своей системе. Не делайте этого, если не имеете возможности перегрузиться. Зом би не причиняют вреда системе, но занимают ценные ресурсы (память и место в таблице процессов). Запустите многозадачный эхо сервер (текст имеется на Web узле), подключитесь к нему и введите команду "bye". Затем закройте соединение.

Теперь введите команду ps aux | grep <имя пользователя> (заменив пара метр своим пользовательским именем). В полученном списке будет присутство вать задание, имеющее статус Z (зомби). Обычно его можно уничтожить, уничто жив предка (эхо сервер).

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

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

помощью команды kill. (Можно попробовать выполнить команду init s или init 1 для очи стки таблицы процессов, но нет гарантии, что она будет работать.) В отличие от процессов, создаваемых с помощью системного вызова fork() или _clone(), библиотека Pthreads позволяет отказаться от владения потоком (отсоединить его). Отсоединив поток, можно продолжить выполнение програм 160 Часть //. Создание серверных приложений www.books-shop.com мы, не дожидаясь его завершения. Объявление соответствующей библиотечной функции выглядит так:

#include int pthread_detach(pthread_t tchild);

Параметр tchild является ссылкой, получаемой от функции pthread_create().

В результате выполнения функции могут возникать следующие ошибки.

• ESRCH. Для заданного параметра tchild не найден поток.

• EINVAL. Поток уже был отсоединен.

Общая схема использования функции pthread_detach() такова:

/****************************************************************/ /*** Пример отсоединения потока от родительского задания. ***/ /*** это позволяет заданию продолжить свою работу, ***/ /*** не проверяя завершение дочернего потока. ***/ /**************************************************************/ /* Создание переменной, содержащей ссылку на поток */ pthread_t tchild;

/* Создание потока */ if ( pthread_create(&tchild, 0, Child_Fn, Child_Arg) != О ) perror( "Could not create thread");

else /* Отсоединяем поток */ pthread_detach(tchild);

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

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

/*** Пример обработчика сигналов, перехватывающего сообщения ***/ /*** о завершении дочернего задания. ***/ /****************************************************************/ #include void sig_child(int sig) { int status;

wait(&status);

/* получаем окончательные результаты */ fprintf(stderr, "Another one bytes the dust\n");

Глава 7. Распределение нагрузки: многозадачность piracy@books-shop.com Чтобы иметь возможность получать подобные уведомления, необходимо свя зать этот обработчик с требуемым сигналом, например:

{ struct sigaction act;

bzero(&act, sizeof (act) );

act. sa_handler = sig_child;

act. sa_f lags = SA_NOCLDSTOP | SA_RESTART;

sigaction (SIGCHLD, &act, 0);

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

Узнать о том, как завершился потомок, можно с помощью переменной status.

Естественно, родительская программа должна понимать ее значения. Например, потомок может возвращать 0 в случае успешного завершения и отрицательное число при возникновении ошибки.

Обработка кодов завершения, возвращаемых процессами Полученное значение сдвигается на 8 битов влево, чтобы осталось только 8 значащих битов. Та ким образом, если возвращается код 500 (0x01 F4), функция wait() запишет в переменную status значение OxF400, остальные биты будут потеряны.

Значение, возвращаемое функцией wait(), нельзя интерпретировать напрямую.

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

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

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

162 Часть II. Создание серверных приложений www.books-shop.com Вызов внешних программ с помощью функций семейства ехес() Никому не нравится изобретать колесо. Несомненно, удобнее собирать ин формацию о заданиях с помощью команды ps, чем делать это программным спо собом. Кроме того, писать анализатор строк намного удобнее на Perl, чем на С.

Каким образом можно воспользоваться преимуществами готовых программ?

Набор системных вызовов семейства ехес() расширяет возможности функции fork(), позволяя вызывать внешние программы и взаимодействовать с ними. Это напоминает использование команд CGI в Web среде.

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

"http://www.server.соm/cgi/<команда>?лараметр1+парметр2+..."

• Клиент посылает серверу сообщение следующего вида:

"http://www.server.com/cgi/ls?tmp+proc" • Сервер создает дочерний процесс и перенаправляет потоки stdin, stdout и stderr в клиентский канал.

• Затем сервер вызывает указанную программу с помощью функции ехес().

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

• ехесl(). Принимает список параметров переменного размера, в котором первым указано полное путевое имя программы. Все следующие пара метры являются аргументами командной строки, начиная с нулевого аргумента (arg[0]). Список заканчивается параметром 0 или NULL.

/* Например: */ if ( execl("/bin/ls", "/bin/ls", " aF", "/etc", NULL) != 0 ) perror("execl failed");

exit( l);

/* в действительности проверка if избыточна все функции семейства ехес() не возвращают значений, если только не завершились неуспешно */ Первые два параметра функции execl () Функция execl () выглядит избыточной. Почему первые два параметра одинаковые? Дело в том, что первый параметр указывает на имя исполняемого файла, а второй является нулевым аргу ментом командной строки (как аргумент arg[0] функции main() в программе на языке С). Это не одно и то же. Не забывайте, что некоторые программы проверяют, под каким именем они вы званы, и в зависимости от этого могут выполнять разные действия.

• execlp(). Аналогична функции ехес(), но полное путевое имя не указы вается.

Глава 7. Распределение нагрузки: многозадачность www.books-shop.com /* Путь к команде ls ищется в переменной PATH */ if ( execlp("ls", "ls", " aF", "/etc", NULL) != 0 ) perror("execlp failed");

exit( l);

• execle(). Аналогична функции execl(), но дополнительный параметр представляет собой массив строк, содержащих установки переменных среды (не забывайте оканчивать его значением 0 или NULL).

/* Например: */ char *env[] = {"PATH=/bin:/usr/bin", "USER=gonzo", "SHELL=/bin/ash", NULL};

if ( execle("/bin/ls", "/bin/ls", " aF", "/etc", NULL, env) != 0 ) perror("execle failed");

exit( l);

• execv(). Принимает два параметра. Первый представляет собой полное путевое имя программы. Второй является массивом аргументов команд ной строки (последний элемент равен 0 или NULL).

/* Например: */ char *args[] = {"/bin/ls", " aF", "/etc", NULL};

if ( execv(args[0], args) != 0 ) perror("execv failed");

exit( l);

• execvp(). Аналогична функции execv(), но полное путевое имя не указы вается (оно ищется в переменной PATH).

/* Например: */ char *args[] = {"ls", " aF", "/etc", NULL};

if ( execvp(args[0], args) != 0 ) perror("execvp failed");

exit( l);

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

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

/*** Фрагмент программы, в котором принимаются запросы ***/ /*** на подключение и вызывается внешняя программа ***/ /*** ("ls ai /etc"). ***/ while(l) { int client, addr size = sizeof(addr);

164 Часть II. Создание серверных приложений www.books-shop.com client = acceptfsd, &addr, &addr_size);

printf("Connected: %s:%d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));

if ( ork() ) close(client);

else { close(sd);

/* клиенту не нужен доступ к сокету */ dup2(client, 0);

/* замещаем поток stdin */ dup2(client, 1);

/* замещаем поток stdout */ dup2(client, 2);

/* замещаем поток stderr */ execl("/bin/ls", "/bin/ls", " al", "/etc", 0);

perror("Exec failed!");

/* что то случилось */ } } Разница между функциями fork() и vfork() В некоторых версиях UNIX можно встретить cистемный вызов vfork(). Егo существование свя зано с тем, что в процессах не происходит совместного использования данных, поэтому потомок принимает копию сегмента данных своего предка. Если же сразу вслед за функцией fork() предполагается вызвать функцию exec(), весь этот блок данных останется в памяти, просто за нимая место. Для решения этой проблемы и появилась функция vfork(), которая подавляет создание копии. Однако в Linux применяется алгоритм копирования при записи (родительские страницы виртуальной памяти копируются только в том случае, если предок или потомок моди фицирует их). Дочерний процесс, запущенный с помощью функции ехес(), не сможет обновить родительские страницы, поэтому система их не копирует. Следовательно, отпадает необходи мость в системном вызове vfork(). В Linux он транслируется в вызов fork().

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

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

Вызов функции ехес() в потоке Интересный вопрос;

что происходит, когда в потоке вызывается функция ехес()? Ведь внешняя программа не может заместить контекст потока, не повлияв на работу других связанных р ним потоков. В ядре Linux применяется методика, при которой внешняя программа не получает дос туп к данным потока, но совместно с ним использует существующие файловые каналы. Поэтому когда внешняя программа завершается и закрывает дескрипторы каналов stdin и stdout, это изменение отражается на всех остальных потоках. Отсюда вывод: вызывайте функцию fork() перед запуском внешней программы из потока.

Глава 7. Распределение нагрузки: многозадачность www.books-shop.com Резюме Многозадачность является средством ускорения работы программ и повыше ния их гибкости. Процессы и потоки позволяют заметно повысить скорость рабо ты клиентов, не говоря уже о серверах, где многозадачность просто необходима.

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

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

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

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

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

166 Часть П. Создание серверных приложений www.books-shop.com Глава Механизмы ввода вывода В этой главе...

необходимоввода вывода: зачем оно Блокирование Когда следует переходить в режим блокирования? Альтернативы блокированию.

Сравнение различных методик ввода вывода Опрос каналов ввода вывода l Асинхронный ввод вывод Устранение нежелательного блокирования с помощью фунщий pоll() и select() Реализация тайм аутов Резюме: выбор методик ввода вывода www.books-shop.com Учитывая современные требования к производительности, не помешает изу чить методы экономии времени при работе в сети. Важно помнить, что самый критический, самый лимитированный ресурс компьютера — это центральный процессор. Любому заданию, выполняющемуся в системе, требуется получить доступ к процессору, причем доступ должен быть ограничен небольшими проме жутками времени. На самом простом уровне многозадачность реализуется путем квантования времени процессора между заданиями. В Linux используется более динамичный алгоритм планирования, учитывающий зафуженность процессора и задержки в каналах ввода вывода.

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

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

Если продолжать удерживать контроль над центральным процессором, нахо дясь в режиме ожидания, то можно повысить скорость реакции приложения на запросы пользователя. Чтобы добиться этого, следует перейти в режим неблоки руемого ввода вывода и задать предельное время ожидания (тайм аут). Для опре деления степени готовности данных Следует либо опросить подсистему ввода вывода, либо приказать ядру послать профамме сигнал при наличии данных. Это позволит профамме сосредоточиться на взаимодействии с пользователем или на выполнении основного вычислительного алгоритма.

Блокирование ввода вывода может применяться в сочетании с многозадачно стью или заменять ее. Это еще одно мощное средство в арсенале профаммиста.

Определить, когда следует применять режим блокирования, а когда — нет, не всегда легко. Этой проблеме и посвящена данная глава.

Блокирование ввода вывода:

зачем оно необходимо?

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

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

168 Часть //. Создание серверных приложений www.books-shop.com Одно из правил гласит, что любое задание периодически должно давать воз можность выполниться другим заданиям. В системе это правило применяется повсеместно. Например, каждое задание получает свою долю процессорного времени. Если в этот момент задание простаивает, оно отказывается от своей доли. Но как оно узнает, когда нужно что то сделать? И разве задание не все гда чем то занято Ответ будет "и да, и нет". Одной из причин простоя является ожидание за вершения какой либо операции ввода вывода. Чтобы понять это, сравните ско рость процессора со скоростью работы жесткого диска или сети.

Даже в самых быстрых дисковых массивах скорость передачи данных состав ляет 160 Мбайт/с, а время позиционирования головки — 5 мс. Если задание вы полняется на процессоре Pentium III с частотой 500 МГц, для выполнения каж дой инструкции в общем случае требуется один такт процессора. В среднем это 2—4 нс. За то время, пока происходит позиционирование головки диска, про грамма может выполнить 1250000 ассемблерных инструкций (машинных кодов).

Дополнительные задержки в стандарте EnergyStar Большинство современных компьютерных систем соответствует стандарту Green или EnergyStar.

Одно из его требований заключается в том, что питание жесткого диска выключается, если к не му нет обращения в течение определенного промежутка времени. Возврат из "спящего" режима обычно занимает 1 2 секунды.

В сети с пропускной способностью 10 Мбит данные передаются от компьюте ра к компьютеру со средней скоростью 300 Кбайт/с (это оптимальный измерен ный показатель для сетей TCP/IP). В сети с пропускной способностью 100 Мбит этот показатель увеличивается лишь в 10 раз (3 Мбайт/с). Даже если не учиты вать время маршрутизации и время, требуемое для обработки запроса, клиенту придется ожидать минимум 1000 инструкций (на аналогично сконфигурирован ной системе). С учетом дополнительных факторов этот показатель смело можно умножать на 1000 5000.

Среднее число машинных кодов, приходящееся на одну строку скомпилиро ванной программы, которая написана на языке С, равно 10. Другими словами, задержка в сети, составляющая 20 мс, эквивалентна 5000000 машинных кодов или 500000 строк на языке С.

Каким образом операционная система справляется с подобными задержками?

Попросту говоря, она переводит задание в "спящий" режим (блокирует его). Ко гда обработка системного запроса завершается, задание "пробуждается" и про должает ВЫПОЛНЯТЬСЯ:

Блокирование заданий, осуществляющих ввод вывод, — это, скорее, правило, чем исключение. Чтобы узнать, сколько заданий в действительности выполняют ся в системе, запустите команду ps aux. Те из них, которые находятся в активном режиме, будут помечены буквой R (running). Задания, обозначенные буквой S (stopped), являются остановленными. Вполне вероятно, что активным в списке будет только одно задание — сама команда ps.

Глава 8. Механизмы ввода вывода www.books-shop.com Когда следует переходить в режим блокирования?

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

Блокирование происходит в следующих ситуациях.

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

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

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

Альтернативы блокированию Какие имеются альтернативы блокированию? Можно заставить программу вы полнять другие действия, пока завершается обработка системного запроса. Про грамма может:

• проверить целостность данных;

• инициировать другие запросы или отслеживать их появление;

• обслуживать несколько других соединений;

• выполнять вычисления, требующие интенсивного использования про цессора.

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

170 Часть П. Создание серверных приложений www.books-shop.com Асинхронный и сигнальный ввод вывод Алгоритмы асинхронного ввода вывода, представленные в этой главе, в действительности рабо тают на основе сигналов, которые посылаются, когда буферы готовы для чтения или записи. При истинно асинхронном вводе выводе, который определен в стандарте POSIX.1, никогда не возни кает блокирование. Например, вызов функции read() немедленно завершается. Буферы счита ются незаполненными до тех пор, пока не завершится операция чтения и программа не получит сигнал. Linux (как и многие другие операционные системы) не соответствует стандарту POSIX.1, касающемуся асинхронного ввода вывода для сокетов. Чтобы не грешить против истины, лучше употреблять термин "сигнальный ввод вывод".

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

Блокирование Тайм аут Опрос Асинхронный режим Рис. 8.1. В каждой из методик ввода вывода у задания разные периоды простоя Глава 8. Механизмы ввода вывода piracy@books-shop.com Работая в режиме блокирования, программа ожидает поступления данных. В режиме опроса программа вызывает функцию recv() до тех пор, пока данные не появятся. В режиме тайм аута программа может вообще не получить данные, ес ли они не пришли вовремя. Тем не менее ядро сохраняет сообщение до момента последующего вызова функции recv(). Наконец, в асинхронном режиме ядро по сылает программе сигнал о поступлении данных.

Следует отметить, что в случае отправки данных процесс будет выглядеть по другому. Основную роль здесь играют буферы, заполнения которых вынуждена ожидать программа. В любом случае в режиме блокирования программа останав ливается, ожидая, пока ядро полностью отправит сообщение. (Это не совсем вер но, так как в действительности ядро ожидает момента, когда можно будет завер шить передачу сообщения, после чего сразу же посылает заданию сигнал "пробуждения", но в целях упрощения программирования можно предполагать, что на момент получения сигнала данные уже отправлены.) В режиме опроса и в асинхронном режиме функция send() помещает сообщение в буфер и немедленно завершается. В схеме, изображенной на рис. 8.2, предполагается, что данные слишком велики и не помещаются целиком в буфере.

Блокирование Тайм аут Опрос Асинхронный режим Рис. 8.2. В процессе записи задание выполняется быстрее благодаря на личию внутренних буферов В следующих разделах рассматриваются особенности каждого из трех небло кируемых режимов.

Опрос каналов ввода вывода Программа может выполнять множество других действий во время ожидания пакета. В конце концов, после того как вызвана функция send(), программу не 172 Часть II. Создание серверных приложений www.books-shop.com интересует, как и когда сообщение покинет компьютер. С ее точки зрения под система ввода вывода отправляет сообщение целиком и за один прием. Но часто это не так. Большинство сообщений разбивается на фрагменты из за ограничен ной пропускной способности сети. Точно так же при приеме сообщения оно мо жет поступать по частям, что не всегда желательно. В обоих случаях программа останавливается и дожидается заполнения буфера.

Методика опроса позволяет избежать блокирования при осуществлении ввода вывода. Ее суть заключается в периодической проверке готовности канала. В пер вую очередь сокет должен быть переведен в неблокируемый режим. Затем необ ходимо проверить значение, возвращаемое системным вызовом send() или recv().

Если возникает ошибка EWOULDBLK или EAGAIN, следует повторить попытку позднее.

Чаще всего подобные действия выполняются в цикле, например таком:

/*** Общий алгоритм опроса ***/.*************************************************/ while ( /*** передача данных ***/ ) { if ( /*** проверка готовности канала ***/ ) /*** обработка данных ***/ /*** другие вычисления ***/ Сутью приведенного фрагмента является наличие секции вычислений. Напом ним, что в Linux применяется алгоритм приоритетного кругового обслуживания заданий. Если программа больше занята обращением к процессору, чем к кана лам ввода вывода, ее эффективность возрастает. Но если не задать секцию вы числений, возникнет поглощающий цикл, в котором приоритет программы увели чивается, хотя она, по сути, ничего не делает. Поглощающие циклы представля ют собой серьезную проблему с точки зрения планирования заданий. Программа может занимать ресурсы процессора, выполняя слишком малый объем работы.

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

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

linclude in fcntl(int fd, int command, int option);

Подобно системному вызову read(), эта функция принимает в первом пара метре либо дескриптор файла, либо дескриптор сокета. Параметр command должен Глава 8. Механизмы ввода вывода www.books-shop.com быть равен F_SETFL, а параметр option — O_NONBLOCK. (У этих параметров очень много возможных значений, которые перечислены в приложении В, "API функции ядра") Формат использования функции таков:

if ( fcntl(sd, F_SETFL, 0_NONBLOCK) != О ) perror("Fcntl — could not set nonblocking");

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

Листинг 8.1. Пример чтения данных по методике опроса /***************************************************************/ /*** Пример алгоритма опроса: чтение аудиопотока, ***/ /*** обработка данных и их воспроизведение ***/ /***************************************************************/ if ( fcntl(sd, F SETFL, 0 NONBLOCK) != О ) perror("FcntI could" not set nonblocking");

done = 0 ;

while ( !done ) { int bytes;

Queue ProcessingQueue;

Queue OutputQueue;

/* — Получаем данные из буфера и помещаем их — */ /* — в очередь обработки — */ if ( (bytes = recv(sd, buffer, sizeof (buffer), 0)) > 0 ) QueueData ( ProcessingQueue, OutputQueue ) ;

/* — Преобразуем определенное число байтов из — */ /* — очереди обработки в аудиоформат (обычно — */ /* — это выполняется быстрее, чем прием данных — */ /* — с помощью функции recv()) — */ ConvertAudio ( ProcessingQueue, OutputQueue ) ;

if ( /*** в выходной очереди накоплено достаточно данных ***/ ) PlayAudio( OutputQueue) ;

/* — Если входной поток закончился — */ /* — и выходная очередь пуста — */ if ( bytes == 0 && /* — выходная очередь пуста — */ ) done = 1;

} Программа запрашивает данные, помещает их в очередь и обрабатывает. Но есть одна проблема: если обе очереди пусты, цикл while станет поглощающим.

Чтобы решить эту проблему, можно добавить в программу небольшой цикл за держки, так как она должна работать в реальном времени.

174 Часть П. Создание серверных приложений www.books-shop.com Запись данных по методике опроса Программный код выполняется в несколько раз быстрее, чем любая функция ввода вывода. (По сути, чтобы производительность компьютера повышалась, не обходимо покупать дополнительную память и заменять медленные устройства ввода вывода более новыми. Приобретение более быстродействующего процессо ра не дает такого эффекта.) Поэтому когда программа посылает сообщение или выдает какой нибудь запрос, операционная система заставляет ее ждать доста точно долго.

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

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

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

Листинг 8.2. Пример записи данных по методике опроса /**************************************************************/ /*** Пример алгоритма опроса: отправка изображения ***/ /*** нескольким клиентам ***/ /***************************************************************/ int pos[MAXCLIENTS];

bzero(pos, sizeof(pos));

for ( i = 0;

i < ClientCount;

i++ ) if ( fcntl(client[i], F_SETFL, 0_NONBLOCK) != 0 ) perrorf'Fcntl — could not set nonblocking");

• • • done = 0;

/*— повторяем до тех пор, пока все клиенты —*/ /*— не получат сообщение целиком —*/ while( !done ) { int bytes;

done = 0;

/*— для всех клиентов —*/ for { i = 0;

i < ClientCount;

i++ ) /*— если имеются неотправленные данные... —*/ if ( pos[i] < size ) { /*— отправляем сообщение, отслеживая, —*/ /*— сколько байтов послано —*/ bytes = send(client[i], buffer+pos[i], size pos[i], 0);

if ( bytes > 0 ) { pos[i] += bytes;

/* если сообщение благополучно отправлено —*/ /*— всем клиентам, завершаем работу —*/ Глава 8. Механизмы ввода вывода www.books-shop.com if ( pos[i] < size ) done = 0;

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

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

• присоединить новое сообщение к старому;

• отменить последнее сообщение и начать передачу нового;

• изменить частоту работы камеры.

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

Установление соединений по методике опроса Одним из редко применяемых алгоритмов опроса является прием запросов на подключение по разным портам. Как правило, серверная программа работает только с одним портом. Но ничто не мешает открыть в программе столько пор тов, сколько необходимо. Рассмотрим пример:

/*** Пример алгоритма опроса: проверка поступления запросов ***/ /*** на подключение по нескольким портам и создание для ***/ /*** каждого запроса нового задания ***/ • • • /*— Установка неблокируемого режима для каждого сокета —*/ for ( i = 0;

i < numports;

i++ ) if ( fcntl(client[i], F_SETFL, O_NONBLOCK) != 0 ) perror("Fcntl can't set nonblocking on port #%d", i);

for (;

;

) /* повторяем бесконечно */ { int client;

for ( i = 0;

i < numports;

i++ ) if ( (client = accept(sd[i], &addr, &size)) > 0 ) SpawnServer(sd[i], i);

/*** служебные действия ***/ 176 Часть II. Создание серверных приложений www.books-shop.com Представленный в программе алгоритм может показаться очень привлекатель ным, но в действительности создавать новое задание для каждой функции accept() не совсем удобно, так как требуется выполнить много вспомогательной работы для каждого задания. В действительности наилучшим решением является использование системного вызова select(), который дожидается изменения со стояния хотя бы одного из каналов ввода вывода в указанном наборе дескрипто ров (дополнительная информация об этой функции представлена ниже).

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

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

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

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

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

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

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

В листинге 8.3 представлен общий алгоритм асинхронного ввода вывода.

Глава 8. Механизмы ввода вывода www.books-shop.com Листинг 8.3. Алгоритм асинхронного ввода вывода /***************************************************************/ /*** Общий алгоритм асинхронного, или сигнального, ***/ /*** ввода вывода ***/ /***************************************************************/ int ready=0;

void sig_io(int sig) { /*** функция recv(): получаем все данные из буфера ***/ /*** функция send(): отправляем все обработанные данные ***/ ready =1;

/* сообщаем программе о завершении транзакции */ } for (;

;

) { if ( ready > 0 ) { /*** Временно блокируем сигнал SIGIO ***/ ready = 0;

/*** функция recv(): копируем данные в буферы для обработки ***/ /*** функция send(): заполняем выходной буфер из очереди обработанных данных ***/ /*** Разблокируем сигнал SIGIO ***/ } /*** Обработка поступающих данных ***/ /*** ИЛИ ***/ /*** Подготовка новых данных для отправки ***/ } Блокирование сигнала SIGIO может показаться несколько необычным. Оно не обходимо из за того, что обработчик сигнала и основная программа имеют доступ к одной и той же переменной, поэтому фактически в данном месте программы при сутствует критическая секция (см. главу 7, "Распределение нагрузки: многозадач ность"). Отключение обработчика подобно применению исключающего семафора.

Сообщения, помещенные в очередь Для обслуживания всех отложенных сообщений может потребоваться несколько раз вызвать в обработчике функцию send( ) или recv(). Ядро может послать программе несколько сигналов одновременно, но сама программа в конкретный момент времени получает сигнал только одного типа. Если два сигнала поступают с очень маленьким интервалом, программа распознает их как один сигнал.

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

178 Часть П. Создание серверных приложений www.books-shop.com /*** Запуск обработчика сигналов SIGIO ***/ /*****************************************************/ if ( fcntl(sd, F_SETFL, O_ASYNC | O_NONBLOCK) < 0 ) PANIC("Can't make socket asynch & nonblocking");

if ( fcntl(sd, F_SETOWN, getpid()) < 0 ) PANIC("Can't own SIGIO");

Когда программа запрашивает получение сигналов ввода вывода, это касается всех сигналов данной группы, а не только SIGIO. Другим возможным сигналом является SIGURG, посылаемый при передаче внеполосных данных.(Дополнительная информация приводится в главе 9, "Повышение производительности".) Чтение данных по запросу Программа может обрабатывать данные в асинхронном режиме по мере их по ступления (по запросу). Когда приходит новая порция данных, ядро посылает программе сигнал. В ответ на это программа извлекает данные из буфера и обра батывает их. Это напоминает конвейерную линию, в которой изделия собираются по мере продвижения.

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

(Помните: можно открыть несколько каналов ввода вывода, но придется вручную проверять, для какого из каналов был послан сигнал.) В качестве примера рас смотрим процедуру обработки VRML документа (Virtual Reality Modeling Language — язык моделирования виртуальной реальности), представленную в листинге 8.4.

Листинг 8.4. Алгоритм асинхронного чтения /***************************************************************/ /*** Пример асинхронного чтения VRML документа: обработка ***/ /*** порции данных во время ожидания следующей порции ***/ int ready=0, bytes;

void sig_io(int sig) { /*— чтение отложенных сообщений —*/ bytes = recv(server, buffer, sizeof(buffer), 0);

if ( bytes < 0 ) perror("SIGIO");

ready =1;

/* сообщаем программе о завершении транзакции */ } /*— Разрешаем асинхронный, неблокируемый ввод вывод —*/ if ( fcntl(sd, F_SETFL, 0_ASYNC | O_NTONBLOCK) < 0 ) PANIC("Can't make socket asynch & nonblocking");

/*— Заявляем о готовности обрабатывать —*/ Глава 8. Механизмы ввода вывода www.books-shop.com /*— сигналы SIGIO и SIGURG —*/ if ( fcntl(sd, F_SETOWN, getpid()) < 0 ) PANIC("Can't own SIGIO");

while ( Idone ) if ( ready > 0 ) /*** Временно блокируем сигнал SIGIO ***/ ready = 0;

FillQueue(Queue, buffer, bytes);

/*** Разблокируем сигнал SIGIO ***/ /*** Обработка поступающих данных в модуле растрирования ***/ /*** в течение короткого промежутка времени или до тех ***/ /*** пор, пока переменная ready не изменится ***/ } Избегайте выполнения большого количества действий в обработчике сигналов.

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

Когда буфер считается пустым?

Когда уровень заполнения входного буфера достигает нижней отметки (минимальное число бай тов, после которого система начинает посылать сигнал SIGIO), программе посылается уведом ление. Этот уровень по умолчанию равен 1, т.е. всякий раз, когда в буфер записывается хотя бы один байт, программе будет отправлен сигнал. В некоторых системах это значение можно уве личить с помощью функции setsockopt{) (описана в главе 9, "Повышение производительно сти"). Но похоже, что в Linux данная возможность не поддерживается.

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

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

Листинг 8.5. Алгоритм асинхронной записи /*** Пример асинхронной записи: генерирование ответа ***/ /*** в процессе обработки запроса ***/ 180 Часть II. Создание серверных приложений www.books-shop.com 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. Механизмы ввода вывода piracy@books-shop.com /* — прием запросов на подключение;

если запросов —*/ /* — слишком много, выдаем сообщение об ошибке —*/ /* — и разрываем связь —*/ if ( (client = acceptf sd, 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 #include linclude 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.

Pages:     | 1 | 2 || 4 | 5 |   ...   | 8 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

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