tofino经控制平面传递寄存器信息实验

tofino经控制平面传递寄存器信息实验

实验目的

为了在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,不能直接使用readwirte函数

  • 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中,RegisterActionapply()函数有两个参数,第一个参数value就是execute的参数(index)所指向的寄存器的值,对value的操作就相当于对寄存器值的操作,第二个参数rvexecute函数的返回值。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),说明这一步并没有被执行到

posted @ 2023-04-05 18:20  瑞图恩灵  阅读(543)  评论(0编辑  收藏  举报