UNIX: разработка сетевых приложений
Коллизии при вызове функции select
Рассматривая данный пример в 4.4BSD, мы можем исследовать еще одну проблему, которая встречается довольно редко и поэтому часто остается непонятой до конца. В разделе 16.13 [128] говорится о коллизиях (collisions), возникающих при вызове функции
несколькими процессами на одном и том же дескрипторе, и о том, каким образом ядро решает эту проблему. Суть проблемы в том, что в структуреselectпредусмотрено место только для одного идентификатора процесса, который выводится из состояния ожидания по готовности дескриптора. Если же имеется несколько процессов, ожидающих, когда будет готов данный дескриптор, то ядро должно вывести из состояния ожидания все процессы, блокированные в вызове функцииsocket, так как ядро не знает, какие именно процессы ожидают готовности данного дескриптора.selectКоллизии при вызове функции
в нашем примере можно форсировать, предваряя вызов функцииselectиз листинга 30.9 вызовом функцииacceptв ожидании готовности к чтению на прослушиваемом сокете. Дочерние процессы будут теперь блокированы в вызове функцииselect, а не в вызове функции accept. В листинге 30.11 показана изменяемая часть функцииselect, при этом измененные по отношению к листингу 30.9 строки отмечены знакамиchild_main.+Листинг 30.11. Модификация листинга 30.9: блокирование в вызове select вместо блокирования в вызове accept
printf("child %ld starting\n", (long)getpid());+ FD_ZERO(&rset);for (;;) {+ FD_SET(listenfd, &rset);+ Select(listenfd+1, &rset, NULL, NULL, NULL);+ if (FD_ISSET(listenfd, &rset) == 0)+ err_quit("listenfd readable");+clilen = addrlen;connfd = Accept(listenfd, cliaddr, &clilen);web_child(connfd); /* обработка запроса */Close(connfd);}Если, проделав это изменение, мы проверим значение счетчика ядра BSD/OS
, мы увидим, что в первом случае при запуске сервера произошло 1814 коллизий, а во втором случае — 2045. Так как при каждом запуске сервера два клиента создают в сумме 5000 соединений, приведенные выше значения указывают, что примерно в 35-40% случаев вызовы функцииnselcollприводят к коллизиям.selectЕсли сравнить значения времени, затраченного центральным процессором в этом примере, то получится, что при добавлении вызова функции
это значение увеличивается с 1,8 до 2,9 с. Частично это объясняется, вероятно, добавлением системного вызова (так как теперь мы вызываем не толькоselect, но еще иaccept), а частично — накладными расходами, связанными с коллизиями.selectИз этого примера следует вывод, что когда несколько процессов блокируются на одном и том же дескрипторе, лучше, чтобы эта блокировка была связана с функцией
, а не с функциейaccept.select30.7. Сервер TCP с предварительным порождением процессов и защитой вызова accept блокировкой файла
Описанная выше реализация, позволяющая нескольким процессам вызывать функцию
на одном и том же прослушиваемом дескрипторе, возможна только для систем 4.4BSD, в которых функцияacceptреализована внутри ядра. Ядра системы SVR4, в которых accept реализована как библиотечная функция, не допускают этого. В самом деле, если мы запустим сервер из предыдущего раздела, в котором имеется несколько дочерних процессов, в Solaris 2.5 (система SVR4), то вскоре после того, как клиенты начнут соединяться с сервером, вызов функцииacceptв одном из дочерних процессов вызовет ошибкуaccept, что свидетельствует об ошибке протокола.EPROTOПРИМЕЧАНИЕПричины возникновения этой проблемы с библиотечной версией функции accept в SVR4 связаны с реализацией потоков STREAMS и тем фактом, что библиотечная функция accept не является атомарной операцией. В Solaris 2.6 эта проблема решена, но в большинстве реализаций SVR4 она остается.
Решением этой проблемы является защита вызова функции
при помощи блокировки, так что в данный момент времени только один процесс может быть блокирован в вызове этой функции. Другие процессы также будут блокированы, так как они будут стремиться установить блокировку для вызова функцииaccept.acceptСуществует несколько способов реализации защиты вызова функции
, о которых рассказывается во втором томе [2] данной серии. В этом разделе мы используем блокировку файла функциейacceptсогласно стандарту POSIX.fcntlЕдинственным изменением в функции
(см. листинг 30.6) будет добавление вызова функцииmainперед началом цикла, в котором создаются дочерние процессы:my_lock_init+ my_lock_init("/tmp/lock.XXXXXX"); /* один файл для всех дочернихпроцессов */for (i = 0; i < nchildren; i++)pids[i] = child_make(i, listenfd, addrlen); /* возвращениеродительского процесса */Функция
остается такой же, как в листинге 30.8. Единственным изменением функцииchild_make(см. листинг 30.9) является блокирование перед вызовом функцииchild_mainи снятие блокировки после завершения этой функции:acceptfor (;;) {clilen = addrlen;+ my_lock_wait();connfd = Accept(listenfd, cliaddr, &clilen);+ my_lock_release();web_child(connfd); /* обработка запроса */Close(connfd);}В листинге 30.12 показана наша функция
, в которой используется блокировка файла согласно стандарту POSIX.my_lock_init