ОБЗОР
#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).