30.7. Сервер TCP с предварительным порождением процессов и защитой вызова accept блокировкой файла

30.7. Сервер TCP с предварительным порождением процессов и защитой вызова accept блокировкой файла

Описанная выше реализация, позволяющая нескольким процессам вызывать функцию accept на одном и том же прослушиваемом дескрипторе, возможна только для систем 4.4BSD, в которых функция accept реализована внутри ядра. Ядра системы SVR4, в которых accept реализована как библиотечная функция, не допускают этого. В самом деле, если мы запустим сервер из предыдущего раздела, в котором имеется несколько дочерних процессов, в Solaris 2.5 (система SVR4), то вскоре после того, как клиенты начнут соединяться с сервером, вызов функции accept в одном из дочерних процессов вызовет ошибку EPROTO, что свидетельствует об ошибке протокола.

ПРИМЕЧАНИЕ

Причины возникновения этой проблемы с библиотечной версией функции accept в SVR4 связаны с реализацией потоков STREAMS и тем фактом, что библиотечная функция accept не является атомарной операцией. В Solaris 2.6 эта проблема решена, но в большинстве реализаций SVR4 она остается.

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

Существует несколько способов реализации защиты вызова функции accept, о которых рассказывается во втором томе[2] данной серии. В этом разделе мы используем блокировку файла функцией fcntl согласно стандарту POSIX.

Единственным изменением в функции main (см. листинг 30.6) будет добавление вызова функции my_lock_init перед началом цикла, в котором создаются дочерние процессы:

+ my_lock_init("/tmp/lock.XXXXXX"); /* один файл для всех дочерних

                                       процессов */

  for (i = 0; i < nchildren; i++)

   pids[i] = child_make(i, listenfd, addrlen); /* возвращение

                                       родительского процесса */

Функция child_make остается такой же, как в листинге 30.8. Единственным изменением функции child_main (см. листинг 30.9) является блокирование перед вызовом функции accept и снятие блокировки после завершения этой функции:

  for (;;) {

   clilen = addrlen;

+  my_lock_wait();

    connfd = Accept(listenfd, cliaddr, &clilen);

+   my_lock_release();

    web_child(connfd); /* обработка запроса */

    Close(connfd);

   }

В листинге 30.12 показана наша функция my_lock_init, в которой используется блокировка файла согласно стандарту POSIX.

Листинг 30.12. Функция my_lock_init: блокировка файла

//server/lock_fcntl.c

 1 #include "unp.h"

 2 static struct flock lock_it, unlock_it;

 3 static int lock_fd = -1;

 4 /* fcntl() не выполнится, если не будет вызвана функция my_lock_init() */

 5 void

 6 my_lock_init(char *pathname)

 7 {

 8  char lock_file[1024];

 9  /* копируем строку вызывающего процесса на случай, если это константа */

10  strncpy(lock_file, pathname, sizeof(lock_file));

11  lock_fd = Mkstemp(lock_file);

12  Unlink(lock_file); /* но lock_fd остается открытым */

13  lock_it.l_type = F_WRLCK;

14  lock_it.l_whence = SEEK_SET;

15  lock_it.l_start = 0;

16  lock_it.l_len = 0;

17  unlock_it.l_type = F_UNLCK;

18  unlock_it.l_whence = SEEK_SET;

19  unlock_it.l_start = 0;

20  unlock_it.l_len = 0;

21 }

9-12 Вызывающий процесс задает шаблон для имени файла в качестве аргумента функции my_lock_init, и функция mkstemp на основе этого шаблона создает уникальное имя файла. Затем создается файл с этим именем и сразу же вызывается функция unlink, в результате чего имя файла удаляется из каталога. Если в программе впоследствии произойдет сбой, то файл исчезнет безвозвратно. Но пока он остается открытым в одном или нескольких процессах (иными словами, пока счетчик ссылок для этого файла больше нуля), сам файл не будет удален. (Отметим, что между удалением имени файла из каталога и закрытием открытого файла существует фундаментальная разница.)

13-20 Инициализируются две структуры flock: одна для блокирования файла, другая для снятия блокировки. Блокируемый диапазон начинается с нуля (l_whence =SEEK_SET, l_start=0). Значение l_len равно нулю, то есть блокирован весь файл. В этот файл ничего не записывается (его длина всегда равна нулю), но такой тип блокировки в любом случае будет правильно обрабатываться ядром.

ПРИМЕЧАНИЕ

Сначала автор инициализировал эти структуры при объявлении:

static struct flock lock_it = { F_WRLCK, 0, 0, 0, 0 };

static struct flock unlock_it = { F_UNLCK, 0, 0, 0, 0 };

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

Исключением из этого правила является ситуация, когда инициализатор структуры обеспечивается реализацией. Например, при инициализации взаимного исключения в POSIX в главе 26 мы писали:

pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER;

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

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

Листинг 30.13. Функции my_lock_wait (установление блокировки файла) и my_lock_release (снятие блокировки файла)

//server/lock_fcntl.c

23 void

24 my_lock_wait()

25 {

26  int rc;

27  while ((rc = fcntl(lock_ld, F_SETLKW, &lock_it)) < 0 {

28   if (errno == EINTR)

29    continue;

30   else

31    errsys("fcntl error for my_lock_wait");

32  }

33 }

34 void

35 my_lock_release()

36 {

37  if (fcntl(lock_fd, F_SETLKW, &unlock_it)) < 0)

38   errsys("fcntl error for my_lock_release");

39 }

Новая версия нашего сервера с предварительным порождением процессов работает теперь под SVR4, гарантируя, что в данный момент времени только один дочерний процесс блокирован в вызове функции accept. Сравнивая строки 2 и 3 в табл. 30.1 (результаты для серверов Digital Unix и BSD/OS), мы видим, что такой тип блокировки увеличивает время, затрачиваемое центральным процессором на узле сервера.

ПРИМЕЧАНИЕ

Веб-сервер Apache (http://www.apache.org) использует технологию предварительного порождения процессов, причем если позволяет реализация, все дочерние процессы блокируются в вызове функции accept, иначе используется блокировка файла для защиты вызова accept.

Данный текст является ознакомительным фрагментом.