Операционные системы
Самостоятельная работа №2
Исследование принципов реализации процессов и очередей многозадачного ядра
(вытесняющая многозадачность, принципы планирования процессов)
Цель работы: познакомиться с принципами реализации процессов и очередей в ядре многозадачной среды.
Общие сведения
В вытесняющей многозадачности переключение процессов происходит по истечении кванта времени. Для пользовательского режима POSIX-системы предоставляют функции работы с интервальным таймером. О сработке таймера программа узнаёт при помощи сигнала.
Для установки обработчика сигнала можно использовать следующую последовательность действий:
#include <signal.h> sigset_t empty; sigemptyset(&empty); struct sigaction sa = {.sa_sigaction=handler, .sa_mask=empty, .sa_flags=SA_SIGINFO|SA_RESTART}; sigaction(TIMER_SIGNAL, &sa, NULL);
Первый параметр функции sigaction — номер сигнала, который будет использоваться системой для оповещении программы о срабатывании интервального таймера. В качестве значения константы TIMER_SIGNAL можно выбрать SIGIO или SIGRTMIN.
В качестве обработчика сигнала данный пример устанавливает функцию handler, которая должна быть объявлена следующим образом:
void handler(int num, siginfo_t *info, void *context);
Запуск таймера можно осуществить при помощи timer_create и timer_settime:
#include <time.h> timer_t timerid; struct sigevent sev = {.sigev_notify=SIGEV_SIGNAL, .sigev_signo=TIMER_SIGNAL}; struct itimerspec spec = {.it_interval.tv_sec=0, .it_interval.tv_nsec=90000000, .it_value.tv_sec=0, .it_value.tv_nsec=90000000}; if (timer_create(CLOCK_MONOTONIC, &sev, &timerid)) { perror("timer_create failed!"); exit(1); } if (timer_settime(timerid, 0, &spec, NULL)) { perror("time_settime failed!"); exit(1); }
Функция timer_create создаёт новый таймер для программы, в структуре sigevent указывается способ оповещения (SIGEV_SIGNAL — по сигналу) и номер сигнала (объявленная ранее константа TIMER_SIGNAL).
Функция timer_settime запускает таймер с указанными в структуре itimerspec параметрами. Поле it_interval задаёт период сработки для периодического таймера, а поле it_value задаёт интервал для первой сработки (значение 0 в поле it_value означает отмену/отключение таймера). Значение времени для обоих полей задаётся в секундах и наносекундах (в данном примере — 90 мс).
Точная полнофункциональная реализация вытеснящей многопоточности на уровне пользователя в POSIX-системах сопряжена с определёнными сложностями.
При вызове обработчика сигнала система создаёт для него отдельный стековый кадр в памяти процесса, а контекст прерванного потока сохраняется системой в контексте вызванного обработчика сигнала. Если выполнять переключение контекста в самом обработчике, то "старым" контекстом окажется не контекст прерванного потока, а контекст обработчика сигнала. Это следует иметь в виду при построении схемы переключений контекстов. Когда прерванному сигналом потоку будет возвращаться управление, сначала произойдёт возврат в контекст сигнала, который прервал этот поток. Затем завершится обработчик сигнала и механизм возврата из сигнала восстановит контекст прерванного потока.
Следует обратить внимание на состояние маски блокируемых сигналов при переключении контекстов. По умолчанию обрабочик сигнала вызывается в контексте, в котором маска сигналов блокирует сигнал, вызвавший этот обработчик. Именно такая маска попадёт в "старый" контекст. Если при переключении произойдёт передача управления на контекст в котором этот сигнал заблокирован, то переключение контекстов по сигналу становится невозможным.
Из-за асинхронной "природы" сигналов не все функции, описанные в стандарте POSIX, пригодны для использования в обработчике сигналов. В частности, функции манипулирования контекстом (setcontext, swapcontext) к таким не относятся. Однако longjmp (аналогичная по своей "природе") в этот список попадает. Практика показывает, что существующие системы (по крайней мере, Linux) "терпимо" относятся к такому отступлению от стандарта.
Кроме того, существует ряд функций, которые в своей работе активно задействуют Thread Local Storage (локальную память потока уровня ядра), которая для реализуемых потоков уровня пользователя является "глобальной". Такие функции становятся нереентерантными и непотокобезопасными в "самодельной" системе потоков уровня пользователя. Пример такой функции — malloc, а также функции, меняющие переменную errno.
Задание 1: Перенесите реализованную в л.р.1 процедуру переключения сопрограмм в обработчик сигналов. Опишите последовательность переключений контекстов и состояние маски блокируемых сигналов при каждом переключении.
Задание 2: Блокирущий системный вызов, использованный в одной сопрограмме, приводит к блокировке всего процесса. В системах управления потоков пользовательского уровня эта проблема решается за счёт собственных "обёрток". Реализуйте замену системному вызову sleep, позволяющую исключить вашу сопрограму из очереди на исполнение на указанное количество секунд:
unsigned int cosleep(unsigned int sec);
Подсказка: глобальное время можно измерять при помощи clock_gettime.
Примите во внимание, что все ваши сопрограммы могут находиться в состоянии ожидания. Чтобы процедура диспетчеризации не находилась в холостом цикле, предусмотрите служебную сопрограмму, бесконечно ожидающую поступления сигнала (pause).
Оформите получившийся проект как библиотеку: выделите заголовочный файл, объектный файл с реализацией, создайте сценарий компиляции (Makefile) с примерами ваших сопрограмм, использующих эту библиотеку.