Mudanças entre as edições de "PTC-2017-2"

De MediaWiki do Campus São José
Ir para navegação Ir para pesquisar
Linha 3 228: Linha 3 228:
  
 
= 07/12: Verificação de protocolos: destrinchando fórmulas LTL =
 
= 07/12: Verificação de protocolos: destrinchando fórmulas LTL =
 +
 +
== Fórmulas LTL em geral ==
 +
 +
As fórmulas LTL simples, sem operadores temporais, são consideradas verdadeiras se avaliarem para VERDADEIRO no primeiro estado do modelo. Quer dizer, elas devem valer no momento em que o modelo for iniciado, e antes de qualquer sentença ser executada. Por exemplo, seja este modelo:
 +
 +
<syntaxhighlight lang=text>
 +
int n=5
 +
 +
active proctype teste() {
 +
  do
 +
  :: n > 0 -> n--
 +
  :: n < 5 -> n++
 +
  od
 +
}
 +
</syntaxhighlight>
 +
 +
 +
Nesse modelo a variável global ''n'' inicia com o valor 5, e nos estados subsequentes ela pode variar aleatoriamente entre 0 e 5. Considerem-se as seguintes fórmulas LTL:
 +
* '''(n == 5)''': esta fórmula avalia para '''verdadeiro''', pois no primeiro estado do modelo de fato ''n'' tem o valor 5.
 +
* '''(n == 4)''': esta fórmula avalia para '''falso''', pois no primeiro estado do modelo ''n'' tem na verdade o valor 5.
 +
* '''<>(n == 4)''': esta fórmula avalia para '''verdadeiro''', pois realmente em algum estado futuro ''n'' terá o valor 4.
  
 
= 14/12: Conclusão =
 
= 14/12: Conclusão =

Edição das 14h51min de 14 de dezembro de 2017

Projeto de Protocolos: Diário de Aula 2017-2

Professor: Marcelo Maia Sobral
Encontros: 5a feira/9:40, 6a feira/9:40 (Semana A)
Atendimento paralelo: 5a de 8:30 às 9:30 h / 6a de 13:30 às 14:30 h

Plano de Ensino

Referências complementares

Avaliações

As avaliações são de dois tipos:

  1. Projetos: feitos em equipes de até dois alunos, são desenvolvidos ao longo da disciplina. Os resultados parciais devem ser entregues por meio de relatórios parciais, e o resultado final deve ser demonstrado na prática e descrito em um relatório conclusivo.
  2. Tarefas: feitas individualmente, e servem para ajustar os conceitos dos projetos (podem aumentá-los ou reduzi-los).


Aluno Projeto 1 Projeto 2 Projeto 3 FINAL

Obs: D* = não fez a avaliação.

Softwares

27/07: Introdução

  • Caracterização de protocolos por meio de um exemplo: sintaxe, comportamento, temporização, semântica. Princípios de projeto e propriedades desejáveis de protocolos. Análise de um protocolo real.


  • Projeto 1: um protocolo de comunicação
  • Projeto 2: um protocolo de aplicação
  • Projeto 3: implementação de um protocolo padrão segundo uma especificação

Um protocolo é uma parte muito importante de um sistema de comunicação. A comunicação de dados pode ser entendida como troca de informação entre dois dispositivos através de algum meio de comunicação. A comunicação ocorre no âmbito de um sistema de telecomunicações, composto por equipamentos (hardware) e programas (softwares). Um sistema básico de comunicação de dados se constitui de cinco componentes:


Rede-intro-1.png


  1. A mensagem: a informação a ser transmitida. O conteúdo da mensagem, seja um texto, música, video, ou qualquer outro tipo de informação, é representada por conjuntos de bits (dígitos binários).
  2. Transmissor: dispositivo que transmite a mensagem.
  3. Receptor: dispositivo que recebe a mensagem.
  4. Meio de comunicação: caminho físico por onde viaja a mensagem do transmissor até o receptor.
  5. Protocolo: conjunto de regras que governa a comunicação de dados.


Os sistemas de comunicação reais, incluídas as redes de computadores, são bem mais complexos do que esse modelo simplificado. No entanto, todos podem ser entendidos, em alguma medida, a partir desse modelo. Nesta disciplina estudam-se princípios e técnicas para projeto de protocolos, incluindo formas de verificar a consistência e correção de seu funcionamento.


Serviço e Protocolo

Um sistema de comunicação provê serviços para as aplicações ou usuários realizarem ações que envolvam a comunicação entre sistemas através de uma rede. Por exemplo, existem serviços para transferência de arquivos, reprodução remota de videos e músicas, execução remota de programas, pesquisa por informação, e muitos outros. O conceito de serviço está relacionado ao de protocolo. Um serviço é provido por entidades que interagem de acordo com um protocolo. Assim, um serviço é um dos elementos envolvidos na especificação de um protocolo. As figuras a seguir mostram a relação entre esses conceitos, primeiro apresentando somente a visão de um serviço para um usuário, e, em seguida, a relação entre serviço e protocolo.

PTC-Servico1.png
Um serviço visto por um usuário


PTC-Protocolo1.png
O serviço provido pelo protocolo

Protocolos reais

Que protocolos existentes despertam suas curiosidades sobre os detalhes de seus projetos ? Identifiquem alguns protocolos, e anotem suas finalidades e características.

Protocolo Finalidade Características
NTP Sincronizar os relógios dos computadores ligados a rede. Utiliza uma versão do algoritmo de Marzullo para determinar o tempo dos servidores corrigindo os efeitos da variação da latência da rede. Utiliza uma hierarquia mestre-escravo onde o servidor envia o horário UTC aos equipamentos da rede, enviando as informações por UDP.
FTP Transferência de arquivos entre máquinas em uma rede TCP/IP Utiliza um modelo cliente-servidor para a transferência de arquivos em ambas as direções (download e upload) de maneira eficaz. Estabelece duas conexões TCP paralelas: uma para controle (persistente) e outra para dados (não-persistente). Caracteriza-se por ser um protocolo de padrão aberto.
SSH Transferência remota de arquivos criptografados entre duas entidades. Modelo de comunicação é cliente e servidor, utilizando para geração das chaves públicas o algoritmo RSA. A porta padrão do servidor SSH é a porta 22.
TELNET Fornece as regras básicas para ligar um cliente a um intérprete de comando (servidor). O protocolo Telnet baseia-se em uma conexão TCP para enviar dados em formato ASCII codificados em 8 bits entre os quais se intercalam sequências de controle Telnet. Ele fornece, assim, um sistema orientado para a comunicação, bidirecional (half-duplex), codificado em 8 bits, fácil de aplicar.
SMTP Usado para a transferência de e-mail Funciona como roteador do correio eletrônico. É transmitido sobre o protocolo TCP, sendo composto por 3 entidades: Agente do usuário, emissor e receptor. A comunicação entre o emissor e receptor é feita através do código ASCII.
RTP É um protocolo de redes utilizado para entregar áudio e vídeo sobre IP. É implementado tipicamente sobre UDP e usado em conjunto com o RTCP (RTP Control Protocol). Enquanto o RTP lida com a mídia em sí, o RTCP monitora as estatísticas de transmissão e o Controle de Qualidade e lida com a sincronização de multiplos streams.
SSH Protocolo de rede que estabelece uma conexão criptografada em meio de comunicação não seguro na estrutura cliente-servidor. O cliente conecta-se ao servidor através de autenticação com troca de chaves (um exemplo de troca de chaves é a de Diffie-Hellman). Ao iniciar a comunicação a autenticação e criptografia são negociados entre as entidades e a comunicação é estabelecida de forma segura, possibilitando o acesso remoto de dispositivos e transferência de arquivos. Implementado sobre o TCP.

Análise dos protocolos

De acordo com Gerard Holzmann, no capítulo 2 de seu livro Design and Validation of Computer Protocols, um protocolo é composto por cinco elementos:

  1. O serviço oferecido pelo protocolo
  2. As considerações sobre o ambiente em que o protocolo é executado
  3. O vocabulário de mensagens usadas para implementar o protocolo
  4. A codificação (ou formato) de cada mensagem do vocabulário
  5. O comportamento, definido por regras de intercâmbio responsáveis pela consistência das trocas de mensagens


Com base nesses elementos, deve-se complementar ou adequar a análise dos protocolos selecionados:

Protocolo Serviço Ambiente de execução Vocabulário Codificação Comportamento
NTP Sincronização dos relógios dos computadores ligados a rede. Usa um canal UDP para intercâmbio de mensagens. Pode perder mensagens. Latências de transmissão variáveis (ex: devido a congestionamentos) podem ocorrer. NTP Data packet binária (ver RFC 5905) Há um bom resumo neste documento.
FTP Transferência de arquivos entre computadores conectados em uma rede. Usa dois canais TCP paralelos para intercâmbio de de dados de controle e arquivos. Garante confiabilidade, ou seja, entrega do arquivo enquanto protege de erros de transmissão. Conjunto de comandos: USER, PASS, LIST, STOR, PORT, QUIT. Textual (ASCII ou EBCDIC) ou binária. ( ver RFC 959) 1- Cliente realiza conexão de controle (porta 21) com servidor (modo passivo ou ativo). 2- Com a confirmação positiva, servidor mantém a conexão de controle aberta e aguarda as solicitações de transferência. 3- Em modo de execução ativo o servidor inicia a conexão de dados. 4- Em modo de execução passivo o cliente inicia a conexão de dados para iniciar a transferência.
SSH Transferência remota de arquivos criptografados entre duas entidades. (ver RFC 4253) TCP/IP atuando acima da camada de transporte e por meio da porta 22 SSH_MSG_USERAUTH_REQUEST, SSH_MSG_USERAUTH_FAILURE, SSH_MSG_USERAUTH_SUCCESS e outras mensagens (ver RFC 4253) ISO-10646 UTF-8 (ver RFC 3629) (ver Cisco - Protocol Basics: Secure Shell Protocol)
SMTP Envio de transferência de e-mail Funciona online, encapsulado em um pacote TCP/IP, por padrão na porta 25. HELO: Inicia a comunição. Mail from: Endereço do remetente. 250 OK: Confirmação de recebmento. RCPT TO: Endereço do destinatário. DATA: Corpo da mensagem. ASCII A lógica de trabalho é quase a mesma utilizada pelo correio convencional, sendo que o SMTP atua praticamente como o carteiro. Após serem capturadas, as mensagens são enviadas para ele que, em seguida, encaminha os e-mails aos destinatários finais. https://blog.saphir.com.br/smtp-o-que-e-e-como-funciona/
TELNET Permitir obter uma interface de terminais e aplicações pela Internet. Baseia-se em uma conexão TCP para enviar dados entre os quais se intercalam sequências de controle Telnet. Ele fornece, assim, um sistema orientado para a comunicação, bidirecional (half-duplex). https://tools.ietf.org/html/rfc854 Codificado em formato ASCII de 8 bits. As opções do Telnet afetam separadamente cada direção do canal de dados. Assim, cada extremidade pode negociar com as opções, ou seja, definir as opções desejadas, como utilizar (DO), não utilizar (DON' T), permitir que a outra extremidade utilize (WILL), não permitir que a outra extremidade utilize (WON' T). Desta maneira, cada uma das partes pode emitir um pedido de utilização de uma opção. A outra parte deverá, então, responder se aceita ou não a utilização da opção.

http://br.ccm.net/contents/286-o-protocolo-telnet#qual-e-o-principio-das-opcoes-negociadas

RTP Entrega de áudio e vídeo sobre IP. implementado sobre canal UDP. Apresenta perda. RTP Header Binária. Overview interessante do protocolo
SSH Protocolo de rede que estabelece uma conexão criptografada em meio de comunicação não seguro na estrutura cliente-servidor. Utiliza TCP como canal de comunicação. Geralmente utilizado para acesso remoto. As mensagens possuem suas próprias RFCs e podem ser conferidas no item 10.1 de RFC 4251 ISO-10646 UTF-8 Resumo neste documento (em inglês)

28/07: Diretrizes para projeto de um protocolo

Propriedades desejáveis de um protocolo

Ainda segundo Gerard Holzmann, no capítulo 2 de seu livro Design and Validation of Computer Protocols, um protocolo possui algumas propriedades desejáveis:

  • Simplicidade: um protocolo bem estruturado pode ser construído com um pequeno número de partes bem projetadas e bem entendidas.
  • Modularidade: um protocolo que realiza uma função complexa pode ser construído com partes menores que interagem de maneira simples e bem definida. Cada parte menor é um protocolo leve que pode ser desenvolvido separadamente, verificado, implementado e mantido.
  • Adequação: um protocolo bem formado não é incompleto, nem possui funções que nunca são de fato utilizadas. Um protocolo bem formado se limita aos recursos existentes, além de ser estável e adaptável.
  • Robustez: um protocolo robusto deve funcionar bem em condições normais, e também em situações imprevistas. Ele deve conseguir lidar com cada possível sequência de ações, em todas as possíveis condições. Ele deve ter um projeto mínimo, de forma a remover considerações não essenciais que poderiam impedir sua adaptação a condições não antecipadas.
  • Consistência: protocolos não devem apresentar interações que os levem a falhar, tais como deadlocks, livelocks e terminações inesperadas.


A figura a seguir mostra a arquitetura do protocolo de enlace PPP como exemplo de simplicidade e modularidade:

PTC-Ppp-estrutura.png


Robustez e consistência são aspectos comportamentais do protocolo, que envolvem portanto a dinâmica de seu funcionamento. O comportamento de um protocolo pode ser descrito de algumas formas, sendo usual utilizar diagramas. A figura a seguir apresenta o comportamento em alto-nível do protocolo PPP (mas não significa que dela se possa concluir que ele seja robusto ou consistente):

PTC-Ppp-comportamento.png

Diretrizes de projeto

No mesmo capÍtulo 2 de seu livro, Gerard Holzmann enumera dez regras de projeto de um protocolo:

  1. Definição do problema: certifique-se de que o problema esteja bem definido, com a identificação de todos os critérios de projeto, requisitos e restrições antes de iniciar um projeto.
  2. Definição do serviço: deve-se definir o serviço a ser realizado em cada nível de abstração antes de decidir que estruturas devem ser usadas para implementá-los (o que vem antes de como).
  3. Funcionalidades externas primeiro: projete a funcionalidade externa antes da interna. Primeiro considere a solução como uma caixa-preta e decida como ela interage com seu ambiente. Depois decida como a caixa-preta pode ser organizada internamente. Provavelmente isso consiste de caixas-pretas menores que podem ser refinadas de forma similar.
  4. Mantenha a simplicidade: protocolos extravagantes são mais propensos a ter bugs que protocolos simples. Eles são mais difíceis de implementar, verificar e comumente menos eficientes. Existem poucos problemas realmente complexos em projetos de protocolos. Problemas que aparentam serem complexos costumam ser problemas misturados. A tarefa dos projetistas é identificar os problemas mais simples, separá-los, e então resolvê-los individualmente.
  5. Preservar independência: não conectar o que for independente, o que significa separar questões ortogonais.
  6. Mantenha o projeto extensível: não introduza o que for imaterial. Não restrinja o que for irrelevante. Um bom projeto é facilmente extensível, e resolve uma classe de problemas ao invés de uma única instância.
  7. Crie um protótipo: antes de implementar um projeto, crie um protótipo de alto-nível, e verifique se os critérios do projeto são atingidos.
  8. Torne-o eficiente: implemente o projeto, meça seu desempenho e, se necessário, otimize-o.
  9. Verifique a implementação: confira se a implementação final otimizada é equivalente ao protótipo de alto-nível que foi verificado.
  10. Não pule as regras 1 a 7

TAREFA: um protocolo simples

Uma placa de aquisição de dados possui um conjunto de sensores, cujas medições são realizadas com diferentes frequências. Essa placa envia suas medições para um servidor de armazenamento, que as registra em algum tipo de repositório. O período entre envios da placa é parametrizável, e cada envio de dados pode conter um ou mais valores de medições. Cada medição é composta por uma identificação sobre a grandeza medida, seu timestamp e seu valor. Sendo assim:

1. Especifique o serviço provido pelo protocolo

Exemplo
  • protocolo de aplicação do tipo cliente-servidor
  • protocolo orientado a mensagens
  • transmissão de mensagens que podem conter de 1 até N medições
  • garantia de entrega de mensagens de medição
  • modo não-conectado
  • parâmetros operacionais: período de envio de mensagens, quantidade máxima de medições por mensagem (N)
  • parâmetros operacionais definidos pelo servidor de armazenamento, podendo ser modificados pelo servidor a qualquer momento

2. Faça considerações sobre o ambiente de execução do protocolo (ex: o tipo de canal de comunicação usado e suas características)

Exemplo
  • canal de comunicação UDP
  • cliente executado em plataforma com memória ilimitada e capacidade de processamento elevada
  • enlace com taxas de transmissão na ordem de Mbps
  • erros de bit são virtualmente inexistentes

3. Defina seu vocabulário, e também a codificação de mensagens a ser adotada

Exemplo
  • mensagem de dados:
    • identificação da placa de aquisição
    • número de sequência
    • sequência de tuplas (id_sensor,timestamp,valor)
  • mensagem de confirmação:
    • número de sequência confirmado
    • parâmetros operacionais da placa (opcional)

4. Descreva seu comportamento

Exemplo

PTC-Cliente simples.jpg
Cliente

Ao final, implemente esse protocolo usando seus conhecimentos sobre redes de computadores e sistemas distribuídos. OBS:

  • o canal de comunicação deve ser baseado em um protocolo de transporte. Isso elimina a possibilidade de usar protocolos de aplicação, tais como HTTP (e, por consequência, implementar algo na fa forma de web service ou coisa parecida)
  • as medições devem ser garantidamente entregues no servidor, e sem erros.

A implementação deve ser feita por meio de um protótipo composto por:

  • uma emulação da placa de aquisição de dados
  • o servidor de coleta de dados


A entrega da especificação e do protocolo implementado deve ser feita até dia 04/08.

03/08: Projeto 1: um protocolo de comunicação

Um protocolo de comunicação está relacionado aos mecanismos necessários para a entrega de mensagens entre duas aplicações quaisquer. Considerando uma arquitetura de redes em camadas como TCP/IP, protocolos de comunicação correspondem às camadas de enlace até transporte. Questões como garantia de entrega, controle de sequência, tratamento de erros, sincronização, estabelecimento e término de sessão, multiplexação e delimitação de mensagens, entre possivelmente outras, fazem parte do projeto de tais protocolos. Para introduzir o projeto de um protocolo de comunicação, o primeiro projeto da disciplina envolve um protocolo para estabelecimento de enlace sem-fio ponto-a-ponto.


Considere o caso de uma nova interface de rede sem-fio composta por um transceiver RF capaz de transmitir a distâncias de até 1 km. No caso de distâncias como essa, a taxa de transmissão possível de ser obtida é de 2400 bps, porém distâncias menores possibilitam taxas maiores, até um máximo de 19200 bps. Esse transceiver pode ser usado como uma interface serial do tipo UART. Portanto, com ele podem-se criar enlaces de média distância e baixas taxas de transmissão.


O transceiver RF usado como UART proporciona uma camada física, cuja interface de acesso a serviço oferece operações de envio e recepção de bytes. Nenhuma facilidade para delimitação de mensagens, endereçamento, sincronização e tratamento de erros é fornecida. De fato, tais serviços devem ser implementados em um protocolo de enlace que use esse transceiver como camada física.


O projeto 1 envolve o desenvolvimento de um protocolo de comunicação usando esse transceiver RF, de forma a oferecer um serviço de comunicação com essas características.

O transceiver RF APC 220

O transceiver RF a ser utilizado se chama APC 220. Alguns documentos podem ser úteis:

Um primeiro experimento


O primeiro contato com o transceiver RF envolve escrever um programa que transmita a mensagem Hello world! de um computador a outro usando um enlace sem-fio. Para isso, deve-se:

  1. Configurar dois transceivers RF
  2. Conectá-los a dois computadores diferentes usando adaptadores USB
  3. Testar a comunicação usando programa para comunicação serial (ex: gtkterm, picocom, minicom). OBS: ver esta observação sobre um detalhe quanto ao uso do transceiver via USB.
  4. Escrever um programa que se comunique por meio dos transceivers. Para isso podem ser úteis:


A serial modelada como uma classe C++
#ifndef SERIAL_H
#define	SERIAL_H

#include <termios.h>

class Serial {
public:
    Serial();
    Serial(const char * path, int rate);
    Serial(const Serial& orig);
    virtual ~Serial();
    int get() { return tty_fd;}
    bool cca();
    int write(const char * buffer, int len);
    int read(char * buffer, int len);
    int read(char * buffer, int len, bool block);    
    char read_byte();
private:
    int tty_fd;
};

#endif	/* SERIAL_H */
serial.h


#include <iostream>
#include "Serial.h"

using namespace std;

int main() {
  Serial rf("/dev/ttyUSB0", B9600);
  string msg = "um teste ...\r\n";
  char buffer[32];

  int n = rf.write(msg.c_str(), msg.size());

  cout << "Enviou " << n << " bytes" << endl;

  n = rf.read(buffer, 32);

  cout << "Recebeu " << n << " bytes: ";

  cout.write(buffer, n);

  cout << endl;
}
main.cpp: exemplo de uso da classe serial

Configuração no VirtualBox

O transceiver deve ser conectado a porta USB do computador. O Linux o reconhece e cria o arquivo de dispositivo /dev/ttyUSB0 a ele associado. Com isso a máquina virtual VirtualBox deve ser configurada da seguinte forma:

  1. Habilitar a primeira porta serial (COM1)
  2. O modo dessa serial deve ser Dispositivo no hospedeiro
  3. O caminho do dispositivo deve ser /dev/ttyUSB0

PTC-Vbox-serial.png

TAREFA: início do protocolo de enlace

Implemente a delimitação de mensagens do seu protocolo de enlace, de forma que mensagens de tamanho variável possam ser transmitidas e corretamente recebidas. Essas mensagens pode ter entre 8 e 512 bytes. Em seguida, use-as para transmitir um pequeno arquivo através do enlace sem-fio.

DICA: Ver capítulo 11 do livro "Comunicação de Dados e Redes de Computadores", de Behrouz Forouzan, ou capítulo 5 do livro "Redes de Computadores" de Andrew Tanenbaum.

10/08: Projeto 1: Sincronização e enquadramento


O enquadramento é uma função do protocolo de enlace responsável por delimitar quadros na interface com a camada física. Deve-se ter em mente que a camada física oferece um serviço de envio e recepção de sequências de bytes sem qualquer estrutura. Cabe à camada de enlace delimitar as unidades de dados de protocolo (PDU) dentro dessas sequências de bytes.

Existe mais de uma abordagem para delimitar quadros (ver mais no capítulo 11 de Data Communications and Computer Networks, de Behoruz Forouzan, e capítulo 5 de Redes de Computadores e a Internet, de James Kurose e Keith Ross):

Abordagem Descrição Exemplos
Quadros de tamanho fixo / duração definida Quadros têm sempre mesmo comprimento ou duração ATM, TDMA-based
Sentinela padrão de bits/bytes delimita quadros PPP, HDLC
Contador / duração Cabeçalho contém duração ou comprimento do quadro IEEE 802.11
Presença/ausência de portadora Ausência de portadora delimita quadros IEEE 802.11, IEEE 802.3

Atividade

  1. Escolha uma abordagem viável para o protocolo de enlace a ser desenvolvido
  2. Modele a abordagem usando uma máquina de estados finitos de comunicação, de forma a facilitar sua implementação. Alguns textos introdutórios sobre MEF:
  3. Implemente a abordagem escolhida

Implementação do enquadramento

A técnica de enquadramento escolhida é a do tipo sentinela. Mais especificamente, escolheu-se a versão dessa técnica implementada pelo protocolo PPP:

  • Usa-se a flag 7E (01111110) como delimitador de quadros
  • Usa-se um byte de escape 7D (01111101) para preenchimento de octeto
  • O transmissor deve fazer pelo menos o escape dos bytes 7E e 7D que aparecerem no conteúdo do quadro
    • Cada byte que sofrer o escape deve ser modificado por meio de um XOR 20. Ex: se o byte a sofrer escape for 7E, ele deve ser modificado para 5E (7E XOR 20 = 5E).


Uma máquina de estados para o receptor é esta:

PTC-20162-Fsm-rcv.jpg


Além disso, uma possível implementação dessa função do protocolo poderia ser esta:

#ifndef FRAMING_H
#define FRAMING_H

#include <cstdint>
#include "Serial.h"

class Enquadramento {
 public:
  Enquadramento(Serial & dev, int bytes_min, int bytes_max);
  ~Enquadramento();
 
  // envia o quadro apontado por buffer
  // o tamanho do quadro é dado por bytes 
  void envia(char * buffer, int bytes);
 
  // espera e recebe um quadro, armazenando-o em buffer
  // retorna o tamanho do quadro recebido
  int recebe(char * buffer);
 
 private:
  int min_bytes, max_bytes; // tamanhos mínimo e máximo de quadro
  Serial & porta;  
  char buffer[4096]; // quadros no maximo de 4 kB (hardcoded)
 
  enum Estados {Ocioso, RX, ESC};
 
  // bytes recebidos pela MEF até o momento  
  int n_bytes; 
 
  // estado atual da MEF
  int estado;
 
  // aqui se implementa a máquina de estados de recepção
  // retorna true se reconheceu um quadro completo
  bool handle(char byte);
 
};

#endif


algumas coisas para ajudar
#include <iostream>
//#include <iomanip>
#include <fstream>
#include <stdio.h>
#include <errno.h>
#include "Enquadramento.h"

using namespace std;

void dump(char * buffer, int len) {
   int m = 0, line = 0;

    while (m < len) {
        printf("%02X: ", line*16);

        for (int n=0; n < 16 and m < len; n++, m++) {
            int x = (unsigned char)buffer[m];
            printf("%02X ", x);
        }
        puts("");
        line++;
    }        
}

int main(int argc, char * argv[]) {
  Serial dev("/dev/ttyUSB0", B9600);

  Enquadramento proto(dev, 8, 32);
  char quadro[32];

  proto.envia("1234567890", 10);

  // ou:
  // int bytes = proto.recebe(quadro);
  // dump(quadro, bytes);
}
main.cpp para receber um único quadro

A implementação da máquina de estados

A declaração acima sugere implementar a MEF a partir do método handle da classe Enquadramento. Esse método deve ser executado para cada byte recebido, representando o tratamento de um evento pela MEF. Uma forma usual e direta de implementar uma MEF faz uso de uma estrutura do tipo switch-case. Essa abordagem se baseia em um modelo de programação estruturada. Basicamente ele depende de um algoritmo que executa um procedimento do sistema dependendo de seu estado atual e do evento. A seleção do procedimento se faz com uma estrutura switch-case. O exemplo abaixo mostra o esqueleto de uma MEF implementada usando essa técnica.

// o tratador de eventos de uma MEF hipotética
// A MEF aqui representada nada faz de útil ... 
bool Enquadramento::handle(char byte) {
  switch (estado) {
    case Ocioso: // estado Ocioso
      estado = RX; // muda para RX
      break;
    case RX: // estado RX
      estado = ESC; // muda para ESC
      break;
    case ESC: // estado ESC
      estado = Ocioso; // muda para Ocioso
      break;
  }
}


algumas coisas prontas para ajudar
int Enquadramento::recebe(char * buffer_out) {
  char byte;

  while (true) {
    porta.read(&byte, 1);

    if (handle(byte)) {
       memcpy(buffer_out, buffer, n_bytes);
       return n_bytes;
    }
  }
}
método Enquadramento::recebe


11/08: Projeto 1: enquadramento

O tipo de enquadramento escolhido na aula anterior primeiro foi implementado numa plataforma composta por um computador com Linux. O próximo passo é implementá-lo no Arduino, de forma que se consigam transmitir quadros entre Arduino e computador Linux.

A atividade de hoje envolve:

  1. Integrar o transceiver RF no Arduino, de acordo com este tutorial.
  2. Implementar o enquadramento no Arduino ... ver referência sobre uso da serial
  3. Transmitir quadros do Arduino para o computador Linux: esses quadros devem ter comprimento entre 8 e 256 bytes, e conter dados de fácil verificação
  4. Transmitir quadros do computador Linux para o Arduino
Um programa de teste para receber bytes da serial
#include <iostream>
#include "Serial.h"
 
using namespace std;
 
int main() {
  Serial rf("/dev/ttyUSB0", B9600);
  char buffer[32];
 
  int c = 0;
  while (true) { 
    n = rf.read(buffer, 32);     
    cout.write(buffer, n);
    c += n;
    if (c > 19) {
      c = 0;
      cout << endl;
    }
  }
}

DICA para desenvolvimento do protocolo

Para desenvolver o protocolo, há duas opções:

  1. Implementá-lo diretamente na plataforma proposta, composta por PC com Linux e Arduino: isso envolve implementar o protocolo no Linux e também no Arduino.
  2. Implementar um protótipo no Linux usando um programa emulador de link serial: isso faz uso do programa serialemu, que emula um link serial com determinada taxa de bits, BER e atraso de propagação. Para usá-lo deve-se fazer o seguinte:
    1. Obtenha o código-fonte do serialemu
    2. Descompacte o arquivo Serialemu.zip, e entre no subdiretório Serialemu.
    3. Compile-o com este comando:
      aluno@M2:~/Serialemu$ make
      
    4. Copie o programa compilado para algum subdiretório mais conveniente ... por exemplo, /home/aluno, e depois mude para esse subdiretório:
      aluno@M2:~/Serialemu$ cp -a dist/Debug/GNU-Linux/serialemu /home/aluno/
      aluno@M2:~/Serialemu$ cd /home/aluno
      aluno@M2:~/$
      
    5. Execute-o de forma que ele apresente suas opções de execução:
      aluno@M2:~$ ./serialemu -h
      Uso: ./serialemu [-b BER][-a atraso][-f][-B taxa_bits] | -h
      
      BER: taxa de erro de bit, que deve estar no intervalo  [0,1]
      atraso: atraso de propagação, em milissegundos.
      taxa_bits: taxa de bits em bits/segundo
      -f: executa em primeiro plano (nao faz fork)
      
    6. Execute-o então da forma desejada, selecionando a taxa de bits (default: ilimitada), BER (default: 0) e atraso de propagação (default: 0). O serialemu automaticamente vai para segundo plano (daemonize), liberando o terminal. Ex:
      aluno@M2:~$ ./serialemu -B 9600
      /dev/pts/17 /dev/pts/2
      aluno@M2:~$
      
      ... e anote os dois caminhos informados pelo serialemu: eles são as duas portas seriais que correspondem às pontas do link serial emulado.
    7. Execute seu protocolo usando essas portas seriais virtuais.


Mesma que se opte pelo uso do emulador de serial, deve-se notar que, ao final, o protocolo deve ser demonstrado na plataforma Linux + Arduino. Assim, o uso do emulador de serial tem por finalidade somente facilitar o desenvolvimento dos mecanismos básicos do protocolo.

17/08: Projeto 1: mecanismos básicos de um protocolo de comunicação

Um protocolo de comunicação pode incorporar um subconjunto de mecanismos elementares para que o serviço seja devidamente provido. Diferentes autores enumeram esses mecanismos, para fins de fácil compreensão. Por exemplo, Georg Holzmann no cap. 2 de Design and Validation of Computer Protocols identifica os seguintes tipos de mecanismos:

  • Início e término de intercâmbio de dados
  • Sincronização de transmissores e receptores
  • Detecção e correção de erros de transmissão
  • Formatação e codificação de dados

Mais especificamente, Controle de erros é detalhado no cap. 3 e Controle de fluxo no cap. 4.


Hartmut Konig, no cap. 5 de Protocol Engineering, 2nd ed. identifica estes mecanismos:

  • Controle de erros
  • Sincronização
  • Gerenciamento de conexão
  • Codificação e decodificação de PDU
  • Ajustes em tamanhos de PDUs
  • Sequenciamento
  • Controle de fluxo
  • Controle de taxa


Outro autor, Robin Sharp, no cap. 4 de Principles of Protocol Design, identifica estes mecanismos:

  • Controle de sequência e de erros
  • Controle de fluxo
  • Indicação de mudança de estado do par
  • Mudança de modo de serviço
  • Multiplexação e separação
  • Segmentação e remontagem
  • Priorização


Como se pode notar, essas classificações possuem sobreposições, porém também sensíveis diferenças. Por vezes é questão de nomenclatura, em outras de ênfase em aspectos de um protocolo. No nosso caso, devemos conhecer e entender esses mecanismos para identificar quais deles são úteis para nosso protocolo de comunicação. Para realizar essa análise, antes de mais nada devemos definir o serviço a ser provido por nosso novo protocolo.


Essas funções de protocolo, para melhor entendimento e definição, devem, sempre que possível, ser relacionadas com o conceito de camadas (ver, por exemplo modelo de referência OSI). A figura a seguir resume alguns termos elementares que aparecem em um modelo em camadas (N é o número da camada), sendo eles:

  • PDU: unidade de dados do protocolo (coloquialmente: pacote)
  • SDU: unidade de dados de serviço (payload, carga do pacote)
  • PCI: informações de controle do protocolo (cabeçalho)
  • SAP: ponto de acesso a serviço, ou uma identificação usada para requisitar serviços de um protocolo (ex: endereço IP para protocolo IP, port para protocolo TCP)

PTC-Sdu-pdu.png


Definição do protocolo

Especifique seu protocolo, iniciando pelo serviço a ser oferecido. Em seguida, enumere e descreva os mecanismos a serem utilizados para implementar as funções de seu protocolo.

Serviço:

  • protocolo de enlace ponto-a-ponto, para camada física do tipo UART
  • encapsulamento de mensagens entre 8 e 256 bytes
  • recepção de mensagens livres de erros
  • garantia de entrega
  • controle de acesso ao meio
  • conectado (estabelecimento de sessão)

24/08: Projeto 1: controle de erros


O controle de erros envolve:

  1. Detecção de erros: cada mensagem recebida deve ter verificada a integridade de seu conteúdo. Mensagens corrompidas devem ser rejeitadas (descartadas)
  2. Recuperação de erros: mensagens descartadas devido a erros podem ser recuperadas. Existem duas abordagens:
    • Correção de erros: mensagens incluem bits de redundância que possibilita, dentro de certos limites, corrigir bits corrompidos.
    • Retransmissão: mensagens descartadas são simplesmente retransmitidas

O protocolo de comunicação a ser desenvolvido no projeto 1 deve receber apenas mensagens corretas. Para isso, pelo menos deve ser implementada uma função de detecção de erros.

Detecção de erros

A detecção de erros implica adiciona à mensagem transmitida uma certa quantidade de bits de redundância. Os valores desses bits são determinados com base no conteúdo da mensagem. Na recepção, calcula-se novamente o valor dos bits de redundância e compara-se com os bits de redundância incluídos na mensagem. Se forem iguais, presume-se que mensagem está correta.

PTC-Erros.png
Bits de redundância para detecção de erros


A detecção de erros envolve basicamente a escolha de uma dentre duas técnicas:

  • Checksum (soma de verificação): os bits de redundância são calculados como uma soma de todos os octetos da mensagem.
  • CRC (verificação de redundância cíclica): os bits de redundância são calculados usando uma forma de álgebra polinomial.

Atividade

  1. Analise e escolha uma das técnicas para detecção de erros para seu protocolo.
  2. Implemente a técnica escolhida.


Exemplo da classe Enquadramento com métodos para cálculo e verificação de CRC
#ifndef FRAMING_H
#define FRAMING_H
 
#include <cstdint>
#include "Serial.h"
 
class Enquadramento {
 public:
  Enquadramento(Serial & dev, int bytes_min, int bytes_max);
  ~Enquadramento();
 
  // envia o quadro apontado por buffer
  // o tamanho do quadro é dado por bytes 
  void envia(char * buffer, int bytes);
 
  // espera e recebe um quadro, armazenando-o em buffer
  // retorna o tamanho do quadro recebido
  int recebe(char * buffer);
 
 private:
  int min_bytes, max_bytes; // tamanhos mínimo e máximo de quadro
  Serial & porta;  
  char buffer[4096]; // quadros no maximo de 4 kB (hardcoded)
 
  enum Estados {Q0, Q1, Q2};
 
  // bytes recebidos pela MEF até o momento  
  int n_bytes; 
 
  // estado atual da MEF
  int estado;
 
  // aqui se implementa a máquina de estados de recepção
  // retorna true se reconheceu um quadro completo
  bool handle(char byte);
 
  // verifica o CRC do conteúdo contido em "buffer". Os dois últimos 
  // bytes desse buffer contém o valor de CRC
  bool check_crc(char * buffer, int len);

  // gera o valor de CRC dos bytes contidos em buffer. O valor de CRC
  // é escrito em buffer[len] e buffer[len+1]
  void gen_crc(char * buffer, int len);

  // calcula o valor de CRC dos bytes contidos em "cp".
  // "fcs" deve ter o valor PPPINITFCS16
  // O resultado é o valor de CRC (16 bits)
  // OBS: adaptado da RFC 1662 (enquadramento no PPP)
  uint16_t pppfcs16(uint16_t fcs, char * cp, int len);

};
 
#endif

25/08: Projeto 1: Garantia de entrega no protocolo de enlace

A garantia de entrega pode ser definida como um serviço do protocolo que garante que uma mensagem foi entregue ao destino. Enquanto uma mensagem não tiver sua entrega assegurada, ela permanece na fila de saída mantida no transmissor pelo protocolo.


Mecanismos ARQ (Automatic Repeat reQuest) podem ser incorporados a protocolos para garantir a entrega de mensagens, preservando a ordem de envio e buscando eficiência no uso do canal. Tais mecanismos se baseiam em alguns elementos:

  • Mensagens de dados
  • Mensagens de confirmação positiva (ACK) e negativa (NAK)
  • Numeração de sequência de mensagens
  • Retransmissão de mensagens perdidas ou recusadas

Maiores detalhes podem ser lidos ndesta descrição de mecanismos ARQ, incluindo uma análise simplificada de seu desempenho.

Escolha do mecanismo ARQ a ser utilizado no protocolo de enlace

Qual dentre os mecanismos ARQ deve ser o mais apropriado para o tipo de enlace do protocolo ?


Dentre os três mecanismos ARQ elementares, pare-e-espere atende plenamente a necessidade de garantia de entrega e eficiência no uso do canal de comunicação do protocolo de enlace. Isso se conclui com a análise de utilização do canal usando pare-e-espere:












Portanto, a utilização do meio seria no máximo em torno de 0.99 (ou 99%).


Modelagem do mecanismo ARQ

O mecanismo ARQ pare-e-espere deve ser modelado antes de ser implementado no protocolo. Sua disposição na estrutura do protocolo deve ser esta:


PTC-Proj1-Protocolo-estrutura.jpg


Assim, a subcamada ARQ envia e recebe quadros de dados e de confirmação da subcamada inferior (detecção de erros). O mecanismo ARQ implementado nessa subcamada pode ser modelado como duas máquinas de estado finitas: uma para transmissão e outra para recepção:

PTC-proj1-Mef-arq-tx.jpg
MEF para a transmissão do ARQ


PTC-proj1-Mef-arq-rx.jpg
MEF para a recepção do ARQ


Duas questões despontam quanto à modelagem com máquinas de estados finitos:

  1. As MEF projetadas podem ser minimizadas (terem menos estados) ?
  2. As MEF podem ser combinadas em uma nova MEF que contenha o comportamento tanto do transmissor quanto do receptor ?

Para investigar essas questões, devem-se estudar máquinas de estados finitos no contexto de protocolos de comunicação. MEF é uma ferramenta de modelagem útil para representar o comportamento ou regras de procedimento (proceding rules) de protocolos.

Conjunto de mensagens (vocabulário) e regras de procedimento (gramática)

O protocolo proposto no projeto 1 deve ser especificado. Isso implica sua descrição sem ambiguidades, para que possa ser corretamente implementado. A especificação envolve a definição do serviço oferecido, seu vocabulário e sintaxe, sua gramática, e a verificação de seu comportamento. Sua implementação se chama entidade de protocolo, sendo composta por software e (por vezes) hardware.


PTC-Protocolo-1.png
Diagrama simplificado da entidade de protocolo


A entidade de protocolo a ser desenvolvida implica pelo menos:

  • Especificar e implementar a interface de acesso ao protocolo para seus usuários.
  • Definir um vocabulário de mensagens, e a sintaxe abstrata dessas mensagens.
  • Modelar as regras de procedimento, que determinam as sequências de mensagens e eventos aceitas pelo protocolo.
  • Especificar a codificação das mensagens, o que se denomina sua sintaxe concreta.

Vocabulário

Um protocolo envolve intercâmbio de mensagens entre duas ou mais entidades. O conjunto de mensagens que compõe o protocolo é chamado de vocabulário.

Ex: um protocolo do tipo stop-and-wait possui um vocabulário dado por:


Essas mensagens podem ser usadas em sequências como mostrado neste diagrama de sequência temporal:

PTC-Stop-wait.png


Cada mensagem carrega alguma informação composta por dados (o conteúdo gerado pela aplicação) e meta-dados (o conteúdo de controle gerado e usado pelo próprio protocolo). O formato das mensagens é dado pela 'sintaxe do protocolo, e existem diferentes métodos para sua descrição. Esse aspecto do protocolo deve ser trabalhado mais adiante.


Regras de procedimento

Intercâmbios de mensagens entre entidades de um protocolo devem respeitar regras quanto às sequências válidas de mensagens. O conjunto dessas regras é chamado de regras de procedimento (procedure rules).


Um protocolo é um sistema a eventos-discretos. Isso significa que as ações em um protocolo ocorrem em instantes pontuais, e não continuamente. Essas ações, ou acontecimentos, são chamadas de eventos. Por exemplo, a recepção ou envio de uma mensagem, a ocorrência de timeout, o início ou término de uma sessão são eventos ao longo de uma comunicação. Assim, um protocolo interage com seu ambiente (canal de comunicação, usuário), sendo acionado por eventos (ex: recepção de mensagem) que são respondidos com a realização de ações (ex: envio de mensagem). Seu comportamento depende do histórico de eventos passados, o que é chamado de estado. Esse tipo de sistema demanda modelos específicos para a descrição das sequências possíveis de eventos, incluindo a informação sobre o estado do protocolo.


Um diagrama de sequência temporal, como mostrado no exemplo do protocolo stop-and-wait, apesar de ilustrativo não pode ser usado para especificar as regras de um protocolo. Esse tipo de diagrama é útil para apresentar uma sequência específica de troca de mensagens, mas não todas as sequências possíveis. Quer dizer, ele não tem expressividade para especificar todas as possíveis sequências de mensagens durante as comunicações. Outros tipos de diagramas e métodos formais devem ser usados nesse caso.

Máquinas de Estados Finitos


O protocolo stop-and-wait usado como exemplo envia uma mensagem por vez, aguardando sua confirmação para enviar uma nova mensagem. Caso a confirmação não seja recebida, a última mensagem é retransmitida. Um exemplo de protocolo que usa esse tipo de mecanismo de controle de erros é o MAC CSMA/CA do padrão IEEE 802.11. Uma versão para o stop-and-wait usa um bit para numerar mensagens, de forma a evitar a duplicação de mensagens no receptor. Assim, seu vocabulário é composto pelas mensagens msg0, msg1, ack0, ack1. As regras de procedimento desse protocolo podem ser ilustradas usando diagramas de máquinas de estados finitos, como mostrado a seguir:


PTC-Stop-and-wait-fsm-tx.png
Máquinas de estados finitos do transmissor para o ARQ stop-and-wait
PTC-Stop-and-wait-fsm-rx.png
Máquinas de estados finitos do receptor para o ARQ stop-and-wait


Uma máquina de estados finitos (ou simplesmente MEF) é composta de um conjunto de estados (as bolas) e transições (as setas). Um estado representa uma instância de comportamento do sistema (ex: ocioso, espera), e uma transição representa uma mudança de estado. Uma transição possui um estado inicial e um ou mais estados finais, além de uma condição para que ocorra (a isso se chama evento). Esse modelo básico de MEF possui expressividade para modelar alguns sistemas, e apresenta diversas propriedades importantes para analisar o comportamento desses sistemas.

Formalmente, uma MEF é definida pela tupla (Q, q0, S, T), sendo:

  • Q: um conjunto não-vazio de estados
  • q0: um elemento de Q denominado estado inicial
  • S: um conjunto de eventos (ou mensagens), o qual forma um vocabulário
  • T: uma relação de transição de estados

A relação de transição de estados T é usualmente representada por uma tabela cujas linhas contêm o estado inicial, uma ação, e o estado final. No caso da MEF do transmissor do protocolo stop-and-wait, essa tabela poderia ser:

Estado inicial Ação Estado final
0 envia m0 1
1 recebe ack1 2
1 recebe ack0 0
1 timeout 0
2 envia m1 3
3 recebe ack0 0
3 recebe ack1 2
3 timeout 2


Para o projeto do protocolo de comunicação, a MEF tem duas finalidades:

  • Modelar as regras de procedimento do protocolo: a MEF torna possível conceber o comportamento do protocolo, definindo o que deve ser feito a depender das mensagens recebidas e transmitidas, entre outros eventos. Além disso, mecanismos internos do protocolo também podem ser modelados com MEF (ex: enquadramento).
  • Verificar o comportamento do protocolo: o projeto do protocolo pode esconder problemas sutis e difíceis de identificar. Existem técnicas e ferramentas que auxiliam na verificação da correção das regras de procedimento do protocolo, evidenciando problemas tais como impasses e perdas de sincronismo.

Num primeiro momento, as MEF serão utilizadas para modelar o protocolo de comunicação. Seu uso para verificar a correção do protocolo deve ser realizado após ter sido construído um protótipo.

Atividade: MEF da garantia de entrega do protocolo de comunicação

O protocolo de comunicação deve ser modelado com uma MEF. Algumas funções do protocolo podem ser convenientemente representadas com MEF, tais como:

  1. Enquadramento (framing)
  2. Garantia de entrega (mecanismo ARQ)
  3. Gerenciamento de conexão
  4. Controle de acesso ao meio

Tendo como base a garantia de entrega, modele-a com uma MEF. Para isso identifique os componentes da MEF capazes de representar as regras de procedimento dessa função do protocolo: estados, eventos e transições. Em seguida, faça um diagrama da MEF da garantia de entrega.

31/08: Projeto 1: finalização da primeira etapa

A primeira etapa do projeto do protocolo de comunicação envolveu criar um protótipo que se comunique por meio de um link serial emulado. O protótipo dessa etapa deve implementar o enquadramento, detecção de erros e a garantia de entrega com ARQ do tipo pare-e-espere. Para fim de avaliação, o código-fonte comentado e a respectiva documentação do software dessa primeira versão do protocolo devem ser entregues por meio do Moodle:


A modelagem da garantia de entrega

A garantia de entrega deve ser feita com um mecanismo ARQ do tipo pare-e-espere. A máquina de estados finitos a seguir resume o comportamento do ARQ para o protocolo de comunicação.


PTC-Mef-arq2.jpg

A MEF do ARQ: N é o número de sequência de envio, e M o de recepção


A notação dos rótulos das transições da máquina de estados é:

evento / ação

... sendo evento o evento que dispara a transição, e ação as ações a serem realizadas ao final da transição.


Tanto eventos quanto ações podem conter envios e recepções de mensagens. Envios são da forma:

canal!mensagem

E recepções são assim:

canal?mensagem

... sendo que canal pode ser omitido, se o canal por onde ocorre o envio ou recepção for evidente.

As sequências de processamento do protocolo

O protocolo até o momento apresenta funcionalidades elementares que o tornam capaz de estabelecer enlaces rudimentares. Dois blocos funcionais importantes foram definidos:

  • Framing (enquadramento): cuida da delimitação de quadros e da detecção de erros.
  • ARQ (garantia de entrega): define o formato de quadro e trata dos mecanismos para entrega e retransmissão.


PTC-Protocolo-estrutura2.jpg
A estrutura atual do protocolo


Presentemente apenas o enquadramento foi implementado e de forma simplificada. No caso, supôs-se que as pontas de enlace se revezam entre transmissões e recepções, de forma que as sequências de envio e recepção de cada lado do enlace são predefinidas. Em outras palavras, o protótipo atual do protocolo não é capaz de reagir assincronamente aos eventos a que está sujeito em situações usuais: a qualquer instante pode-se receber um quadro ou haver a necessidade de transmitir um quadro. Para que o protocolo seja de fato funcional, nenhuma suposição pode ser feita sobre as sequências de quadros recebidas e transmitidas, tampouco as velocidade relativas das aplicações que se comunicam por meio do enlace. Isso implica repensar o modelo de sistema do protocolo para que ele possa atender esses eventos.


A busca de soluções para o protocolo inicia com uma investigação sobre as sequências de processamento que podem ocorrer. A figura a seguir identifica três sequências representativas (há algumas outras, mas essas são esclarecedoras):

PTC-Sequencias-protocolo2.jpg
Algumas sequências de processamento do protocolo


Deve-se observar que as sequências apresentadas na figura possuem diferentes origens, como se pode ver pelas setas numeradas:

  • Envio: esta sequência inicia com (1) o surgimento de uma mensagem fornecida por uma aplicação. Essa mensagem deve então (2) ser encapsulada em um quadro de dados na subcamada ARQ. Em seguida, o quadro gerado em ARQ (3) deve ser delimitado e transmitido pela subcamada de enquadramento. Ao final, a subcamada de enquadramento (4) deve receber o quadro de confirmação transmitido pelo outro lado do enlace, e esse quadro (5) deve ser repassado para a subcamada ARQ.
  • Recepção: esta sequência inicia com (1) a recepção de um byte vindo do transceiver RF (subcamada PHY). Se, após a recepção desse byte, um quadro completo e livre de erros foi recebido, (2) ele é repassado à subcamada ARQ. Se for um quadro de dados, uma confirmação é gerada e transmitida via subcamada de enquadramento, conforme passos 3 e 4, e (5) o conteúdo do quadro é entregue à aplicação. Se for um quadro de confirmação, os passos 3 a 5 não são executados.
  • Timeout e retransmissão: na ocorrência de um (1) timeout na subcamada ARQ, o último quadro de dados é retransmitido, conforme passos 2 e 3.


Do ponto de vista do modelo de sistema, uma questão importante trata da identificação e tratamento dos eventos pelo protocolo. Conforme as sequências exemplificadas, há estes eventos:

  • mensagem vinda da aplicação: pode ser feita de forma síncrona, o que implica a aplicação enviar uma mensagem e ficar bloqueada até que o protocolo confirme sua entrega.
  • bytes vindos da camada física: pode ser feito de forma síncrona ou assíncrona. No primeiro caso, a aplicação deve esperar que o protocolo receba algum quadro. No segundo, quadros podem ser recebidos a qualquer momento e então serem notificados para a subcamada ARQ.
  • timeout: tempos máximos de espera devem interromper ações em andamento e serem tratados.


Assim, devem-se:

  1. Definir um modelo de software para o protocolo: do ponto de vista da estrutura, parece mais simples que cada subcamada seja implementada como uma classe, e que o protocolo como um todo seja uma composição de objetos dessas classes. Do ponto de vista do comportamento, deve-se especificar como a aplicação interage com o protocolo, e como os diferentes eventos são tratados pelo protocolo.
  2. Investigar técnicas para atendimento de eventos: o protocolo deve ser capaz de atender eventos conforme a necessidade. Isso envolve identificar mecanismos e facilidades apropriados da plataforma de software (sistema operacional, bibliotecas de programação).

14/09: Projeto 1: Timeouts

O protocolo de enlace projetado possui limites de tempo para suas sequências de ações em ao menos estes casos:

  • Enquadramento: há um limite de tempo para a recepção de um novo byte de um quadro. Se esse tempo for excedido, considera-se que os bytes restantes foram perdidos e assim o quadro incompleto deve ser descartado.
  • ARQ: há um limite de tempo para a recepção de uma confirmação de um quadro de dados. Se esse tempo for excedido, uma retransmissão deve ser feita.

Os prazos para realizações de ações, ou para esperas, se chamam timeouts. Do ponto de vista do comportamento do protocolo, um timeout é representado como um evento a ser tratado na máquina de estados correspondente. A programação de timeouts envolve interromper a execução normal do protocolo quando um prazo for excedido.


Timeouts podem ser implementados em sistemas POSIX de diferentes formas:

  1. Usando setitimer ou alarm combinado com sinal SIGALRM
  2. Usando timer_create combinado com um sinal definido pelo programador
  3. Usando select
  4. Em um loop que lê o relógio a cada iteração, desviando o fluxo de execução se o timeout ocorrer
  5. ... e possivelmente outras !


A ideia é escolher a técnica mais apropriada para o software em que o timeout será gerado. As técnicas 1 e 2 acima disparam aviso de timeout de forma assíncrona, assemelhando-se a interrupções por software. A técnica 3 avisa sobre um timeout de forma síncrona, sendo útil quando existe uma espera bloqueada com limite de tempo. A técnica 4 é genérica, e pode ser usada praticamente em qualquer sistema (não somente POSIX !), inclusive no Arduino. No caso de uso de threads, apenas as técnicas 2 e 3 se aplicam.

Implementação de timeout

Investigue as técnicas citadas, e escolha aquela que considerar adequada para uso em seu protocolo.

Timeout com alarm e signal

A função alarm programa um temporizador para o envio de um sinal do tipo SIGALRM. Esse sinal deve ser capturado e tratado, para que se obtenha o efeito de um timeout. A captura do sinal envolve usar a chamada de sistema signal, informando como parâmetros o tipo de sinal a ser capturado e uma função que funciona como tratador de sinal.

Exemplo de timeout com alarm e signal
#include <signal.h>
#include <unistd.h>
#include <iostream>

using namespace std;

// este é o tratador do timeout
void timeout(int s) {
  cout << "Timeout !!!" << endl;
  _exit(0);
}

int main() {
  char buffer[128];

  cout << "Digite algo no prazo de 5 segundos: ";
  cout.flush();

  // programa o tratador do timeout, o qual deve ser uma função
  // ou método estático de classe
  signal(SIGALRM,timeout);

  // programa um timeout de 5 segundos
  alarm(5);

  string algo;
  getline(cin, algo);

  // cancela o timeout
  alarm(0);

  cout << "Você digitou: " << algo << endl;

  return 0;
}

Timeout com timer_create e signal

A API Posix oferece timers (temporizadores), que são mais flexíveis que alarm (ou setitimer). Ao contrário de alarm, um processo pode possuir múltiplos timers Posix. Esses timers, ao dispararem, podem notificar um processo ou thread por meio de um sinal.

Exemplo de timeout com timer_create
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <iostream>

using namespace std;

// este é o tratador de sinal
void timeout(int s) {
  cout << "Timeout !!!" << endl;
  _exit(0);
}

int main() {
  sigevent_t ev;
  timer_t timer;

  // especifica o timer: ele vai gerar um sinal do tipo SIGUSR2
  ev.sigev_notify = SIGEV_SIGNAL;
  ev.sigev_signo = SIGUSR2;

  // cria o timer
  if (timer_create(CLOCK_REALTIME, &ev, &timer) < 0) {
    perror("");
    return errno;
  }
  
  // especifica que o timeout deve disparar uma única vez,
  // e após 5 segundos
  struct itimerspec ts = {{0, 0}, {5,0}};
  timer_settime(timer, 0, &ts, NULL);

  // programa o tratador de sinal
  struct sigaction int_handler;
  int_handler.sa_handler=timeout;
  sigaction(SIGUSR2,&int_handler,0);

  cout << "Digite algo no prazo de 5 segundos: ";
  cout.flush();

  string algo;
  getline(cin, algo);

  // cancela o timeout
  ts.it_value.tv_sec = 0;
  timer_settime(timer, 0, &ts, NULL);

  cout << "Você digitou: " << algo << endl;
  return 0;
}

Timeout com select

A chamada select possibilita esperar por dados em múltiplos descritores de arquivos, os quais podem ser arquivos abertos, dispositivos de entrada e saída ou mesmo sockets. Essa espera pode ser limitada a um prazo, desta forma tendo-se uma forma de timeout.

O exemplo a seguir mostra o uso de 'select' para esperar que o usuário digite alguma coisa. Se nada for digitado num prazo de 5 segundos, o programa termina com uma mensagem.

Exemplo de timeout com select
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <iostream>
#include <fcntl.h>
 
using namespace std;
 
int main() {
   int fd = 0; // o descritor de arquivo da entrada padrão
  
   // faz com que fd opere em modo não-bloqueante
   int op = fcntl(fd, F_GETFL);
   fcntl(fd, F_SETFL, op | O_NONBLOCK);
 
   // cria um conjunto de descritores
   fd_set r;
 
   // inicia o conjunto de descritores, e nele
   // acrescenta fd
   FD_ZERO(&r);
   FD_SET(fd, &r);
 
   cout << "Digite alguma coisa ... você tem 5 segundos: ";
   cout.flush();

   // chama select para monitorar o conjunto de descritores,
   // com timeout de 5 segundos
   // O valor de retorno de seelct é a quantidade de 
   // descritores prontos para serem acessados
   timeval timeout = {5,0};
   int n = select(fd+1, &r, NULL, NULL, &timeout);

   if (n > 0) { // algo foi digitado dentro do prazo 
     char buffer[1024];
     int n = read(fd, buffer, 1024);
     cout << "Você digitou: ";
     cout.write(buffer, n);
     cout << endl;
   } else {
    cout << "Timeout !!!" << endl;
   }
}

Diferenciação de eventos nas MEF do protocolo

O componente ARQ está sujeito a pelo menos três tipos de eventos:

  • payload: notificado quando a aplicação chama o método envia do Protocolo
  • quadro vindo do Enquadramento: notificado pelo Enquadramento por meio do método receive
  • timeout: notificado de alguma forma ainda a ser determinada


O componente Enquadramento, por sua vez, responde a dois eventos:

  • byte: notificado pela Serial por meio de seu método read
  • timeout: notificado de alguma forma ainda a ser determinada


Esses eventos devem ser tratados por meio das respectivas máquinas de estados. Para diferenciá-los, uma forma é declarar um tipo Evento privativo, o qual encapsule o tipo de evento e os dados a ele associados. Por exemplo, no caso do ARQ:

class ARQ {
 public:
   // métodos públicos

 private:
  enum TipoEvento {Payload, Quadro, Timeout};

  // esta struct descreve um Evento
  struct Evento {
    TipoEvento tipo;
    char * ptr;
    int bytes;

    // construtor sem parâmetros: cria um Evento Timeout
    Evento() { tipo = Timeout;}

    // construtor com parâmetros: cria um evento Payload ou Quadro
    Evento(TipoEvento t, char * p, int len) : tipo(t), ptr(p), bytes(len) {}
  };

  // executa a MEF, passando como parâmetro um evento
  void handle(Evento & e);
};

Atividade

  1. Remodele sua MEF para que possa diferenciar os eventos a serem tratados
  2. Implemente timeouts no Enquadramento
  3. Implemente timeouts no ARQ

21/09: Projeto 1: O controle de acesso ao meio

O enlace a ser estabelecido usa um canal compartilhado. Somente um dos lados do enlace pode transmitir em um determinado momento. Por isso é necessário que o protocolo inclua um mecanismo de controle de acesso ao meio (MAC) para arbitrar as transmissões no enlace.

Três abordagens para acesso ao meio podem ser identificadas (maiores detalhes estão no capítulo 5, seção 5.3, do livro Redes de Computadores e a Internet, 5a. edição, de James Kurose e Keith Ross):

  1. Por revezamento: usando mensagens especiais, o direito de acesso ao meio é definido inequivocamente para cada um dos lados alternadamente. Esse acesso ao meio é livre de disputa, o que significa que colisões não ocorrem.
  2. Por divisão de tempo: ambos os lados se mantêm sincronizados, e usam intervalos de tempo cíclicos predeterminados para suas transmissões (isso é uma forma de TDMA). Essa abordagem também é livre de disputa.
  3. Aleatória: cada um dos lados pode tentar transmitir em instantes arbitrários. Se transmissões se sobrepuserem (colisões), as mensagens envolvidas são perdidas e devem ser retransmitidas. Usam-se esperas de tempo aleatórias para evitar que as retransmissões causem novas colisões. Essa abordagem envolve disputa por acesso ao meio.


Para nosso protocolo, as abordagens 1 e 3 são viáveis. Para a abordagem 1, pode-se imaginar um acesso ao meio do tipo mestre-escravo. Para a abordagem 3, o acesso ao meio clássico Aloha poderia ser facilmente implementado.

Aloha

Um controle de acesso ao meio do tipo Aloha. Ele pode ser integrado ao ARQ:

PTC-Mef-arq-aloha.jpg


Mestre-escravo

Um controle de acesso ao meio do tipo Mestre-Escravo. Ele também pode ser integrado ao ARQ:

PTC-Mef-arq-ms.jpg

Atividade

  1. Escolha um dos MAC e incorpore-o a seu protocolo.

22/09: Gerenciamento de sessão

A próxima função do protocolo de enlace envolve o estabelecimento, manutenção e terminação de conexão. Sendo um protocolo ponto-a-ponto, deve-se estabelecer um enlace entre as duas pontas participantes antes de poder transferir dados. Isso evita que as transmissões entre um par de participantes sejam confundidas com transmissões de outros pares.

Nas seções 5.2 e 5.3 do capítulo 5 do livro Protocol Engineering, 2nd ed, de Hartmut Konig, há uma explicação sobre o gerenciamento de conexões e diversas formas de implementá-la. No caso do protocolo de enlace, serão usados estabelecimento e terminação explícitos de conexões, e manutenção de conexão.

Estabelecimento de conexão

Para criar uma conexão deve-se:

  • estabelecer a conexão: ambos participantes devem se sincronizar para aceitar ou recusar a conexão
  • negociar parâmetros de conexão: a conexão pode envolver parâmetros operacionais do protocolo que devem ser comuns a ambos participantes. Como exemplo, citam-se tamanho máximo de PDU e identificador de conexão.

A sincronização entre os participantes implica a troca de mensagens, a qual pode ser feita com duas (2-way handshaking) ou 3 (3-way handshaking) mensagens:


PTC-2-way 3-way.png


A sincronização do tipo 2-way é adequada em comunicações unidirecionais, mas não para comunicações bidirecionais. Isso se deve ao fato de que a primeira mensagem (CR) contém parâmetros de conexão do participante que iniciou o processo, e a segunda mensagem (CC) contém parâmetros do outro participante. Se a mensagem CC for perdida, o participante iniciador da conexão não tem como saber se a conexão foi aceita e quais os parâmetros definidos pelo outro participante. Assim, uma terceira mensagem enviada pelo iniciador serve para que o outro participante saiba que a conexão foi estabelecida, e que assim ele pode usá-la para enviar dados. De forma resumida:

  1. Ao receber a mensagem CC, o iniciador já pode enviar dados pela conexão
  2. Ao receber a terceira mensagem, o outro participante também pode enviar dados pela conexão. OBS: essa terceira mensagem pode ser uma mensagem de dados comum, pois ela serve para que o outro participante saiba que a conexão foi estabelecida com os parâmetros negociados.

Manutenção de conexão

Um enlace pode ter momentos de ociosidade, quando não há mensagens de dados para serem transmitidas. Isso pode ser confundido com casos em que o enlace se rompe (ex: um dos participantes é desligado, ou o meio de comunicação é seccionado). Para evitar que um enlace rompido seja interpretado com um enlace ocioso (e vice-versa), o protocolo deve fazer a manutenção de conexão.

A manutenção de conexão pode ser feita com o envio periódico de mensagens de verificação. Por exemplo, o protocolo PPP usa mensagens Keep-Alive enviadas a cada 10 segundos para monitorar o estado do enlace. Se três mensagens Keep-Alive consecutivas forem perdidas, o enlace é terminado. Nesse caso, após um certo tempo (ex: 30 segundos) o protocolo PPP tenta restabelecer o enlace. O protocolo TCP também possui um mecanismo opcional para manutenção de conexão chamado de Keep Alive. Uma abordagem como essa poderia ser incluída no protocolo de enlace em desenvolvimento.

Terminação de conexão

A terminação de conexão implica duas necessidades:

  1. A sincronização entre os participantes quanto ao término da conexão (ambos participantes devem fechar a conexão)
  2. A garantia de que todas as mensagens de dados pendentes sejam entregues


A solução não é tão simples quanto parece. Uma discussão detalhada pode ser lida na seção 5.3.3 do livro Protocol Engineering, 2nd ed, de Hartmut Konig. Com base nessa explicação e no fato de que o protocolo de enlace em desenvolvimento é bidirecional, deve-se usar a terminação de conexão feita por ambos participantes do enlace. Assim um participante envia uma mensagem para terminação de conexão, e após sua confirmação entra-se em estado de conexão parcialmente fechada (half-close connection). O outro participante envia suas mensagens pendentes, e em seguida envia sua mensagem de terminação de conexão. Após a confirmação dessa última mensagem de terminação, a conexão é considerada terminada. A figura a seguir exemplifica esse procedimento.


PTC-Half-close.png


Uma simplificação pode ser feita se ambos participantes encerrarem a conexão simultaneamente. Nesse caso, a terminação pode ser sincronizada com três mensagens (3-way handshake):


PTC-3-way-close.png


Por fim, pode acontecer de mensagens de terminação de conexão serem perdidas. Isso manteria um ou mesmo ambos participantes esperando indefinidamente pelo término de conexão. Uma solução para evitar essa situação é usar timeout para a espera de confirmação da mensagem de término de conexão.

Atividade

  1. Identifique em que parte da estrutura do seu protocolo devem se encaixar o gerenciamento de conexão
  2. Modele o gerenciamento de conexão do seu protocolo de enlace.

Conclusão do projeto 1

O protocolo desenvolvido no projeto 1 se destina a transportar dados entre placas de aquisição de dados e servidores de coleta. O protótipo se baseia em um Arduino UNO como placa de aquisição, e um PC como servidor de coleta. As funções do protocolo são:

  • Enquadramento
  • Garantia de entrega com ARQ do tipo pare-e-espere
  • Controle de acesso ao meio


A versão final do protocolo deve ser demonstrada com o Arduino e o PC, usando o transceiver RF como camada física. A apresentação do protocolo desenvolvido deve ser feita até dia 6/10, em horário a combinar com o professor. O relatório deve ser entregue segundo o modelo de artigo/relatório técnico disponibilizado pelo professor Emerson Mello.

28/09: Projeto 1: conclusão

05/10: Projeto 2: um protocolo de aplicação

Projeto 2: um protocolo de aplicação; especificação e modelagem informal do protocolo

  • Objetivo:
    • Propor um protocolo de aplicação para o sistema de aquisição de dados
    • Descrever formalmente o protocolo proposto

Protocolos de aplicação


Protocolos de aplicação têm a finalidade de efetuar intercâmbio de dados entre aplicações específicas. Por sua própria definição, são protocolos muito especializados quando comparados a protocolos de comunicação. Estes últimos oferecem serviços de comunicação mais genéricos, capazes de transportarem dados independente de suas representações e suportarem diferentes modos de interação. Protocolos de aplicação, por sua vez, são projetados para o intercâmbio de dados com representação específica, e para realizarem interações com características particulares entre aplicações.

Alguns protocolos de aplicação conhecidos são:


Protocolos de aplicação podem ser caracterizados em diferentes dimensões, porém duas delas são bastante evidentes:

  • Arquitetura da aplicação: entidades se comunicam com um protocolos segundo algum modelo de comunicação, o qual define as características das interações. Por exemplo: quem inicia a comunicação; quantas entidades podem se comunicar em uma sessão; como entidades se identificam ou se endereçam. Modelos bem conhecidos são cliente-servidor, P2P e publisher-subscriber.
  • Sintaxe e codificação: mensagens de um protocolo são estruturadas para conterem um ou mais dados, os quais podem estar representados de diferentes maneiras. Por exemplo, mensagens podem conter uma quantidade de dados fixa ou variável, dados de um único tipo ou de diferentes tipos, e dados podem ser representados codificados em ASCII, UTF-8 ou segundo alguma codificação binária.


No início da disciplina realizou-se um exercício sobre a especificação e implementação de um protocolo para aquisição de dados. Naquele momento o desenvolvimento do protocolo foi em larga medida informal. No projeto 2 o protocolo de aquisição de dados será retomado, porém seguindo as diretrizes para especificação e desenvolvimento de protocolos.

Especificação do protocolo

  • Objetivos:
    • definir o serviço a ser provido pelo protocolo
    • determinar o vocabulário do protocolo
    • especificar suas regras de procedimento usando máquinas de estado finitos


O protocolo de aplicação a ser desenvolvido deve ser especificado. Isso implica sua descrição sem ambiguidades, para que possa ser corretamente implementado. A especificação envolve a definição do serviço oferecido, seu vocabulário e sintaxe, sua gramática, e a verificação de seu comportamento. Sua implementação se chama entidade de protocolo, sendo composta por software e (por vezes) hardware.

Isso envolve:

  1. Descrever a comunicação envolvida na aplicação
  2. Definir o serviço a ser provido pelo protocolo, o que inclui a interface de acesso a serviço

Atividade: modelo do protocolo de aplicação com MEF

  1. Serviço:
  2. Defina o vocabulário do protocolo de aplicação, incluindo os tipos de mensagens e suas sintaxes.
  3. Modele o comportamento do protocolo de aplicação com uma MEF. Para isso identifique os componentes da MEF capazes de representar as regras de procedimento do protocolo: estados, mensagens e eventos, transições. Em seguida, faça um diagrama da MEF do protocolo.

06/10: Projeto 2: codificação das mensagens

vocabulário e gramática do protocolo; sintaxe: representação textual, ASN.1

Objetivos:

  • representar mensagens com sintaxe abstrata (ex: ABNF, ASN.1, ou outras)
  • codificar mensagens com sintaxe concreta

Cada mensagem que compõe o vocabulário de um protocolo carrega informações que dizem respeito a seu conteúdo (ou payload) e meta-dados do protocolo. Por exemplo, mensagens do protocolo TCP (chamadas de segmentos), como mostrado na figura a seguir, contêm diversas informações de controle que formam um cabeçalho, e um corpo de mensagem que contém os dados transportados pela mensagem.

PTC-Tcp-segment.gif


O TCP é um protocolo de transporte, e usa uma codificação binária em suas mensagens com representação big endian (todos protocolos de comunicação da Internet usam essa representação - ver RFC 1700). Já o protocolo de aplicação SMTP (Simple Mail Transfer Protocol) usa uma codificação textual em suas mensagens. O exemplo a seguir mostra a conversação entre um cliente (prefixo C:) e um servidor SMTP (prefixo S:).

S: 220 smtp.example.com ESMTP Postfix

C: HELO relay.example.org 
S: 250 Hello relay.example.org, I am glad to meet you

C: MAIL FROM:<bob@example.org>
S: 250 Ok

C: RCPT TO:<alice@example.com>
S: 250 Ok

C: RCPT TO:<theboss@example.com>
S: 250 Ok

C: DATA
S: 354 End data with <CR><LF>.<CR><LF>

C: From: "Bob Example" <bob@example.org>
C: To: "Alice Example" <alice@example.com>
C: Cc: theboss@example.com
C: Date: Tue, 15 January 2008 16:02:43 -0500
C: Subject: Test message
C: 
C: Hello Alice.
C: This is a test message with 5 header fields and 4 lines in the message body.
C: Your friend,
C: Bob
C: .
S: 250 Ok: queued as 12345

C: QUIT
S: 221 Bye


Outro protocolo de aplicação muito utilizado é o HTTP (Hypertext Transfer Protocol), que usa uma representação híbrida em suas mensagens (predominantemente textual, mas pode ter conteúdo binário).

Requisição (enviada pelo cliente) Resposta (enviada pelo servidor)
GET /~msobral/ptc/docs/ HTTP/1.1
Host: tele.sj.ifsc.edu.br
HTTP/1.1 200 OK
Date: Mon, 21 Sep 2015 17:04:04 GMT
Server: Apache
Vary: Accept-Encoding
Content-Length: 870
Content-Type: text/html;charset=UTF-8

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
 <head>
  <title>Index of /~msobral/ptc/docs</title>
 </head>
 <body>
<h1>Index of /~msobral/ptc/docs</h1>
<table><tr><th><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a>
</th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a 
href="?C=D;O=A">Description</a></th></tr><tr><th colspan="5"><hr></th></tr>
<tr><td valign="top"><img src="/icons/back.gif" alt="[DIR]"></td><td><a href="/~msobral
/ptc/">Parent Directory</a></td><td>&nbsp;</td><td align="right">  - </td><td>&nbsp;</td></tr>
<tr><td valign="top"><img src="/icons/layout.gif" alt="[   ]"></td><td><a 
href="state.pdf">state.pdf</a></td><td align="right">08-Sep-2015 16:47  </td><td 
align="right">130K</td><td>&nbsp;</td></tr>
<tr><th colspan="5"><hr></th></tr>
</table>
</body></html>


Esses exemplos apresentam basicamente dois tipos de codificação:

  1. Textual: as informações são representadas em texto legível, o que facilita a interpretação por seres humanos porém gera mensagens mais longas. A representação em texto pode facilitar também o intercâmbio de informações entre computadores com diferentes arquiteturas e aplicações escritas com diferentes linguagens de programação. Por fim, um detalhe importante é a codificação de caractere adotada (ex: no caso do HTTP apresentado, usa-se codificação UTF-8).
  2. Binária: as informações são representadas por sequências de bits, segundo algum esquema de codificação, o que gera potencialmente mensagens mais curtas, porém de difícil legibilidade por seres humanos. A representação binária deve seguir regras estritas para o correto intercâmbio entre computadores de diferentes arquiteturas e programas escritos em diferentes linguagens de programação.


Hoje será estudado como cada tipo de representação pode ser especificado.

Sintaxe abstrata

Sintaxe abstrata é a especificação do conjunto de valores que compõem uma estrutura de dados, independente de linguagem de programação e arquitetura de hardware. Por exemplo, para representar um ativo em negociação identificaram-se como informações necessárias o nome do ativo, seu código de negociação, seu valor monetário e a data e horário desse valor. Assim pode-se especificar uma estrutura de dados que represente esse ativo, sendo ela composta de:

NOME: cadeia de caracteres com comprimento máximo de 16 caracteres
CODIGO: numero inteiro
VALOR: numero inteiro (quantidade de centavos)
DATA: string numerica com 8 caracteres (DDMMYYYY)
HORARIO: string numerica com 6 caracteres (HHMMSS)


A descrição acima é a sintaxe abstrata da estrutura de dados. Ela descreve que valores compõem a estrutura, e quais os tipos desses valores. No entanto, ela não especifica sua sintaxe concreta, que determina como essa estrutura de dados é representada, ou codificada, durante uma transmissão. Sua codificação pode ser feita de diferentes formas, sendo alguns exemplos:

  • Com uma representação textual XML
  • Com uma representação binária compacta little-endian ou big-endian, composta por números inteiros de 32 bits, caracteres ASCII ou UTF-8
  • Com uma representação binária do tipo TLV (Type-Length-Value)
  • Com uma representação textual em CSV ou JSON


Ambos conceitos são de grande importância no projeto de protocolos. A sintaxe abstrata especifica as estruturas de dados existentes nos intercâmbios realizados pelo protocolo. Essa especificação deve ser independente de arquitetura de computador ou linguagem de programação, podendo ser traduzida para uso em qualquer plataforma de hardware e/ou software, para que o protocolo possa ser usado nessas plataformas. A sintaxe concreta especifica como essas estruturas de dados devem ser codificadas para transmissão. Dessa forma, sistemas em comunicação podem estar em acordo quanto ao significado dos dados transmitidos. Dada sua importância, existem padrões para especificação de sintaxe abstrata e sintaxe concreta, sendo duas delas proeminentes:

... e algumas outras propostas por empresas ou grupos de desenvolvimento de software correndo por fora:

  • Protocol-buffers: linguagem de especificação de sintaxe abstrata criada pelo Google. Possui suporte a várias linguagens de programação, como Java, Python e C++. Há um projeto relacionado para uma biblioteca com pequeno footprint.
  • MessagePack: define um formato de serialização binária para sintaxe concreta. Não possui uma notação para sintaxe abstrata: a serialização é feita diretamente a partir de valores representados na linguagem de programação alvo. Suporta muitas linguagens, tais como C, C++, Python e Java.

ASN.1


De acordo com a Introdução a ASN.1, Abstract Syntax Notation number One é um padrão que define um formalismo para a especificação de tipos abstratos de dados. ASN.1 não é uma linguagem de programação, e sim uma notação para especificar tipos de dados.


A notação fornece um certo número de tipos básicos predefinidos, tais como:

  • INTEGER: números inteiros
  • BOOLEAN: valores booleanos
  • IA5String, UniversalString, NumericString, PrintableString: cadeias de caracteres
  • BIT STRING: cadeias de bits
  • OBJECT IDENTIFIER: identificador de objeto


Ela torna possível a definição de tipos de dados, tais como:

  • SEQUENCE: estruturas de dados (struct)
  • SEQUENCE OF: listas
  • CHOICE: escolha entre valores


Uma definição em ASN.1 pode ser mapeada para uma estrutura de dados em linguagem de programação como C, C++, Java e outras (Python, Perl, ...). Assim, essa estrutura de dados pode ser usada em software que implementa um protocolo. A codificação e decodificação entre essa estrutura de dados e os dados transmitidos e recebidos deve ser feita por meio de algoritmos disponibilizados em bibliotecas. Com isso, os dados em transmissão podem ser representados de diferentes maneiras, como TLV, XML e outras, independente da especificação da estrutura de dados.

A conversão da especificação em ASN.1 para uma linguagem de programação específica deve ser realizada com um compilador ASN.1. Existem ferramentas como essa para diversas linguagens, como C, C++ e Java (e mesmo Python). Em particular, existe um compilador ASN.1 gratuito capaz de traduzir as especificações para linguagem C, além de prover uma bilbioteca para codificação e decodificação.


O exemplo sobre a descrição de ativos pode ser usado para introduzir a linguagem ASN.1. A estrutura de dados Ativo pode ser especificada usando a declaração mostrada na coluna da esquerda. A estrutura de dados em linguagem C gerada pelo compilador ASN.1 está na coluna da direita:

ASN.1 Linguagem C
Module-Exemplo DEFINITIONS AUTOMATIC TAGS ::=
BEGIN

Ativo ::= SEQUENCE {
  nome PrintableString (SIZE(1..16)),
  codigo INTEGER,
  valor INTEGER,
  data NumericString(SIZE(8)),
  horario NumericString (SIZE(6))
}

END
/* Ativo */
typedef struct Ativo {
        PrintableString_t        nome;
        long     codigo;
        long     valor;
        NumericString_t  data;
        NumericString_t  horario;
        
        /* Context for parsing across buffer boundaries */
        asn_struct_ctx_t _asn_ctx;
} Ativo_t;

A documentação do compilador ASN.1 e uma demonstração de como escrever um programa que use uma API ASN.1 está aqui:


De forma alternativa, existem compiladores ASN.1 para outras linguagens de programação, como este:

Atividade

  1. Seja um tipo de mensagem contendo estes dados:
    id: número inteiro
    nome: string composta por caracteres imprimíveis
    valores: lista de números inteiros
    timestamp: data e horário
    
    ... especifique-a com ASN.1.
  2. Compile sua especificação usando o Compilador ASN.1
  3. Escreva dois programas de teste:
    • Um programa deve codificar mensagens, gravando-as em um arquivo
    • O outro programa deve decodificá-las, carregando os dados do arquivo e transformando-os em instâncias das mensagens.

15/10: Projeto 2: ASN.1

Continuação da atividade sobre especificação ASN.1 e codificação de mensagens

16/10: Projeto 2: ASN.1

Para exercitar o uso de ASN.1 para especificação da sintaxe do protocolo, a seguinte atividade deve ser realizada:

  • Transmissão e recepção de mensagens: escreva um programa transmissor e outro receptor, os quais se comunicam por uma conexão TCP. O transmissor envia uma sucessão de mensagens, as quais devem ser decodificadas e apresentadas pelo receptor. Use as mensagens do seu protocolo de aplicação.

Revisão sobre sockets TCP


O uso sockets TCP usando a API de sockets pode ser resumida nos seguintes passos:

Cliente:

  1. Criar um socket (socket)
  2. Vinculá-lo a um endereço e port (bind)
  3. Conectá-lo a um servidor (connect)
  4. Enviar e receber dados (send, recv)
  5. Encerrá-lo (close)


Servidor:

  1. Criar um socket (socket)
  2. Vinculá-lo a um endereço e port (bind)
  3. Ativá-lo para que possa receber conexões (listen)
  4. Receber conexão (accept) ... isso cria um novo socket automaticamente, através do qual se pode:
    1. Enviar e receber dados (send, recv)
    2. Encerrá-lo (close)
  5. Encerrar o socket principal (close) ... isso impede a recepção de novas conexões, mas não encerra as conexões já aceitas e em andamento.


Exemplos existem nos tutoriais indicados nos links no início desta seção. Porém para facilitar o uso de sockets foram criadas algumas classes C++:


As classes são TCPClientSocket e TCPServerSocket. A descrição de suas interfaces está em TCPBaseSocket.h (ver comentários). Objetos dessas classes funcionam como sockets, porém com simplificações (não são necessários todos os passos da API de sockets do sistema operacional). Existe um exemplo de utilização nos arquivos client.cpp e server.cpp:

Exemplo de cliente
#include <cstdlib>
#include <iostream>
#include "TCPBaseSocket.h"

using namespace std;

/*
 * 
 */
int main(int argc, char** argv) {
    TCPClientSocket client;
    
    // conecta no servidor da wiki (pode-se usar o nome DNS ou 
    // o endereço IP do servidor)
    client.connect("wiki.sj.ifsc.edu.br", 80);
    
    // faz uma requisição HTTP
    client.send("GET / HTTP/1.1\n\n");
    
    // recebe e mostra a resposta do servidor
    string resp = client.recv(10240);
    
    cout << resp << endl;
    
    return 0;
}
Exemplo de servidor
#include <cstdlib>
#include <iostream>
#include "TCPBaseSocket.h"

using namespace std;
/*
 * 
 */
int main(int argc, char** argv) {
    // cria um socket servidor que recebe conexões no port 8000
    TCPServerSocket server(8000);
    
    // fica eternamente recebendo novas conexões
    // e dados de conexões existentes
    while (true) {
        try {
            // aguarda uma nova conexão ou dados em 
            // uma conexão existente
            Connection & sock = server.wait(0);

            string addr;
            unsigned short port;

            // obtém o IP e port do socket da outra ponta da 
            // conexão
            sock.get_peer(addr, port);
           
            // se for nova conexão, apenas mostra IP e port da outra ponta
            if (sock.isNew()) {                
                cout << "Nova conexão: " << addr << ':' << port << endl;
            } else { 
              // tenta receber até 1024 bytes no socket retornado
              // por "wait"
              string data = sock.recv(1024);

              // conseguiu ler algo desse socket ...
              if (data.size()) {                
                // ...mostra-os na tela e envia-os de volta
                // para a outra ponta da conexão
                cout << "recebeu de " << addr << ':' << port;
                cout << ": " << data << endl;
                
                data = "recebido: " + data;
                sock.send(data);
              }
            }
        } catch (TCPServerSocket::DisconnectedException e) {
            // esta exceção informa que uma conexão foi encerrada
            // o socket correspondente foi invalidado automaticamente
            cout << e.what() << ": " << e.get_addr() << ':';
            cout << e.get_port()<< endl;
        }
    }
    
    return 0;
}
Diagrama de classe da biblioteca Socket++

O diagrama de classes a seguir mostra as classes envolvidas:

PTC-UML-Socket++.jpg

... mas quem quiser tentar (ou conhecer) algo mais completo, pode experimentar a biblioteca Boost.Asio.

TAREFA

1. Crie dois programas: um deles gera mensagens de um protocolo hipotético, codifica-as com DER e envia-as por um socket TCP, e outro recebe mensagens codificadas de um socket TCP, decodifica-as e mostra-as na tela (apresenta primeiro o tipo de mensagem recebida seguido de seu conteúdo). Exemplo de especificação de mensagens:

Teste1 DEFINITIONS AUTOMATIC TAGS ::=
BEGIN

Mensagem ::= SEQUENCE {
  id INTEGER,
  nome PrintableString (SIZE(1..16))
}

END

... e de utilização:

demo.cc
#include <parser_Teste1.h>
#include <iostream>
#include <sstream>

using namespace std;

int main() {
  TMensagem pkt;

  pkt.set_id(1234);
  pkt.set_nome("manoel");

  // verifica se os valores contidos na estrutura de dados respeitam
  // a especificação
  pkt.check_constraints();

  // mostra a estrutura de dados na tela
  cout << "Estrutura de dados em memória (antes de codificação XER):" << endl;
  pkt.show();

  // cria o codificador
  ostringstream out;
  TMensagem::XerSerializer encoder(out);

  // codifica a estrutura de dados
  encoder.serialize(pkt);

  cout << endl;
  cout << out.str() << endl;

  // cria o decodificador
  istringstream inp(out.str());
  TMensagem::XerDeserializer decoder(inp);

  // tenta decodificar uma estrutura de dados
  TMensagem * other = decoder.deserialize();

  cout << endl;

  if (other) {
    cout << "Estrutura de dados obtida da decodificação XER:" << endl;
    other->show();
  } else cerr << "Erro: não consegui decodificar a estrutura de dados ..." << endl;

  // devem-se destruir explicitamente as estruturas de dados obtidas 
  // do decodificador 
  delete other; 
  
}


2. Estenda seu programa para que as mensagens possam ser de dois tipos:

Mensagem DEFINITIONS AUTOMATIC TAGS ::=
BEGIN

Login ::= SEQUENCE {
  usuario PrintableString (SIZE(1..12)),
  senha PrintableString (SIZE(4..12))
}

Dados ::= SEQUENCE {
  seq INTEGER,
  x INTEGER,
  y INTEGER
}

Mensagem ::= SEQUENCE {
  id INTEGER,
  msg CHOICE {
    inicio Login,
    dados Dados
  }
}
 
END

... exemplo de utilização:

  TMensagem pkt;

  // define o valor do campo "id"
  pkt.set_id(8);

  // Acessa o campo choice, que será referenciado
  // pela variável "msg" (note o operador &)
  TMensagem::Choice_msg & msg = pkt.get_msg();

  // Dentro de "msg" acessa-se "inicio": isso faz
  // com que o campo choice contenha um valor do tipo
  // "Login" (não pode mais ser mudado).
  TLogin login = msg1.get_inicio();

  // define os valores dos campos de "inicio"
  login.set_usuario("aluno");
  login.set_senha("blabla...");

26/10: Projeto 2: ABNF


ABNF (Augmented Backus-Naur Form) é uma notação sintática definida pelo IETF para especificar mensagens de protocolos de aplicação da Internet. Ela é usada para especificar mensagens dos protocolos HTTP, SIP, SMTP, IMAP4, entre outros. ABNF possibilita especificar uma gramática para mensagens codificadas textualmente. Portanto, ela se aplica a protocolos cuja sintaxe concreta é uma representação em texto.


Um exemplo de especificação ABNF segue abaixo. Ela representa um endereço de e-mail no formato nome@algum.dominio:

email=trecho arroba 1*8(trecho ".") trecho
trecho=1*16ALPHA
arroba=%d64


Uma especificação ABNF é composta por uma ou mais regras. Cada regra possui um nome e uma declaração. No exemplo acima, há três regras: email, trecho e arroba. Cada regra é declarada da seguinte forma:

regra = elementos crlf

... sendo elementos formado por um ou mais nomes de regras ou valores terminais, e crlf corresponde à sequência \r\n (carriage return + newline). Um valor terminal corresponde a uma sequência de um ou mais caracteres (i.e. a uma string).


As regras de composição de regras estão descritas de forma sucinta na própria RFC 5234.

Compilador ABNF


A geração de mensagens com base em uma gramática ABNF pode ser feita diretamente e de forma simples. Basicamente trata-se de gerar mensagens de texto compostas por valores devidamente representados. No entanto, o processo inverso é um pouco mais difícil. A identificação de mensagens recebidas, e a extração dos valores relevantes ali representados, evolve a utilização de um parser ABNF. Um parser é um programa que verifica um dado de entrada de acordo com uma gramática, decompondo esse dado de entrada em seus elementos de acordo com essa gramática.

Um parser ABNF deve ser capaz de seguir uma gramática ABNF qualquer, e usá-la para reconhecer e decompor mensagens. Usualmente se obtém um parser por meio de um gerador de parser, o qual gera um parser específico para uma dada gramática. O parser gerado é fornecido como código-fonte em alguma linguagem de programação. Existem alguns geradores de parser ABNF na Internet, tais como:

... porém eles não se mostraram apropriados para uso na disciplina (ao menos não com C++). Sendo assim, foi desenvolvido um gerador de parser próprio chamado de ABNF++. Em sua página na wiki há a documentação sobre sua utilização.

Outras sintaxes/codificações

Além de ASN.1 e ABNF, que são padronizadas, existem outras formas de especificar, representar e codificar mensagens. Abaixo seguem algumas possibilidades:

Atividade

  1. Experimente utilizar o gerador de parser para criar um parser para a gramática do exemplo apresentado no início da aula. Em seguida, teste-o com alguns endereços de e-mail.
  2. O reconhecimento de URI é uma tarefa mais complexa. Crie um parser a partir desta gramática, e teste-o com diferentes URI.
  3. Seu protocolo de aplicação poderia ser especificado com ABNF ? Compare a codificação das mensagens com ASN.1 e ABNF, considerando a facilidade para representação das informações e de utilização do esquema de codificação.

09/11: Conclusão do Projeto 2

16/11: Verificação de protocolos


Um protocolo pode ser entendido como um conjunto de regras para interação de processos concorrentes. Um aspecto essencial de um protocolo é o conjunto de sequências de mensagens e ações permitidas. Por exemplo, no protocolo de enlace PTP as sequências a seguir são aceitáveis (prefixo C: identifica uma ponta do enlace, e S: a outra ponta):

  • C:DATA_0, S:ACK_0, C:DATA_1, S:ACK_1
  • C:DATA_0, S:ACK_0, S:DATA_0, C:ACK_0, C:DATA_1, C:DATA_1, S:ACK_1

... mas estas não:

  • C:DATA_0, S:ACK_1, C:DATA_1, S:ACK_0
  • C:DATA_0, S:DATA_0, C:ACK_0, S:ACK_0


As regras de procedimento devem manter sempre o estado do protocolo consistente em todas as entidades de protocolo envolvidas (no exemplo, tanto no cliente quanto no servidor). Isso não é simples de garantir, mesmo com protocolos de baixa complexidade. Tome-se o exemplo de um protocolo para transferência de sequências de caracteres por linhas analógicas, descrito na seção 2.3 do capítulo 2 do livro Design and Validation of Computer Protocols, chamado de protocolo Lynch.


O propósito do protocolo Lynch é transferir arquivos de texto como sequências de caracteres através e uma linha telefônica, com proteção contra erros, supondo que todos erros de transmissão possam ser detectados. Comunicações são full-duplex, e confirmações positivas e negativas são empregadas para implementar um mecanismo de garantia de entrega de mensagens. Cada mensagem possui duas partes: o dado transmitido e informações de controle que se aplica às transmissões em sentido reverso. Supõe-se que mensagens possam ser distorcidas, mas não perdidas, inseridas, duplicadas ou reordenadas, e que exista um módulo de mais baixo nível (ex: encoder/decoder) que transforme uma mensagem distorcida em uma mensagem especial do tipo err. O vocabulário do protocolo é formado por três mensagens:

  • ack: composta por um dado e uma confirmação positiva do dado recebido previamente em sentido contrário.
  • nak: composta por um dado e uma confirmação negativa do dado recebido previamente em sentido contrário.
  • err: mensagem especial que representa uma mensagem recebida com erro.


As regras de procedimento desse protocolo são:

  1. Se a recepção anterior foi livre de erros, a próxima mensagem no canal reverso deve incluir uma confirmação positiva. Se a recepção anterior possuir erro, deve-se incluir uma confirmação negativa.
  2. Se a recepção anterior incluir uma confirmação negativa, ou a recepção anterior possuir erro, deve-se retransmitir a mensagem no canal reverso. Caso contrário, obtém-se uma nova mensagem para transmissão


Essas regras podem ser representadas por diferentes tipos de diagramas, dentre eles este diagrama SDL:


PTC-Lynch2.png
Diagrama SDL para o protocolo Lynch


Como se pode verificar se esse protocolo funciona a contento ? Em outras palavras, que ele possibilite a comunicação livre de erros entre duas entidades ? Antes de mais nada, deve-se definir o que se entende por funcionar a contento. No caso do protocolo Lynch, seu correto funcionamento pode ser expresso por estas propriedades:

  • Garantia de entrega: toda mensagem enviada por uma entidade do protocolo no final das contas é recebida pela outra entidade do protocolo.
  • Duplicatas descartadas: Mensagens duplicadas não são aceitas


Parece simples verificar se essas propriedades sempre valem a longo da operação do protocolo. O protocolo é de baixa complexidade, e isso aparentemente deve ser fácil de conferir.

Atividade

Verifique se as propriedades do protocolo Lynch sempre valem ...


Modelos para verificação

A verificação de propriedades de um protocolo envolve demonstrar que suas propriedades desejadas nunca são violadas. Existem diferentes técnicas para realizar esse tipo de verificação, tais como álgebra de processos, verificação de modelos e até mesmo simulação (se bem que esta última não é capaz de fornecer um veredito definitivo). Nesta disciplina será estudada a verificação de modelos, ou model checking, como é mais conhecida.


A verificação de modelos envolve a criação de um modelo que represente o sistema a ser verificado, a identificação de propriedades desejadas expressadas de acordo com esse modelo, e a busca automática e exaustiva por violações dessas propriedades. Assim, três elementos estão envolvidos:

  1. Um modelo do comportamento do sistema, expressado segundo algum formalismo: tratando-se de um sistema a eventos discretos, um protocolo pode ser modelado de acordo com alguma construção matemática para sistemas de estados finitos. Máquinas de estados finitos se aplicam a essa necessidade, porém alguns incrementos em relação às máquinas de estados vistas anteriormente são necessários. Em particular, o modelo do sistema em que se aplica um protocolo envolve duas ou mais máquinas de estados que se comunicam. Isso pode ser descrito implicitamente por alguma linguagem de programação de propósito específico.
  2. Um conjunto de propriedades desejadas do sistema: as propriedades correspondem a combinações de condições baseadas em atributos do modelo, ou sequências de eventos.
  3. Uma ferramenta que automatize a busca exaustiva de violações de propriedades dentro do modelo: tal ferramenta deve explorar todas as possíveis sequências de eventos no modelo, verificando as propriedades desejadas a cada passo.


Para introduzir a verificação de modelos, um exercício sobre o protocolo Lynch será realizado. Mas antes é necessário conhecer uma linguagem de modelagem para verificação de sistemas e uma ferramenta de verificação que se baseie nessa linguagem.

A linguagem PROMELA e o verificador SPIN

A linguagem PROMELA (Process Meta-Language), conforme introduzida no capítulo 2 e melhor descrita no capítulo 3 do livro The SPIN Model Checker (PDF), é uma linguagem para modelagem de sincronização e coordenação de processos concorrentes. Sendo voltada para verificação de sistemas, essa linguagem não se aplica a expressar algoritmos em geral, tampouco calcular coisas. Seus elementos básicos são:

  • Processos assíncronos
  • Canais de mensagens bufferizados ou não
  • Declarações de sincronização
  • Dados estruturados

Intencionalmente não há noção de tempo, não existe um relógio a ser consultado, não existem números em ponto flutuante e há poucas funções computáveis. Esse desenho enxuto tornam a mdelagem de sistemas concorrentes relativamente simples com a linguagem (ao menos essa é a intenção). A execução e verificação de programas escritos em PROMELA se faz com um verificador de modelos chamado SPIN (Simple PROMELA Interpreter). Essa ferramenta possibilita executar modelos de forma a simulá-los ou a verificá-los exaustivamente.


A modelagem de sistemas envolve portanto a escrita de programas em PROMELA. Assim, a seguir se apresenta uma introdução sobre a linguagem com base em seus elementos básicos.

A instalação do SPIN deve seguir estes passos:

  1. Instale os softwares bison e flex. No Ubuntu ou Debian isso se faz assim:
    sudo apt-get install bison flex
    
  2. Obtenha o código-fonte do SPIN
  3. Descompacte-o em algum diretório
  4. A partir desse diretório, em um terminal entre em Spin/Src6.4.3
  5. Execute o comando make
  6. Ao final, o programa spin deve ter sido compilado.

Processos assíncronos

Um processo é declarado com a palavra-chave proctype, seguida de seu nome. O processo abaixo apresenta o famigerado hello world na saída padrão e em seguida termina.


active proctype hello() {
  printf("hello world: meu id é %d\n", _pid)
}


Para executá-lo, salve-o em um arquivo e em seguida execute-o usando o spin. Por exemplo, se o nome do arquivo que o contém for hello.pml, a execução com spin deve ser assim:

aluno@M2:~$ spin hello.pml
hello world: meu id é 0
1 process created


A palavra-chave active define que o processo deve ser executado assim que o programa iniciar. Uma vez terminado, a execução como um todo encerra, uma vez que nenhum processo permanceceu ativo. Um outro exemplo gera processos indefinidamente, e mostra como se pode iniciar um processo a partir de outro usando o comando run:

active proctype hello() {
  printf("hello world: meu id é %d\n", _pid)
  run hello()
}


Variáveis

Processos podem usar variáveis, cujos escopos podem ser locais ou globais. Variáveis são tipadas, e os tipos básicos são (mais detalhes ver capítulo 3 do livro The SPIN Model Checker:

  • bit
  • bool
  • byte
  • chan
  • mtype
  • pid
  • short
  • int
  • unsigned


A declaração de variáveis é semelhante à de linguagem C, como se pode observar neste exemplo:

mtype = {ack, nak, err}

active proctype hello() {
  int x;
  pid p;
  mtype m = ack

  p = _pid;
  x = x + 1;
  printf("hello world: meu id é %d e x=%d\n", p, x)
  printf("... e m=")
  printm(m)
  printf("\n")
}

Tipos definidos

Podem-se definir tipos de dados de forma semelhante à declaração struct de linguagem C, usando-se a palavra-chave typedef:

typedef Mensagem {
  byte cod;
  int uid
}

active proctype teste() {
  Mensagem msg

  msg.cod = 111
  msg.uid = _pid
  printf("Mensagem: %d, %d\n", msg.cod, msg.uid)
}

.. o que pode ser conveniente para representar mensagens de um protocolo.

Canais de mensagens

Canais de mensagens são usados para modelar a troca de dados entre processos, e podem ser locais ou globais. Cada canal é declarado de forma a possibilitar mensagens de acordo de um determinado tipo, como neste exemplo:

typedef Mensagem = {
  pid uid;
  int cod
}

chan canal = [1] of {int}
chan outro = [8] of {Mensagem}
chan maisum = [0] of {int, byte}

No trecho de programa acima, são declarados três canais:

  • canal: este canal transporta mensagens com do tipo int; ele possui uma fila com até 1 mensagem armazenada (enviada e ainda não recebida)
  • outro: este canal transporta mensagens do tipo Mensagem, e possui uma fila com até 8 mensagens
  • maisum: este canal transporta mensagens compostas por dois campos (int e byte), e não possui fila de mensagens. A ausência de fila implica a comunicação síncrona de processos: o transmissor bloqueia até que o receptor receba a mensagem


A comunicação por meio de um canal se faz com os comandos ! (envio) e ? (recepção), que devem ser usados desta forma:

chan canal1 = [1] of {int}
chan canal2 = [1] of {int, byte}

active proctype p1() {
  int msg
  byte codigo

  // envia uma mensagem pelo canal1
  canal1!9

  // aguarda uma mensagem pelo canal2
  canal2?msg,codigo

  printf("Recebeu %d,%d\n", msg, codigo)
}


active proctype p2() {
  int msg

  // envia uma mensagem pelo canal2
  canal2!2,15

  // aguarda uma mensagem pelo canal1
  canal1?msg

  printf("Recebeu %d\n", msg)
}


O envio é feito informando-se valores para cada campo do tipo de mensagem transportada pelo canal. Esses valores podem ser constantes ou fornecidos por variáveis. O envio não é bloqueante se houver espaço livre na fila do canal para mais uma mensagem. Se o canal estiver cheio, o envio causa o bloqueio do transmissor, mas isso pode ser mudado no Spin (ver seção sobre Message Channels do capítulo 3 do livro sobre o Spin). A recepção é sempre bloqueante, e segue uma sintaxe similar a da operação de envio. Logo após o comando e recepção ?, podem-se informar variáveis para armazenar o conteúdo da mensagem, ou especificar constantes. No segundo caso, a recepção somente é realizada se os valores das constantes forem iguais aos da mensagem a ser recebida através do canal.


Um exemplo simples e representativo é este modelo de um protocolo de bit alternado. Ele é composto por um processo transmissor e um receptor, os quais repetem indefinidamente uma sequência de mensagens. As mensagens se dividem em dados (msg0 e msg1) e confirmação (ack0 e ack1). O programa PROMELA para esse protocolo está a seguir:

mtype = { msg0, msg1, ack0, ack1 };
chan    to_sndr = [2] of { mtype };
chan    to_rcvr = [2] of { mtype };

active proctype Sender()
{
again:  to_rcvr!msg1;
        to_sndr?ack1;
        to_rcvr!msg0;
        to_sndr?ack0;
        goto again
}

active proctype Receiver()
{
again:  to_rcvr?msg1;
        to_sndr!ack1;
        to_rcvr?msg0;
        to_sndr!ack0;
        goto again
}

Executabilidade

Em PROMELA, uma sentença é executável ou não (bloqueável). A semântica de executabilidade de sentenças em PROMELA segue estas regras:

  • atribuições e comandos do tipo print são sempre executáveis. Em outras palavras, nunca bloqueiam.
  • envio de mensagens é executável se o canal não estiver cheio, do contrário é bloqueante
  • recepção de mensagens é executável se houver mensagem no canal E se os valores nela contidos foram consistentes com os valores porventura especificados no comando de recepção
  • expressões são executáveis se avaliarem para verdadeiro (true)

PROMELA define também um modelo de execução não-determinístico. Isso quer dizer que, se uma sequência apresentar uma bifurcação e ambos os ramos forem executáveis, um deles será sorteado para ser executado. Por exemplo:

chan canal1 = [1] of {int}
chan canal2 = [1] of {int}

active proctype p1() {
  int msg1, msg2

Repetir:

  if
  :: canal1?msg1 -> printf("Recebeu msg1=%d\n", msg1)
  :: canal2?msg2 -> printf("Recebeu msg2=%d\n", msg2)
  fi

  goto Repetir
}

active proctype p2() {
  denovo:

  if
  :: canal1!111
  :: canal2!222
  fi
  
  goto denovo
}

No processo p2 do exemplo acima, se somente canal1 tiver uma mensagem, a primeira sentença será executada. Se somente canal2 tiver uma mensagem, a segunda sentença será executada. E se ambos canais tiverem mensagens, qualquer uma das duas sentenças poderá ser executada. E o processo p1, por sua vez, fica indefinidamente enviando mensagens por ambos canais aleatoriamente.

Na estrutura de seleção if, uma sentença especial é dada pela palavra-chave else. Tal sentença é executável somente se nenhuma outra sentença do if for executável.

Repetição

O exemplo do processos transmissor p2 na seção anterior envolveu a repetição de transmissões pelos canais canal1 e canal2. A repetição foi controlada com o comando goto. Em PROMELA existe um comando específico para controlar a repetição. Ele possibilita reescrever o processo p2 daquele exemplo para que fique assim:

active proctype p2() {
  do
  :: canal1!111 -> printf("enviei pelo canal1\n")
  :: canal2!222 -> printf("enviei pelo canal2\n")
  od
}

Da mesma forma que a estrutura de seleção if, a estrutura de repetição do seleciona uma das sentenças executáveis de forma não-determinística. Se nenhuma sentença for executável, então do bloqueia. E, assim como no if, a sentença iniciada com else é executável se nenhuma outra sentença do mesmo bloco de repetição for executável. Por fim, o comando break possibilita interromper a repetição. O exemplo a seguir combina else e break: se não for possível enviar uma mensagem pelo canal1, tampouco pelo canal2, então a repetição é interrompida.


active proctype p2() {
  do
  :: canal1!111 -> printf("enviei pelo canal1\n")
  :: canal2!222 -> printf("enviei pelo canal2\n")
  :: else -> break
  od
}


Outra palavra-chave importante de PROMELA é timeout, cujo significado se assemelha a else. A diferença está no escopo: else é verdadeiro se nenhuma condição do bloco em questão for executável, e timeout é verdadeiro se nenhuma sentença de todos os processos do programa for executável.


Além de do .. od, pode-se usar for para repetir um bloco de sentenças com base em uma faixa de valores.

active proctype p2() {
  int x

  // envia 10 mensagens por canal1 ou canal2,
  // selecionando-os de forma não-determinística
  // os limites da faixa de valores podem ser expressões contendo
  // constantes ou variáveis
  for (x: 1..10) {
    if
    :: canal1!x -> printf("enviei %d pelo canal1\n", x)
    :: canal2!x -> printf("enviei %d pelo canal2\n", x)
    fi
  }
}

Seleção de valores

A seleção ou sorteio de valores pode ser feita com select, que escolhe um valor dentre uma faixa:

proctype teste() {
  int x = 0

  // seleciona um valor entre 1 e 20 e armazena-o em x
  // sai do laço quando x >= 10
  // os limites da faixa devem se expressões contendo somente constantes
  do
  :: x < 10 -> select(x : 1 .. 20)
     printf("%d -> x=%d\n", _pid, x)
  :: else -> break
  od
}

Como gerar um verificador com SPIN

Os exemplos anteriores mostram como simular modelos escritos em PROMELA. A verificação desses modelos envolve a criação de um verificador, o que deve ser feito desta forma:

  1. Execute o spin instruindo-o a gerar um verificador:
    ./spin -a modelo.pml
    
  2. O comando anterior deve gerar um arquivo de programa pan.c. Compile-o com este comando:
    gcc -o pan pan.c
    
  3. Execute o verificador:
    ./pan
    
  4. Pode-se executar o verificador de formas alternativas. Por exemplo, para buscar ciclos sem progresso no modelo:
    ./pan -l
    
  5. Outras opções do verificador podem ser vistas com:
    ./pan -h
    

Atividade

  1. Escreva um modelo para o problema do produtor e consumidor. Suponha que o produtor produza 10 mensagens e depois termine, e o buffer intermediário tenha capacidade para 4 mensagens.
  2. Modifique seu modelo da questão anterior para que existam 2 produtores.
  3. Escreva um modelo para o problema dos filósofos glutões, supondo a existência de 5 filósofos. Verifique se seu modelo é livre de deadlock.
  4. Escreva um modelo para um protocolo do tipo pare-e-espere em um ambiente de execução livre de erros.
  5. Escreva um modelo para um protocolo do tipo pare-e-espere em um ambiente de execução em que erros são possíveis ... seu modelo deve prever a perda de mensagens, retransmissões e descarte de mensagens duplicadas.
  6. Crie um modelo para o protocolo Lynch e use-o para verificar se esse protocolo é livre de erros.
  7. Escreva um modelo para um protocolo que faça autenticação de usuário: a entidade cliente deve conseguir se autenticar na entidade servidora antes de poder enviar e receber mensagens de dados.
  8. Escreva um modelo para o enquadramento que foi implementado no protocolo ponto-a-ponto.
Modelos para o protocolo pare-espere

PTC-Pare-espere-ex4.jpg
Modelos implementam esta MEF

mtype = {data, ack};

chan M = [1] of {mtype, bit};
chan W = [1] of {mtype, bit};

active proctype RX() 
{
  bit sent,recv;
  int seq

  do
  :: W?data,seq // simula perda de mensagem 
  :: W?data,eval(recv) ->
      printf("%d msg correta: enviou ack %d\n", _pid, recv);      
      seq = recv
      recv = !recv
      M!ack,seq
  :: W?data,eval(!recv) ->
      printf("%d msg duplicada: enviou ack %d\n", _pid, !recv);      
      M!ack,!recv
  od
}

active proctype TX() 
{
  bit sent,recv;
  int seq

  // transmite primeira mensagem
  W!data,0

  do
  :: M?ack,seq // simula perda de ack
  :: M?ack,eval(sent) ->
    sent = !sent
    W!data,sent
    printf("%d transmitiu msg %d\n", _pid, sent)
  :: M?ack,eval(!sent) ->
    printf("%d ACK com sequencia errada %d !\n", _pid, !sent)
  :: timeout -> 
    printf("%d retransmitiu msg %d\n", _pid, sent)
    W!data,sent
  od
}


active proctype mef() {
  int x

  estado0:
  do
  :: x < 10 ->
    x++
  :: x == 10 ->
    goto estado1
  od

  estado1:
  do
  :: x > 0 ->
    x--
  :: x == 0 ->
    goto estado0
  od
}
Modelo para demonstrar uma forma de implementar uma máquina de estados com Promela


17/11: Verificação de protocolos (exercícios)

Modelagem de Máquinas de Estados Finitos

Máquinas de estado finitos são facilmente modeladas com Promela, explorando-se rótulos e a estrutura de repetição do .. od. Por exemplo, seja a MEF a seguir:

PTC-Mef0.jpg


Essa MEF pode ser traduzida para um modelo Promela como este. Note cada estado representado por do .. od e prefixado por um rótulo. As expressões sentinelas do do .. od correspondem aos gatilhos das transições que saem daquele estado, e as ações correspondentes representam o que acontece se a transição for disparada. Por fim, a mudança de estado é realizada por um simples goto para o rótulo do próximo estado.

active proctype mef() {
  int x 

  estado0:
  do
  :: x < 10 ->
    x++
  :: x == 10 ->
    goto estado1
  od

  estado1:
  do
  :: x > 0 ->
    x--
  :: x == 0 ->
    goto estado0
  od
}

23/11: Verificação de protocolos (exercícios)

30/11: Verificação de protocolos: propriedades

Hoje continuaremos o estudo da verificação de modelos com PROMELA e SPIN. O resumo a seguir foi escrito a partir do capítulo 4 do livro The SPIN Model Checker, e o capítulo 6 do livro Design and Validation of Computer Protocols.

Verificação de propriedades

Do ponto de vista de um sistema distribuído ou concorrente, podem-se identificar dois requisitos para a correção de um sistema:

  • Segurança (safety): segurança diz respeito ao que não deve ser possível ocorrer no sistema, ou ao conjunto de propriedades que não podem ser violadas. Isso inclui estados que nunca devem ser alcançados, ou sequências de estados as quais não podem ocorrer.
  • Vivacidade (liveness): vivacidade diz respeito ao que sempre deve valer para o sistema, ou ao conjunto de propriedades que sempre devem ser satisfeitas.


Para fins de verificação de propriedades, PROMELA/SPIN definem dois tipos de reivindicações de correção:

  • alcançabilidade: esse tipo de reivindicação se baseia em estados alcançáveis ou inalcançáveis. Isso também se chama propriedades de estado.
  • factibilidade: esse outro tipo de reivindicação se baseia na executabilidade ou não de sequências de estados. Isso também é chamado de propriedades de caminho.


Verificação de condições (assertions)

A verificação de uma condição pode ser feita com o comando assert. Esse comando verifica se uma expressão avalia para verdadeiro e, caso contrário, interrompe a execução do programa. Isso pode ser usado para a verificação pontual de uma propriedade do sistema.

chan canal1 = [1] of {int}
chan canal2 = [1] of {int}

active proctype p1() {
  int msg
  int n1 = 0
  int n2 = 0
 
  do
  :: canal1?msg -> n1++; assert(n1 < 10)
  :: canal2?msg -> n2++; assert(n2 < 15)
  od
}

active proctype p2() {
  do
  :: canal1!1
  :: canal2!1
  od
}

No exemplo acima, o processo p1 aceita que sejam recebidas no máximo 10 mensagens pelo canal1 e 15 mensagens pelo canal2.


Para verificar condições em um modelo, deve-se compilá-lo da seguinte forma:

# opção -a: usada para gerar um verificador, que estará no arquivo pan.c
./spin -a djikstra.pml

# Compila o verificador, que será o programa "pan"
gcc -o pan pan.c

# Executa o verificador
./pan

Meta-rótulos (meta labels)

Três tipos especiais de rótulos podem ser inseridos em um programa para orientar o verificador a identificar certos estados no sistema:

  • end: rótulos com prefixo end marcam estados finais de um processo. Inicialmente, o estado final de um processo é criado implicitamente na chave final de sua declaração }. Porém há situações em que um processo nunca termina, porém isso não se configura um erro. Assim, rótulos end possibilitam informar que outros estados do processo são finais.
  • progress: rótulos com prefixo progress informam que certos estados, se visitados, indicam que um processo está progredindo sua execução. Em outras palavras, que a execução não está presa em algum laço indefinidamente.
  • accept: rótulos com prefixo accept marcam estados que não devem ser visitados infinitamente frequente. Isso pode ser usado para identificar ciclos de visitação de estados os quais não devem ocorrer indefinidamente.

O meta-rótulo end

O exemplo a seguir mostra dois processos que trocam mensagens entre si. O processo ping envia uma mensagem para o processo pong, que a devolve como resposta. Após o envio de dez mensagens, o processo ping termina. O processo pong fica então indefinidamente aguardando uma mensagem que não será recebida. Uma situação como essa é interpretada pelo spin como um estado final indesejado.

chan c1 = [1] of {int}
chan c2 = [1] of {int}

active proctype ping() {
  int x,y

  do
  :: x < 10 ->
    c1!x
    printf("%d enviou %d\n", _pid, x)
    x++
    c2?y
    printf("%d recebeu resposta: %d\n", _pid, y)
  :: else -> break
  od
}


active proctype pong() {
  int x

loop:

  c1?x
  printf("%d recebeu %d\n", _pid, x)
  c2!x
  printf("%d devolveu: %d\n", _pid, x)
  
  goto loop
}

Para experimentar o diagnóstico do spin sobre esse modelo, deve-se compilar o verificador da seguinte forma:

aluno@M1:~$ spin -a ping.pml
aluno@M1:~$ gcc -DSAFETY -o pan pan.c

A execução do verificador, e o resultado por ele apresentado, são mostrados a seguir:

aluno@M1:~$ ./pan
pan:1: invalid end state (at depth 90)
pan: wrote end.pml.trail

(Spin Version 6.4.3 -- 16 December 2014)
	+ Partial Order Reduction

O verificador informou que o modelo termina ou bloqueia em um estado final inválido, e a demonstração de uma sequência de execução onde o problema ocorre está gravada no arquivo end.pml.trail. Para reproduzi-lo usa-se o spin assim (obs: o resultado copiado omite as linhas iniciais):

aluno@M1:~$ spin -t end.pml.trail
spin: trail ends after 91 steps
#processes: 2
		queue 1 (c1): 
		queue 2 (c2): 
 91:	proc  1 (pong:1) end.pml:24 (state 1)
 91:	proc  0 (ping:1) end.pml:16 (state 12) <valid end state>
2 processes created

O diagnóstico mostra que o processo 1 (pong) parou no estado inválido correspondente à linha 24, que contém a sentença c1?x. O processo 0 (ping) terminou em um estado válido na linha 16 (o fim do processo).

No sistema representado por esse modelo, pode-se considerar que o processo pong terminar aguardando por uma mensagem seja um estado final válido. Afinal, o processo pong apenas reage a mensagens recebidas do processo ping. Para descrever isso no modelo, deve-se informar ao spin que a espera por mensagem no processo pong é um estado final válido. Isso pode ser feito com um meta-rótulo end:

chan c1 = [1] of {int}
chan c2 = [1] of {int}

active proctype ping() {
  int x,y

  do
  :: x < 10 ->
    c1!x
    printf("%d enviou %d\n", _pid, x)
    x++
    c2?y
    printf("%d recebeu resposta: %d\n", _pid, y)
  :: else -> break
  od
}


active proctype pong() {
  int x

loop:

end:  c1?x
  printf("%d recebeu %d\n", _pid, x)
  c2!x
  printf("%d devolveu: %d\n", _pid, x)
  
  goto loop
}

Após essa alteração no modelo, deve-se gerar um novo verificador. A execução desse verificador agora informa que o modelo é válido:

aluno@M1:~$ spin -a end.pml
aluno@M1:~$ gcc -DSAFETY -o pan pan.c
aluno@M1:~$ ./pan
(Spin Version 6.4.3 -- 16 December 2014)
	+ Partial Order Reduction

O meta-rótulo progress

Usando o exemplo apresentado no caso do meta-rótulo end, faz-se uma alteração para que ambos processos nunca terminem. O processo ping fica eternamente enviando mensagens para pong, que simplesmente as responde. Esse novo modelo está mostrado a seguir:

chan c1 = [1] of {int}
chan c2 = [1] of {int}

active proctype ping() {
  int x,y

loop:
    c1!x
    printf("%d enviou %d\n", _pid, x)
    x = !x
    c2?y
    printf("%d recebeu resposta: %d\n", _pid, y)

    goto loop
}


active proctype pong() {
  int x

loop:

  c1?x
  printf("%d recebeu %d\n", _pid, x)
  c2!x
  printf("%d devolveu: %d\n", _pid, x)
  
  goto loop
}

Nitidamente não há um deadlock, uma vez que os processos não trancam em disputa por recursos, e tampouco algum processo termina porque não pode mais prosseguir. Por outro lado, ambos processos executam indefinidamente um ciclo de instruções, e isso pode significar um funcionamento anômalo chamado de livelock. O spin é capaz de detectar esse comportamento, e informá-lo como resultado do verificador. No caso do modelo em questão, o uso do verificador deve ser feito desta forma (observe a opção -l passada ao verificador):

aluno@M1:~$ spin -a ping.pml
aluno@M1:~$ gcc -DNP -o pan pan.c
aluno@M1:~$ ./pan -l
pan:1: non-progress cycle (at depth 14)
pan: wrote prog.pml.trail

(Spin Version 6.4.3 -- 16 December 2014)
Warning: Search not completed
	+ Partial Order Reduction

Nem todo ciclo que não termina é um livelock. O verificador pode ser informado de que, se um processo passar de forma infinitamente frequente por um ciclo, isso significa que o processo está progredindo. Para esas finalidade existe o meta-rótulo progress. No exemplo dos processos ping e pong, esse meta-rótulo pode ser usado assim:


chan c1 = [1] of {int}
chan c2 = [1] of {int}

active proctype ping() {
  int x,y

loop:

progress:    c1!x
    printf("%d enviou %d\n", _pid, x)
    x = !x
    c2?y
    printf("%d recebeu resposta: %d\n", _pid, y)

    goto loop
}


active proctype pong() {
  int x

loop:

  c1?x
  printf("%d recebeu %d\n", _pid, x)
  c2!x
  printf("%d devolveu: %d\n", _pid, x)
  
  goto loop
}

O verificador criado a partir desse modelo fornece um diagnóstico sem livelock:

aluno@M1:~$ spin -a ping.pml
aluno@M1:~$ gcc -DNP -o pan pan.c
aluno@M1:~$ ./pan -l

(Spin Version 6.4.3 -- 16 December 2014)
Warning: Search not completed
	+ Partial Order Reduction

Um exemplo baseado num semáforo

O exemplo a seguir mostra o uso de um rótulo end, em um modelo sobre semáforos. O processo Djikstra nunca termina, pois ele implementa um semáforo e deve estar sempre disponível para atender pedidos de entrada em seção crítica. Assim, o estado em que inicia a verificação do semáforo é um estado final.

mtype { p, v };
chan sema = [0] of { mtype };
active proctype Dijkstra()
{       byte count = 1;
end:    do
        :: (count == 1) ->
                sema!p; count = 0
        :: (count == 0) ->
                sema?v; count = 1
        od
}
active [3] proctype user()
{       do
        :: sema?p;        /* enter */
critical: skip;           /* leave */
          sema!v;
        od
}


Uma pequena alteração no exemplo demonstra o uso de um rótulo progress. Neste caso, marca-se o estado em que se libera o semáforo como sendo um estado indicador de progresso. Se o processo Djikstra passar frequentemente por esse estado, o verificador conclui que o processo está em progresso.

active proctype Dijkstra()
{       byte count = 1;
end:    do
        :: (count == 1) ->
progress:       sema!p; count = 0
        :: (count == 0) ->
                sema?v; count = 1
        od
}

Atividade

Para exercitar a verificação de protocolos, faça o seguinte:

  1. Verifique os modelos para o problema do produtor e consumidor, de forma que:
    • Toda mensagem produzida seja efetivamente consumida
    • Toda mensagem deva ser consumida uma única vez
  2. Verifique o modelo para o problema dos filósofos glutões, supondo a existência de 5 filósofos, de forma que:
    • Não exista deadlock
    • Nenhum filósofo morra de fome
    • Um garfo é pego por um único filósofo a cada vez
  3. Verifique o modelo para um protocolo do tipo pare-e-espere em um ambiente de execução em que erros são possíveis, tais como perdas de mensagens (tanto dados quanto ack). As seguintes propriedades devem ser verificadas:
    • Toda mensagem transmitida é entregue em algum momento futuro
    • Toda mensagem recebida é consumida uma única vez
    • Ausência de deadlock
    • Ausência de livelock
  4. Verifique o modelo para um protocolo que faça autenticação de usuário, de forma que:
    • Mensagens são transmitidas somente após cliente autenticado
    • Mensagens recebidas são aceitas apenas após cliente autenticado
  5. Verifique o modelo para o enquadramento que foi implementado no protocolo ponto-a-ponto, de forma que:
    • Quadros com erros de CRC são descartados
    • Flags aparecem somente como delimitadores de quadro
    • Não há byte especial dentro de um quadro, a não ser que seja um ESC
    • Perda de sincronismo acarreta descarte de bytes recebidos
  6. Verifique o protocolo Lynch quanto:
    • não duplicidade de mensagens
    • ausência de deadlock
    • ausência de livelock
  7. Modele o protocolo de enlace sem-fio PTP, identifique critérios de correção e faça sua verificação.

01/12: Verificação de protocolos: lógica temporal


Lógica clássica é boa para descrever um mundo estático, mas sistemas computacionais são dinâmicos. A lógica temporal é uma categoria de formalismos que procura descrever como as propriedades de um sistema variam com o tempo, porém sem se referir explicitamente ao tempo. As propriedades que tipicamente são verificadas com lógica temporal são:

  • Segurança (safety): nada de ruim irá acontecer. Ex: não há deadlock, não ocorrem violações de exclusão mútua, mensagens duplicadas não são entregues.
  • Vivacidade (liveness): algo de bom vai ocorrer. Ex: um programa fatalmente irá terminar, uma mensagem transmitida será corretamente recebida em algum momento futuro.
  • Justiça (fairness): sistemas independentes progridem. Ex: se o meio de transmissão estiver frequentemente disponível, uma entidade de protocolo no final das contas conseguirá transmitir uma mensagem.


A lógica temporal introduz operadores temporais, usados para descrever a dinâmica de sistemas ao longo do tempo. Curiosamente, o tempo não aparece explicitamente em expressões formadas com esses operadores. Para fins de nosso estudo, será considerado um tempo linear, com um único caminho de execução (a alternativa é um tempo ramificado, em que múltiplos caminhos de execução são levados em conta). Ambas abordagens pressupõem um sistema modelado como um sistema de transições com rótulos (LTS - labeled transition system), como, por exemplo, máquinas de estados finitos. Cada passo de um caminho de execução é definido por um conjunto de proposições primitivas que avaliam para verdadeiro. Exemplos de proposições são:

  • msg_tx: mensagem foi transmitida
  • inCS_A: processo A está executando em uma seção crítica
  • x == 0: variável x tem valor 0
  • proc@label: processo proc está executando linha identificada pelo rótulo label

Sendo assim, um estado S é definido pelas proposições que são verdadeiras nesse estado.

Operadores temporais

Os operadores temporais se aplicam a estados, considerando uma sequência de estados. Tomando-se as fórmulas de lógica temporal f e g:

  • Xf: f é verdadeiro no próximo estado (next f)
  • Gf: f é verdadeiro em todos os estados (always f)
  • Ff: f é verdadeiro em algum estado futuro (eventually f)
  • fUg: g é verdadeiro em algum estado futuro, e f é verdadeiro em cada estado até então (f until g)

Outra notação:

  • G:
  • F:
  • X:


Dado o conjunto de proposições P, e fórmulas LTL f e g, as fórmulas LTL podem ser formadas com estas regras:

  • p (uma proposição )
  • Operadores booleanos:
    • : not f
    • : f or g
    • : f and g
    • : f implies g
  • Operadores temporais:
    • f U g


Uma outra forma de visualizar esses operadores temporais está ilustrada a seguir:

PTC-Operadores-ltl.png


Algumas identidades entre fórmulas com esses operadores são:

  • ...
  • ,
  • ,
  • f W g


Propriedades distributivas:


LTL e Spin

O verificador spin é capaz de verificar fórmulas LTL. As proposições primitivas podem ser:

Proposição primitiva Exemplo
expressões baseadas em variáveis globais (x == 0)
rótulos de processos p1@inicio (rótulo inicio do processo p1)
p1[2]@inicio (rótulo inicio do processo p1 com pid 2)
variáveis locais de processos p1:x (variável local x do processo p1)
p1[2]:x (variável local x do processo p1 com pid 2)


Os operadores temporais suportados são (OBS: note a falta do operador next):

Operador LTL no Spin Operador LTL
[] (always)
<> (eventually)
U U (until)
&&
||
->
<->


Variáveis e funções predefinidas que podem ser úteis:

  • _pid: variável local que contém o PID de um processo
  • _last: variável global que contém o PID do processo que contém a última sentença executada
  • enabled(pid): função global que retorna true se existe ao menos uma sentença executável no processo identificado por pid


Para declarar fórmulas LTL em um modelo Promela, basta acrescentá-las ao final do arquivo com esta sintaxe:

ltl nome1 { formula 1}
ltl nome2 { formula 2}
...
ltl nomeN { formula N}


A geração do verificador deve ser feita com estes comandos:

spin -a modelo.pml
gcc -o pan pan.c


Por fim, a execução do verificador se faz assim:

./pan -a

Exemplo de uso

Um primeiro exemplo pode ser aplicado ao caso de exclusão mútua entre processos. Supondo que inCS_X signifique processo X está na seção crítica, as seguintes proposições podem ser definidas:

  1. Processos A e B nunca estão na seção crítica ao mesmo tempo:
  2. Nenhum dos processos monopoliza a seção crítica:


No spin, essa verificação pode ser feita com este modelo e respectivas fórmulas LTL:

mtype { p, v };

chan sema = [0] of { mtype };

active proctype Dijkstra()
{       byte count = 1;
end:    do
        :: (count == 1) ->
                sema!p; count = 0
        :: (count == 0) ->
                sema?v; count = 1
        od
}

active proctype p1()
{       do
        :: sema?p;        /* enter */
critical: skip;           /* leave */
          sema!v;
        od
}

active proctype p2()
{       do
        :: sema?p;        /* enter */
critical: skip;           /* leave */
          sema!v;
        od
}

// Apenas um processo está em sua seção crítica por vez
ltl crit { [] (! p1@critical || ! p2@critical)}

// Nenhum processo monopoliza a seção crítica
ltl justica { [] (p1@critical -> <> ! p1@critical) && [] (p2@critical -> <> ! p2@critical)}

Atividades

1. Para o sistema descrito no modelo Promela a seguir, verifique se o contador atinge o valor 10:

int count;
bool incr;

active proctype counter() {
  do
  :: incr -> count++
  od
}

active proctype env() {
  do
  :: incr = false
  :: incr = true
  od
}
Fórmula LTL

2. Para o sistema do exercício 1, verifique também se a execução dos processos possui a propriedade da justiça: ambos os processos devem executar infinitamente frequente. Dica: lembre das variáveis e funções predefinidas do Spin.

Fórmulas LTL

3. Seja um protocolo ARQ do tipo pare-espere, em que processo p1 envia mensagens para processo p2 por um canal não confiável:

  • Defina sentA para expressar se processo p1 recém enviou mensagem A, e recA se processo p2 recém recebeu mensagem A. Defina também sentB e recB da mesma forma.
  • Defina loss para expressar se canal perdeu última mensagem

Sendo assim, verifique estas propriedades:

  • Se uma mensagem for transmitida, ela será recebida em algum momento
  • Se uma mensagem for recebida, ela foi previamente transmitida

Por fim, escreva esse modelo Promela, declare as fórmulas LTL, e verifique seu modelo.


4. Aplique a verificação com LTL aos problemas propostos aula passada ...

07/12: Verificação de protocolos: destrinchando fórmulas LTL

Fórmulas LTL em geral

As fórmulas LTL simples, sem operadores temporais, são consideradas verdadeiras se avaliarem para VERDADEIRO no primeiro estado do modelo. Quer dizer, elas devem valer no momento em que o modelo for iniciado, e antes de qualquer sentença ser executada. Por exemplo, seja este modelo:

int n=5

active proctype teste() {
  do
  :: n > 0 -> n--
  :: n < 5 -> n++
  od
}


Nesse modelo a variável global n inicia com o valor 5, e nos estados subsequentes ela pode variar aleatoriamente entre 0 e 5. Considerem-se as seguintes fórmulas LTL:

  • (n == 5): esta fórmula avalia para verdadeiro, pois no primeiro estado do modelo de fato n tem o valor 5.
  • (n == 4): esta fórmula avalia para falso, pois no primeiro estado do modelo n tem na verdade o valor 5.
  • <>(n == 4): esta fórmula avalia para verdadeiro, pois realmente em algum estado futuro n terá o valor 4.

14/12: Conclusão

A conclusão da etapa 3 é a verificação do protocolo do projeto 1. Isso tem por objetivos verificar se:

  1. ARQ/MAC está livre de deadlock e livelock
  2. Um quadro transmitido é entregue em algum momento futuro
  3. Quadros duplicados são descartados
  4. Somente um nodo transmite a cada vez
  5. Perdas de sincronismo no enquadramento são recuperadas em algum momento futuro

A verificação deve se basear no protocolo implementado no projeto 1. Assim, o modelo Promela deve ser fiel ao comportamento do protocolo tal qual efetivamente desenvolvido.

A entrega dessa avaliação deve ser composta de:

  1. Modelo escrito em Promela, junto com os recursos usados para verificação (fórmulas LTL, meta-rótulos). O modelo deve estar documentado e comparado ao código-fonte original dos componentes do protocolo do projeto 1 envolvidos.
  2. Um relatório que mostre como cada propriedade foi verificada, e o resultado da verificação.


15/12: Conclusão