Mesurons le surcoût d’un appel système

Publié par cpb
Mar 19 2012

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…

 

 

 

 

 

3 Réponses

  1. Jabbari Oussama dit :

    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

    • cpb dit :

      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.

  2. gch dit :

    Attention avec gettimeofday, sur une configuration x86 suffisamment récente, c’est normalement implémenté sans appel système.

URL de trackback pour cette page