Une application classique a souvent besoin de réaliser des appels système pour accéder à des ressources matérielles ou logicielles contrôlées par le noyau. Que l’on veuille écrire dans un fichier, communiquer sur une socket, accéder à un port d’entrée-sortie, obtenir un supplément de mémoire ou même simplement lire notre numéro de processus (pid), il faudra réaliser un appel système. Mais combien de temps durera-t-il ?
Typiquement un appel système se matérialise par l’invocation explicite d’une interruption logicielle (une trappe) c’est à dire l’exécution d’une instruction assembleur spécifique qui va interrompre le travail du processeur et effectuer un appel vers une routine spécifique (un handler) se trouvant dans la mémoire du noyau. Cette routine est également exécutée avec les privilèges du noyau, ce qui lui permet de réaliser les opérations demandées même si elles impliquent des actions directes sur le matériel.
Sur une architecture PC, c’est classiquement l’interruption 0x80
qui est dédiée à ce travail, le numéro de l’appel système étant codé dans un registre du processeur (EAX
) avant d’invoquer l’instruction assembleur INTR
qui déclenchera le transfert dans l’espace kernel. Sur les systèmes récents, les instructions assembleurs SYSENTER
et SYSEXIT
permettent d’alléger ce mécanisme en simplifiant l’entrée dans le noyau.
Ce qui m’intéresse dans cet article est de savoir quel est le surcoût impliqué par un appel système, indépendamment du travail à réaliser effectivement dans le noyau. Ceci peut permettre de juger des traitements à effectuer dans un driver et de ceux que l’on préférera conserver dans l’espace utilisateur.
Programmes de test
Pour faire cette mesure, il y a une solution simple : invoquons la même routine du kernel dans un programme de l’espace utilisatteur (via un appel système) et dans un module du noyau, puis comparons le temps d’exécution. Une fonction est particulièrement adaptée : do_gettimeofday()
qui est invoquée par l’appel système gettimeofday()
implémenté dans le fichier kernel/time.c
des sources de Linux. Observons la construction de cet appel système. Voici un extrait (provenant des sources de Linux 3.0.0) que je vous conseille d’étudier à l’aide de l’excellent site LXR.
SYSCALL_DEFINE2(gettimeofday, struct timeval __user *, tv, struct timezone __user *, tz) { if (likely(tv != NULL)) { struct timeval ktv; do_gettimeofday(&ktv); if (copy_to_user(tv, &ktv, sizeof(ktv))) return -EFAULT; } if (unlikely(tz != NULL)) { if (copy_to_user(tz, &sys_tz, sizeof(sys_tz))) return -EFAULT; } return 0; }
La macro SYSCALL_DEFINE2()
permet de définir un appel système nommé gettimeofday
, possédant deux arguments. Le premier, nommé tv
, est un pointeur sur une structure timeval
se trouvant dans l’espace utilisateur, le second est un pointeur, nommé tz
, sur une structure timezone
également dans l’espace utilisateur.
Si tv
est non nul l’appel système invoque la routine do_gettimeofday()
pour renseigner une structure timeval
se trouvant dans l’espace kernel. Puis la structure est copiée dans l’espace utilisateur à l’emplacement pointé par tv
.
Le fait que tv
soit non nul est probable (likely) car c’est une utilisation fréquente de cet appel système. En revanche il est peu probable (unlikely) que le pointeur tz
soit non nul, car il est rare qu’une application demande ainsi des informations sur le fuseau horaire dans lequel se trouve le système. Les directives likely
et unlikely
permettent d’aider le compilateur à optimiser le code en se basant sur les comportements usuels des applications, qu’il n’aurait pas pu deviner en analysant simplement le code noyau.
Nous allons donc écrire un petit module qui invoque do_gettimeofday()
en boucle, et affiche les résultats ensuite. Puis nous réaliserons le même travail par l’intermédiaire d’un appel système depuis l’espace utilisteur. Voici notre module.
mesure-gettimeofday-kernel.c #include <linux/version.h> #include <linux/module.h> #include <linux/device.h> #include <linux/time.h> #define NB_MESURES 50 static struct timeval mesures[NB_MESURES]; static int __init init_gettimeofday (void) { int i; for (i = 0; i < NB_MESURES; i++) do_gettimeofday(&(mesures[i])); for (i = 0; i < NB_MESURES; i++) { printk(KERN_INFO "%ld.%06ldn", mesures[i].tv_sec, mesures[i].tv_usec); } return 0; } static void __exit exit_gettimeofday (void) { } module_init(init_gettimeofday); module_exit(exit_gettimeofday); MODULE_LICENSE("GPL");
Ce module appelle donc cinquante fois do_gettimeofday()
lors de son chargement puis nous affiche les résultats dans les traces du kernel. Chargeons-le et observons les résultats.
# make modules make -C /lib/modules/3.0.0-16-generic/build SUBDIRS=/home/cpb/Documents/Livres/Articles/Blog/article-2012-03-19 modules make[1]: entrant dans le répertoire « /usr/src/linux-headers-3.0.0-16-generic » CC [M] /home/cpb/Documents/Livres/Articles/Blog/article-2012-03-19/mesure-gettimeofday-kernel.o Building modules, stage 2. MODPOST 1 modules CC /home/cpb/Documents/Livres/Articles/Blog/article-2012-03-19/mesure-gettimeofday-kernel.mod.o LD [M] /home/cpb/Documents/Livres/Articles/Blog/article-2012-03-19/mesure-gettimeofday-kernel.ko make[1]: quittant le répertoire « /usr/src/linux-headers-3.0.0-16-generic » # insmod ./mesure-gettimeofday-kernel.ko # rmmod mesure_gettimeofday_kernel # dmesg | tail -50 [633997.019941] 1332085840.235456 [633997.019945] 1332085840.235456 [633997.019947] 1332085840.235456 [633997.019950] 1332085840.235456 [633997.019952] 1332085840.235456 [633997.019954] 1332085840.235456 [633997.019957] 1332085840.235456 [633997.019959] 1332085840.235457 [633997.019962] 1332085840.235457 [633997.019964] 1332085840.235457 [633997.019966] 1332085840.235457 [633997.019969] 1332085840.235457 [633997.019971] 1332085840.235457 [633997.019974] 1332085840.235457 [633997.019976] 1332085840.235457 [633997.019978] 1332085840.235457 [633997.019981] 1332085840.235457 [633997.019983] 1332085840.235457 [633997.019985] 1332085840.235458 [633997.019988] 1332085840.235458 [633997.019990] 1332085840.235458 [633997.019992] 1332085840.235458 [633997.019995] 1332085840.235458 [633997.019998] 1332085840.235458 [633997.020056] 1332085840.235458 [633997.020067] 1332085840.235458 [633997.020076] 1332085840.235458 [633997.020085] 1332085840.235458 [633997.020094] 1332085840.235459 [633997.020103] 1332085840.235459 [633997.020111] 1332085840.235459 [633997.020120] 1332085840.235459 [633997.020129] 1332085840.235459 [633997.020137] 1332085840.235459 [633997.020144] 1332085840.235459 [633997.020153] 1332085840.235459 [633997.020162] 1332085840.235459 [633997.020171] 1332085840.235459 [633997.020178] 1332085840.235459 [633997.020186] 1332085840.235460 [633997.020194] 1332085840.235460 [633997.020202] 1332085840.235460 [633997.020210] 1332085840.235460 [633997.020218] 1332085840.235460 [633997.020226] 1332085840.235460 [633997.020234] 1332085840.235460 [633997.020243] 1332085840.235460 [633997.020251] 1332085840.235460 [633997.020259] 1332085840.235460 [633997.020267] 1332085840.235460 #
Les lignes de résultats se décomposent en une première valeur entre crochets qui correspond au timestamp (horodatage) au moment du printk()
, ce qui ne nous intéresse pas, suivi de la valeur obtenue avec do_gettimeofday()
exprimée en secondes (depuis le 01/01/1970), les décimales étant des microsecondes. Nous voyons que les valeurs sont régulières et continues, il y a dix à onze appels à do_gettimeofday()
par microseconde. L’exécution des cinquantes mesures a duré environ cinq microsecondes.
Voici à présent le programme qui effectuera un travail similaire depuis l’espace utilisateur
mesure-gettimeofday-user.c #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/time.h> #define NB_MESURES 50 int main(void) { int i; struct sched_param param; struct timeval mesures[NB_MESURES]; param.sched_priority = 30; if (sched_setscheduler(0, SCHED_FIFO, & param) != 0) { fprintf(stderr, "Pour des resultats plus precis, executez ce programme sous identite 'root'n"); sleep(6); } for (i = 0; i < NB_MESURES; i++) gettimeofday(&(mesures[i]), NULL); for (i = 0; i < NB_MESURES; i++) { fprintf(stdout, "%ld.%06ldn", mesures[i].tv_sec, mesures[i].tv_usec); } return 0; }
On notera que le programme essaye si possible de passer en ordonnancement temps réel pour éviter les incertitudes liées aux préemptions par d’autres tâches. Exécutons-le.
# make cc mesure-gettimeofday-user.c -o mesure-gettimeofday-user # ./mesure-gettimeofday-user 1332086462.451367 1332086462.451368 1332086462.451369 1332086462.451369 1332086462.451370 1332086462.451370 1332086462.451371 1332086462.451371 1332086462.451372 1332086462.451372 1332086462.451373 1332086462.451374 1332086462.451374 1332086462.451375 1332086462.451375 1332086462.451376 1332086462.451376 1332086462.451377 1332086462.451377 1332086462.451378 1332086462.451378 1332086462.451379 1332086462.451380 1332086462.451380 1332086462.451381 1332086462.451381 1332086462.451382 1332086462.451382 1332086462.451383 1332086462.451383 1332086462.451384 1332086462.451384 1332086462.451385 1332086462.451385 1332086462.451386 1332086462.451387 1332086462.451387 1332086462.451388 1332086462.451388 1332086462.451389 1332086462.451389 1332086462.451390 1332086462.451390 1332086462.451391 1332086462.451391 1332086462.451392 1332086462.451393 1332086462.451393 1332086462.451394 1332086462.451394 #
Cette fois, il n’y a qu’une à deux invocations de gettimeofday()
par microseconde. Au total, l’exécution des cinquante appels a pris 28 microsecondes.
Résultats
Nous avons réalisé cinquante invocations de do_gettimeofday()
depuis l’espace kernel en cinq microsecondes environ. Depuis l’espace utilisateur, les mêmes invocations ont duré vingt-huit microsecondes. La différence est de 23 microsecondes pour cinquante itérations, ce qui représente 460 nanosecondes par appel système.
Sur cette machine, dans cette configuration, le surcoût d’un appel système (changement de contexte, modification des privilèges d’exécution, passage par l’ordonnanceur en revenant dans l’espace utilisateur…) par rapport au travail effectivement réalisé est de 460 nanosecondes. Quel est-il sur votre environnement ? Vous pouvez le mesurer facilement avec les exemples ci-dessus, regroupés dans cette archive.
Commentaires, remarques, compléments sont les bienvenus…
Bonjour et merci pour vos excellents articles,
voici mes résultats, je ne note pas de différences notables entre kernel et user et contrairement à vous les mesures dans l’espace kernel ne sont pas toujours régulières, peut-être est ce parce que je n’ai pas un noyau avec le patch RT.
> uname -rsm
Linux 2.6.38-13-generic i686
> cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 15
model name : Intel(R) Core(TM)2 Duo CPU T5450 @ 1.66GHz
stepping : 13
cpu MHz : 1000.000
cache size : 2048 KB
…
cpu cores : 2
…
bogomips : 3333.47
…
processor : 1
vendor_id : GenuineIntel
cpu family : 6
model : 15
model name : Intel(R) Core(TM)2 Duo CPU T5450 @ 1.66GHz
stepping : 13
cpu MHz : 1000.000
cache size : 2048 KB
…
cpu cores : 2
…
bogomips : 3333.58
…
> cat /proc/meminfo | head
MemTotal: 2059248 kB
MemFree: 882020 kB
Buffers: 115972 kB
Cached: 547780 kB
SwapCached: 0 kB
Active: 578444 kB
Inactive: 496480 kB
Active(anon): 411988 kB
Inactive(anon): 2612 kB
Active(file): 166456 kB
* résultat kernel :
[ 1967.119255] 1332193329.005053
[ 1967.119259] 1332193329.005058
[ 1967.119261] 1332193329.005063
[ 1967.119264] 1332193329.005068
[ 1967.119266] 1332193329.005073
[ 1967.119268] 1332193329.005079
[ 1967.119271] 1332193329.005084
[ 1967.119273] 1332193329.005089
[ 1967.119275] 1332193329.005094
[ 1967.119277] 1332193329.005099
[ 1967.119279] 1332193329.005104
[ 1967.119282] 1332193329.005110
[ 1967.119284] 1332193329.005115
[ 1967.119286] 1332193329.005120
[ 1967.119289] 1332193329.005126
[ 1967.119291] 1332193329.005131
[ 1967.119293] 1332193329.005136
[ 1967.119295] 1332193329.005137
[ 1967.119298] 1332193329.005139
[ 1967.119300] 1332193329.005140
[ 1967.119302] 1332193329.005145
[ 1967.119305] 1332193329.005148
[ 1967.119307] 1332193329.005154
[ 1967.119309] 1332193329.005155
[ 1967.119311] 1332193329.005156
[ 1967.119314] 1332193329.005157
[ 1967.119316] 1332193329.005158
[ 1967.119318] 1332193329.005159
[ 1967.119321] 1332193329.005161
[ 1967.119323] 1332193329.005162
[ 1967.119325] 1332193329.005163
[ 1967.119327] 1332193329.005164
[ 1967.119330] 1332193329.005165
[ 1967.119332] 1332193329.005166
[ 1967.119334] 1332193329.005167
[ 1967.119337] 1332193329.005169
[ 1967.119339] 1332193329.005170
[ 1967.119341] 1332193329.005171
[ 1967.119343] 1332193329.005172
[ 1967.119346] 1332193329.005173
[ 1967.119348] 1332193329.005174
[ 1967.119350] 1332193329.005178
[ 1967.119353] 1332193329.005184
[ 1967.119355] 1332193329.005189
[ 1967.119357] 1332193329.005190
[ 1967.119359] 1332193329.005191
[ 1967.119362] 1332193329.005192
[ 1967.119364] 1332193329.005200
[ 1967.119366] 1332193329.005201
[ 1967.119369] 1332193329.005206
* user space :
1332193419.503243
1332193419.503244
1332193419.503246
1332193419.503248
1332193419.503249
1332193419.503251
1332193419.503252
1332193419.503254
1332193419.503255
1332193419.503257
1332193419.503258
1332193419.503260
1332193419.503261
1332193419.503263
1332193419.503264
1332193419.503266
1332193419.503267
1332193419.503269
1332193419.503271
1332193419.503272
1332193419.503274
1332193419.503275
1332193419.503277
1332193419.503279
1332193419.503280
1332193419.503282
1332193419.503283
1332193419.503285
1332193419.503286
1332193419.503288
1332193419.503290
1332193419.503291
1332193419.503293
1332193419.503294
1332193419.503296
1332193419.503305
1332193419.503307
1332193419.503308
1332193419.503313
1332193419.503315
1332193419.503316
1332193419.503318
1332193419.503320
1332193419.503321
1332193419.503323
1332193419.503324
1332193419.503326
1332193419.503328
1332193419.503329
1332193419.503339
Je trouve que les résultats dans l’espace noyau sont particulièrement fluctuants. Le système est chargé ? des entrées-sorties (réseau par exemple) importantes ?
Visiblement les valeurs s’améliorent à partir de
[ 1967.119321] 1332193329.005161
même s’il y a encore des variations ensuite.
Je pense qu’il faudrait essayer de ré-exécuter le code à plusieurs reprises, jusqu’à obtenir des valeurs plus régulières. Cela nous permettrait de comparer les données avec les résultats de l’espace utilisateur.
Attention avec gettimeofday, sur une configuration x86 suffisamment récente, c’est normalement implémenté sans appel système.