Литмир - Электронная Библиотека
Содержание  
A
A

Дескрипторы — это просто индексы массива, содержащегося в структуре

proc
, который ссылается на структуру
file
. Одна из целей дублирования дескрипторов в дочерних процессах, осуществляемого функцией
fork
, заключается в том, чтобы данный дескриптор в дочернем процессе ссылался на ту же структуру
file
, на которую этот дескриптор ссылается в родительском процессе. Каждая структура
file
содержит счетчик ссылок, который начинается с единицы, когда открывается первый файл или сокет, и увеличивается на единицу при каждом вызове функции fork и при каждом дублировании дескриптора (с помощью функции
dup
). В нашем примере с N дочерними процессами счетчик ссылок в структуре
file
будет содержать значение N+1 (учитывая родительский процесс, у которого по-прежнему открыт прослушиваемый дескриптор, хотя родительский процесс никогда не вызывает функцию
accept
).

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

accept
, и все они переводятся родительским процессом в состояние ожидания [128, с. 458]. Когда от клиента прибывает первый запрос на соединение, все N дочерних процессов «просыпаются», так как все они были переведены в состояние ожидания по одному и тому же «каналу ожидания» — полю
so_timeo
структуры
socket
, как совместно использующие один и тот же прослушиваемый дескриптор, указывающий на одну и ту же структуру
socket
. Хотя «проснулись» все N дочерних процессов, только один из них будет связан с клиентом. Остальные N - 1 снова перейдут в состояние ожидания, так как длина очереди клиентских запросов снова станет равна нулю, после того как первый из дочерних процессов займется обработкой поступившего запроса.

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

accept
) очередное клиентское соединение. В следующем разделе мы исследуем, как это влияет на производительность в целом.

Эффект наличия слишком большого количества дочерних процессов

В табл. 30.1 (строка 2) указано время (1,8 с), затрачиваемое центральным процессором в случае наличия 15 дочерних процессов, обслуживающих не более 10 клиентов. Мы можем оценить эффект «общей побудки», увеличивая количество дочерних процессов и оставляя то же максимальное значение количества обслуживаемых клиентов (10). Мы не показываем результаты, получаемые при увеличении количества дочерних потоков, потому что они не настолько интересны. Поскольку любое количество дочерних потоков свыше 10 может считаться избыточным, проблема «общей побудки» усугубляется, а затрачиваемое на управление процессами время увеличивается.

ПРИМЕЧАНИЕ

Некоторые ядра Unix снабжены функцией, которая выводит из состояния ожидания только один процесс для обработки одного клиентского запроса [107]. Чаще всего она называется wakeup_one.

Распределение клиентских соединений между дочерними процессами

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

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

long *cptr, *meter(int); /* для подсчета количества клиентов на один

дочерний процесс */

cptr = meter(nchildren); /* перед порождением дочернего процесса */

В листинге 30.10 показана функция

meter
.

Листинг 30.10. Функция meter, которая размещает массив в совместно используемой памяти

//server/meter.c

 1 #include "unp.h"

 2 #include <sys/mman.h>

 3 /* Размещаем массив "nchildren" длинных целых чисел

 4  * в совместно используемой области памяти.

 5  * Эти числа используются как счетчики количества

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

 6  * см. с. 467-470 книги [110]"

 7  */

 8 long*

 9 meter(int nchildren)

10 {

11  int fd;

12  long *ptr;

13 #ifdef MAP_ANON

14  ptr = Mmap(0, nchildren * sizeof(long), PROT_READ | PROT_WRITE,

15   MAP_ANON | MAP_SHARED, -1, 0);

16 #else

17  fd = Open("/dev/zero", O_RDWR, 0);

18  ptr = Mmap(0, nchildren * sizeof(long), PROT_READ | PROT_WRITE,

19   MAP_SHARED, fd, 0);

20  Close(fd);

21 #endif

22  return (ptr);

23 }

Мы используем неименованное отображение в память, если оно поддерживается (например, в 4.4BSD), или отображение файла

/dev/zero
(например, SVR4). Поскольку массив создается функцией
mmap
до того, как родительский процесс порождает дочерние, этот массив затем используется совместно родительским и всеми дочерними процессами, созданными функцией
fork
.

Затем мы модифицируем нашу функцию

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

В табл. 30.2 показано распределение нагрузки по дочерним процессам. Когда свободные дочерние процессы блокированы вызовом функции

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

348
{"b":"225366","o":1}