第 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)。本章配套代码 us_xfr_sv/cl(stream)与 ud_ucase_sv/cl(datagram)是模板。

      一、核心概念

      本章围绕 6 个核心概念展开:sockaddr_un、stream 流程、datagram 流程、权限控制、socketpair、abstract namespace。

      概念 定义 + 重要性 实现提示

      sockaddr_un 地址结构

      struct sockaddr_un { sa_family_t sun_family; char sun_path[108]; };sun_family 固定 AF_UNIX;sun_path 是 null 结尾路径

      §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;us_xfr_sv/us_xfr_cl 示例;iterative 服务器一次处理一个客户端;close 触发对端 EOF

      Datagram socket(UNIX 域可靠)

      UNIX 域 datagram 在内核中传递——可靠、有序、不重复;与 AF_INET UDP 行为不同;最大尺寸受 SO_SNDBUB 等限制(Linux 可大,其他 UNIX 可能 2048 字节限制)

      §57.3;ud_ucase_sv/ud_ucase_cl 示例;recvfrom 一次一消息;过长截断;客户端 bind 唯一路径(PID 后缀)让服务器能回应

      socket 权限

      bind 创建 socket 文件——权限同普通文件;connect/sendto 需要写权限;父目录需要执行权限;某些系统忽略 socket 文件权限(可移植靠父目录权限)

      §57.4;umask() 调整默认权限;socket 文件 ls -l 显示 s 类型;-F= 后缀

      socketpair()

      socketpair(AF_UNIX, type, 0, sv[2]) 一次创建两个已连接 socket;BSD pipe() 内部实现;典型 fork 后父子通信;socket 不绑地址(无安全漏洞)

      §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

      Whatstruct 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: HELLOResponse 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)。

      Exampleumask 007; bind("/tmp/us_xfr") 创建 srwxrwx---——仅 user/group 访问;其他用户即使知道路径也无法 connect。

      57.5 socketpair() 创建已连接对

      Whatsocketpair(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 / 系统调用)
      类别 内容

      sockaddr_un

      struct sockaddr_un { sa_family_t sun_family; char sun_path[108]; };sun_path null 结尾路径;可移植按 92 字节

      Stream 流程

      服务器:socket→bind→listen→accept(阻塞)→read/write→close;客户端:socket→connect→read/write→close

      Datagram 流程

      服务器:socket→bind→recvfrom→sendto→close;客户端:socket→bind(可选)→sendto→recvfrom→close

      UNIX 域 datagram 特性

      可靠、有序、不重复(与网络 datagram 不同);最大尺寸受 SO_SNDBUF 限制

      权限

      bind 创建 socket 文件(0666 & ~umask);connect/sendto 需写;父目录需执行

      必须 remove()

      bind 前 remove() 防 EADDRINUSE;程序退出前 unlink(除非 abstract)

      socketpair

      socketpair(AF_UNIX, type, 0, sv);一次创建两个 connected socket;BSD pipe() 内部用

      Abstract namespace

      sun_path[0]='\0' 启用;socket 名不在文件系统;socket 关闭自动消失

      umask

      影响 bind 创建的 socket 文件默认权限

      ls 显示

      srwxrwxrwx s 类型;-F= 后缀

      跨主机

      UNIX 域仅本机;跨主机用 AF_INET/AF_INET6

      性能

      UNIX 域比 127.0.0.1 快(不经网络栈)

      四、思维导图

      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 服务器设计(多路复用、并发)。