LEZIONE del 25 Febbraio

La relazione client/server è senz'altro uno degli schemi di interazione fra processi più noti. Tuttavia, il problema della comunicazione fra processi assume connotazioni più generali e che si presentano in modo assai diverso a seconda che tali processi siano in esecuzione sullo stesso elaboratore o su elaboratori diversi.

Nel primo caso la comunicazione è resa possibile dalla condivisione di un'area di memoria (buffer) con un adeguato meccanismo di sincronizzazione (semafori), analogamente a quanto già visto nel caso dei thread.

Nel secondo caso l'interazione è resa possibile attraverso il meccanismo più generale dello scambio messaggio il quale richiede un adeguato ambiente di comunicazione basato su canali entro cui far scorrere le informazioni.

Socket

Lo schema di interazione dello scambio messaggi, pur non richiedendo la presenza di memoria condivisa fra processi, necessita di un ambiente di comunicazione nel quale le primitive di comunicazione send e receive condividano la stessa struttura del messaggio, per mezzo della sua definizione, e l'identificazione univoca del canale, attraverso un appropriato meccanismo di attivazione. Nella figura seguente

Figura 1

viene evidenziata la relazione fra la struttura astratta della comunicazione, realizzata in termini di scambio messaggi, e i necessari protocolli attraverso i quali tale comunicazione può effettivamente ottenersi. In particolare, viene fatto riferimento sia allo schema generale ISO/OSI sia alla specifica implementazione della rete internet.

Qualunque siano i livelli di rete e di trasporto, effettivamente utilizzati nella realizzazione della rete di elaboratori, il metodo generale con cui i processi possono comunicare fra loro è quello noto col termine IPC, o Inter Process Communication, attraverso il quale qualunque processo voglia comunicare con un altro deve dotarsi di un punto terminale di comunicazione, genericamente noto col termine socket.

Nel seguito viene presentato in generale tale schema senza un riferimento preciso ad un qualche sistema operativo ma, semplicemente, fornendo per le socket una definizione in termini di struttura astratta di dati. Successivamente si passa ad un esempio specifico di server che utilizza le primitive di manipolazione delle socket in ambiente Unix.

Implementazione dello Scambio Messaggi mediante Socket

L'implementazione dello scambio messaggi secondo il paradigma delle socket si basa su un metodo generale attraverso il quale qualunque processo che vuole comunicare con un altro si dota di punti terminali di comunicazione, al quale resta associato univocamente un nome.

Le socket sono definite da un tipo, il cui scopo è quello di rappresentare il dominio di comunicazione, da intendersi come un'astrazione che viene introdotta per raggruppare le proprietà comuni a tutti i processi che comunicano con quel tipo specificato di socket. Una di queste proprietà è lo schema utilizzato per denominare le socket.

Le socket sono tipizzate secondo le proprietà di comunicazione visibili all'utente. Si presume che i processi comunichino solamente fra socket dello stesso tipo benchè non vi sia alcuna preclusione alla comunicazione fra socket di tipo diverso purchè il sottostante protocollo di comunicazione lo permetta. Normalmente sono disponibili tre tipi di socket che prendono il nome di

A parte la bidirezionalità del flusso di dati, una coppia di socket stream interconnessa fornisce un'interfaccia molto simile a quella delle pipe. Caratteristica importante delle socket datagram è la non alterazione della delimitazione dei record costituenti il messaggio. Questa socket modella il comportamento di molte reti a commutazione di pacchetto come Ethernet.

Definito un dominio, vi si possono introdurre un certo numero di socket ed accedervi mediante le seguenti operazioni primitive:

A questo punto si possono realizzare le operazioni standard di scambio messaggi. In particolare, per quanto riguarda l'operazione send, tenendo presente che il cliente necessita della connessione solamente per il tempo necessario a spedire il messaggio, si ha
  void Send( Mailbox *Mbx, Message Msg )
  {
   Socket S;

   MakeSocket( DOMAIN, S );
   Connect( &S, Mbx );
   Put( &S, Msg );
   Close( &S );
  }
dove il nome della socket Mbx è, in parte, determinato dalla scelta di DOMAIN, il dominio prescelto all'interno del quale viene stabilita la comunicazione. Come si intuisce facilmente, ogni qualvolta il cliente necessita di inviare un messaggio alla mailbox Mbx, viene creata temporaneamente una socket di trasmissione entro la quale viene introdotto il messaggio; successivamente la socket viene distrutta.

Per quanto riguarda, invece, l'operazione receive, questa può essere implementata nel modo seguente

  void Receive( Socket *S, Message *Msg )
  {
   Socket P;

    Listen( &S, N );		 /* S = socket di comunicazione */
    Accept( &S, &P ); 		 /* P = socket di trasmissione  */
    if  (fork() == 0) {
       Close( &S );
       Get( &P, &Msg );
       Close( &P );
      }
  }

nella quale si fa riferimento alla Mailbox con la corrispondente socket piuttosto che col suo nome globale. Questo è possibile purchè si supponga che esiste un unico server che ascolta sulla socket, cioè, i nomi globali devono considerarsi identificativi di porte. Si deve aggiungere, pertanto, la procedura StartReceive che appare nel seguito

  procedure StartReceive( Mailbox Mbx, Socket *S )
  {
   MakeSocket( DOMAIN, &S );
   Bind( &S, Mbx );		  /* S = socket di trasmissione */
  }

il cui compito è quello di attivare il punto di connessione sul quale il server esegue le accettazioni di messaggi, essendo N il loro numero massimo in attesa di ricezione.

Esempio di Server

Il programma C che segue è un esempio di server iterativo costituito da una routine principale main.c che rappresenta l'ossatura del server e la subroutine guess_action, da realizzarsi a parte nel file action.c, che ne implementa la funzionalità. Per compilarlo usare il comando

gcc -o server -lnsl server.c action.c

Per l'utilizzo del server procedere nel seguente modo.

IL PROGRAMMA PRINCIPALE

Segue la struttura del server che utilizza le primitive per la gestione delle socket. La signal serve per evitare che i figli terminino senza che il padre faccia una wait(0) per 'consumare' il return code.

/*
* server.c
*      main program implementing the skeleton of the server
*
*/
#include <sys/types.h>
#include <stdio.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include <syslog.h>
#include <errno.h>


void guess_action(int in, int out);

main (int argc, char *argv[])

{
     int      port, sock, fd, addrlen;
     int      value;
     struct   sockaddr_in server;
     struct   sockaddr_in client;

        /* preleva gli argomenti passati al main */

     if (argc != 2) {
       fprintf(stderr, "Sintassi: %s ", argv[0]);
       exit(4);
     }

     port = atoi(argv[1]);

       /* per emettere immediatamente l'output in stdout */

     setbuf(stdout,NULL);


        /* get an internet domain socket */

     sock = socket(AF_INET, SOCK_STREAM, 0);

        /* connessione IP, con trasporto TCP : rete internet */

     if (sock < 0) {
       perror("creating stream socket");
       exit(1);
     }


        /* complete the socket structure */

     server.sin_family = AF_INET;                  /* per internet         */
     server.sin_addr.s_addr = htonl(INADDR_ANY);   /* indirizzo nullo:     */
                                                   /*   sceglie il sistema */
     server.sin_port = htons(port);

        /*  riutilizza piu' volte lo stesso indirizzo */
        /*  nel caso che vengano lanciati piu' server successivamente */

     value = 1;
     if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&value,
            sizeof(value)) < 0) {
       perror("during setsockopt");
       exit(5);
     }


        /* bind the socket to the port number */

     if (bind(sock, (struct sockaddr *) &server, sizeof(server)) < 0) {
       perror("binding socket");
       exit(2);
     }

        /* il server attende richieste */
        /* il valore 2 indica la lunghezza massima della coda di attesa */

     listen(sock,2);

     signal(SIGCHLD,SIG_IGN);

        /* il server e' implementato in modo concorrente, */
        /* cioe' viene clonato ad ogni nuova richiesta    */

     while (1) {

        /* attendi che il cliente passi la richiesta */

       addrlen = sizeof(client);
       if ((fd = accept(sock, (struct sockaddr *) &client, &addrlen)) < 0) {
         perror("accepting connection");
         exit(3);
       }

       if (fork() == 0) {

            /* figlio */

         guess_action(fd, fd);

         exit(0);

            /* alla fine il figlio termina (e chiude la connessione)*/
        } else

      close(fd);

            /* il padre chiude la connessione */
     }
}
Come si può notare facilmente da quest'ultimo esempio, la funzionalità del server è garantita da un configurazione adeguata della rete.

Configurazione di Rete

Una delle caratteristiche più interessanti dei moderni elaboratori elettronici è la loro duttilità ad essere utilizzati come nodi di una rete di calcolatori che, come è noto, si assume essere la rete internet cui corrisponde la suite di protocollo TCP/IP. A tale scopo tutti i sistemi operativi devono essere dotati di opportuni moduli in grado di fornire le funzionalità di base (istradamento dei pacchetti e loro trasporto) che, nel caso di UNIX, si possono riassumere in quanto segue

Specificatamente, si farà riferimento al dialetto Linux in una delle sue distribuzioni. L'implementazione delle suddette funzionalità si realizza a diversi livelli, il primo dei quali richiede l'estensione del kernel a caricare il modulo specifico che implementa il driver dell'interfaccia. Si noti che, a questo proposito, le interfacce possono essere

Nel secondo caso si tratta, più propriamente, del driver del modem che connette il calcolatore alla rete telefonica e permette di utilizzare segnali analogici opportunamente pilotati. Il kernel di Linux, comunque, vede entrambi come moduli capaci di colloquiare opportunamente con l'hardware di comunicazione connesso al calcolatore.

In generale, il riconoscimento dell'hardware e il caricamento dell'opportuno modulo è realizzato durante la fase di boot del sistema da uno speciale software che prende il nome di kudzu. L'aggiornamento della configurazione hardware è fatto anch'esso automaticamente.

GESTIONE DEI MODULI DEL KERNEL

Mentre per il riconoscimento dell'hardware ci si basa sulla lettura di un'opportuna memoria ROM in esso contenuta, l'attivazione del driver si realizza selezionando un opportuno modulo dalla lista dei moduli disponibili e caricandolo nel kernel. I comandi di gestione dei moduli sono riportati di seguito

  1. insmod(8):
    installa un modulo caricabile del kernel
  2. rmmod(8):
    rimuovi un modulo caricabile del kernel
  3. modprobe(8):
    gestione di alto livello dei moduli caricabili
  4. depmod(8):
    gestisci le caratteristiche di dipendenza dei moduli caricabili del kernel
  5. lsmod(8):
    lista i moduli caricabili
  6. ksyms(8):
    mostra i simboli esportati dal kernel

A questo punto è possibile attivare l'interfaccia che è in grado di ricevere i comandi del kernel proprio grazie al driver. Di solito i moduli caricati dal kernel sono contenuti in un file che si chiama modules.conf, contenuto nella directory /etc.

Di seguito è riportato un segmento di codice shell-script che realizza l'attivazione della rete rispetto all'interfaccia ethernet

# nel file modules.conf e' contenuta una riga del tipo
# alias eth0 module_name
#
ETHER=(`grep -e eth0 /etc/modules.conf`)
ETHDRV=${ETHER[2]}
/sbin/insmod $ETHDRV
/sbin/ifconfig eth0 up
service network start

Si noti la presenza del comando service, il cui effetto è quello di attivare uno dei servizi tra quelli disponibili nel sistema. Il comando che gestisce tale opportunità ha la struttura seguente

service option ServiceName command

dove command può essere uno dei seguenti: start, stop, restart o status. Per quanto riguarda, invece, ServiceName è riportata del seguito una lista parziale: nfs, apmd, network, nfslock, atd, crond, sendmail, anacron, atfs, ipchains, iptables, isdn, kudzu, lpd, netfs, portmap, syslog, telnet, pcmcia, time, wine, xinetd.

FILE DI CONFIGURAZIONE

Il comando di attivazione di interfaccia ifconfig ha il compito di assegnare l'indirizzo IP all'interfaccia stessa per cui la linea completa di comando dovrebbe essere la seguente

/sbin/ifconfig eth0 address 158.110.144.71 broadcast 255.255.255.0 up

nell'ipotesi di una connessione alla sottorete 144 della rete in classe B 158.110.0.0. Tuttavia è preferibile disporre di un file di configurazione per ciascuna interfaccia ifcfg-InterfaceName, tipicamente contenuto in /etc/sysconfig/network-scripts, che nel caso in esame assume la forma

#file /etc/sysconfig/network-scripts/ifcfg-eth0
#
DEVICE="eth0"
BOOTPROTO="none"
IPADDR="158.110.144.39"
NETMASK="255.255.255.0"
ONBOOT="no"
IPXNETNUM_802_2=""
IPXPRIMARY_802_2="no"
IPXACTIVE_802_2="no"
IPXNETNUM_802_3=""
IPXPRIMARY_802_3="no"
IPXACTIVE_802_3="no"
IPXNETNUM_ETHERII=""
IPXPRIMARY_ETHERII="no"
IPXACTIVE_ETHERII="no"
IPXNETNUM_SNAP=""
IPXPRIMARY_SNAP="no"
IPXACTIVE_SNAP="no"

Ad esso si deve aggiungere un altro file di configurazione molto importante, perchè contiene informazioni fondamentali sul gateway della rete LAN su cui è connessa l'interfaccia. Si tratta del file network, contenuto in /etc/sysconfig

# file /etc/sysconfig/network
#
NETWORKING=yes
HOSTNAME="karl.dimi.uniud.it"
GATEWAY="158.110.144.250"
GATEWAYDEV="eth0"
FORWARD_IPV4="yes"

Per il corretto funzionamento di tutte le funzioni di rete, il S.O. deve essere in grado di riconoscere le reti e gli host localmente connessi. I file di configurazione sono networks e hosts, entrambi contenuti nella directory /etc

# file /etc/networks
#
default		0.0.0.0
loopnet		127.0.0.0
dimi.uniud.it	158.110.144.0


# file /etc/hosts
#
# Do not remove the following line, or various programs
# that require network functionality will fail.
127.0.0.1	localhost
158.110.144.71	karl.dimi.uniud.it	karl
158.110.144.12	ludvig.dimi.uniud.it	ludvig
158.110.144.199	heinz.dimi.uniud.it	heinz
158.110.144.132	ten.dimi.uniud.it	ten

Naturalmente è necessario specificare anche il modo con cui risolvere in indirizzi IP tutti i nomi degli elaboratori che non sono contenuti in hosts. Il file che contiene queste informazioni è resolv.conf, anch'esso contenuto in /etc

# file /etc/resolv.conf
#
search dimi.uniud.it
nameserver 158.110.144.132
nameserver 158.110.1.7
nameserver 130.186.1.53



# file /etc/sysctl.conf
#
# Kernel sysctl configuration file for Red Hat Linux
#
# For binary values, 0 is disabled, 1 is enabled.  See sysctl(8) for
# more details.

# Controls IP packet forwarding
net.ipv4.ip_forward=0

# Controls source route verification
net.ipv4.conf.default.rp_filter = 1

# Controls the System Request debugging functionality of the kernel
kernel.sysrq = 0

# Controls whether core dumps will append the PID to the core filename.
# Useful for debugging multi-threaded applications.
kernel.core_uses_pid = 1

Il file sysctl.conf serve a controllare la gestione dei pacchetti IP sulla rete e può dipendere da specifiche scelte implementative. Infine, nel caso si volesse costruire una rete locale in cui condividere una o più partizioni di disco che un server mette a disposizione col protocollo NFS, e quindi montabili analogamente a partizioni locali, è necessario predisporre le opportune informazioni nel file exports

# file /etc/exports
#
/home ludvig(rw,insecure)
/opt  heinz(rw,insecure,no_squash_root)

nel quale è necessario specificare tutte le opzioni che si ritengono utili per la sicurezza dei dati da condividere. Si faccia attenzione alla configurazione dei firewall.

ESEMPIO DI CONFIGURAZIONE MULTIPLA

L'esempio che segue serve a chiarire come i diversi file citati in precedenza sono necessari per definire una corretta configurazione della rete che, in questo caso, si suppone essere diversa nelle diverse condizioni di lavoro di un certo PC. Lo script riportato di seguito considera 4 casi, due dei quali effettivamente descritti, corrispondenti a 4 diverse condizioni di lavoro: standalone, dialup, Office e Home LAN.

#!/bin/bash
# netconf -service1 -service2 ... configname
#
  ETC="exports hosts networks resolv.conf sysctl.conf"
  SYSCONFIG="network static-routes"
  NETWORK="ifcfg-eth0"

  ROOT="some root path"
  HOME="/$ROOT/etc.home/"
  OFFICE="/$ROOT/etc.office/"

  function StandAlone() {
  ...........
  ...........
  
  ...........
  }

  function DialUp() {
  ...........
  ...........
  
  ...........
  }

  function HomeLan() {
  echo "Starting up Home Network..."
#
  for ITEM in $ETC
  do
    /bin/cp -f $HOME/$ITEM /etc/$ITEM
  done
  for ITEM in $SYSCONFIG
  do
    /bin/cp -f $HOME/sysconfig/$ITEM /etc/sysconfig/$ITEM
  done
  for ITEM in $NETWORK
  do
    /bin/cp -f $HOME/sysconfig/network-scripts/$ITEM
               /etc/sysconfig/network-scripts/$ITEM
  done
#
  /sbin/service pcmcia restart ;
  /bin/hostname karl.home.it
  /sbin/ifconfig eth0 up ;
  /sbin/ipchains -F ;
  /sbin/service network restart ;
#
  for SERVICE in $SERVICES
  do
    echo "**** starting $SERVICE service..."
    /sbin/service $SERVICE start ;
    echo "... done ******"
  done
#
  if [ 'echo `service nfs status` | grep -e running' ] > /dev/null ;
    then
      echo -n "reading exports table..." ;
      /usr/sbin/exportfs -r
      echo "done"
  fi
  echo "...done"
  }

  function OfficeLan() {
  echo "Starting up Office Network..."
#
  for ITEM in $ETC
  do
    /bin/cp -f $OFFICE/$ITEM /etc/$ITEM
  done
  for ITEM in $SYSCONFIG
  do
    /bin/cp -f $OFFICE/sysconfig/$ITEM /etc/sysconfig/$ITEM
  done
  for ITEM in $NETWORK
  do
    /bin/cp -f $OFFICE/sysconfig/network-scripts/$ITEM
               /etc/sysconfig/network-scripts/$ITEM
  done
#
  /sbin/service pcmcia restart
  /bin/hostname karl.dimi.uniud.it
  /sbin/ifconfig eth0 up
  /sbin/ipchains -F
  /sbin/service network restart
#
  for SERVICE in $SERVICES
  do
    echo "**** starting $SERVICE service..."
    /sbin/service $SERVICE start ;
    echo "... done ******"
  done
#
  if [ 'echo `service nfs status` | grep -e running' ] > /dev/null ;
    then
      echo -n "reading exports table..."
      /usr/sbin/exportfs -r
      echo "done"
  fi
  echo "...done"
  }

  function Help() {
  SPACES="   "
  echo "Available Services:"
  echo "${SPACES}help: This Help Information"
  echo "${SPACES}home: Home Network"
  echo "${SPACES}office: Office Network"
  echo "${SPACES}dial: Dial Up Service to a PPP Provider"
  echo "${SPACES}alone: Stand Alone Workstation"
  }

if [ "$#" -eq 0 ]
  then
    echo "insufficient parameters"
    exit 2
fi
#
K=0
SERVICES=""
for ITEM in $*
do
  if [ `echo $ITEM | grep -e "-"` ] > /dev/null ;
    then SERVICES="`echo $ITEM | sed s/-//` $SERVICES"
    else
      K=`expr $K + 1`
      CONFIGNAME=$ITEM
  fi
done
#
if [ $K -ne 1 ] ;
  then
    echo "choose a configuration name (dial,office,home,alone)"
    exit 1
fi
#
case $CONFIGNAME in
alone) StandAlone ;;
dial) DialUp ;;
home) HomeLan ;;
office) OfficeLan ;;
help) Help ;;
*) echo "$CONFIGNAME is not yet implemented" ; exit 3 ;;
esac
#
exit 0

Si lascia per esercizio l'implementazione delle altre configurazioni indicate. Si tenga presente che nel caso di accesso ad internet tramite provider la quasi totalità dei parametri di connessione ed uso della rete sono forniti dinamicamente. La configurazione standalone non necessita di informazioni alcune per la rete.