Un interessante articolo apparso sul sito IBM, attualmente preso come riferimento anche dagli utenti del Pinguino, ci mostra cosa accade “dietro le quinte” quando utilizziamo le socket di Linux, anche in poche righe di codice come quelle di seguito riportate (caso “client”).
#include
[…]
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
address.sun_family = AF_UNIX;
strcpy(address.sun_path, “server_socket”);
len = sizeof(address);
result = connect(sockfd, (struct sockaddr *)&address, len);
[…]
write(sockfd, &ch, 1);
read(sockfd, &ch, 1);
printf(“char from server = %cn”, ch);
close(sockfd);
[…]
Da sempre siamo abituati, ogni volta che si parla di networking, a leggere (o rileggere) la solita teoria sulla pila ISO/OSI. Invece M. Tim Jones, autore dell’articolo, preferisce fare il confronto con la più pratica pila TCP/IP, poiché essa meglio aderisce al networking stack di Linux. Alla base di questa pila si trova il link layer, che si occupa dell’accesso livello fisico (link seriale, Ethernet, o altro); al di sopra di esso c’è il network layer, il quale ha il compito di smistare i pacchetti verso la destinazione e il cui protocollo più comune è sicuramente l’Internet Protocol (IP). Ancora sopra si trova il transport layer, responsabile della comunicazione peer-to-peer (o end-to-end); protocolli di riferimento TCP e UDP. In cima c’è l’application layer, con tutti i suoi protocolli “semantici” come l’HTTP o l’SMTP.
Addentrandoci nell’implementazione Linux di questa pila, notiamo anzitutto che l’architettura di questo sistema operativo si basa su due macrostrutture: User Space e Kernel Space. Il primo si identifica in tutto e per tutto con l’application layer, mentre il secondo è composto da varie interfacce: system call interface; protocol agnostic interface; protocolli di rete; device agnostic interface; device drivers.
La system call interface può essere descritta in due modi: quando una richiesta di connessione è fatta dall’utente, questa è gestita chiamando la funzione sys_socketcall di ./net/socket.c oppure si può ricorrere a semplici chiamate write and read al socket (rappresentato da un semplice file descriptor) più una serie di operazioni specifiche per il networking, come ad esempio la funzione di connect.
Il socket vero e proprio rappresenta lo strato sottostante, ovvero la protocol agnostic interface. Questo livello è in grado di supportare non solo i protocolli TCP e UDP, ma altri quali l’IP o l’Ethernet stesso. La struttura di un socket in Linux è implementata tramite struct sock in linux/include/net/sock.h. Tale struttura definisce tutti gli stati di un socket, incluso anche il protocollo di comunicazione utilizzato. Inoltre, grazie ad un’altra struttura inclusa nell’header sopramenzionato — struct proto — il networking subsystem è in grado di conoscere le caratteristiche e le operazioni possibili per un protocollo.
La sezione networks protocols definisce i protocolli disponibili, inizializzati con la funzione inet_init in linux/net/ipv4/af_inet.c. Questa funzione registra ogni protocollo usando la funzione proto_register definita in linux/net/core/sock.c. Ognuno dei protocolli implementati è mappato nella struttura inetsw_array, inizializzato tramite la funzione inet_register_protosw all’interno di inet_init ed arriviamo finalmente ai dati. Gli scambi di dati veri e propri all’interno di un socket faranno riferimento alla struttura sk_buff in linux/include/linux/skbuff.h; in pratica ciascun pacchetto, spedito o ricevuto, è un sk_buff ciascuno dei quali contiene una struttura per il device — net_device — verso il quale un pacchetto sarà spedito o dal quale sarà ricevuto. In più sk_buff contiene un puntatore relativo al socket utilizzato e, ovviamente, i dati che costituiscono il pacchetto stesso, ovvero un pacchetto TCP piuttosto che un pacchetto UDP comprensivo di header e flag.
Al di sotto di questo strato c’è la device agnostic interface, che è in grado di far comunicare i vari protocolli di più altro livello con i device fisici, prescindendo dalle caratteristiche di questi ultimi. Come anticipato prima, la struttura che rappresenta un device è il net_device codificato in linux/include/linux/netdevice.h. Così, per poter spedire un sk_buff ad un device, questo strato software mette a disposizione la funzione dev_queue_xmit, la quale ha il compito effettivo della trasmissione dati al device relativo. La ricezione, invece, è realizzata tramite la funzione netif_rx. Queste ultime due funzioni si trovano entrambe in linux/net/core/dev.c.
Ultimo strato è il device drivers che si occupa dell’apparato fisico di trasmissione a bassissimo livello. Al momento dell’avvio, un device driver alloca una struttura net_device e inizializza alcune funzioni. Tra queste viene chiamata la hard_start_xmit, che definisce come un pacchetto viene accodato per la trasmissione dopo una chiamata a dev_queue_xmit. Questo rapido excursus mostra cosa avviene a livello kernel, quando utilizziamo le semplici righe di codice C illustrate sopra e dimostra come sia semplice e ben strutturata l’architettura che regge il networking di Linux.
di Cosmos Puglisi - Programmazione.it