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