1. Unix Domain Socket 基础解析
Unix Domain Socket(UDS)是Linux/Unix系统中一种高效的进程间通信(IPC)机制。与网络Socket不同,它专门用于同一主机上的进程通信,通过文件系统路径名而非IP地址和端口号进行标识。在Android Framework的Native层,UDS被广泛应用于系统服务间的通信,如Zygote、SurfaceFlinger等关键组件。
1.1 核心优势与工作原理
UDS相比网络Socket的显著优势在于其通信效率。当两个进程使用UDS通信时,数据传递完全在内核空间完成,避免了网络协议栈的以下开销:
- 无需TCP/IP协议头封装与解析
- 无需计算校验和
- 无需维护序列号和应答机制
- 无需经过网络接口控制器(NIC)
这种设计使得UDS的吞吐量比本地回环(127.0.0.1)的网络Socket高出约30%-50%,延迟降低40%以上。实测数据显示,在x86_64架构上,UDS的传输速率可达10GB/s以上,而本地TCP Socket通常不超过6GB/s。
1.2 地址结构与类型
UDS使用文件系统路径作为地址,其地址结构定义在<sys/un.h>中:
c复制struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* 路径名 */
};
UDS支持两种通信模式:
- SOCK_STREAM:面向连接的字节流,提供可靠、有序的双向通信(类似TCP)
- SOCK_DGRAM:无连接的数据报,提供有边界的消息传输(类似UDP)
在Android系统中,SOCK_STREAM更为常见,因其能保证消息的完整性和顺序,适合系统服务间的可靠通信。
2. TCP风格UDS核心API详解
2.1 套接字创建与配置
创建UDS套接字使用标准的socket()系统调用,但需指定AF_UNIX域:
c复制int socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (socket_fd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
关键参数说明:
AF_UNIX:指定Unix域套接字SOCK_STREAM:选择面向连接的可靠通信- 第三个参数通常为0,表示使用默认协议
注意:创建套接字后应立即设置SO_REUSEADDR选项以避免地址占用问题:
c复制int reuse = 1; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
2.2 地址绑定与监听
绑定操作将套接字与文件系统路径关联:
c复制struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/mysocket.sock", sizeof(addr.sun_path)-1);
unlink(addr.sun_path); // 确保路径未被占用
if (bind(socket_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind failed");
close(socket_fd);
exit(EXIT_FAILURE);
}
绑定后需设置监听队列:
c复制if (listen(socket_fd, 5) < 0) { // 5是等待连接队列的最大长度
perror("listen failed");
close(socket_fd);
exit(EXIT_FAILURE);
}
关键细节:
- sun_path长度通常限制为107字符(包括终止符)
- 绑定的socket文件权限默认为0777,可通过chmod()调整
- Android中建议使用抽象命名空间(路径首字符为'\0')避免文件系统依赖
2.3 连接建立过程
服务端通过accept()接受连接:
c复制struct sockaddr_un client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(socket_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept failed");
continue; // 通常会在循环中处理多个连接
}
客户端使用connect()发起连接:
c复制struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, "/tmp/mysocket.sock", sizeof(server_addr.sun_path)-1);
if (connect(socket_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect failed");
close(socket_fd);
exit(EXIT_FAILURE);
}
2.4 数据读写操作
UDS使用与网络Socket相同的recv()和send()接口:
c复制// 发送数据
char buffer[1024];
ssize_t sent = send(client_fd, buffer, strlen(buffer), 0);
if (sent < 0) {
perror("send failed");
}
// 接收数据
ssize_t received = recv(client_fd, buffer, sizeof(buffer)-1, 0);
if (received < 0) {
perror("recv failed");
} else {
buffer[received] = '\0'; // 确保字符串终止
}
性能优化技巧:
- 对于小消息(<4KB),使用MSG_NOSIGNAL标志避免SIGPIPE信号
- 大文件传输考虑使用sendfile()系统调用
- 高频小消息场景可使用SO_SNDBUF/SO_RCVBUF调整缓冲区大小
3. 完整示例代码解析
3.1 增强版服务端实现
c复制#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SOCKET_PATH "/tmp/uds_server.sock"
#define BUFFER_SIZE 4096
int main() {
int server_fd, client_fd;
struct sockaddr_un server_addr, client_addr;
socklen_t client_len;
char buffer[BUFFER_SIZE];
// 创建并配置套接字
if ((server_fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置地址重用
int reuse = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
perror("setsockopt");
close(server_fd);
exit(EXIT_FAILURE);
}
// 绑定地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path)-1);
unlink(SOCKET_PATH); // 移除已有socket文件
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
close(server_fd);
exit(EXIT_FAILURE);
}
// 设置监听队列
if (listen(server_fd, 5) < 0) {
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server listening on %s\n", SOCKET_PATH);
// 主事件循环
while (1) {
client_len = sizeof(client_addr);
if ((client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len)) < 0) {
perror("accept");
continue;
}
// 处理客户端连接
while (1) {
ssize_t bytes_read = recv(client_fd, buffer, BUFFER_SIZE-1, 0);
if (bytes_read < 0) {
perror("recv");
break;
} else if (bytes_read == 0) {
printf("Client disconnected\n");
break;
}
buffer[bytes_read] = '\0';
printf("Received: %s\n", buffer);
// 回显响应
if (send(client_fd, buffer, bytes_read, MSG_NOSIGNAL) < 0) {
perror("send");
break;
}
}
close(client_fd);
}
close(server_fd);
unlink(SOCKET_PATH);
return 0;
}
3.2 增强版客户端实现
c复制#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define SOCKET_PATH "/tmp/uds_server.sock"
#define BUFFER_SIZE 4096
int main() {
int sock_fd;
struct sockaddr_un server_addr;
char buffer[BUFFER_SIZE];
// 创建套接字
if ((sock_fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path)-1);
// 连接服务器
if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
close(sock_fd);
exit(EXIT_FAILURE);
}
printf("Connected to server at %s\n", SOCKET_PATH);
// 交互循环
for (int i = 0; i < 5; i++) {
snprintf(buffer, BUFFER_SIZE, "Message %d at %ld", i, time(NULL));
if (send(sock_fd, buffer, strlen(buffer), MSG_NOSIGNAL) < 0) {
perror("send");
break;
}
ssize_t bytes_read = recv(sock_fd, buffer, BUFFER_SIZE-1, 0);
if (bytes_read < 0) {
perror("recv");
break;
}
buffer[bytes_read] = '\0';
printf("Server response: %s\n", buffer);
sleep(1);
}
close(sock_fd);
return 0;
}
4. 高级主题与性能优化
4.1 多路复用与并发处理
对于高并发场景,建议使用I/O多路复用技术:
c复制// 使用epoll管理多个连接
int epoll_fd = epoll_create1(0);
struct epoll_event event, events[MAX_EVENTS];
event.events = EPOLLIN;
event.data.fd = server_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);
while (1) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == server_fd) {
// 处理新连接
int client_fd = accept(server_fd, NULL, NULL);
event.events = EPOLLIN | EPOLLET;
event.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event);
} else {
// 处理客户端数据
handle_client(events[i].data.fd);
}
}
}
4.2 零拷贝技术
对于大文件传输,可使用splice()实现零拷贝:
c复制int pipefd[2];
pipe(pipefd);
// 将数据从socket直接传输到文件描述符,无需用户空间缓冲
splice(client_fd, NULL, pipefd[1], NULL, 4096, SPLICE_F_MOVE);
splice(pipefd[0], NULL, dest_fd, NULL, 4096, SPLICE_F_MOVE);
4.3 Android特定优化
在Android环境中:
- 使用
LOCAL_SOCKET宏定义代替硬编码路径 - 考虑SELinux策略,确保有足够的权限访问socket文件
- 对于高频通信,使用
ashmem共享内存配合UDS控制消息
5. 常见问题排查指南
5.1 连接失败排查
-
EACCES错误:
- 检查socket文件权限:
ls -l /tmp/mysocket.sock - 确保进程有读写权限
- Android上检查SELinux策略
- 检查socket文件权限:
-
ENOENT错误:
- 确认服务端已创建socket文件
- 检查路径拼写是否正确
- 对于抽象命名空间,确保路径首字符为'\0'
-
ECONNREFUSED错误:
- 确认服务端正在运行
- 检查服务端是否调用了listen()
5.2 性能问题优化
-
高延迟:
- 增加SO_SNDBUF/SO_RCVBUF大小
- 使用MSG_DONTWAIT标志进行非阻塞操作
- 考虑使用多线程处理连接
-
低吞吐量:
- 增大消息批量处理规模
- 使用writev()/readv()进行分散/聚集IO
- 考虑使用内存映射文件传输大块数据
5.3 资源泄漏检查
-
文件描述符泄漏:
- 使用
lsof -p <pid>检查打开的文件描述符 - 确保每个close()调用都有错误检查
- 使用
-
内存泄漏:
- 使用valgrind检测内存问题
- 特别注意recv()缓冲区的管理
-
Socket文件累积:
- 实现优雅退出机制,自动清理socket文件
- 使用atexit()注册清理函数