Может ли ядро Linux при всей своей гибкости обеспечивать гарантированное время отклика при работе с приложениями?
Ядро Linux является универсальным и приспособлено к работе как с крошечными встраиваемыми устройствами, так и с титаническими серверами… а также со всем спектром машин между этими крайностями! Но может ли такое поразительно адаптивное ядро обеспечить гарантированную скорость отклика для приложения, работающего на всех этих платформах? Если в вашем приложении допустимая задержка при отклике укладывается в 200 микросекунд — то уверенно отвечаем на этот вопрос «да»! (Кстати, для Linux такая планка совсем не высока, но, чтобы её держать, потребуется тщательно подбирать аппаратное обеспечение и, возможно, обратиться за консультацией к специалисту по системам Linux, работающим в режиме реального времени).
Итак, почему же в приложении, работающем под Linux, иногда могут возникать задержки свыше 200 микросекунд? Универсальность ядра Linux требует сбалансировать пропускную способность, время отклика и честность распределения процессорной мощности, чтобы соответствовать требованиям такой универсальности. Если по одному из этих аспектов предъявляются жёсткие требования, то необходимо тонко настраивать как само ядро, так и поведение приложения. В этом посте рассмотрим 10 основных пунктов, которые необходимо учитывать при разработке системы Linux, к которой предъявляются строгие требования по работе в режиме реального времени. По каждому пункту также упомяну, в каком аспекте легко засыпаться разработчику-новичку, только приступающему к программированию систем реального времени под Linux.
❯ 1. Политики и приоритеты при планировании
Если к решаемой задаче предъявляются строгие требования по отклику, то этой задаче нужно правильно присвоить приоритет и подобрать для неё политику планирования. Для этого подойдёт инструмент chrt(1)
или функция sched_setscheduler(2)
. Именно для систем реального времени, как правило, выбирается политика SCHED_FIFO
. Приоритеты обслуживания в режиме реального времени (от 1=низкий до 98=высокий) подбираются в зависимости от того, какие требования предъявляются к данной конкретной задаче и к другим задачам, выполняемым в режиме реального времени. Задачи с более высоким приоритетом будут вытеснять задачи с более низким.
Опасность: Обязательно отключите функцию пропуска тактов в реальном времени (для этого запишите -1
в /proc/sys/kernel/sched_rt_runtime_us
). Пропуск тактов нарушает работу систем реального времени, так как провоцирует сценарии с недопустимой инверсией приоритетов.
Если хотите подробнее разобраться в этой теме, обязательно почитайте обзор планирования на странице man-справки для функции sched(7)
.
❯ 2. Изоляция
Во многих системах одновременно выполняются не одна, а несколько задач реального времени. Если эти задачи назначены одному и тому же ядру процессора, то задержка при отклике может чрезмерно увеличиться для каждой. Этой проблемы можно избежать, если задавать задачи реального времени для чётко разделённых групп ядер процессора. Для этого можно применить инструмент taskset(1)
или функцию sched_setaffinity(2)
.
Часто бывает так, что задачи реального времени дожидаются определённых прерываний. Прерывание может сработать на одном ядре, а задача реального времени будет назначена на другое ядро — и это также может быть источником задержки. Чтобы справиться с этой проблемой, можно ограничить аппаратные прерывания, назначая их только на то же самое ядро, на котором выполняется и задача реального времени, зависящая от этого прерывания. Маску сходства для аппаратных прерываний на ядре ЦП можно задать в виртуальном файле smp_affinity
, находящемся в /proc/irq/IRQ-NUMBER/
.
Наконец, бывает желательно специально выделить часть ядер ЦП именно для обработки задач реального времени. В параметре загрузки isolcpus
ядру сообщается, какие ядра ЦП при загрузке следует исключить из заданных по умолчанию масок сходства ЦП, действующих в системе. Пользуясь упомянутыми выше инструментами и интерфейсами, можно специально выделить конкретные ядра ЦП для работы с задачами и прерываниями реального времени.
Опасность: не на любом железе поддерживается возможность произвольно назначать ядра ЦП для прерываний. Обязательно проверьте файл effective_affinity
и посмотрите, какая настройка там выставлена.
❯ 3. Отказы страниц
Одна из наиболее разрушительных ситуаций при работе с приложениями реального времени возникает при необходимости присвоить или «подкачать» память. Это может быть связано с тем, что данная система Linux настроена на «запрос с запасом» при первом обращении к выделенной или зарезервированной памяти. Либо ситуация может быть связана с подкачкой с диска определённых данных (например, текстовых сегментов), когда впервые вызывается та или иная функция. Независимо от причин, таких ситуаций необходимо избегать, чтобы выполнять требования к отклику.
При работе с приложением реального времени первым делом нужно сконфигурировать библиотеку glibc
так, чтобы при обращении с этим приложением использовалась всего одна неуменьшаемая куча. Так вы гарантируете, что у вас под рукой будет пул готовой к использованию физической оперативной памяти, которую можно предоставить приложению реального времени. Это делается при помощи функции mallopt(3) (M_MMAP_MAX=0
, M_ARENA_MAX=1
, M_TRIM_THRESHOLD=-1)
.
Кроме того, вся выделенная и отображённая виртуальная память должна быть присвоена физической RAM и закреплена, так, чтобы её нельзя было переназначить на другие цели. Это делается при помощи функции mlockall(2) (MCL_CURRENT | MCL_FUTURE)
.
Наконец, поговорим о том, какое количество памяти в куче и в стеке понадобится на срок жизни приложения реального времени — чтобы вовремя срабатывали операции её передачи из стека и кучи в физическую RAM. Такая практика называется «pre-faulting»; обычно для этого заполняется памятью большой буфер в кадре стека, а также выделяется, заполняется и высвобождается большой буфер в куче.
Опасность: не забывайте, что у каждого потока — собственный стек.
❯ 4. Синхронизация
Приложениям реального времени часто нужен доступ к разделяемым ресурсам (например, к данным, находящимся в разделяемой памяти), а такой доступ требует синхронизации. Для этого используйте pthread_mutex
. Это единственный объект, обеспечивающий блокировку и при этом обладающий семантикой владения и динамического изменения приоритетов — что критически важно для недопущения инверсии приоритетов.
К сожалению, динамическое изменение приоритетов по умолчанию не активировано. Чтобы его включить, воспользуйтесь функцией pthread_mutexattr_setprotocol(3) (PTHREAD_PRIO_INHERIT)
. Когда оно включено, владелец с более низким приоритетом получает его повышение до того уровня, на котором приоритет у ожидающего. Это нужно для того, чтобы оспариваемый мьютекс мог быть передан как можно быстрее.
Опасность: с осторожностью пользуйтесь библиотеками, в которых могут применяться собственные мьютексы или другие механизмы синхронизации.
❯ 5. Уведомления при включённой синхронизации
При работе с приложениями реального времени часто приходится дожидаться события, а затем, пробудившись после него, обращаться к разделяемому ресурсу. Такой паттерн обслуживается при помощи условной переменной pthread_cond
. Это ожидающий объект, ассоциированный с мьютексом. Он позволяет ядру при необходимости повышать приоритет, если пробуждаются высокоприоритетные задачи.
Опасность: важно, чтобы уведомляющая задача сначала выполняла уведомление, а только потом высвобождала связанный с ней мьютекс. Так обеспечивается необходимый спор за блокировку, позволяющий избежать сценариев с инверсией приоритетов.
❯ 6. Циклические задачи
При решении циклических задач важно использовать выделенные потоки, спящие до наступления абсолютного времени, в которое они должны выполнить свою работу. Для этого лучше всего подходит функция clock_nanosleep(2)
— единственный API, гарантирующий, что задача реального времени будет разбужена по сигналу таймера высокого разрешения, срабатывающего в контексте аппаратного прерывания.
Опасность: При помощи CLOCK_MONOTONIC
и TIMER_ABSTIME
убедитесь, что задача спит. В противном случае цикл может привести к изменчивости или сместиться по времени.
❯ 7. Конфигурация ядра
В ядре Linux поддерживаются различные модели вытеснения. Для того, чтобы добиться минимальных задержек (в пределах миллисекунды) необходимо использовать модель «полностью вытесняемого ядра» (для работы в реальном времени)» (CONFIG_PREEMPT_RT)
. Так обеспечивается соответствие основным требованиям, в частности, тонкая детализация прерываний, детерминизм и расстановка приоритетов для прерываний. Если такая модель недоступна, это значит, что к ядру необходимо применить серию патчей PREEMPT_RT
.
Опасность: позаботьтесь о том, чтобы правильно сконфигурировать и другие фичи, в частности, масштабирование частоты ЦП, так как при неверной конфигурации возможна изменчивость в аппаратной производительности.
❯ 8. Тестирование
Подготовив систему реального времени к работе, важно определить, каковы максимальные задержки отклика при разных настройках производительности. Так вы сможете в нужной степени понять разные типы задержек, которые могут возникать в разных компонентах системы. Эту информацию отлично предоставляет инструмент cyclictest(8)
, который при этом почти не вмешивается в работу системы и не препятствует функционированию приложений реального времени.
Как правило, чтобы выявить наихудшие варианты задержек, следует прогонять в фоновом режиме различные стресс-тесты. В системе реального времени такой стресс-тест, который не является операцией реального времени, должен минимально влиять на само приложение. Здесь вам могут пригодиться такие инструменты как
hackbench(8)
и stress-ng(1)
. Также важно убедиться, что стресс-тест охватывает все аппаратные компоненты. Цель тестирования такого рода — найти самые тяжёлые пути выполнения в ядре, такие, которые не поддаются вытеснению.
Опасность: обязательно протестируйте задержки и в таких сценариях, когда система работает полностью вхолостую (idle)
. В зависимости от того, как сконфигурировано ядро, именно такой случай может оказаться наихудшим.
❯ 9. Верификация
Даже если кажется, что система реального времени работает хорошо, и все требования по хронометражу соблюдаются, важно удостовериться, что приложение реального времени и работает именно так, как должно. Повышается ли приоритет при споре за блокировку? Является ли clock_nanosleep()
единственным API для работы над циклическими задачами? Происходят ли отказы страниц? Какова задержка при отклике в наихудшем случае, действительно встречающемся в данном приложении реального времени?
Ответы на все эти вопросы можно получить при помощи различных инструментов ядра, в частности, ftrace (соответствующие вызовы: trace-cmd(1)
, kernelshark(1))
, perf(1)
и eBPF
(соответствующий вызов: bpftrace(8))
. Эти инструменты обеспечивают live-трассировку, профилирование и измерение практически любых событий, происходящих в системе. Не пожалейте времени и научитесь эффективно пользоваться этими инструментами, поскольку они помогут вам убедиться, что система работает именно так, как задумывалось.
Опасность: ftrace
, perf
и eBPF
очень эффективны. Однако, как и при работе с любыми измерительными инструментами, важно понимать, как именно акт измерения влияет на всю систему.
❯ 10. Внешние помехи
Также важно понимать и чисто аппаратные «фичи» и ограничения, пусть они обычно и не связаны с Linux. Известные примеры такого рода — прерывания для системного управления (SMI), конфликты за шину памяти/полосу передачи данных, топология ЦП и совместное использование кэша разными ядрами ЦП. Понимая, какое негативное влияние на системы реального времени могут оказывать аппаратные компоненты, можно спроектировать и реализовать программы именно так, чтобы избежать этих проблем или преодолеть их обходными путями.
Опасность: часто оборудование подбирается без учёта требований, предъявляемых к системам реального времени, и в результате приходится большим трудом обходить возникающие проблемы уже при программировании софта. Если в подборе оборудования участвуют как программисты, так и инженеры-железячники, то можно создать такую систему реального времени, которая в долгосрочной перспективе окажется исключительно полезной.
❯ И напоследок...
Этот список нельзя назвать исчерпывающим, но перечисленные здесь 10 главных пунктов достаточно информативны и позволяют спроектировать систему, в худшем случае дающую задержку не более чем в 200 микросекунд — о чём я упоминал в начале этой статьи. На самом деле, задержку можно снизить ещё сильнее, и для этого требуется тонко настраивать Linux, тщательно подбирать оборудование и работать над проектированием, реализацией и сборкой приложения для работы в режиме реального времени.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
Комментарии (6)
notwithstanding
30.07.2025 18:01>допустимая задержка при отклике укладывается в 200 микросекунд
Получается, допустимая задержка – не больше 200 мс.
Apoheliy
30.07.2025 18:01Перевод: При помощи
CLOCK_MONOTONIC
иTIMER_ABSTIME
убедитесь, что задача спит.Оригинал: Make sure the task sleeps with CLOCK_MONOTONIC and TIMER_ABSTIME.
Предложение: Для функций засыпания задачи используйте
CLOCK_MONOTONIC и TIMER_ABSTIME.
Прим.: CLOCK_MONOTONIC - это тип часов.
TIMER_ABSTIME - это способ подсчёта времени.---
Pitfall -> Опасность ? Может: "Обратите внимание" ?
MicrofCorp
"возможно, обратиться ха консультацией к специалисту по системам" - опечатка: вместо "за", написано "ха"
Albert_Wesker Автор
Спасибо, поправил