第 57 章 UNIX 域 Sockets (Sockets: UNIX Domain)
核心结论
-
UNIX 域 socket 定位:本机进程间通信——比 127.0.0.1 loopback 快(不经网络栈);寻址用文件系统路径名。
-
sockaddr_un 结构:
struct sockaddr_un { sa_family_t sun_family; char sun_path[108]; }——sun_path 是 null 结尾路径;早期 BSD 108 字节,HP-UX 92 字节,可移植按 92 字节。 -
Stream socket(典型客户端-服务器):服务器 socket→bind→listen→accept(阻塞)→读写→close;客户端 socket→connect→读写→close;与 pipe 类似但双向、可寻址。
-
Datagram socket(UNIX 域可靠!):与网络 datagram 不同——UNIX 域 datagram 是在内核中传递,可靠 + 有序 + 不重复;最大尺寸受 SO_SNDBUF 限制。
-
权限控制:bind 创建 socket 文件——所有权/权限同普通文件;connect/sendto 需要写权限;父目录需要执行权限;某些系统忽略 socket 文件权限(可移植依赖父目录权限)。
-
socketpair():仅 AF_UNIX——一次创建两个已连接 socket;BSD pipe() 内部用 socketpair 实现;典型用于 fork 后父子通信。
-
Linux abstract 命名空间:sun_path[0] = '\0' 启用——socket 名不在文件系统中;不需要 unlink;适合 chroot 环境。
-
必须 unlink socket 文件:bind 失败若路径已存在(EADDRINUSE);服务器退出前 unlink;stream 客户端 close 后不删除 socket 文件(bind 的责任)。
|
本章主旨
UNIX 域 socket 是「本机 socket」——比 pipe 多了寻址与访问控制,比 AF_INET 127.0.0.1 快(不经网络栈)。读者需要建立的核心模型:(1) 寻址 = 文件系统路径名(可移植);(2) 权限 = 文件权限(同 chown/chmod);(3) 类型 = stream/dgram 与 AF_INET 一致;(4) UNIX 域 dgram 是可靠的(与网络 dgram 不同);(5) abstract namespace 是 Linux 扩展(sun_path[0]=0)。本章配套代码 |
一、核心概念
本章围绕 6 个核心概念展开:sockaddr_un、stream 流程、datagram 流程、权限控制、socketpair、abstract namespace。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
sockaddr_un 地址结构 |
|
§57.1;memset 清零整个结构;strncpy 写入路径(保留末尾 null);可移植按 92 字节 |
Stream socket 客户端-服务器 |
服务器:socket→bind→listen→accept(阻塞)→read/write→close;客户端:socket→connect→read/write→close;服务器先 remove() 再 bind 防 EADDRINUSE |
§57.2; |
Datagram socket(UNIX 域可靠) |
UNIX 域 datagram 在内核中传递——可靠、有序、不重复;与 AF_INET UDP 行为不同;最大尺寸受 SO_SNDBUB 等限制(Linux 可大,其他 UNIX 可能 2048 字节限制) |
§57.3; |
socket 权限 |
bind 创建 socket 文件——权限同普通文件;connect/sendto 需要写权限;父目录需要执行权限;某些系统忽略 socket 文件权限(可移植靠父目录权限) |
§57.4;umask() 调整默认权限;socket 文件 |
socketpair() |
|
§57.5;stream 双向 pipe;2.6.27+ 可 OR SOCK_CLOEXEC / SOCK_NONBLOCK |
Linux abstract namespace |
sun_path[0] = '\0' 启用——socket 名不在文件系统;不需要 unlink;自动随 socket 关闭消失;适合 chroot / 不可写 fs 环境 |
§57.6;abstract 名按字节解释(不是 null 结尾);其他 UNIX 不支持(bind 失败);意外触发 bug(空字符串会创建 abstract) |
二、详细笔记
57.1 sockaddr_un 与 bind
What:struct sockaddr_un 是 UNIX 域地址结构;bind() 创建 socket 文件。
Why:用文件系统路径名寻址——直观;可用文件权限控制访问。
How:
// 摘自《The Linux Programming Interface》 第 57 章
#include <sys/un.h>
#include <sys/socket.h>
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* null 结尾路径 */
};
/* bind 模板 */
const char *SOCKNAME = "/tmp/mysock";
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr)); /* 清零整个结构 */
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCKNAME, sizeof(addr.sun_path) - 1);
if (bind(sfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) errExit("bind");
When:(1) bind 前先 remove() 旧 socket 文件——避免 EADDRINUSE;(2) bind 需父目录可写;(3) 程序退出前 unlink——避免遗留 socket 文件。
Example:/tmp/us_xfr 是典型路径——ls -lF /tmp/us_xfr 显示 srwxr-xr-x 1 … /tmp/us_xfr=(s 类型,= 后缀)。
57.2 Stream socket 客户端-服务器
What:UNIX 域 stream socket 是「带寻址的 pipe」——双向、可靠、字节流。
Why:(1) 比 pipe 多寻址(多个客户端可连同一服务器);(2) 比 AF_INET 127.0.0.1 快(不经网络栈);(3) 可用文件权限控制访问。
How:
// 摘自《The Linux Programming Interface》 第 57 章
/* us_xfr_sv.c 服务器 */
sfd = socket(AF_UNIX, SOCK_STREAM, 0);
remove(SV_SOCK_PATH); /* 防 EADDRINUSE */
bind(sfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sfd, BACKLOG);
for (;;) {
cfd = accept(sfd, NULL, NULL); /* 阻塞 */
while ((numRead = read(cfd, buf, BUF_SIZE)) > 0)
write(STDOUT_FILENO, buf, numRead);
close(cfd);
}
/* us_xfr_cl.c 客户端 */
sfd = socket(AF_UNIX, SOCK_STREAM, 0);
connect(sfd, (struct sockaddr*)&addr, sizeof(addr));
while ((numRead = read(STDIN_FILENO, buf, BUF_SIZE)) > 0)
write(sfd, buf, numRead);
close(sfd); /* 触发对端 EOF */
When:(1) 本机客户端-服务器——首选 UNIX stream(比 TCP 快);(2) 需要访问控制——文件权限;(3) 高并发——iterative 不够,改用 fork 或线程。
Example:./us_xfr_sv > b & 后台服务器;./us_xfr_cl < a 客户端——diff a b 验证数据一致。
57.3 Datagram socket(UNIX 域可靠!)
What:UNIX 域 datagram 在内核中传递——可靠、有序、不重复。
Why:比 stream 简单(不需 listen/accept);适合「查询-响应」模式。
How:
// 摘自《The Linux Programming Interface》 第 57 章
/* ud_ucase_sv.c 服务器 */
sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
remove(SV_SOCK_PATH);
bind(sfd, (struct sockaddr*)&svaddr, sizeof(svaddr));
for (;;) {
len = sizeof(claddr);
numBytes = recvfrom(sfd, buf, BUF_SIZE, 0,
(struct sockaddr*)&claddr, &len);
for (j = 0; j < numBytes; j++)
buf[j] = toupper((unsigned char)buf[j]);
sendto(sfd, buf, numBytes, 0,
(struct sockaddr*)&claddr, len);
}
/* ud_ucase_cl.c 客户端 */
sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
/* bind 唯一路径让服务器能回应 */
snprintf(claddr.sun_path, sizeof(claddr.sun_path),
"/tmp/ud_ucase_cl.%ld", (long)getpid());
bind(sfd, (struct sockaddr*)&claddr, sizeof(claddr));
for (j = 1; j < argc; j++) {
sendto(sfd, argv[j], strlen(argv[j]), 0,
(struct sockaddr*)&svaddr, sizeof(svaddr));
recvfrom(sfd, resp, BUF_SIZE, 0, NULL, NULL);
}
remove(claddr.sun_path);
When:(1) 查询-响应类应用——UNIX dgram 简单可靠;(2) 比 stream 资源占用少;(3) 多个客户端独立(不需 fork)。
Example:./ud_ucase_cl hello world 发两个消息——Response 1: HELLO、Response 2: WORLD。
57.4 socket 权限与安全
What:bind 创建的 socket 文件所有权/权限同普通文件;访问依赖文件权限。
Why:用文件权限控制访问——简单、可与现有文件权限模型集成。
How:
| 操作 | 所需权限 | bind |
|---|---|---|
父目录 w+x |
connect(stream) |
socket 文件 w |
sendto(datagram) |
socket 文件 w |
任何 socket 操作 |
路径每段目录 x |
默认权限 |
0666 & ~umask(所有用户可读写) |
When:(1) 公开服务——umask 022 创建 0755 权限;(2) 私有服务——umask 077 创建 0700;(3) 公开目录(/tmp)——小心恶意抢占路径(用 chdir+bind 或 abstract)。
Example:umask 007; bind("/tmp/us_xfr") 创建 srwxrwx---——仅 user/group 访问;其他用户即使知道路径也无法 connect。
57.5 socketpair() 创建已连接对
What:socketpair(AF_UNIX, type, 0, sv) 一次创建两个已连接 socket。
Why:(1) 替代 pipe()——socket 是双向的,pipe 是单向;(2) BSD pipe() 内部用 socketpair;(3) fork 后父子通信。
How:
// 摘自《The Linux Programming Interface》 第 57 章
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
int sv[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sv); /* 双向 pipe */
if (fork() == 0) {
/* 子进程 */
close(sv[0]);
write(sv[1], "hello", 5);
read(sv[1], buf, sizeof(buf)); /* sv[1] 仍可读 */
_exit(0);
} else {
/* 父进程 */
close(sv[1]);
read(sv[0], buf, sizeof(buf));
write(sv[0], "world", 5);
}
When:(1) fork 后父子通信——socketpair 比 pipe 灵活(双向);(2) pthread——pipe 即可,socketpair 没必要;(3) 避免 socket 文件的路径冲突——socketpair 不绑地址。
Example:第 57 章 §57.5——socketpair 等价于创建一对 connected stream socket;BSD pipe() 内部实现。
57.6 Linux abstract namespace
What:sun_path[0] = '\0' 创建 abstract socket——名字不在文件系统;自动清理。
Why:(1) 避免路径冲突;(2) 不需 unlink;(3) chroot 环境(不可写 fs)。
How:
// 摘自《The Linux Programming Interface》 第 57 章
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
/* addr.sun_path[0] 已是 0(memset 后);abstract 名是 "xyz\0..." */
addr.sun_path[1] = 'x';
addr.sun_path[2] = 'y';
addr.sun_path[3] = 'z';
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
/* 不需 unlink;socket 关闭时自动消失 */
abstract 名按字节解释(不是 null 结尾)——可包含 null 字节。
When:(1) 避免 /tmp 路径冲突——abstract 名是私有命名空间;(2) chroot 环境——没文件系统;(3) 短命 socket——免 unlink。
Example:第 57 章 §57.6——sun_path[0]=0; sun_path[1..3]="xyz" 创建 abstract 名 "\0xyz";ls 看不到,但 connect 可以。
57.7 UNIX 域 vs AF_INET loopback
What:UNIX 域 vs 127.0.0.1 loopback 都能本机通信——UNIX 域更快。
Why:性能与功能取舍。
How:
| 维度 | UNIX 域(AF_UNIX) | AF_INET 127.0.0.1 |
|---|---|---|
网络栈 |
不经 |
经(loopback) |
性能 |
更快(零拷贝) |
慢一点(经协议栈) |
寻址 |
路径名 |
IP + port |
权限控制 |
文件权限 |
端口权限 + 防火墙 |
跨主机 |
不行 |
可以(换 IP) |
可移植性 |
POSIX.1g |
POSIX.1g + BSD |
When:(1) 仅本机——首选 UNIX 域;(2) 需跨主机——AF_INET/AF_INET6;(3) 已有 IPv6 only 环境——AF_INET6 loopback(::1)。
Example:本机服务用 UNIX stream socket 比 127.0.0.1:8080 快约 10-50%(无网络栈开销)。
三、关键图表
|
非可视化条目(API / 系统调用)
|
四、思维导图
mindmap
root((第 57 章 UNIX 域 Sockets))
sockaddr_un
sun_family AF_UNIX
sun_path 路径
108 字节
可移植 92 字节
memset 清零
Stream 流程
服务器 bind listen
服务器 accept 阻塞
客户端 connect
read write 双向
close EOF
remove 防 EADDRINUSE
Datagram 流程
UNIX 域可靠
内核中传递
有序不重复
服务器 bind recvfrom
客户端 sendto recvfrom
过长截断
权限控制
bind 创建 socket 文件
0666 默认
umask 调整
connect 需写
父目录需执行
chown chmod 调整
socketpair
AF_UNIX only
一次两 socket
BSD pipe 内部
fork 后父子用
不绑地址安全
abstract namespace
sun_path 0 启用
不在文件系统
自动清理
chroot 友好
Linux 特有
== 五、重点与易错点
. *bind 前必须 remove 旧文件*——否则 EADDRINUSE;服务器程序常见模式 `remove(SV_SOCK_PATH); bind(...)`。
. *close 不删除 socket 文件*——bind 创建的文件需 unlink;否则进程退出后文件残留。
. *UNIX 域 datagram 可靠*——与 AF_INET UDP 不同;UNIX 域在内核传递,有序、不重复;不要套用 UDP 的不可靠假设。
. *客户端 UDP 也可 bind*——为让服务器能 sendto 回应;典型 `bind("/tmp/cl.%ld", getpid())` 用 PID 后缀唯一化。
. *socketpair 仅 AF_UNIX*——socketpair 不能跨主机;BSD pipe() 内部用 socketpair。
. *socketpair 等价双向 pipe*——比 pipe 灵活(双向、可用 socket 选项)。
. *Abstract namespace 是 Linux 特有*——sun_path[0]='\0';其他 UNIX bind 失败;可移植代码不应依赖。
. *abstract 名按字节解释*——不是 null 结尾;可包含 null 字节;不同长度不同名。
. *bind 父目录需可写*——`/tmp` 通常可写;chroot 环境需提前创建 socket 目录;abstract 绕过此限制。
. *sun_path 长度 92/104/108*——可移植代码用 snprintf 或 strncpy 避免越界;不要硬编码 108。
. *socket 文件权限可被忽略*——某些系统(SUSv3 允许)忽略 socket 文件权限;可移植靠父目录权限。
. *iterative 服务器一次一个客户端*——`us_xfr_sv` 是示例;高并发需 fork 或线程池;select/poll/epoll 多路复用。
. *bind 路径冲突安全风险*——`/tmp` 是 sticky 位目录,非特权只能 unlink 自己的;但 bind 创建前可被恶意抢占;abstract namespace 解决。
. *UNIX 域 vs AF_INET 127.0.0.1*——UNIX 域更快(不经网络栈);仅本机用 UNIX 域;跨主机必须用 AF_INET/AF_INET6。
. *跨章衔接*:第 44 章 pipe/FIFO;第 56 章 socket 总览;第 58 章 TCP/IP;第 59 章 Internet 域 socket;第 60 章 socket 服务器设计(多路复用、并发)。