CPU緩存冷門知識(shí)講解
由于CPU是核心硬件,相信我們?cè)谶x擇CPU的時(shí)候都會(huì)去關(guān)心CPU參數(shù)方面,而在CPU核心參數(shù)中,我們經(jīng)常會(huì)看到緩存(Cache)這個(gè)參數(shù),那么CPU緩存有什么用?下面就讓小編帶你去看看CPU緩存冷門知識(shí)講解,希望能幫助到大家!
計(jì)算機(jī)組成原理 - CPU 高速緩存
按道理來說,循環(huán)1 花費(fèi)的時(shí)間應(yīng)該是循環(huán)2 的 16 倍左右。但實(shí)際上,循環(huán)1 在我的電腦上運(yùn)行需要 50 毫秒,循環(huán)2 只需要 46 毫秒。相差在 15% 之內(nèi),1 倍都沒有。
這就是 CPU Cache (高速緩存)帶來的效果。
程序執(zhí)行時(shí),CPU 將對(duì)應(yīng)的數(shù)據(jù)從內(nèi)存中讀取出來,加載到 CPU Cache 里。這里注意,CPU 是一小塊一小塊來讀取數(shù)據(jù)的,而不是按照單個(gè)數(shù)組元素來讀取數(shù)據(jù)的。
這一小塊一小塊的數(shù)據(jù),在 CPU Cache 里面,我們把它叫作 Cache Line(緩存塊)。
日常用的 Intel 服務(wù)器或者 PC 中,Cache Line 的大小通常是 64 字節(jié)。
上面的循環(huán)2 里面,每隔 16 個(gè)整型數(shù)計(jì)算一次,16 個(gè)整型數(shù)正好是 64 個(gè)字節(jié)。所以,循環(huán)1 和循環(huán)2,都需要把同樣數(shù)量的 Cache Line 數(shù)據(jù)從內(nèi)存中讀取到 CPU Cache 中,導(dǎo)致兩個(gè)程序花費(fèi)的時(shí)間就差別不大了。
CPU Cache 一般有三層,L1/L2 單核私有,L3 多核共享緩存(這里的 L1-L3 指特定的由 SRAM 組成的物理芯片,不是概念上的緩存)。有了 CPU Cache,內(nèi)存中的指令、數(shù)據(jù),會(huì)被加載到 L1-L3 Cache 中,95% 的情況下,CPU 都只需要訪問 L1-L3 Cache,而無(wú)需訪問內(nèi)存。
Cpu Cache 讀取數(shù)據(jù)
現(xiàn)代 CPU 進(jìn)行數(shù)據(jù)讀取的時(shí)候,無(wú)論數(shù)據(jù)是否已經(jīng)存儲(chǔ)在 Cache 中,CPU 始終會(huì)首先訪問 Cache。只有當(dāng) CPU 在 Cache 中找不到數(shù)據(jù)的時(shí)候,才會(huì)去訪問內(nèi)存,并將讀取到的數(shù)據(jù)寫入 Cache 之中。
那么,Cache 如何根據(jù)內(nèi)存地址定位到數(shù)據(jù)呢?
根據(jù)內(nèi)存地址的低位,計(jì)算在 Cache 中的索引;
判斷有效位,確認(rèn) Cache 中的數(shù)據(jù)是有效的;
對(duì)比內(nèi)存地址的高位,和 Cache 中的組標(biāo)記,確認(rèn) Cache 中的數(shù)據(jù)就是我們要訪問的內(nèi)存數(shù)據(jù),從 Cache Line 中讀取到對(duì)應(yīng)的數(shù)據(jù)塊(Data Block);
根據(jù)內(nèi)存地址的Offset位,從Data Block中,讀取希望讀取到的字
如果在2、3這兩個(gè)步驟中,CPU 發(fā)現(xiàn),Cache 中的數(shù)據(jù)并不是要訪問的內(nèi)存地址的數(shù)據(jù),那 CPU 就會(huì)訪問內(nèi)存,并把對(duì)應(yīng)的 Block Data 更新到 Cache Line 中,同時(shí)更新對(duì)應(yīng)的有效位和組標(biāo)記的數(shù)據(jù)。
總結(jié)一下,一個(gè)內(nèi)存的訪問地址,最終包括高位代表的組標(biāo)記、低位代表的索引,以及在對(duì)應(yīng)的Data Block中定位對(duì)應(yīng)字的位置偏移量。
Cpu Cache 寫入數(shù)據(jù)
當(dāng) Cpu 要寫入數(shù)據(jù)時(shí),到底是寫 Cache 還是寫主存呢,如何保證一致性呢?
這里介紹兩種寫入策略。
1. 寫直達(dá)(Write-Through)
寫入前,我們會(huì)先去判斷數(shù)據(jù)是否已經(jīng)在 Cache 里面了。如果數(shù)據(jù)已經(jīng)在 Cache 里面了,我們先把數(shù)據(jù)寫入更新到 Cache 里面,再寫入到主內(nèi)存里面;如果數(shù)據(jù)不在 Cache 里,我們就只更新主內(nèi)存。
這個(gè)策略很直觀,但是問題也很明顯,那就是這個(gè)策略很慢。無(wú)論數(shù)據(jù)是不是在 Cache 里面,我們都需要把數(shù)據(jù)寫到主內(nèi)存里面。這個(gè)方式就有點(diǎn)兒像 Java 里 volatile 關(guān)鍵字,始終都要把數(shù)據(jù)同步到主內(nèi)存里面。
2. 寫回(Write-Back)
如果要寫入的數(shù)據(jù),就在 CPU Cache 里面,那么就只更新 CPU Cache 里面的數(shù)據(jù)。同時(shí)標(biāo)記 CPU Cache 里的這個(gè) Block 是臟(Dirty)的。就是這個(gè)時(shí)候,CPU Cache 里的這個(gè) Block 的數(shù)據(jù),和主內(nèi)存是不一致的。
如果要寫入的數(shù)據(jù)所對(duì)應(yīng)的 Cache Block 里,放的是別的內(nèi)存地址的數(shù)據(jù),那么就要看一看,Cache Block 里的數(shù)據(jù)有沒有被標(biāo)記成臟的。如果是臟的,要先把這個(gè) Cache Block 里面的數(shù)據(jù),寫入到主內(nèi)存里面。然后,再把當(dāng)前要寫入的數(shù)據(jù),寫入到 Cache 里,同時(shí)把 Cache Block 標(biāo)記成臟的。如果 Block 里面的數(shù)據(jù)沒有被標(biāo)記成臟的,那么我們直接把數(shù)據(jù)寫入到 Cache 里面,然后再把 Cache Block 標(biāo)記成臟的就好了。
然而,無(wú)論是寫回還是寫直達(dá),都沒有解決多線程,或者是多個(gè) CPU 核的緩存一致性的問題。
這也就是我們?cè)趯懭胄薷木彺婧螅枰鉀Q的第二個(gè)問題。
要解決這個(gè)問題,我們需要引入一個(gè)新的方法,叫作 MESI 協(xié)議。這是一個(gè)維護(hù)緩存一致性協(xié)議。這個(gè)協(xié)議不僅可以用在 CPU Cache 之間,也可以廣泛用于各種需要使用緩存,同時(shí)緩存之間需要同步的場(chǎng)景下。
多核 CPU Cache 緩存一致性
因?yàn)槎嗪?CPU Cache 在 L3 緩存是共享的,所以一致性問題,只會(huì)出現(xiàn)在 L1/L2 級(jí)這種單核私有緩存的場(chǎng)景中。
我們需要有一種機(jī)制,來同步兩個(gè)不同核心里面的緩存數(shù)據(jù)。這樣的機(jī)制需要滿足兩點(diǎn):
第一點(diǎn)叫寫傳播(Write Propagation)。寫傳播是說,在一個(gè)CPU核心里,我們的Cache數(shù)據(jù)更新,必須能夠傳播到其他的對(duì)應(yīng)節(jié)點(diǎn)的Cache Line里。
第二點(diǎn)叫事務(wù)的串行化(Transaction Serialization),事務(wù)串行化是說,我們?cè)谝粋€(gè)CPU核心里面的讀取和寫入,在其他的節(jié)點(diǎn)看起來,順序是一樣的。
CPU Cache 里做到事務(wù)串行化,需要做到兩點(diǎn),第一點(diǎn)是一個(gè) CPU 核心對(duì)于數(shù)據(jù)的操作,需要同步通信給到其他 CPU 核心。第二點(diǎn)是,如果兩個(gè) CPU 核心里有同一個(gè)數(shù)據(jù)的 Cache,那么對(duì)于這個(gè) Cache 數(shù)據(jù)的更新,需要有一個(gè)“鎖”的概念。只有拿到了對(duì)應(yīng) Cache Block 的“鎖”之后,才能進(jìn)行對(duì)應(yīng)的數(shù)據(jù)更新。接下來,我們就看看實(shí)現(xiàn)了這兩個(gè)機(jī)制的 MESI 協(xié)議。
MESI 協(xié)議,是一種叫作寫失效(Write Invalidate)的協(xié)議。在寫失效協(xié)議里,只有一個(gè) CPU 核心負(fù)責(zé)寫入數(shù)據(jù),其他的核心,只是同步讀取到這個(gè)寫入。在這個(gè) CPU 核心寫入 Cache 之后,它會(huì)去廣播一個(gè)“失效”請(qǐng)求告訴所有其他的 CPU 核心。其他的 CPU 核心,只是去判斷自己是否也有一個(gè)“失效”版本的 Cache Block,然后把這個(gè)也標(biāo)記成失效的就好了。
MESI協(xié)議的由來呢,來自于我們對(duì) Cache Line 的四個(gè)不同的標(biāo)記,分別是:
M:代表已修改(Modified) E:代表獨(dú)占(E__clusive) S:代表共享(Shared) I:代表已失效(Invalidated)
我們先來看看“已修改”和“已失效”,這兩個(gè)狀態(tài)比較容易理解。所謂的“已修改”,就是我們上一講所說的“臟”的 Cache Block。Cache Block 里面的內(nèi)容我們已經(jīng)更新過了,但是還沒有寫回到主內(nèi)存里面。而所謂的“已失效“,自然是這個(gè) Cache Block 里面的數(shù)據(jù)已經(jīng)失效了,我們不可以相信這個(gè) Cache Block 里面的數(shù)據(jù)。
然后,我們?cè)賮砜础蔼?dú)占”和“共享”這兩個(gè)狀態(tài)。這就是 MESI 協(xié)議的精華所在了。無(wú)論是獨(dú)占狀態(tài)還是共享狀態(tài),緩存里面的數(shù)據(jù)都是“干凈”的。這個(gè)“干凈”,自然對(duì)應(yīng)的是前面所說的“臟”的,也就是說,這個(gè)時(shí)候,Cache Block 里面的數(shù)據(jù)和主內(nèi)存里面的數(shù)據(jù)是一致的。
那么“獨(dú)占”和“共享”這兩個(gè)狀態(tài)的差別在哪里呢?這個(gè)差別就在于,在獨(dú)占狀態(tài)下,對(duì)應(yīng)的 Cache Line 只加載到了當(dāng)前 CPU 核所擁有的 Cache 里。其他的 CPU 核,并沒有加載對(duì)應(yīng)的數(shù)據(jù)到自己的 Cache 里。這個(gè)時(shí)候,如果要向獨(dú)占的 Cache Block 寫入數(shù)據(jù),我們可以自由地寫入數(shù)據(jù),而不需要告知其他 CPU 核。
在獨(dú)占狀態(tài)下的數(shù)據(jù),如果收到了一個(gè)來自于總線的讀取對(duì)應(yīng)緩存的請(qǐng)求,它就會(huì)變成共享狀態(tài)。這個(gè)共享狀態(tài)是因?yàn)?,這個(gè)時(shí)候,另外一個(gè) CPU 核心,也把對(duì)應(yīng)的 Cache Block,從內(nèi)存里面加載到了自己的 Cache 里來。
而在共享狀態(tài)下,因?yàn)橥瑯拥臄?shù)據(jù)在多個(gè) CPU 核心的 Cache 里都有。所以,當(dāng)我們想要更新 Cache 里面的數(shù)據(jù)的時(shí)候,不能直接修改,而是要先向所有的其他 CPU 核心廣播一個(gè)請(qǐng)求,要求先把其他 CPU 核心里面的 Cache,都變成無(wú)效的狀態(tài),然后再更新當(dāng)前 Cache 里面的數(shù)據(jù)。這個(gè)廣播操作,一般叫作 RFO(Request For Ownership),也就是獲取當(dāng)前對(duì)應(yīng) Cache Block 數(shù)據(jù)的所有權(quán)。
有沒有覺得這個(gè)操作有點(diǎn)兒像我們?cè)诙嗑€程里面用到的讀寫鎖。在共享狀態(tài)下,大家都可以并行去讀對(duì)應(yīng)的數(shù)據(jù)。但是如果要寫,我們就需要通過一個(gè)鎖,獲取當(dāng)前寫入位置的所有權(quán)。
整個(gè) MESI 的狀態(tài),可以用一個(gè)有限狀態(tài)機(jī)來表示它的狀態(tài)流轉(zhuǎn)。需要注意的是,對(duì)于不同狀態(tài)觸發(fā)的事件操作,可能來自于當(dāng)前 CPU 核心,也可能來自總線里其他 CPU 核心廣播出來的信號(hào)。我把對(duì)應(yīng)的狀態(tài)機(jī)流轉(zhuǎn)圖放在了下面,你可以對(duì)照著 Wikipedia 里面 MESI 的內(nèi)容,仔細(xì)研讀一下。
為什么CPU有多層緩存
緩存的故事
假設(shè)你是一位六十年代的白領(lǐng),在巨大的辦公樓里工作,沒有電腦,你需要閱讀大量的文件并且交叉檢索這些文檔。
你有一個(gè)辦公桌(L1 緩存)。桌上的文件是你正在手頭處理的資料,還有一些是你最近看過的或者你準(zhǔn)備閱讀的。通常我們需要閱讀文件的每一頁(yè)(對(duì)應(yīng)于存儲(chǔ)單元的一個(gè)字節(jié)),但除非它們?cè)谵k公桌上,文件都是作為一個(gè)整體。即只想看某一頁(yè)的內(nèi)容,我們也必須把整份文件抓過來。
辦公室里還有文件柜(L2 緩存)。這些文件柜里存放的是你最近處理過,但目前沒有在使用的文件。辦公桌上的文件在用完后,通常也會(huì)放回文件柜。從文件柜里拿文件就不是順手拈來了——你需要走過去,打開相應(yīng)的抽屜,還要查目錄卡片,才能找到想要的文件——不過這也還比較快了。
有些時(shí)候,其他人也需要查看你的文件柜里的文件。勤雜工會(huì)推著一輛推車(環(huán)路公共汽車)在各個(gè)辦公室轉(zhuǎn)。如果有人在自己的文件柜沒有找到相應(yīng)的資料,他會(huì)寫一個(gè)紙條交給勤雜工。為簡(jiǎn)化起見,假設(shè)這位勤雜工知道所有的東西放哪兒。所以當(dāng)他來到你的辦公室的時(shí)候,他會(huì)檢查你的文件柜里是否有其他人需要的文件,如果有,就把這些文件抽出來放到車上。當(dāng)他轉(zhuǎn)到別的辦公室,就會(huì)把找到的文件放在文件柜里,并留下收條。
有時(shí)候,這些文件并不在文件柜里,而是在辦公桌上。那就不可以直接拿了,需要征詢主人的意見,如果不行,大家就要商量如何協(xié)調(diào)。有大量冗長(zhǎng)的詳盡的合作指引來處理這類情況(至少要一起開會(huì))。
文件柜經(jīng)常會(huì)滿,這時(shí)就不能放新的文件,需要先騰地方,把一些很久都沒用到的文件拿出來。勤雜工會(huì)把這些文件放到地下室里(L3 緩存)。地下室里的文件被密集地堆放到紙箱里或者文件架上,交給文檔管理員處理,其他人都不會(huì)下去,也不會(huì)了解文檔的存放細(xì)節(jié)。
當(dāng)勤雜工來到地下室,會(huì)把一堆不需要的文件放到‘in’框里,同時(shí)他也會(huì)留下一堆紙條,寫著在樓上文件柜里找不到的文件名。文檔管理員會(huì)拿著這些紙條,找到對(duì)應(yīng)的文件,把它們放到‘out’ 框里。下次勤雜工下來的時(shí)候,就可以把‘out’框里的文件拿走,交給需要的人。
現(xiàn)在的問題是,文件還是太多,地下室也放不下,而且辦公大樓的租金都很貴,所以通常公司都會(huì)在離市區(qū)較遠(yuǎn)的地方租一個(gè)倉(cāng)庫(kù)來存放歸檔文件(對(duì)應(yīng)于DRAM內(nèi)存)。文檔管理員會(huì)記錄哪些文件放在地下室,哪些文件放在倉(cāng)庫(kù)。這樣,當(dāng)需要拿文件時(shí),管理員就知道哪些是能在地下室找到,哪些要到倉(cāng)庫(kù)里拿。每天有一兩次,會(huì)有一輛貨車開到倉(cāng)庫(kù)去拿需要的文件,同時(shí)把一些地下室的舊文件運(yùn)過去。
對(duì)勤雜工而言,他并不關(guān)心這些細(xì)節(jié),這些都是文檔管理員在處理。他需要做的就是把紙條放到‘in’框里,從‘out’框里取出文件。
回到最初的問題
那么,這個(gè)類比的意義何在?簡(jiǎn)短而言,一個(gè)具體的模型比模糊的概念更能清晰地闡明物流的意義。實(shí)際上,物流對(duì)設(shè)計(jì)芯片的意義和運(yùn)作一個(gè)高效的辦公文件處理系統(tǒng)是一樣的。
最初的問題是‘為什么不用一個(gè)大的緩存,而是用幾層小的緩存?’。 也就是說,如果一個(gè)4核芯片配置32K一級(jí)緩存,256K二級(jí)緩存和2M三級(jí)緩存,為什么不能用一個(gè)3M的大緩存?
在類比里,類似于問與其給4個(gè)人每人分一個(gè)1.5米寬的辦公桌,為什么不給這4個(gè)人一個(gè)150米寬的大辦公桌?
關(guān)鍵在于,放辦公桌上的目的就是要能觸手可及。如果辦公桌太大,就沒有意義了。難道還需要走50米去拿文件? 對(duì)一級(jí)緩存也是同理,如果太大,存取速度會(huì)變慢,而且會(huì)消耗更多的電力。所以一級(jí)緩存既要足夠大到能起作用,又要小到能夠快速存取。
另外一點(diǎn),一級(jí)緩存處理的存取類型和其他緩存不同。有L1的數(shù)據(jù)緩存,也有L1指令緩存。Intel的CPU還有另外的指令緩存,uOp緩存,既叫L1并發(fā)指令緩存也叫L0指令緩存。
L1數(shù)據(jù)緩存通常只處理1到8個(gè)字節(jié)的數(shù)據(jù),但高層級(jí)的緩存并不處理單獨(dú)的字節(jié)。在我們的類比里,所有不辦公桌上的資料都是以文件為單位(對(duì)應(yīng)于catch line)。 在內(nèi)存中也一樣,高層級(jí)緩存通常是批發(fā)處理數(shù)據(jù),以緩存行為單位(cache line)。
L1指令緩存和數(shù)據(jù)緩存完全不同,就內(nèi)核而言,它是只讀的。(指令內(nèi)存的寫入通常是用非直接的方式,先把指令放入高層的緩存,再載入一級(jí)指令緩存)。由于這個(gè)原因,指令緩存和數(shù)據(jù)緩存通常是分隔的。使用通用的L1緩存意味著把互相沖突的設(shè)計(jì)原則糅合在一起,妥協(xié)的結(jié)果就是任何一個(gè)目的都達(dá)不到。而且用通用的L1緩存處理指令和數(shù)據(jù)負(fù)載也會(huì)很大。
另外,作為程序員,我們通常不關(guān)心內(nèi)存帶寬。例如,每個(gè)時(shí)鐘周期,i7的CPU的內(nèi)核能從L1緩存中讀取16字節(jié)的指令,而且會(huì)不斷地循環(huán)讀取。如果是3GHZ,每個(gè)核可以讀50GB指令/秒。實(shí)際上,通常L1指令緩存的能力都足夠大,很少需要L2緩存參與處理。但如果是通用緩存,就需要預(yù)估指令和數(shù)據(jù)的高并發(fā)情況。(想象一下在L1緩存中用memcopy拷貝幾K數(shù)據(jù)的情況)
順便提一句,如果都在L1緩存,CPU能在一個(gè)時(shí)鐘周期完成許多存取操作?!瓾aswell’或者之后的3GHZ的i7內(nèi)核可以處理超過300GB的指令和數(shù)據(jù), 如果搭配合理的話。這樣的處理能力綽綽有余,但你仍然需要考慮數(shù)據(jù)和指令同時(shí)出現(xiàn)峰值的情況。
L1緩存在設(shè)計(jì)上就是越快越好,以應(yīng)對(duì)峰值情況。只有L1緩存處理不了,才會(huì)轉(zhuǎn)給更高層的緩存,但速度會(huì)降低。因?yàn)楦邔泳彺娓P(guān)心電力效率和存儲(chǔ)密度。
第三點(diǎn),共享。在上面的比喻中,獨(dú)立辦公桌,亦或L1緩存是私有的,如果在你的辦公桌上,你只管拿就好了,不需要詢問其他人。
這很關(guān)鍵。如果4個(gè)人共享一個(gè)大辦公桌,你就不能隨便拿文件,因?yàn)榱硗馊齻€(gè)人可能正在使用。(即使他們只是在閱讀其他文件時(shí)順便參考一下你想使用的文件)。任何時(shí)候,你想要拿什么東西,你需要先叫一聲,‘有人在用嗎?’如果別人在你前面,你就必須等待。或者需要一個(gè)排隊(duì)系統(tǒng),當(dāng)存在資源沖突的時(shí)候,每個(gè)人需要拿張票排隊(duì)等候,或者其它的什么機(jī)制,具體實(shí)現(xiàn)細(xì)節(jié)并不重要,但是所有的事情你都需要和其他人協(xié)調(diào)。
對(duì)多核共享緩存的情況也是這樣。你不能在不通知?jiǎng)e人的情況下隨意動(dòng)那些數(shù)據(jù),所有對(duì)共享緩存的操作都必須協(xié)調(diào)進(jìn)行。
這就是為什么我們要使用私有的L1緩存。L1緩存就是你的辦公桌,你可以隨便使用桌上的文件。L2緩存處理大部分的協(xié)同操作。大部分時(shí)間,工作者(CPU內(nèi)核)坐在辦公桌前,勤雜工會(huì)走過來,把需求列表拿走,同時(shí)把之前你想找的文件放倒文件柜里。整個(gè)過程不會(huì)打斷你的工作(CPU)。
僅僅當(dāng)你和勤雜工都需要拿文件柜里的同一份文件,或者別人想用你辦公桌上的文件,這時(shí)就需要停下手頭工作,進(jìn)行交談。
簡(jiǎn)單而言,L1緩存的工作就是優(yōu)先為CPU內(nèi)核服務(wù)。因?yàn)槭撬接械?,所以基本不需要協(xié)調(diào)工作。L2緩存也是私有的,但是它的工作重心還包括在不打擾內(nèi)核工作的情況下,處理大量的緩存間的數(shù)據(jù)通信。
L3緩存是共享資源,需要全局協(xié)調(diào)。在上面的類比中,工作者只有從勤雜工的推車?yán)锬玫轿募@就是一個(gè)阻塞點(diǎn)。我們只能希望L1和L2緩存足夠大以便這類阻塞點(diǎn)不會(huì)成為性能瓶頸。
附加說明
本篇文章涵蓋了當(dāng)前臺(tái)式機(jī)(筆記本)CPU的緩存架構(gòu):分開的L1/L1 D 緩存,每核統(tǒng)一的L2緩存,共享的L3緩存。
不是每個(gè)系統(tǒng)都象這樣。一些系統(tǒng)并不區(qū)分指令緩存和數(shù)據(jù)緩存;另外一些則把指令和數(shù)據(jù)在所有的緩存級(jí)全部分開。很多L2緩存是多核共享的,L2緩存就象是連接多個(gè)內(nèi)核的公共汽車。還有一些系統(tǒng)有L3和L4緩存。我也沒有提到使用多CPU套接字的系統(tǒng)。
我提到環(huán)路公共汽車是因?yàn)檫@是一個(gè)很好的類比。環(huán)路公共汽車很常見。有些時(shí)候,環(huán)路汽車是個(gè)麻煩(尤其是只需要把兩三個(gè)街區(qū)連起來);有時(shí)候,環(huán)路公共汽車需要和交叉系統(tǒng)連接起來(就象在辦公室,每個(gè)樓層用推車,不同樓層則用電梯)。
作為軟件工程師,我們自然而然地會(huì)假設(shè)模塊A和模塊B可以魔術(shù)般地連接,數(shù)據(jù)則可隨意地從一端流向另一端。內(nèi)存的實(shí)際工作機(jī)制其實(shí)非常復(fù)雜,但抽象出來呈現(xiàn)給程序員的只是一組大平面的字節(jié)排列。
硬件并不象這樣工作。各個(gè)部件之間并不能魔法般地自動(dòng)連接。模塊A和模塊B并非抽象概念,而是具體的物理設(shè)備,實(shí)際上可以看作是非常小的機(jī)器,在硅片上占有實(shí)際的物理空間。芯片都有平面圖,是真正的2D地圖。如果你想連接A和B,就需要一條實(shí)際的導(dǎo)線來連接。 導(dǎo)線也占空間,而且需要消耗電力(越遠(yuǎn)消耗越多)。用一大捆線連接A和B意味著物理上這一塊區(qū)域會(huì)阻礙其他區(qū)域的連接。(當(dāng)然,芯片可以使用多層連接,如果你有興趣,可以搜索‘routing congestion’)。 在芯片里移動(dòng)數(shù)據(jù)實(shí)際上是一個(gè)物流問題,并且超級(jí)復(fù)雜。
所以盡管辦公室的故事只是半開玩笑的類比,‘誰(shuí)需要和誰(shuí)談’、‘這個(gè)系統(tǒng)的幾何構(gòu)造如何——是有意義的布局嗎?’這些問題其實(shí)對(duì)系統(tǒng)設(shè)計(jì)和硬件相關(guān)并有巨大的影響。 利用空間的隱喻來概念化實(shí)際情況是十分有效的。
Intel CPU漏洞技術(shù)解讀:都是緩存惹的禍!
原因一切還是要從CPU指令執(zhí)行的框架——流水線說起。Intel當(dāng)然不至于明知你要用一個(gè)用戶態(tài)的進(jìn)程讀取Kernel內(nèi)存還會(huì)給你許可。但現(xiàn)代CPU流水線的設(shè)計(jì),尤其是和性能優(yōu)化相關(guān)的流水線的特性,讓這一切充滿了變數(shù)。
給所有還沒有看過云杉網(wǎng)絡(luò)連載的系列文章《__86高性能編程箋注系列》的讀者一點(diǎn)背景知識(shí)的介紹:
__86 CPU為了優(yōu)化性能,在處理器架構(gòu)方面做了很多努力。諸如“多級(jí)緩存”這一類的特性,是大家都比較熟悉的概念。還有一些特性,比如分支預(yù)測(cè)和亂序執(zhí)行,也都是一些可以從并行性等方面有效提升程序性能的特性,并且它們也都是組成流水線的幾個(gè)關(guān)鍵環(huán)節(jié)。即便你暫時(shí)還不能準(zhǔn)確理解其含義,但望文生義,也能看出來這肯定是兩個(gè)熵增的過程。熵增帶來無(wú)序,無(wú)序就會(huì)帶來更多漏洞。
緩存的困境講緩存,必然先掛一張memory hierarchy鎮(zhèn)樓:
不過我要說的和這個(gè)沒太大關(guān)系。現(xiàn)在需要考慮的是,如果能讀取到內(nèi)核地址的內(nèi)容,那這部分內(nèi)容最終肯定是跑到緩存中去了,因?yàn)檎嬲苯雍虲PU核心交互的存儲(chǔ)器,就是緩存。這對(duì)一級(jí)緩存(L1 Cache,業(yè)內(nèi)也常用縮寫L1$,取cash之音)提出的要求就是,必須要非??欤ㄓ腥绱瞬拍芨螩PU處理核心的速度。
Side Notes: 為什么在不考慮成本的情況下緩存不是越大越好,也是因?yàn)楫?dāng)緩存規(guī)模越大,查找某一特定數(shù)據(jù)就會(huì)越慢。而緩存首先要滿足的要求就是快,其他的都是次要的。
根據(jù)內(nèi)核的基本知識(shí)我們知道,進(jìn)程運(yùn)行時(shí)都有一個(gè)虛擬地址「Virtual address」和其所對(duì)應(yīng)的物理地址「physical address」。
從虛擬地址到物理地址的翻譯轉(zhuǎn)換也由CPU通過page table完成。Page table并不儲(chǔ)存在CPU里,但近期查找到的Page table entry「PTE」都像數(shù)據(jù)一樣,緩存在了CPU中的translation lookaside buffer「TLB」里。為了不再過多堆砌術(shù)語(yǔ)和名詞,畫張圖說明一下:
當(dāng)CPU根據(jù)程序要求需要讀取某個(gè)地址上的數(shù)據(jù)時(shí),首先會(huì)在L1 Cache中查找。為了適應(yīng)CPU的速度,L1緩存實(shí)現(xiàn)為Virtually inde__ed physically tagged「VIPT」的形式,即用虛擬地址即可直接讀取該虛擬地址對(duì)應(yīng)的物理地址的內(nèi)容,而不再需要多加一道轉(zhuǎn)換的工序。
如果L1 Cache miss,則會(huì)在下級(jí)緩存中查找。但越過L1 Cache之后,對(duì)L2$和L3$的速度要求就不再這么嚴(yán)苛。此時(shí)CPU core給出的虛擬地址請(qǐng)求會(huì)先通過TLB轉(zhuǎn)換為物理地址,再送入下級(jí)緩存中查找。而檢查進(jìn)程有沒有權(quán)限讀取某一地址這一過程,僅在地址轉(zhuǎn)換的時(shí)候發(fā)生,而這種轉(zhuǎn)換和檢查是需要時(shí)間的,所以有意地安排在了L1 Cache之后。
L1緩存這種必須求“快”的特性,成了整個(gè)事件的楔子。
分支預(yù)測(cè)分支預(yù)測(cè)是一種提高流水線執(zhí)行效率的手段。在遇到if..else..這種程序執(zhí)行的分支時(shí),可以通過以往的歷史記錄判斷哪一分支是最可能被執(zhí)行的分支,并在分支判斷條件真正返回判斷結(jié)果之前提前執(zhí)行分支的代碼。詳情可以在上面提到的連載文章中閱讀。
需要強(qiáng)調(diào)的是,提前執(zhí)行的分支代碼,即便事后證明不是正確的分支,其執(zhí)行過程中所讀取的數(shù)據(jù)也可以進(jìn)入L1緩存。在Intel的官網(wǎng)文檔《Intel? 64 and IA-32 Architectures Optimization Reference Manual》第2.3.5.2節(jié)中指:
L1 DCache Loads:
- Be carried out speculatively, before preceding branches are resolved.
- Take cache misses out of order and in an overlapped manner.
Show you the [偽] code:
if (likely(A < B)) { value = __(kernel_address_pointer);}
當(dāng)分支判斷條件A < B被預(yù)測(cè)為真時(shí),CPU會(huì)去提前執(zhí)行對(duì)內(nèi)核地址的讀取。當(dāng)實(shí)際條件為A > B時(shí),雖然內(nèi)核的值不會(huì)真正寫入寄存器(沒有retire),但會(huì)存入L1 Cache,再加之上一節(jié)介紹的,獲取L1 Cache的值毋須地址轉(zhuǎn)換,毋須權(quán)限檢查,這就為內(nèi)核信息的泄漏創(chuàng)造了可能。
從理論上來講,如果可以控制程序的分支判斷,并且可以獲取L1緩存中的數(shù)據(jù)(這個(gè)沒有直接方法,但可以通過其他間接手法)的話,就完全可以獲取內(nèi)核信息。而分支預(yù)測(cè)這種特性是不能隨隨便便就關(guān)閉的,這也就是這次問題會(huì)如此棘手的原因。
亂序執(zhí)行還有一個(gè)原因是亂序執(zhí)行,但原理大致類似。亂序執(zhí)行是Intel在1995年首次引入Pentium Pro處理器的機(jī)制。其過程首先是將我們?cè)趨R編代碼中看到的指令“打散”,成為更細(xì)粒度的微指令「micro-operations」,更小的指令粒度將會(huì)帶來更多的亂序排列的組合,CPU真正執(zhí)行的是這些微指令。
沒有數(shù)據(jù)依賴的微指令在有相應(yīng)執(zhí)行資源的情況下亂序并行執(zhí)行,進(jìn)而提升程序的并行程度,提高程序性能。但引入的問題是,讀取內(nèi)核數(shù)據(jù)的微指令可能會(huì)在流水線發(fā)出e__ception之前將內(nèi)核數(shù)據(jù)寫入L1 Cache。與分支選擇一樣,為通過用戶態(tài)進(jìn)程獲取內(nèi)核代碼提供了可能。
限于篇幅,更詳細(xì)的內(nèi)容讀者可以在國(guó)外安全團(tuán)隊(duì)發(fā)布的消息中獲取。
后續(xù)剛剛查閱之前連載中的一些細(xì)節(jié)的時(shí)候,看到在“流水線”那一章里寫過這樣一段話:
在面對(duì)問題的時(shí)候,人總是會(huì)傾向于引入一個(gè)更復(fù)雜的機(jī)制來解決問題,多級(jí)流水線就是一個(gè)例子。復(fù)雜可以反映出技術(shù)的改良,但“復(fù)雜”本身就是一個(gè)新的問題。這也許就是矛盾永遠(yuǎn)不會(huì)消失,技術(shù)也不會(huì)停止進(jìn)步的原因。但“為學(xué)日益,為道日損”,愈發(fā)復(fù)雜的機(jī)制總會(huì)在某個(gè)時(shí)機(jī)之下發(fā)生大破大立,但可能現(xiàn)在時(shí)機(jī)還沒有到來:D
很難講現(xiàn)在是不是就是所謂的那個(gè)“時(shí)機(jī)”。雖然對(duì)整個(gè)行業(yè)都產(chǎn)生了負(fù)面影響,但我對(duì)此仍保持樂觀。因?yàn)檫@就是事物自然發(fā)展的一個(gè)正常過程。性能損失并不是一件壞事,尤其是對(duì)牙膏廠的用戶來說。
CPU緩存冷門知識(shí)講解相關(guān)文章:
CPU緩存冷門知識(shí)講解
上一篇:CPU漏洞的成因和預(yù)防
下一篇:CPU緩存用處意義解釋