❶ linux下用C++写UDP通信程序该怎么写
用ACE,它提供了很完善的一套构架。
#include "ace/SOCK_Dgram_Mcast.h"
#include "ace/INET_Addr.h"
#include "ace/OS.h"埋渗
#include "ace/ACE.h"
#include "ace/Log_Msg.h"
int ACE_TMAIN (int argc, char* argv[])
{
ACE_Time_Value ti = ACE_Time_Value (0, 20000);
ACE_INET_Addr local_addr ((u_short) 9000);
ACE_INET_Addr recv_addr ;
ACE_SOCK_Dgram recv_dgram;
//打开端口
if (recv_dgram.open (local_addr) == -1) {
ACE_DEBUG ((LM_ERROR, "%p\n", "Recieving data gram open"));
ACE_OS::exit (-1);
}
ACE_Time_Value t = ACE_Time_Value::zero;
while (1) {
//recieve data gram
char buf[2048];
//接收UDP数据,
ssize_t recv = recv_dgram.recv (buf, 2048, recv_addr);
//弯誉脊显示接收了多少数据,以及数据的来虚谨源
ACE_DEBUG ((LM_DEBUG, "%M [%t %N:%l] %s:%d recieved %d\n", recv_addr.get_host_addr (), recv_addr.get_port_number (), recv));
if (recv <= 0) {
ACE_DEBUG ((LM_DEBUG, "%M [%t %N:%l] Can't recieve any data gram from the port (%s:%d)\n", recv_addr.get_host_addr (),recv_addr.get_port_number ()));
ACE_OS::sleep (ti);
continue;
}
}
return 0;
}
❷ linux c编写udp程序的bind报错
addr.sin_addr.s_addr=htons(SERVER_PORT);
这个是IP地址,你给的端口号。
❸ 有谁能给我贴一份可用的 UDP 组播代码吗
组播技术可形象的描述如下:
假设一个企业分布于各地的子公司(两个以上)之间需要通过Internet进行实时的交换信息(数据,声音,图像),他们的计算机可能不属于同一物理网 络,甚至不属于同一自治系统,这种通信的特点是“多点”式的。子公司发出的数据希望其他子公司都能收到,而总部发出的指示全体子公司都应收到。这种多点通信方式为组内广播,即组播技术,也称多播技术,多目网关技术。
1.2、TCP/IP传送方式
组播技术是TCP/IP传送方式的一种。在我们讨论组播技术之前先来看看TCP/IP传送方式。TCP/IP传送方式有三种:单播,广播,组播。
单播(Unicast)传输:在发送者和每一接收者之间需要单独的数据信道。 如果一台主机同时给很少量的接收者传输数据,一般没有什么问题。但如果有大量主机希望获得数据包的同一份拷贝时却很难实现。这将导致发送者负担沉重、延迟长、网络拥塞;为保证一定的服务质量需增加硬件和带宽。
组播(Multicast)传输:它提高了数据传送效率。减少了主干网出现拥塞的可能性。组播组中的主机可以是在同一个物理网络,也可以来自不同的物理网络(如果有组播路由器的支持)。
广播(Broadcast)传输:是指在IP子网内广播数据包,所有在子网内部的主机都将收到这些数据包。广播意味着网络向子网主机都投递一份数据包,不论这些主机是否乐于接收该数据包。然而广播的使用范围非常小, 只在本地子网内有效,因为路由器会封锁广播通信。广播传输增加非接收者的开销。
组播是一种允许一个或多个发送者(组播源)发送单一的数据包到多个接收者(一次的,同时的)的网络技术。 组播源把数据包发送到特定组播组,而只有属于该组播组的地址才能接收到数据包。组播可以大大的节省网络带宽, 因为无论有多少个目标地址,在整个网络的任何一条链路上只传送单一的数据包。 它提高了数据传送效率。减少了主干网出现拥塞的可能性。组播组中的主机可以是在同一个物理网络, 也可以来自不同的物理网络(如果有组播路由器的支持)。
❹ 在进行UDP组播编程时,主机不能退出组播网,是怎么回事
zhshdxhxhztvghnztdyda
❺ Linux环境下C开发_linux搭建c语言开发环境
一:C语言嵌入式Linux工程师的学习需要具备一定的C语言基础,C语言是嵌入式领域最重要也是最主要的编程语言,通过大量编程实例重点理解C语言的基础编程以及高级编程知识。包括:基本数据类型、数组、指针、结构体、链表、文件操作、队列、栈等。
二:Linux基础Linux操作系统的概念、安装方法,详细了解Linux下的目录结构、基本命令、编辑器VI,编译器GCC,调试器GDB和Make项目管理工具,ShellMakefile脚本编写等知识,嵌入式开发环境的搭建。
三:Linux系统编程重点学习标准I/O库,Linux多任务编程中的多进程和多线程,以及进程间通信(pipe、FIFO、消息队列、共享内存、signal、信号量等),同步与互斥对共享资源访问控制等重要知识,主要提升对Linux应用开发的理解和代码调试的能力。
四:Linux网络编程计算机网络在嵌入式Linux系统应用开发过程中使用非常广泛,通过Linux网络发展、TCP/IP协议、socket编程、TCP网络编程、UDP网络编程、Web编程开发等方面入手,全面了解Linux网络应用程序开发。重点学习网络编程相关API,熟练掌握TCP协议服务器的编程方法和并发服务器的实现,了解HTTP协议及其实现方法,熟悉UDP广播、多播的原理及编程方法,掌握混合C/S架构网络通信系统的设计,熟悉HTML,Javascript等Web编程技术及实现方法。
五:数据结构与算法数据结构及算法在嵌入式底层驱动、通信协议、及各种引擎开发中会得到大量应用,对其掌握的好坏直接影响程序的效率、简洁及健壮旅瞎性。此阶段的学习要重点理解数据结构与算法的基础内容,包括顺序表、链表、队列、栈、树、图、哈希表、各种查找排序算法等应用及其C语言实现过程。
六:C、QTC是Linux应用开发主要语言之一,本阶段重点掌握面向对象编程的基本思想以及C的重要内容。图形界面编程是嵌入式开发中非常重要的一个环节。由于QT具有跨平台、面向对象、丰富API、支持2D/3D渲染、支持XML、多国语等强大功能,在嵌入式领域的GUI开发中得到了广范的应用,在本阶段通过基于QT图形库的学习使学员可以熟练编写GUI程序,并移植QT应用程序到Cortex-A8平台。包括IDE使用、QT部件及布局管理器、信息与槽机制的应用、鼠标、键盘及绘图事件处理及文件处理的应用。
七:CortexA8、Linux平台开发通过基于ARMCortex-A8处理s5pv210了解芯片手册的基本阅读技巧,掌握s5pv210系统资源、时钟控制器、电源管理、异常中断控制器、nandflash控制器等模块,为底层平台搭建做好准备。Linux平台包括内核裁减、内核移植、交叉编译、GNU工具使用、内核调试、Bootloader介绍、制作与原理分析、根文件系统制作以及向内核中添加自己的模块,并在s5pv210实验平台上运行自己制作的Linux系统,集成部署Linux系统整个流程。同时了解Android操作系统开发流程。Android系统是基于Linux平台的开源操作系统,该平台由操作系统、中间件、用户界面和应用软件组成,是首个为移动终端打造的真正开放和完整的移动软件,目前它的应用不再局限于移动终端,还包括数据电视、机顶盒、PDA等消费类电子产品。
八:驱动开发拆颤空驱动程序设计是嵌入式Linux开发工作中重要的一部分,也是比较困难的一部分。本阶洞租段的学习要熟悉Linux的内核机制、驱动程序与用户级应用程序的接口,掌握系统对设备的并发操作。熟悉所开发硬件的工作原理,具备ARM硬件接口的基础知识,熟悉ARMCortex-A8处理器s5pv210各资源、掌握Linux设备驱动原理框架,熟悉工程中常见Linux高级字符设备、块设备、网络设备、USB设备等驱动开发,在工作中能独立胜任底层驱动开发。
以上就是列出的关于一名合格嵌入式Linux开发工程师所必学的理论知识,其实,作为一个嵌入式开发人员,专业知识和项目经验同样重要,所以在我们的理论学习中也要有一定的项目实践,锻炼自己的项目开发能力。
❻ 如何在linux下实现udp的多进程方式
tar xzf atop-2.1-1.tar.gz && cd atop-2.1
make && sudo make install
atop
atop交互命令枯宴:
t(手动刷新,默认刷新间隔为10秒,i可以修改时间间隔) g(默认输出) d(磁盘) m(内存)
s(调度) v(可变) c(命令行) p(进程统计) u(用户统计) n(网络,需要内核模块netatop支持)
C(按CPU排序) M(按内存MEM排让皮序) D(按坦败差磁盘DSK排序) N(按网络NET排序)
❼ linux shell命令行向udp端口发送数据
先nc -uv,然岁悔后在输入数据即可
nc -uvz可以用来测试udp端口是否开启监听
比如要向本地10001端正雀橡口发送数据,可以使用
echo "hello" > /dev/udp/localhost/10001
方法二的好处在于,发送完数据后自动退出。当我们需要在命令行下循环向特定udp端口发送数据时,我们只能使用方法二。举旁
例如
while true
do
echo "xxxx" > /dev/udp/localhost/10001
done
❽ linux 下用socket 文件传输问题(UDP)
要下班了,时间急,不写代码了先给你一个思路
1 实现最简单的udp socket 模型,实现发送一个字符串。
2 实现一个简单的打开文件,读取文件的例子,如用fgets(),类似的函数有很多,然后再把读取的文件内容忘另一个文件里写(相关函数fopen(),write(),read())。
3 把上面两个函数结合到一起,在客户端实现打开要传送的文件,按一定的大小读取,读取后调用sendto()发送到服务器端。在服务器端创建一个文件,然后调用recvfrom()接受客户端发送过来的数据,向来是创建的那个文件中写。
下面是改好的udp发送文件的例子。
服务器端程序的编译
gcc -o file_server file_server
客户端程序的编译
gcc -o file_client file_client.c
服务器程序和客户端程应当分别运行在2台计算机上.
服务器端程序的运行,在一个计算机的终端执行
./file_server
客户端程序的运行,在另一个计算机的终端中执行
./file_client 运行服务器程序的计算机的IP地址
根据提示输入要传输的服务器上的文件,该文件在服务器的运行目录上
在实际编程和测试中,可以用2个终端代替2个计算机,这样就可以在一台计算机上测试网络程序,
服务器端程序的运行,在一个终端执行
./file_server
客户端程序的运行,在另一个终端中执行
./file_client 127.0.0.1
说明: 任何计算机都可以通过127.0.0.1访问自己. 也可以用计算机的实际IP地址代替127.0.0.1
//////////////////////////////////////////////////////////////////////////////////////
// file_server.c 文件传输顺序服务器示例
//////////////////////////////////////////////////////////////////////////////////////
//本文件是服务器的代码
#include <netinet/in.h> // for sockaddr_in
#include <sys/types.h> // for socket
#include <sys/socket.h> // for socket
#include <stdio.h> // for printf
#include <stdlib.h> // for exit
#include <string.h> // for bzero
/*
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
*/
#define HELLO_WORLD_SERVER_PORT 6666
#define LENGTH_OF_LISTEN_QUEUE 20
#define BUFFER_SIZE 1024
#define FILE_NAME_MAX_SIZE 512
int main(int argc, char **argv)
{
//设置一个socket地址结构server_addr,代表服务器internet地址, 端口
struct sockaddr_in server_addr, pcliaddr;
bzero(&server_addr,sizeof(server_addr)); //把一段内存区的内容全部设置为0
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htons(INADDR_ANY);
server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
//创建用于internet的据报套接字(UDPt,用server_socket代表服务器socket
// 创建数据报套接字(UDP)
int server_socket = socket(PF_INET,SOCK_DGRAM,0);
if( server_socket < 0)
{
printf("Create Socket Failed!");
exit(1);
}
//把socket和socket地址结构联系起来
if( bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr)))
{
printf("Server Bind Port : %d Failed!", HELLO_WORLD_SERVER_PORT);
exit(1);
}
while (1) //服务器端要一直运行
{
//定义客户端的socket地址结构client_addr
struct sockaddr_in client_addr;
socklen_t n = sizeof(client_addr) ;
int length;
char buffer[BUFFER_SIZE];
bzero(buffer, BUFFER_SIZE);
length = recvfrom(new_server_socket,buffer,BUFFER_SIZE,0,&pcliaddr,&n);
if (length < 0)
{
printf("Server Recieve Data Failed!\n");
break;
}
char file_name[FILE_NAME_MAX_SIZE+1];
bzero(file_name, FILE_NAME_MAX_SIZE+1);
strncpy(file_name, buffer, strlen(buffer)>FILE_NAME_MAX_SIZE?FILE_NAME_MAX_SIZE:strlen(buffer));
// int fp = open(file_name, O_RDONLY);
// if( fp < 0 )
FILE * fp = fopen(file_name,"r");
if(NULL == fp )
{
printf("File:\t%s Not Found\n", file_name);
}
else
{
bzero(buffer, BUFFER_SIZE);
int file_block_length = 0;
// while( (file_block_length = read(fp,buffer,BUFFER_SIZE))>0)
while( (file_block_length = fread(buffer,sizeof(char),BUFFER_SIZE,fp))>0)
{
printf("file_block_length = %d\n",file_block_length);
//发送buffer中的字符串到new_server_socket,实际是给客户端
if(send(new_server_socket,buffer,file_block_length,0)<0)
{
printf("Send File:\t%s Failed\n", file_name);
break;
}
bzero(buffer, BUFFER_SIZE);
}
// close(fp);
fclose(fp);
printf("File:\t%s Transfer Finished\n",file_name);
}
}
}
//////////////////////////////////////////////////////////////////////////////////////
// file_client.c 文件传输客户端程序示例
//////////////////////////////////////////////////////////////////////////////////////
//本文件是客户机的代码
#include <netinet/in.h> // for sockaddr_in
#include <sys/types.h> // for socket
#include <sys/socket.h> // for socket
#include <stdio.h> // for printf
#include <stdlib.h> // for exit
#include <string.h> // for bzero
/*
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
*/
#define HELLO_WORLD_SERVER_PORT 6666
#define BUFFER_SIZE 1024
#define FILE_NAME_MAX_SIZE 512
int main(int argc, char **argv)
{
if (argc != 2)
{
printf("Usage: ./%s ServerIPAddress\n",argv[0]);
exit(1);
}
//设置一个socket地址结构client_addr,代表客户机internet地址, 端口
struct sockaddr_in client_addr;
bzero(&client_addr,sizeof(client_addr)); //把一段内存区的内容全部设置为0
client_addr.sin_family = AF_INET; //internet协议族
client_addr.sin_addr.s_addr = htons(INADDR_ANY);//INADDR_ANY表示自动获取本机地址
client_addr.sin_port = htons(0); //0表示让系统自动分配一个空闲端口
//创建用于internet的流协议(TCP)socket,用client_socket代表客户机socket
int client_socket = socket(AF_INET,SOCK_DGRAM,0);
if( client_socket < 0)
{
printf("Create Socket Failed!\n");
exit(1);
}
//设置一个socket地址结构server_addr,代表服务器的internet地址, 端口
struct sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
if(inet_aton(argv[1],&server_addr.sin_addr) == 0) //服务器的IP地址来自程序的参数
{
printf("Server IP Address Error!\n");
exit(1);
}
server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
socklen_t server_addr_length = sizeof(server_addr);
char file_name[FILE_NAME_MAX_SIZE+1];
bzero(file_name, FILE_NAME_MAX_SIZE+1);
printf("Please Input File Name On Server:\t");
scanf("%s", file_name);
char buffer[BUFFER_SIZE];
bzero(buffer,BUFFER_SIZE);
strncpy(buffer, file_name, strlen(file_name)>BUFFER_SIZE?BUFFER_SIZE:strlen(file_name));
//向服务器发送buffer中的数据
socklen_t n = sizeof(server_addr) ;
sendto(client_socket,buffer,BUFFER_SIZE,0,(struct sockaddr*)&server_addr,n);
// int fp = open(file_name, O_WRONLY|O_CREAT);
// if( fp < 0 )
FILE * fp = fopen(file_name,"w");
if(NULL == fp )
{
printf("File:\t%s Can Not Open To Write\n", file_name);
exit(1);
}
//从服务器接收数据到buffer中
bzero(buffer,BUFFER_SIZE);
int length = 0;
while( length = recv(client_socket,buffer,BUFFER_SIZE,0))
{
if(length < 0)
{
printf("Recieve Data From Server %s Failed!\n", argv[1]);
break;
}
// int write_length = write(fp, buffer,length);
int write_length = fwrite(buffer,sizeof(char),length,fp);
if (write_length<length)
{
printf("File:\t%s Write Failed\n", file_name);
break;
}
bzero(buffer,BUFFER_SIZE);
}
printf("Recieve File:\t %s From Server[%s] Finished\n",file_name, argv[1]);
return 0;
}
❾ linux下udp编程如何同时获取源IP和端口及目的IP和端口
http://www.cnblogs.com/kissazi2/p/3158603.html
❿ 在Linux 上,编写一个每秒接收 100万UDP数据包的程序究竟有多难
首先,我们假设:
测量每秒的数据包(pps)比测量每秒字节数(Bps)更有意思。您可以通过更好的管道输送以及发送更长数据包来获取更高的Bps。而相比之下,提高pps要困难得多。
因为我们对pps感兴趣,我们的实验将使用较短的 UDP 消息。准确来说是 32 字节的 UDP 负载,这相当于以太网层的 74 字节。
在实验中,我们将使用两个物理服务器:“接收器”和“发送器”。
它们都有两个六核2 GHz的 Xeon处理器。每个服务器都启用了 24 个处理器的超线程(HT),有 Solarflare 的 10G 多队列网卡,有 11 个接收队列配置。稍后将详细介绍。
测试程序的源代码分别是:udpsender、udpreceiver。
预备知识
我们使用4321作为UDP数据包的端口,在开始之前,我们必须确保传输不会被iptables干扰:
Shell
receiver$ iptables -I INPUT 1 -p udp --dport 4321 -j ACCEPT
receiver$ iptables -t raw -I PREROUTING 1 -p udp --dport 4321 -j NOTRACK
为了后面测试方便,我们显式地定义IP地址:
Shell
receiver$ for i in `seq 1 20`; do
ip addr add 192.168.254.$i/24 dev eth2;
done
sender$ ip addr add 192.168.254.30/24 dev eth3
1. 简单的方法
开始我们做一些最简单的试验。通过简单地发送和接收,有多少包将会被传送?
模拟发送者的伪代码:
Python
fd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
fd.bind(("0.0.0.0", 65400)) # select source port to rece nondeterminism
fd.connect(("192.168.254.1", 4321))
while True:
fd.sendmmsg(["x00" * 32] * 1024)
因为我们使用了常见的系统调用的send,所以效率不会很高。上下文切换到内核代价很高所以最好避免它。幸运地是,最近Linux加入了一个方便的系统调用叫sendmmsg。它允许我们在一次调用时,发送很多的数据包。那我们就一次发1024个数据包。
模拟接受者的伪代码:
Python
fd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
fd.bind(("0.0.0.0", 4321))
while True:
packets = [None] * 1024
fd.recvmmsg(packets, MSG_WAITFORONE)
同样地,recvmmsg 也是相对于常见的 recv 更有效的一版系统调用。
让我们试试吧:
Shell
sender$ ./udpsender 192.168.254.1:4321
receiver$ ./udpreceiver1 0.0.0.0:4321
0.352M pps 10.730MiB / 90.010Mb
0.284M pps 8.655MiB / 72.603Mb
0.262M pps 7.991MiB / 67.033Mb
0.199M pps 6.081MiB / 51.013Mb
0.195M pps 5.956MiB / 49.966Mb
0.199M pps 6.060MiB / 50.836Mb
0.200M pps 6.097MiB / 51.147Mb
0.197M pps 6.021MiB / 50.509Mb
测试发现,运用最简单的方式可以实现 197k – 350k pps。看起来还不错嘛,但不幸的是,很不稳定啊,这是因为内核在核之间交换我们的程序,那我们把进程附在 CPU 上将会有所帮助
Shell
sender$ taskset -c 1 ./udpsender 192.168.254.1:4321
receiver$ taskset -c 1 ./udpreceiver1 0.0.0.0:4321
0.362M pps 11.058MiB / 92.760Mb
0.374M pps 11.411MiB / 95.723Mb
0.369M pps 11.252MiB / 94.389Mb
0.370M pps 11.289MiB / 94.696Mb
0.365M pps 11.152MiB / 93.552Mb
0.360M pps 10.971MiB / 92.033Mb
现在内核调度器将进程运行在特定的CPU上,这提高了处理器缓存,使数据更加一致,这就是我们想要的啊!
2. 发送更多的数据包
虽然 370k pps 对于简单的程序来说已经很不错了,但是离我们 1Mpps 的目标还有些距离。为了接收更多,首先我们必须发送更多的包。那我们用独立的两个线程发送,如何呢:
Shell
sender$ taskset -c 1,2 ./udpsender
192.168.254.1:4321 192.168.254.1:4321
receiver$ taskset -c 1 ./udpreceiver1 0.0.0.0:4321
0.349M pps 10.651MiB / 89.343Mb
0.354M pps 10.815MiB / 90.724Mb
0.354M pps 10.806MiB / 90.646Mb
0.354M pps 10.811MiB / 90.690Mb
接收一端的数据没有增加,ethtool –S 命令将显示数据包实际上都去哪儿了:
Shell
receiver$ watch 'sudo ethtool -S eth2 |grep rx'
rx_nodesc_drop_cnt: 451.3k/s
rx-0.rx_packets: 8.0/s
rx-1.rx_packets: 0.0/s
rx-2.rx_packets: 0.0/s
rx-3.rx_packets: 0.5/s
rx-4.rx_packets: 355.2k/s
rx-5.rx_packets: 0.0/s
rx-6.rx_packets: 0.0/s
rx-7.rx_packets: 0.5/s
rx-8.rx_packets: 0.0/s
rx-9.rx_packets: 0.0/s
rx-10.rx_packets: 0.0/s
通过这些统计,NIC 显示 4 号 RX 队列已经成功地传输大约 350Kpps。rx_nodesc_drop_cnt 是 Solarflare 特有的计数器,表明NIC发送到内核未能实现发送 450kpps。
有时候,这些数据包没有被发送的原因不是很清晰,然而在我们这种情境下却很清楚:4号RX队列发送数据包到4号CPU,然而4号CPU已经忙不过来了,因为它最忙也只能读350kpps。在htop中显示为:
多队列 NIC 速成课程
从历史上看,网卡拥有单个RX队列,用于硬件和内核之间传递数据包。这样的设计有一个明显的限制,就是不可能比单个CPU处理更多的数据包。
为了利用多核系统,NIC开始支持多个RX队列。这种设计很简单:每个RX队列被附到分开的CPU上,因此,把包送到所有的RX队列网卡可以利用所有的CPU。但是又产生了另一个问题:对于一个数据包,NIC怎么决定把它发送到哪一个RX队列?
用 Round-robin 的方式来平衡是不能接受的,因为这有可能导致单个连接中数据包的重排序。另一种方法是使用数据包的hash值来决定RX号码。Hash值通常由一个元组(源IP,目标IP,源port,目标port)计算而来。这确保了从一个流产生的包将最终在完全相同的RX队列,并且不可能在一个流中重排包。
在我们的例子中,hash值可能是这样的:
Shell
1
RX_queue_number = hash('192.168.254.30', '192.168.254.1', 65400, 4321) % number_of_queues
多队列 hash 算法
Hash算法通过ethtool配置,设置如下:
Shell
receiver$ ethtool -n eth2 rx-flow-hash udp4
UDP over IPV4 flows use these fields for computing Hash flow key:
IP SA
IP DA
对于IPv4 UDP数据包,NIC将hash(源 IP,目标 IP)地址。即
Shell
1
RX_queue_number = hash('192.168.254.30', '192.168.254.1') % number_of_queues
这是相当有限的,因为它忽略了端口号。很多NIC允许自定义hash。再一次,使用ethtool我们可以选择元组(源 IP、目标 IP、源port、目标port)生成hash值。
Shell
receiver$ ethtool -N eth2 rx-flow-hash udp4 sdfn
Cannot change RX network flow hashing options: Operation not supported
不幸地是,我们的NIC不支持自定义,我们只能选用(源 IP、目的 IP) 生成hash。
NUMA性能报告
到目前为止,我们所有的数据包都流向一个RX队列,并且一个CPU。我们可以借这个机会为基准来衡量不同CPU的性能。在我们设置为接收方的主机上有两个单独的处理器,每一个都是一个不同的NUMA节点。
在我们设置中,可以将单线程接收者依附到四个CPU中的一个,四个选项如下:
另一个CPU上运行接收器,但将相同的NUMA节点作为RX队列。性能如上面我们看到的,大约是360 kpps。
将运行接收器的同一 CPU 作为RX队列,我们可以得到大约430 kpps。但这样也会有很高的不稳定性,如果NIC被数据包所淹没,性能将下降到零。
当接收器运行在HT对应的处理RX队列的CPU之上,性能是通常的一半,大约在200kpps左右。
接收器在一个不同的NUMA节点而不是RX队列的CPU上,性能大约是330 kpps。但是数字会不太一致。
虽然运行在一个不同的NUMA节点上有10%的代价,听起来可能不算太坏,但随着规模的变大,问题只会变得更糟。在一些测试中,每个核只能发出250 kpps,在所有跨NUMA测试中,这种不稳定是很糟糕。跨NUMA节点的性能损失,在更高的吞吐量上更明显。在一次测试时,发现在一个坏掉的NUMA节点上运行接收器,性能下降有4倍。
3.多接收IP
因为我们NIC上hash算法的限制,通过RX队列分配数据包的唯一方法是利用多个IP地址。下面是如何将数据包发到不同的目的IP:
1
sender$ taskset -c 1,2 ./udpsender 192.168.254.1:4321 192.168.254.2:4321
ethtool 证实了数据包流向了不同的 RX 队列:
Shell
receiver$ watch 'sudo ethtool -S eth2 |grep rx'
rx-0.rx_packets: 8.0/s
rx-1.rx_packets: 0.0/s
rx-2.rx_packets: 0.0/s
rx-3.rx_packets: 355.2k/s
rx-4.rx_packets: 0.5/s
rx-5.rx_packets: 297.0k/s
rx-6.rx_packets: 0.0/s
rx-7.rx_packets: 0.5/s
rx-8.rx_packets: 0.0/s
rx-9.rx_packets: 0.0/s
rx-10.rx_packets: 0.0/s
接收部分:
Shell
receiver$ taskset -c 1 ./udpreceiver1 0.0.0.0:4321
0.609M pps 18.599MiB / 156.019Mb
0.657M pps 20.039MiB / 168.102Mb
0.649M pps 19.803MiB / 166.120Mb
万岁!有两个核忙于处理RX队列,第三运行应用程序时,可以达到大约650 kpps !
我们可以通过发送数据到三或四个RX队列来增加这个数值,但是很快这个应用就会有另一个瓶颈。这一次rx_nodesc_drop_cnt没有增加,但是netstat接收到了如下错误:
Shell
receiver$ watch 'netstat -s --udp'
Udp:
437.0k/s packets received
0.0/s packets to unknown port received.
386.9k/s packet receive errors
0.0/s packets sent
RcvbufErrors: 123.8k/s
SndbufErrors: 0
InCsumErrors: 0
这意味着虽然NIC能够将数据包发送到内核,但是内核不能将数据包发给应用程序。在我们的case中,只能提供440 kpps,其余的390 kpps + 123 kpps的下降是由于应用程序接收它们不够快。
4.多线程接收
我们需要扩展接收者应用程序。最简单的方式是利用多线程接收,但是不管用:
Shell
sender$ taskset -c 1,2 ./udpsender 192.168.254.1:4321 192.168.254.2:4321
receiver$ taskset -c 1,2 ./udpreceiver1 0.0.0.0:4321 2
0.495M pps 15.108MiB / 126.733Mb
0.480M pps 14.636MiB / 122.775Mb
0.461M pps 14.071MiB / 118.038Mb
0.486M pps 14.820MiB / 124.322Mb
接收性能较于单个线程下降了,这是由UDP接收缓冲区那边的锁竞争导致的。由于两个线程使用相同的套接字描述符,它们花费过多的时间在UDP接收缓冲区的锁竞争。这篇论文详细描述了这一问题。
看来使用多线程从一个描述符接收,并不是最优方案。
5. SO_REUSEPORT
幸运地是,最近有一个解决方案添加到 Linux 了 —— SO_REUSEPORT 标志位(flag)。当这个标志位设置在一个套接字描述符上时,Linux将允许许多进程绑定到相同的端口,事实上,任何数量的进程将允许绑定上去,负载也会均衡分布。
有了SO_REUSEPORT,每一个进程都有一个独立的socket描述符。因此每一个都会拥有一个专用的UDP接收缓冲区。这样就避免了以前遇到的竞争问题:
Shell
1
2
3
4
receiver$ taskset -c 1,2,3,4 ./udpreceiver1 0.0.0.0:4321 4 1
1.114M pps 34.007MiB / 285.271Mb
1.147M pps 34.990MiB / 293.518Mb
1.126M pps 34.374MiB / 288.354Mb
现在更加喜欢了,吞吐量很不错嘛!
更多的调查显示还有进一步改进的空间。即使我们开始4个接收线程,负载也会不均匀地分布:
两个进程接收了所有的工作,而另外两个根本没有数据包。这是因为hash冲突,但是这次是在SO_REUSEPORT层。
结束语
我做了一些进一步的测试,完全一致的RX队列,接收线程在单个NUMA节点可以达到1.4Mpps。在不同的NUMA节点上运行接收者会导致这个数字做多下降到1Mpps。
总之,如果你想要一个完美的性能,你需要做下面这些:
确保流量均匀分布在许多RX队列和SO_REUSEPORT进程上。在实践中,只要有大量的连接(或流动),负载通常是分布式的。
需要有足够的CPU容量去从内核上获取数据包。
To make the things harder, both RX queues and receiver processes should be on a single NUMA node.
为了使事情更加稳定,RX队列和接收进程都应该在单个NUMA节点上。
虽然我们已经表明,在一台Linux机器上接收1Mpps在技术上是可行的,但是应用程序将不会对收到的数据包做任何实际处理——甚至连看都不看内容的流量。别太指望这样的性能,因为对于任何实际应用并没有太大用处。