最初からオペレーティングシステム。 レベル3(古い半分)

このパートでは、割り込み処理を追加し、スケジューラーを取り上げます。 最後に、マルチタスクオペレーティングシステムの要素があります! もちろん、これはトピックの始まりにすぎません。 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コアの状態の次の部分を保存する必要があります。









トラップフレームに保存する必要があるのはそれだけです。 例外ハンドラーを呼び出す前に、スタックに保存します。 ハンドラーがアセンブラーコードに制御を戻した後、この状態を元の状態に戻す必要があります。 必要なものをすべてスタックに配置すると、その内容は次のようになります。







トラップフレーム







この構造の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)。 そこからのものがあります:







  1. 非同期例外の場合、これはまだ実行されていないか、例外が発生した時点で完全に実行されていない最初のコマンドのアドレスです。
  2. 同期例外(システムコールを除く)の場合、これはこの例外を生成する命令のアドレスです。
  3. 例外をスローする命令の場合、これは例外をスローするステートメントに続く命令のアドレスです。


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つのプロセスがフリーズした場合、残りのプロセスを引き続き実行する必要があります。 したがって、プロセスは分離を意味します。 それらは互いにある程度独立して機能します。 おそらくこれらのプロパティはすべて毎日表示されます。ブラウザがフリーズした場合、残りは引き続き機能するのでしょうか、それともフリーズしますか?







いずれにせよ、プロセスの実装は、信頼できないコードとデータの保護、分離、実行、管理のための構造とアルゴリズムの作成から成ります。







プロセスの内部には何がありますか?



プロセスを実装するには、コードを追跡し、データとあらゆる種類のサポート情報を処理する必要があります。 これにより、プロセスの状態を簡単かつ自由に制御し、プロセスを相互に分離できます。 これはすべて、追跡する必要があることを意味します。









スタック、ヒープ、およびコードは、プロセスの物理的な状態全体を構成します。 プロセスの分離、制御、および保護を確保するには、残りの状態が必要です。







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



.









, . :







  1. trap frame trap_frame



    .
  2. trap frame trap_frame



    .
  3. , , .


. . , ?







, , . , . . . trap_frame



. trap frame? ! 2 trap_frame



, .







( ), . . . .







, , . trap frame, context_save



, context_restore



. 1 . , , .









. , . , . () , . さらに。 Rust , .







, : // (threads). , , .







. , . , :







  1. "" trap frame .
  2. context_restore



    .
  3. EL0



    .


, , .







.



, ( , ) , . , , . , , , .


実装



kmain.rs



SCHEDULER



GlobalScheduler



, Scheduler



. kernel/src/process/scheduler.rs



. SCHEDULER



.







, , start()



GlobalScheduler



. — start()



. :







  1. extern



    - , .


    . , . , .
  2. Process



    trap frame.


    trap frame, context_restore



    . , extern



    -. . EL0



    .
  3. 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 — , . . .







, :







int-chain







. , , , .







?



— , , . . , .



/ . , .




. , . , .









, 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 .







:









State



kernel/src/process/state.rs



. State



, . , Waiting



, , , .







round-robin . C



, - 3 5 .







round-robin







:







  1. : B



    , C



    , D



    , : A



    . C



    , , . A



    , .
  2. B



    . .
  3. C



    , , . . C



    D



    . D



    , .
  4. . A



    , A



    .
  5. B



    .
  6. 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 . :







  1. Process::is_ready()



    kernel/src/process/process.rs





    mem::replace() .
  2. Scheduler



    kernel/src/process/scheduler.rs



    .


    switch()



    , , , . . , , wfi



    (wait for interrupt). , , . aarch64.rs



    .
  3. ** GlobalScheduler::start()



    .

    , . , .
  4. .

    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- . :









, 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| { //  `p` });
      
      





Rust .







sleep <ms>



. ms



( ).







, sleep



. , , . . , , . — .







:



sleep



.



, .



u32



FromStr .





, . . , . . . , .








All Articles