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çãoselectpara 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 blocoactionsseparado 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.
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:
- Parser Programável: Onde o programador define a máquina de estados para extrair os cabeçalhos (como visto em 4.2.1).
-
Ingress(Pipeline de Controle de Entrada): Um blococontrolprogramá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. - 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.
-
Egress(Pipeline de Controle de Saída): Um segundo blococontrolprogramá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. -
DeparserProgramável: Onde o programador define a lógicaemit(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.