web-dev-qa-db-fra.com

Que signifient 'real', 'user' et 'sys' dans la sortie du temps (1)?

$ time foo
real        0m0.003s
user        0m0.000s
sys         0m0.004s
$

Que signifient "réel", "utilisateur" et "sys" dans la sortie du temps?

Lequel est significatif lors de l'analyse comparative de mon application?

1559
Iraimbilanja

Statistiques sur les temps réels des processus utilisateur, utilisateur et système

Une de ces choses n'est pas comme les autres. Réel fait référence au temps écoulé réel; User et Sys font référence au temps CPU utilisé uniquement par le processus.

  • Réel est l'heure de l'horloge murale - l'heure du début à la fin de l'appel. Il s'agit de tout le temps écoulé, y compris les tranches de temps utilisées par d'autres processus et le temps que le processus passe bloqué (par exemple, s'il attend la fin des E/S).

  • Utilisateur est la quantité de temps CPU passé en code en mode utilisateur (en dehors du noyau) dans le processus. Ceci est uniquement le temps CPU réel utilisé dans l'exécution du processus. Les autres processus et le temps que le processus passe bloqué ne comptent pas pour ce chiffre.

  • Sys est la quantité de temps CPU passé dans le noyau au sein du processus. Cela signifie que vous devez exécuter le temps CPU nécessaire aux appels système dans le noyau , par opposition au code de la bibliothèque, qui est toujours exécuté dans l'espace utilisateur. Comme "utilisateur", il ne s'agit que du temps CPU utilisé par le processus. Voir ci-dessous une brève description du mode noyau (également appelé mode "superviseur") et du mécanisme d’appel système.

User+Sys vous indiquera le temps CPU réellement utilisé par votre processus. Notez que cela concerne tous les processeurs. Par conséquent, si le processus comporte plusieurs threads (et que ce processus s'exécute sur un ordinateur doté de plusieurs processeurs), il peut potentiellement dépasser le temps d'horloge indiqué par Real (qui se produit généralement). . Notez que dans les résultats, ces chiffres incluent le User et le Sys temps de tous les processus enfants (et de leurs descendants), ainsi que du moment où ils auraient pu être collectés, par ex. par wait(2) ou waitpid(2), bien que les appels système sous-jacents renvoient les statistiques du processus et de ses enfants séparément.

Origine des statistiques déclarées par time (1)

Les statistiques rapportées par time proviennent de divers appels système. "Utilisateur" et "Sys" proviennent de wait (2) ( POSIX ) ou times (2) ( POSIX ) , en fonction du système particulier. "Réel" est calculé à partir des heures de début et de fin recueillies à partir de l'appel gettimeofday (2) . Selon la version du système, diverses autres statistiques, telles que le nombre de changements de contexte, peuvent également être collectées par time.

Sur un ordinateur multiprocesseur, un processus multithread ou un processus forking des enfants peut avoir un temps écoulé inférieur au temps CPU total - différents process ou processus pouvant s'exécuter en parallèle. De plus, les statistiques de temps rapportées proviennent d'origines différentes. Par conséquent, les temps enregistrés pour des tâches très courtes peuvent être sujets à des erreurs d'arrondi, comme le montre l'exemple de l'affiche originale.

Quelques mots sur le mode noyau et utilisateur

Sous Unix, ou sur n’importe quel système d’exploitation à mémoire protégée, 'Kernel' ou 'Supervisor' le mode == désigne un mode privilégié dans lequel la CPU peut fonctionner. Certaines actions privilégiées pouvant affecter la sécurité ou la stabilité ne peut être fait que lorsque la CPU fonctionne dans ce mode; ces actions ne sont pas disponibles pour le code de l'application. Un exemple d'une telle action pourrait être la manipulation de MMU pour accéder à l'espace d'adressage d'un autre processus. Normalement, le code en mode utilisateur ne peut pas le faire (avec une bonne raison), bien qu'il puisse demander mémoire partagée au noyau, ce qui pourrait être lu ou écrit par plus d'un processus. Dans ce cas, la mémoire partagée est explicitement demandée au noyau via un mécanisme sécurisé et les deux processus doivent s'y attacher explicitement pour pouvoir l'utiliser.

Le mode privilégié est généralement appelé mode "noyau" car le noyau est exécuté par la CPU exécutant ce mode. Pour passer en mode noyau, vous devez émettre une instruction spécifique (souvent appelée un piège ) ) qui fait passer le processeur en mode noyau et exécute le code à partir d'un emplacement spécifique dans une table de saut. Pour des raisons de sécurité, vous ne pouvez pas passer en mode noyau et exécuter du code arbitraire - les interruptions sont gérées via une table d'adresses qui ne peuvent pas être écrit à moins que la CPU ne fonctionne en mode superviseur. Vous traitez avec un numéro de trappe explicite et l'adresse est recherchée dans la table de saut; le noyau a un nombre fini de points d'entrée contrôlés.

Les appels "système" de la bibliothèque C (en particulier ceux décrits à la section 2 des pages de manuel) ont un composant en mode utilisateur, que vous appelez réellement à partir de votre programme C. En coulisse, ils peuvent émettre un ou plusieurs appels système au noyau pour effectuer des services spécifiques tels que les E/S, mais leur code est toujours exécuté en mode utilisateur. Il est également tout à fait possible d’émettre directement une interruption en mode noyau à partir de n’importe quel code d’espace utilisateur, bien que vous puissiez avoir besoin d’écrire un extrait du langage Assembly pour configurer correctement les registres de l’appel.

En savoir plus sur 'sys'

Votre code ne peut pas exécuter certaines tâches en mode utilisateur, telles que l'allocation de mémoire ou l'accès au matériel (disque dur, réseau, etc.). Ceux-ci sont sous la supervision du noyau, et lui seul peut les faire. Certaines opérations comme malloc oufread/fwrite invoqueront ces fonctions du noyau, qui compteront alors comme temps "sys". Malheureusement, ce n'est pas aussi simple que "chaque appel à malloc sera compté dans le temps du" système ". L'appel à malloc effectuera lui-même un traitement (toujours compté dans le temps 'utilisateur'), ​​puis quelque part au cours du processus, il pourra appeler la fonction dans le noyau (compté dans le temps 'sys'). Après être revenu de l'appel du noyau, il y aura un peu plus de temps dans 'utilisateur', puis malloc reviendra dans votre code. Pour ce qui est du moment où le commutateur se produit et de la quantité dépensée en mode noyau ... vous ne pouvez pas le dire. Cela dépend de la mise en œuvre de la bibliothèque. En outre, d'autres fonctions apparemment innocentes pourraient également utiliser malloc et autres en arrière-plan, ce qui laissera encore du temps dans 'sys'.

1856

Pour développer le réponse acceptée , je voulais simplement fournir une autre raison pour laquelle realuser + sys.

N'oubliez pas que real représente le temps écoulé réel, tandis que les valeurs user et sys représentent le temps d'exécution de la CPU. En conséquence, sur un système multicœur, le temps user et/ou sys (ainsi que leur somme) peut en réalité dépasser le temps réel. Par exemple, sur une application Java exécutée pour la classe, j'obtiens cet ensemble de valeurs:

real    1m47.363s
user    2m41.318s
sys     0m4.013s
251
lensovet

réel: durée réelle d'exécution du processus, comme si elle était mesurée par un humain doté d'un chronomètre

tilisateur: le temps cumulé passé par tous les processeurs pendant le calcul

sys: temps cumulé passé par toutes les CPU lors de tâches liées au système, telles que l'allocation de mémoire.

Notez que, parfois, utilisateur + système peut être plus grand que réel, car plusieurs processeurs peuvent fonctionner en parallèle.

26
varun

Exemples POSIX C exécutables minimaux

Pour rendre les choses plus concrètes, je souhaite illustrer quelques cas extrêmes de time avec quelques programmes de test C minimaux.

Tous les programmes peuvent être compilés et exécutés avec:

gcc -ggdb3 -o main.out -pthread -std=c99 -pedantic-errors -Wall -Wextra main.c
time ./main.out

et ont été testés sous Ubuntu 18.10, GCC 8.2.0, glibc 2.28, noyau Linux 4.18, ordinateur portable ThinkPad P51, processeur Intel Core i7-7820HQ (4 cœurs/8 threads), 2x Samsung M471A2K43BB1-CRC RAM (2x 16GiB).

dormir

Le sommeil non occupé ne compte ni dans user ni sys, uniquement real.

Par exemple, un programme qui dort une seconde:

#define _XOPEN_SOURCE 700
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    sleep(1);
    return EXIT_SUCCESS;
}

GitHub en amont .

sort quelque chose comme:

real    0m1.003s
user    0m0.001s
sys     0m0.003s

Il en va de même pour les programmes bloqués le IO devenant disponible.

Par exemple, le programme suivant attend que l'utilisateur entre un caractère et appuie sur Entrée:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    printf("%c\n", getchar());
    return EXIT_SUCCESS;
}

GitHub en amont .

Et si vous attendez environ une seconde, le résultat obtenu est identique à celui de l'exemple de sommeil, par exemple:

real    0m1.003s
user    0m0.001s
sys     0m0.003s

Pour cette raison, time peut vous aider à faire la distinction entre les programmes liés CPU et IO: Que signifient les termes "CPU lié" et "I/O lié"?

Plusieurs threads

L'exemple suivant utilise niters itérations de travaux processeur lourds inutiles sur les threads nthreads:

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <inttypes.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

uint64_t niters;

void* my_thread(void *arg) {
    uint64_t *argument, i, result;
    argument = (uint64_t *)arg;
    result = *argument;
    for (i = 0; i < niters; ++i) {
        result = (result * result) - (3 * result) + 1;
    }
    *argument = result;
    return NULL;
}

int main(int argc, char **argv) {
    size_t nthreads;
    pthread_t *threads;
    uint64_t rc, i, *thread_args;

    /* CLI args. */
    if (argc > 1) {
        niters = strtoll(argv[1], NULL, 0);
    } else {
        niters = 1000000000;
    }
    if (argc > 2) {
        nthreads = strtoll(argv[2], NULL, 0);
    } else {
        nthreads = 1;
    }
    threads = malloc(nthreads * sizeof(*threads));
    thread_args = malloc(nthreads * sizeof(*thread_args));

    /* Create all threads */
    for (i = 0; i < nthreads; ++i) {
        thread_args[i] = i;
        rc = pthread_create(
            &threads[i],
            NULL,
            my_thread,
            (void*)&thread_args[i]
        );
        assert(rc == 0);
    }

    /* Wait for all threads to complete */
    for (i = 0; i < nthreads; ++i) {
        rc = pthread_join(threads[i], NULL);
        assert(rc == 0);
        printf("%" PRIu64 " %" PRIu64 "\n", i, thread_args[i]);
    }

    free(threads);
    free(thread_args);
    return EXIT_SUCCESS;
}

GitHub en amont + code de tracé .

Ensuite, nous traçons mur, utilisateur et sys en fonction du nombre de threads pour un nombre fixe de 10 ^ 10 itérations sur mes 8 processeurs hyperthread:

enter image description here

données de tracé .

Le graphique montre que:

  • pour une application monocœur intensive en ressources CPU, mur et utilisateur sont à peu près les mêmes

  • pour 2 noyaux, l'utilisateur est environ 2x mur, ce qui signifie que le temps utilisateur est compté sur tous les threads.

    utilisateur a essentiellement doublé, et tandis que le mur est resté le même.

  • cela continue jusqu'à 8 threads, ce qui correspond au nombre d'hyperthreads de mon ordinateur.

    Après 8h, le mur commence aussi à augmenter, car nous n'avons plus de CPU pour faire plus de travail dans un laps de temps donné!

    Le rapport des plateaux à ce stade.

Sys travail lourd avec sendfile

La charge de travail système la plus lourde à laquelle je pouvais parvenir consistait à utiliser sendfile, qui effectue une opération de copie de fichier sur l’espace noyau: Copier un fichier de manière saine, sûre et efficace

J'ai donc imaginé que cette memcpy dans le noyau serait une opération gourmande en ressources processeur.

D'abord, j'initialise un fichier aléatoire volumineux de 10 Go avec:

dd if=/dev/urandom of=sendfile.in.tmp bs=1K count=10M

Puis lancez le code:

#define _GNU_SOURCE
#include <assert.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char **argv) {
    char *source_path, *dest_path;
    int source, dest;
    struct stat stat_source;
    if (argc > 1) {
        source_path = argv[1];
    } else {
        source_path = "sendfile.in.tmp";
    }
    if (argc > 2) {
        dest_path = argv[2];
    } else {
        dest_path = "sendfile.out.tmp";
    }
    source = open(source_path, O_RDONLY);
    assert(source != -1);
    dest = open(dest_path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
    assert(dest != -1);
    assert(fstat(source, &stat_source) != -1);
    assert(sendfile(dest, source, 0, stat_source.st_size) != -1);
    assert(close(source) != -1);
    assert(close(dest) != -1);
    return EXIT_SUCCESS;
}

GitHub en amont .

ce qui donne essentiellement le temps système comme prévu:

real    0m2.175s
user    0m0.001s
sys     0m1.476s

J'étais aussi curieux de voir si time pourrait faire la distinction entre les appels système de différents processus, alors j'ai essayé:

time ./sendfile.out sendfile.in1.tmp sendfile.out1.tmp &
time ./sendfile.out sendfile.in2.tmp sendfile.out2.tmp &

Et le résultat fut:

real    0m3.651s
user    0m0.000s
sys     0m1.516s

real    0m4.948s
user    0m0.000s
sys     0m1.562s

Le temps système est à peu près le même pour les deux processus que pour un processus unique, mais le temps de traitement sur un mur est plus long, car les processus sont probablement en concurrence pour l'accès en lecture sur disque.

Il semble donc que cela explique en fait quel processus a démarré un travail de noyau donné.

Code source Bash

Quand vous ne faites que time <cmd> sur Ubuntu, il utilise le mot-clé Bash, comme on peut le voir dans:

type time

qui produit:

time is a Shell keyword

Nous avons donc grep source dans le code source de Bash 4.19 pour la chaîne de sortie:

git grep '"user\b'

ce qui nous amène à execute_cmd.c function time_command, qui utilise:

  • gettimeofday() et getrusage() si les deux sont disponibles
  • times() sinon

qui sont tous appels système Linux et fonctions POSIX .

Code source GNU Coreutils

Si nous l'appelons comme:

/usr/bin/time

il utilise ensuite la mise en oeuvre GNU Coreutils.

Celui-ci est un peu plus complexe, mais la source pertinente semble être resuse.c et elle le fait:

  • un appel BSD wait3 non-POSIX s'il est disponible
  • times et gettimeofday sinon

Real indique le temps de traitement total d'un processus; pendant que User indique le temps d'exécution pour les instructions définies par l'utilisateur et que Sys indique l'heure d'exécution des appels système!

Le temps réel inclut également le temps d'attente (le temps d'attente pour les E/S, etc.)

14
susenj