背景
最近项目中使用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时正常
- 最大字节数由test_file文件大小决定,程序设计时限定不超过1M,测试中发现,当数据包为160k时send报错:
关于发送设置为非阻塞模式丢包的问题
- 阻塞模式下,发送没有问题,无丢包
- 非阻塞模式下,发送会报错:
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
,包括最大值和默认值。