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
(Criou página com 'Próxima aula __toc__ A integração do protocolo de enlace com o subsistema de rede do Linux pode ser feita de duas f...')
 
Linha 4: Linha 4:
  
 
A integração do protocolo de enlace com o subsistema de rede do Linux pode ser feita de duas formas:
 
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 [http://tele.sj.ifsc.edu.br/~msobral/ptc/docs/Linux%20Kernel%20Development%203rd%20Edition%20-%20Love%20-%202010.pdf kernel Linux e desenvolvimento de device drivers]. A depuração é difícil. O protocolo fica bem integrado ao sistema operacional.
+
# '''Criação de um device driver no kernel Linux:''' uma tarefa árdua, e que envolve um bom conhecimento sobre o [https://lwn.net/Kernel/LDD3/ kernel Linux e desenvolvimento de device drivers]. 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.
 
# '''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.
  

Edição das 10h05min de 11 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. 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

Multiplexação do acesso a descritores de arquivos

Pelo diagrama sobre a integração ao subsistema de rede do Linux, o driver do protocolo de enlace deve ler dados tanto do descritor de arquivo da interface tun quanto da interface serial do transceiver RF. Porém, ao tentar ler algo de um descritor de arquivos (ex: com chamada de sistema read) o processo é bloqueado. Enquanto estiver bloqueado não se pode ler dados que porventura estejam disponíveis no outro descritor de arquivos. Isso se deve ao fato de que leituras com read serem bloqueantes. Para poder aguardar por dados em ambos descritores de arquivo simultaneamente, o processo do protocolo deve multiplexar o acesso a esses descritores.

Algumas técnicas são bem conhecidas para aguardar dados através de múltiplos descritores de arquivos:

  1. Espera ocupada: os descritores de arquivo são configurados para serem não-bloqueantes, e assim pode-se implementar um laço que tente ler algo de cada descritor de arquivo em sequência. Se um descritor não tiver dados disponíveis, a leitura retorna imediatamente. Esse tipo de solução tem a grande desvantagem de desperdiçar tempo de processador.
  2. Uso de threads: cria-se uma thread para ler de cada descritor de arquivo. Como threads são concorrentes, uma thread pode ficar bloqueada esperando dados em um descritor de arquivo enquanto outra lê outro descritor e processa os dados recebidos. Uma solução como essa envolve um projeto cuidadoso para tratar questões de sincronismo das threads.
  3. Uso da chamada de sistema select: a chamada select possibilita esperar por dados em um conjunto de descritores de arquivos. A chamada em si é bloqueante, porém retorna imediatamente se ao menos um dos descritores de arquivos estiver disponível para acesso. A chamada informa quais descritores podem ser acessados sem risco de bloqueio. Essa solução é simples e não precisa dos cuidados com sincronismo como no caso de threads. O exemplo a seguir mostra o uso de select para esperar por dados vindos da entrada padrão ou a recepção de uma conexão em um socket TCP.
Exemplo de uso de select
#include <sys/select.h>

#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

#define PORT 5555

int cria_socket(int port) {
  int sd = socket(AF_INET, SOCK_STREAM, 6);
  struct sockaddr_in addr;
  socklen_t len;

  // Vincula um endereco local qualquer ao socket
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = htonl(INADDR_ANY);
  addr.sin_port = htons(port);

  len = sizeof(addr);
  int ok = 1;
 
  setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, (void*)&ok, sizeof(ok));
 
  if (bind(sd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
    perror("Ao vincular endereço ao socket");
    return -1;
  }

  listen(sd, 2);

  return sd;
}

int main() {
  printf("Esperando que se tecle algo + ENTER, ou se receba conexão no port %d ...\n\n", PORT);
  fflush(stdout);

  int fd = 0; // o descritor 0 corresponde à entrada padrão ...
  int sd = cria_socket(PORT); // cria um socket TCP no port PORT
  int max_fd = fd;
  if (sd > max_fd) max_fd = sd;

  struct timeval timeout; // para especificar o timeout
  timeout.tv_sec = 5; //timeout de 2 segundos
  timeout.tv_usec = 0;

  fd_set espera; // um conjunto de descritores
  FD_ZERO(&espera); // zera o conjunto de descritores
  FD_SET(fd, &espera); // adiciona "fd" ao conjunto de descritores
  FD_SET(sd, &espera); // adiciona "sd" ao conjunto de descritores

  // aqui se usa select para monitorar os descritores contidos em "espera"
  if (select(max_fd+1, &espera, NULL, NULL, &timeout) == 0) {
    // timeout !!
    puts("Timeout !");
  } else {
    puts("Algo chegou ...");

    // a seguir se verifica que descritores podem ser lidos sem risco de bloqueio
    // i.e.: que descritores estão prontos para serem acessados

    if (FD_ISSET(fd, &espera)) {
      char linha[128];
      int n;

      n = read(fd, linha, 128);
      printf("Leu %d caracteres da entrada padrão: %s\n", n, linha);
    }

    if (FD_ISSET(sd, &espera)) {
      char * msg = "***\r\nrecebi conexão mas desconectei em seguida ...\r\n\r\n";
      int n, con;
      struct sockaddr_in addr;
      socklen_t len = sizeof(addr);

      con = accept(sd, (struct sockaddr*)&addr, &len);
      printf("Recebeu conexão de (%s,%d)\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
      send(con, msg, strlen(msg), 0);
      shutdown(con,  SHUT_RDWR);
    }
  }
}


Excetuando a técnica 1, as demais podem ser usadas na implementação do protocolo.

Atividade

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