Fonctionnalités du noyau Linux (Episode 7) – Travaux pratiques sur les appels système dédiés à la gestion des fichiers

Nous avons vu dans l’épisode 1 l’architecture générale de l’environnement du noyau Linux et la manière dont il gère les processus. L’épisode 2 nous a permis d’entrer dans le détail de la gestion de la mémoire. L’épisode 3 nous a permis de creuser comment le noyau Linux gère les périphériques. Dans l’épisode 4 nous avons vu comment le noyau Linux offre aux applications un service d’appels système.

Dans l’épisode 5 nous avons vu les mécanismes qu’offre le noyau Linux aux processus tournant sur le système pour communiquer entre eux, c’est à dire pour se synchroniser et/ou s’envoyer des données.

Dans l’épisode 6 nous nous sommes amusés avec les principaux appels système que le noyau met à disposition des processus.

Dans cet épisode 7 nous allons faire de même avec les appels système que mis à disposition des programmeurs pour manipuler les fichiers.

Commençons par planter le décor : chaque processus Linux comporte une table des descripteurs de fichiers, dont la taille est indiquée dans les sources du noyau avant compilation de celui-ci. On ne peut donc pas la changer sans recompiler le noyau.

Dans cette table, chaque entrée comporte un numéro et pointe, si la ligne a été ouverte, vers un fichier précis via un parcours dans les arcanes du noyau Linux. Comme tout est fichier sous Unix, ce parcours dans les arcanes prend différents chemins selon que le fichier ouvert soit un fichier sur disque, une socket réseau, un périphérique, etc.

Dans cette table, 3 entrées ont une utilisation particulière.

La première est l’entrée de numéro 0, qu’on appelle l’entrée standard (stdin en anglais). C’est l’entrée par défaut sur laquelle le processus lit les données qu’un autre processus lui envoie via un pipe par exemple.

La deuxième est l’entrée de numéro 1, qu’on appelle la sortie standard (stdout en anglais). C’est la sortie par défaut sur laquelle le processus écrit (lorsqu’on fait un printf par exemple).

La troisième est l’entrée de numéro 2, qu’on appelle l’erreur standard (stderr en anglais). Elle est prévue pour que le processus puisse communiquer au reste du monde (qui devra se brancher en lecture sur cette sortie) ses messages d’erreur.

Toutes les autres entrées de la table des descripteurs de fichiers sont lambda. A noter que l’état exact de cette table est transmis à l’identique aux processus fils dès lors qu’on les crée avec l’appel système fork() déjà vu dans l’épisode 6.

Appels système dédiés à la gestion des fichiers

Tous les appels système que nous allons voir nécessite l’utilisation de #include <fcntl.h>

Appel système pour ouvrir un fichier

int open(char *chemin, int mode, int droits_d_acces);

Cet appel système ouvre un fichier dont le nom est indiqué dans chemin. Le chemin peut être absolu ou relatif.

mode contiendra au choix :

  • O_RDONLY : ouverture en lecture seule
  • O_WRONLY : ouverture en écriture seule
  • O_RDWR : ouverture en lecture/écriture

Plus éventuellement une combinaison de :

  • O_CREAT : le fichier sera créé s’il n’existe pas
  • O_APPEND : le pointeur courant sera positionné en fin de fichier
  • O_TRUNC : le fichier sera vidé de son contenu s’il pré-existait
  • O_EXCL : si utilisé avec O_CREAT fera rater l’appel système si le fichier existe déjà
  • O_SYNC : le fichier sera ouvert en mode synchrone, c’est à dire que les écritures seront bloquées jusqu’à ce qu’elles soient physiquement inscrites sur le disque

droits_d_acces correspond aux droits classiques sur les fichiers : 0777 par exemple correspondra à des droits rwxrwxrwx.

Le retour de l’appel système open peut ramener -1 si l’appel système échoue (l’erreur commise peut alors être affichée par l’appel à perror). Si l’appel système open réussit, une entrée est allouée dans la table des descripteurs de fichiers du processus et le numéro de cette entrée est ramené dans la valeur de retour.

Appel système pour fermer un fichier

int close(int fd);

Cet appel système ferme le fichier pointé par le descripteur de fichier de numéro fd. Le descripteur de fichier de ligne fd est alors libéré.

La valeur de retour est 0 si succès, -1 si échec (par exemple si fd ne correspondait pas à un fichier ouvert).

Ne pas oublier de fermer les fichiers que l’on n’utilise plus sinon la table se remplit complètement et l’on ne peut plus ouvrir de nouveaux fichiers.

Appel système pour lire dans un fichier

int read(int fd, char *adresse, int n);

Cet appel système tente de lire n octets dans le fichier pointé par le descripteur de numéro fd, à partir de la position courante, et met les octets lus dans adresse. Cet appel système échoue si le fichier était ouvert en écriture seule.

La valeur de retour est le nombre d’octets effectivement lus.

Appel système pour écrire dans un fichier

int write(int fd, char *adresse, int n);

Cet appel système tente d’écrire n octets situés en mémoire à adresse dans le fichier pointé par le descripteur fd.

La valeur de retour est le nombre d’octets effectivement écrits.

A noter : si O_SYNC a été utilisé à l’ouverture du fichier, l’appel système effectue une synchronisation disque avant de rendre la main. C’est plus long mais aussi plus sûr.

Appel système pour se déplacer dans un fichier

long lseek(int fd, long deplacement, int reference);

Cet appel système permet de modifier la position courante dans le fichier pointé par le descripteur fd.

deplacement indique le nombre d’octets pour le déplacement, par rapport à la référence.

reference peut contenir l’une des valeurs suivantes :

  • SEEK_SET : la référence est alors le début du fichier
  • SEEK_END : la référence est alors la fin du fichier
  • SEEK_CUR : la référence est la position courante

La valeur de retour est -1 si échec, sinon la nouvelle position courante par rapport au début du fichier.

Appel système pour dupliquer une entrée de la table des descripteurs de fichiers

int dup(int fd);

Cet appel système duplique le descripteur de fichier fd dans le premier descripteur de fichier libre en partant de 0.

La valeur de retour est -1 si échec, sinon le numéro du descripteur de fichier trouvé.

Par exemple, pour utiliser le descripteur X comme sortie standard, on fera la séquence :

X=open(...);
close(1); // fermeture de la sortie standard
dup(X); // duplication de X vers 1, la sortie standard
close(X); // on n'en a plus besoin

Appels système pour tout savoir un fichier

#include <sys/stat.h>
int stat(char *chemin, struct stat *s);
int fstat(int fd, struct stat *s);
int lstat(char *chemin, struct stat *s);

Ces appels système ramènent dans la structure s les informations relatives au fichier indiqué dans chemin ou pointé par fd.

stat est à utiliser pour les fichiers non ouverts par open

fstat est à utiliser pour les fichiers déjà ouverts par open

lstat est identique à stat sauf pour les liens, pour lesquels il ramène les informations relatives au lien lui-même et non au fichier référencé par le lien.

La stucture stat contient toutes les infos relatives à un fichier donné, organisées de la manière suivante :

struct stat {
dev_t st_dev; // donne le device
ino_t st_ino; // donne l'inode
mode_t st_mode; // donne la protection
nlink_t st_nlink; // donne le nombre de liens
uid_t st_uid; // donne le user ID du propriétaire
gid_t st_git; // donne le group ID du propriétaire
dev_t st_rdev; //donne le type de device
off_t st_size; // donne la taille du fichier
unsigned long st_blksize; // donne la taille de bloc du système de fichiers
unsigned long st_blocks; // donne le nombre de blocs alloués
time_t st_atime; // donne le temps du dernier accès
time_t st_mtime; // donne le temps de la dernière modification
time_t st_ctime; // donne le temps de la création
};

Appels système pour modifier les droits d’accès à un fichier

#include <sys/stat.h>
int chmod(char *chemin, int mode);
int fchmod(int fd, int mode);

Ces appels système permettent de modifier les droits d’accès à un fichier référencé soit par son chemin d’accès (relatif ou absolu) soit par un descripteur de fichier ouvert.

La valeur de retour est 0 si succès, -1 si échec.

Seuls root et le propriétaire d’un fichier peuvent modifier ses droits d’accès.

Appels système pour modifier le propriétaire d’un fichier

#include <unistd.h>
int chown(char *chemin, int uid, int gid);
int fchown(int fd, int uid, int gid);
int lchown(char *chemin, int uid, int gid);

Ces appels système permettent de changer l’utilisateur et le groupe propriétaire du fichier référencé par son chemin ou par son descripteur.

La valeur de retour est 0 si succès, -1 si échec.

Seul root peut changer l’utilisateur et le groupe du fichier. Le propriétaire du fichier ne peut changer que le groupe. Le SUID bit et le sticky bit sont remis à 0.

Appel système pour créer un lien vers un fichier

#include <unistd.h>
int link(char *chemin_fichier_existant, char *chemin_fichier_a_creer);

Cet appel système permet de créer un lien vers un fichier existant. Le lien à créer doit être sur le même volume monté que le fichier existant.

La valeur de retour est 0 si succès, et -1 si échec.

A la suite de cet appel système il devient impossible de savoir lequel des deux fichiers (qui référencent le même fichier sur le disque) est l’original.

Appel système pour supprimer un lien

#include <unistd.h>
int unlink(char *chemin);

Cet appel système détruit le lien référencé par chemin. Le fichier n’existe plus que si ce lien était le dernier sur ce fichier.

La valeur de retour est 0 si succès, -1 si échec.

Les processus ayant encore un descripteur ouvert sur ce fichier peuvent continuer à l’utiliser jusqu’à ce que le descripteur soit fermé.

Appel système pour changer de répertoire de travail courant

#include <unistd.h>
int chdir(char *chemin_nouveau_repertoire);

Cet appel système change le répertoire de travail courant du processus qui l’appelle.

La valeur de retour est 0 si succès, -1 si échec.

Appel système pour forcer la synchronisation des disques

#include <unistd.h>
int sync();

Cet appel système demande au noyau de synchroniser les disques avec la mémoire de sorte que les changements effectués sur les fichiers soient effectivement écrits sur disque.

La valeur de retour est toujours 0.

Appel système pour créer un répertoire

int mkdir(char *chemin, int droits_d_acces);

Cet appel système permet de créer le répertoire chemin avec les droits mentionnés.

Le propriétaire et le groupe du répertoire sont hérités de ceux du processus appelant cet appel système.

La valeur de retour est 0 si succès, -1 si échec.

Il faut avoir le droit de créer un répertoire à l’endroit souhaité pour que cet appel système soit un succès.

Appel système pour supprimer un répertoire

int rmdir(char *chemin);

Cet appel système permet de détruire un répertoire. Pour qu’il soit un succès, il faut avoir les droits permettant cette destruction et que le répertoire soit vide.

La valeur de retour est 0 si succès, -1 si échec.

Appel système pour changer les droits par défaut

#include <sys/stat.h>
int umask(int droits);

Cet appel système permet de positionner les droits par défaut à la création d’un fichier ou d’un répertoire. Sa valeur a un impact sur les appels système open et mkdir.

En effet, supposons que ces appels spécifient 666 comme droits (ce qui correspond à rw-rw-rw-) et que le umask courant est positionné sur 022, le fichier ou le répertoire final aura comme droits 644 (c’est à dire rw-r–r–) car la formule appliquée est 666 & ~022 (ce qui équivaut à 666 & 755 = 644)

La valeur de retour est l’ancienne valeur du umask.

Travaux pratiques

Nous allons maintenant mettre en oeuvre la majeure partie de ces appels système dans le TP ci-dessous, mis dans un fichier prog.c dont voici le code source :

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>

int main()
{
int fd,nb;
char chaine[1000];

chdir(getenv("HOME")); // On se place dans le répertoire home de l'utilisateur
mkdir("test",0755); // création du répertoire test
fd=open("./test/fichier", O_CREAT | O_TRUNC | O_WRONLY, 0644); // on crée le fichier ~/test/fichier et on l'ouvre en écriture
strcpy(chaine,"Ceci est mon fichier");
write(fd,chaine,strlen(chaine)); // On écrit la chaine de caractères dans le fichier
close(fd); // on ferme le fichier

fd=open("./test/fichier", O_RDONLY); // on ré-ouvre le fichier, en lecture seule
strcpy(chaine,""); // on efface le contenu de la chaine de caractères
nb=read(fd,chaine, 1000); // on tente de lire 1000 caractères dans le fichier
printf("%d caractères lus : %s\n",nb,chaine);
close(fd); // On ferme le fichier

unlink("./test/fichier"); // on supprime le fichier
rmdir("./test"); // on supprime le répertoire test
}

L’emploi de ces appels système est vraiment très simple. Voici ce que cela donne à l’exécution :

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *