环境说明
我使用配置完成的虚拟机,下载到的是一个2.9G的ova文件(Open Virtual Appliance,就是一种虚拟机文件),打开virtualBox点导入,等它导入,然后选择用户p4,密码是p4,登录。
我也想根P4官方仓库配置,但屡屡失败,我们还是放过自己吧
虚拟机里的终端很难用,如果愿意的话可以使用SSH远程连接,以及设置共享文件夹来用本地ide写代码,不过这并不是本文重点,可自行尝试。
从Makefile了解整个流程
首先要知道一个P4项目分为哪些文件
手动编写的文件(输入):
*.p4:数据面逻辑。可以写多个文件,include。topology.json:规定这个模拟的网里有多少主机交换机,如何连线的。s1-commands.txt:静态流表。可不写,平替后文会讲。
编译器生成的文件(中间产物):
target.json:BMv2 的处理流程,来自于p4文件的编译。p4info.txt:相当于API的角色,描述了表名、动作名与 ID 的对应关系,供控制器(Python/gRPC)识别。
运行时环境(Mininet所需文件):
- 需要
topology.json来创建虚拟网线。 - 需要
target.json来初始化每个 simple_switch 进程。
P4的编译
P4编译器是分层的,前端叫做p4c,负责检查语法,后端叫做bm2-ss,负责把语法树转换成特定硬件或软件能懂的格式。bm2-ss 专门针对 BMv2 软件交换机的 Simple Switch 架构。
以上两个合一块就是P4编译器,叫做 p4c-bm2-ss —— p4c-bm2-ss 之于 P4,就像 gcc 之于 C 语言。
在实际开发中,最标准、最常用的写法如下:
p4c-bm2-ss --p4v 16 \
--p4runtime-files build/target.p4.p4info.txt \
--p4runtime-format text \
-o build/target.json \
./p4src/source.p4
解释
- –p4v 16:指定使用的 P4 语言版本。目前主流为 P4-16,若不指定可能按旧版 P4-14 处理。
- –p4runtime-files
:指定生成的 P4Info 文件路径。 - –p4runtime-format text:规定 P4Info 的排版格式为可读文本。
- -o
:Output。指定生成的 JSON 配置文件路径,这是 BMv2 软件交换机真正运行依赖的逻辑文件。 - ./p4src/source.p4:主程序入口。编译器会从此文件开始递归处理所有 #include 的头文件。
写到Makefile中,就是
P4C = p4c-bm2-ss
P4_MAIN_SOURCE = ./p4src/source.p4
P4_DEPENDENCIES = (wildcard p4src/*.p4)
BUILD_DIR = build
JSON_FILE =(BUILD_DIR)/target.json
INFO_FILE = (BUILD_DIR)/target.p4.p4info.txt
build:(P4_DEPENDENCIES)
mkdir -p (BUILD_DIR)(P4C) --p4v 16 --p4runtime-files (INFO_FILE) -o(JSON_FILE) $(P4_MAIN_SOURCE)
P4_MAIN_SOURCE 和 P4_DEPENDENCIES 分开写的原因是要既满足编译器(源文件位置只接收最主要的那个),又要满足 make 的检查逻辑(全部源文件都是依赖,需检查时间戳)
mininet的运行
run_exercise.py 稍后仔细解释,这里只需将其理解为一个黑盒,功能是把之前准备的一些文件整合、运行起来,得到一个动态运行的虚拟网络。其能接受一些命令行参数,包括
- -t (Topology):指定拓扑 JSON 文件。脚本根据它来创建主机和交换机节点,并像拉网线一样建立连接。
- -j (JSON):指定默认的 P4 编译 JSON 文件。脚本会将此逻辑加载到拓扑中定义的每一个交换机进程中。
- -b (Behavioral-model):指定后端交换机执行程序的类型。
simple_switch: 基础版。
simple_switch_grpc: 进阶版。支持通过 gRPC 远程控制,稍后解释。
写到Makefile中,就是
UTILS_DIR = /home/p4/tutorials/utils
RUN_SCRIPT = (UTILS_DIR)/run_exercise.py
JSON_FILE =(BUILD_DIR)/myelin.json
TOPO_FILE = ./utils/topology.json
run: build
sudo python3 (RUN_SCRIPT) -t(TOPO_FILE) -j $(JSON_FILE) -b simple_switch_grpc
从run_exercise.py看topology.json的解析方式
刚才提到,run_exercise.py能把我们编写的静态配置文件转化为一个动态运行的虚拟网络。它通过解析topology.json来自动化构建虚拟链路、分配端口、启动交换机进程并下发初始化流表,一切准备工作就绪后在终端弹出 mininet> 交互式提示符,将控制权移交给开发者,并在实验结束退出时负责清理残留的进程和网络接口。此文件共389行,通常不需要自己写,其已经涵盖了 95% 的实验需求,直接用就行。
run_exercise.py 源码对 topology.json 的解析逻辑非常刻板,因此topology.json一定要按run_exercise.py的解析逻辑去写。
首先,run_exercise.py 期望的 JSON 根对象必须包含以下三个顶级键:
- hosts: 定义终端节点(即主机)的属性。
- switches: 定义交换机进程及其实验特定的配置。
- links: 定义节点间的物理/逻辑连接。
下面讲定义规范,当然是照着run_exercise.py 的解析逻辑来的。
主机配置 (hosts)
- 每个主机条目支持以下字段:ip、mac、gw、commands
- ip: 必须带掩码(如 10.0.1.1/24),脚本会据此配置接口。
- mac: 建议手动指定(如 08:00:00:00:01:01),便于在 P4 代码中匹配。
- gw: 默认网关。脚本会自动执行 route add default gw 命令。
- commands (进阶): 一个字符串列表。脚本在启动后会依次在主机命名空间执行这些 Shell 命令(例如:[“arp -s 10.0.1.254 08:00:00:00:01:ff”])。
例子
{
"hosts": {
"h1": { "ip": "10.0.1.1/24", "mac": "08:00:00:00:01:01", "gw": "10.0.1.254" },
"h2": { "ip": "10.0.1.2/24", "mac": "08:00:00:00:01:02", "gw": "10.0.1.254" },
"h3": { "ip": "10.0.2.3/24", "mac": "08:00:00:00:02:03", "gw": "10.0.2.254" },
"h4": { "ip": "10.0.2.4/24", "mac": "08:00:00:00:02:04", "gw": "10.0.2.254" }
},
"switches": {…},
"links": […]
}
交换机配置 (switches)
- 脚本为每个交换机条目启动一个独立的 simple_switch 或 simple_switch_grpc 进程。
- cli_input: 相当于交换机的“开机初始化脚本”。P4 代码定义了交换机的“功能”(比如它能识别 IPv4 报文),而 cli_input 文件定义了交换机的“数据”(比如具体的路由表项)。
- runtime_json: 如果使用 P4Runtime (gRPC),指定包含流表定义的 JSON 文件。
- program 特殊用法 后面讲
例子:
{
"hosts": {…},
"switches": {
"s1": { "cli_input": "utils/s1-commands.txt" },
"s2": { "cli_input": "utils/s2-commands.txt" }
},
"links": […]
}
对应的目录结构
.
├── Makefile
├── p4src/
└── utils/
├── topology.json
├── s1-commands.txt <-- 放在这里
└── s2-commands.txt
s1-commands.txt书写格式就不是由run_exercise.py 规定的,而是由 BMv2 的底层工具 simple_switch_CLI 规定的。可以通过查阅此文档知道有哪些命令可用。
其他下发流表的办法
其实topology.json最简单的写法可以不写s1-commands.txt,即不使用 s1-commands.txt 这种静态文件自动下发流表,那么交换机的转发表(Tables)初始状态是空的。则需要通过Control Plane去下发流表项。两种办法:
- 现代SDN,动态下发流表项,通过 gRPC (P4Runtime)
如果在 Makefile 中使用了 -b simple_switch_grpc,交换机会监听 gRPC 端口(s1 通常是 50051,s2 是 50052,以此类推)。自己写一个 Python 脚本,利用 p4runtime_lib,在网络启动后连接 50051 端口,动态地根据网络状况下发流表。此为 P4 进阶功能(如负载均衡、动态路由)的主要配置方式。
- 通过 Thrift (CLI) —— 传统调试方式
没用过,不详。
链路配置 (links)
逻辑上很简单,就是连线,语法上有以下规定:
- 顺序:在定义“主机-交换机”链路时,主机必须放在第一个位置 (node1)。正确:[“h1”, “s1-p1”], 错误:[“s1-p1”, “h1”]
- 命名与端口规则:脚本使用 node.split(‘-‘) 解析端口。格式必须严格遵守 交换机名-p端口号。脚本会提取 – 后的字符串,并去掉首字母 p,将其转换为整数。例如 s1-p3 对应 BMv2 实例的 port 3。
例子:
{
"hosts": {…},
"switches": {…},
"links": [
["h1", "s1-p1"],
["h2", "s1-p2"],
["h3", "s2-p2"],
["h4", "s2-p3"],
["s1-p3", "s2-p1"]
]
}
把上面三个例子合在一起就是run_exercise.py能解析的topology.json了。
实现异构交换机网络
仅供参考,由于我目前没有用到这个,所以并未亲自验证
现在想实现 s1 和 s2 运行不同的 P4 代码,则需对编译阶段(Makefile) 和 配置阶段(topology.json) 同时进行修改。
修改Makefile
假设已经准备了两个p4源文件,p4src/s1_logic.p4 和 p4src/s1_logic.p4,分别控制 s1 和 s2 的逻辑。那么要让p4c-bm2-ss编译器运行两次,生成两个独立的 JSON 描述文件。
build:
mkdir -p (BUILD_DIR)(P4C) --p4v 16 -o (S1_JSON)(S1_SRC)
(P4C) --p4v 16 -o(S2_JSON) $(S2_SRC)
run目标去掉了 -j 参数,其实不去掉也行,因为命令行传入的-j的优先级低于稍后的单独设置。
run: build stop
mkdir -p logs pcaps
sudo python3 (RUN_SCRIPT) -t(TOPO_FILE) -b simple_switch_grpc
修改 topology.json
根据 run_exercise.py 的源码逻辑,它会检查 switches 定义中是否有 “program” 字段。如果有,它会覆盖掉全局默认的 JSON。
所以只需
{
"hosts": {…},
"switches": {
"s1": {
"program": "build/s1.json",
…
},
"s2": {
"program": "build/s2.json",
…
}
},
"links": […]
}




