In che modo le pipeline limitano l'utilizzo della memoria?


36

Brian Kernighan spiega in questo video la prima attrazione dei Bell Labs per i linguaggi / programmi di piccole dimensioni basati su limiti di memoria

Una grande macchina sarebbe di 64 k-byte - K, non M o G - e ciò significava che ogni singolo programma non poteva essere molto grande, e quindi c'era una naturale tendenza a scrivere piccoli programmi, e quindi il meccanismo di pipe, fondamentalmente input reindirizzamento output, ha reso possibile collegare un programma a un altro.

Ma non capisco come ciò possa limitare l'utilizzo della memoria considerando il fatto che i dati devono essere memorizzati nella RAM per trasmettere tra i programmi.

Da Wikipedia :

Nella maggior parte dei sistemi simili a Unix, tutti i processi di una pipeline vengono avviati contemporaneamente [enfasi mia], con i loro flussi opportunamente connessi e gestiti dallo scheduler insieme a tutti gli altri processi in esecuzione sulla macchina. Un aspetto importante di questo, che distingue le pipe Unix dalle altre implementazioni di pipe, è il concetto di buffering: ad esempio un programma di invio può produrre 5000 byte al secondo e un programma di ricezione può essere in grado di accettare solo 100 byte al secondo, ma no i dati vengono persi. Al contrario, l'output del programma di invio viene mantenuto nel buffer. Quando il programma ricevente è pronto per leggere i dati, il programma successivo nella pipeline legge dal buffer. In Linux, la dimensione del buffer è 65536 byte (64 KB). Un filtro di terze parti open source chiamato bfr è disponibile per fornire buffer più grandi, se necessario.

Questo mi confonde ancora di più, in quanto vanifica completamente lo scopo dei piccoli programmi (anche se sarebbero modulari fino a una certa scala).

L'unica cosa che posso pensare come una soluzione alla mia prima domanda (le limitazioni della memoria sono problematiche a seconda della dimensione dei dati) sarebbe che i grandi set di dati non erano semplicemente calcolati all'epoca e il vero problema che le pipeline del problema dovevano risolvere era il quantità di memoria richiesta dai programmi stessi. Ma dato il testo in grassetto nella citazione di Wikipedia, anche questo mi confonde: poiché un programma non è implementato alla volta.

Tutto ciò avrebbe molto senso se venissero utilizzati file temporanei, ma ho capito che i pipe non scrivono sul disco (a meno che non venga usato lo swap).

Esempio:

sed 'simplesubstitution' file | sort | uniq > file2

È chiaro per me che sedsta leggendo il file e sputandolo riga per riga. Ma sort, come afferma BK nel video collegato, è un punto fermo, quindi tutti i dati devono essere letti in memoria (o lo fa?), Quindi vengono passati a uniq, che (secondo la mia mente) sarebbe uno -programma alla volta. Ma tra la prima e la seconda pipe, tutti i dati devono essere in memoria, no?


1
unless swap is usedlo swap viene sempre usato quando non c'è abbastanza RAM
edc65

Risposte:


44

I dati non devono essere archiviati nella RAM. Le pipe bloccano i loro scrittori se i lettori non sono presenti o non riescono a tenere il passo; sotto Linux (e la maggior parte delle altre implementazioni, immagino) c'è un po 'di buffering ma non è necessario. Come menzionato da mtraceur e JdeBP (vedere la risposta di quest'ultimo), le prime versioni di pipe bufferizzate Unix su disco, ed è così che hanno contribuito a limitare l'utilizzo della memoria: una pipeline di elaborazione potrebbe essere suddivisa in piccoli programmi, ognuno dei quali avrebbe elaborato alcuni dati, entro i limiti dei buffer del disco. Piccoli programmi occupano meno memoria e l'uso di pipe significa che l'elaborazione potrebbe essere serializzata: il primo programma verrà eseguito, riempirà il buffer di output, verrà sospeso, quindi verrà pianificato il secondo programma, elaborerà il buffer, ecc. I sistemi moderni sono ordini di grandezza maggiore dei primi sistemi Unix, e può far funzionare molte tubazioni in parallelo; ma per enormi quantità di dati vedresti comunque un effetto simile (e varianti di questo tipo di tecnica sono utilizzate per l'elaborazione dei "big data").

Nel tuo esempio,

sed 'simplesubstitution' file | sort | uniq > file2

sedlegge i dati filese necessario, quindi li scrive finché sortè pronto per leggerli; se sortnon è pronto, la scrittura si blocca. Alla fine i dati vivono effettivamente nella memoria, ma è specifico per sort, ed sortè pronto a gestire qualsiasi problema (userà i file temporanei se la quantità di dati da ordinare è troppo grande).

È possibile visualizzare il comportamento di blocco eseguendo

strace seq 1000000 -1 1 | (sleep 120; sort -n)

Ciò produce una discreta quantità di dati e li dirige verso un processo che non è pronto a leggere nulla per i primi due minuti. Vedrai una serie di writeoperazioni, ma molto rapidamente seqsi fermerà e attenderà che trascorrano i due minuti, bloccati dal kernel (la writechiamata di sistema attende).


13
Questa risposta potrebbe trarre vantaggio dalla spiegazione aggiuntiva del perché la suddivisione dei programmi in molti piccoli risparmia l'uso della memoria: un programma doveva essere in grado di adattarsi in memoria per l'esecuzione, ma solo il programma attualmente in esecuzione . Ogni altro programma veniva scambiato su disco nei primi Unix, con un solo programma scambiato addirittura nella RAM effettiva alla volta. Quindi la CPU eseguiva un programma, che scriveva su una pipe (che all'epoca era su disco ), scambiava quel programma e scambiava quello che leggeva dalla pipe. Modo elegante per trasformare una catena di montaggio logicamente parallela in esecuzione serializzata incrementale.
mtraceur,

6
@malan: più processi possono essere avviati e possono essere in uno stato eseguibile contemporaneamente. Ma al massimo un processo può essere eseguito su ciascuna CPU fisica in un dato momento ed è compito dello scheduler del kernel assegnare allocazioni di "tempo" della CPU a ciascun processo eseguibile. Nei sistemi moderni, un processo che è eseguibile ma che attualmente non è programmato, un timeslice della CPU di solito rimane residente in memoria mentre è in attesa del suo prossimo slice, ma al kernel è permesso di eseguire il paging della memoria di qualsiasi processo su disco e di nuovo in memoria come trova conveniente. (Handwaving alcuni dettagli qui.)
Daniel Pryden il

5
I processi su entrambi i lati di una pipe possono comportarsi efficacemente come co-routine: una parte scrive fino a riempire il buffer e i blocchi di scrittura, a quel punto il processo non può fare nulla con il resto della sua finestra temporale e va in un Modalità di attesa IO. Quindi il sistema operativo fornisce il resto del timeslice (o un altro timeslice in arrivo) sul lato di lettura, che legge fino a quando non è rimasto nulla nel buffer e nei blocchi di lettura successivi, a quel punto il processo di lettura non può fare nulla con il resto di è multiproprietà e cede al sistema operativo. I dati passano attraverso la pipe di un buffer alla volta.
Daniel Pryden,

6
@malan I programmi vengono avviati "allo stesso tempo" concettualmente su tutti i sistemi Unix, solo sui moderni sistemi multiprocessore con abbastanza RAM per tenerli, il che significa che sono letteralmente tutti tenuti nella RAM allo stesso tempo, mentre su un sistema che può tenerli tutti nella RAM allo stesso tempo, alcuni vengono scambiati su disco. Si noti inoltre che "memoria" in molti contesti significa memoria virtuale che è la somma sia dello spazio RAM che dello spazio di swap su disco. Wikipedia si sta concentrando sul concetto piuttosto che sui dettagli di implementazione, soprattutto perché la vecchia Unix ha fatto le cose ora è meno rilevante.
mtraceur,

2
@malan Inoltre, la contraddizione che stai vedendo deriva dai due diversi significati di "memoria" (RAM vs RAM + swap). Stavo parlando solo di RAM hardware, e in quel contesto solo il codice attualmente in esecuzione dalla CPU deve adattarsi alla RAM (che era ciò che stava influenzando le decisioni di cui parla Kernighan), mentre nel contesto di tutti i programmi eseguiti logicamente dal sistema operativo in un determinato momento (a livello astratto fornito al di sopra del time slicing) un programma deve solo adattarsi all'intera memoria virtuale disponibile per il sistema operativo, che include lo spazio di scambio sul disco.
mtraceur,

34

Ma non capisco come ciò possa limitare l'utilizzo della memoria considerando il fatto che i dati devono essere memorizzati nella RAM per trasmettere tra i programmi.

Questo è il tuo errore fondamentale. Le prime versioni di Unix non contenevano i dati di pipe nella RAM. Li hanno memorizzati su disco. I tubi avevano i-nodi; su un dispositivo disco che è stato indicato come dispositivo pipe . L'amministratore di sistema ha eseguito un programma denominato /etc/configper specificare (tra le altre cose) quale volume su quale disco era il dispositivo pipe, quale volume era il dispositivo root e quale dispositivo dump .

La quantità di dati in sospeso è stata limitata dal fatto che solo i blocchi diretti dell'i-node su disco sono stati utilizzati per l'archiviazione. Questo meccanismo ha reso il codice più semplice, poiché per la lettura da una pipe è stato impiegato lo stesso algoritmo utilizzato per la lettura di un file normale, con alcune modifiche dovute al fatto che le pipe non sono ricercabili e il buffer è circolare.

Questo meccanismo fu sostituito da altri dalla metà alla fine degli anni '80. SCO XENIX ha ottenuto il "Sistema di tubazioni ad alte prestazioni", che ha sostituito i-nodi con buffer in-core. 4BSD trasformò pipe senza nome in socket. AT&T ha reimplementato i tubi utilizzando il meccanismo STREAMS.

E, naturalmente, il sortprogramma ha eseguito un tipo interno limitato di blocchi di input da 32 KiB (o qualsiasi quantità minore di memoria che potrebbe allocare se 32 KiB non fosse disponibile), scrivendo i risultati ordinati in stmX??file intermedi in /usr/tmp/cui poi si uniscono esternamente ordinati per fornire il finale produzione.

Ulteriori letture

  • Steve D. Pate (1996). "Comunicazione tra processi". Internals UNIX: un approccio pratico . Addison-Wesley. ISBN 9780201877212.
  • Maurice J. Bach (1987). "Chiamate di sistema per il file system". Il design del sistema operativo Unix . Prentice-Hall. ISBN 0132017571.
  • Steven V. Earhart (1986). " config(1M)". Manuale del programmatore Unix: 3. Strutture di amministrazione del sistema . Holt, Rinehart e Winston. ISBN 0030093139. pagg. 23-28.

1

Hai parzialmente ragione, ma solo per caso .

Nel tuo esempio, tutti i dati devono essere stati effettivamente letti "tra" le pipe, ma non è necessario che siano residenti in memoria (compresa la memoria virtuale). Le consuete implementazioni di sortpossono ordinare i set di dati che non si adattano alla RAM eseguendo i tipi parziali in tempfile e l'unione. Tuttavia, è un dato di fatto che non è possibile emettere una sequenza ordinata prima di aver letto ogni singolo elemento. È abbastanza ovvio. Quindi sì, sortpuò solo iniziare a inviare alla seconda pipe dopo aver letto (e fatto qualunque cosa, possibilmente ordinando parzialmente i tempfile) tutto dal primo. Ma non deve necessariamente conservare tutto nella RAM.

Tuttavia, questo non ha nulla a che vedere con il funzionamento delle pipe. I tubi possono essere nominati (tradizionalmente erano tutti denominati), il che significa niente di più e niente di meno che avere una posizione nel file system, come i file. Ed è proprio quello che una volta erano i pipe, i file (con scritture coalizzate tanto quanto la disponibilità di memoria fisica consentirebbe, come ottimizzazione).

Oggi le pipe sono un piccolo buffer del kernel di dimensioni finite in cui vengono copiati i dati, almeno è quello che succede concettualmente . Se il kernel può aiutarlo, le copie sono eluse giocando i trucchi della VM (ad esempio, il piping da un file di solito rende la stessa pagina disponibile per l'altro processo da leggere, quindi è finalmente solo un'operazione di lettura, non due copie, e no è comunque necessaria memoria aggiuntiva rispetto a quella già utilizzata dalla cache del buffer. In alcune situazioni è possibile ottenere anche una copia zero al 100% o qualcosa di molto vicino.

Se le pipe sono piccole e di dimensioni finite, come può funzionare per qualsiasi quantità sconosciuta (possibilmente grande) di dati? È semplice: quando non si adatta più nulla, la scrittura si blocca fino a quando non c'è più spazio.

La filosofia di molti semplici programmi era molto utile una volta in cui la memoria era molto scarsa. Perché, beh, potresti fare un lavoro a piccoli passi, uno alla volta. Al giorno d'oggi, a parte un po 'di flessibilità extra, i vantaggi non sono più così grandi.
Tuttavia, le pipe sono implementate in modo molto efficiente (dovevano esserlo!), Quindi non ci sono svantaggi, ed è una cosa consolidata che funziona bene e alla quale le persone sono abituate, quindi non è necessario cambiare il paradigma.


Quando dici 'named pipe' (JdeBP sembra dire che c'era un 'dispositivo pipe'), significa che c'era un limite al numero di pipe che potevano essere usate in un dato momento (cioè, c'era un limite a quante volte potresti usare |in un comando)?
Malan,

2
Non ho mai visto un tale limite e non credo che in teoria ce ne sia mai stato uno. In pratica, tutto ciò che ha un nome file ha bisogno di un inode e il numero di inode è, ovviamente, finito. Come il numero di pagine fisiche su un sistema, se non altro. I sistemi moderni garantiscono scritture atomiche 4k, quindi ogni pipe deve possedere almeno una pagina 4k completa, il che pone un limite al numero di pipe che puoi avere. Ma considera di avere un paio di gigabyte di RAM ... praticamente, questo è un limite che non incontrerai mai. Prova a digitare qualche milione di tubi su un terminale ... :)
Damon,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.