|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
ЧАСТЬ 5УДАЛЕННЫЙ ВЫЗОВ ПРОЦЕДУР ГЛАВА 15Двери 15.1. ВведениеПоговорим о схеме клиент-сервер и вызове процедур. Существуют три различных типа вызова процедур, показанные на рис. 15.1. Рис. 15.1. Три типа вызова процедур 1. Локальный вызов процедуры (local procedure call) знаком нам по обычному программированию на языке С. Вызываемая и вызывающая процедуры (функции) при этом относятся к одному и тому же процессу. При этом обычно выполняется некая команда процессора, передающая управление новой процедуре, а вызвавшая процедура сохраняет значение регистров процессора и выделяет место в стеке Под свои локальные переменные. 2. Удаленный вызов процедуры (remote procedure call — RPC) происходит в ситуации, когда вызвавшая и вызываемая процедуры относятся к разным процессам. В такой ситуации мы обычно называем вызвавшую процедуру клиентом, а вызванную — сервером. Во втором сценарии на рис. 15.1 клиент и сервер выполняются на одном и том же узле. Это типичный частный случай третьего сценария, и это именно то, что осуществляется с помощью дверей (doors). Итак, двери дают возможность вызывать процедуру (функцию) другого процесса на том же узле. Один из процессов (сервер) делает процедуру, находящуюся внутри него, доступной для вызова другим процессам (клиентам), создавая для этой процедуры дверь. Мы можем считать двери специальным типом IPC, поскольку при этом между процессами (клиентом и сервером) передается информация в форме аргументов функции и возвращаемых значений. 3. RPC в общем случае дает возможность клиенту на одном узле вызвать процедуру сервера на другом узле, если эти два узла связаны каким-либо образом по сети (третий сценарий на рис. 15.1). Такой вид взаимодействия будет описан в главе 16.
Локальные вызовы процедур являются синхронными (synchronous): вызывающий процесс не получает управление до тех пор, пока не происходит возврат из вызванной процедуры. Потоки могут восприниматься как средство асинхронного вызова процедур: функция (указанная в третьем аргументе pthread_create) выполняется одновременно с вызвавшим процессом. Вызвавший процесс может ожидать завершения вызванного процесса с помощью функции pthread_join. Удаленный вызов процедур может быть как синхронным, так и асинхронным, но мы увидим, что вызовы через двери являются синхронными. Внутри процесса двери идентифицируются дескрипторами. Извне двери могут идентифицироваться именами в файловой системе. Сервер создает дверь вызовом door_create; аргументом этой функции является указатель на процедуру, которая будет связана с данной дверью, а возвращаемое значение является дескриптором двери. Затем сервер связывает полное имя файла с дескриптором двери с помощью функции fattach. Клиент открывает дверь вызовом open, при этом аргументом функции является полное имя файла, которое сервер связал с дверью, а возвращаемым значением — дескриптор, который будет использоваться клиентом для доступа к двери. Затем клиент может вызывать процедуру с помощью door_call. Естественно, программа, являющаяся сервером для некоторой двери, может являться клиентом для другой. Мы уже сказали, что вызовы через двери являются синхронными: когда клиент вызывает door_call, возврата из этой функции не происходит до тех пор, пока процедура на сервере не завершит работу (возможно, с ошибкой). Реализация дверей в Solaris связана с потоками. Каждый раз, когда клиент вызывает процедуру сервера, для обработки этого вызова создается новый поток в процессе-сервере. Работа с потоками обычно осуществляется автоматически функциями библиотеки дверей, при этом потоки создаются по мере необходимости, но мы увидим, что сервер может и сам управлять этими потоками, если это требуется. Это также означает, что одна и та же процедура может выполняться сервером для нескольких клиентов, причем для каждого из них будет создан отдельный поток. Такой сервер является параллельным. Поскольку одновременно могут выполняться несколько экземпляров процедуры сервера (каждая из которых представляет собой отдельный поток), содержимое этих процедур должно соответствовать определенным требованиям, которые обычно предъявляются к многопоточным программам. При удаленном вызове процедуры и данные, и дескрипторы могут быть переданы от клиента к серверу. Обратно также могут быть переданы данные и дескрипторы. Передача дескрипторов вообще является неотъемлемым свойством дверей. Более того, поскольку двери идентифицируются дескрипторами, это позволяет процессу передать дверь другому процессу. Более подробно о передаче дескрипторов будет говориться в разделе 15.8. ПримерНачнем описание интерфейса дверей с простого примера: клиент передает серверу длинное целое, а сервер возвращает клиенту квадрат этого значения тоже как длинное целое. В листинге 15.1[1] приведен текст программы-клиента (в этом примере мы опускаем множество деталей, большую часть которых мы обсудим далее в тексте главы). Листинг 15.1 .Клиент передает серверу длинное целое для возведения его в квадрат//doors/client1.c 1 #include "unpipc.h" 2 int 3 main(int argc, char **argv) 4 { 5 int fd; 6 long ival, oval; 7 door_arg_t arg; 8 if (argc != 3) 9 err_quit("usage: client1 <server-pathname> <integer-value>"); 10 fd = Open(argv[1], O_RDWR); /* открываем дверь */ 11 /* задаем аргументы и указатель на результат */ 12 ival = atol(argv[2]); 13 arg.data_ptr = (char *) &ival; /* аргументы */ 14 arg.data_size = sizeof(long); /* размер аргументов */ 15 arg.desc_ptr = NULL; 16 arg.desc_num = 0; 17 arg.rbuf = (char *) &oval; /* результат */ 18 arg.rsize = sizeof(long); /* размер результата */ 19 /* вызываем процедуру на сервере и выводим результат */ 20 Door_call(fd, &arg); 21 printf("result: %ld\n", oval); 22 exit(0); 23 }Открываем дверь 8-10 Дверь задается полным именем, передаваемым в качестве аргумента командной строки. Она открывается вызовом open. Возвращаемый дескриптор называется дескриптором двери, но часто его самого и называют дверью. Подготовка аргументов и указателя на результат11-18 Структура arg содержит указатели на аргументы и результат. Поле data_ptr указывает на первый байт аргументов, a data_size содержит количество байтов в аргументах. Два поля desc_ptr и desc_num предназначены для передачи дескрипторов, о чем мы будем подробно говорить в разделе 15.8. rbuf указывает на первый байт буфера результата, a rsize задает его размер. Вызов процедуры на сервере и вывод результата19-21 Мы вызываем процедуру на сервере с помощью door_call; аргументами этого вызова являются дескриптор двери и указатель на структуру аргументов. После возвращения из этого вызова программа печатает получившийся результат. Программа-сервер приведена в листинге 15.2. Она состоит из процедуры сервера с именем servproc и функции main. Листинг 15.2. Сервер, возводящий длинное целое в квадрат//doors/server1.c 1 #include "unpipc.h" 2 void 3 servproc(void *cookie, char *dataptr, size_t datasize, 4 door_desc_t *descptr, size_t ndesc) 5 { 6 long arg, result; 7 arg = *((long *) dataptr); 8 result = arg * arg; 9 Door_return((char *) &result, sizeof(result), NULL, 0); 10 } 11 int 12 main(int argc, char **argv) 13 { 14 int fd; 15 if (argc != 2) 16 err_quit("usage: server1 <server-pathname>"); 17 /* создание двери и связывание ее с файлом */ 18 fd = Door_create(servproc, NULL, 0); 19 unlink(argv[1]); 20 Close(Open(argv[1], O_CREAT | O_RDWR, FILE_MODE)); 21 Fattach(fd, argv[1]); 22 /* функция servproc() обрабатывает все запросы клиентов */ 23 for (;;) 24 pause(); 25 }Процедура сервера 2-10 Процедура сервера вызывается с пятью аргументами, но мы используем только один из них — dataptr. Он указывает на первый байт аргумента. Аргумент, представляющий собой длинное целое, передается через этот указатель и возводится в квадрат. Управление передается клиенту вместе с результатом вызовом door_return. Первый аргумент указывает на результат, второй задает его размер, а оставшиеся предназначены для возврата дескрипторов. Создание дескриптора двери и связывание с ним файла17-21 Дескриптор двери создается вызовом door_create. Первый аргумент является указателем на функцию, соответствующую этой двери (servproc). После получения этого дескриптора его нужно связать с некоторым именем в файловой системе, поскольку оно будет использоваться клиентом для подключения к этой двери. Делается это путем создания обычного файла в файловой системе (сначала мы вызываем unlink, на тот случай, если такой файл уже существует, причём возможная ошибка игнорируется) и вызова fattach — функции SVR4, связывающей дескриптор с полным именем файла. Главный поток сервера ничего не делает22-24 Главный поток сервера блокируется при вызове pause. Вся функциональность обеспечивается функцией servproc, которая будет запускаться как отдельный поток каждый раз при получении запроса клиента. Запустим сервер в отдельном окне: solaris % server1 /tmp/server1 После этого запустим пpoгрaммy-клиeнт в другом окне, указав в качестве аргумента то же полное имя, которое было указано при вызове сервера: solaris % client1 /tmp/server19 result: 81 solaris % ls -l /tmp/server1 Drw-r-r– 1 rstevens other1 0 Apr 9 10:09 /tmp/server1 Мы получили ожидаемый результат. Вызвав ls, мы видим, что эта пpoгрaммa выводит букву D в начале строки, соответствующей файлу, указывая, что этот файл является дверью. На рис. 15.2 приведена диaгрaммa работы данного примера. Функция door_call вызывает процедуру на сервере, которая затем вызывает door_return для возврата. На рис. 15.3 приведена диaгрaммa, показывающая, что в действительности происходит при вызове процедуры в другом процессе на том же узле. Рис. 15.2. Внешний вид вызова процедуры в другом процессе На рис. 15.3 выполняются следующие действия: 0. Запускается сервер, вызывает door_create, чтобы создать дескриптор для функции servproc, затем связывает этот дескриптор с именем файла в файловой системе. 1. Запускается клиент и вызывает door_call. Это функция в библиотеке дверей. 2. Библиотечная функция door_call делает системный вызов. При этом указывается процедура, которая должна быть выполнена, а управление передается функции из библиотеки дверей процесса-сервера. 3. Вызывается процедура сервера (servproc в данном примере). 4. Процедура сервера делает все необходимое для обработки запроса клиента и вызывает door_return по завершении работы. 5. Библиотечная функция door_return осуществляет системный вызов, передавая управление ядру. 6. В этом вызове указывается процесс-клиент, которому и передается управление. Рис. 15.3. Что в действительности происходит при вызове процедуры в другом процессе Последующие разделы этой главы описывают интерфейс дверей (doors API) более подробно, с множеством примеров. В приложении А мы убедимся, что двери представляют собой наиболее быструю форму IPC (при измерении времени ожидания). 15.2. Функция door_callФункция door_call вызывается клиентом для обращения к процедуре сервера, выполняемой в адресном пространстве процесса-сервера: #include <door.h> int door_call(int fd, door_arg_t *argp); /* Возвращает 0 в случае успешного завершения. –1 – в случае ошибки */ Дескриптор fd обычно возвращается функцией open (см. листинг 15.1). Полное имя файла, открываемого клиентом, однозначно идентифицирует процедуру сервера, которая вызывается door_call при передаче дескриптора. Второй аргумент — argp — указывает на структуру, описывающую аргументы и приемный буфер для возвращаемых значений: typedef struct door_arg { char *data_ptr; /* при вызове указывает на аргументы, при возврате – на результаты */ size_t data_size; /* при вызове определяет общий размер аргументов в байтах, при возврате – общий размер возвращаемых данных в байтах */ door_desc_t *desc_ptr; /* при вызове указывает на аргументы-дескрипторы, при возврате указывает на возвращаемые дескрипторы */ size_t desc_num; /* при вызове задает количество аргументов-дескрипторов, при возврате задает количество возвращаемых дескрипторов */ char *rbuf; /* указатель на буфер результатов */ size_t rsize; /* размер буфера результатов */ } door_arg_t; При возврате из удаленного вызова эта структура описывает возвращаемые значения. Все поля структуры могут быть изменены при возврате, мы подробно рассмотрим это ниже.
Аргументы и результаты могут быть двух типов: данные и дескрипторы. ■ Аргументы-данные представляют собой последовательность данных длиной data_size байт. На эту последовательность должен указывать data_ptr. Клиент и сервер должны заранее знать формат этих данных (и аргументов, и результатов). Нет способа указать серверу тип аргументов. В пpoгрaммax листингов 15.1 и 15.2 клиент и сервер были написаны таким образом, что они оба знали, что аргумент представлял собой одно длинное целое и возвращаемый результат также был одним длинным целым. Для скрытия внутреннего устройства передаваемых данных их можно объединить в структуру, что упростит работу тому, кто будет читать код несколько лет спустя. Итак, все аргументы можно заключить в одну структуру, результаты — в другую и обе их определить в одном заголовочном файле, используемом клиентом и сервером. Пример будет приведен в листингах 15.8 и 15.9. Если аргументов-данных нет, указатель data_ptr должен быть нулевым и размер данных data_size должен иметь значение 0.
■ Аргументы-дескрипторы хранятся в массиве структур door_desc_t, каждая из которых содержит один передаваемый от клиента серверу дескриптор. Количество структур типа door_desc_t задается аргументом desc_num. (Мы описываем эту структуру и смысл «передачи дескриптора» в разделе 15.8.) Если аргументов-дескрипторов нет, следует передать нулевой указатель desc_ptr и присвоить полю desc_num значение 0. ■ При возврате из функции data_ptr указывает на результаты-данные, a data_size задает размер возвращаемых данных. Если никакие данные не возвращаются, data_size будет иметь значение 0, а значение указателя data_ptr следует игнорировать. ■ Функция может возвращать и дескрипторы, при этом desc_ptr указывает на массив структур типа door_desc_t, каждая из которых содержит один передаваемый сервером клиенту дескриптор. Количество возвращаемых структур типа door_desc_t хранится в поле desc_num. Если дескрипторы не возвращаются, значение desc_num будет равно 0, а указатель desc_ptr следует игнорировать. Можно спокойно использовать один и тот же буфер для передаваемых аргументов и возвращаемых результатов. При вызове door_call и data_ptr, и desc_ptr могут указывать на буфер, указанный аргументом rbuf. Перед вызовом door_call клиент устанавливает указатель rbuf на буфер для результатов, a rsize делает равным размеру буфера. После возвращения из функции и data_ptr, и desc_ptr будут указывать на этот буфер. Если он слишком мал для хранения результатов, возвращаемых сервером, библиотека дверей автоматически выделит новый буфер в адресном пространстве клиента с помощью mmap (раздел 12.2) и обновит значения rbuf и rsize соответствующим образом. Поля data_ptr и desc_ptr будут указывать на новый буфер. Клиент отвечает за то, чтобы обнаружить изменение этих указателей и впоследствии освободить занимаемую память вызовом munmap с аргументами rbuf и rsize. Пример будет приведен в листинге 15.4. 15.3. Функция door_createПроцесс-сервер определяет некоторую функцию как процедуру сервера вызовом door_create: #include <door.h> typedef void Door_server_proc(void *cookie, char *dataptr, size_t datasize, door_desc_t *descptr, size_t ndesc); int door_create(Door_server_proc *proc, void *cookie, u_int attr); /* Возвращает неотрицательный дескриптор в случае успешного завершения, –1 –в случае ошибки */ Здесь мы добавили наше собственное определение типа, что упрощает прототип функции. Это определение утверждает, что процедуры сервера (например, servproc в листинге 15.2) вызываются с пятью аргументами и ничего не возвращают. Когда сервер вызывает door_create, первый аргумент (proc) указывает адрес процедуры сервера, которая будет вызываться через дескриптор двери, возвращаемый этим вызовом. При вызове процедуры сервера ее аргумент cookie содержит значение, передаваемое в качестве второго аргумента door_create. Это дает серверу возможность передавать процедуре какой-либо указатель каждый раз, когда эта процедура вызывается клиентом. Следующих четыре аргумента процедуры сервера — dataptr, datasize, descptr и ndesc — описывают аргументы-данные и аргументы-дескрипторы клиента. Они соответствуют первым четырем полям структуры door_arg_t, описанной в предыдущем разделе. Последний аргумент door_create(attr) описывает специальные атрибуты процедуры сервера и может быть равен либо 0, либо логической сумме двух констант: ■ DOOR_PRIVATE — библиотека дверей автоматически создает новые потоки в процессе-сервере при поступлении запросов от клиентов. По умолчанию эти потоки помещаются в пул потоков и могут использоваться для обслуживания запросов клиентов по всем дверям данного процесса. Указание атрибута DOOR_PRIVATE говорит библиотеке, что для данной двери следует создать собственный пул потоков, отдельный от пула потоков процесса. ■ DOOR_UNREF — когда количество дескрипторов, открытых для данной двери, изменяется с двух до одного, процедура сервера вызывается со вторым аргументом, имеющим значение DOOR_UNREF_DATA. При этом аргумент descptr представляет собой нулевой указатель, а аргументы datasize и ndesc равны нулю. Мы приведем пример использования этого атрибута в листинге 15.13. Возвращаемое сервером значение имеет тип void, поскольку процедура сервера никогда не завершает работу вызовом return. Вместо этого процедура должна вызывать door_return (функция описана в следующем разделе). В листинге 15.2 мы видели, что после получения дескриптора двери вызовом door_create сервер должен вызвать fattach для связывания этого дескриптора с некоторым файлом. Клиент затем может открыть этот файл для получения дескриптора двери, который впоследствии может быть использован при вызове door_call.
Для дескрипторов дверей, создаваемых door_create, устанавливается бит FD_CLOEXEC. Это означает, что дескриптор закрывается при вызове процессом функций типа exec. Что касается вызова fork, несмотря на то что открытые родительским процессом дескрипторы используются дочерним процессом совместно с ним, только родительский процесс будет принимать вызовы от клиентов. Дочерним процессам вызовы не передаются, хотя дескриптор, возвращаемый door_create, и будет в них открыт.
15.4. Функция door_returnПосле завершения работы процедуры сервера возврат из нее осуществляется вызовом door_return. Это приводит к возврату из door_call соответствующего клиента. #include <door.h> int door_return(char *dataptr, size_t datasize, door_desc_t *descptr, size_t ndesc); /* Ничего не возвращает вызвавшему процессу в случае успешного завершения. –1 – в случае ошибки */ Возвращаемые данные задаются аргументами dataptr и datasize, а возвращаемые дескрипторы — descptr и ndesc. 15.5. Функция door_credИнтерфейс дверей предусматривает полезную возможность получения информации о клиенте при каждом вызове. Это осуществляется функцией door_cred: #include <door.h> int door_cred(door_cred_t *cred); /* Возвращает 0 в случае успешного завершения, –1 – в случае ошибки */ Структура, на которую указывает аргумент cred, имеет тип door_cred_t, определяемый как typedef struct door_cred { uid_t dc_euid; /* действующий идентификатор пользователя клиента */ gid_t dc_egid; /* действующий идентификатор группы клиента */ uid_t dc_ruid; /* реальный идентификатор пользователя клиента */ gid_t dc_rgid; /* реальный идентификатор группы клиента */ pid_t dc_pid; /* идентификатор процесса клиента */ } door_cred_t; В эту структуру помещается информация о клиенте при возвращении из вызова door_cred. В разделе 4.4 [21] подробно рассказывается о различиях между действующими и реальными идентификаторами пользователя и группы, а пример использования этой функции приведен в листинге. 15.5. Обратите внимание, что эта функция не принимает никаких дескрипторов. Она возвращает информацию о клиенте, осуществившем конкретный вызов через дверь, и поэтому должна вызываться из процедуры сервера или другой функции, вызываемой из процедуры сервера. 15.6. Функция door_infoТолько что описанная функция door_cred предоставляет серверу информацию о клиенте. Клиент же может получить информацию о сервере, вызвав doo_info: #include <door.h> int door_info(int fd, door_info_t *info); /* Возвращает 0 в случае успешного завершения, –1 – в случае ошибки */ Дескриптор fd указывает на открытую дверь. Структура типа door_info_t, на которую указывает info, после возвращения из функции содержит информацию о сервере: typedef struct doo_info { pid_t di_target; /* идентификатор процесса сервера */ door_ptr_t di_proc; /* процедура сервера */ door_ptr_t di_data; /* принимаемые процедурой сервера данные */ door_attr_t di_attributes; /* атрибуты, связанные с данной дверью */ door_id_t di_uniquifier; /* уникальный номер двери */ } door info t; Поле di_target содержит идентификатор процесса сервера, a di_proc — адрес процедуры сервера в процессе (от которого клиенту, вообще говоря, пользы мало). Указатель, передаваемый процедуре сервера в качестве первого аргумента (cookie), возвращается клиенту в поле di_data. Текущие атрибуты двери помещаются в поле di_attributes, и два из них уже были описаны в разделе 15.3. Это атрибуты DOOR_PRIVATE и DOOR_UNREF. Два других атрибута называются DOOR_LOCAL (процедура является локальной для данного процесса) и DOOR_REVOKE (сервер аннулировал процедуру, связанную с этой дверью, вызвав door_revoke). Каждой двери при создании сопоставляется уникальный в пределах системы номер, который возвращается в поле di_uniquifier. Эта функция обычно вызывается клиентом для получения информации о сервере. Однако она может быть вызвана и из процедуры сервера, причем первым аргументом в этом случае должна быть константа DOOR_QUERY. Тогда функция возвратит информацию о вызвавшем потоке, то есть о данном экземпляре процедуры сервера. В этом случае адреса процедуры сервера и принимаемых аргументов (di_proc и di_data) могут представлять интерес. 15.7. ПримерыВ этом разделе мы приведем примеры использования пяти только что описанных функций. Функция door_infoВ листинге 15.3 приведен текст программы, открывающей дверь и вызывающей door_infо для получения информации об этой двери, которая затем выводится на экран. Листинг 15.3. Вывод информации о двери//doors/doorinfo.c 1 #include "unpipc.h" 2 int 3 main(int argc, char **argv) 4 { 5 int fd; 6 struct stat stat; 7 struct door_info info; 8 if (argc != 2) 9 err_quit("usage; doorinfo <pathname>"); 10 fd = Open(argv[1], O_RDONLY); 11 Fstat(fd, &stat); 12 if (S_ISDOOR(stat.st_mode) == 0) 13 err_quit("pathname is not a door"); 14 Door_info(fd, &info); 15 printf("server PID = %ld, uniquifier = %ld", 16 (long)info.di_target, (long)info.di_uniquifier); 17 if (info.di_attributes & DOOR_LOCAL) 18 printf(", DOOR_LOCAL"); 19 if (info.di_attributes & DOOR_PRIVATE) 20 printf(", DOOR_PRIVATE"); 21 if (info.di_attributes & DOOR_REVOKED) 22 printf(", DOOR_REVOKED"); 23 if (info.di_attributes & DOOR_UNREF) 24 printf(", DOOR_UNREF"); 25 printf("\n"); 26 exit(0); 27 } Сначала программа открывает файл с указанным полным именем и проверяет, что это действительно дверь. Поле st_mode структуры stat в этом случае должно содержать такое значение, что макрос S_ISDOOR будет возвращать значение «истина». Затем вызывается функция door_info. Сначала мы укажем этой программе полное имя файла, не являющегося дверью, а затем попробуем получить информацию о двух встроенных дверях Solaris 2.6: solaris % doorinfo/etc/passwd pathname is not a door solaris % doorinfo /etc/.name_service_door server PID = 308, uniquifier = 18, DOOR_UNREF solaris % doorinfo /etc/.syslog_door server PID = 282, uniquifier = 1635 solaris % ps –f -p 308 root 308 1 0 Apr 01 ? 0:34 /usr/sbin/nscd solaris % ps –f -p 282 root 282 1 0 Apr 01 ? 0:10 /usr/sbin/syslogd –n –z 14 Команду ps мы используем для того, чтобы узнать, какая программа выполняется с идентификатором, возвращаемым door_info. Буфер результатов слишком малКогда мы рассказывали о функции door_call, мы отметили, что если буфер результатов оказывается слишком мал, библиотека дверей осуществляет автоматическое выделение нового буфера. Сейчас мы покажем это на примере. В листинге 15.4 приведен текст новой программы-клиента, которая представляет собой измененную версию листинга 15.2. Листинг 15.4. Вывод адреса полученного результата//doors/client2.c 1 #include "unpipc.h" 2 int 3 main(int argc, char **argv) 4 { 5 int fd; 6 long ival, oval; 7 door_arg_t arg; 8 if (argc != 3) 9 err_quit("usage: client2 <server-pathname> <integer-value>"); 10 fd = Open(argv[1], O_RDWR); /* открываем дверь */ 11 /* подготовка аргументов и указателя на результат */ 12 ival = atol(argv[2]); 13 arg.data_ptr = (char *) &ival; /* аргументы-данные */ 14 arg.data_size = sizeof(long); /* объем данных */ 15 arg.desc_ptr = NULL; 16 arg.desc_num = 0; 17 arg.rbuf = (char *) &oval; /* возвращаемые данные */ 18 arg.rsize = sizeof(long); /* объем возвращаемых данных */ 19 /* вызов процедуры сервера и вывод результата */ 20 Door_call(fd, &arg); 21 printf("&oval = %p, data_ptr = %p, rbuf = %p, rsize = %d\n", 22 &oval, arg.data_ptr, arg.rbuf, arg.rsize); 23 printf("result: %ld\n", *((long *) arg.data_ptr)); 24 exit(0); 25 } 19-22 В этой версии программы на экран выводится адрес переменной oval, содержимое указателя data_ptr, который должен указывать на возвращаемые функцией door_call данные, и адрес и размер приемного буфера (rbuf и rsize). Запустим эту программу, не изменяя размер приемного буфера по сравнению с листингом 15.2. Мы ожидаем, что data_ptr и rbuf будут указывать на переменную oval и rsize будет иметь значение 4 (4 байта в буфере). И действительно, вот что мы видим: solaris % client2 /tmp/server2 22 &oval = effff740, data_ptr = effff740, rbuf = effff740, rsize = 4 result: 484 Изменим только одну строку в листинге 15.4, уменьшив размер буфера клиента до одного байта. Новый вариант строки 18 будет иметь вид: arg.rsize = sizeof(long) – 1; /* размер буфера данных */ Запустим новую программу и увидим, что библиотека автоматически выделила место под новый буфер результатов и data_ptr теперь указывает на новый буфер: solaris % client3 /tmp/server3 33 &oval = effff740, data_ptr = ef620000, rbuf = ef620000, rsize = 4096 result: 1089 Размер выделенного буфера равен 4096 байт, что совпадает с размером страницы в данной системе, который мы узнали в разделе 12.6. Этот пример показывает, что следует всегда обращаться к результатам через указатель data_ptr, а не через переменные, адреса которых были переданы в rbuf. В нашем примере к результату типа «длинное целое» следует обращаться как *(long*)arg.data_ptr, а не oval (что мы делали в листинге 15.2). Новый буфер выделяется вызовом mmap и может быть возвращен системе с помощью munmap. Клиент может повторно использовать этот буфер при новых вызовах door_call. Функция door_cred и информация о клиентеНа этот раз мы изменим нашу функцию servproc из листинга 15.3, добавив в нее вызов door_cred для получения информации о пользователе. В листинге 15.5 приведен текст новой процедуры сервера; функции main клиента и сервера не претерпевают изменений по сравнению с листингами 15.2 и 15.3. Листинг 15.5. Процедура сервера, получающая информацию о клиенте//doors/server4.c 1 #include "unpipc.h" 2 void 3 servproc(void *cookie, char *dataptr, size_t datasize, 4 door_desc_t *descptr, size_t ndesc) 5 { 6 long arg, result; 7 door_cred_t info; 8 /* получение и вывод информации о клиенте */ 9 Door_cred(&info); 10 printf("euid = %ld, ruid = %ld, pid = %ld\n", 11 (long) info.dc_euid, (long) info.dc_ruid, (long) info.dc_pid); 12 arg = *((long *) dataptr); 13 result = arg * arg; 14 Door_return((char *) &result, sizeof(result), NULL, 0); 15 } Сначала мы запустим программу-клиент и увидим, что действующий и реальный идентификаторы клиента совпадают, как мы и предполагали. Затем мы сменим владельца исполняемого файла на привилегированного пользователя, установим бит SUID и запустим программу снова: solaris % client4 /tmp/server4 77 первый запуск клиента result: 5929 solaris % su вход под именем привилегированного пользователя Password: Sun Microsystems Inc. Sun OS 5.6 Generic August 1997 solaris # cd каталог, в котором находится исполняемый файл solaris # ls –l client4 -rwxrwxr-x 1 rstevens other1 139328 Apr 13 06:02 client4 solaris # chown root client4 смена владельца на привилегированного пользователя solaris # chmod u+s client4 включение бита SUID solaris # ls -l client4 проверка разрешений и владельца файла -rwsrwxr-x 1 root other1 139328 Apr 13 06:02 client4 solaris # exit solaris % ls -l client4 -rwsrwxr-x 1 root other1 139328 Apr 13 06:02 client4 solaris % client4 /tmp/server477 и еще раз запускаем программу-клиент result: 5929 Если мы посмотрим, что в это время выводил сервер, то увидим следующую картину: solaris % server4 /tmp/server4 euid = 224, ruid = 224, pid = 3168 euid = 0, ruid = 224, pid = 3176 Действующий идентификатор пользователя при втором запуске изменился. Значение 0 означает привилегированного пользователя. Автоматическое управление потоками сервераЧтобы посмотреть, как осуществляется управление потоками сервера, добавим в процедуру сервера команду выдачи ее идентификатора потока. Добавим в нее также пятисекундную паузу, чтобы имитировать длительное выполнение. За это время мы сможем запустить несколько клиентов. В листинге 15.6 приведен текст новой процедуры сервера. Листинг 15.6. Процедура сервера, выводящая идентификатор потока//doors/server5.c 1 #include "unpipc.h" 2 void 3 servproc(void *cookie, char *dataptr, size_t datasize, 4 door_desc_t *descptr, size_t ndesc) 5 { 6 long arg, result; 7 arg = *((long *) dataptr); 8 printf("thread id %ld, arg = %ld\n", pr_thread_id(NULL), arg); 9 sleep(5); 10 result = arg * arg; 11 Door_return((char*)&result, sizeof(result), NULL, 0); 12 } Здесь используется новая функция из нашей библиотеки — pr_thread_id. Она принимает один аргумент (указатель на идентификатор потока или нулевой указатель вместо идентификатора вызвавшего потока) и возвращает идентификатор этого потока (обычно небольшое целое число, но всегда в формате длинного целого). Процессу всегда можно сопоставить целое число — его идентификатор. Хотя мы и не знаем, к какому типу принадлежит идентификатор процесса (int или long), мы просто преобразуем значение, возвращаемое getpid, к типу long и выводим значение (листинг 9.2). Однако идентификатор потока принадлежит к типу pthread_t, который не обязательно является одним из целых типов. И действительно, в Solaris 2.6 идентификаторами потоков являются короткие целые, тогда как в Digital Unix используются указатели. Однако часто возникает необходимость сопоставлять потокам небольшие целые числа для задач отладки (как в данном примере). Наша библиотечная функция, текст которой приведен в листинге 15.7, решает этот вопрос. Листинг 15.7. Функция pr_thread_id: возвращает небольшой целочисленный идентификатор потока//lib/wrappthread.c 245 long 246 pr_thread_id(pthread_t *ptr) 247 { 248 #if defined(sun) 249 return((ptr == NULL) ? pthread_self() : *ptr); /* Solaris */ 250 #elif defined(__osf__) && defined(__alpha) 251 pthread_t tid; 252 tid = (ptr == NULL) ? pthread_self() : *ptr; /* Digital Unix */ 253 return(pthread_getsequence_np(tid)); 254 #else 255 /* прочие системы */ 256 return((ptr == NULL) ? pthread_self() : *ptr); 257 #endif 258 } Если в данной реализации идентификатор потока не является небольшим целым числом, функция может быть сложнее. Она может осуществлять отображение значений типа pthread_t в целые числа и сохранять эти отображения для последующих вызовов в массиве или связном списке. Эта задача решена в функции thread_name в книге [13]. Вернемся к программе из листинга 15.6. Запустим ее три раза подряд. Поскольку нам приходится ждать возвращения подсказки интерпретатора, чтобы запустить клиент еще раз, мы можем быть уверены, что каждый раз выполняется пятисекундная пауза: solaris % client5 /tmp/server5 55 result: 3025 solaris % client5 /tmp/server5 66 result: 4356 solaris % client5 /tmp/server5 77 result: 5929 Взглянув на текст, выводимый сервером, мы увидим, что клиенты каждый раз обслуживались одним и тем же потоком сервера: solaris % server5 /tmp/server5 thread id 4, arg = 55 thread id 4, arg = 66 thread id 4, arg = 77 Теперь запустим три экземпляра программы-клиента одновременно: solaris % client5 /tmp/server5 11 & client5 /tmp/server5 22 & client5 /tmp/server5 33 & [2] 3812 [3] 3813 [4] 3814 solaris % result: 484 result: 121 result: 1089 Выводимый сервером текст показывает, что для обработки второго и третьего вызова процедуры сервера создаются новые потоки: thread id 4, arg = 22 thread id 5, arg = 11 thread id 6, arg = 33 Затем мы запустим еще два клиента одновременно (первые три уже завершили работу): solaris % client5 /tmp/server5 11 & client5 /tmp/server5 22 & [2] 3830 [3] 3831 solaris % result: 484 result: 121 При этом сервер использует созданные ранее потоки: thread id 6, arg = 22 thread id 5, arg = 11 Этот пример показывает, что серверный процесс (то есть библиотека дверей, подключенная к нему) автоматически создает потоки серверных процедур по мере необходимости. Если приложению требуется контроль над созданием потоков, оно может его осуществить с помощью функций, описанных в разделе 15.9. Мы также убедились, что сервер в этом случае является параллельным (concurrent): одновременно может выполняться несколько экземпляров процедуры сервера в виде отдельных потоков для обслуживания клиентов. Это следует также из того, что результат работы сервера выводится тремя экземплярами клиента одновременно пять секунд спустя после их одновременного запуска. Если бы сервер был последовательным, первый результат появился бы через 5 секунд после запуска, следующий — через 10, а последний — через 15. Автоматическое управление потоками сервера: несколько процедурВ предыдущем примере процесс-сервер содержал лишь одну процедуру сервера. Вопрос, которым мы займемся теперь, звучит так: могут ли несколько процедур одного процесса использовать один и тот же пул потоков сервера? Чтобы узнать ответ, добавим к нашему серверу еще одну процедуру, а заодно перепишем наши программы заново, чтобы продемонстрировать более приличный стиль передачи аргументов и результатов между процессами. Первый файл в этом примере называется squareproc.h. В нем определен один тип данных для входных аргументов функции, возводящей в квадрат, и еще один — для возвращаемых ею результатов. В этом заголовочном файле также определяется полное имя двери для данной процедуры. Его текст его приведен в листинге 15.8. Листинг 15.8. Заголовочный файл squareproc.h//doors/squareproc.h 1 #define PATH_SQUARE_DOOR "/tmp/squareproc_door" 2 typedef struct { /* аргументы squareproc() */ 3 long arg1; 4 } squareproc_in_t; 5 typedef struct { /* возврат squareproc() */ 6 long res1; 7 } squareproc_out_t; Наша новая процедура будет принимать длинное целое и возвращать квадратный корень из него (типа double). Мы определяем полное имя двери этой процедуры, структуры аргументов и результатов в заголовочном файле sqrtproc.h в листинге 15.9. Листинг 15.9. Заголовочный файл sqrtproc.h//doors/sqrtproc.h 1 #define PATH_SQRT_DOOR "/tmp/sqrtproc_door" 2 typedef struct { /* входные данные sqrtproc() */ 3 long arg1; 4 } sqrtproc_in_t; 5 typedef struct { /* возвращаемые sqrtproc() данные */ 6 double res1; 7 } sqrtproc_out_t; Программа-клиент приведена в листинге 15.10. Она последовательно вызывает две процедуры сервера и выводит возвращаемые ими результаты. Эта программа устроена аналогично другим клиентским программам, приведенным в этой главе. Листинг 15.10. Клиент, вызывающий две процедуры//doors/client7.c 1 #include "unpipc.h" 2 #include "squareproc.h" 3 #include "sqrtproc.h" 4 int 5 main(int argc, char **argv) 6 { 7 int fdsquare, fdsqrt; 8 door_arg_t arg; 9 squareproc_in_t square_in; 10 squareproc_out_t square_out; 11 sqrtproc_in_t sqrt_in; 12 sqrtproc_out_t sqrt_out; 13 if (argc != 2) 14 err_quit("usage: client7 <integer-value>"); 15 fdsquare = Open(PATH_SQUARE_DOOR, O_ROWR); 16 fdsqrt = Open(PATH_SQRT_DOOR, O_RDWR); 17 /* подготовка аргументов и вызов squareproc() */ 18 square_in.arg1 = atol(argv[1]); 19 arg.data_ptr = (char*)&square_in; 20 arg.data_size = sizeof(square_in); 21 arg.desc_ptr = NULL; 22 arg.desc_num = 0; 23 arg.rbuf = (char*)&square_out; 24 arg.rsize = sizeof(square_out); 25 Door_call(fdsquare, &arg); 26 /* подготовка аргументов и вызов sqrtproc() */ 27 sqrt_in.arg1 = atol(argv[1]); 28 arg.data_ptr = (char*)&sqrt_in; 29 arg.data_size = sizeof(sqrt_in); 30 arg.desc_ptr = NULL; 31 arg.desc_num = 0; 32 arg.rbuf = (char*)&sqrt_out; 33 arg.rsize = sizeof(sqrt_out); 34 Door_call(fdsqrt, &arg); 35 printf("result: %ld %g\n", square_out.res1, sqrt_out.res1); 36 exit(0); 37 } Текст двух серверных процедур приведен в листинге 15.11. Каждая из них выводит текущий идентификатор потока и значение аргумента, делает 5-секунд-ную паузу, вычисляет результат и завершает работу. Листинг 15.11. Две процедуры сервера//doors/server7.c 1 #include "unpipc.h" 2 #include <math.h> 3 #include "squareproc.h" 4 #include "sqrtproc.h" 5 void 6 squareproc(void *cookie, char *dataptr, size_t datasize, 7 door_desc_t *descptr, size_t ndesc) 8 { 9 squareproc_in_t in; 10 squareproc_out_t out; 11 memcpy(&in, dataptr, min(sizeof(in), datasize)); 12 printf("squareproc: thread id %ld, arg = %ld\n", 13 pr_thread_id(NULL), in.arg1); 14 sleep(5); 15 out.res1 = in.arg1 * in.arg1; 16 Door_return((char *) &out, sizeof(out), NULL, 0); 17 } 18 void 19 sqrtproc(void *cookie, char *dataptr, size_t datasize, 20 door_desc_t *descptr, size_t ndesc) 21 { 22 sqrtproc_in_t in; 23 sqrtproc_out_t out; 24 memcpy(&in, dataptr, min(sizeof(in), datasize)); 25 printf("sqrtproc: thread id %ld, arg = %ld\n", 26 pr_thread_id(NULL), in.arg1); 27 sleep(5); 28 out.res1 = sqrt((double)in.arg1); 29 Door_return((char *) &out, sizeof(out), NULL, 0); 30 } Функция main сервера, текст которой приведен в листинге 15.12, открывает дескрипторы дверей и связывает каждый из них с одной из процедур сервера. Листинг 15.12. Функция main сервера//doors/server7.c 31 int 32 main(int argc, char **argv) 33 { 34 int fd; 35 if (argc != 1) 36 err_quit("usage: server7"); 37 fd = Door_create(squareproc, NULL, 0); 38 unlink(PATH_SQUARE_DOOR); 39 Close(Open(PATH_SQUARE_DOOR, O_CREAT | O_RDWR, FILE_MODE)); 40 Fattach(fd, PATH_SQUARE_DOOR); 41 fd = Door_create(sqrtproc, NULL, 0); 42 unlink(PATH_SQRT_DOOR); 43 Close(Open(PATH_SQRT_DOOR, O_CREAT | O_RDWR, FILE_MODE)); 44 Fattach(fd, PATH_SQRT_DOOR); 45 for (;;) 46 pause(); 47 } Запустим программу-клиент и подождем 10 секунд до вывода результатов (как мы и ожидали): solaris % client7 77 result: 5929 8.77496 Посмотрев на выводимый сервером текст, мы увидим, что один и тот же поток этого процесса использовался для обработки обоих запросов клиента: solaris % server7 squareproc: thread id 4, arg = 77 sqrtproc: thread id 4, arg = 77 Это подтверждает наши предположения о том, что любой поток из пула сервера может использоваться при обработке запросов клиентов для любой процедуры. Атрибут DOOR_UNREF для серверовВ разделе 15.3 мы отметили, что при вызове door_create для создаваемой двери можно указать атрибут DOOR_UNREF. В документации говорится, что если количество дескрипторов, относящихся к этой двери, уменьшается с двух до одного, осуществляется специальный вызов процедуры сервера. Особенность вызова заключается в том, что второй аргумент процедуры сервера (указатель на данные) при этом является константой DOOR_UNREF_DATA. Мы продемонстрируем три способа обращения к двери. 1. Дескриптор, возвращаемый door_create, считается первой ссылкой на эту дверь. Вообще говоря, причина, по которой специальный вызов происходит при изменении количества дескрипторов с 2 на 1, а не с 1 на 0, заключается в том, что первый дескриптор обычно не закрывается сервером до завершения работы. 2. Полное имя, связанное с дверью в файловой системе, также считается ссылкой на дверь. Ее можно удалить вызовом функции fdetach, или запустив программу fdetach, или удалив полное имя из файловой системы (функцией unlink или командой rm). 3. Дескриптор, возвращаемый клиенту функцией open, считается открытой ссылкой до тех пор, пока не будет закрыт либо явным вызовом close, либо неявно, при завершении клиента. Во всех примерах этой главы дескриптор закрывается неявно. Первый пример показывает, что если сервер закрывает свой дескриптор после вызова fattach, немедленно происходит специальный вызов процедуры сервера. В листинге 15.13 приведен текст процедуры сервера и функции main. Листинг 15.13. Процедура сервера, обрабатывающая специальный вызов//doors/serverunref1.c 1 #include "unpipc.h" 2 void 3 servproc(void *cookie, char *dataptr, size_t datasize, 4 door_desc_t *descptr, size_t ndesc) 5 { 6 long arg, result; 7 if (dataptr == DOOR_UNREF_DATA) { 8 printf("door unreferenced\n"); 9 Door_return(NULL, 0, NULL, 0); 10 } 11 arg = *((long*)dataptr); 12 printf("thread id %ld, arg = %ld\n", pr_thread_id(NULL), arg); 13 sleep(6); 14 result = arg * arg; 15 Door_return((char *)&result, sizeof(result), NULL, 0); 16 } 17 int 18 main(int argc, char **argv) 19 { 20 int fd; 21 if (argc != 2) 22 err_quit("usage: server1 <server-pathname>"); 23 /* создание дескриптора и связывание с файлом */ 24 fd = Door_create(servproc, NULL, DOOR_UNREF); 25 unlink(argv[1]); 26 Close(Open(argv[1], O_CREAT | O_RDWR, FILE_MODE)); 27 Fattach(fd, argv[1]); 28 Close(fd); 29 /* процедура servproc() обрабатывает все запросы клиентов */ 30 for(;;) 31 pause(); 32 } 7-10 Процедура сервера распознает специальный вызов и выводит сообщение об этом. Возврат из специального вызова происходит путем вызова door_return с двумя нулевыми указателями и нулевыми значениями размеров. 28 Теперь мы закрываем дескриптор двери после выполнения fattach. Этот дескриптор может быть нужен серверу только для вызовов door_bind, doo_info и door_revoke. Запустив сервер, мы увидим, что немедленно произойдет специальный вызов: solaris % serverunref1 /tmp/door1 door unreferenced Если мы проследим за значением счетчика открытых дескрипторов, мы увидим, что он становится равен 1 после возврата из door_create и 2 после возврата из fattach. Вызов close уменьшает количество открытых дескрипторов с двух до одного, что приводит к специальному вызову процедуры. Единственная оставшаяся ссылка при этом представляет собой имя в файловой системе, а этого клиенту достаточно, чтобы обратиться к двери. Поэтому клиент продолжает работать правильно: solaris % clientunref1 /tmp/door1 11 result: 121 solaris % clientunref1 /tmp/door1 22 result: 484 Более того, дальнейших специальных вызовов серверной процедуры не происходит. Для каждой двери осуществляется только один специальный вызов. Теперь изменим нашу программу-сервер обратно, убрав вызов close для дескриптора двери. Процедура сервера и функция main приведены в листинге 15.14. Листинг 15.14. Сервер, не закрывающий дескриптор двери//doors/serverunref2.c 1 #include "unpipc.h" 2 void 3 servproc(void *cookie. char *dataptr, size_t datasize, 4 door_desc_t *descptr, size_t ndesc) 5 { 6 long arg, result; 7 if (dataptr == DOOR_UNREF_DATA) { 8 printf("door unreferenced\n"); 9 Door_return(NULL, 0, NULL, 0); 10 } 11 arg = *((long *)dataptr); 12 printf("thread id %ld, arg = %ld\n", pr_thread_id(NULL), arg); 13 sleep(6); 14 result = arg * arg; 15 printf("thread id %ld returning\n", pr_thread_id(NULL)); 16 Door_return((char *)&result, sizeof(result), NULL, 0); 17 } 18 int 19 main(int argc, char **argv) 20 { 21 int fd; 23 if (argc != 2) 24 err_quit("usage: server1 <server-pathname>"); 25 /* создание двери, дескриптора и подключение к файлу */ 26 fd = Door_create(servproc, NULL, DOOR_UNREF); 27 unlink(argv[1]); 28 Close(Open(argv[1], O_CREAT | O_RDWR, FILE_MODE)); 29 Fattach(fd, argv[1]); 30 /* servproc() обрабатывает все запросы клиентов */ 31 for(;;) 32 pause(); 33 } Мы оставляем 6-секундную паузу и выводим сообщение о возврате из процедуры сервера. Запустим сервер в одном окне, а из другого проверим существование имени файла двери в файловой системе и удалим его с помощью rm: solaris % ls –l /tmp/door2 Drw-r--r-- 1 rstevens other1 0 Apr 16 08:58 /tmp/door2 solaris % rm /tmp/door2 После удаления имени файла происходит специальный вызов процедуры сервера: solaris % serverunref2 /trap/door2 door unreferenced после удаления файла из файловой системы Если мы проследим за количеством ссылок на эту дверь, то увидим следующее: одна ссылка появляется после вызова door_create, вторая — после fattach. После удаления файла с помощью rm количество ссылок снова уменьшается до единицы, что приводит к специальному вызову процедуры. В последнем примере использования этого атрибута мы снова удалим имя из файловой системы. Но на этот раз мы сначала запустим три экземпляра программы-клиента. Специальный вызов процедуры произойдет только после завершения последнего клиента, потому что каждый экземпляр клиента увеличивает количество ссылок на дверь. Используем сервер из листинга 15.14 и клиент из листинга 15.2. solaris % clientunref2 /tmp/door2 44 & clientunref2 /tmp/door2 55 & clientunref2/tmp/door2 55 & [2] 13552 [3] 13553 [4] 13554 solaris % rm /tmp/door2 клиенты все еще выполняются solaris % result: 1936 result: 3025 result: 4356 Сервер при этом выведет вот что: solaris % serverunref2 /tmp/door2 thread id 4, arg = 44 thread id 5, arg = 55 thread id 6, arg = 66 thread id 4 returning thread id 5 returning thread id 6 returning door unreferenced Проследим за значением счетчика открытых ссылок для этой двери. Он становится равным 1 после вызова door_create, 2 после вызова fattach. Когда три клиента вызывают open, счетчик увеличивается с 2 до 5. После удаления имени файла счетчик уменьшается до 4. После завершения работы трех клиентов счетчик уменьшается с 4 до 1 (последовательно), и последнее его изменение с 2 до 1 приводит к специальному вызову процедуры. Этими примерами мы показали, что хотя описание атрибута DOOR_UNREF выглядит просто («специальный вызов происходит при изменении счетчика ссылок с 2 до 1»), мы должны понимать принципы работы этого счетчика, чтобы им пользоваться. 15.8. Передача дескрипторовКогда мы говорим о передаче открытого дескриптора от одного процесса другому, обычно подразумевается одно из двух: ■ наследование всех открытых дескрипторов родительского процесса дочерним после вызова fork; ■ сохранение открытых дескрипторов при вызове exec. В первом случае процесс открывает дескриптор, вызывает fork, а затем родительский процесс закрывает дескрипторы, предоставляя дочернему возможность работать с ними. При этом открытый дескриптор передается от родительского процесса дочернему. В современных версиях Unix возможности передачи дескрипторов существенно расширены, и теперь имеется возможность передавать открытый дескриптор от одного процесса другому вне зависимости от их родства. Двери являются одним из существующих интерфейсов для передачи дескрипторов от клиента серверу и от сервера клиенту.
Нужно правильно понимать, что именно подразумевается под передачей дескриптора. На рис. 4.7 сервер открывал файл и копировал его целиком через нижний (на рисунке) канал. Если размер файла 1 Мбайт, через канал будет передан 1 Мбайт данных. Но если сервер передает клиенту дескриптор вместо самого файла, то через канал передается только дескриптор (который содержит небольшое количество информации ядра). Клиент может использовать этот дескриптор для считывания содержимого файла. Чтение файла при этом осуществляется именно клиентом, а сервер осуществляет только открытие файла. Нужно понимать, что сервер не может просто записать в канал числовое значение дескриптора, как в следующем фрагменте кода: int fd; fd = Open(…); Write(pipefd, &fd, sizeof(int)); Этот подход не работает. Дескрипторы вычисляются для каждого процесса в отдельности. Предположим, что значение дескриптора файла на сервере равно 4. Даже если дескриптор с тем же значением и открыт клиентом, он почти наверняка относится к другому файлу. Единственная ситуация, в которой дескрипторы одного процесса имеют значение для другого процесса, возникает при вызове fork. Если первый свободный дескриптор сервера имеет значение 4, вызов open вернет именно это значение. Если сервер передает дескриптор 4 клиенту, а у клиента наименьшее свободное значение дескриптора равно 7, нужно, чтобы дескриптор 7 клиента был установлен в соответствие с тем же файлом, что и дескриптор 4 сервера. Рисунки 15.4 в [21] и 18.4 в [23] иллюстрируют, что должно произойти с точки зрения ядра: два дескриптора (4 у сервера и 7 у клиента) должны указывать на один и тот же файл из таблицы ядра. Интерфейсы типа дверей и доменных сокетов Unix скрывают внутренние детали реализации, предоставляя процессам возможность легко передавать дескрипторы друг другу. Дескрипторы передаются через дверь от клиента серверу путем присваивания полю desc_ptr структуры door_arg_t значения указателя на массив структур типа door_desc_t и помещения в поле desc_num количества этих структур. Дескрипторы передаются от сервера клиенту путем присваивания третьему аргументу door_return значения указателя на массив структур door_desc_t и помещения в четвертый аргумент количества передаваемых дескрипторов: Рис. 15.4. Сервер файлов, передающий клиенту дескриптор typedef struct door_desc { door_attr_t d_attributes; /* тег объединения */ union { struct { /* верна, если tag = DOOR_DESCRIPTOR */ int d_descriptor; /* номер дескриптора */ door_id_t d_id; /* уникальный идентификатор */ } d_desc; } d_data; } door_desc_t; Эта структура содержит объединение (union), и первое поле структуры является тегом, идентифицирующим содержимое этого объединения. В настоящий момент определено только одно поле объединения (структура d_desc, описывающая дескриптор), и тег (d_attributes) должен иметь значение DOOR_DESCRIPTOR. ПримерИзменим наш пример с сервером файлов таким образом, чтобы сервер открывал файл, передавал дескриптор клиенту, а клиент копировал содержимое файла в стандартный поток вывода. На рис. 15.4 приведена схема приложения. В листинге 15.15 приведен текст программы клиента. Листинг 15.15. Клиент для сервера, передающего дескриптор//doors/clientfd1.c 1 #include "unpipc.h" 2 int 3 main(int argc, char **argv) 4 { 5 int door, fd; 6 char argbuf[BUFFSIZE], resbuf[BUFFSIZE], buff[BUFFSIZE]; 7 size_t len, n; 8 door_arg_t arg; 9 if (argc != 2) 10 err_quit("usage: clientfd1 <server-pathname>"); 11 door = Open(argv[1], O_RDWR); /* открываем дверь */ 12 Fgets(argbuf, BUFFSIZE, stdin); /* считываем полное имя открываемого файла */ 13 len = strlen(argbuf); 14 if (argbuf[len-1] == '\n') 15 len--; 16 /* подготавливаем аргумент и указатель на результат */ 17 arg.data_ptr = argbuf; /* аргумент-данные */ 18 arg.data_size = len + 1; /* размер данных */ 19 arg.desc_ptr = NULL; 20 arg.desc_num = 0; 21 arg.rbuf = resbuf; /* результаты-данные */ 22 arg.rsize = BUFFSIZE; /* размер возвращаемых данных */ 23 Door_call(door, &arg); /* вызов процедуры сервера */ 24 if (arg.data_size != 0) 25 err_quit("%.*s", arg.data_size, arg.data_ptr); 26 else if (arg.desc_ptr == NULL) 27 err_quit("desc_ptr is NULL"); 28 else if (arg.desc_num != 1) 29 err_quit("desc_num = %d", arg.desc_num); 30 else if (arg.desc_ptr->d_attributes != DOOR_DESCRIPTOR) 31 err_quit("d_attributes = %d", arg.desc_ptr->d_attributes); 32 fd = arg.desc_ptr->d_data.d_desc.d_descriptor; 33 while((n = Read(fd, buff, BUFFSIZE)) > 0) 34 Write(STDOUT_FILENO, buff, n); 35 exit(0); 36 }Открываем дверь, считываем полное имя файла 9-15 Имя файла, связанного с дверью, принимается в качестве аргумента командной строки. Имя файла, который должен быть открыт и выведен, считывается из стандартного потока ввода, а завершающий символ перевода строки удаляется. Подготовка аргументов и указателя на буфер возврата16-22 Подготавливается структура door_arg_t. К размеру имени файла мы добавляем единицу, чтобы сервер мог дополнить его завершающим нулем. Вызов процедуры сервера и проверка результатов23-31 Мы вызываем процедуру сервера и проверяем результат. Должен возвращаться только один дескриптор и никаких данных. Вскоре мы увидим, что сервер возвращает данные (сообщение об ошибке) только в том случае, если он не может открыть файл. В этом случае функция err_quit выводит сообщение об ошибке. Считывание дескриптора и копирование файла32-34 Дескриптор извлекается из структуры door_desc_t, и файл копируется в стандартный поток вывода. В листинге 15.16 приведен текст процедуры сервера. Функция main по сравнению с листингом 15.3 не изменилась. Листинг 15.16. Процедура сервера, открывающая файл и возвращающая клиенту дескриптор//doors/serverfd1.c 1 #include "unpipc.h" 2 void 3 servproc(void *cookie, char *dataptr, size_t datasize, 4 door_desc_t *descptr, size_t ndesc) 5 { 6 int fd; 7 char resbuf[BUFFSIZE]; 8 door_desc_t desc; 9 dataptr[datasize-1] = 0; /* завершающий О */ 10 if ((fd = open(dataptr, O_RDONLY)) == –1) { 11 /* ошибка, нужно сообщить клиенту */ 12 snprintf(resbuf, BUFFSIZE, "%s: can't open, %s", 13 dataptr, strerror(errno)); 14 Door_return(resbuf, strlen(resbuf), NULL, 0); 15 } else { 16 /* ОК, возвращаем дескриптор */ 17 desc.d_data.d_desc.d_descriptor = fd; 18 desc.d_attributes = DOOR_DESCRIPTOR; 19 Door_return(NULL, 0, &desc, 1); 20 } 21 }Открытие файла для клиента 9-14 Мы завершаем полное имя файла клиента нулем и делаем попытку открыть этот файл вызовом open. Если возникает ошибка, сообщение о ней возвращается клиенту. Успешное открытие файла15-20 Если файл был успешно открыт, клиенту возвращается только его дескриптор. Запустим сервер и укажем ему имя двери /tmp/fd1, а затем запустим клиент: solaris % clientfd1 /tmp/fd1 /etc/shadow /etc/shadow: can't open. Permission denied solaris % clientfd1 /tmp/fd1 /no/such/file /no/such/file: can't open. No such file or directory solaris % clientfd1 /tmp/fd1 /etc/ntp.conf файл из двух строк multicastclient 224.0.1.1 driftfile /etc/ntp.drift В первых двух случаях мы указываем имя файла, приводящее к возврату сообщения об ошибке. В третий раз сервер передает клиенту дескриптор файла из двух строк, который благополучно выводится.
15.9. Функция door server_createВ листинге 15.6 мы показали, что библиотека дверей автоматически создает новые потоки для обслуживания запросов клиентов по мере их поступления. Они создаются библиотекой как неприсоединенные потоки (detached threads) с размером стека потока по умолчанию, с отключенной возможностью отмены потока (thread cancellation) и с маской сигналов и классом планирования (scheduling class), унаследованными от потока, вызвавшего door_create. Если мы хотим изменить какой-либо из этих параметров или хотим самостоятельно работать с пулом потоков сервера, можно воспользоваться функцией door_server_create и указать нашу собственную процедуру создания сервера: #include <door.h> typedef void Door_create_proc(door_info_t *); Door_create_proc *door_server_create(Door_create_proc *proc); /* Возвращает указатель на предыдущую процедуру создания сервера */ Как и при объявлении door_create в разделе 15.3, мы используем оператор typedef для упрощения прототипа библиотечной функции. Наш новый тип данных определяет процедуру создания сервера как принимающую один аргумент (указатель на структуру типа door_info_t) и ничего не возвращающую (void). При вызове door_server_create аргументом является указатель на нашу процедуру создания сервера, а возвращается указатель на предыдущую процедуру создания сервера. Наша процедура создания сервера вызывается при возникновении необходимости создания нового потока для обслуживания запроса клиента. Информация о том, какой из процедур сервера требуется новый поток, передается в структуре door_info_t, адрес которой принимается процедурой создания сервера. Поле di_proc содержит адрес процедуры сервера, а поле di_data содержит указатель на аргументы, передаваемые процедуре сервера при вызове. Проще всего изучить происходящее на примере. Программа-клиент не претерпевает никаких изменений по сравнению с листингом 15.1. В программу-сервер добавляются две новые функции помимо процедуры сервера и функции main. На рис. 15.5 приведена схема сервера с четырьмя функциями и последовательностью их регистрации и вызова. Рис. 15.5. Четыре функции в процессе-сервере В листинге 15.17 приведен текст функции main сервера. Листинг 15.17. Функция main для примера с управлением пулом потоков//doors/server6.c 42 int 43 main(int argc, char **argv) 44 { 45 if (argc != 2) 46 err_quit("usage: server6 <server-pathname>"); 47 Door_server_create(my_create); 48 /* создание дескриптора двери и связывание его с именем */ 49 Pthread_mutex_lock(&fdlock); 50 fd = Door_create(servproc, NULL, DOOR_PRIVATE); 51 Pthread_mutex_unlock(&fdlock); 52 unlink(argv[1]); 53 Close(Open(argv[1], O_CREAT | O_RDWR, FILE_MODE)); 54 Fattach(fd, argv[1]); 55 /* servproc() обслуживает запросы клиентов */ 56 for(;;) 57 pause(); 58 } По сравнению с листингом 15.2 было внесено четыре изменения: 1. Убрано объявление дескриптора двери fd (теперь это глобальная переменная, описанная в листинге 15.18). 2. Вызов door_create защищен взаимным исключением (также описанным в листинге 15.18). 3. Вызов door_server_create делается перед созданием двери, при этом указывается процедура создания сервера (my_thread, которая, будет показана позже). 4. В вызове door_create последний аргумент (атрибуты) имеет значение DOOR_PRIVATE вместо 0. Это говорит библиотеке о том, что данная дверь будет иметь собственный пул потоков, называемый частным пулом сервера. Задание процедуры создания сервера с помощью door_server_create и выделение частного пула сервера с помощью DOOR_PRIVATE осуществляются независимо друг от друга. Возможны четыре ситуации: 1. По умолчанию частный пул сервера и процедура создания сервера отсутствуют. Система создает потоки по мере необходимости и они переходят в пул потоков процесса. 2. Указан флаг DOOR_PRIVATE, но процедура создания сервера отсутствует. Система создает потоки по мере необходимости и они отходят в пул потоков процесса, если относятся к тем дверям, для которых флаг DOOR_PRIVATE не был указан, либо в пул данной двери, если она была создана с флагом DOOR_PRIVATE. 3. Отсутствует частный пул сервера, но указана процедура создания сервера. Процедура создания вызывается при необходимости создания нового потока, который затем переходит в пул потоков процесса. 4. Указан флаг DOOR_PRIVATE и процедура создания сервера. Процедура создания сервера вызывается каждый раз при необходимости создания потока. После создания поток должен вызвать door_bind для отнесения его к нужному частному пулу сервера, иначе он будет добавлен к пулу потоков процесса. В листинге 15.18 приведен текст двух новых функций: my_create (процедура создания сервера) и my_thread (функция, выполняемая каждым потоком, который создается my_create). Листинг 15.18. Функции управления потоками//doors/server6.c 13 pthread_mutex_t fdlock = PTHREAD_MUTEX_INITIALIZER; 14 static int fd = –1; /* дескриптор двери */ 15 void * 16 my_thread(void *arg) 17 { 18 int oldstate; 19 door_info_t *iptr = arg; 20 if ((Door_server_proc*)iptr->di_proc == servproc) { 21 Pthread_mutex_lock(&fdlock); 22 Pthread_mutex_unlock(&fdlock); 23 Pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate); 24 Door_bind(fd); 25 Door_return(NULL, 0, NULL, 0); 26 } else 27 err_quit("my_thread: unknown function: %p", arg); 28 return(NULL); /* никогда не выполняется */ 29 } 30 void 31 my_create(door info_t *iptr) 32 { 33 pthread_t tid; 34 pthread_attr_t attr; 35 Pthread_attr_init(&attr); 36 Pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM); 37 Pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 38 Pthread_create(&tid, &attr, my_thread, (void *)iptr); 39 Pthread_attr_destroy(&attr); 40 printf("my_thread: created server thread %ld\n", pr_thread_id(&tid)): 41 }Процедура создания сервера 30-41 Каждый раз при вызове my_create создается новый поток. Перед вызовом pthread_create атрибуты потока инициализируются, область потока устанавливается равной PTHREAD_SCOPE_SYSTEM и поток определяется как неприсоединенный (detached). Созданный поток вызывает функцию my_thread. Аргументом этой функции является указатель на структуру типа door_info_t. Если у нас имеется сервер с несколькими дверьми и мы указываем процедуру создания сервера, эта процедура создания сервера будет вызываться при необходимости создания потока для любой из дверей. Единственный способ, которым эта процедура может определить тип сервера, соответствующий нужной двери, заключается в изучении указателя di_proc в структуре типа door_info_t. Функция, запускающая поток 15-20 При создании потока запускается функция my_thread, указанная в вызове pthread_create. Аргументом является указатель на структуру типа door_info_t, передаваемый my_create. В данном примере есть только одна процедура сервера — servproc, и мы просто проверяем, что аргумент указывает на эту процедуру. Ожидание присваивания дескриптору правильного значения21-22 Процедура создания сервера вызывается в первый раз при вызове door_create для создания первого потока сервера. Этот вызов осуществляется из библиотеки дверей до завершения работы door_create. Однако переменная fd не примет значения дескриптора двери до тех пор, пока не произойдет возврата из функции door_create (проблема курицы и яйца). Поскольку мы знаем, что my_thread выполняется отдельно от основного потока, решение состоит в том, чтобы использовать взаимное исключение fdlock следующим образом: основной поток блокирует взаимное исключение перед вызовом door_create и разблокирует после возврата из door_create (когда дескриптору fd уже присвоено некоторое значение). Функция my_thread делает попытку заблокировать взаимное исключение (ее выполнение приостанавливается до тех пор, пока основной поток не разблокирует это взаимное исключение), а затем разблокирует его. Мы могли бы добавить условную переменную и передавать по ней уведомление, но здесь это не нужно, поскольку мы заранее знаем, в каком порядке будут происходить вызовы. Отключение отмены потока23 При создании нового потока вызовом pthread_create его отмена по умолчанию разрешена. Если отмена потока разрешена и клиент прерывает вызов door_call в процессе его выполнения (что мы продемонстрируем в листинге 15.26), вызываются обработчики отмены потока, после чего он завершается. Если отмена потока отключена (как это делаем мы) и клиент прерывает работу в вызове door_call, процедура сервера спокойно завершает работу (поток не завершается), а результаты door_return просто сбрасываются. Поскольку серверный поток завершается, если происходит отмена потока, и поскольку процедура сервера может в этот момент выполнять какие-то действия (возможно, с заблокированными семафорами или блокировками), библиотека дверей на всякий случай отключает отмену всех создаваемых ею потоков. Если нам нужно, чтобы процедура сервера отменялась при досрочном завершении работы клиента, для этого потока следует включить возможность отмены и приготовиться обработать такую ситуацию. Связывание потока с дверью 24 Вызов door_bind позволяет добавить поток к пулу, связанному с дверью, дескриптор которой передается door_bind в качестве аргумента. Поскольку для этого нам нужно знать дескриптор двери, в этой версии сервера он является глобальной переменной. Делаем поток доступным клиенту25 Мы делаем поток доступным клиенту вызовом door_return с двумя нулевыми указателями и нулевыми значениями длин буферов в качестве аргументов. Процедура сервера приведена в листинге 15.19. Она идентична программе из листинга 15.6. Листинг 15.19. Процедура сервера//doors/server6.c 1 #include "unpipc.h" 2 void 3 servproc(void *cookie, char *dataptr, size_t datasize, 4 door_desc_t *descptr, size_t ndesc) 5 { 6 long arg, result; 7 arg = *((long *) dataptr); 8 printf("thread id %ld, arg = %ld\n", pr_thread_id(NULL), arg); 9 sleep(5); 10 result = arg * arg; 11 Door_return((char *)&result, sizeof(result), NULL, 0); 12 } Чтобы продемонстрировать работу программы, запустим сервер: solaris % server6 /tmp/door6 my_thread: created server thread 4 После запуска сервера и вызова door_create процедура создания сервера запускается в первый раз, хотя клиент мы еще не запустили. При этом создается первый поток, ожидающий запроса от первого клиента. Затем мы запускаем клиент три раза подряд: solaris % client6 /tmp/door6 11 result: 121 solaris % client6 /tmp/door6 22 result: 484 solaris % client6 /tmp/door6 33 result: 1089 Посмотрим, что при этом выводит сервер. При поступлении первого запроса клиента создается новый поток (с идентификатором потока 5), а поток с номером 4 обслуживает все запросы клиентов. Библиотека дверей всегда держит один лишний поток наготове: my_thread: created server thread 5 thread id 4, arg = 11 thread id 4, arg = 22 thread id 4, arg = 33 Запустим теперь три экземпляра клиента одновременно в фоновом режиме: solaris % client6 /tmp/door6 44 &client6 /tmp/door6 55 &client6 /tmp/door6 66 & [2] 4919 [3] 4920 [4] 4921 solaris % result: 1936 result: 4356 result: 3025 Посмотрев на вывод сервера, мы увидим, что было создано два новых потока (с идентификаторами 6 и 7) и потоки 4, 5 и 6 обслужили три запроса от клиентов: thread id 4, arg = 44 my_thread: created server thread 6 thread id 5, arg = 66 my_thread: created server thread 7 thread id 6, arg = 55 15.10. Функции door_bind, door unbind и door_revokeРассмотрим еще три функции, дополняющие интерфейс дверей: #include <door.h> int door_bind(int fd); int door_unbind(void); int door_revoke(int fd); /* Всe три возвращают 0 в случае успешного завершения, –1 – в случае ошибки */ Функция door_bind впервые появилась в листинге 15.18. Она связывает вызвавший ее поток с частным пулом сервера, относящимся к двери с дескриптором fd. Если вызвавший поток уже подключен к какой-либо другой двери, производится его неявное отключение. Функция door_unbind осуществляет явное отключение потока от текущего пула, к которому он подключен. Функция door_revoke отключает доступ к двери с дескриптором fd. Дескриптор двери может быть отменен только процессом, создавшим эту дверь. Все вызовы через эту дверь, находящиеся в процессе выполнения в момент вызова этой функции, будут благополучно завершены. 15.11. Досрочное завершение клиента или сервераВ наших примерах до настоящего момента предполагалось, что в процессе работы клиента и сервера не возникает непредусмотренных ситуаций. Посмотрим, что произойдет, если у клиента или сервера возникнут ошибки. В случае если клиент и сервер являются частями одного процесса (локальный вызов процедуры на рис. 15.1), клиенту не нужно беспокоиться о возникновении ошибок на сервере, и наоборот. Однако если клиент и сервер находятся в различных процессах, нужно учесть возможность досрочного завершения одного из них и предусмотреть способ уведомления второго об этом событии. Об этом нужно заботиться вне зависимости от того, находятся ли клиент и сервер на одном узле или нет. Досрочное завершение сервераЕсли клиент блокируется в вызове door_call, ожидая получения результатов, ему нужно каким-то образом получить уведомление о завершении потока сервера по какой-либо причине. Посмотрим, что происходит в этом случае, прервав работу сервера вызовом pthread_exit. Это приведет к завершению потока сервера (а не всего процесса). В листинге 15.20 приведен текст процедуры сервера. Листинг 15.20. Процедура сервера, завершающая работу сразу после запуска//doors/serverintr1.c 1 #include "unpipc.h" 2 void 3 servproc(void *cookie, char *dataptr, size_t datasize, 4 door_desc_t *descptr, size_t ndesc) 5 { 6 long arg, result; 7 pthread_exit(NULL); /* посмотрим, что произойдет с клиентом */ 8 arg = *((long*)dataptr); 9 result = arg * arg; 10 Door_return((char*)&result, sizeof(result), NULL, 0); 11 } Оставшаяся часть сервера не претерпевает изменений по сравнению с листингом 15.2, а программу-клиент мы берем из листинга 15.1. Запустив клиент, мы увидим, что вызов door_call возвращает ошибку EINTR, если процедура сервера завершается досрочно: solaris % clientintr1 /tmp/door1 11 door_call error: Interrupted system call Непрерываемость системного вызова door_callДокументация на door_call предупреждает, что эта функция не предполагает возможности перезапуска (библиотечная функция door_call делает системный вызов с тем же именем). Мы можем убедиться в этом, изменив процедуру сервера таким образом, чтобы она делала паузу в 6 секунд перед возвращением, что показано в листинге 15.21. Листинг 15.21. Процедура сервера делает паузу в 6 секунд//doors/serverintr2.с 1 #include "unpipc.h" 2 void 3 servproc(void *cookie, char *dataptr, size_t datasize, 4 door_desc_t *descptr, size_t ndesc) 5 { 6 long arg, result; 7 sleep(6); /* клиент получает сигнал SIGCHLD */ 8 arg = *((long*)dataptr); 9 result = arg * arg; 10 Door_return((char*)&result, sizeof(result), NULL, 0); 11 } Изменим теперь клиент из листинга 15.2: установим обработчик сигнала SIGCHLD, добавив порождение процесса и завершение порожденного процесса через 2 секунды. Таким образом, через 2 секунды после вызова door_call дочерний процесс завершит работу, а родительский перехватит сигнал SIGCHLD и произойдет возврат из обработчика сигнала, прерывающий системный вызов door_call. Текст программы-клиента показан в листинге 15.22. Листинг 15.22. Клиент, перехватывающий сигнал SIGCHLD//doors/clientintr2.c 1 #include "unpipc.h" 2 void 3 sig_chld(int signo) 4 { 5 return; /* просто прерываем door_call() */ 6 } 7 int 8 main(int argc, char **argv) 9 { 10 int fd; 11 long ival, oval; 12 door_arg_t arg; 13 if (argc != 3) 14 err_quit("usage: clientintr2 <server-pathname> <integer-value>"); 15 fd = Open(argv[1], O_RDWR); /* открываем дверь */ 16 /* подготовка аргументов и указателя на результат */ 17 ival = atol(argv[2]); 18 arg.data_ptr = (char*)&ival; /* аргументы */ 19 arg.data_size = sizeof(long); /* размер аргументов */ 20 arg.desc_ptr = NULL; 21 arg.desc_num = 0; 22 arg.rbuf = (char*)&oval; /* данные */ 23 arg.rsize = sizeof(long); /* размер данных */ 24 Signal(SIGCHLD, sig_chld); 25 if (Fork() == 0) { 26 sleep(2); /* дочерний процесс */ 27 exit(0); /* отправка SIGCHLD */ 28 } 29 /* вызов процедуры сервера и вывод результата */ 30 Door_call(fd, &arg); 31 printf(result: %ld\n", oval); 32 exit(0); 33 } Клиенту будет возвращена та же ошибка, что и при досрочном завершении сервера — EINTR: solaris % clientintr2 /tmp/door2 22 door_call error: interrupted system call Поэтому нужно блокировать все сигналы, которые могут прервать вызов door_call. Идемпотентные и неидемпотентные процедурыА что произойдет, если мы перехватим сигнал EINTR и вызовем процедуру сервера еще раз, поскольку мы знаем, что эта ошибка возникла из-за нашего собственного прерывания системного вызова перехваченным сигналом (SIGCHLD)? Это может привести к некоторым проблемам, как мы покажем ниже. Изменим сервер так, чтобы он выводил идентификатор вызванного потока, делал паузу в 6 секунд и выводил идентификатор потока по завершении его. В листинге 15.23 приведен текст новой процедуры сервера. Листинг 15.23. Процедура сервера, выводящая свой идентификатор потока дважды//doors/serverintr3.c 1 #include "unpipc.h" 2 void 3 servproc(void *cookie, char *dataptr, size_t datasize, 4 door_desc_t *descptr, size_t ndesc) 5 { 6 long arg, result: 7 printf("thread id %ld called\n", pr_thread_id(NULL)); 8 sleep(6); /* даем клиенту возможность перехватить SIGCHLD */ 9 arg = *((long*)dataptr); 10 result = arg * arg; 11 printf("thread id %ld returning\n", pr_thread_id(NULL)); 12 Door_return((char *) &result, sizeof(result), NULL, 0); 13 } В листинге 15.24 приведен текст программы-клиента. Листинг 15.24. Клиент, вызывающий door_call еще раз, после перехвата EINTR//doors/clientintr3.c 1 #include "unpipc.h" 2 volatile sig_atomic_t caught_sigchld; 3 void 4 sig_chld(int signo) 5 { 6 caught_sigchld = 1; 7 return; /* прерываем вызов door_call() */ 8 } 9 int 10 main(int argc, char **argv) 11 { 12 int fd, rc; 13 long ival, oval; 14 door_arg_t arg; 15 if (argc != 3) 16 err_quit("usage: clientintr3 <server-pathname> <integer-value>"); 17 fd = Open(argv[1], O_RDWR); /* открытие двери */ 18 /* подготовка аргументов и указателя на результаты */ 19 ival = atol(argv[2]); 20 arg.data_ptr = (char*)&ival; /* аргументы */ 21 arg.data_size = sizeof(long); /* размер аргументов */ 22 arg.desc_ptr = NULL; 23 arg.desc_num = 0; 24 arg.rbuf = (char*)&oval; /* возвращаемые данные */ 25 arg.rsize = sizeof(long); /* размер данных */ 26 Signal(SIGCHLD, sig_chld); 27 if (Fork() == 0) { 28 sleep(2); /* дочерний процесс */ 29 exit(0); /* отправка SIGCHLD */ 30 } 31 /* родительский процесс : вызов процедуры сервера и вывод результата */ 32 for (;;) { 33 printf("calling door_call\n"); 34 if ((rc = door_call(fd, &arg)) == 0) 35 break; /* успешное завершение */ 36 if (errno == EINTR && caught_sigchld) { 37 caught_sigchld = 0; 38 continue; /* повторный вызов door_call */ 39 } 40 err_sys("door_call error"); 41 } 42 printf("result: %ld\n", oval); 43 exit(0); 44 } 2-8 Объявляем глобальную переменную caught_sigchld, устанавливая ее в единицу при перехвате сигнала SIGCHLD. 31-42 Вызываем door_call в цикле, пока он не завершится успешно. Глядя на выводимые клиентом результаты, мы можем подумать, что все в порядке: solaris % clientintr3 /tmp/door3 33 calling door_call calling door_call result: 1089 Функция door_call вызывается в первый раз, обработчик сигнала срабатывает через 2 секунды после этого и переменной caught_sigchld присваивается значение 1. door_call при этом возвращает ошибку EINTR и мы вызываем door_call еще раз. Во второй раз процедура завершается успешно. Посмотрев на выводимый сервером текст, мы увидим, что процедура сервера была вызвана дважды: solaris % serverintr3 /tmp/door3 thread id 4 called thread id 4 returning thread id 5 called thread id 5 returning Когда клиент второй раз вызывает door_call, это приводит к запуску нового потока, вызывающего процедуру сервера еще раз. Если процедура сервера идемпотентна, проблем в такой ситуации не возникнет. Однако если она неидемпотентна, это может привести к ошибкам. Термин «идемпотентность» по отношению к процедуре подразумевает, что процедура может быть вызвана произвольное число раз без возникновения ошибок. Наша процедура сервера, вычисляющая квадрат целого числа, идемпотентна: мы получаем правильный результат вне зависимости от того, сколько раз мы ее вызовем. Другим примером является процедура, возвращающая дату и время. Хотя эта процедура и будет возвращать разную информацию при новых вызовах (поскольку дата и время меняются), это не вызовет проблем. Классическим примером неидемпотентной процедуры является процедура уменьшения банковского счета на некоторую величину. Конечный результат будет неверным, если ее вызвать дважды. Досрочное завершение клиентаПосмотрим, каким образом процедура сервера получает уведомление о досрочном завершении клиента. Пpoгрaммa-клиeнт приведена в листинге 15.25. Листинг 15.25. Клиент, досрочно завершающий работу после вызова door_call//doors/clientintr4.c 1 #include "unpipc.h" 2 int 3 main(int argc, char **argv) 4 { 5 int fd; 6 long ival, oval; 7 door_arg_t arg; 8 if (argc != 3) 9 err_quit("usage: clientintr4 <server-pathname> <integer-value>"); 10 fd = Open(argv[1], O_RDWR); /* открываем дверь */ 11 /* подготовка аргументов и указателя на результаты */ 12 ival = atol(argv[2]); 13 arg.data_ptr = (char*)&ival; /* аргументы */ 14 arg.data_size = sizeof(long); /* размер аргументов */ 15 arg.desc_ptr = NULL; 16 arg.desc_num = 0; 17 arg.rbuf = (char*)&oval; /* возвращаемые данные */ 18 arg.rsize = sizeof(long); /* размер возвращаемых данных */ 19 /* вызов процедуры сервера и вывод результата */ 20 alarm(3); 21 Door_call(fd, &arg); 22 printf("result: %ld\n", oval); 23 exit(0); 24 } 20 Единственное изменение заключается в добавлении вызова alarm(3) перед door_call. Эта функция приводит к отправке сигнала SIGALRM через три секунды после вызова, но, поскольку мы его не перехватываем, это приводит к завершению процесса. Поэтому клиент завершится до возврата из door_call, потому что в процедуру сервера вставлена шестисекундная пауза. В листинге 15.26 приведен текст процедуры сервера и обработчик отмены потока. Листинг 15.26. Процедура сервера, обрабатывающая досрочное завершение клиента//doors/serverintr4.с 1 #include "unpipc.h" 2 void 3 servproc_cleanup(void *arg) 4 { 5 printf("servproc cancelled, thread id %ld\n", pr_thread_id(NULL)); 6 } 7 void 8 servproc(void *cookie, char *dataptr, size_t datasize, 9 door_desc_t *descptr, size_t ndesc) 10 { 11 int oldstate, junk; 12 long arg, result; 13 Pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstate); 14 pthread_cleanup_push(servproc_cleanup, NULL); 15 sleep(6); 16 arg = *((long*)dataptr); 17 result = arg * arg; 18 pthread_cleanup_pop(0); 19 Pthread_setcancelstate(oldstate, &junk); 20 Door_return((char*)&result, sizeof(result), NULL, 0); 21 } Вспомните, что мы говорили об отмене выполнения потока в разделе 8.5 и в связи с листингом 15.18. Когда система обнаруживает завершение клиента в процессе выполнения серверной процедуры, потоку, обрабатывающему запрос этого клиента, отправляется запрос на отмену: ■ если поток отключил возможность отмены, ничего не происходит и поток выполняется до завершения (вызов door_return), а результаты сбрасываются; ■ если возможность отмены включена, вызываются обработчики отмены потока, а затем он завершает работу. В тексте процедуры сервера мы сначала вызвали pthread_setcancelstate для включения возможности отмены потока, потому что по умолчанию при создании новых потоков библиотекой возможность их отмены отключается. Эта функция сохраняет текущее состояние потока в переменной oldstate, и мы восстанавливаем его в конце функции. Затем мы вызываем pthread_cleanup_push для регистрации нашего обработчика отмены servproc_cleanup. Эта функция только выводит идентификатор отмененного потока, но вообще она может выполнять все необходимое для корректного завершения процедуры сервера (разблокировать исключения и т. п.). После возвращения из обработчика поток завершается. В текст процедуры сервера мы добавляем 6-секундную паузу, чтобы клиент мог успешно завершить работу в вызове door_call. Запустив клиент дважды, мы увидим сообщение интерпретатора Alarm clock при завершении процесса сигналом SIGALRM: solaris % clientintr4 /tmp/door4 44 Alarm Clock solaris % clientintr4 /tmp/door4 44 Alarm Clock Посмотрим, что при этом выводит сервер. Каждый раз при досрочном завершении клиента поток процедуры сервера действительно отменяется и вызывается обработчик отмены потока: solaris % serverintr4 /tmp/door4 servproc canceled, thread id 4 servproc canceled, thread id 5 Цель, с которой мы вызываем программу-клиент дважды, — показать, что после завершения потока с идентификатором 4 библиотека создает новый поток (с идентификатором 5) для обработки второго запроса клиента. 15.12. РезюмеИнтерфейс дверей позволяет вызывать процедуры в других процессах на том же узле. В следующей главе мы обсудим возможность удаленного вызова процедур в процессах на других узлах. Основные функции этого интерфейса просты в работе и использовании. Сервер вызывает door_create для создания двери и связывания ее с процедурой сервера, а затем вызывает fattach для сопоставления этой двери и имени файла в файловой системе. Клиент вызывает open для этого имени файла и затем может вызвать door_call для вызова процедуры сервера. Возврат из процедуры сервера осуществляется вызовом door_return. Обычно разрешения для двери проверяются только один раз — при ее открытии вызовом open. Проверяются идентификаторы пользователя и группы клиента (и полного имени файла). Одной из полезных функций дверей (по сравнению с другими средствами IPC) является возможность получения информации о клиенте в процессе работы (его действующего и реального идентификаторов). Это может использоваться сервером для принятия решения о предоставлении услуг данному клиенту. Двери предоставляют возможность передачи дескрипторов от клиента серверу и обратно. Это достаточно мощное средство, поскольку дескрипторы в Unix дают возможность обращаться ко множеству объектов (файлам, сокетам или XTI, дверям). При вызове процедур в другом процессе следует учесть возможность досрочного завершения клиента или сервера. Клиент получает уведомление о досрочном завершении сервера с помощью сообщения об ошибке EINTR. Сервер получает уведомление о досрочном завершении клиента в процессе обработки процедуры посредством запроса на отмену выполнения потока данной процедуры. Сервер может обработать этот запрос или проигнорировать его. Упражнения1. Сколько байтов информации передается при вызове door_call от клиента серверу? 2. Есть ли необходимость вызывать fstat для проверки типа дескриптора в листинге 15.3? Уберите этот вызов и посмотрите, что произойдет. 3. В документации Solaris 2.6 для вызова sleep() говорится, что «выполнение текущего процесса приостанавливается». Почему при этом библиотека дверей имеет возможность создать новые потоки в листинге 15.6? 4. В разделе 15.3 мы отмечали, что для создаваемых вызовом door_create дверей автоматически устанавливается бит FD_CLOEXEC. Однако мы можем вызвать fcntl после возврата из door_create и сбросить этот бит. Что произойдет, если мы сделаем это, вызовем exec, а затем обратимся к процедуре сервера из клиента? 5. В листингах 15.23 и 15.24 добавьте вывод текущего времени в вызовах printf сервера и клиента. Запустите клиент и сервер. Почему первый экземпляр процедуры сервера возвращается через две секунды после запуска? 6. Удалите блокировку, защищающую дескриптор fd в листингах 15.17 и 15.18, и убедитесь, что программа больше не работает. Какая при этом возникает ошибка? 7. Если мы хотим лишь испытать возможность отмены потока с процедурой сервера, нужно ли нам устанавливать процедуру создания сервера? 8. Проверьте, что вызов door_revoke дает возможность завершиться работающим с данной процедурой потокам. Выясните, что происходит при вызове door_саll после аннулирования процедуры. 9. В нашем решении предыдущего упражнения и в листинге 15.17 мы говорим, что дескриптор двери должен быть глобальным, если он нужен процедуре сервера или процедуре создания сервера. Это утверждение, вообще говоря, неверно. Перепишите решение предыдущего упражнения, сохранив fd в качестве автоматической переменной функции main. 10. В программе листинга 15.18 мы вызывали pthread_attr_init и pthread_attr_ destroy каждый раз, когда создавался поток. Является ли такое решение оптимальным? ГЛАВА 16Пакет Sun RPC 16.1. ВведениеКогда мы разрабатываем приложение, мы встаем перед выбором: 1. Написать одну большую монолитную программу, которая будет делать все. 2. Разделить приложение на несколько процессов, взаимодействующих друг с другом. Если мы выбираем второй вариант, перед нами опять встает вопрос: 2.1. предполагать ли, что все процессы выполняются на одном узле, что допускает использование средств IPC для взаимодействия между ними, либо 2.2. допускать возможность запуска части процессов на других узлах, что требует какой-либо формы взаимодействия по сети. Взгляните на рис. 15.1. Верхняя ситуация соответствует варианту 1, средняя — варианту 2.1, а нижняя — варианту 2.2. Большая часть этой книги посвящена обсуждению случая 2.1, то есть взаимодействию процессов в пределах одного узла с помощью таких средств IPC, как передача сообщений, разделяемая память и, возможно, некоторых форм синхронизации. Взаимодействие между потоками одного процесса или разных процессов является частным случаем этого сценария. Когда мы выдвигаем требование поддержки возможности сетевого взаимодействия между составляющими приложения, для разработки чаще всего используется явное сетевое программирование. При этом в программе используются вызовы интерфейсов сокетов или XTI, о чем рассказывается в [24]. При использовании интерфейса сокетов клиенты вызывают функции socket, connect, read и write, а серверы вызывают socket, bind, listen, accept, read и write. Большая часть знакомых нам приложений (браузеры, серверы Web, клиенты и серверы Telnet) написаны именно так. Альтернативным способом разработки распределенных приложений является неявное сетевое программирование. Оно осуществляется посредством удаленного вызова процедур (remote procedure call — RPC). Приложение кодируется с использованием тех же вызовов, что и обычное несетевое, но клиент и сервер могут находиться на разных узлах. Сервером называется процесс, предоставляющий другому возможность запускать одну из составляющих его процедур, а клиентом — процесс, вызывающий процедуру сервера. То, что клиент и сервер находятся на разных узлах и для связи между ними используется сетевое взаимодействие, большей частью остается скрыто от программиста. Одним из критериев оценки качества пакета RPC является именно скрытость лежащего в основе интерфейса сети. ПримерВ качестве примера использования RPC перепишем листинги 15.1 и 15.2 для использования Sun RPC вместо дверей. Клиент вызывает процедуру сервера с аргументом типа long, а возвращаемое значение представляет собой квадрат аргумента. В листинге 16.1[1] приведен текст первого файла, square.х. Листинг 16.1. Файл спецификации RPC//sunrpc/square1/square.x 1 struct square_in { /* входные данные (аргумент) */ 2 long arg1; 3 }; 4 struct square_out { /* возвращаемые данные (результат) */ 5 long res1; 6 }; 7 program SQUARE_PROG { 8 version SQUARE_VERS { 9 square_out SQUAREPROC(square_in) = 1; /* номер процедуры = 1 */ 10 } = 1; /* номер версии */ 11 } = 0x31230000; /* номер программы */ Файлы с расширением .х называются файлами спецификации RPC. Они определяют процедуры сервера, их аргументы и возвращаемые значения. Определение аргумента и возвращаемого значения1-6 Мы определяем две структуры — одну для аргументов (одно поле типа long) и одну для результатов (тоже одно поле типа long). Определение программы, версии и процедуры7-11 Мы определяем программу RPC с именем SQUARE_PROG, состоящую из одной версии (SQUARE_VERS), а эта версия представляет собой единственную процедуру сервера с именем SQUAREPROC. Аргументом этой процедуры является структура square_in, а возвращаемым значением — структура square_out. Мы также присваиваем этой процедуре номер 1, как и версии, а программе мы присваиваем 32-разрядный шестнадцатеричный номер. (О номерах программ более подробно говорится в табл. 16.1.) Компилировать спецификацию нужно программой rpcgen, входящей в пакет Sun RPC. Теперь напишем функцию main клиента, который будет осуществлять удаленный вызов процедуры. Текст программы приведен в листинге 16.2. Листинг 16.2. Функция main клиента, делающего удаленный вызов процедуры//sunrpc/square1/client.c 1 #include "unpipc.h" /* наш заголовочный файл */ 2 #include "square.h" /* создается rpcgen */ 3 int 4 main(int argc, char **argv) 5 { 6 CLIENT *cl; 7 square_in in; 8 square_out *outp; 9 if (argc != 3) 10 err_quit("usage: client <hostname> <integer-value>"); 11 cl = Clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, "tcp"); 12 in.arg1 = atol(argv[2]); 13 if ((outp = squareproc_1(&in, cl)) == NULL) 14 err_quit("ls", clnt_sperror(cl, argv[1])); 15 printf("result: %ld\n", outp->res1); 16 exit(0); 17 }Подключение заголовочного файла, создаваемого rpcgen 2 Мы подключаем заголовочный файл square.h, создаваемый функцией rpcgen. Объявление дескриптора клиента6 Мы объявляем дескриптор клиента (client handle) с именем cl. Дескрипторы клиентов выглядят как обычные указатели на тип FILE (поэтому слово CLIENT пишется заглавными буквами). Получение дескриптора клиента11 Мы вызываем функцию clnt_create, создающую клиент RPC: #include <rpc/rpc.h> CLIENT *clnt_create(const char *host, unsigned long prognum, unsigned long versnum, const char *protocol); /* Возвращает ненулевой дескриптор клиента в случае успешного завершения. NULL – в случае ошибки */ Как и с обычными указателями на тип FILE, нам безразлично, на что указывает дескриптор клиента. Скорее всего, это некоторая информационная структура, хранящаяся в ядре. Функция clnt_create создает такую структуру и возвращает нам указатель на нее, а мы передаем его библиотеке RPC времени выполнения каждый раз при удаленном вызове процедуры. Первым аргументом clnt_create должно быть имя или IP-адрес узла, на котором выполняется сервер. Вторым аргументом будет имя программы, третьим — номер версии. Оба эти значения берутся из спецификации (square.х, листинг 16.1). Последний аргумент позволяет указать протокол, обычно TCP или UDP. Вызов удаленной процедуры и вывод результата12-15 Мы вызываем процедуру, причем первый аргумент указывает на входную структуру (&in), а второй содержит дескриптор клиента. В большинстве стандартных функций ввода-вывода дескриптор файла является последним аргументом. Точно так же и в вызовах RPC дескриптор клиента передается последним. Возвращаемое значение представляет собой указатель на структуру, в которой хранится результат работы сервера. Место под входную структуру выделяет программист, а под возвращаемую — пакет RPC. В файле спецификации square.x мы назвали процедуру SQUAREPROC, но из клиента мы вызываем squareproc_1. Существует соглашение о преобразовании имени из файла спецификации к нижнему регистру и добавлении номера версии через символ подчеркивания. Со стороны сервера от нас требуется только написать процедуру. Функция main автоматически создается программой rpcgen. Текст процедуры приведен в листинге 16.3. Листинг 16.3. Процедура сервера, вызываемая с помощью Sun RPC//sunrpc/square1/server.c 1 #include "unpipc.h" 2 #include "square.h" 3 square_out * 4 squareproc_l_svc(square_in *inp, struct svc_req *rqstp) 5 { 6 static square_out out; 7 out.res1 = inp->arg1 * inp->arg1; 8 return(&out); 9 }Аргументы процедуры 3-4 Прежде всего мы замечаем, что к имени процедуры добавился суффикс _svc. Это дает возможность использовать два прототипа функций ANSI С в файле square.x, один из которых определяет функцию, вызываемую клиентом в листинге 16.2 (она принимает дескриптор клиента), а второй — реальную функцию сервера (которая принимает другие аргументы). При вызове процедуры сервера первый аргумент является указателем на входную структуру, а второй — на структуру, передаваемую библиотекой RPC времени выполнения, которая содержит информацию о данном вызове (в этом примере игнорируется для простоты). Выполнение и возврат6-8 Программа считывает входной аргумент и возводит его в квадрат. Результат сохраняется в структуре, адрес которой возвращается процедурой сервера. Поскольку мы возвращаем адрес переменной, эта переменная не может быть автоматической. Мы объявляем ее как статическую (static).
Откомпилируем клиент в системе Solaris, а сервер — в BSD/OS, запустим сервер, а затем клиент: solaris % client bsdi 11 result: 121 solaris % client 209.75.135.35 22 result: 484 В первом случае мы указываем имя узла сервера, а во втором — его IP-адрес. Этим мы демонстрируем возможность использования как имен, так и IP-адресов для задания узла в функции clnt_create. Теперь продемонстрируем некоторые ошибки, возникающие при работе clnt_create, если, например, не существует узел или на нем не запущена программа-сервер: solaris % client nosuchhost 11 nosuchhost: RPC: Unknown host возвращается библиотекой RPC времени выполнения clnt_create error возвращается нашей функцией-оберткой solaris % client localhost 11 localhost: rpc: program not registered clnt_create error Мы написали клиентскую и серверную части программы и продемонстрировали их использование вообще без явного сетевого программирования. Клиент просто вызывает две функции, а сервер вообще состоит из одной функции. Все тонкости использования XTI в Solaris, сокетов в BSD/OS и сетевого ввода-вывода обрабатываются библиотекой RPC времени выполнения. В этом и состоит предназначение RPC — предоставлять возможность создания распределенных приложений без знания сетевого программирования. Другая немаловажная деталь данного примера заключается в том, что в системах Sparc под Solaris и Intel x86 под управлением BSD/OS используется разный порядок байтов. В Sparc используется порядок big endian («тупоконечный»[2]), а в Intel — little endian («остроконечный») (что мы показали в разделе 3.4 [24]). Отличия в порядке байтов также обрабатываются библиотекой RPC времени выполнения автоматически с использованием стандарта XDR (внешнее представление данных), который мы обсудим в разделе 16.8. Создание программы-клиента и программы-сервера в данном случае требует больше операций, чем для любой другой программы этой книги. Выполняемый файл клиента получается следующим образом: solaris % rpcgen-Сsquare.x solaris % cc-сclient.с-оclient.о solaris % cc-сsquare_clnt.c-оsquare_clnt.o solaris % cc-сsquare_xdr.с-оsquare_xdr.o solaris % cc-оclient client.оsquare_clnt.o square_xdr.o libunpipc.a –lnsl Параметр –С говорит rpcgen о необходимости создания прототипов функций ANSI С в заголовочном файле square.h. Программа rpcgen также создает заглушку клиента (client stub) в файле с именем square_clnt.с и файл с именем square_xdr.с, который осуществляет преобразование данных в соответствии со стандартом XDR. Наша библиотека (содержащая функции, используемые в этой книге) называется libunpipc.a, а параметр –lnsl подключает системную библиотеку сетевых функций в Solaris (включая библиотеки RPC и XDR времени выполнения). Аналогичные команды используются для создания сервера, хотя rpcgen уже не нужно запускать снова. Файл square_svc.c содержит функцию main сервера, и файл square_xdr.о, обсуждавшийся выше, также требуется для работы сервера: solaris % cc –с server.с –о server.о solaris % сc –с square_svc.c –о square_svc.o solaris % cc –о server server.о square_svc.o libunpipc.a –lnsl При этом создаются клиент и сервер, выполняемые в системе Solaris. Если клиент и сервер должны быть построены для разных систем (как в предыдущем примере, где клиент выполнялся в Solaris, а сервер — в BSD/OS), могут потребоваться дополнительные действия. Например, некоторые файлы должны быть либо общими (через NFS), либо находиться в обеих системах, а файлы, используемые клиентом и сервером (например, square_xdr.o), должны компилироваться в каждой системе в отдельности. Рис. 16.1. Этапы создания приложения клиент-сервер с использованием RPC На рис. 16.1 приведена схема создания приложения типа клиент-сервер. Три затемненных прямоугольника соответствуют файлам, которые мы должны написать. Штриховые линии показывают файлы, подключаемые через заголовочный файл square.h. На рис. 16.2 изображена схема происходящего при удаленном вызове процедуры. Действия выполняются в следующем порядке: 1. Запускается сервер, который регистрируется в программе, управляющей портами на узле-сервере. Затем запускается клиент и вызывает clnt_create. Эта функция связывается с управляющей портами программой сервера и находит нужный порт. Функция clnt_create также устанавливает соединение с сервером по протоколу TCP (поскольку мы указали TCP в качестве используемого протокола в листинге 16.2). Мы не показываем эти шаги на рисунке и откладываем детальное обсуждение до раздела 16.3. 2. Клиент вызывает локальную процедуру, называемую заглушкой клиента. В листинге 16.2 эта процедура называлась squareproc_1, а файл, содержащий ее, создавался rpcgen автоматически и получал название square_clnt.c. С точки зрения клиента именно эта функция является сервером, к которому он обращается. Целью создания заглушки является упаковка аргументов для удаленного вызова процедуры, помещение их в стандартный формат и создание одного или нескольких сетевых сообщений. Упаковка аргументов клиента в сетевое сообщение называется сортировкой (marshaling). Клиент и заглушка обычно вызывают библиотеки функций RPC (clnt_create в нашем примере). При использовании редактора связей в Solaris эти функции загружаются из библиотеки –lnsl, тогда как в BSD/OS они входят в стандартную библиотеку языка С. 3. Сетевые сообщения отсылаются на удаленную систему заглушкой клиента. Обычно это требует локального системного вызова (например, write или sendto). 4. Сетевые сообщения передаются на удаленную систему. Для этого обычно используются сетевые протоколы TCP и UDP. 5. Заглушка сервера ожидает запросов от клиента на стороне сервера. Она рассортировывает аргументы из сетевых сообщений. 6. Заглушка сервера осуществляет локальный вызов процедуры для запуска настоящей функции сервера (процедуры squareproc_l_svc в листинге 16.3), передавая ей аргументы, полученные в сетевых сообщениях от клиента. 7. После завершения процедуры сервера управление возвращается заглушке сервера, которой передаются все необходимые значения. 8. Заглушка сервера преобразовывает возвращаемые значения к нужному формату и рассортировывает их в сетевые сообщения для отправки обратно клиенту. 9. Сообщения передаются по сети обратно клиенту. 10. Заглушка клиента считывает сообщения из локального ядра (вызовом read или recvfrom). 11. После возможного преобразования возвращаемых значений заглушка клиента передает их функции клиента. Этот этап воспринимается клиентом как завершение работы процедуры. Рис. 16.2. Действия, происходящие при удаленном вызове процедуры ИсторияНаверное, одна из самых старых книг по RPC — это [26]. Как пишет [4], Уайт (White) затем перешел в Xerox и там создал несколько систем RPC. Одна из них была выпущена в качестве отдельного продукта в 1981 году под именем Courier. Классической книгой по RPC является [2]. В ней описаны средства RPC проекта Cedar, работавшего на однопользовательских рабочих станциях Dorado в фирме Xerox в начале 80-х. Xerox реализовал RPC на рабочих станциях еще до того, как большинство людей узнало о том, что рабочие станции существуют! Реализация Courier для Unix распространялась много лет с версиями BSD 4.x, но в настоящий момент эта система RPC представляет только исторический интерес. Sun выпустила первую версию пакета RPC в 1985. Она была разработана Бобом Лайоном (Bob Lyon), ушедшим в Sun из фирмы Xerox в 1983. Официально она называлась ONC/RPC: Open Network Computing Remote Procedure Call (удаленный вызов процедур в открытых вычислительных сетях), но обычно ее называют просто Sun RPC. Технически она аналогична Courier. Первые версии Sun RPC были написаны с использованием интерфейса сокетов и работали с протоколами TCP и UDP. Общедоступный исходный код вышел под названием RPCSRC. В начале 90-х он был переписан под интерфейс транспортного уровня TLI (предшественник XTI), который описан в четвертой части [24]. Теперь этот код работает со всеми протоколами, поддерживаемыми ядром. Общедоступный исходный код обеих версий можно найти по адресу ftp://playground.sun.com/pub/rpc, причем версия, использующая сокеты, называется rpcsrc, а версия, использующая TLI, называется tirpcsrc (название TI-RCP образовано от Transport Independent — транспортно-независимый удаленный вызов процедур). Стандарт RFC 1831 [18] содержит обзор средств Sun RPC и описывает формат сообщений RPC, передаваемых по сети. Стандарт RFC 1832 [19] содержит описание XDR — и поддерживаемых типов данных, и формата их передачи «по проводам». Стандарт RFC 1833 [20] описывает протоколы привязки: RPCBIND и его предшественника, программу отображения портов (port mapper). Одним из наиболее распространенных приложений, использующих Sun RPC, является сетевая файловая система Sun (NFS). Обычно она не создается стандартными средствами RPC, описанными в этой главе (rpcgen и библиотека времени выполнения). Вместо этого большинство подпрограмм библиотеки оптимизируются вручную и добавляются в ядро с целью ускорить их работу. Тем не менее большинство систем, поддерживающих NFS, также поддерживают и Sun RPC. В середине 80-х Apollo соперничала с Sun за рынок рабочих станций и создала свой собственный пакет RPC, призванный вытеснить Sun RPC. Этот новый пакет назывался NCA (Network Computing Architecture — архитектура сетевых вычислений). Протоколом RPC являлся протокол NCA/RPC, а аналогом XDR была схема NDR (Network Data Representation — сетевое представление данных). Интерфейсы между клиентами и серверами определялись с помощью языка NIDL (Network Interface Definition Language — язык определений сетевого интерфейса) аналогично нашему файлу .х из листинга 16.1. Библиотека времени выполнения называлась NCK (Network Computing Kernel). Фирма Apollo была куплена Hewlett-Packard в 1989, и NCA переросла в OSF's DCE (Distributed Computing Environment — среда распределенных вычислений), основной частью которой является RPC. Более подробно о DCE можно узнать по адресу http://www.camp.opengroup.org/tech/dce. Реализация пакета DCE RPC свободно доступна на ftp://gatekeeper.dec.com/pub/DEC/DCE. Этот каталог также содержит описание внутреннего устройства пакета DCE RPC на 171 странице. Существует много версий DCE для разных платформ.
В этой главе мы предполагаем наличие TI-RPC (независимая от протокола версия RPC) для большинства примеров и говорим только о протоколах TCP и UDP, хотя TI-RPC поддерживает все протоколы, какие только могут быть на данном узле. 16.2. МногопоточностьВспомните листинг 15.6, где мы продемонстрировали автоматическое управление потоками, осуществляемое библиотекой дверей. При этом сервер по умолчанию являлся параллельным. Покажем теперь, что средства Sun RPC по умолчанию делают сервер последовательным. Начнем с примера из предыдущего раздела и изменим только процедуру сервера. В листинге 16.4 приведен текст новой функции, выводящей идентификатор потока, делающей 5-секундную паузу, выводящей идентификатор еще раз и завершающей работу. Листинг 16.4. Процедура сервера с 5-секундной паузой//sunrpc/square2/server.c 1 #include "unpipc.h" 2 #include "square.h" 3 square_out * 4 squareproc_1_svc(square_in *inp, struct svc_req *rqstp) 5 { 6 static square_out out; 7 printf("thread %ld started, arg = %ld\n", 8 pr_thread_id(NULL), inp->arg1); 9 sleep(5); 10 out.res1 = inp->arg1 * inp->arg1; 11 printf("thread %ld done\n", pr_thread_id(NULL)); 12 return(&out); 13 } Запустим сервер, а после этого запустим три экземпляра программы-клиента: solaris % client localhost 22 & client localhost 33& client localhost 44 & [3] 25179 [4] 25180 [5] 25181 solaris % result: 484 примерно через 5 секунд после появления подсказки result: 1936 еще через 5 секунд result: 1089 еще через 5 секунд Хотя этого нельзя сказать по выводимому тексту, перед появлением очередного результата проходит примерно 5 секунд. Если мы посмотрим на текст, выводимый сервером, то увидим, что клиенты обрабатываются последовательно: сначала полностью обрабатывается запрос первого клиента, затем второго и третьего: solaris % server thread 1 started, arg = 22 thread 1 done thread 1 started, arg = 44 thread 1 done thread 1 started, arg = 33 thread 1 done Один и тот же поток обслуживает все запросы клиентов. Сервер не является многопоточным по умолчанию.
Возможность создания многопоточного сервера появилась в Solaris 2.4 и реализуется добавлением параметра –М в строку вызова rpcgen. Это делает код, создаваемый rpcgen, защищенным. Другой параметр, –А, позволяет автоматически создавать потоки по мере необходимости для обслуживания запросов клиентов. Мы включаем оба параметра при вызове rpcgen. Однако для реализации многопоточности требуется внести изменения в текст клиента и сервера, чего мы могли ожидать, поскольку использовали тип static в листинге 16.3. Единственное изменение, которое нужно внести в файл square.х, — сменить номер версии с 1 на 2. В объявлениях аргументов процедуры и результатов ничего не изменится. В листинге 16.5 приведен текст новой программы-клиента. Листинг 16.5. Функция main клиента многопоточного сервера//sunrpc/square3/client.c 1 #include "unpipc.h" 2 #include "square.h" 3 int 4 main(int argc, char **argv) 5 { 6 CLIENT *cl; 7 square_in in; 8 square_out out; 9 if (argc != 3) 10 err_quit("usage: client <hostname> <integer-value>"); 11 cl = Clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, "tcp"); 12 in.arg1 = atol(argv[2]); 13 if (squareproc_2(&in, &out, cl) != RPC_SUCCESS) 14 err_quit("%s", clnt_sperror(cl, argv[1])); 15 printf("result: %ld\n", out.res1); 16 exit(0); 17 }Объявление переменной для помещения результата 8 Мы объявляем переменную типа square_out, а не указатель на нее. Новый аргумент в вызове процедуры12-14 Вторым аргументом вызова squareproc_2 становится указатель на переменную out, а последним аргументом является дескриптор клиента. Вместо возвращения указателя на результат (как в листинге 16.2) эта функция будет возвращать либо RPC_SUCCESS, либо некоторое другое значение в случае возникновения ошибок. Перечисление enumclnt_stat в заголовочном файле <rpc/clnt_stat.h> содержит все возможные коды ошибок. В листинге 16.6 приведен текст новой процедуры сервера. Как и программа из листинга 16.4, эта версия выводит идентификатор потока, ждет 5 секунд, а затем завершает работу. Листинг 16.6. Процедура многопоточного сервера//sunrpc/square3/server.c 1 #include "unpipc.h" 2 #include "square.h" 3 bool_t 4 squareproc_2_svc(square_in *inp, square_out *outp, struct svc_req *rqstp) 5 { 6 printf("thread %Id started, arg = %ld\n", 7 pr_thread_id(NULL), inp->arg1); 8 sleep(5); 9 outp->res1 = inp->arg1 * inp->arg1; 10 printf("thread %ld done\n", pr_thread_id(NULL)); 11 return(TRUE); 12 } 13 int 14 square_prog_2_freeresult(SVCXPRT *transp, xdrproc_t xdr_result, 15 caddr_t result) 16 { 17 xdr_free(xdr_result, result); 18 return(1); 19 }Новые аргументы и возвращаемое значение 3-12 Требуемые для реализации многопоточности изменения включают изменение аргументов функций и возвращаемого значения. Вместо возвращения указателя на структуру результатов (как в листинге 16.3) указатель на эту структуру принимается в качестве второго аргумента функции. Указатель на структуру svc_req смещается на третью позицию. Теперь при успешном завершении функции возвращается значение TRUE, а при возникновении ошибок — FALSE. Новая функция, освобождающая память XDR13-19 Еще одно изменение заключается в добавлении функции, освобождающей все автоматически выделенные переменные. Эта функция вызывается из заглушки сервера после завершения работы процедуры сервера и отправки результата клиенту. В нашем примере просто делается вызов подпрограммы xdr_free (о ней будет говориться более подробно в связи с листингом 16.19 и упражнением 16.10). Если процедура сервера выделяла память под сохраняемый результат (например, в виде связного списка), этот вызов освободит занятую память. Создадим программу-клиент и программу-сервер и запустим три экземпляра клиента одновременно: solaris % client localhost 55 & client localhost 66 & client localhost 77 & [3] 25427 [4] 25428 [5] 25429 solaris % result: 4356 result: 3025 result: 5929 На этот раз мы видим, что результаты выводятся одновременно, один за другим. Взглянув на выводимый сервером текст, отметим, что используются три серверных потока и все они выполняются одновременно: solaris % server thread 1 started, arg = 55 thread 4 started, arg = 77 thread 6 started, arg = 66 thread 6 done thread 1 done thread 4 done
16.3. Привязка сервераВ описании листинга 16.5 мы достаточно бегло прошлись по действиям, выполняемым на нулевом этапе: регистрация сервера в локальной программе отображения портов и определение клиентом адреса порта не были разобраны детально. Отметим прежде всего, что на любом узле с сервером RPC должна выполняться программа port mapper (отображение портов). Этой программе присваивается адрес порта TCP 111 и UDP 111, и это единственные фиксированные значения портов Интернета для Sun RPC. Серверы RPC всегда связываются с временным портом, а затем регистрируют его в локальной службе отображения портов. После запуска клиент должен связаться с программой отображения портов, узнать номер временного порта сервера, а затем связаться с самим сервером через этот порт. Программа отображения портов предоставляет также службу имен, область действия которой ограничена системой.
Сервер и клиент работают следующим образом: 1. При переходе системы в многопользовательский режим запускается пpoгрaммa отображения портов. Исполняемый файл этого демона обычно называется portmap или rpcbind. 2. При запуске сервера его функция main, являющаяся частью заглушки сервера, создаваемой rpcgen, вызывает библиотечную функцию svc_create. Эта функция выясняет, какие сетевые протоколы поддерживаются узлом, и создает конечную точку (например, сокет) для каждого протокола, связывая временные порты с конечными точками протоколов TCP и UDP. Затем она связывается с локальной пpoгрaммoй отображения портов для регистрации временных номеров портов TCP и UDP вместе с номером программы и номером версии. Сама программа отображения портов также представляет собой программу RPC, и сервер регистрируется с помощью вызовов RPC (обращенных к известному порту 111). Описание процедур, поддерживаемых пpoгрaммoй отображения портов, дается в стандарте RFC 1833 [20]. Существуют три версии этой программы RPC: вторая версия работает только с портами TCP и UDP, а версии 3и 4 представляют собой новые версии, работающие по протоколу RPCBIND. Можно получить список всех пpoгрaмм RPC, зарегистрированных в пpoгрaм-ме отображения портов, запустив пpoгрaммy rpcinfo. Мы можем запустить эту программу, чтобы убедиться, что порт с номером 111 используется самой программой отображения портов: solaris % rpcinfo –p program vers proto port service 100000 4 tcp 111 rpcbind 100000 3 tcp 111 rpcbind 100000 2 tcp 111 rpcbind 100000 4 udp 111 rpcbind 100000 3 udp 111 rpcbind 100000 2 udp 111 rpcbind (Мы исключили множество несущественных в данный момент строк вывода.) Мы видим, что Solaris 2.6 поддерживает все три версии протокола, все на порте 111, причем как TCP, так и UDP. Соответствие номеров пpoгрaмм RPC их именам обычно устанавливается в файле /etc/rpc. Запустив ту же программу в BSD/OS 3.1, увидим, что в этой системе поддерживается только вторая версия программы отображения портов: bsdi % rpcinfo –p program vers proto port 100000 2 tcp 111 portmapper 100000 2 udp 111 portmapper В Digital Unix 4.0В также поддерживается только вторая версия: alpha % rpcinfo –p program vers proto port 100000 2 tcp 111 portmapper 100000 2 udp 111 portmapper Затем процесс сервера приостанавливает работу, ожидая поступления запросов от клиентов. Это может быть новое соединение TCP или приход дей-тaгрaммы UDP в порт UDP. Если мы запустим rpcinfo после запуска сервера из листинга 16.3, мы увидим следующий результат: solaris % rpcinfo –p program vers proto port service 8243773444 1 udp 8243773444 1 tcp где 824377344 соответствует 0x31230000 (номер пpoгрaммы, присвоенный ей в листинге 16.1). В том же листинге мы присвоили программе номер версии 1. Обратите внимание, что сервер готов принимать запросы от клиентов по протоколам TCP и UDP и клиент может выбирать, какой из этих протоколов он будет использовать при создании дескриптора клиента (последний аргумент clnt_create в листинге 16.2). 3. Клиент запускается и вызывает clnt_create. Аргументами (листинг 16.2) являются имя узла или IP-адрес сервера, номер пpoгрaммы, номер версии и строка, указывающая протокол связи. Запрос RPC направляется пpoгрaммe отображения портов узла сервера (для этого сообщения обычно используется протокол UDP), причем запрашивается информация об указанной версии указанной программы с указанным протоколом. В случае успеха номер порта сохраняется в дескрипторе клиента для обработки всех последующих вызовов RPC через этот дескриптор. В листинге 16.1 мы присвоили нашей программе номер 0x31230000. 32-разрядные номера пpoгрaмм подразделяются на группы, приведенные в табл. 16.1. Таблица 16.1. Диапазоны номеров программ для Sun RPC
Пpoгрaммa rpcinfо выводит список программ, зарегистрированных в системе. Другим источником информации о пpoгрaммax RPC могут являться файлы с расширением .х в каталоге /usr/inciude/rpcsvc. Inetd и серверы RPCПо умолчанию серверы, созданные с помощью rpcgen, могут вызываться сервером верхнего уровня inetd. Этот сервер описывается в разделе 12.5 [24]. Изучение содержимого заглушки сервера, создаваемой rpcgen, показывает, что при запуске функции main сервера она проверяет, является ли стандартный поток ввода конечной точкой XTI, и если так, то предполагается, что сервер был запущен демоном inetd. После создания сервера RPC, который будет вызываться inetd, следует добавить информацию об этом сервере в файл /etc/inetd.conf. Туда помещаются следующие данные: имя программы RPC, поддерживаемые номера программ, протоколы и полное имя исполняемого файла сервера. В качестве примера мы приводим строку из конфигурационного файла Solaris: rstatd/2-4 tli rpc/datagram_v wait root /usr/lib/netsvc/rstat/rpc.rstatd rpc.rstatd Первое поле содержит имя пpoгрaммы (которому будет сопоставлен номер с помощью файла /etc/rpc); поддерживаются версии 2, 3 и 4. Следующее поле задает конечную точку XTI (или сокет), третье поле говорит о том, что поддерживаются все протоколы видимых дeйтaгрaмм. Если свериться с содержимым файла /etc/netconfig, мы узнаем, что таких протоколов два: UDP и /dev/clts. Глава 29 [24] описывает этот файл и адреса XTI. Четвертое поле (wait) указывает демону inetd на необходимость ожидания завершения этого сервера перед включением режима ожидания запроса клиента для конечной точки XTI. Все серверы RPC указывают атрибут wait в конфигурационном файле /etc/inetd.conf. Следующее поле, root, указывает идентификатор пользователя, с которым будет выполняться пpoгрaммa. Последние два поля задают полное имя исполняемого файла пpoгрaммы и имя пpoгрaммы вместе с необходимыми аргументами командной строки (у данной пpoгрaммы они отсутствуют). Демон inetd создаст конечные точки XTI и зарегистрирует их в программе отображения портов для соответствующих номеров пpoгрaмм и версий. Мы можем убедиться в этом с помощью rpcinfo: solaris % rpcinfo | grep statd 100001 2 udp 0.0.0.0.128.11 rstatd superuser 100001 3udp 0.0.0.0.128.11 rstatd superuser 100001 4udp 0.0.0.0.128.11 rstatd superuser 100001 2ticlts \000\000\020. rstatd superuser 100001 3ticlts \000\000\020. rstatd superuser 100001 4ticlts \000\000\020. rstatd superuser Четвертое поле содержит адреса XTI, причем 128×256+11 = 32779, и данное значение является временным номером порта, присвоенным этой концевой точке UDP. Когда дейтаграмма UDP поступает в порт 32779, демон inetd обнаруживает готовность этой дейтаграммы к обработке и вызывает fork, а затем exec для запуска программы /usr/lib/netsvc/rstat/rpc.rstatd. Перед вызовами fork и exec концевая точка XTI будет скопирована в дескрипторы 0, 1 и 2, а все прочие дескрипторы inetd будут закрыты (рис. 12.7 [24]). Демон inetd также прекратит слушать эту конечную точку XTI, не реагируя на запросы пользователей до тех пор, пока сервер (дочерний процесс по отношению к inetd) не завершит работу. Это поведение определяется атрибутом wait. Предположим, что эта программа была создана с помощью rpcgen. Тогда она сможет распознать конечную точку XTI, подключенную к стандартному потоку ввода, и инициализировать ее как конечную точку сервера RPC. Это осуществляется вызовом функций RPC svc_tli_create и svc_reg, которые в данной книге не рассматриваются. Вторая функция (вопреки названию) не регистрирует сервер в программе отображения портов — это делается лишь однажды, при запуске сервера. Функция svc_run прочитает пришедшую дейтаграмму и вызовет соответствующую процедуру сервера для обработки запроса клиента. В обычной ситуации серверы, запускаемые демоном inetd, обрабатывают один запрос клиента и завершают работу, после чего inetd переходит в режим ожидания следующего запроса. Для оптимизации работы системы серверы RPC, созданные rpcgen, ждут поступления нового запроса от клиента в течение некоторого времени (по умолчанию 2 минуты). В этом случае дейтаграмма обрабатывается уже запущенным сервером. Это исключает накладные расходы на вызов fork и exec при поступлении нескольких клиентских запросов подряд. По истечении периода ожидания сервер завершает работу, а демону inetd отсылается сигнал SIGCHLD, после чего он переходит в режим ожидания дейтаграмм по XTI. 16.4. АутентификацияПо умолчанию в запросе RPC не содержится информации о клиенте. Сервер отвечает на запрос клиента, не беспокоясь о том, что это за клиент. Это называется нулевой аутентификацией, или AUTH_NONE. Следующий уровень проверки подлинности называется аутентификацией Unix, или AUTH_SYS. Клиент должен сообщить библиотеке RPC времени выполнения информацию о себе (имя узла, действующий идентификатор пользователя, действующий идентификатор группы, дополнительные идентификаторы группы) для включения в каждый запрос. Изменим программу из листинга 16.2 таким образом, чтобы она включала возможность осуществления аутентификации Unix. В листинге 16.7 приведен новый текст программы-клиента. Листинг 16.7. Клиент, осуществляющий аутентификацию unix//sunrpc/square4/client.с 1 #include "unpipc.h" 2 #include "square.h" 3 int 4 main(int argc, char **argv) 5 { 6 CLIENT *cl; 7 square_in in; 8 square_out out; 9 if (argc != 3) 10 err_quit("usage: client <hostname> <integer-value>"); 11 cl = Clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, "tcp"); 12 auth_destroy(cl->cl_auth); 13 cl->cl_auth = authsys_create_default(); 14 in.arg1 = atol(argv[2]); 15 if (squareproc_2(&in, &out, cl) != RPC_SUCCESS) 16 err_quit("%s", clnt_sperror(cl, argv[1])); 17 printf("result: %ld\n", out.resl); 18 exit(0); 19 } 12-13 Эти строки были добавлены в данной версии программы. Сначала мы вызываем auth_destroy для удаления предыдущей аутентификационной информации, связанной с данным дескриптором клиента (то есть дескриптор нулевой аутентификации, создаваемый по умолчанию). Затем вызов authsys_create_default создает соответствующую аутентификационную структуру Unix и мы сохраняем ее в поле cl_auth структуры CLIENT. Оставшаяся часть клиента не претерпела изменений по сравнению с листингом 16.5. В листинге 16.8 приведен текст процедуры сервера, измененный по сравнению с листингом 16.6. Мы не приводим текст процедуры square_prog_2_freeresult, которая не меняется. Листинг 16.8. Процедура сервера, запрашивающая аутентификацию Unix//sunrpc/square4/server.c 1 #include "unpipc.h" 2 #include "square.h" 3 bool_t 4 squareproc_2_svc(square_in *inp, square_out *outp, struct svc_req *rqstp) 5 { 6 printf("thread %Id started, arg = %ld, auth = %d\n", 7 pr_thread_id(NULL), inp->arg1, rqstp->rq_cred.oa_flavor); 8 if (rqstp->rq_cred.oa_flavor == AUTH_SYS) { 9 struct authsys_parms *au; 10 au = (struct authsys_parms *)rqstp->rq_clntcred; 11 printf("AUTH_SYS: host %s, uid %ld, gid %ld\n", 12 au->aup_machname, (long) au->aup_uid, (long) au->aup_gid); 13 } 14 sleep(5); 15 outp->res1 = inp->arg1 * inp->arg1; 16 printf("thread %ld done\n", pr_thread_id(NULL)); 17 return(TRUE); 18 } 6-8 Теперь мы используем указатель на структуру svc_req, которая всегда передается в качестве одного из аргументов процедуры сервера: struct svc_req { u_long rq_prog; /* номер программы */ u_long rq_vers; /* номер версии */ u_long rq_proc; /* номер процедуры */ struct opaque_auth rq_cred:/* данные о клиенте */ caddr_t rq_clntcred; /* готовые данные (только для чтения) */ SVCXPRT *rq_xprt; /* транспортный дескриптор */ }; struct opaque_auth { enum_t oa_flavor; /* flavor: константа AUTH_xxx */ caddr_t oa_base; /* адрес дополнительной аутентификационной информации */ u_int oa_length; /* не должно превосходить MAX_AUTH_BYTES */ }; Поле rq_cred содержит неформатированную информацию о клиенте, а его поле oa_flavor содержит целое число, определяющее тип аутентификации. Термин «неформатированная» означает, что библиотека не обработала информацию, на которую указывает oa_base. Но если тип идентификации относится к одному из поддерживаемых библиотекой, то в готовой информации о клиенте, на которую указывает rq_clntcred, содержится некоторая структура, соответствующая данному типу аутентификации. Программа выводит тип аутентификации и прове-9_12 ряет, соответствует ли он AUTH_SYS. Для аутентификации Unix указатель на готовую информацию (rq_clntcred) указывает на структуру authsys_parms, содержащую информацию о клиенте: struct authsys_parms { u_long aup_time; /* время создания информации */ char *aup_machname; /* имя узла клиента */ uid_t aup_uid; /* действующий идентификатор пользователя */ gid_t aup_gid; /* действующий идентификатор группы */ u_int aup_len; /* количество элементов в aup_gids[] */ gid_t *aup_gidsl; /* дополнительные идентификаторы группы */ }; Мы получаем указатель на эту структуру и выводим имя узла клиента, его EUID и EGID. Запустив сервер и один экземпляр клиента, посмотрим на выводимый сервером текст: solaris % server thread 1 started, arg = 44, auth = 1 AUTH_SYS: host solaris.kohala.com, uid 765, gid 870 thread 1 done Аутентификация Unix используется редко, поскольку ее легко обойти. Мы можем легко построить собственные пакеты RPC, содержащие аутентификационную информацию в формате Unix, присвоив идентификатору пользователя и группы произвольные значения, и отправить их на сервер. Сервер никак не может проверить, те ли мы, кем представляемся.
Пакеты RPC — как запросы, так и ответы — содержат два поля, относящиеся к аутентификации: данные о пользователе и проверочную информацию (credentials, verifier). Примером такой структуры является документ с фотографией (паспорт, права и т. п.). Данные о пользователе соответствуют написанному в паспорте тексту (имя, адрес, дата рождения и т. п.), а проверочная информация — это фотография. Существуют разные формы проверочной информации: фотография в данном случае полезнее, чем, например, рост, вес и пол. Если документ не содержит проверочной информации (как, например, читательский билет в библиотеке), любой может воспользоваться им и сказать, что он его владелец. В случае нулевой аутентификации пакеты не содержат ни данных о пользователе, ни проверочной информации. В режиме аутентификации Unix данные о пользователе содержат имя узла, идентификаторы пользователя и группы, но поле проверочной информации пусто. Поддерживаются, однако, и другие формы аутентификации, для которых эти два поля содержат другую информацию. ■ AUTH_SHORT — альтернативная форма аутентификации Unix, отправляемая сервером в поле verifier в ответ на запрос клиента. Она содержит меньшее количество информации, чем в режиме аутентификации Unix, и клиент может отсылать ее серверу при последующих запросах. Используется для уменьшения количества передаваемой по сети информации. ■ AUTH_DES — аббревиатура DES означает Data Encryption Standard (стандарт шифрования данных). Эта форма аутентификации основана на использовании криптографии с секретным и открытым ключом. Эта схема также называется защищенным RPC (secure RPC), а если она используется в качестве основы для построения NFS, то такая NFS также называется защищенной. ■ AUTH_KERB — эта схема основана на стандарте Kerberos института MIT. В главе 19 книги [5] подробно рассказывается о двух последних формах аутентификации, включая их настройку и использование. 16.5. Тайм-аут и повторная передачаРассмотрим стратегию обработки тайм-аутов и повторной передачи, используемую в средствах Sun RPC. Существуют два значения тайм-аутов: 1. Общий тайм-аут определяет время ожидания ответа сервера клиентом. Это значение используется протоколами TCP и UDP. 2. Тайм-аут повтора используется только UDP и определяет время ожидания между повторами запросов клиента, если ответ от сервера не приходит. Для протокола TCP необходимость во введении тайм-аута повтора отсутствует, поскольку этот протокол является надежным. Если сервер Не получает запроса от клиента, время ожидания по протоколу TCP со стороны клиента закончится и клиент повторит передачу. Когда сервер получает запрос клиента, он уведомляет об этом последний. Если уведомление о получении будет утрачено по пути к клиенту, тот должен будет еще раз переслать запрос. Повторные запросы сбрасываются сервером, но уведомления об их получении отсылаются клиенту. В надежных протоколах правильность доставки (время ожидания, повторная передача, обработка лишних копий данных и лишних уведомлений) обеспечивается на транспортном уровне и не входит в задачи библиотеки RPC. Один запрос, отправленный клиентом на уровне RPC, будет получен сервером ровно в одном экземпляре на уровне RPC. В противном случае клиент RPC получит сообщение о невозможности связаться с сервером. При этом совершенно не важно, что происходит на сетевом и транспортном уровнях. После создания дескриптора клиента можно использовать функцию clnt_control для получения информации и изменения свойств клиента. Эта функция работает аналогично fcntl для дескрипторов файлов или getsockopt и setsockopt для сокетов: #include <rpc/rpc.h> bool_t clnt_control(CLIENT *cl, unsigned int request, char *ptr); /* Возвращает TRUE в случае успешного завершения, FALSE – в случае ошибки */ Здесь cl представляет собой дескриптор клиента, а на что указывает ptr — зависит от значения request. Изменим программу-клиент из листинга 16.2, добавив в нее вызов данной функции, и выведем значения тайм-аутов. В листинге 16.9 приведен текст новой программы-клиента. Листинг 16.9. Клиент, получающий и печатающий значения времени ожидания RPC//sunrpc/square5/client.c 1 #include "unpipc.h" 2 #include "square.h" 3 int 4 main(int argc, char **argv) 5 { 6 CLIENT *cl; 7 square_in in; 8 square_out *outp; 9 struct timeval tv; 10 if (argc != 4) 11 err_quit("usage: client <hostname> <integer-value> <protocol>"); 12 cl = Clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, argv[3]); 13 Clnt_control(cl, CLGET_TIMEOUT, (char*)&tv); 14 printf("timeout = %ld sec, %ld usec\n", tv.tv_sec, tv.tv_usec); 15 if (clnt_control(cl, CLGET_RETRY_TIMEOUT, (char *) &tv) == TRUE) 16 printf("retry timeout = %ld sec, %ld usec\n", tv.tv_sec, tv.tv_usec); 17 in.arg1 = atol(argv[2]); 18 if ((outp = squareproc_1(&in, cl)) == NULL) 19 err_quit("%s", clnt_sperror(cl, argv[1])); 20 printf(result: %ld\n", outp->res1); 21 exit(0); 22 }Используемый протокол является аргументом командной строки 10-12 Теперь протокол, являющийся последним аргументом clnt_create, указывается в качестве нового параметра командной строки. Получение значения общего тайм-аута13-14 Первым аргументом clnt_control является дескриптор клиента, вторым — тип запроса, а третьим — указатель на буфер. Наш первый запрос имеет значение CLGET_TIMEOUT; при этом возвращается значение общего тайм-аута в структуре timeval, адрес которой передается третьим аргументом. Этот запрос корректен для всех протоколов. Попытка получения тайм-аута повтора15-16 Следующий запрос имеет значение CLGET_RETRY_TIMEOUT. При этом должно возвращаться значение тайм-аута повтора, но этот запрос корректен только для протокола UDP. Следовательно, если функция возвращает значение FALSE, мы ничего не печатаем. Изменим также и программу-сервер, добавив в нее ожидание продолжительностью 1000 секунд вместо 5, чтобы гарантировать получение тайм-аута по запросу клиента. Запустим сервер на узле bsdi, а клиент запустим дважды, один раз указав в качестве протокола TCP, а другой — UDP. Результат будет не таким, как мы ожидали: solaris % date ; client bsdi 44 tcp ; date Wed Apr 22 14:46:57 MST 1998 timeout = 30 sec, 0 usec тайм-аут 30 секунд bsdi: RPC: Timed out Wed Apr 22 14:47:22 MST 1998 но прошло только 25 секунд solaris % date ; client bsdi 55 udp ; date Wed Apr 22 14:48:05 MST 1998 timeout = –1 sec, –1 usec ерунда какая-то retry timeout = 15 sec, 0 usec это значение кажется правильным bsdi: RPC: Timed out Wed Apr 22 14:48:31 MST 1998 около 25 секунд спустя В случае с протоколом TCP значение тайм-аута, возвращенное clnt_control, было 30 секунд, но библиотека возвратила ошибку через 25 секунд. Для протокола UDP было получено значение общего тайм-аута –1. Чтобы понять, что тут происходит, изучим текст заглушки клиента — функции squareproc_1 в файле square_clnt.c, созданном rpcgen. Эта функция вызывает библиотечную функцию с именем clnt_call, причем последним аргументом является структура типа timeval с именем TIMEOUT, объявляемая в этом файле, и инициализируется она значением 25 секунд. Этот аргумент clnt_call отменяет значение общего тайм-аута в 30 секунд для TCP и –1 для UDP. Он используется всегда, если клиент не устанавливает общий тайм-аут явно вызовом clnt_control с запросом CLSET_TIMEOUT. Если мы хотим изменить значение общего тайм-аута, следует вызывать clnt_control, а не изменять содержимое заглушки клиента.
Управление соединением по TCPЕсли мы будем наблюдать с помощью tcpdump за работой клиента и сервера из предыдущего примера, связывающихся по протоколу TCP, мы увидим, что сначала происходит установка соединения (трехэтапное рукопожатие TCP), затем отправляется запрос клиента и сервер отсылает уведомление о приеме этого запроса. Через 25 секунд после этого клиент отсылает серверу FIN, что вызвано завершением работы клиента, после чего следуют оставшиеся три этапа завершения соединения по TCP. В разделе 2.5 [24] эти этапы описаны подробно. Мы хотим показать, что Sun RPC использует соединение по TCP следующим образом: новое соединение по TCP устанавливается при вызове clnt_create и оно используется для всех вызовов процедур, связанных с указанной программой и версией. Соединение по TCP завершается явно вызовом clnt_destroy или неявно по завершении процесса клиента: #include <rpc/rpc.h> void clnt_destroy(CLIENT *cl); Начнем с клиента из листинга 16.2 и изменим его, добавив второй вызов процедуры сервера, вызовы clnt_destroy и pause. В листинге 16.10 приведен текст новой программы-клиента. Листинг 16.10. Клиент для изучения свойств соединения по TCP//sunrpc/square9/client.c 1 #include "unpipc.h" /* наш заголовочный файл*/ 2 #include "square.h" /* создается rpcgen */ 3 int 4 main(int argc, char **argv) 5 { 6 CLIENT, *cl; 7 square_in in; 8 square_out *outp; 9 if (argc != 3) 10 err_quit("usage: client <hostname> <integer-value>"); 11 cl = Clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, "tcp"); 12 in.arg1 = atol(argv[2]); 13 if ((outp = squareproc_1(&in, cl)) == NULL) 14 err_quit("%s", clnt_sperror(c1, argv[1])); 15 printf("result: %ld\n", outp->res1); 16 in.arg1 *= 2; 17 if ((outp = squareproc_1(&in, cl)) == NULL) 18 err_quit("%s", clnt_sperror(cl, argv[1])); 19 printf("result: %ld\n", outp->res1); 20 clnt_destroy(cl); 21 pause(); 22 exit(0); 23 } После запуска получим ожидаемый результат: solaris % client kalae 5 result: 25 result: 100 программа в состоянии ожидания, пока мы не завершим ее вручную Однако проверить наши предыдущие утверждения можно лишь с помощью результатов работы программы tcpdump. Она показывает, что создается одно соединение по TCP (вызовом clnt_create) и оно используется для обоих запросов клиента. Соединение завершается вызовом clnt_destroy, хотя клиент при этом и не завершает свою работу. Идентификатор транзакцийДругая часть стратегии тайм-аутов и повторных передач заключается в использовании идентификаторов транзакций (transaction ID или XID) для распознавания запросов клиента и ответов сервера. Когда клиент вызывает функцию RPC, библиотека присваивает этому вызову 32-разрядный целочисленный номер и это значение отсылается в запросе RPC. Сервер должен добавить к своему ответу этот номер. При повторной отсылке запроса идентификатор не меняется. Служит он двум целям: 1. Клиент проверяет, что XID ответа совпадает с XID запроса. Если совпадения нет, ответ игнорируется. Если используется протокол TCP, у клиента практически нет шансов получить ответ с неправильным идентификатором, но при использовании протокола UDP поверх плохой сети вероятность получения неправильного XID достаточно высока. 2. Серверу разрешается помещать отсылаемые ответы в кэш, и для проверки идентичности ответов используется, в частности, именно XID. Об этом мы вскоре расскажем. Пакет TI-RPC использует определенный алгоритм вычисления XID для нового запроса. Алгоритм этот описан ниже. Значок ^ означает побитовую операцию XOR (исключающее ИЛИ): struct timeval now; gettimeofday(&now, NULL); xid = getpid() ^ now.tv_sec ^ now.tv_usec; Кэш повторных ответовДля включения поддержки кэша повторных ответов в библиотеке RPC сервер должен вызвать функцию svc_dg_enablecache. После включения кэша выключить его нельзя, можно только запустить процесс заново: #include <rpc/rpc.h> int svc_dg_enablecache(SVCXPRT *xprt, unsigned long size); /* Возвращает 1 в случае успешного завершения. 0 – в случае ошибки */ Здесь xprt представляет собой транспортный дескриптор, являющийся полем структуры svc_req (раздел 16.4). Адрес этой структуры является аргументом процедуры сервера. Размер определяет количество записей в выделяемом кэше. Итак, эта функция включает поддержку кэширования всех отсылаемых ответов в очереди размером size записей. Каждый ответ однозначно определяется следующими параметрами: ■ номером программы; ■ номером версии; ■ номером процедуры; ■ XID; ■ адресом клиента (IP-адрес + порт UDP). При получении запроса клиента библиотека RPC ищет в кэше ответ на такой запрос. В случае его наличия ответ отсылается клиенту без повторного вызова процедуры сервера. Цель использования кэша повторных ответов состоит в том, чтобы не нужно было вызывать процедуру сервера несколько раз при получении нескольких копий запроса клиента. Это может быть нужно в случае, если процедура неидемпотентна. Повторный запрос может быть получен из-за того, что ответ был утерян или у клиента время ожидания меньше, чем время передачи ответа по сети. Обратите внимание, что этот кэш действует только для протоколов, работающих с дейтаграммами (таких, как UDP), поскольку при использовании TCP повторный запрос никогда не может быть получен приложением — он будет обработан TCP (см. упражнение 16.6). 16.6. Семантика вызововВ листинге 15.24 мы привели пример клиента интерфейса дверей, повторно отсылавшего запрос на сервер при прерывании вызова door_call перехватываемым сигналом. Затем мы показали, что при этом процедура сервера вызывается дважды, а не однократно. Потом мы разделили процедуры сервера на две группы: идемпотентные, которые могут быть вызваны произвольное количество раз без возникновения ошибок, и неидемпотентные, наподобие вычитания определенной суммы из банковского счета. Вызовы процедур могут быть разбиты на три группы: 1. «Ровно один раз» означает, что процедура была выполнена только один раз. Такого трудно достичь ввиду ненулевой вероятности сбоев в работе сервера. 2. «Не более одного раза» означает, что процедура вовсе не была выполнена или что она была выполнена один раз. Если вызвавшему процессу возвращается результат, мы знаем, что процедура была выполнена. Если процессу возвращается сообщение об ошибке, мы не знаем, была ли процедура выполнена хотя бы один раз или не была выполнена вовсе. 3. «По крайней мере один раз» означает, что процедура была выполнена один раз, а возможно, и больше. Это не вызывает проблем для идемпотентных процедур — клиент продолжает передавать запросы до тех пор, пока не получит правильный ответ. Однако если клиент отправит несколько запросов, существует вероятность, что процедура будет выполнена больше одного раза. При возвращении из локальной процедуры мы можем быть уверены, что она была выполнена ровно один раз. Однако если процесс завершает работу после вызова процедуры, мы не знаем, успела она выполниться или нет. Для удаленных вызовов процедур возможно несколько ситуаций. ■ Если используется протокол TCP и получен ответ, мы можем быть уверены, что удаленная процедура была вызвана ровно один раз. Однако если ответ не был получен (сервер вышел из строя), мы уже не можем сказать, была процедура выполнена или нет. Обеспечение семантики «ровно один раз» при учете возможности досрочного завершения работы сервера и неполадок в сети требует системы обработки транзакций, что лежит за границами возможностей RPC. ■ Если используется UDP без серверного кэша и был получен ответ, мы можем быть уверены, что процедура была вызвана по крайней мере один раз, но возможно, и несколько. ■ Если используется UDP с серверным кэшем и был получен ответ, мы можем быть уверены, что процедура была вызвана ровно один раз. Однако если ответ не был получен, мы оказывается в ситуации «не более одного раза» аналогично сценарию с TCP. ■ Если вы стоите перед выбором: □ TCP, □ UDP с кэшем повторных ответов, □ UDP без кэша повторных ответов — мы можем порекомендовать следующее: □ всегда используйте TCP, если только для приложения не важны накладные расходы на обеспечение надежности; □ используйте систему обработки транзакций для неидемпотентных процедур, корректное выполнение которых важно (работа с банковскими счетами, бронирование авиабилетов и т. п.); □ для неидемпотентных процедур использование TCP предпочтительно по сравнению с UDP и кэшем, поскольку TCP был изначально ориентирован на надежность, а добавление кэша к приложению, использующему UDP, вряд ли даст то же самое, что и использование TCP (см., например, раздел 20.5 [24]); □ для идемпотентных процедур можно использовать UDP без кэша; □ для неидемпотентных процедур использование UDP без кэша опасно. В следующем разделе будут рассмотрены дополнительные преимущества использования TCP. 16.7. Досрочное завершение сервера или клиентаРассмотрим, что произойдет в случае досрочного завершения клиента или сервера при использовании транспортного протокола TCP. Поскольку протокол UDP не подразумевает установку соединения, при завершении процесса его собеседнику не отсылается никаких сообщений. При завершении работы одного из процессов второй дождется тайм-аута, после чего, возможно, повторно отошлет запрос и наконец прекратит попытки, выдав сообщение об ошибке, как показывалось в предыдущем разделе. При завершении работы процесса, установившего соединение по TCP, это соединение завершается отправкой пакета FIN [24, с. 36-37], и мы хотим узнать, что делает библиотека RPC при получении этого пакета. Досрочное завершение сервераЗавершим работу сервера досрочно, в процессе обработки запроса клиента. Единственное изменение в программе-клиенте будет заключаться в удалении аргумента tcp из вызова clnt_call в листинге 16.2 и включении протокола в набор аргументов командной строки, как в листинге 16.9. В процедуру сервера мы добавим вызов abort. Это приведет к завершению работы процесса-сервера и отправке пакета FIN клиенту, что мы можем проверить с помощью tcpdump. Запустим в системе Solaris клиент для сервера, работающего под BSD/OS: solaris % client bsdi 22 tcp bsdi: RPC: Unable to receive; An event requires attention В момент получения клиентом пакета FIN библиотека RPC находилась в состоянии ожидания ответа сервера. Она получила неожиданный ответ и вернула ошибку в вызове squareproc_1. Ошибка (RPC_CANTRECV) сохраняется библиотекой в дескрипторе клиента, и вызов clnt_sperror (из функции-обертки Clnt_create) при этом печатает сообщение Unable to receive. Оставшаяся часть сообщения об ошибке (An event requires attention) соответствует ошибке XTI, сохраненной библиотекой, которая также выводится clnt_sperror. Вызов удаленной процедуры может вернуть одну из примерно 30 различных ошибок RPC_xxx. Все они перечислены в заголовочном файле <rpc/clnt_stat.h>. Если мы поменяем клиент и сервер местами, мы увидим то же сообщение об ошибке, возвращаемое библиотекой RPC (RPC_CANTRECV), но при этом будет выведено дополнительное сообщение: bsdi % client solaris 11 tcp solaris: RPC: Unable to receive; errno = Connection reset by peer Сервер в Solaris не был скомпилирован как многопоточный, и когда мы вызвали abort, была завершена работа всего процесса. Если мы запустим многопоточный сервер и завершим работу только одного потока — того, который обслуживает данный запрос клиента, — все изменится. Чтобы продемонстрировать это, заменим вызов abort на pthread_exit, как мы сделали в пpoгрaммe из листинга 15.20. Запустим клиент в BSD/OS, а многопоточный сервер — в Solaris: bsdi % client solaris 33 tcp solaris: RPC: Timed out После завершения работы потока сервера соединение с клиентом по TCP не разрывается. Оно остается открытым, поэтому клиенту не отсылается пакет FIN. Клиент выходит по тайм-ауту. Мы увидели бы то же сообщение об ошибке, если бы узел, на котором находится сервер, прекратил работу после получения запроса от клиента и отправки уведомления. Досрочное завершение клиентаЕсли клиент, использующий TCP, завершает работу в процессе выполнения процедуры RPC, серверу отправляется пакет FIN. Мы хотим узнать, как библиотека сервера реагирует на этот пакет и уведомляет об этом процедуру сервера. (В разделе 15.11 мы говорили, что поток сервера дверей отменяется при досрочном завершении клиента.) Чтобы сымитировать такую ситуацию, клиент вызывает alarm(3) непосредственно перед вызовом процедуры сервера, а процедура сервера вызывает slеер (6). Так же мы поступили и в нашем примере с дверьми в листингах 15.25 и 15.26. Поскольку клиент не перехватывает сигнал SIGALRM, процесс завершается ядром примерно за 3 секунды до отправки ответа серверу. Запустим клиент в BSD/OS, а сервер в Solaris: bsdi % client solaris 44 tcp Alarm call Случилось то, что мы и ожидали. А вот на сервере не происходит ничего необычного. Процедура сервера благополучно заканчивает 6-секундную паузу и возвращается. Если мы взглянем на передаваемую по сети информацию с помощью tcpdump, мы увидим следующее: ■ при завершении работы клиента (через 3 секунды после запуска) серверу отправляется пакет FIN, и сервер высылает уведомление о его приеме. В TCP для этого используется термин half-close (наполовину закрытое соединение, раздел 18.5 [22]); ■ через 6 секунд после запуска клиента сервер отсылает ответ, который переправляется клиенту протоколом TCP. По соединению TCP можно отправлять данные после получения FIN, поскольку соединения TCP являются двусторонними, о чем говорится в книге [24, с. 130-132]. Клиент отвечает пакетом RST, поскольку он уже завершил работу. Он будет получен сервером при следующем открытии этого соединения, но это ни к чему не приведет. Подведем итоги. ■ При использовании UDP клиенты и серверы RPC не имеют возможности узнать о досрочном завершении одного из них. Они могут выходить по тайм-ауту, если ответ не приходит, но тип ошибки при этом определить не удастся: причина может быть в досрочном завершении процесса, сбое узла, недоступности сети и т. д. ■ Клиент RPC, использующий TCP, может узнать о возникших на сервере проблемах, поскольку при досрочном завершении сервера его конец соединения автоматически закрывается. Это, однако, не помогает, если сервер является многопоточным, поскольку такой сервер не закрывает соединение в случае отмены потока с процедурой сервера. Мы также не получаем информации в случае сбоя узла, поскольку при этом соединение TCP не закрывается. Во всех этих случаях следует использовать выход по тайм-ауту. 16.8. XDR: представление внешних данныхВ предыдущей главе мы использовали двери для вызова процедуры одного процесса из другого процесса. При этом оба процесса выполнялись на одном узле, поэтому необходимости в преобразовании данных не возникало. Однако RPC используется для вызова процедур на разных узлах, которые могут иметь различный формат хранения данных. Прежде всего могут отличаться размеры фундаментальных типов (в некоторых системах long имеет длину 32 бита, а в других — 64). Кроме того, может отличаться порядок битов (big-endian и little-endian, о чем говорится в книге [24, с. 66-69 и 137-140]. Мы уже столкнулись с этой проблемой, когда обсуждали листинг 16.3. Сервер у нас работал на компьютере с little-endian х86, а клиент — на big-endian Sparc, но мы могли без проблем обмениваться данными (в нашем примере — одно длинное целое). Sun RPC использует стандарт XDR (External Data Representation — представление внешних данных) для описания и кодирования данных (RFC 1832 [19]). XDR является одновременно языком описания данных и набором правил для их кодирования. В XDR используется скрытая типизация (implicit typing), то есть отправитель и получатель должны заранее знать тип и порядок данных. Например, два 32-разрядных целых, одно число с плавающей точкой и одинарной точностью и строка символов.
Представление всех типов согласно XDR требует количества байтов, кратного четырем. Эти байты всегда передаются в порядке big-endian. Целые числа со знаком передаются в дополнительном коде, а числа с плавающей точкой передаются в формате IEEE. Поля переменной длины могут содержать до 3 байтов дополнения в конце, так чтобы подогнать начало следующего элемента до адреса, кратного четырем. Например, 5-символьная строка АSСII будет передана как 12 байтов: ■ 4-байтовое целое, содержащее значение 5; ■ 5-байтовая строка; ■ 3 байта со значением 0 (дополнение). При описании XDR и поддерживаемых типов данных следует уточнить три момента. 1. Как объявляются переменные различных типов в файле спецификации RPC (файл с расширением .х)? В наших примерах пока что использовалось только длинное целое. 2. В какой тип языка С преобразуется данный тип программой rpcgen при составлении заголовочного файла? 3. Каков реальный формат передаваемых данных? Таблица 16.2 содержит ответы на первых два вопроса. Для составления этой таблицы мы создали файл спецификации RPC со всеми поддерживаемыми стандартом XDR типами. Этот файл был обработан rpcgen, после чего мы изучили получившийся заголовочный файл. Таблица 16.2. Типы данных, поддерживаемые xdr и rpcgen
Опишем содержимое таблицы более подробно. 1. Декларация const преобразуется в #define. 2. Декларация typedef преобразуется в typedef. 3. Пять целых типов со знаком. Передаются XDR как 32-разрядные значения (первые четыре типа), а последний — как 64-разрядное.
4. Пять целых типов без знака. Первые 4 передаются как 32-разрядные значения, а последнее — как 64-разрядное. 5. Три типа данных с плавающей точкой. Первый передается как 32-разрядное значение, второй — как 64-разрядное, а третий — как 128-разрядное.
6. Тип boolean эквивалентен целому со знаком. Заголовки RPC также определяют константу TRUE равной 1, a FALSE равной 0. 7. Перечисление (enumeration) эквивалентно целому со знаком и совпадает с типом данных enum в С. rpcgen также создает определение типа для данной переменной. 8. Скрытые данные фиксированной длины передаются библиотекой как 8-разрядные значения без интерпретации. 9. Скрытые данные переменной длины также представляют собой последовательность неинтерпретируемых данных, но количество реально передаваемых данных помещается в целочисленную переменную и посылается перед самими данными. При отправке данных такого типа (например, при заполнении списка аргументов перед вызовом RPC) следует указать длину, прежде чем делать вызов. При приеме данного типа данных следует выяснить значение длины, чтобы определить, сколько данных будет принято. 10. Строка представляет собой последовательность ASCII-символов. В памяти строка хранится как обычная строка символов языка С, завершаемая нулем, но при передаче перед ней отправляется целое без знака, в которое помещается количество символов данной строки (без завершающего нуля). При отправке данных такого типа размер строки определяется библиотекой с помощью вызова strlen. При приеме данные такого типа помещаются в строку символов С, завершаемую нулем. 11. Массив фиксированной длины любого типа передается как последовательность n элементов данного типа. 12. Массив переменной длины любого типа передается как целое без знака, указывающее количество элементов, и последовательность элементов данного типа. Максимальное количество элементов в объявлении может быть опущено. Но если это количество указать при компиляции программы, библиотека будет проверять, не превосходит ли реальная длина указанного значения m. 13. Структура передается как последовательность полей. rpcgen также создает определение типа для данного имени переменной (typedef). 14. Размеченное объединение состоит из целочисленного дискриминанта и набора типов данных (ветвей), зависящих от значения дискриминанта. В табл. 16.2 мы показываем, что дискриминант должен быть типа int, но он может быть и unsigned int, и enum, и bool (все эти типы передаются как 32-разрядные целые). При передаче размеченного объединения передается 32-разрядное значение дискриминанта, за которым следует значение той ветви, которая ему соответствует. В ветви default часто объявляется тип void, что означает отсутствие передаваемой вслед за дискриминантом информации. Ниже мы продемонстрируем это на примере. 15. Дополнительные данные представляют собой специальный тип объединения, описанный в примере из листинга 16.24. Объявление XDR выглядит как объявление указателя в языке С, и именно указатель объявляется в созданном заголовочном файле. На рис. 16.3 сведена информация о кодировании различных типов данных в XDR. Рис. 16.3. Кодирование типов данных в XDR Пример: использование XDR без RPCПриведем пример использования XDR без RPC. Мы воспользуемся стандартом XDR для кодирования структуры данных в машинно-независимое представление, в котором они могут быть обработаны другими системами. Этот метод может использоваться для написания файлов или для отправки данных по сети в машинно-независимом формате. В листинге 16.11 приведен текст файла спецификации data .х, который на самом деле является файлом спецификации XDR, поскольку мы не объявляем никаких процедур RPC. Листинг 16.11. Файл спецификации XDR //sunrpc/xdr1/data.x 1 enum result_t { 2 RESULT_INT = 1, RESULT_DOUBLE = 2 3 }; 4 union union_arg switch (result_t result) { 5 case RESULT_INT: 6 int intval; 7 case RESULT_DOUBLE: 8 double doubleval; 9 default: 10 void; 11 }; 12 struct data { 13 short short_arg; 14 long long_arg; 15 string vstring_arg<128>; /* строка переменной длины */ 16 opaque fopaque_arg[3]; /* скрытые данные фиксированной длины */ 17 opaque vopaque_arg<>; /* скрытые данные переменной длины */ 18 short fshort_arg[4]; /* массив фиксированной длины */ 19 long vlong_arg<>; /* массив переменной длины */ 20 union_arg uarg; 21 };Объявление перечисления и размеченного объединения 1-11 Мы объявляем перечислимый тип с двумя значениями и размеченное объединение, использующее это перечисление в качестве дискриминанта. Если дискриминант имеет значение RESULT_INT, после значения дискриминанта передается целое число. Если дискриминант имеет значение RESULT_DOUBLE, за ним передается число с плавающей точкой двойной точности. В противном случае после дискриминанта не передается ничего. Объявление структуры12-21 Мы объявляем структуру, состоящую из различных типов, поддерживаемых XDR. Поскольку мы не объявляем процедур RPC, программа rpcgen не создаст заглушку клиента и заглушку сервера. Однако она создаст заголовочный файл data.h и файл data_xdr.с, содержащий функции XDR, обеспечивающие кодирование и декодирование данных, объявленных в файле data.х. В листинге 16.12 приведен получающийся в результате работы rpcgen заголовочный файл data.h. Содержимое этого файла выглядит так, как мы и предполагали (табл. 16.2). Листинг 16.12. Заголовочный файл data.h, созданный rpcgen из файла data.x//sunrpc/xdr1/data.h 1 /* 2 * Please do not edit this file. It was generated using rpcgen. 3 */ 4 #ifndef _DATA_H_RPCGEN 5 #define _DATA_H_RPCGEN 6 #include <rpc/rpc.h> 7 enum result_t { 8 RESULT_INT = 1, 9 RESULT_DOUBLE = 2 10 }; 11 typedef enum result_t result_t; 12 struct union_arg { 13 result_t result; 14 union { 15 int intVal; 16 double doubleval; 17 } union_arg_u; 18 }; 19 typedef struct union_arg union_arg; 20 struct data { 21 short short_arg; 22 long long_arg; 23 char *vstring_arg; 24 char fopaque_arg[3]; 25 struct { 26 u_int vopaque_arg_len; 27 char *vopaque_arg_val; 28 } vopaque_arg; 29 short fshort_arg[4]; 30 struct { 31 u_int vlong_arg_len; 32 long *vlong_arg_val; 33 } vlong_arg; 34 union_arg uarg; 35 }; 36 typedef struct data data: 37 /* 4the xdr functions */ 38 extern bool_t xdr_result_t(XDR *, result_t*); 39 extern bool_t xdr_union_arg(XDR *, union_arg*); 40 extern bool_t xdr_data(XDR *, data*); 41 #endif /* !_DATA_H_RPCGEN */ В файле data_xdr.с объявляется функция xdr_data, вызываемая для кодирования и декодирования структуры data, которую мы определили. Суффикс имени функции _data соответствует имени нашей структуры из листинга 16.11. Первая программа, которую мы напишем, будет называться write.с. Она будет присваивать значения полям структуры data, вызывать xdr_data для кодирования всех полей в формат XDR и записывать результат в стандартный поток вывода. Эта программа приведена в листинге 16.13. Листинг 16.13. Инициализация структуры и кодирование ее в XDR//sunrpc/xdr1/write.c 1 #include "unpipc.h" 2 #include "data.h" 3 int 4 main(int argc, char **argv) 5 { 6 XDR xhandle; 7 data out; /* структура, с которой мы работаем */ 8 char *buff; /* результат кодирования в XOR */ 9 char vop[2]; 10 long vlong[3]; 11 u_int size; 12 out.short_arg = 1; 13 out.long_arg = 2; 14 out.vstring_arg = "hello, world"; /* присваиваем значение указателю */ 15 out.fopaque_arg[0] = 99; /* скрытые данные фиксированной длины */ 16 out.fopaque_arg[1] = 88; 17 out.fopaque_arg[2] = 77; 18 vop[0] = 33; /* скрытые данные переменной длины */ 19 vop[1] = 44; 20 out.vopaque_arg.vopaque_arg_len = 2; 21 out.vopaque_arg.vopaque_arg_val = vop; 22 out.fshort_arg[0] = 9999; /* массив фиксированной длины */ 23 out.fshort_arg[1] = 8888; 24 out.fshort_arg[2] = 7777; 25 out.fshort_arg[3] = 6666; 26 vlong[0] = 123456; /* массив переменной длины */ 27 vlong[l] = 234567; 28 vlong[2] = 345678; 29 out.vlong_arg.vlong_arg_len = 3; 30 out.vlong_arg.vlong_arg_val = vlong; 31 out.uarg.result = RESULT_INT; /* размеченное объединение */ 32 out.uarg.union_arg_u.intval = 123; 33 buff = Malloc(BUFFSIZE); /* кратен 4-м байтам */ 34 xdrmem_create(&xhandle, buff, BUFFSIZE, XDR_ENCODE); 35 if (xdr_data(&xhandle, &out) != TRUE) 36 err_quit("xdr_data error"); 37 size = xdr_getpos(&xhandle); 38 Write(STDOUT_FILENO, buff, size); 39 exit(0); 40 }Инициализация элементов структуры ненулевыми значениями 12-32 Сначала мы присваиваем полям структуры ненулевые значения. В случае полей переменной длины мы должны установить длину этих полей. Мы присваиваем дискриминанту размеченного объединения значение RESULT_INT и помещаем в его соответствующее поле значение 123. Выделение буфера33 Мы вызываем malloc для выделения буфера, в который подпрограммы XDR будут помещать результаты своей работы. Адрес и размер буфера должны быть кратны четырем. Выделение массива char не гарантирует этого. Создание потока XDR в памяти34 Функция библиотеки времени выполнения xdrmem_create инициализирует буфер, на который указывает buff, предназначенный для использования функциями XDR как поток в памяти. Мы выделяем переменную типа XDR с именем xhandle и передаем адрес этой переменной в качестве первого аргумента. Библиотека XDR времени выполнения хранит в этой переменной всю необходимую информацию (указатель на буфер, текущее положение в буфере и т. п.). Последний аргумент имеет значение XDR_ENCODE, что указывает XDR на необходимость преобразования данных из формата узла в формат XDR. Кодирование структуры35-36 Мы вызываем функцию xdr_data, созданную rpcgen в файле data_xdr.c, и она кодирует структуру out в формат XDR. Возвращаемое значение TRUE говорит об успешном завершении работы функции. Получение размера кодированных данных и запись их в поток вывода37-38 Функция xdr_getpos возвращает текущее положение библиотеки XDR в выходном буфере (то есть сдвиг байта, в который будут помещены очередные данные). Его мы трактуем как размер готовых к записи данных. В листинге 16.14 приведен текст программы read, которая считывает данные из файла, записанного предыдущей программой, и выводит значения всех полей структуры data. Листинг 16.14. Считывание структуры data из формата XDR//sunrpc/xdr1/read.c 1 #include "unpipc.h" 2 #include "data.h" 3 int 4 main(int argc, char **argv) 5 { 6 XDR xhandle; 7 int i; 8 char *buff; 9 data in; 10 ssize_t n; 11 buff = Malloc(BUFFSIZE); /* адрес должен быть кратен 4-м байтам */ 12 n = Read(STDIN_FILENO, buff, BUFFSIZE); 13 printf("read %ld bytes\n", (long) n); 14 xdrmem_create(&xhandle, buff, n, XDR_DECODE); 15 memset(&in, 0, sizeof(in)); 16 if (xdr_data(&xhandle, &in) != TRUE) 17 err_quit("xdr_data error"); 18 printf("short_arg = %d, long_arg = %ld, vstring_arg = '%s'\n", 19 in.short_arg, in.long_arg, in.vstring_arg); 20 printf("fopaque[] = %d, %d, %d\n", 21 in.fopaque_arg[0], in.fopaque_arg[1], in.fopaque_arg[2]); 22 printf("vopaque<> ="); 23 for (i = 0; i < in.vopaque_arg.vopaque_arg_len; i++) 24 printf(" %d", in.vopaque_arg.vopaque_arg_val[i]); 25 printf("\n"); 26 printf("fshort_arg[] = %d, %d, %d, %d\n", in.fshort_arg[0], 27 in.fshort_arg[1], in.fshort_arg[2], in.fshort_arg[3]); 28 printf("vlong<> ="); 29 for (i = 0; i < in.vlong_arg.vlong_arg_len; i++) 30 printf(" %ld", in.vlong_arg.vlong_arg_val[i]); 31 printf("\n"); 32 switch (in.uarg.result) { 33 case RESULT_INT: 34 printf("uarg (int) = %d\n", in.uarg.union_arg_u.intval); 35 break; 36 case RESULT_DOUBLE: 37 printf("uarg (double) = %g\n", in.uarg.union_arg_u.doubleval); 38 break; 39 default: 40 printf("uarg (void)\n"); 41 break; 42 } 43 xdr_free(xdr_data, (char*)&in); 44 exit(0); 45 }Выделение правильно расположенного буфера 11-13 Вызывается функция malloc для выделения буфера. В этот буфер считывается файл, созданный предыдущей программой. Создание потока XDR, инициализация буфера, декодирование14-17 Инициализируем поток XDR, указав флаг XDR_DECODE, означающий, что преобразование производится из формата XDR в формат узла. Мы инициализируем структуру i n нулями и вызываем xdr_data для декодирования содержимого буфера buff в эту структуру. Мы обязаны инициализировать принимающую структуру нулями, поскольку некоторые из подпрограмм XDR (например, xdr_string) требуют выполнения этого условия. xdr_data — это та же функция, которую мы вызывали в листинге 16.13. Изменился только последний аргумент xdrmem_create: в предыдущей программе мы указывали XDR_ENCODE, а в этой — XDR_DECODE. Это значение сохраняется в дескрипторе XDR (xhandle) функцией xdrmem_create и затем используется библиотекой XDR для выбора между кодированием и декодированием данных. Вывод значений полей структуры18-42 Мы выводим значения всех полей структуры data. Освобождение выделенной под XDR памяти43 Для освобождения памяти мы вызываем функцию xdr_free (см. упражнение 16.10). Запустим программу write на компьютере Sparc, перенаправив стандартный вывод в файл с именем data: solaris % write > data solaris % ls -l data -rw-rw-r-- 1 rstevens other1 76 Apr 23 12:32 data Мы видим, что размер файла равен 72 байтам что соответствует рис. 16.4, на котором изображена схема хранения данных. Прочитав этот файл в BSD/OS или Digital Unix, мы получим те результаты, на которые и рассчитывали: bsdi % read < data read 76 bytes short_arg = 1, long_arg = 2, vstring_arg = 'hello, world' fopaque[] =99, 88, 77 vopaque<> = 33 44 fshort_arg[] = 9999, 8888, 7777, 6666 vlong<> = 123456 234567 345678 uarg (int) = 123 alpha % read < data read 76 bytes short_arg = 1, long_arg = 2, vstring_arg = 'hello, world' fopaque[] = 99, 88, 77 vopaque<> = 33 44 fshort_arg[] = 9999, 8888, 7777, 6666 vlong<> = 123456 234567 345678 uarg (int) = 123 Рис. 16.4. Формат потока XDR, записанный в листинге 16.13 Пример: вычисление размера буфераВ предыдущем примере мы выделяли буфер размера BUFFSIZE (определенного в файле unpiрс.h в листинге В.1), и этого было достаточно. К сожалению, не существует простого способа вычислить объем памяти, нужный XDR для кодирования конкретных данных. Вычислить размер структуры вызовом sizeof недостаточно, потому что каждое поле кодируется XDR по отдельности. Нам придется перебирать элементы структуры, прибавляя к конечному результату объем памяти, нужный XDR для кодирования очередного элемента. В листинге 16.15 приведен пример простой структуры с тремя полями. Листинг 16.15. Спецификация XDR для простой структуры//sunrpc/xdrl/examplе.х 1 const MAXC = 4; 2 struct example { 3 short a; 4 double b; 5 short c[MAXC]; 6 }; Программа, текст которой приведен в листинге 16.16, вычисляет размер буфера, требуемого XDR для кодирования этой структуры. Он получается равным 28 байт. Листинг 16.16. Программа, вычисляющая размер буфера XDR//sunrpc/xdr1/example.c 1 #include "unpipc.h" 2 #include "example.h" 3 int 4 main(int argc, char **argv) 5 { 6 int size; 7 example foo; 8 size = RNDUP(sizeof(foo.a)) + RNDUP(sizeof(foo.b)) + 9 RNDUP(sizeof(foo.c[0])) * MAXC; 10 printf("size = %d\n", size); 11 exit(0); 12 } 8-9 Макрос RNDUP определен в файле <rpc/xdr.h>. Он округляет аргумент к ближайшему кратному BYTES_PER_XDR_UNIT (4). Для массива фиксированного размера вычисляется размер каждого элемента, который затем умножается на количество элементов. Проблема возникает в случае использования типов данных переменной длины. Если мы объявим stringd<10>, максимальный размер будет RNDUP(sizeof( int)) (для длины) плюс RNDUP(sizeof(char)*10) (для символов строки). Но мы не можем вычислить размер буфера, если максимальный размер не указан в объявлении переменной (например, float e<>). Лучше всего в этом случае выделять буфер с запасом, а потом проверять, не возвращают ли подпрограммы XDR ошибку (упражнение 16.5). Пример: необязательные данныеСуществуют три способа задания необязательных данных в файле XDR, примеры для всех приведены в листинге 16.17. Листинг 16.17. Файл спецификации XDR, иллюстрирующий способы задания необязательных данных//sunrpc/xdr1/opt1.x 1 union optlong switch (bool flag) { 2 case TRUE: 3 long val; 4 case FALSE: 5 void; 6 }; 7 struct args { 8 optlong arg1; /* объединение с булевским дискриминантом */ 9 long arg2<1>; /* массив переменной длины с одним элементом */ 10 long *arg3; /* указатель */ 11 };Объявление объединения с булевским дискриминантом 1-8 Мы определяем объединение с ветвями FALSE и TRUE и структуру этого типа. Если флаг дискриминанта TRUE, за ним следует значение типа long; в противном случае за ним ничего не следует. После кодирования библиотекой XDR это объединение будет закодировано как: ■ 4 байта флага со значением 1 (TRUE) и 4 байта целочисленного значения либо ■ 4 байта флага со значением 0 (FALSE). Объявление массива переменной длины9 Если мы указываем массив переменной длины с одним возможным элементом, он будет передан как: ■ 4 байта со значением 1 и 4 байта значения либо ■ 4 байта со значением 0. Объявление указателя XDR10 Новый способ определения необязательных данных заключается в объявлении указателя. Он будет закодирован как: ■ 4 байта со значением 1 и 4 байта значения либо ■ 4 байта со значением 0 в зависимости от значения соответствующего указателя при кодировании данных. Если указатель ненулевой, используется первый вариант кодирования. Если указатель нулевой, получится второй вариант. Это удобный способ кодирования необязательных данных в случае, если в нашем коде имеется указатель на эти данные. Важная деталь реализации, благодаря которой оба варианта дают одинаковый результат при кодировании, заключается в том, что значение TRUE равно 1, что совпадает с длиной массива переменной длины, когда в нем есть один элемент. В листинге 16.18 приведен текст заголовочного файла, созданного программой rpcgen для данного файла спецификации. Листинг 16.18. Заголовочный файл, получившийся в результате обработки листинга 16.17//sunrpc/xdr1/opt1.h 7 struct optlong { 8 bool_t flag; 9 union { 10 long val; 11 } optlong_u; 12 }; 13 typedef struct optlong optlong; 14 struct args { 15 optlong arg1; 16 struct { 17 u_int arg2_len; 18 long *arg2_val; 19 } arg2; 20 long *arg3; 21 }; 22 typedef struct args args; 14-21 Хотя все три аргумента кодируются одинаково, способы присваивания и получения их значений в языке С различны. В листинге 16.19 приведен текст простой пpoгрaммы, устанавливающей значения всех трех аргументов так, что ни одно из полей long не кодируется. Листинг 16.19. Ни одно из значений не будет закодировано//sunrpc/xdr1/opt1z.с 1 #include "unpipc.h" 2 #include "opt1.h" 3 int 4 main(int argc, char **argv) 5 { 6 int i; 7 XDR xhandle; 8 char *buff; 9 long *lptr; 10 args out; 11 size_t size; 12 out.arg1.flag = FALSE; 13 out.arg2.arg2_len = 0; 14 out.arg3 = NULL; 15 buff = Malloc(BUFFSIZE); /* Адрес должен быть кратен четырем */ 16 xdrmem_create(&xhandle, buff, BUFFSIZE, XOR_ENCODE); 17 if (xdr_args(&xhandle, &out) != TRUE) 18 err_quit("xdr_args error"); 19 size = xdr_getpos(&xhandle); 20 lptr = (long*)buff; 21 for (i = 0; i < size; i += 4) 22 printf("%ld\n", (long) ntohl(*lptr++)); 23 exit(0); 24 }Присваивание значений 12-14 Дискриминанту объединения присваивается значение FALSE, длина массива переменной длины устанавливается в 0, а указатель делается нулевым (NULL). Выделение буфера и кодирование15-19 Мы выделяем буфер и кодируем структуру out в поток XDR. Вывод содержимого буфера XDR20-22 Мы выводим содержимое буфера XDR по 4 байта, используя функцию ntohl (host-to-network long integer) для преобразования из порядка XDR big-endian в байтовый порядок узла. В результате получается именно то, что должно было быть помещено в буфер библиотекой XDR времени выполнения: solaris % opt1z 0 0 0 Как мы и предполагали, каждому аргументу отводится 4 байта со значением О, указывающим на то, что за ним не следует никаких данных. В листинге 16.20 приведена измененная версия программы, которая присваивает значения всем трем аргументам, кодирует их в поток XDR и выводит его содержимое. Листинг 16.20. Присваивание значений аргументам из листинга 16.17//sunrpc/xdr1/opt1.c 1 #include "unpipc.h" 2 #include "opt1.h" 3 int 4 main(int argc, char **argv) 5 { 6 int i; 7 XOR xhandle; 8 char *buff; 9 long lval2, lval3, *lptr; 10 args out; 11 size_t size; 12 out.arg1.flag = TRUE; 13 out.arg1.optlong_u.val = 5; 14 lval2 = 9876; 15 out.arg2.arg2_len = 1; 16 out.arg2.arg2_val = &lval2; 17 lval3 = 123; 18 out.arg3 = &lval3; 19 buff = Malloc(BUFFSIZE); /* адрес должен быть кратен 4 */ 20 xdrmem_create(&xhandle, buff, BUFFSIZE, XDR_ENCODE); 21 if (xdr_args(&xhandle, &out) != TRUE) 22 err_quit("xdr_args error"); 23 size = xdr_getpos(&xhandle); 24 lptr = (long *) buff; 25 for (i = 0; i < size; i += 4) 26 printf("%ld\n", (long) ntohl(*lptr++)); 27 exit(0); 28 }Присваивание значений 12-18 Для присваивания значения объединению мы устанавливаем дискриминант в TRUE, а затем присваиваем значение полю long. Длину массива мы также сначала устанавливаем в 1. Указатель мы устанавливаем на соответствующее значение в памяти. При запуске этой программы мы получим ожидаемые шесть 4-байтовых значений: solaris % opt1 1 значение дискриминанта TRUE 5 1 длина массива переменной длины 9876 1 флаг для ненулевого указателя 123 Пример: обработка связного спискаЕсли осуществима передача необязательных данных, мы можем расширить возможности указателей в XDR и использовать их для кодирования и декодирования связных списков, содержащих произвольное количество элементов. В нашем примере используется связный список пар имя-значение. Соответствующий файл спецификации XDR приведен в листинге 16.21. Листинг 16.21. Спецификация XDR для связного списка пар имя-значение//sunrpc/xdr1/opt2.x 1 struct mylist { 2 string name<>; 3 long value; 4 mylist *next; 5 }; 6 struct args { 7 mylist *list; 8 }; 1-5 Структура mylist содержит одну пару имя-значение и указатель на следующую структуру такого типа. Указатель в последней структуре списка будет нулевым. В листинге 16.22 приведен текст заголовочного файла, созданного программой rpcgen из файла opt2.х. Листинг 16.22. Заголовочный файл, созданный программой rpcgen//sunrpc/xdr1/opt2.h 7 struct mylist { 8 char *name; 9 long value; 10 struct mylist *next; 11 }; 12 typedef struct mylist mylist; 13 struct args { 14 mylist *list; 15 }; 16 typedef struct args args; В листинге 16.23 приведен текст программы, инициализирующей связный список с тремя парами имя-значение и кодирующей его с помощью библиотеки XDR. Листинг 16.23. Инициализация, кодирование связного списка и вывод результата1 //sunrpc/xdr1/opt2.c 2 #include "unpipc.h" 3 #include "opt2.h" 4 int 5 main(int argc, char **argv) 6 { 7 int i; 8 XDR xhandle; 9 long *lptr; 10 args out; /* структура, которую мы заполняем */ 11 char *buff; /* результат кодирования */ 12 mylist nameval[4]; /* до четырех элементов в списке */ 13 size_t size; 14 out.list = &nameval[2]; /* [2] –> [1] –> [0] */ 15 nameval[2].name = "name1"; 16 nameval[2].value = 0x1111; 17 nameval[2].next = &nameval[1]; 18 nameval[1].name = "namee2"; 19 nameval[1].value = 0x2222; 20 nameval[1].next = &nameval[0]; 21 nameval[0].name = "nameee3"; 22 nameval[0].value = 0x3333; 23 nameval[0].next = NULL; 24 buff = Malloc(BUFFSIZE); /* адрес должен быть кратен 4 */ 25 xdrmem_create(&xhandle, buff, BUFFSIZE, XDR_ENCODE); 26 if (xdr_args(&xhandle, tout) != TRUE) 27 err_quit("xdr_args error"); 28 size = xdr_getpos(&xhandle); 29 lptr = (long*)buff; 30 for (i = 0; i < size; i += 4) 31 printf("%8lx\n", (long)ntohl(*lptr++)); 32 exit(0); 33 }Инициализация связного списка 11-22 Мы выделяем память под четыре элемента, но инициализируем только три из них. Первая запись nameval[2], потом nameval[1] и nameval[0]. Указатель на начало списка (out.list) устанавливается на &nameval[2]. Мы инициализируем список в таком порядке, чтобы показать, что библиотека XDR обрабатывает указатели и порядок в списке оказывается именно таким, каким он был в нашей программе, и не зависит от того, какие массивы для этого используются. Мы также инициализируем значения элементов списка шестнадцатеричными величинами, поскольку будем выводить их в этом формате. Вывод программы показывает, что перед каждым элементом списка идет значение 1 в 4 байтах (что мы можем считать длиной массива переменной длины с одним элементом или булевским значением TRUE). Четвертая запись состоит из 4 байт, в которых записан 0. Она обозначает конец списка: solaris % opt2 1 дальше идет один элемент 5 длина строки 6e616d65 имя(name) 31000000 1 и три байта дополнения 1111 значение 1 один элемент 6 длина строки 6e616d65 имя 65320000 е 2 и 2 байта дополнения 2222 значение 1 один элемент 7 длина строки 6e616d65 имя 65653300 е е 3 и 1 байт дополнения 3333 значение 0 конец списка При декодировании списка библиотека XDR будет динамически выделять память под его элементы и указатели и связывать все это вместе, что позволит легко переходить от одного элемента списка к другому в программе на С. 16.9. Форматы пакетов RPCНа рис. 16.5 приведен формат запроса RPC в пакете TCP. Поскольку TCP передает поток байтов и не предусматривает границ сообщений, приложение должно предусматривать способ разграничения сообщений. Sun RPC определяет запись как запрос или ответ, и каждая запись состоит из одного или более фрагментов. Каждый фрагмент начинается с 4-байтового значения: старший бит является флагом последнего фрагмента, а следующие 31 бит представляют собой счетчик (длина фрагмента). Если бит последнего фрагмента имеет значение 0, данный фрагмент не является последним в записи.
Если вместо TCP используется UDP, первое поле в заголовке UDP будет идентификатором транзакции (XID), как показано на рис. 16.7.
Рис. 16.5. Запрос RPC в пакете TCP Приведем спецификацию XDR для запроса RPC, взятую из RFC 1831. Имена на рис. 16.5 взяты из этой спецификации: enum autn_flavor { AUTH_NONE = 0, AUTH_SYS = 1, AUTH_SHORT = 2 /* and more to be defined */ }; struct opaque_auth { auth_flavor flavor; opaque body<400>; }; enum msg_type { CALL = 0, REPLY = 1 }; struct call_body { unsigned int rpcvers; /* версия RPC: должна быть 2 */ unsigned int prog; /* номер программы */ unsigned int vers; /* номер версии */ unsigned int proc; /* номер процедуры */ opaque_auth cred; /* данные вызывающего */ opaque_auth verf; /* проверочная информация вызывающего */ /* параметры, относящиеся к процедуре */ }; struct rpc_msg { unsigned int xid; union switch (msg_type mtype) { case CALL: call_body cbody; case REPLY: reply_body rbody; } body; }; Содержимое скрытых данных переменной длины, содержащих сведения о пользователе и проверочную информацию, зависит от типа аутентификации. Для нулевой аутентификации, используемой по умолчанию, длина этих данных должна быть установлена в 0. Для аутентификации Unix эти данные содержат следующую структуру: struct authsys_parms { unsigned int stamp; string machinename<255>; unsigned int uid; unsigned int gid; unsigned int gids<16>; }; Если тип аутентификации AUTH_SYS, тип проверки должен быть AUTH_NONE. Формат ответа RPC сложнее, чем формат запроса, поскольку в нем могут передаваться сообщения об ошибках. На рис. 16.6 показаны возможные варианты. На рис. 16.7 показан формат ответа RPC в случае успешного выполнения процедуры. Ответ передается по протоколу UDP. Ниже приводится текст спецификации XDR ответа RPC, взятый из RFC 1831. enum reply_stat { MSG_ACCEPTED = 0, MSG_DENIED = 1 }; enum accept_stat { SUCCESS = 0, /* успешное завершение вызова RPC */ PROG_UNAVAIL = 1, /* требуемый номер программы недоступен */ PROG_MISMATCH = 2, /* требуемый номер версии недоступен */ PROC_UNAVAIL = 3, /* номер процедуры недоступен */ GARBAGE_ARGS = 4, /* не могу декодировать аргументы */ SYSTEM_ERR = 5 /* ошибка выделения памяти и т. п. */ }; struct accepted_reply { opaque_auth verf; union switch (accept_stat stat) { case SUCCESS: opaque results[0]; /* результаты, возвращаемые процедурой */ case PROG_MISMATCH: struct { unsigned int low; /* наименьший поддерживаемый номер программы */ unsigned int high; /* наибольший поддерживаемый номер программы */ } mismatch_info; default: /* PROG_UNAVAIL, PROC_UNAVAIL, GARBAGE_ARGS, SYSTEM_ERR */ void; } reply_data; }; union reply_body switch (reply_stat stat) { case MSG_ACCEPTED: accepted_reply areply; case MSG_DENIED: rejected_reply rreply; } reply; Рис. 16.6. Возможные варианты ответов RPC Вызов может быть отклонен сервером, если номер версии RPC не тот или возникает ошибка аутентификации: enum reject_stat { RPC_MISMATCH = 0, /* номер версии RPC отличен от 2 */ AUTH_ERROR =1 /* ошибка аутентификации */ }; enum auth_stat { AUTH_OK = 0, /* успешное завершение */ /* ошибки на сервере */ AUTH_BADCRED = 1, /* ошибка в личных данных пользователя (нарушена контрольная сумма) */ AUTH_REJECTEDCRED = 2, /* клиент должен начать сеанс заново */ AUTH_BADVERF = 3, /* ошибка в проверочных данных (нарушена контрольная сумма) */ AUTH_REJECTEDVERF = 4, /* проверочные данные устарели или были повторы */ AUTH_TOOWEAK = 5, /* запрос отклонен системой безопасности */ /* ошибки клиента */ AUTH_INVALIDRESP = 6, /* фальшивые проверочные данные в ответе */ AUTH_FAILED = 7 /* причина неизвестна */ }; union rejected_reply switch (reject_stat stat) { case RPC_MISMATCH: struct { unsigned int low; /* наименьший номер версии RPC */ unsigned int high; /* наибольший номер версии RPC */ } mismatch_info; case AUTH_ERROR: auth_stat stat; }; Рис. 16.7. Ответ на успешно обработанный вызов в дейтаграмме UDP 16.10. РезюмеСредства Sun RPC дают возможность создавать распределенные приложения, в которых клиентская часть может выполняться на одном узле, а серверная — на другом. Сначала следует определить процедуры сервера, которые могут быть вызваны клиентом, и написать файл спецификации RPC, описывающий аргументы и возвращаемые значения этих процедур. Затем пишется функция main клиента, вызывающая процедуры сервера, а потом сами эти процедуры. Программа клиента выглядит так, как будто она просто вызывает процедуры сервера, но на самом деле их скрытое взаимодействие по сети обеспечивается библиотекой RPC. Программа rpcgen является краеугольным камнем приложения, использующего RPC. Она считывает файл спецификации и создает заглушку клиента и заглушку сервера, а также функции, вызывающие требуемые подпрограммы XDR, которые осуществляют все необходимые преобразования данных. Библиотека XDR также является важной частью процесса. XDR определяет стандарт обмена данными различного формата между разными системами, у которых может быть по-разному определен, например, размер целого, порядок байтов и т. п. Как мы показали, XDR можно использовать и отдельно от RPC для обмена данными в машинно-независимом стандартном формате. Для передачи данных можно использовать любой механизм (сокеты, XTI, дискеты, компакт-диски или что угодно). В Sun RPC используется свой стандарт именования программ. Каждой программе присваивается 32-разрядный номер программы, 32-разрядный номер версии и 32-разрядный номер процедуры. Каждый узел с сервером RPC должен выполнять программу отображения портов в фоновом режиме (RPCBIND). Серверы RPC привязывают временные порты TCP и UDP к своим процедурам, а затем регистрируют эти порты в программе отображения портов, указывая номера программ и версий. При запуске клиент RPC связывается с программой отображения портов узла, где запущен сервер RPC, и выясняет номер нужного ему порта, а затем связывается с самим сервером по протоколам TCP или UDP. По умолчанию клиенты RPC не предоставляют аутентификационной информации и серверы RPC обрабатывают все приходящие запросы. Это аналогично написанию собственного приложения клиент-сервер с использованием сокетов или XTI. В Sun RPC предоставляются три дополнительные формы аутентификации: аутентификация Unix (предоставляется имя узла клиента, идентификатор пользователя и группы), аутентификация DES (основанная на криптографии с секретным и открытым ключом) и аутентификация Kerberos. Понимание стратегии тайм-аутов и повторных передач пакета RPC важно при использовании RPC (как и любой формы сетевого программирования). При использовании надежного транспортного протокола, такого, как TCP, клиенту RPC нужно использовать только общий тайм-аут, поскольку потеря пакетов или прием лишних копий целиком обрабатываются на транспортном уровне. Когда используется ненадежный транспортный протокол, такой как UDP, пакет RPC использует тайм-аут повтора в дополнение к общему тайм-ауту. Идентификатор транзакций используется клиентом RPC для проверки ответа на соответствие отправленному запросу. Любой вызов процедуры может быть отнесен к группе «ровно один», «не более чем один» или «не менее чем один». Для локальных вызовов этот вопрос можно не принимать во внимание, но при использовании RPC эту разницу следует понимать. Следует также понимать различия между идемпотентными и неидемпотентными процедурами. Sun RPC — это большой пакет, и мы лишь вкратце обсудили особенности его использования. Тем не менее сведений, приведенных в этой главе, должно быть достаточно для написания приложений целиком. Использование rpcgen скрывает многие детали и упрощает кодирование. Документация Sun описывает различные уровни кодирования RPC — упрощенный интерфейс, верхний уровень, средний уровень, уровень экспертов и низкий уровень, но эти категории достаточно бессмысленны. Количество функций в библиотеке RPC — 164, они могут быть разделены следующим образом: ■ 11 auth_ (аутентификация); ■ 26 clnt_ (клиентские); ■ 5 pmap_ (доступ к программе отображения портов); ■ 24 rpc_ (общего назначения); ■ 44 svc_ (серверные); ■ 54 xdr_ (преобразования XDR). Для сравнения отметим, что интерфейсы сокетов и XTI содержат по 25 функций, а интерфейсы дверей, очередей сообщений Posix и System V, семафоров и разделяемой памяти содержат по 10 функций. 15 функций используются для работы с потоками в стандарте Posix, 10 — для работы с условными переменными. 11 функций используются для работы с блокировками чтения-записи Posix и одна при работе с блокировкой записей fcntl. Упражнения1. При запуске сервер регистрируется в программе отображения портов. Что происходит при завершении сервера, например, клавишей завершения программы с терминала? Что произойдет, если на этот сервер впоследствии придет запрос от клиента? 2. Клиент взаимодействует с сервером RPC по протоколу UDP, и кэш не включен. Клиент посылает запрос на сервер, но серверу требуется 20 секунд до отправки ответа. Клиент посылает запрос повторно через 15 секунд, что приводит к повторному запуску процедуры сервера. Что произойдет со вторым ответом сервера? 3. Тип XDR string всегда кодируется в значение длины и последовательность символов. Что произойдет, если мы напишем char с[10] вместо string s<10>? 4. Измените максимальный размер string в листинге 16.11 со 128 на 10 и запустите программу write. Что произойдет? Уберите ограничение длины и сравните файл data_xdr.с с тем, который был создан, когда длина была ограничена. Что изменилось? 5. Измените третий аргумент в вызове xdrmem_create (размер буфера) в листинге 16.13 на 50 и посмотрите, что произойдет. 6. В разделе 16.5 мы описали включение кэша повторных ответов при использовании протокола UDP. Мы можем сказать, что протокол TCP имеет свой собственный кэш такого рода. О чем мы говорим и как велик этот кэш у протокола TCP? (Подсказка: как протокол TCP определяет, что принята копия полученных ранее данных?) 7. Есть пять полей, уникально идентифицирующих каждую запись в кэше сервера. В каком порядке следует их сравнивать, чтобы минимизировать количество сравнений? 8. При просмотре передаваемых пакетов с помощью tcpdump в примере из раздела 16.5, где использовался TCP, мы узнаем, что размер запроса 48 байт, а размер ответа 32 байт (без заголовков TCP и IPv4). Получите этот размер из рисунка 16.3. Каков был бы размер при использовании UDP вместо TCP? 9. Может ли клиент RPC в системе, не поддерживающей потоки, вызвать процедуру сервера, скомпилированную с поддержкой потоков? Что можно сказать о различии в передаваемых аргументах, о котором говорилось в разделе 16.2? 10. В программе read из листинга 16.19 мы выделяли место под буфер, в который считывался файл, и этот буфер содержал указатель vstring_arg. Но где хранится строка, на которую указывает vstring_arg? Измените программу так, чтобы проверить ваше предположение. 11. Sun RPC определяет нулевую процедуру как процедуру с номером 0 (по этой причине мы всегда начинали нумерацию процедур с 1, как в листинге 16.1). Более того, любая заглушка сервера, созданная rpcgen, автоматически определяет эту процедуру (в чем вы можете легко убедиться, посмотрев текст любой заглушки, созданной для одного из наших примеров). Нулевая процедура не принимает никаких аргументов и ничего не возвращает. Часто она используется для проверки работы сервера и измерения скорости передачи пакетов на сервер и обратно. Но если мы посмотрим на заглушку клиента, мы увидим, что в ней не содержится заглушки для этой процедуры. Посмотрите в документации описание функции clnt_call и используйте ее для вызова нулевой процедуры для любого сервера этой главы. 12. Почему в табл А.1 нет записи для сообщения размером 65536 для Sun RPC поверх UDP? Почему нет записей для сообщений длиной 16384 и 32768 в табл. А.2 для Sun RPC поверх UDP? 13. Проверьте, что удаление вызова xdr_free из листинга 16.19 приведет к утечке памяти. Добавьте оператор for(;;) { непосредственно перед вызовом xdrmem_create и завершающую скобку непосредственно перед вызовом xdr_free. Запустите программу и следите за ее размером в памяти с помощью ps. Удалите закрывающую скобку и поставьте ее после вызова xdr_free. Запустите программу снова и последите за ее размером еще раз. Примечания:1 Все исходные тексты, опубликованные в этой книге, вы можете найти по адресу http://www.piter.com/download. 2 Проблема о порядке байтов в слове сродни проблеме лилипутов из «Путешествий Гулливера» Д. Свифта, которые никак не могли договориться, с какого конца начинать есть яйцо. Именно оттуда англоязычные программисты взяли термины little-endian (остроконечник) и big-endian (тупоконечник), подразумевая «little-end-first» и «big-end-first» (младший или старший байт идет первым). — Примеч. перев. |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|