Mudanças entre as edições de "PTC-2017-2"
(88 revisões intermediárias pelo mesmo usuário não estão sendo mostradas) | |||
Linha 3: | Linha 3: | ||
'''Professor:''' [[Marcelo_Maia_Sobral|Marcelo Maia Sobral]] | '''Professor:''' [[Marcelo_Maia_Sobral|Marcelo Maia Sobral]] | ||
<br>'''Encontros:''' 5a feira/9:40, 6a feira/9:40 (Semana A) | <br>'''Encontros:''' 5a feira/9:40, 6a feira/9:40 (Semana A) | ||
− | <br>'''Atendimento paralelo:''' | + | <br>'''Atendimento paralelo:''' 5a de 8:30 às 9:30 h / 6a de 13:30 às 14:30 h |
[[PTC-EngTel_(Plano_de_Ensino)| Plano de Ensino]] | [[PTC-EngTel_(Plano_de_Ensino)| Plano de Ensino]] | ||
Linha 14: | Linha 14: | ||
* [https://www.rfc-editor.org/rfc/rfc3117.txt RFC-1317: On the Design of Application Protocols] | * [https://www.rfc-editor.org/rfc/rfc3117.txt RFC-1317: On the Design of Application Protocols] | ||
* [http://tele.sj.ifsc.edu.br/~msobral/ptc/hotnets.pdf Protocol Design Beyond Graph-Based Models] | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/hotnets.pdf Protocol Design Beyond Graph-Based Models] | ||
+ | * [http://www.prismmodelchecker.org/ PRISM: Probabilistic Model Checker] | ||
* [https://www.infoq.com/news/2014/07/protocol-design-sbe-thompson Protocol Design (entrevista)] | * [https://www.infoq.com/news/2014/07/protocol-design-sbe-thompson Protocol Design (entrevista)] | ||
* [[Introdução_C%2B%2B|Inrodução a C++ (com referências bibliográficas)]] | * [[Introdução_C%2B%2B|Inrodução a C++ (com referências bibliográficas)]] | ||
+ | * [https://people.eecs.berkeley.edu/~sangjin/2013/01/14/NUSE.html NUSE: network stack in usespace ?] | ||
+ | * [https://github.com/kohler/click Click moudlar router] | ||
+ | * [http://savannah.nongnu.org/projects/lwip/ lwIP - A Lightweight TCP/IP stack] | ||
+ | * [https://software.intel.com/en-us/blogs/2015/06/12/user-space-networking-fuels-nfv-performance User Space Networking] | ||
+ | * [http://libos-nuse.github.io/ NUSE: Networking Stack in Userspace] | ||
+ | * [https://shader.kaist.edu/mtcp/ mTCP: a Highly Scalable User-level TCP Stack for Multicore Systems] | ||
+ | * [http://dpdk.org/doc/guides/ DPDK: Data Plane Development Kit] | ||
== Avaliações == | == Avaliações == | ||
Linha 27: | Linha 35: | ||
{| border="1" cellpadding="2" | {| border="1" cellpadding="2" | ||
!Aluno | !Aluno | ||
− | ! | + | !Proto 1 |
− | ! | + | !Proj 1 |
− | ! | + | !Proj 2 |
+ | !Proj 3 | ||
!FINAL | !FINAL | ||
+ | |- | ||
+ | |Bruno||95 ||96 ||100 ||90 ||9 | ||
+ | |- | ||
+ | |Douglas || 0|| 71|| 93|| 75||7 | ||
+ | |- | ||
+ | |Fernando ||0|| 71|| 93|| 75||7 | ||
+ | |- | ||
+ | |Henrique ||82 || 96|| 98||80 ||9 | ||
+ | |- | ||
+ | |Leticia ||0|| || 93|| 75|| | ||
+ | |- | ||
+ | |Lucas || 90||80 ||100 || 70||8 | ||
+ | |- | ||
+ | |Mateus ||90 ||100 ||93 ||75 ||9 | ||
+ | |- | ||
+ | |Mathias ||90 ||80 ||100 ||70 ||8 | ||
+ | |- | ||
+ | |Matuzalem ||82 || 96|| 98||80 ||9 | ||
+ | |- | ||
+ | |Murilo ||95 ||96 ||100 ||90 ||9 | ||
+ | |- | ||
+ | |Natan ||95 ||96 ||100 ||90 ||9 | ||
+ | |- | ||
+ | |Nelson ||90 ||100 ||93 ||75 ||8 | ||
+ | |- | ||
+ | |Paula ||90 ||100 ||93 ||75 ||8 | ||
|} | |} | ||
− | '' | + | |
+ | '''Cálculo do conceito final:''' ... e ajustes finais feitos com base em frequência | ||
+ | |||
+ | <math>\frac {(Proto1 \cdot 0.25 + Proj1 \cdot 0.75) + Proj2 + Proj3}{3}</math> | ||
== Softwares == | == Softwares == | ||
Linha 589: | Linha 627: | ||
== DICA para desenvolvimento do protocolo == | == DICA para desenvolvimento do protocolo == | ||
+ | <!-- | ||
+ | #*# Obtenha o [http://tele.sj.ifsc.edu.br/~msobral/ptc/serialemu.c código-fonte do serialemu] | ||
+ | #*# Compile-o com este comando: <syntaxhighlight lang=bash> | ||
+ | aluno@M2:~$ gcc -o serialemu serialemu.c -lutil -lpthread | ||
+ | </syntaxhighlight> | ||
+ | --> | ||
Para desenvolver o protocolo, há duas opções: | Para desenvolver o protocolo, há duas opções: | ||
# ''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. | # ''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. | ||
# ''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: | # ''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: | ||
− | # | + | ## Obtenha o [http://tele.sj.ifsc.edu.br/~msobral/ptc/Serialemu.zip código-fonte do serialemu] |
− | # | + | ## Descompacte o arquivo ''Serialemu.zip'', e entre no subdiretório ''Serialemu''. |
− | aluno@M2:~$ | + | ## Compile-o com este comando: <syntaxhighlight lang=bash> |
+ | aluno@M2:~/Serialemu$ make | ||
+ | </syntaxhighlight> | ||
+ | ## Copie o programa compilado para algum subdiretório mais conveniente ... por exemplo, ''/home/aluno'', e depois mude para esse subdiretório: <syntaxhighlight lang=bash> | ||
+ | aluno@M2:~/Serialemu$ cp -a dist/Debug/GNU-Linux/serialemu /home/aluno/ | ||
+ | aluno@M2:~/Serialemu$ cd /home/aluno | ||
+ | aluno@M2:~/$ | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | # | + | ## Execute-o de forma que ele apresente suas opções de execução: <syntaxhighlight lang=bash> |
aluno@M2:~$ ./serialemu -h | aluno@M2:~$ ./serialemu -h | ||
Uso: ./serialemu [-b BER][-a atraso][-f][-B taxa_bits] | -h | Uso: ./serialemu [-b BER][-a atraso][-f][-B taxa_bits] | -h | ||
Linha 605: | Linha 655: | ||
-f: executa em primeiro plano (nao faz fork) | -f: executa em primeiro plano (nao faz fork) | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | # | + | ## 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: <syntaxhighlight lang=bash> |
aluno@M2:~$ ./serialemu -B 9600 | aluno@M2:~$ ./serialemu -B 9600 | ||
/dev/pts/17 /dev/pts/2 | /dev/pts/17 /dev/pts/2 | ||
aluno@M2:~$ | aluno@M2:~$ | ||
</syntaxhighlight>... e anote os dois caminhos informados pelo serialemu: eles são as duas portas seriais que correspondem às pontas do link serial emulado. | </syntaxhighlight>... e anote os dois caminhos informados pelo serialemu: eles são as duas portas seriais que correspondem às pontas do link serial emulado. | ||
− | # | + | ## Execute seu protocolo usando essas portas seriais virtuais. |
Linha 678: | Linha 728: | ||
* [http://www.barrgroup.com/Embedded-Systems/How-To/CRC-Calculation-C-Code Implementação de CRC em software] | * [http://www.barrgroup.com/Embedded-Systems/How-To/CRC-Calculation-C-Code Implementação de CRC em software] | ||
* [https://tools.ietf.org/html/rfc1662#page-18 Implementação do CRC-16 para PPP (RFC 1662)] | * [https://tools.ietf.org/html/rfc1662#page-18 Implementação do CRC-16 para PPP (RFC 1662)] | ||
− | * [http:// | + | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/crc_v3.txt ''A Painless Guide to CRC Error Detection Algorithms, 3rd Edition'' por Ross Williams] |
* [http://tele.sj.ifsc.edu.br/~msobral/ptc/crc.pdf Um pequeno resumo sobre CRC (obtido do livro ''Comunicação de Dados e Redes de Computadores'', de William Stallings)] | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/crc.pdf Um pequeno resumo sobre CRC (obtido do livro ''Comunicação de Dados e Redes de Computadores'', de William Stallings)] | ||
Linha 785: | Linha 835: | ||
Qual dentre os mecanismos ARQ deve ser o mais apropriado para o tipo de enlace do protocolo ? | 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 [[Desempenho_ARQ#Desempenho_do_Stop-and-wait|análise de utilização do canal usando ''pare-e-espere'']]: | 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 [[Desempenho_ARQ#Desempenho_do_Stop-and-wait|análise de utilização do canal usando ''pare-e-espere'']]: | ||
Linha 799: | Linha 849: | ||
Portanto, a utilização do meio seria no máximo em torno de 0.99 (ou 99%). | Portanto, a utilização do meio seria no máximo em torno de 0.99 (ou 99%). | ||
− | + | ||
== Modelagem do mecanismo ARQ == | == Modelagem do mecanismo ARQ == | ||
Linha 928: | Linha 978: | ||
# Controle de acesso ao meio | # Controle de acesso ao meio | ||
− | Tendo como base a | + | 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: | ||
+ | * [https://moodle.sj.ifsc.edu.br/mod/assign/view.php?id=3412 Tarefa para entrega do protótipo]: acesse a atividade para maiores detalhes sobre o que deve ser entregue. | ||
+ | |||
+ | |||
+ | == 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. | ||
+ | |||
+ | |||
+ | [[imagem:PTC-Mef-arq2.jpg]] | ||
+ | <br><br>''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. | ||
+ | |||
+ | |||
+ | [[imagem:PTC-Protocolo-estrutura2.jpg]] | ||
+ | <br>''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): | ||
+ | |||
+ | [[imagem:PTC-Sequencias-protocolo2.jpg]] | ||
+ | <br>''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: | ||
+ | # '''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 [http://www.cleibsonalmeida.blog.br/site/uml-composicao-vs-agregacao/ 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. | ||
+ | # '''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: | ||
+ | # Usando [http://manpages.ubuntu.com/manpages/xenial/man2/getitimer.2.html setitimer] ou [http://manpages.ubuntu.com/manpages/xenial/man2/alarm.2.html alarm] combinado com sinal SIGALRM | ||
+ | # Usando [http://manpages.ubuntu.com/manpages/xenial/man2/timer_create.2.html timer_create] combinado com um sinal definido pelo programador | ||
+ | # Usando [http://manpages.ubuntu.com/manpages/xenial/man2/select.2.html select] | ||
+ | # Em um loop que lê o relógio a cada iteração, desviando o fluxo de execução se o ''timeout'' ocorrer | ||
+ | # ... 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 [http://manpages.ubuntu.com/manpages/xenial/man2/alarm.2.html 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 [http://manpages.ubuntu.com/manpages/xenial/man2/signal.2.html signal], informando como parâmetros o tipo de sinal a ser capturado e uma função que funciona como tratador de sinal. | ||
+ | |||
+ | {{collapse top | Exemplo de timeout com alarm e signal}} | ||
+ | <syntaxhighlight lang=c> | ||
+ | #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; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | {{collapse bottom}} | ||
+ | |||
+ | === 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. | ||
+ | |||
+ | {{collapse top | Exemplo de timeout com timer_create}} | ||
+ | <syntaxhighlight lang=c> | ||
+ | #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; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | {{collapse bottom}} | ||
+ | |||
+ | === Timeout com select === | ||
+ | |||
+ | A chamada [http://manpages.ubuntu.com/manpages/xenial/man2/select.2.html 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. | ||
+ | |||
+ | {{collapse top | Exemplo de timeout com select}} | ||
+ | <syntaxhighlight lang=c> | ||
+ | #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; | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | {{collapse bottom}} | ||
+ | |||
+ | == 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: | ||
+ | |||
+ | <syntaxhighlight lang=c> | ||
+ | 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); | ||
+ | }; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == Atividade == | ||
+ | |||
+ | # Remodele sua MEF para que possa diferenciar os eventos a serem tratados | ||
+ | # Implemente timeouts no Enquadramento | ||
+ | # 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 [http://tele.sj.ifsc.edu.br/~msobral/ptc/mac_kurose.pdf capítulo 5, seção 5.3, do livro ''Redes de Computadores e a Internet, 5a. edição'', de James Kurose e Keith Ross]): | ||
+ | # '''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. | ||
+ | # '''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. | ||
+ | # '''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: | ||
+ | |||
+ | [[imagem: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: | ||
+ | |||
+ | [[imagem:PTC-Mef-arq-ms.jpg]] | ||
+ | |||
+ | == Atividade == | ||
+ | |||
+ | # 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 [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/conexoes.pdf 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: | ||
+ | |||
+ | |||
+ | [[imagem:PTC-2-way_3-way.png|500px]] | ||
+ | |||
+ | |||
+ | 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: | ||
+ | # Ao receber a mensagem ''CC'', o iniciador já pode enviar dados pela conexão | ||
+ | # 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 [https://en.wikipedia.org/wiki/Keepalive#TCP_keepalive ''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: | ||
+ | # A sincronização entre os participantes quanto ao término da conexão (ambos participantes devem fechar a conexão) | ||
+ | # 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 [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/conexoes.pdf 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. | ||
+ | |||
+ | |||
+ | [[imagem:PTC-Half-close.png|500px]] | ||
+ | |||
+ | |||
+ | 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''): | ||
+ | |||
+ | |||
+ | [[imagem:PTC-3-way-close.png|300px]] | ||
+ | |||
+ | |||
+ | 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 == | ||
+ | |||
+ | # Identifique em que parte da estrutura do seu protocolo devem se encaixar o gerenciamento de conexão | ||
+ | # 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 [https://github.com/ifsc-saojose/modelos-latex/tree/master/artigo/modelo-ifsc-proppi modelo de artigo/relatório técnico] disponibilizado pelo professor Emerson Mello. | ||
+ | * [https://moodle.sj.ifsc.edu.br/mod/assign/view.php?id=3514 Entrega do relatório via Moodle] | ||
+ | |||
+ | = 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 == | ||
+ | |||
+ | *[http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/cap-2-kurose.pdf Capítulo 2 do livro ''Redes de Computadores e a Internet'', de J. Kurose e K. Ross] | ||
+ | |||
+ | |||
+ | 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: | ||
+ | * [https://www.w3.org/Protocols/ HTTP] e [https://tools.ietf.org/html/rfc7540 HTTP2] | ||
+ | * [https://tools.ietf.org/html/rfc3261 SIP] | ||
+ | * [https://tools.ietf.org/html/rfc5905 NTP] | ||
+ | * [https://tools.ietf.org/html/rfc5321 SMTP] | ||
+ | * [https://www.ietf.org/rfc/rfc4251.txt SSH] | ||
+ | * [https://en.wikipedia.org/wiki/BitTorrent BitTorrent] | ||
+ | * ... e [https://en.wikipedia.org/wiki/Application_layer#Other_protocol_examples muitos outros !!!] | ||
+ | |||
+ | |||
+ | 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 [https://en.wikipedia.org/wiki/Client%E2%80%93server_model ''cliente-servidor''], [https://en.wikipedia.org/wiki/Peer-to-peer ''P2P''] e [http://wiki.c2.com/?PublishSubscribeModel ''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 [[PTC-2017-2#TAREFA:_um_protocolo_simples|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 [[PTC-2017-2#28.2F07:_Diretrizes_para_projeto_de_um_protocolo|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: | ||
+ | # Descrever a comunicação envolvida na aplicação | ||
+ | # 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 == | ||
+ | |||
+ | # '''Serviço:''' | ||
+ | # Defina o vocabulário do protocolo de aplicação, incluindo os tipos de mensagens e suas sintaxes. | ||
+ | # 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 = | ||
+ | |||
+ | * [https://arduino.stackexchange.com/questions/4214/communication-protocol-best-practices-and-patterns/4222 Uma breve discussão sobre protocolos seriais no Arduino] | ||
+ | |||
+ | ''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. | ||
+ | |||
+ | [[imagem:PTC-Tcp-segment.gif]] | ||
+ | |||
+ | |||
+ | O TCP é um protocolo de transporte, e usa uma codificação binária em suas mensagens com representação [https://en.wikipedia.org/wiki/Endianness ''big endian''] (todos protocolos de comunicação da Internet usam essa representação - ver [https://tools.ietf.org/html/rfc1700 RFC 1700]). Já o protocolo de aplicação [https://www.ietf.org/rfc/rfc2821.txt 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:''). | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Outro protocolo de aplicação muito utilizado é o [http://tools.ietf.org/html/rfc2616 HTTP (Hypertext Transfer Protocol)], que usa uma representação híbrida em suas mensagens (predominantemente textual, mas pode ter conteúdo binário). | ||
+ | |||
+ | {| border=1 | ||
+ | !Requisição (enviada pelo cliente) | ||
+ | !Resposta (enviada pelo servidor) | ||
+ | |- | ||
+ | |<syntaxhighlight lang=text> | ||
+ | GET /~msobral/ptc/docs/ HTTP/1.1 | ||
+ | Host: tele.sj.ifsc.edu.br | ||
+ | </syntaxhighlight> || <syntaxhighlight lang=text> | ||
+ | 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> </td><td align="right"> - </td><td> </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> </td></tr> | ||
+ | <tr><th colspan="5"><hr></th></tr> | ||
+ | </table> | ||
+ | </body></html> | ||
+ | </syntaxhighlight> | ||
+ | |} | ||
+ | |||
+ | |||
+ | Esses exemplos apresentam basicamente dois tipos de codificação: | ||
+ | # '''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 [https://en.wikipedia.org/wiki/UTF-8 UTF-8]). | ||
+ | # '''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: | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | 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 [https://en.wikipedia.org/wiki/Endianness ''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 [https://en.wikipedia.org/wiki/Type-length-value TLV (Type-Length-Value)] | ||
+ | * Com uma representação textual em [https://en.wikipedia.org/wiki/Comma-separated_values CSV] ou [https://en.wikipedia.org/wiki/JSON 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: | ||
+ | * [http://www.itu.int/en/ITU-T/asn1/Pages/asn1_project.aspx ASN.1 (Abstract Syntax Notation One)]: ASN.1 é uma linguagem de especificação de sintaxe abstrata definida pelo [http://www.itu.int/ ITU-T]. A sintaxe concreta está dissociada dessa linguagem, podendo ser realizada de diferentes formas, tais como [https://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One BER (Binary Encoding Rules), PER (Packed Encoding Rules), XER (XML Encoding Rules)] e outras. A notação ASN.1 não impõe uma escolha sobre qual codificação utilizar, ficando assim de livre escolha pelo desenvolvedor. | ||
+ | * [http://www.rfc-editor.org/std/std68.txt ABNF (Augmented Backus-Naur Form)]: ABNF é uma notação definida pelo [http://www.ietf.org/ IETF] para expressar tanto a sintaxe abstrata quanto sua codificação textual, sendo portanto voltada para estruturas de dados representadas em modo de texto. Assim, ABNF impõe também a codificação a ser usada nas mensagens. | ||
+ | |||
+ | ... e algumas outras propostas por empresas ou grupos de desenvolvimento de software correndo por fora: | ||
+ | * [https://developers.google.com/protocol-buffers/ 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 [http://jpa.kapsi.fi/nanopb/ projeto relacionado] para uma biblioteca com pequeno footprint. | ||
+ | * [http://msgpack.org/ 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 == | ||
+ | |||
+ | * [http://www.itu.int/en/ITU-T/asn1/Pages/asn1_project.aspx Site oficial sobre ASN.1] | ||
+ | * [http://www.itu.int/en/ITU-T/asn1/Pages/introduction.aspx Introdução a ASN.1] | ||
+ | * [http://www.w3.org/Protocols/HTTP-NG/asn1.html Um exemplo sobre ASN.1] | ||
+ | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/dubuisson-asn1-book.PDF ASN.1: Communication between heterogeneous systems (livro gratuito)] | ||
+ | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/larmouth-asn1-book.pdf ASN.1 Complete (livro gratuito)] | ||
+ | * [http://lionet.info/asn1c/ Compilador ASN.1] ... e esse mesmo projeto [https://github.com/vlm/asn1c no github] | ||
+ | |||
+ | |||
+ | De acordo com a [http://www.itu.int/en/ITU-T/asn1/Pages/introduction.aspx 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 [http://www.red-bean.com/doc/python-pyasn1/pyasn1-tutorial.html Python]). Em particular, existe um [http://lionet.info/asn1c/blog/ 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: | ||
+ | |||
+ | {| border=1 | ||
+ | !ASN.1 | ||
+ | !Linguagem C | ||
+ | |- | ||
+ | |<syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | </syntaxhighlight> || <syntaxhighlight lang=c> | ||
+ | /* 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; | ||
+ | </syntaxhighlight> | ||
+ | |} | ||
+ | |||
+ | A documentação do compilador ASN.1 e uma demonstração de como escrever um programa que use uma API ASN.1 está aqui: | ||
+ | * [[Compilador_ASN1 | Compilador ASN.1 e a API ASN1++]] | ||
+ | |||
+ | |||
+ | De forma alternativa, existem compiladores ASN.1 para outras linguagens de programação, como este: | ||
+ | * [https://www.openmuc.org/asn1/ jASN.1: Compilador ASN.1 para Java] | ||
+ | |||
+ | === Atividade === | ||
+ | |||
+ | # Seja um tipo de mensagem contendo estes dados: <syntaxhighlight lang=text> | ||
+ | id: número inteiro | ||
+ | nome: string composta por caracteres imprimíveis | ||
+ | valores: lista de números inteiros | ||
+ | timestamp: data e horário | ||
+ | </syntaxhighlight>... especifique-a com ASN.1. | ||
+ | # Compile sua especificação usando o [[Compilador_ASN1|Compilador ASN.1]] | ||
+ | # 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 == | ||
+ | |||
+ | * [http://www.linuxhowtos.org/C_C++/socket.htm Um tutorial resumido sobre sockets] | ||
+ | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/cs556-3rd-tutorial.pdf Outro tutorial mais completo (cópia no servidor de Tele)] ([http://www.csd.uoc.gr/~hy556/material/tutorials/cs556-3rd-tutorial.pdf versão original]) | ||
+ | |||
+ | |||
+ | O uso sockets TCP usando a API de sockets pode ser resumida nos seguintes passos: | ||
+ | |||
+ | '''Cliente:''' | ||
+ | # Criar um socket ([http://manpages.ubuntu.com/manpages/trusty/man2/socket.2.html socket]) | ||
+ | # Vinculá-lo a um endereço e port ([http://manpages.ubuntu.com/manpages/trusty/man2/bind.2.html bind]) | ||
+ | # Conectá-lo a um servidor ([http://manpages.ubuntu.com/manpages/trusty/man2/connect.2.html connect]) | ||
+ | # Enviar e receber dados ([http://manpages.ubuntu.com/manpages/trusty/man2/send.2.html send], [http://manpages.ubuntu.com/manpages/trusty/man2/recv.2.html recv]) | ||
+ | # Encerrá-lo ([http://manpages.ubuntu.com/manpages/trusty/man2/close.2.html close]) | ||
+ | |||
+ | |||
+ | '''Servidor:''' | ||
+ | # Criar um socket ([http://manpages.ubuntu.com/manpages/trusty/man2/socket.2.html socket]) | ||
+ | # Vinculá-lo a um endereço e port ([http://manpages.ubuntu.com/manpages/trusty/man2/bind.2.html bind]) | ||
+ | # Ativá-lo para que possa receber conexões ([http://manpages.ubuntu.com/manpages/trusty/man2/listen.2.html listen]) | ||
+ | # Receber conexão ([http://manpages.ubuntu.com/manpages/trusty/man2/accept.2.html accept]) ... isso cria um novo socket automaticamente, através do qual se pode: | ||
+ | ## Enviar e receber dados ([http://manpages.ubuntu.com/manpages/trusty/man2/send.2.html send], [http://manpages.ubuntu.com/manpages/trusty/man2/recv.2.html recv]) | ||
+ | ## Encerrá-lo ([http://manpages.ubuntu.com/manpages/trusty/man2/close.2.html close]) | ||
+ | # Encerrar o socket principal ([http://manpages.ubuntu.com/manpages/trusty/man2/close.2.html 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++: | ||
+ | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/socket++2.tgz Socket++: classes para uso simplificado de socket TCP e UDP] ([https://github.com/mmsobral/socketpp versão no github]) | ||
+ | |||
+ | |||
+ | 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'': | ||
+ | |||
+ | {{collapse top| Exemplo de cliente}} | ||
+ | <syntaxhighlight lang=c> | ||
+ | #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; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | {{collapse bottom}} | ||
+ | |||
+ | {{collapse top| Exemplo de servidor}} | ||
+ | <syntaxhighlight lang=c> | ||
+ | #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; | ||
+ | } | ||
+ | |||
+ | </syntaxhighlight> | ||
+ | {{collapse bottom}} | ||
+ | |||
+ | {{collapse top|Diagrama de classe da biblioteca Socket++}} | ||
+ | O diagrama de classes a seguir mostra as classes envolvidas: | ||
+ | |||
+ | [[imagem:PTC-UML-Socket++.jpg|800px]] | ||
+ | {{collapse bottom}} | ||
+ | |||
+ | ... mas quem quiser tentar (ou conhecer) algo mais completo, pode experimentar a [http://www.boost.org/doc/libs/1_61_0/doc/html/boost_asio.html 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:<syntaxhighlight lang=text> | ||
+ | Teste1 DEFINITIONS AUTOMATIC TAGS ::= | ||
+ | BEGIN | ||
+ | |||
+ | Mensagem ::= SEQUENCE { | ||
+ | id INTEGER, | ||
+ | nome PrintableString (SIZE(1..16)) | ||
+ | } | ||
+ | |||
+ | END | ||
+ | </syntaxhighlight>... e de utilização: | ||
+ | |||
+ | {{collapse top|demo.cc}} | ||
+ | <syntaxhighlight lang=c> | ||
+ | #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; | ||
+ | |||
+ | }</syntaxhighlight> | ||
+ | {{collapse bottom}} | ||
+ | |||
+ | |||
+ | 2. Estenda seu programa para que as mensagens possam ser de dois tipos:<syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | </syntaxhighlight>... exemplo de utilização: <syntaxhighlight lang=c> | ||
+ | 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..."); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | = 26/10: Projeto 2: ABNF = | ||
+ | * [https://tools.ietf.org/html/rfc5234 RFC 5234: ABNF] | ||
+ | * [https://en.wikipedia.org/wiki/Augmented_Backus%E2%80%93Naur_Form ABNF na Wikipedia] | ||
+ | |||
+ | |||
+ | ABNF (''Augmented Backus-Naur Form'') é uma notação sintática definida pelo [http://www.ietf.org/ IETF] para especificar mensagens de protocolos de aplicação da Internet. Ela é usada para especificar mensagens dos protocolos [https://tools.ietf.org/html/rfc7230 HTTP], [https://www.ietf.org/rfc/rfc3261.txt SIP], [http://www.ietf.org/rfc/rfc5321.txt SMTP], [https://tools.ietf.org/html/rfc3501 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'': | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | email=trecho arroba 1*8(trecho ".") trecho | ||
+ | trecho=1*16ALPHA | ||
+ | arroba=%d64 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | 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 [https://tools.ietf.org/html/rfc5234 RFC 5234]. | ||
+ | |||
+ | == Compilador ABNF == | ||
+ | |||
+ | * [[ABNF%2B%2B:_um_gerador_de_parser_ABNF|ABNF++: um gerador de parser 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: | ||
+ | * [http://www.coasttocoastresearch.com/ APG] | ||
+ | * [http://www.parse2.com/ parse2] | ||
+ | ... 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%2B%2B:_um_gerador_de_parser_ABNF|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: | ||
+ | * [http://json.org/ JSON (Javascript Object Notation)]: define uma notação para representar dados, podendo ser considerada a própria sintaxe concreta. | ||
+ | ** [http://www.ietf.org/rfc/rfc7159.txt RFC 7159: The JavaScript Object Notation (JSON) Data Interchange Format] | ||
+ | ** [http://rapidjson.org/ RapidJSON: um parser/gerador JSON para C++] | ||
+ | * [http://msgpack.org/index.html MessagePack]: define uma codificação de dados (sintaxe concreta). Não oferece uma notação para representá-los independente de linguagem e plataforma. | ||
+ | * [https://developers.google.com/protocol-buffers/ Protocol Buffers]: define uma notação para especificar dados que guarda semelhanças com ASN.1. A parte de sintaxe concreta é predefinida. | ||
+ | |||
+ | == Atividade == | ||
+ | |||
+ | # 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. | ||
+ | # O reconhecimento de URI é uma tarefa mais complexa. Crie um parser a partir [http://tele.sj.ifsc.edu.br/~msobral/ptc/abnf/uri.txt desta gramática], e teste-o com diferentes URI. | ||
+ | # 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 = | ||
+ | |||
+ | * Prazo de entrega: '''16/11/2017''' | ||
+ | * Entrega deve ser composta de um arquivo compactado contendo os arquivos do projeto, e um relatório técnico segundo o [https://github.com/ifsc-saojose/modelos-latex/tree/master/artigo/modelo-ifsc-proppi modelo de artigo/relatório] disponibilizado pelo professor Emerson Mello. | ||
+ | * [https://moodle.sj.ifsc.edu.br/mod/assign/view.php?id=3680 Entrega deve ser feita pelo Moodle]. | ||
+ | |||
+ | = 16/11: Verificação de protocolos = | ||
+ | |||
+ | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/spin643.tar.gz Spin (código-fonte)] | ||
+ | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/holzmann/The_SPIN_Model_Checker.chm The SPIN Model Checker (livro em formato CHM)] | ||
+ | ** [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/holzmann/The_SPIN_Model_Checker.pdf The SPIN Model Checker (versão em PDF)] | ||
+ | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/holzmann/cap-5.pdf capítulo 5 do livro ''Design and Validation of Computer Protocols''] | ||
+ | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/SpinTutorial.pdf Um tutorial sobre SPIN] | ||
+ | |||
+ | |||
+ | 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 [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/holzmann/cap-2.pdf 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: | ||
+ | # 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. | ||
+ | # 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 [https://en.wikipedia.org/wiki/Specification_and_Description_Language SDL]: | ||
+ | |||
+ | |||
+ | [[imagem:PTC-Lynch2.png]] | ||
+ | <br>''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 ... | ||
+ | |||
+ | |||
+ | <!-- | ||
+ | [[imagem:PTC-Lynch1.png]] | ||
+ | <br>''Diagrama de tempo que mostra uma sequência de mensagens que causa duplicação de mensagens no receptor'' | ||
+ | --> | ||
+ | == 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 [https://en.wikipedia.org/wiki/Model_checking 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: | ||
+ | # ''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. | ||
+ | # ''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. | ||
+ | # ''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 [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/holzmann/The_SPIN_Model_Checker.chm The SPIN Model Checker] ([http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/holzmann/The_SPIN_Model_Checker.pdf 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: | ||
+ | # Instale os softwares ''bison'' e ''flex''. No Ubuntu ou Debian isso se faz assim: <syntaxhighlight lang=bash> | ||
+ | sudo apt-get install bison flex | ||
+ | </syntaxhighlight> | ||
+ | # Obtenha o [http://tele.sj.ifsc.edu.br/~msobral/ptc/spin643.tar.gz código-fonte do SPIN] | ||
+ | # Descompacte-o em algum diretório | ||
+ | # A partir desse diretório, em um terminal entre em ''Spin/Src6.4.3'' | ||
+ | # Execute o comando ''make'' | ||
+ | # 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. | ||
+ | |||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | active proctype hello() { | ||
+ | printf("hello world: meu id é %d\n", _pid) | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | 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: | ||
+ | |||
+ | <syntaxhighlight lang=bash> | ||
+ | aluno@M2:~$ spin hello.pml | ||
+ | hello world: meu id é 0 | ||
+ | 1 process created | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | 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'': | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | active proctype hello() { | ||
+ | printf("hello world: meu id é %d\n", _pid) | ||
+ | run hello() | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | ==== 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 [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/holzmann/The_SPIN_Model_Checker.chm 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: | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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") | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== Tipos definidos ==== | ||
+ | |||
+ | Podem-se definir tipos de dados de forma semelhante à declaração ''struct'' de linguagem C, usando-se a palavra-chave ''typedef'': | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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) | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | .. 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: | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | typedef Mensagem = { | ||
+ | pid uid; | ||
+ | int cod | ||
+ | } | ||
+ | |||
+ | chan canal = [1] of {int} | ||
+ | chan outro = [8] of {Mensagem} | ||
+ | chan maisum = [0] of {int, byte} | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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) | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | 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: | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== 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: | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | active proctype p2() { | ||
+ | do | ||
+ | :: canal1!111 -> printf("enviei pelo canal1\n") | ||
+ | :: canal2!222 -> printf("enviei pelo canal2\n") | ||
+ | od | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 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. | ||
+ | |||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | active proctype p2() { | ||
+ | do | ||
+ | :: canal1!111 -> printf("enviei pelo canal1\n") | ||
+ | :: canal2!222 -> printf("enviei pelo canal2\n") | ||
+ | :: else -> break | ||
+ | od | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | 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. | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== Seleção de valores ==== | ||
+ | |||
+ | A seleção ou sorteio de valores pode ser feita com ''select'', que escolhe um valor dentre uma faixa: | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== 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: | ||
+ | |||
+ | # Execute o ''spin'' instruindo-o a gerar um verificador:<syntaxhighlight lang=bash> | ||
+ | ./spin -a modelo.pml | ||
+ | </syntaxhighlight> | ||
+ | # O comando anterior deve gerar um arquivo de programa ''pan.c''. Compile-o com este comando: <syntaxhighlight lang=bash> | ||
+ | gcc -o pan pan.c | ||
+ | </syntaxhighlight> | ||
+ | # Execute o verificador: <syntaxhighlight lang=bash> | ||
+ | ./pan | ||
+ | </syntaxhighlight> | ||
+ | # Pode-se executar o verificador de formas alternativas. Por exemplo, para buscar ciclos sem progresso no modelo: <syntaxhighlight lang=bash> | ||
+ | ./pan -l | ||
+ | </syntaxhighlight> | ||
+ | # Outras opções do verificador podem ser vistas com: <syntaxhighlight lang=bash> | ||
+ | ./pan -h | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == Atividade == | ||
+ | |||
+ | # 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. | ||
+ | # Modifique seu modelo da questão anterior para que existam 2 produtores. | ||
+ | # 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. | ||
+ | # Escreva um modelo para um protocolo do tipo pare-e-espere em um ambiente de execução livre de erros. | ||
+ | # 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. | ||
+ | # Crie um modelo para o protocolo Lynch e use-o para verificar se esse protocolo é livre de erros. | ||
+ | # 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. | ||
+ | # Escreva um modelo para o enquadramento que foi implementado no protocolo ponto-a-ponto. | ||
+ | |||
+ | {{collapse top|Modelos para o protocolo pare-espere}} | ||
+ | [[imagem:PTC-Pare-espere-ex4.jpg|400px]] | ||
+ | <br>''Modelos implementam esta MEF'' | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | } | ||
+ | </syntaxhighlight>''Modelo para demonstrar uma forma de implementar uma máquina de estados com Promela'' | ||
+ | |||
+ | <!-- | ||
+ | {{collapse top | Modelo com dois tipos de processo}} | ||
+ | <syntaxhighlight lang=text> | ||
+ | mtype = {data, ack}; | ||
+ | |||
+ | chan M = [1] of {mtype, bit}; | ||
+ | chan W = [1] of {mtype, bit}; | ||
+ | |||
+ | active proctype Mproc() | ||
+ | { | ||
+ | bit sent,recv; | ||
+ | int seq | ||
+ | |||
+ | M!data,0; | ||
+ | |||
+ | do | ||
+ | :: W?data,seq -> | ||
+ | if // simula perda de msg | ||
+ | :: M!ack,seq -> | ||
+ | printf("%d enviou ack %d\n", _pid, recv); | ||
+ | if | ||
+ | :: seq == recv -> recv = !recv | ||
+ | :: else -> skip | ||
+ | fi | ||
+ | :: skip -> | ||
+ | printf("%d perdeu data %d\n", _pid, recv) | ||
+ | fi | ||
+ | :: W?ack,seq -> | ||
+ | if | ||
+ | :: seq == sent -> | ||
+ | sent = !sent | ||
+ | :: else -> skip | ||
+ | fi | ||
+ | M!data,sent | ||
+ | printf("%d transmitiu msg %d\n", _pid, sent) | ||
+ | :: timeout-> | ||
+ | M!data,sent | ||
+ | printf("%d transmitiu msg %d (timeout)\n", _pid, sent) | ||
+ | od | ||
+ | } | ||
+ | |||
+ | active proctype Wproc() | ||
+ | { | ||
+ | bit sent,recv; | ||
+ | int seq | ||
+ | |||
+ | W!data,0; | ||
+ | |||
+ | do | ||
+ | :: M?data,seq -> | ||
+ | if // simula perda de msg | ||
+ | :: W!ack,seq -> | ||
+ | printf("%d enviou ack %d\n", _pid, recv); | ||
+ | if | ||
+ | :: seq == recv -> recv = !recv | ||
+ | :: else -> skip | ||
+ | fi | ||
+ | :: skip -> | ||
+ | printf("%d perdeu data %d\n", _pid, recv) | ||
+ | fi | ||
+ | :: M?ack,seq -> | ||
+ | if | ||
+ | :: seq == sent -> | ||
+ | sent = !sent | ||
+ | :: else -> skip | ||
+ | fi | ||
+ | W!data,sent | ||
+ | printf("%d transmitiu msg %d\n", _pid, sent) | ||
+ | :: timeout-> | ||
+ | W!data,sent | ||
+ | printf("%d transmitiu msg %d (timeout)\n", _pid, sent) | ||
+ | od | ||
+ | } | ||
+ | </syntaxhighlight>Modelo com dois tipos de processos, que representam as duas entidades de protocolo. | ||
+ | {{collapse bottom}} | ||
+ | |||
+ | |||
+ | {{collapse top | Modelo com um tipo de processo e canais parametrizados}} | ||
+ | <syntaxhighlight lang=c> | ||
+ | mtype = {data, ack}; | ||
+ | |||
+ | chan c1 = [1] of {mtype, bit}; | ||
+ | chan c2 = [1] of {mtype, bit}; | ||
+ | |||
+ | proctype Mproc(chan M, W) | ||
+ | { | ||
+ | bit sent,recv; | ||
+ | int seq | ||
+ | |||
+ | M!data,0; | ||
+ | |||
+ | do | ||
+ | :: W?data,seq -> | ||
+ | if // simula perda de msg | ||
+ | :: M!ack,seq -> | ||
+ | printf("%d enviou ack %d\n", _pid, recv); | ||
+ | if | ||
+ | :: seq == recv -> recv = !recv | ||
+ | :: else -> skip | ||
+ | fi | ||
+ | :: skip -> | ||
+ | printf("%d perdeu data %d\n", _pid, recv) | ||
+ | fi | ||
+ | :: W?ack,seq -> | ||
+ | if | ||
+ | :: seq == sent -> | ||
+ | sent = !sent | ||
+ | :: else -> skip | ||
+ | fi | ||
+ | M!data,sent | ||
+ | printf("%d transmitiu msg %d\n", _pid, sent) | ||
+ | :: timeout-> | ||
+ | M!data,sent | ||
+ | printf("%d transmitiu msg %d (timeout)\n", _pid, sent) | ||
+ | od | ||
+ | } | ||
+ | |||
+ | active proctype start() { | ||
+ | run Mproc(c1, c2) | ||
+ | run Mproc(c2, c1) | ||
+ | } | ||
+ | </syntaxhighlight>Modelo com um tipo de processo, que representa uma entidade de protocolo. Esse tipo de processo é instanciado duas vezes, correspondendo aos dois nodos que se comunicam. Os canais de comunicação são parametrizados. | ||
+ | {{collapse bottom}} | ||
+ | --> | ||
+ | {{collapse bottom}} | ||
+ | |||
+ | <!-- | ||
+ | {{collapse top | Um modelo para o protocolo Lynch}} | ||
+ | <syntaxhighlight lang=text> | ||
+ | mtype = {ack, nack, err}; | ||
+ | |||
+ | chan M = [1] of {mtype,int}; | ||
+ | chan W = [1] of {mtype, int}; | ||
+ | |||
+ | active proctype Mproc() | ||
+ | { | ||
+ | int data; // carga da msg | ||
+ | int rdata, odata=-1; | ||
+ | mtype msg; | ||
+ | |||
+ | M!err(data); | ||
+ | |||
+ | end: do | ||
+ | :: W?msg(rdata) -> | ||
+ | if // gera erro de forma nao-deterministica | ||
+ | :: true -> | ||
+ | msg = err | ||
+ | :: true -> | ||
+ | skip | ||
+ | fi; | ||
+ | |||
+ | // mostra informações sobre msg recebida | ||
+ | atomic {printf("%d recebeu ", _pid); | ||
+ | printm(msg); | ||
+ | printf(",%d\n", rdata);} | ||
+ | |||
+ | if | ||
+ | :: msg == ack -> | ||
+ | data = data + 1; | ||
+ | odata = rdata; | ||
+ | M!ack(data) | ||
+ | :: msg == nack -> | ||
+ | odata = rdata; | ||
+ | M!ack(data) | ||
+ | :: msg == err -> | ||
+ | M!nack(data) | ||
+ | fi | ||
+ | od | ||
+ | } | ||
+ | |||
+ | active proctype Wproc() | ||
+ | { | ||
+ | int data; // carga da msg | ||
+ | int rdata,odata=-1; | ||
+ | mtype msg; | ||
+ | |||
+ | W!err(data); | ||
+ | |||
+ | end: do | ||
+ | :: M?msg(rdata) -> | ||
+ | if // gera erro de forma nao-deterministica | ||
+ | :: true -> | ||
+ | msg = err | ||
+ | :: true -> | ||
+ | skip | ||
+ | fi; | ||
+ | |||
+ | // mostra informações sobre msg recebida | ||
+ | atomic {printf("%d recebeu ", _pid); | ||
+ | printm(msg); | ||
+ | printf(",%d\n", rdata);} | ||
+ | |||
+ | if | ||
+ | :: msg == ack -> | ||
+ | data = data + 1; | ||
+ | odata = rdata; | ||
+ | W!ack(data) | ||
+ | :: msg == nack -> | ||
+ | odata = rdata; | ||
+ | W!ack(data) | ||
+ | :: msg == err -> | ||
+ | W!nack(data) | ||
+ | fi | ||
+ | od | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | {{collapse bottom}} | ||
+ | --> | ||
+ | |||
+ | = 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: | ||
+ | |||
+ | [[imagem:PTC-Mef0.jpg|400px]] | ||
+ | |||
+ | |||
+ | 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. | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | = 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 [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/holzmann/The_SPIN_Model_Checker.chm The SPIN Model Checker], e o [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/holzmann/cap-6.pdf 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. | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <syntaxhighlight lang=bash> | ||
+ | # 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 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === 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. | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Para experimentar o diagnóstico do ''spin'' sobre esse modelo, deve-se compilar o verificador da seguinte forma: | ||
+ | |||
+ | <syntaxhighlight lang=bash> | ||
+ | aluno@M1:~$ spin -a ping.pml | ||
+ | aluno@M1:~$ gcc -DSAFETY -o pan pan.c | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | A execução do verificador, e o resultado por ele apresentado, são mostrados a seguir: | ||
+ | |||
+ | <syntaxhighlight lang=bash> | ||
+ | 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 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 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): | ||
+ | |||
+ | <syntaxhighlight lang=bash> | ||
+ | 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 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 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'': | ||
+ | |||
+ | <syntaxhighlight lang=text n> | ||
+ | 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 | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <syntaxhighlight lang=bash> | ||
+ | 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 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== 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: | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Nitidamente não há um [https://en.wikipedia.org/wiki/Deadlock 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 [https://en.wikipedia.org/wiki/Deadlock#Livelock 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): | ||
+ | |||
+ | <syntaxhighlight lang=bash> | ||
+ | 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 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | O verificador criado a partir desse modelo fornece um diagnóstico sem ''livelock'': | ||
+ | |||
+ | <syntaxhighlight lang=bash> | ||
+ | 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 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== 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. | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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 | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | 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. | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | active proctype Dijkstra() | ||
+ | { byte count = 1; | ||
+ | end: do | ||
+ | :: (count == 1) -> | ||
+ | progress: sema!p; count = 0 | ||
+ | :: (count == 0) -> | ||
+ | sema?v; count = 1 | ||
+ | od | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == Atividade == | ||
+ | |||
+ | Para exercitar a verificação de protocolos, faça o seguinte: | ||
+ | # 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 | ||
+ | # 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 | ||
+ | # 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 | ||
+ | # 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 | ||
+ | # 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 | ||
+ | # Verifique o protocolo Lynch quanto: | ||
+ | #* ''não duplicidade de mensagens'' | ||
+ | #* ''ausência de deadlock'' | ||
+ | #* ''ausência de livelock'' | ||
+ | # 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 = | ||
+ | |||
+ | * [http://spinroot.com/spin/Man/ltl.html Manual LTL para Spin] | ||
+ | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/introduction-to-ltl.pdf Introdução a LTL no Spin] | ||
+ | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/ltl.pdf Métodos formais e Lógica Temporal] | ||
+ | * [http://tele.sj.ifsc.edu.br/~msobral/ptc/Spin%20Cheat%20Sheet%20V1.pdf Resumo sobre Spin e LTL] | ||
+ | |||
+ | |||
+ | 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'': | ||
+ | * '''X'''''f'': ''f'' é verdadeiro no próximo estado (''next f'') | ||
+ | * '''G'''''f'': ''f'' é verdadeiro em todos os estados (''always f'') | ||
+ | * '''F'''''f'': ''f'' é verdadeiro em algum estado futuro (''eventually f'') | ||
+ | * ''f'''U'''g'': ''g'' é verdadeiro em algum estado futuro, e ''f'' é verdadeiro em cada estado até então (''f until g'') | ||
+ | |||
+ | Outra notação: | ||
+ | * '''G''': <math>\square</math> | ||
+ | * '''F''': <math>\diamondsuit</math> | ||
+ | * '''X''': <math>\bigcirc</math> | ||
+ | |||
+ | |||
+ | 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 <math>p \in P</math>) | ||
+ | * Operadores booleanos: | ||
+ | ** <math>\neg f</math>: ''not f'' | ||
+ | ** <math>f \vee g</math>: ''f or g'' | ||
+ | ** <math>f \wedge g</math>: ''f and g'' | ||
+ | ** <math>f \implies g</math>: ''f implies g'' | ||
+ | * Operadores temporais: | ||
+ | ** <math>\bigcirc f</math> | ||
+ | ** <math>\square f</math> | ||
+ | ** <math>\diamondsuit f</math> | ||
+ | ** ''f '''U''' g'' | ||
+ | |||
+ | |||
+ | Uma outra forma de visualizar esses operadores temporais está ilustrada a seguir: | ||
+ | |||
+ | [[imagem:PTC-Operadores-ltl.png]] | ||
+ | |||
+ | |||
+ | Algumas identidades entre fórmulas com esses operadores são: | ||
+ | * <math>\square f \Leftrightarrow \neg (\diamondsuit \neg f)</math> ... <math>\diamondsuit f \Leftrightarrow \neg (\square \neg f)</math> | ||
+ | * <math>\square \square f \Leftrightarrow \square f</math>, <math>\diamondsuit \diamondsuit f \Leftrightarrow \diamondsuit f</math> | ||
+ | * <math>\diamondsuit f \Leftrightarrow true U f</math>, <math>\square f \Leftrightarrow \neg (true U \neg f)</math> | ||
+ | * ''f '''W''' g''<math>\Leftrightarrow f U g \vee f</math> | ||
+ | |||
+ | |||
+ | Propriedades distributivas: | ||
+ | * <math>\square (f \wedge g) \Leftrightarrow \square f \wedge \square g</math> | ||
+ | * <math>\diamondsuit (f \vee g) \Leftrightarrow \diamondsuit f \vee \diamondsuit g</math> | ||
+ | * <math>\bigcirc (f U g) \Leftrightarrow (\bigcirc f) U (\bigcirc g)</math> | ||
+ | * <math>\bigcirc (f \vee g) \Leftrightarrow (\bigcirc f) \vee (\bigcirc g)</math> | ||
+ | * <math>\bigcirc (f \wedge g) \Leftrightarrow (\bigcirc f) \wedge (\bigcirc g)</math> | ||
+ | |||
+ | |||
+ | == LTL e Spin == | ||
+ | |||
+ | O verificador ''spin'' é capaz de verificar fórmulas LTL. As proposições primitivas podem ser: | ||
+ | {|border=1 | ||
+ | !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'')<br>''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'')<br>''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''): | ||
+ | |||
+ | {| border=1 | ||
+ | !Operador LTL no Spin | ||
+ | !Operador LTL | ||
+ | |- | ||
+ | | ''[]'' || <math>\square</math> (always) | ||
+ | |- | ||
+ | | ''<>'' || <math>\diamondsuit</math> (eventually) | ||
+ | |- | ||
+ | | ''U'' || U (until) | ||
+ | |- | ||
+ | | ''&&'' || <math>\wedge</math> | ||
+ | |- | ||
+ | | <nowiki>||</nowiki> || <math>\vee</math> | ||
+ | |- | ||
+ | | ''->'' || <math>\implies</math> | ||
+ | |- | ||
+ | | ''<->'' || <math>\Leftrightarrow</math> | ||
+ | |} | ||
+ | |||
+ | |||
+ | 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: | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | ltl nome1 { formula 1} | ||
+ | ltl nome2 { formula 2} | ||
+ | ... | ||
+ | ltl nomeN { formula N} | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | A geração do verificador deve ser feita com estes comandos: | ||
+ | |||
+ | <syntaxhighlight lang=bash> | ||
+ | spin -a modelo.pml | ||
+ | gcc -o pan pan.c | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Por fim, a execução do verificador se faz assim: | ||
+ | |||
+ | <syntaxhighlight lang=bash> | ||
+ | ./pan -a | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == 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: | ||
+ | # Processos A e B nunca estão na seção crítica ao mesmo tempo: | ||
+ | #* <math>\square (\neg inCS_A \vee \neg inCS_B)</math> | ||
+ | # Nenhum dos processos monopoliza a seção crítica: | ||
+ | #* <math>\square (inCS_A \implies \diamondsuit \neg inCS_A)</math> | ||
+ | #* <math>\square (inCS_B \implies \diamondsuit \neg inCS_B)</math> | ||
+ | |||
+ | |||
+ | No ''spin'', essa verificação pode ser feita com este modelo e respectivas fórmulas LTL: | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | 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)} | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == Atividades == | ||
+ | |||
+ | 1. Para o sistema descrito no modelo Promela a seguir, verifique se o contador atinge o valor 10: <syntaxhighlight lang=text> | ||
+ | int count; | ||
+ | bool incr; | ||
+ | |||
+ | active proctype counter() { | ||
+ | do | ||
+ | :: incr -> count++ | ||
+ | od | ||
+ | } | ||
+ | |||
+ | active proctype env() { | ||
+ | do | ||
+ | :: incr = false | ||
+ | :: incr = true | ||
+ | od | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | {{collapse top | Fórmula LTL}} | ||
+ | <!-- | ||
+ | <syntaxhighlight lang=text> | ||
+ | ltl ok { ([] <> (_last == 0)) -> <> (count == 10) } | ||
+ | </syntaxhighlight> | ||
+ | --> | ||
+ | {{collapse bottom}} | ||
+ | |||
+ | 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. <!-- [] <> (incr && _last == 0) --> | ||
+ | |||
+ | {{collapse top | Fórmulas LTL}} | ||
+ | <!-- | ||
+ | <syntaxhighlight lang=text> | ||
+ | // versão do professor | ||
+ | ltl justica1 { ([] enabled(0) -> (<> (_last == 0))) && ([] enabled(1) -> (<> (_last == 1))) } | ||
+ | |||
+ | // versão do André, Gabriel e Maria Luiza | ||
+ | ltl justica2 { ([](_last == 0) -> (<> (_last == 1))) && ([](_last == 1) -> (<> (_last == 0))) } | ||
+ | </syntaxhighlight> | ||
+ | --> | ||
+ | {{collapse bottom}} | ||
+ | |||
+ | 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 <!-- ([] <> !loss) -> ([] (sendA -> FrecA)) --> | ||
+ | * Se uma mensagem for recebida, ela foi previamente transmitida <!-- <> recA -> ((! recA) U sentA) --> | ||
+ | |||
+ | Por fim, escreva esse modelo Promela, declare as fórmulas LTL, e verifique seu modelo. | ||
+ | |||
+ | |||
+ | 4. Aplique a verificação com LTL aos [[PTC-2017-2#Atividade_10|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: | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | int n=7 | ||
+ | |||
+ | active proctype teste() { | ||
+ | do | ||
+ | :: n > 0 -> n-- | ||
+ | :: n < 5 -> n++ | ||
+ | od | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Nesse modelo a variável global ''n'' inicia com o valor 7, e nos estados subsequentes ela pode variar aleatoriamente entre 0 e 5. Considerem-se as seguintes fórmulas LTL: | ||
+ | * '''(n == 7)''': esta fórmula avalia para '''verdadeiro''', pois no primeiro estado do modelo de fato ''n'' tem o valor 7. | ||
+ | * '''(n == 5)''': esta fórmula avalia para '''falso''', pois no primeiro estado do modelo ''n'' tem na verdade o valor 7. | ||
+ | |||
+ | == Operadores temporais == | ||
+ | |||
+ | Os operadores temporais são usados para criar proposições que envolvem mais de um estado. São eles (essas figuras foram copiadas [http://tele.sj.ifsc.edu.br/~msobral/ptc/Spin%20Cheat%20Sheet%20V1.pdf deste resumo sobre Spin e Promela]). | ||
+ | |||
+ | [[imagem:PTC-Always.jpg]] | ||
+ | |||
+ | [[imagem:PTC-Eventually.jpg]] | ||
+ | |||
+ | |||
+ | Assim como fórmulas simples, fórmulas LTL com esses operadores são aplicadas ao estado inicial. | ||
+ | |||
+ | |||
+ | Considerando o exemplo da seção anterior, podem-se declarar estas fórmulas: | ||
+ | * '''[] (n == 7)''': avalia para '''falso''', uma vez que a variável ''n'' inicia com valor 7, mas nos estados subsequentes ela assume outros valores. | ||
+ | * '''[] (n >= 0)''': avalia para '''verdadeiro''', uma vez que a variável ''n'' assume valores entre 0 e 7. | ||
+ | * '''<>(n == 7)''': avalia para '''verdadeiro''', pois a variável ''n'' inicia com valor 7 | ||
+ | * '''<>(n == 5)''': avalia para '''verdadeiro''', pois realmente em algum estado futuro ''n'' terá o valor 5. | ||
+ | * '''<>(n == 0)''': avalia para '''falso''', pois existe ao menos uma sequência de estados em que ''n'' nunca assume o valor 0 (imagine que ''n'' oscile entre 4 e 5 indefinidamente, por exemplo). | ||
+ | |||
+ | |||
+ | Esses operadores temporais podem ser combinados de forma a gerar fórmulas mais ricas: | ||
+ | |||
+ | [[imagem:PTC-Always-eventually.jpg]] | ||
+ | <br>''Indefinidamente frequente: a proposição A é verdadeira ao menos uma vez em um ciclo'' | ||
+ | |||
+ | |||
+ | [[imagem:PTC-Latching.jpg]] | ||
+ | <br>''Travamento: em algum estado futuro a proposição A é verdadeira, e desse estado em diante ela é sempre verdadeira'' | ||
+ | |||
+ | |||
+ | Novamente considerando o exemplo da seção anterior, podem-se avaliar estas fórmulas: | ||
+ | * '''[] <> (n != 5)''': avalia para '''verdadeiro''', pois a variável ''n'' assume valores diferentes de 5 de forma indefinitamente frequente (compare isso com o resultado da fórmula '''<> (n == 5)''' vista anteriormente). | ||
+ | * '''[] <> (n == 5)''': avalia para '''falso''', pois a variável ''n'', apesar de assumir o valor 5 garantidamente ao menos uma vez, pode passar a assumir valores menores que 5 de forma indefinitamente frequente. | ||
+ | * '''<> [] (n < 5)''': avalia para '''falso''', pois a variável ''n'' pode eventualmente assumir o valor 5. | ||
+ | * '''<> [] (n < 6)''': avalia para '''verdadeiro''', pois uma vez que a variável ''n'' assume o valor 5, nunca mais se torna maior que 5. | ||
+ | |||
+ | == Precedência entre propriedades == | ||
+ | |||
+ | Os operadores temporais não podem relacionar propriedades em diferentes estados (ou instantes). Por exemplo, pode-se desejar expressar que uma propriedade A é válida até que outra propriedade B se torne válida. Casos como esse são resolvidos com o operador '''U''' (''until''). | ||
+ | |||
+ | [[imagem:PTC-Until.jpg]] | ||
+ | |||
+ | |||
+ | Dever-se notar que a propriedade A deve ser válida '''pelo menos''' até que B se torne verdadeira. Assim, A pode continuar válida mesmo após B se tornar verdadeira, e ainda assim a relação ''until'' ser verdadeira. Outro detalhe é que basta que B seja verdadeira uma vez para que a relação seja verdadeira. | ||
+ | |||
+ | |||
+ | Exemplificando esse operador com o modelo das seções anteriores, podem-se escrever estas fórmulas: | ||
+ | * '''(n > 5) U (n <= 5)''': avalia para '''verdadeiro''', pois no início a variável decrementa de 7 até 5, e depois seus valores ficam entre 0 e 5. | ||
+ | * '''(n > 5) U (n == 5)''': avalia para '''verdadeiro''', pois no início a variável decrementa de 7 até 5 (apesar de, uma vez atingido o valor 5, a variável ''n'' poder nunca mais assumir novamente esse valor). | ||
+ | * '''(n == 6) U (n <= 5)''': avalia para '''falso''', pois a variável ''n'' inicia com 7 (lembre que fórmulas são avaliadas a partir do primeiro estado do modelo) | ||
+ | * '''<> ((n == 6) U (n <= 5))''': avalia para '''verdadeiro''', pois em algum estado futuro a variável ''n'' terá o valor 6 até que seus valores fiquem então entre 0 e 5. | ||
+ | |||
+ | == Implicação entre propriedades == | ||
+ | |||
+ | A relação de implicação entre propriedades expressada por '''A -> B''' significa que, se uma propriedade A for verdadeira, B também deve ser verdadeira. Por outro lado, se A for falsa, o valor de B é indiferente. Desta forma, essa relação é falsa somente se A for verdadeira e B for falsa. Para fins de recordação, a tabela verdade dessa relação e mostrada a seguir. | ||
+ | |||
+ | {| border=1 | ||
+ | !A | ||
+ | !B | ||
+ | !A -> B | ||
+ | |- | ||
+ | |falso || falso || verdadeiro | ||
+ | |- | ||
+ | |falso || verdadeiro || verdadeiro | ||
+ | |- | ||
+ | |verdadeiro || falso || falso | ||
+ | |- | ||
+ | |verdadeiro || verdadeiro || verdadeiro | ||
+ | |} | ||
+ | |||
+ | |||
+ | A relação de implicação pode ser útil para as fórmulas LTL, pois possibilita expressá-las como ''se algo é verdade, outra coisa deve ser verdade'', ou ''se algo é verdade, outra coisa deve ser verdade no futuro'', entre outras possibilidades. Para exemplificar a relação de implicação, o seguinte modelo é usado: | ||
+ | |||
+ | <syntaxhighlight lang=text> | ||
+ | int n = 5 | ||
+ | bool down = true | ||
+ | |||
+ | active proctype ex2() { | ||
+ | do | ||
+ | :: down -> | ||
+ | n-- | ||
+ | down = (n > 0) | ||
+ | :: else -> | ||
+ | n++ | ||
+ | down = (n == 5) | ||
+ | od | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Com base nesse modelo, algumas verificações podem ser feitas: | ||
+ | * '''[] (down -> <> !down)''': essa fórmula avalia para '''verdadeiro''' e expressa que, sempre que o valor da variável ''down'' for verdadeiro, em algum momento futuro será falso. | ||
+ | * '''[] (down -> <> !down) && [] (!down -> <> down)''': essa fórmula avalia para '''verdadeiro''' e expressa que o valor da variável ''down'' alterna entre verdadeiro e falso. | ||
+ | * '''[]<>(!down -> <> (n==5))''': avalia para '''verdadeiro''', e expressa que sempre que ''down'' é falso, no futuro a variável ''n'' terá valor 5 (e isso acontece de forma indefinidamente frequente). | ||
+ | * '''[]<>(down -> <> (n==0))''': avalia para '''verdadeiro''', e expressa que sempre que ''down'' é verdadeiro, no futuro a variável ''n'' terá valor 0 (e isso acontece de forma indefinidamente frequente). | ||
+ | |||
+ | |||
+ | Deve-se ter um cuidado com o uso da relação de implicação, pois ela avalia para verdadeiro se a propriedade da esquerda for falsa. Quer dizer, se a propriedade da esquerda nunca for verdadeira, então não importa se a propriedade implicada é verdadeira ou falsa. Assim, recomenda-se conferir se a propriedade da esquerda avalia para verdadeiro em ao menos uma possível execução do modelo. Isso pode ser feito com uma fórmula LTL como esta (supondo ''A -> B''): | ||
+ | |||
+ | <> A | ||
+ | |||
+ | Isso significa: ''A é verdadeira no futuro'', OU: ''não é sempre verdade que A seja falsa.'' | ||
+ | |||
+ | = 14/12: Conclusão = | ||
+ | |||
+ | A conclusão da etapa 3 é a verificação do protocolo do projeto 1. Isso tem por objetivos verificar se: | ||
+ | # ARQ/MAC está livre de deadlock e livelock | ||
+ | # Um quadro transmitido é entregue em algum momento futuro | ||
+ | # Quadros duplicados são descartados | ||
+ | # <strike>Somente um nodo transmite a cada vez</strike> | ||
+ | # 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: | ||
+ | # 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. | ||
+ | # Um relatório que mostre como cada propriedade foi verificada, e o resultado da verificação. | ||
+ | |||
+ | |||
+ | * [https://moodle.sj.ifsc.edu.br/mod/assign/view.php?id=3871 Formulário para entrega no Moodle] | ||
+ | |||
+ | = 15/12: Conclusão = |
Edição atual tal como às 20h53min de 20 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
Referências complementares
- ASN.1
- Internet Standard 68 (ABNF)
- SDL Forum
- RFC-1317: On the Design of Application Protocols
- Protocol Design Beyond Graph-Based Models
- PRISM: Probabilistic Model Checker
- Protocol Design (entrevista)
- Inrodução a C++ (com referências bibliográficas)
- NUSE: network stack in usespace ?
- Click moudlar router
- lwIP - A Lightweight TCP/IP stack
- User Space Networking
- NUSE: Networking Stack in Userspace
- mTCP: a Highly Scalable User-level TCP Stack for Multicore Systems
- DPDK: Data Plane Development Kit
Avaliações
As avaliações são de dois tipos:
- 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.
- Tarefas: feitas individualmente, e servem para ajustar os conceitos dos projetos (podem aumentá-los ou reduzi-los).
Aluno | Proto 1 | Proj 1 | Proj 2 | Proj 3 | FINAL |
---|---|---|---|---|---|
Bruno | 95 | 96 | 100 | 90 | 9 |
Douglas | 0 | 71 | 93 | 75 | 7 |
Fernando | 0 | 71 | 93 | 75 | 7 |
Henrique | 82 | 96 | 98 | 80 | 9 |
Leticia | 0 | 93 | 75 | ||
Lucas | 90 | 80 | 100 | 70 | 8 |
Mateus | 90 | 100 | 93 | 75 | 9 |
Mathias | 90 | 80 | 100 | 70 | 8 |
Matuzalem | 82 | 96 | 98 | 80 | 9 |
Murilo | 95 | 96 | 100 | 90 | 9 |
Natan | 95 | 96 | 100 | 90 | 9 |
Nelson | 90 | 100 | 93 | 75 | 8 |
Paula | 90 | 100 | 93 | 75 | 8 |
Cálculo do conceito final: ... e ajustes finais feitos com base em frequência
Softwares
- Compilador ASN.1
- ANTLR (Another Tool for Language Recognition)
- APG (ABNF Parser Generator)
- SPIN Model Checker
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:
- 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).
- Transmissor: dispositivo que transmite a mensagem.
- Receptor: dispositivo que recebe a mensagem.
- Meio de comunicação: caminho físico por onde viaja a mensagem do transmissor até o receptor.
- 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.
Um serviço visto por um usuário
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:
- O serviço oferecido pelo protocolo
- As considerações sobre o ambiente em que o protocolo é executado
- O vocabulário de mensagens usadas para implementar o protocolo
- A codificação (ou formato) de cada mensagem do vocabulário
- 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:
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):
Diretrizes de projeto
No mesmo capÍtulo 2 de seu livro, Gerard Holzmann enumera dez regras de projeto de um protocolo:
- 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.
- 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).
- 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.
- 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.
- Preservar independência: não conectar o que for independente, o que significa separar questões ortogonais.
- 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.
- 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.
- Torne-o eficiente: implemente o projeto, meça seu desempenho e, se necessário, otimize-o.
- Verifique a implementação: confira se a implementação final otimizada é equivalente ao protótipo de alto-nível que foi verificado.
- 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 |
---|
|
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 |
---|
|
3. Defina seu vocabulário, e também a codificação de mensagens a ser adotada
Exemplo |
---|
|
4. Descreva seu comportamento
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:
- Configurar dois transceivers RF
- Conectá-los a dois computadores diferentes usando adaptadores USB
- 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.
- 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 */
#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;
}
|
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:
- Habilitar a primeira porta serial (COM1)
- O modo dessa serial deve ser Dispositivo no hospedeiro
- O caminho do dispositivo deve ser /dev/ttyUSB0
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
- Anatomia de um processo em memória no Linux: revisão sobre uso de memória e sua relação com variáveis, objetos e parâmetros em programas C/C++
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
- Escolha uma abordagem viável para o protocolo de enlace a ser desenvolvido
- 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:
- 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:
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);
}
|
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;
}
}
}
|
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:
- Integrar o transceiver RF no Arduino, de acordo com este tutorial.
- Implementar o enquadramento no Arduino ... ver referência sobre uso da serial
- 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
- 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:
- 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.
- 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:
- Obtenha o código-fonte do serialemu
- Descompacte o arquivo Serialemu.zip, e entre no subdiretório Serialemu.
- Compile-o com este comando:
aluno@M2:~/Serialemu$ make
- 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:~/$
- 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)
- 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: ... e anote os dois caminhos informados pelo serialemu: eles são as duas portas seriais que correspondem às pontas do link serial emulado.
aluno@M2:~$ ./serialemu -B 9600 /dev/pts/17 /dev/pts/2 aluno@M2:~$
- 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)
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
- Implementação de Checksum
- Implementação de CRC em software
- Implementação do CRC-16 para PPP (RFC 1662)
- A Painless Guide to CRC Error Detection Algorithms, 3rd Edition por Ross Williams
- Um pequeno resumo sobre CRC (obtido do livro Comunicação de Dados e Redes de Computadores, de William Stallings)
O controle de erros envolve:
- Detecção de erros: cada mensagem recebida deve ter verificada a integridade de seu conteúdo. Mensagens corrompidas devem ser rejeitadas (descartadas)
- 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.
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
- Analise e escolha uma das técnicas para detecção de erros para seu protocolo.
- 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:
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:
Duas questões despontam quanto à modelagem com máquinas de estados finitos:
- As MEF projetadas podem ser minimizadas (terem menos estados) ?
- 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.
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:
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:
![]() |
![]() |
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:
- Enquadramento (framing)
- Garantia de entrega (mecanismo ARQ)
- Gerenciamento de conexão
- 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:
- Tarefa para entrega do protótipo: acesse a atividade para maiores detalhes sobre o que deve ser entregue.
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.
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.
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):
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:
- 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.
- 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:
- Usando setitimer ou alarm combinado com sinal SIGALRM
- Usando timer_create combinado com um sinal definido pelo programador
- Usando select
- Em um loop que lê o relógio a cada iteração, desviando o fluxo de execução se o timeout ocorrer
- ... 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
- Remodele sua MEF para que possa diferenciar os eventos a serem tratados
- Implemente timeouts no Enquadramento
- 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):
- 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.
- 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.
- 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:
Mestre-escravo
Um controle de acesso ao meio do tipo Mestre-Escravo. Ele também pode ser integrado ao ARQ:
Atividade
- 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:
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:
- Ao receber a mensagem CC, o iniciador já pode enviar dados pela conexão
- 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:
- A sincronização entre os participantes quanto ao término da conexão (ambos participantes devem fechar a conexão)
- 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.
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):
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
- Identifique em que parte da estrutura do seu protocolo devem se encaixar o gerenciamento de conexão
- 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:
- HTTP e HTTP2
- SIP
- NTP
- SMTP
- SSH
- BitTorrent
- ... e muitos outros !!!
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:
- Descrever a comunicação envolvida na aplicação
- 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
- Serviço:
- Defina o vocabulário do protocolo de aplicação, incluindo os tipos de mensagens e suas sintaxes.
- 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.
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> </td><td align="right"> - </td><td> </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> </td></tr>
<tr><th colspan="5"><hr></th></tr>
</table>
</body></html>
|
Esses exemplos apresentam basicamente dois tipos de codificação:
- 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).
- 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:
- ASN.1 (Abstract Syntax Notation One): ASN.1 é uma linguagem de especificação de sintaxe abstrata definida pelo ITU-T. A sintaxe concreta está dissociada dessa linguagem, podendo ser realizada de diferentes formas, tais como BER (Binary Encoding Rules), PER (Packed Encoding Rules), XER (XML Encoding Rules) e outras. A notação ASN.1 não impõe uma escolha sobre qual codificação utilizar, ficando assim de livre escolha pelo desenvolvedor.
- ABNF (Augmented Backus-Naur Form): ABNF é uma notação definida pelo IETF para expressar tanto a sintaxe abstrata quanto sua codificação textual, sendo portanto voltada para estruturas de dados representadas em modo de texto. Assim, ABNF impõe também a codificação a ser usada nas mensagens.
... 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
- Site oficial sobre ASN.1
- Introdução a ASN.1
- Um exemplo sobre ASN.1
- ASN.1: Communication between heterogeneous systems (livro gratuito)
- ASN.1 Complete (livro gratuito)
- Compilador ASN.1 ... e esse mesmo projeto no github
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
- Seja um tipo de mensagem contendo estes dados: ... especifique-a com ASN.1.
id: número inteiro nome: string composta por caracteres imprimíveis valores: lista de números inteiros timestamp: data e horário
- Compile sua especificação usando o Compilador ASN.1
- 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
- Um tutorial resumido sobre sockets
- Outro tutorial mais completo (cópia no servidor de Tele) (versão original)
O uso sockets TCP usando a API de sockets pode ser resumida nos seguintes passos:
Cliente:
- Criar um socket (socket)
- Vinculá-lo a um endereço e port (bind)
- Conectá-lo a um servidor (connect)
- Enviar e receber dados (send, recv)
- Encerrá-lo (close)
Servidor:
- Criar um socket (socket)
- Vinculá-lo a um endereço e port (bind)
- Ativá-lo para que possa receber conexões (listen)
- Receber conexão (accept) ... isso cria um novo socket automaticamente, através do qual se pode:
- 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: |
... 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:
- JSON (Javascript Object Notation): define uma notação para representar dados, podendo ser considerada a própria sintaxe concreta.
- MessagePack: define uma codificação de dados (sintaxe concreta). Não oferece uma notação para representá-los independente de linguagem e plataforma.
- Protocol Buffers: define uma notação para especificar dados que guarda semelhanças com ASN.1. A parte de sintaxe concreta é predefinida.
Atividade
- 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.
- O reconhecimento de URI é uma tarefa mais complexa. Crie um parser a partir desta gramática, e teste-o com diferentes URI.
- 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
- Prazo de entrega: 16/11/2017
- Entrega deve ser composta de um arquivo compactado contendo os arquivos do projeto, e um relatório técnico segundo o modelo de artigo/relatório disponibilizado pelo professor Emerson Mello.
- Entrega deve ser feita pelo Moodle.
16/11: Verificação de protocolos
- Spin (código-fonte)
- The SPIN Model Checker (livro em formato CHM)
- capítulo 5 do livro Design and Validation of Computer Protocols
- Um tutorial sobre SPIN
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:
- 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.
- 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:
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:
- 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.
- 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.
- 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:
- Instale os softwares bison e flex. No Ubuntu ou Debian isso se faz assim:
sudo apt-get install bison flex
- Obtenha o código-fonte do SPIN
- Descompacte-o em algum diretório
- A partir desse diretório, em um terminal entre em Spin/Src6.4.3
- Execute o comando make
- 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:
- Execute o spin instruindo-o a gerar um verificador:
./spin -a modelo.pml
- O comando anterior deve gerar um arquivo de programa pan.c. Compile-o com este comando:
gcc -o pan pan.c
- Execute o verificador:
./pan
- Pode-se executar o verificador de formas alternativas. Por exemplo, para buscar ciclos sem progresso no modelo:
./pan -l
- Outras opções do verificador podem ser vistas com:
./pan -h
Atividade
- 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.
- Modifique seu modelo da questão anterior para que existam 2 produtores.
- 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.
- Escreva um modelo para um protocolo do tipo pare-e-espere em um ambiente de execução livre de erros.
- 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.
- Crie um modelo para o protocolo Lynch e use-o para verificar se esse protocolo é livre de erros.
- 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.
- Escreva um modelo para o enquadramento que foi implementado no protocolo ponto-a-ponto.
Modelos para o protocolo pare-espere |
---|
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
}
|
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:
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:
- 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
- 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
- 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
- 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
- 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
- Verifique o protocolo Lynch quanto:
- não duplicidade de mensagens
- ausência de deadlock
- ausência de livelock
- 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
- Manual LTL para Spin
- Introdução a LTL no Spin
- Métodos formais e Lógica Temporal
- Resumo sobre Spin e LTL
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:
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:
- Processos A e B nunca estão na seção crítica ao mesmo tempo:
- 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=7
active proctype teste() {
do
:: n > 0 -> n--
:: n < 5 -> n++
od
}
Nesse modelo a variável global n inicia com o valor 7, e nos estados subsequentes ela pode variar aleatoriamente entre 0 e 5. Considerem-se as seguintes fórmulas LTL:
- (n == 7): esta fórmula avalia para verdadeiro, pois no primeiro estado do modelo de fato n tem o valor 7.
- (n == 5): esta fórmula avalia para falso, pois no primeiro estado do modelo n tem na verdade o valor 7.
Operadores temporais
Os operadores temporais são usados para criar proposições que envolvem mais de um estado. São eles (essas figuras foram copiadas deste resumo sobre Spin e Promela).
Assim como fórmulas simples, fórmulas LTL com esses operadores são aplicadas ao estado inicial.
Considerando o exemplo da seção anterior, podem-se declarar estas fórmulas:
- [] (n == 7): avalia para falso, uma vez que a variável n inicia com valor 7, mas nos estados subsequentes ela assume outros valores.
- [] (n >= 0): avalia para verdadeiro, uma vez que a variável n assume valores entre 0 e 7.
- <>(n == 7): avalia para verdadeiro, pois a variável n inicia com valor 7
- <>(n == 5): avalia para verdadeiro, pois realmente em algum estado futuro n terá o valor 5.
- <>(n == 0): avalia para falso, pois existe ao menos uma sequência de estados em que n nunca assume o valor 0 (imagine que n oscile entre 4 e 5 indefinidamente, por exemplo).
Esses operadores temporais podem ser combinados de forma a gerar fórmulas mais ricas:
Indefinidamente frequente: a proposição A é verdadeira ao menos uma vez em um ciclo
Travamento: em algum estado futuro a proposição A é verdadeira, e desse estado em diante ela é sempre verdadeira
Novamente considerando o exemplo da seção anterior, podem-se avaliar estas fórmulas:
- [] <> (n != 5): avalia para verdadeiro, pois a variável n assume valores diferentes de 5 de forma indefinitamente frequente (compare isso com o resultado da fórmula <> (n == 5) vista anteriormente).
- [] <> (n == 5): avalia para falso, pois a variável n, apesar de assumir o valor 5 garantidamente ao menos uma vez, pode passar a assumir valores menores que 5 de forma indefinitamente frequente.
- <> [] (n < 5): avalia para falso, pois a variável n pode eventualmente assumir o valor 5.
- <> [] (n < 6): avalia para verdadeiro, pois uma vez que a variável n assume o valor 5, nunca mais se torna maior que 5.
Precedência entre propriedades
Os operadores temporais não podem relacionar propriedades em diferentes estados (ou instantes). Por exemplo, pode-se desejar expressar que uma propriedade A é válida até que outra propriedade B se torne válida. Casos como esse são resolvidos com o operador U (until).
Dever-se notar que a propriedade A deve ser válida pelo menos até que B se torne verdadeira. Assim, A pode continuar válida mesmo após B se tornar verdadeira, e ainda assim a relação until ser verdadeira. Outro detalhe é que basta que B seja verdadeira uma vez para que a relação seja verdadeira.
Exemplificando esse operador com o modelo das seções anteriores, podem-se escrever estas fórmulas:
- (n > 5) U (n <= 5): avalia para verdadeiro, pois no início a variável decrementa de 7 até 5, e depois seus valores ficam entre 0 e 5.
- (n > 5) U (n == 5): avalia para verdadeiro, pois no início a variável decrementa de 7 até 5 (apesar de, uma vez atingido o valor 5, a variável n poder nunca mais assumir novamente esse valor).
- (n == 6) U (n <= 5): avalia para falso, pois a variável n inicia com 7 (lembre que fórmulas são avaliadas a partir do primeiro estado do modelo)
- <> ((n == 6) U (n <= 5)): avalia para verdadeiro, pois em algum estado futuro a variável n terá o valor 6 até que seus valores fiquem então entre 0 e 5.
Implicação entre propriedades
A relação de implicação entre propriedades expressada por A -> B significa que, se uma propriedade A for verdadeira, B também deve ser verdadeira. Por outro lado, se A for falsa, o valor de B é indiferente. Desta forma, essa relação é falsa somente se A for verdadeira e B for falsa. Para fins de recordação, a tabela verdade dessa relação e mostrada a seguir.
A | B | A -> B |
---|---|---|
falso | falso | verdadeiro |
falso | verdadeiro | verdadeiro |
verdadeiro | falso | falso |
verdadeiro | verdadeiro | verdadeiro |
A relação de implicação pode ser útil para as fórmulas LTL, pois possibilita expressá-las como se algo é verdade, outra coisa deve ser verdade, ou se algo é verdade, outra coisa deve ser verdade no futuro, entre outras possibilidades. Para exemplificar a relação de implicação, o seguinte modelo é usado:
int n = 5
bool down = true
active proctype ex2() {
do
:: down ->
n--
down = (n > 0)
:: else ->
n++
down = (n == 5)
od
}
Com base nesse modelo, algumas verificações podem ser feitas:
- [] (down -> <> !down): essa fórmula avalia para verdadeiro e expressa que, sempre que o valor da variável down for verdadeiro, em algum momento futuro será falso.
- [] (down -> <> !down) && [] (!down -> <> down): essa fórmula avalia para verdadeiro e expressa que o valor da variável down alterna entre verdadeiro e falso.
- []<>(!down -> <> (n==5)): avalia para verdadeiro, e expressa que sempre que down é falso, no futuro a variável n terá valor 5 (e isso acontece de forma indefinidamente frequente).
- []<>(down -> <> (n==0)): avalia para verdadeiro, e expressa que sempre que down é verdadeiro, no futuro a variável n terá valor 0 (e isso acontece de forma indefinidamente frequente).
Deve-se ter um cuidado com o uso da relação de implicação, pois ela avalia para verdadeiro se a propriedade da esquerda for falsa. Quer dizer, se a propriedade da esquerda nunca for verdadeira, então não importa se a propriedade implicada é verdadeira ou falsa. Assim, recomenda-se conferir se a propriedade da esquerda avalia para verdadeiro em ao menos uma possível execução do modelo. Isso pode ser feito com uma fórmula LTL como esta (supondo A -> B):
<> A
Isso significa: A é verdadeira no futuro, OU: não é sempre verdade que A seja falsa.
14/12: Conclusão
A conclusão da etapa 3 é a verificação do protocolo do projeto 1. Isso tem por objetivos verificar se:
- ARQ/MAC está livre de deadlock e livelock
- Um quadro transmitido é entregue em algum momento futuro
- Quadros duplicados são descartados
Somente um nodo transmite a cada vez- 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:
- 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.
- Um relatório que mostre como cada propriedade foi verificada, e o resultado da verificação.