27. Unix Domain Socket
Что такое Unix Domain Socket (UDS)
Unix Domain Socket (UDS) — это механизм межпроцессного взаимодействия (IPC), который использует классический API сокетов, но полностью изолирован внутри ядра операционной системы. В отличие от сетевых сокетов, UDS не задействует стек протоколов IP/TCP/UDP, маршрутизацию, ARP или проверку контрольных сумм. Это «частная магистраль» внутри ядра, предназначенная исключительно для обмена данными между процессами на одном хосте.
"Общение двух локальных процессов через сетевой loopback (127.0.0.1) напоминает отправку письма соседу по комнате через почтовое отделение: мы упаковываем байты в конверт TCP-заголовка, наклеиваем марку IP-адреса, прогоняем через сортировочный центр таблицы маршрутизации и отдаем курьеру сетевого драйвера. Unix Domain Socket позволяет просто передать записку из рук в руки через буфер ядра."
Для Go-разработчика UDS критически важен, потому что:
- Docker и контейнеризация используют UDS для связи между демонами и клиентскими CLI (например,
/var/run/docker.sock). - gRPC и HTTP/2 поддерживают UDS как транспорт для локальных микросервисов, экономя ресурсы сетевого стека.
- Он позволяет избежать конфликтов портов и накладных расходов на
localhost-соединения. - Поддерживает передачу файловых дескрипторов (FD passing), что невозможно через обычный TCP.
Типы сокетов и пространства имен (Namespaces)
В ядре Linux UDS поддерживает три типа, реализующие разные семантики:
SOCK_STREAM— надежный, потоковый, ориентированный на байты (аналог TCP). Гарантирует порядок доставки и отсутствие дубликатов.SOCK_DGRAM— пакетный, ориентированный на сообщения (по форме аналог UDP). В отличие от UDP, на Linux UNIX-датаграммы надёжны: они не теряются и не переупорядочиваются (man 7 unix).SOCK_SEQPACKET— надежный, пакетный. Гарантирует доставку целых сообщений в порядке отправки.
UDS использует два типа пространств имен (namespaces) для привязки адреса:
Под капотом: Ядерная реализация и производительность
Когда процесс вызывает socket(AF_UNIX, SOCK_STREAM, 0), ядро создает объект struct sock с привязкой к struct unix_sock. В отличие от сетевого стека, здесь отсутствует очередь SYN, таймауты повторной передачи и алгоритмы управления перегрузкой (congestion control).
Передача данных происходит через буферы ядра (sk_buff), которые копируются из пользовательского пространства процесса-отправителя в ядро, а затем напрямую копируются в адресное пространство процесса-получателя. Современные версии ядра оптимизируют этот путь через unix_sendpage и splice(), позволяя частично обходить копирование буфера (Zero Copy), если данные уже находятся в page cache.
flowchart TB
subgraph LoopbackPath["TCP Loopback (127.0.0.1): Оверхед сетевого стека"]
direction TB
App1["Приложение А"] -->|"Syscall write"| TCP1["TCP Стек (Контрольные суммы, окна)"]
TCP1 --> IP1["IP Стек (Поиск маршрута)"]
IP1 --> Lo["Loopback Драйвер ядра"]
Lo --> IP2["IP Стек (Демультиплексирование)"]
IP2 --> TCP2["TCP Стек (ACK, упорядочивание)"]
TCP2 -->|"Syscall read"| App2["Приложение Б"]
end
subgraph UDSPath["Unix Domain Socket: Прямой путь в ядре"]
direction TB
UApp1["Приложение А"] -->|"copy_from_user"| USock["Ядро: sk_buff / unix_sock буфер"]
USock -->|"copy_to_user"| UApp2["Приложение Б"]
end
style LoopbackPath fill:#1e293b,stroke:#818cf8,color:#fff
style UDSPath fill:#064e3b,stroke:#34d399,color:#fff
Почему UDS быстрее TCP loopback?
При использовании 127.0.0.1:8080 пакет проходит через:
Приложение -> TCP -> IP -> Loopback Driver -> IP -> TCP -> Приложение.
Каждый этап включает парсинг заголовков, проверку контрольных сумм, поиск в таблице маршрутизации и переключение контекстов сетевого стека. UDS пропускает все слои IP/TCP, оставаясь в пространстве AF_UNIX, что дает прирост производительности на 20-40% и снижает задержку на 10-30%.
Идиоматичная работа с UDS в Go
В стандартной библиотеке Go транспорт UDS активируется через тип сети unix.
Сервер
package main
import (
"fmt"
"log"
"net"
"os"
)
func main() {
path := "/tmp/my_service.sock"
// Удаляем старый сокет, если он остался после краша предыдущего запуска
os.Remove(path)
listener, err := net.Listen("unix", path)
if err != nil {
log.Fatalf("Ошибка привязки сокета: %v", err)
}
defer listener.Close()
defer os.Remove(path) // Гарантированная очистка
fmt.Printf("Сервер слушает: %s\n", path)
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("Ошибка accept: %v", err)
continue
}
go handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
log.Printf("Ошибка чтения: %v", err)
return
}
fmt.Printf("Получено: %s\n", string(buf[:n]))
}
Клиент
package main
import (
"log"
"net"
"os"
)
func main() {
path := "/tmp/my_service.sock"
// Проверка существования сокета перед подключением
if _, err := os.Stat(path); os.IsNotExist(err) {
log.Fatalf("Сокет %s не существует", path)
}
conn, err := net.Dial("unix", path)
if err != nil {
log.Fatalf("Ошибка подключения: %v", err)
}
defer conn.Close()
_, err = conn.Write([]byte("Hello UDS"))
if err != nil {
log.Fatalf("Ошибка записи: %v", err)
}
}
Продвинутые возможности и типичные ловушки
Передача файловых дескрипторов (FD Passing)
UDS позволяет передавать открытые файлы, каналы или другие сокеты между процессами через механизм SCM_RIGHTS. Это фундамент для архитектур типа Docker (передача дескриптора stdin/stdout в контейнер) или пулов воркеров.
sequenceDiagram
autonumber
participant ProcessA as Процесс А (Владелец файла)
participant Kernel as Ядро Linux (struct unix_sock)
participant ProcessB as Процесс Б (Воркер)
ProcessA->>ProcessA: Открывает файл: fd = 7
ProcessA->>Kernel: sendmsg(SCM_RIGHTS, fd=7)
Note over Kernel: Ядро увеличивает f_count на struct file
Kernel->>ProcessB: recvmsg() с вспомогательными данными CMSG
Note over Kernel: Ядро выделяет новый FD в таблице Процесса Б (например, fd = 4)
ProcessB->>ProcessB: os.NewFile(4, ...) читает файл напрямую!
// Сервер (отправка FD)
f, err := os.Open("/tmp/data.bin")
if err != nil {
log.Fatal(err)
}
// SCM_RIGHTS: дескриптор уходит как вспомогательные данные (oob) вместе с одним байтом полезной нагрузки.
// conn — *net.UnixConn с установленным соединением с процессом Б.
rights := unix.UnixRights(int(f.Fd()))
if _, _, err := conn.WriteMsgUnix([]byte{'A'}, rights, nil); err != nil {
log.Fatal(err)
}
// Клиент (прием FD)
buf := make([]byte, 1)
oob := make([]byte, unix.CmsgSpace(4)) // место под один int32-дескриптор
_, oobn, _, _, err := conn.ReadMsgUnix(buf, oob)
if err != nil {
log.Fatal(err)
}
msgs, _ := unix.ParseSocketControlMessage(oob[:oobn])
fds, _ := unix.ParseUnixRights(&msgs[0])
// Извлеченный дескриптор оборачивается в os.NewFile(uintptr(fds[0]), "received").
Лимиты путей и права доступа
- Длина пути: Длина пути жестко ограничена размером поля
sun_pathв структуре ядраsockaddr_un(UNIX_PATH_MAX= 108 байт в Linux, 104 байта в BSD/macOS, включая завершающий нулевой байт). Если путь превышает этот лимит, вызовbind()вернет ошибку (EINVALилиENAMETOOLONG). Поэтому для длинных путей в контейнерах и сокетах часто используют относительные пути или абстрактные сокеты. - Права: Файловые UDS наследуют права файловой системы. Если сокет создан от
root, он будет недоступен для обычного пользователя безchmod 777(что опасно). Абстрактные сокеты по умолчанию доступны всем процессам с доступом к ядру.
Сравнение с другими IPC-механизмами
| Механизм | Скорость | Синхронизация | Передача FD | Надежность | Сфера применения в Go |
|---|---|---|---|---|---|
| Unix Domain Socket | Высокая | Встроена (потоки/пакеты) | ✅ SCM_RIGHTS |
✅ Ядро гарантирует доставку | Локальные микросервисы, Docker, gRPC |
| TCP Loopback | Средняя | TCP state machine | ❌ | ✅ Сеть/ядро | Универсально, но избыточно для localhost |
| Pipes / Named Pipes | Высокая | Half-duplex (обычно) | ❌ | ✅ | Локальный обмен потоками, CI/CD пайплайны |
| Shared Memory | Максимальная | Ручная (Mutex/Semaphore) | ❌ | ❌ Риск race condition | Высокочастотный трейдинг, кэширование |
| HTTP/REST | Низкая | Прикладной уровень | ❌ | ✅ Прокси/балансировщики | Кросс-языковая интеграция, API Gateway |
Итог
Unix Domain Socket — это не просто «сокет без сети», а оптимизированный ядерный механизм IPC, который экономит циклы процессора, избегает сетевого стека и предоставляет продвинутые возможности вроде передачи файловых дескрипторов. Для Go-разработчика понимание UDS необходимо для:
- Настройки высокопроизводительных локальных транспортных слоев (gRPC, HTTP/2).
- Интеграции с контейнерными рантаймами и системными демонами.
- Избегания утечек ресурсов (zombie sockets) и проблем с правами доступа.
В следующей статье мы разберем, как Go-программы могут реагировать на внешние события и сигналы ядра: 28. Сигналы в Linux.