简介:这份资源面向SDN与可编程数据平面方向的学习者与研究者,聚焦P4开发环境的完整搭建,解决behavioral-model、p4c、protobuf、thrift等组件版本匹配繁琐、编译依赖易出错的问题。包内整合了behavioral-model、gmock-1.7.0、p4c、protobuf-3.2.0与thrift-0.9.2等核心源码包,覆盖P4编译器、软件交换机、单元测试框架及RPC通信库,压缩包约147.8MB,以gz格式分发,便于在Linux环境下按需解压编译。目前已有609人学习下载,说明该组合在P4入门与实验场景中具有一定参考价值。对于需要复现P4教程、搭建bmv2软件交换机或调试p4c编译流程的读者,可借助这份配套安装包减少逐个查找与版本适配的时间,配合配置教程快速完成环境部署,为后续编写P4程序、验证数据平面行为打下基础。
1. P4 环境配置:为什么 2024 年还要手动编译这套老版本组合
如果你最近在搜 behavioral-model、p4c、protobuf-3.2.0、thrift-0.9.2 这几个词,大概率是拿到了一个 P4 相关的实验环境搭建任务,或者想复现某篇论文里的 P4 数据平面实验。P4 本身是 SDN 领域里做可编程数据平面的核心工具,而 behavioral-model 就是那个常被叫做 bmv2 的软件交换机,p4c 是 P4 编译器,protobuf 和 thrift 则是它们依赖的序列化与 RPC 框架。问题在于,这套组合的版本非常老,protobuf 3.2.0 和 thrift 0.9.2 放在今天的环境里,跟系统自带的库、Python 版本、编译器版本几乎处处冲突。更麻烦的是,p4c 和 bmv2 的构建脚本对这两个依赖的版本卡得很死,你直接装最新版 protobuf 3.20 或者 thrift 0.13,编译到一半就会报符号找不到或者 API 不匹配。所以这篇笔记不讲 P4 语言本身,只讲怎么把这套老版本依赖在本地或者容器里跑通,让你能顺利编译出 bmv2 的simple_switch和 p4c 的p4c-bm2-ss。适合谁看?适合手里有 P4 实验任务、需要自己搭环境、又不想在依赖地狱里耗一整天的工程师。接下来我会按依赖安装顺序、编译参数、常见报错排查、以及一个能验证环境是否可用的最小 P4 程序来展开。
2. 依赖安装顺序与版本锁定:gmock、protobuf、thrift 谁先谁后
2.1 为什么必须从 gmock-1.7.0 开始装
gmock 是 Google Mock 的缩写,bmv2 的单元测试和部分构建脚本会链接它。gmock-1.7.0 这个版本本身不依赖 protobuf,但它会安装libgmock.a和对应的头文件。如果你先装了高版本 gmock,后面编译 bmv2 时可能因为 C++ 标准差异报错。常见做法是源码编译安装到/usr/local,并且不要覆盖系统包管理器里的版本。下面是我在 Ubuntu 18.04 和 20.04 上都验证过的步骤,注意-DBUILD_SHARED_LIBS=OFF是为了生成静态库,避免运行时找不到.so。
# 下载并解压 gmock-1.7.0 wget https://github.com/google/googletest/archive/release-1.7.0.tar.gz -O gmock-1.7.0.tar.gz tar -xzf gmock-1.7.0.tar.gz cd googletest-release-1.7.0 # 创建构建目录,关闭共享库,指定安装前缀 mkdir build && cd build cmake -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX=/usr/local .. make -j$(nproc) sudo make install # 安装后检查头文件和库是否存在 ls /usr/local/include/gmock/gmock.h ls /usr/local/lib/libgmock.a逻辑说明:gmock-1.7.0 的 CMake 脚本默认会同时编译 gtest 和 gmock,BUILD_SHARED_LIBS=OFF确保生成静态库,因为 bmv2 的链接脚本里写的是-lgmock,如果只有动态库而LD_LIBRARY_PATH没配好,运行时会直接报error while loading shared libraries。参数CMAKE_INSTALL_PREFIX=/usr/local是默认值,但显式写出来避免被环境变量干扰。安装完一定要确认libgmock.a存在,如果只有libgmock.so,后面 bmv2 编译会报cannot find -lgmock。
2.2 protobuf-3.2.0 的编译参数与 Python 绑定处理
protobuf 3.2.0 是这套环境里最容易被系统包管理器污染的一环。Ubuntu 20.04 默认源里的 protobuf 是 3.6 以上,直接apt install libprotobuf-dev会导致 p4c 编译时找不到google::protobuf::internal::LogMessage的旧接口。所以必须源码编译,并且安装到独立前缀,比如/usr/local/protobuf-3.2.0,然后在编译 p4c 和 bmv2 时通过PKG_CONFIG_PATH和LD_LIBRARY_PATH指向它。下面命令里的--disable-shared是为了生成静态库,减少运行时库路径问题。
# 下载 protobuf-3.2.0 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=/usr/local/protobuf-3.2.0 --disable-shared CXXFLAGS="-O2" make -j$(nproc) sudo make install # 配置动态链接库搜索路径 echo "/usr/local/protobuf-3.2.0/lib" | sudo tee /etc/ld.so.conf.d/protobuf-3.2.0.conf sudo ldconfig # 验证 protoc 版本 /usr/local/protobuf-3.2.0/bin/protoc --version # 应输出 libprotoc 3.2.0逻辑说明:--disable-shared生成libprotobuf.a,这样 p4c 和 bmv2 链接时不会跟系统里的libprotobuf.so冲突。ld.so.conf.d里加路径是为了让运行时能找到库,但如果你后面编译时显式指定了LDFLAGS,这一步可以跳过。注意 protobuf 3.2.0 的 Python 绑定默认会装到系统 Python 的 site-packages,如果你用 conda 或者 pyenv,需要手动设置PYTHON环境变量指向正确的解释器,否则make install会报Python.h not found。我一般会加--disable-python先跳过 Python 绑定,因为 bmv2 和 p4c 的 C++ 部分不依赖 Python 版 protobuf,只有p4c-bm2-ss的某些脚本会用到,后面单独用 pip 装对应版本即可。
2.3 thrift-0.9.2 的依赖与编译避坑
thrift 0.9.2 是 bmv2 的 RPC 框架依赖,它本身又依赖 boost 和 openssl。在 Ubuntu 20.04 上,系统自带的 boost 版本是 1.71,thrift 0.9.2 的 configure 脚本能识别,但编译时可能因为std::auto_ptr被弃用而报错。解决办法是在 configure 时加CXXFLAGS="-std=c++03"或者打补丁,但更稳妥的是用-Wno-error关掉警告即错误。下面命令里--without-php和--without-ruby是为了减少不必要的语言绑定,加快编译。
# 安装 boost 和 openssl 开发包 sudo apt install -y libboost-dev libboost-system-dev libboost-thread-dev libssl-dev # 下载 thrift-0.9.2 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=/usr/local/thrift-0.9.2 --without-php --without-ruby --without-python --disable-shared make -j$(nproc) sudo make install # 验证 thrift 编译器 /usr/local/thrift-0.9.2/bin/thrift --version # 应输出 Thrift version 0.9.2逻辑说明:--disable-shared同样是为了静态链接,避免 bmv2 运行时找不到libthrift.so。--without-python是因为 thrift 0.9.2 的 Python 绑定在 Python 3.8 以上会编译失败,而 bmv2 的 C++ 部分不需要它。如果你后面要用p4c-bm2-ss的 Python 脚本,可以单独用 pip 装thrift==0.9.2,但注意 pip 版本可能跟系统 Python 版本不兼容,建议在虚拟环境里操作。安装完 thrift 后,把/usr/local/thrift-0.9.2/bin加到PATH,否则 bmv2 的 configure 脚本找不到thrift可执行文件。
3. behavioral-model 与 p4c 的编译:从 configure 到生成 simple_switch
3.1 bmv2 的依赖检查与编译参数
behavioral-model 的源码仓库里有一个install_deps.sh,但那个脚本会尝试用 apt 装依赖,容易把系统里的 protobuf 和 thrift 版本搞乱。我一般手动编译,先确保PKG_CONFIG_PATH指向前面装的 protobuf 和 thrift。bmv2 的 configure 脚本会检查libprotobuf、libthrift、libgmock是否存在,如果找不到就报错。下面命令里的--with-pdfixed是可选项,用于启用固定长度的包解析,对性能有好处,但如果你只是做功能验证,可以不加。
# 下载 bmv2 源码,这里假设你已经有源码包,没有的话从官方仓库克隆 git clone https://github.com/p4lang/behavioral-model.git cd behavioral-model # 设置环境变量,让 configure 找到我们手动装的依赖 export PKG_CONFIG_PATH=/usr/local/protobuf-3.2.0/lib/pkgconfig:/usr/local/thrift-0.9.2/lib/pkgconfig export LDFLAGS="-L/usr/local/protobuf-3.2.0/lib -L/usr/local/thrift-0.9.2/lib -L/usr/local/lib" export CXXFLAGS="-I/usr/local/protobuf-3.2.0/include -I/usr/local/thrift-0.9.2/include -I/usr/local/include" # 生成 configure 脚本 ./autogen.sh # 配置,关闭 debug 信息以加快编译 ./configure --prefix=/usr/local/bmv2 --disable-debug # 编译并安装 make -j$(nproc) sudo make install # 验证 simple_switch 是否生成 ls /usr/local/bmv2/bin/simple_switch逻辑说明:PKG_CONFIG_PATH是关键,如果 protobuf 的.pc文件不在这个路径下,configure 会报Package protobuf was not found。LDFLAGS和CXXFLAGS显式指定头文件和库路径,避免跟系统版本混用。--disable-debug会去掉-g编译选项,减少二进制体积,但如果你需要调试 bmv2 内部行为,可以保留 debug。编译过程中如果报undefined reference to google::protobuf::internal::...,说明链接时用了系统 protobuf 而不是我们装的 3.2.0,检查LDFLAGS里的路径顺序,把/usr/local/protobuf-3.2.0/lib放在最前面。
3.2 p4c 的编译与 p4c-bm2-ss 后端生成
p4c 是 P4 编译器,它依赖 bmv2 的头文件来生成后端代码。所以必须先装好 bmv2,再编译 p4c。p4c 的构建系统用 CMake,需要指定BMV2_INCLUDE_DIR和BMV2_LIBRARY。另外 p4c 对 protobuf 的版本更敏感,如果用了 3.2.0 以上的版本,编译p4c-bm2-ss时会报google::protobuf::Any相关错误。下面命令里的-DENABLE_BMV2=ON是开启 bmv2 后端,-DENABLE_EBPF=OFF是关掉 eBPF 后端,减少编译时间。
# 下载 p4c 源码 git clone https://github.com/p4lang/p4c.git cd p4c mkdir build && cd build # 配置 CMake,指定依赖路径 cmake .. \ -DCMAKE_INSTALL_PREFIX=/usr/local/p4c \ -DCMAKE_PREFIX_PATH="/usr/local/protobuf-3.2.0;/usr/local/thrift-0.9.2;/usr/local/bmv2" \ -DBMV2_INCLUDE_DIR=/usr/local/bmv2/include \ -DBMV2_LIBRARY=/usr/local/bmv2/lib/libbmv2.a \ -DENABLE_BMV2=ON \ -DENABLE_EBPF=OFF \ -DENABLE_DPDK=OFF # 编译并安装 make -j$(nproc) sudo make install # 验证 p4c-bm2-ss 是否生成 ls /usr/local/p4c/bin/p4c-bm2-ss逻辑说明:CMAKE_PREFIX_PATH里用分号分隔多个路径,CMake 会按顺序查找。BMV2_LIBRARY指向静态库libbmv2.a,如果 bmv2 编译时生成了动态库,这里要改成.so。ENABLE_EBPF=OFF和ENABLE_DPDK=OFF是为了避免引入额外的内核依赖,如果你确实需要这些后端,再单独开启。编译 p4c 时如果报thrift/protocol/TProtocol.h not found,检查CMAKE_PREFIX_PATH里 thrift 的路径是否正确,或者手动加-DTHRIFT_INCLUDE_DIR=/usr/local/thrift-0.9.2/include。
3.3 环境变量与动态库路径的最终配置
编译安装完成后,需要把 bmv2 和 p4c 的可执行文件路径加到PATH,把库路径加到LD_LIBRARY_PATH。虽然前面用了静态库,但 bmv2 的simple_switch可能还依赖libboost_thread等系统库,所以LD_LIBRARY_PATH里加上/usr/local/lib和 thrift 的库路径更保险。下面命令可以写进~/.bashrc,但注意不要覆盖系统原有的PATH。
# 追加环境变量,不要覆盖 export PATH=$PATH:/usr/local/bmv2/bin:/usr/local/p4c/bin:/usr/local/thrift-0.9.2/bin export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/protobuf-3.2.0/lib:/usr/local/thrift-0.9.2/lib:/usr/local/lib export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/protobuf-3.2.0/lib/pkgconfig:/usr/local/thrift-0.9.2/lib/pkgconfig # 使配置生效 source ~/.bashrc # 验证所有工具是否可用 which simple_switch which p4c-bm2-ss simple_switch --help | head -5 p4c-bm2-ss --help | head -5逻辑说明:PATH追加而不是替换,避免系统命令找不到。LD_LIBRARY_PATH在静态库场景下不是必须的,但如果你后面用到了动态链接的第三方库,加上更稳妥。PKG_CONFIG_PATH是为了后续编译其他 P4 工具时能找到依赖。验证时如果simple_switch --help报error while loading shared libraries: libthrift-0.9.2.so,说明 thrift 编译时生成了动态库,而LD_LIBRARY_PATH没包含它,检查/usr/local/thrift-0.9.2/lib下是否有.so文件,有的话确认路径已加入。
4. 避坑与排查:protobuf 版本冲突、thrift 符号缺失、gmock 链接失败
4.1 现象:编译 bmv2 时报undefined reference to google::protobuf::internal::LogMessage::LogMessage
原因:系统里存在多个 protobuf 版本,链接器优先找到了/usr/lib/x86_64-linux-gnu/libprotobuf.so,而不是我们手动装的 3.2.0 静态库。常见于 Ubuntu 20.04 默认装了libprotobuf-dev。解决:用apt remove libprotobuf-dev卸载系统版本,或者编译时在LDFLAGS里把/usr/local/protobuf-3.2.0/lib放在最前面,并且加-Wl,-rpath,/usr/local/protobuf-3.2.0/lib强制运行时也走这个路径。我一般直接卸载系统 protobuf,避免后续其他工具误链接。
4.2 现象:p4c 编译到 80% 时报thrift/protocol/TProtocol.h: No such file or directory
原因:CMake 没有正确找到 thrift 的头文件路径。虽然CMAKE_PREFIX_PATH里写了 thrift 路径,但 thrift 0.9.2 的 CMake 配置文件可能不完整,导致THRIFT_INCLUDE_DIR为空。解决:在 cmake 命令里显式加-DTHRIFT_INCLUDE_DIR=/usr/local/thrift-0.9.2/include和-DTHRIFT_LIBRARY=/usr/local/thrift-0.9.2/lib/libthrift.a。如果还是找不到,检查/usr/local/thrift-0.9.2/include/thrift目录是否存在,没有的话说明 thrift 安装不完整,重新make install。
4.3 现象:运行simple_switch时报error while loading shared libraries: libgmock.so.1.7.0
原因:gmock 编译时默认生成了动态库,而 bmv2 链接时用了-lgmock,运行时找不到对应的.so。解决:要么在LD_LIBRARY_PATH里加上/usr/local/lib,要么重新编译 gmock 时加-DBUILD_SHARED_LIBS=OFF生成静态库。我推荐后者,因为静态库省去运行时路径配置的麻烦。如果已经装了动态库,可以手动创建软链接:sudo ln -s /usr/local/lib/libgmock.so.1.7.0 /usr/lib/libgmock.so.1,但不建议这么做,容易跟系统库冲突。
4.4 现象:p4c-bm2-ss编译 P4 程序时报Unknown extern type: counter或类似错误
原因:p4c 的 bmv2 后端依赖 bmv2 的json文件来识别 extern 类型,如果 bmv2 安装不完整或者BMV2_INCLUDE_DIR指向了错误路径,p4c 会找不到bmv2/json下的定义。解决:确认/usr/local/bmv2/include/bmvm2目录存在,并且BMV2_INCLUDE_DIR指向/usr/local/bmv2/include而不是/usr/local/bmv2/include/bmvm2。另外,p4c 编译时如果用了-DENABLE_BMV2=ON,但 bmv2 的版本太老,也可能不匹配,建议用同一时间段的 bmv2 和 p4c 源码。
4.5 现象:make -j$(nproc)时内存不足被 OOM Killer 杀掉
原因:p4c 编译时单个 C++ 文件可能占用 2GB 以上内存,如果nproc是 8 或 16,并行编译会瞬间吃光内存。解决:减少并行数,用make -j4或者make -j2。如果还是不够,加 swap 分区:sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile。这个坑在容器里尤其常见,因为容器默认没有 swap。
5. 验证环境可用性的最小 P4 程序与进阶技巧
5.1 写一个能跑通 bmv2 的 P4_16 程序
环境装好了,怎么确认真的能用?最直接的方法是写一个最小的 P4 程序,用p4c-bm2-ss编译成 JSON,再用simple_switch加载。下面这个程序只做一件事:把从ingress端口进来的包从egress端口发出去,不做任何修改。保存为minimal.p4。
#include <core.p4> #include <v1model.p4> header ethernet_t { bit<48> dstAddr; bit<48> srcAddr; bit<16> etherType; } struct metadata { } struct headers { ethernet_t ethernet; } parser MyParser(packet_in packet, out headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) { state start { packet.extract(hdr.ethernet); transition accept; } } control MyIngress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) { apply { // 简单转发:把入端口映射到出端口 standard_metadata.egress_spec = standard_metadata.ingress_port; } } control MyEgress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) { apply { } } control MyDeparser(packet_out packet, in headers hdr) { apply { packet.emit(hdr.ethernet); } } V1Switch(MyParser(), MyIngress(), MyEgress(), MyDeparser()) main;逻辑说明:standard_metadata.egress_spec = standard_metadata.ingress_port这行让包从哪个口进来就从哪个口出去,相当于一个回环。V1Switch是 v1model 的标准入口,四个参数分别是 parser、ingress、egress、deparser。这个程序不依赖任何 extern,所以能验证 p4c 和 bmv2 的基本功能是否正常。
5.2 编译并加载到 simple_switch 的命令
# 编译 P4 程序,生成 JSON p4c-bm2-ss --p4v 16 --p4runtime-file minimal.p4runtime --p4runtime-format text -o minimal.json minimal.p4 # 启动 simple_switch,加载 JSON,监听所有端口 sudo simple_switch -i 0@veth0 -i 1@veth1 minimal.json # 如果不想用 veth,可以用 --use-files 从 pcap 文件读包 sudo simple_switch --use-files 0@input.pcap minimal.json逻辑说明:--p4v 16指定 P4 语言版本为 P4_16,--p4runtime-file生成 P4Runtime 的文本格式,方便后续用控制器下发流表。-i 0@veth0把端口 0 绑定到veth0网卡,需要提前用ip link add veth0 type veth peer name veth1创建。如果只是验证编译,可以不加-i,直接simple_switch minimal.json,它会启动但不绑定任何端口。启动后如果看到Thrift server listening on port 9090,说明 thrift 也正常工作了。
5.3 用 P4Runtime 下发流表的快速验证
如果你需要验证控制平面,可以用simple_switch_grpc而不是simple_switch,它支持 P4Runtime。启动命令类似,但需要额外指定--grpc-server-addr。下面是一个用 Python 脚本下发流表的片段,依赖p4runtime库,注意这个库的版本要跟 p4c 生成的p4runtime文件匹配。
import grpc from p4.v1 import p4runtime_pb2, p4runtime_pb2_grpc # 连接 simple_switch_grpc 的 P4Runtime 端口 channel = grpc.insecure_channel('localhost:50051') stub = p4runtime_pb2_grpc.P4RuntimeStub(channel) # 构造流表项,这里省略了具体的 table entry 构造 # 实际使用时需要根据 P4 程序里的 table 名称和 match 字段来填 req = p4runtime_pb2.WriteRequest() req.device_id = 1 update = req.updates.add() update.type = p4runtime_pb2.Update.INSERT # ... 填充 table entry ... stub.Write(req)逻辑说明:这段代码只是框架,实际下发流表需要根据 P4 程序里的table定义来构造TableEntry。device_id通常设为 1,Update.INSERT表示插入流表项。如果你只是验证环境,可以跳过这一步,直接用simple_switch的 CLI 或者runtime_CLI来下发。runtime_CLI是 bmv2 自带的交互式工具,用simple_switch_CLI --thrift-port 9090连接,然后输入table_add命令即可。
5.4 一个我踩过的坑:protobuf 3.20 的诱惑
网上搜 protobuf 下载安装,很多人会推荐 3.20 版本,说它性能好、API 稳定。但在 P4 这套环境里,protobuf 3.20 跟 p4c 的旧代码完全不兼容,编译时会报google::protobuf::internal::ArenaImpl相关错误。我一开始不信,觉得改改 CMake 就能过,结果折腾了一下午,最后还是退回 3.2.0。血泪经验是:P4 生态的版本锁定非常严格,不要轻易升级任何一个依赖,除非你确认 p4c 和 bmv2 的源码已经适配了新版本。如果你实在需要用 protobuf 3.20 做其他项目,建议用容器隔离,P4 环境单独一个 Docker 镜像,基础镜像用 Ubuntu 18.04,把所有依赖编译进去,这样不会跟宿主机冲突。
5.5 验证通过后的习惯:保存一个可复现的 Dockerfile
每次手动搭完这套环境,我都会把步骤写成一个 Dockerfile,下次直接docker build就能复现。关键点是在 Dockerfile 里用COPY把源码包拷进去,而不是wget,因为那些老版本的下载链接经常失效。另外,RUN指令里每一步都加&&连接,减少镜像层数。最后用ENTRYPOINT启动simple_switch,这样镜像可以直接当软件交换机用。这个习惯帮我省了很多次重装的时间,也希望帮到你。
本文还有配套的精品资源,点击获取