C Programmation

Lire Syscall Linux

Lire Syscall Linux
Vous devez donc lire les données binaires? Vous voudrez peut-être lire à partir d'un FIFO ou d'un socket? Vous voyez, vous pouvez utiliser la fonction de bibliothèque standard C, mais ce faisant, vous ne bénéficierez pas des fonctionnalités spéciales fournies par Linux Kernel et POSIX. Par exemple, vous pouvez utiliser des délais d'attente pour lire à un certain moment sans recourir à l'interrogation. De plus, vous devrez peut-être lire quelque chose sans vous soucier s'il s'agit d'un fichier ou d'un socket spécial ou de toute autre chose. Votre seule tâche est de lire certains contenus binaires et de les obtenir dans votre application. C'est là que l'appel système de lecture brille.

Lire un fichier normal avec un appel système Linux

La meilleure façon de commencer à travailler avec cette fonction est de lire un fichier normal. C'est la façon la plus simple d'utiliser cet appel système, et pour une raison : il n'a pas autant de contraintes que les autres types de flux ou de tuyaux. Si vous y réfléchissez, c'est logique, lorsque vous lisez la sortie d'une autre application, vous devez avoir une sortie prête avant de la lire et vous devrez donc attendre que cette application écrive cette sortie.

Tout d'abord, une différence clé avec la bibliothèque standard : il n'y a pas de mise en mémoire tampon du tout. Chaque fois que vous appelez la fonction read, vous appelez le noyau Linux, et cela va donc prendre du temps -‌ c'est presque instantané si vous l'appelez une fois, mais peut vous ralentir si vous l'appelez des milliers de fois en une seconde. Par comparaison, la bibliothèque standard mettra en mémoire tampon l'entrée pour vous. Donc, chaque fois que vous appelez read, vous devriez lire plus que quelques octets, mais plutôt un gros tampon comme quelques kilo-octets - sauf si vous avez vraiment besoin de quelques octets, par exemple si vous vérifiez si un fichier existe et n'est pas vide.

Cela a cependant un avantage : chaque fois que vous appelez read, vous êtes sûr d'obtenir les données mises à jour, si une autre application modifie actuellement le fichier. Ceci est particulièrement utile pour les fichiers spéciaux tels que ceux de /proc ou /sys.

Il est temps de vous montrer avec un exemple réel. Ce programme C vérifie si le fichier est PNG ou non. Pour ce faire, il lit le fichier spécifié dans le chemin que vous fournissez dans l'argument de la ligne de commande, et il vérifie si les 8 premiers octets correspondent à un en-tête PNG.

Voici le code :

#inclure
#inclure
#inclure
#inclure
#inclure
#inclure
#inclure
 
énumération typedef
IS_PNG,
TROP COURT,
INVALID_HEADER
pngStatus_t;
 
unsigned int isSyscallSuccessful (const ssize_t readStatus)
renvoie readStatus >= 0;
 

 
/*
* checkPngHeader vérifie si le tableau pngFileHeader correspond à un PNG
* en-tête de fichier.
*
* Actuellement, il ne vérifie que les 8 premiers octets du tableau. Si le tableau est moins
* de 8 octets, TOO_SHORT est renvoyé.
*
* pngFileHeaderLength doit contenir la longueur du tableau. Toute valeur invalide
* peut conduire à un comportement indéfini, tel qu'un plantage de l'application.
*
* Renvoie IS_PNG s'il correspond à un en-tête de fichier PNG. S'il y a au moins
* 8 octets dans le tableau mais ce n'est pas un en-tête PNG, INVALID_HEADER est renvoyé.
*
*/
pngStatus_t checkPngHeader(const unsigned char* const pngFileHeader,
size_t pngFileHeaderLength) char non signé const attenduPngHeader[8] =
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ;
entier je = 0;
 
if (pngFileHeaderLength < sizeof(expectedPngHeader))
renvoie TOO_SHORT ;
 

 
pour (i = 0; je < sizeof(expectedPngHeader); i++)
if (pngFileHeader[i] != attenduPngHeader[i])
renvoie INVALID_HEADER ;
 


 
/* S'il atteint ici, les 8 premiers octets sont conformes à un en-tête PNG. */
renvoie IS_PNG ;

 
int main(int argumentLength,  char *argumentList[])
char *pngFileName = NULL;
caractère non signé pngFileHeader[8] = 0;
 
ssize_t readStatus = 0;
/* Linux utilise un numéro pour identifier un fichier ouvert. */
int fichier png = 0;
pngStatus_t pngCheckResult;
 
if (argumentLongueur != 2)
fputs("Vous devez appeler ce programme en utilisant isPng votre nom de fichier.\n", stderr);
renvoie EXIT_FAILURE ;
 

 
pngFileName = argumentList[1];
pngFile = open(pngFileName, O_RDONLY);
 
si (fichier png == -1)
perror("L'ouverture du fichier fourni a échoué");
renvoie EXIT_FAILURE ;
 

 
/* Lit quelques octets pour identifier si le fichier est PNG. */
readStatus = read(pngFile, pngFileHeader, sizeof(pngFileHeader));
 
if (isSyscallSuccessful(readStatus))
/* Vérifiez si le fichier est un PNG puisqu'il a obtenu les données. */
pngCheckResult = checkPngHeader(pngFileHeader, readStatus);
 
if        (pngCheckResult == TOO_SHORT)
printf("Le fichier %s n'est pas un fichier PNG : il est trop court.\n", pngFileName);
 
else if (pngCheckResult == IS_PNG)
printf("Le fichier %s est un fichier PNG!\n", pngFileName);
 
autre
printf("Le fichier %s n'est pas au format PNG.\n", pngFileName);
 

 
autre
perror("La lecture du fichier a échoué");
renvoie EXIT_FAILURE ;
 

 
/* Fermez le fichier… */
if (close(pngFile) == -1)
perror("La fermeture du fichier fourni a échoué");
renvoie EXIT_FAILURE ;
 

 
pngFichier = 0;
 
renvoie EXIT_SUCCESS ;
 

Vous voyez, c'est un exemple complet, fonctionnel et compilable. N'hésitez pas à le compiler vous-même et à le tester, ça marche vraiment. Vous devez appeler le programme depuis un terminal comme celui-ci :

./isPng votre nom de fichier

Maintenant, concentrons-nous sur l'appel read lui-même :

pngFile = open(pngFileName, O_RDONLY);
si (fichier png == -1)
perror("L'ouverture du fichier fourni a échoué");
renvoie EXIT_FAILURE ;

/* Lit quelques octets pour identifier si le fichier est PNG. */
readStatus = read(pngFile, pngFileHeader, sizeof(pngFileHeader));

La signature de lecture est la suivante (extraite des pages de manuel Linux) :

ssize_t read(int fd, void *buf, size_t count);

Premièrement, l'argument fd représente le descripteur de fichier. J'ai expliqué un peu ce concept dans mon article de fourche.  Un descripteur de fichier est un int représentant un fichier ouvert, socket, pipe, FIFO, périphérique, eh bien c'est beaucoup de choses où les données peuvent être lues ou écrites, généralement de manière similaire à un flux. J'y reviendrai plus en profondeur dans un prochain article.

La fonction open est l'un des moyens de dire à Linux : je veux faire des choses avec le fichier dans ce chemin, veuillez le trouver où il se trouve et me donner accès. Il vous rendra ce descripteur de fichier int appelé et maintenant, si vous voulez faire quelque chose avec ce fichier, utilisez ce numéro. N'oubliez pas d'appeler close lorsque vous avez terminé avec le fichier, comme dans l'exemple.

Vous devez donc fournir ce numéro spécial pour lire. Ensuite, il y a l'argument buf. Vous devez ici fournir un pointeur vers le tableau où read stockera vos données. Enfin, count est le nombre d'octets qu'il lira au plus.

La valeur de retour est de type ssize_t. Type étrange, n'est-ce pas? Cela signifie "signé size_t", en gros c'est un long int. Il renvoie le nombre d'octets qu'il lit avec succès, ou -1 en cas de problème. Vous pouvez trouver la cause exacte du problème dans la variable globale errno créée par Linux, définie dans . Mais pour imprimer un message d'erreur, il est préférable d'utiliser perror car il imprime errno en votre nom.

Dans les fichiers normaux - et seul dans ce cas - read renverra moins que count uniquement si vous avez atteint la fin du fichier. Le tableau buf que vous fournissez doit être assez grand pour contenir au moins un nombre d'octets, ou votre programme peut planter ou créer un bogue de sécurité.

Maintenant, lire n'est pas seulement utile pour les fichiers normaux et si vous voulez ressentir ses super-pouvoirs - Oui je sais que ce n'est dans aucune bande dessinée de Marvel mais il a de vrais pouvoirs - vous voudrez l'utiliser avec d'autres flux tels que des tuyaux ou des sockets. Jetons un coup d'oeil là-dessus :

Fichiers spéciaux Linux et appel système de lecture

Le fait que read fonctionne avec une variété de fichiers tels que des tuyaux, des sockets, des FIFO ou des périphériques spéciaux tels qu'un disque ou un port série est ce qui le rend vraiment plus puissant. Avec quelques adaptations, vous pouvez faire des choses vraiment intéressantes. Premièrement, cela signifie que vous pouvez littéralement écrire des fonctions fonctionnant sur un fichier et l'utiliser avec un tube à la place. C'est intéressant de transmettre des données sans jamais toucher le disque, assurant les meilleures performances.

Cependant, cela déclenche également des règles spéciales. Prenons l'exemple d'une lecture d'une ligne depuis un terminal par rapport à un fichier normal. Lorsque vous appelez read sur un fichier normal, Linux n'a besoin que de quelques millisecondes pour obtenir la quantité de données que vous demandez.

Mais quand il s'agit de terminal, c'est une autre histoire : disons que vous demandez un nom d'utilisateur. L'utilisateur tape dans le terminal son nom d'utilisateur et appuie sur Entrée. Maintenant, vous suivez mes conseils ci-dessus et vous appelez read avec un gros tampon tel que 256 octets.

Si la lecture fonctionnait comme avec les fichiers, elle attendrait que l'utilisateur tape 256 caractères avant de revenir! Votre utilisateur attendrait indéfiniment, puis tuerait malheureusement votre application. Ce n'est certainement pas ce que vous voulez, et vous auriez un gros problème.

D'accord, vous pouvez lire un octet à la fois mais cette solution de contournement est terriblement inefficace, comme je vous l'ai dit plus haut. ça doit marcher mieux que ça.

Mais les développeurs Linux ont pensé lire différemment pour éviter ce problème :

  • Lorsque vous lisez des fichiers normaux, il essaie autant que possible de lire le nombre d'octets et il obtiendra activement les octets du disque si cela est nécessaire.
  • Pour tous les autres types de fichiers, il renverra aussitôt que il y a des données disponibles et au plus compter les octets :
    1. Pour les terminaux, c'est généralement lorsque l'utilisateur appuie sur la touche Entrée.
    2. Pour les sockets TCP, c'est dès que votre ordinateur reçoit quelque chose, peu importe le nombre d'octets qu'il reçoit.
    3. Pour FIFO ou tuyaux, c'est généralement le même montant que ce que l'autre application a écrit, mais le noyau Linux peut fournir moins à la fois si c'est plus pratique.

Vous pouvez donc appeler en toute sécurité avec votre tampon de 2 KiB sans rester enfermé pour toujours. Notez qu'il peut également être interrompu si l'application reçoit un signal. Comme la lecture de toutes ces sources peut prendre des secondes voire des heures - jusqu'à ce que l'autre partie décide d'écrire, après tout - être interrompu par des signaux permet de ne plus rester bloqué trop longtemps.

Cela présente également un inconvénient : lorsque vous souhaitez lire exactement 2 Kio avec ces fichiers spéciaux, vous devez vérifier la valeur de retour de read et appeler read plusieurs fois. read remplira rarement tout votre tampon. Si votre application utilise des signaux, vous devrez également vérifier si la lecture a échoué avec -1 car elle a été interrompue par un signal, en utilisant errno.

Laissez-moi vous montrer comment il peut être intéressant d'utiliser cette propriété spéciale de read :

#define _POSIX_C_SOURCE 1 /* sigaction n'est pas disponible sans ce #define. */
#inclure
#inclure
#inclure
#inclure
#inclure
#inclure
/*
* isSignal indique si read syscall a été interrompu par un signal.
*
* Renvoie TRUE si le syscall de lecture a été interrompu par un signal.
*
* Variables globales : il lit errno défini dans errno.h
*/
unsigned int isSignal (const ssize_t readStatus)
return (readStatus == -1 && errno == EINTR);

unsigned int isSyscallSuccessful (const ssize_t readStatus)
renvoie readStatus >= 0;

/*
* shouldRestartRead indique quand l'appel système de lecture a été interrompu par un
* événement signal ou non, et étant donné que cette raison "d'erreur" est transitoire, nous pouvons
* redémarrer en toute sécurité l'appel de lecture.
*
* Actuellement, il vérifie uniquement si la lecture a été interrompue par un signal, mais il
* pourrait être amélioré pour vérifier si le nombre d'octets cible a été lu et s'il est
* pas le cas, retourne TRUE pour relire.
*
*/
unsigned int shouldRestartRead(const ssize_t readStatus)
return isSignal(readStatus);

/*
* Nous avons besoin d'un gestionnaire vide car l'appel système de lecture ne sera interrompu que si le
* le signal est géré.
*/
void emptyHandler(int ignoré)
revenir;

int main()
/* Est en secondes. */
const int alarmInterval = 5;
const struct sigaction emptySigaction = emptyHandler;
char lineBuf[256] = 0;
ssize_t readStatus = 0;
unsigned int waitTime = 0 ;
/* Ne modifiez pas sigaction sauf si vous savez exactement ce que vous faites. */
sigaction(SIGALRM, &videSigaction, NULL);
alarme (intervalle d'alarme);
fputs("Votre texte :\n", stderr);
fais
/* N'oubliez pas le '\0' */
readStatus = read(STDIN_FILENO, lineBuf, sizeof(lineBuf) - 1);
if (estSignal(readStatus))
waitTime += alarmInterval;
alarme (intervalle d'alarme);
fprintf(stderr, "%u secondes d'inactivité… \n", waitTime);

while (shouldRestartRead(readStatus));
if (isSyscallSuccessful(readStatus))
/* Termine la chaîne pour éviter un bogue lors de sa fourniture à fprintf. */
lineBuf[readStatus] = '\0';
fprintf(stderr, "Vous avez tapé %lu caractères. Voici votre chaîne :\n%s\n", strlen(lineBuf),
lineBuf);
autre
perror("La lecture depuis stdin a échoué");
renvoie EXIT_FAILURE ;

renvoie EXIT_SUCCESS ;

Encore une fois, il s'agit d'une application C complète que vous pouvez compiler et exécuter.

Il fait ce qui suit : il lit une ligne à partir de l'entrée standard. Cependant, toutes les 5 secondes, il imprime une ligne indiquant à l'utilisateur qu'aucune entrée n'a encore été donnée.

Exemple si j'attends 23 secondes avant de taper « Pingouin » :

$ lecture_alarme
Ton texte:
5 secondes d'inactivité…
10 secondes d'inactivité…
15 secondes d'inactivité…
20 secondes d'inactivité…
manchot
Vous avez tapé 8 caractères. Voici votre chaîne :
manchot

C'est incroyablement utile. Il peut être utilisé pour mettre à jour souvent l'interface utilisateur pour imprimer la progression de la lecture ou du traitement de votre application que vous faites. Il peut également être utilisé comme mécanisme de temporisation. Vous pourriez également être interrompu par tout autre signal qui pourrait être utile pour votre application. Quoi qu'il en soit, cela signifie que votre application peut désormais être réactive au lieu de rester bloquée pour toujours.

Les avantages l'emportent donc sur l'inconvénient décrit ci-dessus. Si vous vous demandez si vous devez prendre en charge les fichiers spéciaux dans une application fonctionnant normalement avec des fichiers normaux - et ainsi appeler lis en boucle - Je dirais le faire sauf si vous êtes pressé, mon expérience personnelle a souvent prouvé que remplacer un fichier par un tube ou FIFO peut littéralement rendre une application beaucoup plus utile avec de petits efforts. Il existe même des fonctions C prédéfinies sur Internet qui implémentent cette boucle pour vous : cela s'appelle des fonctions readn.

Conclusion

Comme vous pouvez le voir, la peur et la lecture peuvent se ressembler, elles ne sont pas. Et avec seulement quelques changements sur le fonctionnement de la lecture pour le développeur C, la lecture est beaucoup plus intéressante pour concevoir de nouvelles solutions aux problèmes que vous rencontrez lors du développement d'applications.

La prochaine fois, je vous dirai comment fonctionne write syscall, car lire c'est cool, mais pouvoir faire les deux c'est bien mieux. En attendant, expérimentez la lecture, apprenez à la connaître et je vous souhaite une bonne année!

Meilleurs émulateurs de console de jeu pour Linux
Cet article répertorie les logiciels d'émulation de console de jeu populaires disponibles pour Linux. L'émulation est une couche de compatibilité logi...
Meilleures distributions Linux pour les jeux en 2021
Le système d'exploitation Linux a parcouru un long chemin depuis son apparence originale, simple et basée sur le serveur. Ce système d'exploitation s'...
Comment capturer et diffuser votre session de jeu sur Linux
Dans le passé, jouer à des jeux n'était considéré qu'un passe-temps, mais avec le temps, l'industrie du jeu a connu une croissance énorme en termes de...