1引言
本文档主要包含INTEL DPDK安装和配置说明。目的是让用户快速的开发和运行程序。文档描述了如何在不深入细节的情况下在linux应用开发环境上编译和运行一个DPDK应用程序。
1.1文档总览
以下是DPDK文档列表,建议按照文档顺序阅读:
- Release Note:提供版本特有信息,包括支持的型号,限制,修正的问题,一直的问题等等。也提供一些在常见问题解答中被频繁问道的问题的大案。
- 入门指导(Getting Started Guide(本文档)):描述怎么安装和配置DPDK,目的是让用户快速开发和运行软件。
- 开发手册(Programmer's Guide):描述:
- 软件架构和如何在linux环境下运行一个DPDK范例程序
- DPDK的内容,开发系统(包括如何在DPDK根目录下使用的编译Makefile的指令来编译开发库和运用程序)和移植一个应用程序参考
- 软件优化和需要在新的开发环境考虑的问题。
- 也提供了关键词汇表:
- API手册:提供了DPDK上的函数,数据结构和其它编程结构的详细信息。
- 范例使用手册(Sample Application User Guide):描述了一系列的范例程序,每个章节都描述了一个有特殊功能的示例,提供了编译,运行和使用示例的操作指南。
注意:以上文档都可以从DPDK源码包的同一个位置上分别下载。
2系统必备条件
本章讲的是编译DPDK源码包必须具有的环境。
注意:如果DPDK要用到intel的89XX系列的网卡平台上,请先翻阅89xx网卡系列的入门手册。
2.1X86上BIOS先决条件
对于主要的平台,没有什么特别的BIOS设置才能用DPDK的基本功能。然而总有例外的,对于HPET高精度时钟,能源管理功能,在40g网卡上高性能的小包收发,BIOS设置需要改一下。请看第5章(其它功能的开启)来获取需要做哪些更改的信息。
2.2编译DPDK
注意:测试是在Fedora18上,编译安装指令在其它系统上可能不一样。在其它linux发行版本上的测试细节请参阅DPDKrelease note。
- GNU make
- coreutils:cmp,sed,grep,arch
- gcc:在686/x86_64平台上得大于等于4.5.x版本,对于ppc_64 和x86_x32程序,建议用4.8.x版本。一些特殊的编译标识和连接标示默认是打开的,会影响到性能(例如-fsatckprotector),请参阅你所使用版本的说明文档和使用gcc -dumpspecs来查看。
- c库:intel的架构上要求glibc-devel.i686 / libc6-dev-i386; glibc-devel.x86_64 for 64-bit,ibm power架构则要求glibc-devel.ppc64 for 64 bit
- linux内核源码库头文件用于编译内核模块:kernel-devel.x86_64,kernel - devel.ppc64
- 另外32位和64位编译环境需要的包有:glibc.i686, libgcc.i686, libstdc++.i686 and glibc-devel.i686 for Intel i686/x86_64;glibc.ppc64, libgcc.ppc64, libstdc++.ppc64 and glibc-devel.ppc64 for IBM ppc_64;
注意:x86_32应用程序接口只支持最新的debian发行版和13.10以上版本的unbuntu。唯一支持的gcc版本是4.8+。
注意:python需要2.6或者是2.7,在dpdk安装包中有各种各样的帮助脚本。
可选的工具:
- intel C++编译器(icc)。需要安装一些其它的库,自己看编译器安装的说明文档吧,这个版本已经在icc 12.1上测过
- IBM powerLinux工具链。这是一系列的开源开发工具和能够用到power最新linux硬件特征的动态库,要安装的话看ibm的官方文档
- 编译和使用基于libpcap库的轮询驱动需要libpcap头文件和库(libpcap-devel)。驱动默认是不能用的,要用的话需要在编译时设置config文件中的CONFIG_RTE_LIBRTE_PMD_PCAP=y
2.3运行DPDK程序
要运行dpdk程序,在目标机器上需要一些本地化要求。
2.3.1系统软件
要求:
- 内核版本 >= 2.6.33
当前在使用的内核版本可以通过以下命令获取:
uname -r
对于在更早的内核版本上使用DPDK需要的补丁细节,可以看DPDK的Release Notes中的DPDK FAQ。也要注意redhat 6.2和6.3使用的2.6.32内核版本需要用到所有必须得补丁。
- glibc >= 2.7(因为cpuset相关的操作)
c库版本可以通过使用ldd -version命令获取,一个命令使用结果例子如下:
# ldd --version
ldd (GNU libc) 2.14.90
Copyright (C) 2011 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
- 内核配置
在fedora os和其它普通的linux发行版,例如ubuntu,或者是红帽企业版linux,供应商提供的内核配置可以跑大部分DPDK应用。
对于其它内核版本,要使用DPDK必须使一下选项能支持:
-
- UIO支持
- HUGETLBFS(大页内存用到的库)
- PROC_PAGE_MONITOR支持
- 要使用HPET就要求HPET和HPET_MMAP配置项能支持。看5.1章《高精度定时器(HPET)功能》细节。
2.3.2在linux上使用大页内存
对于要缓存报文的超大内存池的内存分配必须用hugepage,(HUGETLBFS选项在当前允许的内核上必须是支持的,入2.3所说的)使用大页来分配内存,即使只使用少数页面,性能也是能够得到提升的。因为用到更少的页表缓冲条目(TLBs,高速翻译缓存),可以减少翻译虚拟地址到物理地址的时间,如果不用大页,那么用4k的页较高的TLB丢失率会降低性能。实际上还有一个好处就是不会被交换到磁盘上去。
获取DPDK使用的大页内存
分配大页内存最好是在系统启动时或者是在启动后尽早,以便于申请的内存减少碎片,即内存在物理上尽量是连续的。在内核启动时获取大页内存,需要将一个参数传递给内核。
对于2m的页,仅仅只需要将大页选项发送给内核。例如,申请1024个2m的页:
hugepages=1024
对于其它尺寸的大页内存,例如1g的页,页大小必须明确的指定,也能设置系统的默认大页内存大小。例如申请4g大小的大内存通过4个1g内存页的形式,需要将以下选项发给内核:
default_hugepagesz=1G hugepagesz=1G hugepages=4
注意:在intel的架构机器上cpu支持的大页内存的尺寸取决于CPU标识(这些标示可以通过查看/proc/cpuinfo获取),如果pse存在,那么2m页支持,pdpe1gb存在,则1g页支持。在IBM power架构的机器上,支持16M和16G的页。
注意:64位机器,交付时如果平台支持那天生就支持1g页了。
对于2个socket的NUMA系统,申请的大页内存数在系统启动时一般是从两个socket上平均分配(假定两个socket上都有足够的内存),如上,4g就是一个socket上分出2g内存。
可以看内核源码树的Documentation/kernel-parameter.txt文件获取更多的内核选项细节。
可选项:
2m的页也可以在系统启动后申请,通过echo 内存页数目到/sys/devices/目录下的文件nr_hugepages中。对于单node的系统,使用一下命令获取1024个2m内存页:
echo 1024 > /sys/kernel/mm/hugepages/hugepages_2048kb/nr_huge
在numa的机器上,必须精确的在每个node上指定分配的页数:
echo 1024 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 1024 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
注意:对于1g的大内存页是不可能在系统启动之后申请分配的。
2.3.3 linux环境下Xen Domain0支持
当前的内存管理基于linux内核的大内存页机制,在Xen虚拟机监视器上,大页支持无特权账户意味着DPDK程序是作为一个普通用户态程序运行。
然而,Domain0不支持大页的,解决这个限制需要,内核模块rte_dom0_mm加入,使得可以通过IOCTL和MMAP来分配和映射内存。
在DPDK中打开Xen Dom0模式
默认情况下,Xen Dom0在DPDK配置文件中是关闭的,要支持Xen Dom0就需要改变配置项CONFIG_RTE_LIBRTE_XEN_DOM0的值为y,那这个功能在编译时就是打开的。
此外,CONFIG_RTE_EAL_ALLOW_INV_SOCKET_ID选项也应该设为y,避免万一收到错误的socket id。
加载DPDK rte_dom0_mm模块
在Xen Dom0上跑DPDK程序,rte_dom0_mm模块必须带着参数rsv_memsize选项加载入运行中的内核中。模块可以在DPDK目录下的子目录kmod中找到,加载时使用insmod命令,像下面一样操作(假设我们当前处于DPDK的根目录下):
sudo insmod kmod/rte_dom0_mm.ko rsc_memsize=X
X的值可以大于4096(MB)
配置DPDK使用的内存
在加载完rte_dom0_mm.ko内核模块后,用户必须配置DPDK使用的内存大小。通过echo 内存页数目到/sys/devices/目录下的内存大小配置文件中。使用如下命令(假设需要2048MB内存):
echo 2048 >/sys/kernel/mm/dom0_mm/memsize-mB/memsize
用户也可以检查当前已经使用了多少内存:
cat /sys/kernel/mm/dom0-mm/memsize-mB/memsize_revd
Xen Domain0不支持NUMA配置,所以–socket-mem选项对它来说是没用的。
注意:内存大小值不能大于rsv_memsize值
在Xen Domain0上跑DPDK程序
要在Xen Domain0上跑DPDK程序,必须带上额外的选项–xen-dom0
3编译DPDK源码包
3.1安装DPDK和查看源码
首先,解压压缩包并进入DPDK目录:
user@host:~$ unzip DPDK-<version>.zip
user@host:~$ cd DPDK-<version>
user@host:~/DPDK-<version>$ ls
app/ config/ drivers/ examples/ lib/ LICENSE.GPL LICENSE.LGPL Makefile mk/ sc
解压的DPDK目录下有几个子目录:
- lib:DPDK库源码
- drivers:DPDK PMD源码
- app:DPDK程序源码(自动化测试用)
- examples:DPDK应用程序范例源码
- config,tools,scripts,mk:框架相关的makefile文件,脚本,配置文件等。
3.2在目标机器环境下安装DPDK
安装DPDK的结果格式如下(就是在当前目录下回出现一个新目录,目录名格式):
ARCH-MACHINE-EXECENV-TOOLCHAIN
- ARCH:i686, x86_64, ppc_64
- MACHINE:native, ivshmem, power8
- EXECENV:linuxapp,bsdapp
- TOOCHAIN:gcc,icc
我在redhat上的是x86_64-native-linuxapp-gcc(intel的ICC编译器没用过...,据说在intel的机器上,用icc性能高一些)
生成的文件取决于32/64位安装包和机器上的编译器。能实现的结果可以在DPDK/config中看到。defconfig_ prefix没必要用到。
注意:提供的配置文件是根据RTE_MACHINE优化的最优选项集,在配置文件中,RTE_MACHINE配置项
被设为native,意味着编译软件会自动匹配编译的平台。要看更多关于设置和可能值的详细星系,请看DPDK Programmers Guide
当使用intel的C++编译器icc,需要针对64位和32位系统分别执行下面的命令。注意shell脚本更新了PATH环境变量,因此在同一个会话中不需要再执行了。另外,确认编译的安装目录因为目录可能不一样:
source /opt/intel/bin/iccvars.sh intel64
source /opt/intel/bin/iccvars.sh ia32
安装和编译,在DPDK根目录下执行make install T=<target>命令。
例如,用icc编译64位系统,执行:
make install T=x86_64-native-linuxapp-icc
用gcc编译32位系统,命令如下:
make install T=i686-native-linuxapp-gcc
用gcc编译64位系统:
make install T=x86_64*gcc
同时使用gcc和icc编译64位系统:
make install T=x86_64-*
注意:通配符*表示可以同时生成多个结果。
准备编译而不是编译它,例如,配置被改变了需要在编译前make一下,使用make config T=<target>命令:make config T=x86_64-native-linuxapp-gcc
提示:任意一个使用到的内核模块,如igb_uio,rte,必须在目标机器上同一个运行的内核上编译。如果你的DPDK不是要装在本机,那么编译前需要将RTE_KERNELDIR指向需要安装的机器的内核版本的一个拷贝。
晕:忘了保存了........,再来一遍
一旦环境配置好了,那么进入配置的目录修改代码并编译。用户如果要修改编译时的配置,那么修改安装目录下的.config文件。它是安装根目录下的config目录中的defconfig文件的一个拷贝。
cd x86_64-native-linuxapp-gcc
vi .config
make
此外,make clean命令可以清除之前编译产生的文件,以便重新编译。
3.3查看安装完成的DPDK环境目录
一旦编译完成,那么这个DPDK环境目录包含所有的库文件,PMD,所有DPDK应用程序需要用到了DPDK头文件。此外,测试程序和测试PMD程序在对应的build/app目录下,可以用来测试。当前的kmod目录下需要被加载到内核中的木块。
$ ls x86_64-native-linuxapp-gcc
app build hostapp include kmod lib Makefile
3.4加载内核模块实现DPDK用户态IO
跑dpdk程序,对应匹配的uio模块需要加载到运行的内核中。很多情况下,linux内核已经有的uio_pci_generic模块可以提供uio能力,可以通过以下命令加载:
sudo modprobe uio_pci_generic
相对这个正式的方式,DPDK还提供kmod子目录下的igb_uio模块实现这个功能。加载如下:
sudo modprobe uio
sudoi insmod kmod/igb_uio.ko
注意:对于一些缺乏对中断支持的设备,例如虚拟设备(VF),igb_uio需要用来替换uio_pci_generic。
从DPDK1.7以上版本提供VFIO支持以后,对于使用VFIO的平台来说,用不用uio就无所谓了。
3.5在家VFIO模块
运行一个使用VFIO的程序,vfio-pci模块必须加载:
sudo modprobe vfio-pci
当然要使用VFIO,内核也必须支持才行。自动3.6.0内核版本以来都包含有VFIO模块且默认都是加载的,但是最好是看看对应使用的linux发行版本的说明文档以防万一。
哎,用VFIO,内核和BIOS都必须支持且被配置使用IO virtualization(例如 intel@VT-d(bios里面的一个选项)
对于非root用户在跑dpdk程序时,应该赋予相应的权限操作VFIO。可以通过DPDK脚本来实现(在tools目录下,名字是setup.sh)
3.6绑定和解绑网卡从内核模块
自从dpdk1.4版本起,dpdk程序不再需要自动的解绑所有支持dpdk且内核驱动正在用的网卡。取而代之的是,dpdk程序要用到的网卡必须在程序运行前绑定到uio_pci_generic, igb_uio或者vfio-pci模块。在linux内核驱动控制下的网卡都会被dpdk的pmd忽略,也不会被程序使用。
提示:dpdk将,也是默认,不再在启动时自动从linux内核驱动解绑每个网卡。任意一个要被dpdk用到的网卡必须在程序运行前先从linux控制下解绑然后绑定到对应的uio_pci_generic, igb_uio or vfio-pci。
绑定网卡到uio_pci_generic, igb_uio or vfio-pci供dpdk使用,以及将网卡返回给linux系统控制,tools子目录下叫dpdk_nic_bind.py脚本可以提供该功能。这个脚本可以列出当前系统内所有网卡的状态信息,也可以从不同linux驱动绑定或者解绑网卡,包括uio和vfio模块。下面是一些展示脚本如何使用的例子。对于脚本完整的功能和参数介绍可以通过使用脚本带上-help或者是-usage参数。要注意的是使用dpdk_nic_bind.py脚本前需要将uio或者是vfio加载到内核中。
提示:对于使用VFIO的设备会有一些限制。主要归结于IOMMU分组如何工作。任何虚拟设备就其本身而言都可以使用VFIO,但是对于物理设备要求绑定到VFIO,或者是其中一些绑定到VFIO而另外的不绑定到任何东西上。
如果你的设备是在一个PCI-to-PCI网桥之后,网桥将作为设备的IOMMU组的一部分。所以当设备在网桥之后工作于VFIO,网桥驱动也必须与网桥PCI设备解绑。
提示:任何用户可以使用脚本查看网卡状态,解绑和绑定网卡,但是需要root权限。
上面这个xen啊,vfio啊,不是很懂,翻译的不是很清楚,也可能翻译错了,想搞明白还是看原文吧。
看系统内网卡的状态:
root@host:DPDK# ./tools/dpdk_nic_bind.py --status
Network devices using DPDK-compatible driver
============================================
0000:82:00.0 '82599EB 10-Gigabit SFI/SFP+ Network Connection' drv=uio_pci_generic unused=ixgbe
0000:82:00.1 '82599EB 10-Gigabit SFI/SFP+ Network Connection' drv=uio_pci_generic unused=ixgbe
Network devices using kernel driver
===================================
0000:04:00.0 'I350 Gigabit Network Connection' if=em0 drv=igb unused=uio_pci_generic *Active*
0000:04:00.1 'I350 Gigabit Network Connection' if=eth1 drv=igb unused=uio_pci_generic
0000:04:00.2 'I350 Gigabit Network Connection' if=eth2 drv=igb unused=uio_pci_generic
0000:04:00.3 'I350 Gigabit Network Connection' if=eth3 drv=igb unused=uio_pci_generic
Other network devices
=====================
<none>
绑定网卡eth1,04:00.1(eth1的pci号),到uio_pci_generic驱动:
root@host:DPDK# ./tools/dpdk_nic_bind.py --bind=uio_pci_generic 04:00.1
或者是用这种方式:
root@host:DPDK# ./tools/dpdk_nic_bind.py --bind=uio_pci_generic eth1
恢复设备82:00.0,绑定到原有的内核驱动:
root@host:DPDK# ./tools/dpdk_nic_bind.py --bind=ixgbe 82:00.0
4编译和运行范例程序
本章介绍了如何编译和在DPDK环境下运行程序,也指示了范例程序存在哪里。
注意:本章的部分内容可以在第6章描述的使用安装脚本后操作。
4.1编译范例程序
一旦DPDK环境创建完成(例如x86_64-nativelinuxapp-gcc),包含开发程序需要的所有的DPDK库和头文件。
当在linux下编译一个基于dpdk的程序,下面的两个参数要被导出:
RTE_SDK:指向DPDK安装目录
RTE_TARGET:指向DPDK目的环境目录,就是编译dpdk产生的目录,例如x86_64-nativelinuxapp-gcc
下面是创建helloworld程序的例子,这个是在dpdk linux环境在运行的。这个例子可以在${RTE_SDK}/examples目录下找到。
这个目录包含一个main.c文件。这个文件和dpdk目录下的库结合,调用各种初始化dpdk环境的函数,然后加载每个core的入口函数(分发程序)运行。(这个我自己明白但是翻译的不清楚,实际就是在dpdk线程上运行一个入口函数,在函数内再根据所在的逻辑核配置跑对应的功能,入rx,tx,fp)。默认编译产生的可执行二进制文件在build目录下。
user@host:~/DPDK$ cd examples/helloworld/
user@host:~/DPDK/examples/helloworld$ export RTE_SDK=$HOME/DPDK
user@host:~/DPDK/examples/helloworld$ export RTE_TARGET=x86_64-native-linuxapp-gcc
user@host:~/DPDK/examples/helloworld$ make
CC main.o
LD helloworld
INSTALL-APP helloworld
INSTALL-MAP helloworld.map
user@host:~/DPDK/examples/helloworld$ ls build/app
helloworld helloworld.map
注意:在上面的例子中,helloworld是在dpdk目录框架下。然而也有可能它不再dpdk目录下以保证dpdk的完整。在下面的例子中,helloworl程序就是从dpdk目录下将helloworld拷贝到一个新的目录下:
user@host:~$ export RTE_SDK=/home/user/DPDK
user@host:~$ cp -r $(RTE_SDK)/examples/helloworld my_rte_app
user@host:~$ cd my_rte_app/
user@host:~$ export RTE_TARGET=x86_64-native-linuxapp-gcc
user@host:~/my_rte_app$ make
CC main.o
LD helloworld
INSTALL-APP helloworld
INSTALL-MAP helloworld.map
4.2运行一个范例程序
提示:uio驱动和大页内存必须在程序运行前设置。
提示:每个程序使用的网卡必须绑定到内核对应的驱动,实际就是我们家在到内核的dpdk内核模块,如igb_uio等,如3.5所写的,这个动作也要在程序运行前执行。
程序是和dpdk目标环境下的环境抽象层库连接,后者提供了每个dpdk程序通用的一些选项。
下面是需要提供给eal的参数列表:
./rte-app -c COREMASK -n NUM [-b <domain:bus:devid.func>] [--socket-mem=MB,...] [-m MB] [-r NUM]
EAL选项如下:
- -c coremask:16进制的要运行的逻辑核位掩码。注意在不同的平台之间core编号可能不同,需要预先确定好。
- -n NUM:每个处理器插槽的内存通道数
- -b <domain:bus:devid.func>:网卡黑名单,阻止EAL使用的特殊的PCI设备,支持多个-b选项
- -use-device:只使用特殊的网卡设备,可以使用逗号分割的<[domain:]bus:devid.func>值来指定。不能喝-b一起用
- -socket-mem:从特定的socket上分配大页内存
- -m MB:从大页分配内存数,不区分是那个socket。建议用-socket-mem代替这个选项
- -r NUM:内存rank数(说实话,查了那么多资料,还是对这个不是很清楚啊)
- -v:在启动时显示版本信息
- -huge-dir:大页内存挂在的目录
- -file-prefix:用于大页文件的前缀名
- -proc-type:The type of process instance进程类型(主/备吧,我猜的没有用过多进程的)
- -xen-dom0:支持程序在Xen Domain0上不使用大页运行
- -vmware-tsc-map:使用VMvare的TSC代替本地的RDTSC
- -base-virtaddr:特定的虚拟基地址
- -vfio-intr:VFIO使用的特定的中断类型(即使VFIO不使用也没有影响)
-n和-c是必须得,其它都是可选项
拷贝可执行程序到目的机器上,如下运行程序(假设每个socket有4个内存通道,使用0-3核跑该程序):
user@target:~$ ./helloworld -c f -n 4
注意:选项-proc-type和-file-prefix EAL用于跑多个dpdk程序,可以看《dpdk范例程序使用手册》中的多进程范例程序一章和dpdk开发手册获取更多细节。
4.2.1程序使用的逻辑核
对于dpdk程序,参数coremask是必须有的。每个bit的掩码都对应linxu展示的逻辑核编号。这些逻辑核编号对应具体得NUMA上的物理核心,不同的平台会不同,建议在不同的平台上运行不同的例子要考虑选择使用的core的分布。
运行dpdk程序初始化EAL时,会将使用的逻辑核和对应的socket打印出来。这些信息也可用通过查看/proc/cpuinfo来获取,例如,执行 cat /proc/cpuinfo,显示的physical id表明了每个核所属的cpu插槽。这个在我们弄明白逻辑核和socket的映射关系时很有用。
注意:更多的逻辑核分布的拓扑结构视图可以通过使用lstopo获取。在Fedora上,该命令可能安装了,像这样运行:
sudo yum install hwloc
./lstopo
提示:在不同的主板上布局不同,逻辑核的分布也会不同,我们需要在选取程序使用的逻辑核之前检查一下
4.2.2程序使用的大页内存
在程序运行时,使用分配的数量的大页内存(就是你分配多少就用多少了,而不是按需去整),这是在没有指定参数-m和-socket-mem时程序在启动时自动执行的。
如果程序使用-m和-socket-mem指定具体的内存数超出时,程序就会挂掉。然而,程序要求使用的内存比分配保留的大页内存数小,特别是用-m选项指定时,程序也可能会挂掉。原有如下:假设现在系统在socket0和socket1上各有1024个2M的大内存页。如果用户申请使用128M内存,那64个页可能不满足以下限制:
- 内核只在soket1上给程序分配了大页内存。万一程序想要在socket0上创建一个对象,例如队列或者是mempool,程序会报错。要避免这个错误建议使用-socket-mem选项代替-m选项。(就是-m是内核随机分配的内存,不知道在哪个socket上,而-socket-mem是指定socket,我们创建队列,mempool是需要指定使用内存的socke,如果这个socket上内存不足就会报错退出)
- 这些内存页可能在物理内存上随机的分布在任意一个地方,而dpdk EAL试图分配一个在物理上连续的内存块。在这些物理页不连续时,万一程序想要申请一段很大的mempool是灰报错。
-socket-mem选择用于在特定的socket上申请指定数目的内存页。使用-socket-mem带上每个socket申请的内存数实现。例如,使用-socket-mem=0,512,意味着只在socket1上分配512m内存。同样的,在4个socket的机器上,从socket0和socket2上分配各1g内存,参数应该是-socket-mem=1024,0,1024.在其它没有明确指定的cpusocket上不会分配内存,如socket3.如果dpdk在每个socket上不能申请到足够的内存,EAL就会初始化失败。
4.3其它的范例程序
其它的例子程序在${RTE_SDK}/examples目录下。这些程序的创建和运行与本手册中其它章节描述的差不多。另外就是看《dpdk范例程序使用手册》看具体每个程序的描述信息,编译和执行时的特殊指令,以及一些代码的注释说明。
4.4另外的测试程序
除此之外,有两个程序在库创建时也创建了。源文件在DPDK/app下,在测试和测试pmd时调用。可以在库创建完成后,在build/app下找到。
测试程序提供了DPDK各种功能的多种测试。
PMD测试程序提供了一些不同的包收发测试和像如何使用INTEL® 82599万兆网卡的FLow Director这类特征的例子。
5打开其它功能
5.1高精度时钟功能HPET
5.1.1BIOS支持
要使用HPET那么平台的BIOS必须是支持的,否则使用默认的TSC。一般情况下,开机时按F2可以进入bios设置。用户可以选择HPET选项。在intel的Crystal Forest平台的BIOS上,路径是Advanced -> PCH-IOConfiguration -> High Precision Timer -> (Change fromDisabled to Enabled if necessary).
在系统重启后,用下面的指令确认是否打开HPET:
# grep hpet /proc/timer_list
如果没啥返回,HPET在BIOS上肯定打开了,每次在重启后执行上述命令。
5.1.2linux内核支持
dpdk使用平台的HPET通过映射时间戳计时器到用户地址空间,诸如此类的,就需要内核的HPET_MMAP选项打开。
提示:在Fedora上,和其它的注入unbuntu发现版上,HPET的内核选项默认是关闭的。在将选项修改后在编译内核,请通过查看发行版文档来获取确切的指令。
5.1.3在DPDK中打开HPET
默认情况下,DPDK的配置文件中HPET支持选项是禁用的。要使用HPET,那么CONFIG_RTE_LIBEAL_USE_HPET选项需要设置成y,会在编译时使用HPET配置。
应用程序要使用rte_get_hpet_cycles() 和rte_get_hpet_hz()这两个接口,使用HPET最为rte_timer库的默认时间源。API rte_eal_hpet_init()需要在程序初始化时调用。这个API的调用确认HPET是可以使用,返回错误值则说明不能用。例如,如果内核的HPET_MMAP是关闭的。程序可以决定采取什么动作,如果HPET在运行时不能用。
注意:程序要使用时间API,但是明确HPET是不能用时,建议使用rte_get_timer_cycles() and rte_get_timer_hz()代替HPET相关的API。这两个API使用的不是TSC就是HPET,取决于程序是否调用了rte_eal_hpet_init(),做了就看系统是否在运行时支持了。
5.2非root权限下运行dpdk程序
尽管基于dpdk的程序能直接使用到网卡和其它硬件资源,只需要一点小的权限调整即可跑起来而不是作为root用户使用。要实现这些,文件的所有者或者是权限要调整以便确保linux用户账号能够使用dpdk程序:
- 作为大页挂在点服务的目录,例如:/mnt/huge
- 在/dev下用户态io设备文件,例如:/dev/uio0,/dev/uio1等等
- 用户态io sysfs配置和源文件,例如uio0:/sys/class/uio/uio0/device/config /sys/class/uio/uio0/device/resource*
- 如果使用了HPET,/dev/hpet
注意:在一些安装的linux上,会默认创建一个大页的挂载点/dev/hugepages
5.3电量管理和省电功能
要用到dpdk的电源管理功能就要求该平台的bios支持增强型intel SpeedStep®技术,否则,sys文件/sys/devices/system/cpu/cpu0/cpufreq就不会存在且cpu平率调整的电源管理也不恩能够用。查阅相关的bIOS文档看如何实现吧
举例说明,在一些intel平台上,Enhanced IntelSpeedStep® Technology在BIOS的路径是:
Advanced->Processor Configuration->Enhanced Intel SpeedStep® Tech
此外,为了电源管理C3和C6也需要打开。C3和C6路径如下:Advanced->Processor Configuration->Processor C3 Advanced->ProcessorConfiguration-> Processor C6
5.4使用linux的cpu核心隔离来减少上下文切换的开销
当DPDK程序线程固定在系统的摸一个核上运行时,linux调度器可能会将其它的任务调度到该核上运行。为了防止其它负载调度到dpdk使用的核上,需要使用linux内核参数isolcpus来将这些核从linux调度器中隔离开。
例如,如果dpdk程序跑在逻辑核2,4,6上,下面的这个参数需要加到内核选项(就是那个grub文件)中:
isolcpus=2,4,6
5.5 加载kni模块
要跑dpdk KNI例子程序,需要额外加载一个模块kni。在dpdk编译目录下的kmod中,和加载igb_uio一样,使用ismod加载:
#insmod kmod/rte_kni.ko
注意:请看DPDK范例使用手册的KNI示例章节。
5.6通过intel的VT-d虚拟化技术实现IOMMU直接传输跑dpdk
要让linux内核支持intel® VT-d,需要打开以下内核选项:
• IOMMU_SUPPORT
• IOMMU_API
• INTEL_IOMMU
此外,要跑使用intel VT-d技术的dpdk程序,在使用igb_uio驱动时必须带上iommu=pt参数。这会让直接内存访问重新映射。如果内核参数NTEL_IOMMU_DEFAULT_ON没有设置,那么内核参数intel_iommu=on也必须使用。这是为了确保intel IOMMU按照预期初始化。
请注意当强制在igb_uio,vfio-pci驱动上使用iommu=pt,确实可以同时使用iommu=pt and iommu=on。
5.740g网卡上小包的高性能处理
最新版本的固件镜像解决了性能增强的问题,固件更新可以获取更高的处理性能。跟本地的intel工程师联系固件更新。支持固件版本FVL3E的基本驱动将在下一个dpdk版本中整合到一起,当前能用到的版本是4.2.6
5.7.1 打开Extended Tag和设置Max Read Request Size
PCI的extended_tag和max_read_request_size对40g网卡的小包处理性能有巨大的影响。打开extended_tag和设置max_read_request_size为小尺寸例如128字节会对小包的处理性能提升有恒大的帮助。
- 大部分可以在BIOS启动时设置
- 对于其它BIOS,PCI配置可以通过使用命令setpci,或者是dpdk配置文件的特殊配置
- pci设备的地址0xa8处bit7:5处用于设置max_read_request_size,而0xa8处的第八个bit用于开启关闭extended_tag。可以用lspci和setpci来读取读取0xa8对应的值然后回写修改的值。
- 在一般的linux配置文件中,下面的配置项要zuo修改:
CONFIG_RTE_PCI_CONFIG
CONFIG_RTE_PCI_EXTENDED_TAG
CONFIG_RTE_PCI_MAX_READ_REQUEST_SIZE
5.7.2 使用16字节大小的rx描述符
i40e的PMD支持16和32字节大小rx描述符,16个字节可以在小包处理性能上提供帮助。要使用16字节的需要修改配置文件中的配置项CONFIG_RTE_LIBRTE_I40E_16BYTE_RX_DESC。
5.7.3高性能和包延迟权衡
由于硬件设计的原因,每个包描述符的回写是需要网卡内部的中断型号来实现。最小时间间隔的中断信号可以通过配置文件的CONFIG_RTE_LIBRTE_I40E_ITR_INTERVAL配置,编译生效。尽管有默认配置,用户可以考虑性能或者是包处理延迟来调整这个参数。
6快速启动脚本
tools目录下的setup.sh脚本可以帮助用户完成以下任务:
- 创建dpdk库
- 装载和卸载dpdk igb_uio内核模块
- 装载和卸载VFIO内核模块
- 装载和卸载dpdk KNI内核模块
- 创建和删除NUMA或非NUMA下的大页
- 网络端口状态视图和分配dpdk使用的端口
- 赋予非特权用户使用VFIO的权限
- 运行测试程序和测试pmd程序
- 查看内存的大页信息
- 显示/mnt/huge的大页列表
- 移除创建的dpdk库
那些步骤完成后,用户需要编译自己的程序并连接创建的dpdk EAL库。
6.1脚本的使用
setup.sh脚本可以暗转给一定的顺序使用,每一步都给说明方便用户完成想要的任务。下面是每一步骤的简短说明:
step1:创建dpdk库
最开始,用户表虚选择需要创建的dpdk目标类型和创建库是用到的编译选项。
用户必须先有所有的链接库,模块,更新和编译安装,如前面章节中介绍的。
step2:安装环境
用户配置linux系统环境以支持运行dpdk程序。大页可以在numa或非numa系统中建立。任何存在的大页都会被弄走。要用到的内核模块也会在这是插入,dpdk用的网卡端口也会绑定到这个模块。
step3:运行程序
一旦上述步骤完成,用户可能要运行测试程序。测试程序允许用户运行一系列的dpdk功能测试。testpmd测试程序则是测试收发包。
step4:检查系统
这一步提供一些工具查看大页的状态信息
step:系统还原
最后一步是将系统还原到初始的状态。
6.2用例
下面的步骤是展示如何使用setup.sh脚本。脚本的运行需要使用source命令,执行前脚本的一些选项提示用到的值。
提示:setup.sh脚本要运行在root权限下。
user@host:~/rte$ source tools/setup.sh
------------------------------------------------------------------------
RTE_SDK exported as /home/user/rte
------------------------------------------------------------------------
Step 1: Select the DPDK environment to build
------------------------------------------------------------------------
[1] i686-native-linuxapp-gcc
[2] i686-native-linuxapp-icc
[3] ppc_64-power8-linuxapp-gcc
[4] x86_64-ivshmem-linuxapp-gcc
[5] x86_64-ivshmem-linuxapp-icc
[6] x86_64-native-bsdapp-clang
[7] x86_64-native-bsdapp-gcc
[8] x86_64-native-linuxapp-clang
[9] x86_64-native-linuxapp-gcc
[10] x86_64-native-linuxapp-icc
------------------------------------------------------------------------
Step 2: Setup linuxapp environment
------------------------------------------------------------------------
[11] Insert IGB UIO module
[12] Insert VFIO module
[13] Insert KNI module
[14] Setup hugepage mappings for non-NUMA systems
[15] Setup hugepage mappings for NUMA systems
[16] Display current Ethernet device settings
[17] Bind Ethernet device to IGB UIO module
[18] Bind Ethernet device to VFIO module
[19] Setup VFIO permissions
------------------------------------------------------------------------
Step 3: Run test application for linuxapp environment
------------------------------------------------------------------------
[20] Run test application ($RTE_TARGET/app/test)
[21] Run testpmd application in interactive mode ($RTE_TARGET/app/testpmd)
------------------------------------------------------------------------
Step 4: Other tools
------------------------------------------------------------------------
[22] List hugepage info from /proc/meminfo
------------------------------------------------------------------------
Step 5: Uninstall and system cleanup
------------------------------------------------------------------------
[23] Uninstall all targets
[24] Unbind NICs from IGB UIO driver
[25] Remove IGB UIO module
[26] Remove VFIO module
[27] Remove KNI module
[28] Remove hugepage mappings
[29] Exit Script
Option:
下面是创建x86_64-native-linuxapp-gcc dpdk库的命令使用示范:
Option: 9
================== Installing x86_64-native-linuxapp-gcc
Configuration done
== Build lib
...
Build complete
RTE_TARGET exported as x86_64-native -linuxapp-gcc
下面是加载dpdk uio驱动的示例:(感觉不对,25应该按上面说的是卸载驱动啊,2.1没用过,等后面翻译开发手册时再试试吧)
Option: 25
Unloading any existing DPDK UIO module
Loading DPDK UIO module
在numa系统中创建大页的示范。在每个node上分配1024个2m内存页。结果就是程序应该在启动时带上-m 4096参数使用这些内存(实际上即使不提供-m选项,dpdk程序也会自动使用这些内存)
注意:如果显示提示用户删除临时文件,输入y
Option: 15
Removing currently reserved hugepages
mounting /mnt/huge and removing directory
Input the number of 2MB pages for each node
Example: to have 128MB of hugepages available per node,
enter '64' to reserve 64 * 2MB pages on each node
Number of pages for node0: 1024
Number of pages for node1: 1024
Reserving hugepages
Creating /mnt/huge and mounting as hugetlbfs
下面是加载测试程序在一个核上跑的示例:
Option: 20
Enter hex bitmask of cores to execute test app on
Example: to execute app on cores 0 to 7, enter 0xff
bitmask: 0x01
Launching app
EAL: coremask set to 1
EAL: Detected lcore 0 on socket 0
...
EAL: Master core 0 is ready (tid=1b2ad720)
RTE>>
6.3应用程序
一旦用户setup.sh脚本运行过,EAL创建了,大页也搞好了。就可以创建和运行自己的程序了或者是提供的例子程序。
下面是运行/exaples下的helloword程序示例,使用的是0-3核:
rte@rte-desktop:~/rte/examples$ cd helloworld/
rte@rte-desktop:~/rte/examples/helloworld$ make
CC main.o
LD helloworld
INSTALL-APP helloworld
INSTALL-MAP helloworld.map
rte@rte-desktop:~/rte/examples/helloworld$ sudo ./build/app/helloworld -c 0xf -n 3
[sudo] password for rte:
EAL: coremask set to f
EAL: Detected lcore 0 as core 0 on socket 0
EAL: Detected lcore 1 as core 0 on socket 1
EAL: Detected lcore 2 as core 1 on socket 0
EAL: Detected lcore 3 as core 1 on socket 1
EAL: Setting up hugepage memory...
EAL: Ask a virtual area of 0x200000 bytes
EAL: Virtual area found at 0x7f0add800000 (size = 0x200000)
EAL: Ask a virtual area of 0x3d400000 bytes
EAL: Virtual area found at 0x7f0aa0200000 (size = 0x3d400000)
EAL: Ask a virtual area of 0x400000 bytes
EAL: Virtual area found at 0x7f0a9fc00000 (size = 0x400000)
EAL: Ask a virtual area of 0x400000 bytes
EAL: Virtual area found at 0x7f0a9f600000 (size = 0x400000)
EAL: Ask a virtual area of 0x400000 bytes
EAL: Virtual area found at 0x7f0a9f000000 (size = 0x400000)
EAL: Ask a virtual area of 0x800000 bytes
EAL: Virtual area found at 0x7f0a9e600000 (size = 0x800000)
EAL: Ask a virtual area of 0x800000 bytes
EAL: Virtual area found at 0x7f0a9dc00000 (size = 0x800000)
EAL: Ask a virtual area of 0x400000 bytes
EAL: Virtual area found at 0x7f0a9d600000 (size = 0x400000)
EAL: Ask a virtual area of 0x400000 bytes
EAL: Virtual area found at 0x7f0a9d000000 (size = 0x400000)
EAL: Ask a virtual area of 0x400000 bytes
EAL: Virtual area found at 0x7f0a9ca00000 (size = 0x400000)
EAL: Ask a virtual area of 0x200000 bytes
EAL: Virtual area found at 0x7f0a9c600000 (size = 0x200000)
EAL: Ask a virtual area of 0x200000 bytes
EAL: Virtual area found at 0x7f0a9c200000 (size = 0x200000)
EAL: Ask a virtual area of 0x3fc00000 bytes
EAL: Virtual area found at 0x7f0a5c400000 (size = 0x3fc00000)
EAL: Ask a virtual area of 0x200000 bytes
EAL: Virtual area found at 0x7f0a5c000000 (size = 0x200000)
EAL: Requesting 1024 pages of size 2MB from socket 0
EAL: Requesting 1024 pages of size 2MB from socket 1
EAL: Master core 0 is ready (tid=de25b700)
EAL: Core 1 is ready (tid=5b7fe700)
EAL: Core 3 is ready (tid=5a7fc700)
EAL: Core 2 is ready (tid=5affd700)
hello from core 1
hello from core 2
hello from core 3
hello from core 0
Programmer’s Guide
Release 2.1.0
翻译的目的是强化自己对dpdk的理解,看看2.1版本和现在使用的版本的差异,其次就是可能要走了,为那些要上手dpdk,但是又不想看英文资料的同志们做贡献,英语不好,有些地方可能是自己理解的不对,或者是理解了翻译不出来,看到不咋对的还是去看英文资料对照一下吧,请叫我红领巾,哈哈哈。
- 引言
文档提供了关于软件架构.开发环境的信息和优化指导.
对于范例程序和编译指令以及运行每个例子的星系,请查看<DPDK Sample Applications User Guide>
对于编译和运行程序总的说明见<DPDK Getting Started Guide>就是那个dpdk2.1开发入门手册。
1.1 文档总览
这个还是参见入门手册吧,每个文档都有这个….
1.2 相关的出版物
下面这个文档提供了使用dpdk开发程序相关的资料:
Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A: System Programming Guide
Part1:架构概览
- 概要
这部分提供了DPDK架构概览。dpdk的目的是针对数据平台上快速包处理,提供一个简单的,完整的框架。用户使用代码来理解一些技术细节,在其之上开发协议或者是计入自己的协议栈。这两者使用dpdk都是可以实现的。
dpdk框架通过建立一个EAL来实现创建一系列的针对特定环境的库,这是对于intel32/64位架构,linxu用户态编译或者是特定平台的特殊模式。这些环境的创建时通过使用Make文件和配置文件。一旦EAL库创建,用户会将其连接到自己创建的程序上。在EAL之外的其它库,包括哈希算法,最长前缀匹配,ring操作这些库也都提供。范例程序用来向用户展示如何使用dpdk的各种特性。
dpdk实现了包处理的完整的模式,在调用数据平台处理程序前需要分配所有的资源,执行单元运行在逻辑处理核上。(这个我理解的是相对于物理核,intel的超线程将其弄成了两个逻辑核,啥时候看到农企的超线程啊)。这种模式下不支持系统调度和所有的网卡设备都是轮询方式的访问。不使用中断的方式就是因为中断处理程序对性能的影响(深有感触啊)。
此外对于那种运行到完成的模式,管道模式就是通过使用ring在核之间传递报文或者是消息也被用到。这就可以使得工作被分割成多步在不同的核上执行,这样代码运行的更加有效,计算性能用的更彻底。
2.1 开发环境
这部分还是看入门手册吧,感觉有点重叠了。
2.2 环境抽象层EAL
EAL提供了通用的接口,而隐藏了和环境相关的细节。EAL提供的服务有:
l DPDK加载和启动
l 支持多进程和多线程的执行方式
l cpu亲和性的设置
l 系统内存的分配和释放
l 原子操作
l 定时器
l 访问PCI总线
l 调试功能
l CPU 编号
l 中断处理
l 告警操作内存管理
详细信息看第三章《EAL》
2.3 核心模块
核心模块的一系列库对高性能包处理程序提供了所有必须的接口。
Fig.2.1:核心模块结构
2.3.1 ring管理(librte_ring)
ring结构提供了一个无锁的多生产者,多消费者的先进先出的对指定大小表处理的API。它比无锁队列有很多好处,更容易部署,适合大量的操作且更快。ring在《Memory Pool Manager (librte_mempool)》中用到且可以最为不同的核之间或者是在逻辑核上的处理单元直接联系的通用通信机制。
ring 缓存和它的用法见《Ring Library》
2.3.2 Memory Pool管理(librte_mempool)
Memory Pool管理的职责是在内存中分配指定数目对象的pool。每个pool都有以名字区别的id且使用ring来存储未使用的对象。它还提供了其它服务,例如对应core的处理对象入CPU缓存和自动对齐以确保对象都均匀相等处于每个内存通道上。内存pool的分配见《Mempool Library》
2.3.3 网络报文缓存管理(librte_mbuf)
mbuf库提供了创建和销毁buffer的能力,后者可能被dpdk程序用于存储消息缓存。消息缓存可以通过使用dpdk的mempool库在程序启动时创建且存于mempool中。这个库提供了API申请和释放mbufs,操作控制消息buffer(ctrlm-buf)用于普通的消息buffer,以及报文buffer(pktmbuf)用于传输网络报文。
具体看《Mbuf Library》
2.3.4 定时器管理(librte_timer)
这个库向dpdk执行单元提供定时器服务,提供异步执行某一个函数的能力。可以是周期性的调用,也可以是只有一次的调用。 使用EAL提供的定时器接口获取高精度时钟且能在每个核上基于需要初始化。
具体参见《timer libarary》
2.4 网卡的轮询模式驱动PMD架构
dpdk包含支持1gbe,10gbbe和40gbe的PMD,且提供虚拟的以太网控制器,设计成工作在非异步的情况下,基于信号的中断机制。
参见《Poll Mode Driver》
2.5 包转发算法支持
dpdk有哈希(librte_hash)和最长前缀匹配(LPM,librte_lpm)库来支持包转发。
详见《Hash Library》和《LPM Library》
2.6 librte_net
这个库收集了ip协议定义和方便的宏定义。这些都基于FreeBSD IP协议栈的代码且包含协议号(IP头用到的),ip相关的宏定义,IPv4和IPv6头结构和SCTP头结构。
- 环境抽象层EAL
环境抽象层的任务对访问底层资源例如硬件和内存提供入口。它提供了隐藏应用和库的特殊性性的通用接口。它的责任是初始化分配资源(内存,pci设备,定时器,控制台等等)。
EAL提供的典型服务有:
l DPDK加载和启动:DPDK和程序连接成 一个单独的程序且必须通过某种方式加载。
l CPU亲和性/分配处理:EAL提供了一种机制将执行单元分配给特定的核就如同创建一个执行程序一样。
l 系统内存分配:EAL实现不同内存区域的分配,例如用于物理设备交互的内存区域。
l PCI地址的抽象:EAL提供了对PCI地址空间访问的接口。
l 追踪调试功能:日志,栈转存,异常等等。
l 公用功能:libc库不能提供的自旋锁和原子计数器
l CPU特征辨识:决定cpu运行时的特殊功能,例如:Intel AVX。决定当前CPU支持的特性以便编译对应的二进制程序。
l 中断处理:提供注册/反注册对特定中断的回调函数。
l 告警功能:设置或取消运行时特定时间告警处理的回调函数接口。
3.1. linux执行环境下的EAL
在linux用户态空间,DPDK程序使用pthread线程库最为一个用户态程序运行。设备的pci信息和地址空间通过/sys内核接口和像uio_pci_generic或者是igb_uio内核模块来获取。参阅UIO:linux内核中用户态驱动文档。这段内存是在程序中mmap的。
EAL执行物理地址的分配,从hugetblbfs管理的内存通过使用mmap()实现(使用大页来提升性能)。这些内存对于dpdk服务层都是可见的例如Mempool Library。
在这点上,dpdk服务层已经初始化了,接着通过设置线程亲和性的调用,每个执行单元将会分配给特定的逻辑核心想一个user-level线程一样运行。
定时器是由CPU的时间戳计时器(TSC)或者是通过mmap()调用的HPET内核接口提供。
3.1.1. 初始化和core运行
初始化分布是gblic的开始函数做的。检查也是在初始化时做的以确保配置文件中选择的架构宏定义是cpu支持的类型。然后,main()函数被调用。core的初始化和加载使在rte_eal_init()中做的(看API文档)。它由线程库的调用组成(pthread_self(),pthread_create(),pthread_setaffinity_np())。
注意:对象的初始化,例如内存区域,ring,mempool,lpm表和hash表,应该作为整个程序初始化的一部分在主逻辑核上完成。创建和初始化这些对象的函数不是多线程安全的。不管怎么样,一旦初始化完成,对象自身可以安全用于多线程。
3.1.2. 多进程支持
linux下EAL允许同多线程部署模式一样支持多进程。具体看2.20章节《Multi-process Support》
3.1.3. 内存映射和内存分配
大量的物理连续的内存分配是使用hugetlbfs内核文件系统来做的。EAL提供了一个API来申请指定大小的物理连续的内存块。这个API也会返回申请到的内存的物理地址给用户。
注意:内存申请是用rte_malloc库接口来做的,它也是hugetlbfs文件系统大页支持的。
3.1.4. Xen Dom0非大页支持
现存的内存管理机制是基于linux内核的大页机制。然而,Xen Dom0并不支持大页,所以要将一个新的内核模块rte_dom0_mm插入以避开这个限制。
EAL使用IOCTL接口通知内核模块rte_dom0_mm分配指定大小的内存且从这个模块获取所有内存段信息。EAL使用MMAP接口映射分配到的内存。对所有的内存段来说,在其内的物理地址都是连续的,但是实际上硬件地址只是在2MB内连续。
Fig.3.1 linux应用环境下EAL初始化过程
3.1.5. PCI设备访问
EAL使用内核提供的/sys/bus/pci扫描PCI总线上的内容。访问PCI内存,内核模块uio_pci_generic 提供了/dev/uioX设备文件以及/sys中的资源文件,它被映射以获取从用户态程序访问pci地址空间的能力。DPDK特有的igb_uio模块也提供了这一功能。这两个驱动都用到uio内核特性(用户态驱动)。
3.1.6. 每个逻辑核共享变量
注意:逻辑核就是处理器的逻辑执行单元,又是也称作硬件线程。
共享变量是默认的做法。额米格逻辑核的变量的实现是通过使用Thread Local Storage(TLS)线程局部存储?来提供每个线程的本地存储。
3.1.7. 日志
EAL提供了日志API.在linux环境下,日志默认是发送到系统日志文件和终端上.然而,用户可以使用不同的日志机制来代替DPDK提供的日志功能.
调试功能
有一些调试函数转存栈数据. rte_panic()能自动产生一个abort信号,这个信号会触发产生gdb调试用的core文件。
3.1.8. cpu特性
EAL可以在运行时查询CPU状态(通过rte_cpu_get_feature()函数)决定哪个cpu可以用。
3.1.9. 用户态中断事件
l 主线程对用户态的中断和警告处理
EAL创建一个主线程轮询UIO设备文件描述符以检测中断。EAL可以注册或者是反注册一个特定中断的回调函数,这个函数可以在主线程中异步调用。EAL也允许像NIC的中断同样的方式注册定时器中断回调函数。
注意:DPDK PMD,主线程只对连接状态改变的中断处理,例如网卡连接打开和关闭操作。
l 收包中断事件
PMD提供的收发包程序并不限制自身在轮询模式下执行。对于极小的流量减少轮询下的cpu利用率,可以中断轮询并等待wake-up事件的发送。收包中断对于此类的wake-up事件是最佳选择,单也不是唯一的。
EAL提供了事件驱动模式的API。以linuxapp为例,其运行依赖于epoll。EAL线程可以监控添加了所有wake-up事件的文件描述符对象。事件文件描述符可以创建并根据UIO/VFIO说明来映射到中断向量。对于bsdapp,kqueue是可选的,但是尚未实现。
EAL初始化事件描述符和中断向量之间的映射关系,每个设备初始化中断向量和队列之间的映射。这样,EAL实际上是忽略在指定向量上的发生的中断。eth_dev驱动会负责执行后者的映射。
注意:每个RX中断事件队列只支持VFIO,后者支持多个MSIX向量。在UIO中,收包中断和其它发生的中断共享中断向量。所以,当RX中断和LSC(连接状态改变)中断同时发生时(intr_conf.lsc==1 && intr_conf.rxq==1),只有前者生效。
使用网卡设备API控制、打开、关闭RX中断<rte_eth_dev_rx_intr_*>,如果PMD不支持则返回失败。intr_conf.rxq标识用于打开每个设备的RX中断。
3.1.10. 黑名单
EAL pci设备黑名单功能是用于标记网卡某一个端口作为黑名单,以便DPDK忽略该端口。用PCIe描述符(Domain:Bus:Device.Function)将端口标记黑名单。
3.1.11. 复杂指令集功能
i686和x86_64架构上的锁和原子操作。
3.2. 内存分段和内存区(memzone)
物理地址的映射就是EAL通过这个来实现的。物理内存可能是分隔不连续的,所有的内存都由一个内存描述符表管理,且每个描述符(rte_memseg)指向的都是一段连续的物理内存。
在这之上,memzone分配器的角色就是保存一段物理连续的内存。这些内存区的内存被申请到使用时会有一个唯一名字来标示。
rte_memzone描述符也在配置结构体中。可以使用te_eal_get_configuration()接口访问这个结构体。通过名字查找一个内存区会返回一个有内存区物理地址的描述符。
内存区可以以特定的开始地址以指定的对齐参数对齐(默认是cache_line大小的对齐)。对齐值应该是2的幂次方且不少于cache_line大小(64字节)。内存区可以是2m或者是1g的内存页,系统两者都支持。
3.3. 多线程
dpdk通常是指定核上跑指定线程以避免任务调度的开销。这个对于性能的提升很有用,但是缺少灵活性且不是总是有效的。
通过限制cpu的运行频率,电源管理有助于提升CPU效能。然而也能是利用空闲的指令周期来充分的使用CPU全部性能。
通过使用cgroup,cpu使用量可以很轻松的分配。这个提供了另外的方式来提升cpu效能,然而有一个先决条件DPDK必须处理每个核多线程之间的上下文切换。
要更加灵活,就设置线程的cpu亲和性不是对cpu而是cpu集。
3.3.1. EAL线程和逻辑核亲和性
lcore指的是EAL线程,就是一个真正的linux/freeBSD线程。EAL创建和管理eal线程,且通过remote_launch来实现任务分配。在每个EAL线程中,有一个称为_lcore_id TLS是线程的独一无二的id。一般EAL线程使用1:1来绑定物理cpu,_lcore_id通常等于CPU id。
当使用多线程时,绑定不再在线程和指定物理cpu之间总是1:1,EAL线程设为对cpu集的亲和性,而_lcore_id不再和CPU id一样。因为这个,有一个EAL选项-lcores,设置lcore的cpu亲和性。对于指定lcore ID或者是ID组,这个选项允许对EAL线程设置CPU集。
格式模板:-lcores=lcores=’<lcore_set>[@cpu_set][,<lcore_set>[@cpu_set],...]’
lcore_set 和cpu_set可以是一个数,范围或者是组。数必须是“digit([0-9]+)”,范围则是“<number>-<number>”,组则是“(<number|range>[,<number|range>,...])”。
如果@cpu_set的值没有提供,则默认将其设为lcore_set相同的值。
例如:"--lcores='1,2@(5-7),(3-5)@(0,2),(0,6),7-8'"意味着启动9个eal线程“
lcore 0 runs on cpuset 0x41 (cpu 0,6);
lcore 1 runs on cpuset 0x2 (cpu 1);
lcore 2 runs on cpuset 0xe0 (cpu 5,6,7);
lcore 3,4,5 runs on cpuset 0x5 (cpu 0,2);
lcore 6 runs on cpuset 0x41 (cpu 0,6);
lcore 7 runs on cpuset 0x80 (cpu 7);
lcore 8 runs on cpuset 0x100 (cpu 8).
使用这个选项,每个给定的lcore ID可以分配指定的cpu。也兼容corelist(‘-l‘)选项模式。
3.3.2. 非EAL线程支持
在DPDK执行上下文环境中执行用户线程(也称作非EAL线程)时可行的。在非EAL线程中,_lcore_id总是等于LCORE_ID_ANY,这个宏标示有效的非EAL线程,其值是唯一的_lcore_id。一些库会使用传统的id(例如线程标示符TID),有些是一点影响都没有,有些则是由使用限制(如timer和mempool库)。
所有的影响都在Known Issues章节中提到了。
3.3.3. 公共线程API
有两个公用API:rte_thread_set_affinity()和rte_pthread_get_affinity()。当在任意一个线程上下文环境中使用时,TLS会被设置/获取。
那些TLS包括_cpuset和_socket_id:
l _cpuset存的是线程亲和性的CPU组位图
l _socket_id存的是CPU集的NUMA节点。如果cpu集中的cpu分属不同的numa节点,则_socket_id会被设成SOCKET_ID_ANY(-1)。
3.3.4. 已知问题
l rte_mempool
rte_mempool在mempol中使用每个lcore缓存。对于非EAL线程,rte_lcore_id()返回无效值。所以目前当rte_mempool在非EAL线程中使用,put/get操作将忽视mempool缓存且 由于这个会有性能损失。对非EAL线程的mempool的cache支持现在可以了。
l rte_ring
rte_ring支持多生产者入队列和多消费者出队列。然而,它是非抢占的,这有一个消极的影响:使得rte_mempool也是非抢占的。
注意:非抢占的限制意味着:
一个线程对给定的ring执行多生产者入队列时,队列必须不被其它线程抢占执行入队列。
一个线程对给定的ring执行多消费者出队列时,队列必须不被其它先占抢占执行出队列。
忽略这个限制会引起第二个线程自旋知道第一个线程被重新调度。此外,如果第一个线程被更高优先级的上下文抢占,可能会引起死锁。
这不意味着它不能用,很简单,有必要在同样的核上减少多线程同时访问ring的场景。
- 可以用于单生产者和单消费者的场景。
- 在调度策略是SCHED_OTHER(cfs完全公平调度程序)时可以在多生产者/多消费者线程中使用。用户需要在使用前知道其性能损失。
- 在调度策略是SCHED_FIFO或者是SCHED_RR时,不能在多生产者/多消费者环境中使用。
为了rte_ring减少竞争定义了RTE_RING_PAUSE_REP_COUNT,主要是为了情况2,在N次重复暂停后放弃对ring的操作。
它增加了sched_yield()系统调用,当线程自旋等待其它线程完成对ring的操作太久时,这个给了被抢占的线程机会继续执行且完成入/出队列操作。
l rte_timer
在非EAL线程中,没有每个线程自有的日志等级和日志类型,用的是全局的日志等级。
l misc
在非EAL线程中,对rte_ring,rte_mempool,rte_timer的调试统计是不支持的。
3.3.5. cgroup控制
下面是一个简单的cgroup控制的使用例子,有两个线程(他t1和t2)在同样的core(CPU)上做包的I/O。我们期望只有50%的CPU用于包I/O:
mkdir /sys/fs/cgroup/cpu/pkt_io
mkdir /sys/fs/cgroup/cpuset/pkt_io
echo $cpu > /sys/fs/cgroup/cpuset/cpuset.cpus
echo $t0 > /sys/fs/cgroup/cpu/pkt_io/tasks
echo $t0 > /sys/fs/cgroup/cpuset/pkt_io/tasks
echo $t1 > /sys/fs/cgroup/cpu/pkt_io/tasks
echo $t1 > /sys/fs/cgroup/cpuset/pkt_io/tasks
cd /sys/fs/cgroup/cpu/pkt_io
echo 100000 > pkt_io/cpu.cfs_period_us
echo 50000 > pkt_io/cpu.cfs_quota_us
3.4. Malloc
EAL提供了一个malloc API分配任意大小内存。这个提供类似malloc函数功能的API目的是允许从大页内存中分配且便于程序移植。DPDK API参考手册详细介绍了这个函数功能。
一般的,这种分配内存的方式不能用于高速数据处理平台,因为它相对于基于pool的分配方式实在太慢了且在分配和释放时都得加锁。然而,可以在配置生成的代码中用到。
可以从DPDKAPI参考手册中查阅rte_malloc()函数的更多细节信息描述。
3.4.1. Cookies
当CONFIG_RTE_MALLOC_DEBUG选项打开时,分配的内存包含内存区域写保护以帮助识别缓存溢出。
3.4.2. 对齐和NUMA结构的限制
rte_malloc()有一个对齐参数,用于申请对齐于这个值的倍数(2的幂次方)的内存区域。
在支持NUMA的系统中,rte_malloc()函数调用会返回调用者所使用core的NUMA socket上分配的内存。提供一系列的API,以实现直接在指定的NUMA socket上分配内存,或者是在其它core所在的NUMA socket上分配,假如内存是其它一个逻辑核使用的而不是正在执行内存分配的这个核。
3.4.3. 用例
这个API用于应用程序在初始化时请求使用malloc相似的函数分配内存。
在运行是分配和释放数据,在程序的快速通道建议用mempool代替。
3.4.4. 内部实现
3.4.4.1. 数据结构
在malloc库内部有两个数据结构类型使用:
l 结构体malloc_heap:用于跟踪每个socket上的空闲内存
l 结构体 malloc_elem:分配的基本元素且库内用于跟踪空闲内存空间
结构体:malloc_heap
malloc_heap结构体用于管理每个socket上的空闲内存空间。实际上,每一个NUMA node上都有一个heap结构体对象,这样就可以实现线程从其所在运行的NUMA node上分配内存。当不能保证所使用的内存是在运行的NUMA node上,那就和在混合或者随机的node上分配内存好不到那里去。
堆结构体中的主要成员和函数描述如下:
l lock:锁成员是为了实现同步访问堆。堆中的空闲内存是一个链表维护的,为了防止两个线程同时访问这个链表就需要加锁。
l free_head:空闲内存链表头指针指向malloc_heap的空闲内存链表的第一个成员。
注意:malloc_heap结构体并不监测使用的内存块,所以这些内存块除非被重新释放否则就绝对无法接触到,重新释放就是将指向内存块的指针作为参数传给free()函数。
Fig.3.2 malloc库内的malloc heap和malloc elemets例子
结构体:malloc_elem
malloc_elem结构体用于各种内存块的通用结构,用于3中不同的方式:
- 作为一个空闲或者是分配的内存块的头-正常情况(
- 作为一个内存块内的填充头
- 作为一个内存表段的结束标记
结构体中最重要的成员和如何使用如下描述:
注意:在上面的三个使用情况中没有用到的特定成员,这些成员可以认为在那种情况下没有明确的值。例如, 对于填充头padding header,只有成员state和pad有可用的值。
l heap:这个指针是堆结构体分配的内存块的引用返回值。它用于释放的普通内存块,将其添加到堆的空闲内存链表
l prev:这个指针是指向在内存表中当前位置后面(不应该是前面吗?)紧靠着的头结构对象/内存块。当释放一个内存块的时候,这个指针指向的内存块会被检查是否也是空闲的,如果是,就将这两个空闲内存块合并成一个大的内存块。(减少内存碎片)
l next_free:这个指针用于将没有分配的内存块连接到空闲内存链表上。它只用于正常的内存块,malloc()时就查看一个合适的空闲内存块分配,free()时就是将重新释放的内存块添加到空闲链表上。
l state:这个成员有三种值:FREE,BUSY,PAD。前两个表明普通内存块的分配状态,后则表明结构体是一个在内存块起始位置填充无意义数据的尾部的虚设结构体。那就是说,数据在内存块中的开始位置不是在数据库的头上,这是由于数据对齐的限制。如此的话,填充头结构就是用于定位块内实际分配的内存的结构体头部。在内存表的尾部,结构体内这个值为BUSY,确保不会再有元素了。在释放的时候会跳过这个搜索其它的内存块来合并成大的空闲内存块。
l pad:这个代表了当前内存块开始位置填充无用段的长度。在一个正常的内存块头部,将它与头结构体尾部地址相加就是数据段开始地址,就是说这个会作为malloc的返回值给程序。在这段填充区内有虚设的头,头内成员pad有同样的值,从该头结构的地址减去pad值就是实际分配的内存块头结构地址。
l size:数据块的大小,包括自身的头部分。在内存表尾部的那个虚设结构体中,这个size是0,尽管它从没有被检查过。在一个标准的内存块释放时,这个值会代替next指针来定位靠在一起的下一个内存块,万一后者是FREE状态,那么二者就可以合二为一了。
内存分配
在EAL初始化时,所有的内存表都是组织到malloc堆下,这是会将内存表的尾部设置一个BUSY状态的虚设结构体。当CONFIG_RTE_MALLOC_DEBUG选项打开且在内存表的头部有一个FREE状态元素头,虚设机构体中就可能包含一个哨兵值。FREE元素会被加入到malloc堆的空闲链表中。
当程序调用类似malloc函数时,malloc函数会先查看调用线程的lcore_config结构体,确定该线程所在的NUMA节点。NUMA节点用作malloc_heap结构体数组的下标,且会作为其中一个参数和其它参数请求内存大小、类型、对齐值、边界一起传递给heap_alloc()函数。
heap_malloc()会先扫描heap的空闲链表,试图找到一个匹配请求存储数据大小和对齐方式、边界限制的空闲内存块,
当一个匹配的空闲元素标记时,算出的内存指针会被返回给用户。而cacheline大小的内存会在指针之前用malloc_elem装填。由于对齐和边界限制,在元素的开头和结尾会有空白空间,这回导致一下问题:
- 检查尾部空间。如果尾部空间足够大,也就是说>128字节,就会分割这个元素。如果不是,那么就忽略这个尾部空间(白白浪费掉的空间)
- 检查元素头空间。如果空间很小,就是<=128字节,就会用部分空间作为填充头结构,其它的也是浪费掉。然而,如果头空间足够大,那么就将这个空闲元素分割成两个。
从现有的元素的尾部分配内存的好处就是不用调整空闲链表来代替-空闲链表上现有元素只需要调整size变量,且其后的元素也只需将prev指针指向新产生的元素就可以了。
释放内存
要释放一段内存,数据段开始地址地址会传递给free函数。指针值减去malloc_elem结构体打下就是这个内存块的元素头。如果头中type是PAD,那就将指针减去pad值得到实际的内存块元素头结构。
从这个元素头中,我们就拿到了从堆中分配的内存块指针,且它需要在哪里释放。和prev指针一样,通过size可以计算出紧挨着的后面一个元素的头指针。检查前后元素是否是FREE,如果是就与当前的元素合并。这意味着我们不可能有两个FREE状态的元素靠在一起,它们总是会被合并成一个单独的内存块。
- Ring Labrary
ring是管理队列的。取代无限制大小的链表,rte_ring有下面的特性:
l FIFO,先进先出
l 大小是固定的,指针存在表中。
l 无锁实现
l 多个或单个消费者出队列
l 多个或单个生成者入队列
l bulk出队列-指定数目对象出队列,否则失败
l bulk入队列-同上
l burst出队列-按照指定数目最大可能得出队列,可能出的不足数
l burst入队列-按照指定数目最大可能得入队列,可能入的不足数
这个数据机构相对于链表队列的好处是:
l 更快。只需要一个大小为sizeof(void *)的原子操作CAS指令来代替多个double CAS指令。
l 比一个完全无锁队列要简单。
l 适应大量入/出对垒操作。由于指针是存储于表中,多个对象出队列就不会如链表一样会出现多个cache丢失。同样,bulk出队列的开销也不会比单个出队列大。
缺点就是:
l 大小固定
l ring会比链表队列消耗更多内存,即使一个空的ring也是包含至少N个指针大小的内存。
对于ring中生产者和消费者head、tailer指针指向的数据结构中存储的对象的简单展示:
Fig. 4.1Ring结构
4.1 Ring在FreeBSD中的应用参考
下面的代码在FreeBSD8.0中添加,用于一些网络设备的驱动(至少是intel的驱动):
l bufring.h in FreeBSD
l bufring.c in FreeBSD
4.2 linux中无锁环形缓存区
下面是描述linux无锁环形缓冲区设计的链接:http://lwn.net/Articles/340400/
4.3 其它特性
4.3.1 名字
每个ring都是通过独一无二的名字来辨别。不可能创建两个同样名字的ring(通过rte_ring_create()创建ring时,如果名字已经存在就返回空)
4.3.2 阀值
ring可以有一个阀值(临界值)。如果这个阀值配置了,一旦入队列操作到达阀值,那么生产者会得到通知。
这个机制可能用到,例如,IO压力过大时可以通知LAN 暂停。
4.3.3 调试
当调试开关打开(CONFIG_RTE_LIBRTE_RING_DEBUG设置了),ring库会存储每个ring的一些关于出入队列的个数的统计,这个统计是每个core都有的以避免同时访问或者是原子操作。
4.4 应用场景
ring库的应用包括:
l DPDK应用程序间的通讯
l 内存池的分配会用到。
4.5 (ring buffer)环形缓冲区的详细剖析
这部分讲解了ring buffer的运作。ring结构体包括两对头尾指针(head,tail)。一对用于生产者,一对用于消费者。就是下面段落中图标提到的prod_head,prod_tail,cons_head 和cons_tail。
每个图都代表了环形缓冲区的一个状态。函数本地变量在图的上方,底下则是ring结构体的内容。
4.5.1 单生产者入队列
这部分讲的是单个生产者将一个对象加入到ring中发生的事。在这个例子中,只有生产者的head和tail(prod_head与prod_tail)被修改了,有且只有一个生产者啊。
初始状态,prod_head和prod_tail指向同一个位置。
入队列第一步
首先,ring->prod_head和ring->cons_tail的值拷贝到本地变量,prod_next本地变量会指向表中的下一个元素位置,或者是bulk入队列后的几个元素
如果ring中没有足够的空间(通过检查cons_tail来判断),返回error。
入队列第二步
第二步就是修改ring结构体中ring->prod_head的值指向prod_next指向的位置。
指向要添加对象的指针被拷贝到ring中(图中obj4)。
入队列最后一步
一旦对象呗加入到ring中,ring->prod_tail就修改成与ring->prod_head指向的相同位置。入队列操作完成。
Fig. 4.2: Enqueue first step
Fig. 4.3: Enqueue second step
Fig. 4.4: Enqueue last step
4.5.2 单消费者出队列
这段讲解了当单消费者从ring中出队列一个对象时发生了啥。在这个例子中,只有ring结构体中消费者的头尾(cons_head和cons_tail)修改,当且仅当是一个消费者啊。
初始状态下,cons_head和cons_tail指向相同的位置。
出队列第一步
首先,ring->cons_head和ring->prod_tail会拷贝到本地变量cons_head和prod_tail。而本地变量cons_next会指向表中的下一个对象(就是cons_head指向的对象的下一个),或者是bulk出队列的多个对象的下一个。
如果ring中没有足够的对象(通过检测prod_tail),返回errors。
出队列第二步
第二步就是修改ring->cons_head指向和cons_next指向的相同位置。
要删除的对象(obj1)指针会拷贝到用户提供的指针上。
Fig. 4.5: Dequeue last step
Fig. 4.6: Dequeue second step
出队列最后一步
最后,ring->cons_tail修改成ring->cons_head相同的值。出队列操作完成。
Fig. 4.7: Dequeue last step
4.5.3 多生产者入队列
这部分讲了两个生产者同时添加一个对象到ring中发生的事。在本例中,只有生产者的头尾(prod_head和prod_tail)被修改了。
初始状态prod_head和prod_tail执行同一个位置。
对生产者入队列第一步(原文是消费者,整个例子下面都是用的消费者,估计是弄错了)
在两个核上,ring->prod_head和ring->cons_tail都拷贝到本地变量。本地变量prod_next执行表中下一个对象位置,或者是bulk入队列的多个对象的下一个位置。
如果没有足够的空间(检查prod_tail)就返回error。
Fig. 4.8: Multiple consumer enqueue first step
多生产者入队列第二步
第二步就是修改ring结构体的ring->prod_head指向prod_next指向的位置。这个操作时用原子操作Compare And Swap(CAS)指令,其原子的执行下面的操作:
l 如果ring->prod_head不同于本地变量prod_head,CAS操作失败,代码重新执行第一步。
l 否则,ring->prod_head设置成本地的prod_next值,CAS操作成功,继续执行。
在途中,这个操作在核1上执行成功,在核2上失败重新执行第一步。
多生产者入队列第三步
CAS操作不停的尝试直到在核2上成功。
核1更新对象(obj4)到ring中,核2更新对象(obj5)到ring中。
Fig. 4.10: Multiple consumer enqueue third step
多生产者入队列第四步
每个核都要更新ring->prod_tail。那个核上ring->prod_tail等于本地变量prod_head的才能更新。这个在本例中只有核1上可以,本步操作在核1上完成。
多生产者入队列最后一步
一旦ring->prod_tail被核1更新完成,核2就可以更新了。这个操作总在在核2上完成。
4.5.4 无符号32位索引
在先前的图中,prod_head,prod_tail,cons_head,cons_tail的值都是被一个箭头代表。在实际使用中,这些值不是在0到ring的大小减去1之间增加,这是假设的情况。值实际是在0到2^32-1之间变化,我们在访问这个值指向的指针表位置(就是ring自身)时会对这个值做掩码运算。32位无符号表明对这个数的操作(例如加和减)将自动对超出32位数字范围的数取2^32取模。
下面两个例子解释了无符号索引数在ring中如何使用的。
注意:为了简单化,我们将32位的操作用16位的操作示例代替。那4个索引值就用16位无符号整数来定义,与32位无符号整数实际上差不多。
ring包含11000个对象
ring包含12536个对象,
注意:为了简单理解,在上面的例子中我们都是用的65536取模操作。但是在实际的执行时,这是多余而低效的,因为cpu会自动在溢出时做这个。
代码总会保持生产者和消费者索引之间的位置差距在0和sizeof(ring)-1之间。感谢这个特性,我们可以在两个索引值之间做以32位模为基础的减法:这就是为啥这个索引值溢出不是问题。
在任何时间,ring中使用空间和空闲空间在0和sizeof(ring)-1之间,即使第一个减法已经溢出了。
uint32_t entries = (prod_tail - cons_head);
uint32_t free_entries = (mask + cons_tail -prod_head);
本来懂的,翻译着差点把自己搞迷糊了。很简单的东西,就是利用了无符号数的减法原理,prod_tail-cons_head就是已经使用的空间,即使出现prod_tail增长超出2^32的范围,由于无符号数的特性(涉及到补码和反码吧,这个底层的概念忘记是哪个了…..),它会变成超出数-2^32值,此时prod_tail比cons_head小,但是无符号数的相减还是会得到实际的差值。所以就如上所说,即使第一个公式溢出了,始终能得到使用的空间值。再用总空间mask-(prod_tail - cons_head)就得到空闲空间值,去掉括号就是第二个公式,看内核的kfifo就一目了然了。
4.6 参考文件
l bufring.h in FreeBSD (version 8) http://svnweb.freebsd.org/base/release/8.0.0/sys/sys/buf_ring.h?revision=199625&view=markup
l bufring.c in FreeBSD (version 8) http://svnweb.freebsd.org/base/release/8.0.0/sys/kern/subr_bufring.c?revision=199625&view=markup
l Linux Lockless Ring Buffer Design http://lwn.net/Articles/340400/
留言列表