作者: andylin02
学习章节: 第十五章 Unix域协议
关键词: Unix域协议, AF_UNIX, AF_LOCAL, sockaddr_un, 本地IPC, 传递描述符, socketpair, 凭证, 抽象命名空间, SOCK_SEQPACKET


一、章节概述

1.1 本章焦点

第十五章讨论的是Unix域协议(Unix Domain Protocol)。Unix域协议并非一个实际的协议族,而是在单台主机上执行客户/服务器通信的一种方法,所用的API与在不同主机上执行客户/服务器通信所用的套接字API完全相同。

Unix域提供两类套接字:字节流套接字(SOCK_STREAM,类似TCP)和数据报套接字(SOCK_DGRAM,类似UDP),此外Linux自2.6.4起还支持SOCK_SEQPACKET类型,提供有序分组套接字。需要特别注意的是,本书作者曾指出Unix域数据报套接字是不可靠的,但这一说法已过时。在现代多数实现中,Unix域套接字(无论是数据报还是字节流套接字)都是可靠的。

💡 本章核心价值:读完第十五章,你将能够——

  • 理解Unix域协议的使用场景和三大核心优势
  • 掌握Unix域套接字地址结构(sockaddr_un)的正确使用方法
  • 使用Unix域套接字重写TCP客户/服务器程序
  • 使用socketpair进行父子进程间高效通信
  • 通过辅助数据在不同进程间传递文件描述符
  • 理解并应用Linux的抽象命名空间特性

二、为什么要使用Unix域协议

2.1 三大核心优势

使用Unix域协议主要有以下三个理由:

优势 说明 性能数据
性能优势 在源自Berkeley的实现中,Unix域套接字往往比通信两端位于同一主机上的TCP套接字快出一倍 比TCP环回快10%-50%,某些场景下可达2-3倍
传递描述符 可在同一主机上的不同进程间传递文件描述符
客户凭证 较新的实现可将客户的凭证(用户ID和组ID)提供给服务器,提供额外的安全检查措施

Unix域套接字之所以快,是因为它们绕过了整个网络协议栈,无需进行数据包的打包拆包、校验和计算、路由表查找等操作,也减少了上下文切换次数。X Window System就充分利用了这个优势:当一个X11客户启动时,会检查DISPLAY环境变量,如果服务器与客户在同一主机,客户就打开到服务器的Unix域字节流连接,否则打开到服务器的TCP连接。

2.2 Unix域套接字 vs TCP环回性能对比

对比维度 Unix域套接字 TCP环回
协议栈开销 极小,绕过网络协议栈 完整的TCP/IP协议栈处理
数据包头部 无需网络协议头部 需添加TCP/IP头部
校验和计算 通常不需要 需要计算
上下文切换 较少 较多
典型吞吐量 显著更高(30%-100%提升) 基准水平
典型延迟 显著更低 基准水平

在实际基准测试中,Unix域套接字传输100GB数据仅需80.6秒,平均延迟14.46微秒,而AF_INET环回所需时间显著更长。Redis的基准测试也表明,Unix域套接字可以显著快于TCP环回。

三、Unix域套接字地址结构

3.1 sockaddr_un结构

Unix域套接字的地址结构定义在头文件<sys/un.h>中:

#include <sys/un.h>

struct sockaddr_un {
    sa_family_t sun_family;    // 地址族:AF_UNIX 或 AF_LOCAL
    char        sun_path[108]; // 路径名(以空字符结尾)
};

3.2 地址结构字段详解

字段 类型 说明
sun_family sa_family_t 必须设置为AF_UNIXAF_LOCALAF_LOCAL是POSIX对AF_UNIX的重命名,两者在大多数Unix-like系统中等价
sun_path char[108] 文件系统中的路径名,必须以空字符(\0)结尾。路径名长度限制源于4.2 BSD的实现细节,要求本结构能装到128字节的内核缓冲区中

💡 关键理解

  • POSIX规范没有定义sun_path数组的具体大小,建议运行时使用sizeof运算符来获取结构长度,再判断路径名是否能存入其中。
  • SUN_LEN宏接受指向sockaddr_un结构的指针,返回该结构的大小(不包括路径名的空字符):
    #define SUN_LEN(ptr) ((size_t)(((struct sockaddr_un *)0)->sun_path) + strlen((ptr)->sun_path))
    

3.3 bind调用示例

#include "unp.h"

int main(int argc, char **argv)
{
    int sockfd;
    socklen_t len;
    struct sockaddr_un addr1, addr2;

    if (argc != 2)
        err_quit("usage: unixbind <pathname>");

    sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);
    unlink(argv[1]);                     // 如果文件已存在,先删除

    bzero(&addr1, sizeof(addr1));
    addr1.sun_family = AF_LOCAL;
    strncpy(addr1.sun_path, argv[1], sizeof(addr1.sun_path) - 1);
    Bind(sockfd, (SA *)&addr1, SUN_LEN(&addr1));

    len = sizeof(addr2);
    Getsockname(sockfd, (SA *)&addr2, &len);
    printf("bound name = %s, returned len = %d\n", addr2.sun_path, len);

    return 0;
}

运行后会在文件系统中创建一个类型为socket的特殊文件,如:

$ ./unixbind /tmp/moose
bound name = /tmp/moose, returned len = 13
$ ls -l /tmp/moose
srwxrwxr-x 1 user user 0 10月 8 22:18 /tmp/moose

3.4 三种地址类型

根据sun_path的内容,sockaddr_un结构区分三种地址类型:

地址类型 识别方式 说明
普通路径名 sun_path[0] != '\0' 绑定到文件系统中的路径名,bind时在文件系统中创建socket文件
未命名 sun_path[0] == '\0'且长度为0 使用socketpair创建的匿名套接字,没有bind
抽象命名空间 sun_path[0] == '\0'且后续有非空内容 Linux特有特性,不在文件系统中创建文件

四、Unix域套接字编程要点

4.1 关键注意事项

注意事项 说明
bind创建的路径名权限 默认访问权限应为0777,并按当前umask值进行修正
路径名应为绝对路径 避免使用相对路径,因为相对路径的解析依赖于调用者的当前工作目录
unlink预删除 服务器在bind之前应调用unlink删除可能残留的套接字文件
connect的权限检查 调用connect连接Unix域套接字涉及的权限测试等同于调用open以只写方式访问相应的路径名
队列满时的行为 对Unix域字节流套接字的connect调用如果发现监听套接字的队列已满,调用立即返回ECONNREFUSED错误
数据报套接字必须bind 在未绑定的Unix域套接字上发送数据报不会自动给这个套接字捆绑一个路径名,与UDP不同
数据报套接字的可靠性 当前大多数Unix域数据报套接字实现都是可靠的,不会丢包或乱序

五、socketpair函数

5.1 函数原型

socketpair函数创建一对未命名的、相互连接的套接字:

#include <sys/socket.h>

int socketpair(int domain, int type, int protocol, int sv[2]);
// 返回值:成功返回0,出错返回-1

5.2 参数说明

参数 说明
domain 协议族,必须是AF_UNIXAF_LOCAL(Linux上仅支持这两个域)
type 套接字类型:SOCK_STREAMSOCK_DGRAM
protocol 协议,通常设为0
sv[2] 返回的两个套接字描述符,两个描述符不可区分

💡 关键理解

  • sv[0]写入的数据可从sv[1]读取,反之亦然——这是一条双向通信通道,不需要bind。
  • 与pipe不同,socketpair创建的通道是全双工的,而pipe是半双工的。
  • 由于其高效的特性,socketpair特别适合用于父子进程或同一进程内的线程间通信。

5.3 socketpair使用示例

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>

#define CHAR_BUFSIZE 50

int main()
{
    int fd[2], len;
    char message[CHAR_BUFSIZE];

    // 创建一对已连接的Unix域套接字
    if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fd) == -1)
        return 1;

    // 向fd[0]写入数据,可从fd[1]读取;反之亦然
    snprintf(message, CHAR_BUFSIZE, "A message written to fd[0]");
    write(fd[0], message, strlen(message) + 1);

    snprintf(message, CHAR_BUFSIZE, "A message written to fd[1]");
    write(fd[1], message, strlen(message) + 1);

    // 从fd[0]读取写入fd[1]的数据
    len = read(fd[0], message, CHAR_BUFSIZE - 1);
    message[len] = '\0';
    printf("Read from fd[0]: %s\n", message);

    // 从fd[1]读取写入fd[0]的数据
    len = read(fd[1], message, CHAR_BUFSIZE - 1);
    message[len] = '\0';
    printf("Read from fd[1]: %s\n", message);

    close(fd[0]);
    close(fd[1]);
    return 0;
}

运行结果

Read from fd[0]: A message written to fd[1]
Read from fd[1]: A message written to fd[0]

5.4 socketpair vs pipe对比

对比维度 socketpair pipe
方向性 全双工(双向同时通信) 半双工(单向)
命名 匿名(不需要bind) 匿名
通信模型 套接字API(send/recv) 文件I/O(read/write)
灵活性 更灵活,可配合select/epoll 基础功能
适用场景 复杂的双向通信 简单的单向管道

六、完整源代码示例

6.1 Unix域字节流回射服务器

将第五章的TCP回射服务器改写为Unix域套接字版本:

#include "unp.h"

int main(int argc, char **argv)
{
    int listenfd, connfd;
    pid_t childpid;
    socklen_t clilen;
    struct sockaddr_un cliaddr, servaddr;
    void sig_chld(int);

    // 创建Unix域字节流套接字
    listenfd = Socket(AF_LOCAL, SOCK_STREAM, 0);
    
    // 删除可能残留的套接字文件
    unlink(UNIXSTR_PATH);
    
    // 初始化地址结构
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path, UNIXSTR_PATH);
    
    // 绑定并监听
    Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
    Listen(listenfd, LISTENQ);
    
    // 安装信号处理函数(防止僵尸进程)
    Signal(SIGCHLD, sig_chld);
    
    for ( ; ; ) {
        clilen = sizeof(cliaddr);
        connfd = Accept(listenfd, (SA *)&cliaddr, &clilen);
        
        if ((childpid = Fork()) == 0) {
            /* 子进程 */
            Close(listenfd);          // 子进程关闭监听套接字
            str_echo(connfd);         // 回射处理
            Close(connfd);
            exit(0);
        }
        /* 父进程 */
        Close(connfd);                // 父进程关闭已连接套接字
    }
}

6.2 Unix域字节流回射客户端

#include "unp.h"

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_un servaddr;

    // 创建Unix域字节流套接字
    sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);
    
    // 初始化服务器地址
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path, UNIXSTR_PATH);
    
    // 连接服务器
    Connect(sockfd, (SA *)&servaddr, sizeof(servaddr));
    
    // 回射处理
    str_cli(stdin, sockfd);
    
    exit(0);
}

6.3 Unix域数据报回射服务器

数据报版本与UDP版本类似,但需要注意Unix域数据报套接字不会自动绑定路径名:

#include "unp.h"

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_un servaddr, cliaddr;

    // 创建Unix域数据报套接字
    sockfd = Socket(AF_LOCAL, SOCK_DGRAM, 0);
    
    // 删除可能残留的套接字文件
    unlink(UNIXDG_PATH);
    
    // 绑定地址
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path, UNIXDG_PATH);
    Bind(sockfd, (SA *)&servaddr, sizeof(servaddr));
    
    dg_echo(sockfd, (SA *)&cliaddr, sizeof(cliaddr));
}

⚠️ 关键区别:与UDP不同,Unix域数据报套接字的客户端也必须为套接字bind一个路径名,否则服务器无法向客户端发送响应。

七、传递文件描述符

7.1 核心概念

通过Unix域套接字传递文件描述符是Unix域协议最强大的功能之一。传递的原理是:通过辅助数据(ancillary data)将文件描述符从一个进程发送到另一个进程,接收进程将获得一个指向内核中相同文件表项的新描述符。

💡 关键理解:这不是描述符的直接传递,而是在接收进程中创建一个指向内核中相同文件表项的新描述符。因此,文件描述符的引用计数会增加。

7.2 辅助数据处理宏

传递文件描述符需要使用辅助数据(ancillary data),通过sendmsgrecvmsg函数,使用msghdr结构中的msg_controlmsg_controllen成员发送和接收。

辅助数据由一个或多个辅助数据对象构成,每个对象以一个cmsghdr结构开头:

struct cmsghdr {
    socklen_t cmsg_len;     // 结构长度(包括本结构)
    int       cmsg_level;   // 协议级别(SOL_SOCKET)
    int       cmsg_type;    // 协议特定类型(SCM_RIGHTS用于传递描述符)
    // 后面紧跟着实际数据
};

头文件<sys/socket.h>中定义了以下宏以简化辅助数据的处理:

功能
CMSG_FIRSTHDR(mhdrptr) 返回指向第一个cmsghdr结构的指针
CMSG_NXTHDR(mhdrptr, cmsgptr) 返回指向下一个cmsghdr结构的指针
CMSG_DATA(cmsgptr) 返回指向与cmsghdr结构关联的数据的第一个字节的指针
CMSG_LEN(length) 返回给定数据量下存放到cmsg_len中的值
CMSG_SPACE(length) 返回给定数据量下一个辅助数据对象的总大小

7.3 发送文件描述符(发送端)

#include "unp.h"

/* 发送文件描述符到另一端 */
int send_fd(int fd, int fd_to_send)
{
    struct iovec iov[1];
    struct msghdr msg;
    char buf[2];  /* 数据缓冲区(可以是任意数据) */
    union {
        struct cmsghdr cm;
        char control[CMSG_SPACE(sizeof(int))];
    } control_un;
    struct cmsghdr *cmptr;

    // 准备数据(可以是空数据)
    iov[0].iov_base = buf;
    iov[0].iov_len = 1;
    buf[0] = 0;   // 0字节数据表示只有辅助数据

    // 初始化msghdr
    bzero(&msg, sizeof(msg));
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    // 设置辅助数据(用于传递文件描述符)
    msg.msg_control = control_un.control;
    msg.msg_controllen = sizeof(control_un.control);

    cmptr = CMSG_FIRSTHDR(&msg);
    cmptr->cmsg_len = CMSG_LEN(sizeof(int));
    cmptr->cmsg_level = SOL_SOCKET;
    cmptr->cmsg_type = SCM_RIGHTS;
    *((int *)CMSG_DATA(cmptr)) = fd_to_send;

    // 发送消息
    return (sendmsg(fd, &msg, 0));
}

7.4 接收文件描述符(接收端)

#include "unp.h"

/* 从另一端接收文件描述符 */
int recv_fd(int fd, ssize_t (*userfunc)(int, const void *, size_t))
{
    int newfd, n;
    char buf[MAXLINE];
    struct iovec iov[1];
    struct msghdr msg;
    union {
        struct cmsghdr cm;
        char control[CMSG_SPACE(sizeof(int))];
    } control_un;
    struct cmsghdr *cmptr;

    // 准备接收缓冲区
    iov[0].iov_base = buf;
    iov[0].iov_len = sizeof(buf);
    
    bzero(&msg, sizeof(msg));
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    msg.msg_control = control_un.control;
    msg.msg_controllen = sizeof(control_un.control);

    if ((n = recvmsg(fd, &msg, 0)) <= 0)
        return n;

    // 检查辅助数据中是否包含文件描述符
    if ((cmptr = CMSG_FIRSTHDR(&msg)) != NULL &&
        cmptr->cmsg_len == CMSG_LEN(sizeof(int))) {
        if (cmptr->cmsg_level != SOL_SOCKET)
            err_quit("control level != SOL_SOCKET");
        if (cmptr->cmsg_type != SCM_RIGHTS)
            err_quit("control type != SCM_RIGHTS");
        newfd = *((int *)CMSG_DATA(cmptr));
    } else {
        newfd = -1;  // 没有描述符传递
    }

    return newfd;
}

7.5 辅助数据缓冲区对齐技巧

由于msg_control缓冲区必须为cmsghdr结构适当地对齐,常用的技巧是使用联合体(union)确保对齐:

union {
    struct cmsghdr cm;
    char control[CMSG_SPACE(sizeof(int))];
} control_un;

八、传递客户凭证

8.1 凭证传递机制

Unix域套接字较新的实现可以传递客户的凭证(用户ID和组ID)给服务器,从而提供额外的安全检查措施。凭证传递同样通过辅助数据实现:

  • cmsg_level: SOL_SOCKET
  • cmsg_type: SCM_CREDENTIALS (Linux特有)

接收端可以通过接收到的凭证验证客户的身份,决定是否提供服务。

8.2 凭证数据结构

struct ucred {
    pid_t pid;   // 进程ID
    uid_t uid;   // 用户ID
    gid_t gid;   // 组ID
};

💡 应用场景:凭证传递机制在需要实现访问控制的守护进程中非常有用。服务器可以通过验证客户端的UID/GID来决定是否接受请求,而无需客户端提供额外的认证信息。

九、抽象命名空间

9.1 什么是抽象命名空间?

抽象命名空间(Abstract Namespace)是Linux特有的特性,允许将Unix域套接字绑定到一个名称,而无需在文件系统中创建该名称。

9.2 抽象命名空间的使用方式

sockaddr_un.sun_path[0] = 0(即第一个字节为空字符)时,表示使用抽象命名空间。名字以空字符开始,其后可以跟随任何数据(包括\0),名字的长度在调用bindconnectsendto时作为地址的长度传入。

struct sockaddr_un addr;
bzero(&addr, sizeof(addr));
addr.sun_family = AF_UNIX;
addr.sun_path[0] = 0;                    // 标识为抽象命名空间
strcpy(addr.sun_path + 1, "my_socket");  // 名字从第2个字节开始
int len = offsetof(struct sockaddr_un, sun_path) + 1 + strlen("my_socket");
Bind(sockfd, (SA *)&addr, len);

9.3 抽象命名空间的优势

优势 说明
无需清理 地址在套接字关闭时自动消失,没有手动移除套接字文件的麻烦
避免冲突 不污染文件系统命名空间,不会与现有文件冲突
更安全 没有文件系统节点,不易被意外发现
netstat显示 netstat --unix输出中,抽象命名空间的路径以@开头

十、关键图表

10.1 Unix域套接字地址结构图

┌─────────────────────────────────────────────────────────────────────────────┐
│                    struct sockaddr_un 布局                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  0                   1                   2                   3              │
│  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1            │
│  ┌───────────────────────────────────────────────────────────────────────┐ │
│  │          sun_family (2字节)           │                               │ │
│  │          AF_UNIX 或 AF_LOCAL          │                               │ │
│  ├───────────────────────────────────────────────────────────────────────┤ │
│  │                                                                       │ │
│  │                      sun_path[108]                                    │ │
│  │                      (以空字符结尾的路径名)                          │ │
│  │                                                                       │ │
│  └───────────────────────────────────────────────────────────────────────┘ │
│                                                                             │
│  地址类型:                                                                  │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │ 类型              │ sun_path[0]   │ 说明                             │   │
│  ├───────────────────┼───────────────┼─────────────────────────────────┤   │
│  │ 普通路径名        │ 非'\0'        │ 绑定到文件系统中的socket文件      │   │
│  │ 未命名            │ '\0'且长度为0 │ socketpair创建的匿名套接字        │   │
│  │ 抽象命名空间      │ '\0'          │ Linux特有,不创建文件系统节点     │   │
│  └───────────────────┴───────────────┴─────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

10.2 辅助数据传递文件描述符流程图

┌─────────────────────────────────────────────────────────────────────────────┐
│                    通过Unix域套接字传递文件描述符                            │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  进程A                                       进程B                          │
│  ┌─────────────────────────┐                ┌─────────────────────────┐     │
│  │ 打开文件test.txt         │                │ 接收端                   │     │
│  │ fd = 5                  │                │                         │     │
│  └────────────┬────────────┘                │                         │     │
│               │                              │                         │     │
│               ▼                              │                         │     │
│  ┌─────────────────────────┐                │                         │     │
│  │ 准备辅助数据             │                │                         │     │
│  │ cmsg_level=SOL_SOCKET   │                │                         │     │
│  │ cmsg_type=SCM_RIGHTS    │                │                         │     │
│  │ 数据=fd=5               │                │                         │     │
│  └────────────┬────────────┘                │                         │     │
│               │                              │                         │     │
│               ▼                              │                         │     │
│  ┌─────────────────────────┐   sendmsg      │  ┌─────────────────────┐ │     │
│  │ sendmsg()               │ ─────────────→ │  │ recvmsg()           │ │     │
│  └─────────────────────────┘   Unix域套接字  │  └──────────┬──────────┘ │     │
│                                             │             │             │     │
│                                             │             ▼             │     │
│                                             │  ┌─────────────────────┐   │     │
│                                             │  │ 接收辅助数据         │   │     │
│                                             │  │ new_fd = 5          │   │     │
│                                             │  │ (指向相同文件表项)  │   │     │
│                                             │  └─────────────────────┘   │     │
│                                             │             │             │     │
│                                             │             ▼             │     │
│                                             │  ┌─────────────────────┐   │     │
│                                             │  │ 使用new_fd读写文件   │   │     │
│                                             │  └─────────────────────┘   │     │
└─────────────────────────────────────────────────────────────────────────────┘

10.3 Unix域套接字编程模型流程图

┌─────────────────────────────────────────────────────────────────────────────┐
│                Unix域套接字(字节流)客户/服务器模型                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  客户端                                       服务器                         │
│                                                                             │
│  ┌─────────────────────┐                    ┌─────────────────────────────┐ │
│  │ socket(AF_LOCAL,    │                    │ socket(AF_LOCAL, SOCK_STREAM)│ │
│  │   SOCK_STREAM, 0)   │                    │ bind(路径名)                 │ │
│  └──────────┬──────────┘                    │ listen()                     │ │
│             │                                │ unlink()预删除残留文件       │ │
│             ▼                                └──────────────┬──────────────┘ │
│  ┌─────────────────────┐                                   │                 │
│  │ connect(服务器路径名)│                                   ▼                 │
│  └──────────┬──────────┘                    ┌─────────────────────────────┐ │
│             │                                │ accept()(阻塞等待)         │ │
│             ▼                                └──────────────┬──────────────┘ │
│  ┌─────────────────────┐                                   │                 │
│  │ str_cli():          │                                   ▼                 │
│  │   write() → 发送请求│                    ┌─────────────────────────────┐ │
│  │   read() ← 接收回射 │                    │ fork()子进程处理连接         │ │
│  └─────────────────────┘                    └──────────────┬──────────────┘ │
│             │                                               │                 │
│             │                                   ┌───────────▼───────────┐     │
│             │                                   │ str_echo():           │     │
│             └──────────────────────────────────→│   read()接收请求       │     │
│                    数据回射                       │   write()回射响应      │     │
│                                                   └───────────────────────┘     │
│                                                                             │
│  💡 与TCP模型的区别:地址是文件系统路径名,不是IP+端口                        │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

十一、常见问题与注意事项

11.1 常见错误速查表

问题 原因 解决方案
bind: Address already in use 套接字文件已存在 bind前调用unlink()删除残留文件
connect: No such file or directory 服务器路径名不存在 确保服务器已启动并正确绑定了路径
Permission denied 对套接字文件没有访问权限 检查路径的目录和文件的权限设置
connect: Connection refused 监听套接字队列已满 增大listen()的backlog参数
ECONNREFUSED(数据报) 目标路径没有绑定套接字 确保对端已正确bind
发送/接收数据报失败 数据报套接字未bind路径名 Unix域数据报套接字需要显式bind
抽象命名空间不工作 系统不支持或地址长度错误 确保在Linux系统上,正确传入地址长度

11.2 性能最佳实践

场景 推荐方案 说明
同一主机的IPC Unix域套接字 比TCP环回快50%以上
双向通信 socketpair 创建全双工通道,比pipe更灵活
大量小消息 Unix域数据报套接字 保留消息边界,无需额外解析
大数据传输 Unix域字节流套接字 无消息边界限制
高安全需求 结合凭证传递 验证客户端身份
需要清理方便 抽象命名空间(Linux) 自动清理,无残留文件

11.3 可移植性注意事项

特性 可移植性 说明
AF_UNIX 广泛支持 POSIX标准
AF_LOCAL 广泛支持 POSIX标准,与AF_UNIX等价
sockaddr_un.sun_path大小 因系统而异 Linux为108字节,BSD通常为104字节
抽象命名空间 Linux特定 其他系统不支持,需要条件编译
SCM_CREDENTIALS Linux特有 其他系统使用不同的凭证传递机制
数据报套接字可靠性 大多数实现可靠 不要假设绝对可靠,但仍需错误处理

十二、本章小结

12.1 核心知识点回顾

知识点 关键要点
Unix域协议 不是实际协议族,而是在单主机上使用套接字API进行IPC的方法
三大优势 比TCP快一倍、可传递描述符、可传递客户凭证
地址结构 sockaddr_un,使用文件系统路径名作为地址
bind前unlink 必须先删除可能残留的套接字文件
socketpair 创建一对未命名的双向连接套接字,全双工通信
数据报套接字bind Unix域数据报套接字客户端必须显式bind路径名
传递描述符 通过sendmsg/recvmsg和辅助数据(SCM_RIGHTS)实现
辅助数据宏 CMSG_FIRSTHDRCMSG_NXTHDRCMSG_DATACMSG_LENCMSG_SPACE
抽象命名空间 Linux特有,路径名以空字符开头,无文件系统节点,自动清理
性能 比TCP环回快10%-50%,绕过网络协议栈

12.2 本章思维导图

第十五章 Unix域协议
├── Unix域协议概述
│   ├── 定义:不是实际协议族,是IPC方法
│   ├── 提供:SOCK_STREAM(字节流)和SOCK_DGRAM(数据报)
│   └── Linux扩展:SOCK_SEQPACKET(有序分组)
├── 三大核心优势
│   ├── 性能优势:比TCP环回快一倍
│   ├── 传递描述符:进程间传递文件描述符
│   └── 客户凭证:传递UID/GID用于安全检查
├── 地址结构(sockaddr_un)
│   ├── sun_family:AF_UNIX / AF_LOCAL
│   ├── sun_path[108]:文件系统路径名
│   └── SUN_LEN宏:获取地址结构大小
├── 核心函数
│   ├── socket():AF_LOCAL + SOCK_STREAM/DGRAM
│   ├── bind():绑定路径名(bind前需unlink)
│   ├── connect():连接到服务器套接字
│   ├── listen() / accept():服务器端
│   └── socketpair():创建双向匿名套接字对
├── 传递文件描述符
│   ├── 辅助数据(ancillary data)
│   ├── cmsghdr结构 + SCM_RIGHTS类型
│   ├── CMSG_*宏:数据处理
│   └── sendmsg() / recvmsg():收发辅助数据
├── 抽象命名空间(Linux特定)
│   ├── sun_path[0] = '\0'
│   ├── 无文件系统节点
│   └── 自动清理,避免命名冲突
└── 编程注意事项
    ├── bind前需unlink残留文件
    ├── 数据报套接字必须显式bind
    ├── 路径名优先使用绝对路径
    └── 连接队列满时返回ECONNREFUSED

十三、下一章预告

📌 下一篇:《UNIX网络编程》读书笔记(十六):第十六章 非阻塞I/O

第十六章将详细讲解:

  1. 阻塞与非阻塞I/O的核心概念:阻塞I/O与各种非阻塞模型的对比
  2. 设置非阻塞描述符的方法fcntlO_NONBLOCK标志
  3. 非阻塞read和write:处理EAGAIN/EWOULDBLOCK错误
  4. 非阻塞accept:当没有新连接时的行为处理
  5. 非阻塞connect:TCP连接的非阻塞版本,使用select检测连接完成状态
  6. Web客户端示例:使用非阻塞I/O同时发起多个HTTP请求的并发模型
  7. select与非阻塞I/O的组合:实现高效的事件驱动网络编程
  8. connect在非阻塞模式下的特殊处理select检测成功或失败的方法

学习目标:学完第十六章后,你将能够——

  • 使用fcntl将套接字设置为非阻塞模式
  • 正确处理非阻塞I/O的EAGAIN/EWOULDBLOCK错误
  • 编写非阻塞的Web客户端,并发处理多个HTTP请求
  • 正确检测非阻塞connect的完成状态
  • 结合select/pollepoll实现高效的I/O处理

敬请期待!

参考资料

  1. W. Richard Stevens, Bill Fenner, Andrew M. Rudoff. 《UNIX网络编程 卷1:套接字联网API(第3版)》. 北京:人民邮电出版社
  2. UNIX网络编程卷一 学习笔记 第十五章 Unix域协议,CSDN,https://blog.csdn.net/tus00000/article/details/130776543
  3. 《Unix网络编程卷一》第十五章-define_shore_me-ChinaUnix博客,http://blog.chinaunix.net/uid-26423908-id-3081071.html
  4. UNP卷1:第十五章(unix域协议),开源中国,https://my.oschina.net/voler/blog/336832
  5. unix(7) — man-pages,https://manpages.opensuse.org/Leap-16.0/man-pages/unix.7.en.html
  6. socketpair() — QNX,https://qnx.com/developers/docs/7.1/com.qnx.doc.neutrino.lib_ref/topic/s/socketpair.html
  7. socketpair(2) — Debian man-pages,https://manpages.debian.org/trixie/manpages-dev/socketpair.2.en.html
  8. UNIX网络编程读书笔记:辅助数据,博客园,https://www.cnblogs.com/nufangrensheng/p/3607487.html
  9. 15.5 Unix Domain Stream Client/Server — UNP在线,https://books.gigatux.nl/mirror/unixnetworkprogramming/0131411551_ch15lev1sec5.html
  10. unix域套接字实现echo服务,CSDN,https://blog.csdn.net/jichl/article/details/9499365
  11. 在Linux下通过Socket实现本机进程间通信,阿里云开发者社区,https://developer.aliyun.com/article/1688737
  12. Unix domain sockets: 10-50% faster than loopback TCP for local IPC,DevBytes,https://devbytes.co.in/news/unix-domain-sockets-10-50-faster-than-loopback-tcp-for-local-ipc
  13. UNIX域套接字中的抽象名字空间,ChinaUnix,http://blog.chinaunix.net/uid-317451-id-92602.html

本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐