Переключение контекста — это переключение от одной, готовой к выполнению задачи к другой. Это переключение производится с помощью функции
context_switch
, определенной в файле
kernel/sched.c
. Данная функция вызывается функцией
schedule
,
когда новый процесс выбирается для выполнения. При этом выполняются следующие шаги.
• Вызывается функция
switch_mm
, которая определена в файле
include/asm/mmu_context.h
и предназначена для переключения от виртуальной памяти старого процесса к виртуальной памяти нового процесса.
• Вызывается функция
switch_to
, определенная в файле
include/asm/system.h
, для переключения от состояния процессора предыдущего процесса к состоянию процессора нового процесса. Эта процедура включает восстановление информации стека ядра и регистров процессора.
Ядро должно иметь информацию о том, когда вызывать функцию
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
, является готовым к выполнению, то все задачи с более низким приоритетом не могут выполняться до тех пор, пока это задание не завершится.