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

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.

Ptc-Event-loop.jpg
Um loop de eventos (ou dispatcher, poller): eventos são detectados e encaminhados para tratamento para callbacks previamente definidos


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
    // handle_timeout: trata o timeout associado a este callback
    virtual void handle() = 0;
    virtual void handle_timeout() = 0;

    // operator==: compara dois objetos callback
    // necessário para poder diferenciar callbacks ...
    virtual bool operator==(const Callback & o) const;
    
    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
};


Um exemplo para uso do Poller é este:

#include <iostream>
#include <string>
#include "poller.h"

using namespace std;

class CallbackStdin: public Callback {
 public:
  CallbackStdin(long tout): Callback(0, tout) {}

  void handle() {
    string w;

    getline(cin, w);
    cout << "Lido: " << w << endl;
  }

  void handle_timeout() {
     cout << "Timeout !!!" << endl;
  }

};

int main() {
  CallbackStdin cb(5000);
  Poller sched;

  sched.adiciona(&cb);
  sched.despache();
}

Algo parecido em Python

A biblioteca padrão Python possui a classe DefaultSelector, que se assemelha ao Poller. Porém DefaultSelector não possibilita que se associe um diferente timeout para cada callback. Esse tipo de controle deve ser usado pelo código que usa DefaultSelector.

Este exemplo mostra o uso de DefaultSelector:

#!/usr/bin/python3

import selectors
import sys

Timeout = 5 # 5 segundos

# um callback para ler do fileobj
def handle(fileobj):
  s = fileobj.readline()
  print('Lido:', s)

sched = selectors.DefaultSelector()
sched.register(sys.stdin, selectors.EVENT_READ, handle)

while True:
  eventos = sched.select(Timeout)
  if not eventos: # timeout !
    print('Timeout !')
  else:
    for key,mask in eventos:
      cb = key.data # este é o callback !
      cb(key.fileobj)


Devido a essa limitação em DefaultSelector, a classe Poller foi implementada também em Python.

O Poller reimplementado em Python

As classes Poller e Callback foram implementadas em Python, porém explorando algumas facilidades existentes nessa linguagem. Ambas estão disponíveis no módulo poller:


A documentação a seguir foi obtida com o utilitário pydoc.

Help on module poller:

NAME
    poller

CLASSES
    builtins.object
        Callback
        Poller
    
    class Callback(builtins.object)
     |  Classe Callback:
     |  
     |  Define uma classe base para os callbacks
     |  a serem usados pelo Poller. Cada objeto Callback
     |  contém um fileobj e um valor para timeout.
     |  Se fileobj for None, então o callback define 
     |  somente um timer.
     |  Esta classe DEVE ser especializada para que
     |  possa executar as ações desejadas para o tratamento
     |  do evento detectado pelo Poller.
     |  
     |  Methods defined here:
     |  
     |  __init__(self, fileobj=None, timeout=0)
     |      Cria um objeto Callback. 
     |      fileobj: objeto tipo arquivo, podendo ser inclusive 
     |      um descritor de arquivo numérico.
     |      timeout: valor de timeout em segundos, podendo ter parte 
     |      decimal para expressar fração de segundo
     |  
     |  handle(self)
     |      Trata o evento associado a este callback. Tipicamente 
     |      deve-se ler o fileobj e processar os dados lidos. Classes
     |      derivadas devem sobrescrever este método.
     |  
     |  handle_timeout(self)
     |      Trata um timeout associado a este callback. Classes
     |      derivadas devem sobrescrever este método.
     |  
     |  reload_timeout(self)
     |      Recarrega o valor de timeout
     |  
     |  update(self, dt)
     |      Atualiza o tempo restante de timeout
     |  
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |  
     |  __dict__
     |      dictionary for instance variables (if defined)
     |  
     |  __weakref__
     |      list of weak references to the object (if defined)
     |  
     |  isTimer
     |      true se este callback for um timer
    
    class Poller(builtins.object)
     |  Classe Poller: um agendador de eventos que monitora objetos
     |  do tipo arquivo e executa callbacks quando tiverem dados para 
     |  serem lidos. Callbacks devem ser registrados para que 
     |  seus fileobj sejam monitorados. Callbacks que não possuem
     |  fileobj são tratados como timers
     |  
     |  Methods defined here:
     |  
     |  __init__(self)
     |      Initialize self.  See help(type(self)) for accurate signature.
     |  
     |  adiciona(self, cb)
     |      Registra um callback
     |  
     |  despache(self)
     |      Espera por eventos indefinidamente, tratando-os com seus
     |      callbacks
     |  
     |  despache_simples(self)
     |      Espera por um único evento, tratando-o com seu callback
     |  
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |  
     |  __dict__
     |      dictionary for instance variables (if defined)
     |  
     |  __weakref__
     |      list of weak references to the object (if defined)


Um programa de demonstração parecido com aquele em C++ foi criado para mostrar o uso da classe Poller:

#!/usr/bin/env python3

import poller
import sys,time

class CallbackStdin(poller.Callback):
    
    def handle(self):
        l = sys.stdin.readline()
        print('Lido:', l)
        
    def handle_timeout(self):
        print('Timeout !')
   
class CallbackTimer(poller.Callback):

    t0 = time.time()
    
    def __init__(self, tout):
        poller.Callback.__init__(self, None, tout)
        
    def handle_timeout(self):
        print('Timer: t=', time.time()-CallbackTimer.t0)
        
cb = CallbackStdin(sys.stdin, 3)
timer = CallbackTimer(2)
sched = poller.Poller()

sched.adiciona(cb)
sched.adiciona(timer)

sched.despache()


Existe outra API Python chamada asyncio, que é capaz de escalonar eventos e tarefas. Porém ela apresenta um modelo de execução bastante distinto.

Atividade

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