Unix Domain Socket(UDS,也叫 AF_UNIX / AF_LOCAL socket)是 Linux 上用于同一台机器内进程间通信的 socket 机制。它仍然沿用 socketbindlistenacceptconnectsendrecv 这一套编程接口,但地址不再是 IP + 端口,而通常是一个本地路径,例如 ./server.sock/dev/shm/fpm-cgi.sock

UDS 常见类型包括:

  • SOCK_STREAM:面向连接的字节流,使用方式最像 TCP。
  • SOCK_DGRAM:保留消息边界的数据报,在 Linux 上本地 UDS 数据报可靠且不乱序。
  • SOCK_SEQPACKET:面向连接、保留消息边界、按序交付。

相比 127.0.0.1 本机 TCP,UDS 的核心优势是少走很多网络协议栈路径:不需要 IP 层路由、TCP 协议处理、邻居子系统、loopback 虚拟网卡和软中断回环。它在内核里更多表现为“找到对端 socket,然后把数据挂到对端接收队列”。

一、UDS连接过程

先看一段简单的使用源码:

这张图展示了 UDS 的服务端和客户端代码形态。可以看到它和 TCP socket 的 API 非常像:

  • 服务端通过 socket(AF_UNIX, SOCK_STREAM, 0) 创建一个 Unix domain stream socket。
  • 服务端通过 bind 把 socket 绑定到一个路径,例如 ./server.sock。这个路径是服务端身份标识,作用类似 TCP 里的 IP + 端口。
  • 服务端通过 listen 进入监听状态,再用 accept 获取客户端连接。
  • 客户端同样创建 AF_UNIX socket,然后通过 connect 连接服务端路径。

差别不在用户态 API,而在内核实现路径。TCP 的 connect 会涉及三次握手、连接状态机、重传定时器、半连接队列、全连接队列等机制;UDS 面向本机进程,连接建立可以在内核里直接完成。

注意:路径形式的 UDS socket 会在文件系统中留下一个 socket 文件,服务退出时通常要 unlink 清理;Linux 还支持抽象命名空间 socket,不依赖文件系统路径。

来看其中系统的源码:

这张图截取的是 net/unix/af_unix.cunix_stream_connect 的关键逻辑。对于 SOCK_STREAM 类型的 UDS,客户端调用 connect 后,内核大致做四件事:

  1. 为服务端这一侧新建一个 sock 对象,也就是图中的 newsk。这个对象将来会被服务进程通过 accept 拿到。
  2. 申请一个 skb,并把新建的 newsk 和这个 skb 关联起来。这里的 skb 更像“连接通知载体”,不是普通意义上的网络包。
  3. 通过 unix_peer(newsk) = skunix_peer(sk) = newsk 建立两端 sock 的互相引用,并把状态设置成 TCP_ESTABLISHED。这里复用了 socket 层的状态命名,但并不表示真的走了 TCP 握手。
  4. 把带有 newsk 信息的 skb 放到监听 socket 的 sk_receive_queue 中。服务端调用 accept 时,就能从这个队列取出新连接。

图示如下:

这张图把源码过程画成了连接流程:

  • 左侧客户端调用 connect,内核创建一对已经互相关联的 socket 端点。
  • 中间的 socket_1socket_2 分别代表连接双方的内核 sock
  • 右侧服务端监听 socket 的 receive_queue 中收到一个 skb,这个 skb 指向服务端侧的新 socket。
  • 服务端执行 accept 时,不需要等待对端网络报文,而是从监听 socket 的接收队列中取出这个新 socket。

UDS socket 的连接建立过程非常直接:没有 TCP 三次握手,也没有半连接队列、全连接队列、超时重传定时器等网络协议机制。核心动作就是让两个 sock 结构体通过 unix_peer 互相指向对方,再把服务端侧的新连接挂到监听 socket 的接收队列中。

二、UDS发送过程

流程如下:

这张图展示的是通用 socket 发送入口。用户态调用 send 后,内核通常会转换到 sendto / sock_sendmsg 这一类通用 socket 层逻辑。这个阶段主要做两件事:

  • 根据文件描述符找到内核中的 socket 对象。
  • 构造 msghdr,把用户传入的 buffer、长度、flag 等参数整理成内核统一的数据结构。

真正进入哪个协议族的发送函数,取决于 socket->ops。如果是 TCP/IPv4,会走 inet_stream_ops 里的发送实现;如果是 UDS,则会分发到 unix_stream_sendmsg。所以从系统调用入口看,UDS 和 TCP 很相似;从协议族回调开始,路径就明显分叉了。

真正不一样的地方在这里:

这张图是 unix_stream_sendmsg 的核心逻辑。UDS 发送数据时,不需要把数据交给 IP 层、TCP 层、路由和网卡设备子系统,而是:

  1. 通过 unix_peer(sk) 找到当前 socket 对应的对端 sock,也就是变量 other
  2. 申请一个 skb 作为内核缓冲区。
  3. 把用户态要发送的数据拷贝进这个 skb
  4. 调用 skb_queue_tail(&other->sk_receive_queue, skb),把数据直接挂到对端 socket 的接收队列。
  5. 通知对端有数据可读,对端阻塞在 recv / read 上的进程就可以被唤醒。

也就是说,UDS 的“发送”本质上是一次本机内核队列投递:发送方把数据包装成 skb,然后直接入队到接收方的 sk_receive_queue

接收方:

这张图展示的是 unix_stream_recvmsg / unix_stream_read_generic 的接收路径。接收端不需要等协议栈从下往上交付报文,而是直接从自己的 sk_receive_queue 中取出 skb 处理:

  • recvmsg 进入 UDS 的接收函数 unix_stream_recvmsg
  • 内核构造读取状态,然后调用通用读取逻辑。
  • 读取逻辑查看当前 socket 的接收队列,例如图中的 skb_peek(&sk->sk_receive_queue)
  • 找到 skb 后,再把其中的数据拷贝回用户态 buffer。

这就是 UDS 比本机 TCP 更短的关键路径:接收方看到的队列,就是发送方刚刚直接投递的队列。

因此整体的流程如图:

这张总图可以按下面的链路理解:

  1. 客户端进程调用 send(fd, buf, ...)
  2. 系统调用进入内核,分发到 UDS 的 unix_stream_sendmsg
  3. unix_stream_sendmsg 找到 peer socket,把包含用户数据的 skb 放入对端 receive_queue
  4. 服务端进程阻塞在 recvfrom / recvmsg / read 上,数据到达后被唤醒。
  5. 服务端进入 unix_stream_recvmsg,从自己的 receive_queue 取出 skb 并读取数据。

整个过程没有 IP 包封装、TCP 报文处理、路由选择、邻居子系统、loopback 设备发送、软中断回环这一串网络路径。

三、UDS性能对比

这张图对比了 127.0.0.1 本机 TCP 和 UDS 的整体链路。

左边的 127.0.0.1 本机通信不需要经过物理网卡,拔掉网线也不影响它工作。但它仍然是一条“网络协议栈路径”:

  • 用户进程调用 send
  • 数据进入 TCP/IP 协议栈,经过传输层、网络层。
  • 路由查询会命中 local 路由表,并选择 lo 这个 loopback 虚拟网卡。
  • 数据进入网络设备子系统和 loopback 的发送逻辑。
  • loopback 把 skb 放入本机接收路径,触发软中断。
  • 软中断再把数据送回接收协议栈,最后唤醒接收进程。

右边的 UDS 通信则短很多:发送方通过 unix_stream_sendmsgskb 直接放到对端 socket 的 receive_queue,接收方通过 unix_stream_recvmsg 从这个队列读取。它绕开了 TCP/IP 协议栈中大量只为网络通信服务的步骤。

这张图是延迟测试结果,测试源码来自 rigtorp/ipc-bench,测试环境是 4 核 CPU、8G 内存的 KVM 虚机。

从数据看:

  • 小包 100 字节:UDS 平均延迟约 2707 ns,TCP 约 5690 ns,TCP 耗时大约是 UDS 的 2 倍。
  • 中包 1000 字节:UDS 约 2753 ns,TCP 约 5627 ns,仍然接近 2 倍差距。
  • 较大包 100000 字节:UDS 约 24175 ns,TCP 约 32683 ns,UDS 仍更快,但倍数优势缩小。

原因是小包场景里,每次收发的数据很少,系统调用、协议栈、路由、软中断等固定开销占比非常高;UDS 省掉这些固定路径后收益明显。包变大以后,数据拷贝、内存访问、缓存行为等成本占比上升,协议栈开销被摊薄,所以 UDS 仍快,但不再是严格的 2 倍。

这张图是吞吐测试结果:

  • 小包 100 字节:UDS 平均每秒消息数约 1068321,带宽约 854 MB/s;TCP 平均每秒消息数约 483059,带宽约 386 MB/s
  • 中包 1000 字节:UDS 带宽约 7350 MB/s,TCP 约 3293 MB/s
  • 较大包 30000 字节:UDS 带宽约 36118 MB/s,TCP 约 29649 MB/s

吞吐结论和延迟结论一致:小包和中包时,UDS 避免协议栈固定开销,吞吐优势很明显;包变大后,主要瓶颈逐渐转向内存拷贝和内存带宽,UDS 的相对优势会缩小。

实际选型时可以这样记:

  • 同机进程通信、性能敏感、没有跨机器需求:优先考虑 UDS。
  • 需要跨机器、跨主机网络、负载均衡或透明迁移:仍然需要 TCP/IP。
  • 使用路径 socket 时,要注意 socket 文件权限、路径长度限制、服务退出后的清理。
  • UDS 还能通过辅助数据传递文件描述符和进程凭证,这是 TCP 做不到的本机 IPC 能力。

四、扩展阅读