Вытеснение: Как Отобрать Процессор

?Эта статья не имеет смысла без предыдущая, в которой описывались основные механизмы переключения контекста в многозадачной ОС .

Здесь я расскажу, как совместная многозадачность превращается в состязательную вытесняющую многозадачность.

Суть этого преобразования проста.

В машине есть таймер, таймер генерирует прерывания, прерывания приостанавливают выполнение кода потока и передают процессор механизму многозадачности.

Этот уже вполне кооперативно переключает процессор на новый поток, как описано в предыдущая статья .

Но, как обычно, есть нюансы.

См.

код для информации .

«Удаление» самого процессора происходит как в рамках обычного аппаратного прерывания, обычно по таймеру, так и в рамках «программного» прерывания – которое, по сути, является тем же прерыванием, но вызванным специальная инструкция процессора.

Этот метод переключения контекста нужен, если мы (например, в рамках примитива синхронизации) явно останавливаем поток и не хотим ждать прибытия прерывания по таймеру.

Во-первых, прежде чем отбирать процессор у плохих потоков, нужно обслужить само прерывание.

Процессор/контроллер прерываний «знает», что прерывание началось, и его необходимо «успокоить», сообщив нам, что мы завершили обслуживание, прежде чем мы переключимся на другой поток.

В противном случае состояние контроллера прерываний может оказаться весьма странным и система перестанет внятно функционировать.

Да и сам обработчик прерывания будет не рад, если перед его выполнением пару секунд поработают другие потоки.

Поэтому сначала обслужим само прерывание.

Затем мы убедимся, что оно не было вложенным — что мы не прервали обслуживание другого прерывания, которое тоже должно завершиться.

И теперь, когда мы все еще находимся внутри функции обслуживания прерываний, но сами прерывания обслужены и контроллер прерываний правильно уведомлен об этом (и, конечно же, сами прерывания глобально отключены), мы можем проверить, есть ли это мягкий irq-запрос, и если да - обслужите его.

Здесь нужно понимать, что старшие биты переменной irq_nest содержат флаг отключения обслуживания программных прерываний и флаг отсутствие запрос программного прерывания.

То есть если он равен нулю, то при этом вложенность запросов на аппаратное прерывание равна нулю, запрета на программное прерывание нет и Есть запрос программного прерывания.

  
  
  
  
  
  
  
  
   

if(irq_nest) return; // Now for soft IRQs irq_nest = SOFT_IRQ_DISABLED|SOFT_IRQ_NOT_PENDING; hal_softirq_dispatcher(ts); ENABLE_SOFT_IRQ();

Естественно, пока мы обслуживаем программное прерывание, сами программные прерывания запрещены.

Как только обслуживание будет завершено, мы разрешим их снова.

Но — см.

предыдущую статью — если текущий поток удалить из процессора, то мы снова включим программные прерывания — иначе поток, на который мы переключаемся, никогда не переключится обратно.

Ведь для этого нужно выполнить программное прерывание.

Надеюсь, я не запутал вас.

При инициализации потоков ядро регистрирует программный обработчик прерываний.



hal_set_softirq_handler( SOFT_IRQ_THREADS, (void *)phantom_scheduler_soft_interrupt, 0 );

Этот обработчик, помимо всяких проверок, сводится к вызову phantom_thread_switch(), то есть просто приводит к переключению на следующий поток.

Осталось два пункта.

Первый — как явно «отдать» процессор.

Например, когда мы пытаемся захватить уже заблокированный мьютекс, поток необходимо остановить.

Для этого мы вызываем запрос программного прерывания и запрашиваем (любое, но лучше редко используемое) аппаратное прерывание.



void phantom_scheduler_request_soft_irq() { hal_request_softirq(SOFT_IRQ_THREADS); __asm __volatile("int $15"); }

Как указано выше, это приведет к вызову функции phantom_thread_switch из контекста программного прерывания.

Второй: кто будет запрашивать программное прерывание для завершения текущего потока в конце выделенного ему временного интервала процессора? Для этого есть запрос:

void phantom_scheduler_schedule_soft_irq() { hal_request_softirq(SOFT_IRQ_THREADS); }

Вот когда оно исполняется.

Внутри прерывания таймера вызывается специальная функция:

// Called from timer interrupt 100 times per sec. void phantom_scheduler_time_interrupt(void) { if(GET_CURRENT_THREAD()->priority & THREAD_PRIO_MOD_REALTIME) return; // Realtime thread will run until it blocks or reschedule requested if( (GET_CURRENT_THREAD()->ticks_left--) <= 0 ) phantom_scheduler_request_reschedule(); }

Как видите, он уменьшает переменную потока Tickets_left и, если она равна нулю, запрашивает переключение потока.

Сама переменная Tickets_left задается планировщиком при выборе потока для запуска — он записывает в эту переменную количество интервалов по 10 мс, в течение которых поток будет работать (если он не хочет перед этим останавливаться).

Планировщик может установить фиксированное время работы (обслуживание приоритетов посредством частоты размещения потоков на процессоре) или учитывать приоритет (отдавая потокам с более высоким приоритетом более длинные интервалы).

К этому надо добавить, что тот, кто считает, что пришло время решить, кому взять на себя управление процессором, может вызвать phantom_scheduler_request_reschedule().

Например, это может быть уместно, если текущий поток разблокировал примитив синхронизации, на котором был заблокирован поток с высоким (особенно в реальном времени) приоритетом.

Сам по себе этот вызов лишь устанавливает флаг — настоящее переключение произойдет только в конце обслуживания прерывания, как написано выше.

Для полноты картины давайте подробно рассмотрим структуру описания потока (struct phantom_thread).

Поле cpu содержит зависящие от архитектуры поля, в которых хранится состояние процессора при остановке потока.

cpu_id — номер процессора, на котором поток последний раз запускался или работает в данный момент. tid — это просто идентификатор потока.

Владелец используется платформой фантомных объектов для привязки здесь объекта, описывающего поток на уровне приложения.

Если поток обслуживает подсистему совместимости Unix, pid хранит номер процесса Unix, которому принадлежит поток.

Имя предназначено только для целей отладки.



/** NB! Exactly first! Accessed from asm. */ cpu_state_save_t cpu; //! on which CPU this thread is dispatched now int cpu_id; int tid; //! phantom thread ref, etc void * owner; //! if this thread runs Unix simulation process - here is it pid_t pid; const char * name;

ctty — буфер stdin для потока, используемый для связи с графической подсистемой.

stack/kstack — виртуальный и физический адрес сегмента стека соответственно для пользовательского и режима ядра.

start_func и start_func_arg — это точка входа в функцию потока («main») и аргумент этой функции.



wtty_t * ctty; void * stack; physaddr_t stack_pa; size_t stack_size; void * kstack; physaddr_t kstack_pa; size_t kstack_size; void * kstack_top; // What to load to ESP void * start_func_arg; void (*start_func)(void *);

Sleep_flags — признаки засыпания потока по той или иной причине.

Если не ноль, поток не может быть запущен (ожидает мьютекса, таймера, не рожден, мертв и т. д.).

thread_flags — различные признаки потока: поток обслуживает виртуальную машину Phantom, у потока произошел таймаут примитива синхронизации и т. д. waitcond/mutex/sem — поток спит на этом примитиве, ожидая его освобождения.

ownmutex — этот поток заблокировал этот мьютекс, если он умирает, его необходимо освободить.

(Для семафора, к сожалению, не все очевидно.

) Sleep_event — используется, если примитив синхронизации заблокирован таймаутом — подсистема таймера ядра хранит здесь состояние запроса таймера.

цепочка — используется при постановке потока в очередь, если несколько потоков ожидают одного примитива синхронизации.

kill_chain — очередь на эшафот. Посмертной деинициализацией других потоков (освобождением памяти, разблокировкой мьютексов и т. д.) занимается специальный системный поток, и это его очередь.



u_int32_t thread_flags; // THREAD_FLAG_xxx /** if this field is zero, thread is ok to run. */ u_int32_t sleep_flags; //THREAD_SLEEP_xxx hal_cond_t * waitcond; hal_mutex_t * waitmutex; hal_mutex_t * ownmutex; hal_sem_t * waitsem; queue_chain_t chain; // used by mutex/cond code to chain waiting threads queue_chain_t kill_chain; // used kill code to chain threads to kill //* Used to wake with timer, see hal_sleep_msec timedcall_t sleep_event;

snap_lock — поток находится в состоянии, в котором снимок сделать невозможно.

preemption_disabled — поток невозможно удалить из процессора.

На самом деле смысла в этой штуке почти нет, особенно в среде SMP. Death_handler — будет вызван, если поток умер.

выход. catch_handler аналогичен тому, какие сигналы вызываются в пользовательском режиме — функция вызывается, если поток привел к исключению процессора.



int snap_lock; // nonzero = can't begin a snapshot int preemption_disabled; //! void (*handler)( phantom_thread_t * ) void * death_handler; // func to call if thread is killed //! Func to call on trap (a la unix signals), returns nonzero if can't handle int

Теги: #многопоточность #многозадачность #потоки #планировщик #фантом #фантомос #фантомная ОС #Ненормальное программирование #Системное программирование #Программирование микроконтроллеров

Вместе с данным постом часто просматривают: