unix域套接字存在问题及分析

背景

最近项目中使用unix域数据报套接字实现线程间通信,在非阻塞模式下出现了send失败的现象,目前问题还在进一步解决中。

经过查阅资料,虽然《unix网络编程》中说unix域套接字数据报模式是不可靠的,但man unix 显示为可靠,且《unix环境编程》中也说明为可靠,测试情况也表明unix域套接字是可靠的。

unix域套接字用于同一台主机上进程之间的通信,与AF_INET套接字相比,AF_LOCAL通信的效率更高:

  • unix域套接字仅仅是复制数据,不执行协议处理
  • 不需要添加或删除网络报头
  • 不计算校验和,不产生序列号
  • 不需要发送确认报文

关于unix域套接字的基础知识请参考进程间通信的利器——unix域套接字编程详解

使用unix域套接字实现数据收发的一个实现

了解了unix域套接字编程的基本知识,来分析以下程序,使用数据报模式实现两个线程之间的通信,一个线程单纯发,一个线程单纯收。

#include <stdio.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define MAXLEN (1024 * 1014 * 1)/* 单次最大发送缓冲区为1M */
#define UNIX_DGRAM_PATH "/tmp/unix_test_path"

static int get_file_content_and_len(char *file, char *buf, int *len)
{
    int ret;
    struct stat st;
    FILE *fp;

    if (stat(file, &st)) {
       printf("stat error: %s\n", strerror(errno)); 
       return -1;
    }

    *len = st.st_size;
    if (*len >= MAXLEN) {
        printf("file too len[%d]\n", *len);
        return -1;
    }
    printf("file len=%d\n", *len);

    fp = fopen(file, "r");
    if (fp == NULL) {
        printf("fopen error: %s\n", strerror(errno));
        return -1;
    }

    if (fread(buf, 1, *len, fp) != *len) {
        printf("fread error: %s\n", strerror(errno));
        fclose(fp);
        return -1;
    }

    fclose(fp);
    return 0;
}

static void *recv_test(void *arg)
{
    int ret, sockfd;
    struct sockaddr_un servaddr;
    char buf[MAXLEN];
    int len = sizeof(buf);

    sockfd = socket(AF_LOCAL, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        printf("socket error: %s\n", strerror(errno));
        return NULL;
    }

    unlink(UNIX_DGRAM_PATH);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;
    strncpy(servaddr.sun_path, UNIX_DGRAM_PATH, sizeof(servaddr.sun_path) - 1);

    ret = bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    if (ret) {
        printf("bind error: %s\n", strerror(errno));
        return NULL;
    }

    printf("begin recv.\n");
    while(1) {
        if (recvfrom(sockfd, buf, len, 0, NULL, NULL) < 0) {
            printf("recvfrom error: %s\n", strerror(errno));
        }
//        printf("recvfrom len: %d\n", strlen(buf));
    }
}

static void *send_test(char *filename)
{
    char buf[MAXLEN] = { 0 };
    int len;
    struct sockaddr_un servaddr;
    socklen_t addrlen = sizeof(servaddr);
    int fd;
    int ret;

    ret = get_file_content_and_len(filename, buf, &len);
    if (ret) {
        printf("get file content and len error[%d]\n", ret);
        return NULL;
    }

    fd = socket(AF_LOCAL, SOCK_DGRAM, 0);
    if (fd < 0) {
        printf("socket error: %s\n", strerror(errno));
        return NULL;
    }

    /* O_NONBLOCK */
    if (fcntl(fd, F_SETFL, O_NONBLOCK) == -1) {
        printf("fcntl error: %s\n", strerror(errno));
        return NULL;
    }

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;
    strncpy(servaddr.sun_path, UNIX_DGRAM_PATH, sizeof(servaddr.sun_path) - 1);

    printf("begin send. buf_len=%d, len=%d\n", strlen(buf), len);
    while (1) {
        ret = sendto(fd, buf, len, 0, (struct sockaddr *)&servaddr, addrlen);
        if (ret != len) {
            printf("sendto error: %s\n", strerror(errno));
        }
        /* 增加延时,用于测试 */
//        usleep(0);
    }
}

int main(int argc, char **argv)
{
    int ret;
    pthread_t ntid;

    if (argc != 2) {
        printf("Usage: %s filename\n", argv[0]);
        return -1;
    }

    /* 先启动recv */
    if (pthread_create(&ntid, NULL, (void *)recv_test, NULL)) {
        printf("pthread_create recv_test error: %s\n", strerror(errno));
        return -1;
    }
    pthread_detach(ntid);

        /* 延时保证recv就绪 */
    sleep(1);

    /* send */
    if (pthread_create(&ntid, NULL, (void *)send_test, argv[1])) {
        printf("pthread_create sned_test error: %s\n", strerror(errno));
        return -1;
    }
    pthread_detach(ntid);

    while (1) {
        pause();
    }
    return 0;
}

程序功能很简单:

  • 执行 ./a.out test_file即可,其中测试文件是待发送的数据,程序从该文件中读取,最多读取1M
  • 程序先启动recv线程,创建socket并等待接收,接收后的数据丢弃,一直阻塞在recv全力接收
  • 后启动send线程,读取文件并创建socket,然后发送数据,一直全力发送第一次读取到的数据

存在的问题

很简单的程序,经过多种测试,认为至少存在以下问题:

  • 单次发送的最大字节数问题

    • 最大字节数由test_file文件大小决定,程序设计时限定不超过1M,测试中发现,当数据包为160k时send报错:sendto error: Message too long
    • 当文件大小为159k时正常
  • 关于发送设置为非阻塞模式丢包的问题

    • 阻塞模式下,发送没有问题,无丢包
    • 非阻塞模式下,发送会报错:sendto error: Resource temporarily unavailable

EAGAIN or EWOULDBLOCK: The socket is marked nonblocking and the requested operation would block. POSIX.1-2001 allows either error to be returned for this case, and does not require these constants to have the same value, so a portable application should check for both possibilities.

以下三种情况可能导致EAGAIN:

  • 使用fcntl显式指定socket为非阻塞
  • 为sendto设置了MSG_DONTWAIT标志
  • 使用SO_SNDTIMEO为socket设置了超时选项

可能的解决方法

无谓之举

第一个问题,网上看到了类似问题的答案,(unix domain socket)使用udp发送>=128K的消息会报ENOBUFS的错误

但我在本机测试时,160k的包就不行了,而我的内核版本为:

-> % uname -a
Linux virtual-machine 4.2.0-27-generic #32~14.04.1-Ubuntu SMP Fri Jan 22 15:32:27 UTC 2016 i686 i686 i686 GNU/Linux

/proc/slabinfo | grep kmalloc如下:

root@virtual-machine:/usr/src/linux-source-3.13.0# cat /proc/slabinfo | grep kmalloc
dma-kmalloc-8192       0      0   8192    4    8 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-4096       0      0   4096    8    8 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-2048       0      0   2048   16    8 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-1024       0      0   1024   32    8 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-512       32     32    512   32    4 : tunables    0    0    0 : slabdata      1      1      0
dma-kmalloc-256        0      0    256   32    2 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-128        0      0    128   32    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-64         0      0     64   64    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-32         0      0     32  128    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-16         0      0     16  256    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-8          0      0      8  512    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-192        0      0    192   21    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-96         0      0     96   42    1 : tunables    0    0    0 : slabdata      0      0      0
kmalloc-8192          75     88   8192    4    8 : tunables    0    0    0 : slabdata     22     22      0
kmalloc-4096          93    104   4096    8    8 : tunables    0    0    0 : slabdata     13     13      0
kmalloc-2048         208    208   2048   16    8 : tunables    0    0    0 : slabdata     13     13      0
kmalloc-1024        1225   1344   1024   32    8 : tunables    0    0    0 : slabdata     42     42      0
kmalloc-512         2639   2784    512   32    4 : tunables    0    0    0 : slabdata     87     87      0
kmalloc-256         1811   1888    256   32    2 : tunables    0    0    0 : slabdata     59     59      0
kmalloc-192        10734  11193    192   21    1 : tunables    0    0    0 : slabdata    533    533      0
kmalloc-128         2784   2880    128   32    1 : tunables    0    0    0 : slabdata     90     90      0
kmalloc-96         23426  23898     96   42    1 : tunables    0    0    0 : slabdata    569    569      0
kmalloc-64          7430   9408     64   64    1 : tunables    0    0    0 : slabdata    147    147      0
kmalloc-32         24164  25216     32  128    1 : tunables    0    0    0 : slabdata    197    197      0
kmalloc-16         16378  16640     16  256    1 : tunables    0    0    0 : slabdata     65     65      0
kmalloc-8           8704   8704      8  512    1 : tunables    0    0    0 : slabdata     17     17      0

如果内核对内存大小的限制为8192k,那么理论上应该可以发送大小为8192k大小的数据包,其实并不然。

治标不治本

这是stackoverflow上的一个帖子:What’s the practical limit on the size of single packet transmitted over domain socket?

里面给出了较好的解释:

至少3个因素会影响到unix socket单次发送的数据包的大小:
- 内核配置的socket最大缓冲区大小:/proc/sys/net/core/wmem_max,我的机器为163840(160k),它决定了setsockopt (SO_SNDBUF)可以设定的最大值
- 它的值可以通过sysctl更改,如net.core.wmem_max=VALUE,把设置添加到/etc/sysctl.conf以在每次启动时生效。注意:该设置对所有使用socket的协议都生效
- socket能够保存的未读取的数据包的最大个数:/proc/sys/net/unix/max_dgram_qlen,我的机器为10
- 数据包的发送需要连续的内存块,因此还取决于内核能够分配多大的连续内存块(影响的因素较多,如系统IO负载等)

帖子最后对实际极限值给出了一个估计:wmem_max ~8Mb and unix_dgram_qlen ~32

按照这种方法修改了内核参数,只是缓解了错误出现的时机,并没有真正解决问题。

未解之谜

有一个根本性的问题,同一个cpu上,recvfrom全力接收的情况下,sendto为什么还能把send buff填满呢?

即使把发送和接收分别绑定到不同的cpu核上,依然是这样的结果。

难道recvfrom的性能比sendto低吗?通过查找资料,并没有这样的结论。

此问题还需要进一步的验证。

当然,也有可能是程序的问题。欢迎读者批评指正,感激不尽~~

总结

unix域套接字通信总是可靠的。不管是字节流模式还是数据报模式。发送的数据不会乱序,接收方无需考虑乱序包。

由于是在单个cpu上通信,效率较网络通信socket高。

阻塞模式下,数据的收发不会丢包。使用非阻塞模式时,需要考虑发送失败的情况,必要时增加失败重发机制。

要注意unix域套接字通信对于单个数据包有最大值的限制。该值可以通过查询系统参数确定:/proc/sys/net/core/wmem_max,包括最大值和默认值。

  • 3
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值