Чисто реальный Linux.

Сергей А. ЯРЕМЧУК, 14.07.2003, Мой Компьютер Weekly

Сейчас трудно найти задачу, при выполнения которой не нашлось бы места компьютеру. Но весь вопрос в том, что в некоторых индустриальных и научных областях к реакции системы на событие выдвигаются куда более жесткие требования, чем в обычных десктоп-системах. Здесь правят бал уже так называемые системы реального времени. Самая известная из них, наверное, - QNX, развивающаяся уже более 20 лет и за это время прошедшая путь от однодискетных дистрибутивов к полноценным. А что же пингвин? Неужели здесь не нашлось ему места под солнцем? Конечно же, не минула чаша сия и пингвинов.

"PC - Penguin Compatible"

The Universal Dictionary of Penguins

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

Для начала, что такое латентность? Это, попросту говоря, временной интервал между тем моментом, в который вы бы хотели, чтобы произошло какое-то событие, и тем, в который оно действительно произошло. Например, при сильно загруженном процессоре попробуйте набрать быстро текст в OpenOffice - знаки иногда появляются не сразу, а с некоторой задержкой. Вот это и есть латентность. Как вы понимаете, ее величина зависит в том числе и от рабочей частоты процессора, свободной оперативной памяти, скорости работы видеоподсистемы. Но как бы ни была мощна система, все равно до реалтайма в некоторых случаях она не дотягивает. Да и стоит ли ставить трехгигагерцевый Pentium только для того, чтобы зафиксировать показание одного единственного датчика?

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

Процессы

Как известно, система жива процессами, которые играют ключевую роль в любой операционке. Кстати, процесс обычно путают с программой. Программа - это файл или их совокупность (библиотеки, исполняемый модуль, данные и т.д.) Для того чтобы программа могла работать, в системе первоначально создается некоторое окружение, его иногда называют средой выполнения задачи, которое обеспечивает выделение области памяти и других системных ресурсов, включая доступ к функциям ядра, возможность доступа к устройствам ввода/вывода и еще много чего. Тому, кто написал хотя бы пару программ на ассемблере или на С, будет легче это понять. Вот это все и принято называть процессом - попросту говоря, последний представляет собой программу в стадии выполнения. И еще программа может порождать сразу несколько процессов, которые к тому же могут выполняться параллельно или вообще одновременно (например, на многопроцессорных машинах или Hyper-Threading).

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

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

Здесь еще одна заковырка. При предоставлении процессу системных ресурсов происходит так называемое переключение контекста (context switch), с сохранением образа текущего процесса (на что, кстати, тоже требуется немалое время, поэтому латентность в любом случае не будет равна нулю). Так вот, переключение контекста, когда процесс находится в режиме ядра, может привести к краху всей системы, поэтому высокоприоритетному процессу придется терпеливо подождать момента перехода в режим задачи (добровольно он может уступить только в случае недоступности нужного ресурса). Отсюда еще одна задача. Чтобы обеспечить меньшее время отклика, необходимо свести к минимуму (разумному) число ядерных задач, что реализовано в некоторых ядрах реального времени, но за это иногда приходится платить общей стабильностью и "тяжестью" кода. В микрокернелах это, кстати, реализовано немного получше - имеется базовый минимальный набор, остальное навешивается модульно, как на новогоднюю елку, что обеспечивает универсальность и позволяет конструировать системы буквально под определенные задачи. Вообще, планировщик процессов представляет собой довольно сложный механизм, ведь на его плечи ложится выполнение самой трудной и противоречивой задачи. При этом необходимо не только отдать реалтаймовому процессу максимальное процессорное время, не обделяя при этом другие (иначе система попросту остановится), но также иметь возможность при необходимости отобрать требуемый ресурс у менее приоритетной задачи. Каждый компьютер имеет системные часы, которые генерируют аппаратное прерывание через определенные промежутки времени. Интервал между этими прерываниями называется тиком (clock tick).

В различных операционных системах тик имеет свое значение; в Linux, как и в большинстве Unix'ов, он составляет 10 миллисекунд. Откуда узнал? А исходники зачем? Чтобы место на диске занимать? Значение его можно подсмотреть в файле заголовков param.h в константе HZ. Для тика 10 мс значение HZ равно 100. Тик - одна из высокоприоритетных задач (поэтому и времени должен занимать минимум). За его время (необязательно за каждый тик, некоторые задачи выполняются раз в несколько тиков) происходит просмотр статистики использования процессора, перепланирование процессов, обновление системного времени (CLOCKS_PER_SEC 100, в том же файле), отработка некоторых необходимых системных процессов, отложенных вызовов и алармов (посылка определенного сигнала процессу через запрошенное им время).

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

Значение текущего приоритета не является фиксированным, а вычисляется динамически и напрямую зависит от значения nice. Nice, устанавливаемое пользователем, может принимать значение в пределах от -20 до +19, при этом приложению с более высоким приоритетом соответствует значение -20, а те, что имеют +10 (значение по умолчанию) и выше считаются уже низкоприоритетными задачами. Для установки относительного приоритета при запуске приложения используется одноименная команда. Синтаксис такой:

т.е. запустить программу с более высоким, чем обычно, приоритетом можно так:

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

Изменить относительный приоритет можно при помощи команды renice, т.е. запустив следующую команду, изменим текущий приоритет на максимально возможный (при этом используется уже идентификатор процесса):

Чем вам не реалтайм?

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

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

Ну вот, с процессами немного разобрались, по ходу решив некоторые вопросы реалтайма, теперь рассмотрим, какие еще есть варианты. Многие из них отличаются спецификой решения проблемы реального времени. Так, некоторые ориентированы в первую очередь на уменьшение времени первоначального отклика (отработки прерывания) вообще, чего в большинстве случаев достаточно, другие же направлены на максимальный захват времени процессора приложением. Далее, эти решения могут отличаться реализацией, т.е. работают ли они в режиме ядра или в user-mode. Первый режим труднее отлаживать, он более опасен, так как в случае сбоя может привести к общему краху системы, но зато дает преимущество по времени исполнения перед остальными программами.

Еще один метод: как ни странно, может помочь отказ от использования утилиты hdparm для дисковых устройств. На этом можно выиграть до 2 мс, но исключение составляют случаи, когда предвидятся интенсивные операции ввода/вывода, например, обработка аудио или видео (интенсивное I/O может вообще свести на нет все старания) - там без DMA придется совсем худо. В некоторых патчах заменяются как алгоритмы пересчета текущего приоритета, так и все возможные константы (например, увеличение пределов nice до 256), могущие повлиять на общую производительность системы (кэш, блокировка устройств и пр.) Дополнительно для реалтайм-процессов могут вводится свои nice-константы и отдельные коэффициенты пересчета для высокоприоритетных задач. Например, на странице http://www.zip.com.au/~akpm/linux/schedlat.html можно найти патчи к ядру, основная задача которых - измерение и уточнение времени переупорядочивания запросов, в том числе предусматривается таймер более высокого разрешения (это все характерно для большинства проектов).

Интересный механизм применен в патче, который создатель назвал waitqueues (http://www.zip.com.au/~akpm/linux/2.4.0-test11-pre3-task_exclusive.patch). При пробуждении процесса (освободился ресурс или поступила команда) ему обычно присваивается определенный приоритет, и процессы с таким приоритетом обрабатываются по принципу FIFO (первый пришел, первый ушел). Так, вполне справедливо посчитав, что для некоторых задач подойдет обратный LIFO-принцип, автор смог добиться повышения производительности Apache на 5%-10%.

Основной проблемой всегда была и будет возможность отобрать ресурсы у низкоприоритетного процесса, особенно если он выполняется в режиме ядра. Ведь даже на переключение контекста тратится некоторое время. Здесь применено множество технологий, от возможности прерывания во время исполнения в ядерном режиме - т.н. выгружаемое ядро (preemptible-kernel), до временного наследования (inherit) приоритета реального времени низкоприоритетным приложением, чтобы закончить критический раздел и вызвать необходимое прерывание; дополнительно может применяться "резервирование" устройств. Рreemptible-kernel реализуется, как правило, в виде второго ядра, своего рода система в системе, когда процесс обращается к нему с запросом, основная система фактически блокируется на время его выполнения. Исполняется все это в виде загружаемого модуля (Loadable Kernel Module - LKM), который подменяет (перехватывает) наиболее критичные функции, могущие привести к задержкам. Вообще, тема выгружаемого ядра заинтересовала общественность довольно давно, предложения сделать Linux-ядро preemptible к Торвальдсу поступали еще тогда, когда он начал работу над серией 2.4. Тогда он ответил, что реалтайм - bad-idea, а сейчас сказал "yes" поддержке DRM (Digital Rights Management) - всевозможным блокирующим функциям, сработав на руку Голливуду. Вот и пойми, что происходит! Но кто девушку поит, тот ее и танцует, тут уже ничего не поделаешь - стабильность, видать, на то время была превыше всего.

В документе Low-latency mini-howto, который можно найти на странице (http://mstation.org/lowlatency.html), приведен еще один интересный пример. Для того чтобы указать на приложение, которое требует особого внимания со стороны процессора, необходимо использовать заложенный в POSIX-спецификации реалтаймовый вызов SCHED_FIFO политики планировщика задач, с помощью которого можно без проблем добиться режима "мягкого" реального времени для приложения. Там приведены некоторые примеры использования, в том числе и код, который можно встроить в приложение, чтобы использовать эту возможность:

А при запуске приведенного ниже откомпилированного кода есть возможность указать PID процесса, который после этого будет практически единолично распоряжаться процессором:

Аналогичного результата можно добиться, используя вызовы SCHED_RR, CAP_IPC_LOCK, CAP_SYS_NICE, или через подмену значения sys_sched_get_priority_max - функции, возвращающей максимальный реалтайм-приоритет.

В Интернете, если хорошенько поискать, можно найти достаточное количество разнообразных патчей и других разработок, позволяющих реализовать режим реального времени в Linux. Некоторые из них модернизируют какую-то определенную часть ядра, например, таймер высокого разрешения или механизм системы управления процессами, есть и комплексные решения, иногда направленные на работу с конкретным устройством, например, для снижения задержек при работе с аудио. Я хочу упомянуть только о двух проектах, которые прописаны на Linux.org: KURT-Linux (Kansas University Real-Time Linux, http://www.ittc.ku.edu/kurt) и RT-Linux (http://www.rtlinux.org). Первая разрабатывается Information and Telecommunication Technology Center (ITTC) в Канзасском университете и полностью распространяется по лицензии GPL, вторая сейчас является собственностью компании FSMLabs (http://www.fsmlabs.com) и имеет как коммерческий вариант, так и бесплатный, распространяемый под двумя лицензиями - GPL и Open Patent License. Обе разработки используют похожие технологии; субъективно, в работе отличий не заметно, но RT-Linux в Интернете хвалят все кому не лень, найти компьютеры под ее управлением можно генераторах Токамак, используемых в физике, в больницах Перу, на спутниках NASA и в симуляторах F111-C, а KURT-Linux обходят молчанием; к тому же на момент написания статьи на сайте KURT-Linux был патч только к версии 2.4.18 и к iPAQ. Для RT-Linux дополнительно в пакете rtlinux_contrib (6.6 Мб) можно получить различные реалтаймовые драйвера, например для СОМ-порта, или такую полезную штуку как RTLinux Tracer, позволяющую проследить системные и пользовательские события (прерывания, блокировки и т.д.) в режиме реального времени, что может быть полезно для анализа задержек и общей работы системы. В обоих проектах реализованы практически все технологии, о которых я писал, оба позволяют их без проблем использовать на домашнем компьютере без специализированных приложений. Если же разработать приложения, используя новые API, то можно создать на их основе системы, удовлетворяющие условиям "жесткого" реального времени со временем реакции меньше 1 мс.

От теории к практике

Чтобы не казалось, что я зря обо всем этом рассказывал и занимал время и ценное место на страницах журнала, давайте немного протестируем систему - может, это и впрямь не для нас. Так как эта статья появилась в результате моего обещания редактору написать статью об обработке звука в Linux (еще в январе, но проблема насколько интересная, что, если честно, увлекся), то отсюда будем и плясать. В качестве тестовой программы была использована утилита latencytest (http://www.gardena.net/benno/linux/latencytest-0.42-png.tar.gz), которая разрабатывалась как раз для измерения общих задержек во время обработки мультимедиа-данных при различных потрясениях, которые могут возникнуть на обычной десктоп-системе (загрузка процессора вычислениями (жизненный пример - синтезатор, вычисляющий форму волны), большие операции ввода/вывода (запись, копирование, считывание файла 350 Мб), вывод большого количества графики (может привести к блокировке процессора), доступ к системе процессов /proc с обновлениями через 0.01 c).

В ходе тестирования проверялись:

Red Hat Linux 9.0 (см. статью "Дело в шляпе", МК №22 (245)) (Рис. 1) - как пример типичной системы, на которой работают пользователи; все лишние сервисы отключены, ядро использовано установленное по умолчанию с дистрибутивом, т.е. без оптимизации, в качестве оконного менеджера - любимый большинством KDE, включен DMA-режим, рекомендуемый для компьютера, предназначенного для обработки звука, файловая система ext3 (ReiserFS дистрибутивом поддерживается, но программа установки создать его не позволяет);

Рис. 1а. Рис. 1б.

Рис. 1в. Рис. 1г.

CRUX 1.0 (см. статью "Каждому - свой крест", МК №14 (237)) (Рис. 2) - как пример сумасбродного :-) оптимизированного дистрибутива, с откомпилированным оптимизированным ядром 2.4.20 (из комплекта системы), файловая система ext3, но без DMA (как и последующие), оконный менеджер WindowMaker (не самый любимый, но такое было настроение);

Рис. 2а. Рис. 2б.

Рис. 2в. Рис. 2г.

тот же CRUX 1.0 (Рис. 3), но с файловой системой ReiserFS, как пример работы с другой ФС;

Рис. 3а. Рис. 3б.

Рис. 3в. Рис. 3г.

опять тот же CRUX 1.0 (Рис. 4), но на ядро наложен патч 2.4.20-low-latency.patch.gz, взятый с http://www.zip.com.au/~akpm/linux/schedlat.html, файловая система ReiserFS - пример вооруженного до зубов пингвина.

Рис. 4а. Рис. 4б.

Рис. 4в. Рис. 4г.

Остальные варианты, вроде оптимизированного CRUX'a с файловой системой ext3 и включением DMA, или Red Hat с ReiserFS, по результатам сильно не отличаются или просто не показывают общей тенденции, поэтому в итог не включены, чтобы не перегружать лишней информацией. Эксперимент был проведен несколько раз, и были выбраны графики со средним результатом. Тестирование проводилось на компьютере под управлением Celeron 300A (333), 256 Mб ОЗУ, видео ASUS V3000 на чипе Riva 128, звук Creative Live, мышка и коврик удобные. На графиках белая полоса на уровне 5 мс показывает идеальное время задержки, достаточное для того чтобы проиграть один аудиофрагмент (если выше, про MIDI можно просто забыть), при достижении же красной полосы 5.8 мс уже однозначно будут выпадения сигнала. Напомню, что для обработки звука желательно все же иметь не более 2-3 мс. Для удобства восприятия полученная максимальная латентность представлена в таблице. При этом "обычная" латентность процессора равняется 1.16 мс с возможным максимумом 1.42 мс.

Таблица

Из таблицы явно видно, что оптимизация пингвину явно не помешает. Если кто-то серьезно решил заняться обработкой музыки или видео, то, судя по тесту видеоподсистемы, для уменьшения задержек желательно подобрать на замену любимому КDE (с Gnome ситуация такая же) что-нибудь полегче, или на крайний случай пересобрать его из исходников. Интересные результаты показала дисковая подсистема: если на файловой системе ext3 Red Hat превзошел CRUX за счет включенного режима DMA практически в два раза, то простой переход на том же CRUX'e на ReiserFS даже без включения DMA подтвердил, что ext3 (и, конечно же, ext2) свое отслужил, и с ним пора красиво расставаться. Использование патча не позволило перешагнуть желательный 2-мс рубеж, что и подтверждает жизненность того, о чем я тут рассказывал и, судя по цифрам, позволяет считать мою развалюху идеальной системой для профессионального использования (никто почему-то не верит :-)). Кстати, при использовании в патченном варианте файловой системы ext3 двухмиллисекундный рубеж был перейден только в тесте копирования (с результатом 7.7 мс). На таком компьютере, как у меня, чтобы обрабатывать звук (и иметь современную систему вообще), возможно всего три варианта: использовать Windows 98 (двухтысячный тяжеловат), у которого с латентностью проблемы, не говоря уже об общей стабильности системы в целом, попробовать апгрейд для установки более современной версии Windows (вечный процесс!) или попробовать Linux. Когда пару лет назад пришел момент сделать выбор, я остановился на последнем и Рис. 5.не жалею.

На этом, пожалуй, остановимся. По некоторым вопросам, связанным с латентностью и режимом реального времени, можно справиться в Real-time computing FAQ (http://www.faqs.org/faqs/realtime-computing/faq/). Интересно, что глас был услышан, и в следующей серии 2.6. ядро можно будет собрать как preemptible; также будут внесены необходимые изменения, снижающие латентность и оптимизирующие его для работы с мультимедиа-данными, а учитывая, что к тому времени ребята из Namesys подгонят ReiserFS 4, и драйвер ALSA (Advanced Sound Architecture) будет включен в ядро, то... По крайней мере, в последних ядрах серии 2.5.* это все имеется (Рис. 5 - теперь так, скорее всего, будет выглядеть будущая программа конфигурации ядра при make xconfig. Мне не понравилось). А будет товар - будут и приложения, его использующие (хотя, скорее всего, лазейка для проектов вроде RT-Linux все же будет). Поживем - увидим.

P.S. За время написания статьи и одновременной компиляции сразу двух ядер проигрыватель XMMS НИ РАЗУ не заикнулся. И все потому, что разработчики используют вызов SCHED_FIFO. Хитрецы!

P.P.S. Linux forever!





Источник - LinuxBegin.ru
http://linuxbegin.ru

Адрес этой статьи:
http://linuxshop.ru/linuxbegin/article396.html