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?
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:
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.
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:
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:
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.Stockez ("install") la structure retournée dans la liste des fichiers ouverts du processus.
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./dev/sda
, etc.).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.
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.
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.
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:
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).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.
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.
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.