この質問の助けを借りて、知識のギャップを学び、埋めたいと思います。
したがって、ユーザーはスレッド(カーネルレベル)を実行しており、yield
(私が推測するシステムコール)を呼び出すようになりました。スケジューラは、現在のスレッドのコンテキストをTCB(カーネルのどこかに格納されている)に保存し、実行する別のスレッドを選択してコンテキストをロードし、CS:EIP
にジャンプする必要があります。物事を絞り込むために、x86アーキテクチャ上で動作するLinuxで作業しています。今、私は詳細に入りたい:
そのため、最初にシステムコールがあります。
1)yield
のラッパー関数は、システムコール引数をスタックにプッシュします。リターンアドレスをプッシュし、システムコール番号を何らかのレジスター(たとえばEAX
)にプッシュして割り込みを発生させます。
2)割り込みはCPUモードをユーザーからカーネルに変更し、割り込みベクターテーブルにジャンプし、そこからカーネル内の実際のシステムコールにジャンプします。
3)スケジューラは今呼び出され、現在の状態をTCBに保存する必要があると思います。これが私のジレンマです。スケジューラは、その操作を実行するためにユーザースタックではなくカーネルスタックを使用するため(SS
およびSP
を変更する必要がある)、ユーザーの状態をどのように保存しますかプロセス内のレジスタを変更します。フォーラムで、状態を保存するための特別なハードウェア指示があることを読みましたが、スケジューラはどのようにしてそれらにアクセスし、誰がいつこれらの指示を実行しますか?
4)スケジューラーは状態をTCBに保存し、別のTCBをロードします。
5)スケジューラーが元のスレッドを実行すると、コントロールはラッパー関数に戻り、スタックをクリアしてスレッドが再開します。
副次的な質問:スケジューラーは、カーネルのみのスレッド(つまり、カーネルコードのみを実行できるスレッド)として実行されますか?各カーネルスレッドまたは各プロセスに個別のカーネルスタックがありますか?
大まかに言うと、理解すべきメカニズムは2つあります。 1つ目は、カーネルのエントリ/終了メカニズムです。これにより、実行中の単一のスレッドが、ユーザーモードコードの実行から、そのスレッドのコンテキストでのカーネルコードの実行に切り替えられます。 2つ目はコンテキスト切り替えメカニズム自体であり、1つのスレッドのコンテキストでの実行からカーネルモードでの切り替えを行います。
したがって、スレッドAがsched_yield()
を呼び出してスレッドBに置き換えられると、次のようになります。
各ユーザースレッドには、ユーザーモードスタックとカーネルモードスタックの両方があります。スレッドがカーネルに入ると、ユーザーモードスタックの現在の値(_SS:ESP
_)と命令ポインター(_CS:EIP
_)がスレッドのカーネルモードスタックに保存され、CPUがカーネルに切り替わります-modeスタック-_int $80
_ syscallメカニズムを使用すると、これはCPU自体によって実行されます。その後、残りのレジスタ値とフラグもカーネルスタックに保存されます。
スレッドがカーネルからユーザーモードに戻ると、レジスタ値とフラグがカーネルモードスタックからポップされ、ユーザーモードスタックと命令ポインター値がカーネルモードスタックに保存された値から復元されます。
スレッドがコンテキストを切り替えると、スケジューラーが呼び出されます(スケジューラーは個別のスレッドとして実行されるのではなく、常に現在のスレッドのコンテキストで実行されます)。スケジューラコードは、次に実行するプロセスを選択し、switch_to()
関数を呼び出します。この関数は、基本的にカーネルスタックを切り替えるだけです。スタックポインターの現在の値を現在のスレッドのTCB(Linuxでは_struct task_struct
_と呼びます)に保存し、次のTCBから以前に保存したスタックポインターを読み込みます糸。この時点で、カーネルが通常使用しない他のスレッド状態(浮動小数点/ SSEレジスタなど)を保存および復元します。切り替えられるスレッドが同じ仮想メモリ空間を共有しない場合(つまり、異なるプロセスにある場合)、ページテーブルも切り替えられます。
そのため、スレッドのコアユーザーモード状態は、コンテキスト切り替え時に保存および復元されないことがわかります。カーネルに入ったり出たりすると、スレッドのカーネルスタックに保存および復元されます。コンテキストスイッチコードは、ユーザーモードのレジスタ値を上書きすることを心配する必要はありません。これらの値は、その時点までにカーネルスタックに安全に保存されています。
ステップ2で見逃したのは、スタックがスレッドのユーザーレベルのスタック(引数をプッシュした場所)からスレッドの保護レベルのスタックに切り替えられることです。 syscallによって中断されたスレッドの現在のコンテキストは、実際にこの保護されたスタックに保存されます。 ISR内で、カーネルに入る直前に、この保護されたスタックは、theカーネルスタックに再び切り替えられます。カーネル内に入ると、スケジューラー関数などのカーネル関数は、最終的にカーネルスタックを使用します。後で、スレッドがスケジューラによって選択され、システムがISRに戻り、カーネルスタックから新しく選択された(または、より高い優先度のスレッドがアクティブでない場合は前者)スレッドの保護レベルスタックに戻ります。新しいスレッドコンテキスト。したがって、コンテキストはコードによってこのスタックから自動的に復元されます(基盤となるアーキテクチャによって異なります)。最後に、特別な命令により、スタックポインターや命令ポインターなどの最新の扱いにくいレジスタが復元されます。ユーザーランドに戻る...
要約すると、スレッドには(通常)2つのスタックがあり、カーネル自体には1つのスタックがあります。カーネルスタックは、カーネルに入るたびに消去されます。 2.6以降、カーネル自体が何らかの処理のためにスレッド化されるため、カーネルスレッドは一般的なカーネルスタックの横に独自の保護レベルのスタックを持っていることを指摘するのは興味深いことです。
一部のリソース:
この助けを願っています!
カーネル自体にはスタックがありません。プロセスについても同じことが言えます。また、スタックもありません。スレッドは、実行単位と見なされるシステム市民のみです。このため、スレッドのみをスケジュールでき、スレッドのみがスタックを持っています。ただし、カーネルモードコードが大きく悪用する点が1つあります。すべての瞬間システムは、現在アクティブなスレッドのコンテキストで機能します。このため、カーネル自体は現在アクティブなスタックのスタックを再利用できます。カーネルコードまたはユーザーコードのいずれかと同時に実行できるのはそのうちの1つだけであることに注意してください。このため、カーネルが呼び出されると、スレッド内の中断されたアクティビティに制御を戻す前に、スレッドスタックを再利用してクリーンアップを実行するだけです。同じメカニズムが割り込みハンドラーでも機能します。同じメカニズムがシグナルハンドラによって利用されます。
次に、スレッドスタックは2つの分離された部分に分割され、一方はユーザースタックと呼ばれ(スレッドはユーザーモードで実行されるときに使用される)、もう一方はカーネルスタックと呼ばれます(スレッドがカーネルモードで実行されると使用されるため) 。スレッドがユーザーモードとカーネルモードの境界を越えると、CPUは自動的に1つのスタックから別のスタックに切り替えます。両方のスタックは、カーネルとCPUによって別々に追跡されます。カーネルスタックの場合、CPUはスレッドのカーネルスタックの最上部へのポインターを常に念頭に置いています。このアドレスはスレッドに対して一定であるため、簡単です。スレッドは、カーネルに入るたびに空のカーネルスタックを見つけ、ユーザーモードに戻るたびにカーネルスタックをクリーニングします。同時に、スレッドがカーネルモードで実行されている場合、CPUはユーザースタックの最上位へのポインターを意識していません。代わりに、カーネルに入るときに、CPUはカーネルスタックの上部に特別な「割り込み」スタックフレームを作成し、そのフレームにユーザーモードスタックポインターの値を格納します。スレッドがカーネルを終了すると、CPUはESPのクリーンアップの直前に以前に作成された「割り込み」スタックフレームからの値を復元します。(レガシーx86では、int/iretハンドルの入力と終了のペアカーネルモードから)
カーネルモードに入る間、CPUが「割り込み」スタックフレームを作成した直後に、カーネルは残りのCPUレジスタの内容をカーネルスタックにプッシュします。 isは、カーネルコードで使用できるレジスタの値のみを保存することに注意してください。たとえば、カーネルはSSEレジスタに触れないという理由だけでレジスタを保存しません。同様に、CPUにユーザーモードに制御を戻すように要求する直前に、カーネルは以前に保存したコンテンツをレジスタ。
WindowsやLinuxなどのシステムでは、システムスレッド(よくカーネルスレッドと呼ばれますが、混乱を招くことを知っています)という概念があります。システムスレッドは、カーネルモードでのみ実行され、これによりスタックのユーザー部分がないため、一種の特別なスレッドです。カーネルは、補助的なハウスキーピングタスクにそれらを使用します。
スレッド切り替えは、カーネルモードでのみ実行されます。つまり、送信スレッドと受信スレッドの両方がカーネルモードで実行され、両方とも独自のカーネルスタックを使用し、両方のカーネルスタックにユーザースタックの最上部へのポインターを持つ「割り込み」フレームがあります。スレッド切り替えの重要なポイントは、次のような単純なスレッドのカーネルスタック間の切り替えです。
pushad; // save context of outgoing thread on the top of the kernel stack of outgoing thread
; here kernel uses kernel stack of outgoing thread
mov [TCB_of_outgoing_thread], ESP;
mov ESP , [TCB_of_incoming_thread]
; here kernel uses kernel stack of incoming thread
popad; // save context of incoming thread from the top of the kernel stack of incoming thread
カーネルにはスレッド切り替えを実行する関数が1つしかないことに注意してください。これにより、カーネルがスタックを切り替えるたびに、スタックの最上部で着信スレッドのコンテキストを見つけることができます。スタックが切り替わる前に毎回、カーネルが発信スレッドのコンテキストをスタックにプッシュするからです。
また、スタックの切り替え後、ユーザーモードに戻る前に、カーネルはカーネルスタックのトップの新しい値によってCPUの心をリロードすることに注意してください。これにより、新しいアクティブスレッドが将来カーネルに入るときに、CPUによって独自のカーネルスタックに切り替えられるようになります。
また、スレッドの切り替え中にすべてのレジスタがスタックに保存されるわけではなく、FPU/MMX/SSEなどの一部のレジスタは、発信スレッドのTCBの特別に専用の領域に保存されます。カーネルは、2つの理由でここで異なる戦略を採用しています。まず、システム内のすべてのスレッドがそれらを使用するわけではありません。すべてのスレッドのコンテンツをスタックにプッシュしたり、スタックからポップしたりするのは非効率的です。 2つ目は、コンテンツの「高速」保存と読み込みのための特別な指示があります。そして、これらの命令はスタックを使用しません。
また、実際には、スレッドスタックのカーネル部分のサイズは固定されており、TCBの一部として割り当てられます。 (Linuxに当てはまり、Windowsにも当てはまります)