Netmap 是一个高性能收发原始数据包的框架,由 Luigi Rizzo 等人开发完成,其包含了内核模块以及用户态库函数。其目标是,不修改现有操作系统软件以及不需要特殊硬件支持,实现用户态和网卡之间数据包的高性能传递。其原理图如下,数据包不经过操作系统内核进行处理,用户空间程序收发数据包时,直接与网卡进行通信。
代码位置:https://github.com/luigirizzo/netmap
在 Netmap 框架下,内核拥有数据包池,发送环接收环上的数据包不需要动态申请,有数据到达网卡时,当有数据到达后,直接从数据包池中取出一个数据包,然后将数据放入此数据包中,再将数据包的描述符放入接收环中。内核中的数据包池,通过 mmap 技术映射到用户空间。用户态程序最终通过 netmap_if 获取接收发送环 netmap_ring,进行数据包的获取发送。
(1)性能高 :数据包不走传统协议栈,不需要层层解析,用户态直接与网卡的接受环和发送环交互。性能高的具体原因有一下三个:
(2)稳定性高 :有关网卡寄存器数据的维护都是在内核模块进行,用户不会直接操作寄存器。所以在用户态操作时,不会导致操作系统崩溃
(3)亲和性 :可采用了 CPU 亲和性,实现 CPU 和网卡绑定,提高性能。
(4)易用性好 :API 操作简单,用户态只需要调用 ioctl 函数即可完成数据包收发工作
(5)与硬件解耦 :不依赖硬件,只需要对网卡驱动程序稍微做点修改就可以使用此框架(几十行行),传统网卡驱动将数据包传递给操作系统内核中协议栈,而修改后的数据包直接放入 Netmap_ring 供用户使用。
小编推荐自己的linuxC/C++语言技术交流群:【1106675687】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!
1.netmap API 主要为两个头文件 netmap.h 和 netmap_user.h ,当解压下载好的 netmap
程序后,在./netmap/sys/net/目录下,本文主要对这两个头文件进行分析。
2.我们从 netmap_user.h 头文件开始看起。
这两个宏定义是对编译器做优化的,并不会对变量做什么改变。后面看到这两个宏的调用自动忽略就好了。
#ifndef likely
#define likely(x) #define unlikely(x)
builtin_expect(!!(x), 1)
builtin_expect(!!(x), 0)
#endif /* likely and unlikely */
1._NETMAP_OFFSET
解释:该宏定义的作用是将 ptr 指针(强转成 char *类型)向右偏移 offset 个字节,再将其转化为指定的类型 type。
2.NETMAP_IF
解释:该宏定义将_base 指针向右偏移_ofs 个字节后,强转为 netmap_if *类型返回。在 nemap中通过此宏得到 d->nifp 的地址。
3.NETMAP_TXRING
解释:
4.NETMAP_RXRING
解释:通过该宏定义,可以找到 nifp 的第 index 个接收环的地址,其中(nifp)->ring_ofs[]里面的下标为 index+(nifp)->ni_tx_rings+1,正好与发送环的偏移量区间隔开 1 个。(我想这应该是作者特意设计的)
5.NETMAP_BUF
解释:
6.NETMAP_BUF_IDX
解释:
1.调用 nm_open 函数时,如:nmr = nm_open(“netmap:eth0”, NULL, 0, NULL); nm_open()会对传递的 ifname 指针里面的字符串进行分析,提取出网络接口名。
2.nm_open() 会 对 struct nm_desc *d 申 请 内 存 空 间 , 并 通 过 d->fd = open(NETMAP_DEVICE_NAME,O_RDWR);打开一个特殊的设备/dev/netmap 来创建文件描述符d->fd。
3.通过 ioctl(d->fd, NIOCREGIF, &d->req)语句,将 d->fd 绑定到一个特殊的接口,并对 d->req
结构体里面的成员做初始化,包括 a.在共享内存区域中 nifp 的偏移,b.共享区域的大小
nr_memsize,c.tx/rx 环的大小 nr_tx_slots/nr_rx_slots(大小为 256),d.tx/rx 环的数量 nr_tx_rings、
nr_rx_rings(视硬件性能而定)等。
4.接着在 if ((!(new_flags & NM_OPEN_NO_MMAP) || parent) && nm_mmap(d, parent))语句中调用 nm_mmap 函数,继续给 d 指针指向的内存赋值。
nm_mmap()源码:
static int nm_mmap(struct nm_desc *d, const struct nm_desc *parent)
{
//XXX TODO: check if mmap is already done
if (IS_NETMAP_DESC(parent) && parent->mem && parent->req.nr_arg2
== d->req.nr_arg2)
{
/* do not mmap, inherit from parent */
D("do not mmap, inherit from parent");
d->memsize = parent->memsize;
d->mem = parent->mem;
} else
{
/* XXX TODO: 检查如果想申请的内存太大 (or there is overflow)
*/
d->memsize = d->req.nr_memsize; /* 将需要申请的内存大小赋值给 d->memsize */
d->mem = mmap(0, d->memsize, PROT_WRITE | PROT_READ, MAP_SHARED, d->fd, 0); /* 申请共享内存 */
if (d->mem == MAP_FAILED)
{
goto fail;
}
d->done_mmap = 1;
}
{
struct netmap_if *nifp = NETMAP_IF(d->mem, d->req.nr_offset);
/*通过 d->req.nr_offset 这个偏移量的到 nifp 的地址,NETMAP_IF 前面说过
*/
int i;
/*
*for(i=0; i<=2; i++)
* printf("ring_ofs[%d]:0x%x\n",i,nifp->ring_ofs[i]); // 这里是我自己加的,为了手动计算收/发环的偏移量
*/
struct netmap_ring *r = NETMAP_RXRING(nifp,); //对 nifp,找接收包的环 r,因为 index 为 0,所以省略了
*(struct netmap_if **) (uintptr_t) &(d->nifp) = nifp; // 对d->nifp 赋值,虽然 d->nifp 使用 const 定义的,但对其取地址再强值类型转换后,依然可以对其指向的空间进行操作
*(struct netmap_ring **) (uintptr_t) &d->some_ring = r; //同理,对 d->some_ring 进行赋值,此处指向了第一个接受(rx)环。
//printf("buf_ofs:0x%x\n", (u_int)r->buf_ofs);
*(void **) (uintptr_t) &d->buf_start = NETMAP_BUF(r, 0);//计算第一个 buffer 的地址,并存入 d->buf_start 指针中
*(void **) (uintptr_t) &d->buf_end = (char *) d->mem + d->memsize; //计算共享区间的最后一个地址,赋值给 d->buf_end
}
return 0;
fail: return EINVAL;
}
其中:
到这里,netmap 如何访问到内存槽中的每一个 buffer 的,我们都知道了。实际上 netmap 运行的数据结构就和下图描述的一样:
4.在 struct nm_desc 中,nifp,some_ring,buf_start,buf_end 等指针都定义为 const 的, 但我们通过对其取地址再强转指针的方式去往这些指针指向的内存中赋值。
注:在 nm_mmap()中使用 mmap()申请共享的时候,这些数据结构里数据的设计是内核模块就已写好了的,我们在这里其实是在做验证。
nm_nextpkt()源代码:
static u_char *nm_nextpkt(struct nm_desc *d, struct nm_pkthdr *hdr)
{
int ri = d->cur_rx_ring; //当前的接收环的编号
do
{
/* compute current ring to use */
struct netmap_ring *ring = NETMAP_RXRING(d->nifp, ri); // 得到当前 rx 环的地址
if (!nm_ring_empty(ring)) //判断环里是否有新到的包
{
u_int i = ring->cur; //当前该访问哪个槽(buffer)了
u_int idx = ring->slot[i].buf_idx; //得到第 i 个 buffer 的下标
//printf("%d\n", idx);
u_char *buf = (u_char *) NETMAP_BUF(ring, idx); //得到存有到来数据包的地址
// builtin_prefetch(buf);
hdr->ts = ring->ts;
hdr->len = hdr->caplen = ring->slot[i].len;
ring->cur = nm_ring_next(ring, i); //ring->cur 向后移动一位
/* we could postpone advancing head if we want
* to hold the buffer. This can be supported in
* the future.
*/
ring->head = ring->cur;
d->cur_rx_ring = ri; //将当前环(d->cur_rx_ring)指向第 ri 个(因为可能有多个环)。
return buf; //将数据包地址返回
}
ri++;
if (ri > d->last_rx_ring) //如果 ri 超过了 rx 环的数量,则再从第一个 rx 环开始检测是否有包到来。
ri = d->first_rx_ring;
} while (ri != d->cur_rx_ring);
return NULL; /* 什么也没发现 */
}
源代码:
static int nm_inject(struct nm_desc *d, const void *buf, size_t size)
{
u_int c, n = d->last_tx_ring - d->first_tx_ring + 1;
for (c = 0; c < n; c++)
{
/* 计算当前的环去使用(compute current ring to use) */
struct netmap_ring *ring;
uint32_t i, idx;
uint32_t ri = d->cur_tx_ring + c; //该访问第几个 tx 环了
if (ri > d->last_tx_ring) //当超过访问的 tx 环的下标范围时, 从头开始访问
ri = d->first_tx_ring;
ring = NETMAP_TXRING(d->nifp, ri); //得到当前 tx 环的地址
if (nm_ring_empty(ring)) //如果当前 tx 环是满的(ring->cur=ring->tail 表示没地方存数据包了),就跳过
static int nm_inject(struct nm_desc *d, const void *buf, size_t size)
{
u_int c, n = d->last_tx_ring - d->first_tx_ring + 1;
for (c = 0; c < n; c++)
{
/* 计算当前的环去使用(compute current ring to use) */
struct netmap_ring *ring;
uint32_t i, idx;
uint32_t ri = d->cur_tx_ring + c; //该访问第几个 tx 环了
if (ri > d->last_tx_ring) //当超过访问的 tx 环的下标范围时, 从头开始访问
ri = d->first_tx_ring;
ring = NETMAP_TXRING(d->nifp, ri); //得到当前 tx 环的地址
if (nm_ring_empty(ring)) //如果当前 tx 环是满的(ring->cur=ring->tail 表示没地方存数据包了),就跳过
源代码:
static int nm_close(struct nm_desc *d)
{
/*
* ugly trick to avoid unused warnings
*/
static void * xxzt[] attribute ((unused)) =
{ (void *) nm_open, (void *) nm_inject, (void *) nm_dispatch, (void *) nm_nextpkt };
if (d == NULL || d->self != d)
return EINVAL;
if (d->done_mmap && d->mem)
munmap(d->mem, d->memsize); //释放申请的共享内存
if (d->fd != -1)
{
close(d->fd); //关闭文件描述符
}
bzero(d, sizeof(*d)); //将 d 指向的空间全部置 0
free(d); //释放指针 d 指向的空间
return 0;
系统启动后,
添加绑定的网卡的 IP 地址,十六进制 IP 地址,网卡相应的 MAC 地址。代码地址
https://github.com/wangbojing/NtyTcp.git
环境编译,下面以 ubuntu server 版本为例。先安装 netmap
Ubuntu 14.04
https://github.com/wangbojing/netmap.git
Ubuntu 16.04
https://github.com/luigirizzo/netmap.git
#./configure # make
#make install
进入 ntytcp 的目录直接 make
截至目前,40gpbs、32-cores、256G RAM 的 X86 服务器在 Newegg 网站上的报价是几千美元。实际上以这样的硬件配置来看,它完全可以处理 1000 万个以上的并发连接,如果它们不能,那是因为你选择了错误的软件,而不是底层硬件的问题。
可以预见在接下来的 10 年里,因为 IPv6 协议下每个服务器的潜在连接数都是数以百万级的,单机服务器处理数百万的并发连接(甚至千万)并非不可能,但我们需要重新审视目前主流 OS 针对网络编程这一块的具体技术实现。
很多人会想当然的认为,要实现 C10M(即单机千万)并发连接和处理能力,是不可能的。不过事实并非如此,现在系统已经在用你可能不熟悉甚至激进的方式支持千万级别的并发连接。
要知道它是如何做到的,我们首先要了解 Errata Security 的 CEO Robert Graham,以及他在Shmoocon 2013 大会上的“天方夜谈”视频记录: C10M Defending The Internet At Scale(此为 Yutube 视频,你懂的)。
Robert 用一种我以前从未听说的方式来很巧妙地解释了这个问题。他首先介绍了一点有关
Unix 的历史,Unix 的设计初衷并不是一般的服务器操作系统,而是电话网络的控制系统。由于是实际传送数据的电话网络,所以在控制层和数据层之间有明确的界限。问题是我们现在根本不应该使用 Unix 服务器作为数据层的一部分。正如设计只运行一个应用程序的服务器内核,肯定和设计多用户的服务器内核是不同的。
Robert Graham 的结论是:OS 的内核不是解决 C10M 问题的办法,恰恰相反 OS 的内核正是导致 C10M 问题的关键所在。
这也就意味着:
不要让 OS 内核执行所有繁重的任务:将数据包处理、内存管理、处理器调度等任务从内核转移到应用程序高效地完成,让诸如 Linux 这样的 OS 只处理控制层,数据层完全交给应用程序来处理。
最终就是要设计这样一个系统,该系统可以处理千万级别的并发连接,它在 200 个时钟周期
内处理数据包,在 14 万个时钟周期内处理应用程序逻辑。由于一次主存储器访问就要花费
300 个时钟周期,所以这是最大限度的减少代码和缓存丢失的关键。
面向数据层的系统可以每秒处理 1 千万个数据包,面向控制层的系统,每秒只能处理 1 百万个数据包。这似乎很极端,请记住一句老话:可扩展性是专业化的,为了做好一些事情,你不能把性能问题外包给操作系统来解决,你必须自己做。
10 年前,开发人员处理 C10K 可扩展性问题时,尽量避免服务器处理超过 1 万个的并发连接。通过改进操作系统内核以及用事件驱动服务器(典型技术实现如:Nginx 和
Node)代替线程服务器(典型代表:Apache),使得这个问题已经被解决。人们用十年的时间从 Apache 转移到可扩展服务器,在近几年,可扩展服务器的采用率增长得更快了。 以传统网络编程模型作为代表的 Apache 为例,我们来看看它在 C10K 问题上的局限表
现在哪些方面,并针对性的讨论对应的解决方法。Apache 的问题在于服务器的性能会随着连接数的增多而变差,实际上性能和可扩展性并不是一回事。当人们谈论规模时,他们往往是在谈论性能,但是规模和性能是不同的,比如 Apache。持续几秒的短期连接:比如快速事务,如果每秒处理 1000 个事务,只能有约 1000 个并发连接到服务器。如果事务延长
到 10 秒,要维持每秒 1000 个事务则必须打开 1 万个并发连接。这种情况下:尽管你不顾
DoS 攻击,Apache 也会性能陡降,同时大量的下载操作也会使 Apache 崩溃。
如果每秒处理的连接从 5 千增加到 1 万,你会怎么做?比方说,你升级硬件并且提高
处理器速度到原来的 2 倍。到底发生了什么?你得到两倍的性能,但你没有得到两倍的处
理规模。每秒处理的连接可能只达到了 6000。你继续提高速度,情况也没有改善。甚至 16
倍的性能时,仍然不能处理 1 万个并发连接。所以说性能和可扩展性是不一样的。
问题在于 Apache 会创建一个 CGI 进程,然后关闭,这个步骤并没有扩展。为什么呢? 内核使用的 O(N^2)算法使服务器无法处理 1 万个并发连接。
OS 内核中的两个基本问题:
通过上述针对 Apache 所表现出的问题,实际上彻底解决并发性能问题的解决方法的根本就是改进 OS 内核使其在常数时间内查找,使线程切换时间与线程数量无关,使用一个新的可扩展 epoll()/IOCompletionPort 常数时间去做 socket 查询。
因为线程调度并没有得到扩展,所以服务器大规模对 socket 使用 epoll 方法,这样就导致需要使用异步编程模式,而这些编程模式正是 Nginx 和 Node 类型服务器具有的。所以当从 Apache 迁移到 Nginx 和 Node 类型服务器时,即使在一个配置较低的服务器上增加连接数,性能也不会突降。所以在处理 C10K 连接时,一台笔记本电脑的速度甚至超过了 16 核的服务器。这也是前一个 10 年解决 C10K 问题的普遍方法。
实现 10M(即 1 千万)的并发连接挑战意味着什么:
1.理由概述
硬件不是 10M 问题的性能瓶颈所在处,真正的问题出在软件上,尤其是*nux 操作系统。理由如下面这几点:
2.解决思路
解决这些问题的关键在于如何将功能逻辑做好恰当的划分,比如专门负责控制逻辑的控制面和专门负责数据逻辑的数据面。数据面专门负责数据的处理,属于资源消耗的主要因素, 压力巨大,而相比如此,控制面只负责一些偶尔才有非业务逻辑,比如与外部用户的交互、信息的统计等等。我之前接触过几种网络数据处理框架,比如 Intel 的 DPDK、6wind、windriver, 它们都针对 Linux 系统做了特别的补充设计,增加了数据面、快速路径等等特性,其性能的提升自然是相当巨大。
看一下这些高性能框架的共同特点:
3.关于 Intel 的 DPDK 框架/ Netmap 开源框架
随着网络技术的不断创新和市场的发展,越来越多的网络设备基础架构开始向基于通用处理器平台的架构方向融合,期望用更低的成本和更短的产品开发周期来提供多样的网络单元和丰富的功能,如应用处理、控制处理、包处理、信号处理等。为了适应这一新的产业趋势,
Intel 推出了基于 Intel x86 架构 DPDK (Data Plane Development Kit,数据平面开发套件) 实现了高效灵活的包处理解决方案。经过近 6 年的发展,DPDK 已经发展成支持多种高性能网卡和多通用处理器平台的开源软件工具包。
综上所述,解决 C10M 问题的关键主要是从下面几个方面入手:
网卡问题
CPU 问题
内存问题
以 Linux 为例,解决的思咯就是将控制层交给 Linux,应用程序管理数据。应用程序与内核之间没有交互、没有线程调度、没有系统调用、没有中断,什么都没有。 然而,你有的是在 Linux 上运行的代码,你可以正常调试,这不是某种怪异的硬件系统,需要特定的工程师。你需要定制的硬件在数据层提升性能,但是必须是在你熟悉的编程和开发环境上进行。