Программирование ARM RT-Thread: управление потоками Mon, February 26 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

RT-Thread: управление потоками Печать
Добавил(а) microsin   

Когда мы сталкиваемся с большой задачей в повседневной жизни, то обычно стараемся поделить её на несколько мелких, понятных и более простых задач. Затем можно решать эти задачи постепенно, одну за одной, пока вся общая большая задача не будет выполнена. В многопоточной операционной системе разработчикам также нужно поделить свое сложное приложение на несколько небольших частей, с планируемым и прогнозируемым поведением. Когда общее приложение обоснованно поделено на правильно выполняемые задачи, дизайн системы будет удовлетворять по емкости и времени реакции требованиям операционной системы реального времени (real-time operation system, RTOS).

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

RT Thread task switching fig01

Рис. 1. Внутренняя структура упрощенной системы, в которой работают две подзадачи.

В RT-Thread объект приложения, соответствующей каждой такой подзадаче, называется потоком (thread). Поток является носителем одной из задач, и он является базовой планируемой единице в RT-Thread. Здесь термин "планируемой" означает, что потоку выделяется процессорное время в соответствии с его приоритетом и приоритетом других задач. В потоке описан алгоритм задачи и среда его выполнения. Когда поток активен, он может работать со своей рабочей средой выполнения (environment, или контекст). К контексту относится, в частности, каждая локальная переменная потока, данные, стеки, содержимое регистров, информация о памяти и т. п. В зависимости от установленного для потока приоритета он может получить больше или меньше свободного процессорного времени.

В этой статье (перевод документации [1]) приведена общая вводная информация по API для работы потоками (thread management) в RT-Thread. После прочтения вы получите более глубокое понимание механизма управления потоками в RT-Thread. Здесь рассмотрены состояния потоков и такие вопросы, как создание потоков, заем нужен поток ожидания (idle thread) и т. д.

[Функции Thread Management. Принципы многозадачности RT-Thread]

Основное назначение для RT-Thread thread management - управлять потоками и планировать из выполнение. В системе существует 2 типа потоков: system thread и user thread. System thread это поток, который создает ядро системы (RT-Thread kernel). User thread это поток, который создан приложением. Оба типа потока создадут объект потока (thread object) в контейнере ядра (kernel object container). Когда поток удаляется, он также будет удален из этого контейнера. Как показано на следующем рисунке, у каждого потока есть несколько важных атрибутов, такие как блок управления (Thread Control Block, TCB), стек (thread stack), функция входа (entry function, или тело потока), и т. д.

RT Thread object container fig02

Рис. 2. RT-Thread kernel Object Container.

Планировщик RT-Thread (thread scheduler) работает по принципу вытесняющей многозадачности (preemptive sheduler), и его основная цель - вовремя найти задачу с самым высоким в настоящий момент приоритетом и предоставить ему процессорное время за счет приостановки других (менее приоритетных, или с таким же приоритетом) задач. Задача с самым высоким приоритетом становится готовой к запуску (переходит в состояние Ready), и всегда может получить право на обладание временем CPU.

Когда текущий работающий поток в состоянии Ready вступает в конкуренцию за процессорное время с более высокоприоритетным потоком, планировщик приостанавливает этот работающий поток (вытеснение) и немедленно предоставляет CPU для более приоритетного потока.

Если обработчик прерывания (interrupt service routine, ISR) создает условия для перехода в состояние Ready более приоритетного потока, чем выполняющийся текущий поток, то код ISR завершается, планировщиком прерванный поток приостанавливается, и начинает выполняться поток с более высоким приоритетом.

Когда планировщик планирует запуск каждого потока и производит их переключение, сначала сохраняется контекст (локальные переменные, содержимое регистров, место в коде, где поток был приостановлен, и т. п.) текущего выполняющегося потока. Когда этому потоку возвращается управление, планировщик восстанавливает его контекст.

Thread Control Block. В RT-Thread данные управления потоком (thread control block, TCB) представлены в структуре rt_thread, данные которой используются операционной системой для управления потоками. В структуре rt_thread сохраняется информация о потоке, такая как приоритет (priority), имя потока (может использоваться для отладки), статус потока, и т. д. В rt_thread также включена структура связанного списка для соединения потоков, коллекция событий для блокировки потоков на событии (event collection of thread waiting) и т. п. Структура rt_thread определена следующим образом:

/* Thread Control Block */
struct rt_thread
{
    /* rt object */
    char        name[RT_NAME_MAX];          /**< имя потока */
    rt_uint8_t  type;                       /**< тип объекта */
    rt_uint8_t  flags;                      /**< флаги потока */
 
#ifdef RT_USING_MODULE
    void       *module_id;                  /**< ID модуля приложения */
#endif
 
    rt_list_t   list;                       /**< список объектов */
    rt_list_t   tlist;                      /**< список потоков */
 
    /* stack point and entry */
    void       *sp;                         /**< Stack Point, указатель стека */
    void       *entry;                      /**< указатель на точку входа
                                                (функцию потока) */
    void       *parameter;                  /**< параметр, переданный
                                                 в функцию потока */
    void       *stack_addr;                 /**< адрес стека в памяти */
    rt_uint32_t stack_size;                 /**< размер стека */
 
    /* error code */
    rt_err_t    error;                      /**< код ошибки */
    rt_uint8_t  stat;                       /**< код состояния потока */
 
    /* priority */
    rt_uint8_t  current_priority;           /**< текущий приоритет */
    rt_uint8_t  init_priority;              /**< начальный приоритет */
#if RT_THREAD_PRIORITY_MAX > 32
    rt_uint8_t  number;
    rt_uint8_t  high_mask;
#endif
    rt_uint32_t number_mask;
 
#if defined(RT_USING_EVENT)
    /* thread event */
    rt_uint32_t event_set;
    rt_uint8_t  event_info;
#endif
 
#if defined(RT_USING_SIGNALS)
    rt_sigset_t     sig_pending;            /**< ожидающие обработки сигналы */
    rt_sigset_t     sig_mask;               /**< биты маски сигналов */
 
    void            *sig_ret;               /**< указатель стека возврата из сигнала */
    rt_sighandler_t *sig_vectors;           /**< вектора обработчиков сигналов */
    void            *si_list;               /**< список информации о сигналах */
#endif
 
    rt_ubase_t  init_tick;                  /**< тик инициализации потока */
    rt_ubase_t  remaining_tick;             /**< осталось тиков */
 
    struct rt_timer thread_timer;           /**< встроенный таймер потока */
 
    void (*cleanup)(struct rt_thread *tid); /**< функция очистки при завершении потока */
 
    rt_uint32_t user_data;                  /**< приватные данные пользователя,
                                                 относящиеся к потоку */
};
typedef struct rt_thread *rt_thread_t;

Поле init_priority хранит начальный приоритет потока, который он получил при создании, и значение приоритета не изменится в течение всего времени жизни потока (пока пользователь на запустит функцию, которая позволяет вручную поменять приоритет). Функция cleanup будет вызвана из потока ожидания (idle thread), когда поток выйдет из своей функции. Последнее поле user_data может быть заполнено пользователем для потока, и здесь могут храниться любые данные, предназначенные только для этого потока.

Thread Stack. У каждого потока RT-Thread имеется независимый стек (Thread Stack), пространство под который выделяется при создании потока. Когда поток вытесняется, его контекст сохраняется в этом стеке планировщиком. Когда поток снова получает управление, его контекст планировщик восстановит из стека.

Стек потока также используется для хранения локальных переменных в функциях, т. е. локальные переменные функций, которые вызвал поток, сохраняются в стеке этого потока вместе с адресом возврата из функции. Локальные переменные в функциях также могут сохранятся в регистрах CPU (как принято в архитектуре ARM), и когда эта функция вызывается из другой функции, эти локальные переменные также сохраняются в стеке текущего потока.

Для первого запуска потока его контекст может быть создан вручную, чтобы установить начальные значения окружения, такие как точка входа (entry function, регистр PC), передаваемый на входе параметр (entry parameter, регистр R0), адрес возврата (return position, регистр LR), текущий статус CPU (machine operating status, регистр CPSR).

Направление заполнения стека в памяти зависит от архитектуры процессора. Версии RT-Thread до 3.1.0 позволяли стеку расти только от старшим адресам к младшим. Для архитектуры ARM Cortex-M стек потока может быть сконструирован, как показано на рисунке ниже.

RT Thread thread stack fig03

Рис. 3. Организация стека ARM Cortex-M.

При настройке размера стека создаваемого потока учитывают объем свободной памяти используемого CPU/MCU и прогнозируемое количество вложенных вызовов функций и ISR. Многие модели MCU имеют ограниченное пространство RAM, которое можно выделить под стеки потоков. Изначально можно установить при отладке размер стека 1K или 2K байт, и потом в консоли отладки FinSH [3] использовать команду list_thread (или ps) для получения информации о расходе памяти стека работающих потоков. Эта команда покажет максимальное использование (в процентах) стека потоков, и по этой информации объем стека для каждого потока можно впоследствии подстроить в большую или меньшую сторону. Пример вывода команды ps, столбец "max used" показывает максимальное зарегистрированное использование стека потоком:

thread   pri  status   sp_addr       sp     stack size  max used left tick  error
-------- ---  ------- ---------- ---------- ----------  -------- ---------- -----
ble        4  suspend 0x0041f03c 0x0041ff70 0x00001000    12%    0x00000005 000
core_thr   2  suspend 0x0041da1c 0x0041e140 0x00000800    10%    0x00000003 000
wpas_thr   5  suspend 0x0041bf88 0x0041ce90 0x00001000    06%    0x00000005 000
kmsgbk     3  suspend 0x00419ef4 0x0041ae68 0x00001000    03%    0x00000005 000
tshell    20  ready   0x00417db4 0x00419c40 0x00002000    06%    0x00000008 000
ntp_sync  26  suspend 0x0041709c 0x00417618 0x00000600    27%    0x00000001 000
tcpip      4  suspend 0x00415990 0x004160b0 0x00000800    10%    0x0000000f 000
temp_det   5  suspend 0x00415240 0x00415568 0x00000400    29%    0x00000005 000
tidle     31  ready   0x003f97dc 0x003f9960 0x00000200    24%    0x0000000c 000
timer      6  suspend 0x003f9bb8 0x003fab38 0x00001000    05%    0x00000008 000
mythread   2  suspend 0x00413eb0 0x00414e28 0x00001000    03%    0x00000011 000

Thread State. В любой момент времени на одном процессорном ядре может работать только один поток, все остальные будут находиться в приостановленном состоянии. Различают несколько возможных состояний, относящихся к процессу выполнения потоком: начальное состояние (initial state), приостановка (suspended state), готовность к запуску (ready state), и т. д. В RT-Thread у потока могут быть 5 состояний, и операционная система (планировщик) будет автоматически менять это состояние в зависимости от текущих условий.

5 состояний потока в RT-Thread показаны в следующей таблице:

Состояние Описание
Initial State Начальное состояние потока. В нем он находится, когда только что был создан, но еще не запущен. В начальном состоянии поток не участвует в работе планировщика. В RT-Thread это состояние определено макросом RT_THREAD_INIT.
Ready State Состояние готовности. В этом состоянии поток ставится в очередь на выполнение наряду с другими задачами. В соответствии с приоритетом потока он находится в ожидании получения процессорного времени. Как только CPU снова станет доступным после приостановки текущего работающего потока, планировщик немедленно найдет тот поток в состоянии Ready, который имеет самый высокий приоритет, и запустит его (переведет в состояние Running). Если в состоянии готовности находится несколько потоков с одинаковым приоритетом, то планировщик будет их запускать по очереди. В RT-Thread состояние Ready определено макросом RT_THREAD_READY.
Running State Рабочее состояние потока, его код выполняется. В одноядерной системе в любой момент времени работает только один поток, дескриптор которого вернет функция rt_thread_self. В многоядерной системе может одновременно находиться в состоянии Running несколько потоков. В RT-Thread состояние Running определено макросом RT_THREAD_RUNNING.
Suspended State Это состояние известно также как состояние блокировки, Blocking State. Поток может быть приостановлен (заблокирован) в ожидании освобождения какого-нибудь ресурса, или же поток может быть произвольно заблокирован на некоторое время (например, для формирования задержки в алгоритме). Потоки могут быть заблокированы на объектах семафора, очереди мютекса. В состоянии остановки поток не участвует в конкуренции за процессорное время наряду с другими потоками, он ждет истечения определенного таймаута, либо наступления какого-нибудь события (например публикации семафора или появления данных в очереди). В RT-Thread состояние Suspended определено макросом RT_THREAD_SUSPEND.
Closed State Поток закрыт, в это состояние он попадает, когда завершает свою работу (производит выход из своей функции). Поток в закрытом состоянии не участвует в планировании запуска задач. В RT-Thread состояние Closed определено макросом RT_THREAD_CLOSE.

Thread Priority. У каждого потока в RT-Thread имеется приоритет, согласно которого планировщик распределяет процессорное время между потоками. Чем приоритет у потока выше, тем у него больше возможности перейти в состояние Running.

RT-Thread поддерживает максимум 256 уровней приоритета потоков (0 .. 255). Чем меньше число, обозначающее приоритет, тем выше приоритет, и соответственно 0 обозначает самый наивысший приоритет. Такое количество доступных приоритетов необходимо далеко не всегда. В некоторых системах, где не так много ресурсов, вы можете конфигурацию системы так, что будет поддерживаться только 8 или 32 уровня приоритета, в зависимости от требований текущей ситуации. Для MCU семейств ARM Cortex-M обычно используется 32 уровня приоритета. По умолчанию самый низкий приоритет назначается специальной задаче ожидания (idle thread), которая является системной и обычно никак не применяется в приложении (хотя для специальных целей может быть назначена функция обработчика, вызываемая из idle thread).

Time Slice. У каждого потока есть параметр выделенного кванта процессорного времени (time slice), однако time slice имеет значение только для состояния Ready потоков с одинаковым приоритетом. Планировщик использует слайсинг для переключения контекста между потоками с одинаковым приоритетом, используя метод циклического вытеснения, предоставляя ресурс CPU потокам по очереди. В этом случае time slice играет роль ограничения продолжительности состояния Running потока, и длительность этого состояния определяется периодом тика системы (OS Tick).

Предположим, что в определенный момент времени 2 потока A и B с одинаковым приоритетом находятся в состоянии Ready. Time slice потока A установлено в 10, и time slice потока B установлено в 5. Когда в системе нет в состоянии Ready никаких других потоков с более высоким приоритетом, чем у A и B, планировщик будет поочередно запускать то поток A, то поток B. Поток A будет при этом работать в течение 10 тиков системы, а поток B в течение 5 тиков (см. рис. 4).

RT Thread time slice fig04

Рис. 4. Распределение процессорного времени между потоками с одинаковым приоритетом.

Thread Entry Function. "Entry" в информации TCB обозначает точку входа в поток, или адрес функции потока, где описан весь алгоритм работы потока. Функция потока полностью разрабатывается пользователем системы. Обычно используется 2 варианта режима работы кода для функции потока: с бесконечным циклом (Infinite Loop Mode) и или с последовательным описанием действий и последующим завершением (Sequential Execution Mode, или Finite-Cycle Mode).

Infinite Loop Mode. Функция с бесконечным циклом, это наиболее часто используемый режим работы потока. В операционных системах реального времени (RTOS) потоки обычно пассивны, и большинство времени проводят в заблокированном состоянии, ожидая наступления какого-либо события. Это может быть как внешнее событие (например, поступление данных от последовательного порта, или нажатие пользователем на кнопку), так и внутреннее (истечение таймаута задержки). При наступлении события поток просыпается выполняет некие действия, после чего снова блокируется в ожидании события. В этом режиме функция потока может выглядеть следующим образом:

void thread_entry(void* paramenter)
{
   // В этом месте может быть первоначальная инициализация.
   ...
   
   // Вход в бесконечный цикл:
   while (1)
   {
      /* Ожидание возникновения какого-либо события.
         Это может быть блокировка на очереди или
         семафоре, с таймаутом или без него. */
 
      /* Обработка события */
   }
}

На первый взгляд у потоков нет ограничений на выполнение всех операций, которые могут быть выполнены. Однако в таком случае, когда для потоков RTOS явно назначены приоритеты, если поток с самым высоким приоритетом зависнет в бесконечном цикле, то у потоков с приоритетом ниже не будет никаких шансов запуститься, чтобы выполнить свою работу. Поэтому в программе, разработанной на основе RTOS, не должно возникать ситуации, когда потоки всегда находятся в состоянии Running, и должно существовать какое-то действие в коде потока, которое заставляет его блокироваться, т. е. отказаться от потребления ресурса CPU - например блокироваться на ожидании события, или приостанавливаться с помощью вызова функции задержки. Тогда планировщик может предоставить освободившееся процессорное время другим потокам. Цель создания функции потока с бесконечным циклом состоит в том, чтобы позволить потоку периодически запускать свои действия и никогда при этом не завершаться.

Sequential Execution, или Finite-Cycle Mode. Это функция без бесконечного цикла, описывающая последовательный набор действий. Такие потоки могут быть описаны как "однократные", после выполнения своих действий они будут автоматически уделены системой. Ресурсы, выделенные для такого потока, освобождаются сборщиком мусора, вызываемого из потока ожидания.

static void thread_entry(void* parameter)
{
   /* Обработка транзакции #1 */
   ...
 
   /* Обработка транзакции #2 */
   ...
 
   /* Обработка транзакции #3 */
   ...
}

Thread Error Code. Один поток представляет собой один из выполняемых сценариев (алгоритмов) приложения. Код ошибки непосредственно привязан к окружению выполнения (execution environment), так что каждый поток снабжен специальной переменной, содержащей код ошибки потока (thread error code). Код ошибки включает следующие варианты ошибок:

#define RT_EOK      0 /* No error, отсутствие ошибки                          */
#define RT_ERROR    1 /* Regular error, общая или неопределенная ошибка       */
#define RT_ETIMEOUT 2 /* Timeout error, таймаут                               */
#define RT_EFULL    3 /* Resource is full, переполнение ресурса               */
#define RT_EEMPTY   4 /* No resource, отсутствие ресурса                      */
#define RT_ENOMEM   5 /* No memory, нехватка памяти                           */
#define RT_ENOSYS   6 /* System does not support, система это не поддерживает */
#define RT_EBUSY    7 /* System busy, система занята                          */
#define RT_EIO      8 /* IO error, ошибка ввода/вывода                        */
#define RT_EINTR    9 /* Interrupt system call, системный вызов прерывания    */
#define RT_EINVAL  10 /* Invalid Parameter, недопустимый параметр             */

[Переключение между состояниями потока]

RT-Thread предоставляет набор системных функций, реализующие переключение между 5 состояниями потоков. Взаимосвязь между состояниями потоков показана на следующем рисунке:

RT Thread thread states fig05

Рис. 5. Диаграмма переходов между состояниями потока.

Поток входит в начальное состояние (RT_THREAD_INIT) при вызове функций rt_thread_create/rt_thread_init. Из начального состояния поток перейдет в состояние готовности (RT_THREAD_READY) вызовом функции rt_thread_startup. Когда поток находится в состоянии готовности, планировщик может перевести его в рабочее состояние (RT_THREAD_RUNNING). Когда из тела потока вызывается одна из функций rt_thread_delay, rt_sem_take, rt_mutex_take, rt_mb_recv, или когда для него больше нет ресурса CPU, поток переходит в состояние приостановки (RT_THREAD_SUSPEND). Если потоки в состоянии приостановки при ожидании таймаута не получили ресурс CPU, или когда другие потоки освободили CPU, то поток вернется в состояние Ready.

Если поток находится в состоянии приостановки после вызова функций rt_thread_delete/rt_thread_detach, то он будет переведен в состояние закрытия (RT_THREAD_CLOSE). Для потока в рабочем состоянии (RT_THREAD_RUNNING), если его работа завершена, то будет выполнена функция rt_thread_exit как последнее действие потока для его перехода в закрытое состояние (RT_THREAD_CLOSE).

Примечание: в RT-Thread у потока фактически нет состояния Running, его эквивалентом является состояние Ready.

[Системный поток]

Как уже упоминалось, системный поток (system thread) это поток, который создается кодом системы, и поток пользователя (user thread) это соответственно тот поток, который был создан вызовами функциями API-интерфейса управления потоками (thread management). К системным потокам в ядре RT-Thread относятся задача ожидания (idle thread) и основной поток (main thread).

Idle Thread. Поток ожидания создается системой с самым низким приоритетом, и он всегда находится в состоянии Ready. Когда в системе нет других потоков в состоянии Ready, планировщик предоставляет время CPU потоку ожидания, в котором содержится бесконечный цикл, который никогда не приостанавливается. Дополнительно поток ожидания обслуживает специальные функции в RT-Thread.

Если какой-либо поток завершается, то система должна его удалить и освободить соответствующие ресурсы. При этом автоматически вызывается функция rt_thread_exit, где сначала поток удаляется из системной очереди готовности (system ready queue), затем состояние потока меняется на Closed - это означает, что поток больше не участвует в работе планировщика. После этого ресурсы потока переводятся в очередь дисфункции (rt_thread_defunct queue, это очередь потоков, которые находятся в закрытом состоянии, и ресурсы их еще не освобождены). Поток ожидания как раз занимается освобождением ресурсов закрытых потоков и удалением потока.

Также поток ожидания предоставляет интерфейс для запуска из своего тела hook-функции, назначенной пользователем. Это обработчик, который вызывается при активности потока ожидания, что подходит для таких операций, как управление энергопотреблением, учет статистики процессорного времени (такой, какую отображает команда ps консоли FinSH), перезапуск watchdog, и т. п.

Main Thread. Когда система запускается, она создает главный поток (main thread). Функция этого потока называется main_thread_entry. Из главного потока запускается функция входа в приложение пользователя main (application entry function). После запуска планировщика системы планировщик запустит main thread. Пользователи могут добавить код своего приложения в реализацию функции main.

/* Основная функция системы, системный поток */
void main_thread_entry(void *parameter)
{
   /* Инициализация компонентов RT-Thread */
   rt_components_init();
   /* Запуск функции main системы */
#if defined (__CC_ARM)
   extern int $Super$$main(void);
   $Super$$main(); /* Для компилятора ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
   extern int main(void);
   main();
#endif
}

RT Thread main thread fig06

Рис. 6. Процесс запуска системы.

[API управления потоками]

На следующей картинке показаны операции с потоками, такие как создание/инициализация, запуск, удаление/отключение. Вы можете использовать rt_thread_create для создания динамического потока (dynamic thread), и rt_thread_init для инициализации статического потока. Различие между динамическим и статическим потоками в том, что для динамического потока система автоматически выделяет пространство для стека и TCB из памяти кучи (heap, динамически выделяемая память), а для статического потока память выделяется программистом. Таким образом, динамический поток создается во время работы приложения (runtime), и он может быть создан только после того, как была создана куча системы. Память для статического потока выделяется в момент компиляции приложения, память под стек потока и его дескриптор выделяется программистом при написании кода приложения.

RT Thread thread ops fig07

Рис. 7. Операции над потоками и соответствующие API-функции RT-Thread.

Создание и удаление потока. Чтобы стать исполняемым объектом, поток должен быть создан ядром операционной системы. Динамический поток вы можете создать следующей функцией:

rt_thread_t rt_thread_create(const char* name,
                            void (*entry)(void* parameter),
                            void* parameter,
                            rt_uint32_t stack_size,
                            rt_uint8_t priority,
                            rt_uint32_t tick);

Когда вызывается эта функция, система выделит дескриптор потока (thread handle) из динамической памяти (дескриптор это значение типа rt_thread_t, которое возвратит функция), и также выделит соответствующее пространство из динамической кучи для стека, размер которого определяется параметром stack_size. Выделяемая под стек память выравнивается в адресном пространстве в соответствии макросом RT_ALIGN_SIZE, сконфигурированном в rtconfig.h (обычно этот макрос определен как 4, что означает, что адрес начала области памяти должен нацело делиться на 4).

Параметры функции rt_thread_create и возвращаемое из неё значение описаны в следующей таблице:

Параметр Описание
name Имя потока: текст, максимальная длина которого задана макросом RT_NAME_MAX в rtconfig.h. Если текст имени длиннее, то он будет автоматически обрезан до значения RT_NAME_MAX.
entry Функция тела потока (thread entry function).
parameter Параметр, передаваемый в функцию потока.
stack_size Размер стека потока в байтах.
priority Приоритет потока. Диапазон возможных значений для приоритетов основан на системной конфигурации, что задается определением макроса RT_THREAD_PRIORITY_MAX в rtconfig.h. Самый низкий возможный приоритет обозначается значением RT_THREAD_PRIORITY_MAX-1, самый высокий приоритет значением 0.
tick Выделенный для потока интервал времени выполнения (time slice), указывается в системных тиках.
Возвращаемое значение
TCB При успешном завершении функции будет возвращен дескриптор потока (thread handle), представляющий собой указатель на структуру Thread Control Block потока.
RT_NULL Это возвращенное значение показывает ошибку создания потока.

Для некоторых потоков, созданных с помощью rt_thread_create, если он больше не нужен, ли когда произошла ошибка внутри потока, можно использовать функцию удаления потока из системы:

rt_err_t rt_thread_delete(rt_thread_t thread);

После вызова этой функции объект потока удаляется из списка потоков (thread list) и удаляется из менеджера потоков (kernel object manager). Как следствие пространство в куче, выделенное под стек потока и его TCB, будет освобождено, и может повторно использоваться в других частях приложения. Фактически вызов функции rt_thread_delete потоком переводит его в состояние RT_THREAD_CLOSE, и помещает в очередь дисфункции потоков (rt_thread_defunct queue). Физические действия по удалению (освобождение памяти TCB и стека потока) должны быть завершены позже, когда запустится сборщик мусора, вызванный из задачи ожидания (idle thread), когда она запустится.

Параметр функции rt_thread_delete и возвращаемое из неё значение описаны в следующей таблице:

Параметр Описание
thread Дескриптор потока, подлежащего удалению.
Возвращаемое значение
RT_EOK Показывает успешное удаление потока.
RT_ERROR Ошибка удаления потока.

Эту функцию допустимо использовать только для динамических потоков, когда в системе разрешена динамическая куча (dynamic heap), что задается определением макроса RT_USING_HEAP.

Инициализация и отсоединение потока. Инициализация потока может быть осуществлена функцией rt_thread_init, когда создается статический поток. "Статический" обозначает, что память для потока выделяется из глобальных переменных, определенных в момент написания/компиляции программы. Ядро RT-Thread не отвечает за выделение и освобождение памяти статического потока.

rt_err_t rt_thread_init(struct rt_thread* thread,
                        const char* name,
                        void (*entry)(void* parameter),
                        void* parameter,
                        void* stack_start,
                        rt_uint32_t stack_size,
                        rt_uint8_t priority,
                        rt_uint32_t tick);

Обратите внимание, что у этой функции по сравнению с rt_thread_create на 2 параметра больше: thread и stack_start. Это соответственно указатели на области памяти TCB и стека, эти области должны быть выделены пользователем самостоятельно. Обычно это делается статически, в момент написания кода приложения. Следует отметить, что адрес для предоставленного пользователем стека должен быть выровнен в соответствии с требованиями системы (например, для ARM требуется выравнивание на 4 байта, т. е. адрес начала области памяти должен нацело делиться на 4).

Параметры функции rt_thread_init и возвращаемое из неё значение описаны в следующей таблице:

Параметр Описание
thread Thread handle, дескриптор потока. Это указатель на структуру TCB, которую пользователь должен определить как глобальную (static) переменную.
name Имя потока: текст, максимальная длина которого задана макросом RT_NAME_MAX в rtconfig.h. Если текст имени длиннее, то он будет автоматически обрезан до значения RT_NAME_MAX.
entry Функция тела потока (thread entry function).
parameter Параметр, передаваемый в функцию потока.
stack_start Адрес начала области стека в памяти.
stack_size Размер стека потока в байтах.
priority Приоритет потока. Диапазон возможных значений для приоритетов основан на системной конфигурации, что задается определением макроса RT_THREAD_PRIORITY_MAX в rtconfig.h. Самый низкий возможный приоритет обозначается значением RT_THREAD_PRIORITY_MAX-1, самый высокий приоритет значением 0.
tick Выделенный для потока интервал времени выполнения (time slice), указывается в системных тиках.
Возвращаемое значение
RT_EOK Показывает успешную инициализацию потока.
RT_ERROR Ошибка инициализации потока.

Для потоков, которые были инициализированы с помощью rt_thread_init, использование rt_thread_detach приведет к тому, что объект потока будет отсоединен (detach) от очереди потоков и менеджера потоков ядра.

rt_err_t rt_thread_detach (rt_thread_t thread);

Параметр функции rt_thread_detach и возвращаемое из неё значение описаны в следующей таблице:

Параметр Описание
thread Thread handle, дескриптор потока. Это указатель на структуру TCB, которую пользователь указал при вызове rt_thread_init.
Возвращаемое значение
RT_EOK Поток был успешно отключен.
RT_ERROR Ошибка отключения потока.

Назначение функции rt_thread_detach то же самое, что и функции rt_thread_delete. Их отличие в том, что функция rt_thread_delete манипулирует объектом потока, созданным rt_thread_create, а функция rt_thread_detach предназначена для отключения потока, инициализированного функцией rt_thread_init. Еще одно отличие: статический поток, инициализированный rt_thread_init, не должен отключать самого себя вызовом rt_thread_detach.

Запуск потока. Поток создается (инициализируется) в состоянии Initial, и не попадает в очередь планировщика, пока не перейдет в состояние готовности Ready. После успешного создания/инициализации потока для его перевода в состояние готовности мы можем вызвать функцию rt_thread_startup:

rt_err_t rt_thread_startup(rt_thread_t thread);

Когда эта функция вызвана, состояние потока меняется на Ready, и он помещается очередь потоков, попадая под управление планировщика. Если у только что запущенного этой функцией потока приоритет самый высокий среди всех остальных потоков, то текущий поток немедленно приостанавливается (переходит в состояние Suspended), и получает управление этот новый созданный поток (переходит в состояние Running).

Параметр функции rt_thread_startup и возвращаемое из неё значение описаны в следующей таблице:

Параметр Описание
thread Thread handle, дескриптор потока.
Возвращаемое значение
RT_EOK Поток был успешно запущен.
RT_ERROR Ошибка запуска потока.

Получение информации о текущем потоке. Во время работы программы одна и та же часть кода может быть запущена разными потоками. Чтобы выяснить, какой поток в настоящий момент работает, может использоваться функция rt_thread_self:

rt_thread_t rt_thread_self(void);

Функция возвратит дескриптор потока, из которого она была вызвана. Пример получения имени текущего потока с помощью функции rt_thread_self:

const char *thread_self_name (void)
{
   const char *name;
   rt_thread_t tid = rt_thread_self();
   return (RT_NULL != tid) ? tid->name : "!not_fnd";
}

[Освобождение потоком ресурса CPU]

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

rt_err_t rt_thread_yield(void);

После вызова этой функции текущий поток просто удалит сам себя из очереди готовых к запуску потоков (ready priority thread queue), затем приостановит сам себя, поместив в конец этой очереди, после чего планировщик выполнит переключение контекста на другой находящийся в этой очереди поток. Если же в очереди нет потоков с таким же приоритетом, то вызвавший функцию rt_thread_yield поток продолжит свое выполнение без приостановки.

Действие функции rt_thread_yield подобно функции rt_schedule, однако поведение системы полностью отличается в случае, когда существуют другие потоки в состоянии Ready с таким же приоритетом. После выполнения rt_thread_yield управление перейдет к следующему находящемуся в очереди потоку, текущий поток приостановится. После выполнения rt_schedule текущий поток необязательно будет приостановлен. Даже если он будет приостановлен, он не будет помещен в конец списка потоков Ready. Вместо этого будет выбран и запущен поток с самым высоким приоритетом. Если же в системе в настоящий момент нет потока с более высоким приоритетом, готового к запуску, то после выполнения rt_schedule система продолжит работу текущего потока, никакого переключения контекста и изменения очереди потоков не произойдет.

Thread Sleep. На практике иногда необходимо "усыпить" поток на некоторое время, чтобы обеспечить задержку в выполняемом алгоритме. Это называется "thread sleep". Когда поток уходит в "сон" он освобождает процессорное время для других потоков. Для thread sleep можно использовать функции:

rt_err_t rt_thread_sleep(rt_tick_t tick);
rt_err_t rt_thread_delay(rt_tick_t tick);
rt_err_t rt_thread_mdelay(rt_int32_t ms);

Эти три функции делают одно и то же. Вызов функции из тела потока приведет к его приостановке (переходу в состояние Suspend) на указанное время. После истечения этого времени поток снова перейдет в состояние готовности Ready.

Параметры функций и их возвращаемые значения описаны в следующей таблице:

Параметр Описание
tick/ms Время сна потока. Оно может либо задаваться в тиках системы (для функций rt_thread_sleep и rt_thread_delay), или в миллисекундах (rt_thread_mdelay).
Возвращаемое значение
RT_EOK Успешное завершение операции задержки потока.

[Приостановка (Suspend) и возобновление (Resume) работы потока]

Когда поток вызовет rt_thread_delay, он произвольным образом приостанавливает сам себя. Когда будет вызвана такая функция, как rt_sem_take или rt_mb_recv, то в случае недоступности запрашиваемого поток будет также приостановлен. Если поток ждет в состоянии приостановки большее количество времени, чем задано в параметре таймаута, то он больше не будет ждать, и вернется в состояние готовности Ready. Или, когда другие потоки опубликуют/освободят запрошенный ресурс, которого ждет поток, то он также вернется в состояние готовности.

Поток может быть также приостановлен вызовом следующей функции:

rt_err_t rt_thread_suspend (rt_thread_t thread);

Параметр функции rt_thread_suspend и её возвращаемое значение описаны в следующей таблице:

Параметр Описание
thread Thread handle, дескриптор потока.
Возвращаемое значение
RT_EOK Поток был успешно приостановлен.
RT_ERROR Ошибка приостановки потока, потому что он не находится в состоянии RT_THREAD_READY.

Примечание: обычно вы не должны использовать эту функцию для приостановки потоком самого себя. Если же вы решили на самом деле приостановить в коде текущий поток, то сразу после вызова функции rt_thread_suspend необходимо вставить вызов rt_schedule.

Переключение контекста вызовом функции rt_thread_suspend произойдет вручную. Следует иметь в виду, что использовать функцию rt_thread_suspend не рекомендуется, потому что это слишком усложняет понимание, что на самом деле происходит в приложении.

Можно возобновить работу потока, который был перед этим приостановлен вызовом rt_thread_suspend, чтобы он снова перешел в состояние Ready и было помещен в очередь готовности системы (system's ready queue). Если восстановленный поток окажется первым в списке приоритетов, то система выполнит переключение контекста. Возобновление потока выполняется следующей функцией:

rt_err_t rt_thread_resume (rt_thread_t thread);

Параметр функции rt_thread_resume и её возвращаемое значение описаны в следующей таблице:

Параметр Описание
thread Thread handle, дескриптор потока.
Возвращаемое значение
RT_EOK Работа потока была успешно возобновлена.
RT_ERROR Ошибка возобновления потока, потому что он не находится в состоянии RT_THREAD_SUSPEND.

Управление потоком. Когда нужно управлять другим (не текущим) потоком, как например динамически поменять приоритет потока (что тоже кстати не рекомендуется, незачем плодить сущности), можно использовать следующую функцию:

rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);

Параметры функции rt_thread_control и её возвращаемое значение описаны в следующей таблице:

Параметр Описание
thread Thread handle, дескриптор потока.
cmd Запрашиваемая для потока команда.
arg Параметр управления.
Возвращаемое значение
RT_EOK Корректное выполнение управления.
RT_ERROR Ошибка управления.

Поддерживаются следующие команды управления (параметр cmd), которые определены в файле заголовка rtdef.h:

/**
 * Определения команд управления потоком.
 */
#define RT_THREAD_CTRL_STARTUP          0x00 /**< Запуск потока.                 */
#define RT_THREAD_CTRL_CLOSE            0x01 /**< Закрытие потока.               */
#define RT_THREAD_CTRL_CHANGE_PRIORITY  0x02 /**< Изменение приоритета потока.   */
#define RT_THREAD_CTRL_INFO             0x03 /**< Получение информации о потоке. */

Действие RT_THREAD_CTRL_STARTUP аналогично вызову rt_thread_startup. Команда RT_THREAD_CTRL_CLOSE закроет поток, что эквивалентно вызову rt_thread_delete. Команда RT_THREAD_CTRL_CHANGE_PRIORITY динамически изменит приоритет потока.

Установка и удаление функции обработчика задачи ожидания. Функция обработчика задачи ожидания (idle hook), будучи установленной, будет вызываться из тела системной задачи ожидания (idle thread). Это может использоваться для автоматически повторяемых задач, таких как мерцание светодиодом LED (может показывать, что система не зависла), для управления энергопотреблением или для других функций. Установка и удаление idle hook производится функциями:

rt_err_t rt_thread_idle_sethook(void (*hook)(void));
rt_err_t rt_thread_idle_delhook(void (*hook)(void));

Параметр и возвращаемые значения функции rt_thread_idle_sethook описаны в следующей таблице:

Параметр Описание
hook Функция обработчика ожидания.
Возвращаемое значение
RT_EOK Функция обработчика была успешно установлена.
RT_EFULL Установка была неудачной.

Параметр и возвращаемые значения функции rt_thread_idle_delhook описаны в следующей таблице:

Параметр Описание
hook Функция обработчика ожидания.
Возвращаемое значение
RT_EOK Функция обработчика была успешно удалена.
RT_ENOSYS Удаление было неудачным.

Примечание: задача ожидания (idle thread) это поток, который всегда находится в состоянии готовности к запуску (Ready). Таким образом, hook-функция задачи ожидания должна гарантировать, что системный функционал задачи ожидания не будет в любой момент надолго приостановлен. Поэтому в hook-функции нельзя использовать такие вызовы, как rt_thread_delay, rt_sem_take, и т. п., потому что они могут привести к приостановке задачи ожидания.

Установка Scheduler Hook. Во время работы системы существует специальный процесс, обрабатывающий прерывания тика, который переключает контексты задач - планировщик. Другими словами, переключение контекста это самое частое событие в системе. Иногда пользователю может понадобиться информация о том, какие задачи активируются в любой момент времени, и для этой цели можно установить функцию обработчика планировщика - sheduler hook. Эта hook-функция будет вызвана, когда система переключает контексты потоков:

void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to));

В качестве входного параметра функция rt_scheduler_sethook принимает указатель на определяемую пользователем hook-функцию:

void shedhook (struct rt_thread* from, struct rt_thread* to);

У этой hook-функции параметры это указатели на TCB потоков, между которыми произошло переключение контекста:

Параметр Описание
from Указатель на блок управления потоком, и который система хочет вытеснить (приостановить).
to Указатель на блок управления потоком, которому система хочет передать процессорное время.

Примечание: имейте в виду, что hook-функция планировщика должна быть написана очень аккуратно, иначе она может нарушить работу всей системы. Hook-функция не должна вызывать системные API-функции, а действия в hook-функции не должны влиять на переключение контекста, и её код должен выполняться минимальное время.

[Пример приложения, основанного на потоках]

Ниже показаны простые примеры приложений, которые можно запустить в симуляторе Keil.

Создание потоков. Этот пример создает динамический поток и инициализирует статический поток, демонстрируя оба способа запуска потоков. Когда поток завершит работу, он автоматически удаляется системой. Другой поток будет продолжать печатать значение счетчика.

#include < rtthread.h>
 
#define THREAD_PRIORITY         25
#define THREAD_STACK_SIZE       512
#define THREAD_TIMESLICE        5
 
static rt_thread_t tid1 = RT_NULL;
 
/* Entry-функция для Thread 1 */
static void thread1_entry(void *parameter)
{
    rt_uint32_t count = 0;
    while (1)
    {
        /* Поток 1 работает с низким приоритетом, и печатает в каждой
        итерации значение счетчика count. */
        rt_kprintf("thread1 count: %d\n", count ++);
        rt_thread_mdelay(500);
    }
}
 
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* Entry-функция для Thread 2 */
static void thread2_entry(void *param)
{
    rt_uint32_t count = 0;
 
    /* У потока 2 приоритет выше, чем у потока 1, и
       поток 2 будет запущен в первую очередь */
    for (count = 0; count < 10 ; count++)
    {
        /* Поток 2 печатает значение своего счетчика */
        rt_kprintf("thread2 count: %d\n", count);
    }
    rt_kprintf("thread2 exit\n");
    /* Также поток 2 будет автоматически отключен из системы, когда
    произойдет выход из его функции. */
}
 
/* Пример многопоточного приложения */
int thread_sample(void)
{
    /* Создание потока 1 с именем thread1,с функцией входа thread1_entry */
    tid1 = rt_thread_create("thread1",
                            thread1_entry, RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
 
    /* Запуск этого потока, если память для его TCB была выделена успешно */
    if (tid1 != RT_NULL)
        rt_thread_startup(tid1);
 
    /* Создание потока 2, с именем thread2,с функцией входа thread2_entry */
    rt_thread_init(&thread2,
                   "thread2",
                   thread2_entry,
                   RT_NULL,
                   &thread2_stack[0],
                   sizeof(thread2_stack),
                   THREAD_PRIORITY - 1, THREAD_TIMESLICE);
    rt_thread_startup(&thread2);
 
    return 0;
}
 
/* Экспорт в список команд (msh command list) */
MSH_CMD_EXPORT(thread_sample, thread sample);

Запуск симуляции выведен следующий результат:

 \ | /
- RT -     Thread Operating System
 / | \     3.1.0 build Aug 24 2018
 2006 - 2018 Copyright by rt-thread team
msh >thread_sample
msh >thread2 count: 0
thread2 count: 1
thread2 count: 2
thread2 count: 3
thread2 count: 4
thread2 count: 5
thread2 count: 6
thread2 count: 7
thread2 count: 8
thread2 count: 9
thread2 exit
thread1 count: 0
thread1 count: 1
thread1 count: 2
thread1 count: 3
...

Когда поток 2 досчитает до определенного значения, он завершит свою работу. После этого 2 будет автоматически удален из системы, и это видно по логу консоли msh - поток thread2 больше не увеличивает свой счетчик. Поток thread1 продолжит работу бесконечно, выводя значение своего счетчика.

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

Пример использования слайсинга времени (Time Slice Round-Robin Scheduling). В этом примере создается 2 потока, которые всегда печатают значение своих счетчиков:

#include < rtthread.h>
 
#define THREAD_STACK_SIZE   1024
#define THREAD_PRIORITY     20
#define THREAD_TIMESLICE    10
 
/* У потоков 1 и 2 общая функция потока, но параметры запуска отличаются */
static void thread_entry(void* parameter)
{
    rt_uint32_t value;
    rt_uint32_t count = 0;
 
    value = (rt_uint32_t)parameter;
    while (1)
    {
        if(0 == (count % 5))
        {
            rt_kprintf("thread %d is running ,thread %d count = %d\n", value, value, count);
            if(count> 200)
                return;
        }
         count++;
     }
}
 
int timeslice_sample(void)
{
    rt_thread_t tid = RT_NULL;
 
    /* Создание потока 1 */
    tid = rt_thread_create("thread1",
                            thread_entry, (void*)1,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
    if (tid != RT_NULL)
        rt_thread_startup(tid);
 
    /* Создание потока 2 */
    tid = rt_thread_create("thread2",
                            thread_entry, (void*)2,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE-5);
    if (tid != RT_NULL)
        rt_thread_startup(tid);
 
    return 0;
}
 
/* Экспорт в список команд msh  */
MSH_CMD_EXPORT(timeslice_sample, timeslice sample);

Результат работы будет выглядеть так:

 \ | /
- RT -     Thread Operating System
 / | \     3.1.0 build Aug 27 2018
 2006 - 2018 Copyright by rt-thread team
msh >timeslice_sample
msh >thread 1 is running ,thread 1 count = 0
thread 1 is running ,thread 1 count = 5
thread 1 is running ,thread 1 count = 10
thread 1 is running ,thread 1 count = 15
...
thread 1 is running ,thread 1 count = 125
thread 1 is rthread 2 is running ,thread 2 count = 0
thread 2 is running ,thread 2 count = 5
thread 2 is running ,thread 2 count = 10
thread 2 is running ,thread 2 count = 15
thread 2 is running ,thread 2 count = 20
thread 2 is running ,thread 2 count = 25
thread 2 is running ,thread 2 count = 30
thread 2 is running ,thread 2 count = 35
thread 2 is running ,thread 2 count = 40
thread 2 is running ,thread 2 count = 45
thread 2 is running ,thread 2 count = 50
thread 2 is running ,thread 2 count = 55
thread 2 is running ,thread 2 count = 60
thread 2 is running ,thread 2 cunning ,thread 2 count = 65
thread 1 is running ,thread 1 count = 135
..
thread 2 is running ,thread 2 count = 205

Как видно из этого лога, поток 2 работает наполовину меньше времени, чем поток 1.

Пример Hook-функции планировщика. Мы можем установить перехватчик планировщика, чтобы выполнять некоторые действия, когда происходит переключение контекста. Этот пример печатает информацию о переключениях между потоками, вывод этих сообщений выполняется в hook-функции планировщика.

#include < rtthread.h>
 
#define THREAD_STACK_SIZE   1024
#define THREAD_PRIORITY     20
#define THREAD_TIMESLICE    10
 
/* Счетчики для каждого потока */
volatile rt_uint32_t count[2];
 
/* У потоков 1 и 2 общая функция потока, но параметры запуска отличаются */
static void thread_entry(void* parameter)
{
    rt_uint32_t value;
 
    value = (rt_uint32_t)parameter;
    while (1)
    {
        rt_kprintf("thread %d is running\n", value);
        rt_thread_mdelay(1000); // Delay for a while
    }
}
 
static rt_thread_t tid1 = RT_NULL;
static rt_thread_t tid2 = RT_NULL;
 
static void hook_of_scheduler(struct rt_thread* from, struct rt_thread* to)
{
    rt_kprintf("from: %s -->  to: %s \n", from->name , to->name);
}
 
int scheduler_hook(void)
{
    /* Установка hook-функции планировщика */
    rt_scheduler_sethook(hook_of_scheduler);
 
    /* Создание потока 1 */
    tid1 = rt_thread_create("thread1",
                            thread_entry, (void*)1,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
    if (tid1 != RT_NULL)
        rt_thread_startup(tid1);
 
    /* Создание потока 2 */
    tid2 = rt_thread_create("thread2",
                            thread_entry, (void*)2,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY,THREAD_TIMESLICE - 5);
    if (tid2 != RT_NULL)
        rt_thread_startup(tid2);
 
    return 0;
}
 
/* Экспорт в список команд msh  */
MSH_CMD_EXPORT(scheduler_hook, scheduler_hook sample);

Результат работы симуляции примера:

 \ | /
- RT -     Thread Operating System
 / | \     3.1.0 build Aug 27 2018
 2006 - 2018 Copyright by rt-thread team
msh > scheduler_hook
msh >from: tshell -->  to: thread1
thread 1 is running
from: thread1 -->  to: thread2
thread 2 is running
from: thread2 -->  to: tidle
from: tidle -->  to: thread1
thread 1 is running
from: thread1 -->  to: tidle
from: tidle -->  to: thread2
thread 2 is running
from: thread2 -->  to: tidle
...

Как видно из лога, у функции перехватчика планировщика есть доступ к информации переключении потоков - выводится, какой поток вытеснен, и какой получил управление.

[Ссылки]

1. RT-Thread Thread Management site:rt-thread.io.
2. RT-Thread Studio IDE.
3Консоль FinSH.

 

Добавить комментарий


Защитный код
Обновить

Top of Page