0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線(xiàn)課程
  • 觀(guān)看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

一文徹底搞懂內(nèi)存屏障與volatile

jf_B3xDfZry ? 來(lái)源:C語(yǔ)言學(xué)習(xí)聯(lián)盟 ? 作者:C語(yǔ)言學(xué)習(xí)聯(lián)盟 ? 2022-11-29 11:43 ? 次閱讀

最有價(jià)值的寫(xiě)在最前面

內(nèi)存屏障與 volatile 是高并發(fā)編程中比較常用的兩個(gè)技術(shù),無(wú)鎖隊(duì)列的時(shí)候就會(huì)用到這兩項(xiàng)技術(shù)。然而這兩項(xiàng)技術(shù)涉及比較廣的基礎(chǔ)知識(shí),所以比較難以理解,也比較不容易解釋清楚。關(guān)于內(nèi)存屏障和 volatile 網(wǎng)上有相當(dāng)多的資料,但是總感覺(jué)還是不夠系統(tǒng)和深入。當(dāng)然由于我自身水平有限,所以也不敢保證就能把這兩個(gè)概念說(shuō)清楚。所以在文章的開(kāi)始,先列舉一些我在學(xué)習(xí)過(guò)程中比較好的資料。1.基本概念
  • 這篇博客深入淺出的介紹了內(nèi)存屏障和volatile的概念,并且列舉了一些非常好的用例,可以直觀(guān)感受內(nèi)存屏障與volatile的作用。并且列舉了 linux 內(nèi)核中著名的無(wú)鎖隊(duì)列 kfifo 是如何使用內(nèi)存屏障的。
  • 這篇博客講解了 LOCK 前綴與內(nèi)存屏障的關(guān)系,LOCK 是實(shí)現(xiàn) CAS 操作的關(guān)鍵,所以弄清楚 LOCK 的作用也是非常有必要的。
  • 《深入理解計(jì)算機(jī)系統(tǒng)》第三章、第四章、第六章《深入理解計(jì)算機(jī)系統(tǒng)》是一本神書(shū)(本文后面都簡(jiǎn)稱(chēng)CSAPP),有多神相信就不用我介紹了。第三章介紹了while循環(huán)的機(jī)器指令,第四章有關(guān)于分支預(yù)測(cè)的相關(guān)知識(shí),第六章有關(guān)于緩存的知識(shí)。
2.深入理解
  • 《Memory Barriers: a Hardware View for Software Hackers》該文章深入淺出地講解了MESI的基本概念,MESI 引起的緩存可見(jiàn)性問(wèn)題,從而引出了內(nèi)存屏障的作用,以及為什么要使用內(nèi)存屏障。該文章非常值得一讀。
《Memory Ordering in Modern Microprocessors》該文章和上一篇是同一個(gè)作者。該文章對(duì)上一篇中第6部分的內(nèi)容進(jìn)行了更加詳細(xì)的說(shuō)明。
3.Java volatile在剛開(kāi)始學(xué)習(xí)volatile和內(nèi)存屏障的時(shí)候,在網(wǎng)上搜到很多的資料都是講java實(shí)現(xiàn)的。volatile這個(gè)關(guān)鍵字在java和 CC++ 里面有非常大的區(qū)別,容易引起誤會(huì)。主要區(qū)別在于,java volatile 具有緩存同步的功能,而 CC++ 沒(méi)有這個(gè)功能,具體原因本文會(huì)簡(jiǎn)單講下。詳細(xì)內(nèi)容參見(jiàn)B站馬士兵老師的課程。 4.無(wú)鎖隊(duì)列實(shí)踐理論結(jié)合實(shí)踐,關(guān)于無(wú)鎖隊(duì)列的實(shí)現(xiàn)有幾篇文章值得一讀:
  • 單生產(chǎn)者——單消費(fèi)者模型 講解kfifo的實(shí)現(xiàn),kfifo是linux內(nèi)核實(shí)現(xiàn)的無(wú)鎖隊(duì)列,非常具有參考價(jià)值。
  • 多對(duì)多模型 多個(gè)生產(chǎn)者和消費(fèi)者,需要用到CAS操作。
個(gè)人認(rèn)為,如果把這些資料里的內(nèi)容都看懂了,后面的內(nèi)容其實(shí)也就可以不用看了,哈哈。好了,下面開(kāi)始我個(gè)人對(duì)于內(nèi)存屏障和 volatile 的一些粗淺的見(jiàn)解。

volatile

關(guān)于 volatile 關(guān)鍵字 這里有詳細(xì)描述。主要是為了防止優(yōu)化編譯帶來(lái)的一些問(wèn)題。注意:volatile 只作用于編譯階段,對(duì)運(yùn)行階段沒(méi)有任何影響。

1.防止直接從寄存器中獲取全局變量的值


	//disorder_test.c #include #include #include #defineQUEUE_LEN1//為了測(cè)試方便 typedefstruct { intm_flag; longlongm_data; }QUEUENODE,LPQUEUENODE; longlonggoods; QUEUENODEm_queue[QUEUE_LEN]; void*Push(void*param) { longlongdata=*(longlong*)param; intpos=data%QUEUE_LEN;  while(m_queue[pos].m_flag);  m_queue[pos].m_data=data; m_queue[pos].m_flag=1; returnNULL; } void*Pop(void*param) { intpos=*(longlong*)param%QUEUE_LEN;  while(!m_queue[pos].m_flag);  goods=m_queue[pos].m_data; m_queue[pos].m_flag=0; returnNULL; } intmain() { longlongi=1; memset(m_queue,0,sizeof(m_queue)); pthread_tpit1,pit2; while(1) { pthread_create(&pit1,NULL,&Push,&i); pthread_create(&pit2,NULL,&Pop,&i); //waitforpthreadstop pthread_join(pit1,NULL); pthread_join(pit2,NULL); printf("goods:%lld ",goods); i++; } } 如上面代碼所示,該代碼使用一個(gè)定長(zhǎng)循環(huán)隊(duì)列,實(shí)現(xiàn)了一個(gè)生產(chǎn)者-消費(fèi)者模型。該代碼中,只有一個(gè)生產(chǎn)者和一個(gè)消費(fèi)者。QUEUENODE定義了一個(gè)具體的商品。其中有兩個(gè)變量,m_flag用于標(biāo)識(shí)隊(duì)列中對(duì)應(yīng)位置是否存在商品,m_flag為 1 表示生產(chǎn)者已經(jīng)生產(chǎn)了商品,m_flag為 0 表示商品還未被生產(chǎn)。m_data表示商品具體的值。m_queue為一個(gè)全局的循環(huán)隊(duì)列。Push函數(shù)向隊(duì)列中放入商品,在push前首先判斷指定位置是否存在商品,如果存在則等待(通過(guò)while自旋來(lái)實(shí)現(xiàn)),否則首先放入商品(為m_data賦值),再設(shè)置m_flag為 1。Pop函數(shù)用于從隊(duì)列中獲取商品,pop之前先判斷指定位置是否存在商品,如果不存在則等待(通過(guò)while自旋來(lái)實(shí)現(xiàn)),否則首先取出商品(將m_data賦值給goods),再設(shè)置m_flag為 0。main函數(shù)是一個(gè)死循環(huán),每次開(kāi)啟兩個(gè)線(xiàn)程,一個(gè)線(xiàn)程向隊(duì)列中push商品,一個(gè)線(xiàn)程從隊(duì)列中pop線(xiàn)程,然后等待兩個(gè)線(xiàn)程結(jié)束,最后打印出通過(guò)pop獲取到的商品的值,即goods。OK,現(xiàn)在用非優(yōu)化編譯編譯該代碼,并運(yùn)行:

	gccdisorder_test.c-odisorder_test-lpthread ./disorder_test 17fcac06-6cb0-11ed-8abf-dac502259ad0.pngOK,看起來(lái)一切正常。現(xiàn)在我們換成優(yōu)化編譯試試:

	gccdisorder_test.c-O2-odisorder_test-lpthread ./disorder_test 18075d68-6cb0-11ed-8abf-dac502259ad0.pngimg程序陷入了死循環(huán)…發(fā)生了什么?現(xiàn)在我們來(lái)看看這段代碼的匯編,首先是非優(yōu)化編譯版本:

	gcc-Sdisorder_test.c catdisorder_test.s 1815001c-6cb0-11ed-8abf-dac502259ad0.pngimg這里我們只標(biāo)注出最關(guān)鍵的部分,即 push 中的 while 循環(huán)。我們注意到,while 中每次循環(huán)都會(huì)執(zhí)行取值和運(yùn)算操作,然后才執(zhí)行 testl 判斷。我們?cè)賮?lái)看看優(yōu)化版本。

	gcc-S-O2disorder_test.c catdisorder_test.s 1822fb72-6cb0-11ed-8abf-dac502259ad0.pngimg這里就非??膳铝?,可以看到.L4本身就是一個(gè)死循環(huán),前面 testl 之后如果發(fā)現(xiàn)不滿(mǎn)足條件,則直接跳進(jìn)死循環(huán)。這是為什么?我們來(lái)看看push的代碼:

	void*Push(void*param) { longlongdata=*(longlong*)param; intpos=data%QUEUE_LEN; while(m_queue[pos].m_flag) ; m_queue[pos].m_data=data; m_queue[pos].m_flag=1; returnNULL; } while循環(huán)會(huì)檢測(cè)m_queue[pos].m_flag,而在這個(gè)函數(shù)中,只有當(dāng)m_queue[pos].m_flag為0時(shí),循環(huán)才會(huì)跳出,執(zhí)行l(wèi)ine7及之后的代碼,而在line8才會(huì)對(duì)m_flag進(jìn)行修改。所以編譯器認(rèn)為在循環(huán)的過(guò)程中,沒(méi)人會(huì)修改m_flag。既然沒(méi)有修改m_flag,只要m_flag一開(kāi)始的值不為0,那么m_flag就是一個(gè)不會(huì)改變的值,當(dāng)然就是死循環(huán)!顯然編譯器并不知道另一個(gè)線(xiàn)程會(huì)執(zhí)行pop函數(shù),而pop會(huì)修改m_flag的值。如果觀(guān)察pop的匯編代碼也會(huì)發(fā)現(xiàn)完全相同的優(yōu)化邏輯。所以,在這種情況下,就需要程序員顯式的告訴編譯器,m_flag是一個(gè)會(huì)發(fā)生改變的值,所以不要嘗試做這樣的優(yōu)化。這就是volatile關(guān)鍵字?,F(xiàn)在我們給m_flag加上volatile關(guān)鍵字:

	typedefstruct { volatileintm_flag; longlongm_data; }QUEUENODE,LPQUEUENODE; 再次優(yōu)化編譯并運(yùn)行程序:

	gccdisorder_test.c-O2-odisorder_test-lpthread ./disorder_test 1839c834-6cb0-11ed-8abf-dac502259ad0.pngOK,一切正常!現(xiàn)在我們?cè)賮?lái)看看匯編代碼:184e006a-6cb0-11ed-8abf-dac502259ad0.png現(xiàn)在每次循環(huán)都會(huì)執(zhí)行movl指令去獲取m_flag的值!一切都變得美好了。

2.防止指令亂序

volatile 的第二個(gè)作用就是防止編譯時(shí)產(chǎn)生的指令亂序。這個(gè)很簡(jiǎn)單,有如下代碼:

	//test.c intx,y,r; voidf() { x=r; y=1; } voidmain() { f(); } 這次,我們直接對(duì)比非優(yōu)化編譯與優(yōu)化編譯的匯編代碼。
  • 非優(yōu)化編譯
187de8d4-6cb0-11ed-8abf-dac502259ad0.png
  • 優(yōu)化編譯
1893309a-6cb0-11ed-8abf-dac502259ad0.png不難發(fā)現(xiàn),優(yōu)化編譯的版本,交換了x=r 和y=1的順序,先將 y 的值賦值為 1,再將 x 值賦值為 r?,F(xiàn)在我們將 x,y, r 加上volatile關(guān)鍵字。

	volatileintx,y,r; 再次查看匯編代碼:18b93312-6cb0-11ed-8abf-dac502259ad0.png指令順序和代碼順序一致。在 https://www.runoob.com/w3cnote/c-volatile-keyword.html 介紹 volatile 時(shí)有這樣一段描述 “當(dāng)使用 volatile 聲明的變量的值的時(shí)候,系統(tǒng)總是重新從它所在的內(nèi)存讀取數(shù)據(jù),即使它前面的指令剛剛從該處讀取過(guò)數(shù)據(jù)”。然而,實(shí)際情況真的是每次都從內(nèi)存中讀取數(shù)據(jù)么?其實(shí)這只是一個(gè)籠統(tǒng)的說(shuō)法,更為準(zhǔn)確的說(shuō)法應(yīng)該是,系統(tǒng)不會(huì)直接從寄存器中讀取 volatile 修飾的變量。因?yàn)椋拇嫫鞯淖x寫(xiě)性能遠(yuǎn)高于內(nèi)存,所以在CPU寄存器和內(nèi)存之前,通常有多級(jí)高速緩存。18c69b88-6cb0-11ed-8abf-dac502259ad0.png相信大家都見(jiàn)過(guò)這樣一張著名的圖,不難發(fā)現(xiàn),圖中,在內(nèi)存與寄存器之間,存在 L1、L2、L3 這樣三級(jí)緩存。所以指令在進(jìn)行訪(fǎng)存操作的時(shí)候,會(huì)首先逐級(jí)查看緩存中是否有對(duì)應(yīng)的數(shù)據(jù),如果3級(jí)緩存有沒(méi)有期望的數(shù)據(jù),才會(huì)訪(fǎng)問(wèn)內(nèi)存。而通常在多核CPU中緩存是如下圖所示的這樣一種結(jié)構(gòu):18e6e6d6-6cb0-11ed-8abf-dac502259ad0.png每個(gè) CPU core 都有自己獨(dú)立的 L1 和 L2 緩存,多個(gè) core 共享一個(gè)L3緩存,多個(gè) CPU 有各自的 L3 緩存,多個(gè)CPU 共享內(nèi)存。每個(gè) core 都有自己獨(dú)立的 L1 和 L2 緩存,緩存可以獨(dú)立讀寫(xiě)!這個(gè)就可怕了,因?yàn)檫@就存在不同 core 讀寫(xiě)同一份數(shù)據(jù)的可能,如果不加任何限制,豈不天下大亂了?所以對(duì)于多核 CPU,需要一種機(jī)制來(lái)對(duì)緩存中的數(shù)據(jù)進(jìn)行同步。這也就是我們接下來(lái)要講的MESI。

MESI

MESI 在《Memory Barriers: a Hardware View for Software Hackers》一文中有非常詳細(xì)的描述,這里只對(duì)一些關(guān)鍵問(wèn)題進(jìn)行闡述。在描述 MESI 之前,我們先說(shuō)明兩個(gè)重要的操作:
  • LoadLoad是指CPU從Cache中加載數(shù)據(jù)。
  • StoreStore是指CPU將數(shù)據(jù)寫(xiě)回Cache。
在《Memory Barriers: a Hardware View for Software Hackers》還有一個(gè)操作叫 write back(寫(xiě)回),是指將Cache數(shù)據(jù)寫(xiě)回內(nèi)存。在 CSAPP 中,第4章講到指令的6個(gè)階段其中也有一個(gè)階段叫write back,這里是指將執(zhí)行階段的結(jié)果寫(xiě)回到寄存器,這兩個(gè)概念不要混淆了。
MESI 是指緩存行的四種狀態(tài):I:invalid,最簡(jiǎn)單的一種狀態(tài),表示該緩存行沒(méi)有數(shù)據(jù),顯然這也是緩存行的初始狀態(tài)。S:shared,該緩存行中的數(shù)據(jù)被其他CPU共享。在shared狀態(tài)下,緩存行為只讀,不可以修改。E:exclusive,該緩存行中的數(shù)據(jù)沒(méi)有被其他CPU共享,且緩存中的數(shù)據(jù)與內(nèi)存中保持一致。在exclusive狀態(tài)下,緩存行可以修改。M:modified,該緩存行保存了唯一一份 up-to-date 的數(shù)據(jù)。即該緩存行中的數(shù)據(jù)沒(méi)有被其他CPU共享,且緩存行的數(shù)據(jù)與內(nèi)存不一致。這四種狀態(tài)之間是可以互相轉(zhuǎn)換的,具體的轉(zhuǎn)換方式在《Memory Barriers: a Hardware View for Software Hackers》一文中也有非常詳細(xì)的描述(重要的是事情說(shuō)三遍,這篇文章很重要?。。。?。這里我們只對(duì)部分狀態(tài)轉(zhuǎn)換加以說(shuō)明。
  • I to S
緩存的初始狀態(tài)為I,即沒(méi)有數(shù)據(jù)。當(dāng)緩存行通過(guò)Read消息將數(shù)據(jù)加載進(jìn)來(lái)后,其狀態(tài)就變成了S。這個(gè)Read消息可以發(fā)送給其他緩存行,因?yàn)樾枰臄?shù)據(jù)可能在其他緩存行中,顯然當(dāng)前緩存行加載完數(shù)據(jù)后,該數(shù)據(jù)就被至少兩個(gè)緩存行共享,狀態(tài)就應(yīng)該為S。還有一種可能,就是沒(méi)有緩存行有這個(gè)數(shù)據(jù),此時(shí)就需要從內(nèi)存中加載該數(shù)據(jù),加載完成后,只有當(dāng)前緩存行有這個(gè)數(shù)據(jù)。這個(gè)狀態(tài)看起來(lái)更像是狀態(tài)E,但實(shí)際上這種情況狀態(tài)依然是S。我個(gè)人猜想,這或許是為了提升Read操作的性能,因?yàn)镽ead并沒(méi)有要修改數(shù)據(jù)的意思,所以沒(méi)必要去區(qū)分Read之后數(shù)據(jù)是否真的被共享了。
  • S to E
我們前面說(shuō)到,S狀態(tài)的緩存行是只讀的,如果想要修改怎么辦?直接改可以么?當(dāng)然不行,如果直接改那么就會(huì)出現(xiàn)同一份數(shù)據(jù)在不同的緩存行中值不同!這顯然是不可接受的。所以如果一個(gè)CPU希望修改處于S狀態(tài)的緩存行里面的數(shù)據(jù),就需要向其他CPU發(fā)invalidate消息,收到invalidate消息的CPU需要將對(duì)應(yīng)緩存行的狀態(tài)改為invalid,即相應(yīng)緩存行就不再持有這份數(shù)據(jù)了,改完之后需要回一個(gè)invalidate acknowledge消息。當(dāng)發(fā)出invalidate消息的CPU收到所有的invalidate acknowledge后就現(xiàn)在這份數(shù)據(jù)有他獨(dú)占了,于是將相應(yīng)緩存行的狀態(tài)改了為E。不難看出由S狀態(tài)轉(zhuǎn)變?yōu)镋狀態(tài)是比較耗時(shí)的,因?yàn)樾枰却蠧PU都回送invalidate acknowledge消息。
  • E to M
狀態(tài)E到狀態(tài)M的轉(zhuǎn)變就非常簡(jiǎn)單了,因?yàn)榫彺嬉呀?jīng)處于E也就是獨(dú)占狀態(tài)了,此時(shí)當(dāng)前CPU就可以修改這個(gè)緩存行的值,也就是前面提到過(guò)的Store操作。Store操作之后緩存行的狀態(tài)就由之前的E變?yōu)榱薓。其實(shí)從MESI的規(guī)定,不難看出,MESI確保了緩存的一致性,即不會(huì)存在共享同一個(gè)數(shù)據(jù)的兩個(gè)緩存行中數(shù)據(jù)值不一致。數(shù)據(jù)在修改之前總是需要等待所有共享了該數(shù)據(jù)的其他緩存行失效。然而對(duì)于CPU來(lái)講,這樣的等待是漫長(zhǎng)且低效的。于是工程師們?yōu)榱颂岣咝蔬M(jìn)行了一些優(yōu)化,而正是這樣的優(yōu)化引入了緩存可見(jiàn)性的問(wèn)題。

Store Buffer


	a=1; b=a+1; assert(b==2); 如上面代碼所示。首先 line2 的加法運(yùn)算要使用到 line1 中的變量a,所以?xún)尚写a是存在數(shù)據(jù)相關(guān)性的,那么編譯器不會(huì)嘗試交換指令順序。我們假設(shè)現(xiàn)在變量 a 在 CPU1 中,變量 b 在 CPU0 中,且初始值均為0。假設(shè)現(xiàn)在 CPU0 要執(zhí)行上述代碼,根據(jù)前面 MESI 的規(guī)定,上述代碼的執(zhí)行順序如下:
  • CPU0 執(zhí)行 a= 1
在執(zhí)行過(guò)程中,發(fā)現(xiàn) a 并不在 CPU0 中,所以需要發(fā)送 read 消息讀取 a 的值。而讀取之后又需要修改 a 的值,就需要發(fā)送 invalidate 消息。這兩個(gè)消息可以用 read invalidate 消息來(lái)代替。CUP1 在收到 read invalidate 消息后會(huì)發(fā)送相應(yīng)緩存行中 a 的值,并且 invalidate 該緩存行,然后發(fā)送 invalidate acknowledge 消息。CPU0需要等待CPU1傳回的a值以及invalidate acknowledge,然后才能修改a的值,最后將對(duì)應(yīng)緩存行的狀態(tài)改為M。
  • CPU0執(zhí)行b=a+1
此時(shí)a,b均在CPU0的中,所以直接執(zhí)行就好。
  • CPU0執(zhí)行assert(b == 2)
顯然此時(shí)b的值一定為2。這個(gè)流程的關(guān)鍵在于 CPU0 需要等待 CPU1 回傳的消息,而前面說(shuō)過(guò)這樣的等待很耗時(shí)。a = 1;這行代碼不難發(fā)現(xiàn),不論 CPU1 回傳給 CPU0 的值是什么,我們會(huì)將 a 的值最終修改為1,那么我們真正需要等待的只是 invalidate acknowledge。那么我們是不是可以先將a = 1;這條指令緩存起來(lái),繼續(xù)執(zhí)行后面的操作,等收到 invalidate acknowledge 之后再來(lái)真正修改 a 的值呢?答案是肯定的,如下圖所示:18f74634-6cb0-11ed-8abf-dac502259ad0.png

Store Buffer的問(wèn)題

在 CPU 和 cache 之前,引入了一個(gè)稱(chēng)為 store buffer 的緩存。現(xiàn)在,我們?cè)趫?zhí)行a=1時(shí),如果需要等待 invalidate acknowledge,那么就先將a=1寫(xiě)入這個(gè)store buffer,然后繼續(xù)執(zhí)行后面的代碼,等到收到 invalidate acknowledge 再將 store buffer 中的值寫(xiě)入緩存。好了,那么現(xiàn)在問(wèn)題來(lái)了。有了store buffer之后,前面代碼就可以是這樣的一種執(zhí)行順序。
  • CPU0 執(zhí)行a= 1
在執(zhí)行過(guò)程中,發(fā)現(xiàn)a并不在CPU0中,所以CPU0向CPU1發(fā)送read invalidate消息。然后將a = 1寫(xiě)入store buffer。繼續(xù)執(zhí)行后面的代碼。
  • CPU0執(zhí)行b=a+1
在執(zhí)行過(guò)程中,CPU0 收到了 CPU1 傳回的 a 值0。CPU0 將 a 的值加載到緩存中,然后執(zhí)行 a+1,于是得到了 b 的值1。此時(shí)CPU0 到了invalidate acknowledged,于是使用 store buffer 中的條目,將 cache 中 a 的值修改為1。然而已經(jīng)沒(méi)有什么卵用了。
  • CPU0執(zhí)行assert(b == 2)
顯然此時(shí) b 的值一定為 1。上述問(wèn)題違反了 CPU 的 self-consistency,即每個(gè)CPU需要保證自身的操作看起來(lái)與代碼順序一致。于是對(duì)于CPU進(jìn)行了改進(jìn),同一個(gè) CPU 的 store 操作可以直接作用于后面的 load 操作。所以 CPU0 在 load a 時(shí)發(fā)現(xiàn) store buffer 中 a 的正確值應(yīng)該是1,于是使用這個(gè)值進(jìn)行后面的運(yùn)算。1907dabc-6cb0-11ed-8abf-dac502259ad0.png這個(gè)改進(jìn)可以解決 CPU 的 self-consistency 問(wèn)題,但是卻解決不了 global memory ordering 問(wèn)題。有如下代碼:

	voidfoo(void) { a=1; b=1; } voidbar(void) { while(b==0) continue; assert(a==1); } 假設(shè),a,b初始值為0。a 在CPU1中且為 exclusive 狀態(tài),b 在 CPU0 中且為 exclusive 狀態(tài),CPU0 執(zhí)行 foo(),CPU1 執(zhí)行 bar()。情況如下:
  • CPU0執(zhí)行 a=1
在執(zhí)行過(guò)程中發(fā)現(xiàn)a不在CPU0的緩存中,于是發(fā)送 read invalidate給 CPU1,然后將 a=1 寫(xiě)入 store buffer。繼續(xù)執(zhí)行。
  • CPU1 執(zhí)行 whie(b == 0)
在執(zhí)行過(guò)程中發(fā)現(xiàn) b 不在 CPU1 的緩存行中,于是發(fā)送 read 給CPU0。
  • CPU0執(zhí)行b=1
由于 b 在 CPU0 中且為獨(dú)占,于是這句話(huà)直接就執(zhí)行成功了。
  • CPU0 收到 CPU1 的 read 消息
于是將b的值1送回給CPU1,并且將緩存行狀態(tài)修改為shared。
  • CPU1 收到 CPU0 的 read ack
于是得知 b 的值為1,從而跳出循環(huán),繼續(xù)向后執(zhí)行。
  • CPU1執(zhí)行assert(a == 1);
注意,此時(shí) CPU1 還未收到 read invalidate 消息。由于 a 在CPU1中依然是獨(dú)占,所以 CPU1 直接從緩存中獲取到 a 的值0。于是 assert 失敗。(注意,a = 1 是存在于 CPU0 的 store buffer 中,而不是 CPU1。)
  • CPU1 收到 CPU0 的read invalidate
CPU1向CPU0傳回a的值0,以及invalid ack。
  • CPU0收到CPU1的值以及invalid ack
CPU0 使用 store buffer 中的條目,將 cache 中 a 的值修改為1。

內(nèi)存屏障

造成上述問(wèn)題的核心是a=1;還沒(méi)有被所有CPU的可見(jiàn)的時(shí)候,b=1;已經(jīng)被所有CPU都可見(jiàn)了。而a=1不可見(jiàn)的原因是 store buffer 中的數(shù)據(jù)還沒(méi)有應(yīng)用到緩存行中。解決這個(gè)問(wèn)題可以有兩種思路:
  • store buffer 中還有數(shù)據(jù)時(shí)暫停執(zhí)行。
  • store buffer中還有數(shù)據(jù)時(shí)把后續(xù)的 store 操作也寫(xiě)入 store buffer。
這里就要用到內(nèi)存屏障了。修改上述代碼如下:

	voidfoo(void) { a=1; smp_mb();//內(nèi)存屏障 b=1; } voidbar(void) { while(b==0)continue; assert(a==1); } 按照思路1,CPU0 執(zhí)行到 line4 時(shí),發(fā)現(xiàn) store buffer 中有 a=1,于是暫停執(zhí)行,直到 store buffer 中的數(shù)據(jù)應(yīng)用到cache中,再繼續(xù)執(zhí)行 b=1。這樣便沒(méi)問(wèn)題了。按照思路2,CPU0 執(zhí)行到 line4 時(shí),發(fā)現(xiàn) store buffer中有 a=1,于是將該條目做一個(gè)標(biāo)記(標(biāo)記store buffer中的所有當(dāng)前條目)。在執(zhí)行b=1時(shí),發(fā)現(xiàn)store buffer中有一個(gè)帶標(biāo)記的條目,于是將b=1也寫(xiě)入store buffer,這樣b=1對(duì)于CPU1也就不可見(jiàn)了。只有當(dāng)代標(biāo)記的條目應(yīng)用于緩存之后,后續(xù)條目才可以應(yīng)用于緩存。這相當(dāng)于只有當(dāng)標(biāo)記條目都應(yīng)用于緩存后,后續(xù)的store操作才能進(jìn)行。通過(guò)這兩種方式就很好的解決了緩存可見(jiàn)性問(wèn)題。仔細(xì)觀(guān)察這個(gè)流程,其實(shí)感覺(jué)有點(diǎn)數(shù)據(jù)庫(kù)事務(wù)的意思,哈哈,技術(shù)果然都是互通的。不難發(fā)現(xiàn),內(nèi)存屏障限制了CPU的執(zhí)行流程,所以同樣會(huì)有一定的性能損失,但是顯然不滿(mǎn)足正確性任何性能都是扯淡。

Invalidate Queue

在使用了內(nèi)存屏障之后,store buffer中就可能堆積很多條目,因?yàn)楸仨毜鹊綆в袠?biāo)記的條目應(yīng)用到緩存行。store buffer的大小也是有限的,當(dāng)store buffer滿(mǎn)了之后便又會(huì)出現(xiàn)前面提到的性能問(wèn)題。所以還有什么優(yōu)化的方式么?MESI 性能問(wèn)題的核心是 Invalidate ack 耗時(shí)太長(zhǎng)。而這個(gè)耗時(shí)長(zhǎng)的原因是,CPU必須確保cache真的被invalidate了才會(huì)發(fā)送 Invalidate ack。而在CPU忙時(shí)顯然會(huì)增加 Invalidate ack 的延遲。那么我們是不是也可以像store buffer那樣把invalidate 消息緩存起來(lái)呢?這個(gè)顯然也是可以的。于是,工程師們又增加了invalidate queue來(lái)緩存 invalidate 消息。192d5968-6cb0-11ed-8abf-dac502259ad0.pngCPU收到invalidate消息后,不用真正等到 cache invalidate,只需要將 invalidate 消息存放到 Invalidatae Queue 中就可以發(fā)送 invalidate ack了。而收到 invalidate ack 的 CPU 就可以將 store buffer 中相應(yīng)的條目應(yīng)用到 cache。

Invalidate Queue的問(wèn)題

前面store buffer的經(jīng)驗(yàn)告訴我們,天下沒(méi)有免費(fèi)的午餐。Invalid Buffer的引入同樣也會(huì)帶來(lái)問(wèn)題。我們?cè)賮?lái)看看前面的代碼:

	voidfoo(void) { a=1; smp_mb();//內(nèi)存屏障 b=1; } voidbar(void) { while(b==0)continue; assert(a==1); } 假設(shè),a,b初始值為0。a在CPU0和CPU1之前共享,狀態(tài)為shared,b在CPU0中且為exclusive狀態(tài),CPU0執(zhí)行foo(),CPU1執(zhí)行bar()。情況如下:
  • CPU0執(zhí)行a=1
在執(zhí)行過(guò)程中發(fā)現(xiàn)a的狀態(tài)為shared,于是發(fā)送invalidate給CPU1,然后將a=1寫(xiě)入store buffer。繼續(xù)執(zhí)行。
  • CPU1執(zhí)行whie(b == 0)
在執(zhí)行過(guò)程中發(fā)現(xiàn)b不在CPU1的緩存行中,于是發(fā)送read給CPU0。
  • CPU1收到invalidate消息
CPU1將invalidate存入invalidate queue,然后立即返回invalidate ack。
  • CPU0收到invalidate ack
CPU0將store buffer中的條目應(yīng)用到cache上,此時(shí)a的值為1。
  • CPU0執(zhí)行b=1;
由于b在CPU0上獨(dú)占,且store buffer為空,所以直接就執(zhí)行成功了。
  • CPU0收到CPU1的read消息
于是將b的值1送回給CPU1,并且將緩存行狀態(tài)修改為shared。
  • CPU1收到CPU0的read ack
于是得知b的值為1,從而跳出循環(huán),繼續(xù)向后執(zhí)行。
  • CPU1執(zhí)行assert(a == 1);
注意,此時(shí)invalidate消息在invalidate queue中,所以CPU1并未對(duì)相應(yīng)緩存執(zhí)行ivalidate操作,所以此時(shí)原始的緩存行對(duì)于CPU1是可見(jiàn)的,于是獲取到了a的原始值0,導(dǎo)致assert失敗。這個(gè)問(wèn)題的核心很簡(jiǎn)單,就是在獲取緩存行的時(shí)候沒(méi)有檢查invalidate queue。解決方法也很簡(jiǎn)單,使用內(nèi)存屏障。

	voidfoo(void) { a=1; smp_mb();//內(nèi)存屏障 b=1; } voidbar(void) { while(b==0)continue; smp_mb();//內(nèi)存屏障 assert(a==1); } 使用內(nèi)存屏障后,會(huì)標(biāo)記store buffer中的所有當(dāng)前條目,只有當(dāng)所有標(biāo)記的條目都應(yīng)用于緩存后,后續(xù)的load操作才能進(jìn)行。
When a given CPU executes a memory barrier, it marks all the entries currently in its invalidate queue, and forces any subsequent load to wait until all marked entries have been applied to the CPU’s cache.
所以在加上內(nèi)存屏障之后,在執(zhí)行 assert(a == 1)之前需要先將invalidate queue中的條目應(yīng)用于緩存行。所以在執(zhí)行a== 1時(shí),CPU1 會(huì)發(fā)現(xiàn) a 不在 CPU1 的緩存,從而給 CPU0 發(fā)送read消息,獲得 a 的值1,最終assert(a == 1); 成功。
其實(shí)在這里內(nèi)存屏障還有一個(gè)非常重要的作用,因?yàn)閍==1并不一定要等 b != 0時(shí)才會(huì)執(zhí)行。這又是為什么?while (b == 0) continue;是一個(gè)條件循環(huán),條件循環(huán)的本質(zhì)是條件分支+無(wú)條件循環(huán)(IF+LOOP)。在執(zhí)行條件分支時(shí),為了更好的利用指令流水,有一種被稱(chēng)作分支預(yù)測(cè)的機(jī)制。所以實(shí)際執(zhí)行的時(shí)候可能會(huì)假定條件分支的值為FALSE,從而提前執(zhí)行 assert(a == 1);關(guān)于while循環(huán)和指令流水可以參見(jiàn)CSAPP的第三、第四章。

三種內(nèi)存屏障

smp_mb(); 會(huì)同時(shí)作用于store buffer和invalidate queue,所以被稱(chēng)為全屏障。在上述代碼中,我們不難發(fā)現(xiàn)一個(gè)問(wèn)題,foo()函數(shù)只會(huì)用到store buffer,而bar()函數(shù)只會(huì)用到invalidate queue。根據(jù)這個(gè)特點(diǎn),除了全屏障之外通常還有讀屏障(smp rmb())和寫(xiě)屏障(smp rmb())。讀屏障只作用于invalidate queue,而寫(xiě)屏障只作用于store buffer。所以上述代碼還可以修改為下面的方式:

	voidfoo(void) { a=1; smp_wmb();//寫(xiě)屏障 b=1; } voidbar(void) { while(b==0)continue; smp_rmb();//讀屏障 assert(a==1); } 

內(nèi)存屏障的使用

什么時(shí)候需要使用內(nèi)存屏障

其實(shí),在我們?nèi)粘5拈_(kāi)發(fā)中,尤其是應(yīng)用研發(fā)。我們根本就用不上內(nèi)存屏障?這是為什么?雖然內(nèi)存屏障用不上,但是在并發(fā)編程里面鎖的概念卻無(wú)處不在!信號(hào)量、臨界區(qū)等等。然而這些技術(shù)的背后都是內(nèi)存屏障。道理其實(shí)很簡(jiǎn)單,種種的線(xiàn)程進(jìn)程同步的手段,實(shí)際上都相當(dāng)于鎖。對(duì)于臨界資源的訪(fǎng)問(wèn),我們總是希望先上鎖,再訪(fǎng)問(wèn)。所以顯然,我們肯定不希望加鎖后的操作由于CPU的種種優(yōu)化跑到了加鎖前去執(zhí)行。那么這種時(shí)候自然就需要使用內(nèi)存屏障。所以,對(duì)于使用了 線(xiàn)程進(jìn)程 同步的手段進(jìn)行加鎖的代碼,不用擔(dān)心內(nèi)存屏障的問(wèn)題。只有為了提高并發(fā)性采用的很多無(wú)鎖設(shè)計(jì),才需要考慮內(nèi)存屏障的問(wèn)題。
當(dāng)然,對(duì)于單線(xiàn)程開(kāi)發(fā)和單核CPU也不用擔(dān)心內(nèi)存屏障的問(wèn)題。
補(bǔ)充:鎖是如何實(shí)現(xiàn)的通常情況下,鎖都是基于一種叫做CAS(compare-and-swap)的操作實(shí)現(xiàn)的。CAS的代碼如下:

	static__inline__int tas(volatileslock_t*lock) { registerslock_t_res=1; __asm____volatile__( "lock " "xchgb%0,%1 " :"+q"(_res),"+m"(*lock) :/*noinputs*/ :"memory","cc"); return(int)_res; } 其中:xchgb 就是實(shí)現(xiàn) CAS 的指令,而在 xchgb 之前有一個(gè) lock 前綴,這個(gè)前綴的作用是鎖總線(xiàn),達(dá)到的效果就是內(nèi)存屏障的效果。這也就是為什么使用了鎖就不用擔(dān)心內(nèi)存屏障的問(wèn)題了。而 JAVA 對(duì)于內(nèi)存屏障的底層實(shí)現(xiàn)其實(shí)就是用的這個(gè)lock。

實(shí)際案例

linux 內(nèi)核的無(wú)鎖隊(duì)列 kfifo 就使用了內(nèi)存屏障。這里主要說(shuō)明__kfifo_put()函數(shù)和__kfifo_get()。__kfifo_put()用于向隊(duì)列中寫(xiě)入數(shù)據(jù),__kfifo_get()用于從隊(duì)列中獲取數(shù)據(jù)。

	/** *__kfifo_put-putssomedataintotheFIFO,nolockingversion *@fifo:thefifotobeused. *@buffer:thedatatobeadded. *@len:thelengthofthedatatobeadded. * *Thisfunctioncopiesatmost@lenbytesfromthe@bufferinto *theFIFOdependingonthefreespace,andreturnsthenumberof *bytescopied. * *Notethatwithonlyoneconcurrentreaderandoneconcurrent *writer,youdon'tneedextralockingtousethesefunctions. */ unsignedint__kfifo_put(structkfifo*fifo, unsignedchar*buffer,unsignedintlen) { unsignedintl; len=min(len,fifo->size-fifo->in+fifo->out); /* *Ensurethatwesamplethefifo->outindex-before-we *startputtingbytesintothekfifo. * line19是讀操作,line30之后是寫(xiě)操作(向隊(duì)列中寫(xiě)數(shù)據(jù)),所以需要使用全屏障(隔離讀和寫(xiě))。 */ smp_mb(); /*firstputthedatastartingfromfifo->intobufferend*/ l=min(len,fifo->size-(fifo->in&(fifo->size-1))); memcpy(fifo->buffer+(fifo->in&(fifo->size-1)),buffer,l); /*thenputtherest(ifany)atthebeginningofthebuffer*/ memcpy(fifo->buffer,buffer+l,len-l); /* *Ensurethatweaddthebytestothekfifo-before- *weupdatethefifo->inindex. * line34是寫(xiě)操作,line44也是寫(xiě)操作,所以使用寫(xiě)屏障(隔離寫(xiě)和寫(xiě))。 */ smp_wmb(); fifo->in+=len; returnlen; } EXPORT_SYMBOL(__kfifo_put); 

	/** *__kfifo_get-getssomedatafromtheFIFO,nolockingversion *@fifo:thefifotobeused. *@buffer:wherethedatamustbecopied. *@len:thesizeofthedestinationbuffer. * *Thisfunctioncopiesatmost@lenbytesfromtheFIFOintothe *@bufferandreturnsthenumberofcopiedbytes. * *Notethatwithonlyoneconcurrentreaderandoneconcurrent *writer,youdon'tneedextralockingtousethesefunctions. */ unsignedint__kfifo_get(structkfifo*fifo, unsignedchar*buffer,unsignedintlen) { unsignedintl; len=min(len,fifo->in-fifo->out); /* *Ensurethatwesamplethefifo->inindex-before-we *startremovingbytesfromthekfifo. * line18讀操作,line29是讀操作(從隊(duì)列中讀數(shù)據(jù)),所以需要使用讀屏障(隔離讀和讀)。 */ smp_rmb(); /*firstgetthedatafromfifo->outuntiltheendofthebuffer*/ l=min(len,fifo->size-(fifo->out&(fifo->size-1))); memcpy(buffer,fifo->buffer+(fifo->out&(fifo->size-1)),l); /*thengettherest(ifany)fromthebeginningofthebuffer*/ memcpy(buffer+l,fifo->buffer,len-l); /* *Ensurethatweremovethebytesfromthekfifo-before- *weupdatethefifo->outindex. * line33是讀操作,line43是寫(xiě)操作,所以需要使用全屏障(隔離讀和寫(xiě))。 */ smp_mb(); fifo->out+=len; returnlen; } EXPORT_SYMBOL(__kfifo_get); kfifo 的詳細(xì)內(nèi)容,請(qǐng)查閱相關(guān)資料,這里不再贅述。

深入理解

我們不難發(fā)現(xiàn),不論是__kfifo_put還是__kfifo_get都使用了兩次內(nèi)存屏障。我們以__kfifo_put為例子來(lái)觀(guān)察下這兩個(gè)內(nèi)存屏障,在__kfifo_put中,第一次使用內(nèi)存屏障是 line27 的 smp_mb 第二次是 line42 的 smp_wmb。現(xiàn)在思考一個(gè)問(wèn)題,這兩個(gè)內(nèi)存屏障可以省略么?為了解決這個(gè)問(wèn)題,我們需要思考,如果省略了內(nèi)存屏障會(huì)有什么問(wèn)題?

省略 smp_mb

  • 省略 smp_mb 會(huì)出現(xiàn)優(yōu)化編譯導(dǎo)致的指令亂序么?
smp_mb 位于 line19 和 line30 之間,如果省略了 smp_mb,在優(yōu)化編譯的情況下 line19 的代碼會(huì)和 lin30 的代碼交換順序么?不會(huì)!因?yàn)檫@兩行代碼有數(shù)據(jù)相關(guān)性,line30 會(huì)使用 line19 計(jì)算出的 len 值。
  • 省略 smp_mb 會(huì)造成緩存可見(jiàn)性問(wèn)題么?
會(huì)!fifo->out由__kfifo_get函數(shù)修改。如果省略smp_mb在執(zhí)行l(wèi)ine30之前,__kfifo_get對(duì)于fifo->out的修改對(duì)于__kfifo_put可能不可見(jiàn)。不可見(jiàn)會(huì)造成什么后果?在__kfifo_get中會(huì)增加fifo->out的長(zhǎng)度,如果這個(gè)增加不可見(jiàn),那么line19的len值就會(huì)小一些(相對(duì)于可見(jiàn)情況),也就是說(shuō)可以put的數(shù)據(jù)就少一些,除此之外并沒(méi)有什么其他后果。kfifo隊(duì)列依然可以正常工作。綜上所述,如果省略smp_mb,會(huì)造成一些性能問(wèn)題,但不會(huì)有正確性問(wèn)題。

省略smp_wmb

  • 省略smp_wmb會(huì)出現(xiàn)優(yōu)化編譯導(dǎo)致的指令亂序么?
smp_wmb位于line34和line44之間,如果省略了smp_wmb,在優(yōu)化編譯的情況下line34的代碼會(huì)和lin44的代碼交換順序么?有可能!因?yàn)檫@兩行代碼沒(méi)有數(shù)據(jù)相關(guān)性,是相互獨(dú)立的代碼。
  • 省略smp_wmb會(huì)造成緩存可見(jiàn)性問(wèn)題么?
會(huì)!line43對(duì)于fifo->out的修改可能比line33的memcpy更早的被其他CPU感知!這就相當(dāng)于,數(shù)據(jù)都還沒(méi)有拷貝進(jìn)去,就告訴別人數(shù)據(jù)已經(jīng)準(zhǔn)備好,你來(lái)取吧!所以如果這個(gè)時(shí)候另一個(gè)CPU運(yùn)行的__kfifo_get函數(shù),不幸的相信了這句鬼話(huà),就會(huì)取出之前的老數(shù)據(jù)。這個(gè)是存在正確性問(wèn)題的!綜上所述,如果省略smp_wmb,會(huì)引起正確性問(wèn)題。

驗(yàn)證

好了,我們可以驗(yàn)證下上面的說(shuō)法。上面闡述的代碼是linux新版本的kfifo。我們可以看看老版本的kfifo是如何實(shí)現(xiàn)的。在linux-3.0.10內(nèi)核代碼中,可以找到老版本的kfifo。其中最重要的兩個(gè)函數(shù)是__kfifo_in(對(duì)應(yīng)__kfifo_put)和__kfifo_out(對(duì)應(yīng)__kfifo_get)。為了方便閱讀,我將__kfifo_in中的函數(shù)調(diào)用直接展開(kāi),如下圖:19412588-6cb0-11ed-8abf-dac502259ad0.png不難發(fā)現(xiàn),老版的 __kfifo_in 就只使用了一個(gè)內(nèi)存屏障,在 memcpy 和修改 fifo->in 之間,這也就是我們之前說(shuō)的那個(gè)不可以省略的 smp_wmb。

	

	
		

審核編輯:湯梓紅


聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀(guān)點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 計(jì)算機(jī)
    +關(guān)注

    關(guān)注

    19

    文章

    7292

    瀏覽量

    87523
  • volatile
    +關(guān)注

    關(guān)注

    0

    文章

    44

    瀏覽量

    12990
  • 內(nèi)存屏障
    +關(guān)注

    關(guān)注

    0

    文章

    3

    瀏覽量

    1732

原文標(biāo)題:【C語(yǔ)言】徹底搞懂內(nèi)存屏障與volatile

文章出處:【微信號(hào):C語(yǔ)言學(xué)習(xí)聯(lián)盟,微信公眾號(hào):C語(yǔ)言學(xué)習(xí)聯(lián)盟】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    從硬件引申出內(nèi)存屏障,帶你深入了解Linux內(nèi)核RCU

    本文從硬件的角度引申出內(nèi)存屏障,這不是內(nèi)存屏障的詳盡手冊(cè),但是相關(guān)知識(shí)對(duì)于理解RCU有所幫助。
    的頭像 發(fā)表于 09-19 11:39 ?6087次閱讀
    從硬件引申出<b class='flag-5'>內(nèi)存</b><b class='flag-5'>屏障</b>,帶你深入了解Linux內(nèi)核RCU

    淺談緩存致性協(xié)議 處理器與內(nèi)存之間交互技術(shù)

    在多線(xiàn)程并發(fā)的世界里synchronized、volatile、JMM是我們繞不過(guò)去的技術(shù)坎,而重排序、可見(jiàn)性、內(nèi)存屏障又有時(shí)候搞得你臉懵逼。
    的頭像 發(fā)表于 10-16 14:39 ?3651次閱讀
    淺談緩存<b class='flag-5'>一</b>致性協(xié)議 處理器與<b class='flag-5'>內(nèi)存</b>之間交互技術(shù)

    ARM體系結(jié)構(gòu)之內(nèi)存序與內(nèi)存屏障

    本文介紹 Armv8-A 架構(gòu)的內(nèi)存序模型,并介紹 arm 的各種內(nèi)存屏障。本文還會(huì)指出些需要明確內(nèi)存保序的場(chǎng)景,并指明如何使用
    發(fā)表于 06-15 18:19 ?1517次閱讀
    ARM體系結(jié)構(gòu)之<b class='flag-5'>內(nèi)存</b>序與<b class='flag-5'>內(nèi)存</b><b class='flag-5'>屏障</b>

    搞懂UPS主要內(nèi)容

    導(dǎo)讀:UPS是系統(tǒng)集成項(xiàng)目中常用到的設(shè)備,也是機(jī)房必備的設(shè)備。本文簡(jiǎn)單介紹了UPS的種類(lèi)、功能、原理,品質(zhì)選擇與配置選擇方式,基礎(chǔ)維護(hù)等相關(guān)的內(nèi)容。搞懂UPS本文主要內(nèi)容:UPS種類(lèi)、功能
    發(fā)表于 09-15 07:49

    導(dǎo)致ARM內(nèi)存屏障的原因究竟有哪些

    與程序員的代碼邏輯不符,導(dǎo)致些錯(cuò)誤的發(fā)生,為了保證內(nèi)存訪(fǎng)問(wèn)的致性,也是保證程序的正確性,使用內(nèi)存屏障來(lái)保證
    發(fā)表于 05-09 09:32

    徹底搞懂C語(yǔ)言指針

    指針是個(gè)特殊的變量,它里面存儲(chǔ)的數(shù)值被解釋為內(nèi)存里面的個(gè)地址想要徹底搞懂它,就必須從計(jì)算機(jī)的底層進(jìn)行解釋?zhuān)@是你的
    發(fā)表于 07-22 14:48

    學(xué)習(xí)下ARM內(nèi)存屏障(memory barrier)指令

    ;等待前面的指令完成后更改系統(tǒng)寄存器。DMB(Data Memory Barrier)指令是內(nèi)存屏障指令,它確保了屏障之前的內(nèi)存訪(fǎng)問(wèn)與之
    發(fā)表于 02-07 14:08

    內(nèi)存屏障是什么

    內(nèi)存屏障,也稱(chēng)內(nèi)存柵欄,內(nèi)存柵障,屏障指令等, 是類(lèi)同步
    發(fā)表于 11-14 09:43 ?6483次閱讀
    <b class='flag-5'>內(nèi)存</b><b class='flag-5'>屏障</b>是什么

    volatile修飾的變量的認(rèn)識(shí)和理解

    ,所有的讀操作都可以看到這個(gè)修改,即便使用了本地緩存也樣,volatile會(huì)被立即寫(xiě)入到主內(nèi)存中,而讀的操作就發(fā)生在主內(nèi)存中。在非volatile
    發(fā)表于 12-01 11:36 ?5669次閱讀
    <b class='flag-5'>volatile</b>修飾的變量的認(rèn)識(shí)和理解

    搞懂幾種常見(jiàn)的射頻電路類(lèi)型及主要指標(biāo)

    搞懂幾種常見(jiàn)的射頻電路類(lèi)型及主要指標(biāo)。
    發(fā)表于 07-27 10:26 ?9次下載
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>搞懂</b>幾種常見(jiàn)的射頻電路類(lèi)型及主要指標(biāo)

    C語(yǔ)言中的關(guān)鍵字volatile到底有什么用呢

    內(nèi)存屏障類(lèi)機(jī)器指令,該指令對(duì)處理器在該屏障指令之前與之后的內(nèi)存操作進(jìn)行了限制,確保不會(huì)出現(xiàn)重排問(wèn)題。而
    的頭像 發(fā)表于 08-19 15:20 ?2564次閱讀
    C語(yǔ)言中的關(guān)鍵字<b class='flag-5'>volatile</b>到底有什么用呢

    Linux內(nèi)核的內(nèi)存屏障的原理和用法分析

    圈里流傳著句話(huà)“珍愛(ài)生命,遠(yuǎn)離屏障”,這足以說(shuō)明內(nèi)存屏障個(gè)相當(dāng)晦澀和難以準(zhǔn)確把握的東西。使用過(guò)弱的
    的頭像 發(fā)表于 09-05 09:13 ?1911次閱讀

    詳解volatile關(guān)鍵字

    volatile 是易變的、不穩(wěn)定的意思。和const樣是種類(lèi)型修飾符,volatile關(guān)鍵字修飾的變量,編譯器對(duì)訪(fǎng)問(wèn)該變量的代碼不再進(jìn)行優(yōu)化,從而可以提供對(duì)特殊地址的穩(wěn)定訪(fǎng)問(wèn)。
    的頭像 發(fā)表于 02-15 11:54 ?1000次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b>詳解<b class='flag-5'>volatile</b>關(guān)鍵字

    volatile的原理

    今天來(lái)了解下面試題:你對(duì) volatile 了解多少。要了解 volatile 關(guān)鍵字,就得從 Java 內(nèi)存模型開(kāi)始。最后到 volatile
    的頭像 發(fā)表于 10-10 16:33 ?337次閱讀
    <b class='flag-5'>volatile</b>的原理

    搞懂DDR內(nèi)存原理

    內(nèi)存(DRAM-RandomAccessMemory)作為當(dāng)代數(shù)字系統(tǒng)最主要的核心部件之,從各種終端設(shè)備到核心層數(shù)據(jù)處理和存儲(chǔ)設(shè)備,從各種消費(fèi)類(lèi)電子設(shè)備到社會(huì)各行業(yè)專(zhuān)用設(shè)備,是各種級(jí)別的CPU進(jìn)行
    的頭像 發(fā)表于 05-09 17:09 ?2011次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>搞懂</b>DDR<b class='flag-5'>內(nèi)存</b>原理