
Распределениенагрузки Глава
многозадачность 7
В этой главе... |
|
Понятие о многозадачности: процессы и потоки |
136 |
Обгоняя время: исключающие семафоры и гонки |
|
|
160 |
Управление дочерними заданиями и задания зомби |
164 |
Расширение существующих версий клиента и |
|
сервера |
167 |
Вызов внешних программ с помощью функций |
|
семейства ехес() |
168 |
Резюме |
171 |
Ⱦɚɧɧɚɹ ɜɟɪɫɢɹ ɤɧɢɝɢ ɜɵɩɭɳɟɧɚ ɷɥɟɤɬɪɨɧɧɵɦ ɢɡɞɚɬɟɥɶɫɬɜɨɦ %RRNV VKRS Ɋɚɫɩɪɨɫɬɪɚɧɟɧɢɟ ɩɪɨɞɚɠɚ ɩɟɪɟɡɚɩɢɫɶ ɞɚɧɧɨɣ ɤɧɢɝɢ ɢɥɢ ɟɟ ɱɚɫɬɟɣ ɁȺɉɊȿɓȿɇɕ Ɉ ɜɫɟɯ ɧɚɪɭɲɟɧɢɹɯ ɩɪɨɫɶɛɚ ɫɨɨɛɳɚɬɶ ɩɨ ɚɞɪɟɫɭ 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.0
и2.2 скорость переключения заданий почти такая же. Сходство возникает из за четко отлаженного алгоритма переключения.
ВLinux также поддерживается симметричная мультипроцессорная обработка. Если программа написана с учетом многозадачности, то в мультипроцессорной
системе она получает дополнительное ускорение. (На момент написания книги в Linux могло одновременно поддерживаться максимум 16 процессоров.)
Глава 7. Распределение нагрузки: многозадачность |
133 |
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. Распределение нагрузки: многозадачность |
135 |
www.books-shop.com
Планирование заданий в Linux
В многозадачных операционных системах применяются различные методики планирования зада ний. В Linux используется схема приоритетного кругового обслуживания. В этой схеме каждое задание по очереди получает свою долю процессорного времени. Задания с высоким приорите том перемещаются по списку быстрее, чем те, у которых низкий приоритет.
Сравнение процессов и потоков
Различия между процессами и потоками не всегда очевидны. В следующей таблице проведено их детальное сравнение.
Процессы
После успешного вызова функции fork() сущест вуют два процесса, выполняющихся параллельно
Потоки
Родительская программа указывает имя функции, которая будет выполняться в качестве дочернего потока
Дочерний процесс должен быть явно завершен с |
Дочерний поток можно завершить явно либо неявно |
помощью системного вызова exit () |
с помощью функции pthread_exit(void* arg) |
|
или инструкции return |
Общих данных нет; единственная информация, пе |
Потомок имеет доступ к данным предка, принимая |
редаваемая потомку, — это снимок данных роди |
от него параметры и возвращая значения |
тельского процесса |
|
Дочерний процесс всегда связан с родительским; когда процесс потомок завершается, его предок должен произвести очистку
Поскольку данные процесса недоступны другим процессам, не происходит конфликтов при доступе к ресурсам
Независимая работа с файловой системой
Таблицы дескрипторов открытых файлов не являют ся общими; операционная система копирует табли цы, поэтому если в двух процессах открыт один и тот же файл, то закрытие его в одном процессе не приведет к изменению работы другого процесса
Дочерний поток может выполняться независимо от родительского и завершиться без его вмешательст ва (если поток не является независимым, родитель ская программа также должна производить очистку после него)
Все совместно используемые данные должны быть идентифицированы и заблокированы, чтобы не про изошло их повреждение
Потомок реагирует на все изменения текущего ка талога (команда chdir), корневого каталога (команда enroot) и стандартного режима доступа к файлам (команда umask)
Совместное использование таблиц дескрипторов; если дочерний поток закрывает файл, родительский поток теряет к нему доступ
Сигналы обрабатываются независимо |
Один поток может блокировать сигнал с помощью |
|
функции sigprocmask(), не влияя на работу дру |
|
гих потоков |
Создание процесса
Многозадачность чаще всего реализуется с помощью процессов. Процесс представляет собой новый экземпляр программы, наследующий от нее копии де скрипторов открытых каналов ввода вывода и не обменивающийся никакими
136 Часть И. Создание серверных приложений
www.books-shop.com
другими данными. Для порождения нового процесса предназначен системный вызов fork():
#include <unistd.h> 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. Распределение нагрузки: многозадачность |
137 |
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.2 происходит распределение полномочий. Когда какая то внешняя программа по сылает запрос, предок создает потомка, который обрабатывает запрос. Большин ство серверов работает именно по такой схеме.
Часто требуется одновременно выполнять разные действия. Они могут быть одинаковыми с алгоритмической точки зрения, но использовать отличающиеся наборы данных. Не имея подобной возможности, программам пришлось бы тра тить время на повторное выполнение одних и тех же функций. Дифференцирова! ние означает разделение заданий таким образом, чтобы они не дублировали друг друга. Хотя программа, представленная в листинге 7.3, корректна с синтаксиче ской точки зрения, она уничтожает суть многозадачности, так как в ней не про исходит дифференцирования.
Листинг 7.3. Ветвление без дифференцирования
/*************************************************************/
/*** |
Это пример дублирования. В подобном варианте вызова ***/ |
|
/*** |
функции fork() задания дублируют друг друга, что |
***/ |
/*** |
приводит к бессмысленной трате ресурсов процессора. |
***/ |
/*************************************************************/
/*— какие то действия —*/ fork();
/*— продолжение —*/
В нашей книге многозадачность без дифференцирования называется дублиро! ванием или слиянием. Как правило, подобной ситуации следует избегать. Дубли рование может также произойти, когда процесс не завершился корректно путем явного вызова функции exit().
138 |
Часть И. Создание серверных приложений |
www.books-shop.com
Устойчивость к ошибкам за счет слияния
Дублирование процессов может применяться при реализации отказоустойчивых систем. В отка зоустойчивой системе вычисления дублируются с целью повышения достоверности результатов. Запускается несколько одинаковых заданий, каждое из которых закрепляется за отдельным про цессором {эта возможность еще не реализована в Linux). Через определенный промежуток вре мени все задания посылают свои результаты модулю проверки. Если в каком то процессоре произошел сбой, полученные данные будут отличаться. Соответствующее задание выгружается.
Если при выполнении функции fork() произошла ошибка, то, очевидно, воз никли проблемы с таблицей страниц процесса или с ресурсами памяти. Одним из признаков перегруженности системы является отказ в предоставлении ресурсов. Виртуальная память и таблицы страниц являются основой правильного функцио нирования операционной системы. Поскольку эти ресурсы очень важны, Linux ограничивает общий объем ресурсов, которыми может владеть процесс. Когда не возможно выделить блок памяти требуемого размера или нельзя создать новое за дание, значит, система испытывает острую нехватку памяти.
Создание потока
Благодаря потокам можно организовать совместный доступ к ресурсам со сто роны родительской программы и всех ее дочерних заданий. Создавая потоки, программа может поручить им обработку данных, с тем чтобы самой сосредото читься на решении основной задачи. Например, один поток может читать графи ческий файл с диска, а другой — отображать его на экране. В процессах столь тесного взаимодействия, как правило, не требуется.
Одной из наиболее известных реализаций многопотоковых функций является библиотека Pthreads. Она совместима со стандартом POSIX 1с. Программы, в ко торых используется эта библиотека, будут выполняться в других операционных системах, поддерживающих стандарт POSIX. В библиотеке Pthreads новый поток создается с помощью функции pthread_create ():
#include <pthread.h>
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. Распределение нагрузки: многозадачность |
139 |
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. Пример создания процесса
/*** |
В этом примере создается процесс |
***/ |
voidChild_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