Ejemplos mínimos de POSIX C ejecutables
Para hacer las cosas más concretas, quiero ejemplificar algunos casos extremos time
con algunos programas mínimos de prueba C.
Todos los programas se pueden compilar y ejecutar con:
gcc -ggdb3 -o main.out -pthread -std=c99 -pedantic-errors -Wall -Wextra main.c
time ./main.out
y han sido probados en Ubuntu 18.10, GCC 8.2.0, glibc 2.28, Linux kernel 4.18, laptop ThinkPad P51, CPU Intel Core i7-7820HQ (4 núcleos / 8 hilos), 2x Samsung M471A2K43BB1-CRC RAM (2x 16GiB).
dormir
El sueño no ocupado no cuenta en ninguno user
o sys
solo real
.
Por ejemplo, un programa que duerme un segundo:
#define _XOPEN_SOURCE 700
#include <stdlib.h>
#include <unistd.h>
int main(void) {
sleep(1);
return EXIT_SUCCESS;
}
GitHub aguas arriba .
produce algo como:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
Lo mismo vale para los programas bloqueados cuando IO está disponible.
Por ejemplo, el siguiente programa espera a que el usuario ingrese un carácter y presione enter:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
printf("%c\n", getchar());
return EXIT_SUCCESS;
}
GitHub aguas arriba .
Y si espera aproximadamente un segundo, se genera como en el ejemplo de reposo algo así como:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
Por esta razón, time
puede ayudarlo a distinguir entre los programas vinculados a la CPU y a las E / S: ¿Qué significan los términos "CPU vinculados" y "E / S atados"?
Hilos múltiples
El siguiente ejemplo realiza niters
iteraciones de trabajo inútil puramente vinculado a la CPU en nthreads
subprocesos:
#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 upstream + código de trama .
Luego graficamos wall, user y sys en función del número de subprocesos para un 10 ^ 10 iteraciones fijas en mi 8 CPU hyperthread:
Trazar datos .
Del gráfico, vemos que:
para una aplicación de núcleo único intensivo de CPU, el muro y el usuario son casi lo mismo
para 2 núcleos, el usuario mide aproximadamente 2 veces la pared, lo que significa que el tiempo del usuario se cuenta en todos los subprocesos.
El usuario básicamente se duplicó, y mientras que la pared se mantuvo igual.
esto continúa hasta 8 subprocesos, lo que coincide con mi número de hyperthreads en mi computadora.
Después de 8, el muro comienza a aumentar también, ¡porque no tenemos CPU adicionales para poner más trabajo en un período de tiempo determinado!
La relación se estabiliza en este punto.
Tenga en cuenta que este gráfico es tan claro y simple porque el trabajo está puramente vinculado a la CPU: si estuviera vinculado a la memoria, obtendríamos una caída en el rendimiento mucho antes con menos núcleos porque los accesos a la memoria serían un cuello de botella como se muestra en What Qué significan los términos "enlazado a la CPU" y "enlazado de E / S"?
Sys trabajo pesado con sendfile
La carga de trabajo de sys más pesada que se me ocurrió fue usar el sendfile
, que realiza una operación de copia de archivos en el espacio del kernel: copie un archivo de una manera sana, segura y eficiente
Así que imaginé que este en el núcleo memcpy
será una operación intensiva de la CPU.
Primero inicializo un gran archivo aleatorio de 10GiB con:
dd if=/dev/urandom of=sendfile.in.tmp bs=1K count=10M
Luego ejecuta el código:
#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 aguas arriba .
que proporciona básicamente el tiempo del sistema como se esperaba:
real 0m2.175s
user 0m0.001s
sys 0m1.476s
También tenía curiosidad por ver si time
distinguiría entre syscalls de diferentes procesos, así que intenté:
time ./sendfile.out sendfile.in1.tmp sendfile.out1.tmp &
time ./sendfile.out sendfile.in2.tmp sendfile.out2.tmp &
Y el resultado fue:
real 0m3.651s
user 0m0.000s
sys 0m1.516s
real 0m4.948s
user 0m0.000s
sys 0m1.562s
El tiempo del sistema es casi el mismo para ambos que para un solo proceso, pero el tiempo del muro es mayor porque los procesos compiten por un acceso de lectura de disco probable.
Por lo tanto, parece que de hecho explica qué proceso inició un trabajo de núcleo dado.
Código fuente de Bash
Cuando lo hace solo time <cmd>
en Ubuntu, usa la palabra clave Bash como se puede ver en:
type time
que salidas:
time is a shell keyword
Entonces grep fuente en el código fuente Bash 4.19 para la cadena de salida:
git grep '"user\b'
lo que nos lleva a la función execute_cmd.ctime_command
, que usa:
gettimeofday()
y getrusage()
si ambos están disponibles
times()
de otra manera
todos los cuales son llamadas al sistema Linux y funciones POSIX .
Código fuente de GNU Coreutils
Si lo llamamos como:
/usr/bin/time
entonces usa la implementación GNU Coreutils.
Este es un poco más complejo, pero la fuente relevante parece estar en resuse.c y lo hace:
- una
wait3
llamada BSD no POSIX si está disponible
times
y de lo gettimeofday
contrario