1、简介

  Unix Domain Socket(简称 Unix Socket)是一种在同一主机内进行进程间通信(IPC)的高效机制。与网络套接字不同,它不依赖 IP 协议栈,而是通过文件系统路径(或抽象命名空间)作为地址标识,实现本地进程间的可靠或不可靠通信。
其核心优势在于:

  • 零网络开销;
  • 支持流式(SOCK_STREAM)和数据报(SOCK_DGRAM)两种语义
  • 可传递文件描述符、凭证等高级特性(本文暂不展开)。

  本文将从接口使用、内核对象模型和通信流程三个层面,深入剖析 Unix Socket 的实现机制。

2、核心编程接口

  Unix Socket 使用标准 BSD 套接字 API,服务端与客户端流程如下:

服务端

int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
bind(sockfd, (struct sockaddr*)&addr, addrlen);
listen(sockfd, backlog);
int connfd = accept(sockfd, NULL, NULL);  // 返回新连接的 fd

客户端

int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
connect(sockfd, (struct sockaddr*)&addr, addrlen);

3、内部实现模型

  在 SylixOS 系统中,每个 Unix Socket 由两个核心结构体协同管理:

  • struct af_unix_t:表示 AF_UNIX 协议层的控制块,包含连接状态、对端指针、接收队列等;
  • struct socket_t:表示通用套接字抽象,持有文件描述符(fd),负责与用户空间交互。

  二者通过指针相互绑定,共同构成一个完整的 Unix Socket 实例。

typedef struct af_unix_t {
    LW_LIST_LINE        UNIX_lineManage;
    LW_LIST_LINE        UNIX_linePath;

    unix_temp_mode_t    UNIX_tempmode;                                  /*  无文件模式 DGRAM            */
    BOOL                UNIX_bHasConn;                                  /*  DGRAM 被连接过              */
    
    LW_LIST_RING        UNIX_ringConnect;                               /*  连接队列                    */
    LW_LIST_RING_HEADER UNIX_pringConnect;                              /*  等待连接的队列              */
    INT                 UNIX_iConnNum;                                  /*  等待连接的数量              */
    
    INT                 UNIX_iReuse;                                    /*  REUSE flag                  */
    INT                 UNIX_iFlag;                                     /*  NONBLOCK or NOT             */
    INT                 UNIX_iType;                                     /*  STREAM / DGRAM / SEQPACKET  */
    
#define __AF_UNIX_STATUS_NONE       0                                   /*  SOCK_DGRAM 永远处于此状态   */
#define __AF_UNIX_STATUS_LISTEN     1
#define __AF_UNIX_STATUS_CONNECT    2
#define __AF_UNIX_STATUS_ESTAB      3
    INT                 UNIX_iStatus;                                   /*  当前状态 (仅针对 STREAM)    */
    
#define __AF_UNIX_SHUTD_R           0x01
#define __AF_UNIX_SHUTD_W           0x02
    INT                 UNIX_iShutDFlag;                                /*  当前 shutdown 状态          */
    INT                 UNIX_iBacklog;                                  /*  等待的背景连接数量          */
    
    struct af_unix_t   *UNIX_pafunxPeer;                                /*  连接的远程节点              */
    AF_UNIX_Q           UNIX_unixq;                                     /*  接收数据队列                */
    size_t              UNIX_stMaxBufSize;                              /*  最大接收缓冲大小            */
                                                                        /*  超过此数量写入时, 需要阻塞  */
    LW_OBJECT_HANDLE    UNIX_hCanRead;                                  /*  可读                        */
    LW_OBJECT_HANDLE    UNIX_hCanWrite;                                 /*  可写 (也可用作 accept)      */
    
    ULONG               UNIX_ulSendTimeout;                             /*  发送超时 tick               */
    ULONG               UNIX_ulRecvTimeout;                             /*  读取超时 tick               */
    ULONG               UNIX_ulConnTimeout;                             /*  连接超时 tick               */
    
    struct linger       UNIX_linger;                                    /*  延迟关闭                    */
    INT                 UNIX_iPassCred;                                 /*  是否允许传送认证信息        */
    
    PVOID               UNIX_sockFile;                                  /*  socket file                 */
    CHAR                UNIX_cFile[MAX_FILENAME_LENGTH];
} AF_UNIX_T;
typedef struct {
    LW_LIST_LINE        SOCK_lineManage;                                /*  管理链表                    */
    INT                 SOCK_iFamily;                                   /*  协议簇                      */
    
    union {
        INT             SOCKF_iLwipFd;                                  /*  lwip 文件描述符             */
#if LW_CFG_NET_UNIX_EN > 0
        AF_UNIX_T      *SOCKF_pafunix;                                  /*  AF_UNIX 控制块              */
#endif
#if LW_CFG_NET_ROUTER > 0
        AF_ROUTE_T     *SOCKF_pafroute;                                 /*  AF_ROUTE 控制块             */
#endif
#if LW_CFG_NET_SCTP_EN > 0
        AF_SCTP_T      *SOCKF_pafsctp;                                  /*  AF_SCTP 控制块              */
#endif
        AF_PACKET_T    *SOCKF_pafpacket;                                /*  AF_PACKET 控制块            */

        PVOID           SOCKF_pvContext;
    } SOCK_family;
    
    INT                 SOCK_iSoErr;                                    /*  最后一次错误                */
    LW_SEL_WAKEUPLIST   SOCK_selwulist;
} SOCKET_T;

3.1 socket():创建套接字

调用 socket(AF_UNIX, type, 0) 时:

  • 分配 af_unix_t 结构,并初始化为未绑定状态
  • 创建对应的 socket_t 对象,返回其文件描述符(fd)
  • 根据 type 设置套接字类型:
    • SOCK_STREAM:面向连接、可靠、有序(由内核模拟 TCP 语义)
    • SOCK_DGRAM:无连接、基于消息、不可靠(类似 UDP)

注意:此时套接字尚未绑定地址,也无法通信

3.2 bind():绑定地址

bind(s, (struct sockaddr_un*)&addr, addrlen);
/*********************************************************************************************************
  Definitions for UNIX IPC domain
*********************************************************************************************************/

struct sockaddr_un {
    uint8_t sun_len;                                                /* sockaddr len including null      */
    uint8_t sun_family;                                             /* AF_UNIX                          */
    char    sun_path[104];                                          /* path name (gag)                  */
};

执行过程:

  • 解析 addr.sun_path 作为该套接字的唯一标识(路径名或抽象名)
  • 将当前 af_unix_t 实例注册到全局哈希表 _G_plineAfUnixPath 中,以 sun_path 为键
  • 记录绑定状态,后续可通过该路径被其他任务寻址

此步骤仅适用于服务端(或需显式命名的客户端)

3.3 listen():进入监听状态

LW_API int          listen(int s, int backlog);

作用:

  • 将 af_unix_t 的状态置为 __AF_UNIX_STATUS_LISTEN
  • 初始化连接请求队列(UNIX_pringConnect),容量由 backlog 限制

此后,该套接字可接收来自客户端的连接请求。

3.4 accept():接受连接(服务端)

LW_API int          accept(int s, struct sockaddr *addr, socklen_t *addrlen);

在 UNIX domain socket 中,accept() 实际涉及 三类 socket 对象:

角色 内核/实现层常见称呼
监听 Listening socket(listen_unix)
主动发起连接的 socket Connecting socket / Client socket(connecting_unix)
accept 创建的 socket Accepted socket / Connected socket(accepted_unix)

当服务端调用 accept() 时,系统根据监听套接字的状态分两种情况处理:

1️⃣ 无待处理连接的情况

如果监听 socket(listen_unix)的等待连接队列为空,即当前不存在处于 connect() 阻塞或已完成握手但尚未被 accept 的客户端连接请求,则:

  • 当前线程调用 API_SemaphoreBPend 进入阻塞态
  • 直到有新的连接请求被放入等待队列,或发生异常唤醒

2️⃣ 存在待处理连接的情况

若监听 socket 的等待连接队列非空,则 accept() 立即返回,并完成如下操作:

a. 取出等待连接节点

  • 从监听套接字的 UNIX_pringConnect 队列中取出一个客户端连接请求(记为 connecting_unix)

b. 创建服务端连接 socket

  • 分配新的 af_unix_t(记为 accepted_unix),继承监听套接字的类型与属性。

c. 建立双向关联关系(关键步骤)

  • 通过 UNIX_pafunxPeer 字段,将:
connecting_unix->UNIX_pafunxPeer = accepted_unix;
accepted_unix->UNIX_pafunxPeer = connecting_unix;

📌 该双向引用标志着一条唯一的 UNIX domain socket 通信通道正式建立

d. 更新连接状态

  • 双方状态均设为 __AF_UNIX_STATUS_ESTAB(已建立)。

e. 唤醒客户端连接线程

  • 唤醒因 connect() 阻塞在 connecting_unix 上的客户端线程,使其 connect() 调用返回成功

f. 通知可写事件

  • 唤醒所有阻塞在 connecting_unix 上、等待写事件的 select / poll 任务,表示该 socket 已具备可写能力

g. 创建用户态 fd

  • 为 accepted_unix 分配新的 socket_t,返回其 fd 给应用层。服务端后续通过此 fd 与客户端通信。

关键点:对于面向连接的 socket 来说,每次 accept() 都会创建一对新的 af_unix_t 实例,实现“监听套接字 ≠ 通信套接字”的经典模型。

typedef struct af_unix_t {
	......
	struct af_unix_t   *UNIX_pafunxPeer;                                /*  连接的远程节点              */
	......
    LW_LIST_RING        UNIX_ringConnect;                               /*  连接队列                    */
    LW_LIST_RING_HEADER UNIX_pringConnect;                              /*  等待连接的队列              */
	......
}

3.5 connect():发起连接(客户端)

LW_API int          connect(int s, const struct sockaddr *name, socklen_t namelen);

流程如下:

  • 根据 addr.sun_path 在全局表 _G_plineAfUnixPath 中查找对应的监听套接字(listen_unix);
  • 若目标不存在或非监听状态,返回错误;
  • 根据套接字类型处理:
    • SOCK_DGRAM:直接将本端 af_unix_t 与 listen_unix 绑定(仅限首次连接);
    • SOCK_STREAM:将本端 af_unix_t(connecting_unix)加入 listen_unix 的连接请求队列;
  • 若为流式套接字,客户端线程阻塞,等待服务端 accept() 唤醒;
  • 唤醒后,connecting_unix 与 accepted_unix 已建立 peer 关系,可开始通信。

4、数据通信

  一旦连接建立(ESTAB 状态),双方即可通过 send()/recv() 交换数据。以下以客户端发送、服务端接收为例说明。

4.1 send():发送数据

LW_API ssize_t      send(int s, const void *data, size_t size, int flags);

执行逻辑:

  • 通过 af_unix_t->UNIX_pafunxPeer 找到对端的 af_unix_t
  • 将数据拷贝至对端的接收队列 UNIX_unixq
  • 若队列满(超过 UNIX_stMaxBufSize),发送方阻塞
  • 若写入成功,尝试唤醒因 recv() 阻塞的对端线程

数据传输本质是内核内存中的拷贝,无网络协议开销

typedef struct af_unix_t {
	......
	AF_UNIX_Q           UNIX_unixq;                                     /*  接收数据队列                */
    size_t              UNIX_stMaxBufSize;                              /*  最大接收缓冲大小            */
 	......                                                              /*  超过此数量写入时, 需要阻塞  */
}

4.2 recv():接收数据

LW_API ssize_t      recv(int s, void *mem, size_t len, int flags);

执行逻辑:

  • 检查本端 af_unix_t 的 UNIX_unixq 是否有数据
  • 若有,出队并返回
  • 若无,调用 API_SemaphoreBPend 阻塞,直到有数据到达或连接关闭

5、关键设计总结

  • 地址即索引
    Unix Socket 的 sun_path(虽然根文件系统会创建该文件),但本质上来说,sun_path 只是一个全局唯一的命名标识,用于在内核中定位目标套接字。这解决了“无 IP 地址如何寻址”的问题。

  • 连接模型分离
    监听套接字(listening)与通信套接字(accepted / connected)严格分离。每次 accept() 都生成一对新的 af_unix_t 实例,确保并发连接互不干扰。
    在这里插入图片描述

  • 通信基于内存拷贝
    数据通过 af_unix_t 之间的接收队列传递,高效安全。

  • 状态驱动行为
    套接字的状态(未绑定、监听、已建立等)决定了其可执行的操作,是整个协议逻辑的核心。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐