Mudanças entre as edições de "PTC29008: Projeto 1: Integração com subsistema de rede do Linux"

De MediaWiki do Campus São José
Ir para navegação Ir para pesquisar
Linha 170: Linha 170:
  
  
== Atividade ==
+
= Um modelo assíncrono para o protocolo =
 +
 
 +
A primeira versão do protocolo segue um modelo síncrono, em que a interface de acesso ao protocolo é composta por chamadas bloqueantes. Isso simplificou o projeto, porém tornou-o limitado. Por exemplo:
 +
* o protocolo somente recebe um quadro se a aplicação que o utiliza chamar a operação ''recebe''.
 +
* o gerenciamento de sessão não é capaz de realizar a manutenção do enlace
 +
* comunicações bidirecionais não funcionam bem
 +
 
 +
A segunda versão do protocolo busca sanar essas limitações, redesenhando-o segundo um modelo assíncrono. Nesse modelo, o protocolo fica no controle das ações, detectando eventos para tratá-los. Cada evento detectado é encaminhado para o bloco funcional a que se destina. Nenhuma ação do protocolo é bloqueante, pois eventos devem ser tratados o mais rápido possível. Um protocolo que funcione dessa forma deve portanto possuir algum mecanismo de detecção e encaminhamento de eventos.
 +
 
 +
Na versão de referência do protocolo feita em C++, a detecção e encaminhamento de eventos é realizada por um componente denominado ''Poller''. O ''Poller'' monitora um conjunto de descritores de arquivos para fins de leitura. Cada descritor é associado a algum bloco funcional do protocolo por meio de um [http://stackoverflow.com/questions/9596276/how-to-explain-callbacks-in-plain-english-how-are-they-different-from-calling-o Callback], o qual inclui também um tempo máximo de espera por dados nesse descritor. Quando um descritor tem dados para serem lidos, o ''Poller'' executa o ''Callback'' correspondente, que tem a responsabilidade de realizar a leitura do descritor e processar os valores lidos. Se um timeout ocorrer, o ''Poller'' excuta o ''Callback'' correspondente, para que possa tratar o timeout. Por fim, se um descritor for um número negativo, o ''Poller'' o considera um ''timer'' (uma vez que somente seu timeout deve ser levado em conta). Esse ''Poller'' está declarado como mostrado a seguir:
 +
 
 +
<syntaxhighlight lang=c>
 +
// Poller: um despachador de eventos
 +
// Um objeto poller é capaz de monitorar um conjunto de descritores de arquivos
 +
// e executar um callback para cada desccritor pronto para acesso
 +
// Cada descritor pode especificar um timeout próprio
 +
class Poller {
 +
public:
 +
  Poller();
 +
  ~Poller();
 +
 
 +
  // adiciona um evento a ser vigiado, representado por um Callback
 +
  void adiciona(Callback * cb);
 +
 
 +
  // remove callback associado ao descritor de arquivo fd
 +
  void remove(int fd);
 +
  void remove(Callback * cb);
 +
 
 +
  // remove todos callbacks
 +
  void limpa();
 +
 
 +
  // vigia os descritores cadastrados e despacha os eventos (chama os callbacks)
 +
  // para ser lido, ou até que expire o timeout (em milissegundos)
 +
  void despache_simples();
 +
  void despache();
 +
 
 +
protected:
 +
    list<Callback*> cbs_to;
 +
    map<int,Callback*> cbs;
 +
};
 +
</syntaxhighlight>
 +
 
 +
 
 +
Um ''Callback'', por sua vez, é definido pela classe abstrata ''Callback''. Cada tipo de evento deve ter seu ''Callback'' específico definido como uma especialização dessa classe.
 +
 
 +
<syntaxhighlight lang=c>
 +
// classe abstrata para os callbacks do poller
 +
class Callback {
 +
public:
 +
    // fd: descritor de arquivo a ser monitorado. Se < 0, este callback é um timer
 +
    // tout: timeout em milissegundos. Se < 0, este callback não tem timeout
 +
    Callback(int fd, long tout);
 +
   
 +
    // cria um callback para um timer (equivalente ao construtor anterior com fd=-1)
 +
    // out: timeout
 +
    Callback(long tout);
 +
   
 +
    // ao especializar esta classe, devem-se implementar estes dois métodos !
 +
    // handle: trata o evento representado neste callback
 +
    virtual void handle() = 0;
 +
    // operator==: compara dois objetos callback
 +
    virtual bool operator==(const Callback & o) const = 0;
 +
   
 +
    int filedesc() const;
 +
    int timeout() const;
 +
    void update(long dt); // ajusta timeout restante
 +
    void reload_timeout();
 +
protected:
 +
    int fd; // se < 0, este callback se torna um simples timer
 +
    long tout;
 +
    long base_tout;// milissegundos. Se <= 0, este callback não tem timeout
 +
};
 +
</syntaxhighlight>
 +
 
 +
* [http://tele.sj.ifsc.edu.br/~msobral/ptc/Poller.tgz Código-fonte do Poller (C++)]
 +
 
 +
= Atividade =
  
 
Integre seu protocolo com o kernel Linux. Antes de qualquer coisa, o enquadramento e a detecção de erros devem estar funcionando corretamente.
 
Integre seu protocolo com o kernel Linux. Antes de qualquer coisa, o enquadramento e a detecção de erros devem estar funcionando corretamente.

Edição das 20h36min de 26 de setembro de 2018

Próxima aula

A integração do protocolo de enlace com o subsistema de rede do Linux pode ser feita de duas formas:

  1. Criação de um device driver no kernel Linux: uma tarefa árdua, e que envolve um bom conhecimento sobre o kernel Linux e desenvolvimento de device drivers. As bibliotecas de programação usuais disponíveis em espaço de usuário não podem ser usadas. A depuração é difícil. O protocolo fica bem integrado ao sistema operacional.
  2. Implementação do protocolo em espaço de usuário: um protótipo em espaço de usuário é mais fácil de criar, pois pode usar as APIs existentes para a linguagem de programação escolhida. A depuração fica facilitada. Porém a integração com o sistema operacional apresenta um certo overhead devido ao fluxo de processamento transitar frequentemente entre espaços de sistema e de usuário.


Para o objetivo do projeto 1, a segunda opção é a mais adequada. A implementação em espaço de usuário deve usar uma interface de rede do tipo tun para fazer a integração com o subsistema de rede. Com isso, existe uma interface de rede associada ao enlace estabelecido pelo protocolo, a qual possui parâmetros de rede IP (endereço, máscara), e rotas podem ser definidas para destinos alcançáveis através dela. Com isso, qualquer aplicação TCP/IP pode se comunicar usando o protocolo desenvolvido.


A figura a seguir mostra um diagrama que esquematiza a integração do protótipo com o Linux, evidenciando a interface de rede do tipo tun.


PTC-Proto-tap.jpg


A criação e configuração de uma interface tun pode ser feita de forma programática, usando chamadas de sistema do Linux. O código-fonte a seguir demonstra como uma interface dessas pode ser criada e configurada.


função para criar uma interface tun
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <net/if.h>
#include <linux/if_tun.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> 


// tun_alloc: cria uma interface tun
// Parâmetro de entrada: 
//  char * dev: o nome da interface ser criada. Se for NULL, interface será denominada pelo sistema (ex: tun0)
//
// Retorno: valor inteiro
//  - se > 0: o descritor de arquivo da interface tun criada.
//  - se < 0, ocorreu um erro

#define MTU 256
#define MASK "255.255.255.252"

int tun_alloc(char *dev)
  {
      struct ifreq ifr;
      int fd, err;

      if( (fd = open("/dev/net/tun", O_RDWR)) < 0 ) {
         perror("");
         return  -1;
      }

      memset(&ifr, 0, sizeof(ifr));

      /* Flags: IFF_TUN   - TUN device (no Ethernet headers) 
       *        IFF_TAP   - TAP device  
       *
       *        IFF_NO_PI - Do not provide packet information  
       */ 
      //ifr.ifr_flags = IFF_TUN | IFF_NO_PI; 
      ifr.ifr_flags = IFF_TUN; 
      if( *dev )
         strncpy(ifr.ifr_name, dev, IFNAMSIZ);

      err = ioctl(fd, TUNSETIFF, (void *) &ifr);
      if( err < 0 ){
         close(fd);
	 perror("");
         return err;
      }
      return fd;
  }    

  // esta outra função configura a interface tun: define os endereços IP do enlace ponto-a-ponto,
  // a máscara de rede /30 e ativa a interface. Ela faz o mesmo que o comando:
  // ifconfig nome_tun IP_da_interface dstaddr IP_da_outra_ponta_do_enlace
  // 
  // esta função deve ser usada assim, supondo que a interface tun se chame tun0, e os endereços IP
  // do enlace sejam 10.0.0.1 e 10.0.0.2:
  //
  // if (set_ip("tun0", "10.0.0.1", "10.0.0.2") < 0) {
  //   perror("so configurar a interface tun");
  //   return 0;
  // }
  //
  // Maiores detalhes sobre as chamadas de sistemas utilizadas: ver "man netdevice", ou 
  // http://man7.org/linux/man-pages/man7/netdevice.7.html

int set_ip(char *dev, char * ip, char * dst) {
  struct ifreq ifr;
  struct sockaddr_in *addr;
  int ok;
  int sd;

  // cria um socket para configurar a interface
  // esse socket não tem nada de especial ... 
  sd = socket(AF_INET, SOCK_DGRAM, 0);
  if (sd < 0) return sd;

  // zera todos os bytes da struct ifreq
  // essa struct contém os atributos a serem configurados na interface
  bzero(&ifr, sizeof(ifr));

  // usa o ponteiro addr para referenciar o campo de endereço da struct ifreq
  // isso facilita o preenchimento dos atributos desse campo
  addr = (struct sockaddr_in*)&(ifr.ifr_ifru.ifru_addr);

  // preenche o campo endereço com o enderço IP da interface
  addr->sin_addr.s_addr = inet_addr(ip);
  addr->sin_family = AF_INET;
  addr->sin_port = 0;

  // escreve o nome da interface na struct ifreq. Isso é necessário
  // para o kernel saber que interface é alvo da operação
  strncpy(ifr.ifr_name, dev, IFNAMSIZ);

  // executa uma operação de configuração de endereço IP de interface
  ok = ioctl(sd, SIOCSIFADDR, &ifr);
  if (ok < 0) return ok;

  // preenche o campo endereço com o endereço IP da outra ponta do enlace
  addr->sin_addr.s_addr = inet_addr(dst);

  // executa uma operação de configuração de endereço IP de destino da interface  
  ok = ioctl(sd, SIOCSIFDSTADDR, &ifr);
  if (ok < 0) return ok;

  // preenche o campo endereço com a máscara de rede da interface
  addr->sin_addr.s_addr = inet_addr(MASK);


  // executa uma operação de configuração de máscara de rede da interface  
  ok = ioctl(sd, SIOCSIFNETMASK, &ifr);
  if (ok < 0) return ok;

  // executa uma operação de configuração de MTU da interface  
  ifr.ifr_mtu = MTU;
  ok = ioctl(sd, SIOCSIFMTU, &ifr);
  if (ok < 0) return ok;

  // lê as flags da interface
  ok = ioctl(sd, SIOCGIFFLAGS, &ifr);
  if (ok < 0) return ok;

  // acrescenta flags UP (ativa) e RUNNING (pronta para uso)
  ifr.ifr_flags |= IFF_UP | IFF_RUNNING;
  
  // executa uma operação de configuração flags da interface  
  return ioctl(sd, SIOCSIFFLAGS, &ifr);
}


Após criar a interface, pode-se testar a comunicação com um simples ping:

ping 10.0.0.2

Para o protocolo PTP, pode ser mais conveniente pensar na interface tun como um objeto. Sua implementação como uma classe pode ser feita como mostrado a seguir:


Um modelo assíncrono para o protocolo

A primeira versão do protocolo segue um modelo síncrono, em que a interface de acesso ao protocolo é composta por chamadas bloqueantes. Isso simplificou o projeto, porém tornou-o limitado. Por exemplo:

  • o protocolo somente recebe um quadro se a aplicação que o utiliza chamar a operação recebe.
  • o gerenciamento de sessão não é capaz de realizar a manutenção do enlace
  • comunicações bidirecionais não funcionam bem

A segunda versão do protocolo busca sanar essas limitações, redesenhando-o segundo um modelo assíncrono. Nesse modelo, o protocolo fica no controle das ações, detectando eventos para tratá-los. Cada evento detectado é encaminhado para o bloco funcional a que se destina. Nenhuma ação do protocolo é bloqueante, pois eventos devem ser tratados o mais rápido possível. Um protocolo que funcione dessa forma deve portanto possuir algum mecanismo de detecção e encaminhamento de eventos.

Na versão de referência do protocolo feita em C++, a detecção e encaminhamento de eventos é realizada por um componente denominado Poller. O Poller monitora um conjunto de descritores de arquivos para fins de leitura. Cada descritor é associado a algum bloco funcional do protocolo por meio de um Callback, o qual inclui também um tempo máximo de espera por dados nesse descritor. Quando um descritor tem dados para serem lidos, o Poller executa o Callback correspondente, que tem a responsabilidade de realizar a leitura do descritor e processar os valores lidos. Se um timeout ocorrer, o Poller excuta o Callback correspondente, para que possa tratar o timeout. Por fim, se um descritor for um número negativo, o Poller o considera um timer (uma vez que somente seu timeout deve ser levado em conta). Esse Poller está declarado como mostrado a seguir:

// Poller: um despachador de eventos
// Um objeto poller é capaz de monitorar um conjunto de descritores de arquivos
// e executar um callback para cada desccritor pronto para acesso
// Cada descritor pode especificar um timeout próprio
class Poller {
 public:
  Poller();
  ~Poller();

  // adiciona um evento a ser vigiado, representado por um Callback
  void adiciona(Callback * cb);
  
  // remove callback associado ao descritor de arquivo fd
  void remove(int fd);
  void remove(Callback * cb);
  
  // remove todos callbacks
  void limpa();
  
  // vigia os descritores cadastrados e despacha os eventos (chama os callbacks)
  // para ser lido, ou até que expire o timeout (em milissegundos)
  void despache_simples();
  void despache();
  
 protected:
     list<Callback*> cbs_to;
     map<int,Callback*> cbs;
};


Um Callback, por sua vez, é definido pela classe abstrata Callback. Cada tipo de evento deve ter seu Callback específico definido como uma especialização dessa classe.

// classe abstrata para os callbacks do poller
class Callback {
public:
    // fd: descritor de arquivo a ser monitorado. Se < 0, este callback é um timer
    // tout: timeout em milissegundos. Se < 0, este callback não tem timeout
    Callback(int fd, long tout);
    
    // cria um callback para um timer (equivalente ao construtor anterior com fd=-1)
    // out: timeout
    Callback(long tout);
    
    // ao especializar esta classe, devem-se implementar estes dois métodos !
    // handle: trata o evento representado neste callback
    virtual void handle() = 0;
    // operator==: compara dois objetos callback
    virtual bool operator==(const Callback & o) const = 0;
    
    int filedesc() const;
    int timeout() const;
    void update(long dt); // ajusta timeout restante
    void reload_timeout();
protected:
    int fd; // se < 0, este callback se torna um simples timer
    long tout; 
    long base_tout;// milissegundos. Se <= 0, este callback não tem timeout
};

Atividade

Integre seu protocolo com o kernel Linux. Antes de qualquer coisa, o enquadramento e a detecção de erros devem estar funcionando corretamente.