В зависимости от частоты процессора, объема свободной оперативной памяти и скорости работы видеоподсистемы стандартное ядро Linux имеет время отклика в диапазоне от 10 до 100 мс (ядра серии 2.2.* даже до 150 мс), чего вполне достаточно для обычного использования. Но существуют задачи, для которых такая латентность считается невероятно большой. Например, обработка звука требует задержки не более 5 мс суммарно для всей системы, включая реакцию периферийных устройств. Давай разбираться, как можно уменьшить это значение в пингвине.

 

О чем это мы?

Работая на компьютере, нам постоянно приходится сталкиваться с различного рода задержками. Попробуй одновременно компилить ядро, слушать музыку, качать фильмы по сетке и набирать текст в OpenOffice.org. Я уверен, что рано или поздно ты столкнешься с тем, что знаки во Writer станут появляться через пару секунд после того, как ты нажмешь на кнопки клавиатуры, да и проигрыватель начнет постоянно заикаться. Как же можно уменьшить время отклика приложений в Linux - операционной системе разделения времени, где все процессы равны (ну, почти)? Ведь она разрабатывалась с упором на оптимизацию системной производительности в целом, и об обеспечении ограниченного времени ответа приложениям
речи не шло.

 

Используем подручные средства

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

Для того чтобы культурно распределить ресурсы и никого при этом не обделить, в любой системе имеется своя подсистема управления процессами, работающая по принципу «каждому по способностям, каждому по труду». Процесс может работать в двух режимах: в режиме ядра (kernel mode) и в пользовательском режиме (user mode), где он выполняет простые инструкции, не требующие особых «системных» данных. Но когда такие услуги понадобятся, процесс перейдет в режим ядра, хотя инструкции по-прежнему будут выполняться от имени процесса. Все это сделано специально, чтобы защитить рабочее пространство ядра от пользовательского процесса. Остальные процессы либо готовятся к
запуску, ожидая, когда планировщик их выберет, либо находятся в режиме сна (asleep), дожидаясь недоступного на данный момент времени ресурса. С последним все просто. Когда поступает сигнал с подконтрольного устройства, процесс объявляет себя TASK_RUNNING и становится в очередь готовности к запуску. Если он имеет высший приоритет, то ядро переключается на его выполнение.

Но есть еще одна заковырка. При предоставлении процессу системных ресурсов происходит так называемое переключение контекста (context switch), сохраняющее образ текущего процесса (на что, кстати, тоже требуется какое-то время, поэтому латентность даже в идеальном случае не будет равна нулю). Так вот переключение контекста, когда процесс находится в режиме ядра, может привести к краху всей системы. Поэтому высокоприоритетному процессу придется терпеливо подождать момента перехода в режим задачи, а это может произойти в двух случаях: работа сделана или необходимый ресурс недоступен. То есть, чтобы обеспечить меньшее время отклика, необходимо свести к минимуму число ядерных
задач
. Но за такое решение приходится платить общей стабильностью и «тяжестью» кода. В микроядрах это, кстати, реализовано значительно лучше - имеется базовый минимальный набор, остальное навешивается модульно, как на новогоднюю елку, что обеспечивает универсальность и позволяет конструировать системы под конкретные задачи.

Что касается планирования процессов, то оно завязано на приоритете. Планировщик попросту выбирает следующий процесс, имеющий наивысший приоритет. При этом менее приоритетный процесс, выполняющийся в тот момент, может даже полностью не отработать свой квант до конца. Каждый процесс имеет два вида приоритета: относительный (p->nice, по умолчанию до 100 уровней приоритетов), устанавливаемый при запуске приложения, и текущий, на основании которого и происходит планирование. Значение текущего приоритета не является фиксированным, а вычисляется динамически и напрямую зависит от nice. Значение, устанавливаемое пользователем, может находиться в пределах от -20 до +19, при этом приложению с более
высоким приоритетом соответствует значение -20, а +10 (по умолчанию) и выше считаются уже низкоприоритетными задачами. Например, для запуска программы с более высоким, чем обычно, приоритетом, делаем так:

$ sudo nice --20 mplayer

А с более низким:

$ sudo nice -20 job &

Чтобы изменить относительный приоритет процесса, следует использовать идентификатор процесса, а не название:

$ sudo renice --20 PID

Кстати, уменьшить время отклика можно, отказавшись от использования утилиты hdparm для дисковых устройств (исключение составляют случаи, когда предвидятся интенсивные операции ввода/вывода, например, обработка аудио- или видеоданных). Так мы получим выигрыш в районе 2 мс.

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

Каждый компьютер имеет системные часы, которые генерируют аппаратное прерывание через определенные промежутки времени. Интервал между этими прерываниями называется тиком (clock tick). В различных операционных системах тик имеет свое значение. В Linux, как и в большинстве никсов, он составляет 10 мс. Значение можно подсмотреть в файле заголовков include/linux/param.h, в константе HZ. Для тика 10 мс значение HZ равно 100. Чем эта цифра больше, тем чаще тикает планировщик. Тик - одна из высокоприоритетных задач, которая должна занимать минимум времени. За время тика происходит просмотр статистики использования процессора, перепланирование процессов, обновление системного
времени (CLOCKS_PER_SEC), отработка необходимых системных процессов, отложенных вызовов и алармов (посылка определенного сигнала процессу через запрашиваемое им время).

Все эти тонкости использовались в различных патчах реализации lowlatency для ядра 2.4. Заменялись не только алгоритмы пересчета текущего приоритета, но и все возможные константы (например, предел nice увеличивали до 256), кроме того, устанавливался таймер с частотой вплоть до 1000 HZ. Пример такого решения найдешь на странице www.zip.com.au/~akpm/linux/schedlat.html.

Для того чтобы указать на приложение, которое требует особого внимания со стороны процессора, можно использовать и заложенный в спецификации POSIX real-time вызов SCHED_FIFO (нечто вроде перехода в режим «мягкого» реального времени). Подобный результат достигается при использовании вызовов SCHED_RR, CAP_IPC_LOCK, CAP_SYS_NICE или через подмену значения sys_sched_get_priority_max - функции, возвращающей максимальный real-time приоритет. Именно использование SCHED_FIFO приводит к тому, что проигрыватель xmms, запущенный под root, практически не заикается даже при запредельных нагрузках на систему.

 

Выгружаемое ядро

Основной проблемой real-time является возможность захвата ресурсов у низкоприоритетного процесса, особенно если он выполняется в режиме ядра. Ведь даже на переключение контекста тратится некоторое время. Сотни разработчиков по всему миру пытались применить милиарды технологий: от возможности прерывания во время исполнения в ядерном режиме (preemptible kernel, выгружаемое ядро) до временного наследования (inherit) приоритета реального времени низкоприоритетным приложением, чтобы он мог поскорее закончить критический раздел кода и отдать управление.

Тема выгружаемого ядра заинтересовала общественность еще в период господства ветки 2.2. Линус Торвальдс сказал, что real-time - плохая идея, и до поры preemptible реализовывалось исключительно с помощью патчей. Но уже при подготовке ветки 2.6 в исходный код была добавлена возможность сделать ядро выгружаемым (PREEMPT_RT). Preemptible-kernel реализуется, как правило, в виде второго ядра. Если процесс обращается к нему с запросом, основная система фактически блокируется на время его выполнения. Исполняется все это в виде загружаемого модуля, который подменяет/перехватывает наиболее критичные функции, способные привести к задержкам. Но не все так просто. В своем интервью один
из инженеров MontaVista (компании-разработчика одного из real-time решений на базе Linux) заявил, что в ядре 2.6 около 11 000 участков кода просто невозможно сделать preemptible.

В интернете, если хорошенько поискать, можно найти достаточное количество разнообразных патчей, позволяющих реализовать режим реального времени в Linux, но, как правило, большая часть проектов уже устарела и предлагает изменения к ядру 2.4. Например, KURT-Linux (www.ittc.ku.edu/kurt) и RT-Linux (www.rtlinux.org). Обе Линукса используют похожие технологии, и субъективно отличий в их работе не заметно, но в интернете хвалят все кому не лень именно RT-Linux. Найти компьютеры под ее управлением можно на генераторах «Токамак», в больницах Перу, на
спутниках NASA
и в симуляторах F111-C. Кстати, если в Ubuntu ввести sudo apt-cache search real-time, то ты обнаружишь наличие пакета со старой 3.1pre3-3 версией RT-Linux.

 

Патчим ядро

Для тестирования задержек следует использовать специальные утилиты. В Сети можно найти несколько решений. Например, latencytest, которая разрабатывалась как раз для измерения общих задержек при обработке мультимедийных данных во время различных потрясений, которые могут возникнуть на обычном десктопе (загрузка процессора сложными вычислениями; трудоемкие операции ввода/вывода: запись, копирование, считывание файла размером 350 Мб; вывод большого количества графики; доступ к системе процессов /proc с обновлением через 0,01 с). Эта утилита считается уже устаревшей, зато вся информация выводится с помощью
наглядных графиков, что очень удобно при сравнении результатов. К современным бенчмаркам можно отнести rt-test и pi_tests.

На стандартном ядре запускаем утилиту rt-test, только запустив, мы получаем значение 0,125 мс, при увеличении нагрузки оно возрастает до 15,402 мс. Следует обратить внимание на параметр Criteria, который равен 100 микросекунд. В нашем случае результат теста - FAIL, то есть до real-time еще далеко. Ставим lowlatency-ядро - обычное ядро, но с таймером 1000 HZ и уменьшенным временем отклика:

$ sudo apt-get install linux-lowlatency

Перезагружаемся и запускаем rt-test еще раз.

$ sudo rt-test all

Стартовое значение латентности теперь равно 0,073 мс, а максимальное – 2,907 мс. Уже лучше. Хотя Criteria по-прежнему FAIL, но музычка в Amarok'е при приличной загрузке системы больше не прерывается.

Из всех решений по реализации системы реального времени до сегодняшнего дня дошло только одно, предлагаемое Инго Молнаром. Ходили слухи о том, что выпускаемые им патчи (www.kernel.org/pub/linux/kernel/projects/rt) будут включены в ядро 2.6.22, но пока этого не случилось. Для установки rt поклонникам Fedora достаточно ввести yum install kernel-rt, остальным придется немного покомпилировать. Качаем и применяем патч к своему ядру:

$ wget –c www.kernel.org/pub/linux/kernel/projects/rt/older/patch-2.6.22.1-rt9
$ tar xjvf linux-2.6.22.1.tar.bz2
$ cd linux-2.6.22.1
$ sudo patch -p1 < ../patch-2.6.22.1-rt9
$ make menuconfig

При конфигурировании обнаружится ряд дополнительных параметров. Среди них Tickless System (Dynamic Ticks) (NO_HZ) – динамически изменяемый тик; High Resolution Timer Support (HIGH_RES_TIMERS) – таймер высокого разрешения, почитать о нем можно здесь - www.linuxsymposium.org/2006/linuxsymposium_procv1.pdf. В секции Preemption Mode нас интересует Complete Preemption (Real-Time) (PREEMPT_RT), хотя доступны еще No Forced Preemption (Server) (PREEMPT_NONE), Voluntary Kernel Preemption (Desktop) (PREEMPT_VOLUNTARY), Preemptible Kernel (Low-Latency Desktop) (PREEMPT_DESKTOP). В
секции «Block layer - Default I/O scheduler» появился планировщик полностью справедливой очереди CFS.

После перезагрузки системы, введя dmesg, можно увидеть, что ядро стало PREEMPT RT, таймер часов работает нестабильно («Clocksource tsc unstable»), а ps aux показывает наличие большого числа новых процессов. Но нас больше интересует результат работы rt-test. Так вот все наши ухищрения привели к тому, что максимальное значение латентности теперь не превышает 0,07 мс. Вуаля, тест пройден!


Полную версию статьи
читай в ноябрьском номере Хакера!

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии