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

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

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都没有找到原因,因此分享此文,以警惕后者。

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

声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:notice#nhooo.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。