Le langage C

Base de donnée: (suite)



Par :

Guilhem de Wailly (gdw@erian-concept.com)



Résumé

Dans cette série d'articles, nous présentons le langage C. Il ne s'agit pas de réécrire les ouvrages de référence donnés en annexe, mais de donner, au travers du C, une méthode de programmation basée sur la notion d'interface.

Au fur et à mesure de notre progression, nous décrirons les éléments indispensables du langage et conduirons le lecteur à consulter les ouvrages de références. Le but poursuivi est clairement de parvenir à construire des programmes de manière segmentée.

Les mois derniers, nous avons entamé l'étude des tables de hachage. Il nous restait à écrire les fonctions de lecture et de sauvegarde des tables dans des fichiers, et nous en avons profité pour y glisser la description des fonctions d'entrées/sortie de la bibliothèque standard du langage C. Nous allons continuer ce mois-ci avec des fonctions de gestion de fichier.

Cette description sera aussi le prétexte pour aborder certains points nouveaux du langage C.

Description

L'article du mois dernier a présenté des fonctions C pour lire et écrire dans des fichiers. Nous l'avions terminé avec trois fonctions pour manipuler les fichiers avec leur nom. Il s'agissait de rename pour renommer un fichier, de unlink pour le supprimer et de stat pour obtenir des informations sur lui.

Nous allons continuer ce mois-ci avec d'autres fonctions de gestion externes de fichiers, complétant ainsi notre panoplie d'outils.

Droits d'accès

Les fonctions et les constantes décrites dans cette section sont définies dans les fichiers d'entêtes sys/types.h et unistd.h. Il faudra donc inclure ces entêtes avant toute utilisation.

int chmod (char * fichier, mode_t mode)

Cette fonction permet de modifier les droits associés au fichier dont le nom est donné en argument. Les droits dont on parle ici sont les droits que l'on voit apparaître lorsque l'on tape la commande Unix ls -l dans un répertoire.

Le paramètre mode détermine les nouveaux droits du fichier, si on a effectivement le droit de modifier les droits sur ce fichier.

La variable mode est un ou binaire entre des constantes prédéfinies :

ï‚· S_IRUSR S_IWUSR S_IXUSR : Ces constantes correspondent aux droits associés à l'utilisateur propriétaire du fichier, pour, respectivement la lecture, l'écriture et l'exécution sur le fichier.
En général, ces constantes correspondent, respectivement, aux nombres en octal 0400, 0200 et 0100.
Le droit d'exécution sur un répertoire donne le droit de ``traverser'' ce répertoire. Le droit de traversée permet d'accéder à un fichier se trouvant dans ce répertoire. Le droit en lecture sur le répertoire donne l'accès à la liste des entrées du répertoire. Le droit en écriture sur un répertoire donne le droit de modifier la liste des entrées ; c'est pour cette raison que l'on peut supprimer un fichier qui ne nous appartient pas dans un répertoire où on a le droit en écriture. Il existe cependant une parade décrite un peu plus loin dans le texte.

ï‚· S_RWXU : Cette constante est la combinaison des trois constantes précédentes, et elle correspond à (S_IRUSR|S_IWUSR|S_IXUSR).

ï‚· S_IRGRP S_IWGRP S_IXGRP : Ces constantes correspondent aux droits associés au groupe d'utilisateurs propriétaire du fichier, pour, respectivement la lecture, l'écriture et l'exécution sur le fichier.
En général, ces constantes correspondent respectivement aux nombres en octal 0040, 0020 et 0010.

ï‚· S_RWXG : Cette constante est la combinaison des trois constantes précédentes, soit (S_IRGRP|S_IWGRP|S_IXGRP).

ï‚· S_IROTH S_IWOTH S_IXOTH : Ces constantes correspondent aux droit associés aux utilisateur n'étant pas propriétaires du fichier et n'appartenant pas au groupe d'utilisateurs propriétaire du fichier, pour respectivement la lecture, l'écriture et l'exécution sur le fichier.
En général, ces constantes correspondent, respectivement, aux nombres en octal 0004, 0002 et 0001.

ï‚· S_RWXO : Cette constante est la combinaison des trois constantes précédentes, soit (S_IROTH|S_IWOTH|S_IXOTH).

Si nous voulons donner les droits -rwxr-x--- à un fichier, c'est à dire le droit en lecture, écriture et exécution pour le propriétaire du fichier, et le droit en lecture et écriture pour les utilisateurs appartenant au groupe propriétaire du fichier et aucun droit aux autres utilisateurs, nous écririons en C:

chmod ("un_fichier",
       S_IRUSR | S_IWUSR | S_IXUSR |
       S_IRGRP           | S_IXGRP);

Maintenant, si nous tapons sous Unix la commande :

$ ls -l un_fichier

nous devrions obtenir :

-rwxr-x--- 1 gdw users 21 Dec 1 21:03 un_fichier

La fonction retourne l'entier 0 en cas de succès et l'entier -1 en cas d'erreur.

Il existe trois autres bits de droits dont la signification est différente s'il s'agit d'un fichier ou d'un répertoire :

ï‚· S_ISUID : Cette constante permet de placer le S bit pour l'utilisateur. Cela rend le fichier exécutable et permet qu'il soit exécuté au nom de son propriétaire en non de l'utilisateur qui lance la commande.
Cette constante a pour valeur octal 04000.
Supposons qu'un fichier exécutable pgm appartient à l'utilisateur toto et qu'il n'aie pas ce bit actif. Si l'utilisateur titi exécute le fichier pgm, le processus créé appartient à titi. Si on positionne ce bit sur le fichier pgm et que titi l'exécute, le processus créé appartient à toto. Certains programmes destinés à modifier des informations critiques n'appartenant qu'au super utilisateur on se bit positionné ; c'est notamment le cas de la commande /usr/bin/passwd qui modifie le fichier /etc/passwd appartenant au super utilisateur. Ces programmes sont potentiellement très dangereux, car s'ils sont interrompu en permettant de taper des commandes, elles sont tapées au nom du propriétaire du programme.
Ce bit n'a pas d'action sur un répertoire.

ï‚· S_ISGIG : Cette constante permet de placer le S bit pour le groupe d'utilisateur auquel appartient le fichier. Cela permet qu'il soit exécuté au nom du groupe auquel appartient le fichier et non au nom du groupe auquel appartient l'utilisateur ayant lancé le programme.
Appliqué à un répertoire, ce bit a pour effet d'utiliser le groupe auquel le répertoire appartient lorsqu'un fichier y est créé au lieu du nom de groupe auquel l'utilisateur appartient.
Cette constante a pour valeur octal 02000.

ï‚· S_ISVTX : Lorsque ce bit est appliqué à un programme, ce programme reste en mémoire même lorsqu'il est terminé. De cette manière, la prochaine fois qu'il sera exécuté, le système d'exploitation pourra le trouver plus rapidement. Notez que sur les systèmes d'exploitations Unix récents, ce mode de fonctionnement est dépassé car la gestion des caches disques est optimisées.
Appliqué à un répertoire, ce bit permet de n'autoriser la suppression des fichiers se trouvant dans ce répertoire qu'au propriétaire de ces fichier et au propriétaire du répertorie. Observez bien votre répertoire /tmp et les droits qui y sont associés, et vous verrez une lettre t témoignant que les choses sont décidément bien faites ... sous Unix !
Cette constante a pour valeur octal 01000.

int fchmod (int descripteur, mode_t mode)

Cette fonction a le même fonctionnement que la précédente, mais elle agit sur un descripteur de fichier, un entier obtenu par exemple avec la fonction open, au lieu d'un nom de fichier.

int chown (char * fichier, uid_t uid, gid_t gid)

Cette fonction permet de changer le propriétaire d'un fichier. Seul le super utilisateur a le droit d'utiliser cette fonction. Le nouveau propriétaire est identifié par son uid, c'est à dire son numéro d'utilisateur que l'on trouve définit dans le fichier /etc/passwd et le groupe des utilisateurs propriétaires par le gid définit dans le fichier /etc/group.

int fchown (int descripteur, uid_t uid, gid_t gid))

Cette fonction agit comme la précédente mais elle utilise un descripteur de fichier ouvert au lieu d'un nom de fichier.

Liens

int link (char * ancien, char * nouveau)

Cette fonction permet de créer un lien 'matériel' nommé nouveau sur le fichier ancien. Notez que ancien doit être un fichier et non un répertoire et qu'il doit appartenir au même périphérique que nouveau.

La fonction retourne 0 si tout s'est bien passé, et -1 si une erreur a eu lieu.

Un lien matériel est un réorganisation su système de fichiers qui permet de faire pointer deux entrées de répertoires vers le même fichier. La seule information que l'on puisse obtenir à propos des liens matériels est le nombre de liens qui existent sur un fichier donné. La commande ls -la fournit ce nombre. Par exemple :

$ ls -la /bin/gawk

affichera :

-rwxr-xr-x 2 root root 146804 Apr 6 1999 gawk

Le chiffre 2 avant le mot root indique que deux entrées de répertoires pointent sur le même fichier. On peut connaître l'adresse d'un fichier dans le périphérique, appelée inode, avec la commande ls -li :

$ ls -li /bin/gawk*

affichra :

26660 2 root root 146804 Apr 6 1999 gawk
26660 2 root root 146804 Apr 6 1999 gawk-3.0.3

Ainsi, on voit que les deux entrées gawk et gawk-3.0.3 ont la même adresse sur le disque, à savoir 26660, et qui correspondent au même fichier. Cela implique que modifier l'un revient à modifier l'autre aussi. Si on en supprime un, l'autre continue d'exister, mais son nombre de lien diminue de un.

int symlink (char * ancien, char * nouveau)

Cette fonction permet de créer un lien symbolique nommé nouveau sur la cible ancien. La fonction retourne 0 en cas de succès et -1 en cas d'erreur.

Les liens symboliques ne sont pas une réorganisation du système de fichier de sorte que deux entrées pointent sur le même fichier, mais un fichier à part entière. Ce fichier est un fichier spéciale reconnu par le système d'exploitation et qui contient le chemin de l'entrée de répertoire vers laquelle il pointe. La commande ls va, là encore nous aider à les identifier :

ls -la /bin/awk

affiche :

lrwxrwxrwx 1 root root 4 Nov 4 1999 /bin/awk -> gawk

La lettre l dans les droits nous montre que ce fichier est un lien symbolique, et qui pointe vers l'entrée gawk. Notez qu'un lien peut pointer vers une cible située dans un autre répertoire.

A la différence des liens matériels qui imposent que ancien soit un fichier situé sur le même périphérique, les liens symboliques peuvent être créés même si la cible n'existe pas. De plus, la cible peut être un répertoire et se situer sur un autre périphérique.

Bien sur cette souplesse a un coût car elle impose au système d'exploitation à ouvrir le fichier de lien et à lire son contenu avant de pouvoir accéder à la cible, ce qui occasionne un ralentissement de l'accès.

La très grande souplesse provient du fait que l'accès à la cible par les fonctions standards d'entrées / sortie est totalement transparent : par exemple, la fonction open ne fera aucune différence entre un lien et un fichier réel.

Par exemple :

int    status;
FILE * fichier;

/* création du lien nommé le_lien_symbolique
* vers un_fichier_existant */

status = symlink ("un_fichier_existant",
                  "le_lien_symbolique");
if (status) {
  fprintf (sdterr,
           "impossible de créer le lien\n");
  abort();
}
/* la fonction fopen ne fait pas la différence
* entre les deux fichiers : on ouvre le lien
* comme un fichier ordinaire */

fichier = fopen ("le_lien_symbolique"
                 "r");
...
fclose (fichier);

int lstat (char * lien_symbolique, struct stat * buffer)

Le système d'exploitation occulte complètement l'existence du lien pour les fonctions classique : manipuler le lien, c'est manipuler la cible. Cependant, il doit exister une fonction qui retourne des informations sur le fichier lien lui même, sans se préoccuper de la cible. Cette fonction, c'est lstat qui remplit la structure struct stat buffer que nous avons décrite le mois dernier avec les informations concernant le lien lui-même.

Cette fonction retourne 0 sauf lorsqu'elle retourne -1 en cas d'erreur.

Répertoires

Dans Unix, les répertories sont des fichiers qui contiennent une entrée pour chaque fichier leur appartenant. Une entrée est un nom, un numéro de propriétaire (UID), un numéro de groupe d'utilisateur (GID), des droits (type, utilisateur, groupe, autres), des informations (taille, nombre de liens, ...), des dates (accès, modification, création) et l'adresses sur le disque du fichier pointé. La bibliothèque standard nous offre un moyen d'accéder simplement à la liste des entrées d'un répertoire.

Ces fonctionnalités sont définies dans les entêtes dirent.h et unistd.h que l'on veillera à inclure avant de les utiliser.

DIR * opendir (char * chemin)

Cette fonction retourne un pointeur vers une structure d`écrivant le répertoire chemin, dont on souhaite obtenir la liste des fichiers qui lui appartiennent. Le contenu de cette structure ne nous intéresse pas car on y accède au travers des fonctions décrites ici. En cas d'erreur, la fonction retourne 0.

struct dirent * readdir (DIR * repertoire)

Cette fonction retourne un pointeur vers une structure de donnée struct dirent décrivant l'entrée suivante dans le répertoire dont le descripteur est donnée en argument. S'il n'y a plus d'entrée, la fonction retourne le pointeur nul.

La structure struct dirent contient différents champs. Le seul dont on soit certain et qui existe sur tous les systèmes Unix est le champs char d_name[] qui contient le nom de l'entrée. Les autres champs n'existent pas dans tous les systèmes et/ou ils ont des significations légèrement différentes. Les informations que ces champs supplémentaires seraient susceptibles d'apporter peuvent être obtenues de façon beaucoup plus portable et précise par la fonction stat agissant sur le nom d'une entrée.

void rewinddir (DIR * repertoire)

La fonction readdir de lecture des entrées dans le descripteur de répertoire avance à chaque appel jusqu'à ce qu'il n'y ait plus d'entrée à retourner.

La fonction rewinddir permet de replacer le curseur d'entrée sur la première entrée.

off_t telldir (DIR * repertoire)

Cette fonction retourne le numéro de l'entrée courante. La valeur retournée est de type off_t qui peut être convertie (par un cast) sans problème vers un entier.

void seekdir (DIR * repertoire, off_t deplacement)

Cette fonction permet de déplacer le curseur d'entrée sur l'entrée située au déplacement donné en argument. Cela permet un accès aléatoire aux entrées d'un répertoire.

int closedir (DIR * repertoire)

Cette dernière fonction clôture l'utilisation du descripteur de répertoire donnée en argument et libère toutes les ressources qui y sont associées.

Utilisons ces fonctions pour réaliser un petit programme permettant d'afficher la liste des entrées d'un répertoire donné :

#include <dirent.h>
#include <stat.h>
#include <stdio.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

void main (void) {
 DIR * repertoire;
char nom[1000];

  /* saisie du nom du répertoire */
  printf ("Entrez le nom du répertoire à lister : ");
  scanf ("%1000s", nom);

  /* ouverture du descripteur de répertoire */
  repertoire = opendir (nom);
if (repertoire) {
   struct dirent * entree;

    /* parcours des entrées */
   while ((entree = readdir (repertoire))) {
      struct stat infos;

      if (! stat (entree->d_name, & infos)) {
        /* nom */
     printf ("%s\n\tdroits=", entree->d_name);

        /* type */
     if      (S_ISDIR  (infos->st_mode)) putc ('d');
     else if (S_ISCHR  (infos->st_mode)) putc ('c');
     else if (S_ISBLK  (infos->st_mode)) putc ('b');
     else if (S_ISREG  (infos->st_mode)) putc ('-');
     else if (S_ISFIFO (infos->st_mode)) putc ('f');
     else if (S_ISLNK  (infos->st_mode)) putc ('l');
     else if (S_ISSOCK (infos->st_mode)) putc ('s');

        /* droits du propriétaire */
     putc (infos->st_mode & S_IRUSR ? 'r' : '-');
     putc (infos->st_mode & S_IWUSR ? 'w' : '-');
     putc (infos->st_mode & S_IXUSR
              ? 'x'
              : infos->st_mode & S_IUID ? 's' : '-');

        /* droits du groupe */
     putc (infos->st_mode & S_IRGRP ? 'r' : '-');
     putc (infos->st_mode & S_IWGRP ? 'w' : '-');
     putc (infos->st_mode & S_IXGRP
              ? 'x'
              : infos->st_mode & S_IGID ? 's' : '-');

        /* droits des autres */
     putc (infos->st_mode & S_IROTH ? 'r' : '-');
     putc (infos->st_mode & S_IWOTH ? 'w' : '-');
     putc (infos->st_mode & S_IXOTH ? 'x' : '-');

        /* sticky bit */
     putc (infos->st_mode & S_ISVTX ? 't' : '-');

        /* informations */
     printf ("\n\tliens=%d\n", infos->st_nlink);
     printf ("\tuid=%d\n", infos->st_uid);
     printf ("\tgid=%d\n", infos->st_gid);
     printf ("\ttaille=%d\n", infos->st_size);

        /* dates de dernier accès/modification/création */
     printf ("\taccés=%s", ctime (infos->st_atime));
     printf ("\tmodif=%s", ctime (infos->st_mtime));
     printf ("\tcréé =%s", ctime (infos->st_ctime));
      }
      else {
        printf ("Erreur: impossible d'accéder à %s\n",
                entree->d_name);
      }
    }
closedir (repertoire);
}
else{
    fprintf (stderr,
             "Error: ne peut ouvrir le répertoire %s\n",
             nom);
    exit (1);
}
}

int mkdir (char * chemin, mode_t mode)

Cette fonction permet de créer un répertoire vide dont le chemin est donnée en argument. Les droits de ce chemin sont spécifiés par l'argument mode, construit avec les constantes que l'on a déjà vues plus haut.

Si la fonction échoue, elle retourne -1 ; sinon, elle retourne 0.

int rmdir (char * chemin)

Cette fonction détruit le répertoire vide dont le chemin est donné en argument. Si la fonction échoue, elle retourne -1, et 0 en cas de succès. Attention, le répertoire doit être vide pour pouvoir être supprimé ; si ce n'est pas le cas, il faut le vider de toutes ses entrées.

int chroot (char * chemin)

Cette fonction ne peut être invoquée avec succès que par le super utilisateur root. Elle permet de déplacer la racine de la hiérarchie des fichiers au chemin qui est donnée en argument. Ce chemin doit correspondre à un répertoire. Une fois cette fonction invoquée, il n'est plus possible de revenir en arrière, pour le processus ayant invoquée la fonction et pour tous ses processus fils.

Cette fonction est très utilisée pour réaliser des serveurs sécurisés (on dit chrootés) du fait du caractère irréversible de l'opération.

La fonction chroot retourne 0 si tout c'est bien passé, et -1 si elle a échouée.

Nous avons maintenant terminé notre exploration des fonctions d'entrée / sortie proposées par le langage C et le système Unix. Nous les utiliserons la prochaine pour (enfin) réaliser la sauvegarde et la restauration des tables de hachages.

L'auteur

Guilhem de Wailly, directeur de la société Erian Concept, partenaire NetUltra (www.netultra.net) et Getek (www.getek.fr).

Votre interlocuteur Linux !

Support, formations, configurations, administration, développements Linux.

http://www.erian-concept.com

Références

Langage C - norme ANSI : bon ouvrage de référence
Ph. DRIX
Masson

Programmer en C++: une bonne référence
S.C. DEWHURST et K.T. STARK
Masson

Le Langage C: la bible!
B.W. KERNIGHAN et D.M. RITCHIE
Masson

gcc: compilateur C du GNU, et tous les outils de développement
http://www.gnu.org

Kdevel: un environnement KDE de programmation C
http://samuel.cs.uni-potsdam.de/~smeier/kdevelop_new/index.html