マルチタスクを行う

私は、OS開発に関する一般的な記事とOS固有のファントム記事を交互にしようとしています。 この記事は一般的な概要です。 もちろん、Phantomコードの例を示します。



原則として、マルチタスクメカニズムの実装自体は非常に簡単です。 単独で。 しかし、第一に微妙な点があり、第二に、他のサブシステムと連携する必要があります。 たとえば、同期プリミティブの同じ実装は、マルチタスクの実装に非常に密接に関連しています。 割り込みおよび実行サブシステムと同じように、非禁止接続があります。 しかし、それについては後で。



まず、接続されているモジュールは、タスクスイッチングサブシステム(コンテキスト)とスケジューリングサブシステムの2つだけです。 今日は2番目についてはほとんど説明しませんが、簡単に説明します。



Shedulerは、「プロセッサが現在どのスレッドを提供すべきか」という質問に答える関数です。 それだけです 最も単純なシェダーは、すべてのスレッドを単純に繰り返します(ただし、もちろん、実行の準備ができており、停止していません)(RRアルゴリズム)。 実際のシェダーは、いくつかの優先度クラスを組み合わせることができる一方で、優先度、スレッドの動作(インタラクティブなものは計算的なものよりも多くなります)、アフィニティ(スレッドが最後に動作したプロセッサ)などを考慮します。 通常、これはリアルタイムクラス(このクラスのスレッドが少なくとも1つある場合-動作する)、タイムシェアリングクラス、およびアイドルクラスです(プロセッサは、前の2つのクラスが空の場合、つまり実行可能なスレッドがない場合のみ取得します)。



とりあえず、シェダラーを完成させましょう。



プロセッサをあるス​​レッドから取得して別のスレッドに渡すことができるサブシステムに進みましょう。



繰り返しますが、簡単なものから始めましょう。 マルチタスクは、協調的で事前応答性があります。



協調は非常に単純です-すべてのスレッドは時々、正直かつ故意にプロセッサを「与え」、yield()関数を呼び出します。



実際にはほとんど使用されませんが、すべてのプリエンプティブマルチタスクの中心には協調的です。 厳密に言えば、すべてのプリエンプティブマルチタスクは、タイマー割り込みを使用してユーザースレッドからプロセッサを選択し、非常に協力して「手で」別のスレッドに切り替えることになります。



協調マルチタスク自体は、1つの機能のみに基づいています。 すべてのスレッドが同時に呼び出した関数。



この場所について少し考える必要があります。 これは事実です。現在動作しているスレッド(簡単にするためにシングルプロセッサシステムと見なします)を除くシステムのすべてのスレッドの観点から、それらはすべてコンテキストスイッチング関数と呼ばれ、中断されました。 その途中で。 動作中のスレッド(それ自体または割り込みによる強制)も、プロセッサを「与える」ときが来たときにこの関数を呼び出します。 この呼び出しにより、呼び出し元のスレッドが停止し、コンテキスト切り替え機能が別のスレッドに戻ります。 (シェダーが選択したもの)。



少し後、コンテキストスイッチングコード全体を検討しますが、現在はまさにコアであり、実際にスレッドを切り替えます。



Intel 32ビットの実装リファレンス



簡単にするためにコードを少し整理しました。



// called and returns with interrupts disabled

/* void phantom_switch_context(

phantom_thread_t *from,

phantom_thread_t *to,

int *unlock );

*/

ENTRY(phantom_switch_context)



movl 4(%esp),%eax // sw from (store to)



movl (%esp),%ecx // IP

movl %ecx, CSTATE_EIP(%eax)

movl %ebp, CSTATE_EBP(%eax)



// we saved ebp, can use it.

movl %esp, %ebp

// params are on bp now



pushl %ebx

pushl %edi

pushl %esi

movl %cr2, %esi

pushl %esi



movl %esp, CSTATE_ESP(%eax)



// saved ok, now load



movl 8(%ebp),%eax // sw to (load from)



movl CSTATE_ESP(%eax), %esp



popl %esi

movl %esi, %cr2

popl %esi

popl %edi

popl %ebx



movl CSTATE_EIP(%eax), %ecx

movl %ecx, (%esp) // IP



// now move original params ptr to ecx, as we will use and restore ebp

movl %ebp, %ecx



movl CSTATE_EBP(%eax), %ebp



// Done, unlock the spinlock given



movl 12(%ecx),%ecx // Lock ptr

pushl %ecx

call EXT(hal_spin_unlock)

popl %ecx



// now we have in eax (which is int ret val) old lock value



ret









この関数は、切り替え後のスレッド、切り替え先のスレッド、切り替え後にロック解除するスピンロックの3つの引数を取ります。



その意味は非常に単純です。 プロセッサのすべての(重要な)状態をスタックに追加し、現在のスレッドを記述する構造体の特別なフィールドにスタックポインターの位置を書き込みます。 次に、新しいスレッドの構造からスタックポインタの位置を削除し、スタックポインタを復元して、スタックから新しいスレッドのプロセッサ状態を削除します。 その後、関数はすべて新しいスレッドに戻ります。



これの前に、前のスレッドが渡したスピンロックを加熱します-ご覧のように、切り替え後、つまり古いスレッドが既に停止しているときに厳密に加熱します-これは同期プリミティブを実装し、マルチプロセッシングをサポートするために重要です。 (切り替える前にスピンロックを加熱すると、別のプロセッサがまだ完全に非アクティブ化していないスレッドをアクティブ化しようとする場合があります。)



私たちが検討したのは、実現のまさに中心です。 ただし、この関数は直接呼び出されません(通常、コードのアーキテクチャ依存部分のみを実装します)が、phantom_thread_switch()ラッパーから呼び出されます。



phantom_thread_switch()コードを参照してください。



ラッパーで何が起こるか。 手順を実行しましょう。



割り込みのコンテキストにないこと(割り込みを一時停止できない、プロセッサハードウェアの状態の整合性が侵害されていること)、およびスレッドサブシステムが一般的にアクティブになっていることを確認します。 保証された中断を無効にします。 それは私たちが今すぐに絶対に必要としないものです。



  assert_not_interrupt(); assert(threads_inited); int ie = hal_save_cli();
      
      







一般的なコンテキストスイッチスピンロックをロックします。 実際には、CPUごとに実行できますが、念のため-一度に1つのコンテキストスイッチのみを切り替えます。 スレッドの記述構造からスピンロックへのリンクを削除します。これは、切り替え後にロックを解除する必要があります-同期プリミティブによって渡されました。 スピンロックが再度ロック解除されないように、構造内のリンクをゼロにします。



  hal_spin_lock(&schedlock); toUnlock = GET_CURRENT_THREAD()->sw_unlock; GET_CURRENT_THREAD()->sw_unlock = 0;
      
      







現在のスレッドから最後の「ティック」を取得してみましょう-プロセッサ上で動作するための計画間隔。 これはシェドラー用ですので、ここでは詳しく説明しません。 プロセッサを誰に渡すかをシェダーに尋ねます。 現在のスレッドが誰であったかを覚えておいてください。 シェダラーが狂わず、動作する権利を持たないスレッド(非ゼロのsleep_flags)を開始するように提案していない順序を確認してください。



  // Eat rest of tick GET_CURRENT_THREAD()->ticks_left--; phantom_thread_t *next = phantom_scheduler_select_thread_to_run(); phantom_thread_t *old = GET_CURRENT_THREAD(); assert( !next->sleep_flags );
      
      







ガベージに関与しないように、同じスレッドを開始する必要があるかどうかを確認し、同じスレッドである場合は、このイベントを統計に記録して、演習を終了します。



  if(next == old) { STAT_INC_CNT(STAT_CNT_THREAD_SAME); goto exit; }
      
      







実行の準備ができているスレッドのキューから新しいスレッドを削除します(その中で、シェダーはプロセッサのポーズをとる申請者を検索します)。 実際、いくつかのキューがありますが、これらは詳細です-すべてから削除します。



古いスレッドがブロックされていない場合は、逆にキューに入れます(これはグローバルschedlockが便利な場所です)ので、将来プロセッサに割り当てられるふりをすることができます。 (ブロックされている場合、ロックを解除した人がキューに返されます。)



  t_dequeue_runq(next); if(!old->sleep_flags) t_enqueue_runq(old);
      
      







その後、すべてが厳しいです。 phantom_switch_contextを呼び出した後、別のスレッドで作業すると、LOCAL VARIABLESの他の値があることを明確に理解する必要があります。 特に、開始するスレッドのハンドルへのポインターを格納する次の変数には、スレッドの開始後に誤った値が含まれます。 したがって、切り替え後ではなく、切り替える前に現在どのスレッドが動作しているかに関する情報を格納するグローバル変数を修正します。 (実際には、後にも可能ですが、別の変数から。:)



次に、実際に切り替える前にソフトウェア割り込みを有効にし、その後は禁止します。 これは、ソフトウェア割り込みから正確に発生するコンテキストのプリエンプティブスイッチングであり、それらを操作するための特定のプロトコルを保証する必要があるためです。 これについては別に説明しますが、その瞬間は本当に薄いです。



  // do it before - after we will have stack switched and can't access // correct 'next' SET_CURRENT_THREAD(next); hal_enable_softirq(); phantom_switch_context(old, next, toUnlock ); hal_disable_softirq();
      
      







さらに-すべてのローカル変数の値が変更されました。 安心のために、私はそれらをもう使用しません(実際、それは可能です-それらに含まれているものを正確に理解する必要があるだけです)、再び私は自分が誰で、どのスレッドが実行されているかを見つけます。 まず、彼女がどのプロセッサで「目覚めた」かを知らせ、次に、切り替え後のコンテキスト回復のアーキテクチャ固有の機能が呼び出されます。



  phantom_thread_t *t = GET_CURRENT_THREAD(); t->cpu_id = GET_CPU_ID(); arch_adjust_after_thread_switch(t);
      
      







Intelの場合、この特定の関数は、カーネルモードに切り替えるときにスタックの最上位を復元します。



  cpu_tss[ncpu].esp0 = (addr_t)t->kstack_top;
      
      







コンテキストの切り替え後にスレッドがユーザーモードに戻る場合に必要です。ユーザーモードからの割り込みとシステムコールは、プロセッサによるスタックのハードウェア切り替えにつながります。もちろん、スレッドごとにこのようなスタックは個別です。



私が言及していない微妙な点もあります。 たとえば、従来Intelでは、プロセッサの状態を復元するときに、浮動小数点レジスタとSSEは復元されません-代わりに、それらへのアクセスを拒否するためのフラグが設定されます。 スレッドコードがこれらのレジスタを実際に使用しようとすると、これらのレジスタの状態を復元する例外が発生します。 それらはかなり重いですが、誰もがそれらを使用するわけではなく、そのような最適化は理にかなっています。



スレッドの開始について説明します。 新しいスレッドを作成するには、... phantom_switch_context()から戻ることができる必要があります!



これは、スタックポインターを新しいスレッドに切り替えた時点でphantom_switch_context()内にあるときに発生する画像など、新しいスレッドのスタック上で "収集"する必要があることを意味します。 この場合、phantom_switch_context()からの戻りアドレスは、最初に、スレッドの切り替え後にphantom_thread_switch()が行うこと、次にスレッドの初期化を完了し、最後に実行する必要がある関数を呼び出す関数のアドレスでなければなりません。新しいスレッドの一部として。



この記事では、プリエンプティビティ自体は考慮しませんでした。古いスレッドからプロセッサがどのように「離脱」するか、そしていつ誰がphantom_thread_switch()を呼び出すかを正確に考慮していません。 しかし、これは別の記事です。



All Articles