Que fait réellement l'ouverture d'un fichier?

266

Dans tous les langages de programmation (que j'utilise au moins), vous devez ouvrir un fichier avant de pouvoir y lire ou y écrire.

Mais que fait réellement cette opération ouverte?

Les pages de manuel pour les fonctions typiques ne vous disent rien d'autre que d'ouvrir un fichier en lecture / écriture:

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

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

De toute évidence, grâce à l'utilisation de la fonction, vous pouvez dire qu'elle implique la création d'une sorte d'objet qui facilite l'accès à un fichier.

Une autre façon de mettre cela serait, si je devais implémenter une openfonction, que faudrait-il faire sous Linux?

jramm
la source
13
Modification de cette question pour se concentrer sur Cet Linux; car ce que font Linux et Windows diffère. Sinon, c'est un peu trop large. De plus, tout langage de niveau supérieur finira par appeler une API C pour le système ou se compilera en C pour s'exécuter, donc laisser au niveau de "C" le place au plus petit dénominateur commun.
George Stocker
1
Sans oublier que tous les langages de programmation n'ont pas cette fonctionnalité, ou c'est une fonctionnalité qui dépend fortement de l'environnement. Certes, rare de nos jours, bien sûr, mais à ce jour, la gestion des fichiers est une partie complètement facultative d'ANSI Forth, et n'était même pas présente dans certaines implémentations dans le passé.

Réponses:

184

Dans presque tous les langages de haut niveau, la fonction qui ouvre un fichier est un wrapper autour de l'appel système du noyau correspondant. Il peut également faire d'autres choses fantaisistes, mais dans les systèmes d'exploitation contemporains, l'ouverture d'un fichier doit toujours passer par le noyau.

C'est pourquoi les arguments de la fopenfonction de bibliothèque, ou de Pythonopen ressemblent étroitement aux arguments de l' open(2)appel système.

En plus d'ouvrir le fichier, ces fonctions mettent généralement en place un tampon qui sera par conséquent utilisé avec les opérations de lecture / écriture. Le but de ce tampon est de garantir que chaque fois que vous voulez lire N octets, l'appel de bibliothèque correspondant renverra N octets, que les appels vers les appels système sous-jacents retournent moins.

Je ne suis pas réellement intéressé à implémenter ma propre fonction; juste pour comprendre ce qui se passe ... "au-delà de la langue" si vous voulez.

Dans les systèmes d'exploitation de type Unix, un appel réussi à open renvoie un "descripteur de fichier" qui est simplement un entier dans le contexte du processus utilisateur. Ce descripteur est par conséquent transmis à tout appel qui interagit avec le fichier ouvert et après l'appel close, le descripteur devient invalide.

Il est important de noter que l'appel à openagit comme un point de validation où sont effectués divers contrôles. Si toutes les conditions ne sont pas remplies, l'appel échoue en renvoyant à la -1place du descripteur et le type d'erreur est indiqué dans errno. Les contrôles essentiels sont:

  • Si le fichier existe;
  • Indique si le processus appelant est autorisé à ouvrir ce fichier dans le mode spécifié. Ceci est déterminé en faisant correspondre les autorisations de fichier, l'ID de propriétaire et l'ID de groupe aux ID respectifs du processus appelant.

Dans le contexte du noyau, il doit y avoir une sorte de mappage entre les descripteurs de fichiers du processus et les fichiers physiquement ouverts. La structure de données interne qui est mappée au descripteur peut contenir encore un autre tampon qui traite des périphériques basés sur des blocs, ou un pointeur interne qui pointe vers la position actuelle de lecture / écriture.

Blagovest Buyukliev
la source
2
Il convient de noter que dans les systèmes d'exploitation de type Unix, les descripteurs de fichiers de structure dans le noyau sont mappés, est appelé "description de fichier ouvert". Les FD de processus sont donc mappés aux OFD du noyau. Ceci est important pour comprendre la documentation. Par exemple, voyez man dup2et vérifiez la subtilité entre un descripteur de fichier ouvert (c'est-à-dire un FD qui se trouve être ouvert) et une description de fichier ouvert (un OFD).
rodrigo
1
Oui, les autorisations sont vérifiées lors de l'ouverture. Vous pouvez aller lire la source de l'implémentation "ouverte" du noyau: lxr.free-electrons.com/source/fs/open.c bien qu'il délègue la majeure partie du travail au pilote de système de fichiers spécifique.
pjc50
1
(sur les systèmes ext2, cela impliquera de lire les entrées du répertoire pour identifier quel inode contient les métadonnées, puis de charger cet inode dans le cache d'inode. Notez qu'il peut y avoir des pseudofilesystems comme "/ proc" et "/ sys" qui peuvent faire des choses arbitraires lorsque vous ouvrez un fichier)
pjc50
1
Notez que les contrôles sur le fichier ouvert - que le fichier existe, que vous avez l'autorisation - ne sont en pratique pas suffisants. Le fichier peut disparaître ou ses autorisations peuvent changer sous vos pieds. Certains systèmes de fichiers tentent d'empêcher cela, mais tant que votre système d'exploitation prend en charge le stockage réseau, il est impossible d'empêcher (un système d'exploitation peut "paniquer" si le système de fichiers local se comporte mal et est raisonnable: celui qui le fait lorsqu'un partage réseau ne l'est pas) un OS viable). Ces vérifications sont également effectuées à l'ouverture du fichier, mais doivent (effectivement) être effectuées à tous les autres accès aux fichiers.
Yakk - Adam Nevraumont
2
Sans oublier l'évaluation et / ou la création de serrures. Ceux-ci peuvent être partagés ou exclusifs et peuvent affecter l'ensemble du fichier, ou seulement une partie de celui-ci.
Thinkeye
83

Je vous suggère de jeter un œil à ce guide à travers une version simplifiée de l' open()appel système . Il utilise l'extrait de code suivant, qui est représentatif de ce qui se passe en arrière-plan lorsque vous ouvrez un fichier.

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  }

En bref, voici ce que fait ce code, ligne par ligne:

  1. Allouez un bloc de mémoire contrôlée par le noyau et copiez-y le nom de fichier à partir de la mémoire contrôlée par l'utilisateur.
  2. Choisissez un descripteur de fichier inutilisé, que vous pouvez considérer comme un index entier dans une liste évolutive de fichiers actuellement ouverts. Chaque processus a sa propre liste, bien qu'elle soit maintenue par le noyau; votre code ne peut pas y accéder directement. Une entrée de la liste contient toutes les informations que le système de fichiers sous-jacent utilisera pour extraire les octets du disque, comme le numéro d'inode, les autorisations de processus, les indicateurs d'ouverture, etc.
  3. La filp_openfonction a l'implémentation

    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);
    }

    ce qui fait deux choses:

    1. Utilisez le système de fichiers pour rechercher l'inode (ou plus généralement, quel que soit l'identifiant interne utilisé par le système de fichiers) correspondant au nom de fichier ou au chemin d'accès transmis.
    2. Créez un struct fileavec les informations essentielles sur l'inode et renvoyez-le. Cette structure devient l'entrée dans cette liste de fichiers ouverts que j'ai mentionnée plus tôt.
  4. Stockez ("installez") la structure retournée dans la liste des fichiers ouverts du processus.

  5. Libérez le bloc alloué de mémoire contrôlée par le noyau.
  6. Retour le descripteur de fichier, qui peut ensuite être transmis à des fonctions d'exploitation de fichiers comme read(), write()et close(). Chacun d'eux transférera le contrôle au noyau, qui peut utiliser le descripteur de fichier pour rechercher le pointeur de fichier correspondant dans la liste du processus, et utiliser les informations de ce pointeur de fichier pour effectuer la lecture, l'écriture ou la fermeture.

Si vous vous sentez ambitieux, vous pouvez comparer cet exemple simplifié à l'implémentation de l' open()appel système dans le noyau Linux, une fonction appelée do_sys_open(). Vous ne devriez pas avoir de mal à trouver les similitudes.


Bien sûr, ce n'est que la "couche supérieure" de ce qui se passe lorsque vous appelez open()- ou plus précisément, c'est le morceau de code de noyau le plus élevé qui est invoqué lors de l'ouverture d'un fichier. Un langage de programmation de haut niveau pourrait ajouter des couches supplémentaires en plus de cela. Il y a beaucoup de choses aux niveaux inférieurs. (Merci à Ruslan et pjc50 pour leur explication.) En gros, de haut en bas:

  • open_namei() et dentry_open() invoquer le code du système de fichiers, qui fait également partie du noyau, pour accéder aux métadonnées et au contenu des fichiers et répertoires. Le système de fichiers lit les octets bruts du disque et interprète ces modèles d'octets comme une arborescence de fichiers et de répertoires.
  • Le système de fichiers utilise la couche de périphérique de bloc , toujours une partie du noyau, pour obtenir ces octets bruts du lecteur. (Fait amusant: Linux vous permet d'accéder aux données brutes de la couche de périphérique de bloc à l'aide de /dev/sdaet similaires.)
  • La couche de périphérique de bloc invoque un pilote de périphérique de stockage, qui est également du code noyau, pour traduire d'une instruction de niveau moyen comme "lire le secteur X" en instructions d'entrée / sortie individuelles dans le code machine. Il existe plusieurs types de pilotes de périphérique de stockage, notamment IDE , (S) ATA , SCSI , Firewire , etc., correspondant aux différentes normes de communication qu'un lecteur peut utiliser. (Notez que le nommage est un gâchis.)
  • Les instructions d'E / S utilisent les capacités intégrées de la puce du processeur et du contrôleur de la carte mère pour envoyer et recevoir des signaux électriques sur le fil allant au lecteur physique. C'est du matériel, pas du logiciel.
  • À l'autre extrémité du fil, le micrologiciel du disque (code de contrôle intégré) interprète les signaux électriques pour faire tourner les plateaux et déplacer les têtes (HDD), ou lire une cellule ROM flash (SSD), ou tout ce qui est nécessaire pour accéder aux données sur ce type de périphérique de stockage.

Cela peut également être quelque peu incorrect en raison de la mise en cache . :-P Sérieusement cependant, il y a beaucoup de détails que j'ai omis - une personne (pas moi) pourrait écrire plusieurs livres décrivant comment tout ce processus fonctionne. Mais cela devrait vous donner une idée.

David Z
la source
67

Tout système de fichiers ou système d'exploitation dont vous voulez parler me convient. Agréable!


Sur un ZX Spectrum, l'initialisation d'une LOADcommande mettra le système en boucle serrée, lisant la ligne Audio In.

Le début des données est indiqué par une tonalité constante, et ensuite une séquence d'impulsions longues / courtes suit, où une impulsion courte est pour un binaire 0et une plus longue pour un binaire 1( https://en.wikipedia.org/ wiki / ZX_Spectrum_software ). La boucle de charge étroite rassemble des bits jusqu'à ce qu'elle remplisse un octet (8 bits), les stocke en mémoire, augmente le pointeur de mémoire, puis revient en boucle pour rechercher plus de bits.

En règle générale, la première chose qu'un chargeur lirait est un en- tête de format court et fixe , indiquant au moins le nombre d'octets à attendre, et éventuellement des informations supplémentaires telles que le nom de fichier, le type de fichier et l'adresse de chargement. Après avoir lu ce court en-tête, le programme pourrait décider de continuer à charger la majeure partie des données ou de quitter la routine de chargement et d'afficher un message approprié pour l'utilisateur.

Un état de fin de fichier peut être reconnu en recevant autant d'octets que prévu (soit un nombre fixe d'octets, câblé dans le logiciel, soit un nombre variable tel qu'indiqué dans un en-tête). Une erreur s'est produite si la boucle de chargement n'a pas reçu d'impulsion dans la plage de fréquences attendue pendant un certain temps.


Un peu d'histoire sur cette réponse

La procédure décrite charge les données d'une bande audio ordinaire - d'où la nécessité de balayer l'entrée audio (elle est connectée avec une prise standard aux magnétophones). Une LOADcommande est techniquement identique à openun fichier - mais il est lié physiquement en fait le chargement du fichier. En effet, le magnétophone n'est pas contrôlé par l'ordinateur et vous ne pouvez pas (avec succès) ouvrir un fichier sans le charger.

La "boucle serrée" est mentionnée car (1) le CPU, un Z80-A (si la mémoire est bonne), était vraiment lent: 3,5 MHz, et (2) le Spectrum n'avait pas d'horloge interne! Cela signifie qu'il devait compter avec précision le nombre d' états T (temps d'instruction) pour chaque. Célibataire. instruction. à l'intérieur de cette boucle, juste pour maintenir le timing précis du bip.
Heureusement, cette faible vitesse du processeur avait l'avantage distinct que vous pouviez calculer le nombre de cycles sur une feuille de papier, et donc le temps réel qu'ils prendraient.

usr2564301
la source
10
@BillWoodger: eh bien oui. Mais c'est une bonne question (je veux dire la vôtre). J'ai voté pour la clôture comme «trop large» et ma réponse vise à illustrer à quel point la question est extrêmement large.
usr2564301
8
Je pense que vous élargissez un peu trop la réponse. ZX Spectrum avait une commande OPEN, et c'était totalement différent de LOAD. Et plus difficile à comprendre.
rodrigo
3
Je suis également en désaccord sur la fermeture de la question, mais j'aime vraiment votre réponse.
Enzo Ferber
23
Bien que j'aie modifié ma question pour la limiter au système d'exploitation Linux / Windows afin de la garder ouverte, cette réponse est entièrement valide et utile. Comme indiqué dans ma question, je ne cherche pas à mettre en œuvre quelque chose ou à amener d'autres personnes à faire mon travail, je cherche à apprendre. Pour apprendre, vous devez poser les «grandes» questions. Si nous fermons constamment les questions sur SO pour être `` trop large '', cela risque de devenir un endroit pour amener les gens à écrire votre code pour vous sans donner d'explication sur quoi, où et pourquoi. Je préfère le garder comme un endroit où je peux apprendre.
jramm
14
Cette réponse semble prouver que votre interprétation de la question est trop large, plutôt que la question elle-même est trop large.
jwg
17

Cela dépend du système d'exploitation ce qui se passe exactement lorsque vous ouvrez un fichier. Ci-dessous, je décris ce qui se passe sous Linux car cela vous donne une idée de ce qui se passe lorsque vous ouvrez un fichier et vous pouvez vérifier le code source si vous êtes intéressé par plus de détails. Je ne couvre pas les autorisations car cela rendrait cette réponse trop longue.

Sous Linux, chaque fichier est reconnu par une structure appelée inode. Chaque structure a un numéro unique et chaque fichier ne reçoit qu'un seul numéro d'inode. Cette structure stocke les métadonnées d'un fichier, par exemple la taille du fichier, les autorisations de fichier, les horodatages et le pointeur sur les blocs de disque, mais pas le nom de fichier lui-même. Chaque fichier (et répertoire) contient une entrée de nom de fichier et le numéro d'inode pour la recherche. Lorsque vous ouvrez un fichier, en supposant que vous disposez des autorisations appropriées, un descripteur de fichier est créé à l'aide du numéro d'inode unique associé au nom de fichier. Comme de nombreux processus / applications peuvent pointer vers le même fichier, inode a un champ de lien qui conserve le nombre total de liens vers le fichier. Si un fichier est présent dans un répertoire, son nombre de liens est un, s'il a un lien dur, son nombre de liens sera de deux et si un fichier est ouvert par un processus, le nombre de liens sera incrémenté de 1.

Alex
la source
6
Qu'est-ce que cela a à voir avec la vraie question?
Bill Woodger
1
Il décrit ce qui se passe à un bas niveau lorsque vous ouvrez un fichier sous Linux. Je suis d'accord que la question est assez large, donc ce n'est peut-être pas la réponse que Jramm cherchait.
Alex
1
Encore une fois, pas de vérification des autorisations?
Bill Woodger
11

Tenue de livres, surtout. Cela inclut diverses vérifications telles que "Le fichier existe-t-il?" et "Ai-je les autorisations pour ouvrir ce fichier en écriture?".

Mais c'est tout ce qui concerne le noyau - à moins que vous n'implémentiez votre propre système d'exploitation jouet, il n'y a pas grand-chose à explorer (si vous l'êtes, amusez-vous - c'est une excellente expérience d'apprentissage). Bien sûr, vous devez toujours apprendre tous les codes d'erreur possibles que vous pouvez recevoir lors de l'ouverture d'un fichier, afin de pouvoir les gérer correctement - mais ce sont généralement de jolies petites abstractions.

La partie la plus importante au niveau du code est qu'elle vous donne une poignée sur le fichier ouvert, que vous utilisez pour toutes les autres opérations que vous effectuez avec un fichier. Ne pourriez-vous pas utiliser le nom de fichier au lieu de ce descripteur arbitraire? Eh bien, bien sûr - mais l'utilisation d'une poignée vous offre certains avantages:

  • Le système peut suivre tous les fichiers actuellement ouverts et empêcher leur suppression (par exemple).
  • Les systèmes d'exploitation modernes sont construits autour des poignées - il y a des tonnes de choses utiles que vous pouvez faire avec les poignées, et tous les différents types de poignées se comportent presque de manière identique. Par exemple, lorsqu'une opération d'E / S asynchrone se termine sur un descripteur de fichier Windows, le descripteur est signalé - cela vous permet de bloquer sur le descripteur jusqu'à ce qu'il soit signalé, ou de terminer l'opération de manière entièrement asynchrone. L'attente sur un descripteur de fichier est exactement la même chose que l'attente sur un descripteur de thread (signalé par exemple à la fin du thread), un descripteur de processus (à nouveau, signalé à la fin du processus) ou un socket (lorsqu'une opération asynchrone se termine). Tout aussi important, les descripteurs appartiennent à leurs processus respectifs, donc lorsqu'un processus se termine de façon inattendue (ou que l'application est mal écrite), le système d'exploitation sait quels descripteurs il peut libérer.
  • La plupart des opérations sont positionnelles - vous à readpartir de la dernière position de votre fichier. En utilisant une poignée pour identifier une "ouverture" particulière d'un fichier, vous pouvez avoir plusieurs poignées simultanées vers le même fichier, chacune lisant à partir de leur propre emplacement. D'une certaine manière, le handle agit comme une fenêtre mobile dans le fichier (et un moyen d'émettre des demandes d'E / S asynchrones, ce qui est très pratique).
  • Les poignées sont beaucoup plus petites que les noms de fichiers. Un handle est généralement de la taille d'un pointeur, généralement 4 ou 8 octets. D'un autre côté, les noms de fichiers peuvent avoir des centaines d'octets.
  • Les descripteurs permettent au système d'exploitation de déplacer le fichier, même si les applications l'ont ouvert - le descripteur est toujours valide et il pointe toujours vers le même fichier, même si le nom du fichier a changé.

Il y a aussi d'autres astuces que vous pouvez faire (par exemple, partager des poignées entre les processus pour avoir un canal de communication sans utiliser de fichier physique; sur les systèmes Unix, les fichiers sont également utilisés pour les appareils et divers autres canaux virtuels, donc ce n'est pas strictement nécessaire ), mais ils ne sont pas vraiment liés à l' openopération elle-même, donc je ne vais pas m'y plonger.

Luaan
la source
7

Au cœur de celui-ci lors de l'ouverture pour la lecture, rien de fantaisiste n'a réellement besoin se produire. Il suffit de vérifier que le fichier existe et que l'application dispose de privilèges suffisants pour le lire et créer un descripteur sur lequel vous pouvez émettre des commandes de lecture dans le fichier.

C'est sur ces commandes que la lecture réelle sera envoyée.

Le système d'exploitation aura souvent une longueur d'avance sur la lecture en lançant une opération de lecture pour remplir le tampon associé à la poignée. Ensuite, lorsque vous effectuez la lecture, il peut retourner immédiatement le contenu du tampon plutôt que d'attendre sur le disque IO.

Pour ouvrir un nouveau fichier en écriture, le système d'exploitation devra ajouter une entrée dans le répertoire du nouveau fichier (actuellement vide). Et encore une fois un handle est créé sur lequel vous pouvez émettre les commandes d'écriture.

monstre à cliquet
la source
5

Fondamentalement, un appel à open doit rechercher le fichier, puis enregistrer tout ce dont il a besoin pour que les opérations d'E / S ultérieures puissent le retrouver. C'est assez vague, mais ce sera vrai sur tous les systèmes d'exploitation auxquels je peux immédiatement penser. Les spécificités varient d'une plateforme à l'autre. De nombreuses réponses déjà mentionnées ici parlent des systèmes d'exploitation de bureau modernes. J'ai fait un peu de programmation sur CP / M, donc j'offrirai mes connaissances sur la façon dont cela fonctionne sur CP / M (MS-DOS fonctionne probablement de la même manière, mais pour des raisons de sécurité, ce n'est normalement pas fait comme ça aujourd'hui) ).

Sur CP / M, vous avez une chose appelée FCB (comme vous l'avez mentionné C, vous pouvez l'appeler une structure; c'est vraiment une zone contiguë de 35 octets dans la RAM contenant divers champs). Le FCB a des champs pour écrire le nom de fichier et un entier (4 bits) identifiant l'unité de disque. Ensuite, lorsque vous appelez le fichier ouvert du noyau, vous passez un pointeur vers cette structure en le plaçant dans l'un des registres du processeur. Quelque temps plus tard, le système d'exploitation revient avec la structure légèrement modifiée. Quelles que soient les E / S que vous faites pour ce fichier, vous passez un pointeur vers cette structure vers l'appel système.

Que fait CP / M avec ce FCB? Il réserve certains champs pour son propre usage et les utilise pour garder une trace du fichier, il vaut donc mieux ne jamais les toucher depuis l'intérieur de votre programme. L'opération d'ouverture de fichier recherche dans la table au début du disque un fichier portant le même nom que ce qui se trouve dans le FCB (le caractère générique «?» Correspond à n'importe quel caractère). S'il trouve un fichier, il copie certaines informations dans le FCB, y compris les emplacements physiques du fichier sur le disque, afin que les appels d'E / S ultérieurs appellent finalement le BIOS qui peut transmettre ces emplacements au pilote de disque. À ce niveau, les détails varient.

OmarL
la source
-7

En termes simples, lorsque vous ouvrez un fichier, vous demandez en fait au système d'exploitation de charger le fichier souhaité (copier le contenu du fichier) depuis le stockage secondaire vers la RAM pour le traitement. Et la raison derrière cela (Chargement d'un fichier) est que vous ne pouvez pas traiter le fichier directement depuis le disque dur en raison de sa vitesse extrêmement lente par rapport à Ram.

La commande open génère un appel système qui à son tour copie le contenu du fichier du stockage secondaire (disque dur) vers le stockage principal (Ram).

Et nous «fermons» un fichier car le contenu modifié du fichier doit être reflété dans le fichier d'origine qui se trouve sur le disque dur. :)

J'espère que cela pourra aider.


la source