Linux PREEMPT_RT: SCHED_OTHER работает лучше, чем SCHED_FIFO. Почему?

Я экспериментирую с возможностями 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, &param);

    // Use SCHED_OTHER
    // param.sched_priority = 0;
    // result = sched_setscheduler(pid, SCHED_OTHER, &param);

    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 даст мне более низкое максимальное время цикла и меньшее количество переключений контекста. Почему я получаю эти результаты?

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

sonicwave 14.01.2023 12:11

@sonicwave Программа работает в бесконечном цикле без сна. Гипотеза домашнего хозяйства может быть верной. Если я добавлю сон во внутренний цикл, я получу результаты, аналогичные SCHED_OTHER, но все же немного хуже, чем то, что я получаю с SCHED_OTHER и без сна. Тем не менее, я хотел бы лучше понять, что домашнее хозяйство.

Verax 14.01.2023 12:17

Я помню, у нас были некоторые проблемы с зависанием подсистемы RCU из-за голодания (быстрый поиск показывает access.redhat.com/solutions/2260151, где упоминается возможная причина наличия длительных потоков в реальном времени). Мы исправили это, добавив во внутренний цикл очень короткий сон. Я предполагаю, что в этом случае вы также можете исправить ситуацию, настроив свой приоритет SCHED_FIFO ниже любого приоритета обратных вызовов RCU, установленного в настоящее время - вы просто хотите разрешить ядру запускать свои различные задачи по крайней мере один раз в какое-то время.

sonicwave 14.01.2023 13:54

Кроме того, наше ядро ​​довольно часто жаловалось на это в dmesg — так что, может быть, стоит туда заглянуть?

sonicwave 14.01.2023 13:56
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
4
80
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

У меня есть предположение. Если вы запустите 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 микросекунд более точным ожиданием вращения.

Этот метод также предотвращает превращение ядра реального времени в обогреватель.

Другие вопросы по теме