ホームページ > 記事 > システムチュートリアル > Linux カーネルのメモリバリアの詳細な説明
######序文######
キャッシュの一貫性
ソフトウェア介入の方法も非常に簡単で、メモリバリアを挿入するだけです。実際、メモリバリアという用語はプロセッサ開発者によって作られたものであり、私たちにとっては理解しにくいものです。メモリバリアにより、キャッシュの一貫性が簡単に失われる可能性があり、変更されたキャッシュを他の CPU が参照できるようにすることができるかどうかすら疑問になりますが、そう考えるのは間違いです。いわゆるメモリ バリアは、プロセッサの観点から見ると、読み取りおよび書き込み操作をシリアル化するために使用され、ソフトウェアの観点から見ると、逐次一貫性の問題を解決するために使用されます。コンパイラは、コードの実行順序を乱したくないのでしょうか? プロセッサは、コードを順序どおりに実行したくないのでしょうか? メモリ バリアを挿入することは、コンパイラに命令の前後の順序を変更するように指示することと同じです。バリアは元に戻すことはできません。プロセッサに対して、バリアの前の命令のみを待機できるように指示します。命令が実行された後、バリアの後ろの命令の実行を開始できます。もちろん、メモリの障壁によってコンパイラの混乱を防ぐことはできますが、プロセッサにはまだ解決策があります。プロセッサには、複数発行、アウトオブオーダー実行、およびシーケンシャル完了という概念はありませんか? メモリ バリア中は、前の命令の読み取りおよび書き込み操作が、プロセッサが実行される前に完了していることを確認するだけで十分です。次の命令の読み取りおよび書き込み操作が完了します。したがって、メモリ バリアには、読み取りバリア、書き込みバリア、読み取り/書き込みバリアの 3 種類があります。たとえば、x86 より前では、書き込み操作は順序どおりに完了することが保証されていたため、書き込みバリアは必要ありませんでしたが、一部の ia32 プロセッサでは、書き込み操作が順序どおりに完了しないため、書き込みバリアも必要になりました。
実際、特殊な読み書きバリア命令に加えて、ロック プレフィックスを持つ命令など、読み書きバリア機能を使用して実行される命令が多数あります。特殊な読み取りおよび書き込みバリア命令が登場するまで、Linux はロックに依存して生き残っていました。
読み取りおよび書き込みバリアをどこに挿入するかについては、ソフトウェアのニーズによって異なります。読み取り/書き込みバリアは逐次一貫性を完全に達成することはできませんが、マルチプロセッサ上のスレッドは実行順序を常に監視しているわけではありません。監視するときに逐次一貫性が遵守されていると判断される限り、スレッドは実行順序を監視します。実行によってコードに予期しない状況が発生することはありません。いわゆる予期せぬ状況、たとえば、スレッドが最初に変数 a に値を割り当て、次に変数 b に値を割り当てます。その結果、他のプロセッサで実行されているスレッドが調べて、b に値が割り当てられていることがわかります。しかし、 a には値が割り当てられていません (注: この不一致はキャッシュの不一致が原因ではなく、プロセッサの書き込み操作が完了する順序の不一致によって引き起こされます)。この場合、割り当てと割り当ての間に書き込みバリアを追加する必要があります。 a と b の代入。
SMP を使用すると、スレッドは複数のプロセッサ上で同時に実行を開始します。スレッドである限り、通信と同期の要件が存在します。幸いなことに、SMP システムは共有メモリを使用するため、すべてのプロセッサが同じメモリ内容を認識します。独立した L1 キャッシュがありますが、キャッシュの整合性処理は依然としてハードウェアによって処理されます。異なるプロセッサ上のスレッドが同じデータにアクセスしたい場合は、クリティカル セクションと同期が必要です。同期は何に依存しますか?以前の UP システムでは、上部ではセマフォに依存し、下部では割り込みと読み取り、変更、書き込み命令をオフにしていました。現在、SMP システムでは割り込みをオフにする機能は廃止されており、同じプロセッサ上のスレッドを同期する必要があることに変わりはありませんが、それだけに頼るだけでは十分ではなくなりました。読み取り変更書き込み命令?もうない。命令内の読み取り操作が完了し、書き込み操作が実行されていない場合、別のプロセッサーが読み取り操作または書き込み操作を実行する可能性があります。キャッシュ コヒーレンス プロトコルは進歩していますが、どの命令がこの読み取り操作を発行したかを予測できるほどまだ進歩していません。そこで、x86 はロック プレフィックスを伴う命令を発明しました。この命令が実行されると、命令内の読み取りおよび書き込みアドレスを含むすべてのキャッシュ ラインが無効になり、メモリ バスがロックされます。このように、他のプロセッサが同じアドレスまたは同じキャッシュ ライン上のアドレスを読み書きしたい場合、キャッシュから実行することも (キャッシュ内の関連するラインの有効期限が切れている)、キャッシュから実行することもできません。メモリ バス (メモリ バス全体に障害が発生しました)、ロックされています)、最終的にアトミック実行の目標を達成します。もちろん、P6 プロセッサ以降では、ロック プレフィックス命令でアクセスするアドレスがすでにキャッシュ内にある場合、メモリ バスをロックする必要はなく、アトミック操作を完了できます (これはおそらく、マルチプロセッサの内部共通機能の追加) L2キャッシュのため)
メモリ バスがロックされるため、未完了の読み取りおよび書き込み操作は、ロック プレフィックスが付いた命令が実行される前に完了し、メモリ バリアとしても機能します。
現在、マルチプロセッサ間のスレッドの同期には、上部でスピン ロックが使用され、下部でロック プレフィックスを使用した読み取り、変更、書き込み命令が使用されます。もちろん、実際の同期には、プロセッサのタスク スケジューリングの無効化、タスクオフ割り込みの追加、外部セマフォの追加も含まれます。 Linux でのこの種のスピン ロックの実装は 4 世代にわたる開発を経て、より効率的かつ強力になりました。
\#ifdef CONFIG_SMP \#define smp_mb() mb() \#define smp_rmb() rmb() \#define smp_wmb() wmb() \#else \#define smp_mb() barrier() \#define smp_rmb() barrier() \#define smp_wmb() barrier() \#endif
CONFIG_SMP就是用来支持多处理器的。如果是UP(uniprocessor)系统,就会翻译成barrier()。
#define barrier() asm volatile(“”: : :”memory”)
barrier()的作用,就是告诉编译器,内存的变量值都改变了,之前存在寄存器里的变量副本无效,要访问变量还需再访问内存。这样做足以满足UP中所有的内存屏障。
\#ifdef CONFIG_X86_32 /* \* Some non-Intel clones support out of order store. wmb() ceases to be a \* nop for these. */ \#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2) \#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2) \#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM) \#else \#define mb() asm volatile("mfence":::"memory") \#define rmb() asm volatile("lfence":::"memory") \#define wmb() asm volatile("sfence" ::: "memory") \#endif
如果是SMP系统,内存屏障就会翻译成对应的mb()、rmb()和wmb()。这里CONFIG_X86_32的意思是说这是一个32位x86系统,否则就是64位的x86系统。现在的linux内核将32位x86和64位x86融合在同一个x86目录,所以需要增加这个配置选项。
可以看到,如果是64位x86,肯定有mfence、lfence和sfence三条指令,而32位的x86系统则不一定,所以需要进一步查看cpu是否支持这三条新的指令,不行则用加锁的方式来增加内存屏障。
SFENCE,LFENCE,MFENCE指令提供了高效的方式来保证读写内存的排序,这种操作发生在产生弱排序数据的程序和读取这个数据的程序之间。 SFENCE——串行化发生在SFENCE指令之前的写操作但是不影响读操作。 LFENCE——串行化发生在SFENCE指令之前的读操作但是不影响写操作。 MFENCE——串行化发生在MFENCE指令之前的读写操作。 sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成。 lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成。 mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
至于带lock的内存操作,会在锁内存总线之前,就把之前的读写操作结束,功能相当于mfence,当然执行效率上要差一些。
说起来,现在写点底层代码真不容易,既要注意SMP问题,又要注意cpu乱序读写问题,还要注意cache问题,还有设备DMA问题,等等。
多处理器间同步的实现
多处理器间同步所使用的自旋锁实现,已经有专门的文章介绍
以上がLinux カーネルのメモリバリアの詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。