$ 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?
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'.
Pour développer le réponse acceptée , je voulais simplement fournir une autre raison pour laquelle real
user
+ 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
• 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.
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;
}
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;
}
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:
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;
}
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 disponiblestimes()
sinonqui 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:
wait3
non-POSIX s'il est disponibletimes
et gettimeofday
sinonReal 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.)