はじめに
mruby/c は、基本的には mruby バイトコードの Virtual machine (VM) ですが、それに加えて複数の mruby プログラム (mruby バイトコード) をプリエンプティブに複数同時実行するためのスケジューラを含んでおり、mruby に対する mruby/c の特徴の一つとなっています。今回この複数同時実行をある程度制御するための仕組みを Task クラスとして整備したので、備忘録的に記事にします。
なお、この記事の執筆現在(2024/01/29) の master ブランチの内容です。今後のリリースには反映されると思いますが、まだ未定です。
やりたいこと
- タスクに名前をつけて識別
- 一時停止と再開
- タスク終了と他タスクの終了待ち
- タスクプライオリティーの変更
- 実行権を手放す
- タスクリストの保持
- タスクの状態の取得
スケジューラは、OS 無しでマルチタスクを実行するよう設計しており、よってこのスケジューラに全機能を実装しています。
これを実現するために必要とするハードウェアリソースは、HALに関数およびマクロで定義するようにしており、クリティカルセクションのための割り込み禁止と許可、および周期タイマー割り込みのたった3種類だけです。
タスクの状態遷移と優先度
前知識としてタスクの状態遷移と優先度について記述します。
一つのプログラムは一つのタスクになり、それぞれ以下の状態を持ちます。
- READY
- 実行状態
- WAITING
- 待ち状態(sleep待ち、Mutex待ちなど)
- SUSPENDED
- 一時停止状態
- DORMANT
- 停止状態(プログラム終了)
これは、概念と用語を ITRON から拝借していますね。
タスクは(特別の操作をしない限り)READY で始まり、実行を開始します。sleep
などの待ちを発生させるメソッドが呼ばれると WAITING に遷移し、待ち条件が無くなると再び READY になって実行を再開します。なお、I/O 待ちも作りたいと思っているのですが、ライブラリの作成方法に強く影響(密結合)してしまうこという懸念から、いまだ結論が出せていません。
タスクは、状態に加えて実行優先度(Priority) を持っており、以下のように動作します。
同じ優先度を持つタスクが複数ある場合
同じ優先度のプログラムがプリエンプティブに同時実行されます。
違う優先度のタスクがある場合
高優先度のタスクのみが実行され、そのタスクが RUNNING でなくなる(例えば sleep を実行して WAITING 状態になる)まで、低優先度のタスクは待たされます。
このあたりの動作は、一般的な RTOS と同じにしており、UNIX などとは違いますね。
タスクに名前をつけて識別
さて本題です。
mruby/c のタスクは、UNIX のタスク等とは違い親子関係がないですから、今まではタスクを特定する方法がありませんでした。Taskクラスを実装するにあたり、mruby/c ではタスクに名前をつけて識別できるようにしました。この仕組みは、同じリアルタイムOSである QNX からアイデアを頂いています。
# 自タスクに名前をつける
Task.name = "Task1"
こうしてつけた名前を使って、別のタスクから Task オブジェクトを生成します。
# 別タスク (e.g.Task2) から Task1を操作するオブジェクトを得る
task1 = Task.get("Task1")
有益な例は、後ほど掲載します。
一時停止と再開
自タスク、他タスクとも任意のタイミングで一時停止できます。
# 自タスクの一時停止
Task.suspend
# 他タスク (Task1) の一時停止
task1.suspend
実行再開は、自分からはできませんので、他タスクから行う事になります。
# Task1 の実行再開
task1.resume
タスク終了と他タスクの終了待ち
自タスク、他タスクとも任意のタイミングで終了できます。
一時停止との違いは、一時停止は SUSPENDED 状態になり、終了は DORMANT 状態になることです。
# 自タスクの終了
Task.terminate
# 他タスクの終了
task1.terminate
タスクの終了を待つ方法も用意しています。これは、CRuby の Thread クラスから拝借しています。
# 他タスクの終了待ち
task1.join
タスクプライオリティーの変更
各タスクは実行優先度(プライオリティー)を持っており、任意のタイミングで変更できます。
# 自タスクのプライオリティー変更
Task.priority = n
# 他タスクのプライオリティー変更
task1.priority = n
デフォルトのプライオリティー値は、128です。値は 0 から 255 までの数値で指定し、小さいほど優先度が高くなります。実際のシステムはそこまでの段階は必要ないと思いますが、実装上の都合からです。
また、待ち状態と組み合わさった時の優先度逆転については、内部的に対応準備だけしてありますが、実際の実装はまだです。
実行権を手放す
自ら一旦実行権を手放します。
Task.pass
同じ優先度を持ったタスクがある場合に、そのタスクに実行権が移ります。
タスクリストの保持
現在のタスク一覧を得ることができます。
ほぼ、デバッグ用にしか使わないと思います。
# 全タスクを、タスクオブジェクトの配列として得る
task_list = Task.list # -> Array[Task]
# 全タスクを、タスク名(String)の配列として得る
task_list = Task.name_list # -> Array[String]
タスクの状態の取得
タスクの状態 (RUNNING, ...) を得ることができます。こちらもほぼデバッグにしか使わないと思います。
s = task1.status # -> String
その他
現在の実装は、マイコンに1つのCPUコアが載っている場合のみ対応しています。正確には2つ以上のコアが載っていても、mruby/c では1つのコアのみで動かす場合に対応しています。そのため、上記の説明も1コアを前提にして書いているところがいくつかあります。
内部的には、一部の関数ではマルチコアに対しての考慮もして実装をしていますが、HALの機能もマルチコア対応の拡張が必要になりますし、メモリ管理モジュール側も対応しないといけないし、後回しになっているところです。
プログラム例
Task1の終了をTask2が待つサンプル
# Task1
Task.name = "Task1"
puts "Task1: start"
sleep 2
puts "Task1: done"
# Task2
Task.name = "Task2"
puts "Task2: start"
until task1 = Task.get("Task1") # Task1 の object を得る。
Task.pass
end
puts "Task2: Waiting for Task1 done"
task1.join
puts "Task2: Waiting done"
sleep 1
puts "Task2 done"
実行例
Task1: start
Task2: start
Task2: Waiting for Task1 done
(ここで2秒待ち)
Task1: done
Task2: Waiting done
(ここで1秒待ち)
Task2 done
優先度の変更サンプル
Task.name = "Task1"
puts "Task1: start"
100.times {|i| puts "Task1: output. #{i}" }
until task2 = Task.get("Task2") # Task2 の object を得る。
Task.pass
end
task2.priority = 10 # Task2の優先度を上げる
100.times {|i| puts "Task1: output. #{i}" }
puts "Task1: done"
Task.name = "Task2"
puts "\t\tTask2: start"
200.times {|i| puts "\t\tTask2: output. #{i}"}
puts "\t\tTask2: done"
実行例
Task1: start
Task1: output. 0
Task1: output. 1
Task1: output. 2
Task1: output. 3
Task1: output. 4
Task2: start
Task2: output. 0
Task2: output. 1
Task2: output. 2
Task2: output. 3
Task2: output. 4
Task1: output. 5
Task1: output. 6
Task1: output. 7
Task1: output. 8
Task1: output. 9
Task1: output. 10
Task2: output. 5
Task2: output. 6
Task2: output. 7
Task2: output. 8
Task2: output. 9
Task2: output. 10
Task1: output. 11
(中略 - Task1とTask2が6行ずつ交互に表示)
Task1: output. 99
(Task1 100行表示終わり、Task2の優先度が上がるためTask2のみ実行されるようになる)
Task2: output. 95
Task2: output. 96
Task2: output. 97
Task2: output. 98
Task2: output. 99
Task2: output. 100
Task2: output. 101
Task2: output. 102
Task2: output. 103
Task2: output. 104
Task2: output. 105
Task2: output. 106
Task2: output. 107
Task2: output. 108
Task2: output. 109
Task2: output. 110
Task2: output. 111
Task2: output. 112
Task2: output. 113
(中略)
Task2: output. 199
Task2: done
(Task2が終わったので、Task1が実行再開される)
Task1: output. 0
Task1: output. 1
Task1: output. 2
Task1: output. 3
Task1: output. 4
Task1: output. 5
Task1: output. 6
Task1: output. 7
Task1: output. 8
Task1: output. 9
Task1: output. 10
Task1: output. 11
Task1: output. 12
Task1: output. 13
(中略)
Task1: output. 99
Task1: done
おわりに
リリースまでに、まだまだ仕様を変更する可能性があります。ご意見などありましたら、githubの方にでも、お寄せください。