I/O

Guillaume Chanel

Remerciements à Jean-Luc Falcone

Canaux

Canaux

image

Merci à Jacques Menu

Descripteur de Canal

  • Un descripteur de canal d'entrée/sortie est représenté par un entier non-négatif qui correspond à un index dans la table des canaux ouverts par le processus.
  • Trois descripteurs sont créés au lancement d'un processus:
    0Entrée standard
    1Sortie standard
    2Erreur standard

Ouverture de canal

A l'ouverture d'un canal d'entrée/sortie:

  • un élément est ajouté à la table des canaux ouverts du noyau.
  • un descripteur y est associé (indice).
  • un pointeur vers l'élément est introduit dans la table des canaux ouverts du processus à l'indice associé.
Remarques

Un processus peut ouvrir plusieurs canaux vers un même inode:

  • Plusieurs éléments distincts seront créés dans la tables du noyau
  • Plusieurs descripteurs distincts seront créés.

Table des canaux ouverts

Un élément de la tables des canaux ouverts contient (entre autre):

  • Mode d'accès aux données (O_RDONLY, O_WRONLY, O_RDWR)
  • L'état du canal (O_APPEND, O_NONBLOCK, O_ASYNC)
  • Système de fichier et numéro d'inode
  • Compteur de références pointant sur l'élément
  • Pour les fichiers normaux: la position actuelle
  • Le bit close-on-exec

Opérations sur les canaux

Les mêmes fonctions sont utilisés quelque soit le type de fichier (fichier de données, socket, pipe, périphériques, etc.):

OpérationAppel système
Ouvertureopen
Lecture de donnéesread
Ecriture de donnéeswrite
Contrôle du fonctionementfcntl
dup
dup2
Fermetureclose

Ouverture, Fermeture et Contrôle

Ouvrir un canal (open)

L'appel système suivant ouvre un canal:

int open(const char *pathname, int flags, mode_t mode);
  • pathname est le nom du fichier
  • flags est un champ de bit indiquant le mode d'accès au fichier
  • mode indique les permissions si le fichier est créé par open (cf. O_CREAT).
  • Retourne soit le descripteur créé, soit -1 en cas d'erreur.
  • Le descripteur retourné est le plus petit descripteur possible.

Flags

Les flags suivants peuvent être passés à la fonction open:

O_RDONLYLecture seule
O_WRONLYEcriture seule
O_RDWRLecture et écriture
O_APPENDEcrit à la fin
O_CREATCrée le fichier s'il n'existe pas
O_TRUNCEfface le fichier s'il existe
O_EXCLErreur si O_CREATE est spécifié et que le fichier existe
Remarques

Les trois premiers flags sont particuliers:

  • On ne peut pas les combiner entre eux
  • On doit en passer au moins un.

Mode (permissions)

Lorsque que le flag O_CREAT est passé, il faut spécifier les permissions:

int fd = open("/tmp/foo.txt", O_RDWR | O_CREAT | O_OEXCL, 0640);

Si O_CREAT est absent, le mode est ignoré.

Ouverture multiple

Si un fichier est ouvert plusieurs fois par un ou plusieurs processus:

  • Plusieurs canaux seront créés
  • Dans le cas d'un fichier normal, chaque canal aura sa position dans le fichier et éventuellement ses propres buffer
  • Toutes les opérations s'effectuent indépendamment et en parallèle
Attention

Le développeur est responsable de coordonner l'accès aux fichiers. Pour en savoir plus vous renseigner sur les verrous (lock)

Fermer un canal (close)

  • L'appel système suivant permet de fermer un canal ouvert fd:
int close(int fd);
  • Le descripteur est liberé et pourra être recyclé (attention)
  • Retourne 0 en cas de succès et -1 en cas d'erreur
  • Le compteur de référence de l'élément de la table des canaux ouverts est décrémenté
    • s'il atteint 0, l'élément est effacé et les ressources libérées
  • Si fd est la dernière indirection vers un nom de fichier effacé par unlink, le fichier est effectivement effacé.

Fermeture automatique d'un canal

  • Lors de la terminaison d'un processus par exit ou abort, le noyau ferme tous les descripteurs (équivalent à close).
  • Egalement lors d'un appel à execv() si le bit close-on-exec est égal à 1.

Manipulation des descripteurs (fcntl)

On peut manipuler finement un descripteur avec:

int fcntl(int fd, int cmd, ... /* arg */ );
  • fd est un descripteur et cmd la commande.
  • Utilisé par exemple pour dupliquer les descritpeurs (slide suivant)
  • Beaucoup d'autre manipulations peuvent être effectuées sur les descripteurs avec fcntl (voir man).

Duplication de descripteurs

dup et dup2 permettent de dupliquer des descripteurs de fichiers qui pointerons sur la même entrée de la table des canaux.


                            #include <unistd.h>
                            int dup(int fildes);
                            int dup2(int fildes, int fildes2);
                        
  • fildes: un descripteur à dupliquer
  • retourne le nouveau descripteur ou -1 en cas d'erreur
  • pour dup2 seulement: ferme le descripteur existant fildes2 et y stocke un nouveau descripteur qui est un duplicata de fildes

dup est équivalent à:


                            fcntl(fildes, F_DUPFD, 0);
                        

Lecture/Ecriture

Lecture: bas niveau (read)

Pour lire les données d'un descripteur, on peut utiliser:

ssize_t read(int fd, void *buf, size_t count);
  • Essaie de lire jusqu'à count bytes depuis le descripteur fd
  • Copie les bytes lus dans buf
  • Retourne le nombre de bytes effectivement lus en cas de succès
  • Retourne -1 en cas d'erreur (cf. errno)
  • Le nombre de bytes retournés peut être plus petit que ce qui est demandé
  • La position avance d'autant de bytes

Ecriture: bas niveau (write)

Pour écrire des données sur un descripteur, on peut utiliser:

ssize_t write(int fd, void *buf, size_t count);
  • Essaie d'écrire jusqu'à count bytes sur le descripteur fd
  • buf contient les bytes à écrire
  • Retourne le nombre de bytes effectivement écrits en cas de succès
  • Retourne -1 en cas d'erreur (cf. errno)
  • Le nombre de bytes écrits peut être plus petit que ce qui est demandé
  • La position avance d'autant de bytes

Notes sur read et write

Sur des fichiers standards read et write retournent moins que demandé quand:

  • read arrive en fin de fichier;
  • write n'a plus d'espace sur le système de fichier.

Mais il faut penser que:

  • une lecture/ecriture peut toujours être intérrompue par un signal;
  • les utilisateurs peuvent passer à votre programme tout type de fichiers qui se comportent différemments (pipes, sockets, charater devices, etc.)
Attention

Il faut donc bien gérer les cas où read/write font des opérations partielles.

Une autre solution est de faire appel aux entrées/sorties bufferisées.

Exemple (examples/copy.c)

Include example there (see script)

Accès aléatoire (lseek)

  • La position d'un fichier ouvert est à 0
  • Elle avance à chaque lecture/écriture
  • Si le type de fichier le permet, on peut changer la position avec:
off_t lseek(int fd, off_t offset, int whence);
  • Avance la position du descripteur fd
  • L'offset est a interprété en fonction de l'argument whence (slide suivant)
  • En cas de succès retourne la position en byte depuis le début du fichier.
  • En cas d'échec, retourne -1 (cf. errno)

Accès aléatoire (2)

La nouvelle position, dépend de whence et d'offset

whenceNouvelle position
SEEK_SEToffset
SEEK_CURposition courant + offset
SEEK_ENDfin du fichier + offset
Remarque

Ecrire plus loin que la fin du fichier crée des trous, remplis de \0.

Exemple (examples/seekStruct.c)

Include example there (see script)

Fichiers temporaires

Fichiers temporaires (temporary files)

  • Un programme peut utiliser des fichier temporaires, par exemple pour:
    • Soulager la mémoire vive
    • Téléchargement partiel
    • Communication entre processus
Problème
  • Il faut créer un nom unique pour éviter d'écraser d'autres fichiers existant.
  • Il faut être sûr que le fichier soit effacé lorsque l'on quitte les processus.

Créer et ouvrir un fichier temporaire (mkstemp)

La fonction mkstemp permet de créer et d'ouvrir un fichier temporaire:

int mkstemp(char *template);
  • Il faut passer un modèle pour le nom du fichier (*template), terminé par 6 "X".
  • En cas de succès, la chaîne *template est modifiée avec le vrai nom du fichier.
  • Le fichier est créé avec les permissions 0600
  • Retourne un descripteur de fichier ouvert en cas de succès (-1 sinon).

Exemple mkstemp


char name[15] = "";
int fd = -1;
strncpy( name, "/tmp/ed.XXXXXX", sizeof name );
fd = mkstemp( name );
if( fd < 0 ) {
    //Gerer l'erreur
}
else {
    printf( "The temporaray filename is %s\n", name );
}
                    

Effacer automatiquement

  • Rappel:
    • unlink n'efface pas un fichier, seulement son nom
    • Le fichier est effacé s'il n'y a pas de descripteur ouvert.
  • On peut alors immédiatement appeler unlink sur un fichier temporaire créé.
  • Le processus peut toujours interragir avec le fichier via le descripteur.
  • A la fermeture du descripteur (ou à la terminaison du processus) le fichier est effacé.
  • Aucun autre processus ne peut y accéder.

Répertoires temporaires (mkdtemp)

On peut aussi créer des répertoires temporaires:

char *mkdtemp(char *template);
  • Le template suit les mêmes règles que pour mkstemp.
  • Le répertoire créé a les permissions 0700.
  • En cas de succès retourne le template (modifié).
  • En cas d'écher retourne NULL.

PIPES et FIFO

Rappel sur les pipes et FIFO

Rappel:

$ ls -lh /dev | more
  • Le noyau crée un canal de communication anonyme (pipe en anglais, tube en français) entre les processus ls et more
  • Il est également possible de créer des tubes nommées (named pipes ou FIFO).

Tubes anonymes

On peut créer un canal de communication anonyme en utilisant:

int pipe(int fildes[2]);
  • filedes[0] est un descripteur de fichier représentant la sortie du tube/pipe (i.e. on peu lire sur ce descripteur);
  • filedes[1] est un descripteur de fichier représentant l'entrée du tube/pipe (i.e. on peu écrire sur ce descripteur);
  • retourne -1 en cas d'erreur (voir errno);
  • Pas d'accès aléatoire possible.

Concepts generaux des pipes

Les tubes et FIFO:

  • Permettent une communication à haute vitesse entre deux processus sur la même machine.
  • Ont deux extrémités: une ouverte en lecture et une ouverte en écriture.
  • Sont unidirectionels: en conséquence du point précédent l'information ne transite que dans un sens.

  • Sont bloquants:
    • L'ouverture d'une extrémité bloque jusqu'à l'ouverture de l'autre extrémité.
    • Permet d'établir des Rendez-Vous
    • Possibilité de deadlocks !

Pipe nommé: Commande mkfifo(1)

On peut créer un FIFO avec la commande:

mkfifo [OPTION]... NOM...
  • NOM est le nom du FIFO à créer
  • Parmi les options on peut passer les permissions du FIFO par l'option -m MODE.
Exemple shell

$ mkfifo -m 0640 /tmp/fifo1
$ ls -lh /dev > /tmp/fifo1

$ more /tmp/fifo1  # Dans un autre shell
                        

Fonction POSIX mkfifo(2)

On peut créer un FIFO avec l'appel système:

int mkfifo(const char *pathname, mode_t mode);
  • pathname est le nom du fichier à créer
  • mode représente les permissions (modifiées mode & ~umask)
  • Un FIFO peut être ouvert en lecture/écriture comme n'importe quel fichier (open/read) mais il faut veiller à respecter la directionalité du fifo
  • Pas d'accès aléatoire possible.

Exemple - producer

Include example there (see script)

Exemple - consumer

Include example there (see script)