简介:这套面向 P4 可编程数据平面的离线安装包,专为需要快速搭建 P4 开发环境的初学者与研究人员准备,能有效解决各组件版本匹配繁琐、在线获取不稳定等常见问题。包体内整合了 behavioral-model(即 BMv2 软件交换机)、P4 编译器 p4c,以及 protobuf-3.2.0、thrift-0.9.2、gmock-1.7.0 等关键依赖,其中 behavioral-model 负责构建可编程软件交换机,p4c 完成 P4 程序的编译转换,protobuf 与 thrift 用于支撑南北向接口通信,gmock 为相关单元测试提供辅助库,整体相辅相成,覆盖开发、调试、验证等主要环节。整套资源以 gz 压缩格式打包发布,体积约 147.8MB,适合在无外网或网络受限的环境里离线部署与实验复现,可配合配套博文教程快速完成环境验证。目前已有 608 人浏览学习,说明该组件组合对 P4 入门环境搭建具有较高的参考价值,尤其适合高校网络相关专业学生、SDN 方向的初学者以及企业技术预研人员。依靠这套经过验证的安装包,读者可以减少因版本冲突或依赖缺失造成的反复尝试,把更多时间用于理解 P4 语法、数据平面逻辑和实验设计本身,快速进入实际编程环节。 如果你照着p4lang官方仓库的README去配P4开发环境,大概率会在protobuf版本上折腾到半夜,这几乎是每个刚接触P4的人都会撞上的墙。behavioral-model、p4c、protobuf-3.2.0、thrift-0.9.2、gmock-1.7.0,这套组合是P4软件交换机生态里一套经过验证的依赖快照,能真正把P4编译器和模拟交换机串起来跑通。这篇就把我这次从零构建整个环境、并用一段最小P4流水线验证全链路的过程完整记录下来,包括每一步为什么这么做、踩过的坑和解决办法。适合正在搭P4开发环境、做可编程数据平面实验、搞SDN课程项目,或者准备做P4Runtime控制器开发但卡在环境上的人。
1. 先弄明白这套版本组合到底在解决什么问题
1.1 四个组件在P4工具链里的分工
很多人把p4c和behavioral-model当成一个整体来装,却搞不清它们各自负责什么,结果就是环境出问题时完全没头绪。实际上P4开发环境是一条完整流水线,每个组件都有自己的角色。
p4c是P4语言编译器,负责把P4-16代码编译成特定target能加载的配置产物。对于bmv2来说,产物就是simple_switch运行时需要加载的JSON文件,同时还会生成p4info文件,告诉控制面这个数据平面有哪些表、哪些动作、哪些字段可以操作。
behavioral-model就是常说的bmv2,是P4语言的参考软件交换机实现。它提供simple_switch、simple_switch_grpc等目标,加载p4c生成的JSON文件后,就能像一台真实交换机一样处理数据包。数据平面跑在用户态,效率不高,但行为逻辑是参照真实硬件交换芯片建模的,用来验证P4程序逻辑完全够用。
thrift和protobuf是底层支撑。simple_switch的CLI控制通道走thrift,simple_switch_CLI就是通过thrift接口连接交换机下发流表的。protobuf则承担了P4Runtime这条线的序列化工作,simple_switch_grpc、PI库、p4c里的部分生成代码都依赖它。gmock-1.7.0是Google的C++测试框架,p4c和bmv2的回归测试都建立在它之上。
这四个组件外加测试框架,合起来就是你本地跑P4实验的最小闭环:p4c编译代码,simple_switch加载运行,CLI下发流表,veth网卡发包验证。
1.2 为什么必须锁死protobuf-3.2.0和thrift-0.9.2
这是整套环境里最容易被低估的决定。protobuf的C++ API在3.x中期和后期有过明显调整,比如google::protobuf::Message的部分接口、反射机制、gRPC配套代码的生成方式都有变化。早期bmv2的PI库和p4c中对protobuf的调用,是按protobuf 3.2这个时代的接口写的。你要是用Ubuntu 22.04自带的protobuf 3.21去编译,会看到成片的类型不匹配、成员函数不存在之类的报错。这些错误不是改两个函数签名就能糊弄过去的,背后是整个序列化框架的兼容性断裂。
thrift也是同样的逻辑。simple_switch的CLI接口在0.9.2时代成型,后续thrift版本改动过transport层和server端的部分实现,如果强行用新版本,编译时可能出现接口对不上,运行时则可能直接连不上CLI。锁死版本,本质上就是锁死一套“已知能编译、已知能运行”的接口矩阵。
我自己一般会把这种版本关系理解成:P4工具链里的依赖是牵一发动全身的,protobuf这类序列化库一旦成为基础设施,升级就必须整条链路一起升,而不能单独换某个库。移动领域里把protobuf作为通信框架底座的工程,也一直是这个管理思路。
1.3 gmock-1.7.0在这套环境里真正的作用
需要说清楚的是,gmock不是P4编译和运行的必需依赖,它是测试框架。p4c的CMakeLists里有一个ENABLE_GTESTS开关,打开后会去查找gtest和gmock,用来编译并运行P4测试套件。bmv2的测试代码同样依赖它。
对大多数人来说,装gmock的意义在于:第一,以后你自己写P4相关C++模块、想加单元测试时可以直接用;第二,如果你要深入p4c内部,跑它的回归测试确认自己没改坏东西,gmock就是必需品。但如果你只是想用P4写数据平面逻辑,编译p4c时关掉测试开关完全没问题,能省下不少编译时间和磁盘空间。
我在这套环境里是先装好gmock,但编译p4c时先关闭GTESTS,保证主链路能跑通。等后面想跑P4测试套件,再重新开开关编译,这样既不会卡在第一关,又给后续留好了空间。
2. 安装前的核心决策:把整套环境隔离到/opt/p4env
2.1 为什么不能直接装进/usr/local
这个决策直接影响你后面会不会花一整晚排查符号冲突。很多教程会让人直接./configure && make && sudo make install,默认装进/usr/local,但如果系统里之前装过libprotobuf-dev这类包,/usr/lib和/usr/local/include里就会同时存在两套protobuf。编译器的include搜索顺序、动态库加载顺序稍有一点不对,编译出来的程序链接的可能是旧的头文件加新的动态库,这种“头尾不一致”的问题最难查。
Ubuntu 20.04的apt源里protobuf是3.6.1,Ubuntu 22.04是3.21.12,都和我们要的3.2.0差着代际。与其在系统目录里玩“覆盖、回滚”这种心跳游戏,不如直接从安装开始就把整套环境隔离到一个独立目录里。这个思路和Python虚拟环境、Node的node_modules本质上是一样的:让项目自己拥有一套完全可控的依赖副本。
2.2 目录结构与环境变量规划
我的做法是统一使用/opt/p4env作为这套环境的根目录。结构很简单:
/opt/p4env ├── bin ├── include ├── lib └── srcbin放可执行文件,include放头文件,lib放动态库和静态库,src放源码包。所有用源码安装的组件,configure或cmake时都显式指定--prefix=/opt/p4env,这样整个环境自包含,卸载时直接删目录就行,不会污染系统。
环境变量这样配:
export P4ENV=/opt/p4env export PATH=$P4ENV/bin:$PATH export LD_LIBRARY_PATH=$P4ENV/lib:$LD_LIBRARY_PATH export PKG_CONFIG_PATH=$P4ENV/lib/pkgconfig:$PKG_CONFIG_PATH export CMAKE_PREFIX_PATH=$P4ENV把这五行写进~/.bashrc并source。看得出来,我同时设置了pkg-config和cmake的搜索路径,因为老派的autotools工程习惯用pkg-config找protobuf,而p4c这类cmake工程会走find_package机制。两条路都指到/opt/p4env,能少踩很多“明明装了却找不到”的坑。
2.3 系统基础依赖准备
基础依赖用系统包管理器装,这部分不需要源码编译。以Ubuntu/Debian为例:
sudo apt update sudo apt install -y automake autoconf libtool build-essential \ cmake python3 python3-pip git pkg-config libssl-dev \ libgmp-dev libpcap-dev libevent-dev \ libboost-dev libboost-system-dev libboost-filesystem-devboost和gmp是bmv2编译时会用到的,libpcap是抓包和发送原始数据包时需要的,libevent是thrift和bmv2的事件处理依赖。这些提前装好,后面configure时才不会突然报Boost not found之类的错误。还有一点,simple_switch运行时要开raw socket绑定veth网卡,所以后面测试时sudo是少不了的。
3. 三个依赖库的源码构建:protobuf、thrift、gmock的顺序坑
3.1 protobuf-3.2.0:autotools路径比cmake路径更省事
protobuf官方从3.x早期开始就同时提供autotools和cmake两种构建方式。对3.2.0这个版本,我建议走autotools路径,因为它的cmake配置在那个年代还比较简陋,依赖项的处理不如configure脚本完整。
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.2.0/protobuf-cpp-3.2.0.tar.gz tar -xzf protobuf-cpp-3.2.0.tar.gz cd protobuf-3.2.0 ./configure --prefix=/opt/p4env make -j2 sudo make install这里我特意用-j2而不是-j$(nproc),在编译老版本C++库时是个实用习惯。老工程的Makefile在并行编译时偶尔会有中间产物竞争,高并行度反而容易出偶发编译错误,而且8核机器上全核编译,内存占用也会突然飙高,2并行虽然慢点,但胜在稳。
编译完成后检查一下动态库:
ls /opt/p4env/lib/libprotobuf.so*应该能看到libprotobuf.so.12这样的版本化文件。如果configure阶段报autotools版本相关的错误,先跑一次autoreconf -i重新生成configure脚本再试,这个操作能解决大部分老源码配新系统的问题。
3.2 thrift-0.9.2:裁剪语言绑定,避免构建噩梦
thrift最折磨人的点在于它默认会为十几种语言生成绑定代码,包括Java、Node.js、Perl、Ruby、Haskell等等。这些语言绑定各自依赖不同的构建工具,比如Java要ant、Node要npm,构建机里缺任何一个都可能让整个编译失败。实际上我们只需要C++运行时。
wget https://archive.apache.org/dist/thrift/0.9.2/thrift-0.9.2.tar.gz tar -xzf thrift-0.9.2.tar.gz cd thrift-0.9.2 ./configure --prefix=/opt/p4env \ --without-csharp --without-java --without-erlang --without-nodejs \ --without-lua --without-perl --without-php --without-ruby \ --without-haskell --without-go --without-d make -j2 sudo make install这串--without-*参数就是告诉configure“这些语言的绑定我一个都不要”,只保留C++库。实测下来,裁剪后的编译时间能少一半以上。
还有一个很容易踩的坑:新版gcc编译thrift 0.9.2这种老代码时,可能会报nullptr was not declared in this scope或者uint32_t was not declared。前者是因为代码按C++98的写法但编译器标准模式不对,后者是缺少合适的头文件包含。解决办法很粗暴,configure之前加一行:
export CXXFLAGS="-std=c++11 -include cstdint"实测这行环境变量能解决大部分thrift 0.9.2在新gcc下的编译问题。
3.3 gmock-1.7.0:一把梭的构建方式
gmock从1.7.0开始被整合进googletest仓库统一发布,所以下载release-1.7.0的包就行,里面同时包含gtest和gmock的源码。
wget https://github.com/google/googletest/archive/release-1.7.0.tar.gz tar -xzf release-1.7.0.tar.gz cd googletest-release-1.7.0 ./configure --prefix=/opt/p4env make -j2 sudo make install安装完检查:
ls /opt/p4env/lib/libgtest* /opt/p4env/lib/libgmock*能看到libgtest.a、libgtest_main.a、libgmock.a这些库就算成功了。这个版本用autotools走得很顺,不需要额外配置。因为它是静态库,也不涉及动态库加载路径的问题,装好之后就可以安静躺着,等p4c的测试开关用到它时再上场。
4. 编译p4c与behavioral-model:主程序入场与三项验证
4.1 p4c的cmake参数与内存控制
p4c是这套环境里编译时间最长的组件,依赖也多,clone时必须带子模块:
git clone --recursive https://github.com/p4lang/p4c.git cd p4c mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=RELEASE \ -DCMAKE_INSTALL_PREFIX=/opt/p4env \ -DENABLE_GTESTS=OFF make -j2 sudo make install--recursive必须加,p4c的仓库拉下来需要同步它的多个子模块,少了它cmake配置阶段就会报缺文件。-DENABLE_GTESTS=OFF前面提到过,测试框架我们装好了,但编译p4c时先不启用,因为启用后cmake会尝试下载额外的googletest源码,网络不好时能卡半天。
p4c的链接阶段很吃内存,大并行编译容易把内存打满。我的建议依然是make -j2。首次编译p4c需要一段时间,这个耐心点,属于正常耗时。
4.2 behavioral-model的configure选项取舍
bmv2的源码目录名是behavioral-model,clone后需要先跑autogen.sh生成configure脚本,这个步骤不能跳,很多人直接执行configure会报找不到Makefile.in之类的错。
git clone https://github.com/p4lang/behavioral-model.git cd behavioral-model ./autogen.sh ./configure --prefix=/opt/p4env make -j2 sudo make installconfigure这步有几个选项得说清楚。默认不带任何选项编出来的就是基础simple_switch加thrift CLI,这套对绝大多数本地实验够用了。如果你以后想接P4Runtime控制器,configure时就要加--with-pi,但代价是你必须先装好和protobuf 3.2匹配的gRPC版本,又是一套独立的版本矩阵,建议有明确需求时再碰。想用nanomsg做pub/sub消息接口的话,先装libnanomsg-dev,configure时加--with-nanomsg,这个可选功能对常规实验影响不大。
4.3 构建完成后的三连检查
两个主程序都装好后,先做最基础的三项检查:
which p4c p4c --version simple_switch --version三行都能输出正确路径和版本就说明PATH没问题。接下来查动态库链接情况,这一步非常关键:
ldd /opt/p4env/bin/simple_switch | grep -E 'protobuf|thrift'正常应该看到两行路径或地址指向/opt/p4env/lib下的libprotobuf.so和libthrift.so。如果发现指向的是/usr/lib/x86_64-linux-gnu下的libprotobuf,说明LD_LIBRARY_PATH设置顺序有问题,或者系统里有旧版本protobuf残留正在抢占。这个检查能让你在跑任何P4程序之前就发现大部分版本冲突,别等编译时才后悔。
5. 用最小P4流水线验证整套环境是否真的能用
5.1 最小可编译的P4_16示例程序
环境装好只是第一步,真正验证环境能不能跑,要写一段最小P4流水线。下面这段P4_16代码做的事情很简单:解析以太网和IPv4头,查IPv4目的地址,命中就把包扔到指定端口,没命中就丢包。保存为basic.p4。
#include <core.p4> #include <v1model.p4> const bit<16> TYPE_IPV4 = 0x0800; header ethernet_t { macAddr_t dstAddr; macAddr_t srcAddr; bit<16> etherType; } header ipv4_t { bit<4> version; bit<4> ihl; bit<8> diffserv; bit<16> totalLen; bit<16> identification; bit<3> flags; bit<13> fragOffset; bit<8> ttl; bit<8> protocol; bit<16> hdrChecksum; ipv4Addr_t srcAddr; ipv4Addr_t dstAddr; } struct headers { ethernet_t ethernet; ipv4_t ipv4; } struct metadata { /* 本示例不使用元数据字段 */ } parser MyParser(packet_in b, out headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) { state start { b.extract(hdr.ethernet); if (hdr.ethernet.etherType == TYPE_IPV4) { b.extract(hdr.ipv4); } transition accept; } } action drop_action() { mark_to_drop(standard_metadata); } action ipv4_forward(egressSpec_t port) { standard_metadata.egress_spec = port; } table ipv4_lpm { key = { hdr.ipv4.dstAddr: lpm; } actions = { ipv4_forward; drop_action; } default_action = drop_action(); size = 1024; } control MyIngress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) { apply { if (hdr.ipv4.isValid()) { ipv4_lpm.apply(); } } } control MyEgress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) { apply { } } control MyDeparser(packet_out b, in headers hdr) { apply { b.emit(hdr.ethernet); b.emit(hdr.ipv4); } } control MyVerifyChecksum(inout headers hdr, inout metadata meta) { apply { } } control MyComputeChecksum(inout headers hdr, inout metadata meta) { apply { } } V1Switch( MyParser(), MyVerifyChecksum(), MyIngress(), MyEgress(), MyComputeChecksum(), MyDeparser() ) main;这里macAddr_t、ipv4Addr_t、egressSpec_t都是v1model.p4标准库定义好的类型,直接用即可。lpm匹配用小前缀能做精确匹配,也能做网段匹配,是实际转发场景里最常用的匹配类型。
5.2 编译并启动simple_switch
先创建两对veth虚拟网卡,模拟交换机的两个端口:
sudo ip link add veth0 type veth peer name veth1 sudo ip link add veth2 type veth peer name veth3 sudo ip link set veth0 up sudo ip link set veth1 up sudo ip link set veth2 up sudo ip link set veth3 up用p4c编译P4程序,指定bmv2目标和v1model架构:
p4c --target bmv2 --arch v1model --std p4-16 basic.p4 -o build如果编译成功,build目录下会生成basic.json和basic.p4info.txt。可以确认一下:
ls -l build/basic.json build/basic.p4info.txt然后启动simple_switch,把端口0绑定veth0,端口1绑定veth2:
sudo simple_switch -i 0@veth0 -i 1@veth2 --log-console build/basic.json-i 0@veth0的含义是数据面端口0与veth0绑定。外部数据包从veth1发出时,会被当成从端口0进入交换机;交换机从端口1转发出去的包会走veth2,对端veth3可以抓到。这样一对veth就模拟了一条物理链路。
5.3 表项下发与抓包验证
新开一个终端,用simple_switch_CLI连接交换机。默认thrift端口是9090:
simple_switch_CLI进入CLI后下发一条流表:目的IP是10.0.1.10的IPv4包,执行ipv4_forward动作,从端口1出去。
table_add ipv4_lpm ipv4_forward 10.0.1.10/32 => 1 table_print ipv4_lpm接下来用scapy验证。装好scapy后,在另一个终端发一个以太网帧,目的MAC随便填一个非空地址,源IP设为10.0.1.1,目的IP设为10.0.1.10,从veth1发出去:
from scapy.all import Ether, IP, sendp pkt = Ether(src='00:00:00:00:00:01', dst='00:00:00:00:00:02') \ / IP(src='10.0.1.1', dst='10.0.1.10') / b'hello p4' sendp(pkt, iface='veth1')然后在veth3上抓包确认:
from scapy.all import sniff packets = sniff(iface='veth3', count=1, timeout=5) packets.summary()能抓到从交换机转发出来的包,说明p4c编译正确、simple_switch加载无误、表项下发成功、数据面转发逻辑正常,整套环境跑通了。抓不到的话,优先检查表项前缀是否匹配、动作端口是否写成了0,以及veth每对的正反端是否搞混。
6. 高频报错自查清单:从动态库冲突到编译期报错
| 报错特征 | 根因 | 解决办法 |
|---|---|---|
error while loading shared libraries: libprotobuf.so.12: cannot open shared object file | LD_LIBRARY_PATH未设置或顺序不对 | export LD_LIBRARY_PATH=/opt/p4env/lib:$LD_LIBRARY_PATH,必要时sudo ldconfig |
configure: error: C++ preprocessor "/lib/cpp" fails sanity check | 系统缺g++编译器 | sudo apt install build-essential |
编译P4程序时报arch not specified | p4c命令缺少目标架构参数 | 加--arch v1model |
p4c编译期大量google::protobuf::Message相关类型不匹配 | 编译时找到的是系统新版protobuf头文件 | 确认LD_LIBRARY_PATH和CPATH没被污染,用pkg-config --modversion protobuf验证版本 |
thrift 0.9.2编译报error: 'nullptr' was not declared | gcc标准模式与老代码不兼容 | configure前export CXXFLAGS="-std=c++11 -include cstdint" |
| cmake配置p4c时找不到protobuf | CMAKE_PREFIX_PATH没指到/opt/p4env | export CMAKE_PREFIX_PATH=/opt/p4env,或cmake时加-DCMAKE_PREFIX_PATH |
这里重点展开三种出现频率最高的场景。
动态库加载失败是出现率最高的错误,它的本质是程序运行时按LD_LIBRARY_PATH的搜索顺序找不到对应的.so文件。报错信息里会有完整的so文件名,比如libprotobuf.so.12,你可以用find /opt/p4env/lib -name "libprotobuf.so*"先确认文件确实存在,再检查环境变量是否生效。
protobuf多版本并存的问题是另一大杀器。很多人的机器上曾经通过apt或pip装过protobuf,导致/usr/lib、/usr/local/lib、/opt/p4env/lib里各有一份。这时ldd命令会给你最真实的答案,专治这种隐性问题。编译p4c之前,先在命令行执行pkg-config --modversion protobuf,如果输出的不是3.2开头的版本,就回到环境变量部分检查PKG_CONFIG_PATH。
cmake找不到依赖的问题,解法相对固定。p4c的CMakeLists依赖find_package(Protobuf)和find_package(GTest),这两个模块在查找时会参考CMAKE_PREFIX_PATH。只要环境变量里配好了CMAKE_PREFIX_PATH=/opt/p4env,同时让protobuf的cmake配置文件和pkg-config文件都位于$P4ENV/lib/cmake/protobuf和$P4ENV/lib/pkgconfig下,一般都能顺利通过。
最后分享一个我自己的习惯。整套环境跑通之后,我会把从下载源码到安装的完整过程写成一个build.sh,源码包统一放/opt/p4env/src,版本号写死,换机器时直接执行一遍就能复现。以后如果p4c和bmv2要升级,我会新建一个/opt/p4env-next目录,完整升级验证通过后再切换,而不是在原环境里单点升级某个库。P4Runtime那条线还牵涉gRPC和PI库,那又是另一套版本矩阵,建议单独再开一个环境目录,和这套基础环境互不干扰。这样环境干净,出问题也好排查。
本文还有配套的精品资源,点击获取