配置P4项目基于Mininet & BMv2

环境说明

我使用配置完成的虚拟机,下载到的是一个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去下发流表项。两种办法:

  1. 现代SDN,动态下发流表项,通过 gRPC (P4Runtime)

如果在 Makefile 中使用了 -b simple_switch_grpc,交换机会监听 gRPC 端口(s1 通常是 50051,s2 是 50052,以此类推)。自己写一个 Python 脚本,利用 p4runtime_lib,在网络启动后连接 50051 端口,动态地根据网络状况下发流表。此为 P4 进阶功能(如负载均衡、动态路由)的主要配置方式。

  1. 通过 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": […]
}