Alma Mater Studiorum - Universita' di Bologna
Sede di Cesena
Reti di Calcolatori
Esercitazione 1
Implementazione di un
superserver Unix di rete
Vedi:
• W.R. Stevens, Unix Network Programming, Prentice Hall
Copyright © 2006-2014 by D. Romagnoli & C. Salati
1
Servizi nel mondo di Unix
• Tipicamente uno host internet supporta svariati servizi di rete (e.g.
ftp, telnet, TFTP, ...).
• Ogni servizio di rete, concorrente o sequenziale, dovrebbe
• comportarsi secondo uno degli schemi visti a lezione.
• essere attivato dal sistema alla partenza e rimanere in
permanenza attivo in attesa di richieste di clienti;
richieste che potrebbero pero’ anche non arrivare mai!
(in gergo Unix un processo che si comporta in questo modo e’
chiamato daemon, tradotto impropriamente, come demone)
• In realta’ la struttura dei servizi standard nel mondo Unix (e.g. cat,
csh, …) non e’ quella degli schemi visti a lezione per i servizi di rete.
• Un servizio standard Unix ha la struttura di un filtro Unix.
• Quindi, quello che vorremmo davvero e’ che anche i servizi di rete
fossero strutturati come dei normali filtri Unix.
• Cosi’ potremmo anche esportare automaticamente in rete tutti i
servizi locali implementati come filtri Unix (e.g. il servizio cat).
2
Struttura canonica di un server CO concorrente
// trascuriamo la trattazione degli errori
int sockfd, newSockfd;
sockfd = socket(. . .);
bind(sockfd, . . .);
listen(sockfd, 5);
for (;;) {
newSockfd = accept(sockfd, . . .);
if (fork() == 0) { // processo figlio/clone
close(sockfd);
doYourJob(newSockfd);
close(newSockfd);
exit(0);
} else {
// processo padre
close (newSockfd);
}
3
}
Struttura canonica di un server CO sequenziale
// trascuriamo la trattazione degli errori
int sockfd, newSockfd;
sockfd = socket(. . .);
bind(sockfd, . . .);
listen(sockfd, 5);
for (;;) {
newSockfd = accept(sockfd, . . .);
doYourJob(newSockfd);
close(newSockfd);
}
• Il server sa che opera su risorse reali di tipo socket
• Il server sa su quali risorse opera (conosce il sockaddr del socket)
• Il server e’ attivo in permanenza in attesa di nuovi clienti
4
Struttura canonica di un server CO: in realta’ …
.1
• Il server e’ composto di 2 parti:
1. Una parte generica, indipendente dal particolare servizio che
viene offerto, di attesa di nuovi clienti
2. Una parte specifica, dipendente dal particolare servizio che
viene offerto, e che riguarda il servizio del (l’interazione con il)
singolo cliente
• Parte generica del server:
• Vede e gestisce esplicitamente l’accesso al Servizio di Trasposto
tramite system call specifiche dell’API socket
• Conosce il sockaddr del server socket
• E’ sempre viva, in attesa di nuovi clienti (nuove richieste di
servizio)
• E’ lei che determina se il servizio e’ sequenziale o concorrente
5
Struttura canonica di un server CO: in realta’ …
.2
• Parte specifica del server (sostanzialmente, funzione
doYourJob()):
• Vede l’accesso al Servizio di Trasporto soltanto in modo opaco,
tramite l’uso del file descriptor associato al socket di
comunicazione con il cliente, e utilizzando solo le system call
generiche read() e write()
• Il file descriptor tramite cui interagire con il cliente e’ l’unico
parametro di input della parte specifica
• Non conosce il sockaddr del server socket, e in realta’ non sa
nemmeno che la risorsa reale rappresentata dal file descriptor
tramite cui interagire con il cliente e’ un socket
• La durata della sua vita e’ collegata a quella del servizio offerto al
cliente cui e’ associata
 La parte generica potrebbe essere messa a fattor comune tra tutti i
server
6
Struttura di un filtro Unix
.1
• E’ creato dinamicamente quando il servizio da lui implementato e’ invocato dal
cliente.
 Non e’ attivo in permanenza in attesa di nuovi clienti.
 E’ chi attiva il servizio (e.g. una shell) che determina se il servizio stesso
sara’ eseguito in modo concorrente o sequenziale.
• Serve un singolo cliente, poi muore.
• Interagisce con il mondo esterno tramite
• standard input (in realta’ il file descriptor = 0) e
• standard output (in realta’ il file descriptor = 1) ,
• che accede tramite operazioni di read() e write().
• Al momento della sua attivazione i file descriptor 0 e 1 sono gia’ aperti e
collegati a risorse reali del sistema:
 e’ su queste risorse che il filtro opera,
 rispettivamente, in lettura (read()) e in scrittura (write()).
• E’ chi attiva il servizio che nel momento in cui lo fa (e.g. tramite command
line/shell) definisce il significato dei (le risorse reali associate ai) file descriptor
0 e 1.
7
Struttura di un filtro Unix
.2
• Il processo figlio / filtro Unix eredita dal processo padre i file descriptor
0 e 1 gia’ aperti sulle risorse di sistema che esso dovra’ utilizzare.
• Il filtro ignora quali siano le risorse reali di sistema che accede tramite
i file descriptor 0 e 1.
• Il filtro ignora anche quale sia il tipo delle risorse che accede tramite i
file descriptor 0 e 1:
• File vero e proprio
• Device driver
• Pipe
• ...
 E quindi, anche: socket!
• Il filtro sa che puo’ operare su queste risorse tramite operazioni di
read() e write().
• Se la risorsa di sistema e’ un TSAP, questo deve essere:
• Gia’ bind-ato
•
Gia’ connesso ad un pari remoto
8
Struttura di un filtro Unix: esempio d’uso
#cat
copia standard input in standard output
#cat > dstFile
copia standard input in dstFile
#cat srcFile
copia srcFile in standard output,
visualizza cioe’ su console il contenuto
di srcFile
#cat srcFile > dstFile copia srcFile in dstFile
#cat > dstFile
Questo testo sara’ inserito in dstFile.
^D
#
 # e’ il prompt della shell in esecuzione
9
Struttura canonica di un filtro Unix
//
//
//
//
//
l’interazione con il mondo esterno avviene
tramite i due file descriptor 0 e 1
che il server-filtro eredita gia’ aperti e
associati alle risorse di sistema opportune
dal processo padre
doYourJob(0, 1);
exit(0);
N.B.:
• I file descriptor 0 e 1 sono di norma associati a risorse reali
diverse ma niente impedisce che essi siano associati ad una
stessa risorsa (che in questo caso deve essere capace di
supportare contemporaneamente operazioni di read e di write!).
• In realta’ poi, un socket rappresenta davvero 2 risorse separate:
• Uno stream di byte in ingresso
• Uno stream di byte in uscita
10
Struttura di un server CO e filtri Unix
int sockfd, newSockfd;
sockfd = socket(. . .);
bind(sockfd, . . .);
listen(sockfd, 5);
for (;;) {
newSockfd = accept(sockfd, . . .);
if (fork() == 0) { // processo figlio/clone
close(sockfd);
// la parte specifica del server e’ come un
// filtro Unix, con 2 fd come parametri di
// ingresso, input fd e output fd
doYourJob(newSockfd /*in*/, newSockfd /*out*/);
close(newSockfd);
exit(0);
} else {
// processo padre
close (newSockfd);
}
11
}
Server di rete e filtri Unix
.1
• La parte generica dei server di rete puo’ essere messa a fattor comune:
•
Gestisce l’attesa di nuovi clienti per tutti i servizi di rete.
•
Determina se l’attivazione di ciascuno di questi servizi deve avvenire in
modo sequenziale o concorrente.
•
Attiva un server specifico quando vede arrivare dalla rete una richiesta
di nuovo servizio indirizzata a lui.
(quindi deve conoscere a priori il file name del programma eseguibile
che implementa il servizio!)
•
Si comporta rispetto ai clienti sulla rete come fa una shell rispetto
all’operatore seduto al terminale!
• I servizi specifici di rete sono implementati come filtri Unix cosi’ come i
servizi specifici locali.
 A questo punto, in realta’, non c’e’ nessuna differenza tra un servizio di
rete e un servizio locale.
 cat puo’ ad esempio essere utilizzato per realizzare un servizio di eco
TCP! Come?
12
Server di rete e filtri Unix
.2
• Quando si interagisce con i servizi tramite shell, si identifica il servizio che si
vuole attivare con il suo nome.
 Indicando il file name del programma eseguibile (filtro Unix) che
implementa il servizio e che si vuole che sia messo in esecuzione.
• Quando si interagisce con i servizi tramite il superserver (generico) di rete,
si identifica il servizio che si vuole attivare attraverso la sua porta (il suo
TSAP) well known.
 Ovviamente rimane comunque necessario, per il superserver di rete,
conoscere quale e’ il file name del programma eseguibile (filtro Unix)
che implementa il servizio che e’ stato richiesto, altrimenti come
potrebbe attivarlo?
• E’ ovvio che il superserver non puo’ implementare direttamente nessun
servizio, nemmeno se questo e’ sequenziale.
 Come la shell il superserver non conosce la semantica e il protocollo
dei diversi servizi che offre.
 Il superserver deve comunque rimanere in attesa di nuove richieste di
servizio, relative ad altri servizi.
13
File e file descriptor in Unix
• Nel mondo Unix tutte le risorse del sistema, indipendentemente da
quale sia il loro tipo reale, sono accedute in modo uniforme:
1. Tramite l’uso di un file descriptor (sono viste come dei file).
2. Tramite uno stesso insieme di system call (circa).
• Un file descriptor e’ una handle che consente ad un processo di
accedere alla risorsa reale associata a quel file descriptor.
1. L’associazione di una risorsa reale ad un file descriptor avviene
(di norma) tramite l’invocazione della system call open() (ma un
filtro eredita i file descriptor su cui operare dal processo padre).
2. La disassociazione di una risorsa reale da un file descriptor
avviene tramite l’invocazione della system call close().
• Implementativamente un file descriptor e’ costituito da un intero non
negativo di piccole dimensioni (“a small, nonnegative integer”, Linux man page):
 attualmente con valore compreso tra 0 e 1023.
• Dal punto di vista del sistema operativo il file descriptor e’ un indice
nel vettore User File Descriptor Table contenuto nel descrittore di
14
ciascun processo.
La User File Descriptor Table
• Nella User File Descriptor Table sono contenuti i riferimenti a tutte le
risorse reali utilizzate (accedibili) dal processo in un dato momento.
• Quando un processo esegue una system call open() Unix cerca
nella User File Descriptor Table la prima entry libera a partire da 0 e la
associa alla risorsa reale riferita nella system call open().
• Quando un processo esegue una system call close() Unix dichiara
libero il file descriptor (la entry corrispondente nella User File
Descriptor Table) riferito nella chiamata della system call.
• Eseguendo la system call fork() tutte le risorse del sistema che
erano accedibili dal processo padre rimangono accedibili anche dal
processo figlio, e cio’ utilizzando gli stessi valori di file descriptor.
 Al momento dell’esecuzione della system call fork() la User File
Descriptor Table del processo padre viene copiata nella User File
Descriptor Table del processo figlio.
• Dal punto di vista dell’utente e’ possibile clonare un file descriptor su
un altro file descriptor tramite una delle due system call dup() e
15
dup2().
File Descriptor Table
16
Unix system programming
• int dup(int fd);
• fd è il file descriptor da duplicare
• L’effetto di una invocazione di dup() è di copiare l’elemento fd
della tabella dei file aperti nella prima posizione libera (quella con
l’indice minimo tra quelle disponibili).
• Restituisce il nuovo file descriptor (quello destinazione
dell’operazione di copiatura),
oppure -1 (in caso di errore).
• int dup2(int oldfd, int newfd);
• oldfd è il file descriptor da duplicare.
• newfd è il file descriptor in cui deve essere duplicato.
• L’effetto di una invocazione di dup2() è di copiare l’elemento
oldfd della tabella dei file aperti nell’elemento newfd della stessa
tabella.
• N.B. se newfd era gia’ in uso al momento dell’invocazione di
dup2() esso viene implicitamente chiuso (tramite close()).
• Restituisce newfd oppure -1 (in caso di errore).
17
Protocollo di attivazione di un filtro Unix
• Per attivare un servizio standard (un filtro Unix), il processo padre
(e.g. una shell) deve duplicare se’ stesso tramite l’invocazione della
system call fork() per poi mettere in esecuzione nel suo clone figlio il
codice eseguibile del programma filtro.
• Per attivare correttamente un filtro Unix e’ quindi sufficiente che il
processo padre:
• Si cloni tramite chiamata alla system call fork().
• Nel processo figlio dup-lichi sui file descriptor 0 e 1 i file descriptor
(le risorse reali) su cui vuole fare lavorare il server-figlio.
(e chiuda tutti gli altri file descriptor che non interessano al serverfiglio)
• Nel processo clone figlio metta in esecuzione il codice del filtro
tramite invocazione della system call exec().
• N.B.: I file descriptor 0 e 1 sono come due parametri formali a cui il
chiamante associa come argomenti attuali le risorse reali che il filtro
18
Unix deve utilizzare per input e output.
La system call fork()
int fork(void);
• Crea un nuovo processo che e’ un clone esatto del processo chiamante
(padre).
• Il nuovo processo (figlio):
• Esegue lo stesso codice del processo padre e, al termine della system
call, ha il program counter posizionato all’istruzione successiva a quella
contenente l’invocazione della system call fork().
• Come spazio dati ha una copia di quello del processo padre.
• Condivide le risorse di sistema accedibili dal processo padre:
la sua User File Descriptor Table e’ una copia di quella del processo
padre.
• La system call fork() ritorna:
• In caso di errore, un numero negativo al processo padre
(il processo figlio non e’ nemmeno stato creato).
• In caso di terminazione corretta (esecuzione con successo),
• Il PID del processo figlio al processo padre,
• 0 al processo figlio.
Cio’ permette di capire, al ritorno dalla fork(), se si e’
il processo padre oppure il processo figlio.
19
Il superserver inetd di Unix
• Il superserver inetd e’ un demone (un processo attivato dal sistema alla
partenza e che rimane sempre attivo).
• inetd ha 3 scopi (il terzo conseguenza del secondo):
1. Evitare che tutti i diversi server di rete del sistema debbano essere attivi in
permanenza.

inetd rimane in attesa delle richieste dei clienti al posto dei singoli
server supportati, e attiva un server solo al momento in cui c’e’
effettivamente una richiesta di servizio pendente per lui.
2. Consentire ai singoli server di ignorare l’interazione con la rete,
comportandosi (essendo implementati) come normali filtri Unix.
3. Consentire quindi di esportare sulla rete qualunque servizio implementato
come filtro Unix (in particolare, servizi gia’ esistenti).
• Per fare questo inetd deve conoscere quali sono i servizi di rete definiti sul
sistema:
• Su quale servizio di trasporto (TCP o UDP) e su quale porta well known e’
offerto un servizio.
• Quale e’ il file eseguibile (filtro) che implementa il servizio.
• Il superserver acquisisce le informazioni sui servizi che deve supportare
da un file di configurazione.
20
Il file di configurazione del superserver inetd
Assumeremo che la sintassi del file di configurazione sia la seguente:
• Ogni riga del file descrive un servizio.
• Ogni servizio e’ descritto da una riga che ha la sintassi seguente:
<servizio> ::= <filter pathName>
<transport protocol>
<port number>
<wait flag>
<argument list>
• <filter pathName> e’ il pathname del codice eseguibile che
implementa il servizio (come filtro).
• <transport protocol> puo’ assumere i valori tcp o udp.
• <port number> e’ il numero della porta well known che identifica il
servizio sulla rete.
• <wait flag> indica se il servizio deve essere offerto in forma
sequenziale (wait) o concorrente (nowait).
• <argument list> specifica una lista di parametri (N.B.: uguali per
ogni invocazione) che il superserver deve passare al filtro che
21
implementa il servizio nel momento in cui lo attiva.
Il file di configurazione del superserver: esercizio
• Nota che ci potrebbero essere diversi parametri che caratterizzano
un Servizio Applicativo dal punto di vista del suo utilizzo del
Servizio di Trasporto.
• Quali potrebbero essere ad esempio dei parametri significativi di
Trasporto specifici di ciascun Servizio Applicativo nel caso di
• servizi Applicativi basati sul Servizio di Trasporto TCP?
• servizi Applicativi basati sul Servizio di Trasporto UDP?
• Come dovrebbe essere modificata di conseguenza la struttura del
file di configurazione del superserver?
22
Esempio di file di configurazione del superserver inetd
/bin/cat tcp 10001 nowait
/bin/csh tcp 10002 nowait –i
(o bash se non funziona csh)
/bin/java tcp 10003 wait prog1 arg1
• La prima linea descrive il servizio cat (copiatura da file a file) su TCP
che utilizza la porta 10001 e funziona in modalita’ concorrente (nowait).
N.B.: questo servizio, essendo i file di input e di output associati ad un
unico socket, implementa sulla rete un servizio di eco.
• La seconda linea descrive il servizio shell (csh) su TCP che utilizza la
porta 10002, funziona in modalita’ concorrente (nowait) e ha come
parametro di ingresso “-i”.
• La terza linea descrive il servizio prog1 su TCP implementato in Java,
che utilizza la porta 10003, funziona in modalita’ sequenziale (wait) e
ha come parametro di ingresso “arg1”.
 Attenzione: per i servizi implementati in Java il nome dell’eseguibile
che deve essere messo in esecuzione dal superserver e’ quello
della Java VM, alla quale occorre passare il nome del servizio
effettivo (prog1 nel nostro caso), piu’ gli eventuali parametri. 23
Servizi sequenziali e servizi concorrenti
.1
• Ogni servizio, implementato come un filtro, ignora la nozione di servizio
sequenziale/concorrente.
• E’ il superserver che e’ responsabile, per ogni servizio, di implementare
questa nozione, in base all’indicazione relativa presente nel file di
configurazione.
 N.B.: e’ ovvio che il superserver non puo’ comunque entrare nei
dialoghi applicativi relativi ai diversi servizi, visto che non ne
conosce nemmeno le regole.
• Se un servizio e’ definito come concorrente (nowait) il superserver,
anche dopo avere attivato una istanza del servizio, continuera’ ad
attendere sulla porta well known del servizio richieste di altri clienti.
• Se un servizio e’ definito come sequenziale (wait) il superserver, una
volta attivata una istanza del servizio, non attendera’ piu’ alcun evento
(nuova richiesta di servizio) sulla porta well known del servizio fino a
che l’istanza precedente non sia terminata.
 Nel caso di un servizio sequenziale il superserver deve avere la
possibilita’ di capire quando il filtro che ha attivato per
24
implementare il servizio ha terminato la sua esecuzione.
Servizi sequenziali e servizi concorrenti
.2
• Il superserver, dopo avere attivato una istanza di un servizio S, deve
comunque rimanere attivo, anche se S e’ un servizio sequenziale:
 Il superserver deve comunque gestire nuove richieste, provenienti
dalla rete, destinate ad S o ad un altro servizio.
• Se S e’ un servizio concorrente, l’istanza di S e il superserver saranno
eseguiti in parallelo e in modo completamente indipendente.
 Nessuno dei due ha piu’ bisogno di avere a che fare con l’altro.
• Se pero’ S e’ un servizio sequenziale il superserver deve essere
informato della terminazione dell’istanza di S che ha attivato.
• S e’ implementato da un normale filtro, sia che esso sia concorrente
sia che esso sia sequenziale!
• In Unix ci sono normali meccanismi di sistema operativo che
consentono ad un processo figlio di informare il processo padre
della propria terminazione e anche di ritornargli un valore (intero)!
• L ’esecuzione della system call exit(n) o dell’istruzione
return(n) nella funzione main() del processo figlio lo terminano
e consentono di informare il padre di cio’ e di passargli il valore n.
25
Sincronizzazione del superserver con i processi figli .1
• In realta’ il superserver deve comunque gestire in qualche modo
anche la morte dei processi figli che implementano servizi concorrenti,
altrimenti questi processi rimarrebbero nel sistema come zombi.
 Basta pero’ che il processo padre prenda atto della morte di un
processo figlio per evitare che questo diventi uno zombi.
• Il superserver puo’ prendere atto della (aspettare la) morte di un
processo figlio invocando la system call wait().
• La system call wait() permette di ricavare il PID (quindi l’identita’) di
un figlio che muore e anche la ragione di questa morte.
L’interfaccia della funzione e’:
int wait(int* err);
•
•
Il valore di ritorno e’ il PID del processo figlio che e’ morto.
Il parametro di ritorno err fornisce lo stato di terminazione del
figlio (in errore, e se del caso quale, o meno).
 err e’ il valore ritornato dal processo figlio come argomento
della system call exit() o dell’istruzione return() che lo
26
ha terminato.
Sincronizzazione del superserver con i processi figli
.2
• Poiche’ la system call wait() e’ bloccante, il superserver deve
chiamarla solo quando e’ sicuro della morte (gia’ avvenuta) di un
processo figlio.
• Altrimenti l’intero superserver rimarrebbe bloccato in attesa della
morte di un figlio, e verrebbe ad assumere il comportamento di
superserver super-sequenziale!
• Il superserver e’ informato (in modo asincrono) della morte di un
processo figlio perche’, quando questo evento accade, il sistema
operativo gli invia il segnale (interrupt software) SIGCLD (detto anche
SIGCHLD).
• Per poter catturare effettivamente il segnale, il superserver deve
registrare sul sistema operativo, tramite chiamata della system call
signal(), il suo interesse per l’evento (se non lo fa, il segnale non
gli e’ inviato).
27
Unix system programming
• void (* signal(int sig, void (*func)()))(int);
• sig è l’intero (o il nome simbolico) che individua il segnale da
gestire
• il parametro func è un puntatore a una funzione che indica
l’azione da associare al segnale; in particolare func può:
• puntare alla routine di gestione dell’interruzione (handler)
• valere SIG_IGN (nel caso di segnale ignorato)
• valere SIG_DFL (nel caso di azione di default)
• ritorna un puntatore a funzione:
• al precedente gestore del segnale
• SIG_ERR (-1), nel caso di errore
• Una funzione che e’ uno handler di segnali per un processo deve
avere il seguente prototipo:
void signalHandler(int sig);
28
Struttura di uno handler di segnali
• L’argomento di ingresso di un signal handler e’ l’identificatore numerico
del segnale che e’ scattato: cio’ permette di registrare la stessa funzione
per piu’ segnali e di gestire poi uno switch in funzione del segnale che si
e’ manifestato.
• Nel nostro caso lo handler deve gestire solo il segnale SIGCLD.
• Per fare questo nel proprio corpo invochera’ la system call wait()
che, dato il contesto della chiamata, sara’ non bloccante.
• Nel caso di morte di un figlio relativo ad un servizio concorrente la
presa d’atto del fatto, tramite chiamata a wait(), e’ gia’ sufficiente
ad evitare che il figlio morto diventi uno zombi.
• Nel caso di morte di un figlio relativo ad un servizio sequenziale il
superserver dovra’ anche riabilitare il servizio, dovra’ cioe’ tornare ad
aspettare richieste di nuovi clienti sulla relativa porta well known.
• Lo handler viene attivato in seguito alla ricezione di uno dei segnali a
cui e’ stato associato, e viene visto dal sistema come una thread
secondaria del processo, in aggiunta a quella principale
(che potrebbe eventualmente essere stata interrotta!).
• In uscita un signal handler non ritorna alcun valore.
29
Aspetti particolari dei segnali e di SIGCLD
• Un aspetto particolare della chiamata alla funzione wait() e’ che
essa permette di rimuovere i processi terminati dallo stato di zombi.
 Infatti tutti i processi, quando terminano, vengono posti dal
sistema operativo nello stato di zombi, in cui rimangono o fino alla
morte del processo che li ha creati o fino a che questo prende atto
dell’evento tramite la chiamata della funzione wait().
• Bisogna anche osservare che la ricezione di un segnale da parte di un
processo interrompe l’esecuzione di una system call eventualmente in
corso (bloccata):
in questo caso la system call termina con codice di errore EINTR.
• Poiche’ il superserver deve gestire il segnale di SIGCLD deve
prendere in considerazione e gestire questa possibilita’.
 La cosa e’ vera in particolare per la system call select(), che e’
bloccante e che costituisce il cuore del superserver.
 Infatti il superserver deve rimanere in attesa di richieste,
contemporaneamente, sulle porte well known di tutti i servizi
che esso supporta (e che in quel momento sono abilitati).
30
Gestione del ritorno da select()
. . .
rs = select(. . ., NULL);
if (rs<0) {
if (errno == EINTR) {
// morte di un figlio, non e’ un
// errore vero, ma non c’e’ neanche
// nessun socket pronto
. . .
} else {
// e’ un errore vero
. . .
}
}
// tutto OK e ci sono socket pronti
. . .
31
Schema del superserver inetd
Per gestire la morte di un
figlio. Alla morte di un figlio
se il servizio era di tipo wait
occorre reinserire nella select
il socket-descriptor associato
al servizio
Per ogni servizio listato nel
file di configurazione
socket()
bind()
listen()
(solo per TCP)
Ciclo infinito
signal()
select()
Test per lettura
nfd=accept()
(solo per TCP)
close nfd
padre
Se wait-flag==wait
elimina dalla select il
fd associato al servizio
figlio
fork()
close di tutti i fd diversi da quello del socket;
dup(nfd); dup(sfd); su stdin e stdout
execle() del server-program
32
La system call exec()
• Negli schemi standard dei server di rete concorrenti visti a lezione il
server padre, quando deve servire un nuovo cliente, esegue il
fork() di un processo figlio, che puo’ immediatamente procedere a
fornire il servizio al cliente perche’ il suo codice e’ identico a quello
del padre.
• Nel caso del superserver, pero’, il padre non contiene il codice
capace di implementare ciascuno dei singoli servizi che esso offre,
ma solo quello per la loro attivazione.
• Come puo’ il superserver arrivare a fornire il servizio richiesto dal
cliente?
• Ci riesce perche’ (come una shell) il clone figlio del superserver non
continua ad eseguire lo stesso codice del superserver ma, dopo una
fase iniziale di housekeeping, mette in esecuzione nel proprio
contesto un nuovo programma, quello del filtro Unix che implementa
effettivamente il servizio desiderato dal cliente.
 N.B.: e’ evidente che tutto questo meccanismo si basa sulla nozione
33
di porta well known (che indentifica il servizio desiderato)!
La system call exec()
• La funzione execle() permette di mettere in esecuzione, nel
contesto del processo chiamante (cioe’ mantenendo il possesso delle
stesse risorse reali possedute dal processo chiamante), un ben
determinato programma.
• Noi la useremo nel processo clone-figlio per specializzarlo a fornire il
servizio specifico per il quale e’ stato creato (sia per servizi concorrenti
che per servizi sequenziali).
• La system call execle() e’ una funzione ad argomenti variabili di cui
i primi due sono obbligatori e sono:
• nome del file contenente il programma eseguibile che si vuole
eseguire, comprensivo dell’intero path;
• nome del programma.
segue poi la lista degli argomenti terminata da un NULL, infine l’ultimo
argomento e’ un puntatore all’enviroment.
• Quindi la chiamata:
execle(“/bin/csh”, “csh”, “-i”, NULL, env);
34
mette in esecuzione il programma csh passandogli il parametro “-i”.
La system call execle()
int execle(char *pathname, char *arg0, …,
char *argn, (char *)NULL, char **envp);
• La funzione, in effetti, ritorna al chiamante solo in caso di errore!
• Se non c’e’ errore, l’effetto della system call e’ di passare il controllo
alla prima istruzione del programma contenuto nel file pathname.
• Dal punto di vista C la modalita’ di passaggio degli argomenti arg0
.. argn equivale a passare un array NULL-terminato di puntatori,
come e’ in effetti il parametro di ingresso argv della funzione main().
• arg0 per convenzione deve essere il nome del programma, come
indicato alla pagina precedente.
• L’ultimo parametro e’ un puntatore ad una esplicita environment list,
che e’ a sua volta implementata come un array NULL-terminato di
puntatori a stringa.
• Quello che faremo e’ fare ereditare al processo figlio lo stesso
environment del superserver.
35
Struttura del superserver
.1
1. Il parametro di ingresso envp della funzione main()
int main (int argc, char **argv, char **envp);
consente al superserver di procurarsi il riferimento envp
all’environment da utilizzare in execle().
2. La funzione main() del superserver apre in lettura il file di
configurazione.
3. Per ogni linea di tale file ricava i parametri del servizio descritto in
quella linea e li salva in una opportuna struttura dati:
 tutti i servizi sono inizialmente definiti come abilitati.
4. Per ogni servizio, crea un socket e lo bind()-a alla porta well known
su cui quel servizio e’ offerto (quella che e’ indicata nel file di
configurazione, e il cui numero deve essere congruente con le
indicazioni IANA).
5. Se il servizio utilizza il trasporto TCP il superserver mette anche in
stato listen()-ing il socket relativo.
36
Struttura del superserver
.2
7. Anche il file descriptor del socket well known deve essere salvato
nella struttura dati di cui sopra.
8. Il superserver chiama la system call signal() per registrare il
proprio interesse a gestire il segnale SIGCLD.
9. Terminata l’inizializzazione il superserver entra in un ciclo infinito in
cui, tramite chiamata della system call select(), attende l’arrivo di
nuove richieste di servizio da nuovi clienti;
quando queste arrivano, attiva per ciascuna di esse (per il tramite di
un corrispondente processo figlio) l’opportuno server specifico (che
e’/deve essere strutturato come un filtro Unix).
10. All’interno del ciclo (reinizializza e) riempie il parametro di
ingresso readfds della select() con tutti e soli i file
descriptor dei servizi correntemente abilitati.
Se un servizio nowait e’ gia’ attivo, esso non puo’ essere
nuovamente attivato in questo momento, e quindi non deve essere
37
in stato abilitato.
Struttura del superserver
.3
11. Al termine della select(), trascurando i casi di errore, scandisce il
parametro di ritorno readfds (N.B. quindi readfds e’ un parametro
value-result!) che, a questo punto, lista tutti i file descriptor sui quali e’
pendente una nuova richiesta di servizio da parte di un nuovo cliente.
Per ciascuna di queste richieste mette in funzione il server specifico.
12. Se il servizio specifico utilizza il servizio di trasporto TCP, chiama la
system call accept() per prendere in carico la nuova connessione e
ricavare il corrispondente socket descriptor nfd: ad esso e’ associata
la nuova richiesta di servizio.
38
Struttura del superserver
.4
11. Se il servizio specifico utilizza il servizio di trasporto UDP, registra
come file descriptor nfd sul quale interagire con il cliente il file
descriptor della relativa porta well known?
Cio’ e’ possibile? No!
 Nel caso di un servizio UDP concorrente lo schema visto a lezione
prevede che il servizio sia effettivamente fornito su una porta
diversa da quella well known, altrimenti sarebbe impossibile
utilizzare la porta well known per aspettare nuovi clienti! Allora?
 Nel caso di un servizio UDP sequenziale si e’ visto che
l’implementazione piu’ conveniente si basa sull’utilizzo da parte
del server specifico di una porta (effimera) diversa da quella, well
known, su cui il servizio e’ offerto. Allora?
39
Struttura del superserver
.5
14. Chiama la system call fork() per generare il processo che fornira’
effettivamente il servizio alla richiesta corrente.
15. Nel ramo del padre, se il servizio considerato e’ di tipo TCP :
• Chiude il socket nfd.
• Se il servizio considerato e’ di tipo wait allora registra nella
struttura dati di descrizione dei servizi il PID del processo figlio e
marca il servizio come disabilitato
(se il servizio considerato e’ di tipo nowait non c’e’ niente di
specifico da fare).
• Passa a considerare la prossima richiesta pendente o, se non ce
ne sono piu’, ritorna all’inizio del ciclo principale.
16. Nel ramo del figlio:
• Chiude i file descriptor 0 e 1.
• Duplica sui file descriptor 0 e 1 il file descriptor nfd.
• Chiude tutti i file descriptor del processo ad eccezione di 0, 1 e 2.
• Chiama la system call execle() per attivare il servizio richiesto.
40
Struttura dati di descrizione dei servizi
• Ci sono svariate modalita’ per salvare le informazioni relative ai
servizi che devono essere supportati.
• La migliore e’ quella di utilizzare una coda linkata (doppiamente) di
strutture (ogni struttura descrive un singolo servizio).
 Questa soluzione ha il vantaggio di non porre limiti al numero
massimo di servizi supportabili dal superserver
• Un’altra possibilita’ e’ quella di utilizzare un vettore di strutture.
 Questa soluzione (quella consigliata) ha lo svantaggio di limitare
ad un valore massimo il numero di servizi supportabili.
 Il problema puo’ in realta’ essere risolto facilmente allocando
l’array dinamicamente in modo che abbia dimensioni sufficienti a
contenere la descrizione di tutti i servizi supportati. Per sapere
quanto deve essere grande l’array basta, per prima cosa, contare
il numero di righe del file di configurazione.
41
Esempio di struttura dati di descrizione dei servizi
.1
typedef struct SrvElmT {
char
tr[4];
// tcp se il servizio usa il trasporto TCP,
// udp se usa il trasporto UDP
char
conc[7];
// wait se il servizio e' di tipo sequenziale,
// nowait altrimenti
char
port[8];
char
srvFullName[500];
// pathname completo del file di codice eseguibile
// del programma
char
srvName[20];
// nome del programma
// continua . . .
42
Esempio di struttura dati di descrizione dei servizi
.2
// . . . Continua:
int
numArgs;
// numero di argomenti da passare alla execle,
// 0 in caso di assenza di essi
char
argv1[20];
char
argv2[20];
char
argv3[20];
char
argv4[20];
char
argv5[20];
int
fd;
// socket descriptor associato alla porta well
// known del servizio
int
pid;
// significativo solo per servizi di tipo wait (?)
} SrvElmT, *SrvElmTP;
43
SrvElmT
srvArray[20]; // massimo 20 servizi
Servizi UDP
• I servizi UDP non possono essere trattati in modo analogo a quelli TCP.
• La ragione di fondo e’ che mentre in TCP un socket rappresenta una
connessione (e quindi una relazione gia’ instaurata con un singolo
cliente), in UDP esso rappresenta una porta (e quindi c’e’ l’eventualita’ di
potere/dovere interagire su quella porta con piu’ clienti, vecchi e nuovi).
• Diverse conseguenze sul superserver:
• Un servizio concorrente UDP non puo’ essere trattato in modo
analogo ad un servizio concorrente TCP.
 Se mantenesse il servizio abilitato, alla prossima select() il
superserver potrebbe trovare gia’ pendente come nuova
richiesta quella del servizio appena attivato, perche’ il datagram
che aveva richiesto il servizio all’iterazione precedente potrebbe
essere ancora da consumare.
• Per realizzare un servizio UDP sequenziale come un filtro Unix (ma
con interazioni a messaggio, non a stream!) bisogna gestire il fatto
che il server filtro deve interagire solo con un singolo cliente e non
puo’ farlo esplicitamente (cioe’ eliminando lui le interazioni non
44
volute sulla porta well known: i filtri Unix non lo fanno!).
Servizio CL concorrente
.1.1
SuperServerp
socket server di associazione
porta UDP server well-known
porta UDP effimera
socket client
Client UDP
45
Servizio CL concorrente
.1.2
SuperServerp
socket server di associazione
socket server di comunicazione
porta UDP server well-known
porta UDP server effimera
porta UDP effimera
socket client
Client UDP
46
Servizio CL concorrente
.1.3
fork()
SuperServerp
Server Figlio
socket server di associazione
socket server di comunicazione
porta UDP server well-known
porta UDP server effimera
porta UDP effimera
socket client
Client UDP
• Il server figlio, appena attivato,
dopo la exec(), potrebbe
andare subito a leggere il
datagram pendente nel socket
well known, ma chi dice che
arriverebbe a farlo prima della
nuova select() di
SuperServerp
• E comunque, questo non
sarebbe il comportamento di
47
un filtro Unix!
Servizi UDP implementati tramite filtri Unix
.1
• Il servizio deve interagire con un singolo cliente, senza conoscerne
necessariamente l’identita’.
 Non deve nemmeno sapere che sta interagendo con il cliente
tramite un socket UDP!
• Poiche’ deve poter ignorare l’identita’ del cliente remoto (infatti opera
sui file descriptor 0 e 1 tramite operazioni di read() e write())
bisogna che il socket UDP che e’ associato ai file descriptor 0 e 1 sia
gia’ connesso al cliente!
• E’ il superserver che deve passare al server specifico una porta UDP
gia’ connessa!
• Ma per un server UDP concorrente la porta con cui interagire con il
cliente non puo’ essere la porta well known, deve essere una porta
effimera.
• Quindi e’ questa porta che deve essere passata al server specifico
come associata ai file descriptor 0 e 1, non la porta well known.
• Nota che la disciplina di utilizzo delle porte che va bene per un
server UDP concorrente si puo’ applicare altrettanto bene ad un
48
server sequenziale!
Servizio CL concorrente
.2.1
SuperServerp
socket server di associazione
porta UDP server well-known
porta UDP effimera
socket client
Client UDP
49
Servizio CL concorrente
.2.2
SuperServerp
socket server di associazione
socket server di comunicazione
porta UDP server well-known
porta UDP server effimera
porta UDP effimera
socket client
Client UDP
50
Servizio CL concorrente
.2.3
SuperServerp ?
recvfrom(MSG_PEEK?)
socket server di associazione
socket server di comunicazione
porta UDP server well-known ?
porta UDP server effimera
porta UDP effimera
socket client
Client UDP
51
Servizio CL concorrente
SuperServerp ?
.2.4
connect(socket client)
socket server di associazione
socket server di comunicazione
porta UDP server well-known ?
porta UDP server effimera
porta UDP effimera
socket client
Client UDP
52
Servizio CL concorrente
.2.5
fork()
SuperServerp ?
SuperServerf ?
socket server di associazione
socket server di comunicazione
porta UDP server well-known ?
porta UDP server effimera
porta UDP effimera
socket client
Client UDP
53
Servizio CL concorrente
SuperServerp ?
.2.6
Server Figlio
exec()
socket server di associazione
socket server di comunicazione
porta UDP server well-known ?
porta UDP server effimera
porta UDP effimera
socket client
Client UDP
• Il server figlio, dopo la exec(),
non ha comunque piu’ il
messaggio nella sua memoria
dati centrale
• E ovviamente, secondo il
protocollo di attivazione di un
server-filtro Unix, non ha
nemmeno piu’ accesso alla
porta well known
• Come fa a vedere il
messaggio?
54
Servizi UDP implementati tramite filtri Unix
.2
• Pero’ il datagram del cliente che ha portato all’attivazione del servizio
UDP e’ registrato nella porta well known, non nella porta effimera che
vogliamo passare al server specifico.
 Come puo’ il server specifico andarlo ad accedere?
 Oltretutto la sua presenza nella porta well known puo’ costituire
un problema per il superserver che, nel caso di un servizio UDP
concorrente, deve utilizzare la porta well known per aspettare
richieste di nuovi clienti e non deve considerare 2 volte una
stessa richiesta.
• Quindi bisogna che il superserver estragga il datagram UDP dalla
porta well known e lo inserisca nella porta effimera prima di attivare il
servizio.
• N.B.: L’utilizzo di una seconda porta (effimera) per interagire con il
client e’ strettamente necessario solo per realizzare un servizio UDP
concorrente, ma torna comodo anche nel caso di un servizio UDP
sequenziale.
55
Servizio CL concorrente
.3.1
SuperServerp
socket server di associazione
porta UDP server well-known
porta UDP effimera
socket client
Client UDP
56
Servizio CL concorrente
.3.2
SuperServerp
socket server di associazione
socket server di comunicazione
porta UDP server well-known
porta UDP server effimera
porta UDP effimera
socket client
Client UDP
57
Servizio CL concorrente
.3.3
SuperServerp
recvfrom()
socket server di associazione
socket server di comunicazione
porta UDP server well-known
porta UDP server effimera
porta UDP effimera
socket client
Client UDP
58
Servizio CL concorrente
.3.4
SuperServerp
sendto(porta server effimera)
socket server di associazione
socket server di comunicazione
porta UDP server well-known
porta UDP server effimera
porta UDP effimera
socket client
Client UDP
59
Servizio CL concorrente
SuperServerp
.3.5
connect(socket client)
socket server di associazione
socket server di comunicazione
porta UDP server well-known
porta UDP server effimera
porta UDP effimera
socket client
Client UDP
60
Servizio CL concorrente
.3.6
fork()
SuperServerp
SuperServerf
socket server di associazione
socket server di comunicazione
porta UDP server well-known
porta UDP server effimera
porta UDP effimera
socket client
Client UDP
61
Servizio CL concorrente
SuperServerp
.3.7
Server Figlio
exec()
socket server di associazione
socket server di comunicazione
porta UDP server well-known
porta UDP server effimera
porta UDP effimera
socket client
Client UDP
62
Servizi UDP implementati tramite filtri Unix
.3
1. Il superserver acquisisce l’indirizzo del cliente e il datagram UDP
che ha attivato il servizio eseguendo una system call recvfrom()
sul socket della porta well known.
2. Il superserver crea un nuovo socket e lo bind-a ad una porta
effimera.
3. Il superserver invia il datagram UDP che ha portato all’attivazione
del servizio alla porta effimera appena creata.
 N.B.: per inviare il datagram il superserver puo’ utilizzare sia la
porta effimera appena creata (la stessa che e’ destinazione del
datagram!) che la porta well known del servizio UDP!
4. Il superserver connette la porta effimera appena creata
all’indirizzo del cliente.
 N.B.: l’ordine di queste due operazioni e’ fondamentale perche’
se il superserver connettesse la porta effimera prima di inviarle
il datagram vedrebbe il suo datagram venire cestinato!
63
bind(), INADDR_ANY, e porte effimere
• Quando il superserver utilizza la system call bind() per associare il socket
server UDP di comunicazione ad una porta effimera, assegna al parametro
*myaddr il valore 0.0.0.0:0 (cioe’ INADDR_ANY:0).
• Notare che l’indirizzo 0.0.0.0:0 non e’ un indirizzo valido (vero), e che non e’
utilizzabile come indirizzo destinazione in una operazione di rete:
 INADDR_ANY non e’ un indirizzo di rete valido.
 La porta numero 0 non esiste.
•
Effettuare il bind all’indirizzo 0.0.0.0:0 non significa chiedere effettivamente
l’associazione a questo indirizzo (che non e’ un indirizzo valido), ma chiedere
l’associazione a una qualunque porta effimera libera e a tutti gli indirizzi IP della
macchina.
•
Nella system call bind() il parametro *myaddr e’ di ingresso, non di ingresso
uscita.
•
Ma allora come posso sapere a quale porta effimera il socket e’ stato
effettivamente associato, cosi’ da potergli inviare il datagram UDP del cliente?
 Vedi dispense, parte teoria.
•
E quale indirizzo IP devo utilizzare come destinazione della sendto()?
 Ovviamente 127.0.0.1! (perche’?)
64
Servizi UDP implementati tramite filtri Unix
.4
5. A questo punto il superserver puo’ comportarsi con un servizio
UDP cosi’ come si comporta con un servizio TCP, utilizzando il
socket descriptor della porta effimera come nfd.
• Vedi punto 13 della struttura del superserver.
6. Nota che avendo gia’ estratto dalla porta well known del servizio
UDP il datagram che ha portato a questa attivazione del servizio, il
superserver puo’, nel caso di un servizio UDP concorrente,
continuare a considerare abilitato il servizio anche nelle iterazioni
successive, andando ad attendere immediatamente l’arrivo di
nuovi datagram di richiesta.
65
Servizi UDP implementati tramite filtri Unix
.5
• Dal punto di vista del server specifico l’unico vincolo che c’e’, nel
caso di utilizzo del servizio di trasporto UDP, e’ che bisogna inviare
o ricevere un intero PDU applicativo tramite ogni singola operazione
di write() e read().
• Il servizio specifico non puo’ per esempio leggere da standard
input singoli caratteri se il pari gliene manda molti in un singolo
datagram.
• Ma se un server specifico avesse bisogno di sapere chi c’e’ dall’altra
parte della rete?
• Ovvio che in questo caso dovrebbe anche sapere a priori che
sta interagendo con un cliente remoto tramite il servizio di
trasporto UDP.
• Basterebbe che chiamasse la funzione getpeername().
• N.B.: lo stesso discorso si applica ovviamente anche ad un
server TCP (che ovviamente interagisce con il pari tramite una
connessione. La cosa e’ meno scontata per un server UDP!). 66
Servizi TCP e setsockopt()
• Immaginiamo il seguente scenario:
• Il superserver riceve una richiesta per il servizio concorrente
TCP S definito sulla porta well known WK, e di conseguenza
attiva il processo figlio PS per offrire il servizio.
• Il servizio e’ ovviamente offerto tramite una connessione che ha
come end-point locale la porta WK.
• Mentre il processo PS e’ ancora attivo il superserver, per un
motivo o per l’altro, termina (e.g. e’ abortito) e viene riattivato.
• Alla partenza il superserver deve ovviamente appropriarsi di
tutte le porte well known che gestisce.
• Questo costituisce un problema per la rete? Ci sono ambiguita’?
• Questo scenario vi ricorda qualcosa descritto parlando di
setsockopt()?
• Durante l’implementazione del superserver tenete conto di quanto
detto qui.
67
Esercizio 1: superserver
.1
• Realizzare un superserver di rete.
• Il superserver deve essere configurato tramite file di configurazione.
• Il superserver deve essere in grado di gestire sia servizi TCP che
servizi UDP, sia servizi concorrenti (nowait) che servizi sequenziali
(wait), indipendentemente dal tipo di servizio di Trasporto utilizzato.
• Verificare il superserver interagendo con i servizi TCP cat e csh, e
con un servizio UDP di echo (da realizzare a partire dagli esempi
presentati nelle dispense).
• Come modulo cliente per il test dei servizi TCP si puo’ utilizzare un
normale Hyperterm di Windows (o un client TELNET).
• Come modulo cliente per il test dei servizi UDP si puo’ utilizzare il
client di echo visto a lezione con gli opportuni adattamenti.
• Attenzione: esiste il pericolo di una corsa critica tra la morte di un
server sequenziale figlio e la registrazione di questo server come gia’
attivo da parte del superserver!
Opzionale: Cercate in qualche modo di gestire questa situazione!
68
Esercizio 1: superserver
.2
• Per testare il buon funzionamento del superserver rispetto a servizi
sequenziali e’ necessario avere server che terminano.
• Tutti i servizi implementati come filtri Unix e basati su TCP terminano
nel momento in cui vedono chiudersi la connessione con il client, e
questo vale anche per cat e csh.
• Nell’esempio di servizio di eco “sequenziale” basato sul trasporto
UDP visto a lezione il server, pero’ non termina mai.
 N.B.: esso non e’ nemmeno implementato come un filtro Unix
• Occorrera’ quindi definire un protocollo applicativo che possa portare
alla terminazione del server UDP di eco.
• A noi interessa solo poter testare il superserver, quindi quale sia
questo “protocollo” non ci interessa. Esempi di possibilita’:
•
Il server UDP di eco termina dopo avere echeggiato N messaggi.
•
Il server UDP di eco termina quando riceve un messagio vuoto.
•
Il server UDP di eco viene terminato tramite kill da console.
69
Scarica

Struttura di un filtro Unix - Dipartimento di Matematica e Informatica