セマフォの取得
セマフォーは排他制御として使いますが、他にスピンロックというのがあります。スピンロックはロックを獲得(変数のチェック)するまで、ビジーウエイトと言ってループで待ちますが、セマフォーを他のタスクに実行を譲ります。「そしたらセマフォーの方がいいじゃん。」ということですが、「排他区間が短い場合、タスクスイッチングするより、ビジーウエイトでループし続けた方がパフォーマンスいいですね。」という事のようです。実は、このセマフォ、実装する上でスピンロックを使っているんですね。
スピンロックというのは、マルチCPUシステムでの排他制御を実現する手法として誕生したもののようで、単一CPUシステムでは割り込みを禁止すれば、それ以降の排他性は保障されます。しかしマルチCPUでは他のCPUからの割り込みが発生するわけです。割り込み禁止のCLIはそのCPUに対してだけです。このような背景でスピンロックが登場したようです。
セマフォ取得はdown関数が呼ばれます。まずspin_lock_irqsave関数で、CPUの割り込み状態をしめすEFLAGSレジスタをflagsにセットしたのち割り込み禁止として、スピンロックを仕掛けるようです。EFLAGSレジスタをflagsにセットするのは、spin_unlock_irqrestore関数でdown関数呼び出しの割り込み状態(許可か禁止か)を戻すためです。割り込み禁止、許可はCLIとSTIですが、実はこの命令EFLAGSレジスタのビット9をセットするだけで、CPUとしてはFLAGSレジスタのビット9が1の場合、割り込み許可、0の場合割り込み禁止としているようで、このEFLAGSレジスタをもとに戻すと言うことは、その時の割り込み状態を元に戻すことにほかなりません。
ここで疑問がわいてきます。spin_lock_irqsave関数では割り込み禁止してスピンロックを仕掛けるということですが、「スピンロックを仕掛けるのなら、わだわだ割り込み禁止する必要があるのかなと。」割り込み禁止しなくてそのCPUで割り込みがが発生し場合、そしてその割り込み処理がこの排他区間を参照したとすると、スピンロックでビジーウエイトするわけだから・・・。しかし、このCPUで動作している処理がこのスピンロックを持っているわけですから、割り込みでビジーウエイトすることになる方は、同じCPU下の処理だけにいつまでたってもスピンロックを取得できないことになってしまいます。ずーーとビジーウエイトしていたら、スピンロックを解放する処理が動かない。たぶんですけど。
sem->countが正なら、1デクリメントしてセマフォ獲得です。割り込み状態をもとに戻して、スピンロックを解除して、セマフォ獲得を呼び出した処理に戻ります。
sem->countが0以下の場合、セマフォ獲得できません。__down関数を呼び出してその時の処理を行います。
なお、schedule_timeout関数前後で、割り込みを許可しスピンロックを解除し、再び割り込み禁止しスピンロックを設定しています。schedule_timeout関数で一旦この処理の流れから別の流れに行くわけで、いいかえるとスピンロック区間は一旦終了となるわけです。
timeoutはたぶんセマフォ待つ時間を設定して、タイムオーバしたらループを抜けるようです。
そして、waiter.upが0より大きくなったら(このセマフォを取得している別のタスクがup関数で1にセット)セマフォ獲得できたということでreturn 0を返して呼び出し元はセマフォ獲得ということです。
スピンロックというのは、マルチCPUシステムでの排他制御を実現する手法として誕生したもののようで、単一CPUシステムでは割り込みを禁止すれば、それ以降の排他性は保障されます。しかしマルチCPUでは他のCPUからの割り込みが発生するわけです。割り込み禁止のCLIはそのCPUに対してだけです。このような背景でスピンロックが登場したようです。
セマフォ取得はdown関数が呼ばれます。まずspin_lock_irqsave関数で、CPUの割り込み状態をしめすEFLAGSレジスタをflagsにセットしたのち割り込み禁止として、スピンロックを仕掛けるようです。EFLAGSレジスタをflagsにセットするのは、spin_unlock_irqrestore関数でdown関数呼び出しの割り込み状態(許可か禁止か)を戻すためです。割り込み禁止、許可はCLIとSTIですが、実はこの命令EFLAGSレジスタのビット9をセットするだけで、CPUとしてはFLAGSレジスタのビット9が1の場合、割り込み許可、0の場合割り込み禁止としているようで、このEFLAGSレジスタをもとに戻すと言うことは、その時の割り込み状態を元に戻すことにほかなりません。
ここで疑問がわいてきます。spin_lock_irqsave関数では割り込み禁止してスピンロックを仕掛けるということですが、「スピンロックを仕掛けるのなら、わだわだ割り込み禁止する必要があるのかなと。」割り込み禁止しなくてそのCPUで割り込みがが発生し場合、そしてその割り込み処理がこの排他区間を参照したとすると、スピンロックでビジーウエイトするわけだから・・・。しかし、このCPUで動作している処理がこのスピンロックを持っているわけですから、割り込みでビジーウエイトすることになる方は、同じCPU下の処理だけにいつまでたってもスピンロックを取得できないことになってしまいます。ずーーとビジーウエイトしていたら、スピンロックを解放する処理が動かない。たぶんですけど。
sem->countが正なら、1デクリメントしてセマフォ獲得です。割り込み状態をもとに戻して、スピンロックを解除して、セマフォ獲得を呼び出した処理に戻ります。
sem->countが0以下の場合、セマフォ獲得できません。__down関数を呼び出してその時の処理を行います。
void down(struct semaphore *sem) { unsigned long flags; spin_lock_irqsave(&sem->lock, flags); if (likely(sem->count > 0)) sem->count--; else __down(sem); spin_unlock_irqrestore(&sem->lock, flags); }__down関数は__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT)で__down_common関数をコールします。
static noinline void __sched __down(struct semaphore *sem) { __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT); }list_add_tail関数でセマフォのウエイティングリストにタスクを挿入し、waiter.upが正(セマフォを空いた)になるまでループし続けます。ただし、そのループにschedule_timeout関数を呼び出して、タスクのスイッチングを行っています。signal_pending_state関数はセマフォを待っているタスクがKILLされた場合の処理のようで、KILLされたらループを抜けて、セマフォのウエイティングリストからタスクを削除しています。
なお、schedule_timeout関数前後で、割り込みを許可しスピンロックを解除し、再び割り込み禁止しスピンロックを設定しています。schedule_timeout関数で一旦この処理の流れから別の流れに行くわけで、いいかえるとスピンロック区間は一旦終了となるわけです。
timeoutはたぶんセマフォ待つ時間を設定して、タイムオーバしたらループを抜けるようです。
そして、waiter.upが0より大きくなったら(このセマフォを取得している別のタスクがup関数で1にセット)セマフォ獲得できたということでreturn 0を返して呼び出し元はセマフォ獲得ということです。
static inline int __sched __down_common(struct semaphore *sem, long state, long timeout) { struct task_struct *task = current; struct semaphore_waiter waiter; list_add_tail(&waiter.list, &sem->wait_list); waiter.task = task; waiter.up = 0; for (;;) { if (signal_pending_state(state, task)) goto interrupted; if (timeout <= 0) goto timed_out; __set_task_state(task, state); spin_unlock_irq(&sem->lock); timeout = schedule_timeout(timeout); spin_lock_irq(&sem->lock); if (waiter.up) return 0; } timed_out: list_del(&waiter.list); return -ETIME; interrupted: list_del(&waiter.list); return -EINTR; }