Linux でのマルチプロセス (スレッド) 同期と相互排除について

Linux デバイス ドライバで解決しなければならない問題の 1 つは、共有リソースへの複数プロセスの同時アクセスです. 同時アクセスは競合状態につながります. Linux は競合状態の問題を解決するさまざまな方法を提供しており, これらの方法はさまざまなアプリケーションに適しています.シナリオ。

Linux カーネルは、マルチプロセス、マルチスレッドのオペレーティング システムであり、かなり完全なカーネル同期方法を提供します。カーネル同期方式のリストは次のとおりです。
割り込みマスキング
アトミック操作
スピン ロック
読み書きスピン ロック
シーケンス ロック
セマフォ
読み書きセマフォ
BKL (ビッグ カーネル ロック)
Seq ロック


1. 並行性と競合:

定義:
同時実行とは、複数の実行ユニットが同時に並列に実行されることを指しますが、同時実行ユニットの共有リソース (ハードウェア リソースおよびソフトウェア グローバル変数、静的変数など) へのアクセスは、競合状態 (競合状態) に容易につながる可能性があります
Linux では、主な競合状態は次の状況で発生します。
1. 対称型マルチプロセッサ (SMP)
複数の CPU の特徴は、複数の CPU が共通のシステム バスを使用するため、共通の周辺機器とメモリにアクセスできることです。
2. 単一の CPU 内のプロセスとそれをプリエンプトするプロセス
3.
複数の同時実行ユニットが割り込み (ハード割り込み、ソフト割り込み、タスクレット、下半分) とプロセス間の共有リソースにアクセスできる限り、競合状態が発生する可能性があります。 .
プロセスがアクセスしているリソースに割り込みハンドラーがアクセスする場合にも、競合状態が発生する可能性があります。
複数の割り込みは、それ自体が並行性を引き起こし、競合状態を引き起こす可能性があります (優先度の高い割り込みによって割り込みが中断されます)。

競合問題を解決する方法は、共有リソースへの相互排他アクセスを確保することです. いわゆる相互排他アクセスとは、ある実行ユニットが共有リソースにアクセスすると、他の実行ユニットがアクセスできないようにすることです.

共有リソースにアクセスするコード領域はクリティカル領域と呼ばれます. クリティカル領域は相互排除メカニズムによって保護する必要があります. 割り込みマスキング, アトミック操作, スピンロック, セマフォはすべてLinuxデバイスドライバで使用できる相互排他的アプローチです. .

重要な領域と競合状態:
いわゆる重要な領域は、共有データにアクセスして操作するコード セグメントです. 重要な領域での同時アクセスを避けるために、プログラマーはこれらのコードがアトミックに実行されること、つまりコードが実行されていることを確認する必要があります.クリティカルセクション全体が分割できない命令であるように、最後まで中断することはできません. 実行の2つのスレッドが同じクリティカルセクションにある可能性がある場合, プログラムにはバグが含まれています. これが発生した場合, 私たちはこれらを競合状態と呼びます,同時実行を回避し、競合状態を回避することを同期と呼びます。

デッドロック:
デッドロックには特定の条件が必要です: 1 つ以上の実行スレッドと 1 つ以上のリソースが必要です。各スレッドはリソースの 1 つを待機していますが、すべてのリソースが占有されています。すべてのスレッドは互いに待機していますが、すでに占有しているリソースを決して解放しないため、スレッドを続行できず、デッドロックが発生します。

2.遮断シールド

単一の CPU 内で競合状態を回避する簡単な方法は、クリティカル セクションに入る前にシステムの割り込みをマスクすることです。
Linux カーネルのプロセス スケジューリングやその他の操作は割り込みに依存しているため、カーネル プリエンプション プロセス間の同時実行を回避できます。

割り込みマスクの使用方法:
local_irq_disable()//シールド割り込み
//クリティカル エリア
local_irq_enable()//割り込みを有効にする

機能:
Linux システムの非同期 IO やプロセス スケジューリングなどの多くの重要な操作は割り込みに依存しているため、割り込みシールド期間中はすべての割り込みを処理できないため、長期間のシールドは非常に危険であり、データの損失やシステム クラッシュを引き起こす可能性があります。 、割り込みをマスクした後、現在のカーネル実行パスがクリティカルセクションのコードをできるだけ早く実行する必要があります。
割り込みマスクは CPU 内の割り込みしか禁止できないため、複数の CPU による競合状態を解決できないため、競合状態を回避するために割り込みマスクだけを使用することはお勧めできませんが、スピンロックと組み合わせて使用​​するのが一般的です。

3. アトミック操作

定義: アトミック操作は、実行が他のコード パスによって中断されない操作です。
(アトムはもともと分割できない粒子を指すため、アトミック操作は分割できない命令です)
(命令が「アトミック」に実行され、中断できないことを保証します)
アトミック操作は分割不可能であり、実行後は他のタスクによって中断されませんまたはイベント。ユニプロセッサ システム (UniProcessor) では、割り込みは命令間でしか発生しないため、1 つの命令で完了できる操作は「アトミック操作」と見なすことができます。これは、重要なリソースを相互に排除するために、一部の CPU 命令システムに test_and_set や test_and_clear などの命令が導入されている理由でもあります。ただし、Symmetric Multi-Processor (Symmetric Multi-Processor) 構造では異なり、システム内で複数のプロセッサが独立して動作しているため、1 つの命令で完了する操作でも干渉する可能性があります。例として decl (デクリメント命令) を見てみましょう。これは、2 つのメモリ アクセスを伴う典型的な "read-modify-write" プロセスです。

一般的な理解:
アトミック操作は、その名前が示すように、アトムのように分割できないことを意味します。操作はアトミック操作です. つまり, 操作はアトミックに実行されます. 一度に実行する必要があります. 実行プロセスは, OSの他の動作によって中断することはできません. それは全体的なプロセスです. その実行中, OS にその他の動作を挿入することはできません

分類: Linux カーネルは、カーネルにアトミック操作を実装するための一連の関数を提供します.これは、整数アトミック操作とビット アトミック操作に分けられます.共通点は、操作がいずれの場合もアトミックであり、カーネル コードが安全に呼び出すことができることです.それらを中断することなく。

アトミック整数操作:整数のアトミック操作は、atomic_t 型データでのみ処理できます. C 言語の int 型を直接使用する代わりに、ここで特別なデータ型が導入された理由は、主に
2 つの理由によるものです。
オペランドのatomic_tタイプのみを受け入れます。これにより、アトミック操作がこの特別なタイプのデータでのみ使用できるようになり、同時に、このタイプのデータが他の非アトミック関数;
2 番目に、atomic_t 型を使用して、コンパイラが対応する値へのアクセスを最適化しないようにします。これにより、アトミック操作が最終的にエイリアスではなく正しいメモリ アドレスを受け取り、最終的に異なるアーキテクチャでアトミック操作を実装するときに、 atomic_t を使用すると、違いを隠すことができます。
アトミック整数演算の最も一般的な用途は、カウンターの実装です。
アトミック操作は、操作がアトミックであること、完了または未完了であることのみを保証できることを説明する必要があります。操作の半分の可能性はありませんが、アトミック操作は操作の順序を保証しません。 、2 つの操作が特定の完了順序に従って実行されることを保証することはできません。アトミック操作の連続性を保証する場合は、メモリ バリア命令を使用します。

atomic_t と ATOMIC_INIT(i) を定義
typedef struct { volatile int counter; } アトミック_t;
#define ATOMIC_INIT(i) { (i) }

コードを書くとき, アトミック操作を使用できるときは, 複雑なロックメカニズムを使用しないようにしてください. ほとんどのアーキテクチャでは, アトミック操作は, より複雑な同期方法よりもシステムへのオーバーヘッドが少なくなります. キャッシュラインへの影響も小さいです.高いパフォーマンスが必要な場合は、さまざまな同期方法をテストして比較することをお勧めします。

アトミック ビット操作:
ビット レベル データを操作する関数は、通常のメモリ アドレスを操作します。パラメータはポインタとビット番号です。

便宜上, カーネルは上記の操作に対応する一連の非アトミック ビット関数も提供します. 非アトミック ビット関数の動作はアトミック ビット関数の動作とまったく同じです. ただし, 前者はアトミック性を保証しません.であり、その名前のプレフィックスにはさらに 2 つのアンダースコアがあります。たとえば、test_bit() の非アトミック版は _test_bit() です。アトミック操作が必要ない場合 (たとえば、データをロックで保護している場合)、これらの非アトミック ビット関数は、_test_bit() よりも効率的です。アトミック ビット関数は、より高速に実行される場合があります。

4.スピンロック

スピン ロックの導入:
各クリティカル セクションが変数を追加するのと同じくらい簡単にできるとよいのですが、残念ながら、これは当てはまりませんが、クリティカル セクションは複数の機能にまたがることができます。右 フォーマット変換と解析を実行し、最終的に別のデータ構造に追加します. 実行プロセス全体はアトミックでなければなりません. データが更新される前に、他のコードはデータを読み取ることができません. 明らかに, 単純なアトミック操作は無力です (ユニプロセッサではシステム (UniProcessor) では、単一の命令で完了できる操作は「アトミック操作」と見なすことができます。これは、割り込みが命令間でのみ発生する可能性があるためです)。これには、より複雑な同期方法 (保護を提供するためのロック) を使用する必要があります。

スピンロックの紹介:

Linux カーネルで最も一般的なロックはスピン ロック (スピン ロック) です. スピン ロックを保持できる実行可能スレッドは最大でも 1 つだけです. 実行スレッドが競合する (既に保持されている) スピン ロックを取得しようとすると, スレッドはは、ロックが再び利用可能になるまで、ループ、スピン、待機を繰り返します. ロックが競合していない場合、ロックを要求している実行スレッドはすぐにロックを取得して実行を続けることができます. いつでも, スピンロックは複数を防ぐことができます.実行スレッドは同時に理解ゾーンに入ります。同じロックを複数の場所で使用できることに注意してください。たとえば、特定のデータへのすべてのアクセスを保護して同期することができます

スピンロックの競合により、ロックが使用可能になるのを待っている間に、それを要求したスレッドがスピンする (特にプロセッサ時間の浪費) ため、スピンロックを長時間保持しないでください。ロックの競合に対処するために別の方法を使用できます。要求スレッドをスリープさせ、ロックが再び使用可能になるまでウェイクアップさせて、プロセッサが循環する必要がないようにします。他のコードもある程度のオーバーヘッドをもたらします。ここには 2 つの明らかなコンテキスト スイッチがあり、ブロックされたスレッドをスワップアウトしてスワップインする必要があります。したがって、スピンロックを保持するのに費やされる時間は、2 つのコンテキストスイッチを完了するのに費やされる時間よりも短いことが望ましい.可能な限り短くすることで、セマフォは上記の 2 番目のメカニズムを提供できます。

スピン ロックは割り込みハンドラで使用できます (セマフォはスリープを引き起こすため、ここでは使用できません) 割り込みハンドラでスピン ロックを使用する場合は、(プロセッサ上の現在の割り込み要求で) ロックを取得する前に、必ずローカル割り込みを無効にしてください。そうしないと、割り込みハンドラーはロックを保持しているカーネル コードを中断し、既に保持されているスピン ロックを求めて競合を試みる可能性があります。割り込みハンドラが実行を終了するまで実行できません. これはまさに前の章で述べた二重要求のデッドロックです. 閉じる必要があるのは現在のプロセッサの割り込みのみであることに注意してください. 割り込みが別のプロセッサで発生した場合, 処理は行われません.割り込みハンドラーが同じロックでスピンした場合でも、(別のプロセッサ上の) ロック所有者が最終的にロックを解放するのを防ぎます。

スピンロックの簡単な理解:
スピンロックを理解する最も簡単な方法は、クリティカル セクションを「現在実行中です。しばらくお待ちください」または「現在実行中です。実行していません。使用できます」のいずれかとしてマークする変数として扱うことです。 . 実行ユニットAが最初にルーチンに入ると、スピンロックを保持します.実行ユニットBが同じルーチンに入ろうとすると、スピンロックが保持されていることがわかり、実行ユニットAが終了するまで入ることができません.リリースされました。

スピンロックの API 関数:

実際、導入されたいくつかのセマフォおよび相互排除メカニズムの基礎となるソース コードは、スピン ロックの再パッケージ化として理解できるスピン ロックを使用します。したがって、ここから、スピンロックが通常セマフォよりも高いパフォーマンスを提供できる理由を理解できます。
スピン ロックは、"locked" と "unlocked" の 2 つの値しか持てないミューテックス デバイスです。通常、整数の単一ビットとして実装されます。
「テストと設定」操作はアトミックに実行する必要があります。
カーネルコードがスピンロックを所有している場合は常に、関連する CPU でのプリエンプションは禁止されています。
スピン ロックに適用されるコア ルール:
(1)スピン ロックを所有するコードは、サービス割り込みを除いて、アトミックである必要があります(場合によっては、割り込みサービスなど、CPU を放棄することはできません。また、スピン ロックを取得する必要があります。この種のロック トラップを回避するには、スピン ロックがある場合は割り込みを無効にする必要があります)。また、CPU をあきらめることもできません (多くの予期しない場所で発生する可能性があるスリープなど)。そうしないと、CPU が永久にスピンする可能性があります (クラッシュ)。
(2)スピンロックを所有している時間が短いほど良い

スピン ロックは、マルチプロセッサの同期メカニズム用に設計されていないことを強調しておく必要があります. シングル プロセッサの場合 (シングル プロセッサおよび非プリエンプティブ カーネルの場合、スピン ロックは何もしません)、カーネルはスピンロック メカニズムを導入します.プリエンプティブル カーネルの場合、カーネルのプリエンプション メカニズムを有効にするかどうかを設定するためだけに使用されます. つまり、ロックとロック解除は、実際にはカーネル プリエンプション機能の禁止または有効になります. カーネルがプリエンプションをサポートしていない場合、スピンロックはカーネルにまったくコンパイルされません。
spinlock_t 型はカーネルで使用され、スピン ロックを表します。

<linux/spinlock_types.h>:
typedef struct { raw_spinlock_t raw_lock; #if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP) unsigned int break_lock; #endif } spinlock_t;




SMP をサポートしないカーネルの場合、struct raw_spinlock_t は何も持たず、空の構造体です。マルチプロセッサをサポートするカーネルの場合、struct raw_spinlock_t は次のように定義されます。

typedef struct { unsigned int slock; } raw_spinlock_t;

slock はスピンロックの状態を示し、"1" はスピンロックがアンロック状態 (UNLOCK) であることを示し、"0" はスピンロックがロック状態 (LOCKED) であることを示します。
break_lock は、プロセスが現在スピン ロックを待機しているかどうかを示します。明らかに、プリエンプションをサポートする SMP カーネルでのみ機能します。

スピンロックの実装は複雑なプロセスです. 実装に必要なコードやロジックの量が多いため複雑ではありません. 実際, その実装コードは非常に小さいです. スピンロックの実装はアーキテクチャと密接に関連しています.コアコードは基本的にアセンブリ言語で記述されています.関連の構造に関連するコアコードは<asm/spinlock.h>などの関連ディレクトリに配置されています. 私たちドライバー開発者は、このようなスピンロックの内部の詳細を理解する必要はありません.興味がある場合は、Linux カーネルのソース コードを参照してください。駆動するスピンロック インターフェースについては、 <linux/spinlock.h> ヘッダー ファイルをインクルードするだけです。
スピンロックの APIを詳しく紹介する前に、スピンロックの基本的な使用形式を見てみましょう

spin_lock(&ロック);

spin_unlock(&lock);

使い方としては、spinlock の API はまだ非常に単純です. 一般に、使用する API は次のとおりです. 実際、それらはすべて <linux/spinlock.h> で定義されているマクロ インターフェイスです. 実際の実装は次のとおりです.

<asm/spinlock.h>中
#include <linux/spinlock.h>
SPIN_LOCK_UNLOCKED
DEFINE_SPINLOCK
spin_lock_init( spinlock_t *)
spin_lock(spinlock_t *)
spin_unlock(spinlock_t
*) spin_lock_irq(spinlock_t *)
spin_unlock_irq(spinlock_t *)
spin_lock_irqsace(spinlock_t *,unsigned長いフラグ)
spin_unlock_irqsace(spinlock_t *, unsigned long フラグ)
spin_trylock(spinlock_t *)
spin_is_locked(spinlock_t *)


スピンロックの初期化には 2 つの初期化形式があり、1 つは静的初期化で、もう 1 つは動的初期化です。静的スピンロック オブジェクトの場合、マクロである SPIN_LOCK_UNLOCKED で初期化します。もちろん, スピンロックを宣言して一緒に初期化することもできます. これは DEFINE_SPINLOCK マクロの仕事です. したがって, 次の 2 行のコードは同等です.
DEFINE_SPINLOCK (ロック);
spinlock_t ロック = SPIN_LOCK_UNLOCKED;

通常、spin_lock_init 関数は動的に作成された spinlock_t オブジェクトを初期化するために使用され、そのパラメーターは spinlock_t オブジェクトへのポインターです。もちろん、初期化されていない静的な spinlock_t オブジェクトを初期化することもできます。
spinlock_t *lock

spin_lock_init(lock);

• ロックの取得 カーネル
は、スピンロックを取得するために 3 つの関数を提供します。
spin_lock: 指定されたスピンロックを獲得します。
spin_lock_irq: ローカル割り込みを無効にし、スピン ロックを取得します。
spin_lock_irqsace: ローカル割り込みステータスを保存し、ローカル割り込みを無効にしてスピンロックを取得し、ローカル割り込みステータスを返します。

割り込みハンドラでスピンロックを使用することができます. この場合, ローカル割り込みをオフにする機能を持つ関数を使用する必要があります. ロックする前に割り込みフラグを保存するため, spin_lock_irqsave を使用することをお勧めします.正常に復元されます。ロック時に spin_lock_irq 割り込みがオフになっていると、ロック解除時に誤って割り込みがオンになります。

スピン ロックの取得に関連する他の 2 つの関数は
次のとおりです。
spin_is_locked(): 指定されたスピン ロックが取得されているかどうかを判断します。はいの場合はゼロ以外を返し、それ以外の場合は 0 を返します。
• ロック
の解放 ロックの取得に対応して、カーネルはスピンロックを解放するための 3 つの相対関数を提供します。
spin_unlock: 指定されたスピン ロックを解放します。
spin_unlock_irq: スピン ロックを解放し、ローカル割り込みをアクティブにします。
spin_unlock_irqsave: スピン ロックを解放し、保存されたローカル割り込み状態を復元します。

5、読み書きスピンロック

クリティカル セクションによって保護されたデータが読み取りおよび書き込み可能である場合、書き込み操作がない限り、読み取りのために同時操作をサポートできます。書き込み操作のみが相互に排他的であるというこの要件については、スピン ロックを引き続き使用する場合、この要件を満たすことは明らかに不可能です (読み取り操作には無駄が多すぎます)。このため、カーネルは別の種類のロックを提供します。読み取り/書き込みスピン ロック、読み取りスピン ロックは共有スピン ロックとも呼ばれ、書き込みスピン ロックは排他的スピン ロックとも呼ばれます。
読み書きスピンロックは、スピンロックより粒度の小さいロック機構で、「スピン」の概念はそのままですが、書き込み操作に関しては、最大で 1 つの書き込みプロセスしか実行できません。 、複数の読み取り実行ユニットが存在する可能性があります。もちろん、読み取りと書き込みを同時に実行することはできません。
読み取り/書き込みスピン ロックの使用は、通常のスピン ロックの使用と同様です. まず、読み取り/書き込みスピン ロック オブジェクトを初期化する必要があります: // 静的初期化 rwlock_t rwlock = RW_LOCK_UNLOCKED; // 動的初期化 rwlock_t
*
rwlock
;

rw_lock_init
( rwlock);

読み取り操作コードで共有データの読み取りスピン ロックを取得します:
read_lock(&rwlock);

read_unlock(&rwlock);

書き込みコードで共有データの書き込みスピンロックを取得します:
write_lock(&rwlock);

write_unlock(&rwlock);

多数の書き込み操作がある場合、書き込み操作は書き込みスピン ロックでスピンし、書き込み不足状態 (読み取りスピン ロックが完全に解放されるのを待機する) になることに注意してください。読み取りスピンロックを自由に取得します。

スピンロックの読み書きの機能は通常のスピンロックと似ているので、ここではいちいち紹介しませんが、下の表にまとめておきます。

RW_LOCK_UNLOCKED
rw_lock_init(rwlock_t *)
read_lock(rwlock_t *)
read_unlock(rwlock_t *)
read_lock_irq(rwlock_t *) read_unlock_irq(rwlock_t
*) read_lock_irqsave
(rwlock_t *, unsigned long) read_unlock_irqsave(rwlock_t
*, unsigned long)
write_lock(rwlock_t *)
write_lock(rwlock_t *) *)
write_lock_irq(rwlock_t *)
write_unlock_irq(rwlock_t *)
write_lock_irqsave(rwlock_t *, unsigned long)
write_unlock_irqsave(rwlock_t *, unsigned long)
rw_is_locked(rwlock_t *)

六、シークエンス

シーケンス ロック (seqlock) は、読み取り/書き込みロックの最適化です. シーケンス ロックを使用すると、読み取り実行ユニットが書き込み実行ユニットによってブロックされることはありません. 共有リソースが書き込み操作を実行している場合でも、読み取りを続行できます.書き込み実行ユニットが書き込み操作を完了するのを待つことなく、書き込み実行ユニットは、書き込み操作を実行する前に、すべての読み取り実行ユニットが読み取り操作を完了するのを待つ必要がない。
ただし、書き込み実行ユニットと書き込み実行ユニットは依然として相互に排他的です。つまり、書き込み実行ユニットが書き込み操作を実行している場合、書き込み実行ユニットがシーケンスロックを解放するまで、他の書き込み実行ユニットはスピンする必要があります。
読み取り実行ユニットが読み取り操作中に既に書き込み操作を実行している場合、取得したデータが完全であることを確認するために、読み取り実行ユニットはデータを再読み取りする必要があります。比較的小さい. パフォーマンスは非常に優れており, 読み取りと書き込みを同時に行うことができるため, 同時実行性が大幅に向上します. シーケンスには制限があることに注意してください
.書き込み実行ユニットはポインターを無効にする可能性がありますが、読み取り実行ユニットがポインターにアクセスしようとすると、Oops が発生します。

セブン、セマフォ

Linux のセマフォはスリープ ロックです. タスクが既に占有されているセマフォを取得しようとすると, セマフォはそれを待機キューにプッシュしてからスリープ状態にします. このとき, プロセッサは自由を取り戻すことができます. 実行するために他のコードでは、セマフォを保持しているプロセスがセマフォを解放すると、待機キュー内のどのタスクが起床され、セマフォが取得されます。
セマフォまたはフラグは、オペレーティング システムで学習する古典的な P/V プリミティブ操作です。
P: セマフォの値が 0 より大きい場合は、セマフォの値をデクリメントし、プログラムは実行を継続します。それ以外の場合は、スリープしてセマフォが 0 より大きくなるのを待ちます。
V: セマフォの値をインクリメントします。インクリメントされたセマフォの値が 0 より大きい場合、待機中のプロセスを起こします。

セマフォの値は、同時にクリティカルセクションに入ることができるプロセスの数を決定します. セマフォの初期値が 1 の場合, セマフォは相互排除セマフォ (MUTEX) です. ゼロ以外の値が 1 より大きいセマフォは、カウンティング セマフォと呼ばれることもあります。一般的なドライバが使用するセマフォはミューテックス セマフォです。スピンロックと同様に, セマフォの実装もアーキテクチャと密接に関連しています. 具体的な実装は <asm/semaphore.h> ヘッダー ファイルで定義されています. x86_32 システムの場合, その定義
は次のとおりです. int sleepers; wait_queue_head_t wait; };




セマフォの初期値カウントはアトミック操作型であるatomic_t型であり、カーネル同期技術でもあり、セマフォがアトミック操作に基づいていることがわかります。アトミック操作については、アトミック操作のセクションで詳しく紹介します。

セマフォの使用は、作成、取得、および解放を含むスピンロックに似ています。最初にセマフォの基本的な使い方を示しましょう:
static DECLARE_MUTEX(my_sem);

if (down_interruptible(&my_sem))

{ return -ERESTARTSYS; } アップ(&my_sem)



Linux カーネルのセマフォ関数インターフェイスは次のとおりです:
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
seam_init(struct semaphore *, int);
init_MUTEX(struct semaphore *);
init_MUTEX_LOCKED(struct semaphore *)
down_interruptible( struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)

セマフォの初期化 セマフォの初期化には、静的初期化と動的初期化があります。静的初期化は、セマフォを静的に宣言および初期化するために使用されます。
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);

動的に宣言または作成されたセマフォの場合、次の関数を初期化に使用できます:
seam_init(sem, count);
init_MUTEX(sem);
init_MUTEX_LOCKED(struct semaphore *)

明らかに、MUTEX を使用した関数はミューテックス セマフォを初期化します。LOCKED は、セマフォをロック状態に初期化します。
• セマフォを使用する セマフォ
が初期化された後、それを使用できます
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)

down 関数は指定されたセマフォを取得しようとします. セマフォが既に使用されている場合, プロセスは中断できないスリープ状態に入ります. down_interruptible は、プロセスを割り込み可能なスリープ状態にします。プロセス状態の詳細については、カーネルのプロセス管理で詳しく紹介しています。

down_trylock はセマフォの取得を試み、取得に成功した場合は 0 を返し、失敗した場合は直ちに 0 以外を返します。

クリティカル セクションを出るときに up 関数を使用してセマフォを解放し、セマフォのスリープ キューが空でない場合は、待機中のプロセスの 1 つを起動します。

8 つの読み取りおよび書き込みセマフォ

スピン ロックと同様に、セマフォにも読み取りセマフォと書き込みセマフォがあります。読み取りおよび書き込みセマフォ API は <linux/rwsem.h> ヘッダー ファイルで定義されており、その定義は実際にはアーキテクチャに関連しているため、具体的な実装は <asm/rwsem.h> ヘッダー ファイルで定義されています。 x86 の例:
struct rw_semaphore { signed long count; spinlock_t wait_lock; struct list_head wait_list; };



最初に説明することは、すべての読み取りおよび書き込みセマフォは相互に排他的なセマフォであるということです。読み取りロックは共有ロックです。つまり、複数の読み取りプロセスが同時にセマフォを保持できますが、書き込みロックは排他ロックであり、相互に排他的なセマフォを同時に保持できる書き込みロックは 1 つだけです。明らかに、排他的な読み取りロックを含め、書き込みロックは排他的です。書き込みロックは共有ロックであるため、書き込みロックを保持しているプロセスがない限り、複数の読み取りプロセスがロックを保持できます。常にロックを正常に保持するため、書き込みプロセスが飢餓状態に書き込みます。

読み書きセマフォを使用する前に初期化が必要ですが、ご想像のとおり、使用中の読み書きスピンロックとほとんど同じです。最初に、読み書きセマフォの作成と初期化を見てみましょう。
// 静的初期化
static DECLARE_RWSEM(rwsem_name);

//
static struct rw_semaphore rw_sem;
init_rwsem(&rw_sem);を動的に初期化します。

読み取りプロセスは、セマフォ保護クリティカル セクション データを取得します:
down_read(&rw_sem);

up_read(&rw_sem);

書き込みプロセスは、セマフォ保護クリティカル セクション データを取得します:
down_write(&rw_sem);

up_write(&rw_sem);

読み取りおよび書き込みセマフォ API の詳細については、次の表を参照してください。

#include <linux/rwsem.h>
DECLARE_RWSET(名前);
init_rwsem(struct rw_semaphore *);
void down_read(struct rw_semaphore *sem);
void down_write(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);

スピンロックと同様に、down_read_trylock と down_write_trylock はセマフォの取得を試み、取得が成功した場合は 1 を返し、それ以外の場合は 0 を返します。戻り値がセマフォの対応する関数と逆になっているのは不思議で、使用する際には注意が必要です。

九、スピンロックとセマフォの違い

ドライバ プログラムでは、複数のスレッドが同時に同じリソースにアクセスすると (ドライバ プログラムのグローバル変数は典型的な共有リソースです)、「競合」が発生する可能性があるため、共有リソースの同時実行性を制御する必要があります。Linux カーネルで同時実行制御を解決するために最も一般的に使用される方法は、スピン ロックとセマフォです (ほとんどの場合ミューテックスとして使用されます)。

スピンロックとセマフォは「似ているが似ていない」とは、機能が似ていることを意味し、「異なる」とは、本質と実装メカニズムがまったく異なっており、同じカテゴリに属していないことを意味します。

スピン ロックによって呼び出し元がスリープ状態になることはありません。スピン ロックが他の実行ユニットによって保持されている場合、呼び出し元はループを続け、スピン ロックの所有者がロックを解放したかどうかを確認します。「スピン」とは「その場でスピン」を意味します。 . セマフォによって呼び出し元がスリープ状態になり、ロックが取得されない限り、プロセスが実行キューからドラッグされます。これが彼らの「違い」です。

ただし、セマフォであろうとスピンロックであろうと、いつでも最大1つのホルダーが存在できます。つまり、最大1つの実行ユニットがいつでもロックを取得できます。それが「似ている」ということです。

スピン ロックとセマフォの上記の特性を考慮して、一般的に言って、スピン ロックは非常に短い保持時間に適しており、あらゆるコンテキストで使用できます。セマフォは長い保持時間に適しており、コンテキスト使用のプロセスでのみ使用できます。 . 保護された共有リソースがプロセス コンテキストでのみアクセスされる場合、共有リソースはセマフォで保護できます。共有リソースへのアクセス時間が非常に短い場合は、スピン ロックも適切な選択です。ただし、保護された共有リソースに割り込みコンテキスト (下半分の割り込みハンドラーと上半分のソフト割り込みを含む) でアクセスする必要がある場合は、スピン ロックを使用する必要があります。
相違点は次のとおりです。
1. セマフォを競合するプロセスは、ロックが再び使用可能になるのを待っている間にスリープするため、セマフォはロックが長時間保持される状況に適しています。
2. 逆に、ロックを保持する時間が短い場合は、ロックが占有されている時間全体よりもスリープによる時間が長くなる可能性があるため、セマフォを使用するのは適切ではありません。
3. ロックが競合すると実行スレッドがスリープするため、セマフォ ロックはプロセス コンテキストでのみ取得できます。
4. 他のプロセスが同じセマフォを取得しようとするときにデッドロックが発生しないため、セマフォを保持している間はスリープに入ることができます (もちろん、スリープする必要はありません)。最終的には実行を継続します)。
5. セマフォを占有している間は、スピン ロックを占有できません。これは、セマフォを待っている間はスリープできますが、スピン ロックを保持したままスリープすることは許可されていないためです。
6. セマフォ ロックによって保護されたクリティカル セクションには、ブロッキングを引き起こす可能性のあるコードが含まれる可能性がありますが、スピン ロックは、そのようなコードを含むクリティカル セクションを保護するために使用されることは絶対に避ける必要があります。 、別のプロセスがこのスピンロックを取得しようとし、デッドロックが発生します。
7. セマフォはスピン ロックとは異なり、カーネルのプリエンプションを禁止しません (スピン ロックが保持されている場合、カーネルはプリエンプトされません)。そのため、セマフォを保持しているコードはプリエンプトされます。待機時間をスケジュールすると、悪影響があります。
上記で紹介した同期機構の方式以外にも、BKL(ビッグカーネルロック)やSeqロックなどがあります。
BKL はグローバル スピン ロックであり、主に Linux の元の SMP からきめ細かいロック メカニズムへの移行を容易にするために使用されます。
Seq ロックは共有データの読み取りと書き込みに使用され、そのようなロックの実現はシーケンス カウンターのみに依存します。

おすすめ

転載: blog.csdn.net/qq_44333320/article/details/125986257
おすすめ