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.
Данный текст является ознакомительным фрагментом.