Интересны не только затраты на порождение нового процесса (мы еще будем к ним неоднократно возвращаться), но и то, насколько «эффективно» сосуществуют параллельные процессы в ОС, насколько быстро происходит переключение контекста с одного процесса на другой. Для самой грубой оценки этих затрат создадим простейшее приложение (файл p5.cc):
Затраты на взаимное переключение процессов#include <stdlib.h>
#include <inttypes.h>
#include <iostream.h>
#include <unistd.h>
#include <sched.h>
#include <sys/neutrino.h>
int main(int argc, char* argv[]) {
unsigned long N = 1000;
if (argc > 1 && atoi(argv[1]) > 0)
N = atoi(argv[1]);
pid_t pid = fork();
if (pid == -1)
cout << "fork error" << endl, exit(EXIT_FAILURE);
uint64_t t = ClockCycles();
for (unsigned long i = 0; i < N; i++) sched_yield();
t = ClockCycles() - t;
delay(200);
cout << pid << "\t: cycles - " << t << "; on sched - " << (t/N) / 2 << endl;
exit(EXIT_SUCCESS);
}
Два одновременно выполняющихся процесса настолько симметричны и идентичны, что они даже не анализируют PID после выполнения fork(), они только в максимальном темпе «перепасовывают» друг другу активность, как волейболисты делают это с мячом (рис. 2.2).
Рис. 2.2. Симметричное взаимодействие потоков
Рисунок 2.2 иллюстрирует взаимодействие двух идентичных процессов: вся их «работа» состоит лишь в том, чтобы как можно быстрее передать управление партнеру. Такую схему, когда два и более как можно более идентичных потоков или процессов в максимально высоком темпе (на порядок превосходящем последовательность «естественной» RR-диспетчеризации) обмениваются активностью, мы будем неоднократно использовать в дальнейшем для различных механизмов, называя ее для простоты «симметричной схемой».
ПримечаниеЧтобы максимально упростить код приложения, при его написании мы не трогали события «естественной» диспетчеризации, имеющие место при RR-диспетчеризации каждые 4 системных тика (по умолчанию это ~4 миллисекунды). Как сейчас покажут результаты, события принудительной диспетчеризации происходят с периодичностью порядка 1 микросекунды, т.e. в 4000 раз чаще, и возмущения, возможно вносимые RR-диспетчеризацией, можно считать не настолько существенными.
Вот результаты выполнения этой программы:
# nice -n-19 p5 1000000
1069102 : cycles - 1234175656; on sched — 617
0 : cycles - 1234176052; on sched - 617
# nice -n-19 p5 100000
1003566 : cycles - 123439225; on sched — 617
0 : cycles - 123440347; on sched - 617
# nice -n-19 p5 10000
1019950 : cycles - 12339084; on sched — 616
0 : cycles - 12341520; on sched - 617
# nice -n-19 p5 1000
1036334 : cycles - 1243117; on sched — 621
0 : cycles - 1245123; on sched - 622
# nice -n-19 p5 100
1052718 : cycles - 130740; on sched — 653
0 : cycles - 132615; on sched - 663
Видна на удивление устойчивая оценка, практически не зависящая от общего числа актов диспетчеризации, изменяющегося на 4 порядка.
Отбросив мелкие добавки, привносимые инкрементом и проверкой счетчика цикла, можно считать, что передача управления от процесса к процессу требует порядка 600 циклов процессора (это порядка 1,2 микросекунды на компьютере 533 МГц, на котором выполнялся этот тест).
Последующие расширения[14] POSIX специфицируют широкий спектр механизмов «легких процессов» — потоков (группа API pthread_*()). Техника потоков вводит новую парадигму программирования вместо уже ставших традиционными UNIX-методов. Это обстоятельство часто недооценивается. Например, использование pthread_create() вместо fork() может на порядки повысить скорость реакций, особенно в ОС с отсутствием механизмов COW (copy on write) при создании дубликатов физических страниц RAM сегментов данных (таких как QNX, хотя механизмы COW вряд ли вообще применимы в ОС реального времени) [4]. Другой пример: использование множественных потоков вместо ожиданий на множестве дескрипторов в операторе select().
Однако очень часто эти две парадигмы, традиционная и потоковая, не сочетаются в рамках единого кода из-за небезопасности (not thread safe) традиционных механизмов UNIX (fork(), select() и др.) в многопоточной среде. Тогда приходится использовать либо одну, либо другую парадигму как альтернативы, не смешивая их между собой. Или смешивать, но с большой осторожностью и с хорошим пониманием того, что при этом может произойти в каждом случае.
Поток можно понимать как любой автономный последовательный (линейный) набор команд процессора. Источником этого линейного кода для потока могут служить:
• бинарный исполняемый файл, на основе которого системой или вызовом группы spawn() запускается новый процесс и создается его главный поток;