高性能收发原始数据包的框架(Netmap)

news2024/11/16 13:38:13

一、Netmap 简介


Netmap 是一个高性能收发原始数据包的框架,由 Luigi Rizzo 等人开发完成,其包含了内核模块以及用户态库函数。其目标是,不修改现有操作系统软件以及不需要特殊硬件支持,实现用户态和网卡之间数据包的高性能传递。其原理图如下,数据包不经过操作系统内核进行处理,用户空间程序收发数据包时,直接与网卡进行通信。
 

 

下面是我们用了netmap下的情况,你看到了,我们的netmap接收到数据后采用mmap的方式

直接映射到内存上,那么我们取数据就可以直接从内存上取数据,就是从网卡取数据了,不需

要像内核协议栈那样,拷贝两次才行,那么这就大大提高了网络的传输速度

 1.数据结构

 在 Netmap 框架下,内核拥有数据包池,发送环接收环上的数据包不需要动态申请,有数据到达网卡时,当有数据到达后,直接从数据包池中取出一个数据包,然后将数据放入此数据包中,再将数据包的描述符放入接收环中。内核中的数据包池,通过 mmap 技术映射到用户空间。用户态程序最终通过 netmap_if 获取接收发送环 netmap_ring,进行数据包的获取发送。

2.特点总结


(1)性能高 :数据包不走传统协议栈,不需要层层解析,用户态直接与网卡的接受环和发送环交互。性能高的具体原因有一下三个:

(a)系统调用以及处理数据包的时间花费少
(b)不需要进行数据包的内存分配:采用数据包池,当有数据到达后,直接从数据包池中取出一个数据包,然后将数据放入此数据包中,再将数据包的描述符放入接收环中。
(c)数据拷贝次数少:内核中的数据包采用 mmap 技术映射到用户态。所以数据包在到达用户态时,不需要进行数据包的拷贝。
(2)稳定性高 :有关网卡寄存器数据的维护都是在内核模块进行,用户不会直接操作寄存器。所以在用户态操作时,不会导致操作系统崩溃

(3)亲和性 :可采用了 CPU 亲和性,实现 CPU 和网卡绑定,提高性能。

(4)易用性好 :API 操作简单,用户态只需要调用 ioctl 函数即可完成数据包收发工作

(5)与硬件解耦 :不依赖硬件,只需要对网卡驱动程序稍微做点修改就可以使用此框架(几十行行),传统网卡驱动将数据包传递给操作系统内核中协议栈,而修改后的数据包直接放入 Netmap_ring 供用户使用。
 

 二、Netmap API 介绍

1.简要说明


netmap API 主要为两个头文件 netmap.h 和 netmap_user.h ,当解压下载好的 netmap
程序后,在./netmap/sys/net/目录下,本文主要对这两个头文件进行分析。
2.我们从 netmap_user.h 头文件开始看起
 

2.likely()和 unlikely()

这两个宏定义是对编译器做优化的,并不会对变量做什么改变。后面看到这两个宏的调用自动忽略就好了。

#ifndef likely

#define likely(x) #define unlikely(x)

3.netmap.h 头文件


1.netmap.h 被 netmap_user.h 调用, 里面定义了一些宏和几个主要的结构体, 如 nmreq{}, netmap_if{}, netmap_ring{}, netmap_slot{} 。
2.一个网卡(或者网络接口)只有一个 netmap_if{}结构,在使用 mmap()申请的共享内存中, 通过 netmap_if{}结构可以访问到任何一个发送/接收环(也就是 netmap_ring{}结构,一个netmap_if{}可以对应多发送/接收环,这应该和物理硬件有关 ,我在虚拟机下只有一对环,在真实主机上有两队环)。
3.找到 netmap_ring{}的地址后,我们就可以找到环中每一个 buffer 的地址(buffer 里面存储的是将要发送/接收的数据包)。后面会讲解这是如何实现的。
4.通过一个 nifp 是如何访问到多个收/发环的,通过一个 ring 如何找到多个不同的 buffer 地址的,其实都是通过存储这些结构体相邻的后面一部分空间实现。(申请共享内存的时候, 这些均已被设计好)
 

4.几个重要的宏定义


1._NETMAP_OFFSET

解释:该宏定义的作用是将 ptr 指针(强转成 char *类型)向右偏移 offset 个字节,再将其转化为指定的类型 type。

2.NETMAP_IF
解释:该宏定义将_base 指针向右偏移_ofs 个字节后,强转为 netmap_if *类型返回。在 nemap中通过此宏得到 d->nifp 的地址。

3.NETMAP_TXRING
解释:

1.通过该宏定义,可以找到 nifp 的第 index 个发送环的地址(index 是从 0 开始的),ring_ofs[index]为偏移量,由内核生成。
2.其中,我们注意到 struct netmap_if{}最后面只定义了 const ssize_t ring_ofs[0],实际上其它的 netmap 环的偏移量都写在了该结构体后面的内存地址里面,直接访问就可以了。
4.NETMAP_RXRING
解释:通过该宏定义,可以找到 nifp 的第 index 个接收环的地址,其中(nifp)->ring_ofs[]里面的下标为 index+(nifp)->ni_tx_rings+1,正好与发送环的偏移量区间隔开 1 个。(我想这应该是作者特意设计的)
 

5.NETMAP_BUF
解释:

1.通过该宏定义,可以找到 ring 这个环的第 index 个 buffer 的地址(buffer 里面存的就是我们接收/将发送的完整数据包),每个 buffer 占的长度是 2048 字节(在(ring)->nr_buf_size
也给出了)。
2.其中(ring) ->buf_ofs 是固定的偏移量,不同的环这个值不相同,但所有的(char *)(ring)+(ring)->buf_ofs 会指向同一个地址,也就是存放 buffer 的连续内存的开始地址 (d->buf_start 会指向该地址)。
6.NETMAP_BUF_IDX
解释:

在讲 NETMAP_BUF 的时候我们说(char *)(ring) + (ring)->buf_ofs)总会指向存放 buffer
的起始位置( 无论是哪一个环), 在这段内存中将第一个 buffer 下标标记为 0 的话,
NETMAP_BUF_IDX 计算的恰好是指针 buf 所指 buffer 的下标。上面几个宏一时没弄懂也没关系,下面调用的时候还会提的。


5.nm_open 函数(主要功能是打开设备,申请并创建初始化出fd所需要的内存)


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 指针指向的内存赋值。
 

6.nm_mmap 函数(映射函数)

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; 
} 

 

7.nm_nextpkt 函数


1.nm_nextpkt()是用来接收网卡上到来的数据包的函数。
2.nm_nextpkt()会将所有 rx 环都检查一遍,当发现有一个 rx 环有需要接收的数据包时, 得到这个数据包的地址,并返回。所以 nm_nextpkt()每次只能取一个数据包。
 

 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; /* 什么也没发现 */ 
} 

8.nm_inject 函数


1.nm_inject()是用来往共享内存中写入待发送的数据包数据的。数据包经共享内存拷贝到网卡,然后发送出去。所以 nm_inject()是用来发包的。
2.nm_inject()也会查找所有的发送环(tx 环),找到一个可以发送的槽,就将数据包写入并返回,所以每次函数调用也只能发送一个包。

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 表示没地方存数据包了),就跳过 

9.nm_close 函数

  • 1.nm_close 函数就是回收动态内存,回收共享内存,关闭文件描述符什么的了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1201039.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Git系列之分支与标签的使用及应用场景模拟

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是君易--鑨&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的博客专栏《Git实战开发》。&#x1f3af;&#x1f3af; &a…

【网络奇遇记】我和因特网的初相遇2 —— 三种交换方式

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;网络奇遇记、数据结构 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 前言一. 电路交换1.1 电路交换讲解1.2 电路交换实例 二. 分组交换1.1 分组交换讲解1.2 分组交换实例…

Go 14岁了

今天我们庆祝Go开源十四周年&#xff01;Go度过了美好的一年&#xff0c;发布了两个功能齐全的版本和其他重要的里程碑。 我们在2月份发布了Go 1.20&#xff0c;在8月份发布了Go 1.21&#xff0c;更多地关注实现改进而不是新的语言更改。 在Go 1.20中&#xff0c;我们预览了配置…

基于Python+Django的图书管理系统

项目介绍 图书是人类文明传播的一个重要方式&#xff0c;很多历史悠久的文明都是通过图书来进行传递的&#xff0c;虽然随着时代的进步电子信息技术发展很快&#xff0c;但是纸质图书的地位仍然是非常稳固的&#xff0c;为了能够让知识拥有更加快捷方便的传递方式我们开发了本…

Typora-PicGo-七牛云图床

Typora-PicGo-七牛云图床 问题描述&#xff1a; 每次使用Typora写完笔记后&#xff0c;想要将笔记上传至CSDN会发现一个问题&#xff0c;由于没有配置图床&#xff0c;笔记中的图片需要一张一张的上传到CSDN&#xff0c;非常麻烦&#xff0c;若使用PicGo并搭配七牛云的10G免费…

Django配置文件,request,链接mysql方法,Orm简介

三板斧问题(views.py) HttpResponse # 返回的是字符串render # 渲染一个HTML静态文件&#xff0c;模板文件redirect # 重定向的 在视图文件中得视图函数必须要接收一个形参request&#xff0c;并且&#xff0c;视图函数也要有返回值&#xff…

Linux - 基础IO(重定向 - 重定向模拟实现 - shell 当中的 重定向)- 下篇

前言 上一篇博客当中&#xff0c;我们对 文件 在操作系统当中是 如何就管理的&#xff0c;这个问题做了 详细描述&#xff0c;本篇博客将基于上篇 博客当中的内容进行 阐述&#xff0c;如有疑问&#xff0c;请参考上篇博客&#xff1a; Linux - 基础IO&#xff08;Linux 当中…

matlab 多自由度的车辆垂向振动模型 车辆平稳性研究

1、内容简介 略 17-可以交流、咨询、答疑 多自由度的车辆垂向振动模型 多自由度的车辆垂向振动模型&#xff0c;包含四分之一车体模型、半车模型和整车模型 垂向振动模型、四分之一车体模型、半车模型和整车模型 2、内容说明 略 3、仿真分析 略 4、参考论文 略 链接&…

第七章 块为结构建模 P3|系统建模语言SysML实用指南学习

仅供个人学习记录 块行为建模 块提供了行为情境&#xff0c;行为这个 SysML 词条覆盖了块如何处理输如/输出和其内部状态改变的所有描述。 块可以指定某个行为作为其主行为或者分类器行为&#xff0c;该行为在块实例化后启动执行。其他行为可以指定为方法&#xff0c;提供了处…

人机交互——自然语言生成

自然语言生成是让计算机自动或半自动地生成自然语言的文本。这个领域涉及到自然语言处理、语言学、计算机科学等多个领域的知识。 1.简介 自然语言生成系统可以分为基于规则的方法和基于统计的方法两大类。基于规则的方法主要依靠专家知识库和语言学规则来生成文本&#xff0…

【Redis】list列表

上一篇&#xff1a; String 类型 https://blog.csdn.net/m0_67930426/article/details/134362606?spm1001.2014.3001.5501 目录 Lpush LRange Rpush Lpop Rpop Lindex Ltrim Lset 列表不存在的情况 如果列表存在 Linsert ​编辑 在………之前插入 在……后面插入…

Windows系统安装Redis、配置环境变量

系列文章目录 第一章 Java线程池技术应用 第二章 CountDownLatch和Semaphone的应用 第三章 Spring Cloud 简介 第四章 Spring Cloud Netflix 之 Eureka 第五章 Spring Cloud Netflix 之 Ribbon 第六章 Spring Cloud 之 OpenFeign 第七章 Spring Cloud 之 GateWay 第八章 Sprin…

【 第十一章】软件设计师 之 面向对象设计与结构化分析设计

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 备考资料导航 软考好处&#xff1a;软考的…

域名无法访问了,如何找回浏览器的缓存

背景需求 双十一即将来临&#xff0c;这意味着我购买了三年低配的阿里服务器&#xff0c;而它的服务期限也即将到期。为了提前做好准备&#xff0c;我在一周前对静态网站进行了备份&#xff0c;并成功地使用了Vercel进行部署&#xff08;已经有了域名&#xff09;。相比于付费…

腾讯云3年轻量应用服务器2核2G4M带宽540元,它来了

腾讯云轻量应用服务器特价是有新用户限制的&#xff0c;所以阿腾云建议大家选择3年期轻量应用服务器&#xff0c;一劳永逸&#xff0c;免去续费困扰。腾讯云轻量应用服务器3年可以选择2核2G4M和2核4G5M带宽&#xff0c;3年轻量2核2G4M服务器540元&#xff0c;2核4G5M轻量应用服…

jdk21 虚拟线程原理及使用分享

虚拟线程概述 jdk21已于北京时间9月19日21点正式发布, 其中引人注目的就是虚拟线程(Virtual Thread)随之正式发布, 不再是此前jdk19、jdk20中的预览版本。 平台线程&#xff1a;java传统的线程是对系统线程的包装&#xff0c;为了区别于虚拟线程&#xff0c;因此将通过传统方式…

C#源代码生成器深入讲解一

C#源代码生成器 01 源代码生成器初体验 新建一个类库&#xff0c;一定是standard2.0版本&#xff0c;否则会出问题。引用Nuget包Microsoft.CodeAnalysis.Common新建一个类&#xff0c;继承自ISourceGenerator接口 //一定要写&#xff0c;制定语言 [Generator(LanguageNames.…

Django 基于ORM的CURD、外键关联,请求的生命周期

文章目录 基于ORM进行的CURDORM外键关联Django请求的生命周期流程图 基于ORM进行的CURD 本质上就是通过面向对象的方式&#xff0c;对数据库的数据进行增、删、改、查。 这里将会将我们之前所有内容结合到一起&#xff0c;首先确保基于上序操作已经建立好了UserInfo表&#xff…

Three.js——基于原生WebGL封装运行的三维引擎

文章目录 前言一、什么是WebGL&#xff1f;二、Three.js 特性 前言 Three.js中文官网 Three.js是基于原生WebGL封装运行的三维引擎&#xff0c;在所有WebGL引擎中&#xff0c;Three.js是国内文资料最多、使用最广泛的三维引擎。既然Threejs是一款WebGL三维引擎&#xff0c;那么…

Python 使用tkinter的Scrollbar方法创建Text水平和垂直滚动条

在Python的Tkinter中&#xff0c;可以使用Scrollbar来实现Text组件的上下或左右滑动。首先&#xff0c;需要创建一个Scrollbar对象并将其与Text组件绑定&#xff0c;然后将Scrollbar放置在Text组件的右侧或底侧&#xff0c;使其能够控制Text组件的上下或左右滑动。 运行结果&am…