Cosa fa realmente l'apertura di un file?


266

In tutti i linguaggi di programmazione (che uso almeno), è necessario aprire un file prima di poter leggere o scrivere su di esso.

Ma cosa fa realmente questa operazione aperta?

Le pagine del manuale per le funzioni tipiche in realtà non dicono altro che "apre un file per leggere / scrivere":

http://www.cplusplus.com/reference/cstdio/fopen/

https://docs.python.org/3/library/functions.html#open

Ovviamente, attraverso l'uso della funzione si può dire che comporta la creazione di un qualche tipo di oggetto che facilita l'accesso a un file.

Un altro modo per dirlo sarebbe, se dovessi implementare una openfunzione, cosa dovrebbe fare su Linux?


13
Modifica di questa domanda per concentrarsi su Ce Linux; poiché ciò che fanno Linux e Windows differisce. Altrimenti, è un po 'troppo ampio. Inoltre, qualsiasi linguaggio di livello superiore finirà per chiamare un'API C per il sistema o la compilazione in C per l'esecuzione, quindi lasciare al livello di "C" lo sta mettendo al minimo comune denominatore.
George Stocker,

1
Per non parlare del fatto che non tutti i linguaggi di programmazione dispongono di questa funzione o che è altamente dipendente dall'ambiente. Certamente in questi giorni, ovviamente, ma fino ad oggi la gestione dei file è una parte completamente opzionale di ANSI Forth, e in passato non era nemmeno presente in alcune implementazioni.

Risposte:


184

In quasi tutti i linguaggi di alto livello, la funzione che apre un file è un wrapper attorno alla chiamata di sistema del kernel corrispondente. Può anche fare altre cose fantasiose, ma nei sistemi operativi contemporanei, l'apertura di un file deve sempre passare attraverso il kernel.

Questo è il motivo per cui gli argomenti della fopenfunzione libreria, o Python, openassomigliano molto agli argomenti della open(2)chiamata di sistema.

Oltre all'apertura del file, queste funzioni di solito impostano un buffer che verrà di conseguenza utilizzato con le operazioni di lettura / scrittura. Lo scopo di questo buffer è garantire che ogni volta che si desidera leggere N byte, la chiamata alla libreria corrispondente restituirà N byte, indipendentemente dal fatto che le chiamate alle chiamate di sistema sottostanti restituiscano meno.

In realtà non mi interessa implementare la mia funzione; solo per capire cosa diavolo sta succedendo ... "oltre la lingua" se vuoi.

Nei sistemi operativi simili a Unix, una chiamata riuscita a openrestituire un "descrittore di file" che è semplicemente un numero intero nel contesto del processo dell'utente. Di conseguenza, questo descrittore viene passato a qualsiasi chiamata che interagisce con il file aperto e, dopo averlo richiamato close, il descrittore diventa non valido.

È importante notare che la chiamata ad openagire come un punto di convalida in cui vengono effettuati vari controlli. Se non tutte le condizioni sono soddisfatte, la chiamata fallisce restituendo al -1posto del descrittore e il tipo di errore è indicato in errno. I controlli essenziali sono:

  • Se il file esiste;
  • Se il processo di chiamata ha il privilegio di aprire questo file nella modalità specificata. Questo viene determinato abbinando le autorizzazioni del file, l'ID proprietario e l'ID gruppo ai rispettivi ID del processo chiamante.

Nel contesto del kernel, ci deve essere una sorta di mappatura tra i descrittori di file del processo e i file aperti fisicamente. La struttura interna dei dati mappata al descrittore può contenere ancora un altro buffer che si occupa di dispositivi basati su blocchi o un puntatore interno che punta alla posizione di lettura / scrittura corrente.


2
Vale la pena notare che nei sistemi operativi simili a Unix, i descrittori dei file di struttura nel kernel sono mappati, è chiamato "descrizione del file aperto". Quindi gli FD di processo sono mappati agli OFD del kernel. Questo è importante per comprendere la documentazione. Ad esempio, vedere man dup2e controllare la sottigliezza tra un descrittore di file aperto (ovvero un FD che risulta essere aperto) e una descrizione del file aperto (un OFD).
Rodrigo,

1
Sì, le autorizzazioni sono controllate a tempo aperto. Puoi andare a leggere la fonte per l'implementazione "aperta" del kernel: lxr.free-electrons.com/source/fs/open.c sebbene deleghi la maggior parte del lavoro al driver del file system specifico.
pjc50,

1
(sui sistemi ext2 questo comporterà la lettura delle voci della directory per identificare in quale inode ha i metadati, quindi il caricamento di quell'inode nella cache dell'inode. Notare che potrebbero esserci sistemi pseudofiles come "/ proc" e "/ sys" che possono fare cose arbitrarie quando apri un file)
pjc50,

1
Si noti che i controlli all'apertura del file - che il file esiste, che si dispone dell'autorizzazione - non sono, in pratica, sufficienti. Il file può scomparire o le sue autorizzazioni possono cambiare sotto i tuoi piedi. Alcuni file system tentano di impedirlo, ma fintanto che il sistema operativo in uso supporta l'archiviazione di rete è impossibile prevenirlo (un sistema operativo può "prendere il panico" se il file system locale si comporta male ed essere ragionevole: uno che lo fa quando una condivisione di rete non lo è un sistema operativo valido). Tali controlli vengono eseguiti anche all'apertura del file, ma devono (efficacemente) essere eseguiti anche a tutti gli altri accessi ai file.
Yakk - Adam Nevraumont,

2
Da non dimenticare la valutazione e / o la creazione di blocchi. Questi possono essere condivisi o esclusivi e possono influenzare l'intero file o solo una parte di esso.
Thinkeye,

83

Ti suggerirei di dare un'occhiata a questa guida attraverso una versione semplificata della open()chiamata di sistema . Utilizza il seguente frammento di codice, che è rappresentativo di ciò che accade dietro le quinte quando si apre un file.

0  int sys_open(const char *filename, int flags, int mode) {
1      char *tmp = getname(filename);
2      int fd = get_unused_fd();
3      struct file *f = filp_open(tmp, flags, mode);
4      fd_install(fd, f);
5      putname(tmp);
6      return fd;
7  }

In breve, ecco cosa fa quel codice, riga per riga:

  1. Allocare un blocco di memoria controllata dal kernel e copiare il nome file in esso dalla memoria controllata dall'utente.
  2. Scegli un descrittore di file non utilizzato, che puoi considerare come un indice intero in un elenco coltivabile di file attualmente aperti. Ogni processo ha il suo elenco, sebbene sia gestito dal kernel; il tuo codice non può accedervi direttamente. Una voce nell'elenco contiene tutte le informazioni che verranno utilizzate dal file system sottostante per estrarre i byte dal disco, come il numero di inode, le autorizzazioni di processo, i flag aperti e così via.
  3. La filp_openfunzione ha l'implementazione

    struct file *filp_open(const char *filename, int flags, int mode) {
            struct nameidata nd;
            open_namei(filename, flags, mode, &nd);
            return dentry_open(nd.dentry, nd.mnt, flags);
    }

    che fa due cose:

    1. Usa il filesystem per cercare l'inode (o più in generale, qualunque tipo di identificatore interno usi il filesystem) corrispondente al nome del file o al percorso che è stato passato.
    2. Creare un struct filecon le informazioni essenziali sull'inode e restituirlo. Questa struttura diventa la voce in quell'elenco di file aperti che ho menzionato in precedenza.
  4. Memorizza ("installa") la struttura restituita nell'elenco dei file aperti del processo.

  5. Liberare il blocco allocato di memoria controllata dal kernel.
  6. Restituisce il descrittore di file, che possono poi essere passata alle funzioni operative di file come read(), write()e close(). Ognuno di questi passerà il controllo al kernel, che può usare il descrittore di file per cercare il puntatore del file corrispondente nell'elenco dei processi e usare le informazioni in quel puntatore di file per eseguire effettivamente la lettura, la scrittura o la chiusura.

Se ti senti ambizioso, puoi confrontare questo esempio semplificato con l'implementazione della open()chiamata di sistema nel kernel Linux, una funzione chiamata do_sys_open(). Non dovresti avere problemi a trovare le somiglianze.


Ovviamente, questo è solo il "livello superiore" di ciò che accade quando chiami open()- o più precisamente, è il pezzo più alto di codice del kernel che viene invocato nel processo di apertura di un file. Un linguaggio di programmazione di alto livello potrebbe aggiungere ulteriori livelli. C'è molto che succede ai livelli più bassi. (Grazie a Ruslan e pjc50 per la spiegazione.) Circa, dall'alto verso il basso:

  • open_namei()e dentry_open()invocare il codice del filesystem, che è anche parte del kernel, per accedere a metadati e contenuti per file e directory. Il filesystem legge byte grezzi dal disco e interpreta questi schemi di byte come un albero di file e directory.
  • Il filesystem utilizza il layer del dispositivo a blocchi , sempre parte del kernel, per ottenere quei byte grezzi dall'unità. (Fatto divertente: Linux ti consente di accedere ai dati grezzi dal livello del dispositivo a blocchi usando /dev/sdae simili.)
  • Il livello del dispositivo a blocchi richiama un driver del dispositivo di archiviazione, che è anche codice del kernel, per tradurre da un'istruzione di livello medio come "leggi settore X" in singole istruzioni di input / output nel codice macchina. Esistono diversi tipi di driver di dispositivo di archiviazione, tra cui IDE , (S) ATA , SCSI , Firewire e così via, corrispondenti ai diversi standard di comunicazione che un'unità potrebbe utilizzare. (Nota che la denominazione è un casino.)
  • Le istruzioni di I / O utilizzano le funzionalità integrate del chip del processore e del controller della scheda madre per inviare e ricevere segnali elettrici sul filo che va all'unità fisica. Questo è hardware, non software.
  • All'altra estremità del filo, il firmware del disco (codice di controllo incorporato) interpreta i segnali elettrici per far girare i piatti e spostare le testine (HDD), o leggere una cella ROM flash (SSD) o qualsiasi altra cosa sia necessaria per accedere ai dati su quel tipo di dispositivo di archiviazione.

Questo potrebbe anche essere in qualche modo errato a causa della memorizzazione nella cache . :-P Seriamente, ci sono molti dettagli che ho lasciato fuori - una persona (non io) potrebbe scrivere più libri che descrivono come funziona l'intero processo. Ma questo dovrebbe darti un'idea.


67

Qualsiasi file system o sistema operativo di cui vuoi parlare va bene per me. Bello!


Su uno spettro ZX, inizializzazione a LOAD comando metterà il sistema in un circuito stretto, leggendo la linea Audio In.

L'inizio dei dati è indicato da un tono costante e successivamente segue una sequenza di impulsi lunghi / brevi, in cui un impulso breve è per un binario 0e uno più lungo per un binario 1( https://en.wikipedia.org/ wiki / ZX_Spectrum_software ). Il circuito di caricamento stretto raccoglie i bit fino a quando non riempie un byte (8 bit), lo memorizza in memoria, aumenta il puntatore di memoria, quindi torna indietro per cercare altri bit.

In genere, la prima cosa che un caricatore potrebbe leggere è un'intestazione di formato fissa, breve, che indica almeno il numero di byte previsti e, eventualmente, ulteriori informazioni come il nome del file, il tipo di file e l'indirizzo di caricamento. Dopo aver letto questa breve intestazione, il programma potrebbe decidere se continuare a caricare la maggior parte dei dati o uscire dalla routine di caricamento e visualizzare un messaggio appropriato per l'utente.

Uno stato di fine file può essere riconosciuto ricevendo tutti i byte previsti (o un numero fisso di byte, cablato nel software o un numero variabile come indicato in un'intestazione). Si è verificato un errore se il loop di caricamento non ha ricevuto un impulso nella gamma di frequenza prevista per un certo periodo di tempo.


Un piccolo retroscena su questa risposta

La procedura descritta carica i dati da un normale nastro audio - da qui la necessità di scansionare Audio In (collegato con una spina standard ai registratori a nastro). Un LOADcomando è tecnicamente uguale a openun file, ma è fisicamente legato al caricamento effettivo del file. Questo perché il registratore non è controllato dal computer e non è possibile (correttamente) aprire un file ma non caricarlo.

Lo "stretto circuito" è menzionato perché (1) la CPU, uno Z80-A (se la memoria serve), era davvero lenta: 3,5 MHz e (2) lo spettro non aveva un clock interno! Ciò significa che doveva tenere accuratamente il conto degli stati T (tempi di istruzione) per ciascuno. singolo. istruzioni. all'interno di quel loop, solo per mantenere la precisione del segnale acustico.
Fortunatamente, quella bassa velocità della CPU aveva il netto vantaggio di poter calcolare il numero di cicli su un pezzo di carta e quindi il tempo reale che avrebbero impiegato.


10
@BillWoodger: beh si. Ma è una domanda giusta (intendo la tua). Ho votato per chiudere come "troppo ampio" e la mia risposta intende illustrare quanto sia estremamente ampia la domanda.
usr2564301,

8
Penso che stai allargando un po 'la risposta. ZX Spectrum aveva un comando OPEN, ed era completamente diverso da LOAD. E più difficile da capire.
Rodrigo,

3
Sono anche in disaccordo sulla chiusura della domanda, ma mi piace molto la tua risposta.
Enzo Ferber,

23
Anche se ho modificato la mia domanda per limitarmi al sistema operativo linux / windows nel tentativo di tenerlo aperto, questa risposta è del tutto valida e utile. Come affermato nella mia domanda, non sto cercando di implementare qualcosa o di convincere altre persone a fare il mio lavoro, sto cercando di imparare. Per imparare devi porre le "grandi" domande. Se chiudiamo costantemente le domande su SO per essere "troppo ampio", rischia di diventare un posto per convincere le persone a scrivere il tuo codice per te senza dare alcuna spiegazione di cosa, dove o perché. Preferirei tenerlo come un posto dove posso venire ad imparare.
Jramm,

14
Questa risposta sembra dimostrare che la tua interpretazione della domanda è troppo ampia, piuttosto che la domanda stessa è troppo ampia.
jwg

17

Dipende dal sistema operativo che cosa succede esattamente quando si apre un file. Di seguito descrivo cosa succede in Linux in quanto ti dà un'idea di cosa succede quando apri un file e potresti controllare il codice sorgente se sei interessato a maggiori dettagli. Non sto coprendo le autorizzazioni in quanto renderebbe questa risposta troppo lunga.

In Linux ogni file è riconosciuto da una struttura chiamata inode. Ogni struttura ha un numero univoco e ogni file ottiene un solo numero di inode. Questa struttura memorizza i metadati per un file, ad esempio dimensione del file, permessi dei file, timestamp e puntatore a blocchi del disco, tuttavia, non il nome del file stesso. Ogni file (e directory) contiene una voce del nome file e il numero di inode per la ricerca. Quando si apre un file, supponendo che si disponga delle autorizzazioni pertinenti, viene creato un descrittore di file utilizzando il numero di inode univoco associato al nome del file. Poiché molti processi / applicazioni possono puntare allo stesso file, inode ha un campo link che mantiene il conteggio totale dei link al file. Se un file è presente in una directory, il suo conteggio dei collegamenti è uno, se ha un collegamento reale il suo conteggio dei collegamenti sarà due e se un file viene aperto da un processo, il conteggio dei collegamenti verrà incrementato di 1.


6
Cosa c'entra questo con la vera domanda?
Bill Woodger,

1
Descrive cosa succede a basso livello quando si apre un file in Linux. Sono d'accordo che la domanda sia piuttosto ampia, quindi questa potrebbe non essere stata la risposta che Jramm stava cercando.
Alex,

1
Quindi, ancora, nessun controllo per le autorizzazioni?
Bill Woodger,

11

Contabilità, principalmente. Ciò include vari controlli come "Il file esiste?" e "Ho i permessi per aprire questo file per la scrittura?".

Ma questo è tutto roba del kernel - a meno che tu non stia implementando il tuo sistema operativo giocattolo, non c'è molto da approfondire (se lo sei, divertiti - è un'ottima esperienza di apprendimento). Ovviamente, dovresti ancora imparare tutti i possibili codici di errore che puoi ricevere durante l'apertura di un file, in modo da poterli gestire correttamente - ma quelli sono di solito piccole astrazioni.

La parte più importante a livello di codice è che ti dà un handle per il file aperto, che usi per tutte le altre operazioni che fai con un file. Non potresti usare il nome file invece di questo handle arbitrario? Bene, certo - ma l'uso di una maniglia offre alcuni vantaggi:

  • Il sistema può tenere traccia di tutti i file attualmente aperti e impedirne l'eliminazione (ad esempio).
  • I moderni sistemi operativi sono costruiti attorno alle maniglie: ci sono tonnellate di cose utili che puoi fare con le maniglie e tutti i diversi tipi di maniglie si comportano in modo quasi identico. Ad esempio, quando un'operazione I / O asincrona viene completata su un handle di file Windows, viene segnalato l'handle: ciò consente di bloccare l'handle fino a quando non viene segnalato, oppure di completare l'operazione in modo completamente asincrono. Attendere su un handle di file equivale esattamente a aspettare su un handle di thread (segnalato ad esempio quando termina il thread), un handle di processo (di nuovo, segnalato quando termina il processo) o un socket (al termine di un'operazione asincrona). Altrettanto importante, gli handle sono di proprietà dei rispettivi processi, quindi quando un processo viene interrotto in modo imprevisto (o l'applicazione è scritta male), il sistema operativo sa quali handle può rilasciare.
  • La maggior parte delle operazioni sono posizionali: tu readdall'ultima posizione nel tuo file. Utilizzando un handle per identificare una particolare "apertura" di un file, è possibile avere più handle simultanei nello stesso file, ognuno leggendo dai propri luoghi. In un certo senso, l'handle agisce come una finestra mobile nel file (e un modo per inviare richieste I / O asincrone, che sono molto utili).
  • Le maniglie sono molto più piccole dei nomi dei file. Un handle ha in genere le dimensioni di un puntatore, in genere 4 o 8 byte. D'altra parte, i nomi dei file possono avere centinaia di byte.
  • Gli handle consentono al sistema operativo di spostare il file, anche se le applicazioni lo hanno aperto: l'handle è ancora valido e punta ancora allo stesso file, anche se il nome del file è cambiato.

Ci sono anche alcuni altri trucchi che puoi fare (ad esempio, condividere handle tra processi per avere un canale di comunicazione senza utilizzare un file fisico; sui sistemi unix, i file vengono utilizzati anche per dispositivi e vari altri canali virtuali, quindi non è strettamente necessario ), ma non sono realmente legati openall'operazione stessa, quindi non approfondirò.


7

Al centro di ciò, quando si apre per la lettura, in realtà non deve succedere nulla di speciale . Tutto ciò che deve fare è verificare che il file esista e l'applicazione disponga di privilegi sufficienti per leggerlo e creare un handle su cui è possibile inviare comandi di lettura al file.

È su quei comandi che verrà inviata la lettura effettiva.

Il sistema operativo ottiene spesso un vantaggio sulla lettura avviando un'operazione di lettura per riempire il buffer associato all'handle. Quindi, quando si esegue effettivamente la lettura, è possibile restituire immediatamente il contenuto del buffer anziché attendere l'IO del disco.

Per aprire un nuovo file per la scrittura, il sistema operativo dovrà aggiungere una voce nella directory per il nuovo file (attualmente vuoto). E di nuovo viene creato un handle su cui è possibile emettere i comandi di scrittura.


5

Fondamentalmente, una chiamata per aprire deve trovare il file e quindi registrare tutto ciò che è necessario in modo che le successive operazioni di I / O possano trovarlo di nuovo. È piuttosto vago, ma sarà vero su tutti i sistemi operativi a cui posso immediatamente pensare. Le specifiche variano da piattaforma a piattaforma. Molte risposte già qui parlano dei moderni sistemi operativi desktop. Ho fatto un po 'di programmazione su CP / M, quindi offrirò le mie conoscenze su come funziona su CP / M (MS-DOS probabilmente funziona allo stesso modo, ma per motivi di sicurezza, normalmente non è fatto così oggi ).

Su CP / M hai una cosa chiamata FCB (come hai detto C, potresti chiamarla struct; in realtà è un'area contigua di 35 byte nella RAM contenente vari campi). FCB dispone di campi per scrivere il nome file e un numero intero (4 bit) che identifica l'unità disco. Quindi, quando si chiama Open File del kernel, si passa un puntatore a questa struttura inserendolo in uno dei registri della CPU. Qualche tempo dopo, il sistema operativo ritorna con la struttura leggermente modificata. Qualunque I / O fai a questo file, passi un puntatore a questa struttura alla chiamata di sistema.

Cosa fa CP / M con questo FCB? Si riserva alcuni campi per uso personale e li utilizza per tenere traccia del file, quindi è meglio non toccarli mai all'interno del programma. L'operazione Apri file cerca nella tabella all'inizio del disco un file con lo stesso nome di quello che c'è nell'FCB (il carattere jolly '?' Corrisponde a qualsiasi carattere). Se trova un file, copia alcune informazioni nell'FCB, comprese le posizioni fisiche del file sul disco, in modo che le successive chiamate I / O chiamino il BIOS che può passare queste posizioni al driver del disco. A questo livello, le specifiche variano.


-7

In parole povere, quando si apre un file in realtà si richiede al sistema operativo di caricare il file desiderato (copiare il contenuto del file) dalla memoria secondaria in ram per l'elaborazione. E il motivo dietro questo (caricamento di un file) è perché non è possibile elaborare il file direttamente dal disco rigido a causa della sua velocità estremamente lenta rispetto a Ram.

Il comando open genererà una chiamata di sistema che a sua volta copia il contenuto del file dalla memoria secondaria (disco rigido) alla memoria primaria (Ram).

E chiudiamo un file perché il contenuto modificato del file deve essere riflesso nel file originale che si trova sul disco rigido. :)

Spero che aiuti.

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.