このパートでは、割り込み処理を追加し、スケジューラーを取り上げます。 最後に、マルチタスクオペレーティングシステムの要素があります! もちろん、これはトピックの始まりにすぎません。 1つのタイマー割り込み、1つのシステムコール、単純なスレッドスケジューラの基本部分。 複雑なことは何もありません。 ただし、「真似」をせずに最も実際のプロセスを処理する本格的なシステムを作成するための踏み台を準備します。 これらのあなたのlinupsや他の人と同じように。 このコースの終わりまでに、半分以下がすでに残っています。
ゼロラボ
最初のラボ: 若い半分と古い半分
2番目のラボ: 若い半分と古い半分
サードラボ: ヤングハーフ
サブフェーズE:例外からの戻り
このサブフェーズでは、あらゆる種類、形状、色の例外ハンドラーから戻るコードを記述します。 主な作業はkernel/ext/init.Sとkernel/src/trapsフォルダーで実行されます。
復習
handle_exceptionから無限ループを削除しようとすると、ほとんどの場合、Raspberry Piは例外ループに入ります。 つまり 誤って処理された例外は何度も発生し、場合によってはデバッグシェルがクラッシュします。 これはすべて、例外ハンドラーがコードが実行されたポイントに戻ろうとすると、プロセッサーの状態(特にレジスター内のデータ)が、このコードで何が起こっているかを考慮せずに変化したという事実によるものです。
たとえば、次のコードを検討してください。
1: mov x3, #127 2: mov x4, #127 3: brk 10 4: cmp x3, x4 5: beq safety 6: b oh_no
brk例外が発生すると、例外ベクトルが呼び出され、最終的にhandle_exceptionます。 Rustによってコンパイルされたこの同じhandle_exception関数は、とりわけ、ダーティトリックのためにレジスタx3およびx4を使用します。 例外ハンドラーがbrk呼び出し場所に戻ると、 x3とx4状態は予想されるものとは完全に異なります。 したがって、5行目のbeqでは、正しい状態が保証されません。 コードはsafetyにジャンプするかもしれませんし、そうでないかもしれません。
その結果、例外ハンドラーがその裁量でプロセッサーの状態全体を使用するためには、このハンドラーが作業を開始する前に、処理コンテキスト全体(レジスターなど)を保存しておく必要があります。 ハンドラーがその神聖な使命を完了した後、以前に保存されたコンテキストを復元する必要があります。 すべては、外部コードが完璧に機能したという事実に基づいています。 コンテキストを保存/復元するプロセスは、コンテキストスイッチと呼ばれます。
コンテキストスイッチングを行う理由
ここでは、 スイッチという言葉はあまり適切ではないようです。 同じコンテキストに戻るだけですよね?
場合によっては、そうです。 ただし、実際には、同じ実行コンテキストに戻ることはほとんどありません。 多くの場合、このコンテキストを変更して、プロセッサがあらゆる種類のさまざまな有用な処理を実行するようにします。 たとえば、異なるプロセス間の切り替えを実装する必要がある場合、あるコンテキストを別のコンテキストに置き換えます。 したがって、マルチタスクを実現します。 システムコールを実装する場合、戻り値を実装するためにレジスタの値を変更する必要があります。 ブレークポイントの場合でも、次のコマンドが実行されるようにELRレジスタを変更する必要があります(そうしないと、 brkハンドラーが何度も呼び出されます)。
このサブフェーズでは、コンテキストの維持/復元に取り組みます。 保存されたコンテキストを含む構造は、トラップフレームと呼ばれます。 未完成のTrapFrame構造は、 kernel/src/traps/trap_frame.rs 。 Rustから保存されたレジスタにアクセスするために、この構造を使用します。 一方、この構造体はアセンブラーコードで埋めます。 handle_exceptionは、 tfパラメーターを介してhandle_exception関数にこの構造体へのポインターを渡すだけです。
トラップフレーム

トラップフレームは、プロセッサコンテキスト全体を含む構造に付ける名前です。 「トラップフレーム」という名前は、「トラップ」(トラップ)という用語に由来します。これは、イベントが発生したときにプロセッサがより高い特権レベルを呼び出すメカニズムを説明する一般的な用語です。 これらすべてをロシア語で指定するのに適した用語については知りません。 この場合、英語の用語のみを使用する方が便利だと思います。
トラップフレームを作成するにはさまざまな方法がありますが、その本質は同じです。 実行に必要なすべての状態をRAMに保存する必要があります。 ほとんどの実装は、すべての状態をスタックにプッシュします。 スタックをレジスタの内容で満たした後、スタックの最上部へのポインターがトラップへのポインターになります。 引き続き使用するのはこのバリエーションです。
この時点で、Cortex-A53コアの状態の次の部分を保存する必要があります。
x0 ... x30つまり すべての64ビットレジスタ、そのうち最大31個。q0 ... q31はすべて128ビットのSIMD / FPレジスタです。pcソフトウェアカウンター。
ELR_ELxレジスタがこれを担当します。 PCである場合とそうでない場合があります。 いずれにしても、これは例外ハンドラーを実行した後に戻るべきアドレスです。 通常、 ELR_ELxにはPCが直接含まれるか、 PC + 4が含まれます。 次のコマンドのアドレス。PSTATEプロセッサステータスフラグ。
プロセッサの状態は、前のレベルELxレジスタSPSR_ELxを介して送信されることを思い出してください。spスタック境界へのポインター。
その内容は、例外レベルs SP_ELsを介してアクセスできます。TPIDR現在の「プロセスID」の64ビット値。
例外レベルs TPIDR_ELsから値を取得できます。
トラップフレームに保存する必要があるのはそれだけです。 例外ハンドラーを呼び出す前に、スタックに保存します。 ハンドラーがアセンブラーコードに制御を戻した後、この状態を元の状態に戻す必要があります。 必要なものをすべてスタックに配置すると、その内容は次のようになります。

この構造のSPおよびTPIDRに注意してください。 それらは正確にスタックポインターとソーススレッドIDであり、割り込み状態の一部ではないはずです。 EL0が唯一の可能なソースであるため、 SP_EL0およびTPIDR_EL0読み取ることで取得できます。 この場合、現在のSP (例外ベクトルによって使用される)は、トラップフレームの開始を示します。 もちろん、必要な値をこのスタックに配置した直後。
スタックに必要な値を入力しhandle_exception 、 handle_exception 3番目の引数としてスタックの最上部へのポインターを渡します。 この引数のタイプは&mut TrapFrameです。 すでに述べたように、この同じTrapFrameはkernel/src/traps/trap_frame.rs 。 この構造を追加する必要があります。
スレッドIDとは何ですか?
TPIDRレジスタ( TPIDR_ELx )により、OSは現在実行されているものに関する情報を保存できます。 後でプロセスを実装し、このレジスタにプロセス識別子を保存します。 今、このレジスタを保存して復元します。
優先例外の返信先住所
処理がELxレベルの例外が発生すると、CPUは優先戻りアドレスをELR_ELxます。 詳細はドキュメントに記載されています( ref :D1.10.1)。 そこからのものがあります:
- 非同期例外の場合、これはまだ実行されていないか、例外が発生した時点で完全に実行されていない最初のコマンドのアドレスです。
- 同期例外(システムコールを除く)の場合、これはこの例外を生成する命令のアドレスです。
- 例外をスローする命令の場合、これは例外をスローするステートメントに続く命令のアドレスです。
brk命令は2番目のカテゴリに属します。 したがって、 brkコマンドの後も実行を継続する場合は、次の命令のアドレスがELR_ELx含まれていることを確認する必要があります。 AArch64のすべての命令のサイズは32ビットなので、この値をELR_ELx + 4で上書きするだけで十分です。
実装
os/kernel/ext/init.Sからcontext_saveとcontext_restoreを実装することからos/kernel/ext/init.S 。 context_saveルーチンは、必要なすべてのレジスターをスタックにスタックしてhandle_exception 、 handle_exceptionを呼び出して、3番目の引数としてトラップフレームを含む必要なすべての引数をこの関数に渡します。 context_restoreしたらcontext_restoreルーチンに入ります。 このルーチンは、コンテキストを復元する必要があります。
HANDLERマクロによって作成された指示に注意してください。 そこでは、すでにx0とx30保存と復元が実行されます。 context_{save, restore}プロシージャで保存/復元するとき、これらのレジスタに触れないでください。 ただし、これらのレジスタはトラップフレーム内になければなりません。
コンテキストを切り替える際のパフォーマンスの損失を最小限に抑えるために、次のようにスタックから値をプッシュしてスタックする必要があります。
// `x1`, `x5`, `x12` `x13` sub SP, SP, #32 stp x1, x5, [SP] stp x12, x13, [SP, #16] // `x1`, `x5`, `x12` `x13` ldp x1, x5, [SP] ldp x12, x13, [SP, #16] add SP, SP, #32
SP常に16バイトにアライメントされていることを確認してください。 このアプローチでは、トラップフレームにreservedが作成reservedれることがわかります。 この最もreservedれているものはゼロで埋める必要があります。
これらの2つのルーチンが完了したら、 kernel/src/traps/trap_frame.rsのTrapFrame構造にTrapFrameしてkernel/src/traps/trap_frame.rs 。 フィールドの順序とサイズがcontext_save保存したものと正確に一致していることを確認し、 tfをパラメーターとして渡します。
最後に、 brk例外ハンドラーから戻る前にELR 4 ELRをhandle_exceptionに追加します。 コンテキスト切り替えを正常に実装すると、デバッグシェルを終了した後、カーネルは正常に動作するはずです。 すべての準備が整ったら、次の手順に進みます。
トラップフレームの内容はダイアグラムと完全に一致する必要はありませんが、すべて同じデータを含む必要があります。
また、 qnレジスタのサイズは128ビットであることを忘れないでください!
ヒント:
handle_exceptionを呼び出すには、トラップフレームの一部ではないレジスタの保存/復元を処理する必要があります。
Rustには、128ビット値用のu128およびi128があります。
msrおよびmsrを使用して、特殊レジスターの読み取り/書き込みを行います。
context_saveバージョンには、約45命令が必要です。
context_restoreバージョンには、約41命令が必要です。
TrapFrameは、合計サイズが800バイトの68フィールドで構成されています。
浮動小数点数のレジスタをどのように遅延処理できますか? [遅延フロート]
すべての128ビットSIMD / FPレジスタの保存と復元は非常に高価です。 これらは、 TrapFrame構造で512バイトの800バイトを占有します! これらのレジスターを例外のソースまたはコンテキストを切り替える目的で実際に使用した場合にのみ、これらのレジスターを処理することが理想的です。
AArch64アーキテクチャにより、これらのレジスタの使用を選択的に有効/無効にすることができます。 これらのレジスタが実際に使用されている場合にのみ、この機会を使用してこれらのレジスタを遅延ロードする方法はありますか? しかし同時に、これらのレジスタをコード内で自由に使用できるようにします。 例外ハンドラー用にどのようなコードを作成しますか? 追加の状態と追加方法を追加するために、 TrapFrameの構造を何らかの方法で変更する必要がありますか。 状態を維持する必要がありますか?
フェーズ2:これはプロセスです。
この部分では、最もおいしいものに移ります。 カスタムプロセスを実装します。 Processの状態を処理するProcess構造の実装から始めましょう。 次に、最初のプロセスを開始します。 その後、ラウンドロビンのようなプロセススケジューラを実装します。 これを行うには、割り込みコントローラードライバーを実装し、タイマー割り込みを有効にする必要があります。 次に、タイマー割り込みが発生したときにスケジューラを起動し、次のプロセスに進むためにコンテキストを切り替えます。 最後に、最初のシステムコールsleepを実装します。
このフェーズが完了すると、すでに最小限の、しかし非常に本格的なマルチタスクオペレーティングシステムが用意されます。 現時点では、プロセスはカーネルや他のプロセスと物理メモリを共有します。 ただし、すでに次のフェーズでは、この誤解に対処し、仮想メモリを実装します。 プロセスを互いに分離し、ユーザー空間プログラムの遊び心のあるライターからカーネルメモリを保護するため。
サブフェーズA:プロセス
このサブフェーズでは、 kernel/src/process/process.rsのProcessタイプの機能に必要なすべてを実装しkernel/src/process/process.rs 。 このコードはすべて、次のサブフェーズで役立ちます。
プロセスとは何ですか?
プロセスは、カーネルによって実行、管理、保護されるコードとデータのコンテナです。 実際、これはカーネルの外部にあるすべてに適用されるコードの唯一の部分です。 コードがプロセスの一部として実行されるか、コードがカーネルの一部として実行されます。 多くの異なるオペレーティングシステムアーキテクチャがありますが(特に純粋に研究に関する場合)、ほとんどすべてにユーザープロセスと見なせる概念があります。
ほとんどの場合、プロセスは限られた特権セットで実行されます(この例ではEL0 )。 すべては、カーネルがシステム全体に必要なレベルの安定性とセキュリティを提供できることを保証するためです。 プロセスの1つが故障した場合、他のプロセスが同じ運命に陥ることは望ましくありません。 さらに、この結果がシステム全体の完全な崩壊になることは望ましくありません。 さらに、プロセスが相互に干渉しないようにします。 1つのプロセスがフリーズした場合、残りのプロセスを引き続き実行する必要があります。 したがって、プロセスは分離を意味します。 それらは互いにある程度独立して機能します。 おそらくこれらのプロパティはすべて毎日表示されます。ブラウザがフリーズした場合、残りは引き続き機能するのでしょうか、それともフリーズしますか?
いずれにせよ、プロセスの実装は、信頼できないコードとデータの保護、分離、実行、管理のための構造とアルゴリズムの作成から成ります。
プロセスの内部には何がありますか?
プロセスを実装するには、コードを追跡し、データとあらゆる種類のサポート情報を処理する必要があります。 これにより、プロセスの状態を簡単かつ自由に制御し、プロセスを相互に分離できます。 これはすべて、追跡する必要があることを意味します。
- スタック
各プロセスには、固有のスタックが必要です。 プロセスを実装するとき、プロセススタックとしての使用に適したメモリセクションを割り当てる必要があります。 そしてもちろん、プロセススタックへのポインタを変更して、このメモリ領域を指すようにする必要があります。 - ヒープ
動的メモリを使用するには、各プロセスが独自のヒープを割り当てる必要があります。 最初は、ヒープは完全に空ですが、特別なシステムコールを使用してヒープを拡張できます。 現時点ではこのトピックを残し、将来はこのトピックに戻ります。 - コード
プロセスは、コードを実行しない場合、実質的に役に立ちません。 したがって、カーネルはプロセスコードを何らかの方法でメモリにロードし、必要に応じてこのコードに制御を移す必要があります。 - 仮想アドレス空間
プロセスにカーネルメモリや他のプロセスのメモリにアクセスする機能を与えたくないので、各プロセスは仮想メモリなどを使用する独自のアドレス空間によって制限されます。 - スケジューラーステータス
ほとんどの場合、プロセッサコアよりも多くのプロセスがあると想定しています。 カーネルは、一度に1つのスレッドの命令しか実行できません。 したがって、プロセスの同時実行のために、CPU時間を多重化するメカニズムが必要です(したがって、コマンドのスレッドがいくつかあります)。 スケジューラーのタスクは、どのプロセスが開始し、どの時点でこれがすべて起こるかを決定することです。 これを正しく行うために、スケジューラは、プロセスの計画準備ができているかどうかを知る必要があります。 各プロセスに保存されているスケジューラの状態はまったく同じです。 - 実行状況
複数のプロセス間でプロセス時間を正しく多重化するために、このプロセスを停止するときにプロセスの状態を保存する必要があります。 さて、このプロセスをオンに戻した時点での状態の正しい復元を忘れないでください。 実際、この状態を処理するために必要なことはすべてすでに済ませています。 これを行うには、 TrapFrameを作成する必要がありました。 各プロセスはこの状態を適切に保存する必要があります。
スタック、ヒープ、およびコードは、プロセスの物理的な状態全体を構成します。 プロセスの分離、制御、および保護を確保するには、残りの状態が必要です。
kernel/src/process/process.rsのProcess構造には、このすべての情報が含まれます。 現在(このフェーズでは)すべてのプロセスは共有メモリを使用し、コード、ヒープ、または仮想アドレススペースのフィールドはありません。 しかし、それらは少し後で追加します。
プロセスはカーネルを信頼する必要がありますか? [カーネル不信]
一般に、コアがプロセスに不信感を抱くべきであることは明らかです。 しかし、プロセスはカーネルを信頼する必要がありますか? もしそうなら、プロセスはカーネルに何を期待すべきですか?
2つのプロセスがスタックを共有している場合、何が問題になる可能性がありますか? [分離スタック]
同じスタックを共有する2つのプロセスが同時に実行されているとします。 最初に:スタックの同時使用は何を意味しますか? 第二に、なぜこれら2つのプロセスが互いに干渉し、互いに迅速に破壊する可能性が高いのですか? 3番目:単一のスタックが分割された場合に、2つのプロセスを静かに共存させるために必要なプロセスのプロパティを決定します。 言い換えれば、死なずに同じスタックを使用するために、このような2つのプロセスが従わなければならないルールは何ですか?
実装
kernel/src/process/process.rsからProcess必要なすべてを実装する時が来ました。 , , Stack , kernel/src/process/stack.rs . , , . State , , . kernel/src/process/state.rs . , .
Process::new() . . ! — .
? [stack-drop]
Stack 1MiB . 16 . , , , ?
? [lazy-stacks]
Stack 1MiB . . , , ?
? [stack-size]
. 1MiB. , , ? , , ?
B:
( EL0 ). kernel/src/process/scheduler.rs kernel/src/kmain.rs .
, . :
- trap frame
trap_frame . - trap frame
trap_frame . - , , .
. . , ?
, , . , . . . trap_frame . trap frame? ! 2 trap_frame , .
( ), . . . .
, , . trap frame, context_save , context_restore . 1 . , , .
. , . , . () , . さらに。 Rust , .
, : // (threads). , , .
. , . , :
- "" trap frame .
context_restore .EL0 .
, , .
.
, ( , ) , . , , . , , , .
実装
kmain.rs SCHEDULER GlobalScheduler , Scheduler . kernel/src/process/scheduler.rs . SCHEDULER .
, , start() GlobalScheduler . — start() . :
extern - , .
. , . , .Process trap frame.
trap frame, context_restore . , extern -. . EL0 .context_restore , eret EL0 .
trap frame . :
context_restore .
: . . , , context_restore , , .- (
sp ) ( _start ). , EL1 . : ldr adr sp . , sp . 0 . .EL0 eret .
. , tf trap frame, x0 , x1 :
unsafe { asm!("mov x0, $0 mov x1, x0" :: "r"(tf) :: "volatile"); }
— SCHEDULER.start() kmain . kmain . . , extern - EL0 .
, , . brk extern - :
extern fn run_shell() { unsafe { asm!("brk 1" :::: "volatile"); } unsafe { asm!("brk 2" :::: "volatile"); } shell::shell("user0> "); unsafe { asm!("brk 3" :::: "volatile"); } loop { shell::shell("user1> "); } }
. LowerAArch64 , . , — .
:
6 .
, T Box<T> &*box .
, unsafe -.
C:

BCM2837. , . , .
os/pi/src/interrupt.rs ,
os/pi/src/timer.rs os/kernel/src/traps .
AArch64 — , . . .
, :

. , , , .
?
— , , . . , .
/ . , .
. , . , .
, CPU. , .
, , , . , , , . , , . , , .
CPU
(unmasked) , . (masked) . . , , , , . , . .
EL0 , .
IRQ IRQ? [reentrant-irq]
IRQ IRQ . , ? IRQ?
. IRQ (). handle_exception kernel/src/traps/mod.rs , handle_irq kernel/src/traps/irq.rs . , , , , . handle_irq .
実装
pi/src/interrupt.rs . 7 BCM2873 . / IRQ, Interrupt . FIQ BasicIRQ .
tick_in() pi/src/timer.rs . 12 BCM2873 . tick_in() .
TICK . GlobalScheduler::start() kernel/src/process/scheduler.rs . TICK .
handle_exception kernel/src/traps/mod.rs , handle_irq kernel/src/traps/irq.rs . handle_irq TICK , , TICK .
, TICK . LowerAArch64 , (kind) Irq . . — .
TICK !
TICK . 2 . , , , . 1 10 . TICK 10 .
D:
round-robin . kernel/src/process/scheduler.rs , kernel/src/process/process.rs kernel/src/traps/irq.rs .
計画中
, . -, CPU. . . , . .
. round-robin . . ( TICK ), . , . round-robin .
:
- Ready
, . , . - Running
, . - Waiting
, , . , , . , . つまり .
State kernel/src/process/state.rs . State , . , Waiting , , , .
round-robin . C , - 3 5 .

:
- :
B , C , D , : A . C , , . A , . B . .C , , . . C D . D , .- .
A , A . B .C . , . . C .
? [wait-queue]
round-robin : , . round-robin ? , ( / ) /?
Scheduler kernel/src/process/scheduler.rs , . Scheduler::add() . . TPIDR .
, Scheduler::switch() . new_state , trap frame , trap frame. , , , .
, , , process.is_ready() , kernel/src/process/process.rs . true , Ready , .
TICK . , . GlobalScheduler add() switch() Scheduler .
? [new-state]
scheduler.switch() , . , , . ?
実装
round-robin . :
Process::is_ready() kernel/src/process/process.rs
mem::replace() .Scheduler kernel/src/process/scheduler.rs .
switch() , , , . . , , wfi (wait for interrupt). , , . aarch64.rs .- **
GlobalScheduler::start() .
, . , . - .
SCHEDULER.switch() , .
, GlobalScheduler::start() . . ( extern -) , , . , .
, , TICK . . — .
!
unsafe !
mem::replace() state .
, ? [wfi]
wfi , , . wfi , . , ?
: , .
E: Sleep
sleep . kernel/src/shell.rs kernel/src/traps .
— , . svc #n , Svc(n) , n — , . , brk #n Brk(n) , , svc . — , , .
100 . sleep . . .
, , . , unix- . :
n svc #n .- 7
x0 ... x6 . - 7
x0 ... x6 . x7 .
- .
, 7 , u32 u64 , u64 , , :
fn syscall_7(a: u32, b: u64) -> Result<(u64, u64), Error> { let error: u64; let result_one: u64; let result_two: u64; unsafe { asm!("mov w0, $3 mov x1, $4 svc 7 mov $0, x0 mov $1, x1 mov $2, x7" : "=r"(result_one), "=r"(result_two), "=r"(error) : "r"(a), "r"(b) : "x0", "x1", "x7") } if error != 0 { Err(Error::from(error)) } else { Ok((result_one, result_two)) } }
注意してください。 , .
? [syscall-error]
unix- , Linux, ( x0 ) . . . , ? ?
Sleep
sleep 1 . u32 . , . u32 . , . :
(1) sleep(u32) -> u32
? [sleep-elapsed]
( ) , ? , , ? , ?
実装
sleep . handle_exception kernel/src/traps/mod.rs . , handle_syscall kernel/src/traps/syscalls.rs . handle_syscall . sleep . Box<FnMut> , . :
let boxed_fnmut = Box::new(move |p| {
Rust .
sleep <ms> . ms ( ).
, sleep . , , . . , , . — .
:
sleep .
, .
u32 FromStr .
, . . , . . . , .