Текст книги "UNIX: разработка сетевых приложений"
Автор книги: Уильям Ричард Стивенс
Соавторы: Эндрю М. Рудофф,Билл Феннер
Жанр:
ОС и Сети
сообщить о нарушении
Текущая страница: 34 (всего у книги 88 страниц) [доступный отрывок для чтения: 36 страниц]
Функция gethostbynameиз раздела 11.3 имеет интересную особенность, которую мы еще не рассматривали: она не допускает повторное вхождение (nonreentrant). Мы еще столкнемся с этой проблемой в главе 23, когда будем обсуждать потоки, но не менее интересно найти решение этой проблемы сейчас, без необходимости обращаться к понятию потоков.
Сначала посмотрим, как эта функция работает. Если мы изучим ее исходный код (это несложно, поскольку исходный код для всей реализации BIND свободно доступен), то увидим, что обе функции – и gethostbyname, и gethostbyaddr– содержатся в одном файле, который имеет следующий вид:
staticstruct hostent host; /* здесь хранится результат */
struct hostent*
gethostbyname(const char *hostname) {
return(gethostbyname2(hostname, family));
}
struct hostent*
gethostbyname2(const char *hostname, int family) {
/* вызов функций DNS для запроса А или AAAA */
/* заполнение структуры адреса узла */
return(&host);
}
struct hostent*
gethostbyaddr(const char *addr, size_t len, int family) {
/* вызов функций DNS для запроса PTR в домене in-addr.arpa */
/* заполнение структуры адреса узла */
return(&host);
}
Мы выделили полужирным шрифтом спецификатор класса памяти staticитоговой структуры, потому что основная проблема в нем. Тот факт, что эти три функции используют общую переменную host, представляет другую проблему, которую мы обсудим в упражнении 11.1. (Вспомните табл. 11.4.) Функция gethostbyname2появилась в BIND 4.9.4 с добавлением поддержки IPv6. Мы будем игнорировать тот факт, что когда мы вызываем функцию gethostbyname, задействуется функция gethostbyname2, поскольку это не относится к предмету обсуждения.
Проблема повторного вхождения может возникнуть в нормальном процессе Unix, вызывающем функцию gethostbynameили gethostbyaddrи из управляющего элемента главного потока, и из обработчика сигнала. Когда вызывается обработчик сигнала (допустим, это сигнал SIGALRM, который генерируется раз в секунду), главный поток управляющего элемента процесса временно останавливается и вызывается функция обработки сигнала. Рассмотрим следующую ситуацию:
main() {
struct hostent *hptr;
...
signal(SIGALRM, sig_alrm);
...
hptr = gethostbyname( ... );
...
}
void
sig_alrm(int signo) {
struct hostent *hptr;
...
hptr = gethostbyname( ... );
...
}
Если главный поток управления в момент остановки находится в середине выполнения функции gethostbyname(допустим, функция заполнила переменную hostи должна сейчас возвратить управление), а затем обработчик сигналов вызывает функцию gethostbyname, то поскольку в процессе существует только один экземпляр переменной host, эта переменная используется снова. При этом значения переменных, вычисленные при вызове из главного потока управления, заменяются значениями, вычисленными при вызове из обработчика сигнала.
Если мы посмотрим на функции преобразования имен и адресов, представленные в этой главе и в главе 9, вместе с функциями inet_ XXXиз главы 4, мы заметим следующее:
■ Функции gethostbyname, gethostbyname2, gethostbyaddr, getservbynameи getservbyportтрадиционно не допускают повторного вхождения, поскольку все они возвращают указатель на статическую структуру.
Некоторые реализации, поддерживающие программные потоки (Solaris 2.x), предоставляют версии этих четырех функций, допускающие повторное вхождение, с именами, оканчивающимися суффиксом _r. О них рассказывается в следующем разделе.
В качестве альтернативы некоторые реализации с поддержкой программных потоков (Digital Unix 4.0 и HP_UX 10.30) предоставляют версии этих функций, допускающие повторное вхождение за счет использования собственных данных программных потоков.
■ Функции inet_ptonи inet_ntopвсегда допускают повторное вхождение.
■ Исторически функция inet_ntoaне допускает повторное вхождение, но некоторые реализации с поддержкой потоков предоставляют версию, допускающую повторное вхождение, которая строится на основе собственных данных потоков.
■ Функция getaddrinfoдопускает повторное вхождение, только если она сама вызывает функции, допускающие повторное вхождение, то есть если она вызывает соответствующую версию функции gethostbynameили getservbynameдля имени узла или имени службы. Одной из причин, по которым вся память для результатов ее выполнения выделяется динамически, является возможность повторного вхождения.
■ Функция getnameinfoдопускает повторное вхождение, только если она сама вызывает такие функции, то есть если она вызывает соответствующую версию функции gethostbyaddrдля получения имени узла или функции getservbyportдля получения имени службы. Обратите внимание, что обе результирующих строки (для имени узла и для имени службы) размещаются в памяти вызывающим процессом, чтобы обеспечить возможность повторного вхождения.
Похожая проблема возникает с переменной errno. Исторически существовало по одной копии этой целочисленной переменной для каждого процесса. Если процесс выполняет системный вызов, возвращающий ошибку, то в этой переменной хранится целочисленный код ошибки. Например, функция closeиз стандартной библиотеки языка С может выполнить примерно такую последовательность действий:
■ поместить аргумент системного вызова (целочисленный дескриптор) в регистр;
■ поместить значение в другой регистр, указывая, что был сделан системный вызов функции close;
■ активизировать системный вызов (переключиться на ядро со специальной инструкцией);
■ проверить значение регистра, чтобы увидеть, что произошла ошибка;
■ если ошибки нет, возвратить (0);
■ сохранить значение какого-то другого регистра в переменной errno;
■ возвратить (-1).
Прежде всего заметим, что если ошибки не происходит, значение переменной errnoне изменяется. Поэтому мы не можем посмотреть значение этой переменной, пока мы не узнаем, что произошла ошибка (обычно на это указывает возвращаемое функцией значение -1).
Будем считать, что программа проверяет возвращаемое значение функции closeи затем выводит значение переменной errno, если произошла ошибка, как в следующем примере:
if (close(fd) < 0) {
fprintf(stderr, "close error, errno = $dn", errno);
exit(1);
}
Существует небольшой промежуток времени между сохранением кода ошибки в переменной errno в тот момент, когда системный вызов возвращает управление, и выводом этого значения программой. В течение этого промежутка другой программный поток внутри процесса (то есть обработчик сигналов) может изменить значение переменной errno. Если, например, при вызове обработчика сигналов главный поток управления находится между closeи fprintfи обработчик сигналов делает какой-то другой системный вызов, возвращающий ошибку (допустим, вызывается функция write), то значение переменной errno, записанное при вызове функции close, заменяется на значение, записанное при вызове функции write.
При рассмотрении этих двух проблем в связи с обработчиками сигналов одним из решений проблемы с функцией gethostbyname(возвращающей указатель на статическую переменную) будет не вызывать из обработчика сигнала функции, которые не допускают повторное вхождение. Проблемы с переменной errno(одна глобальная переменная, которая может быть изменена обработчиком сигнала) можно избежать, перекодировав обработчик сигнала так, чтобы он сохранял и восстанавливал значение переменной errnoследующим образом:
void sig_alrm(int signo) {
int errno_save;
errno_save = errno; /* сохраняем значение этой переменной
при вхождении */
if (write( ... ) != nbytes)
fprintf(stderr, "write error, errno = %dn", errno);
errno = errno_save; /* восстанавливаем значение этой переменной
при завершении */
}
В этом коде мы также вызываем функцию fprintf, стандартную функцию ввода-вывода, из обработчика сигнала. Это еще одна проблема повторного вхождения, поскольку многие версии функций стандартной библиотеки ввода-вывода не допускают повторного вхождения: стандартные функции ввода-вывода не должны вызываться из обработчиков сигналов.
Мы вернемся к проблеме повторного вхождения в главе 26 и увидим, как проблема с переменной errnoрешается с помощью потоков. В следующем разделе описываются некоторые версии функций имен узлов, допускающие повторное вхождение.
Чтобы превратить функцию, не допускающую повторное вхождение, такую как gethostbyname, в повторно входимую, можно воспользоваться двумя способами.
1. Вместо заполнения и возвращения статической структуры вызывающий процесс размещает структуру в памяти, и функция, допускающая повторное вхождение, заполняет эту структуру. Эта технология используется для перехода от функции gethostbyname(которая не допускает повторное вхождение) к функции gethostbyname_r(которая допускает повторное вхождение). Но это решение усложняется, поскольку помимо того, что вызывающий процесс должен предоставить структуру hostentдля заполнения, эта структура также указывает на другую информацию: каноническое имя, массив указателей на псевдонимы, строки псевдонимов, массив указателей на адреса и сами адреса (см., например, рис. 11.2). Вызывающий процесс должен предоставить один большой буфер, используемый для дополнительной информации, и заполняемая структура hostentбудет содержать различные указатели на этот буфер. При этом добавляется как минимум три аргумента функции: указатель на заполняемую структуру hostent, указатель на буфер, используемый для всей прочей информации, и размер этого буфера. Требуется также четвертый дополнительный аргумент – указатель на целое число, в котором будет храниться код ошибки, поскольку глобальная целочисленная переменная h_errnoбольше не может использоваться. (Глобальная целочисленная переменная h_errnoсоздает ту же проблему повторного вхождения, которая описана нами для переменной errno.)
Эта технология также используется функциями getnameinfoи inet_ntop.
2. Входящая функция вызывает функцию mallocи динамически выделяет память. Это технология, используемая функцией getaddrinfo. Проблема при таком подходе заключается в том, что приложение, вызывающее эту функцию, должно вызвать также функцию freeaddrinfo, чтобы освободить динамическую память. Если эта функция не вызывается, происходит утечка памяти: каждый раз, когда процесс вызывает функцию, выделяющую память, объем памяти, задействованной процессом, возрастает. Если процесс выполняется в течение длительного времени (что свойственно сетевым серверам), то потребление памяти этим процессом с течением времени неуклонно растет.
Обсудим функции Solaris 2.x, допускающие повторное вхождение, не используемые для сопоставления имен с адресами, и наоборот (то есть для разрешения имен).
#include
struct hostent *gethostbyname_r(const char * hostname,
struct hostent * result, char * buf, int buflen, int * h_errnop);
struct hostent *gethostbyaddr_r(const char * addr, int len,
int type, struct hostent * result, char * buf, int buflen,
int * h_errnop);
Обе функции возвращают: непустой указатель в случае успешного выполнения, NULL в случае ошибки
Для каждой функции требуется четыре дополнительных аргумента. Аргумент result – это структура hostent, размещенная в памяти вызывающим процессом и заполняемая данной функцией. При успешном выполнении функции этот указатель также является возвращаемым значением.
Аргумент buf– это буфер, размещенный в памяти вызывающим процессом, a buflen – его размер. Буфер будет содержать каноническое имя, массив указателей на псевдонимы, строки псевдонимов, массив указателей на адреса и сами адреса. Все указатели в структуре hostent, на которую указывает result, указывают на этот буфер. Насколько большим должен быть этот буфер? К сожалению, все, что сказано в большинстве руководств, это что-то неопределенное вроде «Буфер должен быть достаточно большим, чтобы содержать все данные, связанные с записью узла». Текущие реализации функции gethostbynameмогут возвращать до 35 указателей на альтернативные имена (псевдонимы), до 35 указателей на адреса и использовать буфер размером 8192 байт для хранения альтернативных имен (псевдонимов) и адресов. Поэтому буфер размером 8192 байт можно считать подходящим.
Если происходит ошибка, код ошибки возвращается через указатель h_errnop, а не через глобальную переменную h_errno.
11.20. Устаревшие функции поиска адресов IPv6ПРИМЕЧАНИЕ
К сожалению, проблема повторного вхождения гораздо серьезнее, чем может показаться. Во-первых, не существует стандарта относительно повторного вхождения и функций gethostbyname и gethostbyaddr. POSIX утверждает, что эти две функции не обязаны быть безопасными в многопоточной среде.
Во-вторых, не существует стандарта для функций _r. В этом разделе (в качестве примера) мы привели две функции _r, предоставляемые Solaris 2.x. В Linux присутствуют аналогичные функции, возвращающие hostent в качестве аргумента типа значение-результат. В Digital Unix и HP-UX имеются версии этих функций с другими аргументами. Первые два аргумента функции gethostbyname_r такие же, как и в версии Solaris, но оставшиеся три аргумента версии Solaris объединены в новую структуру hostent_data (которая должна быть размещена в памяти вызывающим процессом), а указатель на эту структуру – это третий и последний аргумент. Обычные функции gethostbyname и gethostbyaddr в Digital Unix 4.0 и в HP-UX 10.30 допускают повторное вхождение при использовании собственных данных потоков (см. раздел 23.5). Интересный рассказ о разработке функций _r Solaris 2.x содержится в [70].
Наконец, хотя версия функции gethostbyname, допускающая повторное вхождение, может обеспечить безопасность, когда ее одновременно вызывают несколько различных потоков, это ничего не говорит нам о возможности повторного вхождения для лежащих в ее основе функций распознавателя.
В процессе разработки IPv6 интерфейс поиска адресов IPv6 много раз претерпевал серьезные изменения. В какой-то момент интерфейс был сочтен усложненным и недостаточно гибким, так что от него полностью отказались в RFC 2553 [38]. Документ RFC 2553 предлагал собственные функции, которые в RFC 3493 [36] были попросту заменены getaddrinfoи getnameinfo. В этом разделе мы вкратце рассмотрим старые интерфейсы на тот случай, если вам придется переписывать программы, использующие их.
Поскольку функция gethostbynameне имеет аргумента для указания нужного семейства адресов (подобного hints.ai_familyдля getaddrinfo), в первом варианте API использовалась константа RES_USE_INET6, которая должна была добавляться к флагам распознавателя посредством внутреннего интерфейса. Этот API был недостаточно переносимым, поскольку системам, использовавшим альтернативные внутренние интерфейсы распознавателя, приходилось имитировать интерфейс BIND.
Включение RES_USE_INET6приводило к тому, что функция gethostbynameначинала поиск с записей AAAA, а записи А возвращались только в случае отсутствия первых. Поскольку в структуре hostentесть только одно поле длины адреса, функция gethostbynameмогла возвращать адреса только одного типа (либо IPv6, либо IPv4).
Кроме того, включение RES_USE_INET6приводило к тому, что функция gethostbyname2начинала возвращать адреса IPv4 в преобразованном к IPv6 виде.
Функция gethostbyname2имеет добавочный аргумент, позволяющий задать семейство адресов.
#include
struct hostent *gethostbyname2(const char * hostname, int family);
Возвращает: непустой указатель в случае успешного выполнения, в случае ошибки возвращает NULL и задает значение переменной h_errno
Возвращаемое значение то же, что и у функции gethostbyname– указатель на структуру hostent, и сама эта структура устроена так же. Логика функции зависит от аргумента familyи параметра распознавателя RES_USE_INET6(который мы упомянули в конце предыдущего раздела).
Документ RFC 2553 [38] запретил использование RES_USE_INET6и gethostbyname2из-за глобальности флага RES_USE_INET6и желания предоставить больше возможностей по управлению возвращаемыми сведениями. Для решения перечисленных проблем была предложена функция getipnodebyname.
#include
#include
struct hostent *getipnodebyname(const char * name, int af,
int flags, int * error_num);
Возвращает: ненулевой указатель в случае успешного завершения, нулевой в случае ошибки
Функция возвращает указатель на ту же структуру hostent, которая использовалась gethostbyname. Аргументы afи flagsнепосредственно соответствуют полям hints.ai_familyи hints.ai_flags. Для обеспечения безопасности в многопоточной среде возвращаемое значение выделяется динамически, поэтому его приходится освобождать вызовом freehostent.
#include
void freehostent(struct hostent * ptr);
Функции getipnodebynameи getipnodebyaddrбыли отменены в RFC 3493 [36], а вместо них было предложено использовать getaddrinfoи getnameinfo.
В этой главе мы сфокусировали внимание на именах узлов, IP-адресах, именах и номерах портов служб. Если же обобщить полученную информацию, мы увидим, что существует четыре типа данных (имеющих отношение к сетям), которые могут понадобиться приложению: узлы, сети, протоколы и службы. В большинстве случаев происходит поиск данных, относящихся к узлам (функции gethostbynameи gethostbyaddr), реже – к службам (функции getservbynameи getservbyaddr) и еще реже – к сетям и протоколам.
Все четыре типа данных могут храниться в файле, и для каждого из четырех типов определены три функции:
1. Функция get XXXent, читающая следующую запись в файле, при необходимости открывающая файл.
2. Функция set XXXent, которая открывает файл (если он еще не открыт) и переходит к началу файла.
3. Функция end XXXent, закрывающая файл.
Для каждого из четырех типов данных определяется его собственная структура (соответственно, структуры hostent, netent, protoentи servent), что требует включения заголовка .
В дополнение к трем функциям get, setи end, которые допускают последовательную обработку файла, для каждого из четырех типов данных предоставляются функции ключевого поиска, или поиска по ключу( keyed lookup). Эти функции последовательно проходят файл (вызывая функцию get XXXentдля чтения каждой строки файла), но вместо того чтобы возвращать каждую строку вызывающему процессу, эти функции ищут элемент, совпадающий с аргументом. Имена функций поиска по ключу имеют вид get XXXby YYY. Например, две функции ключевого поиска для информации об узле – это функции gethostbyname(ищет элемент, совпадающий с именем узла) и gethostbyaddr(ищет элемент, совпадающий с IP-адресом). Таблица 11.5 обобщает эту информацию.
Таблица 11.5. Четыре типа данных, относящихся к сетям
| Узлы | /etc/hosts | Hostent | gethostbyaddr, gethostbyname |
| Сети | /etc/networks | Netent | getnetbyaddr, getnetbyname |
| Протоколы | /etc/protocols | Protoent | getprotobyname, getprotobynumber |
| Службы | /etc/services | Servent | getservbyname, getservbyport |
Как это применяется, если используется DNS? Прежде всего, с помощью DNS возможен доступ только к информации об узле и о сети. Информация о протоколе и службах всегда считывается из соответствующего файла. Ранее в этой главе мы отмечали (см. подраздел «Альтернативы DNS»), что в разных реализациях отличаются способы, с помощью которых администратор определяет, что именно использовать для получения информации об узле и сети – DNS или файл.
Далее, если DNS используется для получения информации об узле и о сети, имеют смысл только функции поиска по ключу. Используя, например, функцию gethostent, не стоит надеяться, что она выполнит последовательный перебор всех записей DNS! Если вызывается функция gethostent, она считывает только информацию об узлах и не использует DNS.
ПРИМЕЧАНИЕ
Хотя информацию о сети можно сделать доступной с помощью DNS, очень немногие пользуются этим. На с. 347-348 [1] рассказывается об этой возможности. Однако обычно администраторы создают и обслуживают файл /etc/networks, используемый вместо DNS. Программа netstat с параметром -i использует этот файл, если он есть, и выводит имя каждой сети. Однако бесклассовая адресация (см. раздел А.4) делает эти функции бесполезными, а поскольку они не поддерживают IPv6, новые приложения не должны использовать их.



























