このパートでは、割り込み処理を追加し、スケジューラーを取り上げます。 最後に、マルチタスクオペレーティングシステムの要素があります! もちろん、これはトピックの始まりにすぎません。 1つのタイマー割り込み、1つのシステムコール、単純なスレッドスケジューラの基本部分。 複雑なことは何もありません。 ただし、「真似」をせずに最も実際のプロセスを処理する本格的なシステムを作成するための踏み台を準備します。 これらのあなたのlinupsや他の人と同じように。 このコースの終わりまでに、半分以下がすでに残っています。
サードラボ: ヤングハーフ
サブフェーズ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
.
-
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
.
-
x7
0
— . -
x7
1
— . -
x7
- — .
-
- .
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 .
, . . , . . . , .