自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

深入理解 Linux 內(nèi)存優(yōu)化:如何使用屏障提升性能

系統(tǒng) Linux
內(nèi)存優(yōu)化屏障究竟是如何在 Linux 系統(tǒng)中發(fā)揮作用的?它又能為我們的應(yīng)用性能帶來怎樣的提升?接下來,就讓我們一同揭開 Linux 內(nèi)存優(yōu)化屏障的神秘面紗,探尋其中的奧秘 。

在當(dāng)今快節(jié)奏的數(shù)字時(shí)代,無論是運(yùn)行大型數(shù)據(jù)庫的服務(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ù)的存儲與讀取,為上層應(yīng)用的穩(wěn)定運(yùn)行筑牢根基。

然而,隨著計(jì)算機(jī)硬件性能的突飛猛進(jìn),尤其是多核處理器的廣泛普及,內(nèi)存訪問的復(fù)雜性也呈指數(shù)級增長。為了充分挖掘硬件潛力,提升系統(tǒng)整體性能,現(xiàn)代計(jì)算機(jī)往往采用亂序執(zhí)行、緩存機(jī)制等優(yōu)化手段。但這也帶來了新的挑戰(zhàn):內(nèi)存操作的順序可能變得難以捉摸,數(shù)據(jù)一致性問題時(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ù)依然能夠按照開發(fā)者預(yù)期的方式流動。那么,內(nèi)存優(yōu)化屏障究竟是如何在 Linux 系統(tǒng)中發(fā)揮作用的?它又能為我們的應(yīng)用性能帶來怎樣的提升?接下來,就讓我們一同揭開 Linux 內(nèi)存優(yōu)化屏障的神秘面紗,探尋其中的奧秘 。

一、內(nèi)存屏障簡介

1. 內(nèi)存屏障概述

在計(jì)算機(jī)系統(tǒng)中,為了提升性能,現(xiàn)代 CPU 和編譯器常常會對指令進(jìn)行重排序。指令重排序是指在不改變程序最終執(zhí)行結(jié)果的前提下,調(diào)整指令的執(zhí)行順序,以充分利用 CPU 的資源,提高執(zhí)行效率 。例如,當(dāng) CPU 執(zhí)行一系列指令時(shí),如果某些指令之間不存在數(shù)據(jù)依賴關(guān)系,CPU 可能會先執(zhí)行后面的指令,再執(zhí)行前面的指令。

在單線程環(huán)境下,指令重排序通常不會帶來問題,因?yàn)槌绦虻膱?zhí)行結(jié)果仍然符合預(yù)期。然而,在多線程環(huán)境中,指令重排序可能會導(dǎo)致意想不到的結(jié)果,因?yàn)椴煌€程之間的操作可能會相互干擾。比如,線程 A 和線程 B 同時(shí)訪問共享內(nèi)存,線程 A 對共享變量的修改可能不會立即被線程 B 看到,這就導(dǎo)致了數(shù)據(jù)可見性問題。

為了解決這些問題,內(nèi)存優(yōu)化屏障應(yīng)運(yùn)而生。內(nèi)存優(yōu)化屏障是一種特殊的指令或機(jī)制,它可以阻止 CPU 和編譯器對特定指令進(jìn)行重排序,從而保證內(nèi)存操作的順序性和可見性。通過使用內(nèi)存優(yōu)化屏障,程序員可以確保在多線程環(huán)境下,內(nèi)存操作按照預(yù)期的順序執(zhí)行,避免數(shù)據(jù)競爭和其他并發(fā)問題。

2. 為什么會出現(xiàn)內(nèi)存屏障?

由于現(xiàn)在計(jì)算機(jī)存在多級緩存且多核場景,為了保證讀取到的數(shù)據(jù)一致性以及并行運(yùn)行時(shí)所計(jì)算出來的結(jié)果一致,在硬件層面實(shí)現(xiàn)一些指令,從而來保證指定執(zhí)行的指令的先后順序。比如上圖:雙核cpu,每個(gè)核心都擁有獨(dú)立的一二級緩存,而緩存與緩存之間需要保證數(shù)據(jù)的一致性所以這里才需要加添屏障來確保數(shù)據(jù)的一致性。三級緩存為各CPU共享,最后都是主內(nèi)存,所以這些存在交互的CPU都需要通過屏障手段來保證數(shù)據(jù)的唯一性。

內(nèi)存屏障存在的意義就是為了解決程序在運(yùn)行過程中出現(xiàn)的內(nèi)存亂序訪問問題,內(nèi)存亂序訪問行為出現(xiàn)的理由是為了提高程序運(yùn)行時(shí)的性能,Memory Bariier能夠讓CPU或編譯器在內(nèi)存訪問上有序。

(1) 運(yùn)行時(shí)內(nèi)存亂序訪問

運(yùn)行時(shí),CPU本身是會亂序執(zhí)行指令的。早期的處理器為有序處理器(in-order processors),總是按開發(fā)者編寫的順序執(zhí)行指令, 如果指令的輸入操作對象(input operands)不可用(通常由于需要從內(nèi)存中獲?。?那么處理器不會轉(zhuǎn)而執(zhí)行那些輸入操作對象可用的指令,而是等待當(dāng)前輸入操作對象可用。

相比之下,亂序處理器(out-of-order processors)會先處理那些有可用輸入操作對象的指令(而非順序執(zhí)行) 從而避免了等待,提高了效率?,F(xiàn)代計(jì)算機(jī)上,處理器運(yùn)行的速度比內(nèi)存快很多, 有序處理器花在等待可用數(shù)據(jù)的時(shí)間里已可處理大量指令了。即便現(xiàn)代處理器會亂序執(zhí)行, 但在單個(gè)CPU上,指令能通過指令隊(duì)列順序獲取并執(zhí)行,結(jié)果利用隊(duì)列順序返回寄存器堆(詳情可參考http:// http://en.wikipedia.org/wiki/Out-of-order_execution),這使得程序執(zhí)行時(shí)所有的內(nèi)存訪問操作看起來像是按程序代碼編寫的順序執(zhí)行的, 因此內(nèi)存屏障是沒有必要使用的(前提是不考慮編譯器優(yōu)化的情況下)。

(2) SMP架構(gòu)需要內(nèi)存屏障的進(jìn)一步解釋

從體系結(jié)構(gòu)上來看,首先在SMP架構(gòu)下,每個(gè)CPU與內(nèi)存之間,都配有自己的高速緩存(Cache),以減少訪問內(nèi)存時(shí)的沖突采用高速緩存的寫操作有兩種模式:

  • 穿透(Write through)模式,每次寫時(shí),都直接將數(shù)據(jù)寫回內(nèi)存中,效率相對較低;
  • 回寫(Write back)模式,寫的時(shí)候先寫回告訴緩存,然后由高速緩存的硬件再周轉(zhuǎn)復(fù)用緩沖線(Cache Line)時(shí)自動將數(shù)據(jù)寫回內(nèi)存, 或者由軟件主動地“沖刷”有關(guān)的緩沖線(Cache Line)。

出于性能的考慮,系統(tǒng)往往采用的是模式2來完成數(shù)據(jù)寫入;正是由于存在高速緩存這一層,正是由于采用了Write back模式的數(shù)據(jù)寫入,才導(dǎo)致在SMP架構(gòu)下,對高速緩存的運(yùn)用可能改變對內(nèi)存操作的順序。

已上面的一個(gè)簡短代碼為例:

// thread 0 -- 在CPU0上運(yùn)行
x = 42;
ok = 1;
 
// thread 1 – 在CPU1上運(yùn)行
while(!ok);
print(x);

這里CPU1執(zhí)行時(shí), x一定是打印出42嗎?讓我們來看看以下圖為例的說明:

假設(shè),正好CPU0的高速緩存中有x,此時(shí)CPU0僅僅是將x=42寫入到了高速緩存中,另外一個(gè)ok也在高速緩存中,但由于周轉(zhuǎn)復(fù)用高速緩沖線(Cache Line)而導(dǎo)致將ok=1刷會到了內(nèi)存中,此時(shí)CPU1首先執(zhí)行對ok內(nèi)存的讀取操作,他讀到了ok為1的結(jié)果,進(jìn)而跳出循環(huán),讀取x的內(nèi)容,而此時(shí),由于實(shí)際寫入的x(42)還只在CPU0的高速緩存中,導(dǎo)致CPU1讀到的數(shù)據(jù)為x(17)。

程序中編排好的內(nèi)存訪問順序(指令序:program ordering)是先寫入x,再寫入y。而實(shí)際上出現(xiàn)在該CPU外部,即系統(tǒng)總線上的次序(處理器序:processor ordering),卻是先寫入y,再寫入x(這個(gè)例子中x還未寫入)。

在SMP架構(gòu)中,每個(gè)CPU都只知道自己何時(shí)會改變內(nèi)存的內(nèi)容,但是都不知道別的CPU會在什么時(shí)候改變內(nèi)存的內(nèi)容,也不知道自己本地的高速緩存中的內(nèi)容是否與內(nèi)存中的內(nèi)容不一致。

反過來,每個(gè)CPU都可能因?yàn)楦淖兞藘?nèi)存內(nèi)容,而使得其他CPU的高速緩存變的不一致了。在SMP架構(gòu)下,由于高速緩存的存在而導(dǎo)致的內(nèi)存訪問次序(讀或?qū)懚加锌赡軙虮桓淖儯┑母淖兒苡锌赡苡绊懙紺PU間的同步與互斥。

因此需要有一種手段,使得在某些操作之前,把這種“欠下”的內(nèi)存操作(本例中的x=42的內(nèi)存寫入)全都最終地、物理地完成,就好像把欠下的債都結(jié)清,然后再開始新的(通常是比較重要的)活動一樣。這種手段就是內(nèi)存屏障,其本質(zhì)原理就是對系統(tǒng)總線加鎖。

回過頭來,我們再來看看為什么非SMP架構(gòu)(UP架構(gòu))下,運(yùn)行時(shí)內(nèi)存亂序訪問不存在。

在單處理器架構(gòu)下,各個(gè)進(jìn)程在宏觀上是并行的,但是在微觀上卻是串行的,因?yàn)樵谕粫r(shí)間點(diǎn)上,只有一個(gè)進(jìn)程真正在運(yùn)行(系統(tǒng)中只有一個(gè)處理器)。

在這種情況下,我們再來看看上面提到的例子:

線程0和線程1的指令都將在CPU0上按照指令序執(zhí)行。thread0通過CPU0完成x=42的高速緩存寫入后,再將ok=1寫入內(nèi)存,此后串行的將thread0換出,thread1換入,及時(shí)此時(shí)x=42并未寫入內(nèi)存,但由于thread1的執(zhí)行仍然是在CPU0上執(zhí)行,他仍然訪問的是CPU0的高速緩存,因此,及時(shí)x=42還未寫回到內(nèi)存中,thread1勢必還是先從高速緩存中讀到x=42,再從內(nèi)存中讀到ok=1。

綜上所述,在單CPU上,多線程執(zhí)行不存在運(yùn)行時(shí)內(nèi)存亂序訪問,我們從內(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

這里可看到對內(nèi)存屏障的定義,如果是SMP架構(gòu),smp_mb定義為mb(),mb()為CPU內(nèi)存屏障(接下來要談的),而非SMP架構(gòu)時(shí)(也就是UP架構(gòu)),直接使用編譯器屏障,運(yùn)行時(shí)內(nèi)存亂序訪問并不存在。

(3) 為什么多CPU情況下會存在內(nèi)存亂序訪問?

我們知道每個(gè)CPU都存在Cache,當(dāng)一個(gè)特定數(shù)據(jù)第一次被其他CPU獲取時(shí),此數(shù)據(jù)顯然不在對應(yīng)CPU的Cache中(這就是Cache Miss)。

這意味著CPU要從內(nèi)存中獲取數(shù)據(jù)(這個(gè)過程需要CPU等待數(shù)百個(gè)周期),此數(shù)據(jù)將被加載到CPU的Cache中,這樣后續(xù)就能直接從Cache上快速訪問。

當(dāng)某個(gè)CPU進(jìn)行寫操作時(shí),他必須確保其他CPU已將此數(shù)據(jù)從他們的Cache中移除(以便保證一致性),只有在移除操作完成后,此CPU才能安全地修改數(shù)據(jù)。

顯然,存在多個(gè)Cache時(shí),必須通過一個(gè)Cache一致性協(xié)議來避免數(shù)據(jù)不一致的問題,而這個(gè)通信的過程就可能導(dǎo)致亂序訪問的出現(xiàn),也就是運(yùn)行時(shí)內(nèi)存亂序訪問。

受篇幅所限,這里不再深入討論整個(gè)細(xì)節(jié),有興趣的讀者可以研究《Memory Barriers: a Hardware View for Software Hackers》這篇文章,它詳細(xì)地分析了整個(gè)過程。

現(xiàn)在通過一個(gè)例子來直觀地說明多CPU下內(nèi)存亂序訪問的問題:

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的值不會同時(shí)為0,而假如run1和run2在不同的CPU下執(zhí)行完成后,由于存在內(nèi)存亂序訪問的可能,這時(shí)r1和r2可能同時(shí)為0。我們可以使用CPU內(nèi)存屏障來避免運(yùn)行時(shí)內(nèi)存亂序訪問(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í)行效率,會對代碼進(jìn)行優(yōu)化,其中指令重排是一種常見的優(yōu)化手段。例如,對于下面的 C 代碼:

int a = 1;
int b = 2;

在沒有數(shù)據(jù)依賴的情況下,編譯器可能會將其編譯成匯編代碼時(shí),交換這兩條指令的順序,先執(zhí)行b = 2,再執(zhí)行a = 1。在單線程環(huán)境下,這種重排通常不會影響程序的最終結(jié)果。但在多線程環(huán)境中,當(dāng)多個(gè)線程共享數(shù)據(jù)時(shí),這種重排可能會導(dǎo)致數(shù)據(jù)一致性問題 。

為了禁止編譯器對特定指令進(jìn)行重排,Linux 內(nèi)核提供了優(yōu)化屏障機(jī)制。在 Linux 內(nèi)核中,通過barrier()宏來實(shí)現(xiàn)優(yōu)化屏障 。barrier()宏的定義如下:

#define barrier() __asm__ __volatile__("" ::: "memory")

__asm__表示這是一段匯編代碼,__volatile__告訴編譯器不要對這段代碼進(jìn)行優(yōu)化,即不要改變其前后代碼塊的順序 。"memory"表示內(nèi)存中的變量值可能會發(fā)生變化,編譯器不能使用寄存器中的值來優(yōu)化,而應(yīng)該重新從內(nèi)存中加載變量的值。這樣,在barrier()宏之前的指令不會被移動到barrier()宏之后,之后的指令也不會被移動到之前,從而保證了編譯器層面的指令順序。

2. CPU 執(zhí)行優(yōu)化與內(nèi)存屏障

現(xiàn)代 CPU 為了提高執(zhí)行效率,采用了超標(biāo)量體系結(jié)構(gòu)和亂序執(zhí)行技術(shù)。CPU 在執(zhí)行指令時(shí),會按照程序順序取出一批指令,分析找出沒有依賴關(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)較長時(shí)間時(shí),CPU 不會等待指令A(yù)完成,而是會繼續(xù)執(zhí)行后續(xù)沒有數(shù)據(jù)依賴的指令B、C等,直到指令A(yù)的數(shù)據(jù)讀取完成,再繼續(xù)執(zhí)行指令A(yù)的后續(xù)操作 。

雖然 CPU 的亂序執(zhí)行可以提高執(zhí)行效率,但在某些情況下,這種亂序執(zhí)行可能會導(dǎo)致問題。比如,在多處理器系統(tǒng)中,一個(gè)處理器修改數(shù)據(jù)后,可能不會把數(shù)據(jù)立即同步到自己的緩存或者其他處理器的緩存,導(dǎo)致其他處理器不能立即看到最新的數(shù)據(jù)。為了解決這個(gè)問題,需要使用內(nèi)存屏障來保證 CPU 執(zhí)行指令的順序 。

內(nèi)存屏障確保在屏障原語前的指令完成后,才會啟動原語之后的指令操作。在不同的 CPU 架構(gòu)中,有不同的指令來實(shí)現(xiàn)內(nèi)存屏障的功能。例如,在 X86 系統(tǒng)中,以下這些匯編指令可以充當(dāng)內(nèi)存屏障:

  • 所有操作 I/O 端口的指令;
  • 前綴lock的指令,如lock;addl $0,0(%esp),雖然這條指令本身沒有實(shí)際意義(對棧頂保存的內(nèi)存地址內(nèi)的內(nèi)容加上 0),但lock前綴對數(shù)據(jù)總線加鎖,從而使該條指令成為內(nèi)存屏障;
  • 所有寫控制寄存器、系統(tǒng)寄存器或 debug 寄存器的指令(比如,cli和sti指令,可以改變eflags寄存器的IF標(biāo)志);
  • lfence、sfence和mfence匯編指令,分別用來實(shí)現(xiàn)讀內(nèi)存屏障、寫內(nèi)存屏障和讀 / 寫內(nèi)存屏障;
  • 特殊的匯編指令,比如iret指令,可以終止中斷或異常處理程序。

在 ARM 系統(tǒng)中,則使用ldrex和strex匯編指令實(shí)現(xiàn)內(nèi)存屏障。這些內(nèi)存屏障指令能夠阻止 CPU 對指令的亂序執(zhí)行,確保內(nèi)存操作的順序性和可見性,從而保證多線程環(huán)境下程序的正確執(zhí)行。

三、內(nèi)存屏障的多元類型與功能詳解

在 Linux 系統(tǒng)中,內(nèi)存屏障主要包括通用內(nèi)存屏障、讀內(nèi)存屏障、寫內(nèi)存屏障和讀寫內(nèi)存屏障,它們各自在保證內(nèi)存操作的順序性和可見性方面發(fā)揮著關(guān)鍵作用 。

1. 通用內(nèi)存屏障

通用內(nèi)存屏障(mb)確保在其之前的所有內(nèi)存讀寫操作都在其之后的內(nèi)存讀寫操作之前完成 。它保證了其前后的讀寫指令順序,防止編譯器和 CPU 對這些指令進(jìn)行重排序。在 Linux 內(nèi)核中,mb()函數(shù)被定義用來實(shí)現(xiàn)通用內(nèi)存屏障,其定義如下:

#ifdef CONFIG_SMP
    #define mb() asm volatile("mfence" ::: "memory")
#else
    #define mb() barrier()
#endif

在 SMP(對稱多處理)系統(tǒng)中,如果是 64 位 CPU 或支持mfence指令的 32 位 CPU,mb()宏被定義為asm volatile("mfence" ::: "memory") 。mfence指令是 x86 架構(gòu)下的一條匯編指令,它會使 CPU 等待,直到之前所有的內(nèi)存讀寫操作都完成,才會執(zhí)行之后的內(nèi)存操作,從而保證了內(nèi)存操作的順序性。

asm volatile表示這是一段匯編代碼,并且禁止編譯器對其進(jìn)行優(yōu)化,::: "memory"告訴編譯器內(nèi)存中的數(shù)據(jù)可能會被修改,不能依賴寄存器中的舊值 。在單處理器(UP)系統(tǒng)中,mb()被定義為barrier(),barrier()宏通過asm volatile("":::"memory")` 來實(shí)現(xiàn),同樣是為了防止編譯器對內(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。如果沒有使用讀內(nèi)存屏障,由于指令重排序,線程 B 可能會先讀取y,再讀取x,導(dǎo)致讀取到的數(shù)據(jù)不符合預(yù)期 。通過在讀取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)存屏障指令,它保證了在其之前的讀操作都完成后,才會執(zhí)行之后的讀操作 。在 UP 系統(tǒng)中,rmb()同樣被定義為barrier() 。

3. 寫內(nèi)存屏障

寫內(nèi)存屏障(wmb)保證在其之前的所有寫操作都在其之后的寫操作之前完成 。它確保了寫指令的順序,防止寫操作被重排序。在數(shù)據(jù)更新場景中,寫內(nèi)存屏障尤為重要。例如,在一個(gè)多線程程序中,線程 A 需要先更新共享變量x,再更新共享變量y,并且要求其他線程能夠按照這個(gè)順序看到更新后的值 。

如果沒有使用寫內(nèi)存屏障,由于指令重排序,其他線程可能會先看到y(tǒng)的更新,再看到x的更新,導(dǎo)致數(shù)據(jù)不一致 。通過在更新x和y之間插入寫內(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)下的寫內(nèi)存屏障指令,它保證了在其之前的寫操作都完成后,才會執(zhí)行之后的寫操作 。在 UP 系統(tǒng)中,wmb()也被定義為barrier() 。

4. 讀寫內(nèi)存屏障

讀寫內(nèi)存屏障既保證了讀操作的順序,也保證了寫操作的順序 。它確保了在其之前的所有讀寫操作都在其之后的讀寫操作之前完成 。在一些復(fù)雜的數(shù)據(jù)結(jié)構(gòu)讀寫場景中,讀寫內(nèi)存屏障非常有用。例如,在一個(gè)多線程程序中,線程 A 需要先寫入數(shù)據(jù)到共享數(shù)據(jù)結(jié)構(gòu),然后讀取該數(shù)據(jù)結(jié)構(gòu)中的其他部分;線程 B 則需要先讀取線程 A 寫入的數(shù)據(jù),然后再寫入新的數(shù)據(jù) 。通過在這些讀寫操作之間插入讀寫內(nèi)存屏障,可以保證線程 A 和線程 B 的讀寫操作按照預(yù)期的順序進(jìn)行,避免數(shù)據(jù)競爭和不一致的問題 。

在 Linux 內(nèi)核中,并沒有專門定義一個(gè)獨(dú)立的讀寫內(nèi)存屏障函數(shù),通??梢允褂猛ㄓ脙?nèi)存屏障mb()來實(shí)現(xiàn)讀寫內(nèi)存屏障的功能,因?yàn)閙b()同時(shí)保證了讀寫操作的順序 。

四、應(yīng)用案例深度解析

1. 多核處理器環(huán)境下的同步

在多核處理器環(huán)境中,每個(gè)核心都有自己的高速緩存,當(dāng)多個(gè)核心同時(shí)訪問共享內(nèi)存時(shí),就可能出現(xiàn)緩存一致性問題 。例如,核心 A 修改了共享內(nèi)存中的數(shù)據(jù),并將其寫入自己的緩存,但此時(shí)核心 B 的緩存中仍然是舊數(shù)據(jù)。如果核心 B 繼續(xù)從自己的緩存中讀取數(shù)據(jù),就會讀到不一致的數(shù)據(jù) 。

為了解決這個(gè)問題,內(nèi)存屏障被廣泛應(yīng)用。內(nèi)存屏障可以確保在屏障之前的內(nèi)存操作都完成后,才會執(zhí)行屏障之后的內(nèi)存操作,從而保證了緩存一致性 。例如,在 X86 架構(gòu)中,mfence指令可以作為內(nèi)存屏障,它會使 CPU 等待,直到之前所有的內(nèi)存讀寫操作都完成,才會執(zhí)行之后的內(nèi)存操作 。

在多線程編程中,當(dāng)一個(gè)線程修改了共享數(shù)據(jù)后,通過插入內(nèi)存屏障,可以確保其他線程能夠立即看到這個(gè)修改 。假設(shè)線程 A 和線程 B 共享一個(gè)變量x,線程 A 修改了x的值后,插入一個(gè)內(nèi)存屏障,然后線程 B 讀取x的值,由于內(nèi)存屏障的作用,線程 B 讀取到的一定是線程 A 修改后的最新值 。

2. 設(shè)備驅(qū)動開發(fā)中的應(yīng)用

在設(shè)備驅(qū)動開發(fā)中,內(nèi)存屏障也起著關(guān)鍵作用。設(shè)備驅(qū)動程序需要與硬件設(shè)備進(jìn)行交互,而硬件設(shè)備的操作通常需要按照特定的順序進(jìn)行 。例如,在對硬件寄存器進(jìn)行操作時(shí),必須確保先寫入配置信息,再啟動設(shè)備 。如果沒有內(nèi)存屏障,編譯器和 CPU 可能會對這些操作進(jìn)行重排序,導(dǎo)致設(shè)備無法正常工作 。

以串口驅(qū)動為例,在向串口發(fā)送數(shù)據(jù)時(shí),需要先檢查串口發(fā)送緩沖區(qū)是否為空,然后再將數(shù)據(jù)寫入緩沖區(qū) 。如果這兩個(gè)操作被重排序,就可能導(dǎo)致數(shù)據(jù)丟失 。通過在這兩個(gè)操作之間插入內(nèi)存屏障,可以確保先檢查緩沖區(qū),再寫入數(shù)據(jù),從而保證串口通信的正確性 。在 Linux 內(nèi)核中,串口驅(qū)動代碼可能會如下實(shí)現(xiàn):

// 檢查串口發(fā)送緩沖區(qū)是否為空
while (readl(serial_port + STATUS_REGISTER) & TX_FIFO_FULL);
// 插入寫內(nèi)存屏障
wmb();
// 將數(shù)據(jù)寫入串口發(fā)送緩沖區(qū)
writel(data, serial_port + DATA_REGISTER);

在這個(gè)例子中,wmb()函數(shù)作為寫內(nèi)存屏障,確保了在寫入數(shù)據(jù)之前,先完成對緩沖區(qū)狀態(tài)的檢查,從而保證了串口驅(qū)動的正常工作 。

3. RCU 機(jī)制中的關(guān)鍵角色

RCU(Read - Copy - Update)機(jī)制是 Linux 內(nèi)核中一種高效的同步機(jī)制,主要用于讀多寫少的場景 。在 RCU 機(jī)制中,內(nèi)存屏障發(fā)揮著至關(guān)重要的作用 。

在 RCU 中,讀操作不需要加鎖,這大大提高了讀操作的效率 。然而,為了保證數(shù)據(jù)的一致性,在寫操作時(shí)需要采取一些特殊的措施 。當(dāng)一個(gè)寫者需要更新數(shù)據(jù)時(shí),它首先會創(chuàng)建一個(gè)數(shù)據(jù)的副本,在副本上進(jìn)行修改,然后將修改后的副本替換原來的數(shù)據(jù) 。在這個(gè)過程中,內(nèi)存屏障用于確保讀操作能夠看到正確的數(shù)據(jù) 。

例如,在 Linux 內(nèi)核的鏈表操作中,經(jīng)常會使用 RCU 機(jī)制 。當(dāng)一個(gè)線程要向鏈表中插入一個(gè)新節(jié)點(diǎn)時(shí),它會先創(chuàng)建新節(jié)點(diǎn),設(shè)置好節(jié)點(diǎn)的指針,然后使用rcu_assign_pointer函數(shù)來更新鏈表的指針 。rcu_assign_pointer函數(shù)內(nèi)部會使用內(nèi)存屏障,確保在新節(jié)點(diǎn)的指針設(shè)置完成后,其他線程才能看到這個(gè)新節(jié)點(diǎn) 。這樣,在多線程環(huán)境下,讀線程可以在不加鎖的情況下安全地遍歷鏈表,而寫線程也可以在不影響讀線程的情況下更新鏈表,從而提高了系統(tǒng)的并發(fā)性能 。

4. 內(nèi)存一致性模型

內(nèi)存一致性模型(Memory Consistency Model)是用來描述多線程對共享存儲器的訪問行為,在不同的內(nèi)存一致性模型里,多線程對共享存儲器的訪問行為有非常大的差別。這些差別會嚴(yán)重影響程序的執(zhí)行邏輯,甚至?xí)斐绍浖壿媶栴}。

不同的處理器架構(gòu),使用了不同的內(nèi)存一致性模型,目前有多種內(nèi)存一致性模型,從上到下模型的限制由強(qiáng)到弱:

  • 順序一致性(Sequential Consistency)模型
  • 完全存儲定序(Total Store Order)模型
  • 部分存儲定序(Part Store Order)模型
  • 寬松存儲(Relax Memory Order)模型

注意,這里說的內(nèi)存模型是針對可以同時(shí)執(zhí)行多線程的平臺,如果只能同時(shí)執(zhí)行一個(gè)線程,也就是系統(tǒng)中一共只有一個(gè)CPU核,那么它一定是滿足順序一致性模型的。

對于內(nèi)存的訪問,我們只關(guān)心兩種類型的指令的順序,一種是讀取,一種是寫入。對于讀取和加載指令來說,它們兩兩一起,一共有四種組合:

  • LoadLoad:前一條指令是讀取,后一條指令也是讀取。
  • LoadStore:前一條指令是讀取,后一條指令是寫入。
  • StoreLoad:前一條指令是寫入,后一條指令是讀取。
  • StoreStore:前一條指令是寫入,后一條指令也是寫入。

(1) 順序一致性模型

順序存儲模型是最簡單的存儲模型,也稱為強(qiáng)定序模型。CPU會按照代碼來執(zhí)行所有的讀取與寫入指令,即按照它們在程序中出現(xiàn)的次序來執(zhí)行。同時(shí),從主存儲器和系統(tǒng)中其它CPU的角度來看,感知到數(shù)據(jù)變化的順序也完全是按照指令執(zhí)行的次序。也可以理解為,在程序看來,CPU不會對指令進(jìn)行任何重排序的操作。在這種模型下執(zhí)行的程序是完全不需要內(nèi)存屏障的。但是,帶來的問題就是性能會比較差,現(xiàn)在已經(jīng)沒有符合這種內(nèi)存一致性模型的系統(tǒng)了。

為了提高系統(tǒng)的性能,不同架構(gòu)都會或多或少的對這種強(qiáng)一致性模型進(jìn)行了放松,允許對某些指令組合進(jìn)行重排序。注意,這里處理器對讀取或?qū)懭氩僮鞯姆潘?,是以兩個(gè)操作之間不存在數(shù)據(jù)依賴性為前提的,處理器不會對存在數(shù)據(jù)依賴性的兩個(gè)內(nèi)存操作做重排序。

(2) 完全存儲定序模型

這種內(nèi)存一致性模型允許對StoreLoad指令組合進(jìn)行重排序,如果第一條指令是寫入,第二條指令是讀取,那么有可能在程序看來,讀取指令先于寫入指令執(zhí)行。但是,對于其它另外三種指令組合還是可以保證按照順序執(zhí)行。

這種模型就相當(dāng)于前面提到的,在CPU和緩存中間加入了存儲緩沖,而且這個(gè)緩沖還是一個(gè)滿足先入先出(FIFO)的隊(duì)列。先入先出隊(duì)列就保證了對StoreStore這種指令組合也能保證按照順序被感知。

我們非常熟悉的X86架構(gòu)就是使用的這種內(nèi)存一致性模型。

(3) 部分存儲定序模型

這種內(nèi)存一致性模型除了允許對StoreLoad指令組合進(jìn)行重排序外,還允許對StoreStore指令組合進(jìn)行重排序。但是,對于其它另外兩種指令組合還是可以保證按照順序執(zhí)行。

這種模型就相當(dāng)于也在CPU和緩存中間加入了存儲緩沖,但是這個(gè)緩沖不是先入先出的。

(4) 寬松存儲模型

這種內(nèi)存一致性模型允許對上面說的四種指令組合都進(jìn)行重排序。

這種模型就相當(dāng)于前面說的,既有存儲緩沖,又有無效隊(duì)列的情況。

這種內(nèi)存模型下其實(shí)還有一個(gè)細(xì)微的差別,就是所謂的數(shù)據(jù)依賴性的問題。例如下面的程序,假設(shè)變量A初始值是0:

CPU 0

CPU 1

A = 1;

Q = P;

<write barrier>

B = *Q;

P = &A;


五、使用注意事項(xiàng)與性能考量

1. 避免過度使用

雖然內(nèi)存屏障是解決多線程環(huán)境下內(nèi)存一致性問題的有力工具,但過度使用會對系統(tǒng)性能產(chǎn)生負(fù)面影響 。內(nèi)存屏障會阻止 CPU 和編譯器對指令進(jìn)行重排序,這在一定程度上限制了它們的優(yōu)化能力,從而增加了指令執(zhí)行的時(shí)間 。在一些不必要的場景中使用內(nèi)存屏障,會導(dǎo)致性能下降 。

例如,在單線程環(huán)境中,由于不存在多線程并發(fā)訪問共享數(shù)據(jù)的問題,使用內(nèi)存屏障是完全沒有必要的,這只會浪費(fèi)系統(tǒng)資源 。在多線程環(huán)境中,如果共享數(shù)據(jù)的訪問沒有數(shù)據(jù)競爭問題,也不應(yīng)隨意使用內(nèi)存屏障 。比如,在一個(gè)多線程程序中,多個(gè)線程只是讀取共享數(shù)據(jù),而不進(jìn)行寫操作,此時(shí)使用內(nèi)存屏障并不能帶來任何好處,反而會降低性能 。因此,在使用內(nèi)存屏障時(shí),需要仔細(xì)分析代碼的執(zhí)行邏輯和數(shù)據(jù)訪問模式,確保只在必要的地方使用內(nèi)存屏障,以避免不必要的性能損失 。

2. 選擇合適的屏障類型

不同類型的內(nèi)存屏障在功能和適用場景上有所不同,因此根據(jù)具體的場景選擇合適的內(nèi)存屏障類型至關(guān)重要 。如果只需要保證讀操作的順序,那么使用讀內(nèi)存屏障(rmb)即可;如果只需要保證寫操作的順序,使用寫內(nèi)存屏障(wmb)就足夠了 。在一些復(fù)雜的場景中,可能需要同時(shí)保證讀寫操作的順序,這時(shí)就需要使用通用內(nèi)存屏障(mb)或讀寫內(nèi)存屏障 。

例如,在一個(gè)多線程程序中,線程 A 需要先讀取共享變量x,再讀取共享變量y,并且要求這兩個(gè)讀操作按照順序進(jìn)行,此時(shí)就可以在讀取x和y之間插入讀內(nèi)存屏障 。如果線程 A 需要先寫入共享變量x,再寫入共享變量y,并且要求其他線程能夠按照這個(gè)順序看到更新后的值,那么就應(yīng)該在寫入x和y之間插入寫內(nèi)存屏障 。在一些涉及復(fù)雜數(shù)據(jù)結(jié)構(gòu)讀寫的場景中,可能需要使用通用內(nèi)存屏障來保證讀寫操作的順序 。

比如,在一個(gè)多線程程序中,線程 A 需要先寫入數(shù)據(jù)到共享鏈表,然后讀取鏈表中的其他部分,線程 B 則需要先讀取線程 A 寫入的數(shù)據(jù),然后再寫入新的數(shù)據(jù),這種情況下就可以使用通用內(nèi)存屏障來確保線程 A 和線程 B 的讀寫操作按照預(yù)期的順序進(jìn)行 。因此,在使用內(nèi)存屏障時(shí),需要根據(jù)具體的場景和需求,選擇合適的內(nèi)存屏障類型,以充分發(fā)揮內(nèi)存屏障的作用,同時(shí)避免不必要的性能開銷 。

3. 性能監(jiān)測與優(yōu)化

為了確保內(nèi)存屏障的使用不會對系統(tǒng)性能造成過大的影響,使用工具監(jiān)測內(nèi)存屏障對性能的影響,并根據(jù)監(jiān)測結(jié)果進(jìn)行優(yōu)化是很有必要的 。在 Linux 系統(tǒng)中,可以使用 perf 工具來監(jiān)測內(nèi)存屏障對性能的影響 。perf 是一個(gè)性能分析工具,它可以收集系統(tǒng)的性能數(shù)據(jù),包括CPU使用率、內(nèi)存訪問次數(shù)等 。通過使用perf 工具,可以了解內(nèi)存屏障的使用對系統(tǒng)性能的影響,從而找到性能瓶頸,并進(jìn)行優(yōu)化 。

例如,可以使用 perf record 命令來收集性能數(shù)據(jù),然后使用 perf report 命令來查看性能報(bào)告 。在性能報(bào)告中,可以看到各個(gè)函數(shù)的 CPU 使用率、內(nèi)存訪問次數(shù)等信息,從而找到內(nèi)存屏障使用較多的函數(shù),并分析其對性能的影響 。如果發(fā)現(xiàn)某個(gè)函數(shù)中內(nèi)存屏障的使用導(dǎo)致了性能下降,可以嘗試優(yōu)化該函數(shù)的代碼,減少內(nèi)存屏障的使用,或者選擇更合適的內(nèi)存屏障類型 。

除了使用 perf 工具外,還可以通過代碼優(yōu)化、算法改進(jìn)等方式來提高系統(tǒng)性能 。例如,可以減少不必要的內(nèi)存訪問,優(yōu)化數(shù)據(jù)結(jié)構(gòu),提高代碼的并行性等 。通過綜合使用這些方法,可以有效地提高系統(tǒng)性能,確保內(nèi)存屏障的使用不會對系統(tǒng)性能造成過大的影響 。


責(zé)任編輯:趙寧寧 來源: 深度 Linux
相關(guān)推薦

2013-06-20 10:25:56

2023-11-05 12:05:35

JVM內(nèi)存

2019-04-08 16:50:33

前端性能監(jiān)控

2022-08-21 16:52:27

Linux虛擬內(nèi)存

2023-09-19 22:47:39

Java內(nèi)存

2022-07-04 08:01:01

鎖優(yōu)化Java虛擬機(jī)

2020-11-04 15:35:13

Golang內(nèi)存程序員

2023-02-10 08:11:43

Linux系統(tǒng)調(diào)用

2022-11-09 08:12:07

2022-12-28 09:07:41

2021-11-26 00:00:48

JVM內(nèi)存區(qū)域

2022-07-06 08:05:52

Java對象JVM

2015-12-28 11:41:57

JVM內(nèi)存區(qū)域內(nèi)存溢出

2021-08-31 10:32:11

LinuxPage Cache命令

2023-11-26 17:59:00

React組件參數(shù)

2010-06-01 15:25:27

JavaCLASSPATH

2016-12-08 15:36:59

HashMap數(shù)據(jù)結(jié)構(gòu)hash函數(shù)

2020-07-21 08:26:08

SpringSecurity過濾器

2022-09-26 08:01:31

線程LIFO操作方式

2020-12-04 11:40:53

Linux
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號