Trasferimento dati efficiente su Linux con la tecnica “zero-copy”

By Francesco Allassia

Trasferimento dati: l’approccio tradizionale

Consideriamo lo scenario in cui un file letto dal disco deve essere trasferito ad un altro programma in rete. Questo scenario descrive il comportamento di molti tipi di applicazioni, tra cui applicazioni Web, client o server FTP, email, e così via.
Il funzionamento classico prevede le seguenti due chiamate di base:

File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

Anche se è concettualmente il funzionamento è abbastanza semplice, internamente l’operazione richiede quattro switch di contesto tra user-mode (um) e kernel-mode (km) e quattro copie di dati prima che l’operazione venga completata.

  1. La chiamata “read” provoca il primo switch di contesto da user-mode a kernel-mode. Internamente un “sys_read ” (o equivalente) è incaricato a leggere i dati dal file. La prima copia è eseguita dal motore DMA (Direct Memory Access), che legge il contenuto del file dal disco e lo memorizza in un buffer del kernel address-space.
  2. Il dato viene poi copiato dal buffer di sistema ad un buffer dell’address-space dell’applicazione. La chiamata “read” termina provocando il secondo switch di contesto (km->um).
  3. La chiamata “send” al socket provoca il terzo switch di contesto (um->km). Una terza copia è effettuata per mettere i dati nuovamente in un buffer del kernel address-space. Questa volta però i dati vengono messi in un altro buffer, che è associato al socket di destinazione.
  4. La “send” termina provocando il quarto switch di contesto. Indipendentemente e in modo asincrono, una quarta copia viene fatta quando il DMA trasferisce i dati dal buffer al motore del protocollo di rete.

Trasferimento dati: l’approccio zero-copy

Se si riesamina lo scenario tradizionale, si noterà che la seconda e la terza copia dei dati non sono effettivamente necessarie. L’applicazione non rappresenta altro che una cache per i dati letti dal file e trasferiti al socket-buffer. Invece i dati potrebbero essere trasferiti direttamente dal read-buffer al socket-buffer.
Il metodo “transferTo” permette di fare esattamente questo trasferendo i dati da un canale file ad un altro canale scrivibile.
Internamente, il processo di zero-copy dipende dal sistema operativo: nei sistemi Linux, questa chiamata viene instradata alla chiamata di sistema “sendfile” che trasferisce i dati da un descrittore di file ad un altro.
Quindi l’azione di “file.read” e “socket.send” può essere rimpiazzata dall’azione “transferTo”.

Questo è solo il primo miglioramento: si è ridotto gli switch di contesto da quattro a due e il numero di copie dei dati da quattro a tre. Siamo in grado di ridurre ulteriormente la duplicazione dei dati se la sottostante interfaccia di rete gestisce la zero-copy.
Con alcune delle moderne schede, è possibile copiare i dati semplicemente fornendo alla scheda il loro descrittore. Dal kernel Linux 2.4 in poi, il descrittore del socket buffer è stato modificato per soddisfare il requisito della zero-copy. Questo approccio non solo riduce gli switch di contesto, ma elimina anche le copie di dati che richiedono il coinvolgimento della CPU.

Con la zero-copy gli step eseguiti sono:

  1. Il metodo “transferTo” richiede il trasferimento dei dati dal file ad un buffer del kernel.
  2. Al socket-buffer viene passato solo il descrittore che contiene le informazioni sulla posizione e la lunghezza dei dati; il motore DMA consegna infine il riferimento ai dati direttamente al motore del protocollo di rete.

Prestazioni a confronto

Dopo aver eseguito il performance-test su un server Linux con kernel 2.6.22-14 e misurato il tempo di esecuzione in millisecondi sia con l’approccio tradizionale che con lo zero-copy, si è raggiunti i seguenti risultati:

File size Normal file transfer (ms) transferTo (ms)
7MB 156 45
21MB 337 128
63MB 843 387
98MB 1320 617
200MB 2124 1150
350MB 3631 1762
700MB 13498 4422
1GB 18399 8537

Tag: , , , , , , , , , , ,

Lascia un commento