Linux同期プリミティブのシーケンスロック(シーケンスロック)

シーケンスロックはライターに高い優先順位を与え、リーダーが読んでいるときでもライターが実行し続けることを可能にします。この戦略の利点は、ライターが待機しないことです。欠点は、リーダーが有効なコピーを取得するまで同じデータを繰り返し読み取らなければならない場合があることです。

Linuxカーネルコードでは、シーケンスロックはseqlock_t構造として定義されています(コードはinclude / linux / seqlock.hにあります)。

typedef struct { struct seqcount seqcount; spinlock_t lock; } seqlock_t;したがって、スピンロックロックと、現在のロックのシーケンス番号を表すseqcountが含まれます。seqcount構造は次のように定義されます。



typedef struct seqcount { 符号なしシーケンス; } seqcount_t;は、符号なし整数を含むシーケンス変数です。



初期化

シーケンスロックは、使用する前に初期化する必要があります。通常、次の2つの方法があります。

DEFINE_SEQLOCK(lock1);

seqlock_t lock2;
seqlock_init(&lock2);
ご覧のとおり、最初の方法は、シーケンスロック変数をマクロで直接定義して初期化することです。

#define SEQCNT_ZERO(lockname){。sequence = 0、…}

#define __SEQLOCK_UNLOCKED(lockname)
{。
seqcount = SEQCNT_ZERO(lockname)
、. lock = __SPIN_LOCK_UNLOCKED(lockname)
}

#define DEFINE_SEQLOCK(x)
seqlock_t x = _ x)
したがって、マクロ定義の初期化を通じて直接、seqlock_t構造変数を定義し、ロック解除される内部スピンロックロックを初期化し、シーケンス番号を表すseqcount変数を0に初期化します。

2番目の方法は、seqlock_t構造変数を自分で定義してから、seqlock_init関数を呼び出して初期化することです。

static inline void __seqcount_init(seqcount_t * s、const char * name、
struct lock_class_key * key)
{ s-> sequence = 0; } #define seqcount_init(s)__ seqcount_init(s、NULL、NULL)#define seqlock_init(x )do { seqcount_init(&(x)-> seqcount); spin_lock_init(&(x)-> lock); } while(0)は、内部スピンロック変数lockも初期化し、シーケンス番号を表すseqcount変数を初期化します。 0。











書き込み操作

シーケンスロックは、ライターとリーダーを区別します。ライターの場合、一般的に次の使用法が使用されます。

write_seqlock(&seq_lock);
/ *データの変更* /

write_sequnlock(&seq_lock);
write_seqlock関数とwrite_sequnlock関数の間にデータを変更するためのコードを挟むだけです。

書き込みシーケンスロックを取得するためのwrite_seqlock関数は、次のように定義されています。

static inline void write_seqlock(seqlock_t sl)
{ /
スピンロックを取得* /
spin_lock(&sl-> lock);
write_seqcount_begin(&sl-> seqcount);
}
最初にシーケンスロック内のスピンロックを取得してから、write_seqcount_begin関数を呼び出します。

static inline void write_seqcount_begin(seqcount_t * s)
{ write_seqcount_begin_nested(s、0); }接着调用write_seqcount_begin_nested関数数:


static inline void write_seqcount_begin_nested(seqcount_t * s、int subclass)
{ raw_write_seqcount_begin(s); }最後に、raw_write_seqcount_begin関数が呼び出されます。



static inline void raw_write_seqcount_begin(seqcount_t s)
{ /
累積シーケンス番号/
s-> sequence ++;
/
書き込みメモリバリア* /
smp_wmb();
}
シーケンスロックにシーケンス番号を累積した後、書き込みメモリバリアが追加されます。これは、write_seqlock関数が正式に実行された後、変更されたデータコードの前に、システム内の他のモジュールがシーケンス番号が累積されたことを認識できるようにするためです。これは、累積シーケンス番号の命令が後続の変更されたデータコードに並べ替えられないようにするためです。そうしないと、データコードを変更するコードが少し実行され、他のCPUがシーケンス番号が変更されたことをまだ検出していない可能性があります。一貫性のない読み取りデータを引き起こします。もちろん、読み取り時に対応する読み取りメモリバリアも追加する必要があります。

書き込みシーケンスロックを解放するwrite_sequnlock関数の機能は、基本的に、次のように定義されているロックを取得するプロセスを逆にすることです。

static inline void write_sequnlock(seqlock_t sl)
{ write_seqcount_end(&sl-> seqcount); /

スピンロックを解放する* /
spin_unlock(&sl-> lock);
}
最初にwrite_seqcount_end関数を呼び出し、次にスピンロックを解放します。

static inline void write_seqcount_end(seqcount_t * s)
{ raw_write_seqcount_end(s); }接着调用raw_write_seqcount_end関数数:



ボイドraw_write_seqcount_endインライン静的(seqcount_t S)
{ /
書き込みメモリバリア/
smp_wmb();
/
累算シーケンス番号* /
S->配列++;
}
最初の書き込みメモリバリアを追加し、累算シーケンス番号。これは、データを変更するコードが実行された後にのみシーケンス番号を累積できるようにするためです。したがって、前のwrite_seqlockで使用された書き込みメモリバリアとここでwrite_sequnlockで使用された書き込みメモリバリアはペアになり、データ変更操作を実行するための重要なセクションを形成します。

同時に、シーケンスロックのシーケンス番号は1に初期化されているため、書き込みロックがロックされるとシーケンス番号が1増加し、書き込みロックが開かれるとシーケンス番号が1増加するため、シーケンス番号を読み取るときに表示する必要があります。 1人のライターが書き込みシーケンスロックを取得しており、読み取りシーケンス番号が偶数の場合、現在書き込みシーケンスロックを取得しているライターがいないことを示す必要があります。

さらに、さまざまなライターの場合、シリアルロックはスピンロックによって保護されているため、一度に1人のライターしか存在できません。

最後に、非常に重要なことですが、書き込みシーケンスのロックによって現在のプロセスがスリープ状態になることはありません。

読み取り操作

次に、シーケンシャルロックのリーダーを分析します。読者の場合、通常は次の使用法を使用します。

unsigned int seq;

do { seq = read_seqbegin(&seq_lock); / *データの読み取り* / } while read_seqretry(&seq_lock、seq);通常、read_seqbegin関数を使用してシーケンスロックのシーケンス番号を読み取り、実際のデータ読み取り操作を実行します。 、最後にread_seqretry関数を呼び出して、現在のシーケンスロックのシーケンス番号が以前に読み取ったシーケンスロックと一致しているかどうかを確認します。それらが一貫している場合は、読み取りプロセス中に書き込みを行っているライターがいないことを証明し、直接終了できます。一貫性がない場合は、少なくとも1人のライターが読み取りプロセス中にデータを変更したことを意味し、ループで上記の手順を繰り返します。 、前後に読み取られるシーケンス番号が一致するまで。




現在のシーケンスロックシーケンス番号を読み取るread_seqbegin関数は、次のように定義されています。

static inline unsigned read_seqbegin(const seqlock_t * sl)
{ return read_seqcount_begin(&sl-> seqcount); }调用了raw_read_seqcount_begin関数数:


static inline unsigned read_seqcount_begin(const seqcount_t * s)
{ return raw_read_seqcount_begin(s); }次に、raw_read_seqcount_begin関数が呼び出されます。



static inline unsigned raw_read_seqcount_begin(const seqcount_t s)
{ /
シーケンス番号の読み取り/
unsigned ret = __read_seqcount_begin(s);
/
メモリバリアの読み取り* /
smp_rmb();
return ret;
}
最初に__read_seqcount_begin関数を呼び出して、現在のシーケンスロックを読み取ります。次に、シーケンス番号に読み取りメモリバリアが追加されます。

static inline unsigned __read_seqcount_begin(const seqcount_t * s)
{ unsigned ret;

繰り返し:
/ *読み取りシーケンスロックのシーケンス番号/
ret = READ_ONCE(s-> sequence);
/
シーケンス番号が奇数の場合、ライターが書き込みを行っていることを意味します/
if(unlikely(ret&1)){ /
ループ待機/
cpu_relax();
goto repeat;
}
/
偶数になるまでシーケンス番号を
返す* / return ret;
}
最初にシーケンスロックのシーケンス番号を読み取り、READ_ONCEを追加して、コンパイラがそれとその後の条件付き判断を最適化しないようにします。実行の順序が狂っています。次に、シーケンス番号が奇数かどうかを判断します。前述のように、奇数の場合は、ライターが書き込みシーケンスロックを保持していることを意味します。このとき、cpu_relax関数を呼び出してCPUの制御を放棄し、シーケンスを最初から再度読み取ります。均等になるまで数えます。

cpu_relax関数は、各アーキテクチャ自体によって実装されます。Arm64アーキテクチャの実装は次のとおりです(コードはarch / arm64 / include / asm /processor.hにあります)。

static inline void cpu_relax(void)
{ asm volatile( "yield" ::: "memory"); }ほとんどのArm64実装では、yield命令はnopnull命令と同等です。現在実行中のスレッドは何の関係もないことを現在のCPUコアに通知するだけであり、現在のCPUコアは他のことを実行できます。通常、この種の命令はハイパースレッディングをサポートするCPUコアにのみ役立ちますが、現在のすべてのArm64実装はハイパースレッディングテクノロジをサポートしていないため、空の命令としてのみ処理されます。


次に、現在のシーケンスロックのシーケンス番号が以前に読み取ったシーケンスロックと一致しているかどうかを判断するread_seqretry関数の実装を見てみましょう。

static inline unsigned read_seqretry(const seqlock_t * sl、unsigned start)
{ return read_seqcount_retry(&sl-> seqcount、start); }调用了read_seqcount_retry関数数:


static inline int read_seqcount_retry(const seqcount_t s、unsigned start)
{ /
読み取りメモリバリア* /
smp_rmb();
return __read_seqcount_retry(s、start);
}
最初に読み取りメモリバリアを追加し、前のread_seqbeginで使用された読み取りメモリバリアはペアになっており、データを読み取る操作を実行するために使用される重要なセクションを形成します。次に、__ read_seqcount_retry関数が呼び出されます。

static inline int __read_seqcount_retry(const seqcount_t * s、unsigned start)
{ returnlikely (s-> sequence!= start); }現在のシーケンスロックのシーケンス番号が着信開始パラメーターの値と一致しているかどうかを判断するだけです。


リーダーはスピンロックによって保護されていないため、複数のリーダーが同時にデータを読み取ることができ、読み取りシーケンスロックによって現在のプロセスがスリープ状態になることはありません。

使用するシーン

シーケンスロックは万能薬ではなく、それに適した使用シナリオは次の条件を満たす必要があります。

読み取りが多く、書き込みが少ないシナリオに適しています。以前にコードを分析したとき、ライターはスピンロックによって保護されているため、一度に1人のライターのみがデータを書き込むことができ、リーダーは他のロックによって保護されず、同時に読み取ることがわかりました。そのため、本来の書き込み性能は高くなく、データの読み取り期間中は書き込みを行わないようにする必要があります。書き込みが多い場合は、読み取りを再試行し続けるため、パフォーマンスに大きな影響があります。
保護するデータは一般的に多すぎません。そうしないと、パフォーマンスに影響します。
保護されたデータ構造には、ライターによって変更され、リーダーによって間接的に参照されるポインターは含まれません。そうしないと、リーダーがポインターが指すデータを読み取っているときに、ライターがポインターを無効にする可能性があります。
リーダーの重要なセクションコードには、データの読み取り以外に他の副作用を引き起こす他の操作はありません。そうしないと、複数のリーダーの操作が互いに競合します。これは、シーケンシャルロックのリーダーは他のロックによって保護されておらず、全員が同時に読み取るため、読み取りメモリバリアのペアを使用してそれらを保護するだけです。
シーケンスロックによって、リーダーとライターがスリープ状態になることはありません。
Linuxカーネルで最も一般的な、システムjiffiesの更新は、使用されるシーケンシャルロックです。

注:C / C ++ Linux Advanced Server Architectの学習資料とqun:812855908が必要です(データには、C / C ++、Linux、golangテクノロジー、Nginx、ZeroMQ、MySQL、Redis、fastdfs、MongoDB、ZK、ストリーミングメディア、CDN、P2P、 K8S、Docker、TCP / IP、coroutine、DPDK、ffmpegなど)、無料で共有ここに写真の説明を挿入

おすすめ

転載: blog.csdn.net/qq_40989769/article/details/107715048