web-dev-qa-db-fra.com

comment select () est-il alerté lorsqu'un fd devient "prêt"?

Je ne sais pas pourquoi j'ai du mal à trouver cela, mais je regarde un code Linux où nous utilisons select() attendant un descripteur de fichier pour signaler qu'il est prêt. Dans la page de manuel de select:

select() and pselect() allow a program to monitor multiple file descriptors,
waiting until one or more of the file descriptors become "ready" for some
class of I/O operation 

Donc, c'est génial ... J'appelle select sur un descripteur, lui donne une valeur de temporisation et commence à attendre que l'indication disparaisse. Comment le descripteur de fichier (ou le propriétaire du descripteur) signale-t-il qu'il est "prêt" de telle sorte que l'instruction select() renvoie?

22
Mike

Il signale qu'il est prêt par retour.

select attend les événements qui sont généralement hors du contrôle de votre programme. En gros, en appelant select, votre programme dit "Je n'ai rien à faire jusqu'à ..., veuillez suspendre mon processus".

La condition que vous spécifiez est un ensemble d'événements dont chacun vous réveillera.

Par exemple, si vous téléchargez quelque chose, votre boucle devra attendre l'arrivée de nouvelles données, un délai d'expiration si le transfert est bloqué ou l'utilisateur à interrompre, ce qui est précisément ce que fait select.

Lorsque vous avez plusieurs téléchargements, les données arrivant sur l'une des connexions déclenchent une activité dans votre programme (vous devez écrire les données sur le disque), vous devez donc donner une liste de toutes les connexions de téléchargement à select dans la liste des descripteurs de fichiers à surveiller pour "lire".

Lorsque vous téléchargez des données quelque part en même temps, vous utilisez à nouveau select pour voir si la connexion accepte actuellement les données. Si l'autre côté est sur ligne commutée, il n'acceptera les données que lentement, de sorte que votre tampon d'envoi local est toujours plein, et toute tentative d'écrire plus de données se bloquerait jusqu'à ce que l'espace tampon soit disponible, ou échouerait. En passant le descripteur de fichier que nous envoyons à select en tant que descripteur "d'écriture", nous sommes avertis dès que l'espace tampon est disponible pour l'envoi.

L'idée générale est que votre programme devient piloté par les événements, c'est-à-dire qu'il réagit aux événements externes d'une boucle de message commune plutôt que d'effectuer des opérations séquentielles. Vous dites au noyau "c'est l'ensemble des événements pour lesquels je veux faire quelque chose", et le noyau vous donne un ensemble d'événements qui se sont produits. Il est assez courant que deux événements se produisent simultanément; par exemple, un TCP accusé de réception a été inclus dans un paquet de données, cela peut rendre le même fd à la fois lisible (les données sont disponibles) et inscriptible (les données acquittées ont été supprimées du tampon d'envoi), donc vous doit être prêt à gérer tous les événements avant d'appeler à nouveau select.

L'un des points les plus subtils est que select vous donne essentiellement la promesse qu'une invocation de read ou write ne bloquera pas, sans aucune garantie sur l'appel lui-même. Par exemple, si un octet d'espace tampon est disponible, vous pouvez essayer d'écrire 10 octets, et le noyau reviendra et dira "J'ai écrit 1 octet", vous devez donc être prêt à gérer ce cas également. Une approche typique consiste à avoir un tampon "données à écrire dans ce fd", et tant qu'il n'est pas vide, le fd est ajouté à l'ensemble d'écriture et l'événement "inscriptible" est géré en tentant d'écrire tout les données actuellement dans le tampon. Si le tampon est vide par la suite, très bien, sinon, attendez à nouveau "inscriptible".

L'ensemble "exceptionnel" est rarement utilisé - il est utilisé pour les protocoles qui ont des données hors bande où il est possible que le transfert de données se bloque, tandis que d'autres données doivent passer. Si votre programme ne peut pas actuellement accepter les données d'un descripteur de fichier "lisible" (par exemple, vous téléchargez et le disque est plein), vous ne voulez pas inclure le descripteur dans l'ensemble "lisible", car vous ne pouvez pas gérer l'événement et select retournerait immédiatement s'il était à nouveau invoqué. Si le récepteur inclut le fd dans l'ensemble "exceptionnel" et que l'expéditeur demande à sa pile IP d'envoyer un paquet avec des données "urgentes", le récepteur est alors réveillé et peut décider de supprimer les données non gérées et de se resynchroniser avec l'expéditeur . Le protocole telnet l'utilise, par exemple, pour la gestion Ctrl-C. À moins que vous ne conceviez un protocole qui nécessite une telle fonctionnalité, vous pouvez facilement le laisser sans danger.

Exemple de code obligatoire:

#include <sys/types.h>
#include <sys/select.h>

#include <unistd.h>

#include <stdbool.h>

static inline int max(int lhs, int rhs) {
    if(lhs > rhs)
        return lhs;
    else
        return rhs;
}

void copy(int from, int to) {
    char buffer[10];
    int readp = 0;
    int writep = 0;
    bool eof = false;
    for(;;) {
        fd_set readfds, writefds;
        FD_ZERO(&readfds);
        FD_ZERO(&writefds);

        int ravail, wavail;
        if(readp < writep) {
            ravail = writep - readp - 1;
            wavail = sizeof buffer - writep;
        }
        else {
            ravail = sizeof buffer - readp;
            wavail = readp - writep;
        }

        if(!eof && ravail)
            FD_SET(from, &readfds);
        if(wavail)
            FD_SET(to, &writefds);
        else if(eof)
            break;
        int rc = select(max(from,to)+1, &readfds, &writefds, NULL, NULL);
        if(rc == -1)
            break;
        if(FD_ISSET(from, &readfds))
        {
            ssize_t nread = read(from, &buffer[readp], ravail);
            if(nread < 1)
                eof = true;
            readp = readp + nread;
        }
        if(FD_ISSET(to, &writefds))
        {
            ssize_t nwritten = write(to, &buffer[writep], wavail);
            if(nwritten < 1)
                break;
            writep = writep + nwritten;
        }
        if(readp == sizeof buffer && writep != 0)
            readp = 0;
        if(writep == sizeof buffer)
            writep = 0;
    }
}

Nous essayons de lire si nous avons de l'espace tampon disponible et il n'y a pas de fin de fichier ou d'erreur du côté lecture, et nous essayons d'écrire si nous avons des données dans le tampon; si la fin du fichier est atteinte et que le tampon est vide, alors nous avons terminé.

Ce code se comportera clairement sous-optimal (c'est un exemple de code), mais vous devriez être en mesure de voir qu'il est acceptable que le noyau fasse moins que ce que nous avons demandé à la fois en lecture et en écriture, auquel cas nous revenons simplement en disant "chaque fois que vous êtes prêt ", et que nous ne lisons ni n'écrivons sans demander s'il bloquera.

29
Simon Richter

De la même page de manuel:

À la sortie, les ensembles sont modifiés sur place pour indiquer quels descripteurs de fichier ont réellement changé d'état.

Utilisez donc FD_ISSET() sur les ensembles passés pour sélectionner quels FD sont prêts.

8