使用udp发送>=128K的消息会报ENOBUFS的错误的解决方法

所属分类: 软件编程 / C 语言 阅读数: 99
收藏 0 赞 0 分享

这是一个困扰我两天的问题,

Google和Baidu没有找到解决方法!

此文为记录这个问题,并给出原因和解决方法。

1、Unix domain socket简介

unix域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方法,所用API于在不同主机上执行客户/服务器通信所有的API(套接字API,如AF_INET、AF_INET6等类型的API)相同。unix域协议可以视为是进程之间本地通信IPC的一种。

unix域提供两类套接口:字节流套接口(类似TCP)和数据报套接口(类似UDP)。使用Unix域套接口的理由有三:

Unix域套接口往往比位于同一主机的TCP套接口快出一倍。Unix域套接口可用于在同一主机上的不同进程之间传递描述字。Unix域套接口把客户的凭证(用户ID和用户组ID)提供给服务器,从而实现能够提供额外的安全检查措施。

Unix域中用域标识客户和服务器的协议地址是普通文件系统中的路径名(类比:IPv4协议的地址由一个32位地址和一个16位端口号构成,IPv6协议的地址由一个128位地址和16位端口号构成。)。

2、问题描述

简单介绍了Unix域套接口之后,进入主题——描述我碰到的问题。由于unix域套接口用于本机间进程通信比网络套接口效率高,因为它是不经过协议栈的!在项目中选择了unix域的数据报套接口。在使用过程中碰到了如下,问题:发送<128K的消息时,客户、进程可以正常收发消息;发送>=128K的消息时,发送端(sendto)返回ENOBUFS的错误。

服务器的代码如下:

服务器端

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/un.h>
#include<errno.h>

//define send and recv buf size
#define BUFSIZE 512*1024

//define unix domain socket path
#define pmmanager "/tmp/pmmanager"
#define pmapi "/tmp/pmapi"

int main(int argc, char** argv)
{
char rx_buf[BUFSIZE];
int pmmanager_fd, ret;
socklen_t len;
struct sockaddr_un pmmanager_addr, pmapi_addr;

//create pmmanager socket fd
pmmanager_fd = socket(AF_UNIX, SOCK_DGRAM, 0);
if(pmmanager_fd == -1)
{
perror("cannot create pmmanager fd.");
}

unlink(pmmanager);
memset(&pmmanager_addr, 0, sizeof(pmmanager_addr));
pmmanager_addr.sun_family = AF_UNIX;
strncpy(pmmanager_addr.sun_path, pmmanager, sizeof(pmmanager_addr.sun_path)-1);

//bind pmmanager_fd to pmmanager_addr
ret = bind(pmmanager_fd, (struct sockaddr*)&pmmanager_addr, sizeof(pmmanager_addr));
if(ret == -1)
{
perror("can not bind pmmanager_addr");
}

int recvBufSize;
len = sizeof(recvBufSize);
ret = getsockopt(pmmanager_fd, SOL_SOCKET, SO_RCVBUF, &recvBufSize, &len);
if(ret ==-1)
{
perror("getsocket error.");
}
printf("Before setsockopt, SO_RCVBUF-%d\n",recvBufSize); 
recvBufSize = 512*1024;
ret = setsockopt(pmmanager_fd, SOL_SOCKET, SO_RCVBUF, &recvBufSize, len);
if(ret == -1)
{
perror("setsockopt error.");
}
ret = getsockopt(pmmanager_fd, SOL_SOCKET, SO_RCVBUF, &recvBufSize, &len);
if(ret ==-1)
{
perror("getsocket error.");
}
printf("Set recv buf successful, SO_RCVBUF-%d\n",recvBufSize); 

int recvSize;
memset(&pmapi_addr, 0, sizeof(pmapi_addr));
len = sizeof(pmapi_addr);
printf("==============wait for msg from pmapi====================\n");
for(;;)
{
memset(rx_buf, 0, sizeof(rx_buf));
recvSize = recvfrom(pmmanager_fd, rx_buf, sizeof(rx_buf), 0, (struct sockaddr*)&pmapi_addr, &len);
if(recvSize == -1)
{
perror("recvfrom error.");
}
printf("Recved message from pmapi: %s\n", rx_buf);
}
}

客户端的代码如下:

客户端

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/un.h>
#include<errno.h>

//define send and recv buf size
#define BUFSIZE 250*1024

//define unix domain socket path
#define pmmanager "/tmp/pmmanager"
#define pmapi "/tmp/pmapi"

int main(int argc, char** argv)
{
char tx_buf[BUFSIZE];
int pmapi_fd, ret;
socklen_t len;
struct sockaddr_un pmmanager_addr, pmapi_addr;

//create pmmanager socket fd
pmapi_fd = socket(AF_UNIX, SOCK_DGRAM, 0);
if(pmapi_fd == -1)
{
perror("cannot create pmapi fd.");
}

unlink(pmapi);
//configure pmapi's addr
memset(&pmapi_addr, 0, sizeof(pmapi_addr));
pmapi_addr.sun_family = AF_UNIX;
strncpy(pmapi_addr.sun_path, pmapi, sizeof(pmapi_addr.sun_path)-1);
//bind pmapi_fd to pmapi_addr
ret = bind(pmapi_fd, (struct sockaddr*)&pmapi_addr, sizeof(pmapi_addr));
if(ret == -1)
{
perror("bind error.");
}

int sendBufSize;
len = sizeof(sendBufSize);
ret = getsockopt(pmapi_fd, SOL_SOCKET, SO_SNDBUF, &sendBufSize, &len);
if(ret ==-1)
{
perror("getsocket error.");
}
printf("Before setsockopt, SO_SNDBUF-%d\n",sendBufSize); 
sendBufSize = 512*1024;
ret = setsockopt(pmapi_fd, SOL_SOCKET, SO_SNDBUF, &sendBufSize, len);
if(ret == -1)
{
perror("setsockopt error.");
}
ret = getsockopt(pmapi_fd, SOL_SOCKET, SO_SNDBUF, &sendBufSize, &len);
if(ret ==-1)
{
perror("getsocket error.");
}
printf("Set send buf successful, SO_SNDBUF-%d\n\n\n", sendBufSize); 

//configure pmmanager's addr
memset(&pmmanager_addr, 0, sizeof(pmmanager_addr));
pmmanager_addr.sun_family = AF_UNIX;
strncpy(pmmanager_addr.sun_path, pmmanager, sizeof(pmmanager_addr)-1);
len = sizeof(pmmanager_addr);

int sendSize = 0;
int i;
for(i=1; i<=4; i++)
{
memset(tx_buf, '0', sizeof(tx_buf));
sprintf(tx_buf, "send msg %d to pmmanager.", i);
printf("%s, msg size - %d\n",tx_buf, sizeof(tx_buf));
sendSize = sendto(pmapi_fd, tx_buf, sizeof(tx_buf), 0, (struct sockaddr*)&pmmanager_addr, len);
if(sendSize == -1)
{
perror("sendto error.");
}
printf("Send message to pmmanager: %s\n\n\n", tx_buf);
}
}

3、可能碰到的另外一个问题

如果你没有设置足够大的发送缓冲区大小,你很有可能碰到EMSGSIZE的错误!因为应用程序写了一个大于套机口发送缓冲区大小的数据报,内核报EMSGSIZE错误。如下图:

(注意:UDP套接口有发送缓冲区的大小,并且可以通过SO_SNDBUF套接口选项修改。不过它仅仅是写到套接口的UDP数据报的大小,因为UDP是不可靠的,它不必保存应用进程的数据拷贝,因此无需一个真正的发送缓冲区。)上面的代码已经设置了足够大的发送缓冲区大小。

4、我的尝试

在sendto发送>=128K大小的消息时,返回ENOBUFS错误。

我怀疑是否是sendto()的原因,我改用sendmsg(),未果还是返回这个错误。有人说是:“发送消息太频繁,间隔太短”。其实项目中发送消息根本就不频繁,背着死马当活马医,未果还是返回这个错误。尝试修改/proc/sys/net/core下面的各种相关选项,如

未果,还是返回这个错误。(其它路径下的相关选项也试了,不行)?我无从下手了,不知道128K的这个限制在哪?既然“No buffer space available”,我怎样给他空间?5、最终原因及解决办法(都是内核惹得祸!!)至此,我实在没有办法了,不知道如何解决!但是从错误ENOBUFS的说明:

ENOBUFS means there is no sufficient memory available and the system(kernel) can not allocate any more.Application will usually retry the operation when it detects this error from asystem call since it indicates there is a transient resource shortage.It is the Operating system that refuses the resource request from the listener.The virtual memory allocation routine of the OS will determine if a swap can bemade to disk of a real memory segment thereby allowing the listener access tosome more real memory.

可以看出一些端倪,这肯定跟内存分配有关!而且限制在分配128K就失败!利用Socket进行进程间的通信,需要经过Linux内核:进程1将数据写到内核,进程2从内核读取数据。内核必须申请一个空间来存放数据包!实际上,socket发送数据包时,需要从slab中申请一块cache存放数据包。在2.6.21内核中(这就是我们公司服务器的内核版本),slab分配器最大支持的size为128K(详情可见/proc/slabinfo)。在2.6.31内核中,slab分配器最大支持的size大小为32M。所以2.6.21内核上,发送大于128K的数据包时,Kmalloc()会失败,并返回no buffer的错误。建议:对于本地进程通信,可以使用其它的IPC方式,进行数据通信,如shm、pipe等。找出了原因,可以采用以下方式来解决该问题:升级内核,或修改内核的这个限制。改用unix 域udp套接口为unix域tcp套接口(最终我们采用的方式)。改用其它的IPC方式(这个涉及到太多的修改,故我们放弃使用)。附/proc/slabinfo 信息:size-131072即128K的限制!

代码

size-131072(DMA) 0 0 131072 1 32 : tunables 8 4 0 : slabdata 0 0 0
size-131072 0 0 131072 1 32 : tunables 8 4 0 : slabdata 0 0 0
size-65536(DMA) 0 0 65536 1 16 : tunables 8 4 0 : slabdata 0 0 0
size-65536 0 0 65536 1 16 : tunables 8 4 0 : slabdata 0 0 0
size-32768(DMA) 0 0 32768 1 8 : tunables 8 4 0 : slabdata 0 0 0
size-32768 0 0 32768 1 8 : tunables 8 4 0 : slabdata 0 0 0
size-16384(DMA) 0 0 16384 1 4 : tunables 8 4 0 : slabdata 0 0 0
size-16384 0 0 16384 1 4 : tunables 8 4 0 : slabdata 0 0 0
size-8192(DMA) 0 0 8192 1 2 : tunables 8 4 0 : slabdata 0 0 0
size-8192 0 0 8192 1 2 : tunables 8 4 0 : slabdata 0 0 0
size-4096(DMA) 0 0 4096 1 1 : tunables 24 12 0 : slabdata 0 0 0
size-4096 4 4 4096 1 1 : tunables 24 12 0 : slabdata 4 4 0
size-2048(DMA) 0 0 2048 2 1 : tunables 24 12 0 : slabdata 0 0 0
size-2048 12 14 2048 2 1 : tunables 24 12 0 : slabdata 7 7 0
size-1024(DMA) 0 0 1024 4 1 : tunables 54 27 0 : slabdata 0 0 0
size-1024 11 12 1024 4 1 : tunables 54 27 0 : slabdata 3 3 0
size-512(DMA) 0 0 512 8 1 : tunables 54 27 0 : slabdata 0 0 0
size-512 208 208 512 8 1 : tunables 54 27 0 : slabdata 26 26 0
size-256(DMA) 0 0 256 15 1 : tunables 120 60 0 : slabdata 0 0 0
size-256 75 75 256 15 1 : tunables 120 60 0 : slabdata 5 5 0
size-192(DMA) 0 0 192 20 1 : tunables 120 60 0 : slabdata 0 0 0
size-192 40 40 192 20 1 : tunables 120 60 0 : slabdata 2 2 0
size-128(DMA) 0 0 128 30 1 : tunables 120 60 0 : slabdata 0 0 0
size-128 86 90 128 30 1 : tunables 120 60 0 : slabdata 3 3 0
size-96(DMA) 0 0 96 40 1 : tunables 120 60 0 : slabdata 0 0 0
size-96 388 400 96 40 1 : tunables 120 60 0 : slabdata 10 10 0
size-64(DMA) 0 0 64 59 1 : tunables 120 60 0 : slabdata 0 0 0
size-32(DMA) 0 0 32 113 1 : tunables 120 60 0 : slabdata 0 0 0
size-64 451 472 64 59 1 : tunables 120 60 0 : slabdata 8 8 0
size-32 871 904 32 113 1 : tunables 120 60 0 : slabdata 8 8 0

我在Ubuntu 10.10上测试,不会报ENOBUFS的错误。内核版本为:

/proc/slabinfo的信息如下,跟上面的有些差异:

kmalloc的最大限制是8192K,故我们运行上述程序没有问题!原来都是内核惹得祸阿,害我困惑那么久!!!baidu和google都没有找到原因,因此分享此文,以警惕后者。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

更多精彩内容其他人还在看

C++中四种对象生存期和作用域以及static的用法总结分析

以下是对C++中四种对象生存期和作用域以及static的用法进行了详细的介绍,需要的朋友可以过来参考下
收藏 0 赞 0 分享

C++嵌套类与局部类详细解析

从作用域的角度看,嵌套类被隐藏在外围类之中,该类名只能在外围类中使用。如果在外围类之外的作用域使用该类名时,需要加名字限定
收藏 0 赞 0 分享

C++空类详解

以下是对C++中的空类进行了详细的介绍,需要的朋友可以过来参考下
收藏 0 赞 0 分享

C++之友元:友元函数和友元类详解

友元是一种允许非类成员函数访问类的非公有成员的一种机制。可以把一个函数指定为类的友元,也可以把整个类指定为另一个类的友元
收藏 0 赞 0 分享

C++中返回指向函数的指针示例

int (*ff(int)) (int *,int);表示:ff(int)是一个函数,带有一个int型的形参,该函数返回int (*) (int *,int),它是一个指向函数的指针,所指向的函数返回int型并带有两个分别是Int*和int型的形参
收藏 0 赞 0 分享

C数据结构之单链表详细示例分析

以下是对C语言中的单链表进行了详细的分析介绍,需要的朋友可以过来参考下
收藏 0 赞 0 分享

C数据结构之双链表详细示例分析

以下是对c语言中的双链表进行了详细的分析介绍,需要的朋友可以过来参考下
收藏 0 赞 0 分享

浅析如何在c语言中调用Linux脚本

如何在c语言中调用Linux脚本呢?下面小编就为大家详细的介绍一下吧!需要的朋友可以过来参考下
收藏 0 赞 0 分享

深入解析unsigned int 和 int

以下是对unsigned int和int进行了详细的分析介绍,需要的朋友可以过来参考下
收藏 0 赞 0 分享

浅谈C++中的string 类型占几个字节

本篇文章小编并不是为大家讲解string类型的用法,而是讲解我个人比较好奇的问题,就是string 类型占几个字节
收藏 0 赞 0 分享
查看更多