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

このパートでは、Rustのスキルを向上させ、有用なユーティリティとライブラリをいくつか作成します。 GPIO、UART、組み込みタイマー用のドライバーを作成します。 XMODEMプロトコルを実装します。 これらすべてを使用して、単純なシェルとブートローダーを作成します。 読む前に、 を読むことを強くお勧めします。 少なくとも最初から最後まで。 怠zyな、しかしもう少し経験を積んだ人には、 これをお勧めできます。 ロシア語では、 ここで掘ることができます







もちろん、 ゼロレベルをバイパスすることまったく価値がありません。 また、この部分の半分はラズベリーを必要としません。







便利な資料





フェーズ0:はじめに



念のため、コース互換のファームウェアとハ​​ードウェアを使用していることをもう一度確認してください。









さらに、次のプログラムをインストールする必要があります: git



wget



tar



screen



make



など、 ゼロレベルに必要なすべてのもの。 このパートでは、 socat



をインストールする必要があります。







前回Windowsで必要なすべてを実行できた場合、今回はすべてが動作するはずです。 しかし、突然サポートがない場合。 私もオリジナルの作者も、Vendaを手に入れることも、掘り下げたいという欲求も持っていません。







コード検索



この部分のコードを次のように複製できます。







 git clone https://web.stanford.edu/class/cs140e/assignments/1-shell/skeleton.git 1-shell
      
      





自分でリポジトリの内容を自由に探索してください。







ご質問



このラボおよび以下のすべてのラボでは、 質問があります 。 あなたはネタバレを使用してコメントで直接答えることができます。 以下に例を示します。







他のGPIOピンをどのように構成して使用しますか? [assignment0]



前回は、点滅するLEDの名前に16ピンGPIOを使用しました。 レジスタGPFSEL1



GPSET0



およびGPCLR0



ます。 ピン27を使用する場合、どのレジスタが便利ですか? そして、この27 GPIOピンの物理的な接触は何ですか?

角括弧は、 questions/



ディレクトリ内のファイル名を示します。 コメントで回答する必要があるため、これは私たちにとって特に重要ではありません。 自分が答えたと確信できるまで、他人の答えを読まないでください。 そうでなければ、面白くありません。 ただし、これらのタグはスポイラーのヘッダーとして使用できます。 ただし、これらのファイルに最初に書き込むことをお勧めします。 便宜上。







フェーズ1:観覧車(しゃれ)





(ラスタについての十分な知識が既にある場合、この部分は完全にスキップできます。)







トレーニングのために、わがままな目的のためにRustのプログラムを編集します。 一部は編集後にコンパイルする必要があります。 その他コンパイルしないでください 。 3番目の場合、テストは正常に完了する必要があります。







ferris-wheel/



ディレクトリの腸では、以下を見つけることができます:









test.sh test.sh



はまだありtest.sh



。 彼はタスクの正しさをチェックします。 実行すると、予想どおりではない場所と内容が非常に一般的に説明されます。 次のようなもの:







 $ ./test.sh ERROR: compile-pass/borrow-1.rs failed to compile! ERROR: compile-pass/const.rs failed to compile! ... 0 passes, 25 failures
      
      





さらに、スクリプトは-v



フラグを受け入れます。 このフラグがスクリプトに渡されると、コンパイラーが吐き出すエラーが表示されます。







 $ ./test.sh -v ERROR: compile-pass/borrow-1.rs failed to compile! ---------------------- stderr -------------------------- warning: unused variable: `z` --> /.../ferris-wheel/compile-pass/borrow-1.rs:9:9 | 9 | let z = *y; | ^ | = note: #[warn(unused_variables)] on by default = note: to avoid this warning, consider using `_z` instead error[E0507]: cannot move out of borrowed content --> /.../ferris-wheel/compile-pass/borrow-1.rs:9:13 | 9 | let z = *y; | ^^ | | | cannot move out of borrowed content | help: consider using a reference instead: `&*y` error: aborting due to previous error ... 0 passes, 25 failures
      
      





このスクリプトは、文字列をフィルターとしても使用します。 使用可能な場合、このフィルターに一致するファイルパス($ディレクトリ/ $ファイル名)のみがチェックされます。 例:







 ./test.sh trait ERROR: compile-pass/trait-namespace.rs failed to compile! ERROR: run-pass/trait-impl.rs failed to compile! 0 passes, 2 failures
      
      





一方が他方に干渉することはなく、フィルターと-v



組み合わせることができます。 このようなもの: ./test.sh -v filter









どのくらい変更できますか?



各ファイルにはコメントが含まれており、コメントがどの程度損なわれる可能性があるかを示します(差分予算)。 つまり プログラムを修正するために行うことができる変更の最大数。 このフレームワークに適合しない決定は、失敗したとみなすことができます。







例として。 ファイルcompile-pass/try.rs



コメントがあります。







 // FIXME: Make me compile. Diff budget: 12 line additions and 2 characters.
      
      





追加できるコードは12行までです(空の行も考慮されます)。 そして、2文字を変更 (追加/変更/削除)します。 git diff



を使用して、行ごとの変更を確認できます。 そして、 git diff --word-diff-regex=.



同じですが、文字ごとに。







別の例:







 // FIXME: Make me compile! Diff budget: 1 line.
      
      





kakbeは、コードの1行のみを変更 (追加/変更/拡張)できることを示しています。







一般規則



変更後、プログラムの意図された機能は保持されるはずです。 コンパイルするために特定の関数の本体を変更する必要がある場合、 unimplemented!()



を追加するだけでは不十分だとしましょうunimplemented!()



。 疑わしい場合は、できる限りのことを試してください。 さて、またはコメントでお願いします。







これに加えて、次のダーティメソッドを実行することは完全に推奨されません。









すべてのタスクが完了すると、 test.sh



25 passes, 0 failures



を出力します







ヒント:ファイル名にはソリューションのキーが含まれている場合があります。



ヒント: この居心地の良いチャットルームでは 、Rustに関する質問にすばやく回答できます 。 この記事のコメントよりも速い。



どうした 修理は何でしたか? なぜ機能するのですか? [ファイル名]



この部分の各プログラムについて、ソースコードの何が問題であったかを説明する必要があります。 次に説明する ハードコア どのような変更が行われたか、これらの修正がなぜ汚い仕事をするのか。 適切な説明を歓迎します。 あなたはすべてがあなたにとても明白であると思うなら、あなたは書くことができません。 怠lazなら-あなたはまったく何も書くことができません。


フェーズ2:酸化





この段階では、コマンドライン用にいくつかのライブラリと1つのユーティリティを作成します。 サブディレクトリstack-vec



volatile



ttywrite



およびxmodem



ttywrite



xmodem



。 また、壊れていない場合に回答できる質問がいくつかあります。 各部分は貨物によって管理されています。 少なくともこれらのコマンドは便利と呼ぶことができます:









Cargoについては、別の小冊子Cargo Bookがあります。 そこから、すべてが詳細に機能する方法について必要な情報を学ぶことができます。







サブフェーズA:StackVec



オペレーティングシステムが扱う最も重要な機能の1つは、メモリの割り当てです。 C、Rust、Java、Python、またはほぼすべてのアプリケーションが一般的にmalloc()



呼び出す場合、十分なメモリがない場合、システムコールが最終的に使用され、オペレーティングシステムに追加のメモリが要求されます。 オペレーティングシステムは、まだメモリが誰も占有していないかどうかを判断します。 その場合、このメモリからOSがプロセッサに少し振りかけます。







メモリ割り当て- 非陰茎



すべてのlinupsなどの最新のOSには、メモリ管理に関連する多くのトリックが含まれています。 たとえば、最適化松葉杖の順序で、一定量のメモリが要求されると、仮想的に割り当てられます。 この場合、アプリケーションがこのメモリを使用しようとするまで、物理メモリは割り当てられません。 一方、単純化された配布の錯覚は、アプリケーションに対して作成されます。 オペレーティングシステムは見事に嘘をつくことができます(

内部のVec



String



およびBox



などの構造体は、 malloc()



を使用して独自のニーズに合わせてメモリを割り当てます。 つまり、これらの構造にはオペレーティングシステムのサポートが必要です。 特に、OSがメモリを割り当てる方法を知っている必要があります。 このパートはまだ始まっていません(次のシリーズを参照)ので、メモリ管理は一切ありません。 したがって、これらのすべてのVec



(まだ)使用することはできません。







これはVec



集中的な混乱であり、あらゆる点で適切な抽象化です! これにより、あらゆる種類の微妙な点を覚える必要なく、 .push()



および.pop()



観点から考えることができます。 完全なメモリアロケータなしでVec



似たものを取得できますか?









もちろん。 最初に思い浮かぶのは、メモリの予備割り当てと、その上に必要な抽象化を実装する特定の構造へのその後の転送です。 バイナリファイルに直接、またはスタックのどこかにメモリを静的に割り当てることができます。 どちらの場合も、そのようなメモリは、事前に決められた固定サイズでなければなりません。







このサブフェーズでは、 StackVec



を実装しStackVec



。これは、 Vec



が標準ライブラリから提供するAPIと同様のAPIを提供します。 ただし、事前に割り当てられたメモリを使用します。 この同じStackVec



、コマンドラインを実装するときに役立ちます(フェーズ3)。 stack-vec



サブディレクトリで作業しstack-vec



。 その中には、すでに次のものがあります。









StackVec



インターフェース



StackVec<T>



は、 StackVec::new()



呼び出すことによって作成されます。 f-ii new



引数として、タイプT



スライスT



ます。 StackVec<T>



は、 Vec



類似のメソッドとほぼ同じ方法で使用される多くのメソッドを実装します。 たとえば、 StackVec<u8>



ます。







 let mut storage = [0u8; 1024]; let mut vec = StackVec::new(&mut storage); for i in 0..10 { vec.push(i * i).expect("can push 1024 times"); } for (i, v) in vec.iter().enumerate() { assert_eq!(*v, (i * i) as u8); } let last_element = vec.pop().expect("has elements"); assert_eq!(last_element, 9 * 9);
      
      





StackVec



StackVec



既に次のように宣言されStackVec



います。







 pub struct StackVec<'a, T: 'a> { storage: &'a mut [T], len: usize, }
      
      





StackVec



理解StackVec





StackVec



デバイスに関する質問がいくつかあります。







push



Result



返すのはなぜですか?
[プッシュ失敗]



標準ライブラリにあるVec



push



メソッドには、戻り値がありません。 ただし、 StackVec



からのpush



StackVec



ようなものがあります。何らかのエラーがある可能性があることを示す結果を返します。 Vec



と違ってStackVec::push()



失敗するのはなぜですか?



T



生涯に制限する必要があるのはなぜですか?
[寿命]



コンパイラは、このStackVec



を拒否しStackVec





 struct StackVec<'a, T> { buffer: &'a mut [T], len: usize }
      
      







T



に制約を追加すると、すべてが機能します。

 struct StackVec<'a, T: 'a> { buffer: &'a mut [T], len: usize }
      
      







なぜこの制限が必要なのですか? Rustがこの制限に従わないとどうなりますか?



StackVec



pop



メソッドにT: Clone



必要なのStackVec



なぜですか?
[clone-for-pop]



Vec<T>



標準ライブラリのpop



メソッドはT



実装されますが、 StackVec



pop



メソッドはT



Clone



プロパティを実装する場合にのみ実装されます。 なぜそうなのでしょうか? この制限を克服するとどうなりますか?


StackVec



実装



stack-vec/src/lib.rs



すべてのunimplemented!()



メソッドをStackVec



stack-vec/src/lib.rs



。 各メソッドにはすでにドキュメントがあります(たとえば、そこから何が必要かは明確です)。 これに加えて、 src/tests.rs



ファイルには、実装が正しいことを確認するのに役立つテストがあります。 cargo test



コマンドを使用してテストを実行できます。 さらに、 StackVec



クラスのIntoIterator



DerefMut



およびIntoIterator



を実装する必要があります。 そして&StackVec



IntoIterator IntoIterator



。 これらの特性を実装しないと、テストは失敗します。 実装が正しいことを確認し、質問に答えられるようになったらすぐに、次のサブフェーズに進みます。







どのテストにDeref



実装が必要ですか?
[deref-in-tests]



str/tests.rs



からすべてのテストコードを読み取りstr/tests.rs



Deref



実装がない場合、どのテストをコンパイルしたくないのですか? DerefMut



どうですか? なんで?



実際、テストは完了していません



提供される単体テストは基本的な機能をカバーしていますが、くしゃみをすべてテストするわけではありません。 そのようなスペースを探し、偉大な正義の名の下に、テストの神にさらにテストを追加してください。



ヒント:ゼロフェーズliftime



ジョブからのソリューションが役立つ場合があります。


サブフェーズB: volatile







このパートでは、揮発性メモリアクセスについて説明し、 volatile/



サブディレクトリのコードを読み取ります。 独自のコードを書くことはしませんが、セルフテストには疑問があります。







典型的なオペレーティングシステムと同様に、コンパイラは非常に巧妙なトリックを巧みに実行します。 最適化の名において、彼らはあなたが意図したもののように見える何かをしています。 実際、内部には非常に強力な魔術があります。 そのような魔術の良い例は、デッドコードの削除です。 コンパイラがコードが実行に影響を与えないことを証明できる場合、デッドコードは迅速かつ断定的に切り取られます。 そのようなコードがあるとしましょう:







 fn f() { let mut x = 0; let y = &mut x; *y = 10; }
      
      





コンパイラーは、記録後に*y



読み取られないという少し合理的な理由を考えるかもしれません。 このため、コンパイラは結果のバイナリファイルからこの部分を単純に除外できます。 このように議論を続けると、コンパイラはy



自体の宣言を切断してからx



を切断することが適切であると判断します。 最後に、 f()



への呼び出しはナイフの下に行きます。







この種の最適化は非常に便利で価値があります。 それらのおかげで、プログラムは結果に影響を与えることなく加速されます。 確かに、場合によっては、そのような詐欺は予期せぬ結果をもたらす可能性があります。 たとえば、 y



は書き込みのみ可能なレジスタを指します。 この場合、 *y



への書き込みは、 *y



を読み取らなくても、かなり観察可能な効果があります。 コンパイラがこれを知らない場合、最適化段階でこの部分を取得するだけで、プログラムは期待どおりに動作しません。







このようなものの読み取り/書き込みが、居心地の良い世界自体に影響を与えることをコンパイラーにどのように納得させることができますか? これはまさに、揮発性メモリアクセスの意味です。 コンパイラは、そのようなサイトへのアクセスを最適化しないことを誓います。







さびたvolatile





Rustでは、 write_volatile



write_volatile



を使用して、生のポインターを読み書きできます。







これらはどのような生のポインタですか?



これまで、リンクを密接に知ることができました(これは&T



および&mut T



)。 Rustのrawポインター( *const T



および*mut T



)は、ボローチェッカーの有効期間を追跡しない基本的に同じリンクです。 これらの生のポインターを使用した読み取り/書き込みは、CおよびC ++の愛好家でよく見られる同じ足の負傷につながる可能性があります。 Rustでは、このような操作は安全ではないと考えています。 したがって、このすべてをunsafe



タグでマークすることが必須です。 ドキュメントで生のポインタの詳細を読んでください。

write_volatile



write_volatile



毎回書くことは十分に悲しいです(うつ病によって引き起こされる迷惑なエラーにつながる可能性があるという事実に加えて)。 幸いなことに、Rustは私たちの生活をより簡単で安全にする機会を提供してくれます。 一方では、単純にvolatile



ラッパーを作成し(素敵なCのvolatile



キーワードにほとんど似ています)、すべての読み取り/書き込みがコード内に残るようにします。 ボーナスとして、読み取り専用または書き込み専用のラッパーを定義できます(ラッパーはありません。必要に応じてトランクとスピンを提供します)。







Volatile



ReadVolatile



WriteVolatile



およびUniqueVolatile





volatile/



ディレクトリ内のvolatile



(誰が考えたでしょうか?)これら4つのタイプを実装します。これらは、その名前から明らかなことを行います。 詳細はドキュメントをご覧ください。 このドキュメントを便利な形式で実際に読むには、 volatile/



ディレクトリで直接cargo doc --open



呼び出してください。







UniqueVolatile



があるのはなぜですか?
[ユニーク揮発性]



Volatile



UniqueVolatile



使用すると、揮発性メモリアクセスをUniqueVolatile



できます。 ドキュメントに基づいて、2つのタイプの違いは何ですか?

src/lib.rs



。 自分のスキルの精神でコードを読んでください。 その後(コードを読んで)、次の2つの質問に答えます。 終了方法-次のサブフェーズに進むことができます。







読み書きの制限はどのように整理されていますか? [強制]



WriteVolatile



WriteVolatile



は、それぞれポインターの読み取りと書き込みを不可能にします。 これはどのように行われますか?



通常の方法の代わりに特性を使用する利点は何ですか? [特性]



注意深く調べると、各タイプが独自のnew



メソッドを1つだけ実装していることを置き換えることができます。 他のすべてのメソッドは、 Writeable



方法でReadable



Writeable



およびReadableWriteable



実装に関連しています。 このすべての利益は何ですか? このアプローチの少なくとも2つの利点を説明してください。



read



write



安全でnew



安全でないのはなぜですか?
[安全]



read



write



が安全であると見なされるように、 new



に関して何が真実である必要がありますか? 対照的に、 read



write



安全write



はありませんwrite



代わりにnew



を安全とマークするのは安全でしょうか?

ヒント:これらすべてのメソッドのドキュメントを読んでください。

なぜnew



の使用を強制するのですか?
[pub-constructor]







タイプVolatile



が次のように宣言された場合:







 struct Volatile<T>(pub *mut T);
      
      





次に、 new



を呼び出す代わりにVolatile(ptr)



を使用してVolatile(ptr)



型の値を作成できます。 静的呼び出しnew



ラッパーを作成する使用法は何ですか?







ヒント:両方のオプションの安全性ステートメントの意味を考慮してください。







マクロは何をしますか? [マクロ]



readable!



マクロは何をしますかreadable!



writeable!



そしてreadable_writeable!





サブフェーズC: xmodem





このサブフェーズでは、XMODEMファイル転送プロトコル( xmodem/



サブディレクトリ)を実装します。 主な作業はxmodem/src/lib.rs









XMODEMは、1977年に開発された単純なファイル転送プロトコルです。 パケットチェックサム、送信キャンセル、およびエラーが発生した場合に送信を自動的に再試行する機能があります。 UARTなどのシリアルインターフェイスを介して情報を送信するために広く使用されています。 プロトコルの主要な機能はシンプルです。 wikiで詳細を読んでください: XMODEM (誰でも記事をロシア語に翻訳できます)。







プロトコル



プロトコル自体については、テキストファイル「X-Modem File Transfer Protocolの理解」で詳細に説明されています。 ここでいくつかの説明を繰り返します。







ウィキペディアからの説明に基づいて実装を行わないでください!



教育学からの説明は高レベルで有用ですが、多くの詳細はここで実装するものとは異なります。 教育学はプロトコルの概要としてのみ使用してください。

XMODEMは非常にバイナリプロトコルです。生のバイトが送受信されます。 さらに、プロトコルは半二重です。送信者または受信者はいつでもデータを送信しますが、両方を同時に送信することはありません。 最後に、これはパケットプロトコルです。データは128バイトのブロック(パケット)に分割されます。 プロトコルは、どのバイトを送信するか、いつ送信するか、何を示すか、そして後でそれらを読み取る方法を決定します。







まず、いくつかの定数を定義しましょう。







 const SOH: u8 = 0x01; const EOT: u8 = 0x04; const ACK: u8 = 0x06; const NAK: u8 = 0x15; const CAN: u8 = 0x18;
      
      





, NAK



, NAK



. , NAK



, . NAK



, .







, , . 1. (.. 255), 0.









, :







  1. SOH



  2. ( 255 - $_



    )


    • 256
  3. :

    • NAK



      , ( 10 )
    • ACK



      ,


, :







  1. SOH



    EOT





    • ,
    • EOT







    • ,
  2. (128 )


    • つまり 256


    • , NAK



    • , ACK





, , , CAN



. CAN



— .







, :







  1. EOT



  2. NAK



    ( — )
  3. EOT



  4. ACK



    ( — )


, ( EOT



):







  1. NAK



  2. EOT



    ( , )
  3. ACK





XMODEM



XMODEM . , . expect_byte



, expect_byte_or_cancel



, read_packet



write_packet



src/lib.rs



. Xmodem



: packet



started



. , .







expect_byte



expect_byte_or_cancel



. ( read_byte



write_byte



) read_packet



write_packet



. , , transmit



receive



. / . , . cargo test



. , — .







std.



std::io



. std



.



:

{read, write}_packet



33 .



io::Read io::Write (, , ).



?







, .


D: ttywrite





ttywrite



. XMODEM. xmodem



. ttywrite/src/main.rs



. test.sh



. - socat



.







?



, . . , . UART, .



TTY?

TTY — (TeltTYpe writer). , . ( ) . /dev/



, tty.




ttywrite



- . structopt , clap . , , Cargo.toml



. structopt . , , structopt .







, , --help



. , cargo run



--



. : cargo run -- --help



. . main.rs



. Opt



. .







, ? [invalid]



. -f



idk



. structopt



, ?

, . . , .









main



serial::open . open



serial , . open



TTYPort , / / ( io::Read



io::Write



). ( SerialDevice



).









ttywrite



. , , opt



main



. , stdin



. . . -r



, . , xmodem



. ( ).







XMODEM Xmodem::transfer



Xmodem::transmit_with_progress



. transmit_with_progress



. :







 fn progress_fn(progress: Progress) { println!("Progress: {:?}", progress); } Xmodem::transmit_with_progress(data, to, progress_fn)
      
      





test.sh



ttywrite



. :







 Opening PTYs... Running test 1/10. wrote 333 bytes to input ... Running test 10/10. wrote 232 bytes to input SUCCESS
      
      









stdin



io::stdin() .



io::copy() .



main()



35 .



TTYPort .





test.sh



-r



?
[bad-test]



-r



. XMODEM. , ? XMODEM ? ?





UPD . . .








All Articles