Многозадачность в Linux: вытесняющая vs невытесняющая
Многозадачность - это способность операционной системы выполнять несколько задач одновременно. Но “одновременно” - это иллюзия. На самом деле процессор переключается между задачами так быстро, что создаётся впечатление параллельной работы.
Понимание того, как работает многозадачность, критично для написания эффективных приложений и понимания поведения системы под нагрузкой.
Что такое многозадачность
Представьте повара на кухне. Он готовит несколько блюд: варит суп, жарит мясо, печёт пирог. Он не может делать всё одновременно - у него две руки. Но он переключается между задачами: помешал суп, перевернул мясо, проверил пирог.
Так же работает процессор. Он выполняет одну задачу, потом переключается на другую, потом на третью. Переключение происходит так быстро (тысячи раз в секунду), что кажется, будто всё работает параллельно.
Два типа многозадачности
Невытесняющая многозадачность (Cooperative Multitasking)
В невытесняющей многозадачности программа сама решает, когда отдать управление другим программам. Это как вежливый разговор: вы говорите, потом добровольно замолкаете, давая слово другому.
Как это работает:
Программа выполняется до тех пор, пока сама не решит передать управление операционной системе. Она может работать секунду, минуту или вообще никогда не отдать управление.
Проблемы:
- Зависание системы - если одна программа зависла и не отдаёт управление, вся система встаёт
- Недобросовестные программы - программа может захватить процессор и не отпускать
- Нет гарантий - нельзя гарантировать время отклика
Пример из истории:
Windows 3.1 и Mac OS до версии X использовали невытесняющую многозадачность. Если программа зависала, приходилось перезагружать весь компьютер.
Современное применение:
Сегодня невытесняющая многозадачность используется внутри программ для кооперативных корутин (goroutines в Go, async/await в JavaScript), но не на уровне операционной системы.
Вытесняющая многозадачность (Preemptive Multitasking)
В вытесняющей многозадачности операционная система принудительно забирает управление у программы через определённые интервалы времени. Это как модератор дискуссии: вы говорите, но через минуту модератор вас прерывает и даёт слово другому.
Как это работает:
Операционная система использует таймер (обычно срабатывает каждые 1-10 миллисекунд). Когда таймер срабатывает, происходит прерывание, и планировщик решает, какую задачу выполнять дальше.
Преимущества:
- Отзывчивость - система всегда реагирует на действия пользователя
- Справедливость - каждая программа получает процессорное время
- Стабильность - зависшая программа не блокирует всю систему
- Приоритеты - важные задачи могут получать больше времени
Linux использует вытесняющую многозадачность.
Как работает вытесняющая многозадачность в Linux
Квант времени (Time Slice)
Каждому процессу выделяется квант времени - промежуток, в течение которого он может выполняться. В Linux это обычно 1-10 миллисекунд.
Когда квант истекает, планировщик:
- Сохраняет состояние текущего процесса (регистры, счётчик команд)
- Выбирает следующий процесс для выполнения
- Восстанавливает состояние выбранного процесса
- Передаёт ему управление
Этот процесс называется переключением контекста (context switch).
Планировщик (Scheduler)
Планировщик - это часть ядра Linux, которая решает, какой процесс выполнять следующим.
Критерии выбора:
- Приоритет - процессы с высоким приоритетом выполняются чаще
- Время ожидания - процессы, которые долго ждали, получают преимущество
- Тип задачи - интерактивные задачи (GUI) важнее фоновых
- CPU affinity - привязка к конкретному ядру процессора
Классы планирования в Linux:
- SCHED_NORMAL - обычные процессы (99% программ)
- SCHED_FIFO - реального времени,
FIFO очередь - SCHED_RR - реального времени,
Round-Robin - SCHED_BATCH - фоновые задачи
- SCHED_IDLE - самый низкий приоритет
Приоритеты
В Linux есть два типа приоритетов:
Nice value (-20 до +19):
- -20 - самый высокий приоритет
- 0 - нормальный приоритет
- +19 - самый низкий приоритет
Обычный пользователь может только понижать приоритет своих процессов. Повышать может только root.
Real-time priority (1-99):
- Используется для задач реального времени
- Приоритет 99 - самый высокий
- Требует root-прав
Состояния процесса
Процесс в Linux может находиться в нескольких состояниях:
- Running (R) - выполняется или готов к выполнению
- Sleeping (S) - ждёт события (ввод-вывод, сигнал)
- Uninterruptible Sleep (D) - ждёт ввод-вывод, нельзя прервать
- Stopped (T) - остановлен (Ctrl+Z)
- Zombie (Z) - завершился, но родитель не прочитал статус
Планировщик выбирает только из процессов в состоянии Running.
Почему важно понимать разницу
1. Производительность приложений
Если ваше приложение выполняет длительные вычисления без переключения контекста, оно может показаться пользователю “зависшим”, даже если технически оно работает.
Плохо:
// Блокирует поток на 10 секунд
func ProcessData(data []byte) {
for i := 0; i < 10000000000; i++ {
// Вычисления
}
}
Хорошо:
// Периодически отдаёт управление
func ProcessData(data []byte) {
for i := 0; i < 10000000000; i++ {
// Вычисления
if i % 1000000 == 0 {
runtime.Gosched() // Отдать управление другим горутинам
}
}
}
2. Отзывчивость системы
Понимание приоритетов помогает правильно настроить систему. Например, GUI приложения должны иметь более высокий приоритет, чем фоновые задачи.
# Запустить с низким приоритетом
nice -n 19 ./background-task
# Изменить приоритет работающего процесса
renice -n 10 -p 1234
3. Реальное время
Для задач реального времени (аудио, видео, управление оборудованием) критично понимать, как работает планировщик.
# Запустить с приоритетом реального времени
chrt -f 50 ./realtime-app
4. Отладка проблем
Когда система “тормозит”, понимание многозадачности помогает найти причину:
# Посмотреть загрузку процессора по процессам
top
# Посмотреть переключения контекста
vmstat 1
# Посмотреть приоритеты процессов
ps -eo pid,ni,pri,comm
Многоядерность и многозадачность
На многоядерном процессоре несколько задач действительно выполняются одновременно - каждое ядро работает независимо.
Но:
Если у вас 4 ядра и 100 процессов, планировщик всё равно переключает их на каждом ядре.
Важные концепции:
- CPU affinity - привязка процесса к конкретному ядру
- Load balancing - распределение нагрузки между ядрами
- Cache locality - процесс лучше работает на том же ядре
# Привязать процесс к ядрам 0 и 1
taskset -c 0,1 ./my-app
# Посмотреть на каком ядре работает процесс
ps -eo pid,psr,comm
Практические советы
1. Не блокируйте главный поток
В GUI приложениях длительные операции выполняйте в фоновых потоках.
2. Используйте правильные приоритеты
- Интерактивные задачи - нормальный или повышенный приоритет
- Фоновые задачи - пониженный приоритет
- Задачи реального времени -
SCHED_FIFOилиSCHED_RR
3. Минимизируйте переключения контекста
Частые переключения контекста снижают производительность. Группируйте работу в батчи.
4. Учитывайте NUMA
На серверах с несколькими процессорами память может быть “ближе” к одним ядрам и “дальше” к другим. Это влияет на производительность.
# Посмотреть NUMA топологию
numactl --hardware
# Запустить на конкретном NUMA узле
numactl --cpunodebind=0 --membind=0 ./my-app
Заключение
Многозадачность в Linux - это вытесняющая многозадачность, где операционная система принудительно переключает процессы.
Ключевые моменты:
- Вытесняющая многозадачность обеспечивает отзывчивость и стабильность
- Планировщик решает, какой процесс выполнять
- Приоритеты влияют на распределение процессорного времени
- Переключение контекста имеет накладные расходы
- Многоядерность позволяет истинный параллелизм
Понимание этих концепций помогает:
- Писать более эффективные приложения
- Правильно настраивать систему
- Отлаживать проблемы производительности
- Работать с задачами реального времени
Linux предоставляет мощные инструменты для управления многозадачностью. Используйте их с умом.
Дополнительные ресурсы: