Modifica binaria durante l'esecuzione


10

Mi capita spesso di imbattermi in una situazione di sviluppo, dove sto eseguendo un file binario, dico a.outin background mentre fa un lungo lavoro. Mentre lo faccio, apporto modifiche al codice C che ha prodotto a.oute compilato di a.outnuovo. Finora non ho avuto problemi con questo. Il processo in esecuzione a.outcontinua normalmente, non si arresta mai in modo anomalo ed esegue sempre il vecchio codice da cui è stato originariamente avviato.

Tuttavia, diciamo che a.outera un file enorme, forse paragonabile alla dimensione della RAM. Cosa succederebbe in questo caso? E dire che è collegato a un file oggetto condiviso libblas.so, cosa succede se ho modificato libblas.sodurante il runtime? Cosa succederebbe?

La mia domanda principale è: il sistema operativo garantisce che quando eseguo a.out, il codice originale verrà sempre eseguito normalmente, come per il binario originale , indipendentemente dalle dimensioni del .sofile binario o dei file a cui si collega, anche quando questi .oe i .sofile vengono modificati durante tempo di esecuzione?

So che ci sono queste domande che affrontano problemi simili: /programming/8506865/when-a-binary-file-runs-does-it-copy-its-entire-binary-data-into-memory -at-once Cosa succede se si modifica uno script durante l'esecuzione? Come è possibile eseguire un aggiornamento live mentre un programma è in esecuzione?

Il che mi ha aiutato a capire qualcosa in più su questo, ma non penso che stiano chiedendo esattamente cosa voglio, che è una regola generale per le conseguenze della modifica di un binario durante l'esecuzione


Per me, le domande che hai collegato (in particolare Stack Overflow) forniscono già un aiuto significativo nella comprensione di queste conseguenze (o della loro assenza). Poiché il kernel carica il programma in aree / segmenti di testo di memoria , non dovrebbe essere influenzato dalle modifiche apportate tramite il sottosistema di file.
John WH Smith

@JohnWHSmith Su Stackoverflow, la risposta principale dice if they are read-only copies of something already on disc (like an executable, or a shared object file), they just get de-allocated and are reloaded from their source, quindi ho avuto l'impressione che se il tuo binario è enorme, quindi se una parte del tuo binario esce dalla RAM, ma è quindi necessaria di nuovo viene "ricaricata dalla sorgente" - quindi qualsiasi modifica in il .(s)ofile verrà riflesso durante l'esecuzione. Ma ovviamente potrei aver frainteso - motivo per cui sto ponendo questa domanda più specifica
texasflood

@JohnWHSmith Anche la seconda risposta dice No, it only loads the necessary pages into memory. This is demand paging.Quindi avevo l'impressione che ciò che chiedevo non potesse essere garantito.
texasflood

Risposte:


11

Mentre la domanda Stack Overflow all'inizio sembrava essere abbastanza, capisco, dai tuoi commenti, perché potresti ancora avere dubbi su questo. Per me, questo è esattamente il tipo di situazione critica coinvolta quando i due sottosistemi UNIX (processi e file) comunicano.

Come forse saprai, i sistemi UNIX sono generalmente divisi in due sottosistemi: il sottosistema di file e il sottosistema di processo. Ora, a meno che non sia diversamente indicato tramite una chiamata di sistema, il kernel non dovrebbe avere questi due sottosistemi che interagiscono tra loro. Esiste tuttavia un'eccezione: il caricamento di un file eseguibile nelle aree di testo di un processo . Naturalmente, si potrebbe sostenere che questa operazione è anche innescata da una chiamata di sistema ( execve), ma questo è generalmente noto per essere l' unico caso in cui il sottosistema di processo fa una richiesta implicita al sottosistema di file.

Poiché il sottosistema di processo naturalmente non ha modo di gestire i file (altrimenti non avrebbe senso dividere il tutto in due), deve usare qualunque cosa il sottosistema di file fornisca per accedere ai file. Ciò significa anche che il sottosistema di processo è sottoposto a qualsiasi misura il sottosistema di file prende in merito all'edizione / eliminazione dei file. Su questo punto, consiglierei di leggere la risposta di Gilles a questa domanda di U&L . Il resto della mia risposta si basa su questo più generale di Gilles.

La prima cosa da notare è che internamente i file sono accessibili solo tramite inode . Se al kernel viene assegnato un percorso, il suo primo passo sarà quello di tradurlo in un inode da utilizzare per tutte le altre operazioni. Quando un processo carica un eseguibile in memoria, lo fa attraverso il suo inode, che è stato fornito dal sottosistema di file dopo la traduzione di un percorso. Gli Inodi possono essere associati a più percorsi (collegamenti) e i programmi possono eliminare solo collegamenti. Per eliminare un file e il suo inode, userland deve rimuovere tutti i collegamenti esistenti a tale inode e assicurarsi che sia completamente inutilizzato. Quando queste condizioni sono soddisfatte, il kernel cancellerà automaticamente il file dal disco.

Se dai un'occhiata alla parte sostituibile degli eseguibili della risposta di Gilles, vedrai che a seconda di come modifichi / elimini il file, il kernel reagirà / si adatterà in modo diverso, sempre attraverso un meccanismo implementato all'interno del sottosistema di file.

  • Se provi la strategia 1 ( apri / tronca a zero / scrivi o apri / scrivi / tronca a nuove dimensioni ), vedrai che il kernel non si preoccuperà di gestire la tua richiesta. Verrà visualizzato un errore 26: File di testo occupato ( ETXTBSY). Nessuna conseguenza.
  • Se provi la seconda strategia, il primo passo è eliminare il tuo eseguibile. Tuttavia, poiché viene utilizzato da un processo, il sottosistema di file avvierà e impedirà che il file (e il suo inode) vengano effettivamente eliminati dal disco. Da questo punto, l'unico modo per accedere al contenuto del vecchio file è farlo attraverso il suo inode, che è ciò che fa il sottosistema di processo ogni volta che è necessario caricare nuovi dati in sezioni di testo (internamente, non ha senso usare percorsi, tranne quando li traduce in inode). Anche se hai scollegatoil file (rimosso tutti i suoi percorsi), il processo può ancora usarlo come se non avessi fatto nulla. La creazione di un nuovo file con il vecchio percorso non cambia nulla: al nuovo file verrà assegnato un inode completamente nuovo, di cui il processo in esecuzione non è a conoscenza.

Le strategie 2 e 3 sono sicure anche per gli eseguibili: sebbene i file eseguibili (e le librerie caricate dinamicamente) non siano file aperti nel senso di avere un descrittore di file, si comportano in modo molto simile. Finché alcuni programmi eseguono il codice, il file rimane sul disco anche senza una voce di directory.

  • La strategia tre è abbastanza simile poiché l' mvoperazione è atomica. Ciò richiederà probabilmente l'uso della renamechiamata di sistema e poiché i processi non possono essere interrotti mentre si è in modalità kernel, nulla può interferire con questa operazione fino al completamento (corretto o meno). Ancora una volta, non c'è alterazione dell'inode del vecchio file: ne viene creato uno nuovo e i processi già in esecuzione non ne avranno conoscenza, anche se è stato associato a uno dei collegamenti del vecchio inode.

Con la strategia 3, la fase di spostamento del nuovo file sul nome esistente rimuove la voce della directory che porta al vecchio contenuto e crea una voce della directory che porta al nuovo contenuto. Questo viene fatto in un'unica operazione atomica, quindi questa strategia ha un grande vantaggio: se un processo apre il file in qualsiasi momento, vedrà il vecchio contenuto o il nuovo contenuto - non c'è rischio di ottenere contenuti misti o il file no esistente.

Ricompilazione di un file : quando si utilizza gcc(e il comportamento è probabilmente simile per molti altri compilatori), si utilizza la strategia 2. È possibile vederlo eseguendo uno stracedei processi del compilatore:

stat("a.out", {st_mode=S_IFREG|0750, st_size=8511, ...}) = 0
unlink("a.out") = 0
open("a.out", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
chmod("a.out", 0750) = 0
  • Il compilatore rileva che il file esiste già tramite le chiamate di sistema state lstat.
  • Il file non è collegato . Qui, sebbene non sia più accessibile tramite il nome a.out, il suo inode e il suo contenuto rimangono sul disco, fintanto che vengono utilizzati da processi già in esecuzione.
  • Un nuovo file viene creato e reso eseguibile con il nome a.out. Questo è un inode nuovo di zecca e nuovi contenuti, ai quali non sono interessati i processi già in esecuzione.

Ora, quando si tratta di librerie condivise, si applicherà lo stesso comportamento. Finché un oggetto libreria viene utilizzato da un processo, non verrà eliminato dal disco, indipendentemente da come si cambiano i suoi collegamenti. Ogni volta che qualcosa deve essere caricato in memoria, il kernel lo farà attraverso l'inode del file e quindi ignorerà le modifiche apportate ai suoi collegamenti (come associarli a nuovi file).


Risposta fantastica e dettagliata. Questo spiega la mia confusione. Quindi ho ragione nel supporre che, poiché l'inode è ancora disponibile, i dati del file binario originale sono ancora sul disco e quindi usare dfper calcolare il numero di byte liberi sul disco è sbagliato in quanto non accetta inode che tutti i collegamenti al filesystem rimossi sono stati presi in considerazione? Quindi dovrei usare df -i? (Questa è solo una curiosità tecnica, non ho davvero bisogno di conoscere l'esatto utilizzo del disco!)
texasflood

1
Giusto per chiarire per i futuri lettori - la mia confusione era che pensavo all'esecuzione, l'intero binario sarebbe stato caricato nella RAM, quindi se la RAM era piccola, allora parte del binario avrebbe lasciato la RAM e avrebbe dovuto essere ricaricata dal disco - il che avrebbe causare problemi se si modifica il file. Ma la risposta ha chiarito che il binario non viene mai realmente rimosso dal disco anche se tu rmo te mvcome l'inode al file originale non viene rimosso fino a quando tutti i processi rimuovono il loro collegamento a quell'inode.
texasflood

@texasflood Exactly. Una volta rimossi tutti i percorsi, nessun nuovo processo ( dfincluso) può ottenere informazioni sull'inode. Qualunque nuova informazione che trovi sia correlata al nuovo file e al nuovo inode. Il punto principale qui è che il sottosistema di processo non ha interesse per questo problema, quindi le nozioni di gestione della memoria (paginazione della domanda, scambio di processo, errori di pagina, ...) sono completamente irrilevanti. Questo è un problema di sottosistema di file ed è gestito dal sottosistema di file. Il sottosistema di processo non si preoccupa di questo, non è per questo che è qui.
John WH Smith,

@texasflood Una nota su df -i: questo strumento probabilmente recupera informazioni dal superblocco della fs, o dalla sua cache, il che significa che può includere l'inode del vecchio binario (per il quale tutti i collegamenti sono stati cancellati). Questo non significa che i nuovi processi siano liberi di usare quei vecchi dati, comunque.
John WH Smith

2

La mia comprensione è che a causa della mappatura della memoria di un processo in esecuzione, il kernel non consentirebbe l'aggiornamento di una parte riservata del file mappato. Immagino che nel caso in cui un processo sia in esecuzione, quindi tutto il suo file è riservato, quindi l'aggiornamento perché hai compilato una nuova versione del tuo sorgente si traduce effettivamente nella creazione di un nuovo set di inode. In breve, le versioni precedenti dell'eseguibile rimangono accessibili sul disco attraverso eventi di errore di pagina. Quindi anche se aggiorni un file enorme, dovrebbe rimanere accessibile e il kernel dovrebbe comunque vedere la versione non trattata per tutto il tempo in cui il processo è in esecuzione. Gli inode del file originale non devono essere riutilizzati finché il processo è in esecuzione.

Questo ovviamente deve essere confermato.


2

Questo non è sempre il caso quando si sostituisce un file .jar. Le risorse Jar e alcuni caricatori di classi di riflessione runtime non vengono letti dal disco fino a quando il programma non richiede esplicitamente le informazioni.

Questo è solo un problema perché un jar è semplicemente un archivio anziché un singolo eseguibile che viene mappato in memoria. Questo è leggermente off-stopic ma è ancora una conseguenza della tua domanda e qualcosa con cui mi sono sparato ai piedi.

Quindi per gli eseguibili: sì. Per i file jar: forse (a seconda dell'implementazione).

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.