私はおもちゃのOSを書いています(スリープの実装について)



おもちゃのOSを扱うことに特化したブログの別の投稿。 前回 、シンプルなAHCI(SATA)ドライバーの必要性について書いた。 この方向に進む前に、ドライバーインフラストラクチャの概要を決めることにしました。一般的なドライバーインターフェイス+更新されたストレージデバイスドライバーインターフェイスです。 これらのインターフェイスの定式化により、以前は注意を払っていなかった問題である移植性の問題が明らかになりました。



プラットフォームに依存しないコード(たとえば、スケジューラの大部分、kprintfなどの補助コード)は、x86_64専用のコード(システム記述子テーブル、APIC、割り込みなど)と混同されます。 x86_64に緊密にバインドされたドライバーインターフェイスの作成を妨げるものは何もありませんでしたが(特に、PCIアドレスで自由に操作できます)、特定のプラットフォームに固有のコードを一般的なポータブルコードから明確に分離しなければ、状況を悪化させるだけであることが明らかになりました。 そこで、書かれたものすべてを反復処理して、一般的なコード(ルートsrc /)をプラットフォーム固有のコード(src / x86_64 /)から分離することにしました。 これは私が最後の2週間行ってきたことです。



スケジューラーの例を使用して、コード分離のメカニズムを簡単に説明します。 src / schedule.hスケジューラインターフェイスには、プラットフォーム依存の静的インライン関数(インターフェイスと内部の両方)を含む特別なsrc / x86_64 / schedule.incファイルが含まれています(#include)。 すべての内部文字(インターフェイスに関連していないが、静的ではない)には、接頭辞「__」が付いています。 メインスケジューラの実装はsrc / schedule.cにあり、個別の内部関数とアセンブラコードはsrc / x86_64 / schedule.cにあります。 したがって、スケジューラコードは2つのディレクトリに「スプレー」されます。 もちろん、この複雑さは一般的な場合に限られますが、多くのモジュールは単純化されたスキームに従って構築されます。 たとえば、cpu_info(論理プロセッサ情報)の場合、ヘッダーはsrc /にあり、実装はsrc / x86_64 /にあります。 または、完全にプラットフォーム固有のAPICを完全にsrc / x86_64 /に配置します。



約束のスリープ機能について説明します。 ミューテックスとは異なり、sleepの実装には、スケジューラの修正が必要です(最小限ではありますが)。 次の関数がインターフェース部分( src / schedule.h )に追加されました:



typedef void (*timer_proc)(uint64_t ticks); uint64_t get_ticks(void); timer_proc get_timer_proc(void); uint64_t get_timer_ticks(void); // is zeroed after triggering void set_timer_proc(timer_proc proc); // called within a timer interrupt void set_timer_ticks(uint64_t ticks); // not thread-safe
      
      





つまり 現在、スケジューラはタイマーとしても機能します。ティックカウンター(内部タイマー割り込み)を保存し、ティック数が指定数に達するとすぐにハンドラー関数を呼び出します。 このメカニズム( src / schedule.c )の実装を検討してください。



 static inline void handle_timer(int cpu) { if (cpu == get_bsp_cpu()) ticks++; if (timer_ticks[cpu] && timer_ticks[cpu] <= ticks) { uint64_t prev_ticks = timer_ticks[cpu]; timer_ticks[cpu] = 0; set_outer_spinlock(true); timer_proc_(prev_ticks); set_outer_spinlock(false); } }
      
      





handle_timer関数は、各タイマー割り込みで呼び出されます。 ティックカウンタはブートストラッププロセッサに対してのみインクリメントされるという事実にもかかわらず、タイマーは論理プロセッサごとに個別にプログラムされます。 ハンドラー内のrelease_spinlock呼び出しが誤ってSTI命令を実行しないように、set_outer_spinlockでハンドラー呼び出しをラップする必要があります(割り込みのコンテキストにいることを忘れないでください)。



スケジューラのこの高度な機能を使用して、sleep( src / sync.c )を実装できます。



 struct sleep_node { struct sleep_node *next; thread_id thread; uint64_t ticks; }; static struct sleep_data { struct sleep_node *tail; struct mem_pool pool; struct spinlock lock; } sleeping[CONFIG_CPUS_MAX]; static void sleep_timer_proc(UNUSED uint64_t ticks) { struct sleep_data *slp = &sleeping[get_cpu()]; if (slp->tail) { struct sleep_node *node = slp->tail; slp->tail = slp->tail->next; if (slp->tail) set_timer_ticks(slp->tail->ticks); resume_thread(node->thread); free_block(&slp->pool, node); } } err_code sleep(uint64_t period) { struct sleep_data *slp = &sleeping[get_cpu()]; err_code err = ERR_NONE; acquire_spinlock(&slp->lock, 0); struct sleep_node *node = alloc_block(&slp->pool); if (node) { node->thread = get_thread(); node->ticks = get_ticks() + period / CONFIG_SCHEDULER_TICK_INTERVAL; if (!slp->tail || slp->tail->ticks > node->ticks) { // is first to wake up node->next = slp->tail, slp->tail = node; set_timer_ticks(node->ticks); } else { struct sleep_node *prev = slp->tail; while (prev->next && prev->next->ticks <= node->ticks) prev = prev->next; node->next = prev->next, prev->next = node; } pause_this_thread(&slp->lock); } else err = ERR_OUT_OF_MEMORY; if (err) release_spinlock(&slp->lock); return err; }
      
      





上記のコードにはいくつかの明確化が必要です。



1. sleep_data構造のインスタンスは、論理プロセッサに対応しています。 mem_poolはスレッドセーフではないため、sleep_nodeのプールはプロセッサごとに独立しています。



そのため、ミューテックスコードに重大な誤りが隠れています。すべてのミューテックスに対して、mutex_nodeの単一プールが提供されますが、各ミューテックスには独自のプールが必要です。 私は近い将来これを修正する予定です。



2.コードからわかるように、リストに追加されると、ストリームは目覚めた時間(ティック)でランク付けされます。



3. sleep_timer_proc関数は、タイマー割り込みのコンテキストでスケジューラーによって呼び出されるハンドラーです。 彼女の仕事は、目的のフローを起こすことです。



残りは十分に透明に見えます。



All Articles