私はおもちゃのOSを書いています(ミューテックスの実装について)



玩具OSの開発に関するブログを続けています(以前の投稿: onetwothree )。 コーディングの一時停止後(5月の休日、結局)、私は仕事を続けます。 PCIバスのスキャンをスケッチしました。 このことは、SATAコントローラーを使用するために必要です。次に行うことは、単純なディスクドライバーです。 これにより、読み取り専用メモリのアドレス空間への投影を試すことができます(スワップは論理的に終了します)。 それまでの間、ミューテックスの実装について説明したいと思います。



ミューテックス( src / sync.hおよびsrc / sync.cで定義および実装)を実装するために、前の2つの投稿で説明した既存のスケジューラーを変更する必要はありません。 ミューテックスは、ストリームの開始と一時停止の2つの機能のみに基づいて構築できます( src / schedule.hを参照)。



struct mutex { struct __mutex_node *head, *tail; struct spinlock mlock, ilock; }; static inline void create_mutex(struct mutex *mutex) { mutex->head = mutex->tail = NULL; create_spinlock(&mutex->mlock); create_spinlock(&mutex->ilock); }
      
      





ミューテックスの実装には、2つのスピンロックとスリープスレッドのキューが含まれます。 最初のスピンロック(mlock)は、保護されたミューテックスリソースへのアクセスを担当します。 ミューテックス自体がキャプチャされた場合にのみキャプチャされます。 2番目のスピンロック(ilock)は、待機スレッドのキューを同時変更から保護します。



それでは、どのように機能しますか? スレッドがミューテックスを取得しようとすると、mlockのキャプチャが試行され、N回試行されます。 彼が成功すると、ミューテックスがキャプチャされます。 そうでない場合は、安全に(つまりilock経由で)待機中のスレッドのキューに自分自身を追加し、スリープ状態にする必要があります。



 static inline err_code acquire_mutex(struct mutex *mutex) { extern err_code __sleep_in_mutex(struct mutex *mutex); if (!acquire_spinlock_int(&mutex->mlock, 1000)) return __sleep_in_mutex(mutex); return ERR_NONE; } struct __mutex_node { struct __mutex_node *next; thread_id id; }; INTERNAL err_code __sleep_in_mutex(struct mutex *mutex) { struct __mutex_node *node = NULL; bool acquired; acquire_spinlock(&mutex->ilock, 0); acquired = acquire_spinlock_int(&mutex->mlock, 1); if (!acquired) { node = alloc_block(&mutex_node_pool); if (node) { node->next = NULL; node->id = get_thread(); if (mutex->head) mutex->head->next = node; mutex->head = node; if (!mutex->tail) mutex->tail = node; pause_this_thread(&mutex->ilock); } } if (!node) release_spinlock(&mutex->ilock); return (acquired || node) ? ERR_NONE : ERR_OUT_OF_MEMORY; }
      
      





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



1. acquire_spinlock_int関数は、spinlockが解放されるまで割り込みを無効にしないことを除いて、acquire_spinlockと似ています。 mlockをキャプチャするとき、割り込みを無効にしたくありません。mutexを所有するのに時間がかかることがあります。 もう1つは、ilockをキャプチャして、スレッドをキューに追加したい場合です。この操作は迅速に行われるはずです。



2. __sleep_in_mutex関数の次の行は、一見無意味です。



  acquired = acquire_spinlock_int(&mutex->mlock, 1);
      
      





実際、なぜ失敗したのにスピンロックのキャプチャを再試行するのですか? その後、最初の試行とilockのキャプチャの間に、mutexの所有者がそれを返すことができ、その後になって初めてストリームがプランニングクォンタムを受け取ります。 2回目の試行がなければ、キューに自分自身を追加し、永遠にスリープ状態にします。 したがって、ilockをキャプチャした後、再度確認することが重要です(戻り時にmutexの所有者が確実にキャプチャします)。



3. alloc_blockおよびfree_block関数は、固定サイズの事前に割り当てられたメモリブロックのプールを参照します( src / memory.hを参照)。 このプールの本質は、ブロック(この場合はstruct __mutex_node)が必要なときに低速のmallocを呼び出さないことです。 ところで、私はまだ実装なしでこのプールを持っています(mallocを直接呼び出すスタブのみ)。 誰かが最初のポートを実装したり、2番目のポートを移植したりしたい場合は、書き込みます。



4.最初の試行後にスリープ状態にできるのに、なぜNがmlockのキャプチャを試行するのですか? できますが、それはあまり効果的ではありません。 コンテキスト切り替え時間は、スピンロックを取得する1回の試行よりも大幅に長くなります。 したがって、安楽死する前にN回試行する(コード1000で天井から取られます;将来的には実用的な測定を行い、より合理的なNを導き出して正当化する必要があります)。



5.コードは、pause_threadの修正バージョンpause_this_threadを使用します。 電流を安楽死させることに加えて、送信されたスピンロックをアトミックに(中断して)解放します。



ミューテックスが解放されると、ホストはilockをキャプチャし、キュー内の待機スレッドをチェックします。 ストリームが見つかると、それが起動し、ミューテックスの新しい所有者になります。 保留中のスレッドがない場合、ホストはmlockを返し、終了します。



 static inline void release_mutex(struct mutex *mutex) { extern void __awake_in_mutex(struct mutex *mutex); acquire_spinlock(&mutex->ilock, 0); if (mutex->tail) __awake_in_mutex(mutex); else release_spinlock_int(&mutex->mlock); release_spinlock(&mutex->ilock); } INTERNAL void __awake_in_mutex(struct mutex *mutex) { struct __mutex_node *node; err_code err; do { node = mutex->tail; mutex->tail = node->next; if (mutex->head == node) mutex->head = NULL; err = resume_thread(node->id); free_block(&mutex_node_pool, node); } while (mutex->tail && err); if (!mutex->tail) release_spinlock_int(&mutex->mlock); }
      
      





スリープ機能の実装について詳しくお話ししたかったのですが、この記事にはすでに考えられる十分な食べ物が含まれているので、次回まで延期します。



PSコードにエラーが見つかった場合は、必ず記述してください。



All Articles