前言
已經不記得什麼時候第一次接觸Unix Domain Socket(下文簡稱UDS),在我印象中,所謂UDS基本等同於本地環回接口(lo)上的TCP或者UDP,而事實上UDS所用的API也確實是套接字API。也許正因爲這些先入爲主的觀點,自從進入後臺開發這個領域,基本沒有正眼瞧過UDS,覺得不過是單機版的TCP/UDP而已。
最近shane兄的《OIDB分set實踐和技術方案》在KM上引發了激烈的討論,當時不少人提到可以藉助eventfd執行事件通知,事後我想實際做一些性能評測時,卻遇到一個比較棘手的問題:eventfd創建出來的“文件描述符”在無父子關係的多進程環境中該如何共享呢?大家熟知的pipe有所謂的“命名管道”,可eventfd沒有這概念。後來jayyi提到可以藉助Unix域協議傳遞文件描述符,從那一刻起,我才決定好好研究一下這個“最熟悉的陌生人”。
概述
UDS提供兩類套接字:字節流套接字(類似TCP)和數據報套接字(類似UDP)。根據《UNIX網絡編程卷1》描述,使用UDS有以下3個理由:
1. 在源自Berkeley的實現中,Unix域套接字往往比通信兩端位於同一個主機的TCP套接字快出一倍
2. Unix域套接字可用於在同一個主機上的不同進程之間傳遞描述符
3. Unix域套接字較新的實現把客戶的憑證(用戶ID和組ID)提供給服務器,從而能夠提供額外的安全檢查措施
字節流模式
/*
* unix_stream_server.c
*/
#define _GNU_SOURCE //for struct ucred
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>
#define SERVER_PATH "/tmp/stream_server.unix"
static int ConnHandler(int fd)
{
struct ucred credentials;
socklen_t ucred_length = sizeof(credentials);
if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &credentials, &ucred_length) < 0) {
perror("getsockopt fail.");
return -1;
}
printf("[CONN]pid=%d uid=%d gid=%d\n", (int)credentials.pid, (int)credentials.uid, (int)credentials.gid);
char buffer[65535];
uint64_t total = 0;
int readn = 0;
while ((readn = read(fd, buffer, sizeof(buffer))) > 0) {
total += readn;
}
if (readn == 0) {
printf("client close conn.\n");
} else {
perror("read fail.");
}
printf("total=%lu\n", total);
close(fd);
return 0;
}
int main(void)
{
unlink(SERVER_PATH);
int fd = socket(PF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
perror("open socket fail.");
return -1;
}
struct sockaddr_un local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sun_family = AF_UNIX;
snprintf(local_addr.sun_path, sizeof(local_addr.sun_path), SERVER_PATH);
if (bind(fd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) {
perror("bind fail.");
return -1;
}
if (listen(fd, 0) < 0) {
perror("listen fail.");
return -1;
}
while (1) {
struct sockaddr_un client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int conn_fd = accept(fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (conn_fd < 0) {
perror("accept fail.");
return -1;
}
pid_t child = fork();
if (child == 0) {
return ConnHandler(conn_fd);
}
close(conn_fd);
}
return 0;
}
/*
* unix_stream_client.c
*/
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>
#define SERVER_PATH "/tmp/stream_server.unix"
int main(int argc, char *argv[])
{
uint64_t send_size = 1024;
if (argc > 1) {
send_size = strtoull(argv[1], NULL, 0);
}
printf("send_size=%lu\n", send_size);
int fd = socket(PF_UNIX, SOCK_STREAM, 0);
if(fd < 0) {
perror("open socket fail.");
return -1;
}
struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
snprintf(server_addr.sun_path, sizeof(server_addr.sun_path), SERVER_PATH);
if (connect(fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("connect fail.");
return -1;
}
char buffer[65535];
uint64_t left = send_size;
while (left > 0) {
int writen = write(fd, buffer, left > sizeof(buffer) ? sizeof(buffer) : left);
if (writen <= 0) {
perror("write fail.");
return -1;
}
left -= writen;
}
printf("write succ.\n");
return 0;
}
注意點:
1. UDS捆綁路徑必須使用絕對路徑,POSIX聲稱使用相對路徑將導致不可預測的結果
2. UDS捆綁路徑若已存在,bind會失敗,所以必須先調用unlink刪除路徑
3. UDS字節流套接字connect時,如果監聽套接字的隊列已滿,會返回失敗
數據報模式
/*
* unix_dgram_server.c
*/
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>
#define SERVER_PATH "/tmp/dgram_server.unix"
int main(void)
{
unlink(SERVER_PATH);
int fd = socket(PF_UNIX, SOCK_DGRAM, 0);
if(fd < 0) {
perror("open socket fail.");
return -1;
}
struct sockaddr_un local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sun_family = AF_UNIX;
snprintf(local_addr.sun_path, sizeof(local_addr.sun_path), SERVER_PATH);
if(bind(fd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) {
perror("bind fail.");
return -1;
}
while (1) {
char buffer[65535];
struct sockaddr_un peer_addr;
socklen_t peer_len = sizeof(peer_addr);
int recvn = recvfrom(fd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer_addr, &peer_len);
if (recvn < 0) {
perror("recvfrom fail.");
continue;
}
int sendn = sendto(fd, buffer, recvn, 0, (struct sockaddr *)&peer_addr, peer_len);
if (sendn != recvn) {
perror("sendto fail.");
continue;
}
}
return 0;
}
/*
* unix_dgram_client.c
*/
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SERVER_PATH "/tmp/dgram_server.unix"
int main(int argc, char *argv[])
{
int loop_size = 1024, loop_num = 1;
if (argc > 1) {
loop_size = strtoul(argv[1], NULL, 0);
}
if (argc > 2) {
loop_num = strtoul(argv[2], NULL, 0);
}
printf("loop_size=%d loop_num=%d\n", loop_size, loop_num);
int fd = socket(PF_UNIX, SOCK_DGRAM, 0);
if(fd < 0) {
perror("open socket fail.");
return -1;
}
struct sockaddr_un local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sun_family = AF_UNIX;
snprintf(local_addr.sun_path, sizeof(local_addr.sun_path), tmpnam(NULL));
if(bind(fd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) {
perror("bind fail.");
return -1;
}
struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
snprintf(server_addr.sun_path, sizeof(server_addr.sun_path), SERVER_PATH);
char *buffer = (char *)malloc(loop_size);
int i = 0;
for (i = 0; i < loop_num; ++i) {
int sendn = sendto(fd, buffer, loop_size, 0, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (sendn != loop_size) {
perror("sendto fail.");
continue;
}
int recvn = recvfrom(fd, buffer, loop_size, 0, NULL, NULL);
if (recvn != sendn) {
perror("recvfrom fail.");
continue;
}
}
printf("send succ.\n");
return 0;
}
注意點:
1. 與UDP客戶端不同,UDS客戶端必須顯式bind路徑名(tmpnam),否則服務端無法發送應答
2. 與UDP類似,若recvfrom時提供的buffer過小,數據報文會被截斷,我暫時沒有找到很好的辦法捕捉到這個錯誤,求高人指點(UDS未實現MSG_TRUNC標記)
描述符傳遞
int SendFd(int sock_fd, int send_fd)
{
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
struct iovec io_vector;
io_vector.iov_base = "F";
io_vector.iov_len = 1;
msg.msg_iov = &io_vector;
msg.msg_iovlen = 1;
char control[CMSG_SPACE(sizeof(int))] = {0};
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
struct cmsghdr *cmptr = CMSG_FIRSTHDR(&msg);
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
cmptr->cmsg_len = CMSG_LEN(sizeof(int));
*((int *)CMSG_DATA(cmptr)) = send_fd;
if (sendmsg(sock_fd, &msg, 0) < 0) {
perror("sendmsg fail.");
return -1;
}
return 0;
}
int ReadFd(int sock_fd)
{
struct msghdr msg;
memset(&msg, 0, sizeof(struct msghdr));
char buffer[1];
struct iovec io_vector;
io_vector.iov_base = buffer;
io_vector.iov_len = sizeof(buffer);
msg.msg_iov = &io_vector;
msg.msg_iovlen = 1;
char control[CMSG_SPACE(sizeof(int))] = {0};
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
if (recvmsg(sock_fd, &msg, 0) < 0) {
perror("recvmsg fail.");
return -1;
}
if (msg.msg_flags & MSG_CTRUNC) {
printf("control data truncated.\n");
return -1;
}
struct cmsghdr *msptr = CMSG_FIRSTHDR(&msg);
if (!msptr
|| msptr->cmsg_level != SOL_SOCKET
|| msptr->cmsg_type != SCM_RIGHTS
|| msptr->cmsg_len != CMSG_LEN(sizeof(int))) {
printf("check fail.\n");
return -1;
}
return *((int *)CMSG_DATA(msptr));
}
注意點:
1. 這裏給出的例子是基於字節流模式,其實數據報模式也可以
2. 這裏的文件描述符可以通過open、pipe、mkfifo、socket等獲取,類型不限
3. 發送進程在調用sendmsg之後立即關閉該描述符,並不影響接收進程
客戶憑證
struct ucred credentials;
socklen_t ucred_length = sizeof(credentials);
if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &credentials, &ucred_length) < 0) {
perror("getsockopt fail.");
return -1;
}
int pid = credentials.pid, uid = credentials.uid, gid = credentials.gid;
注意點:
1. 使用ucred結構體定義需要添加“#define _GNU_SOURCE”,不要直接使用”#define __USE_GNU”,這是glibc的內部宏
性能評估
機器配置
編譯命令:gcc –g –Wall –o xxx xxx.c
字節流UDS VS 本地環回TCP
測試場景:Client向Server寫入10GB數據量耗費時間,每次寫入65535B,分多次寫入
測試結果:
|
R1 |
R2 |
R3 |
R4 |
R5 |
AVG |
UDS |
1.395 |
1.732 |
1.566 |
1.629 |
1.308 |
1.526 |
TCP |
2.493 |
2.497 |
2.959 |
2.707 |
2.446 |
2.620 |
數據報UDS VS 本地環回UDP
測試場景:Client向Server寫入100w大小爲1KB的數據包,echo模式
測試結果:
|
R1 |
R2 |
R3 |
R4 |
R5 |
AVG |
UDS |
9.661 |
9.308 |
8.669 |
9.837 |
8.469 |
9.189 |
UDP |
10.738 |
11.280 |
11.257 |
11.480 |
10.304 |
11.012 |
不難看出,數據報模式和本地環回UDP性能差距不大,而字節流模式和本地環回TCP性能差距還是比較大的。猜測可能原因:TCP的擁塞控制、ACK、重傳等機制造成的冗餘開銷相比UDP更爲客觀,除正常的pack/unpack、校驗碼等因素。
小結
最後,聊一聊關於UDS的緩衝區問題。在摸索的過程中,層嘗試Google、諮詢牛人等多方無果,最後經過反覆嘗試,基本得出以下結論:
1. 對於UDS而言,僅SO_SNDBUF生效,SO_RCVBUF無效,對於net.core.wmem_default、net.core.wmem_max、net.core.rmem_default、net.core.rmem_max就不言而喻了
2. 對於字節流模式而言,SO_SNDBUF決定了發送方的緩衝區大小,這很容易理解
3. 對於數據報模式而言,一方面,SO_SNDBUF決定了發送方的緩衝區大小,另一方面,net.unix.max_dgram_qlen參數決定了最大報文數
4. 與UDP不同,UDP的接收緩衝區如果滿了,後續的數據報會自動丟棄;而UDS的數據報模式,如果發送緩衝區達到上限,在阻塞模式下,發送方將被阻塞,非阻塞模式下也可以捕獲到響應錯誤。個人感覺這個特性也挺讚的,可以用於dispatcher-workers模型的任務分發,per-worker-per-queue,dispatcher可以非常容易感知worker的負載情況,當然是用字節流模式的同樣可以感知,但是無需人爲分包還是比較爽快的