Heim  >  Artikel  >  System-Tutorial  >  Eine detaillierte Erklärung der Speicherbarrieren im Linux-Kernel

Eine detaillierte Erklärung der Speicherbarrieren im Linux-Kernel

WBOY
WBOYnach vorne
2024-02-10 15:00:24403Durchsuche

Vorwort

Ich habe zuvor einen Diskussionsartikel über sequentielle Konsistenz und Cache-Konsistenz gelesen und habe ein klareres Verständnis für den Unterschied und die Verbindung zwischen diesen beiden Konzepten. Im Linux-Kernel gibt es viele Synchronisations- und Barrieremechanismen, die ich hier zusammenfassen möchte.

Eine detaillierte Erklärung der Speicherbarrieren im Linux-Kernel

Cache-Konsistenz

Früher dachte ich immer, dass viele Mechanismen in Linux die Cache-Konsistenz gewährleisten sollen, aber tatsächlich wird der größte Teil der Cache-Konsistenz durch Hardware-Mechanismen erreicht. Lediglich bei der Verwendung von Anweisungen mit dem Lock-Präfix hat es etwas mit Caching zu tun (das ist zwar definitiv nicht streng, aber aus heutiger Sicht in den meisten Fällen der Fall). In den meisten Fällen möchten wir die sequentielle Konsistenz sicherstellen.

Cache-Kohärenz bedeutet, dass in einem Multiprozessorsystem jede CPU über einen eigenen L1-Cache verfügt. Da der Inhalt desselben Speichers im L1-Cache verschiedener CPUs zwischengespeichert werden kann, muss eine CPU beim Ändern ihres zwischengespeicherten Inhalts sicherstellen, dass beim Lesen dieser Daten auch eine andere CPU den neuesten Inhalt lesen kann. Aber keine Sorge, diese komplexe Arbeit wird vollständig von der Hardware erledigt. Durch die Implementierung des MESI-Protokolls kann die Hardware die Arbeit an der Cache-Kohärenz problemlos erledigen. Selbst wenn mehrere CPUs gleichzeitig schreiben, gibt es kein Problem. Ob im eigenen Cache, im Cache anderer CPUs oder im Speicher, eine CPU kann immer die neuesten Daten lesen. So funktioniert die Cache-Konsistenz.

Sequentielle Konsistenz

Die sogenannte sequentielle Konsistenz bezieht sich auf ein völlig anderes Konzept als die Cache-Konsistenz, obwohl beide Produkte der Entwicklung von Prozessoren sind. Da sich die Compiler-Technologie ständig weiterentwickelt, kann es sein, dass sich die Reihenfolge bestimmter Vorgänge ändert, um Ihren Code zu optimieren. Die Konzepte der Multi-Issue- und Out-of-Order-Ausführung sind in Prozessoren schon seit langem präsent. Das Ergebnis ist, dass die tatsächliche Reihenfolge der ausgeführten Anweisungen geringfügig von der Ausführungsreihenfolge des Codes während der Programmierung abweicht. Unter einem einzelnen Prozessor ist das natürlich nichts. Solange Ihr eigener Code nicht durchgeht, wird es niemanden stören. Der Compiler und der Prozessor stören gleichzeitig, dass ihr eigener Code nicht entdeckt werden kann. Bei Multiprozessoren ist dies jedoch nicht der Fall. Die Reihenfolge, in der Anweisungen auf einem Prozessor ausgeführt werden, kann einen großen Einfluss auf den auf anderen Prozessoren ausgeführten Code haben. Daher gibt es das Konzept der sequentiellen Konsistenz, das sicherstellt, dass die Ausführungsreihenfolge von Threads auf einem Prozessor aus Sicht der Threads auf anderen Prozessoren dieselbe ist. Die Lösung dieses Problems kann nicht allein durch den Prozessor oder Compiler gelöst werden, sondern erfordert einen Softwareeingriff.

Gedächtnisbarriere

Die Art und Weise des Softwareeingriffs ist ebenfalls sehr einfach, nämlich das Einfügen einer Speicherbarriere. Tatsächlich wurde der Begriff „Speicherbarriere“ von Prozessorentwicklern geprägt, was es für uns schwierig macht, ihn zu verstehen. Speicherbarrieren können leicht zur Cache-Konsistenz führen und sogar dazu führen, dass wir daran zweifeln, dass andere CPUs den geänderten Cache sehen können. Die sogenannte Speicherbarriere wird aus Prozessorsicht zur Serialisierung von Lese- und Schreibvorgängen verwendet. Aus Softwaresicht wird sie zur Lösung des Problems der sequentiellen Konsistenz verwendet. Möchte der Compiler die Reihenfolge der Codeausführung nicht stören? Möchte der Prozessor den Code nicht in der falschen Reihenfolge ausführen? Die Barriere kann nicht rückgängig gemacht werden. Sie teilt dem Prozessor mit, dass er nur auf die Anweisungen vor der Barriere warten kann. Nachdem die Anweisung ausgeführt wurde, können die Anweisungen hinter der Barriere ausgeführt werden. Natürlich können Speicherbarrieren den Compiler davon abhalten, herumzuspielen, aber der Prozessor hat immer noch eine Möglichkeit. Gibt es im Prozessor nicht ein Konzept der Multi-Issue-, Out-of-Order-Ausführung und sequentiellen Vervollständigung? Während der Speicherbarriere muss nur sichergestellt werden, dass die Lese- und Schreibvorgänge der vorherigen Anweisungen abgeschlossen sein müssen Die Lese- und Schreibvorgänge der folgenden Anweisungen sind abgeschlossen. Daher gibt es drei Arten von Speicherbarrieren: Lesebarrieren, Schreibbarrieren und Lese-/Schreibbarrieren. Vor x86 wurde beispielsweise garantiert, dass Schreibvorgänge in der richtigen Reihenfolge abgeschlossen werden, sodass keine Schreibbarrieren erforderlich sind. Bei einigen ia32-Prozessoren werden die Schreibvorgänge jetzt jedoch nicht in der richtigen Reihenfolge ausgeführt, sodass auch Schreibbarrieren erforderlich sind.
Tatsächlich gibt es neben speziellen Lese-/Schreibbarriere-Anweisungen viele Anweisungen, die mit Lese-/Schreibbarriere-Funktionen ausgeführt werden, beispielsweise Anweisungen mit einem Sperrpräfix. Vor dem Aufkommen spezieller Lese- und Schreibbarriereanweisungen war Linux zum Überleben auf Sperren angewiesen.
Wo die Lese- und Schreibbarrieren eingefügt werden, hängt von den Anforderungen der Software ab. Die Lese-Schreib-Barriere kann die sequentielle Konsistenz nicht vollständig erreichen, aber der Thread auf dem Multiprozessor wird nicht immer auf Ihre Ausführungsreihenfolge starren, solange er beim Durchsehen denkt, dass Sie die sequentielle Konsistenz einhalten Die Ausführung führt nicht zu unerwarteten Situationen im Code. In der sogenannten unerwarteten Situation weist Ihr Thread beispielsweise zuerst der Variablen a einen Wert zu und weist dann der Variablen b einen Wert zu. Infolgedessen schauen Threads, die auf anderen Prozessoren ausgeführt werden, nach und stellen fest, dass b ein Wert zugewiesen wurde. aber a wurde kein Wert zugewiesen (Hinweis: Diese Inkonsistenz wird nicht durch Cache-Inkonsistenz verursacht, sondern durch die Inkonsistenz in der Reihenfolge, in der die Prozessor-Schreibvorgänge abgeschlossen werden. In diesem Fall muss eine Schreibbarriere zwischen der Zuweisung hinzugefügt werden.) von a und die Zuordnung von b.

Synchronisation zwischen mehreren Prozessoren

Mit SMP werden Threads gleichzeitig auf mehreren Prozessoren ausgeführt. Solange es sich um einen Thread handelt, bestehen Kommunikations- und Synchronisierungsanforderungen. Glücklicherweise verwendet das SMP-System gemeinsam genutzten Speicher, was bedeutet, dass alle Prozessoren den gleichen Speicherinhalt sehen. Obwohl es einen unabhängigen L1-Cache gibt, wird die Cache-Konsistenzverarbeitung immer noch von der Hardware übernommen. Wenn Threads auf verschiedenen Prozessoren auf dieselben Daten zugreifen möchten, benötigen sie kritische Abschnitte und Synchronisierung. Wovon hängt die Synchronisierung ab? Im UP-System haben wir uns zuvor oben auf Semaphoren verlassen und unten Interrupts sowie Lese-, Änderungs- und Schreibanweisungen deaktiviert. In SMP-Systemen wurde das Ausschalten von Interrupts nun abgeschafft. Obwohl es immer noch notwendig ist, Threads auf demselben Prozessor zu synchronisieren, reicht es nicht mehr aus, sich nur darauf zu verlassen. Anweisungen lesen, ändern, schreiben? Nicht länger. Wenn der Lesevorgang in Ihrer Anweisung abgeschlossen ist und der Schreibvorgang nicht ausgeführt wird, führt möglicherweise ein anderer Prozessor einen Lesevorgang oder einen Schreibvorgang aus. Das Cache-Kohärenzprotokoll ist fortgeschritten, aber noch nicht weit genug, um vorherzusagen, welcher Befehl diesen Lesevorgang ausgegeben hat. Also erfand x86 Anweisungen mit Sperrpräfix. Wenn dieser Befehl ausgeführt wird, werden alle Cache-Zeilen, die die Lese- und Schreibadressen im Befehl enthalten, ungültig gemacht und der Speicherbus wird gesperrt. Wenn andere Prozessoren auf diese Weise dieselbe Adresse oder die Adresse in derselben Cache-Zeile lesen oder schreiben möchten, können sie dies weder aus dem Cache tun (die entsprechende Zeile im Cache ist abgelaufen), noch können sie dies aus dem Cache tun Speicherbus (der gesamte Speicherbus ist ausgefallen), wodurch endlich das Ziel der atomaren Ausführung erreicht wird. Wenn sich ab dem P6-Prozessor die Adresse, auf die der Sperrpräfixbefehl zugreifen soll, bereits im Cache befindet, ist es natürlich nicht erforderlich, den Speicherbus zu sperren, und die atomare Operation kann abgeschlossen werden (obwohl ich vermute, dass dies daran liegt). die Hinzufügung der internen gemeinsamen Funktion des Multiprozessors).

Da der Speicherbus gesperrt wird, werden unvollendete Lese- und Schreibvorgänge abgeschlossen, bevor Anweisungen mit dem Sperrpräfix ausgeführt werden, das auch als Speicherbarriere dient.
Heutzutage werden für die Thread-Synchronisation zwischen Multiprozessoren oben Spin-Locks und unten Lese-, Änderungs- und Schreibanweisungen mit Lock-Präfix verwendet. Natürlich umfasst die eigentliche Synchronisierung auch das Deaktivieren der Task-Planung des Prozessors, das Hinzufügen von Task-Off-Interrupts und das Hinzufügen eines Semaphors außerhalb. Die Implementierung dieser Art von Spin-Lock in Linux hat vier Entwicklungsgenerationen durchlaufen und ist effizienter und leistungsfähiger geworden.

内存屏障的实现

\#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问题,等等。

多处理器间同步的实现
多处理器间同步所使用的自旋锁实现,已经有专门的文章介绍

Das obige ist der detaillierte Inhalt vonEine detaillierte Erklärung der Speicherbarrieren im Linux-Kernel. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:lxlinux.net. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen