I/O

Guillaume Chanel

Remerciements à Jean-Luc Falcone

Cours système d'exploitation by Guillaume Chanel, Jean-Luc Falcone and University of Geneva is licensed under CC BY-NC-SA 4.0

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):

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
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_NONBLOCKCanal non-bloqué par une lecture/é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 (cf. verrous).

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.
  • permet de réserver un fichier (cf. verrous)

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

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'argument whence permet d'interpréter l'offset
  • 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)

Verrous

Scénario

  • Un fichier contient le montant disponible de comptes bancaires.
  • Les comptes sont écrits séquentiellements
Fonction de retrait

int withdraw( int fd, int account, unsigned int amount ) {
    unsigned int before = GET_AMOUNT( fd, account );
    unsigned int after;
    if( before < amount )
        return -1;
    after = before - amount;
    SET_AMOUNT( fd, account, after );
    return after;
}
                        

Problème

  • Le compte #10 est crédité de 300 CHF.
  • Les deux processus A et B sont exécutés en parallèle.
  • A appelle:
    withdraw( accounts, 10, 200 )
  • B appelle:
    withdraw( accounts, 10, 150 )
  • Qu'est qui pourrait se passer ?

Verrous (locks)

  • Les processus peuvent poser des verrous sur des fichiers pour se coordoner.

  • Il y a deux types de stratégie:

    • verrouillage facultatif
    • verrouillage obligatoire

Verrouillage facultatif (advisory locks)

  • Un verrou facultatif n'est pas pris en compte par les opérations de lecture/écriture.
  • Le programmeur peut:
    • Poser un verrou
    • Vérifier si un verrou existe
    • Enlever un verrou
  • Un verrou peut porter sur une partie d'un fichier
  • Standard POSIX

Verrouillage obligatoire (mandatory locks)

Un verrou obligatoire bloque les opérations de lecture/écriture

Attention
  • Non standard (dépend des OS et des sytèmes de fichiers).
  • Risque de blocage si un programmeur oublie de dévérouiller.
  • Un programme peut être bloqué par son propre verrou.
  • Sous GNU/Linux, doit être activé explicitement sur le système de fichiers.

Exemple verrou

Fonction de retrait

int withdraw( int fd, int account, unsigned int amount ) {
    LOCK( fd, account );
    unsigned int before = GET_AMOUNT( fd, account );
    unsigned int after;
    if( before < amount )
        return -1;
    after = before - amount;
    SET_AMOUNT( fd, account, after );
    UNLOCK( fd, account );
    return after;
}
                        

Types de verrous

Les verrous peuvent être:

partagésplusieurs verrous peuvent être posés en même temps (shared)
exclusifsun seul verrou peut être posé à la fois.

Ces deux types permettent de résoudre le Readers-Writers-Problem.

Structure d'un verrou (struct flock)

man fcntl

struct flock {
    ...
    short l_type;    /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */
    short l_whence;  /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
    off_t l_start;   /* Starting offset for lock */
    off_t l_len;     /* Number of bytes to lock */
    pid_t l_pid;     /* PID of process blocking our lock (F_GETLK only) */
    ...
};
                        

Type de verrous (constantes utilisées)

F_RDLCKplusieurs verrous peuvent être posés en même temps (verrou partagé, shared)
F_WRLCKun seul verrou peut être posé à la fois (verrou exclusif, exclusive)
F_UNLCKpas de verrou (débloque un verrou existant).

Position du verrou

l_whencefonctionne comme pour lseek
l_startdonne le début du verrou (dépend de l_whence)
l_lenNombre de "bytes" verouillés
Remarque: peu de lien entre le vérrou et les données vérouillées
  • l_start ne peut pas pointer avant le début du fichier.
  • Le verrou peut dépasser le fichier
  • Il n'y a pas vraiment de lien entre la taille du fichier et la plage réservée par le verrou (sauf le début)
  • On peut aussi interpréter les offsets+tailles comme des "enregistrements".

Poser/Enlever un verrou (fcntl)

L'appel système suivant permet de poser/enlever les verrous:

int fcntl(int fd, int cmd, struct flock *lock);
  • fd spécifie le descripteur du fichier.
  • Le comportement varie selon la commande (cmd) utilisée
  • Le pointeur lock permet de passer ou de recevoir les paramètres du verrou.

Commande F_SETLK

  • La commande F_SETLK permet de poser/enlever un verrou.
  • L'action dépend de lock->l_type:
    F_RDLCKessaie de poser un verrou partagé
    F_WRLCKessaie de poser un verrou exclusif
    F_UNLCKenlève un verrou précédent
  • Retourne immédiatement
  • Si un conflit empêche de poser le verrou retourne -1 et produit les erreurs EACCES ou EAGAIN.

Commande F_SETLK: LOCK


int LOCK( int fd, int account ) {
    struct flock fl;
    fl.l_type = F_WRLCK;
    fl.l_whence = SEEK_SET;
    fl.l_start = account;
    fl.l_len = 1;
    if (fcntl(fd, F_SETLK, &fl) == -1) {
        if (errno == EACCES || errno == EAGAIN) {
            //Attendre ou faire autre chose
            return LOCK( fd, account );
        } else return -1;
    } else return 0;
}
                    

Commande F_SETLK: UNLOCK


int UNLOCK( int fd, int account ) {
    struct flock fl;
    fl.l_type = F_UNLCK;
    fl.l_whence = SEEK_SET;
    fl.l_start = account;
    fl.l_len = 1;
    return fcntl(fd, F_SETLK, &fl);
}
                    

Commande F_SETLKW

  • La commande F_SETLKW permet de poser/enlever un verrou.
  • L'action dépend de lock->l_type:
    F_RDLCKessaie de poser un verrou partagé
    F_WRLCKessaie de poser un verrou exclusif
    F_UNLCKenlève un verrou précédent
  • Attends en cas de conflit
  • Si un signal est capturé l'appel se termine avec un retour de -1 et produit l'erreur EINTR.

Commande F_SETLKW: LOCK


int LOCK( int fd, int account ) {
    struct flock fl;
    fl.l_type = F_WRLCK;
    fl.l_whence = SEEK_SET;
    fl.l_start = account;
    fl.l_len = 1;
    return fcntl(fd, F_SETLKW, &fl);
}
                    

Commande F_GETLK

  • La commande F_GETLK permet d'obtenir des informations sur un verrou.
  • La structure *lock doit être remplie avec les informations concernant un verrou que l'on souhaite poser.
  • Elle est modifiée après l'appel:
    • Si le verrou peut être placé, lock->l_type vaut F_UNLCK
    • Si le verrou ne peut être placé, *lock contient des informations sur un des verrous déjà en place.
  • Elle retourne immédiatement.

Problèmes possibles

Attention
  • Tous les verrous posés par un processus sont automatiquement enlevés lorsque le descripteur est fermé.
  • Tous les descripteurs d'un processus sont fermés à sa terminaison, donc tous les verrous sont automatiquement enlevés.
  • Ne sont pas réentrants.
  • Nécéssite la coopération de tous les processus…

Lock files

  • Approche alternative: représenter les verrous comme des fichiers
  • On peut utiliser open avec les flags O_CREAT|O_EXCL.
  • Il ne faut pas oublier des les enlever.
  • Survit à la terminaison du processus.
Attention
  • En cas de crash, le fichier subsistera…
  • Problème affectant (entre autre) Mozilla Firefox

Lock files: Exemple


#define LOCK_FILE ".lock"

int LOCK() {
    int fd = open( LOCK_FILE, (O_CREAT|O_EXCL), 0600 );
    if( fd < 0 && errno == EEXIST ) {
        //ATTENDRE
        return LOCK();
    }
    return fd;
}

int UNLOCK( int fd ) {
    close(fd);
    return unlink(LOCK_FILE);
}
                    

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.