Come rendere sempre “fallito” la lettura e la scrittura dello stesso file nella stessa pipeline?


9

Di 'che ho il seguente script:

#!/bin/bash
for i in $(seq 1000)
do
    cp /etc/passwd tmp
    cat tmp | head -1 | head -1 | head -1 > tmp  #this is the key line
    cat tmp
done

Sulla riga chiave, leggo e scrivo lo stesso file tmpche a volte non riesce.

(L'ho letto è a causa delle condizioni di gara perché i processi nella pipeline sono eseguiti in parallelo, il che non capisco perché - ognuno headdeve prendere i dati dal precedente, non è vero? Questa NON è la mia domanda principale, ma puoi anche rispondere.)

Quando eseguo lo script, genera circa 200 righe. Esiste un modo per forzare questo script a generare sempre 0 righe (quindi il reindirizzamento I / O su tmpviene sempre preparato per primo e quindi i dati vengono sempre distrutti)? Per essere chiari, intendo cambiare le impostazioni di sistema, non questo script.

Grazie per le tue idee

Risposte:


2

La risposta di Gilles spiega le condizioni di gara. Risponderò solo a questa parte:

Esiste un modo per forzare questo script a generare sempre 0 righe (quindi il reindirizzamento I / O su tmp viene sempre preparato per primo e quindi i dati vengono sempre distrutti)? Per essere chiari, intendo cambiare le impostazioni di sistema

IDK se esiste già uno strumento per questo, ma ho un'idea di come implementarlo. (Nota che questo non sarebbe sempre 0 righe, solo un utile tester che cattura facilmente razze semplici come questa e alcune gare più complicate. Vedi il commento di @Gilles .) Non garantirebbe che uno script sia sicuro , ma potrebbe essere uno strumento utile nei test, simile al test di un programma multi-thread su CPU diverse, incluse CPU non x86 ordinate debolmente come ARM.

Lo avresti eseguito come racechecker bash foo.sh

Utilizzare le stesse strutture di tracciamento / intercettazione delle chiamate di sistema strace -fe ltrace -futilizzare per collegarsi a ogni processo figlio. (Su Linux, questa è la stessa ptracechiamata di sistema utilizzata da GDB e altri debugger per impostare punti di interruzione, passaggio singolo e modificare la memoria / i registri di un altro processo.)

Strumento le opene openatsistema chiamate: quando un processo in esecuzione in questo strumento fa una una open(2)chiamata di sistema (o openat) con O_RDONLY, forse il sonno per 1/2 o 1 secondo. Lascia che le altre openchiamate di sistema (specialmente quelle incluse O_TRUNC) vengano eseguite senza indugio.

Ciò dovrebbe consentire allo sceneggiatore di vincere la gara in quasi tutte le condizioni di gara, a meno che anche il carico del sistema non fosse elevato, o fosse una condizione di gara complicata in cui il troncamento non avveniva fino a dopo qualche altra lettura. Quindi una variazione casuale di quali open()s (e forse read()s o scrive) sono in ritardo aumenterebbe la potenza di rilevamento di questo strumento, ma ovviamente senza test per un infinito lasso di tempo con un simulatore di ritardo che alla fine coprirà tutte le possibili situazioni in cui è possibile imbattersi nel mondo reale, non puoi essere sicuro che i tuoi script siano liberi dalle razze se non li leggi attentamente e dimostri che non lo sono.


Probabilmente ne avresti bisogno per inserire nella whitelist (non ritardare open) i file /usr/bine /usr/libquindi l'avvio del processo non dura per sempre. (Il collegamento dinamico di runtime deve contenere open()più file (guarda strace -eopen /bin/trueo /bin/lsqualche volta), anche se se la shell genitrice stessa sta eseguendo il troncamento, andrà bene. Ma sarà comunque buono per questo strumento non rendere gli script irragionevolmente lenti).

O forse autorizzare ogni file che il processo di chiamata non ha l'autorizzazione a troncare in primo luogo. cioè il processo di tracciamento può effettuare una access(2)chiamata di sistema prima di sospendere effettivamente il processo che voleva open()un file.


racecheckeresso stesso dovrebbe essere scritto in C, non in shell, ma potrebbe forse usare straceil codice come punto di partenza e potrebbe non richiedere molto lavoro per essere implementato.

Forse potresti ottenere la stessa funzionalità con un filesystem FUSE . Probabilmente esiste un esempio FUSE di un puro filesystem passthrough, quindi è possibile aggiungere controlli alla open()funzione in quello che lo fa dormire per le aperture di sola lettura ma lasciare che il troncamento avvenga immediatamente.


La tua idea per un controllore di gara non funziona davvero. Innanzitutto, c'è il problema che i timeout non sono affidabili: un giorno l'altro ragazzo impiegherà più tempo del previsto (è un problema classico con script di build o test, che sembrano funzionare per un po 'e poi fallire in modi difficili da eseguire il debug quando il carico di lavoro si espande e molte cose vengono eseguite in parallelo). Ma oltre a questo, a quale apertura aggiungerai un ritardo? Al fine di rilevare qualcosa di interessante, dovresti fare molte corse con diversi schemi di ritardo e confrontare i loro risultati.
Gilles 'SO- smetti di essere malvagio' il

@Gilles: Giusto, qualsiasi ritardo ragionevolmente breve non garantisce che il troncato vincerà la gara (su una macchina pesantemente come fai notare). L'idea qui è che usi questo per testare il tuo script alcune volte, non che usi racecheckersempre. E probabilmente vorrai che il tempo di sonno aperto per la lettura sia configurabile a beneficio delle persone su macchine molto caricate che vogliono impostarlo più in alto, come 10 secondi. O impostarla abbassare, come 0,1 secondi per una lunga o script inefficienti che i file riaprire un sacco .
Peter Cordes,

@Gilles: Ottima idea sui diversi modelli di ritardo, che potrebbero permetterti di catturare più gare rispetto alla semplice roba all'interno della stessa pipeline che "dovrebbe essere ovvia (una volta che sai come funzionano le shell)" come il caso dell'OP. Ma "che si apre?" qualsiasi aperto di sola lettura, con una whitelist o un altro modo per non ritardare l'avvio del processo.
Peter Cordes,

Immagino che stai pensando a gare più complesse con lavori in background che non si troncano fino a quando non viene completato qualche altro processo? Sì, potrebbe essere necessaria una variazione casuale per catturarla. O forse guardare l'albero dei processi e ritardare le "prime" letture di più, per cercare di invertire il solito ordinamento. Potresti rendere lo strumento sempre più complicato per simulare sempre più possibilità di riordino, ma a un certo punto devi ancora progettare correttamente i tuoi programmi se stai eseguendo il multitasking. I test automatizzati potrebbero essere utili per script più semplici in cui i possibili problemi sono più limitati.
Peter Cordes,

È abbastanza simile al test del codice multi-thread, in particolare degli algoritmi lockless: il ragionamento logico sul perché è corretto è molto importante, così come i test, perché non puoi contare sul test su un particolare set di macchine per produrre tutti i riordini che potrebbero essere un problema se non hai chiuso tutte le scappatoie. Ma proprio come testare su un'architettura debolmente ordinata come ARM o PowerPC è una buona idea in pratica, testare uno script in un sistema che ritardi artificialmente le cose può esporre alcune razze, quindi è meglio di niente. Puoi sempre introdurre bug che non cattureranno!
Peter Cordes,

18

Perché c'è una condizione di gara

I due lati di un tubo vengono eseguiti in parallelo, non uno dopo l'altro. C'è un modo molto semplice per dimostrarlo: corri

time sleep 1 | sleep 1

Questo richiede un secondo, non due.

La shell avvia due processi figlio e attende il completamento di entrambi. Questi due processi vengono eseguiti in parallelo: l'unico motivo per cui uno di essi si sincronizzerebbe con l'altro è quando deve attendere l'altro. Il punto più comune di sincronizzazione è quando il lato destro si blocca in attesa della lettura dei dati sull'input standard e si sblocca quando il lato sinistro scrive più dati. Il contrario può anche accadere, quando il lato destro è lento nella lettura dei dati e il lato sinistro si blocca nella sua operazione di scrittura fino a quando il lato destro non legge più dati (c'è un buffer nel pipe stesso, gestito dal kernel, ma ha una dimensione massima ridotta).

Per osservare un punto di sincronizzazione, osservare i seguenti comandi ( sh -xstampa ogni comando mentre lo esegue):

time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'

Gioca con le variazioni fino a quando non ti senti a tuo agio con ciò che osservi.

Dato il comando composto

cat tmp | head -1 > tmp

il processo a sinistra esegue le seguenti operazioni (ho elencato solo i passaggi rilevanti per la mia spiegazione):

  1. Eseguire il programma esterno catcon l'argomento tmp.
  2. Aperto tmpper la lettura.
  3. Sebbene non abbia raggiunto la fine del file, leggi un blocco dal file e scrivilo nell'output standard.

Il processo a destra procede come segue:

  1. Reindirizzare l'output standard su tmp, troncando il file nel processo.
  2. Eseguire il programma esterno headcon l'argomento -1.
  3. Leggere una riga dallo standard input e scriverlo nello standard output.

L'unico punto di sincronizzazione è che right-3 attende che left-3 abbia elaborato una riga completa. Non c'è sincronizzazione tra left-2 e right-1, quindi possono avvenire in entrambi gli ordini. L'ordine in cui si verificano non è prevedibile: dipende dall'architettura della CPU, dalla shell, dal kernel, da quali core si pianificano i processi, da ciò che interrompe la CPU in quel momento, ecc.

Come cambiare il comportamento

Non è possibile modificare il comportamento modificando un'impostazione di sistema. Il computer fa quello che gli dici di fare. Gli hai detto di troncare tmpe leggere tmpin parallelo, quindi fa le due cose in parallelo.

Ok, c'è un'impostazione di sistema che potresti cambiare: potresti sostituirla /bin/bashcon un altro programma che non è bash. Spero che sia ovvio che questa non è una buona idea.

Se si desidera che il troncamento avvenga prima del lato sinistro del tubo, è necessario inserirlo all'esterno della tubazione, ad esempio:

{ cat tmp | head -1; } >tmp

o

( exec >tmp; cat tmp | head -1 )

Non ho idea del perché tu voglia questo però. Qual è il punto di lettura di un file che sai essere vuoto?

Al contrario, se si desidera che il reindirizzamento dell'output (incluso il troncamento) si verifichi al cattermine della lettura, è necessario bufferizzare completamente i dati in memoria, ad es.

line=$(cat tmp | head -1)
printf %s "$line" >tmp

oppure scrivi in ​​un altro file e poi spostalo in posizione. Questo è di solito il modo più efficace per eseguire operazioni negli script e presenta il vantaggio che il file viene scritto per intero prima che sia visibile attraverso il nome originale.

cat tmp | head -1 >new && mv new tmp

La collezione moreutils include un programma che fa proprio questo, chiamato sponge.

cat tmp | head -1 | sponge tmp

Come rilevare il problema automaticamente

Se il tuo obiettivo era prendere script scritti male e capire automaticamente dove si rompono, allora scusa, la vita non è così semplice. L'analisi di runtime non trova in modo affidabile il problema perché a volte cattermina la lettura prima che si verifichi il troncamento. L'analisi statica può in linea di principio farlo; l'esempio semplificato nella tua domanda viene colto da Shellcheck , ma potrebbe non rilevare un problema simile in uno script più complesso.


Questo era il mio obiettivo, per determinare se la sceneggiatura fosse ben scritta o meno. Se lo script avrebbe potuto distruggere i dati in questo modo, volevo solo che li distruggesse ogni volta. Non è bello sapere che questo è quasi impossibile. Grazie a te, ora so qual è il problema e cercherò di trovare una soluzione.
Karlosss,

@karlosss: Hmm, mi chiedo se potresti usare la stessa traccia di tracciamento / intercettazione di chiamate di sistema di strace(es. Linux ptrace) per far opendormire tutte le chiamate di sistema per la lettura (in tutti i processi figlio) per mezzo secondo, quindi quando corri con un troncamento, il troncamento vincerà quasi sempre.
Peter Cordes,

@PeterCordes Sono un novizio di questo, se riesci a gestire un modo per raggiungere questo obiettivo e scriverlo come risposta, lo accetterò.
Karlosss,

@PeterCordes Non puoi garantire che il troncamento vincerà con un ritardo. Funzionerà la maggior parte del tempo, ma a volte su una macchina pesantemente caricata la tua sceneggiatura fallirà in modi più o meno misteriosi.
Gilles 'SO- smetti di essere malvagio' il

@Gilles: discutiamo di questo sotto la mia risposta.
Peter Cordes,
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.