Я экспериментирую с возможностями Raspberry Pi 3/4 в реальном времени. Я написал следующую программу на C++ для тестирования.
// Compile with:
// g++ realtime_task.cpp -o realtime_task -lrt && sudo setcap CAP_SYS_NICE+ep realtime_task
#include <cstdio>
#include <sched.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstdbool>
#include <chrono>
#include <algorithm>
using namespace std;
using namespace chrono;
using namespace chrono_literals;
int main(int argc, char **argv)
{
// allocate this process to the 4th core (core 3)
pid_t pid = getpid();
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(3, &cpuset);
int result = 0;
result = sched_setaffinity(pid, sizeof(cpu_set_t), &cpuset);
if (result != 0)
{
perror("`sched_setaffinity` failed");
}
struct sched_param param;
// Use SCHED_FIFO
param.sched_priority = 99;
result = sched_setscheduler(pid, SCHED_FIFO, ¶m);
// Use SCHED_OTHER
// param.sched_priority = 0;
// result = sched_setscheduler(pid, SCHED_OTHER, ¶m);
if (result != 0)
{
perror("`sched_setscheduler` failed");
}
uint32_t count = 0;
uint32_t total_loop_time_us = 0;
uint32_t min_loop_time_us = numeric_limits<uint32_t>::max();
uint32_t avg_loop_time_us = 0;
uint32_t max_loop_time_us = 0;
while(true)
{
count++;
auto start = steady_clock::now();
auto loop_time_us = duration_cast<microseconds>(steady_clock::now() - start).count();
while(loop_time_us < 500)
{
loop_time_us = duration_cast<microseconds>(steady_clock::now() - start).count();
}
min_loop_time_us = min((uint32_t)loop_time_us, (uint32_t)min_loop_time_us);
total_loop_time_us += loop_time_us;
avg_loop_time_us = total_loop_time_us / count;
max_loop_time_us = max((uint32_t)loop_time_us, (uint32_t)max_loop_time_us);
if ((count % 1000) == 0)
{
printf("%u %u %u\r", min_loop_time_us, avg_loop_time_us, max_loop_time_us);
fflush(stdout);
}
}
return 0;
}
Я пропатчил ядро патчем PREEMPT_RT. uname
сообщает 5.15.84-v8+ #1613 SMP PREEMPT
и все работает нормально.
Аргументы командной строки ядра должны isolcpus=3 irqaffinity=0-2
изолировать 4-е ядро (ядро 3) и зарезервировать его для программы, указанной выше. Я вижу в htop, что моя программа - единственный процесс, работающий на 4-м ядре (ядро 3).
При использовании политики SCHED_FIFO
она сообщает о следующих минимальном, среднем и максимальном времени цикла...
MIN AVG MAX
500 522 50042
.. и htop сообщает:
CPU▽ CTXT PID USER PRI NI VIRT RES SHR S CPU% MEM% TIME+ Command
3 1 37238 pi RT 0 4672 1264 1108 R 97.5 0.0 0:07.57 ./realtime_task
При использовании политики SCHED_OTHER
она сообщает о следующих минимальном, среднем и максимальном времени цикла...
MIN AVG MAX
500 500 524
.. и htop сообщает:
CPU▽ CTXT PID USER PRI NI VIRT RES SHR S CPU% MEM% TIME+ Command
3 0 36065 pi 20 0 4672 1260 1108 R 100. 0.0 1:30.16 ./realtime_task
Это противоположно тому, что я ожидаю. Я ожидаю, что SCHED_FIFO
даст мне более низкое максимальное время цикла и меньшее количество переключений контекста. Почему я получаю эти результаты?
@sonicwave Программа работает в бесконечном цикле без сна. Гипотеза домашнего хозяйства может быть верной. Если я добавлю сон во внутренний цикл, я получу результаты, аналогичные SCHED_OTHER, но все же немного хуже, чем то, что я получаю с SCHED_OTHER и без сна. Тем не менее, я хотел бы лучше понять, что домашнее хозяйство.
Я помню, у нас были некоторые проблемы с зависанием подсистемы RCU из-за голодания (быстрый поиск показывает access.redhat.com/solutions/2260151, где упоминается возможная причина наличия длительных потоков в реальном времени). Мы исправили это, добавив во внутренний цикл очень короткий сон. Я предполагаю, что в этом случае вы также можете исправить ситуацию, настроив свой приоритет SCHED_FIFO ниже любого приоритета обратных вызовов RCU, установленного в настоящее время - вы просто хотите разрешить ядру запускать свои различные задачи по крайней мере один раз в какое-то время.
Кроме того, наше ядро довольно часто жаловалось на это в dmesg — так что, может быть, стоит туда заглянуть?
У меня есть предположение. Если вы запустите SCHED_OTHER, но установите для своего процесса приоритет, отличный от значения по умолчанию, он будет продвигать этот процесс по сравнению с любым другим процессом по умолчанию. Так что ваша задача, по сути, одна из всех остальных и, следовательно, удерживать процессор еще дольше. Вот что написано в документации: https://man7.org/linux/man-pages/man7/sched.7.html
SCHED_OTHER: планирование разделения времени по умолчанию в Linux. SCHED_OTHER может использоваться только со статическим приоритетом 0 (т. е. потоки в соответствии с политиками реального времени всегда имеют приоритет над SCHED_OTHER процессы). SCHED_OTHER — стандартное разделение времени в Linux. планировщик, предназначенный для всех потоков, не требующих специальные механизмы реального времени.
Проблема оказалась в троттлинге в реальном времени. Когда происходит дросселирование, в выводе dmesg появляется сообщение.
После отключения с помощью echo -1 > /proc/sys/kernel/sched_rt_runtime_us
политика SCHED_FIFO
работала должным образом. Когда стрессорная программа вводится на ядра 0–2, тогда SCHED_FIFO
работает намного лучше, чем SCHED_OTHER
.
Однако есть лучший способ избежать дросселирования в реальном времени, не отключая его для всей системы. Для листинга программы в исходном вопросе измените этот код...
auto loop_time_us = duration_cast<microseconds>(steady_clock::now() - start).count();
while(loop_time_us < 500)
{
loop_time_us = duration_cast<microseconds>(steady_clock::now() - start).count();
}
... к этому коду ...
this_thread::sleep_for(400us);
auto loop_time_us = duration_cast<microseconds>(steady_clock::now() - start).count();
while(loop_time_us < 500)
{
loop_time_us = duration_cast<microseconds>(steady_clock::now() - start).count();
}
Вызов this_thread::sleep_for
предотвратит использование процессом всего отведенного времени в /proc/sys/kernel/sched_rt_period_us
и, таким образом, предотвратит регулирование в реальном времени. Поскольку sleep_for
не очень точен, вы просто не спите полные 500 микросекунд и используете цикл while(loop_time_us < 500)
, чтобы заполнить оставшиеся 100 микросекунд более точным ожиданием вращения.
Этот метод также предотвращает превращение ядра реального времени в обогреватель.
Ваша программа работает в бесконечном цикле на ядре или регулярно спит? (т.е. за 1 мс каждую 1 с). Насколько я понимаю, Linux необходимо выполнять некоторую работу по обслуживанию каждого ядра (в том числе изолированного) в фоновом режиме, поэтому программы должны позволять этому происходить.