ОБЗОР
#include <sys/socket.h>#include <sys/un.h>
unix_socket = socket(AF_UNIX, type, 0);
error = socketpair(AF_UNIX, type, 0, int *sv);
ОПИСАНИЕ
Семейство сокетов AF_UNIX (также известное, как AF_LOCAL) используется для эффективного взаимодействия между процессами на одной машине. Доменные сокеты UNIX могут быть как безымянными, так и иметь имя файла в файловой системе (типизированный сокет). В Linux также поддерживается абстрактное пространство имён, которое не зависит от файловой системы.Допустимые типы сокета для домена UNIX: потоковый сокет SOCK_STREAM, датаграмный сокет SOCK_DGRAM, сохраняющий границы сообщений (в большинстве реализаций UNIX, доменные датаграмные сокеты UNIX всегда надёжны и не меняют порядок датаграмм); и (начиная с Linux 2.6.4) ориентированный на соединение задающий последовательность пакетам сокет SOCK_SEQPACKET, сохраняющий границы сообщений и доставляющий сообщения в том же порядке, в каком они были отправлены.
Доменные сокеты UNIX поддерживают передачу файловых дескрипторов или информацию (credentials) о процессе другим процессам, используя вспомогательные (ancillary) данные.
Формат адреса
Адрес доменного сокета UNIX представляет собой следующую структуру:struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[108]; /* имя пути */ };
Поле sun_family всегда содержит AF_UNIX. В Linux размер sun_path равен 108 байтам; также смотрите ЗАМЕЧАНИЯ ниже.
В различных системных вызовах (например, bind(2), connect(2) и sendto(2)) в качестве входных данных используется параметр sockaddr_un. Другие системные вызовы (например, getsockname(2), getpeername(2), recvfrom(2) и accept(2)) возвращают результат в параметре этого типа.
В sockaddr_un структуре различают три типа адресов:
- *
-
с именем пути: доменный сокет UNIX может быть привязан к имени пути (с
завершающимся null) в файловой системе с помощью bind(2). При возврате
адреса имени пути сокета (одним и системных вызовов, упомянутых выше), его
длина равна
offsetof(struct sockaddr_un, sun_path) + strlen(sun_path) + 1и sun_path содержит путь, оканчивающийся null (в Linux, указанное выше выражение offsetof() равно sizeof(sa_family_t), но в некоторых реализациях включаются другие поля перед sun_path, поэтому выражение offsetof() описывает размер адресной структуры более переносимым способом).
- Дополнительную информацию о путях сокета смотрите далее.
- *
- безымянный: Потоковый сокет, который не привязан к имени пути с помощью bind(), не имеет имени. Аналогично, два сокета, создаваемые socketpair(), также не имеют имён. При возврате адреса сокета его длина равна sizeof(sa_family_t), а значение sun_path не используется.
- *
- абстрактный: абстрактный адрес сокета отличается (от имени пути сокета) тем, что значением sun_path[0] является байт null ('\0'). Адрес сокета в этом пространстве имён определяется дополнительными байтами в sun_path, количество которых определяется длиной указанной структуры адреса. Байты null в имени не имеют специального значения. Имя не связано с именем пути в файловой системе. При возврате адреса абстрактного сокета возвращаемое значение addrlen больше чем sizeof(sa_family_t) (т.е. больше 2), а имя сокета содержится в первых (addrlen - sizeof(sa_family_t)) байтах sun_path. Пространство имён абстрактных сокетов является непереносимым расширением Linux.
Путевые сокеты
При привязке сокета к пути для максимальной переносимости и простоте кодирования нужно учесть несколько правил:- *
- Имя пути в sun_path должно завершаться null.
- *
- Длина имени пути, включая завершающий байт null, не должна превышать размер sun_path.
- *
-
Аргумент addrlen, описывающий включаемую структуру sockaddr_un, должен
содержать значение, как минимум:
offsetof(struct sockaddr_un, sun_path)+strlen(addr.sun_path)+1
- или, проще говоря, для addrlen можно использовать sizeof(struct sockaddr_un).
Есть несколько реализаций по работе с адресами доменных сокетов UNIX, которые не следуют данным правилам. Например, в некоторых реализациях (но не во всех) добавляется конечный null, если если его нет в sun_path.
При написании переносимых приложений учтите, что в некоторых реализациях размер sun_pathравен 92 байтам.
Различные системные вызовы (например, accept(2), recvfrom(2), getsockname(2), getpeername(2)) возвращают адресные структуры сокета. В случае с доменными сокетами UNIX аргумент значение-результат addrlen, передаваемый вызову, должен быть инициализирован как описано выше. При возврате в аргументе содержится реальный размер адресной структуры. Вызывающий должен проверить полученное значение этого аргумента: если оно превышает значение до вызова, то не гарантируется наличие конечного null в sun_path (смотрите ДЕФЕКТЫ).
Параметры сокета
В силу исторических причин эти параметры сокетов относятся к типу SOL_SOCKET, даже если они относятся к AF_UNIX. Они могут быть установлены с помощью setsockopt(2) и прочитаны с помощью getsockopt(2); тип SOL_SOCKET указывается в качестве семейства сокета.- SO_PASSCRED
- Разрешает приём информационных данных (credentials) посылающего процесса во вспомогательном сообщении. Если при включении этого параметра сокет пока ещё не соединён, то в абстрактном пространстве имён будет автоматически создано уникальное имя. Ожидается целочисленный логический флаг.
Свойство автоматической привязки
Если в вызов bind(2) передано значение addrlen равное sizeof(sa_family_t), или для сокета, который не привязан к адресу явно, был указан параметр сокета SO_PASSCRED, то сокет автоматически привязывается к абстрактному адресу. Адрес состоит из байта null и 5 байтов символов из набора [0-9a-f]. Таким образом, максимальное количество автоматически привязываемых адресов равно 2^20 (в Linux 2.1.15, когда была добавлена автоматическая привязка, использовалось 8 байт, и, таким образом, ограничение было 2^32 адресов. В Linux 2.3.15 количество байт сократили до 5).Программный интерфейс сокетов
В следующих параграфах описываются специфичные тонкости доменов и неподдерживаемые возможности программного интерфейса сокетов для доменных сокетов UNIX в Linux.Доменные сокеты UNIX не поддерживают передачу внеполосных данных (флаг MSG_OOB у send(2) и recv(2)).
Флаг MSG_MORE у send(2) не поддерживается доменными сокетами UNIX.
Использование MSG_TRUNC в аргументе flags у recv(2) не поддерживается доменными сокетами UNIX.
Параметр сокета SO_SNDBUF учитывается в доменных сокетах UNIX, а параметр SO_RCVBUF --- нет. Для датаграмных сокетов значение SO_SNDBUF считается максимальным размером для исходящих датаграмм. Это ограничение, вычисляемое как удвоенное значение (см. socket(7)) параметра, содержит меньше 32 байт накладных расходов.
Вспомогательные сообщения
Вспомогательные данные отправляются и принимаются с помощью sendmsg(2) и recvmsg(2). В силу исторических причин перечисленные типы вспомогательных сообщений относятся к типу SOL_SOCKET, даже если они относятся к AF_UNIX. Для того, чтобы отправить их, установите значение поля cmsg_level структуры cmsghdr равным SOL_SOCKET, а в значении поля cmsg_type укажите его тип. Дополнительная информация приведена в cmsg(3).- SCM_RIGHTS
- Передать или принять набор открытых файловых дескрипторов из другого процесса. Часть с данными содержит целочисленный массив файловых дескрипторов. Переданные файловые дескрипторы действуют так, как если бы они были созданы dup(2).
- SCM_CREDENTIALS
-
Передать или принять информацию о UNIX. Может быть использовано для
аутентификации. Информация передаётся в виде структуры struct ucred
вспомогательного сообщения. Эта структура определена в
<sys/socket.h> следующим образом:
struct ucred { pid_t pid; /* идентификатор посылающего процесса */ uid_t uid; /* идентификатор пользователя посылающего процесса */ gid_t gid; /* идентификатор группы посылающего процесса */ };
Начиная с glibc 2.8, чтобы получить определение данной структуры должен быть определён макрос тестирования свойств _GNU_SOURCE (до включения каких-либо заголовочных файлов).
Информация (credentials), указываемая отправителем, проверяется ядром. Процесс с идентификатором эффективного пользователя 0 может указывать значения, отличные от его собственных. Отправитель должен указать идентификатор своего процесса (если только он не имеет мандата CAP_SYS_ADMIN), свой идентификатор пользователя, эффективный идентификатор или сохранённый set-user-ID (если только он не имеет CAP_SETUID) и идентификатор своей группы, эффективный идентификатор группы или сохранённый set-group-ID (если только он не имеет CAP_SETGID). Для получения сообщения со структурой struct ucred для сокета нужно включить параметр SO_PASSCRED.
Вызовы ioctl
Следующие вызовы ioctl(2) возвращают информацию в аргументе value. Корректный синтаксис:
-
int value; error = ioctl(unix_socket, ioctl_type, &value);
Значением ioctl_type может быть:
- SIOCINQ
- Для сокета SOCK_STREAM функция возвращает количество непрочитанных данных в очереди в приёмном буфере. Сокет не должен быть в состоянии LISTEN, иначе возвращается ошибка (EINVAL). Значение SIOCINQ определено в <linux/sockios.h>. В качестве альтернативы вы можете использовать синоним FIONREAD, определённый в <sys/ioctl.h>. Для сокета SOCK_DGRAM возвращаемое значение совпадает с датаграммным сокетом домена Интернета; смотрите udp(7).
ОШИБКИ
- EADDRINUSE
- Заданный локальный адрес уже используется, или сокетный объект файловой системы уже существует.
- ECONNREFUSED
- Удалённый адрес, указанный connect(2) не является слушающим сокетом. Эта ошибка также может возникнуть, если путь назначения не является сокетом.
- ECONNRESET
- Удалённый сокет был неожиданно закрыт.
- EFAULT
- Некорректный адрес пользовательской памяти.
- EINVAL
- Передан неправильный аргумент. Основная причина --- не задано значение AF_UNIX в поле sun_type передаваемых адресов или сокет находится в некорректном состоянии для производимой операции.
- EISCONN
- Вызов connect(2) запущен для уже соединённого сокета, или адрес назначения указывает на соединённый сокет.
- ENOENT
- Путь, указанный в удалённом адресе для connect(2), не существует.
- ENOMEM
- Не хватает памяти.
- ENOTCONN
- Для операции над сокетом требуется адрес назначения, а сокет не соединён.
- EOPNOTSUPP
- Вызвана потоковая операция для непотокового сокета, или произведена попытка использования параметра для внеполосных данных.
- EPERM
- Отправитель указал неправильную информацию (credentials) в структуре struct ucred.
- EPIPE
- Удалённый сокет был закрыт в потоковом сокете. Если разрешено, также будет послан сигнал SIGPIPE. Этого можно избежать, передав флаг MSG_NOSIGNAL при вызове sendmsg(2) или recvmsg(2).
- EPROTONOSUPPORT
- Указанный протокол не является AF_UNIX.
- EPROTOTYPE
- Удалённый сокет не совпадает с типом локального сокета (SOCK_DGRAM против SOCK_STREAM).
- ESOCKTNOSUPPORT
- Неизвестный тип сокета.
При создании сокетного объекта на уровне сокетов или файловой системы могут генерироваться другие ошибки. За дополнительной информацией обращайтесь к соответствующей справочной странице.
ВЕРСИИ
SCM_CREDENTIALS и абстрактное пространство имён появились в Linux 2.2 и не должны использоваться в переносимых программах. Некоторые клоны BSD также поддерживают передачу дополнительной информации (credential), но методы реализации передачи могут серьезно отличаться на разных системах.ЗАМЕЧАНИЯ
В реализации Linux учитываются права доступа к каталогу, в котором находятся сокеты, видимые в файловой системе. Владелец, группа и права могут быть изменены. Если процесс не имеет прав на запись и поиск (запуск) в каталоге, то создать новый сокет в нём не удастся. Соединение сокетных объектов требует права на запись/чтение. Это действие отличается от действий большинства клонов BSD, игнорирующих права доменных сокетов UNIX. Переносимые программы не должны полагаться на эту возможность для обеспечения безопасности.Привязка сокета к имени файла создаёт сокет в файловой системе, который должен быть удалён создателем, когда необходимость в нём отпадёт (с помощью unlink(2)). Обычная система ссылок UNIX также подходит для работы с сокетами; сокет может быть удалён в любое время, а реальное удаление из файловой системы будет произведено при закрытии последней на него ссылки.
Для передачи файловых дескрипторов или информации (credentials) через SOCK_STREAM необходимо передать/принять, по меньшей мере, один байт недополнительных данных в одном из вызовов: sendmsg(2) или recvmsg(2).
В потоковых доменных сокетах UNIX отсутствует такое понятие как внеполосные данные.
ДЕФЕКТЫ
При привязке сокета к адресу Linux является одной из реализаций, которые добавляют конечный null, если он отсутствует в sun_path. В большинстве случаев в этом нет проблемы: когда адрес сокета возвращается, он будет на один байт длиннее чем был перед привязкой сокета. Однако такое неожиданное поведение может привести к следующему: если передаётся 108 не-null байтов при привязке сокета, то с дополнительным конечным null пути превышает длину sizeof(sun_path). В последствии при возврате адреса сокета (например, из accept(2)), если входной аргумент addrlen перед вызовом был равен sizeof(struct sockaddr_un), то в sun_path возвращаемой структуры адреса будет отсутствовать конечный null.Также, некоторые реализации не требуют наличия конечного null при привязке сокета (для определения длины sun_path используется аргумент addrlen) и когда в этих реализациях возвращается адрес сокета, то в sun_path также отсутствует конечный null.
Приложения, которые получают адрес сокета могут содержать код (переносимый) для обработки случая, когда нет конечного null в sun_path, учитывая фактическое количество пригодных байт в пути:
strnlen(addr.sun_path, addrlen - offsetof(sockaddr_un, sun_path))
Или же приложение может перед получением адреса сокета выделить буфер размера sizeof(struct sockaddr_un)+1, который будет обнулён перед возвращением. Возвращающий вызов может задать в addrlen значение sizeof(struct sockaddr_un), и дополнительный нулевой байт здесь будет конечным null в строке, возвращаемой в sun_path:
void *addrp; addrlen = sizeof(struct sockaddr_un); addrp = malloc(addrlen + 1); if (addrp == NULL) /* обработка ошибки */ ; memset(addrp, 0, addrlen + 1); if (getsockname(sfd, (struct sockaddr *) addrp, &addrlen)) == -1) /* обработка ошибки */ ; printf("sun_path = %s\n", ((struct sockaddr_un *) addrp)->sun_path);
Данного беспорядка можно избежать, если гарантировать, что приложения, создающие путевые сокеты, следуют правилам, описанным в общих чертах выше в Путевые сокеты.
ПРИМЕР
В следующем коде демонстрируется использование пакето-упорядочивающих сокетов для локального межпроцессного обмена. Он состоит из двух программ. Программа-сервер ждёт подключения программы-клиента. Клиент посылает свой каждый аргумент командной строки в виде отдельного сообщения. Сервер считает входящие сообщения как целые числа и складывает их. Клиент посылает строку-команду «END». Сервер посылает ответное сообщение, содержащее сумму чисел клиента. Клиент печатает сумму и завершает работу. Сервер ждёт подключение следующего клиента. Для остановки сервера, клиент вызывается с аргументом командной строки «DOWN».Следующий вывод был записан при работе сервера в фоновом режиме и повторяющемся запуске клиента. Выполнение программы-сервера завершилось после получения им команды «DOWN».
Пример вывода
$ ./server & [1] 25887 $ ./client 3 4 Результат = 7 $ ./client 11 -5 Результат = 6 $ ./client DOWN Результат = 0 [1]+ Done ./server $
Исходный код программы
/* * Файл connection.h */ #define SOCKET_NAME "/tmp/9Lq7BNBnBycd6nxy.socket" #define BUFFER_SIZE 12 /* * Файл server.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #include "connection.h" int main(int argc, char *argv[]) { struct sockaddr_un name; int down_flag = 0; int ret; int connection_socket; int data_socket; int result; char buffer[BUFFER_SIZE]; /* * Удалить сокет, оставшийся после последнего * некорректного завершения программы. */ unlink(SOCKET_NAME); /* Создание локального сокета. */ connection_socket = socket(AF_UNIX, SOCK_SEQPACKET, 0); if (connection_socket == -1) { perror("socket"); exit(EXIT_FAILURE); } /* * Для переносимости очищаем всю структуру, так как в некоторых * реализациях имеются дополнительные (нестандартные) поля. */ memset(&name, 0, sizeof(struct sockaddr_un)); /* Привязываем сокет к имени сокета. */ name.sun_family = AF_UNIX; strncpy(name.sun_path, SOCKET_NAME, sizeof(name.sun_path) - 1); ret = bind(connection_socket, (const struct sockaddr *) &name, sizeof(struct sockaddr_un)); if (ret == -1) { perror("bind"); exit(EXIT_FAILURE); } /* * Готовимся принимать подключения. Размер очереди (backlog) * устанавливаем равным 20. Пока один запрос обрабатывается, другие * запросы смогут подождать. */ ret = listen(connection_socket, 20); if (ret == -1) { perror("listen"); exit(EXIT_FAILURE); } /* Основной цикл обработки подключений. */ for (;;) { /* Ожидание входящих подключений. */ data_socket = accept(connection_socket, NULL, NULL); if (ret == -1) { perror("accept"); exit(EXIT_FAILURE); } result = 0; for(;;) { /* Ожидание следующего пакета с данными. */ ret = read(data_socket, buffer, BUFFER_SIZE); if (ret == -1) { perror("read"); exit(EXIT_FAILURE); } /* Проверяем, что буфер завершается 0. */ buffer[BUFFER_SIZE - 1] = 0; /* Обработка команд. */ if (!strncmp(buffer, "DOWN", BUFFER_SIZE)) { down_flag = 1; break; } if (!strncmp(buffer, "END", BUFFER_SIZE)) { break; } /* Добавляем полученную команду. */ result += atoi(buffer); } /* Отправка результата. */ sprintf(buffer, "%d", result); ret = write(data_socket, buffer, BUFFER_SIZE); if (ret == -1) { perror("write"); exit(EXIT_FAILURE); } /* Закрытие сокета. */ close(data_socket); /* Завершаем работу по команде DOWN. */ if (down_flag) { break; } } close(connection_socket); /* Удаляем сокет. */ unlink(SOCKET_NAME); exit(EXIT_SUCCESS); } /* * Файл client.c */ #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #include "connection.h" int main(int argc, char *argv[]) { struct sockaddr_un name; int i; int ret; int data_socket; char buffer[BUFFER_SIZE]; /* Создание локального сокета. */ data_socket = socket(AF_UNIX, SOCK_SEQPACKET, 0); if (data_socket == -1) { perror("socket"); exit(EXIT_FAILURE); } /* * Для переносимости очищаем всю структуру, так как в некоторых * реализациях имеются дополнительные (нестандартные) поля. */ memset(&name, 0, sizeof(struct sockaddr_un)); /* Соединяем сокет с именем сокета. */ name.sun_family = AF_UNIX; strncpy(name.sun_path, SOCKET_NAME, sizeof(name.sun_path) - 1); ret = connect (data_socket, (const struct sockaddr *) &name, sizeof(struct sockaddr_un)); if (ret == -1) { fprintf(stderr, "Сервер выключен.\n"); exit(EXIT_FAILURE); } /* Посылаем аргументы. */ for (i = 1; i < argc; ++i) { ret = write(data_socket, argv[i], strlen(argv[i]) + 1); if (ret == -1) { perror("write"); break; } } /* Отправка результата. */ strcpy (buffer, "END"); ret = write(data_socket, buffer, strlen(buffer) + 1); if (ret == -1) { perror("write"); exit(EXIT_FAILURE); } /* Получение результата. */ ret = read(data_socket, buffer, BUFFER_SIZE); if (ret == -1) { perror("read"); exit(EXIT_FAILURE); } /* Проверяем, что буфер завершается 0. */ buffer[BUFFER_SIZE - 1] = 0; printf("Result = %s\n", buffer); /* Закрытие сокета. */ close(data_socket); exit(EXIT_SUCCESS); }
Пример использования SCM_RIGHTS приведён в cmsg(3).