この記事は、進行中の UNIX カーネル概要シリーズの一部です。
このシリーズの前回の記事では、UNIX プロセスの概要について説明しました。
この記事では、リエントラント カーネル、同期、および UNIX カーネル アーキテクチャのクリティカル セクションについて概要を説明します。
再入可能カーネル
名前が示すように、再入可能カーネルは、複数のプロセスが任意の時点でカーネル モードで実行できるようにするカーネルであり、カーネル データ構造間の一貫性の問題も発生させません。
シングル プロセッサ システムでは、特定の瞬間に 1 つのプロセスしか実行できないことはわかっていますが、カーネル モードで他のプロセスがブロックされ、実行を待機している可能性があります。
たとえば、再入可能カーネルでは、「read()」呼び出しを待機しているプロセスが、カーネル モードでの実行を待機しているプロセスに CPU を解放することを決定する場合があります。
さて、なぜカーネルが再入可能になったのかという疑問が頭に浮かぶかもしれません。さて、カーネルが再入可能でない例から始めて、カーネル モードで複数のプロセスを実行できるようにするとどうなるか見てみましょう。
プロセスがカーネル モードで実行され、カーネル データ構造とそれに関連付けられたいくつかのグローバル値にアクセスしていると仮定します。
- プロセス名が「A」だとします。
- 'A' はグローバル変数にアクセスして、値がゼロでないかどうかを確認し (計算などを実行できるようにするため)、ロジックの一部でこの値を使用しようとする直前に、プロセス ' B' が発生します。
- このプロセス「B」は、同じグローバル変数の値にアクセスしようとし、値を減らします。
- 別のコンテキスト スイッチが発生し、プロセス「A」が実行に戻ります。
- 「A」は「B」がすでに値を減らしていることを認識していないため、この値を再度使用しようとします。
- これが問題です。プロセス「A」は、別のプロセス「B」によって値が変更されたため、グローバル変数の 2 つの異なる値を認識します。
これで、カーネルが再入可能である必要がある理由がわかりました。発生する可能性のある別の質問は、カーネルを再入可能にする方法ですか?
基本的に、カーネルを再入可能にするために次の点を考慮することができます:
- ローカル (スタック) 変数のみを変更し、グローバル変数やデータ構造を変更しないカーネル関数を作成します。このようなタイプの関数は、再入可能関数とも呼ばれます。
- カーネルで再入可能な関数のみを使用することを厳密に順守することは、実行可能な解決策ではありません。したがって、使用される別の手法は「ロック メカニズム」であり、特定の時間に 1 つのプロセスだけが再入不可関数を使用できるようにします。
上記の点から、再入可能関数と非再入可能関数のロックメカニズムの使用が、カーネルを再入可能にするための核心であることは明らかです。再入可能関数の実装は優れたプログラミングに関連しているため、ロック メカニズムは同期の概念に関連しています。
同期とクリティカル セクション
上記の例で説明したように、再入可能カーネルは、カーネル グローバル変数とデータ構造への同期アクセスを必要とします。
これらのグローバル変数とデータ構造を操作するコードのチャンクは、クリティカル セクションと呼ばれます。
カーネル制御パスが (グローバル値またはデータ構造を使用しているときに) コンテキスト スイッチが原因で中断された場合、他の制御パスは同じグローバル値またはデータ構造にアクセスできません。そうしないと、悲惨な結果を招く可能性があります。
振り返ってみると、同期が必要な理由がわかりますか?答えは、カーネルのグローバル変数とデータ構造を安全に使用することです。これは、アトミック操作によっても実現できます。アトミック操作とは、操作中に読み取られた、または変更された状態を他のプロセスが読み取ったり変更したりすることなく、常に実行される操作です。残念ながら、アトミック操作はどこにでも適用できるわけではありません。たとえば、カーネル内のリンクされたリストから要素を削除することは、アトミック操作にすることはできません。
カーネル制御パスを同期する方法に戻りましょう。
カーネル プリエンプションの無効化
カーネル プリエンプションは、カーネルが 1 つのタスクを強制的に中断/中断し、カーネル リソースで待機していた別の優先度の高いタスクを実行できるようにする概念です。
簡単に言えば、実行中のプロセスがカーネルによって強制的に中断され、他のプロセスが実行されるカーネルモードでのプロセスのコンテキスト切り替えです。
定義に従うと、まさにこのカーネルの機能 (プロセスがカーネル モードにあるときにプリエンプトする) が同期の問題を引き起こすことがわかります。この問題の解決策は、カーネル プリエンプションを無効にすることです。これにより、カーネル モードで現在実行中のプロセスが自発的に CPU を解放し、すべてのカーネル データ構造とグローバル変数が一貫した状態にあることを確認した場合にのみ、カーネル モードでのコンテキスト スイッチが発生するようになります。
明らかに、カーネル プリエンプションを無効にすることはあまり洗練されたソリューションではありません。マルチプロセッサ システムを使用している場合、2 つの CPU が同じクリティカル セクションに同時にアクセスできるため、このソリューションはうまくいきません。
割り込みの無効化
カーネル内で同期を実現するために適用できるもう 1 つのメカニズムは、プロセスが重要な領域に入る前にすべてのハードウェア割り込みを無効にし、その非常に重要な領域を離れた後にそれらを有効にすることです。重要な領域が大きい場合、割り込みが非常に長い間無効になり、割り込みであるという独自の目的が無効になり、ハードウェアのアクティビティがフリーズする可能性があるため、このソリューションも洗練されたソリューションではありません。
セマフォ
これは、カーネル内で同期を提供するための最も一般的な方法です。
これは、ユニプロセッサ システムとマルチプロセッサ システムの両方で有効です。この概念によれば、セマフォは、各データ構造に関連付けられ、特定のデータ構造にアクセスしようとするときにすべてのカーネル スレッドによってチェックされるカウンターと考えることができます。
セマフォには、カウンター値、セマフォの取得 (データ構造へのアクセス) を待機しているプロセスのリスト、およびこのセマフォに関連付けられたカウンターの値を増減する 2 つのメソッドに関する情報が含まれています。
動作ロジックは次のとおりです:
- プロセスが特定のデータ構造にアクセスしようとすると、最初にデータ構造のセマフォに関連付けられたカウンターがチェックされます。
- カウンターが正の場合、プロセスはセマフォを取得し、カウンターの値を減らし、クリティカル領域を実行し、セマフォ カウンターを増やします。
- ただし、プロセスがカウンターの値がゼロであることを検出した場合、そのプロセスは、セマフォの取得を待機しているプロセスのリスト (セマフォに関連付けられている) に追加されます。
- これで、カウンターが正になると、セマフォを待っているすべてのプロセスがセマフォを取得しようとします。
- 再び獲得したものは、カウンターを減少させ、クリティカル領域を実行してから、他のプロセスが待機モードに戻る間、カウンターを増加させます。
デッドロックの回避
セマフォのような同期スキームを使用すると、「デッドロック」の副作用が生じます。
例を見てみましょう:
- プロセス A が特定のデータ構造のセマフォを取得し、プロセス B が別のデータ構造のセマフォを取得するとします。
- 次のステップでは、両方のプロセスが、互いに取得したデータ構造のセマフォを取得しようとしています。つまり、プロセス A は、プロセス B によって既に取得されているセマフォを取得しようとしています。また、その逆も同様です。
- プロセスが別のプロセスがリソースを解放するのを待っている間に、別のプロセスが最初のプロセスがリソースを解放するのを待っているこのような状況は、デッドロックとして知られています。
- デッドロックにより、カーネル制御パスが完全にフリーズする可能性があります。
このタイプのデッドロックは、膨大な数のカーネル ロックが使用されている設計でより頻繁に発生します。これらの設計では、デッドロック状態が発生しないと判断することが非常に困難になります。 Linux などの OS では、順番に取得することでデッドロックを回避しています。