Выбрать главу

Ядро должно иметь информацию о том, когда вызывать функцию schedule(). Если эта функция будет вызываться только тогда, когда программный код вызывает ее явно, то пользовательские программы могут выполняться неопределенное время. Поэтому ядро поддерживает флаг need_resched для того, чтобы сигнализировать, необходимо ли вызывать функцию schedule() (табл. 4.2). Этот флаг устанавливается функцией scheduler_tick(), когда процесс истрачивает свой квант времени, и функцией try_to_wake_up(), когда процесс с приоритетом более высоким, чем у текущего процесса, возвращается к выполнению. Ядро проверяет значение этого флага, и если он установлен, то вызывается функция schedule() для переключения на новый процесс. Этот флаг является сообщением ядру о том, что планировщик должен быть активизирован по возможности раньше, потому что другой процесс должен начать выполнение.

Таблица 4.2. Функции для управления флагом need_resched

Функция Назначение
set_tsk_need_resched(task) Установить флаг need_resched для данного процесса
clear_tsk_need_resched(task) Очистить флаг need_resched для данного процесса
need_resched() Проверить значение флага need_resched для данного процесса. Возвращается значение true, если этот флаг установлен, и false, если не установлен

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

Этот флаг не является глобальной переменной, так как обращение к дескриптору процесса получается более быстрым, чем обращение к глобальным данным (из-за скорости обращения к переменной current и потому, что соответствующие данные могут находиться в кэше). Исторически, этот флаг был глобальным в ядрах до серии 2.2. В ядрах серий 2.2 и 2.4 этот флаг принадлежал структуре task_struct и имел тип int. В серии ядер 2.6 этот флаг перемещен в один определенный бит специальной переменной флагов структуры thread_info. Легко видеть, что разработчики ядра никогда не могут быть всем довольны.

Вытеснение пространства пользователя

Вытеснение пространства пользователя (user preemption) происходит в тот момент, когда ядро собирается возвратить управление режиму пользователя, при этом устанавливается флаг need_resched и, соответственно, активизируется планировщик. Когда ядро возвращает управление в пространство пользователя, то оно находится в безопасном и "спокойном" состоянии. Другими словами, если продолжение выполнения текущего задания является безопасным, то безопасным будет также и выбор нового задания для выполнения. Поэтому когда ядро готовится возвратить управление в режим пользователя или при возврате из прерывания или после системного вызова, происходит проверка флага need_resched. Если этот флаг установлен, то активизируется планировщик и выбирает новый, более подходящий процесс для исполнения. Как процедура возврата из прерывания, так и процедура возврата из системного вызова являются зависимыми от аппаратной платформы и обычно реализуются на языке ассемблера в файле entry.S (этот файл, кроме кода входа в режим ядра, также содержит и код выхода из режима ядра). Если коротко, то вытеснение пространства пользователя может произойти в следующих случаях.

• При возврате в пространство пользователя из системного вызова.

• При возврате в пространство пользователя из обработчика прерывания.

Вытеснение пространства ядра

Ядро операционной системы Linux, в отличие от ядер большинства вариантов ОС Unix, является полностью преемптивным (вытесняемым, preemptible). В непреемптивных ядрах код ядра выполняется до завершения. Иными словами, планировщик не может осуществить планирование для выполнения другого задания, пока какое-либо задание выполняется в пространстве ядра — код ядра планируется на выполнение кооперативно, а не посредством вытеснения. Код ядра выполняется до тех пор, пока он не завершится (возвратит управление в пространство пользователя) или пока явно не заблокируется. С появлением серии ядер 2.6, ядро Linux стало преемптивным: теперь есть возможность вытеснить задание в любой момент, конечно, пока ядро находится в состоянии, когда безопасно производить перепланирование выполнения.

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

Первое изменение, внесенное для поддержки вытеснения пространства ядра, — это введение счетчика преемптивности preempt_count в структуру thread_info каждого процесса. Значение этого счетчика вначале равно нулю и увеличивается на единицу при каждом захвате блокировки, а также уменьшается на единицу при каждом освобождении блокировки. Когда значение счетчика равно нулю— ядро является вытесняемым. При возврате из обработчика прерывания, если возврат выполняется в пространство ядра, ядро проверяет значения переменных need_resched и preempt_count. Если флаг need_resched установлен и значение счетчика preempt_count равно нулю, значит, более важное задание готово к выполнению и выполнять вытеснение безопасно. Далее активизируется планировщик. Если значение счетчика preempt_count не равно нулю, значит, удерживается захваченная блокировка и выполнять вытеснение не безопасно. В таком случае возврат из обработчика прерывания происходит в текущее выполняющееся задание. Когда освобождаются все блокировки, удерживаемые текущим заданием, значение счетчика preempt_count становится равным нулю. При этом код, осуществляющий освобождение блокировки, проверяет, не установлен ли флаг need_resched. Если установлен, то активизируется планировщик. Иногда коду ядра необходимо иметь возможность запрещать или разрешать вытеснение в режиме ядра, что будет рассмотрено в главе 9.

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

Вытеснение пространства ядра может произойти в следующих случаях.

• При возврате из обработчика прерывания в пространство ядра.

• Когда код ядра снова становится преемптивным.

• Если задача, работающая в режиме ядра, явно вызывает функцию schedule().

• Если задача, работающая в режиме ядра, переходит в приостановленное состояние, т.е. блокируется (что приводит к вызову функции schedule()).

Режим реального времени

Операционная система Linux обеспечивает две стратегии планирования в режиме реального времени (real-lime): SCHED_FIFO и SCHED_RR. Стратегия планирования SCHED_OTHER является обычной стратегией планирования, т.е. стратегий планирования не в режиме реального времени. Стратегия SCHED_FIFO обеспечивает простой алгоритм планирования по идеологии "первым вошел — первым обслужен" (first-in first-out, FIFO) без квантов времени. Готовое к выполнению задание со стратегией планирования SCHED_FIFO всегда будет планироваться на выполнение перед всеми заданиями со стратегией планирования SCHED_OTHER. Когда задание со стратегией SCHED_FIFO становится готовым к выполнению, то оно будет продолжать выполняться до тех пор, пока не заблокируется или пока явно не отдаст управление. Две или более задач с одинаковым приоритетом, имеющие стратегию планирования SCHED_FIFO, будут планироваться на выполнение по круговому алгоритму (round-robin). Если задание, имеющее стратегию планирования SCHED_FIFO, является готовым к выполнению, то все задачи с более низким приоритетом не могут выполняться до тех пор, пока это задание не завершится.

Стратегия SCHED_RR аналогична стратегии SCHED_FIFO, за исключением того, что процесс может выполняться только до тех пор, пока не израсходует предопределенный ему квант времени. Таким образом, стратегия SCHED_RR — это стратегия SCHED_FIFO с квантами времени, т.е. круговой алгоритм планирования (round-robin) реального времени. Когда истекает квант времени процесса со стратегией планирования SCHED_RR, то другие процессы с таким же приоритетом планируются по круговому алгоритму. Квант времени используется только для того, чтобы перепланировать выполнение заданий с таким же приоритетом. Так же как в случае стратегии SCHED_FIFO, процесс с более высоким приоритетом сразу же вытесняет процессы с более низким приоритетом, а процесс с более низким приоритетом никогда не сможет вытеснить процесс со стратегией планирования SCHED_RR, даже если у последнего истек квант времени.

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

Стратегии планирования реального времени в операционной системе Linux обеспечивают так называемый мягкий режим реального времени (soft real-time). Мягкий режим реального времени обозначает, что ядро пытается планировать выполнение пользовательских программ в границах допустимых временных сроков, но не всегда гарантирует выполнение этой задачи. В противоположность этому операционные системы с жестким режимом реального времени (hard real-time) всегда гарантируют выполнение всех требований по планированию выполнения процессов в заданных пределах. Операционная система Linux не может гарантировать возможности планирования задач реального времени. Тем не менее стратегия планирования ОС Linux гарантирует, что задачи реального времени будут выполняться всякий раз, когда они готовы к выполнению. Хотя в ОС Linux и отсутствуют средства, гарантирующие работу в жестком режиме реального времени, тем не менее производительность планировщика ОС Linux в режиме реального времени достаточно хорошая. Ядро серии 2.6 в состоянии удовлетворить очень жестким временным требованиям.

Приоритеты реального времени лежат в диапазоне от 1 до MAX_RT_PRIO минус 1, По умолчанию значение константы MAX_RT_PRIO равно 100, поэтому диапазон значений приоритетов реального времени по умолчанию составляет от 1 до 99. Это пространство приоритетов объединяется с пространством значений параметра nice для стратегии планирования SCHED_OTHER, которое соответствует диапазону приоритетов от значения MAX_RT_PRIO до значения (MAX_RT_PRIO+40). По умолчанию это означает, что диапазон значений параметра nice от -20 до +19 взаимно однозначно отображается в диапазон значений приоритетов от 100 до 139.