Примеры минимального запуска POSIX C
Чтобы сделать вещи более конкретными, я хочу привести в качестве примера несколько крайних случаев time
с некоторыми минимальными тестовыми программами C.
Все программы можно скомпилировать и запустить с помощью:
gcc -ggdb3 -o main.out -pthread -std=c99 -pedantic-errors -Wall -Wextra main.c
time ./main.out
и были протестированы в Ubuntu 18.10, GCC 8.2.0, glibc 2.28, ядре Linux 4.18, ноутбуке ThinkPad P51, процессоре Intel Core i7-7820HQ (4 ядра / 8 потоков), 2x оперативной памяти Samsung M471A2K43BB1-CRC (2x 16 ГБ) ).
сон
Не занятой сон не учитывается ни в user
, ни в sys
, только real
.
Например, программа, которая спит секунду:
#define _XOPEN_SOURCE 700
#include <stdlib.h>
#include <unistd.h>
int main(void) {
sleep(1);
return EXIT_SUCCESS;
}
GitHub upstream .
выводит что-то вроде:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
То же самое относится и к программам, заблокированным при вводе-выводе.
Например, следующая программа ждет, пока пользователь введет символ, и нажмите клавишу ввода:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
printf("%c\n", getchar());
return EXIT_SUCCESS;
}
GitHub upstream .
И если вы хотите на одну секунду, выводит, как в примере сна, что-то вроде:
real 0m1.003s
user 0m0.001s
sys 0m0.003s
Несколько нитей
В следующем примере выполняется niters
итераций бесполезной тяжелой работы процессора в потоках 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 upstream + код сюжета .
Затем мы отображаем wall, user и sys как функцию количества потоков для фиксированных 10 ^ 10 итераций на моем 8-поточном процессоре с гиперпотоками:
График данных .
Из графика видно, что:
для одноядерных приложений, интенсивно использующих процессор, настенные и пользовательские примерно одинаковы
для 2 ядер, пользователь примерно в 2 раза больше стены, что означает, что время пользователя учитывается во всех потоках.
пользователь в основном удвоился, а стена осталась прежней.
это продолжается до 8 потоков, что соответствует моему числу гиперпотоков в моем компьютере.
После 8 стена также начинает увеличиваться, потому что у нас нет лишних процессоров, чтобы выполнять больше работы за данный промежуток времени!
Соотношение плато на данный момент.
Sys тяжелая работа с sendfile
Самой тяжелой рабочей нагрузкой sys, которую я мог придумать, было использование sendfile
, который выполняет операцию копирования файла в пространстве ядра: Копирование файла разумным, безопасным и эффективным способом
Итак, я представлял, что этот memcpy
в ядре будет загружать процессор.
Сначала я инициализирую большой случайный файл 10 ГБ:
dd if=/dev/urandom of=sendfile.in.tmp bs=1K count=10M
Затем запустите код:
#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 upstream .
, что дает в основном системное время, как и ожидалось:
real 0m2.175s
user 0m0.001s
sys 0m1.476s
Мне также было любопытно посмотреть, различит ли time
системные вызовы разных процессов, поэтому я попытался:
time ./sendfile.out sendfile.in1.tmp sendfile.out1.tmp &
time ./sendfile.out sendfile.in2.tmp sendfile.out2.tmp &
И результат был:
real 0m3.651s
user 0m0.000s
sys 0m1.516s
real 0m4.948s
user 0m0.000s
sys 0m1.562s
Время sys примерно одинаково для обоих процессов, но для одного времени больше, потому что процессы конкурируют за доступ к чтению с диска.
Похоже, что на самом деле он учитывает, какой процесс запустил данную работу ядра.
Исходный код Bash
Когда вы просто используете time <cmd>
в Ubuntu, он использует ключевое слово Bash, как видно из:
type time
который выводит:
time is a shell keyword
Итак, мы ищем исходный код в исходном коде Bash 4.19 для строки вывода:
git grep '"user\b'
, что приводит нас к execute_cmd.c функции time_command
, которая использует:
gettimeofday()
и getrusage()
, если доступны оба
times()
в противном случае
все это системные вызовы Linux и функции POSIX .
Исходный код GNU Coreutils
Если мы назовем это как:
/usr/bin/time
тогда он использует реализацию GNU Coreutils.
Это немного сложнее, но соответствующий источник, похоже, находится по адресу resuse.c , и он делает:
- вызов не POSIX BSD
wait3
, если он доступен
times
и gettimeofday
в противном случае