そしてまた:
- Linuxカーネルマルチタスク:割り込みとタスクレット
- Linuxカーネルマルチタスク:ワークキュー
- プロトスレッドと協調マルチタスク
それは何であり、なぜこれがすべて必要なのですか?
1つの記事シリーズで、遅延割り込み処理と協調マルチタスクを組み合わせることにしたのは偶然ではありませんでした。 サイクルで検討するエンティティ、およびそれらが実装するアイデアは、示されたタスクのコンテキストだけでなく、リアルタイムシステムでも一般的に非常に重要です。
それらは主に計画の問題によって結ばれています。 たとえば、最初の記事で説明したタスクレットは、非プリエンプティブマルチタスクのルールに従って機能します。 ところで、協調マルチタスクについては、以前の記事のいずれかですでに詳しく説明しました。
協調的なマルチタスクとプロトスレッド
プロトスレッドは、純粋なCに実装された軽量のスタックレススレッドです。可能な使用法の1つは、大きなオーバーヘッドを必要としない協調マルチタスクの実装です。 たとえば、メモリ制限が厳しい組み込みシステムや、ワイヤレスセンサーネットワークのノードで使用できます。
Habrahabrには、プロトスレッドに基づいたマルチタスクについての優れた記事 ldirが既にあります。問題の実際的な側面、つまりライブラリの機能、その適用方法について検討しています。 この記事には、興味深い実例が付属しています。
これがどのように機能するかについての詳細があります。 このセクションの後半で、プロトスレッドの作成者であるAdam DunkelsのWebサイトからの情報の無料である程度処理された翻訳を提供します。
プロトスレッドの主な機能と利点:
- 複雑なステートマシンと完全なマルチスレッドを使用せずに制御フローを順次実装します。
- Cの関数内で条件付きロックを使用し、
- プロトスレッドはスタックがないため非常に軽量です。
- プロトスレッドは他の関数を呼び出すことができますが、呼び出された関数内でロックすることはできないため、子プロトスレッドでロックを使用する各関数をラップする必要があります。 したがって、明示的なブロックのみが使用されます。
プロトスレッドは、プログラムの特定の場所での現在の実行状態を表す継続を使用して実装されますが、呼び出し履歴とローカル変数はサポートしません。 ここでは、特に注意する必要があります-ローカル変数を非常に慎重に使用する必要があります! プロトスレッドには専用のスタックはありません;ローカル変数を保存する場所はありません。 ストリームが1回だけ使用される場合、変数は静的として宣言できます。 それ以外の場合は、たとえば、構造内にプロトスレッドをラップし、変数をそこに保存できます。 一般に、ローカル変数の不適切な使用は予測できない結果につながる可能性があることに注意する必要があると言いたいと思います。
検討中の実装では、状態の役割は正の整数(プログラムの現在の行番号)によって果たされます。 DuffメソッドやSimon Tathamを実装するための共同プログラムでの発生と同様に、switch()を使用して制御が行われます。 プロトスレッドはPythonのジェネレーターに似ていますが、異なる目的のために設計されています。プロトスレッドはCの関数内でロックを提供し、Pythonはジェネレーターから複数の出口を提供します。
実装の重要な制限:protothread自体の中のコードは、switch()演算子を完全に使用できません。 ただし、この制限は、gccなどの一部のコンパイラの機能を使用して可能です。
マクロは、単純な関数とは異なり、標準のC構文を使用してのみ制御フローを変更できるため、ライブラリ全体がマクロに実装されます。
コアAPIには次のマクロが含まれています。
- 初期化マクロPT_INIT、
- プロトスレッドの開始と終了を宣言するPT_BEGINおよびPT_ENDマクロ、
- 条件PT_WAIT_UNTILおよびPT_WAIT_WHILEを待機するためのマクロ、
- 子プロトスレッドPT_WAIT_THREADおよびPT_SPAWNの実行を待機するマクロ、
- マクロはPT_RESTARTを再起動し、PT_EXITを終了します。
- スケジューリング(基本的に起動)のマクロPT_SCHEDULE、
- 製品価値マクロPT_YIELD、
- セマフォPT_SEM_INIT、PT_SEM_WAIT、およびPT_SEM_SIGNALを使用するためのマクロ。
それでは、マクロの仕組みを見てみましょう。 まず、プロトスレッドを使用するプログラムを検討します。 Protothreadの例では、永遠のループを実行します。最初に、カウンターが特定の値に達するまで待機し、メッセージを表示してカウンターをリセットします。
#include "pt.h" static int counter; static struct pt example_pt; static PT_THREAD(example(struct pt *pt)) { PT_BEGIN(pt); while(1) { PT_WAIT_UNTIL(pt, counter == 1000); printf("Threshold reached\n"); counter = 0; } PT_END(pt); } int main(void) { counter = 0; PT_INIT(&example_pt); while(1) { example(&example_pt); counter++; } return 0; }
ここで、例で使用されているマクロの簡易バージョンを見てみましょう。
struct pt { unsigned short lc; }; #define PT_THREAD(name_args) char name_args #define PT_BEGIN(pt) switch(pt->lc) { case 0: #define PT_WAIT_UNTIL(pt, c) pt->lc = __LINE__; case __LINE__: \ if(!(c)) return 0 #define PT_END(pt) } pt->lc = 0; return 2 #define PT_INIT(pt) pt->lc = 0
pt構造は、単一のlcフィールドで構成されます(ローカル継続の略)。 わずかに複雑なマクロPT_WAIT_UNTILと同様に、それぞれswitchステートメントを開閉するPT_BEGINとPT_ENDに注意してください。 プログラムファイルの現在の行番号を返す組み込みマクロ__LINE__を使用します。
サンプルプリプロセッサのソースバージョンとデプロイ済みバージョンを比較します。
| |
Protothreadの例は通常のC関数のようになり、戻り値を使用して、プロトスレッドのステータス(何かを待ってブロックされたか、完了したか、終了したか、別の値を生成したか)を判断します。 PT_BEGINマクロにはcase 0命令が含まれているため、初期値pt-> lcは0であるため、このマクロの直後のコードが最初に実行されます。
PT_WAIT_UNTILマクロが何に展開されたかをご覧ください。 pt-> lcフィールドには現在12が割り当てられています。これは行番号であり、ケース12がすぐに表示されます。このスイッチのおかげで、ジャンプ先が正確にわかります。 条件が満たされない場合、関数は0を返します。これは、ストリームが待機していることを意味します(ライブラリ自体では、これらすべての定数がレンダリングされます)。 次回、例()がmainで呼び出されると、実行はそれぞれケース12で続行されます。つまり、待機条件が満たされているかどうかがチェックされます。 カウンターが1000に達するとすぐに条件が真になり、example()は0を返さず、メッセージを出力してカウンターをリセットします。 さらに、予想どおり、サイクルの本体の先頭に移動します。
以前の記事の1つで 、プリミティブな協調スケジューラのコードを引用しました(セクション「呼び出しの順序を保持する非プリエンプティブスケジューラ」)。 小さな変更を行った後、プロトスレッドに適応させることができます。 これは非常に単純なので、コードを提供しません。 しかし、一緒に遊ぶことをお勧めします。
比較
最後に、タスクレット、ワークキュー、およびプロトスレッドを比較することを提案します。 実際、もちろん、一方では、プロトスレッドと下半分の割り込みを処理する手段とを比較することはあまり正しくありません-結局、それらは異なるタスク用に作成されているため、互いに交換することはそれほど簡単ではありません。 一方、ワークキューも上位3つから多少ノックアウトされています。他のワークキューとは異なり、計画の混雑のルールに従って機能し、その範囲ははるかに広くなっています。
比較有用なアイデアを抽出するために、より可能性を高めます。
そして、ここに比較表があります:
タスクレット | ワークキュー | プロトスレッド | |
---|---|---|---|
| いいえ-それらはsoftirqとして処理されます(少なくともx86上のLinuxでは、すべてのハンドラーに共通の専用スタック上で) | はい-ワーカースレッドのスタックで実行されます。ワーカースレッドの数はタスクの数よりもはるかに少ないです | いや |
| 高速-最小限の追加処理 | 高速ですが、タスクレットとは異なり、ワーカーが互いに入れ替わるときにコンテキストの切り替えが必要です | 非常に高速-追加の処理は実質的になく、コンパイラによる最適化の余地があります |
| なし(スピンロックを除く) | 完全にプレゼント | 擬似セマフォ。 原始的なイベントの期待 |
| softirqとしての協力プランナー。 タスクレットはハードウェア割り込みによってのみ置き換えられます | 労働者は仕事のスケジューラの役割を果たし、彼ら自身はメインスケジューラによって制御されます | ユーザーが実装する協調プランナー |
これらのアプローチにはそれぞれ長所と短所があり、ユーザーの要求に応じてさまざまなタスクに使用されます。
たとえば、Emboxには、プロトスレッドやタスクレットのような独自のスタックを持たない軽量スレッドを実装するというアイデアがありましたが、ワークキューで実装されるようにメインスケジューラによって制御され、少なくとも何らかの形の待機メカニズムをサポートしますイベント(さらにそれ以上-フルスレッドが使用する同じAPIのサブセットを使用します)。 このアプローチには、いくつかの魅力的なアプリケーションがあります。
- 遅延割り込み処理のメカニズムを置き換える。
- スケジューラで軽量スレッドのみを使用すると、リソースに制限のある組み込みシステムでもほぼ完全なマルチタスクを使用できます。
- 前のシナリオは他の何かに適しています。システムを新しいプロセッサアーキテクチャに移植する場合、まず少なくとも実行可能なシステムを取得する必要があります。 この時点での新しいアセンブラでの本格的なcontext_switchの実装は、頭痛の種です。 軽量ストリームのみを使用する場合、これは必要ありません。
私たちがどのようにそれを手に入れたか、どのような結果を達成したかについて、しばらくしてから次回お話しします。