Pourquoi ce document ?

Ce document sert de rangement fourre-tout à toutes les définitions des termes accessoires rencontrés au long du cours d'Unix et qui auraient considérablement alourdi la lecture de ce dernier en retardant inutilement le lecteur.

Voici, grosso modo, le plan retenu :

  1. Organisation des données dans un programme
    1. Les classes de stockage
    2. La pile
    3. Le tas
  2. Liaison statique vs liaison dynamique
    1. Liaison statique
    2. Liaison dynamique
  3. Manipulation des fichiers
    1. Primitives de bas niveau
    2. Primitives de haut niveau
    3. Accès à l'inode
    4. Bas niveau <=> Haut niveau

Organisation des données dans un programme

La classe de stockage des données

Les données utilisées par un programme peuvent être rangées en 3 grandes catégories en fonction de leur classe de stockage c'est à dire de leur emplacement de stockage et de leur pérennité dans le programme :

Données statiques :
Les données statiques, c'est à dire les variables (et constantes) globales du programme (comprenant également les membres statiques des classes C++, par exemple) et les variables locales à une fonction (ou une méthode) mais déclarées explicitement statiques (par l'emploi du mot clef static) afin que leur valeur soit sauvegardée d'un appel sur l'autre. Habituellement, ces données sont placées dans deux segments respectivement nommés BSS et DATA selon qu'elles ont été initialisées en dur ou non.
Données dynamiques :
Par opposition aux données statiques, les données dynamiques ne sont pas résentes à l'origine dans le programme dans l'exécutable mais allouées au cours de son exécution. Elle peuvent être crées sur la pile par un mécanisme (non portable) genre alloca ou sur le tas par une fonction de bas niveau (setbrk, brk, etc.) ou de haut niveau (famille de malloc ou new du C++).
Les variables automatiques :
Ce sont typiquement les variables locales des fonctions C ou des méthodes C++. Elles sont allouées sur la pile à l'entrée de la fonction et détruites immédiatement après.

La gestion de la pile

La gestion de la pile peut varier considérablement d'un système Unix à un autre. Dans la plupart des cas, la taille maximale de la pile est fixée en dur dans l'exécutable. Une option du lieur permet de la modifier. Dans d'autres cas, la taille de la pile est variable, celle-ci pouvant croître au cours de l'exécution.

La pile est utilisée dans plusieurs cas :

La gestion du tas

Le tas (heap) est l'espace mémoire du système non encore alloué. Il est manipulable par deux grandes catégories de primitives : celles de bas niveau ... et celles de haut niveau !

Afin de comprendre la différence, introduisons la notion de point de rupture (breakpoint). Celui-ci représente la plus grande valeur de pointeur valide dans l'espace d'adressage de votre processus. Toute modification de la taille de votre espace d'adressage passe par une augmentation du point de rupture, ce qui pourra se faire directement par l'intermédiaire des primitives de bas niveau ou indirectement par celles de haut niveau.

Les primitives de bas niveau

Elles permettent de modifier directement la valeur du point de rupture, soit en lui affectant une valeur (setbrk) soit en lui ajoutant un increment (sbrk). L'utilisation de la première forme est particulièrement ardue et doit être réservée aux utilisateurs les plus avertis. La deuxième forme est moins délicate et vous pouvez jouer avec pour en étudier les effets.

Les primitives de haut niveau

Ce sont toutes les fonctions de la famille [m|c|re]alloc ainsi que l'opérateur new du C++. Elles permettent une gestion plus efficace de la mémoire en implémentant, en particulier un mécanisme de prise en compte de la fragmentation. Lorsqu'une opération de haut niveau nécessite une augmentation de la taille de l'espace d'adressage, la fonction de bas niveau setbrk est appelée.

La liaison des bibliothèques

Vous connaissez tous le mécanisme d'édition des liens. Vous avez plusieurs fichiers source, tous sont compilés vers des fichiers objet qui sont ensuite rassemblés dans un exécutable. Bien, ceci concerne votre code personnel, mais qu'en est il, par exemple des fonctions standard du C, des fonctions mathématiques standard ou encore des primitives X11 que vous invoquez ? Vous ajoutez à la fin de votre ligne de commande des options genre -lm ou -lX11 pour indiquer que le code des fonctions utilisées est à chercher dans une bibliothèque de fichiers objets.

Liaison statique

Par défaut, les bibliothèques sont liées statiquement, ce qui signifie qu'à l'instar du code de vos propres fonctions, celui des fonctions de bibliothèque est ajouté dans votre exécutable. Les pointeurs d'appel sont alors reliés à l'emplacement des fonctions dans l'exécutable par ld. Les fichiers de bibliothèques utilisés ont l'extension .a.

Avantage de la liaison statique :
votre exécutable est autonome, il contient tout le code nécessaire à son exécution
Inconvénient majeur de la liaison statique :
Ajouter le code des fonctions de bibliothèque à chaque exécutable augmente sensiblement la taille de chaque exécutable mais surtout, si plusieurs exécutables utilisent la même fonction, celle-ci est chargée en mémoire autant de fois que d'exécutables présents.
Réponse : les bibliothèques dynamiques
L'intérêt d'une bibliothèque dynamique est d'être partagée c'est à dire qu'elle ne sera chargée qu'une seule fois en mémoire pour tous les exécutables qui l'utilisent, d'où un gain de place absolument considérable. En effet, si les utilitaires standard du système (genre ls, wc ou tee) n'utilisaient pas ce mécanisme, il faudrait multiplier par 16 la capacité mémoire des ordinateurs !

Liaison dynamique

La liaison dynamique des bibliothèques repose sur l'existence d'une forme spéciale du fichier de bibliothèque. Cette forme particulière est en fait constituée de deux fichiers, le premier (.sa) est utilisé lors de l'édition de liens ; le second (.so) lors de l'exécution.

L'édition se déroule alors en deux phases :

  1. Lors de la création de l'exécutable (première édition de liens), le code des fonctions n'est pas recopié dans l'exécutable. En revanche, ce dernier contient des informations indiquant qu'il doit y avoir une seconde édition de liens au moment de l'éxécution.
  2. A l'exécution, le système repère dans l'exécutable la présence d'informations concernant une bibliothèque dynamique. A ce moment la, le lieur dynamique (habituellement /etc/ld.so) vérifie que cette dernière est déjà chargée en mémoire (sinon, il va la chercher sur disque et la charge), effectue un élargissement de l'espace d'adressage du processus permettant d'englober la bibliothèque dynamique, puis relie les pointeurs d'appel de l'exécutable aux adresses des fonctions dans la bibliothèque dynamique. Ainsi, tous les programmes utilisant une même bibliothèque dynamique utilisent bien le même code.

Soit dit au passage, vous avez tout à fait la possibilité de créer vos propres bibliothèques, pour la version statiques, il vous suffit d'utiliser les utilitaires standard ar et ranlib. Pour la version dynamique, c'est beaucoup plus compliqué ... et très dépendant de la version du système que vous utilisez !

La gestion des fichiers en C

Ici encore, il existe des mécanismes de haut et bas niveau. Les mécanismes de bas niveau s'appuient directement sur la notion de descripteur de fichier alors que les mécanismes de haut niveau utilisent des structures spéciales de type FILE.

Toutes les commandes de traitement de fichier utilisent la constante EOF pour marquer la fin des fichiers.

Nous traiterons de manière séparée les fonctions d'accès à l'inode dont le nom et la syntaxe sont quelque peu confondantes.

Les mécanismes de bas niveau

Introduction

Bien que ces mécanismes ne soient pas normalisés ANSI, on les retrouve sur la majorité des plateformes de développement. En outre, sous Unix, ils sont indispensables car seuls à même de réaliser certaines opérations. Ils s'appuient directement sur la notion de handle de fichier. Pour mémoire, rappelons que chaque fichier ouvert est associé à un descripteur. Il y a autant de descripteurs associé au fichier que d'ouvertures. Les descripteurs sont rangés dans une table, chacun contient des renseignements importants sur le type d'acces accordé sur le fichier en question. Le handle n'est ni plus ni moins que l'index dans la table.

Si la conformité au standard ANSI est une obsession pour vous, et, à mon avis, vous avez entièrement raison, vous ne voudrez probablement utiliser ques les primitives de haut niveau. Toutefois, il est rassurant de savoir que l'on peut toujours compter sur des mécanismes de plus bas niveau parfois !

Récapitulatif succinct des primitives de bas niveau

int open(const char *path, int access [, mode_t mode]); Ouverture du fichier de nom path, avec les options spécifiées dans access et mode
int creat (const char * path, mode_t mode); Crée un nouveau fichier de nom path avec les options mode et renvoie son handle
int read(int handle, void *buf, unsigned len); Lecture de len octets depuis le fichier spéficié par handle vers l'emplacement de mémoire pointé par buf. Il doit y avoir au moins len octets de disponibles à l'emplacement pointé par buf
int write(int handle, void *buf, unsigned len); Ecrit len octets depuis l'emplacement mémoire pointé par buf vers le fichier de descripteur handle
int dup(int handle); Duplique le descripteur associé à handle et renvoie un nouveau handle
int fcntl (int handle, int, ...); Permet de modifier le comportement des opérations sur le fichier de descripteur handle
int eof(int handle); Teste si la fin du fichier est atteinte
long tell(int handle); Indique la position courante dans le fichier associé au handle passé en paramètre. La position est toujours donnée relativement au début du fichier.
long lseek(int handle, long offset, int fromwhere); Déplace le pointeur courant du fichier associé à handle de offset octets à partir de la position fromwhere. En fonction de la valeur de fromwhere vous pouvez indique une position à partir du début du fichier, de sa fin, ou de la position courante.
int close(int handle); Fermeture du descripteur associé au handle passé en paramètre

Il est important de noter que les opérations de bas niveau ne sont pas bufferisées ! Toute opération d'écriture est immédiatement répercutée sur le fichier concerné. Réciproquement, toute opération de lecture nécessite un accès disque. De ce fait l'utilisation ces primitives peut s'avérer plus lent que celui des primitives de haut niveau.

Dernière précaution à respecter, le nom des constantes numériques utilisées par open fcntl etc. est normalisé POSIX, mais pas la valeur de chacune de ces constante. Aussi, plus que jamais, il convient d'utiliser les constantes plutôt que des valeurs bien que cela soit parfois tentant (utiliser 4 pour un accès en lecture, par exemple !).

Petit détail merdique qui peut avoir son importance, il faut inclure le fichier <io.h> pour que tout ca marche !

Les mécanismes de haut niveau

Introduction

Contrairement aux mécanismes de bas niveau, ceux de haut niveau sont normalisés ANSI, et donc, disponibles, virtuellement partout ... Ils ne s'appuient plus sur la notion de handle (tous les systèmes d'exploitation ne sont pas sensés gérer une table de descripteurs) mais sur une structure sensée pouvoir représenter l'état d'un fichier dans n'importe quel système : FILE. On ne manipule jamais directement celle-ci mais plutôt des pointeurs alloués dynamiquement par la fonction d'ouverture (fopen) et désalloués par la fonction de fermeture (fclose).

Résumé succinct des principales fonctions de haut niveau

Vous noterez (nons sans sourire, j'en suis bien convaincu) que la plupart de ces primitives ont pour nom celui d'une primitive de bas niveau précédé de la lettre «f». Marrant non ?

FILE *fopen(const char *filename, const char *mode);   Ouverture de fichier, par opposition à open qui utilise des constantes numériques, fopen utilise une chaîne de caractères « mnémoniques » Toutes les autres opérations utilisent le FILE * renvoyé par fopen
int fclose(FILE *stream); Fermeture de fichier
int fflush(FILE *stream); Vide le buffer (lecture/écriture) associé au fichier
int feof(FILE *stream) Teste si la fin du fichier est atteinte
size_t fread(void *ptr, size_t size, size_t n, FILE *stream); Lecture de n éléments de taille size octets depuis le fichier stream. Les éléments sont logés à l'emplacement mémoire désigné par ptr Attention, verifiez que la mémoire libre est suffisante ! La valeur de retour indique le nombre d'octets réellement lus.
size_t fwrite(const void *ptr, size_t size, size_t n, FILE *stream); Ecrit dans le fichier stream de n elements de taille size localisés en mémoire par ptr.La valeur de retour indique le nombre d'octets réellement lus.
int fputs(const char *s, FILE *stream); Ecrit la chaîne s dans le fichier stream. Ajoute automatiquement un saut de ligne !
char *fgets(char *s, int n, FILE *stream); Assurément la commande de lecture la plus utile pour les fichiers en mode texte ! Lit une ligne depuis stream jusqu'à concurrence de n-1 caractères et les loge dans la chaîne s. Supprime le caractère de saut de ligne si une ligne complète est lue.Notez que s doit être dimensionnée à n caractères à cause du caractère 0 ajouté automatiquement.
int fscanf(FILE *stream, const char *format, ...) Bon, ça, vous connaissez tous : cette immonde fonction permet de lire des données sur le fichier stream gràce à la chaîne de format format. Toutefois, n'oubliez jamais que :
  • vous devez passer des pointeurs pour les données à lire
  • Toujours mettre exactement le même nombre de variables que de champs de lecture dans le format
  • Le résultat de fscanf et de toutes les autres fonctions de type ZZscanf est le nombre d'éléments réellement lus, décodés et stockés dans les variables associées
int fprintf(FILE *stream, const char *format, ...) Encore une fonction que vous connaissez bien ! fprintf permet de faire des écritures formatées dans le fichier stream conformément à la chaîne de format format
long int ftell(FILE *stream); Renvoie la position du pointeur courant (relativement au début du fichier) du fichier stream
int fseek(FILE *stream, long offset, int fromwhere); Permet de déplacer le pointeur courant du fichier stream de offset octets à partir du point de référence fromwhere qui peut être :
  1. Le début du fichier
  2. La fin du fichier
  3. L'emplacement courant
Si offset est positif, on va chercher à avancer dans le fichier, sinon, on va tenter de reculer !
int getc(FILE *stream); Lit le prochain caractère dans le fichier stream. Notez que le type renvoyé est int et non pas char.
int ungetc(int c, FILE *stream); Place le caractère c dans le tampon de lecture du fichier stream de manière à ce qu'il soit renvoyé par la prochaine commande de lecture. Le caractère n'est pas placé physiquement dans le fichier mais uniquement dans le buffer de lecture, aussi certaines opérations risquent de le supprimer.

Vous noterez également l'existence des fonctions fgetpos et fsetpos qui permettent de placer des marqueurs sur un fichier puis d'y revenir ultérieurement.

On ne le dira jamais assez, la seule commande sure de lecture dans un fichier texte (en particulier, le clavier ... ) est fgets qui permet :

N'oubliez pas que vous pouvez utiliser fgets sur la console avec stdin en paramètre !

Si vous avez besoin d'extraire des éléments de la chaîne lue, vous pouvez utiliser :

Les fonctions d'accès à l'inode

Description des fonctions

Les fonctions fstat et stat (toutes deux non normalisées ANSI mais reconnues par le standard POSIX) permettent d'obtenir les informations contenues dans l'inode. Notons également que ces fonctions sont disponibles sous DOS et Windows, bien que ces deux systèmes n'aient aucune structure de donnée comparable à un inode.

Il est impératif de note que le fichier doit être ouvert pour lui appliquer ces fonctions.

int fstat(int handle, struct stat *statbuf); Stockent dans la structure statbuf les informations relatives au fichier précédemment ouvert et désigné soit par un descripteurhandle soit par son chemin path
int stat(const char *path, struct stat *statbuf);

La structure struct stat

Voici, tel que fourni par l'examen du fichier sys/stat.h le prototype (POSIX) de la structure stat. Comme vous pouvez le constater, la structure de type BSD est plus complète que celle de System V. Le texte original a été commenté en Français pour une compréhension plus aisée.

struct	stat 
{
dev_t   st_dev;   /* si le fichier est associé à un périphérique, réf de celui-ci */
ino_t   st_ino;   /* Numéro d'inode */
mode_t  st_mode;  /* Mode d'ouverture du fichier */
nlink_t st_nlink; /* Nombre de liens physiques (entrées de répertoires) associés au fichier */
uid_t   st_uid;   /* Numéro du propriétaires */
gid_t   st_gid;   /* Numéro du groupe */
dev_t   st_rdev;  /* Identique st_dev */
off_t   st_size;  /* Taille du fichier (consultez man pour l'unite, habituellement en nb de blocs) */
/* SysV/sco doesn't have the rest... But Solaris, eabi does.  */
#if defined(__svr4__) && !defined(__PPC__) && !defined(__sun__)
time_t  st_atime; /* Date de dernier acces */
time_t  st_mtime; /* Date de derniere modification du contenu */
time_t  st_ctime; /* Date de dernier changement (changement attributs et ou modification du contenu*/
#else
time_t  st_atime;     /* Date de dernier acces */
long    st_spare1;    /* Spécial SUN */
time_t  st_mtime;     /* Date de derniere modification du contenu */
long    st_spare2;    /* Spécial SUN */
time_t  st_ctime;     /* Date de dernier changement (changement attributs et ou modification du contenu*/
long    st_spare3;    /* Spécial SUN */
long    st_blksize;   /* Taille de chaque bloc */
long    st_blocks;    /* Taille en nb de blocs */
long    st_spare4[2]; /* Spécial SUN */
#endif
};

Passer des fonctions de haut niveau à celles de bas niveau et réciproquement

Les deux fonctions suivantes permettent respectivement d'obtenir le handle d'un fichier connu par un FILE * ou une structure FILE à partir de son descripteur.

int fileno(FILE *stream); Renvoie un descripteur associé au fichier attaché à stream Si plusieurs descripteurs sont associés à ce fichier, le premier créé est renvoyé.
FILE *fdopen(int handle, char *type); Crée une nouvelle structure FILE associée au descripteur de fichier handle Le mode d'ouverture spécifié dans type doit correspondre à celui déjà fourni lors de l'obtention du descripteur.
Ce texte a été conçu par l'Ours Blanc des Carpathes Il appartient de droit à l'ISIMA, Université Blaise-Pascal, Clermont-Ferrand II. Tous droits réservés.

monstyle