Уильям Стивенс - UNIX: взаимодействие процессов
Нужно понимать, что сервер не может просто записать в канал числовое значение дескриптора, как в следующем фрагменте кода:
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
В первых двух случаях мы указываем имя файла, приводящее к возврату сообщения об ошибке. В третий раз сервер передает клиенту дескриптор файла из двух строк, который благополучно выводится.
ПРИМЕЧАНИЕ
Существует проблема, связанная с передачей дескриптора через дверь. Чтобы она проявилась в нашем примере, достаточно добавить вызов printf к процедуре сервера сразу после успешного вызова open. Вы увидите, что значение дескриптора каждый раз увеличивается на единицу. Проблема в том, что сервер не закрывает дескрипторы после передачи их клиенту. Сделать это, вообще говоря, нелегко. Логично было бы выполнять закрытие дескриптора после возврата из door_return, после успешной отправки дескриптора клиенту, но возврата из door_return не происходит! Если бы мы использовали sendmsg для передачи дескриптора через доменный сокет Unix или ioctl для передачи дескриптора через канал в SVR4, мы могли бы закрыть его после возврата из sendmsg или ioctl. Однако с дверьми все по-другому, поскольку возврата из функции door_return не происходит. Единственный способ обойти проблему заключается в том, что процедура сервера должна запоминать все открытые дескрипторы и закрывать их некоторое время спустя, что несколько запутывает код.
Эта проблема должна быть исправлена в Solaris 2.7 добавлением атрибута DOOR RELEASE. Отправитель устанавливает поле d_attributes равным DOOR DESCRIPTOR | DOOR_RELEASE, что говорит системе о необходимости закрывать дескриптор после передачи его клиенту.
15.9. Функция door server_create
В листинге 15.6 мы показали, что библиотека дверей автоматически создает новые потоки для обслуживания запросов клиентов по мере их поступления. Они создаются библиотекой как неприсоединенные потоки (detached threads) с размером стека потока по умолчанию, с отключенной возможностью отмены потока (thread cancellation) и с маской сигналов и классом планирования (scheduling class), унаследованными от потока, вызвавшего door_create. Если мы хотим изменить какой-либо из этих параметров или хотим самостоятельно работать с пулом потоков сервера, можно воспользоваться функцией door_server_create и указать нашу собственную процедуру создания сервера: