4. A Arquitetura de um Programa P4

4.1. O Conceito de "Arquitetura" em P4

A Seção 3 introduziu as abstrações da linguagem P4, componentes como headers, parsers e controls. Contudo, um programa P4 não é apenas um conjunto desordenado desses blocos; ele precisa de uma "planta" que defina formalmente quais blocos existem, como são nomeados e como se conectam. Essa "planta" é o que a especificação P4 define como uma Arquitetura.

A Arquitetura é o contrato, ou a interface, que o dispositivo target (o hardware ou software) expõe ao programador P4. Ela especifica quais componentes são programáveis (ex: um parser, um pipeline de ingress, um pipeline de egress) e, crucialmente, como eles se interconectam—quais metadados e cabeçalhos são passados de um estágio para o outro.

A linguagem formaliza esse contrato através da declaração architecture, geralmente fornecida pelo fabricante do target. Ela define os "slots" programáveis que o desenvolvedor deve preencher.


/* * Exemplo de declaração de uma Arquitetura simples 
 * (definida pelo fabricante do target).
 */
architecture MySimplePipe<H, M>(
    Parser<H, M>   p,      // "Slot" para um parser
    Control<H, M>  proc,   // "Slot" para um bloco de controle
    Deparser<H>    d       // "Slot" para um deparser
);

O programador P4, então, utiliza a declaração package para construir o programa final. O package instancia a arquitetura desejada e fornece as implementações concretas (os blocos parser e control escritos pelo programador) para preencher os "slots" definidos pela arquitetura.


/* * Exemplo de instanciação da Arquitetura (escrito pelo programador P4).
 * O 'package' conecta as implementações aos "slots" da arquitetura.
 */
 package MySwitch(MySimplePipe main);

Portanto, a arquitetura é a abstração que separa a lógica do programa (o "o quê" o switch faz, escrito pelo programador) da plataforma target (o "onde" ele executa, definido pelo fabricante). Isso permite que um mesmo programa P4, escrito para uma arquitetura comum, possa ser compilado para diferentes targets (ex: um ASIC ou um switch de software) que implementem essa mesma arquitetura.

4.2. Os Blocos Programáveis Centrais

Enquanto a arquitetura define a estrutura geral do pipeline, os blocos programáveis centrais — parser, control e deparser — definem a lógica de processamento de pacotes em si. A Seção 3.2 introduziu o que esses blocos fazem; esta seção foca em como eles são implementados sintática e semanticamente na linguagem P4.

4.2.1. O Parser como Máquina de Estados

O parser P4 não é apenas um conceito, mas é implementado explicitamente como uma Máquina de Estados Finitos (MEF). Sua função é receber o pacote bruto (packet_in) e, estado por estado, extrair os cabeçalhos definidos pelo programador, populando a estrutura de dados de cabeçalhos (headers).

A implementação utiliza três palavras-chave principais:

  • state: Define um estado da máquina (ex: start, parse_ethernet).
  • extract(): Primitiva que instrui o parser a extrair um cabeçalho específico do pacote.
  • transition: Define a lógica de mudança de estado, geralmente usando uma instrução select para inspecionar um campo recém-extraído e decidir o próximo estado.

Abaixo, um exemplo de um parser simples que processa cabeçalhos Ethernet e IPv4:


#include <core.p4>

// (Definições de header_t ethernet e ipv4_t omitidas por brevidade)

parser MyParser(packet_in pkt,
                out headers hdr) {

    // O estado inicial obrigatório
    state start {
        // Extrai o cabeçalho Ethernet
        pkt.extract(hdr.ethernet);
        // Transiciona incondicionalmente para o próximo estado
        transition parse_ipv4;
    }

    state parse_ipv4 {
        // Transição condicional (baseada em dados)
        transition select(hdr.ethernet.etherType) {
            // Se for IPv4, extrai e termina
            TYPE_IPV4: parse_final_ipv4;
            // Caso contrário, aceita (termina) sem extrair mais
            default: accept; 
        }
    }
    
    state parse_final_ipv4 {
        pkt.extract(hdr.ipv4);
        // O estado final obrigatório
        transition accept;
    }
}

4.2.2. O Control como Lógica Imperativa

Diferente do parser, que é uma máquina de estados declarativa, o bloco control é onde a lógica de processamento de pacotes imperativa é definida. Se o parser extrai dados, o control processa esses dados (cabeçalhos e metadados) para tomar decisões.

A principal função de um bloco control é orquestrar a aplicação de uma ou mais tabelas match-action. Ele permite que o programador defina uma lógica complexa, usando construções como if/else, variáveis locais e chamadas de outras tabelas ou blocos control, para decidir a sequência de processamento.

A definição de uma tabela match-action dentro de um control especifica os seguintes componentes:

  • key: Os campos (dos cabeçalhos ou metadados) que serão usados para construir a chave de busca.
  • actions: O conjunto de ações que a tabela pode executar. A lógica das ações é definida (como código imperativo) em um bloco actions separado ou inline.
  • default_action: A ação a ser executada caso nenhuma entrada da tabela combine com o pacote.

O processamento do control é iniciado em seu bloco apply {}, que dita a ordem das operações.


control MyIngress(inout headers hdr,
                  inout metadata meta) {

    // 1. Definição da lógica das Ações
    action drop() {
        // Modifica metadados para sinalizar o descarte
        meta.standard_metadata.drop = 1; 
    }

    action set_egress_port(PortId_t port) {
        // Define a porta de saída
        meta.standard_metadata.egress_spec = port;
    }

    // 2. Definição da Tabela
    table ipv4_forward {
        key = {
            hdr.ipv4.dstAddr: lpm; // lpm = Longest Prefix Match
        }
        actions = {
            set_egress_port;
            drop;
        }
        size = 1024;
        default_action = drop();
    }

    // 3. Lógica Imperativa de execução
    apply {
        if (hdr.ipv4.isValid()) {
            // Aplica a tabela (executa o match-action)
            ipv4_forward.apply(); 
        } else {
            // Ação padrão se não for IPv4
            drop(); 
        }
    }
}

Neste exemplo, o bloco apply verifica primeiro se o cabeçalho IPv4 é válido. Se for, ele invoca o método .apply() da tabela ipv4_forward. O resultado da tabela (a execução da ação set_egress_port ou drop) altera os metadados, que serão usados por estágios subsequentes do pipeline.

4.2.3. O Deparser e a Reconstrução de Pacotes

O deparser (ou "desanalisador") é o componente final do pipeline de processamento de pacotes. Sua função é a inversa do parser: ele pega a representação interna dos cabeçalhos (a estrutura headers, possivelmente modificada pelos blocos control) e a serializa de volta em um fluxo de bytes (packet_out) pronto para transmissão.

Assim como o control, o deparser é implementado como um bloco de lógica imperativa. A primitiva fundamental deste bloco é o método emit(). Este método recebe uma estrutura de cabeçalho como argumento e anexa seus campos, já serializados, ao buffer do pacote de saída.

A ordem na qual o programador invoca o emit() é crucial, pois ela dita a ordem exata em que os cabeçalhos aparecerão no pacote final na rede. Isso concede ao programador controle total sobre o formato do pacote de saída, permitindo a fácil adição de novos cabeçalhos (como túneis ou shims) que tenham sido adicionados durante o processamento.


// (Definições de header_t ethernet e ipv4_t omitidas por brevidade)

// O deparser recebe os cabeçalhos (potencialmente modificados)
control MyDeparser(packet_out pkt, in headers hdr) {
    
    // Lógica imperativa de emissão
    apply {
        // Emite o cabeçalho Ethernet primeiro
        pkt.emit(hdr.ethernet);

        // Emite o cabeçalho IPv4 somente se ele for válido
        if (hdr.ipv4.isValid()) {
            pkt.emit(hdr.ipv4);
        }
        
        // (Outros cabeçalhos, como TCP/UDP, seriam emitidos aqui)
    }
}

Neste exemplo, o deparser primeiro emite o cabeçalho Ethernet. Em seguida, ele verifica se o cabeçalho IPv4 (que foi extraído pelo parser e talvez modificado pelo control) ainda é válido antes de emiti-lo. Após a execução do bloco apply, o pacote é transmitido.

4.3. Interagindo com o Alvo: O Papel dos Objetos extern

Os blocos parser e control definem a lógica de processamento de pacotes, mas as redes modernas exigem mais do que apenas lógica match-action sem estado (stateless). Elas precisam manter estado (stateful, ex: contadores de fluxo, medições) e executar operações de hardware especializadas (ex: cálculo de checksums, operações criptográficas).

A linguagem P4 não tenta reinventar essas funções em software, pois elas já são otimizadas em hardware no target. Em vez disso, ela fornece uma "ponte" para que o programa P4 possa controlar esses recursos: os objetos extern.

Um extern é um objeto cuja interface (API) é definida em P4, mas cuja implementação é fornecida pelo fabricante do target. O programa P4 pode instanciar esses objetos e chamar seus métodos, mas não pode ver ou modificar seu comportamento interno. Eles funcionam como "caixas-pretas" de funcionalidade específica do hardware que a arquitetura expõe ao programador.

Os exemplos mais comuns de externs, encontrados na maioria das arquiteturas, são:

  • Counter(): Usado para contar pacotes ou bytes que correspondem a uma regra específica.
  • Register(): Um array de memória stateful no plano de dados. Permite que o programa P4 leia e escreva valores entre pacotes, sendo essencial para implementar algoritmos stateful complexos.
  • Checksum() / Hash(): Invoca unidades de hardware dedicadas para (re)calcular checksums (como IP ou TCP) ou realizar funções de hash rapidamente.

O exemplo abaixo mostra como um extern (um contador) é instanciado em um bloco control e invocado dentro de uma ação.


control MyIngress(inout headers hdr,
                  inout metadata meta) {
    
    // 1. Instanciação do objeto 'extern' Counter.
    // O 'extern' Counter é definido pela arquitetura.
    // Aqui, criamos uma instância chamada 'my_flow_counter' com 1024 entradas.
    Counter(1024) my_flow_counter;

    // 2. Definição da Ação que usa o 'extern'
    action count_and_forward(PortId_t port) {
        // Invoca o método .count() do 'extern'
        // O argumento '0' é o índice do contador a ser incrementado.
        my_flow_counter.count(0); 

        // Define a porta de saída (lógica P4 normal)
        meta.standard_metadata.egress_spec = port;
    }

    table my_table {
        key = { hdr.ipv4.dstAddr: exact; }
        actions = { count_and_forward; }
        // ...
    }
    
    apply {
        my_table.apply();
    }
}

Dessa forma, os externs são o mecanismo que permite ao programa P4 abstrato controlar os recursos específicos e otimizados do hardware subjacente, tornando a linguagem poderosa o suficiente para aplicações do mundo real.

4.4. O Modelo Padrão: A Arquitetura V1Model

As seções anteriores descreveram os conceitos de architecture, parser, control e extern de forma abstrata. Para "aterrar" esses conceitos, é útil analisar a arquitetura padrão de fato utilizada na maioria dos tutoriais, exemplos e ferramentas de código aberto do P4: a V1Model (Very Simple Switch Model).

A V1Model não é uma arquitetura de hardware específica, mas sim um modelo de referência genérico que define um pipeline de switch programável. Ela é a arquitetura-alvo padrão para o compilador P4 de referência (p4c) e é implementada pelo switch de software bmv2 (Behavioral Model version 2), que serve como a principal ferramenta para desenvolvimento e depuração de programas P4.

Diagrama da arquitetura V1Model mostrando o fluxo: Parser, Ingress Pipeline, Buffer (Traffic Manager), Egress Pipeline e Deparser.
Figura 4.1 – O modelo de encaminhamento abstrato implementado pela arquitetura V1Model. (Fonte: Adaptado de [6]).

O "contrato" (conforme definido na Seção 4.1) que a V1Model expõe ao programador define um pipeline lógico claro, conforme ilustrado na Figura 4.1:

  1. Parser Programável: Onde o programador define a máquina de estados para extrair os cabeçalhos (como visto em 4.2.1).
  2. Ingress (Pipeline de Controle de Entrada): Um bloco control programável (como visto em 4.2.2) onde ocorre a principal lógica de match-action (ex: ACLs, roteamento L3). É aqui que o destino do pacote é decidido.
  3. Traffic Manager (Gerenciador de Tráfego): Um componente de função fixa (não programável em P4) que lida com o enfileiramento (queuing) e agendamento (scheduling) de pacotes. Ele recebe o pacote do Ingress e o enfileira para a porta de saída decidida.
  4. Egress (Pipeline de Controle de Saída): Um segundo bloco control programável. Este bloco processa o pacote imediatamente antes de ele ser enviado. É tipicamente usado para reescrita de cabeçalhos (ex: decrementar TTL, reescrever MAC address) ou contagem de saída.
  5. Deparser Programável: Onde o programador define a lógica emit (como visto em 4.2.3) para serializar os cabeçalhos (possivelmente modificados) de volta ao formato de bytes.

Crucialmente, a V1Model também define uma estrutura de metadados padrão, a standard_metadata_t. Esta estrutura é o principal meio de comunicação entre os blocos do pipeline. Por exemplo, o Ingress toma uma decisão de encaminhamento e a armazena no campo standard_metadata.egress_spec. O Traffic Manager lê este campo para saber para qual fila enviar o pacote. O Egress pode então ler este e outros campos para executar sua lógica final.

O programa P4 final para esta arquitetura é um package que instancia a V1Model e "conecta" as implementações do programador (seus blocos MyParser, MyIngress, MyEgress, MyDeparser) aos "slots" correspondentes definidos pela arquitetura. A Seção 5 demonstrará a construção de um switch completo usando este modelo.