网路的核心是报文的转发过程,linux网路是通过内核协议栈进行转发的,报文控制平面和数据转发平面没有分离,不适合处理大规模网络数据包,因为linux分为内核区和用户区,报文先进入内核区然后拷贝到用户区,供给上层应用程序处理。并且为了全面的支持用户空间的各个功能,协议栈中嵌入了大量用于对接的接口。如果能让应用程序直接接管网络数据包处理、内存管理以及CPU调度,那么性能可以得到一个质的提升。
如今的处理器都是多核,而且内存也越来越大,可以提高多核和大内存的扩展性,减少CPU多核之间任务的切换,内存cache miss,因为内存的访问速度永远也赶不上cache和cpu的频率,为了能让性能平行扩展,最好是少访问。
要提高网路报文转发,从如下几个方面着手:
1.控制层留给Linux做,其它数据层全部由应用程序来处理。
2.减少系统调度、系统调用、系统中断,上下文切换等
3.摒弃Linux内核协议栈,将数据包传输到用户空间定制协议栈
4.使用多核编程技术替代多线程,将OS绑在指定核上运行
5.针对SMP系统,使CPU尽量使用所在NUMA系统节点的内存,减少内存刷写
6.使用大页面,减少访问
7.采用无锁技术解竞争
Intel® DPDK全称Intel Data Plane Development Kit,是intel提供的数据平面开发工具集,主要应用x86通用平台,为用户空间高效的数据包处理提供库函数和驱动的支持。它不同于Linux系统以通用性设计为目的,而是专注于网络应用中数据包的高性能处理。
需要强调的是,DPDK应用程序是运行在用户空间上利用自身提供的数据平面库来收发数据包,绕过了Linux内核协议栈对数据包处理过程。Linux内核将DPDK应用程序看作是一个普通的用户态进程,包括它的编译、连接和加载方式和普通程序没有什么两样。DPDK程序启动后只能有一个主线程,然后创建一些子线程并绑定到指定CPU核心上运行。
EAL(Environment Abstraction Layer)即环境抽象层,为应用提供了一个通用接口,隐藏了与底层库与设备打交道的相关细节。EAL实现了DPDK运行的初始化工作,基于大页表的内存分配,多核亲缘性设置,原子和锁操作,并将PCI设备地址映射到用户空间,方便应用程序访问。
Buffer Manager API通过预先从EAL上分配固定大小的多个内存对象,避免了在运行过程中动态进行内存分配和回收来提高效率,常常用作数据包buffer来使用。
Queue Manager API以高效的方式实现了无锁的FIFO环形队列,适合与一个生产者多个消费者、一个消费者多个生产者模型来避免等待,并且支持批量无锁的操作。
Flow Classification API通过Intel SSE基于多元组实现了高效的hash算法,以便快速的将数据包进行分类处理。该API一般用于路由查找过程中的最长前缀匹配中,安全产品中根据Flow五元组来标记不同用户的场景也可以使用。
PMD则实现了Intel 1GbE、10GbE和40GbE网卡下基于轮询收发包的工作模式,大大加速网卡收发包性能。
DPDK的优势:
DPDK拦截中断,不触发后续中断流程,并绕过协议栈,通过UIO技术将网卡收到的报文拷贝到应用层处理,报文不再经过内核协议栈。减少了中断,DPDK的包全部在用户控件使用内存池管理,内核控件与用户空间的内存交互不用进行拷贝,只做控制权转移,减少报文拷贝过程,提高报文的转发效率。
DPDK核心技术如下:
(1)通过UIO技术将报文拷贝到应用空间处理
(2)通过大页内存,降低cache miss ,提高命中率,进而cpu访问速度
(3)通过CPU亲和性,绑定网卡和线程到固定的core,减少cpu任务切换
(4)通过无锁队列,减少资源竞争
接下来深入学习总结一下dpdk所用到的技术,加深理解。
1、hugepage: 使用大页缓存支持来提高内存访问效率。
为实现物理地址到虚拟地址的转换,Linux一般通过查找TLB来进行快速映射,如果在查找TLB没有命中,就会触发一次缺页中断,将访问内存来重新刷新TLB页表。Linux下默认页大小为4K,当用户程序占用4M的内存时,就需要1K的页表项,如果使用2M的页面,那么只需要2条页表项,这样有两个好处:
第一是使用hugepage的内存所需的页表项比较少,对于需要大量内存的进程来说节省了很多开销,像oracle之类的大型数据库优化都使用了大页面配置;
第二是TLB冲突概率降低,TLB是cpu中单独的一块高速cache,一般只能容纳100条页表项,采用hugepage可以大大降低TLB miss的开销。
DPDK目前支持了2M和1G两种方式的hugepage。通过修改默认/etc/grub.conf中hugepage配置为:
default_hugepagesz=1G hugepagesz=1G hugepages=32 isolcpus=0-22
然后通过
mount –t hugetlbfs nodev /mnt/huge
就将hugepage文件系统hugetlbfs挂在/mnt/huge目录下,然后用户进程就可以使用mmap映射hugepage目标文件来使用大页面了。测试表明应用使用大页表比使用4K的页表性能提高10%~15%
2、UIO,PMD,用户态轮询驱动,可以减小上下文切换开销,方便实现虚拟机和主机零拷贝zero copy。
当前Linux操作系统都是通过中断方式通知CPU来收发数据包,我们假定网卡每收到10个数据包触发一次软中断,一个CPU核心每秒最多处理2w次中断,那么当一个核每秒收到20w个包时就占用了100%,此刻它无法做其它任何操作。
DPDK针对Intel网卡实现了基于轮询方式的PMD(Poll Mode Drivers)驱动,该驱动由API、用户空间运行的驱动程序构成,该驱动使用无中断方式直接操作网卡的接收和发送队列(除了链路状态通知仍必须采用中断方式以外)。目前PMD驱动支持Intel的大部分1G、10G和40G的网卡。
PMD驱动从网卡上接收到数据包后,会直接通过DMA方式传输到预分配的内存中,同时更新无锁环形队列中的数据包指针,不断轮询的应用程序很快就能感知收到数据包,并在预分配的内存地址上直接处理数据包,这个过程非常简洁。
如果要是让Linux来处理收包过程,首先网卡通过中断方式通知协议栈对数据包进行处理,协议栈先会对数据包进行合法性进行必要的校验,然后判断数据包目标是否本机的socket,满足条件则会将数据包拷贝一份向上递交给用户socket来处理,不仅处理路径冗长,还需要从内核到应用层的一次拷贝过程。
dpdk利用uio支持,提供应用空间下PMD驱动程序的支持,也就是说网卡驱动是运行在用户空间的,减下了报文在用户空间和应用空间的多次拷贝。
3、 利用LINUX亲和性支持,把控制面线程及各个数据面线程绑定到不同的CPU核,节省了 线程在各个CPU核来回调度。
多线程的初衷是提高整体应用程序的性能,但是如果不加注意,就会将多线程的创建和销毁开销,锁竞争,访存冲突,cache失效,上下文切换等诸多消耗性能的因素引入进来。
为了进一步提高性能,就必须仔细考虑线程在CPU不同核上的分布情况,这也就是常说的多核编程。多核编程和多线程有很大的不同:多线程是指每个CPU上可以运行多个线程,涉及到线程调度、锁机制以及上下文的切换;而多核则是每个CPU核一个线程,核心之间访问数据无需上锁。为了最大限度减少线程调度的资源消耗,需要将Linux绑定在特定的核上,释放其余核心来专供应用程序使用。
同时还需要考虑CPU特性和系统是否支持NUMA架构,如果支持的话,不同插槽上CPU的进程要避免访问远端内存,尽量访问本端内存。
4、 提供内存池和无锁环形缓存管理,加快内存访问效率。多个收发包集中到一个cache line,在内存池中实现,无需反复申请和释放。
模块分解:
在最底部的内核态DPDK有几个模块:
核心组件:
1、 环境抽象层(EAL):为DPDK其它组件和应用程序提供一个屏蔽具体平台特性的统一接口,EAL提供的功能主要有:DPDK加载和启动;支持多核或多线程执行类型;CPU核亲和性处理;原子操作和锁操作接口;时钟参考;PCI总线访问接口;跟踪和调试接口;CPU特性采集接口;中断和告警接口等。
2、 堆内存管理组件(MALLOC):堆内存管理组件为应用程序提供从大页内存分配堆内存的接口。当需要分配大量内存小块时(如用于存储列表中每个表项指针的内存),使用这些接口可以减少TLB缺页。
3、 网络报文缓存块管理组件(Mbuf):提供应用程序创建和释放用于存储报文信息的缓存块的接口,这些MBUF存储在一内存池中。提供两种类型的MBUF,一种用于存储一般信息,一种用于存储报文数据。
4、 内存池管理组件(Mempool):为应用程序和其它组件提供分配内存池的接口,内存池是一个由固定大小的多个内存块组成的内存容器,可用于存储相同对像实体,如报文缓存块等。内存池由内存池的名称(一个字符串)来唯一标识,它由一个环缓中区和一组核本地缓存队列组成,每个核从自已的缓存队列分配内存块,当本地缓存队列减少到一定程度时,从内存环缓冲区中申请内存块来补充本地队列。
5、 环缓冲区管理组件(RING):环缓冲区管理组件为应用程序和其它组件提供一个无锁的多生产者多消费者FIFO队列API。
6、 定时器组件(Timer):提供一些异步周期执行的接口(也可以只执行一次),可以指定某个函数在规定的时间异步的执行,就像LIBC中的timer定时器,但是这里的定时器需要应用程序在主循环中周期调用rte_timer_manage来使定时器得到执行,使用起来没有那么方便。定时器组件的时间参考来自EAL层提供的时间接口。
除了以上六个核心组件外,DPDK还提供以下功能:
1、 以太网轮询模式驱动(PMD)架构:把以太网驱动从内核移到应用层,采用同步轮询机制而不是内核态的异步中断机制来提高报文的接收和发送效率。
2、 报文转发算法支持:Hash 库和LPM库为报文转发算法提供支持。
3、 网络协议定义和相关宏定义:基于FreeBSD IP协议栈的相关定义如:TCP、UDP、SCTP等协议头定义。
4、 报文QOS调度库:支持随机早检测、流量整形、严格优先级和加权随机循环优先级调度等相关QOS功能。
5、 内核网络接口库(KNI):提供一种DPDK应用程序与内核协议栈的通信的方法,类似普通LINUX的TUN/TAP接口,但比TUN/TAP接口效率高。每个物理网口可以虚拟出多个KNI接口。
RTE:Run-Time Environment
EAL:Environment Abstraction Layer
PMD: Poll-Mode Driver
Memory Manager (librte_malloc,内存管理器)
Librte_malloc 库提供一组API,用于从hugepages创建的memzones中分配内存而不是在堆中分配。这有助于改善Linux用户空间环境下典型的从堆中大量分配4KB页面而容易引起TLB不命中。
Memory Pool Manager (librte_mempool,内存池管理器)
内存池管理器负责分配的内存中的pool对象。一个pool由名称标识,并使用一个ring来存储空闲对象。它提供了其他一些可选的服务,例如每个core的对象缓存和对齐方式帮助器,以确保将填充的对象在所有内存通道上均匀分布。
Ring Manager (librte_ring,环形队列管理器)
在一个有限大小的表中,ring结构提供了一个无锁的多生产者多消费者的FIFO API。相较于无锁队列,它有一些的优势 ;更容易实现,适应于大容量操作和速度更快。一个ring用在内存池管理器(librte_mempool),也可用作cores和(或)在一个逻辑core之上的连接在一起的执行块的通用沟通机制。
Network Packet Buffer Management (librte_mbuf,网络报文缓冲管理)
mbuf 库提供了创建和销毁缓冲区,英特尔 ® DPDK 应用程序可能用来存储消息缓冲。创建消息缓冲区在启动时间和存储在 mempool,并使用英特尔 ® DPDK mempool 库。
此库提供一组 API,用于分配或释放 mbufs,操纵控制消息缓冲区(ctrlmbuf) ——普通的消息缓冲区,还操作数据包缓冲区 (pktmbuf) ——用来进行网络数据包。
Timer Manager (librte_timer,定时器管理)
timer库向英特尔 ® DPDK 执行单位提供定时器服务,保证以异步方式执行函数的能力。它可以是定期调用,或只是一次性调用。它使用环境抽象层 (EAL) 提供的到的 HPET 接口来获取精确时间的引用,并根据需求在每个核心启动。
在root权限下:
1)配置大页内存
echo 128 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
mkdir /mnt/huge
mount -t hugetlbfs nodev /mnt/huge
cat /proc/meminfo | grep Huge
2)编译dpdk+helloworld
dpdk源码下载:http://www.dpdk.org/browse/dpdk/refs/tags?h=releases
进入dpdk主目录,输以下命令进行编译
1,设置环境变量:
export RTE_SDK=/home/dpdk-20.08
export RTE_TARGET=x86_64-native-linuxapp-gcc
export KERNELDIR=/lib/modules/3.10.0-862.14.1.6_97.x86_64/build/
ps:可以通过env命令查看已设置的环境变量
2,设置编译生成动态库so和igb_uio:
vim config/common_base
CONFIG_RTE_BUILD_SHARED_LIB=y
CONFIG_RTE_EAL_IGB_UIO=y
3,编译
make config T=x86_64-native-linuxapp-gcc
make install T=x86_64-native-linuxapp-gcc RTE_KERNELDIR=/lib/modules/3.10.0-862.14.1.6_97.x86_64/build/ -j 8 'TOOLCHAIN_CFLAGS=-msse4.2 -g -Wall -Werror -fstack-protector-strong -fPIC' 'EXTRA_LDFLAGS=-z relro -z now -z noexecstack' MAKE_PAUSE=n
编译成功后可以在 x86_64-native-linuxapp-gcc/lib/ 目录下看到对应的so, 拷贝到/lib64目录下
cp x86_64-native-linuxapp-gcc/lib/lib* /lib64/
在DPDK的examples路径下面有许多示例应用,可以在examples路径下面运行make,将这些应用全都编译好,然后执行,从helloword开始:
# cd examples
# make
# ./helloworld/x86_64-native-linuxapp-gcc/helloworld -c 0xf -n 2
EAL: Detected 56 lcore(s)
EAL: Detected 2 NUMA nodes
EAL: Multi-process socket /var/run/dpdk/rte/mp_socket
EAL: Selected IOVA mode 'VA'
EAL: Probing VFIO support...
EAL: VFIO support initialized
EAL: No legacy callbacks, legacy socket not created
hello from core 1
hello from core 0
hello from core 3
hello from core 2
该程序自动检测逻辑核,0核为主逻辑核,默认运行结果如上述打印。
执行该程序时默认加的参数,具体含义如下:
EAL common options:
-c COREMASK Hexadecimal bitmask of cores to run on
-l CORELIST List of cores to run on
The argument format is [-c2][,c3[-c4],...]
where c1, c2, etc are core indexes between 0 and 128
--lcores COREMAP Map lcore set to physical cpu set
The argument format is
'[<,lcores[@cpus]>...]'
lcores and cpus list are grouped by '(' and ')'
Within the group, '-' is used for range separator,
',' is used for single number separator.
'( )' can be omitted for single element group,
'@' can be omitted if cpus and lcores have the same value
-s SERVICE COREMASK Hexadecimal bitmask of cores to be used as service cores
--master-lcore ID Core ID that is used as master
--mbuf-pool-ops-name Pool ops name for mbuf to use
-n CHANNELS Number of memory channels
-m MB Memory to allocate (see also --socket-mem)
-r RANKS Force number of memory ranks (don't detect)
-b, --pci-blacklist Add a PCI device in black list.
Prevent EAL from using this PCI device. The argument
format is .
-w, --pci-whitelist Add a PCI device in white list.
Only use the specified PCI devices. The argument format
is <[domain:]bus:devid.func>. This option can be present
several times (once per device).
[NOTE: PCI whitelist cannot be used with -b option]
--vdev Add a virtual device.
The argument format is [,key=val,...]
(ex: --vdev=net_pcap0,iface=eth2).
--iova-mode Set IOVA mode. 'pa' for IOVA_PA
'va' for IOVA_VA
-d LIB.so|DIR Add a driver or driver directory
(can be used multiple times)
--vmware-tsc-map Use VMware TSC map instead of native RDTSC
--proc-type Type of this process (primary|secondary|auto)
--syslog Set syslog facility
--log-level= Set global log level
--log-level=:
Set specific log level
--trace=
Enable trace based on regular expression trace name.
By default, the trace is disabled.
User must specify this option to enable trace.
--trace-dir=
Specify trace directory for trace output.
By default, trace output will created at
$HOME directory and parameter must be
specified once only.
--trace-bufsz=
Specify maximum size of allocated memory
for trace output for each thread. Valid
unit can be either 'B|K|M' for 'Bytes',
'KBytes' and 'MBytes' respectively.
Default is 1MB and parameter must be
specified once only.
--trace-mode=
Specify the mode of update of trace
output file. Either update on a file can
be wrapped or discarded when file size
reaches its maximum limit.
Default mode is 'overwrite' and parameter
must be specified once only.
-v Display version information on startup
-h, --help This help
--in-memory Operate entirely in memory. This will
disable secondary process support
--base-virtaddr Base virtual address
--telemetry Enable telemetry support (on by default)
--no-telemetry Disable telemetry support
EAL options for DEBUG use only:
--huge-unlink Unlink hugepage files after init
--no-huge Use malloc instead of hugetlbfs
--no-pci Disable PCI
--no-hpet Disable HPET
--no-shconf No shared config (mmap'd files)
EAL Linux options:
--socket-mem Memory to allocate on sockets (comma separated values)
--socket-limit Limit memory allocation on sockets (comma separated values)
--huge-dir Directory where hugetlbfs is mounted
--file-prefix Prefix for hugepage filenames
--create-uio-dev Create /dev/uioX (usually done by hotplug)
--vfio-intr Interrupt mode for VFIO (legacy|msi|msix)
--vfio-vf-token VF token (UUID) shared between SR-IOV PF and VFs
--legacy-mem Legacy memory mode (no dynamic allocation, contiguous segments)
--single-file-segments Put all hugepage memory in single files
--match-allocations Free hugepages exactly as allocated
3)转发实践
1,安装igb_uio驱动
modprobe uio
rmmod igb_uio.ko
insmod x86_64-native-linuxapp-gcc/kmod/igb_uio.ko
2,绑定网卡
先看一下当前网卡的状态:
./usertools/dpdk-devbind.py --status
再进行绑定:
./usertools/dpdk-devbind.py -b igb_uio 0000:02:06.0
./usertools/dpdk-devbind.py -b igb_uio 0000:02:06.1
ps:如果网卡有接口名,如eth1, eth2, 也可以在-b igb_uio后面使用接口名, 而不使用pci地址
DPDK搭建环境完成后,网卡绑定到相应IGB_UIO驱动接口上,所有的网络数据包都会到DPDK,例如examples/l2fwd程序,网卡接收网络数据包,再从另一个网卡转发出去。