一、本质是什么?
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 Socket | Named 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 list、pm2 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.sock | CLI → Daemon 的指令通道(start/stop/restart/list) | 请求-响应(RPC) |
~/.pm2/pub.sock | Daemon → 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.sock
│
3. 发送 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 的性能对比
| 指标 | UDS | TCP 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 端口。