(unix domain socket)使用udp发送>=128K的消息会报ENOBUFS的错误

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 a system 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 be made to disk of a real memory segment thereby allowing the listener access to some 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都没有找到原因,因此分享此文,以警惕后者。

 

作者:吴秦
出处:http://www.cnblogs.com/skynet/
本文基于署名 2.5 中国大陆 许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名吴秦 (包含链接).

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的本地domain套接字udp协议的客户端和服务器端互相发送接受消息的c语言代码。 服务器端代码: ```c #include <stdio.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #define SOCKET_NAME "/tmp/udp_socket" int main() { int server_fd; struct sockaddr_un server_addr; char buffer[1024] = {0}; if ((server_fd = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) { perror("socket failed"); return 1; } memset(&server_addr, 0, sizeof(struct sockaddr_un)); server_addr.sun_family = AF_UNIX; strncpy(server_addr.sun_path, SOCKET_NAME, sizeof(server_addr.sun_path) - 1); if (bind(server_fd, (const struct sockaddr*) &server_addr, sizeof(struct sockaddr_un)) == -1) { perror("bind failed"); close(server_fd); return 1; } while (1) { struct sockaddr_un client_addr; socklen_t len = sizeof(struct sockaddr_un); int n = recvfrom(server_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &client_addr, &len); if (n == -1) { perror("recvfrom failed"); continue; } printf("Received message from client: %s\n", buffer); if (sendto(server_fd, "Hello from server", 18, 0, (struct sockaddr*) &client_addr, len) == -1) { perror("sendto failed"); } } close(server_fd); unlink(SOCKET_NAME); return 0; } ``` 客户端代码: ```c #include <stdio.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #define SOCKET_NAME "/tmp/udp_socket" int main() { int client_fd; struct sockaddr_un server_addr; char buffer[1024] = {0}; if ((client_fd = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) { perror("socket failed"); return 1; } memset(&server_addr, 0, sizeof(struct sockaddr_un)); server_addr.sun_family = AF_UNIX; strncpy(server_addr.sun_path, SOCKET_NAME, sizeof(server_addr.sun_path) - 1); if (connect(client_fd, (const struct sockaddr*) &server_addr, sizeof(struct sockaddr_un)) == -1) { perror("connect failed"); close(client_fd); return 1; } if (send(client_fd, "Hello from client", 18, 0) == -1) { perror("send failed"); close(client_fd); return 1; } int n = read(client_fd, buffer, sizeof(buffer)); if (n == -1) { perror("read failed"); close(client_fd); return 1; } printf("Received message from server: %s\n", buffer); close(client_fd); return 0; } ``` 注意,这段代码使用了本地domain套接字,因此需要在服务器和客户端之间使用同一个地址。在这个例子中,我们使用了`/tmp/udp_socket`作为套接字地址。这个地址应该在客户端和服务器端代码中保持一致。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值