Mudanças entre as edições de "PTC29008: Projeto 1: Integração com subsistema de rede do Linux"
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
A integração do protocolo de enlace com o subsistema de rede do Linux pode ser feita de duas formas:
- 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.
- 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.
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.