[C/C++后端开发学习]10 基于netmap的用户态协议栈(一)

本文探讨了用户态协议栈的重要性,特别是为了解决内核拷贝问题。重点介绍了netmap框架,包括其主要接口、安装步骤和数据结构定义。通过实例展示了如何使用netmap实现一个简单的UDP协议数据接收,解决ARP失效问题,以及初步实现ARP和ICMP协议。内容包括测试环境设置、数据包封装、代码编写等关键环节。
摘要由CSDN通过智能技术生成

用户态协议栈的意义

在内核实现协议栈往往存在两次拷贝过程,一是网卡中的数据通过sk_buff拷贝到内核,之后再从内核拷贝到进程用户空间。

于是我们考虑如何加快这个过程,减少拷贝次数。一种思路:通过DMA直接将数据从网卡拷贝到内存中,于是应用程序可以直接通过mmap从内存中取得数据。由于DMA的工作是不需要CPU干预的,所以对于CPU来说相当于没有做任何的拷贝操作,即所谓零拷贝。

那么应用进程如何通过这种方式从网卡直接取得数据?常用几种方法:

  • 使用raw socket
  • netmap框架
  • dpdk框架

netmap

netmap是一个可用于用户层应用程序收发原始网络数据的高性能框架,包含了内核驱动模块及API库函数。

Userspace clients can dynamically switch NICs into netmap mode and send and receive raw packets through memory mapped buffers.

其基本原理可参考Netmap分析

常用接口说明

主要头文件:netmap.hnetmap_user.h,位于源码包的./netmap/sys/net/目录下。

netmap.hnetmap_user.h调用,里面定义了一些宏和几个主要的结构体。一般来说,如果仅仅只是想要收发数据,在上手时我们知道下面几个接口就可以了。

  • nm_open
struct nm_desc *nm_open(const char *ifname, const struct nmreq *req,
						uint64_t new_flags, const struct nm_desc *arg)

nm_open针对ifname指示的网卡接口启用netmap并返回针对该接口的描述符结构体。一般直接这样调用既可:

struct nm_desc *nmr = nm_open("netmap:eth1", NULL, 0, NULL);

struct nm_desc中包含一个fd指向/dev/netmap,可用于poll、epoll等系统调用。

  • nm_nextpkt

nm_nextpkt()用于接收网卡上收到的数据包。它会将内部所有接收环检查一遍,如果有需要接收的数据包,则返回这个数据包。一次只能返回一个以太网数据包。因为接收到的数据包没有经过协议栈处理,因此需要在用户程序中自己解析。

读一个数据包时一般这样调用,stream即为数据在缓冲区中的首地址,struct nm_pkthdr为返回的数据包头部信息,不需要管头部的话直接从stream去取数据就行了。

struct nm_pkthdr nmhead = {
   0};
char* stream = nm_nextpkt(nmr, &nmhead);
  • nm_inject

nm_inject()是用于往共享内存中写入待发送的数据包,数据再被从共享内存拷贝到网卡,进而发送出去。它检查所有的发送环,找到一个可以发送的槽后将数据写入。一次只能发送一个包,包的长度由参数指定。一般的调用方式:

nm_inject(nmr, &datapack, packlen);
  • nm_close

简单的理解就是nm_open的逆过程,回收动态内存,回收共享内存,关闭文件描述符等等。

安装netmap

下载netmap的github地址。按照说明安装完成之后,可以调用下面的指令进行测试:

sudo pkt-gen -i eth0 -f rx   # eth0 是网卡名称

在这之前还需要先执行一下:sudo insmod netmap.ko。该驱动模块挂载之后,/dev/目录下会多一个字符型设备:

crw-rw---- 1 root root 10, 56 Nov  7 10:53 /dev/netmap

之后,一旦调用接口nm_open,网卡的数据就不从内核协议栈走了,这时候最好在虚拟机中建两个网卡,一个用于netmap,一个用于ssh等应用程序的正常工作。

协议栈的数据结构定义

下图是实现一个UDP协议时的网络协议栈结构及其数据封装的简化形式,我们的用户态协议栈需要实现链路层、网络层以及传输层。按照图中数据包结构,我们需要依次提取链路层、网络层、传输的首部,并最终得到用户数据。
在这里插入图片描述

链路层首部

在这里插入图片描述

typedef unsigned char _u8;
typedef unsigned short _u16;
typedef unsigned int _u32;
#pragma pack(1) // 告诉编译器以一个字节对齐

#define ETH_LEN 6
#define IP_LEN 4
#define PROTO_IP	0x0800	// 参考上图
#define PROTO_ARP	0x0806
#define PROTO_RARP	0x0835
struct eth_header
{
   
    _u8 dst_mac[ETH_LEN];
    _u8 src_mac[ETH_LEN];
    _u16 type;
};

链路层使用的地址为MAC地址。

网络层首部

在这里插入图片描述

// IP协议类型的字段定义在头文件: <arpa/inet.h>
struct ip_header
{
   
    _u8 header_len : 4, // 首部长度是低4位,版本是高4位
        version : 4;
    _u8 tos;
    _u16 total_len;
    _u16 id;
    _u16 flag_off;
    _u8 ttl;
    _u8 proto;
    _u16 header_check;
    _u32 src_ip;
    _u32 dst_ip;
};

struct ip_packet
{
   
    struct eth_header eth;
    struct ip_header ip;
};

网络层使用的地址为IP地址。IP首部中8位协议类型的定义见内核源码include/uapi/linux/in.h

注意:

  • 字节序问题,结构体定义时一定要注意网络字节序为大端字节序。大端模式下当我们看一个字节时,其bit位从左到右为高位到低位,这与我们正常描述一个字节的二进制形式时是一致的;但当一个数据为多个字节时,字节内的bit顺序还是这样,但多个字节之间高低位需要逆序排列。所以从首部数据中看,4位首部长度和4位版本组成1个字节,4位版本是在高4位,4位首部长度是在低4位;但当我们看16位总长度字段时,如大端系统数据存储为 0x12(低8位) 0x34(高8位),实际这个值是0x1234,在小端系统实际应存储为0x34(低8位) 0x12(高8位),因此需要做转换。总的来说,大小端只影响我们习惯上看多个字节之间的顺序,不影响我们习惯上看1个字节内bit位的顺序。
  • 首部长度字段是指首部占多少个32 bit(即多少个unsigned int),而不是多少字节!!小心这个坑。

IP包的总长度与MTU:MTU是网卡的限制,数据超出限制后会分片发出;与IP包头部中指定的总长度之间并不冲突。

传输层首部

UDP首部:

在这里插入图片描述

在这里插入图片描述

struct udp_header
{
   
    _u16 src_port;
    _u16 dst_port;
    _u16 length;
    _u16 check;
};

TCP首部:

在这里插入图片描述

在这里插入图片描述

struct tcp_header
{
   
    _u16 src_port;
    _u16 dst_port;
    _u32 seq_num;
    _u32 ack_num;
    _u16 res1 : 4,
         header_len : 4,
         fin : 1,
         syn : 1,
         rst : 1,
         psh : 1,
         ack : 1,
         urg : 1,
         res2 : 2;
    _u16 win_size;
    _u16 check;
    _u16 urg_ptr;
};

传输层使用的地址标识为端口。

基于netmap的用户态协议栈实现(一)

这篇文章是协议栈实现的第一部分,我们先简单地实现UDP接收、ARP响应和ICMP响应。

1 上手:实现一个简单的UDP协议数据接收

简单起见,我们先以UDP为例来入手用户栈协议。

测试环境

由于很多因素的影响,一般无法在常见的云服务器上测试netmap。我在虚拟机上的 Ubuntu 运行所编写的用户态协议栈实现程序,然后在Windows 10 上通过网络调试助手发送数据进行测试。我在虚拟机中添加了两个网卡,其中用于测试的网卡配置成了NAT模式(非必须)。一般来说在虚拟机中调用ifconfig会看到虚拟网卡ens33等等,我们需要先将其修改为类似eth0的物理网卡的形式。修改方法参考这篇文章。文中使用的是grub2,而我们的系统可能用的是grub,需要根据实际情况修改,实际只需做前两步就可以了。

UDP数据包封装

首先定义UDP的整个数据包。这里涉及到了柔性数组(零长度数组)用以定义用户数据包的起始地址而不占用实际的结构体空间。

struct udp_packet
{
   
    struct eth_header eth;
    struct ip_header ip;
    struct udp_header udp;
    _u8 payload[0]; // 柔性数组
};

代码编写

使用netmap库需包含头文件#include <net/netmap_user.h>,并在这之前添加宏定义NETMAP_WITH_LIBS

netmap内部接收数据时维护了一个环形缓冲区(ringbuffer)。读数据使用函数nm_nextpkt(),每次读一个数据包。返回的是当前数据在缓冲区中的首地址。

此外,由于网络协议栈中采用的是网络字节序(大端),因此应对数据做适当的字节序转换。

具体的代码如下所示,该程序接收完整的UDP数据包并将具体数据打印出来。关于数据结构的定义前面已给出,下面的代码中就不包含了。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/poll.h>
#<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值