web-dev-qa-db-fra.com

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

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

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

Les pages de manuel pour des fonctions typiques ne vous disent pas réellement autre chose que cela "ouvre un fichier pour la 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 constater que cela implique la création d'un type d'objet facilitant l'accès à un fichier.

Autrement dit, si je devais implémenter une fonction open, que faudrait-il faire sous Linux?

258
jramm

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. Cela peut aussi faire d’autres choses fantaisistes, mais dans les systèmes d’exploitation actuels, l’ouverture d’un fichier doit toujours passer par le noyau.

C'est pourquoi les arguments de la fonction de bibliothèque fopen ou le open de Python ressemblent beaucoup aux arguments de l'appel système open(2).

En plus d'ouvrir le fichier, ces fonctions établissent généralement 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 renvoient moins.

Je ne suis pas réellement intéressé par la mise en œuvre de 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 interagissant avec le fichier ouvert. Après avoir appelé close, le descripteur devient invalide.

Il est important de noter que l'appel à open agit comme un point de validation auquel diverses vérifications sont effectuées. Si toutes les conditions ne sont pas remplies, l'appel échoue en renvoyant -1 à la place 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 a le privilège d'ouvrir ce fichier dans le mode spécifié. Cela 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 exister une sorte de mappage entre les descripteurs de fichier du processus et les fichiers physiquement ouverts. La structure de données interne mappée sur le descripteur peut contenir encore un autre tampon qui traite les périphériques basés sur des blocs, ou un pointeur interne qui pointe vers la position de lecture/écriture en cours.

181

Je vous conseillerais de consulter ce guide décrit une version simplifiée de l'appel système open() . Il utilise l'extrait de code suivant, qui est représentatif de ce qui se passe dans les coulisses 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 des 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 les informations que le système de fichiers sous-jacent utilisera pour extraire des octets du disque, telles que le numéro d'inode, les autorisations de processus, les indicateurs ouverts, etc.
  3. La fonction filp_open 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);
    }
    

    qui fait deux choses:

    1. Utilisez le système de fichiers pour rechercher l'inode (ou plus généralement, quel que soit le type d'identificateur interne utilisé par le système de fichiers) correspondant au nom de fichier ou au chemin d'accès qui a été transmis.
    2. Créez un struct file avec 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 ("install") 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. Renvoie le descripteur de fichier, qui peut ensuite être transmis à des fonctions d'opération sur les fichiers telles que read(), write() et close(). Chacune de celles-ci passera le contrôle au noyau, qui pourra utiliser le descripteur de fichier pour rechercher le pointeur de fichier correspondant dans la liste du processus et utiliser les informations contenues dans ce pointeur de fichier pour effectuer la lecture, l'écriture ou la fermeture.

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


Bien entendu, il ne s'agit que de la "couche supérieure" de ce qui se passe lorsque vous appelez open() - ou plus précisément, c'est le code de noyau le plus élevé qui est appelé lors de l'ouverture d'un fichier. Un langage de programmation de haut niveau peut ajouter des couches supplémentaires. Il se passe beaucoup de choses à des niveaux inférieurs. (Merci à Ruslan et pjc5 pour avoir expliqué.) En gros, de haut en bas:

  • open_namei() et dentry_open() invoquent du code de système de fichiers, qui fait également partie du noyau, pour accéder aux métadonnées et au contenu des fichiers et des 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 bloc de périphérique en bloc , qui fait également partie du noyau, pour obtenir ces octets bruts à partir du lecteur. (Fait amusant: Linux vous permet d’accéder aux données brutes de la couche de périphériques en mode bloc en utilisant /dev/sda, etc.).
  • La couche de périphérique en mode bloc appelle un pilote de périphérique de stockage, qui est également du code du noyau, pour traduire une instruction de niveau moyen telle que "secteur de lecture X" en instruction individuelle instructions d'entrée/sortie en code machine. Il existe plusieurs types de pilotes de périphérique de stockage, notamment IDE , (S) ATA , SCSI , Firewire , et ainsi de suite, correspondant aux différentes normes de communication qu'un lecteur peut utiliser. (Notez que la dénomination est un gâchis.)
  • Les instructions d'E/S utilisent les fonctionnalités intégrées de la puce de 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 des logiciels.
  • À l’autre bout 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 (disque dur), ou lire une cellule flash ROM (SSD), est nécessaire pour accéder aux données sur ce type de périphérique de stockage.

Cela peut aussi être quelque peu incorrect en raison de la mise en cache . Sérieusement, il y a beaucoup de détails que j'ai laissés de côté - une personne (pas moi) pourrait écrire plusieurs livres décrivant le fonctionnement de tout ce processus. Mais cela devrait vous donner une idée.

81
David Z

Tout système de fichiers ou système d'exploitation dont vous voulez parler me convient très bien. Nice!


Sur un spectre ZX, l'initialisation d'une commande LOAD placera le système dans une boucle serrée, lisant la ligne d'entrée audio.

Le début des données est indiqué par une tonalité constante, suivie d'une séquence d'impulsions longues/courtes, une impulsion courte correspondant à un binaire 0 et une plus longue à un binaire 1 ( https://en.wikipedia.org/wiki/ZX_Spectrum_software ). La boucle de charge serrée rassemble des bits jusqu'à remplir un octet (8 bits), la stocke en mémoire, augmente le pointeur de mémoire, puis revient en boucle pour rechercher d'autres bits.

En règle générale, la première chose qu’un chargeur lit, c’est un bref en-tête de format fixe , indiquant au moins le nombre d’octets à attendre et éventuellement des informations supplémentaires telles que comme nom de fichier, type de fichier et adresse de chargement. Après avoir lu ce court en-tête, le programme pourrait décider de continuer le chargement de 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 numéro de variable comme 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 de contexte sur cette réponse

La procédure décrite charge les données d'une bande audio normale - d'où la nécessité de balayer l'entrée audio (connectée avec une prise standard à des magnétophones). Une commande LOAD est techniquement identique à open un fichier - mais elle est liée physiquement au chargement du fichier. En effet, le magnétophone n'est pas contrôlé par l'ordinateur et vous ne pouvez pas (correctement) ouvrir un fichier sans le charger.

La "boucle étroite" est mentionnée car (1) le processeur, un Z80-A (si la mémoire le permet), était très lent: 3,5 MHz, et (2) le spectre n'avait pas d'horloge interne! Cela signifie qu’il devait tenir précisément le compte des états T (temps d’instruction) pour chaque. Célibataire. instruction. à l'intérieur de cette boucle, juste pour maintenir la synchronisation précise des bips sonores.
Heureusement, cette faible vitesse du processeur présente l’avantage de pouvoir calculer le nombre de cycles sur un morceau de papier, et donc le temps réel qu’ils prendraient.

68
usr2564301

Tout dépend du système d'exploitation, de 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. Vous pouvez vérifier le code source si vous souhaitez plus de détails. Je ne couvre pas les autorisations car cela rendrait la 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 sur le fichier, les horodatages et le pointeur sur les blocs de disque, mais pas le nom du fichier lui-même. Chaque fichier (et répertoire) contient une entrée de nom de fichier et le numéro d'inode à rechercher. Lorsque vous ouvrez un fichier, en supposant que vous disposiez 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 possède 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 égal à un. S'il possède un lien physique, son nombre de liens sera égal à deux et si un fichier est ouvert par un processus, le nombre de liens est incrémenté de 1.

17
Jaco

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

Mais tout cela concerne le noyau - à moins que vous n'utilisiez votre propre système d'exploitation jouet, il n'y a pas grand chose à explorer (si vous y êtes, amusez-vous - c'est une excellente expérience d'apprentissage). Bien sûr, vous devez tout de même connaître 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’il vous donne un handle au 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? Bien sûr, mais utiliser une poignée vous procure certains avantages:

  • Le système peut garder une trace de tous les fichiers actuellement ouverts et empêcher leur suppression (par exemple).
  • Les systèmes d’exploitation modernes sont construits autour de poignées - il existe une multitude de choses utiles que vous pouvez faire avec les poignées, et tous les types de poignées se comportent de manière presque identique. Par exemple, lorsqu'une opération d'E/S asynchrone se termine sur un descripteur de fichier Windows, celui-ci est signalé. Cela vous permet de bloquer le traitement jusqu'à ce qu'il soit signalé ou de terminer l'opération de manière totalement asynchrone. Attendre un descripteur de fichier revient exactement à attendre un descripteur de thread (signalé, par exemple, à la fin du thread), un descripteur de processus (à nouveau, signalé à la fin du processus) ou un socket (à la fin de certaines opérations asynchrones). De manière tout aussi importante, les descripteurs sont la propriété de leurs processus respectifs. Ainsi, lorsqu'un processus est arrêté de manière inattendue (ou que l'application est mal écrite), le système d'exploitation sait quels traitements il peut libérer.
  • La plupart des opérations sont positionnelles - vous read à partir de la dernière position de votre fichier. En utilisant un descripteur pour identifier une "ouverture" particulière d'un fichier, vous pouvez avoir plusieurs descripteurs simultanés dans le même fichier, chacun lisant à partir de leurs propres emplacements. D'une certaine manière, le handle agit comme une fenêtre mobile dans le fichier (et un moyen d'émettre des requêtes d'E/S asynchrones, qui sont très pratiques).
  • Les poignées sont beaucoup plus petites que les noms de fichiers. Un handle est généralement la taille d'un pointeur, généralement 4 ou 8 octets. D'autre part, les noms de fichiers peuvent avoir des centaines d'octets.
  • Les poignées permettent au système d'exploitation de déplacer le fichier, même si les applications l'ont ouvert - la poignée est toujours valide et pointe toujours sur le même fichier, même si le nom de fichier a été modifié.

Vous pouvez également effectuer d’autres astuces (par exemple, partager les descripteurs entre processus pour disposer d’un canal de communication sans en utilisant un fichier physique; sur les systèmes unix, les fichiers sont également utilisés pour les périphériques et divers autres canaux virtuels, ce n'est donc pas strictement nécessaire), mais ils ne sont pas vraiment liés à l'opération open elle-même, je ne vais donc pas approfondir cette question.

11
Luaan

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

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

Le système d'exploitation prend souvent une longueur d'avance sur la lecture en commençant une opération de lecture pour remplir le tampon associé au descripteur. Ensuite, lorsque vous effectuez la lecture, le contenu du tampon peut être renvoyé immédiatement plutôt que de devoir 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 poignée est créée sur laquelle vous pouvez émettre les commandes d'écriture.

7
ratchet freak

En gros, un appel à ouvrir doit rechercher le fichier, puis enregistrer ce dont il a besoin pour que les opérations d'E/S ultérieur puissent le retrouver. C'est assez vague, mais ce sera le cas pour tous les systèmes d'exploitation auxquels je peux penser immédiatement. Les spécificités varient d'une plateforme à l'autre. De nombreuses réponses évoquent déjà les systèmes d’exploitation de bureau modernes. J'ai fait une petite programmation sur CP/M, je vais donc proposer mes connaissances sur son fonctionnement sur CP/M (MS-DOS fonctionne probablement de la même manière, mais pour des raisons de sécurité, cela ne se fait pas normalement aujourd'hui ).

Sur CP/M, vous avez un élément appelé FCB (comme vous l'avez mentionné, vous pouvez l'appeler une structure; il s'agit en réalité d'une zone contiguë de 35 octets dans RAM contenant divers champs). Le FCB a des champs pour écrire le nom du fichier et un entier (4 bits) identifiant le lecteur de disque. Ensuite, lorsque vous appelez le fichier ouvert du noyau, vous passez un pointeur sur cette structure en la plaçant dans l'un des registres de la CPU. Quelque temps plus tard, le système d'exploitation revient avec la structure légèrement modifiée. Quelle que soit l'entrée/sortie que vous fassiez sur ce fichier, vous passez un pointeur sur cette structure à 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. Vous feriez donc mieux de ne pas les toucher de l'intérieur de votre programme. L'opération Open File 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 le ou les emplacements physiques du fichier sur le disque, de sorte que les appels d'E/S ultérieurs appellent le BIOS qui peut éventuellement transférer ces emplacements au pilote de disque. A ce niveau, les spécificités varient.

5
Wilson