Алгоритм планирования
В предыдущих разделах была рассмотрена в самых общих чертах теория работы планировщика процессов в операционной системе Linux. Теперь, когда мы разобрались с основами, можно более глубоко погрузиться в то, как именно работает планировщик ОС Linux.
Программный код планировщика операционной системы Linux содержится в файле kernel/sched.c
. Алгоритм планирования и соответствующий программный код были существенно переработаны в те времена, когда началась разработка ядер серии 2.5. Следовательно, программный код планировщика является полностью новым и отличается от планировщиков предыдущих версий. Новый планировщик разрабатывался для того, чтобы удовлетворять указанным ниже требованиям.
• Должен быть реализован полноценный O(1)-планировщик. Любой алгоритм, использованный в новом планировщике, должен завершать свою работу за постоянный период времени, независимо от числа выполняющихся процессов и других обстоятельств.
• Должна обеспечиваться хорошая масштабируемость для SMP-систем. Каждый процессор должен иметь свои индивидуальные элементы блокировок и свою индивидуальную очередь выполнения.
• Должна быть реализована улучшенная SMP-привязка (SMP affinity). Задания, для выполнения на каждом процессоре многопроцессорной системы, должны быть распределены правильным образом, и, по возможности, выполнение этих задач должно продолжаться на одном и том же процессоре. Осуществлять миграцию заданий с одного процессора на другой необходимо только для уменьшения дисбаланса между размерами очередей выполнения всех процессоров.
• Должна быть обеспечена хорошая производительность для интерактивных приложений. Даже при значительной загрузке система должна иметь хорошую реакцию и немедленно планировать выполнение интерактивных задач.
• Должна быть обеспечена равнодоступность ресурсов (fairness). Ни один процесс не должен ощущать нехватку квантов времени за допустимый период. Кроме того, ни один процесс не должен получить недопустимо большое значение кванта времени.
• Должна быть обеспечена оптимизация для наиболее распространенного случая, когда число готовых к выполнению процессов равно 1–2, но в то же время должно обеспечиваться хорошее масштабирование и на случай большого числа процессоров, на которых выполняется большое число процессов.
Новый планировщик позволяет удовлетворить всем этим требованиям.
Очереди выполнения
Основная структура данных планировщика — это очередь выполнения (runqueue). Очередь выполнения определена в файле kernel/sched.c
[21] в виде структуры struct runqueue
. Она представляет собой список готовых к выполнению процессов для данного процессора.
Для каждого процессора определяется своя очередь выполнения. Каждый готовый к выполнению процесс может находиться в одной и только в одной очереди выполнения. Кроме этого, очередь выполнения содержит информацию, необходимую для планирования выполнения на данном процессоре. Следовательно, очередь выполнения — это действительно основная структура данных планировщика для каждого процессора. Рассмотрим нашу структуру, описание которой приведено ниже. Комментарии объясняют назначения каждого поля.
struct runqueue {
spinlock_t lock; /* спин-блокировка для зашиты этой очереди выполнения */
unsigned long nr_running; /* количество задач, готовых к выполнению */
unsigned long nr_switches; /* количество переключений контекста */
unsigned long expired timestamp; /* время последнего обмена массивами */
unsigned long nr_uninterruptible; /* количество заданий в состоянии
непрерываемого ожидания */
unsigned long long timestamp last tick; /* последняя метка времени
планировщика */
struct task_struct *curr; /* текущее задание, выполняемое на данном
процессоре */
struct task_struct *idle; /* холостая задача данного процессора */
struct mm_struct *prev_mm; /* поле mm_struct последнего выполняемого
задания */
struct prio_array *active; /* указатель на активный массив приоритетов */
struct prio_array *expired; /* указатель на истекший
массив приоритетов */
struct prio_array arrays[2]; /* массивы приоритетов */
struct task_struct *migration_thread; /* миграционный поток для
данного процессора */
struct list_head migration_queue; /* миграционная очередь для
данного процессора */
atomic_t nr_iowait; /* количество заданий, ожидающих на ввод-вывод */
};
Поскольку очередь выполнения — это основная структура данных планировщика, существует группа макросов, которые используются для доступа к определенным очередям выполнения. Макрос cpu_rq(processor)
возвращает указатель на очередь выполнения, связанную с процессором, имеющим заданный номер. Аналогично макрос this_rq()
возвращает указатель на очередь, связанную с текущим процессором. И наконец, макрос task_rq(task)
возвращает указатель на очередь, в которой находится соответствующее задание.
Перед тем как производить манипуляции с очередью выполнения, ее необходимо заблокировать (блокировки подробно рассматриваются в главе 8, "Введение в синхронизацию выполнения кода ядра"). Так как очередь выполнения уникальна для каждого процессора, процессору редко необходимо блокировать очередь выполнения другого процессора (однако, как будет показано далее, такое все же иногда случается). Блокировка очереди выполнения позволяет запретить внесение любых изменений в структуру данных очереди, пока процессор, удерживающий блокировку, выполняет операции чтения или записи полей этой структуры. Наиболее часто встречается ситуация, когда необходимо заблокировать очередь выполнения, в которой выполняется текущее задание. В этом случае используются функции task_rq_lock()
и task_rq_unlock()
, как показано ниже.
struct runqueue *rq;
unsigned long flags;
rq = task_rq_lock(task, &flags);
/* здесь можно производить манипуляции с очередью выполнения */
task_rq_unlock(rq, flags);
Альтернативными функциями выступают функция this_rq_lock()
, которая позволяет заблокировать текущую очередь выполнения, и функция rq_unlock(struct runqueue *rq)
, позволяющая разблокировать указанную в аргументе очередь.
Для предотвращения взаимных блокировок код, который захватывает блокировки нескольких очередей, всегда должен захватывать блокировки в одном и том же порядке, а именно в порядке увеличения значения адреса памяти очереди (в главе 8, "Введение в синхронизацию выполнения кода ядра", приведено полное описание). Пример, как это осуществить, показан ниже.
21
Может возникнуть вопрос: почему используется файл kernel/sched.c
, а не заголовочный файл include/linux/sched.h
? Потому что желательно абстрагироваться от реализации кода планировщика и обеспечить доступность для остального кода ядра только лишь некоторых интерфейсов.