Unix Domain Socket / Named Pipe 深度解析(结合 pm2)

4 阅读3分钟

一、本质是什么?

Unix Domain Socket(UDS)和 Named Pipe(命名管道)都是操作系统内核提供的 本机 IPC 机制,核心思想是:

用一个文件系统路径作为"地址",让多个进程通过内核缓冲区交换数据,数据从不离开本机内存。

与 TCP Socket 不同,UDS 走的是内核内存拷贝,不经过网络协议栈(无需 IP 寻址、TCP 握手、校验和),因此延迟极低、吞吐极高。

进程 A                     内核                      进程 B
  │                         │                          │
  │  write("/tmp/app.sock") │                          │
  │ ──────────────────────► │                          │
  │                         │  内核缓冲区(零拷贝)     │
  │                         │ ────────────────────────►│
  │                         │                          │  read()

文件系统中的 .sock 文件只是一个**"门牌号"**,真实数据存在内核缓冲区,不会写入磁盘。


二、UDS vs Named Pipe 区别

特性Unix Domain SocketNamed Pipe (FIFO)
通信方向全双工半双工(单向)
连接模型类 TCP,有 connect/accept无连接,open 即用
多客户端✅ 支持❌ 较难
文件标识.sock 文件mkfifo 创建的 FIFO 文件
Node.js 支持net.createServer('/tmp/x.sock')fs.createReadStream('/tmp/x.fifo')

pm2 使用的是 Unix Domain Socket(全双工,支持多客户端)。


三、pm2 中是如何用的?

pm2 是一个典型的 Master-Worker 架构,CLI 命令(如 pm2 listpm2 restart app)需要与后台常驻的 Daemon 进程通信,用的正是 UDS。

整体架构

┌─────────────────────────────────────────────────────┐
│                      用户终端                        │
│                                                     │
│   $ pm2 start app.js         $ pm2 list             │
│        │                          │                 │
│   pm2 CLI 进程              pm2 CLI 进程             │
└────────┼──────────────────────────┼─────────────────┘
         │  UDS 连接                │  UDS 连接
         │  (~/.pm2/pub.sock)       │  (~/.pm2/rpc.sock)
         ▼                          ▼
┌─────────────────────────────────────────────────────┐
│                  pm2 Daemon 进程                     │
│              (后台常驻,随第一个 pm2 命令启动)          │
│                                                     │
│   ┌──────────────┐    ┌──────────────┐              │
│   │   RPC Server  │    │   Pub Server  │             │
│   │ rpc.sock      │    │ pub.sock      │             │
│   └──────────────┘    └──────────────┘              │
│                                                     │
│         管理 →  Worker 进程池                        │
│                 ├── app.js (pid: 1234)               │
│                 ├── app.js (pid: 1235)               │
│                 └── app.js (pid: 1236)               │
└─────────────────────────────────────────────────────┘

pm2 的两条 Socket 通道

pm2 Daemon 监听两个 .sock 文件,默认位于 ~/.pm2/:

文件用途通信模式
~/.pm2/rpc.sockCLI → Daemon 的指令通道(start/stop/restart/list)请求-响应(RPC)
~/.pm2/pub.sockDaemon → CLI 的事件推送通道(日志、状态变更)发布-订阅(Pub/Sub)
# 可以直接看到这两个 socket 文件
ls -la ~/.pm2/*.sock
# srw-rw-rw-  ~/.pm2/rpc.sock
# srw-rw-rw-  ~/.pm2/pub.sock

四、一次 pm2 list 的完整链路

1. 用户执行 $ pm2 list
        │
2. CLI 进程 connect → ~/.pm2/rpc.sock3. 发送 JSON 消息: { method: 'getMonitorData', params: {} }
        │
4. Daemon 收到请求,收集所有 Worker 进程状态
        │
5. 通过同一条 UDS 连接返回 JSON 响应(进程列表数据)
        │
6. CLI 进程渲染表格,打印到终端
        │
7. CLI 进程断开连接,退出

整个过程不经过网络,延迟在微秒级。


五、用 Node.js 模拟 pm2 的 RPC 通信

// daemon.js —— 模拟 pm2 Daemon,监听 UDS
const net = require('net');
const fs = require('fs');
const SOCK = '/tmp/pm2-demo.sock';

// 清理旧的 sock 文件(进程异常退出时可能残留)
if (fs.existsSync(SOCK)) fs.unlinkSync(SOCK);

const workers = [
  { id: 0, name: 'app', pid: 1234, status: 'online', cpu: '2%' },
  { id: 1, name: 'app', pid: 1235, status: 'online', cpu: '1%' },
];

net.createServer(socket => {
  socket.on('data', data => {
    const req = JSON.parse(data.toString());

    if (req.method === 'getMonitorData') {
      socket.write(JSON.stringify({ result: workers }));
    } else if (req.method === 'restart') {
      workers[req.id].pid = Math.floor(Math.random() * 9000 + 1000);
      socket.write(JSON.stringify({ result: 'ok' }));
    }
  });
}).listen(SOCK, () => {
  console.log(`Daemon listening on ${SOCK}`);
});
// cli.js —— 模拟 pm2 CLI,发起 RPC 调用
const net = require('net');
const SOCK = '/tmp/pm2-demo.sock';

function rpc(method, params = {}) {
  return new Promise((resolve, reject) => {
    const client = net.connect(SOCK, () => {
      client.write(JSON.stringify({ method, ...params }));
    });
    client.on('data', data => {
      resolve(JSON.parse(data.toString()).result);
      client.destroy();
    });
    client.on('error', reject);
  });
}

(async () => {
  // 模拟 pm2 list
  const list = await rpc('getMonitorData');
  console.table(list);

  // 模拟 pm2 restart 0
  await rpc('restart', { id: 0 });
  console.log('restarted');
})();

六、内核层面发生了什么?

cli 进程                    内核                      daemon 进程
   │                         │                            │
   │  connect("/tmp/x.sock") │                            │
   │ ──────────────────────► │  创建 socket pair          │
   │                         │ ──────────────────────────►│ accept()
   │                         │                            │
   │  write(json_bytes)      │                            │
   │ ──────────────────────► │  放入发送缓冲区             │
   │                         │ ──────────────────────────►│ read()
   │                         │                            │
   │                         │  ◄────────────────────────│ write(result)
   │  read()                 │                            │
   │ ◄────────────────────── │                            │

关键点:

  • .sock 文件不存储任何数据,只是内核 socket 的文件系统入口
  • 数据在内核的 socket 缓冲区 中流转,不经过网络协议栈
  • 内核会做 1~2 次内存拷贝(send buffer → recv buffer),现代内核可优化为零拷贝
  • 文件权限(srw-rw-rw-)控制哪些进程可以连接,这是 UDS 的安全边界

七、与 TCP localhost 的性能对比

指标UDSTCP localhost
延迟~5–10 µs~20–50 µs
吞吐极高高(受协议栈限制)
CPU 开销低(无协议栈)中(需处理 TCP/IP)
安全文件权限控制端口暴露风险
跨机器

pm2 选择 UDS 而非 TCP 127.0.0.1 的原因:更快、无端口冲突、天然安全隔离。


八、总结

UDS 本质 = 文件系统寻址 + 内核缓冲区传输 + 零网络开销

pm2 应用 = 两条 UDS 通道(RPC 指令 + Pub 事件)实现 CLI ↔ Daemon 解耦通信

pm2 的设计很好地体现了 UDS 的优势:Daemon 常驻后台,CLI 每次执行命令只做一次短连接,通信成本极低,且不占用任何 TCP 端口。