Unix域協議簡介

前言

已經不記得什麼時候第一次接觸Unix Domain Socket(下文簡稱UDS),在我印象中,所謂UDS基本等同於本地環回接口(lo)上的TCP或者UDP,而事實上UDS所用的API也確實是套接字API。也許正因爲這些先入爲主的觀點,自從進入後臺開發這個領域,基本沒有正眼瞧過UDS,覺得不過是單機版的TCP/UDP而已。

最近shane兄的《OIDBset實踐和技術方案》在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.         這裏的文件描述符可以通過openpipemkfifosocket等獲取,類型不限

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

測試場景:ClientServer寫入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

測試場景:ClientServer寫入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_defaultnet.core.wmem_maxnet.core.rmem_defaultnet.core.rmem_max就不言而喻了

2.         對於字節流模式而言,SO_SNDBUF決定了發送方的緩衝區大小,這很容易理解

3.         對於數據報模式而言,一方面,SO_SNDBUF決定了發送方的緩衝區大小,另一方面,net.unix.max_dgram_qlen參數決定了最大報文數

4.         UDP不同,UDP的接收緩衝區如果滿了,後續的數據報會自動丟棄;而UDS的數據報模式,如果發送緩衝區達到上限,在阻塞模式下,發送方將被阻塞,非阻塞模式下也可以捕獲到響應錯誤。個人感覺這個特性也挺讚的,可以用於dispatcher-workers模型的任務分發,per-worker-per-queuedispatcher可以非常容易感知worker的負載情況,當然是用字節流模式的同樣可以感知,但是無需人爲分包還是比較爽快的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章