'# Linux socket编程(12):Unix套接字之socketpair、sendmsg和recvmsg详解
一、背景与问题
Unix域套接字(Unix domain socket)是Linux中用于本地进程间通信(IPC)的机制,相较于TCP/IP套接字,它具有更低的延迟和更高的性能。在Linux的socket编程中,socketpair()、sendmsg()和recvmsg()是三个关键函数,它们分别解决了以下问题:
socketpair():创建一对双向通信的套接字,适用于父子进程或兄弟进程的通信场景。sendmsg()/recvmsg():支持复杂的控制信息传递,适用于需要传递文件描述符、带外数据等特殊场景。- Unix域套接字的特性:支持文件描述符传递、无网络协议开销、绑定到文件路径等。
本文将深入解析这三个函数的实现原理、使用场景、性能特性,并通过完整案例展示其在实际项目中的应用。
二、基本原理
1. socketpair()的原理
socketpair()创建两个文件描述符,这两个描述符指向同一个Unix域套接字。其核心机制如下:
- 双向通信:通过
SOCK_DGRAM或SOCK_STREAM创建的套接字,支持双向数据传输。 - 文件描述符复制:通过
dup()或fork()复制文件描述符,确保父子进程共享同一个套接字。 - 内核缓冲区:内核维护发送和接收缓冲区,支持异步通信。
函数原型:
int socketpair(int domain, int type, int protocol, int sv[2]);2. sendmsg()与recvmsg()的原理
这两个函数提供了更灵活的消息传递机制,支持以下功能:
- 带控制信息的通信:通过
cmsghdr结构体传递额外信息(如文件描述符)。 - 带外数据:通过
MSG_OOB标志发送/接收带外数据。 - 多缓冲区支持:支持同时发送多个缓冲区的数据。
函数原型:
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);三、环境准备
1. 开发环境
- 编译器:
gcc(Linux环境下) - 基础库:
sys/socket.h、sys/un.h、sys/uio.h(用于sendmsg/recvmsg)
2. 编译命令示例
gcc -o socketpair_example socketpair_example.c四、核心实现
示例1:使用socketpair()创建双向通信
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int sv[2];
char buf[128];
// 创建socketpair
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) == -1) {
perror("socketpair");
exit(EXIT_FAILURE);
}
// 父进程写入
write(sv[1], "Hello from parent", 15);
// 子进程读取
read(sv[0], buf, sizeof(buf));
printf("Child received: %s\n", buf);
return 0;
}关键代码解释:
AF_UNIX:指定Unix域套接字。SOCK_DGRAM:使用数据报套接字,支持非可靠传输。write()/read():用于发送和接收数据。
注意:该示例需要在父子进程间运行,因此需配合fork()使用。
示例2:使用sendmsg()和recvmsg()传递控制信息
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/uio.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int sv[2];
char buf[128];
struct msghdr msg;
struct cmsghdr *cmsg;
char control[1024];
// 创建socketpair
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) == -1) {
perror("socketpair");
exit(EXIT_FAILURE);
}
// 准备控制信息
memset(&msg, 0, sizeof(msg));
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
// 添加控制信息(文件描述符)
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
*(int *)CMSG_DATA(cmsg) = 42; // 示例文件描述符
// 发送消息
sendmsg(sv[1], &msg, 0);
// 接收消息
memset(&msg, 0, sizeof(msg));
recvmsg(sv[0], &msg, 0);
// 解析控制信息
cmsg = CMSG_FIRSTHDR(&msg);
if (cmsg) {
printf("Received fd: %d\n", *(int *)CMSG_DATA(cmsg));
}
return 0;
}关键代码解释:
CMSG_HDR():获取控制信息头。SCM_RIGHTS:传递文件描述符的标志。CMSG_DATA():访问控制信息的数据区域。
注意:该示例演示了如何通过控制信息传递文件描述符,适用于需要共享文件描述符的场景(如进程间管道通信)。
示例3:使用sendmsg()发送带外数据
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int sv[2];
char buf[128];
// 创建socketpair
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
perror("socketpair");
exit(EXIT_FAILURE);
}
// 父进程发送带外数据
sendmsg(sv[1], (struct msghdr*)NULL, MSG_OOB | MSG_DONTWAIT);
// 子进程接收带外数据
recvmsg(sv[0], (struct msghdr*)NULL, 0);
return 0;
}关键代码解释:
MSG_OOB:发送带外数据。MSG_DONTWAIT:非阻塞模式。
注意:该示例需要使用SOCK_STREAM套接字,并且需要处理带外数据的特殊接收逻辑。
五、完整案例
案例:Unix域套接字实现进程间通信
1. 服务端代码(server.c)
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int server_fd, client_fd;
struct sockaddr_un addr;
char buffer[1024];
// 创建套接字
server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 绑定地址
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/mysocket", sizeof(addr.sun_path) - 1);
if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 5) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
// 接受连接
socklen_t addr_len = sizeof(addr);
client_fd = accept(server_fd, (struct sockaddr*)&addr, &addr_len);
if (client_fd == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
// 接收数据
read(client_fd, buffer, sizeof(buffer));
printf("Received: %s\n", buffer);
// 关闭套接字
close(client_fd);
close(server_fd);
unlink("/tmp/mysocket");
return 0;
}2. 客户端代码(client.c)
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int client_fd;
struct sockaddr_un addr;
char buffer[1024] = "Hello from client";
// 创建套接字
client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (client_fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 连接服务器
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/mysocket", sizeof(addr.sun_path) - 1);
if (connect(client_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("connect");
exit(EXIT_FAILURE);
}
// 发送数据
write(client_fd, buffer, sizeof(buffer));
close(client_fd);
return 0;
}运行流程:
- 编译并运行
server.c,创建Unix域套接字。 - 运行
client.c,连接服务器并发送数据。 - 服务器接收数据并打印。
注意:确保/tmp/mysocket文件权限正确,避免权限错误。
六、源码解析
1. socketpair()的内核实现
在kernel/socket.c中,socketpair()函数创建两个文件描述符,通过dup()复制文件描述符,并将它们绑定到同一个套接字结构体。关键代码如下:
int socketpair(int domain, int type, int protocol, int *sv)
{
int fd1, fd2;
int err = 0;
struct socket *sock1, *sock2;
sock1 = sock_alloc();
sock2 = sock_alloc();
if (!sock1 || !sock2) {
err = -ENOMEM;
goto out;
}
// 创建套接字并设置为双向通信
if (sock_create(domain, type, protocol, &sock1) < 0) {
err = -EINVAL;
goto out;
}
// 复制文件描述符
fd1 = dup(sock1->fd);
fd2 = dup(sock2->fd);
if (fd1 < 0 || fd2 < 0) {
err = -EINVAL;
goto out;
}
*sv = fd1;
*sv += 1; // 假设sv是数组
return 0;
}2. sendmsg()/recvmsg()的内核实现
在kernel/socket.c中,sendmsg()函数处理消息的发送,通过遍历msghdr结构体中的缓冲区,将数据发送到内核缓冲区。关键逻辑如下:
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)
{
struct socket *sock = sockfd_to_socket(sockfd);
ssize_t ret;
if (!sock)
return -EBADF;
ret = sock_sendmsg(sock, msg, flags);
return ret;
}七、进阶使用
1. 使用socketpair()实现多进程通信
在多进程环境中,socketpair()常用于父子进程通信。例如:
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int sv[2];
pid_t pid;
// 创建socketpair
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) == -1) {
perror("socketpair");
exit(EXIT_FAILURE);
}
pid = fork();
if (pid == 0) {
// 子进程
printf("Child: %d\n", sv[0]);
write(sv[0], "Hello from child", 15);
} else {
// 父进程
printf("Parent: %d\n", sv[1]);
read(sv[1], buffer, sizeof(buffer));
printf("Parent received: %s\n", buffer);
}
return 0;
}2. 使用sendmsg()传递文件描述符
在需要传递文件描述符的场景中,sendmsg()是更安全的选择。例如:
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/uio.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int sv[2];
int fd = open("test.txt", O_RDONLY);
char buf[128];
struct msghdr msg;
struct cmsghdr *cmsg;
char control[1024];
// 创建socketpair
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) == -1) {
perror("socketpair");
exit(EXIT_FAILURE);
}
// 准备控制信息
memset(&msg, 0, sizeof(msg));
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
*(int *)CMSG_DATA(cmsg) = fd;
sendmsg(sv[1], &msg, 0);
read(sv[0], buf, sizeof(buf));
printf("Received: %s\n", buf);
close(fd);
return 0;
}八、性能与工程实践
1. 性能优化
- 减少系统调用:避免频繁调用
sendmsg()/recvmsg(),可以将多个数据包合并为一次发送。 - 使用非阻塞模式:通过
O_NONBLOCK标志避免阻塞等待。 - 调整内核参数:通过
/proc/sys/net/unix调整缓冲区大小。
2. 安全风险
- 文件权限控制:确保Unix域套接字文件的权限为
600,避免未授权访问。 - 路径安全:避免使用
/tmp等公共路径,改用自定义目录(如/var/run/myapp/)。 - 控制信息验证:对传递的文件描述符进行合法性校验,防止恶意注入。
3. 异常处理
- 处理
EAGAIN/EWOULDBLOCK:在非阻塞模式下,避免因无数据可读而挂起。 - 清理资源:在进程退出时,使用
unlink()删除Unix域套接字文件。
九、常见问题与踩坑
1. 文件描述符未复制导致通信失败
错误示例:
int sv[2];
socketpair(AF_UNIX, SOCK_DGRAM, 0, sv); // 未复制描述符
write(sv[1], "Hello", 5); // sv[1]未复制原因:未复制文件描述符,导致sv[1]未指向有效套接字。
解决办法:使用dup()或fork()复制文件描述符。
2. 控制信息结构体填充错误
错误示例:
struct cmsghdr *cmsg;
cmsg->cmsg_len = CMSG_LEN(4); // 错误的长度计算原因:未计算CMSG_LEN(),导致内存越界。
解决办法:使用CMSG_LEN()宏计算结构体大小。
3. 未处理带外数据
错误示例:
recvmsg(sv[0], &msg, 0); // 未处理带外数据标志原因:未检查MSG_OOB标志,导致带外数据丢失。
解决办法:在recvmsg()调用中添加MSG_OOB标志。
十、最佳实践
1. 推荐使用场景
- 本地进程间通信:适合需要快速传输小数据的场景(如事件通知、状态同步)。
- 文件描述符传递:用于共享文件、管道等资源。
- 多进程通信:通过
socketpair()实现父子进程或兄弟进程的通信。
2. 不推荐使用场景
- 跨网络通信:Unix域套接字不支持跨主机通信。
- 高并发场景:需要考虑内核缓冲区的限制,可能需要调整参数。
- 安全敏感场景:需严格控制文件权限和路径。
十一、总结
本文深入解析了socketpair()、sendmsg()和recvmsg()在Unix域套接字中的应用,覆盖了其工作原理、实现细节、使用场景、性能优化和安全风险。通过多个代码示例和完整案例,展示了这些函数在实际项目中的应用方法。在选择Unix域套接字时,需根据具体需求权衡其优势(如低延迟、高效率)和局限性(如本地通信限制),并结合安全性和可维护性进行合理设计。对于需要在本地进程间高效传递数据、文件描述符或特殊控制信息的场景,Unix域套接字是一个强大且值得推荐的工具。