Canalblog
Editer l'article Suivre ce blog Administration + Créer mon blog
Publicité
INGENIEUR 3A JOUR PIGIER 2005
22 mars 2005

Unix

Cours Système D.Revuz Résumé : Cours de conception de systèmes et d'utilistion d'UNIX Ce poly est a l'usage des etudiants de l'ESITCOM et du deuxième cycle d'informatique de Marne la Vallée comme support du cours SYSTÈMES d'EXPLOITATION. Cette version 2, apportte de nombreuse correction de typo et autre, je remercie D. Lecorfec Pour sa lecture attentive, et les remarques sur le fond seront prises en compte dans la prochaine version. Ce poly a une version HTML disponible sur le Web a l'adresse suivante : http://www-igm.univ-mlv.fr/~dr/NCS/. Ce document a de nombreux défauts en particulier sont manque d'homogénéité, et le manque d'explications sur certaines parties (explication données en général oralement en cours). Au menu l'essentiel d'UNIX : SGF, processus, signaux, mémoire, mémoire virtuelle, manipulation terminaux, tubes, IPC. Quelques détours : micro-noyaux, sécurité. Un chapitre important mais un peut court : les problèmes de programation distribué. Peut être une petit incursion sur les pages de G. Roussel vous en apprendrons plus, sur les threads, les serveurs, JAVA etc . http://massena.univ-mlv.fr/ roussel Prérequis : pour la partie conceptuelle pas de prérequis. pour la partie technique une compétance raisonable en C/C++ est nécessaire. Évolutions futures : dr@univ-mlv.fr (j'attend vos remarques), uniformisation de la présentation, nétoyage des points obscurs, corrections orthographiques, complement sur fcntl, ioctl, plus d'exemples, des sujets de projets . Chapitre 1 Bibliographie J.-M. Rifflet. La programation sous UNIX. Ediscience, 1993. Le manuel de référence. A.Tanenbaum. Systèmes d'exploitation, sysytèmes centralisés, systèmes distribués. Inter-Editions, 1994. Cours général sur les sytèmes d'exploitation. M. Bach. The design of the UNIX operating system. 1986. Prentice-Hall, Englewood Cliffs, N.J. ISBN 0-13-201757-1 J. Beauquier & B. Bérard Systèmes d'exploitation concepts et algorithmes. 1991. McGraw-Hill. ISBN 2-7042-1221-X W.R. Stevens, UNIX Network Programming. 1990 Prentice-Hall, Englewood Cliffs, N.J. W.R. Stevens, Advanced Programming in the UNIX Environnement Addison-Wesley ISBN 0-201-56317-7 Chapitre 2 Introduction et Historique 2.1 Historique 2.1.1 les débuts (1945-55) L'ENIACENIAC soit 20000 tubes à vide, fait 20 tonnes et occupe 160 m2. Chaque Calculateur est unique et une équipe travaille à la fois à la fabrication, la programmation, la maintenance et l'utilisation. Ce sont des machines sans mémoire, exécutant un seul programme à la fois. Le chargement des programmes et des données se fait au mieux avec des cartes ou des bandes perforées. Durant cette période de nombreuses tâches sont automatisées, chargement, assemblage, édition de liens (avec des bibliothèques). Plus tard sont développés les compilateurs permettant d'utiliser des langages de plus haut niveau. 2.1.2 Transistors et traitement par lots 1955-65 Invention de la mémoire ¾® Fabrication industrielle, commercialisation de machines. Une séance-type de programmation : écriture sur cartes [programmeur] chargement des cartes compilateur [opérateur] chargement des cartes du programme création du code intermédiaire (assemblage) chargement des cartes de l'assembleur création du code en langage machine exécution du programme un problème : à chaque erreur réalisation d'un dump (listing de l'image mémoire) pour le programmeur, qui n'est pas sur place pour réaliser une correction à chaud (pas de périphériques interactifs trop coûteux). Solutions : le traitement par lots (batch proccessing)traitement par lots pour regrouper et exécuter par groupes les travaux similaires le moniteur résidentmoniteur résident qui enchaîne automatiquement les travaux, un nouveau type de cartes est introduit, qui donne des instructions à réaliser par le moniteur, (charger, exécuter, etc.). Ce moniteur résidant est l'ancêtre des systèmes d'exploitation, (la mémoire qui est apparue avec les transistors est découpée en deux zones : moniteur, utilisateur). La différence de vitesse entre les E/S et l'unité centrale (UC) devient flagrante : les périphériques sont alors dotés de circuits autonomes leur permettant de faire certains traitements sans le secours de l'UC. Ces circuits sont parfois des ordinateurs plus petits (dont le coût du temps de calcul est plus faible). 2.1.3 VLSI et Multiprogrammation 1965-80 Circuits intégrés ¾® moindre coût de fabrication ¾® évolution rapide des modèles ¾® vente aux entreprises IBM lance l'idée d'un système unique adapté à plusieurs machines : OS/360. Arrivée des disques magnétiques. Apparait le principe de multiprogrammation, les entrées-sorties sur disque étant effectuées de façon asynchrone avec des calculs sur l'unité centrale (parallèlisation). Multiprogrammation : plusieurs programmes sont en même temps en mémoire et sur le disque, si le programme en cours d'exécution demande une opération d'entrée-sortie alors un autre programme est exécuté pendant que se déroule l'opération. 2.1.4 UNIX 1969 Ken Thompson, Dennis Ritchie 1971 + Brian Kernighan 1973 C 1984 100000 cpu /UNIX 1993 UNIX est le système de référence Avec de nombreux standards AES, SVID2, XPG2, XPG3, XPG4, POSIX.1 OSF et des innovations comme : les micro-noyaux, MACH, CHORUS, MASIX Des copies : Windows NT par exemple ... Le succès d'UNIX sans doute parce que : Ecrit dans un langage de haut niveau : C (C++, Objective C) ; une interface simple et puissante : les shells, qui fournissent des services de haut niveau ; Des primitives puissantes qui permettent de simplifier l'écriture des programmes ; Un système de fichier hiérarchique qui permet une maintenance simple et une implémentation efficace ; Un format générique pour les fichiers, le flot d'octets qui simplifie l'écriture des programmes ; Il fournit une interface simple aux périphériques ; Il est multi-utilisateurs et multi-tâches ; Il cache complètement l'architecture de la machine à l'utilisateur. 2.1.5 Des points forts Système né dans le monde de la recherche intégration de concepts avancés Diffusion ouverte accès aux sources Langage (de haut niveau ) compilation séparée, conditionnelle, paramétrage, précompilation Enrichissement constant Ouverture (paramétrabilité du poste de travail) Souplesse des entrées/sorties uniformité Facilités de communication inter-systèmes Communautés d'utilisateurs (/etc/groups) Langages de commandes (flexibles et puissants) Aspect multi-utilisateurs connections de tout type de terminal, bibliothèques, etc Parallélisme multi-tâches : "scheduling" par tâche communication entre tâches multiprocesseurs Interface système/applications appels système, bibliothèque le système de gestion de fichiers hiérarchie Interfaces graphiques normées : X11. 2.1.6 Des points faibles Sécurité (pire encore en réseau ) Amélioration avec les A.C.L. Fragilité du S.G.F. pertes de fichiers possible en cas de crash Gestion et rattrapage des interruptions pas de temps réel (Q.N.X.). Mécanisme de création de processus lourd Amélioration avec les threads. Une édition de liens statique Amélioration avec les librairies partagées. Rattrapage d'erreur du compilateur C standard peu aisé ! Coût en ressources Gestion ¾® verrous sur fichiers 2.2 Structure générale des systèmes d'exploitation Un système d'exploitation est un programme qui sert d'interface entre un utilisateur et un ordinateur. Un système d'exploitation est un ensemble de procédures manuelles et automatiques qui permet à un groupe d'utilisateurs de partager efficacement un ordinateur. Brinch Hansen. Il est plus facile de définir un système d'exploitation par ce qu'il fait que par ce qu'il est. J.L. Peterson. Un système d'exploitation est un ensemble de procédures cohérentes qui a pour but de gérer la pénurie de ressources. J-l. Stehlé P. Hochard. Quelques systèmes : le batch Le traitement par lot (disparus). interactifs Pour les utilisateurs (ce cher UNIX). temps réels Pour manipuler des situations physiques par des périphériques (OS9 un petit frère futé d'UNIX). distribués UNIX?, les micros noyaux? l'avenir? moniteurs transactionnels Ce sont des applications qui manipulent des objets à tâches multiples comme les comptes dans une banque, des réservations, etc SE orientés objets Micro Noyaux. 2.2.1 Les couches fonctionnelles -------------------------------------------------------------------------------- Figure 2.1 : Vue générale du système -------------------------------------------------------------------------------- Couches fonctionnelles : Programmes utilisateurs Programmes d'application éditeurs/tableurs/BD/CAO Programmes système assembleurs/compilateurs/éditeurs de liens/chargeurs système d'exploitation langage machine microprogramme machines physiques 2.2.2 L'architecture du système -------------------------------------------------------------------------------- Figure 2.2 : Point de vue utilisateur -------------------------------------------------------------------------------- L'architecture globale d'UNIX est une architecture par couches (coquilles) successsives comme le montre la figure 2.2. Les utilisateure communiquent avec la couche la plus évoluée celle des applications. Le programmeur lui va pouvoir en fonction de ces besoins utiliser des couches de plus en plus profondes. Chaque couche est construite pour pouvoir être utilisée sans connaitre les couches inférieures (ni leur fonctionnement, ni leur interface). Cette hiérarchie d'encapsulation permet d'écrire des applications plus portables si elles sont écrites dans les couches hautes. Pour des applications où le temps de calcul prime devant la portabilité, les couches basses seront utilisées. 2.2.3 L'architecture du noyau -------------------------------------------------------------------------------- Figure 2.3 : Architecture du noyau -------------------------------------------------------------------------------- L'autre approche architecturale est l'architecture interne du Noyau (kernel).noyau C'est à dire l'architecture du programme qui va nous interfacer avec le matériel. Le but ici est de simplifier la compréhension et la fabrication du système. Nous cherchons donc ici à décomposer le noyau en parties disjointes (qui sont concevables et programmables de façons disjointes). La Figure 2.3 donne une idée de ce que peut être l'architecture interne d'un noyau UNIX. Noter bien la position extérieure des bibliothèques bibliothèques. Chapitre 3 Système de Gestion de Fichiers Système de Gestion de Fichiers Le système de gestion de fichiers est un outil de manipulation des fichiers et de la structure d'arborescence des fichiers sur disque et a aussi le rôle sous UNIX de conserver toutes les informations dont la pérennité est importante pour le système. Ainsi tous les objets importants du système sont référencés dans le système de fichiers (mémoire, terminaux, périphériques variés, etc). Il permet de plus une utilisation facile des fichiers et gère de façon transparente les différents problèmes d'accès aux supports de masse (partage, débit, droits, etc). 3.1 Le concept de fichier fichier L'unité logique de base du S.G.F. le fichier. Le contenu est entièrement défini par le créateur. Sur Unix les fichiers ne sont pas typés. Un fichier Unix est une suite finie de bytes (octets). Matérialisé par une inode et des blocs du disque. L'inode définit le fichier, soit principalement les informations : le propriétaire et le groupe propriétaire, propriétaire groupe les droits d'accès des différents utilisateurs,droits la taille, la date de création, la localisation sur disque. on trouvera sur d'autre systèmes d'autres structures d'information pour décrire les fichiers. Un nom est lié à un fichier (une référence indique un fichier) mais un fichier n'est pas lié à une référence, un fichier peut exister sans avoir de nom dans l'arborescence.référence 3.2 Fichiers ordinaires / Fichiers spéciaux. fichier!ordinairesfichier!spéciaux On a deux grands types de fichiers sous Unix : les fichiers standards que sont par exemple les fichiers texte, les exécutables, etc. C'est-à-dire tout ce qui est manipulé par les utilisateurs. Les fichiers spéciaux périphériques, mémoire etc, qui ne sont manipulables que par l'intermédiaire du système. Les catalogues sont des fichiers spéciaux, il faut en effet pour les manipuler physiquement faire appel au système 1. Les fichiers physiques dans le répertoire /dev fichier!physiques Character devices les terminaux (claviers, écrans) les imprimantes etc Block devices la mémoire les disques les bandes magnétiques etc Les fichiers à usages logiques et non physiques liens symboliques pseudo-terminaux sockets tubes nommés Ce dernier type de fichiers spéciaux est utilisé pour servir d'interface entre disques, entre machines et simuler : des terminaux, des lignes de communication, etc. 3.2.1 Les catalogues (historique) Les arborescences de fichiers et de catalogues, organisées comme un graphe acyclique 2, apparaissent avec le projet MULTICS. Cette organisation logique du disque a les avantages suivants : -------------------------------------------------------------------------------- Figure 3.1 : l'arborescence MULTICS -------------------------------------------------------------------------------- Une racine, un accès absolu aisé. Une structure dynamique. Une grande puissance d'expression. Un graphe acyclique. L'organisation est arborescente avec quelques connections supplémentaires (liens multiples sur un même fichier) qui en font un graphe. Mais ce graphe doit rester acyclique, pour les raisons suivantes : L'ensemble des algorithmes simples utilisables sur des graphe acycliques comme le parcours, la vérification des fichiers libres, etc. deviennent beaucoup plus difficiles à écrire pour des graphes admettant des cycles. Des algorithmes de ramasse-miettes doivent être utilisés pour savoir si certains objets sont utilisés on non et pour récuperer les inodes ou blocs perdus après un crash. Tous les algorithmes de détection dans un graphe quelconque ont une complexité beaucoup plus grande que ceux qui peuvent profiter de l'acyclicité du graphe. Sous Unix nous sommes assurés que le graphe est acyclique car il est interdit d'avoir plusieurs références pour un même catalogue (sauf la référence spéciale ".." ). Sous UNIX c'est un graphe acyclique ! 3.3 Les inodes inodesfichiers!inodes L'inode est le centre de tous les échanges entre le disque et la mémoire. L'inode est la structure qui contient toutes les informations sur un fichier donné à l'exception de sa référence, dans l'arborescence. Les informations stockées dans une inode disque sont : utilisateur propriétaire propriétaire groupe propriétairegroupe type de fichier droits d'accès date de dernier accès date de dernière modification date de dernière modification de l'inode taille du fichier adresses des blocs-disque contenant le fichier. Dans une inode en mémoire (fichier en cours d'utilisation par un processus) on trouve d'autres informations supplémentaires : le statut de l'inode { locked, waiting P inode à écrire, fichier à écrire, le fichier est un point de montage } Et deux valeurs qui permettent de localiser l'inode sur un des disques logiques : Numéro du disque logique Numéro de l'inode dans le disque cette information est inutile sur le disque (on a une bijection entre la position de l'inode sur disque et le numéro d'inode). On trouve aussi d'autres types d'informations comme l'accès à la table des verrous ou bien des informations sur les disques à distance dans les points de montage. 3.4 Organisation des disques System V L'organisation disque décrite sur la figure 3.2 est la plus simple que l'on peut trouver de nos jours sous UNIX, il en existe d'autres (cf. section 3.8) où l'on peut en particulier placer un même disque logique sur plusieurs disques physiques (dangereux), certaines où les blocs sont fragmentables, etc. -------------------------------------------------------------------------------- Figure 3.2 : Organisation des blocs et des inodes (SYS V) -------------------------------------------------------------------------------- Boot bloc utilisé au chargement du système.boot bloc Super Bloc il contient toutes les informations générales sur le disque logique.super bloc Inode list Table des inodes. blocs les blocs de données chainés à la création du disque (mkfs). Les blocs de données ne sont pas fragmentables sous Système V. 3.5 Adressage des blocs dans les inodes Le système d'adressage des blocs dans les inodes (système V) consiste en 13 adresses de blocs. Les dix premières adresses sont des adresses qui pointent directement sur les blocs de données du fichier. Les autres sont des adresses indirectes vers des blocs de données contenant des adresses. La figure 3.3 nous montre les trois niveaux d'indirection. L'intérêt de cette représentation est d'économiser sur la taille des inodes tout en permettant un accès rapide au petits fichiers (la majorité des fichiers sont petits). Mais en laissant la possibilité de créer de très gros fichiers : 10+256+(256 × 256 )+( 256 × 256 × 256) blocs disques. -------------------------------------------------------------------------------- Figure 3.3 : Adressage direct et indirect des inode UNIX -------------------------------------------------------------------------------- inodes 3.6 Allocation des inodes d'un disque inodes L'allocation des inodes est réalisée en recherchant dans la zone des inodes du disque une inode libre. Pour accélérer cette recherche : un tampon d'inodes libres est géré dans le SuperBloc, de plus l'indice de la première inode libre est gardé en référence dans le SuperBloc afin de redémarrer la recherche qu'à partir de la première inode réellement libre. -------------------------------------------------------------------------------- Figure 3.4 : Inodes libres dans le SuperBloc. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 3.5 : Allocation d'une inode. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 3.6 : Si le SuperBloc est vide. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 3.7 : Libération d'une inode avec le SuperBloc plein. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 3.8 : Le numéro d'inode inférieur au numéro de référence. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 3.9 : Le numéro d'inode supérieur au numéro de référence. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 3.10 : Faille de l'algorithme d'allocation. -------------------------------------------------------------------------------- Mais ce système a une faille qu'il faut prévoir dans l'écriture dans l'algorithme ialloc d'allocation d'inode, cette faille est décrite dans la Figure 3.10 3.7 Allocation des blocs-disque L'algorithme utilisé pour gérer l'allocation des inodes s'appuie sur le fait que l'on peut tester si une inode est libre ou non en regardant son contenu. Ceci n'est plus vrai pour les blocs. La solution est de chaîner les blocs. Ce chaînage est réalisé par blocs d'adresses pour accélérer les accès et profiter au maximum du buffer cache. Il existe donc un bloc d'adresses dans le super bloc qui sert de zone de travail pour l'allocateur de blocs. L'utilisation de ce bloc et le mécanisme d'allocation sont décrits dans les Figures 3.11 à 3.16 -------------------------------------------------------------------------------- Figure 3.11 : Liste chainée de blocs. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 3.12 : Etat initial du SuperBloc. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 3.13 : Libération du bloc 978. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 3.14 : Allocation du bloc 978. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 3.15 : Allocation du bloc 109. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 3.16 : Libération du bloc 612. -------------------------------------------------------------------------------- 3.8 Les systèmes de fichiers ffs/ufs de BSD ffs Les disques sous BSD sont organisés par groupes de cylindres et chacun de ces groupes a la même organisation que les disques logiques System V, avec en plus une table de groupes de cylindres qui permet d'organiser l'allocation des blocs de façon à réduire le déplacement des têtes de lecture (ce qui augmente le débit). Quelques différences : Les blocs de données sont plus grands (4K ou 8K) mais fragmentables. Une inode contient 12 adresses directes, une adresse indirecte et 2 adresses indirectes doubles. Enfin, les répertoires sont composés d'enregistrements de tailles variables (le nom des liens est en effet limité à 14 en System V, et à 255 en BSD, c.f. entrées-sorties sur répertoires), la norme POSIX fixe la taille maximum des liens à 255 (MAXNAMLEN). -------------------------------------------------------------------------------- 1 les répertoires restent accessibles en lecture comme des fichiers ordinaires (essayez de faire cat "."), mais l'accès en écriture est contraint, pour assurer la structure arborescente. 2 Ce n'est pas un arbre car un fichier peut avoir plusieurs références Chapitre 4 Le Buffer Cache 4.1 Introduction au buffer cache buffer cache Le buffer cache est un ensemble de structures de données et d'algorithmes qui permettent de minimiser le nombre des accès disque. Ce qui est très important car les disques sont très lents relativement au CPU et un noyau qui se chargerait de toutes les entrées/sorties serait d'une grande lenteur et l'unité de traitement ne serait effectivement utilisée qu'à un faible pourcentage (voir Historique). Deux idées pour réduire le nombre des accès disques : bufferiser les différentes commandes d'écriture et de lecture de façon à faire un accès disque uniquement pour une quantité de données de taille raisonnable (un bloc disque).bufferiser Eviter des écritures inutiles quand les données peuvent encore être changées (écriture différées). 4.1.1 Avantages et désavantages du buffer cache Un accès uniforme au disque. Le noyau n'a pas à connaître la raison de l'entrée-sortie. Il copie les données depuis et vers des tampons (que ce soient des données, des inodes ou le superbloc). Ce mécanisme est modulaire et s'intègre facilement à l'ensemble du système qu'il rend plus facile à écrire. Rend l'utilisation des entrées-sorties plus simple pour l'utilisateur qui n'a pas à se soucier des problèmes d'alignement, il rend les programmes portables sur d'autres UNIX 1. Il réduit le trafic disque et de ce fait augmente la capacité du système. Attention : le nombre de tampons ne doit pas trop réduire la mémoire centrale utilisable. L'implémentation du buffer cache protège contre certaines écritures "concurrentes" L'écriture différée pose un problème dans le cas d'un crash du système. En effet si votre machine s'arrête (coupure de courant) et que un (ou plusieurs) blocs sont marqués "à écrire" ils n'ont donc pas étés sauvegardés physiquement. L'intégrité des données n'est donc pas assurée en cas de crash. Le buffer cache nécessite que l'on effectue une recopie (interne à la mémoire, de la zone utilisateur au cache ou inversement) pour toute entrée-sortie. Dans le cas de transferts nombreux ceci ralentit les entrées-sorties . 4.2 Le buffer cache, structures de données. -------------------------------------------------------------------------------- Figure 4.1 : Structure des entêtes de Bloc du Buffer Cache -------------------------------------------------------------------------------- Le statut d'un bloc cache est une combinaison des états suivants : verrouillé l'accès est reservé à un processus. valide (les données contenues dans le bloc sont valides). "à écrire" les données du bloc doivent être écrites sur disque avant de réallouer le bloc ( c'est de l'écriture retardée). actif le noyau est en train d'écrire/lire le bloc sur le disque. attendu un processus attend la libération du bloc. 4.2.1 La liste doublement chaînée des blocs libres Les tampons libres appartiennent simultanément à deux listes doublement chaînées : la liste des blocs libres et la hash-liste correspondant au dernier bloc ayant été contenu dans ce tampon. -------------------------------------------------------------------------------- Figure 4.2 : La liste des tampons libres. -------------------------------------------------------------------------------- L'insertion dans la liste des tampons libres se fait en fin de liste, la suppression (allocation du tampon à un bloc donné) se fait en début de liste, ainsi le tampon alloué est le plus vieux tampon libéré2. Ceci permet une réponse immédiate si le bloc correspondant est réutilisé avant que le tampon ne soit alloué à un autre bloc. 4.3 L'algorithme de la primitive getblk Algorithme getblk (allocation d'un tampon) entree : # disque logique , # de block sortie : un tampon verrouille utilisable pour manipuler bloc { while (tampon non trouve) { if (tampon dans sa hash liste) { if (tampon actif ) { [5] sleep attente de la liberation du tampon continuer } [1] verrouiller le tampon retirer le tampon de la liste des tampons libres retourner le tampon } else /* n'est pas dans la hash liste */ { if (aucun tampon libre ) { [4] sleep attente de la liberation d'un tampon continuer } retirer le tampon de la liste libre [3] if (le tampon est a ecrire) { lancer la sauvegarde sur disque continuer } [2] retirer le buffer de son ancienne liste de hashage, le placer sur la nouvelle retourner le tampon } } } -------------------------------------------------------------------------------- Figure 4.3 : Etat du buffer cache avant les scénarios 1, 2 et 3. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 4.4 : Scénario 1- Demande d'un tampon pour le bloc-disque 4. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 4.5 : Scénario 2- Demande d'un tampon pour le bloc-disque 41. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 4.6 : Scénario 3- Demande pour le bloc 18 (3 & 5 marqués à écrire). -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 4.7 : Scénario 4- Plus de blocs libres. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Figure 4.8 : Scénario 5- Demande pour le bloc 17 qui est déjà utilisé. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- 1 Les problèmes d'alignement existent toujours quand on transfère des données, cf. protocoles XDR,RPC 2 ordre fifo : first in first out Chapitre 5 La bibliothèque standard 5.1 Les descripteurs de fichiers. Le fichier d'inclusion contient la définition du type FILE. stdio.h@stdio.h Ce type est une structure contenant les informations nécessaires au système pour la manipulation d'un fichier ouvert. Le contenu exact de cette structure peut varier d'un système à l'autre (UNIX, VMS, autre). Toutes les fonctions d'E/S utilisent en premier argument un pointeur sur une telle structure : FILE *. FILE@FILE Le rôle de cet argument est d'indiquer le fichier sur lequel on doit effectuer l'opération d'écriture ou de lecture. Pour pouvoir utiliser une fonction d'entrée-sortie il faut donc avoir une valeur pour ce premier argument, c'est le rôle de la fonction fopen de nous fournir ce pointeur en "ouvrant" le fichier. stdlib!printf@printf stdlib!scanf@scanf Les deux fonctions printf et scanf sont des synonymes de fprintf(stdout, format, ...) et fscanf(stdin, format, ...) où stdout et stdin sont des expressions de type FILE * définies sous forme de macro-définitions dans le fichier . Avec POSIX ce sont effectivement des fonctions. Sous UNIX les fichiers ouverts par un processus le restent dans ses fils. Par exemple le shell a en général trois fichiers ouverts : stdin le terminal ouvert en lecture. stdlib!stdin@stdin stdout le terminal ouvert en écriture. stdlib!stdout@stdout stderr le terminal ouvert en écriture, et en mode non bufferisé. stdlib!stderr@stderr ainsi si l'exécution d'un programme C est réalisée à partir du shell le programme C a déjà ces trois descripteurs de fichiers utilisables. C'est pourquoi il est en général possible d'utiliser printf et scanf sans ouvrir préalablement de fichiers. Mais si l'entrée standard n'est pas ouverte, scanf échoue : #include main() { int i; if (scanf("%d", &i) == EOF) { printf("l\'entree standard est fermee\n"); } else { printf("l\'entree standard est ouverte\n"); } } Compilé,(a.out), cela donne les deux sorties suivantes : $ a.out l'entree standard est ouverte $ a.out <&- # fermeture de l'entree standard en ksh l'entree standard est fermee De même printf échoue si la sortie standard est fermée. 5.1.1 Ouverture d'un fichier La fonction de la bibliothèque standard fopen permet d'ouvrir un fichier ou de le créer. #include FILE *fopen(const char *filename, const char *type); stdlib!fopen@fopen filename est une référence absolue ou relative du fichier à ouvrir; si le fichier n'existe pas alors il est créé si et seulement si l'utilisateur du processus a l'autorisation d'écrire dans le répertoire. type est une des chaînes suivantes : "r" ouverture en lecture au début du fichier "w" ouverture en écriture au début du fichier avec écrasement du fichier si il existe (le fichier est vidé de son contenu à l'ouverture). "a" ouverture en écriture à la fin du fichier (mode append). "r+","w+","a+" ouverture en lecture écriture respectivement au début du fichier, au début du fichier avec écrasement, à la fin du fichier. FILE *f; ... if ((f = fopen("toto", "r")) == NULL) { fprintf(stderr, "impossible d'ouvrir toto\n"); exit(1); } ... La fonction retourne un pointeur sur un descripteur du fichier ouvert ou NULL en cas d'échec, (accès interdit, création impossible, etc). 5.1.2 Redirection d'un descripteur : freopen redirection stdlib!freopen@freopen Permet d'associer un descripteur déjà utilisé à une autre ouverture de fichier. Ceci permet de réaliser facilement les redirections du shell. FILE *freopen(const char *ref, const char *mode, FILE *f) Par exemple les redirections de la ligne shell : com >ref2 peuvent être réalisées avec if (!freopen("ref1", "r", stdin) || !freopen("ref2", "a", stdout)) { fprintf(stderr, "erreur sur une redirection\n"); exit(1); } execl("./com", "com", NULL); 5.1.3 Création de fichiers temporaires La fonction stdlib!tmpfile@tmpfile #include FILE *tmpfile(void); crée et ouvre en écriture un nouveau fichier temporaire, qui sera détruit (un unlink est réalisé immédiatement) à la fin de l'exécution du processus, attention le descripteur est hérité par les fils. Cette fonction utilise la fonction char *tmpnam(char *ptr); stdlib!tmpnam@tmpnam Cette fonction génère un nouveau nom de fichier et place celui-ci dans la zone pointée par ptr si ptr ¹ NULL, la zone réservée doit être d'au moins L_tmpnam octets. Si ptr = NULL une zone statique est utilisée. 5.1.4 Ecriture non formatée Les deux fonctions suivantes permettent d'écrire et de lire des zones mémoire, le contenu de la mémoire est directement écrit sur disque sans transformation, et réciproquement le contenu du disque est placé tel quel en mémoire. L'intérêt de ces fonctions est d'obtenir des entrées sorties plus rapides et des sauvegardes disque plus compactes mais malheureusement illisibles (binaire). #include int fwrite(void *add, size_t ta, size_t nbobjets, FILE *f); stdlib!fwrite@fwrite Ecrit nbobjets de taille ta qui se trouvent à l'adresse add dans le fichier de descripteur f. #include int fread(void *add, size_t ta, size_t nbobjets, FILE *f); stdlib!fread@fread Lit nbobjets de taille ta dans le fichier de descripteur f et les place à partir de l'adresse add en mémoire. Attention : La fonction fread retourne 0 si l'on essaye de lire au delà du fichier. Pour écrire une boucle de lecture propre on utilise la fonction feof(FILE *) : int n[2]; while (fread(n, sizeof(int), 2, f), !feof(f)) printf("%d %d \n", n[0], n[1]); stdlib!feof@feof 5.1.5 Accès séquentiel On distingue deux techniques d'accès aux supports magnétiques : accès séquentiel L'accès séquentiel qui consiste à traiter les informations dans l'ordre où elle apparaissent sur le support (bandes). Le lecteur physique avance avec la lecture, et se positionne sur le début de l'enregistrement suivant. L'accès direct qui consiste à se placer directement sur l'information sans parcourir celles qui la précèdent (disques). Le lecteur physique reste sur le même enregistrement après une lecture.accès direct En langage C l'accès est séquentiel mais il est possible de déplacer le "pointeur de fichier" c'est à dire sélectionner l'indice du prochain octet à lire ou écrire.pointeur de fichier Comme nous venons de le voir dans les modes d'ouverture, le pointeur de fichier peut être initialement placé en début ou fin de fichier. Les quatre fonctions d'entrée-sortie (fgetc, fputc, fscanf, fprintf) travaillent séquentiellement à partir de cette origine fixée par fopen. 5.1.6 Manipulation du pointeur de fichier Le pointeur de fichier est un entier long qui indique à partir de quel octet du fichier la prochaine fonction d'entrée-sortie doit s'effectuer. En début de fichier cet entier est nul. #include int fseek(FILE *f, long pos, int direction); stdlib!fseek@fseek f le descripteur du fichier dans lequel ont déplace le pointeur. direction est une des trois constantes entières suivantes : SEEK_SET positionnement sur l'octet pos du fichier SEEK_CUR positionnement sur le pos-ième octet après la position courante du pointeur de fichier. (équivalent à SEEK_SET courant+pos). SEEK_END positionnement sur le pos-ième octet après la fin du fichier. Remarquer que pos est un entier signé : il est possible se placer sur le 4ième octet avant la fin du fichier : fseek(f, -4L, SEEK_END); 5.1.7 Un exemple d'accès direct sur un fichier d'entiers. La fonction suivante lit le n-ième entier d'un fichier d'entiers préalablement écrit grâce à fwrite : int lirenieme(int n, FILE *f) { int buf; fseek(f, sizeof(int)*(n-1), SEEK_SET); fread(&buf, sizeof(int), 1, f); return buf; } \istd{fseek}\istd{fread} 5.1.8 Les autres fonctions de déplacement du pointeur de fichier. La fonction ftell long int ftell(FILE *); stdlib!ftell@ftell retourne la position courante du pointeur. La fonction rewind void rewind(FILE *f); stdlib!rewind@rewind équivalent à : (void) fseek (f, 0L, 0) 5.2 Les tampons de fichiers de stdlib. La bibliothèque standard utilise des tampons pour minimiser le nombre d'appels système. Il est possible de tester l'efficacité de cette bufferisation en comparant la vitesse de recopie d'un même fichier avec un tampon de taille 1 octet et un tampon adapté à la machine, la différence devient vite très importante. Une façon simple de le percevoir est d'écrire un programme com qui réalise des écritures sur la sortie standard ligne par ligne, de regarder sa vitesse puis de comparer avec la commande suivantes :com | cat la bibliothèque standard utilisant des buffer différents dans les deux cas une différence de vitese d'exécution est perceptible (sur une machine lente la différence de vitesse est évidente, mais elle existe aussi sur une rapide...). 5.2.1 Les modes de bufferisation par défaut. Le mode de bufferisation des fichiers ouverts par la bibliothèque standard dépend du type de périphérique. bufferisation Si le fichier est un terminal la bufferisation est faite ligne à ligne. En écriture le tampon est vidé à chaque écriture d'un '\n' , ou quand il est plein (première des deux occurences). En lecture le tampon est rempli après chaque validation (RC), si l'on tape trop de caractères le terminal proteste (beep) le buffer clavier étant plein. Si le fichier est sur un disque magnétique En écriture le tampon est vidé avant de déborder. En lecture le tampon est rempli quand il est vide. Le shell de login change le mode de bufferisation de stderr qui est un fichier terminal à non bufferisé. Nous avons donc à notre disposition trois modes de bufferisation standards : Non bufferisé (sortie erreur standard), Bufferisé par ligne (lecture/écriture sur terminal), Bufferisé par blocs (taille des tampons du buffer cache). Les terminaux acceptent d'autres modes de bufferisation plus complexes en entrée que nous étudierons avec les particularités de ces périphériques (chapître 12). Un exemple de réouverture de la sortie standard, avec perte du mode de bufferisation : #include main() { freopen("/dev/tty", "w", stderr); fprintf(stderr, "texte non termine par un newline "); sleep(12); exit(0); /* realise fclose(stderr) qui realise fflush(stderr) */ } stdlib!freopen@freopen Il faut attendre 12 secondes l'affichage. 5.2.2 Manipulation des tampons de la bibliothèque standard. Un tampon alloué automatiquement (malloc) est associé à chaque ouverture de fichier par fopen au moment de la première entrée-sortie sur le fichier. La manipulation des tampons de la bibliothèque standard comporte deux aspects : Manipulation de la bufferisation de façon ponctuelle (vidange). Positionnement du mode de bufferisation. Manipulations ponctuelles La fonction suivante permet de vider le tampon associé au FILE * f : #include fflush(FILE *f); stdlib!fflush@fflush En écriture force la copie du tampon associé à la structure f dans le tampon système (ne garantit pas l'écriture en cas d'interruption du système!). En lecture détruit le contenu du tampon, si l'on est en mode ligne uniquement jusqu'au premier caractère '\n'. La fonction fclose() réalise un fflush() avant de fermer le fichier. La fonction exit() appel fclose() sur tous les fichiers ouvert par fopen (freopen,tmpfile,...) avant de terminer le processus. stdlib!fclose@fclose Manipulations du mode de bufferisation et de la taille du tampon. La primitive int setvbuf(FILE *f, char *adresse, int mode, size_t taille); stdlib!setvbuf@setvbuf permet un changement du mode de bufferisation du fichier f avec un tampon de taille taille fourni par l'utilisateur à l'adresse adresse si elle est non nulle, avec le mode défini par les macro-définitions suivantes () : _IOFBF bufferise _IONBF Non bufferise _IOMYBUF Mon buffer _IOLBF bufferise par ligne (ex: les terminaux) Attention : Il ne faut pas appeler cette fonction après l'allocation automatique réalisée par la bibliothèque standard après le premier appel à une fonction d'entrée-sortie sur le fichier. Il est fortement conseillé que la zone mémoire pointée par adresse soit au moins d'une taille égale à taille. Seul un passage au mode bufferisé en ligne ou non bufferisé peut être réalisé après l'allocation automatique du tampon, au risque de perdre ce tampon (absence d 'appel de free). Ce qui permet par exemple de changer le mode de bufferisation de la sortie standard après un fork. Attention ce peut être dangereux, pour le contenu courant du tampon comme le montre l'exemple suivant. Avant cette fonction de norme POSIX on utilisait trois fonctions : void setbuf(FILE *f, char *buf); void setbuffer(FILE *f,char *adresse,size_t t); void setlignebuf(FILE *f); stdlib!setbuf@setbuf stdlib!setbuffer@setbuffer stdlib!setlignebuf@setlignebuf #include main() { printf("BonJour "); switch(fork()) { case -1 : exit(1); case 0 : printf("je suis le fils"); /* version 1 sans la ligne suivante version 2 avec */ setbuffer(stdout, NULL, 0); sleep(1); printf("Encore le fils"); break; default : printf("je suis le pere"); sleep(2); } printf("\n"); } version 1 fork_stdlib BonJour je suis le fils Encore le fils BonJour je suis le pere version 2 Encore le fils BonJour je suis le pere 5.3 Manipulation des liens d'un fichier Changer le nom d'un fichier : int rename(const char *de,const char *vers); stdlib!rename@rename permet de renommer un fichier (ou un répertoire). Il faut que les deux références soient de même type (fichier ou répertoire) dans le même système de fichiers. Rappel : ceci n'a d'effet que sur l'arborescence de fichiers. Détruire une référence : int remove(const char *filename); stdlib!remove@remove Détruit le lien donné en argument, le système récupère l'inode et les blocs associés au fichier si c'était le dernier lien. 5.4 Lancement d'une commande shell #include int system(const char *chaine_de_commande); stdlib!system@system Crée un processus ``/bin/posix/sh'' qui exécute la commande ; il y a attente de la fin du shell, (la commande peut elle être lancée en mode détaché ce qui fait que le shell retourne immédiatement sans faire un wait). Ce mécanisme est très coûteux. Attention la commande system bloque les signaux SIGINT et SIGQUIT, il faut analyser la valeur de retour de system de la même façons que celle de wait. Il est conseillé de bloquer ces deux signaux avant l'appel de system . 5.5 Terminaison d'un processus _exit La primitive de terminaison de processus de bas niveau : #include void _exit(int valeur); appels systèmes!_exit@_exit La primitive _exit est la fonction de terminaison "bas niveau" elle ferme les descripteurs ouverts par open, opendir ou hérités du processus père. la valeur est fournie au processus père qui la récupère par l'appel système wait. Cette valeur est le code de retour de processus en shell. Cette primitive est automatiquement appelée à la fin de la fonction main (sauf en cas d'appels récursifs de main). exit La fonction de terminaison de processus de stdlib : #include void exit(int valeur); stdlib!exit@exit la fonction exit : lance les fonctions définies par atexit. ferme l'ensemble des descripteurs ouverts grâce à la bibliothèque standard (fopen). détruit les fichiers fabriqués par la primitive tmpfile appelle _exit avec valeur. atexit La primitive atexit permet de spécifier des fonctions à appeler en fin d'exécution, elle sont lancées par exit dans l'ordre inverse de leur positionnement par atexit. #include int atexit(void (*fonction) (void )); stdlib!atexit@atexit Exemple : void bob(void) {printf("coucou\n");} void bib(void) {printf("cuicui ");} main(int argc) { atexit(bob); atexit(bib); if (argc - 1) exit(0); else _exit(0); } $ make atexit cc atexit.c -o atexit $ atexit $ atexit unargument cuicui coucou $ 5.6 Gestion des erreurs Les fonctions de la bibliothèque standard positionnent deux indicateurs d'erreur, la fonction suivante les repositionne : void clearerr(FILE *); stdlib!clearerr@clearerr stdlib!feof@feof La fonction int feof(FILE *) est vraie si la fin de fichier est atteinte sur ce canal, int ferror(FILE *) est vraie si une erreur a eu lieu pendant la dernière tentative de lecture ou d'écriture sur ce canal. Une description en langue naturelle de la dernière erreur peut être obtenue grace à void perror(const char *message); stdlib!perror@perror l'affichage se fait sur la sortie erreur standard (stderr). 5.7 Création et destruction de répertoires Création d'un répertoire vide (même syntaxe que creat) : #include int mkdir(char *ref, mode_t mode); stdlib!mkdir@mkdir Destruction : int rmdir(char *ref); stdlib!rmdir@rmdir avec les mêmes restrictions que pour les shells sur le contenu du répertoire (impossible de détruire un répertoire non vide). Chapitre 6 Appels système du Système de Gestion de Fichier Les appels système d'entrées-sorties ou entrées-sorties de bas niveau sont rudimentaires mais polymorphes, en effet c'est eux qui permettent d'écrire des programmes indépendamment des supports physiques sur lesquels se font les entrées/sorties et de pouvoir facilement changer les supports physiques associés a une entrée-sortie. Les appels système du système de gestion de fichier sont : appels systèmes!introduction@introduction open/creat ouverture/création d'un fichier read/write lecture/ecriture sur un fichier ouvert lseek déplacement du pointeur de fichier dup,dup2 copie d'ouverture de fichier close fermeture d'un fichier mount chargement d'un disque mknode création d'un inode de fichier spécial pipe création d'un tube fcntl manipulation des caractéristiques des ouvertures de fichiers Les appels système sont réalisés par le noyau et retournent -1 en cas d'erreur. -------------------------------------------------------------------------------- Figure 6.1 : Tables du système de fichiers. -------------------------------------------------------------------------------- 6.1 open #include int open(char *ref, int mode, int perm); appels systèmes!open@open Ouverture du fichier de référence (absolue ou relative à ".") ref. Le mode d'ouverture est une conjonction des masques suivants : O_RDONLY /* open for reading */ O_WRONLY /* open for writing */ O_RDWR /* open for read & write */ O_NDELAY /* non-blocking open */ O_APPEND /* append on each write */ O_CREAT /* open with file create */ O_TRUNC /* open with truncation */ O_EXCL /* error on create if file exists*/ Le paramètre permission n'a de sens qu'à la création du fichier, il permet de positionner les valeurs du champ mode de l'inode. Les droits effectivement positionnés dépendent de la valeur de umask, grace à la formule droits = perm & ~ umask. La valeur par défaut de umask est 066 (valeur octale). La valeur de retour de open est le numéro dans la table de descripteurs du processus qui a été utilisé par open. Ce numéro est appelé descripteur de l'ouverture. Ce descripteur est utilisé dans les autres appels système pour spécifier l'ouverture de fichier sur laquelle on veut travailler1, et -1 en cas d'échec de l'ouverture. 6.1.1 Déroulement interne d'un appel de open Le système détermine l'inode du fichier référence (namei). Soit l'inode est dans la table des inodes en mémoire. Soit il alloue une entrée et recopie l'inode du disque (iget). Le système vérifie les droits d'accès dans le mode demandé. Il alloue une entrée dans la table des fichiers ouverts du système, et positionne le curseur de lecture écriture dans le fichier (offset = 0, sauf dans le cas du mode O_APPEND offset=taille du fichier). Le système alloue une place dans la table des descripteurs _iob du fichier. Il renvoie au processus le numéro de descripteur, c'est à dire le numéro de l'entrée qu'il vient d'allouer dans le tableau _iob. Si l'opération a échoué dans une des étapes le système renvoie -1. -------------------------------------------------------------------------------- Figure 6.2 : Avant l'ouverture, descripteurs standard ouverts, puis après l'ouverture de ''toto''. -------------------------------------------------------------------------------- 6.2 creat Création d'un fichier et ouverture en écriture. int creat(char *reference, int permissions); appels systèmes!creat@creat Le système détermine l'inode du catalogue où l'on demande la création du fichier. Si il existe déjà une inode pour le fichier Le noyau lit l'inode en question (allocation dans la table des inodes en mémoire), vérifie que c'est un fichier ordinaire autorisé en écriture par le propriétaire effectif du processus, sinon échec. Le système libère les blocs de données et réduit la taille du fichier à zéro, il ne modifie pas les droits qu'avait le fichier antérieurement.
Publicité
Publicité
Commentaires
INGENIEUR 3A JOUR PIGIER 2005
Publicité
Publicité