socketpair() 用于创建 一对互相连接的套接字(sockets) 。
这两个 socket 是 全双工的,也就是说:
-
A 给 B 写数据
-
B 给 A 写数据
两个方向都可行。
就像一个 管道(pipe) ,但 比 pipe 更强大:
至于为什么更强大,其实也很好理解,套接字毕竟是和网络相关的,所以网络那一块的高级特性,它也能用
| 特性 | pipe | socketpair |
|---|---|---|
| 单/双向 | 单向 | 双向 |
| 用法 | 文件描述符 | socket API |
| 支持 send/recv | ❌ | ✔ |
| 支持跨进程 | ✔ | ✔ |
| 支持传输文件描述符 | ❌ | ✔(UNIX domain) |
函数原型
cpp
int socketpair(int domain, int type, int protocol, int sv[2]);
调用后:
-
sv[0]和sv[1]是 已经互相连接 的两个 socket(半双工/全双工取决于 type)。分别给两个进程使用 -
写入一端 → 另一端能读到
-
常用于:本地进程间通信 IPC、线程通知、事件唤醒、子进程通信、libevent/epoll trick
-
返回0表示成功,返回-1表示失败.
第一个参数:domain
这个域决定"通信方式",socketpair 只允许本地通信,所以可用值非常有限。
| 值 | 意义 | 推荐度 |
|---|---|---|
AF_UNIX(或 AF_LOCAL) |
UNIX 本地套接字(最常见、最推荐) | ⭐⭐⭐⭐⭐ |
AF_INET |
不可以 用于 socketpair | ❌ |
AF_INET6 |
不可以 用于 socketpair | ❌ |
➡️ 实际开发中 99.9% 固定写 AF_UNIX
第二个参数:type
表示"通信语义":
| 值 | 描述 | 双工性 | 是否保证可靠有序 |
|---|---|---|---|
SOCK_STREAM |
类似 TCP | 全双工 | ✔️ 有序、可靠 |
SOCK_DGRAM |
类似 UDP,但本地不丢包 | 全双工 | ✔️ 有序、可靠(但消息保边界) |
注意:
-
AF_UNIX + SOCK_DGRAM是可靠的、不丢包的(不像 UDP) -
SOCK_STREAM更常见(像管道) -
SOCK_DGRAM会保持消息边界(适合模块间消息队列)
第三个参数:protocol
这个参数几乎 永远填 0。
原因:
-
在
AF_UNIX下,协议族基本只支持一种协议 -
你可以理解为 "默认协议"
可取值:
| 值 | 描述 |
|---|---|
0 |
自动选择默认协议 ← 最常用 |
| 其他值 | 别填,会失败 |
第四个参数:sv(输出数组)
输出:
-
sv[0]← 一个已连接的 socket -
sv[1]← 另一个已连接的 socket
行为:
-
写
sv[0]→ 在sv[1]读到 -
写
sv[1]→ 在sv[0]读到
比 pipe() 强得多:
-
pipe()只能单向;socketpair()可全双工 -
socketpair()能配 epoll/边沿触发,比 pipe 好用
创建公式
最常用法(99%):
cpp
int sv[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
含义:
-
AF_UNIX = 本地 IPC
-
SOCK_STREAM = 有序字节流(像 TCP)
-
0 = 默认协议
-
sv 两端可读可写(全双工)
socketpair 父子进程双向通信
cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <errno.h>
int main() {
int sv[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
perror("socketpair");
return 1;
}
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
//子进程
else if (pid == 0) {
close(sv[0]);//关闭父进程的端口
int child_fd = sv[1];
//子进程发给父进程
const char* msg = "Hello from child";
write(child_fd, msg, sizeof(msg));
//子进程接受父进程的数据
char buf[1024] = { '\0' };
int n = read(child_fd, buf, sizeof(buf));
if (n > 0) {
std::cout << "[Child] Receive: " << buf << std::endl;
}
close(child_fd);
return 0;
}
//父进程
else {
close(sv[1]); // 父关闭子端,只保留自己的
int parent_fd = sv[0];
// 父接收子的数据
char buf[1024] = { 0 };
int n = read(parent_fd, buf, sizeof(buf));
if (n > 0)
std::cout << "[Parent] Received: " << buf << std::endl;
// 父发送回子
const char* reply = "Hello from parent";
write(parent_fd, reply, strlen(reply));
close(parent_fd);
return 0;
}
}
输出:(输出不全是输出缓冲的问题)
bash
[Parent] Received: Hello fr
[Child] Receive: Hello from parent
可以看到,父子进程可以相互通信。
作为对比,可以看《Linux C编程实战》笔记:管道_main和submain管道-CSDN博客
这一章里,管道如果要实现全双工通信,必须开两个管道
socketpair + sendmsg/recvmsg + SCM_RIGHTS 来 传递文件描述符(FD)
这是 Unix 进程间通信的高级能力之一:
-
A 进程把一个 FD(文件、socket、管道、设备等)"发送"给 B 进程
-
B 收到后,得到可以直接读写的 FD,就像自己打开的一样
-
但 B 不需要知道文件路径,也不需要权限(权限由发送者负责)
cpp
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <stdlib.h>
int send_fd(int socket, int fd) {
struct msghdr msg = {0};
char buf[CMSG_SPACE(sizeof(int))]; // 控制信息空间
memset(buf, 0, sizeof(buf));
struct iovec io = { .iov_base = (void*)"F", .iov_len = 1 };
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS; // 关键
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
// 把 fd 写入控制数据区
*((int*) CMSG_DATA(cmsg)) = fd;
return sendmsg(socket, &msg, 0);
}
int recv_fd(int socket) {
struct msghdr msg = {0};
char m_buffer[1];
struct iovec io = { .iov_base = m_buffer, .iov_len = sizeof(m_buffer) };
msg.msg_iov = &io;
msg.msg_iovlen = 1;
char c_buffer[CMSG_SPACE(sizeof(int))];
msg.msg_control = c_buffer;
msg.msg_controllen = sizeof(c_buffer);
if (recvmsg(socket, &msg, 0) < 0) {
perror("recvmsg");
return -1;
}
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
if (cmsg == nullptr) {
std::cerr << "No control message received\n";
return -1;
}
int fd;
memcpy(&fd, CMSG_DATA(cmsg), sizeof(int));
return fd;
}
int main() {
int sv[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
pid_t pid = fork();
if (pid == 0) {
// 子进程
close(sv[0]);
int child_sock = sv[1];
int fd = recv_fd(child_sock);
if (fd < 0) {
std::cerr << "Child failed to receive fd\n";
return 1;
}
std::cout << "[Child] Received FD = " << fd << std::endl;
// 读文件
char buf[128] = {0};
int n = read(fd, buf, sizeof(buf));
std::cout << "[Child] Read content: " << buf << std::endl;
close(fd);
close(child_sock);
} else {
// 父进程
close(sv[1]);
int parent_sock = sv[0];
// 父打开文件
int fd = open("test.txt", O_RDONLY|O_CREAT, S_IRUSR|S_IWUSR| S_IXUSR);
if (fd < 0) {
perror("open");
return 1;
}
std::cout << "[Parent] Sending FD = " << fd << std::endl;
send_fd(parent_sock, fd);
close(fd);
close(parent_sock);
}
return 0;
}
sendmsg/recvmsg 的难点是这块:
cpp
msghdr
├── iovec(普通数据)
└── control(控制数据,包括 FD)
└── cmsghdr(描述控制数据类型)
└── CMSG_DATA(真正的 FD)
CMSG_DATA(cmsg) 就是 FD 存放位置。
SCM_RIGHTS 就表示"我要传递文件描述符"。
struct msghdr
cpp
struct msghdr {
void *msg_name; // 地址,可用于 UDP,这里不用
socklen_t msg_namelen;
struct iovec *msg_iov; // 普通数据缓冲区
size_t msg_iovlen; // 数组元素个数
void *msg_control; // 控制消息(用于传 fd)
size_t msg_controllen; // 控制消息长度
int msg_flags;// 接受消息时的标志
};
struct iovec
cpp
struct iovec {
void *iov_base; // 数据起始
size_t iov_len; // 长度
};
用于发送普通数据(比如本例中的 "F")。
struct cmsghdr(控制消息头)
cpp
struct cmsghdr {
size_t cmsg_len; // "cmsg_data 的数据长度" + "cmsghdr 结构体本身的长度"
int cmsg_level; // 指明该控制消息来自哪个协议族
int cmsg_type; // 具体控制消息类型
// 后面跟着实际数据,CMSG_DATA() 访问
};
当我们用于传递FD的时候
cpp
cmsg_level = SOL_SOCKET
cmsg_type = SCM_RIGHTS
控制消息一定要通过 CMSG_* 宏访问
CMSG_SPACE
cpp
size_t CMSG_SPACE(size_t len);
计算控制消息需要的 总空间(包含对齐 padding) 。用于为 msg_control 分配缓存大小。
| 参数 | 类型 | 含义 |
|---|---|---|
len |
size_t | 要存放的实际数据长度(例如要传多个 FD 时是 n * sizeof(int)) |
返回:cmsghdr 结构体长度 + 数据长度 + 必要的对齐填充
即:最终控制消息在 msghdr 中占用的字节数(已对齐)
✔ 使用场景
为控制消息区分配空间:
cpp
char buf[CMSG_SPACE(sizeof(int))];
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
如果你想传 3 个文件描述符:
cpp
CMSG_SPACE(3 * sizeof(int))
对于这个buf,实际上它才会被提供给内核
cpp
recvmsg(sock, &msg, 0);
// 内核会把 FD 放到 buf 里,你通过 CMSG_NXTHDR/CMSG_DATA() 取出来
CMSG_LEN
cpp
size_t CMSG_LEN(size_t len);
作用:填充 cmsg_len 字段的正确值(不包含尾部 padding)
| 参数 | 含义 |
|---|---|
len |
数据部分的真实长度 |
✔ 返回值
cmsghdr 自身长度 + 数据长度
(不包含 padding)(直接赋值给cmsg_len)
✔ 典型用法
cpp
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
这个值会被内核检查,必须正确。
CMSG_DATA
cpp
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);
作用:取得指向数据区域的指针
| 参数 | 类型 | 含义 |
|---|---|---|
cmsg |
struct cmsghdr* |
指向控制消息头的指针 |
✔ 返回值
返回指向数据开始处的 unsigned char* 指针
✔ 典型用法(发送 FD)
cpp
*((int*) CMSG_DATA(cmsg)) = fd;
多个 FD:
cpp
int *fds = (int*)CMSG_DATA(cmsg);
fds[0] = fd1;
fds[1] = fd2;
CMSG_FIRSTHDR
cpp
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msg);
作用:取得 msg 中 第一个 控制消息的头部
| 参数 | 类型 | 含义 |
|---|---|---|
msg |
struct msghdr* |
单次 recvmsg/sendmsg 的消息头 |
✔ 返回值
-
如果
msg_controllen<sizeof(cmsghdr)→ 返回 NULL -
否则返回:
cpp
(struct cmsghdr*)msg->msg_control
✔ 典型用法(接收 FD)
cpp
struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
if (cmsg && cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) {
int fd = *((int*) CMSG_DATA(cmsg));
}
CMSG_NXTHDR
cpp
struct cmsghdr *CMSG_NXTHDR(struct msghdr *msg, struct cmsghdr *cmsg);
作用:取得当前控制消息之后的下一个控制消息头
(如果只有一个控制消息,通常用不到)
| 参数 | 类型 | 含义 |
|---|---|---|
msg |
struct msghdr* |
主消息头 |
cmsg |
struct cmsghdr* |
当前控制消息 |
✔ 返回值
-
若下一个控制消息存在 → 返回指针
-
否则 → 返回 NULL
✔ 使用场景
当一个消息里包含多个控制信息:
cpp
for (cmsg = CMSG_FIRSTHDR(&msg);
cmsg != NULL;
cmsg = CMSG_NXTHDR(&msg, cmsg))
{
...
}
例如一个消息同时传:
-
FD
-
Socket 超时设置
-
权限信息
一般实际很少用。
sendmsg 与 recvmsg
cpp
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
由于是socket系列的函数,flags也是类似的选择,例子见https://blog.csdn.net/ouliten/article/details/148208970?spm=1001.2014.3001.5502#t12