Книга: UNIX: разработка сетевых приложений

Пример передачи дескриптора

Пример передачи дескриптора

Теперь мы представим пример передачи дескриптора. Мы напишем программу под названием mycat, которой в качестве аргумента командной строки передается полное имя файла. Эта программа открывает файл и копирует его в стандартный поток вывода. Но вместо вызова обычной функции Unix open мы вызываем нашу собственную функцию my_open. Эта функция создает потоковый канал и вызывает функции fork и exec для запуска другой программы, открывающей нужный файл. Эта программа должна затем передать дескриптор обратно родительскому процессу по потоковому каналу.

На рис. 15.1 показан первый шаг: наша программа mycat после создания потокового канала при помощи вызова функции socketpair. Мы обозначили два дескриптора, возвращаемых функцией socketpair, как [0] и [1].


Рис. 15.1. Программа mycat после создания потокового канала при использовании функции socketpair

Затем процесс взывает функцию fork, и дочерний процесс вызывает функцию exec для выполнения программы openfile. Родительский процесс закрывает дескриптор [1], а дочерний процесс закрывает дескриптор [0]. (Нет разницы, на каком конце потокового канала происходит закрытие. Дочерний процесс мог бы закрыть [1], а родительский — [0].) При этом получается схема, показанная на рис. 15.2.


Рис. 15.2. Программа mycat после запуска программы openfile

Родительский процесс должен передать программе openfile три фрагмента информации: полное имя открываемого файла, режим открытия (только чтение чтение и запись или только запись) и номер дескриптора, соответствующий его концу потокового канала (который мы обозначили [1]). Мы выбрали такой способ передачи этих трех элементов, как ввод аргументов командной строки при вызове функции exec. Альтернативным способом будет отправка этих элементов в качестве данных по потоковому каналу. Программа отправляет обратно открытый дескриптор по потоковому каналу и завершается. Статус выхода программы сообщает родительскому процессу, смог ли файл открыться, и если нет, то какого типа ошибка произошла.

Преимущество выполнения дополнительной программы для открытия файла заключается в том, что за счет приравнивания привилегий пользователя к привилегиям владельца файла мы получаем возможность открывать те файлы, которые не имеем права открывать в обычной ситуации. Эта программа позволяет расширить концепцию обычных прав доступа Unix (пользователь, группа и все остальные) и включить любые формы проверки прав доступа. Мы начнем с программы mycat, показанной в листинге 15.7.

Листинг 15.7. Программа mycat: копирование файла в стандартный поток вывода

//unixdomain/mycat.c
 1 #include "unp.h"
 2 int my_open(const char*, int);
 3 int
 4 main(int argc, char **argv)
 5 {
 6  int fd, n;
 7  char buff[BUFFSIZE];
 8  if (argc != 2)
 9   err_quit("usage: mycat <pathname>");
10  if ((fd = my_open(argv[1], O_RDONLY)) < 0)
11   err_sys("cannot open %s", argv[1]);
12  while ((n = Read(fd, buff, BUFFSIZE)) > 0)
13   Write(STDOUT_FILENO, buff, n);
14  exit(0);
15 }

Если мы заменим вызов функции my_open вызовом функции open, эта простая программа всего лишь скопирует файл в стандартный поток вывода.

Функция my_open, показанная в листинге 15.8, должна выглядеть для вызывающего процесса как обычная функция Unix open. Она получает два аргумента — полное имя и режим открытия (например, O_RDONLY обозначает, что файл доступен только для чтения), открывает файл и возвращает дескриптор.

Листинг 15.8. Функция my_open: открытие файла и возвращение дескриптора

//unixdomain/myopen.c
 1 #include "unp.h"
 2 int
 3 my_open(const char *pathname, int mode)
 4 {
 5  int fd, sockfd[2], status;
 6  pid_t childpid;
 7  char c, argsockfd[10], argmode[10];
 8  Socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd);
 9  if ((childpid = Fork()) == 0) { /* дочерний процесс */
10   Close(sockfd[0]);
11   snprintf(argsockfd, sizeof(argsockfd), "%d", sockfd[1]);
12   snprintf(argmode, sizeof(argmode), "%d", mode);
13   execl("./openfile", "openfile", argsockfd, pathname, argmode,
14    (char*)NULL);
15   err_sys("execl error");
16  }
17  /* родительский процесс - ожидание завершения дочернего процесса */
18  Close(sockfd[1]); /* закрываем конец, который мы не используем */
19  Waitpid(childpid, &status, 0);
20  if (WIFEXITED(status) == 0)
21   err_quit("child did not terminate");
22  if ((status = WEXITSTATUS(status)) == 0)
23   Read_fd(sockfd[0], &c, 1, &fd);
24  else {
25   errno = status; /* установка значения errno в статус дочернего
                        процесса */
26   fd = -1;
27  }
28  Close(sockfd[0]);
29  return (fd);
30 }

Создание потокового канала

8 Функция socketpair создает потоковый канал. Возвращаются два дескриптора: sockfd[0] и sockfd[1]. Это состояние, которое мы показали на рис. 15.1.

Функции fork и exec

9-16 Вызывается функция fork, после чего дочерний процесс закрывает один конец потокового канала. Номер дескриптора другого конца потокового канала помещается в массив argsockfd, а режим открытия помещается в массив argmode. Мы вызываем функцию snprintf, поскольку аргументы функции exec должны быть символьными строками. Выполняется программа openfile. Функция execl возвращает управление только в том случае, если она встретит ошибку. При удачном выполнении начинает выполняться функция main программы openfile.

Родительский процесс в ожидании завершения дочернего процесса

17-22 Родительский процесс закрывает другой конец потокового канала и вызывает функцию waitpid для ожидания завершения дочернего процесса. Статус завершения дочернего процесса возвращается в переменной status, и сначала мы проверяем, что программа завершилась нормально (то есть не была завершена из-за возникновения какого-либо сигнала). Затем макрос WEXITSTATUS преобразует статус завершения в статус выхода, значение которого должно быть между 0 и 255. Мы вскоре увидим, что если при открытии необходимого файла программой openfile происходит ошибка, то эта программа завершается, причем статус ее завершения равен соответствующему значению переменной errno.

Получение дескриптора

23 Наша функция read_fd, которую мы показываем в следующем листинге, получает дескриптор потокового канала. Кроме получения дескриптора мы считываем 1 байт данных, но ничего с этими данными не делаем.

ПРИМЕЧАНИЕ

При отправке и получении дескриптора по потоковому каналу мы всегда отправляем как минимум 1 байт данных, даже если получатель никак эти данные не обрабатывает. Иначе получатель не сможет распознать, что значит нулевое возвращаемое значение из функции read_fd: отсутствие данных (но, возможно, есть дескриптор) или конец файла.

В листинге 15.9 показана функция readfd, вызывающая функцию recvmsg для получения данных и дескриптора через доменный сокет Unix. Первые три аргумента этой функции те же, что и для функции read, а четвертый (recvfd) является указателем на целое число. После выполнения этой функции recvfd будет указывать на полученный дескриптор.

Листинг 15.9. Функция read_fd: получение данных и дескриптора

//lib/read_fd.c
 1 #include "unp.h"
 2 ssize_t
 3 read_fd(int fd, void *ptr, size_t nbytes, int *recvfd)
 4 {
 5  struct msghdr msg;
 6  struct iovec iov[1];
 7  ssize_t n;
 8  int newfd;
 9 #ifdef HAVE_MSGHDR_MSG_CONTROL
10  union {
11   struct cmsghdr cm;
12   char control[CMSG_SPACE(sizeof(int))];
13  } control_un;
14  struct cmsghdr *cmptr;
15  msg.msg_control = control_un.control;
16  msg.msg_controllen = sizeof(control_un.control);
17 #else
18  msg.msg_accrights = (caddr_t)&newfd;
19  msg.msg_accrightslen = sizeof(int);
20 #endif
21  msg.msg_name = NULL;
22  msg.msg_namelen = 0;
23  iov[0].iov_base = ptr;
24  iov[0].iov_len = nbytes;
25  msg.msg_iov = iov;
26  msg.msg_iovlen = 1;
27  if ((n = recvmsg(fd, &msg, 0)) <= 0)
28   return (n);
29 #ifdef HAVE_MSGHDR_MSG_CONTROL
30  if ((cmptr = CMSG_FIRSTHDR(&msg)) != NULL &&
31   mptr->cmsg_len == CMSG_LEN(sizeof(int))) {
32   if (cmptr->cmsg_level != SOL_SOCKET)
33    err_quit("control level != SOL_SOCKET");
34   if (cmptr->cmsg_type != SCM_RIGHTS)
35    err_quit("control type != SCM_RIGHTS");
36   *recvfd = *((int*)CMSG_DATA(cmptr));
37  } else
38   *recvfd = -1; /* дескриптор не был передан */
39 #else
40  if (msg.msg_accrightslen == sizeof(int))
41   *recvfd = newfd;
42  else
43   *recvfd = -1; /* дескриптор не был передан */
44 #endif
45  return (n);
46 }
8-26
 Эта функция должна работать с обеими версиями функции recvmsg: с элементом msg_control и с элементом msg_accrights. Наш заголовочный файл config.h (см. листинг Г.2) определяет константу HAVE_MSGHDR_MSG_CONTROL, если поддерживается версия функции recvmsg с msg_control.

Проверка выравнивания буфера msg_control

10-13 Буфер msg_control должен быть выровнен в соответствии со структурой msghdr. Просто выделить в памяти массив типа char недостаточно. Здесь мы объявляем объединение, состоящее из структуры cmsghdr и символьного массива, что гарантирует необходимое выравнивание массива. Возможно и другое решение — вызвать функцию malloc, но это потребует освобождения памяти перед завершением функции.

27-45 Вызывается функция recvmsg. Если возвращаются вспомогательные данные, их формат будет таким, как показано на рис. 14.4. Мы проверяем, верны ли длина, уровень и тип, затем получаем вновь созданный дескриптор и возвращаем его через указатель вызывающего процесса recvfd. Макрос CMSG_DATA возвращает указатель на элемент cmsg_data объекта вспомогательных данных как указатель на элемент типа unsigned char. Мы преобразуем его к указателю на элемент типа int и получаем целочисленный дескриптор, на который указывает этот указатель.

Если поддерживается более старый элемент msg_accrights, то длина должна быть равна размеру целого числа, а вновь созданный дескриптор возвращается через указатель recvfd вызывающего процесса.

В листинге 15.10 показана программа openfile. Она получает три аргумента командной строки, которые должны быть переданы, и вызывает обычную функцию open.

Листинг 15.10. Программа openfile: открытие файла и передача дескриптора обратно

//unixdomain/openfile.c
 1 #include "unp.h"
 2 int
 3 main(int argc, char **argv)
 4 {
 5  int fd;
 6  ssize_t n;
 7  if (argc != 4)
 8   err_quit("openfile <sockfd#> <filename> <mode>");
 9  if ((fd = open(argv[2], atoi(argv[3]))) < 0)
10   exit((errno > 0) ? errno : 255);
11  if ((n = write_fd(atoi(argv[1]), "", 1, fd)) < 0)
12   exit((errno > 0) ? errno : 255);
13  exit(0);
14 }

Аргументы командной строки

6-7 Поскольку два из трех аргументов командной строки были превращены в символьные строки функцией my_open, они преобразуются обратно в целые числа при помощи функции atoi.

Открытие файла

9-10 Файл открывается с помощью функции open. Если встречается ошибка, статус завершения этого процесса содержит значение переменной errno, соответствующее ошибке функции open.

Передача дескриптора обратно

11-12 Дескриптор передается обратно функцией write_fd, которую мы покажем в следующем листинге. Затем этот процесс завершается, но ранее в этой главе мы сказали, что отправляющий процесс может закрыть переданный дескриптор (это происходит, когда мы вызываем функцию exit), поскольку ядро знает, что дескриптор находится в состоянии передачи («в полете»), и оставляет его открытым для принимающего процесса.

ПРИМЕЧАНИЕ

Статус выхода должен лежать в пределах от 0 до 255. Максимальное значение переменной errno — около 150. Альтернативный способ, при котором не требуется, чтобы значение переменной errno было меньше 256, заключается в том, чтобы передать обратно указание на ошибку в виде обычных данных при вызове функции sendmsg.

В листинге 15.11 показана последняя функция, write_fd, вызывающая функцию sendmsg для отправки дескриптора (и, возможно, еще каких-либо данных, которые мы не используем) через доменный сокет Unix.

Листинг 15.11. Функция write_fd: передача дескриптора при помощи вызова функции sendmsg

//lib/write_fd.c
 1 #include "unp.h"
 2 ssize_t
 3 write_fd(int fd, void *ptr, size_t nbytes, int sendfd)
 4 {
 5  struct msghdr msg;
 6  struct iovec iov[1];
 7 #ifdef HAVE_MSGHDR_MSG_CONTROL
 8  union {
 9   struct cmsghdr cm;
10   char control[CMSG_SPACE(sizeof(int))];
11  } control_un;
12  struct cmsghdr *cmptr;
13  msg.msg_control = control_un.control;
14  msg.msg_controllen = sizeof(control_un.control);
15  cmptr = CMSG_FIRSTHDR(&msg);
16  cmptr->cmsg_len = CMSG_LEN(sizeof(int));
17  cmptr->cmsg_level = SOL_SOCKET;
18  cmptr->cmsg_type = SCM_RIGHTS;
19  *((int*)CMSG_DATA(cmptr)) = sendfd;
20 #else
21  msg.msg_accrights = (caddr_t)&sendfd;
22  msg.msg_accrightslen = sizeof(int);
23 #endif
24  msg.msg_name = NULL;
25  msg.msg_namelen = 0;
26  iov[0].iov_base = ptr;
27  iov[0].iov_len = nbytes;
28  msg.msg_iov = iov;
29  msg.msg_iovlen = 1;
30  return (sendmsg(fd, &msg, 0));
31 }

Как и в случае функции read_fg, эта функция обрабатывает либо вспомогательные данные, либо права доступа, которые предшествовали вспомогательным данным в более ранних реализациях. В любом случае инициализируется структура msghdr и затем вызывается функция sendmsg.

В разделе 28.7 мы приводим пример передачи дескриптора, в котором участвуют неродственные (unrelated) процессы, а в разделе 30.9 — пример, где задействованы родственные процессы. В них мы будем использовать функции read_fd и write_fd, которые только что описали.

Оглавление книги


Генерация: 1.508. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз