Книга: 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-26recvmsg
: с элементом 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
, которые только что описали.
- Пример
- Пример установочного скрипта
- Пример из практики
- ПРИМЕР ПРОСТОЙ ПРОГРАММЫ НА ЯЗЫКЕ СИ
- Примеры получения статистики
- Пример применения метода «пять почему»
- Пример 12-8. Частота встречаемости отдельных слов
- 1.2.5. Пример программы
- 1.2. Понятие информации. Общая характеристика процессов сбора, передачи, обработки и накопления информации
- Пример 17-10. Блочный комментарий
- Примеры
- 2. Пример создания базового отношения в записи на псевдокоде