深入理解 Linux 內(nèi)存優(yōu)化:如何使用屏障提升性能
在當(dāng)今快節(jié)奏的數(shù)字時(shí)代,無(wú)論是運(yùn)行大型數(shù)據(jù)庫(kù)的服務(wù)器,還是流暢播放高清視頻的多媒體設(shè)備,亦或是精準(zhǔn)控制生產(chǎn)流程的工業(yè)控制系統(tǒng),其背后的 Linux 系統(tǒng)都肩負(fù)著高效管理內(nèi)存的重任。內(nèi)存管理,作為 Linux 內(nèi)核的核心職能之一,就如同精密儀器中的齒輪組,有條不紊地協(xié)調(diào)著數(shù)據(jù)的存儲(chǔ)與讀取,為上層應(yīng)用的穩(wěn)定運(yùn)行筑牢根基。
然而,隨著計(jì)算機(jī)硬件性能的突飛猛進(jìn),尤其是多核處理器的廣泛普及,內(nèi)存訪問(wèn)的復(fù)雜性也呈指數(shù)級(jí)增長(zhǎng)。為了充分挖掘硬件潛力,提升系統(tǒng)整體性能,現(xiàn)代計(jì)算機(jī)往往采用亂序執(zhí)行、緩存機(jī)制等優(yōu)化手段。但這也帶來(lái)了新的挑戰(zhàn):內(nèi)存操作的順序可能變得難以捉摸,數(shù)據(jù)一致性問(wèn)題時(shí)有發(fā)生,進(jìn)而影響到應(yīng)用程序的正確性與穩(wěn)定性。
在這一背景下,內(nèi)存優(yōu)化屏障應(yīng)運(yùn)而生,它宛如一把精準(zhǔn)的 “秩序之鎖”,巧妙地控制著內(nèi)存操作的先后順序,確保在復(fù)雜的硬件架構(gòu)與優(yōu)化策略下,數(shù)據(jù)依然能夠按照開(kāi)發(fā)者預(yù)期的方式流動(dòng)。那么,內(nèi)存優(yōu)化屏障究竟是如何在 Linux 系統(tǒng)中發(fā)揮作用的?它又能為我們的應(yīng)用性能帶來(lái)怎樣的提升?接下來(lái),就讓我們一同揭開(kāi) Linux 內(nèi)存優(yōu)化屏障的神秘面紗,探尋其中的奧秘 。
一、內(nèi)存屏障簡(jiǎn)介
1. 內(nèi)存屏障概述
在計(jì)算機(jī)系統(tǒng)中,為了提升性能,現(xiàn)代 CPU 和編譯器常常會(huì)對(duì)指令進(jìn)行重排序。指令重排序是指在不改變程序最終執(zhí)行結(jié)果的前提下,調(diào)整指令的執(zhí)行順序,以充分利用 CPU 的資源,提高執(zhí)行效率 。例如,當(dāng) CPU 執(zhí)行一系列指令時(shí),如果某些指令之間不存在數(shù)據(jù)依賴關(guān)系,CPU 可能會(huì)先執(zhí)行后面的指令,再執(zhí)行前面的指令。
在單線程環(huán)境下,指令重排序通常不會(huì)帶來(lái)問(wèn)題,因?yàn)槌绦虻膱?zhí)行結(jié)果仍然符合預(yù)期。然而,在多線程環(huán)境中,指令重排序可能會(huì)導(dǎo)致意想不到的結(jié)果,因?yàn)椴煌€程之間的操作可能會(huì)相互干擾。比如,線程 A 和線程 B 同時(shí)訪問(wèn)共享內(nèi)存,線程 A 對(duì)共享變量的修改可能不會(huì)立即被線程 B 看到,這就導(dǎo)致了數(shù)據(jù)可見(jiàn)性問(wèn)題。
為了解決這些問(wèn)題,內(nèi)存優(yōu)化屏障應(yīng)運(yùn)而生。內(nèi)存優(yōu)化屏障是一種特殊的指令或機(jī)制,它可以阻止 CPU 和編譯器對(duì)特定指令進(jìn)行重排序,從而保證內(nèi)存操作的順序性和可見(jiàn)性。通過(guò)使用內(nèi)存優(yōu)化屏障,程序員可以確保在多線程環(huán)境下,內(nèi)存操作按照預(yù)期的順序執(zhí)行,避免數(shù)據(jù)競(jìng)爭(zhēng)和其他并發(fā)問(wèn)題。
2. 為什么會(huì)出現(xiàn)內(nèi)存屏障?
由于現(xiàn)在計(jì)算機(jī)存在多級(jí)緩存且多核場(chǎng)景,為了保證讀取到的數(shù)據(jù)一致性以及并行運(yùn)行時(shí)所計(jì)算出來(lái)的結(jié)果一致,在硬件層面實(shí)現(xiàn)一些指令,從而來(lái)保證指定執(zhí)行的指令的先后順序。比如上圖:雙核cpu,每個(gè)核心都擁有獨(dú)立的一二級(jí)緩存,而緩存與緩存之間需要保證數(shù)據(jù)的一致性所以這里才需要加添屏障來(lái)確保數(shù)據(jù)的一致性。三級(jí)緩存為各CPU共享,最后都是主內(nèi)存,所以這些存在交互的CPU都需要通過(guò)屏障手段來(lái)保證數(shù)據(jù)的唯一性。
內(nèi)存屏障存在的意義就是為了解決程序在運(yùn)行過(guò)程中出現(xiàn)的內(nèi)存亂序訪問(wèn)問(wèn)題,內(nèi)存亂序訪問(wèn)行為出現(xiàn)的理由是為了提高程序運(yùn)行時(shí)的性能,Memory Bariier能夠讓CPU或編譯器在內(nèi)存訪問(wèn)上有序。
(1) 運(yùn)行時(shí)內(nèi)存亂序訪問(wèn)
運(yùn)行時(shí),CPU本身是會(huì)亂序執(zhí)行指令的。早期的處理器為有序處理器(in-order processors),總是按開(kāi)發(fā)者編寫(xiě)的順序執(zhí)行指令, 如果指令的輸入操作對(duì)象(input operands)不可用(通常由于需要從內(nèi)存中獲?。?, 那么處理器不會(huì)轉(zhuǎn)而執(zhí)行那些輸入操作對(duì)象可用的指令,而是等待當(dāng)前輸入操作對(duì)象可用。
相比之下,亂序處理器(out-of-order processors)會(huì)先處理那些有可用輸入操作對(duì)象的指令(而非順序執(zhí)行) 從而避免了等待,提高了效率?,F(xiàn)代計(jì)算機(jī)上,處理器運(yùn)行的速度比內(nèi)存快很多, 有序處理器花在等待可用數(shù)據(jù)的時(shí)間里已可處理大量指令了。即便現(xiàn)代處理器會(huì)亂序執(zhí)行, 但在單個(gè)CPU上,指令能通過(guò)指令隊(duì)列順序獲取并執(zhí)行,結(jié)果利用隊(duì)列順序返回寄存器堆(詳情可參考http:// http://en.wikipedia.org/wiki/Out-of-order_execution),這使得程序執(zhí)行時(shí)所有的內(nèi)存訪問(wèn)操作看起來(lái)像是按程序代碼編寫(xiě)的順序執(zhí)行的, 因此內(nèi)存屏障是沒(méi)有必要使用的(前提是不考慮編譯器優(yōu)化的情況下)。
(2) SMP架構(gòu)需要內(nèi)存屏障的進(jìn)一步解釋
從體系結(jié)構(gòu)上來(lái)看,首先在SMP架構(gòu)下,每個(gè)CPU與內(nèi)存之間,都配有自己的高速緩存(Cache),以減少訪問(wèn)內(nèi)存時(shí)的沖突采用高速緩存的寫(xiě)操作有兩種模式:
- 穿透(Write through)模式,每次寫(xiě)時(shí),都直接將數(shù)據(jù)寫(xiě)回內(nèi)存中,效率相對(duì)較低;
- 回寫(xiě)(Write back)模式,寫(xiě)的時(shí)候先寫(xiě)回告訴緩存,然后由高速緩存的硬件再周轉(zhuǎn)復(fù)用緩沖線(Cache Line)時(shí)自動(dòng)將數(shù)據(jù)寫(xiě)回內(nèi)存, 或者由軟件主動(dòng)地“沖刷”有關(guān)的緩沖線(Cache Line)。
出于性能的考慮,系統(tǒng)往往采用的是模式2來(lái)完成數(shù)據(jù)寫(xiě)入;正是由于存在高速緩存這一層,正是由于采用了Write back模式的數(shù)據(jù)寫(xiě)入,才導(dǎo)致在SMP架構(gòu)下,對(duì)高速緩存的運(yùn)用可能改變對(duì)內(nèi)存操作的順序。
已上面的一個(gè)簡(jiǎn)短代碼為例:
// thread 0 -- 在CPU0上運(yùn)行
x = 42;
ok = 1;
// thread 1 – 在CPU1上運(yùn)行
while(!ok);
print(x);
這里CPU1執(zhí)行時(shí), x一定是打印出42嗎?讓我們來(lái)看看以下圖為例的說(shuō)明:
假設(shè),正好CPU0的高速緩存中有x,此時(shí)CPU0僅僅是將x=42寫(xiě)入到了高速緩存中,另外一個(gè)ok也在高速緩存中,但由于周轉(zhuǎn)復(fù)用高速緩沖線(Cache Line)而導(dǎo)致將ok=1刷會(huì)到了內(nèi)存中,此時(shí)CPU1首先執(zhí)行對(duì)ok內(nèi)存的讀取操作,他讀到了ok為1的結(jié)果,進(jìn)而跳出循環(huán),讀取x的內(nèi)容,而此時(shí),由于實(shí)際寫(xiě)入的x(42)還只在CPU0的高速緩存中,導(dǎo)致CPU1讀到的數(shù)據(jù)為x(17)。
程序中編排好的內(nèi)存訪問(wèn)順序(指令序:program ordering)是先寫(xiě)入x,再寫(xiě)入y。而實(shí)際上出現(xiàn)在該CPU外部,即系統(tǒng)總線上的次序(處理器序:processor ordering),卻是先寫(xiě)入y,再寫(xiě)入x(這個(gè)例子中x還未寫(xiě)入)。
在SMP架構(gòu)中,每個(gè)CPU都只知道自己何時(shí)會(huì)改變內(nèi)存的內(nèi)容,但是都不知道別的CPU會(huì)在什么時(shí)候改變內(nèi)存的內(nèi)容,也不知道自己本地的高速緩存中的內(nèi)容是否與內(nèi)存中的內(nèi)容不一致。
反過(guò)來(lái),每個(gè)CPU都可能因?yàn)楦淖兞藘?nèi)存內(nèi)容,而使得其他CPU的高速緩存變的不一致了。在SMP架構(gòu)下,由于高速緩存的存在而導(dǎo)致的內(nèi)存訪問(wèn)次序(讀或?qū)懚加锌赡軙?shū)序被改變)的改變很有可能影響到CPU間的同步與互斥。
因此需要有一種手段,使得在某些操作之前,把這種“欠下”的內(nèi)存操作(本例中的x=42的內(nèi)存寫(xiě)入)全都最終地、物理地完成,就好像把欠下的債都結(jié)清,然后再開(kāi)始新的(通常是比較重要的)活動(dòng)一樣。這種手段就是內(nèi)存屏障,其本質(zhì)原理就是對(duì)系統(tǒng)總線加鎖。
回過(guò)頭來(lái),我們?cè)賮?lái)看看為什么非SMP架構(gòu)(UP架構(gòu))下,運(yùn)行時(shí)內(nèi)存亂序訪問(wèn)不存在。
在單處理器架構(gòu)下,各個(gè)進(jìn)程在宏觀上是并行的,但是在微觀上卻是串行的,因?yàn)樵谕粫r(shí)間點(diǎn)上,只有一個(gè)進(jìn)程真正在運(yùn)行(系統(tǒng)中只有一個(gè)處理器)。
在這種情況下,我們?cè)賮?lái)看看上面提到的例子:
線程0和線程1的指令都將在CPU0上按照指令序執(zhí)行。thread0通過(guò)CPU0完成x=42的高速緩存寫(xiě)入后,再將ok=1寫(xiě)入內(nèi)存,此后串行的將thread0換出,thread1換入,及時(shí)此時(shí)x=42并未寫(xiě)入內(nèi)存,但由于thread1的執(zhí)行仍然是在CPU0上執(zhí)行,他仍然訪問(wèn)的是CPU0的高速緩存,因此,及時(shí)x=42還未寫(xiě)回到內(nèi)存中,thread1勢(shì)必還是先從高速緩存中讀到x=42,再?gòu)膬?nèi)存中讀到ok=1。
綜上所述,在單CPU上,多線程執(zhí)行不存在運(yùn)行時(shí)內(nèi)存亂序訪問(wèn),我們從內(nèi)核源碼也可得到類似結(jié)論(代碼不完全摘錄)
#define barrier() __asm__ __volatile__("": : :"memory")
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)
#ifdef CONFIG_SMP
#define smp_mb() mb()
#define smp_rmb() rmb()
#define smp_wmb() wmb()
#define smp_read_barrier_depends() read_barrier_depends()
#define set_mb(var, value) do { (void) xchg(&var, value); } while (0)
#else
#define smp_mb() barrier()
#define smp_rmb() barrier()
#define smp_wmb() barrier()
#define smp_read_barrier_depends() do { } while(0)
#define set_mb(var, value) do { var = value; barrier(); } while (0)
#endif
這里可看到對(duì)內(nèi)存屏障的定義,如果是SMP架構(gòu),smp_mb定義為mb(),mb()為CPU內(nèi)存屏障(接下來(lái)要談的),而非SMP架構(gòu)時(shí)(也就是UP架構(gòu)),直接使用編譯器屏障,運(yùn)行時(shí)內(nèi)存亂序訪問(wèn)并不存在。
(3) 為什么多CPU情況下會(huì)存在內(nèi)存亂序訪問(wèn)?
我們知道每個(gè)CPU都存在Cache,當(dāng)一個(gè)特定數(shù)據(jù)第一次被其他CPU獲取時(shí),此數(shù)據(jù)顯然不在對(duì)應(yīng)CPU的Cache中(這就是Cache Miss)。
這意味著CPU要從內(nèi)存中獲取數(shù)據(jù)(這個(gè)過(guò)程需要CPU等待數(shù)百個(gè)周期),此數(shù)據(jù)將被加載到CPU的Cache中,這樣后續(xù)就能直接從Cache上快速訪問(wèn)。
當(dāng)某個(gè)CPU進(jìn)行寫(xiě)操作時(shí),他必須確保其他CPU已將此數(shù)據(jù)從他們的Cache中移除(以便保證一致性),只有在移除操作完成后,此CPU才能安全地修改數(shù)據(jù)。
顯然,存在多個(gè)Cache時(shí),必須通過(guò)一個(gè)Cache一致性協(xié)議來(lái)避免數(shù)據(jù)不一致的問(wèn)題,而這個(gè)通信的過(guò)程就可能導(dǎo)致亂序訪問(wèn)的出現(xiàn),也就是運(yùn)行時(shí)內(nèi)存亂序訪問(wèn)。
受篇幅所限,這里不再深入討論整個(gè)細(xì)節(jié),有興趣的讀者可以研究《Memory Barriers: a Hardware View for Software Hackers》這篇文章,它詳細(xì)地分析了整個(gè)過(guò)程。
現(xiàn)在通過(guò)一個(gè)例子來(lái)直觀地說(shuō)明多CPU下內(nèi)存亂序訪問(wèn)的問(wèn)題:
volatile int x, y, r1, r2;
//thread 1
void run1()
{
x = 1;
r1 = y;
}
//thread 2
void run2
{
y = 1;
r2 = x;
}
變量x、y、r1、r2均被初始化為0,run1和run2運(yùn)行在不同的線程中。
如果run1和run2在同一個(gè)cpu下執(zhí)行完成,那么就如我們所料,r1和r2的值不會(huì)同時(shí)為0,而假如run1和run2在不同的CPU下執(zhí)行完成后,由于存在內(nèi)存亂序訪問(wèn)的可能,這時(shí)r1和r2可能同時(shí)為0。我們可以使用CPU內(nèi)存屏障來(lái)避免運(yùn)行時(shí)內(nèi)存亂序訪問(wèn)(x86_64):
void run1()
{
x = 1;
//CPU內(nèi)存屏障,保證x=1在r1=y之前執(zhí)行
__asm__ __volatile__("mfence":::"memory");
r1 = y;
}
//thread 2
void run2
{
y = 1;
//CPU內(nèi)存屏障,保證y = 1在r2 = x之前執(zhí)行
__asm__ __volatile__("mfence":::"memory");
r2 = x;
}
二、內(nèi)存屏障核心原理
1.譯器優(yōu)化與優(yōu)化屏障
在程序編譯階段,編譯器為了提高代碼的執(zhí)行效率,會(huì)對(duì)代碼進(jìn)行優(yōu)化,其中指令重排是一種常見(jiàn)的優(yōu)化手段。例如,對(duì)于下面的 C 代碼:
int a = 1;
int b = 2;
在沒(méi)有數(shù)據(jù)依賴的情況下,編譯器可能會(huì)將其編譯成匯編代碼時(shí),交換這兩條指令的順序,先執(zhí)行b = 2,再執(zhí)行a = 1。在單線程環(huán)境下,這種重排通常不會(huì)影響程序的最終結(jié)果。但在多線程環(huán)境中,當(dāng)多個(gè)線程共享數(shù)據(jù)時(shí),這種重排可能會(huì)導(dǎo)致數(shù)據(jù)一致性問(wèn)題 。
為了禁止編譯器對(duì)特定指令進(jìn)行重排,Linux 內(nèi)核提供了優(yōu)化屏障機(jī)制。在 Linux 內(nèi)核中,通過(guò)barrier()宏來(lái)實(shí)現(xiàn)優(yōu)化屏障 。barrier()宏的定義如下:
#define barrier() __asm__ __volatile__("" ::: "memory")
__asm__表示這是一段匯編代碼,__volatile__告訴編譯器不要對(duì)這段代碼進(jìn)行優(yōu)化,即不要改變其前后代碼塊的順序 。"memory"表示內(nèi)存中的變量值可能會(huì)發(fā)生變化,編譯器不能使用寄存器中的值來(lái)優(yōu)化,而應(yīng)該重新從內(nèi)存中加載變量的值。這樣,在barrier()宏之前的指令不會(huì)被移動(dòng)到barrier()宏之后,之后的指令也不會(huì)被移動(dòng)到之前,從而保證了編譯器層面的指令順序。
2. CPU 執(zhí)行優(yōu)化與內(nèi)存屏障
現(xiàn)代 CPU 為了提高執(zhí)行效率,采用了超標(biāo)量體系結(jié)構(gòu)和亂序執(zhí)行技術(shù)。CPU 在執(zhí)行指令時(shí),會(huì)按照程序順序取出一批指令,分析找出沒(méi)有依賴關(guān)系的指令,發(fā)給多個(gè)獨(dú)立的執(zhí)行單元并行執(zhí)行,最后按照程序順序提交執(zhí)行結(jié)果,即 “順序取指令,亂序執(zhí)行,順序提交執(zhí)行結(jié)果” 。
例如,當(dāng) CPU 執(zhí)行指令A(yù)需要從內(nèi)存中讀取數(shù)據(jù),而這個(gè)讀取操作需要花費(fèi)較長(zhǎng)時(shí)間時(shí),CPU 不會(huì)等待指令A(yù)完成,而是會(huì)繼續(xù)執(zhí)行后續(xù)沒(méi)有數(shù)據(jù)依賴的指令B、C等,直到指令A(yù)的數(shù)據(jù)讀取完成,再繼續(xù)執(zhí)行指令A(yù)的后續(xù)操作 。
雖然 CPU 的亂序執(zhí)行可以提高執(zhí)行效率,但在某些情況下,這種亂序執(zhí)行可能會(huì)導(dǎo)致問(wèn)題。比如,在多處理器系統(tǒng)中,一個(gè)處理器修改數(shù)據(jù)后,可能不會(huì)把數(shù)據(jù)立即同步到自己的緩存或者其他處理器的緩存,導(dǎo)致其他處理器不能立即看到最新的數(shù)據(jù)。為了解決這個(gè)問(wèn)題,需要使用內(nèi)存屏障來(lái)保證 CPU 執(zhí)行指令的順序 。
內(nèi)存屏障確保在屏障原語(yǔ)前的指令完成后,才會(huì)啟動(dòng)原語(yǔ)之后的指令操作。在不同的 CPU 架構(gòu)中,有不同的指令來(lái)實(shí)現(xiàn)內(nèi)存屏障的功能。例如,在 X86 系統(tǒng)中,以下這些匯編指令可以充當(dāng)內(nèi)存屏障:
- 所有操作 I/O 端口的指令;
- 前綴lock的指令,如lock;addl $0,0(%esp),雖然這條指令本身沒(méi)有實(shí)際意義(對(duì)棧頂保存的內(nèi)存地址內(nèi)的內(nèi)容加上 0),但lock前綴對(duì)數(shù)據(jù)總線加鎖,從而使該條指令成為內(nèi)存屏障;
- 所有寫(xiě)控制寄存器、系統(tǒng)寄存器或 debug 寄存器的指令(比如,cli和sti指令,可以改變eflags寄存器的IF標(biāo)志);
- lfence、sfence和mfence匯編指令,分別用來(lái)實(shí)現(xiàn)讀內(nèi)存屏障、寫(xiě)內(nèi)存屏障和讀 / 寫(xiě)內(nèi)存屏障;
- 特殊的匯編指令,比如iret指令,可以終止中斷或異常處理程序。
在 ARM 系統(tǒng)中,則使用ldrex和strex匯編指令實(shí)現(xiàn)內(nèi)存屏障。這些內(nèi)存屏障指令能夠阻止 CPU 對(duì)指令的亂序執(zhí)行,確保內(nèi)存操作的順序性和可見(jiàn)性,從而保證多線程環(huán)境下程序的正確執(zhí)行。
三、內(nèi)存屏障的多元類型與功能詳解
在 Linux 系統(tǒng)中,內(nèi)存屏障主要包括通用內(nèi)存屏障、讀內(nèi)存屏障、寫(xiě)內(nèi)存屏障和讀寫(xiě)內(nèi)存屏障,它們各自在保證內(nèi)存操作的順序性和可見(jiàn)性方面發(fā)揮著關(guān)鍵作用 。
1. 通用內(nèi)存屏障
通用內(nèi)存屏障(mb)確保在其之前的所有內(nèi)存讀寫(xiě)操作都在其之后的內(nèi)存讀寫(xiě)操作之前完成 。它保證了其前后的讀寫(xiě)指令順序,防止編譯器和 CPU 對(duì)這些指令進(jìn)行重排序。在 Linux 內(nèi)核中,mb()函數(shù)被定義用來(lái)實(shí)現(xiàn)通用內(nèi)存屏障,其定義如下:
#ifdef CONFIG_SMP
#define mb() asm volatile("mfence" ::: "memory")
#else
#define mb() barrier()
#endif
在 SMP(對(duì)稱多處理)系統(tǒng)中,如果是 64 位 CPU 或支持mfence指令的 32 位 CPU,mb()宏被定義為asm volatile("mfence" ::: "memory") 。mfence指令是 x86 架構(gòu)下的一條匯編指令,它會(huì)使 CPU 等待,直到之前所有的內(nèi)存讀寫(xiě)操作都完成,才會(huì)執(zhí)行之后的內(nèi)存操作,從而保證了內(nèi)存操作的順序性。
asm volatile表示這是一段匯編代碼,并且禁止編譯器對(duì)其進(jìn)行優(yōu)化,::: "memory"告訴編譯器內(nèi)存中的數(shù)據(jù)可能會(huì)被修改,不能依賴寄存器中的舊值 。在單處理器(UP)系統(tǒng)中,mb()被定義為barrier(),barrier()宏通過(guò)asm volatile("":::"memory")` 來(lái)實(shí)現(xiàn),同樣是為了防止編譯器對(duì)內(nèi)存操作進(jìn)行重排序 。
2. 讀內(nèi)存屏障
讀內(nèi)存屏障(rmb)保證在其之前的所有讀操作都在其之后的讀操作之前完成 。它確保了讀指令的順序,防止讀操作被重排序。在多線程環(huán)境中,當(dāng)多個(gè)線程同時(shí)讀取共享數(shù)據(jù)時(shí),讀內(nèi)存屏障可以保證每個(gè)線程讀取到的數(shù)據(jù)是按照預(yù)期的順序更新的 。
例如,在一個(gè)多線程程序中,線程 A 和線程 B 都需要讀取共享變量x和y的值,并且要求先讀取x,再讀取y。如果沒(méi)有使用讀內(nèi)存屏障,由于指令重排序,線程 B 可能會(huì)先讀取y,再讀取x,導(dǎo)致讀取到的數(shù)據(jù)不符合預(yù)期 。通過(guò)在讀取x和y之間插入讀內(nèi)存屏障,就可以保證線程 B 先讀取x,再讀取y,從而保證了數(shù)據(jù)的一致性 。
在 Linux 內(nèi)核中,rmb()函數(shù)的定義如下:
#ifdef CONFIG_SMP
#define rmb() asm volatile("lfence" ::: "memory")
#else
#define rmb() barrier()
#endif
在 SMP 系統(tǒng)中,如果是 64 位 CPU 或支持lfence指令的 32 位 CPU,rmb()宏被定義為asm volatile("lfence" ::: "memory") 。lfence指令是 x86 架構(gòu)下的讀內(nèi)存屏障指令,它保證了在其之前的讀操作都完成后,才會(huì)執(zhí)行之后的讀操作 。在 UP 系統(tǒng)中,rmb()同樣被定義為barrier() 。
3. 寫(xiě)內(nèi)存屏障
寫(xiě)內(nèi)存屏障(wmb)保證在其之前的所有寫(xiě)操作都在其之后的寫(xiě)操作之前完成 。它確保了寫(xiě)指令的順序,防止寫(xiě)操作被重排序。在數(shù)據(jù)更新場(chǎng)景中,寫(xiě)內(nèi)存屏障尤為重要。例如,在一個(gè)多線程程序中,線程 A 需要先更新共享變量x,再更新共享變量y,并且要求其他線程能夠按照這個(gè)順序看到更新后的值 。
如果沒(méi)有使用寫(xiě)內(nèi)存屏障,由于指令重排序,其他線程可能會(huì)先看到y(tǒng)的更新,再看到x的更新,導(dǎo)致數(shù)據(jù)不一致 。通過(guò)在更新x和y之間插入寫(xiě)內(nèi)存屏障,就可以保證其他線程先看到x的更新,再看到y(tǒng)的更新,從而保證了數(shù)據(jù)的一致性 。
在 Linux 內(nèi)核中,wmb()函數(shù)的定義如下:
#ifdef CONFIG_SMP
#define wmb() asm volatile("sfence" ::: "memory")
#else
#define wmb() barrier()
#endif
在 SMP 系統(tǒng)中,如果是 64 位 CPU 或支持sfence指令的 32 位 CPU,wmb()宏被定義為asm volatile("sfence" ::: "memory") 。sfence指令是 x86 架構(gòu)下的寫(xiě)內(nèi)存屏障指令,它保證了在其之前的寫(xiě)操作都完成后,才會(huì)執(zhí)行之后的寫(xiě)操作 。在 UP 系統(tǒng)中,wmb()也被定義為barrier() 。
4. 讀寫(xiě)內(nèi)存屏障
讀寫(xiě)內(nèi)存屏障既保證了讀操作的順序,也保證了寫(xiě)操作的順序 。它確保了在其之前的所有讀寫(xiě)操作都在其之后的讀寫(xiě)操作之前完成 。在一些復(fù)雜的數(shù)據(jù)結(jié)構(gòu)讀寫(xiě)場(chǎng)景中,讀寫(xiě)內(nèi)存屏障非常有用。例如,在一個(gè)多線程程序中,線程 A 需要先寫(xiě)入數(shù)據(jù)到共享數(shù)據(jù)結(jié)構(gòu),然后讀取該數(shù)據(jù)結(jié)構(gòu)中的其他部分;線程 B 則需要先讀取線程 A 寫(xiě)入的數(shù)據(jù),然后再寫(xiě)入新的數(shù)據(jù) 。通過(guò)在這些讀寫(xiě)操作之間插入讀寫(xiě)內(nèi)存屏障,可以保證線程 A 和線程 B 的讀寫(xiě)操作按照預(yù)期的順序進(jìn)行,避免數(shù)據(jù)競(jìng)爭(zhēng)和不一致的問(wèn)題 。
在 Linux 內(nèi)核中,并沒(méi)有專門(mén)定義一個(gè)獨(dú)立的讀寫(xiě)內(nèi)存屏障函數(shù),通??梢允褂猛ㄓ脙?nèi)存屏障mb()來(lái)實(shí)現(xiàn)讀寫(xiě)內(nèi)存屏障的功能,因?yàn)閙b()同時(shí)保證了讀寫(xiě)操作的順序 。
四、應(yīng)用案例深度解析
1. 多核處理器環(huán)境下的同步
在多核處理器環(huán)境中,每個(gè)核心都有自己的高速緩存,當(dāng)多個(gè)核心同時(shí)訪問(wèn)共享內(nèi)存時(shí),就可能出現(xiàn)緩存一致性問(wèn)題 。例如,核心 A 修改了共享內(nèi)存中的數(shù)據(jù),并將其寫(xiě)入自己的緩存,但此時(shí)核心 B 的緩存中仍然是舊數(shù)據(jù)。如果核心 B 繼續(xù)從自己的緩存中讀取數(shù)據(jù),就會(huì)讀到不一致的數(shù)據(jù) 。
為了解決這個(gè)問(wèn)題,內(nèi)存屏障被廣泛應(yīng)用。內(nèi)存屏障可以確保在屏障之前的內(nèi)存操作都完成后,才會(huì)執(zhí)行屏障之后的內(nèi)存操作,從而保證了緩存一致性 。例如,在 X86 架構(gòu)中,mfence指令可以作為內(nèi)存屏障,它會(huì)使 CPU 等待,直到之前所有的內(nèi)存讀寫(xiě)操作都完成,才會(huì)執(zhí)行之后的內(nèi)存操作 。
在多線程編程中,當(dāng)一個(gè)線程修改了共享數(shù)據(jù)后,通過(guò)插入內(nèi)存屏障,可以確保其他線程能夠立即看到這個(gè)修改 。假設(shè)線程 A 和線程 B 共享一個(gè)變量x,線程 A 修改了x的值后,插入一個(gè)內(nèi)存屏障,然后線程 B 讀取x的值,由于內(nèi)存屏障的作用,線程 B 讀取到的一定是線程 A 修改后的最新值 。
2. 設(shè)備驅(qū)動(dòng)開(kāi)發(fā)中的應(yīng)用
在設(shè)備驅(qū)動(dòng)開(kāi)發(fā)中,內(nèi)存屏障也起著關(guān)鍵作用。設(shè)備驅(qū)動(dòng)程序需要與硬件設(shè)備進(jìn)行交互,而硬件設(shè)備的操作通常需要按照特定的順序進(jìn)行 。例如,在對(duì)硬件寄存器進(jìn)行操作時(shí),必須確保先寫(xiě)入配置信息,再啟動(dòng)設(shè)備 。如果沒(méi)有內(nèi)存屏障,編譯器和 CPU 可能會(huì)對(duì)這些操作進(jìn)行重排序,導(dǎo)致設(shè)備無(wú)法正常工作 。
以串口驅(qū)動(dòng)為例,在向串口發(fā)送數(shù)據(jù)時(shí),需要先檢查串口發(fā)送緩沖區(qū)是否為空,然后再將數(shù)據(jù)寫(xiě)入緩沖區(qū) 。如果這兩個(gè)操作被重排序,就可能導(dǎo)致數(shù)據(jù)丟失 。通過(guò)在這兩個(gè)操作之間插入內(nèi)存屏障,可以確保先檢查緩沖區(qū),再寫(xiě)入數(shù)據(jù),從而保證串口通信的正確性 。在 Linux 內(nèi)核中,串口驅(qū)動(dòng)代碼可能會(huì)如下實(shí)現(xiàn):
// 檢查串口發(fā)送緩沖區(qū)是否為空
while (readl(serial_port + STATUS_REGISTER) & TX_FIFO_FULL);
// 插入寫(xiě)內(nèi)存屏障
wmb();
// 將數(shù)據(jù)寫(xiě)入串口發(fā)送緩沖區(qū)
writel(data, serial_port + DATA_REGISTER);
在這個(gè)例子中,wmb()函數(shù)作為寫(xiě)內(nèi)存屏障,確保了在寫(xiě)入數(shù)據(jù)之前,先完成對(duì)緩沖區(qū)狀態(tài)的檢查,從而保證了串口驅(qū)動(dòng)的正常工作 。
3. RCU 機(jī)制中的關(guān)鍵角色
RCU(Read - Copy - Update)機(jī)制是 Linux 內(nèi)核中一種高效的同步機(jī)制,主要用于讀多寫(xiě)少的場(chǎng)景 。在 RCU 機(jī)制中,內(nèi)存屏障發(fā)揮著至關(guān)重要的作用 。
在 RCU 中,讀操作不需要加鎖,這大大提高了讀操作的效率 。然而,為了保證數(shù)據(jù)的一致性,在寫(xiě)操作時(shí)需要采取一些特殊的措施 。當(dāng)一個(gè)寫(xiě)者需要更新數(shù)據(jù)時(shí),它首先會(huì)創(chuàng)建一個(gè)數(shù)據(jù)的副本,在副本上進(jìn)行修改,然后將修改后的副本替換原來(lái)的數(shù)據(jù) 。在這個(gè)過(guò)程中,內(nèi)存屏障用于確保讀操作能夠看到正確的數(shù)據(jù) 。
例如,在 Linux 內(nèi)核的鏈表操作中,經(jīng)常會(huì)使用 RCU 機(jī)制 。當(dāng)一個(gè)線程要向鏈表中插入一個(gè)新節(jié)點(diǎn)時(shí),它會(huì)先創(chuàng)建新節(jié)點(diǎn),設(shè)置好節(jié)點(diǎn)的指針,然后使用rcu_assign_pointer函數(shù)來(lái)更新鏈表的指針 。rcu_assign_pointer函數(shù)內(nèi)部會(huì)使用內(nèi)存屏障,確保在新節(jié)點(diǎn)的指針設(shè)置完成后,其他線程才能看到這個(gè)新節(jié)點(diǎn) 。這樣,在多線程環(huán)境下,讀線程可以在不加鎖的情況下安全地遍歷鏈表,而寫(xiě)線程也可以在不影響讀線程的情況下更新鏈表,從而提高了系統(tǒng)的并發(fā)性能 。
4. 內(nèi)存一致性模型
內(nèi)存一致性模型(Memory Consistency Model)是用來(lái)描述多線程對(duì)共享存儲(chǔ)器的訪問(wèn)行為,在不同的內(nèi)存一致性模型里,多線程對(duì)共享存儲(chǔ)器的訪問(wèn)行為有非常大的差別。這些差別會(huì)嚴(yán)重影響程序的執(zhí)行邏輯,甚至?xí)斐绍浖壿媶?wèn)題。
不同的處理器架構(gòu),使用了不同的內(nèi)存一致性模型,目前有多種內(nèi)存一致性模型,從上到下模型的限制由強(qiáng)到弱:
- 順序一致性(Sequential Consistency)模型
- 完全存儲(chǔ)定序(Total Store Order)模型
- 部分存儲(chǔ)定序(Part Store Order)模型
- 寬松存儲(chǔ)(Relax Memory Order)模型
注意,這里說(shuō)的內(nèi)存模型是針對(duì)可以同時(shí)執(zhí)行多線程的平臺(tái),如果只能同時(shí)執(zhí)行一個(gè)線程,也就是系統(tǒng)中一共只有一個(gè)CPU核,那么它一定是滿足順序一致性模型的。
對(duì)于內(nèi)存的訪問(wèn),我們只關(guān)心兩種類型的指令的順序,一種是讀取,一種是寫(xiě)入。對(duì)于讀取和加載指令來(lái)說(shuō),它們兩兩一起,一共有四種組合:
- LoadLoad:前一條指令是讀取,后一條指令也是讀取。
- LoadStore:前一條指令是讀取,后一條指令是寫(xiě)入。
- StoreLoad:前一條指令是寫(xiě)入,后一條指令是讀取。
- StoreStore:前一條指令是寫(xiě)入,后一條指令也是寫(xiě)入。
(1) 順序一致性模型
順序存儲(chǔ)模型是最簡(jiǎn)單的存儲(chǔ)模型,也稱為強(qiáng)定序模型。CPU會(huì)按照代碼來(lái)執(zhí)行所有的讀取與寫(xiě)入指令,即按照它們?cè)诔绦蛑谐霈F(xiàn)的次序來(lái)執(zhí)行。同時(shí),從主存儲(chǔ)器和系統(tǒng)中其它CPU的角度來(lái)看,感知到數(shù)據(jù)變化的順序也完全是按照指令執(zhí)行的次序。也可以理解為,在程序看來(lái),CPU不會(huì)對(duì)指令進(jìn)行任何重排序的操作。在這種模型下執(zhí)行的程序是完全不需要內(nèi)存屏障的。但是,帶來(lái)的問(wèn)題就是性能會(huì)比較差,現(xiàn)在已經(jīng)沒(méi)有符合這種內(nèi)存一致性模型的系統(tǒng)了。
為了提高系統(tǒng)的性能,不同架構(gòu)都會(huì)或多或少的對(duì)這種強(qiáng)一致性模型進(jìn)行了放松,允許對(duì)某些指令組合進(jìn)行重排序。注意,這里處理器對(duì)讀取或?qū)懭氩僮鞯姆潘?,是以兩個(gè)操作之間不存在數(shù)據(jù)依賴性為前提的,處理器不會(huì)對(duì)存在數(shù)據(jù)依賴性的兩個(gè)內(nèi)存操作做重排序。
(2) 完全存儲(chǔ)定序模型
這種內(nèi)存一致性模型允許對(duì)StoreLoad指令組合進(jìn)行重排序,如果第一條指令是寫(xiě)入,第二條指令是讀取,那么有可能在程序看來(lái),讀取指令先于寫(xiě)入指令執(zhí)行。但是,對(duì)于其它另外三種指令組合還是可以保證按照順序執(zhí)行。
這種模型就相當(dāng)于前面提到的,在CPU和緩存中間加入了存儲(chǔ)緩沖,而且這個(gè)緩沖還是一個(gè)滿足先入先出(FIFO)的隊(duì)列。先入先出隊(duì)列就保證了對(duì)StoreStore這種指令組合也能保證按照順序被感知。
我們非常熟悉的X86架構(gòu)就是使用的這種內(nèi)存一致性模型。
(3) 部分存儲(chǔ)定序模型
這種內(nèi)存一致性模型除了允許對(duì)StoreLoad指令組合進(jìn)行重排序外,還允許對(duì)StoreStore指令組合進(jìn)行重排序。但是,對(duì)于其它另外兩種指令組合還是可以保證按照順序執(zhí)行。
這種模型就相當(dāng)于也在CPU和緩存中間加入了存儲(chǔ)緩沖,但是這個(gè)緩沖不是先入先出的。
(4) 寬松存儲(chǔ)模型
這種內(nèi)存一致性模型允許對(duì)上面說(shuō)的四種指令組合都進(jìn)行重排序。
這種模型就相當(dāng)于前面說(shuō)的,既有存儲(chǔ)緩沖,又有無(wú)效隊(duì)列的情況。
這種內(nèi)存模型下其實(shí)還有一個(gè)細(xì)微的差別,就是所謂的數(shù)據(jù)依賴性的問(wèn)題。例如下面的程序,假設(shè)變量A初始值是0:
CPU 0 | CPU 1 |
A = 1; | Q = P; |
<write barrier> | B = *Q; |
P = &A; |
五、使用注意事項(xiàng)與性能考量
1. 避免過(guò)度使用
雖然內(nèi)存屏障是解決多線程環(huán)境下內(nèi)存一致性問(wèn)題的有力工具,但過(guò)度使用會(huì)對(duì)系統(tǒng)性能產(chǎn)生負(fù)面影響 。內(nèi)存屏障會(huì)阻止 CPU 和編譯器對(duì)指令進(jìn)行重排序,這在一定程度上限制了它們的優(yōu)化能力,從而增加了指令執(zhí)行的時(shí)間 。在一些不必要的場(chǎng)景中使用內(nèi)存屏障,會(huì)導(dǎo)致性能下降 。
例如,在單線程環(huán)境中,由于不存在多線程并發(fā)訪問(wèn)共享數(shù)據(jù)的問(wèn)題,使用內(nèi)存屏障是完全沒(méi)有必要的,這只會(huì)浪費(fèi)系統(tǒng)資源 。在多線程環(huán)境中,如果共享數(shù)據(jù)的訪問(wèn)沒(méi)有數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題,也不應(yīng)隨意使用內(nèi)存屏障 。比如,在一個(gè)多線程程序中,多個(gè)線程只是讀取共享數(shù)據(jù),而不進(jìn)行寫(xiě)操作,此時(shí)使用內(nèi)存屏障并不能帶來(lái)任何好處,反而會(huì)降低性能 。因此,在使用內(nèi)存屏障時(shí),需要仔細(xì)分析代碼的執(zhí)行邏輯和數(shù)據(jù)訪問(wèn)模式,確保只在必要的地方使用內(nèi)存屏障,以避免不必要的性能損失 。
2. 選擇合適的屏障類型
不同類型的內(nèi)存屏障在功能和適用場(chǎng)景上有所不同,因此根據(jù)具體的場(chǎng)景選擇合適的內(nèi)存屏障類型至關(guān)重要 。如果只需要保證讀操作的順序,那么使用讀內(nèi)存屏障(rmb)即可;如果只需要保證寫(xiě)操作的順序,使用寫(xiě)內(nèi)存屏障(wmb)就足夠了 。在一些復(fù)雜的場(chǎng)景中,可能需要同時(shí)保證讀寫(xiě)操作的順序,這時(shí)就需要使用通用內(nèi)存屏障(mb)或讀寫(xiě)內(nèi)存屏障 。
例如,在一個(gè)多線程程序中,線程 A 需要先讀取共享變量x,再讀取共享變量y,并且要求這兩個(gè)讀操作按照順序進(jìn)行,此時(shí)就可以在讀取x和y之間插入讀內(nèi)存屏障 。如果線程 A 需要先寫(xiě)入共享變量x,再寫(xiě)入共享變量y,并且要求其他線程能夠按照這個(gè)順序看到更新后的值,那么就應(yīng)該在寫(xiě)入x和y之間插入寫(xiě)內(nèi)存屏障 。在一些涉及復(fù)雜數(shù)據(jù)結(jié)構(gòu)讀寫(xiě)的場(chǎng)景中,可能需要使用通用內(nèi)存屏障來(lái)保證讀寫(xiě)操作的順序 。
比如,在一個(gè)多線程程序中,線程 A 需要先寫(xiě)入數(shù)據(jù)到共享鏈表,然后讀取鏈表中的其他部分,線程 B 則需要先讀取線程 A 寫(xiě)入的數(shù)據(jù),然后再寫(xiě)入新的數(shù)據(jù),這種情況下就可以使用通用內(nèi)存屏障來(lái)確保線程 A 和線程 B 的讀寫(xiě)操作按照預(yù)期的順序進(jìn)行 。因此,在使用內(nèi)存屏障時(shí),需要根據(jù)具體的場(chǎng)景和需求,選擇合適的內(nèi)存屏障類型,以充分發(fā)揮內(nèi)存屏障的作用,同時(shí)避免不必要的性能開(kāi)銷 。
3. 性能監(jiān)測(cè)與優(yōu)化
為了確保內(nèi)存屏障的使用不會(huì)對(duì)系統(tǒng)性能造成過(guò)大的影響,使用工具監(jiān)測(cè)內(nèi)存屏障對(duì)性能的影響,并根據(jù)監(jiān)測(cè)結(jié)果進(jìn)行優(yōu)化是很有必要的 。在 Linux 系統(tǒng)中,可以使用 perf 工具來(lái)監(jiān)測(cè)內(nèi)存屏障對(duì)性能的影響 。perf 是一個(gè)性能分析工具,它可以收集系統(tǒng)的性能數(shù)據(jù),包括CPU使用率、內(nèi)存訪問(wèn)次數(shù)等 。通過(guò)使用perf 工具,可以了解內(nèi)存屏障的使用對(duì)系統(tǒng)性能的影響,從而找到性能瓶頸,并進(jìn)行優(yōu)化 。
例如,可以使用 perf record 命令來(lái)收集性能數(shù)據(jù),然后使用 perf report 命令來(lái)查看性能報(bào)告 。在性能報(bào)告中,可以看到各個(gè)函數(shù)的 CPU 使用率、內(nèi)存訪問(wèn)次數(shù)等信息,從而找到內(nèi)存屏障使用較多的函數(shù),并分析其對(duì)性能的影響 。如果發(fā)現(xiàn)某個(gè)函數(shù)中內(nèi)存屏障的使用導(dǎo)致了性能下降,可以嘗試優(yōu)化該函數(shù)的代碼,減少內(nèi)存屏障的使用,或者選擇更合適的內(nèi)存屏障類型 。
除了使用 perf 工具外,還可以通過(guò)代碼優(yōu)化、算法改進(jìn)等方式來(lái)提高系統(tǒng)性能 。例如,可以減少不必要的內(nèi)存訪問(wèn),優(yōu)化數(shù)據(jù)結(jié)構(gòu),提高代碼的并行性等 。通過(guò)綜合使用這些方法,可以有效地提高系統(tǒng)性能,確保內(nèi)存屏障的使用不會(huì)對(duì)系統(tǒng)性能造成過(guò)大的影響 。