经控制平面传递寄存器信息实验
实验目的
为了在tofino上实现微突发缓解所需的微突发检测,需要在ingress阶段获取出端口的队列信息。但由于硬件限制,ingress阶段不能直接访问在egress阶段获取的队列长度信息,因此需要一种方法,将每个端口的队列长度是否超过阈值的信息,传递给ingress pipeline。实现该功能有几种可能的方案,包括recirculate、上报控制平面后下发、构造信使数据包(发送给相邻交换机,等待携带信息的信使数据包返回ingress)等。本实验的目的是,通过控制平面对寄存器信息的读取和写入的方法,实现微突发检测。
任务分解
- 在egress pipeline中实现队列长度监控和微突发检测
- 在egress pipeline中定义一组寄存器并写入队列信息
- 控制平面读取egress阶段的寄存器值
- 在ingress pipeline中定义格式相同的一组寄存器,控制平面将读取到的值下发给ingress阶段的寄存器
实验过程与分析
为了获取队列长度信息,我们可以利用tofino中的标准元数据,在tofino官方文档中,可以找到关于Egress Intrinsic Metadata的描述:
header egress_intrinsic_metadata_t {
PortId_t egress_port;
bit<19> enq_qdepth; //数据包入队时的队列长度
bit<2> enq_congest_stat;
bit<18> enq_tstamp;
bit<19> deq_qdepth; //数据包出队时的队列长度
bit<2> deq_congest_stat;
bit<8> app_pool_congest_stat;
bit<16> egress_rid;
bit<1> egress_rid_first;
QueueId_t egress_qid;
bit<3> egress_cos;
bit<1> deflection_flag;
bit<16> pkt_length;
}
所以,我们可以获取每个数据包的deq_qdepth
值,并将其与事先给定的队列长度阈值进行比较,如果超过阈值,就认为需要发送该端口的微突发警报。得到该信息之后,就可以考虑如何定义并写入寄存器了,在bf-sde-9.x.0/install/share/p4c/p4include/tofino.p4
文件中,可以找到关于寄存器以及寄存器操作的描述:
/// Register
extern Register<T, I> {
/// Instantiate an array of <size> registers. The initial value is
/// undefined.
Register(bit<32> size);
/// Initialize an array of <size> registers and set their value to
/// initial_value.
Register(bit<32> size, T initial_value);
/// Return the value of register at specified index.
T read(in I index);
/// Write value to register at specified index.
void write(in I index, in T value);
}
extern RegisterAction<T, I, U> {
RegisterAction(Register<T, I> reg);
U execute(in I index); /* {
U rv;
T value = reg.read(index);
apply(value, rv);
reg.write(index, value);
return rv;
} */
// Apply the implemented abstract method using an index that increments each
// time. This method is useful for stateful logging.
U execute_log();
// Abstract method that needs to be implemented when RegisterAction is
// instantiated.
// @param value : register value.
// @param rv : return value.
@synchronous(execute, execute_log)
abstract void apply(inout T value, @optional out U rv);
U predicate(@optional in bool cmplo,
@optional in bool cmphi); /* return the 4-bit predicate value */
}
我们需要用tofino专有的语法来定义和使用寄存器,例如下面的例子:
Register<bit<32>, bit<16>>(PORT_NUMBER, 0) queue_state_egress;//第二个参数为初始值
RegisterAction<bit<32>, bit<16>, bit<32>>(queue_state_egress) write_queue_state = {
void apply(inout bit<32> register_data, out bit<32> result){
if(eg_intr_md.deq_qdepth >= THRESHOLD){
register_data = 32w1;
}
else{
register_data = 32w7;
}
result=register_data;
}
};
apply {
bit<32> rv = write_queue_state.execute((bit<16>)eg_intr_md.egress_port);
}
到这一步,理论上就可以往寄存器里写数据了,但是如何验证数据是否成功写入寄存器,可以通过控制平面查看。首先在交换机上将p4代码运行起来,配置好端口和流表,使得p4代码在一些数据包上执行。接下来在控制面找到寄存器信息,例如在bfshell
中,进入:
bfrt. control_plane(程序名). pipe. Egress(定义位置).queue_state_egress(寄存器名)
进入寄存器之后,可以看到如下图所示的可用命令,在bfrt的使用过程中,如果不清楚可以使用什么命令,也可以直接输入?
来获取相关信息,如果不清楚具体命令的使用方法,可以使用help(命令名)
的方法获取帮助,例如help(mod)
在寄存器中,可以使用dump(from_hw=True, table=True)
命令(注意True首字母大写),获取当前寄存器的值(以表格形式给出),也可以不需要加table
参数,或者直接dump
。以表格形式给出的示例图如下:
也可以使用dump(from_hw=True, json=True)
命令来获取以json
格式给出的寄存器信息:
如果已经验证数值成功写入,就可以考虑如何将控制面获取到的信息下发给ingress阶段的寄存器了。同理,我们可以在ingress pipeline中定义与egress pipeline中格式相同的寄存器,并使用mod
命令,进行寄存器值的下发。使用帮助功能,可以看到mod
命令可以有两个参数,第一个参数是用来指定要修改哪个寄存器的,第二个参数用来指定具体要写入的值。例如mod(register_index=0XA,f1=22)
的效果是在编号为0xA的寄存器中,写入22数值
验证寄存器值是否成功下发的方法,还是通过bfrt
的寄存器读取功能,观察mod
命令执行之后,再次进行读取所看到的寄存器值是否发生对应的变化。
到目前为止,我们对寄存器所作的读取和写入操作都是手动输入命令执行的,但实际应用中,人工输入命令的方式延迟过大,显然需要控制平面能够自动执行这一系列过程,将egress阶段的队列信息不断地同步到ingress阶段。让控制平面自动进行处理的方法,是实现一个控制器脚本,使得控制器脚本除了可以下发流表之外,还可以实现我们需要的控制面功能。具体的实现方法可参考cocosketch的control.py,例如:
PORT_NUMBER = 33
p4 = bfrt.control_plane
# 下发流表部分,这些命令需要先执行
while True:
egress_register_text = p4.pipe.Egress.queue_state_egress.dump(from_hw=True,json=True)
egress_register = json.loads(egress_register_text)
ingress_register = p4.pipe.Ingress.queue_state_ingress
pos = 0x0
while pos<PORT_NUMBER:
f = str(egress_register[pos]['data']['Egress.queue_state_egress.f1'][0])
ingress_register.mod(register_index=pos,f1=f)
ingress_register_text = ingress_register.dump(from_hw=True,table=True)
print(ingress_register_text)
# print(f)
pos += 1
pos = 0x0
控制器脚本的使用方法,是在运行了p4文件之后,在另一个窗口运行python文件,使用的命令格式为./run_bfshell.sh -b <python文件的绝对路径> -i
常见问题/误区及解决方法
- 直接在
apply{}
中调用寄存器的write
操作,例如:
Register<bit<32>, bit<16>>(32w33, 0) queue_state_egress;
apply{
queue_state_egress.write(16w1, 32w9);
}
则会出现报错:The method call of read and write on a Register is currently not supported in p4c. Please use RegisterAction to describe any register programming.
,这说明对寄存器进行的任何操作都需要通过RegisterAction
,不能直接使用read
或wirte
函数
- 在
RegisterAction
里的apply
中进行reg.write(in I index, in T value)
操作,例如:
RegisterAction<bit<32>, bit<16>, bit<32>>(queue_state_egress) write_queue_state = {
void apply(inout bit<32> register_data){
if(eg_intr_md.deq_qdepth >= THRESHOLD){
queue_state_egress.write(16w0x0001, 32w2);
}
else{
queue_state_egress.write(16w0x0002, 32w3);
}
}
};
则会出现报错:visitor returned non-MethodCallExpression type: Register.write(queue_state_egress_0, 1, 2);
,这是因为代码中的.write
写法,是bmv2
的写法,tofino有专用的语法,并不是直接在RegisterAction
中执行就可以直接解决误区1的。tofino中,RegisterAction
的apply()
函数有两个参数,第一个参数value
就是execute
的参数(index)所指向的寄存器的值,对value的操作就相当于对寄存器值的操作,第二个参数rv
是execute
函数的返回值。execute
里的apply
函数,其实就是我们在RegisterAction
里面自定义的那个apply
函数
- header不允许重复定义,如果已经在ingress部分定义过的
_h
,那egress就不能重新定义,直接用即可
相关工具/调试方法
- 使用tcpdump命令抓包
在服务器上监听特定网卡:sudo tcpdump -i <网卡名>
,例如sudo tcpdump -i enp101s0f0
过滤来自特定目的端口的数据包:sudo tcpdump -i <网卡名> dst port <端口号>
,例如sudo tcpdump -i enp101s0f0 dst port 82
即过滤目的端口为82的数据包,同理,将dst
替换为src
可以过滤特定源端口的数据包,更广泛地说,还可以同理过滤指定源/目的IP地址的数据包
- 使用scapy工具发包
初始化:要使用scapy工具,可以使用sudo scapy
命令直接进入,也可以sudo python
先进入python界面,再通过from scapy.all import *
命令使用scapy工具
定义要发送的数据包:使用<数据包名> = Ether()/IP()/TCP()/"<负载信息>"
格式来自定义要发送的数据包,其中Ether()
部分、IP()
部分都可以定义源地址和目的地址,TCP()
部分可以定义源端口和目的端口,例如:
p=Ether()/IP(src="192.168.1.1",dst="192.168.22.22")/TCP(sport=5000,dport=82)/"payload"
发送数据包:使用send()
函数或者sendp()
函数进行发包,区别在于send()
是三层发包,sendp()
是二层发包,在这些命令中可以指定发送数据包的网卡,发送的数量等信息,例如下述命令的含义是从enp101s0f1
网卡发送数据包p,发送的数量为5个数据包
sendp(p,iface="enp101s0f1",count=5)
- 使用修改端口的方法进行debug
在tofino上对p4代码进行debug是比较困难的,当想要知道程序是否执行到某一步,可以通过修改端口的方法来进行判断。例如,在上面的例子中,我们将TCP的源端口定义为5000,那么在Egress的apply中,我们可以进行一些添加:
apply{
bit<32> rv = write_queue_state.execute((bit<16>)eg_intr_md.egress_port);
hdr.tcp.srcPort=5003;
}
进行了这样的修改之后,如果我们在接收端进行的tcpdump发现接收到的数据包端口号变为5003,则说明代码已经成功执行到了这一步,如果没有发生变化(还是5000),说明这一步并没有被执行到
标签:queue,value,传递,egress,寄存器,平面,bit,数据包 From: https://www.cnblogs.com/CCchaos/p/17290247.html