Invia dati a più socket utilizzando pipe, tee () e splice ()

Sto duplicando una pipe “master” con tee () per scrivere su più socket usando splice (). Naturalmente questi tubi si svuotano a velocità diverse a seconda di quanto posso giuntare () alle prese di destinazione. Quindi quando passo ad aggiungere dati alla pipe “master” e poi tee () di nuovo, potrei avere una situazione in cui posso scrivere 64 KB nella pipe ma solo tee 4KB in uno dei pipe “slave”. Suppongo quindi che se spago () tutta la pipe “master” alla presa, non sarò mai in grado di tee () i restanti 60KB a quella pipe slave. È vero? Immagino di poter tenere traccia di un tee_offset (a partire da 0) che ho impostato all’inizio dei dati “non sottoposti a” e quindi non lo ho corretto. Quindi in questo caso imposterei tee_offset su 4096 e non unire più di quello finché non sarò in grado di collegarlo a tutti gli altri pipe. Sono sulla strada giusta qui? Qualche suggerimento / avvertimento per me?

Se ho capito bene, hai una fonte di dati in tempo reale che vuoi colbind a più socket. Hai una sola pipe “sorgente” collegata a qualsiasi cosa stia producendo i tuoi dati, e hai una “destinazione” pipe per ogni socket su cui desideri inviare i dati. Quello che stai facendo è usare tee() per copiare i dati dal pipe sorgente a ciascuna delle pipe di destinazione e splice() per copiarlo dalle pipe di destinazione ai socket stessi.

Il problema fondamentale che stai per colpire qui è se uno dei socket semplicemente non riesce a tenere il passo – se stai producendo dati più velocemente di quanto tu possa inviarlo, allora avrai un problema. Questo non è legato al tuo uso delle pipe, è solo una questione fondamentale. Quindi, ti consigliamo di scegliere una strategia per far fronte in questo caso: ti suggerisco di gestirlo anche se non ti aspetti che sia comune, perché spesso queste cose ti vengono in mente. Le tue scelte di base consistono nel chiudere il socket incriminato o saltare i dati fino a quando non viene cancellato il suo buffer di output: quest’ultima opzione potrebbe essere più adatta per lo streaming audio / video, ad esempio.

Il problema relativo al tuo uso delle pipe, tuttavia, è che su Linux la dimensione del buffer di una pipe è alquanto inflessibile. Il valore predefinito è 64 KB da Linux 2.6.11 (la chiamata tee() stata aggiunta in 2.6.17) – consultare la manpage pipe . Dal 2.6.35 questo valore può essere modificato tramite l’opzione F_SETPIPE_SZ a fcntl() (consultare la manpage fcntl ) fino al limite specificato da /proc/sys/fs/pipe-size-max , ma il buffering è ancora più complicato per cambiare on-demand rispetto a uno schema allocato dynamicmente nello spazio utente. Ciò significa che la tua capacità di gestire socket lenti sarà alquanto limitata, indipendentemente dal fatto che sia accettabile in base alla frequenza con cui prevedi di ricevere ed essere in grado di inviare dati.

Supponendo che questa strategia di buffering sia accettabile, hai ragione nel ritenere che dovrai tenere traccia della quantità di dati che ogni pipe di destinazione ha consumato dalla sorgente, ed è sicuro solo scartare i dati che tutte le pipe di destinazione hanno consumato. Ciò è alquanto complicato dal fatto che tee() non ha il concetto di offset: è ansible copiare solo dall’inizio della pipe. La conseguenza di ciò è che puoi copiare solo alla velocità del socket più lento, dal momento che non puoi usare tee() per copiare su una pipe di destinazione finché alcuni dati non sono stati consumati dalla sorgente, e non puoi fallo fino a quando tutte le prese hanno i dati che stai per consumare.

Il modo in cui gestisci ciò dipende dall’importanza dei tuoi dati. Se hai davvero bisogno della velocità di tee() e splice() , e sei sicuro che un socket lento sarà un evento estremamente raro, potresti fare qualcosa di simile (ho pensato che tu stia usando IO non bloccante e un singolo thread, ma qualcosa di simile funzionerebbe anche con più thread):

  1. Assicurati che tutti i pipe non siano bloccanti (usa fcntl(d, F_SETFL, O_NONBLOCK) per rendere ogni descrittore di file non bloccante).
  2. Inizializza una variabile read_counter per ogni pipe di destinazione a zero.
  3. Usa qualcosa come epoll () per aspettare che ci sia qualcosa nella pipe sorgente.
  4. Loop su tutte le pipe di destinazione dove read_counter è zero, chiamando tee() per trasferire i dati su ognuno. Assicurati di passare SPLICE_F_NONBLOCK nelle bandiere.
  5. Incrementa read_counter per ogni pipe di destinazione per l’importo trasferito da tee() . Tieni traccia del valore risultante più basso.
  6. Trova il valore risultante più basso di read_counter – se questo non è zero, allora scarta quella quantità di dati dalla pipe sorgente (usando una chiamata splice() con una destinazione aperta su /dev/null , per esempio). Dopo aver scartato i dati, sottrarre la quantità scartata da read_counter su tutti i tubi (poiché questo era il valore più basso, allora questo non può comportare che nessuno di essi diventi negativo).
  7. Ripeti dal passaggio 3 .

Nota: una cosa che mi ha fatto inciampare in passato è che SPLICE_F_NONBLOCK influisce sul fatto che le operazioni tee() e splice() sulle pipe non siano bloccanti, e il O_NONBLOCK impostato con fnctl() influenzi le interazioni con altre chiamate ( ad esempio read() e write() ) non sono bloccanti. Se vuoi che tutto sia non-bloccante, imposta entrambi. Ricordati anche di rendere i tuoi socket non bloccanti o le chiamate splice() per trasferire dati a loro potrebbero bloccare (a meno che sia quello che vuoi, se stai usando un approccio thread).

Come potete vedere, questa strategia ha un grosso problema: non appena un socket si blocca, tutto si ferma: il tubo di destinazione per quel socket si riempirà, e quindi il tubo sorgente diventerà stagnante. Quindi, se raggiungi la fase in cui tee() restituisce EAGAIN al punto 4, dovrai chiudere il socket o almeno “disconnetterlo” (cioè toglierlo dal ciclo) in modo da non scrivere qualsiasi altra cosa fino a quando il suo buffer di output è vuoto. La scelta dipende dal fatto che il stream di dati possa essere ripristinato dal fatto che alcuni di essi sono saltati.

Se vuoi gestire la latenza della rete in modo più agevole, dovrai eseguire più operazioni di buffering e questo coinvolgerà entrambi i buffer dello spazio utente (il che nega piuttosto i vantaggi di tee() e splice() ) o forse buffer basato su disco. Il buffering basato su disco sarà quasi certamente molto più lento del buffering spazio utente, e quindi non appropriato visto che presumibilmente si desidera molta velocità da quando si è scelto tee() e splice() in primo luogo, ma cito per completezza.

Una cosa che vale la pena notare se si finisce per inserire dati dallo spazio utente in qualsiasi punto è la chiamata vmsplice() che può eseguire “raccogliere l’output” dallo spazio utente in una pipe, in modo simile alla chiamata writev() . Questo potrebbe essere utile se stai facendo abbastanza buffer che hai diviso i tuoi dati tra più buffer allocati diversi (ad esempio se stai usando un approccio di allocatore di pool).

Infine, si potrebbe immaginare di scambiare socket tra lo schema “veloce” di usare tee() e splice() e, se non riescono a tenere il passo, spostarli su un buffer dello spazio utente più lento. Questo complicherà la tua implementazione, ma se gestisci un numero elevato di connessioni e solo una piccola parte di esse è lenta, stai comunque riducendo la quantità di copia allo spazio utente che è in qualche modo coinvolta. Tuttavia, questo sarebbe solo sempre una misura a breve termine per far fronte a problemi di rete transienti – come ho detto in origine, hai un problema fondamentale se i tuoi socket sono più lenti della tua sorgente. Alla fine avresti raggiunto qualche limite di buffering e hai bisogno di saltare i dati o chiudere le connessioni.

Nel complesso, considererei attentamente il motivo per cui avete bisogno della velocità di tee() e splice() e se, per il vostro caso d’uso, semplicemente il buffering dello spazio utente in memoria o su disco sarebbe più appropriato. Se sei sicuro che le velocità saranno sempre alte, e il buffering limitato è accettabile, l’approccio descritto sopra dovrebbe funzionare.

Inoltre, una cosa che dovrei menzionare è che questo renderà il tuo codice estremamente specifico per Linux – non sono a conoscenza che queste chiamate siano supportate in altre varianti di Unix. La chiamata a sendfile() è più limitata di splice() , ma potrebbe essere piuttosto più portabile. Se vuoi davvero che le cose siano portabili, attenersi al buffering dello spazio utente.

Fammi sapere se c’è qualcosa su cui ti piacerebbe avere maggiori dettagli.