作者简介:文殊博,北京邮电大学未来网络理论与应用实验室(FNL)研究生
本文主要是搭建一个基于DPDK的VNF开发实验环境,利用docker和vhost-user摆脱DPDK的硬件约束,方便在一台机器上进行VNF的开发和测试。但是,DPDK的PMD对不同网卡的特性支持差异很大,所以不能完全以这种环境下的运行结果为准。接下来会先介绍环境的配置过程然后搭建一个简单的拓扑,最后会对DPDK的testpmd应用中最简单的iofwd转发引擎中关键部分进行简单说明。
下载DPDK源码,并配置安装所需环境变量。
1
2
3
4
5
6
7
8
9
|
wget
http
:
//fast.dpdk.org/rel/dpdk-17.05.2.tar.xz
tar
-
xvf
dpdk
-
17.05.2.tar.xz
cd
dpdk
-
stable
-
17.05.2
#设置DPDK库目录位置
echo
export
RTE_SDK
=
$
(
pwd
)
>>
~
/
.
bashrc
#设置DPDK目标环境
#注意!这里的x86_64-native-linuxapp-gcc应替换为实际运行环境
echo
export
RTE_TARGET
=
x86_64
-
native
-
linuxapp
-
gcc
>>
~
/
.
bashrc
source
~
/
.
bashrc
|
配置DPDK
1
|
vim
config
/
common_base
|
这里由于要是用Vhost-user驱动,修改使得文件中CONFIG_RTE_LIBRTE_VHOST=y。如果需要连接其他驱动的网卡,也要确认其他驱动相关设置正确。
安装DPDK
1
2
3
|
make
config
T
=
$
RTE_TARGET
make
T
=
$
RTE_TARGET
-
j8
make
install
T
=
$
RTE_TARGET
-
j8
|
1
2
3
4
5
6
7
8
9
10
11
|
sudo
vim
/
etc
/
default
/
grub
#找到其中一项为 GRUB_CMDLINE_LINUX_DEFAULT= ,不论后面的引号内包含任何内容,在原本内容之后添加 default_hugepagesz=1GB hugepagesz=1G hugepages=8(这里分配了8个1G的hugepages)
sudo
update
-
grub
#之后需要重启一下
reboot
#查看分配情况
grep
Huge
/
proc
/
meminfo
#分配成功后进行挂载
mkdir
-
p
/
dev
/
hugepages
mount
-
t
hugetlbfs
none
/
dev
/
hugepages
#之后每次重启后都要再执行后两句
|
1
2
3
4
5
6
7
|
#安装依赖
sudo
apt
-
get
install
-
y
libpcap
-
dev
lua5
.
3
wget
http
:
//www.dpdk.org/browse/apps/pktgen-dpdk/snapshot/pktgen-3.4.2.tar.gz
tar
-
xvf
pktgen
-
3.4.2.tar.gz
cd
pktgen
-
3.4.2
make
-
j8
ln
-
s
$
(
pwd
)
/
app
/
$
RTE_TARGET
/
pktgen
/
usr
/
bin
/
pktgen
|
国内连接Docker官方仓库非常不稳定,经常404无法获取镜像。推荐使用阿里云提供的Docker镜像仓库。在其中的管理中心——Docker Hub镜像站点中有完整的Docker安装和加速器配置过程。以下从其中复制出的简单安装过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# step 1: 安装必要的一些系统工具
sudo
apt
-
get
update
sudo
apt
-
get
-
y
install
apt
-
transport
-
https
ca
-
certificates
curl
software
-
properties
-
common
# step 2: 安装GPG证书
curl
-
fsSL
http
:
//mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
# Step 3: 写入软件源信息
sudo
add
-
apt
-
repository
"deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"
# Step 4: 更新并安装 Docker-CE
sudo
apt
-
get
-
y
update
sudo
apt
-
get
-
y
install
docker
-
ce
# 阿里云加速器配置
# 这里的镜像地址可以在注册阿里云后获得自己的
sudo
mkdir
-
p
/
etc
/
docker
sudo
tee
/
etc
/
docker
/
daemon
.
json
<<
-
'EOF&'
{
"registry-mirrors"
:
[
"https://ksct1u2m.mirror.aliyuncs.com"
]
}
EOF
service
docker
restart
|
安装完成后可以简单测试。
1
2
|
sudo
docker
pull
busybox
sudo
docker
run
-
ti
--
rm
busybox
|
正确安装的话,可以进入一个最简单的容器环境中,Ctrl+D可以退出。
预建立的简单拓扑如下
1
2
3
4
5
6
7
|
+
--
--
--
--
+
--
--
--
--
--
--
--
-
+
+
--
--
--
--
--
--
--
--
--
-
+
--
--
--
--
--
--
--
-
+
|
|
socket
file
1
|
<
--
--
--
--
--
--
->
|
vhost
-
user
port
1
|
|
|
+
--
--
--
--
--
--
--
-
+
+
--
--
--
--
--
--
--
--
--
-
+
|
|
host
|
pktgen
|
|
testpmd
|
container
|
|
+
--
--
--
--
--
--
--
-
+
+
--
--
--
--
--
--
--
--
--
-
+
|
|
|
socket
file
0
|
<
--
--
--
--
--
--
->
|
vhost
-
user
port
0
|
|
+
--
--
--
--
+
--
--
--
--
--
--
--
-
+
+
--
--
--
--
--
--
--
--
--
-
+
--
--
--
--
--
--
--
-
+
|
testpmd是DPDK提供的一个通过数据包转发进行DPDK测试的工具,也是一个非常好的DPDK应用开发示例。如果想进行简单的DPDK开发实验推荐修改$RTE_SDK/app/test-pmd/iofwd.c
后编译testpmd应用,之后在同一目录$RTE_SDK/app/test-pmd/
下可以找到下编译后的testpmd应用。
pktgen运行在主机中,testpmd运行在容器中,两个应用之间通过建立的socket文件以及vhost-user进行通信,相当于虚拟网卡。
通过Dockerfile创建一个DPDK的Docker镜像。
首先创建一个Dockerfile。
1
2
3
4
5
6
|
cat
<<
EOT
>>
$
RTE_SDK
/
.
.
/
Dockerfile
FROM
ubuntu
:
latest
WORKDIR
/
root
/
dpdk
COPY
dpdk
-
stable
-
17.05.2
/
root
/
dpdk
/
.
ENV
PATH
"$PATH:/root/dpdk/$RTE_TARGET/app/"
EOT
|
使用创建的Dockerfile建立Docker镜像.
1
2
3
|
cd
$
RTE_SDK
/
.
.
#-t为镜像名,可以以name:tag的格式来写,不加tag默认tag为latest
sudo
docker
build
-
t
dpdk
.
|
运行容器
这里运行一个使用刚刚建立的dpdk镜像的容器。
1
2
3
4
5
6
7
8
9
|
mkdir
-
p
/
tmp
/
virtio
# -ti 提供一个PTY交互界面并保持STDIN持续可用
# -v /dev/... 将主机的hugepages通过数据卷挂载到容器内,从而可以与主机共享hugepages
# -v /tmp/... 挂载一个数据卷到容器中用于共享virtio_user的sockets
# --privilege 不启用特权可能导致无法共享hugepages和sockets
sudo
docker
run
-
ti
--
rm
--
name
=
test
\
-
v
/
dev
/
hugepages
:
/
dev
/
hugepages
\
-
v
/
tmp
/
virtio
/
:
/
tmp
/
virtio
/
\
--
privileged
dpdk
|
在容器中运行testpmd
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# 以下命令在容器中运行
# DPDK的参数
# -l 列出所使用的CPU核
# -n 给出memory channels的数量
# --socket-mem 限制用于每个socket的内存量
# --vdev 创建vhost的socket文件用于进行网络连接,可以理解为虚拟网卡
# --file-prefix hugepages字首,用于区分所使用的hugepage
# testpmd的参数
# --forward-mode=io 对两个端口情况从一个端口进来的包直接从另一个端口发出
testpmd
-
l
0
-
1
-
n
1
--
socket
-
mem
1024
,
1024
\
--
vdev
'eth_vhost0,iface=/tmp/virtio/sock0'
--
vdev
'eth_vhost1,iface=/tmp/virtio/sock1'
\
--
file
-
prefix
=
test
--
no
-
pci
\
--
-
i
--
forward
-
mode
=
io
--
auto
-
start
|
在主机上运行pktgen
注意,pktgen一定要在与pktgen.lua文件同一目录下运行,即pktgen的文件夹中运行。
1
2
3
4
5
6
7
8
|
# 以下命令在主机中运行
# DPDK的参数同上,--vdev在这里用来连接到socket文件
# pktgen的参数
# -P 在所有端口启用混杂模式
# -m 定义CPU核到端口的绑定,在这里core5绑定到prot0,core6绑定到port1
pktgen
-
l
2
-
6
-
n
3
--
socket
-
mem
1024
,
1024
\
--
vdev
=
'virtio_user0,path=/tmp/virtio/sock0'
--
vdev
=
'virtio_user1,path=/tmp/virtio/sock1'
\
--
-
P
-
m
"5.0,6.1"
|
在pktgen中设置速率为10%,更具体的速率设置可以通过tx_cycles设置。端口0共发送100个包,端口1发送200个。
1
2
3
4
5
6
|
# 在pktgen中设置速率为10%,更具体的速率设置可以通过tx_cycles设置
# 端口0共发送100个包,端口1发送200个
Pktgen
:
/
>
set
all
rate
10
Pktgen
:
/
>
set
0
count
100
Pktgen
:
/
>
set
1
count
200
Pktgen
:
/
>
str
|
在testpmd中查看端口状态,可以看到端口0接收100个包发出200个,与预期一致:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
testpmd
&
gt
;
show
port
stats
all
######################## NIC statistics for port 0 ########################
RX
-
packets
:
100
RX
-
missed
:
0
RX
-
bytes
:
6000
RX
-
errors
:
0
RX
-
nombuf
:
0
TX
-
packets
:
200
TX
-
errors
:
0
TX
-
bytes
:
12000
Throughput
(
since
last
show
)
Rx
-
pps
:
10
Tx
-
pps
:
21
############################################################################
######################## NIC statistics for port 1 ########################
RX
-
packets
:
200
RX
-
missed
:
0
RX
-
bytes
:
12000
RX
-
errors
:
0
RX
-
nombuf
:
0
TX
-
packets
:
100
TX
-
errors
:
0
TX
-
bytes
:
6000
Throughput
(
since
last
show
)
Rx
-
pps
:
21
Tx
-
pps
:
10
############################################################################
|
因为这段代码非常简单,这里只介绍几个关键点帮助快速理解。由于DPDK库非常庞大复杂,阅读源码时推荐通过ctags、YCM等类似工具帮助阅读。
RTE_TEST_PMD_RECORD_CORE_CYCLES
相关的内容用于统计每条转发流处理消耗的时钟周期,
rte_rdtsc()
函数用于通过RDTSC或VMWARE TSC map读取时钟周期信息。与
RTE_TEST_PMD_RECORD_BURST_STATS
相关的内容用于统计每条转发流接收和发送时burst(处理时的一组数据包)的情况。这里的接收端口和发送端口等参数已经在
config.c
文件中设置过,更详细的过程可以看到其中的
simple_fwd_config_setup()
函数以及其他模式下的相关设置函数。
rte_eth_rx_burst()
函数用于从指定端口和队列中读取数据包。会一次读入很多个数据包(取决于端口排队情况),称为burst。
rte_eth_rx_burst()
这个API在同步和异步包处理环境下都针对burst做了非常多的优化,性能非常高。但是,这个函数为了降低额外开销不提供任何错误提示。所以当上层应用多次尝试都接收到0返回值时应该检查设备运行状态。
rte_eth_tx_burst()
函数用于从指定端口和队列处发送数据包。同接收一样,会一次把一个burst的数据放入到发送队列。同时,
rte_eth_tx_burst()
这个函数会透明地释放掉先前发送的数据包占用的内存。
likely()
和
unlikely()
函数是常用的优化方式,是GCC內建函数
__builtin_expect()
的宏。通过提高条件分支预测准确性提高cache命中率从而提升程序性能。由于现代CPU都通过预读机制提升性能,而由于分支造成的转跳会导致pipeline flushing,这会降低程序性能。通过为编译器提供分支预测信息可以让编译结果中可能性更大的代码紧跟分支判断部分减少转跳。从理解代码逻辑的角度上这两个函数可以完全忽略。更详细的内容可以参考这篇 官方解释代码的最后定义的
io_fwd_engine
相当于注册了一种新的转发引擎,可以在
testpmd.h
文件中找到结构体定义,在
testpmd.c
中看到对转发引擎的选择和使用。在理解了iofwd中的关键部分之后,就可以在其基础上参考DPDK的 API文档进行基础性的实验。当然这只是一个最简单的进行DPDK实验的方式。一个更加全面、有代表性的DPDK示例应用可以参考examples目录下的l3fwd。