Zephyrでタスク(スレッド)
RTOSとしてのZephyrのタスクを扱う。Linux系ということもあり、実質はスレッドである。また、優先度も扱ってみる。その2同様、Wio Terminal利用。
題材
2つのスレッドAおよびBを動かし、それぞれで、メッセージを表示。その順番により、タスク(スレッド)スイッチを把握する。スイッチ動作を確認するため、スレッド内にあえてsleepなどを用いていない部分もある。ソースコードを見た方が早いだろう。なお、今回の構成は下記。
% tree
.
├── CMakeLists.txt
├── prj.conf
└── src
└── main.c
Buildや書き込みについては、その2を参照。
ソースコード+検証
CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(1stThread)
FILE(GLOB app_sources src/*.c)
target_sources(app PRIVATE ${app_sources})
ここはノーコメント。
prj.conf
2つのケース実施。
Case 1:prj.confそのものを使用しない
Case 2:下記を使用
CONFIG_TIMESLICING=n
これは同一優先度をもつスレッドで、タイムスライスを無効化するもの。デフォルトでは有効化されている。以降、いくつかサンプルを紹介するが、Case 1または2のいずれかも明記。
main.c
スレッドAとBが同一優先度+prj.conf未使用(上述Case 1)
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#define STACK_SIZE 1024
#define PRIORITY 7
#define BASE 1000000
// スレッドA
void thread_a_entry(void *p1, void *p2, void *p3)
{
volatile uint32_t loop_count = 0;
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
while (1) {
loop_count++;
if (loop_count % (10*BASE) == 0) {
printk("Thread A: %d\n", loop_count/(10*BASE));
}
if (loop_count >= 50*BASE) {
loop_count = 0;
}
}
}
// スレッドB
void thread_b_entry(void *p1, void *p2, void *p3)
{
volatile uint32_t loop_count = 0;
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
while (1) {
loop_count++;
if (loop_count % (10*BASE) == 0) {
printk("\tThread B: %d\n", loop_count/(10*BASE));
}
if (loop_count >= 50*BASE) {
loop_count = 0;
}
}
}
// スレッドを静的に定義および起動
K_THREAD_DEFINE(thread_a_id, STACK_SIZE, thread_a_entry, NULL, NULL, NULL, PRIORITY, 0, 0);
K_THREAD_DEFINE(thread_b_id, STACK_SIZE, thread_b_entry, NULL, NULL, NULL, PRIORITY, 0, 0);
まず、Zephyrでは、マクロ「K_THREAD_DEFINE()」によりスレッド(タスク)を作成するのが簡単なようだ。この場合、main()でのスレッド作成は不要。
本コードでは、定期的に、Thread AおよびBのメッセージ+カウンタを表示するだけ。後述する関数などにより、スレッド(タスク)スイッチを明示的に実施させないため、相当数ループさせている。ループ数については、トライ&エラーで選んだもの。ボードにより異なるであろう。実行結果は下記となる。
*** Booting Zephyr OS build v4.4.0-1647-g29d97bdaf61b ***
Thread A: 1
Thread B: 1
Thread A: 2
Thread B: 2
Thread A: 3
Thread B: 3
Thread A: 4
Thread B: 4
Thread A: 5
Thread B: 5
Thread A: 1
Thread B: 1
Thread A: 2
Thread B: 2
...
スレッドAとB同一優先度のため、それぞれのメッセージが順番に表示されており、スレッドが交互に実施されていることが想定される。かなりの回数(1千万回(=10*BASE))ごとの表示ではなく、かなり少ない数ごとの表示であれば、結果が多少異なる可能性はある。
なお、当然であるが、prj.confに「CONFIG_TIMESLICING=y」と記載しても同様の結果となる。
スレッドAとBが同一優先度+prj.conf使用(上述Case 2)
コードは先ほどのものと同様。結果のみ下記。
*** Booting Zephyr OS build v4.4.0-1647-g29d97bdaf61b ***
Thread A: 1
Thread A: 2
Thread A: 3
Thread A: 4
Thread A: 5
Thread A: 1
Thread A: 2
Thread A: 3
Thread A: 4
Thread A: 5
Thread A: 1
...
タイムスライスが無効化されているため、スレッドAの結果しか見えず、スレッドスイッチが行われないように見える。こちらも、かなりの回数(1千万回(=10*BASE))ごとの表示ではなく、かなり少ない数ごとの表示であれば、結果が多少異なる可能性はある。
スレッドAとBが同一優先度+k_yield()使用+prj.conf未使用(上述Case 1)
同一または高い優先度へのスイッチを可能とするk_yield()を利用。スレッドのみコード記載。
// スレッドA
void thread_a_entry(void *p1, void *p2, void *p3)
{
volatile uint32_t loop_count = 0;
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
while (1) {
loop_count++;
if (loop_count % (10*BASE) == 0) {
printk("Thread A: %d\n", loop_count/(10*BASE));
}
if (loop_count >= 50*BASE) {
loop_count = 0;
k_yield(); // 同一または高優先度タスク(スレッド)へスイッチ
}
}
}
// スレッドB
void thread_b_entry(void *p1, void *p2, void *p3)
{
volatile uint32_t loop_count = 0;
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
while (1) {
loop_count++;
if (loop_count % (10*BASE) == 0) {
printk("\tThread B: %d\n", loop_count/(10*BASE));
}
if (loop_count >= 50*BASE) {
loop_count = 0;
k_yield(); // 同一または高優先度タスク(スレッド)へスイッチ
}
}
}
カウンタが最大値(50*BASE)になった時、k_yield()を実行する。結果を記載。
*** Booting Zephyr OS build v4.4.0-1647-g29d97bdaf61b ***
Thread B: 1
Thread A: 1
Thread B: 2
Thread A: 2
Thread B: 3
Thread A: 3
Thread B: 4
Thread A: 4
Thread B: 5
Thread A: 5
Thread B: 1
Thread A: 1
Thread B: 2
Thread A: 2
...
タイムスライスが有効のため、k_yield()の効果が見られず、最初の結果と同じである。
スレッドAとBが同一優先度+k_yield()使用+prj.conf使用(上述Case 2)
コードは一つ前(k_yield()含む)と同じ。結果を記載。
*** Booting Zephyr OS build v4.4.0-1647-g29d97bdaf61b ***
Thread A: 1
Thread A: 2
Thread A: 3
Thread A: 4
Thread A: 5
Thread B: 1
Thread B: 2
Thread B: 3
Thread B: 4
Thread B: 5
Thread A: 1
Thread A: 2
Thread A: 3
Thread A: 4
Thread A: 5
Thread B: 1
Thread B: 2
...
カウンタが最大値(50*BASE)になった時、k_yield()実行により、スレッドスイッチ(AからBへ or BからAへ)が行われていることがわかる。
スレッドAの優先度がスレッドBよりも高い+prj.conf未使用(上述Case 1)
スレッドAの優先度がBよりも高いが、工夫をすれば、Bも実行されるコードである。
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#define STACK_SIZE 1024
#define PRIORITY 7
#define BASE 100000
// スレッドA
void thread_a_entry(void *p1, void *p2, void *p3)
{
volatile uint32_t loop_count = 0;
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
while (1) {
loop_count++;
if (loop_count % (100*BASE) == 0) {
printk("Thread A: %d\n", loop_count/(100*BASE));
k_msleep(10); // 低優先度タスクも含めて全てのタスクへスイッチ
}
if (loop_count >= 500*BASE) {
loop_count = 0;
}
}
}
// スレッドB
void thread_b_entry(void *p1, void *p2, void *p3)
{
volatile uint32_t loop_count = 0;
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
while (1) {
loop_count++;
if (loop_count % BASE == 0) {
printk("\tThread B: %d\n", loop_count/BASE);
}
if (loop_count >= 5*BASE) {
loop_count = 0;
}
}
}
// 優先度 A:PRIORITY-1 B:PRIORITY
K_THREAD_DEFINE(thread_a_id, STACK_SIZE, thread_a_entry, NULL, NULL, NULL, PRIORITY-1, 0, 0);
K_THREAD_DEFINE(thread_b_id, STACK_SIZE, thread_b_entry, NULL, NULL, NULL, PRIORITY, 0, 0);
スレッドAにはk_msleep()が使われている。これにより、低優先度含めて全てのスレッドにスイッチされる。ただし、スレッドBにはk_msleep()は使われていない。スレッドAとBの繰り返し回数を工夫した。結果を下記する。
Thread A: 1
Thread A: 2
Thread A: 3
Thread B: 1
Thread A: 4
Thread A: 5
Thread B: 2
Thread A: 1
Thread A: 2
Thread A: 3
Thread B: 3
Thread A: 4
Thread A: 5
Thread B: 4
Thread A: 1
...
優先度が低いにも関わらず、スレッドAのk_msleep()のタイミングで、スレッドBが実行されていることがわかる。
動的スレッド作成のコード
マクロ「K_THREAD_DEFINE()」ではなく、同じ内容で、「k_thread_create()」を用いた動的スレッド作成を用いるコードを下記する。
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#define STACK_SIZE 1024
#define PRIORITY 7
#define BASE 1000000
// スレッド管理用構造体
struct k_thread thread_a_data;
struct k_thread thread_b_data;
// スタック領域
K_THREAD_STACK_DEFINE(thread_a_stack, STACK_SIZE);
K_THREAD_STACK_DEFINE(thread_b_stack, STACK_SIZE);
// スレッドA
void thread_a_entry(void *p1, void *p2, void *p3)
{
volatile uint32_t loop_count = 0;
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
while (1) {
loop_count++;
if (loop_count % (10*BASE) == 0) {
printk("Thread A: %d\n", loop_count/(10*BASE));
}
if (loop_count >= 50*BASE) {
loop_count = 0;
k_yield(); // 同一または高優先度タスクへスイッチ
}
}
}
// スレッドB
void thread_b_entry(void *p1, void *p2, void *p3)
{
volatile uint32_t loop_count = 0;
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
while (1) {
loop_count++;
if (loop_count % (10*BASE) == 0) {
printk("\tThread B: %d\n", loop_count/(10*BASE));
}
if (loop_count >= 50*BASE) {
loop_count = 0;
k_yield(); // 同一または高優先度タスクへスイッチ
}
}
}
int main(void)
{
printk("Starting threads dynamically...\n");
// スレッドAを動的に生成・起動
k_thread_create(&thread_a_data,
thread_a_stack,
K_THREAD_STACK_SIZEOF(thread_a_stack),
thread_a_entry,
NULL, NULL, NULL,
PRIORITY,
0,
K_NO_WAIT);
// スレッドBを動的に生成・起動
k_thread_create(&thread_b_data,
thread_b_stack,
K_THREAD_STACK_SIZEOF(thread_b_stack),
thread_b_entry,
NULL, NULL, NULL,
PRIORITY,
0,
K_NO_WAIT);
return 0;
}
終わりに
タスクではなくスレッドであることがLinuxを感じる。いずれにせよ、まずはRTOSとしての優先度付きマルチスレッドの基本を理解。