C Programmation

Tutoriel d'appel système Linux avec C

Tutoriel d'appel système Linux avec C
Dans notre dernier article sur les appels système Linux, j'ai défini un appel système, discuté des raisons pour lesquelles on pourrait les utiliser dans un programme et approfondi leurs avantages et inconvénients. J'ai même donné un bref exemple en assembleur au sein de C. Il a illustré le point et décrit comment passer l'appel, mais n'a rien fait de productif. Pas exactement un exercice de développement passionnant, mais il a illustré le point.

Dans cet article, nous allons utiliser des appels système réels pour effectuer un travail réel dans notre programme C. Tout d'abord, nous verrons si vous devez utiliser un appel système, puis nous fournirons un exemple utilisant l'appel sendfile() qui peut considérablement améliorer les performances de copie de fichiers. Enfin, nous passerons en revue quelques points à retenir lors de l'utilisation des appels système Linux.

Avez-vous besoin d'un appel système?

Bien qu'il soit inévitable que vous utilisiez un appel système à un moment donné de votre carrière de développement C, à moins que vous ne visiez des performances élevées ou une fonctionnalité de type particulier, la bibliothèque glibc et les autres bibliothèques de base incluses dans les principales distributions Linux prendront en charge la majorité des vos besoins.

La bibliothèque standard glibc fournit un cadre multiplateforme et bien testé pour exécuter des fonctions qui nécessiteraient autrement des appels système spécifiques au système. Par exemple, vous pouvez lire un fichier avec fscanf(), fread(), getc(), etc., ou vous pouvez utiliser l'appel système Linux read(). Les fonctions de la glibc offrent plus de fonctionnalités (i.e. meilleure gestion des erreurs, E/S formatées, etc.) et fonctionnera sur tous les systèmes pris en charge par la glibc.

D'autre part, il y a des moments où des performances sans compromis et une exécution exacte sont essentielles. Le wrapper fourni par fread() va ajouter une surcharge, et bien que mineur, n'est pas entièrement transparent. De plus, vous ne voudrez peut-être pas ou n'aurez pas besoin des fonctionnalités supplémentaires fournies par le wrapper. Dans ce cas, vous êtes mieux servi avec un appel système.

Vous pouvez également utiliser des appels système pour exécuter des fonctions non encore supportées par la glibc. Si votre copie de la glibc est à jour, ce ne sera guère un problème, mais le développement sur des distributions plus anciennes avec des noyaux plus récents peut nécessiter cette technique.

Maintenant que vous avez lu les clauses de non-responsabilité, les avertissements et les détours potentiels, examinons maintenant quelques exemples pratiques.

Sur quel processeur sommes-nous?

Une question que la plupart des programmes ne pensent probablement pas à poser, mais néanmoins valable. Ceci est un exemple d'appel système qui ne peut pas être dupliqué avec la glibc et n'est pas couvert par un wrapper glibc. Dans ce code, nous appellerons l'appel getcpu() directement via la fonction syscall(). La fonction syscall fonctionne comme suit :

syscall(SYS_call, arg1, arg2,… ​​);

Le premier argument, SYS_call, est une définition qui représente le numéro de l'appel système. Lorsque vous incluez sys/syscall.h, ceux-ci sont inclus. La première partie est SYS_ et la deuxième partie est le nom de l'appel système.

Les arguments de l'appel vont dans arg1, arg2 ci-dessus. Certains appels nécessitent plus d'arguments, et ils continueront dans l'ordre à partir de leur page de manuel. N'oubliez pas que la plupart des arguments, en particulier pour les retours, nécessiteront des pointeurs vers des tableaux de caractères ou de la mémoire allouée via la fonction malloc.

Exemple 1.c

#inclure
#inclure
#inclure
#inclure
 
int main()
 
processeur non signé, nœud ;
 
// Obtenir le cœur du processeur actuel et le nœud NUMA via un appel système
// Notez que cela n'a pas de wrapper glibc donc nous devons l'appeler directement
syscall(SYS_getcpu, &cpu, &node, NULL);
 
// Afficher les informations
printf("Ce programme s'exécute sur le cœur du processeur %u et le nœud NUMA %u.\n\n", cpu, nœud);
 
renvoie 0 ;
 

 
Pour compiler et exécuter :
 
exemple gcc1.c -o exemple1
./Exemple 1

Pour des résultats plus intéressants, vous pouvez faire tourner des threads via la bibliothèque pthreads, puis appeler cette fonction pour voir sur quel processeur votre thread s'exécute.

Sendfile : performances supérieures

Sendfile fournit un excellent exemple d'amélioration des performances grâce aux appels système. La fonction sendfile() copie les données d'un descripteur de fichier à un autre. Plutôt que d'utiliser plusieurs fonctions fread() et fwrite(), sendfile effectue le transfert dans l'espace du noyau, réduisant ainsi la surcharge et augmentant ainsi les performances.

Dans cet exemple, nous allons copier 64 Mo de données d'un fichier à un autre. Dans un test, nous allons utiliser les méthodes de lecture/écriture standard dans la bibliothèque standard. Dans l'autre, nous utiliserons les appels système et l'appel sendfile() pour faire exploser ces données d'un emplacement à un autre.

essai1.c (glibc)

#inclure
#inclure
#inclure
#inclure
 
#define BUFFER_SIZE 67108864
#define BUFFER_1 "buffer1"
#define BUFFER_2 "buffer2"
 
int main()
 
FICHIER *fOut, *fIn;
 
printf("\nTest d'E/S avec les fonctions traditionnelles de la glibc.\n\n");
 
// Récupérer un tampon BUFFER_SIZE.
// Le tampon contiendra des données aléatoires mais nous ne nous en soucions pas.
printf("Allocation de 64 Mo de mémoire tampon :                     " );
char *buffer = (char *) malloc(BUFFER_SIZE);
printf("FAIT\n");
 
// Ecrit le tampon dans fOut
printf("Ecriture des données dans le premier tampon :                " );
fOut = fopen(TAMPON_1, "wb");
fwrite(buffer, sizeof(char), BUFFER_SIZE, fOut);
fferme(fSortie);
printf("FAIT\n");
 
printf("Copie des données du premier fichier vers le second :      " );
fIn = fopen(TAMPON_1, "rb");
fOut = fopen(TAMPON_2, "wb");
fread(buffer, sizeof(char), BUFFER_SIZE, fIn);
fwrite(buffer, sizeof(char), BUFFER_SIZE, fOut);
fferme(fIn);
fferme(fSortie);
printf("FAIT\n");
 
printf("Libération du tampon :                             " );
libre (tampon);
printf("FAIT\n");
 
printf("Suppression de fichiers :                             " );
supprimer(TAMPON_1);
supprimer(TAMPON_2) ;
printf("FAIT\n");
 
renvoie 0 ;
 

test2.c (appels système)

#inclure
#inclure
#inclure
#inclure
#inclure
#inclure
#inclure
#inclure
#inclure
 
#define BUFFER_SIZE 67108864
 
int main()
 
int fOut, fIn;
 
printf("\nTest d'E/S avec sendfile() et appels système associés.\n\n");
 
// Récupérer un tampon BUFFER_SIZE.
// Le tampon contiendra des données aléatoires mais nous ne nous en soucions pas.
printf("Allocation de 64 Mo de mémoire tampon :                     " );
char *buffer = (char *) malloc(BUFFER_SIZE);
printf("FAIT\n");
 
// Ecrit le tampon dans fOut
printf("Ecriture des données dans le premier tampon :                " );
fOut = open("buffer1", O_RDONLY);
write(fOut, &buffer, BUFFER_SIZE);
fermer(fSortie);
printf("FAIT\n");
 
printf("Copie des données du premier fichier vers le second :      " );
fIn = open("buffer1", O_RDONLY);
fOut = open("buffer2", O_RDONLY);
sendfile(fOut, fIn, 0, BUFFER_SIZE);
fermer(fIn);
fermer(fSortie);
printf("FAIT\n");
 
printf("Libération du tampon :                             " );
libre (tampon);
printf("FAIT\n");
 
printf("Suppression de fichiers :                             " );
unlink("buffer1");
unlink("buffer2");
printf("FAIT\n");
 
renvoie 0 ;
 

Compiler et exécuter les tests 1 et 2

Pour construire ces exemples, vous aurez besoin des outils de développement installés sur votre distribution. Sur Debian et Ubuntu, vous pouvez l'installer avec :

apt install build-essentials

Puis compilez avec :

test gcc1.c -o test1 && gcc test2.c -o test2

Pour exécuter les deux et tester les performances, exécutez :

temps ./test1 && heure ./test2

Vous devriez obtenir des résultats comme celui-ci :

Test d'E/S avec les fonctions glibc traditionnelles.

Allocation de 64 Mo de mémoire tampon :                     TERMINÉ
Écriture des données dans le premier tampon :                TERMINÉ
Copie des données du premier fichier vers le second :      TERMINÉ
Libérer le tampon :                              TERMINÉ
Suppression de fichiers :                              FAIT
réel    0m0.397s
utilisateur    0m0.des milliers
sys     0m0.203s
Test d'E/S avec sendfile() et appels système associés.
Allocation de 64 Mo de mémoire tampon :                     TERMINÉ
Écriture des données dans le premier tampon :                TERMINÉ
Copie des données du premier fichier vers le second :      DONE
Libérer le tampon :                              TERMINÉ
Suppression de fichiers :                              TERMINÉ
réel    0m0.019s
utilisateur    0m0.des milliers
sys     0m0.016s

Comme vous pouvez le voir, le code qui utilise les appels système s'exécute beaucoup plus rapidement que l'équivalent de la glibc.

Choses à retenir

Les appels système peuvent augmenter les performances et fournir des fonctionnalités supplémentaires, mais ils ne sont pas sans inconvénients. Vous devrez peser les avantages des appels système par rapport au manque de portabilité de la plate-forme et aux fonctionnalités parfois réduites par rapport aux fonctions de la bibliothèque.

Lors de l'utilisation de certains appels système, vous devez veiller à utiliser les ressources renvoyées par les appels système plutôt que les fonctions de bibliothèque. Par exemple, la structure FILE utilisée pour les fonctions fopen(), fread(), fwrite() et fclose() de la glibc n'est pas la même que le numéro de descripteur de fichier de l'appel système open() (renvoyé sous forme d'entier). Les mélanger peut entraîner des problèmes.

En général, les appels système Linux ont moins de voies de pare-chocs que les fonctions glibc. S'il est vrai que les appels système ont une gestion et un rapport d'erreurs, vous obtiendrez des fonctionnalités plus détaillées à partir d'une fonction glibc.

Et enfin, un mot sur la sécurité. Les appels système s'interfacent directement avec le noyau. Le noyau Linux dispose de protections étendues contre les manigances des utilisateurs, mais des bogues non découverts existent. Ne croyez pas qu'un appel système validera votre entrée ou vous isolera des problèmes de sécurité. Il est sage de s'assurer que les données que vous transmettez à un appel système sont nettoyées. Naturellement, c'est un bon conseil pour tout appel d'API, mais vous ne pouvez pas être trop prudent lorsque vous travaillez avec le noyau.

J'espère que vous avez apprécié cette plongée plus profonde dans le pays des appels système Linux. Pour une liste complète des appels système Linux, consultez notre liste principale.

Comment développer un jeu sur Linux
Il y a dix ans, peu d'utilisateurs de Linux prédisaient que leur système d'exploitation préféré serait un jour une plate-forme de jeu populaire pour l...
Ports Open Source des moteurs de jeux commerciaux
Les récréations de moteur de jeu gratuites, open source et multiplateformes peuvent être utilisées pour jouer à d'anciens ainsi qu'à certains des titr...
Meilleurs jeux de ligne de commande pour Linux
La ligne de commande n'est pas seulement votre plus grand allié lorsque vous utilisez Linux, elle peut également être une source de divertissement car...