昨天讀了 Baron 大佬寫的介紹 Cache 細節(jié)的文檔,天哪,太詳細了,簡直面面俱到~ 大佬就是大佬。
看完不禁想起我在 CSDN 博客上公開發(fā)表的第一篇文章,關于 Cache ?何時需要對作廢、何時需要刷新的分析說明,原文寫于 2016 年,忍不住在這里分享一下,比較簡單,希望對 Cache 操作不了解的朋友有些幫助。
Baron 寫了很多安全方面的文章,自謙是非著名的Trustzone/TEE/安全渣渣,研究方向包括 ARM Trustzone、TEE, 各種 Linux 和 Android 安全, CSDN 博客地址: https://blog.csdn.net/weixin_42135087
1. 什么是 Cache?
高速緩存(Cache)主要是為了解決CPU運算速度與內存(Memory)讀寫速度不匹配的矛盾而存在, 是CPU與內存之間的臨時存貯器,容量小,但是交換速度比內存快。
百度百科是這樣介紹緩存的:
CPU要讀取一個數(shù)據(jù)時,首先從Cache中查找,如果找到就立即讀取并送給CPU處理;如果沒有找到,就用相對慢的速度從內存中讀取并送給CPU處理,同時把這個數(shù)據(jù)所在的數(shù)據(jù)塊調入Cache中,可以使得以后對整塊數(shù)據(jù)的讀取都從Cache中進行,不必再調用內存。
正是這樣的讀取機制使CPU讀取Cache的命中率非常高(大多數(shù)CPU可達90%左右),也就是說CPU下一次要讀取的數(shù)據(jù)90%都在Cache中,只有大約10%需要從內存讀取。這大大節(jié)省了CPU直接讀取內存的時間,也使CPU讀取數(shù)據(jù)時基本無需等待。總的來說,CPU讀取數(shù)據(jù)的順序是先Cache后內存。
2. Cache 的分類
Cache 的硬件實現(xiàn)中通常包含一級 Cache(L1 Cache),二級 Cache(L2 Cache)甚至多級 Cache;
對于一級Cache,又有 Instruction Cache(指令緩存,通常稱為 I-Cache)和 Data Cache(數(shù)據(jù)緩存,通常稱為 D-Cache)之分,本文準備不討論各級 Cache 的區(qū)別以及 I-Cache 和 D-Cache 的細節(jié),僅將這些所有實現(xiàn)籠統(tǒng)稱為Cache。
本文僅針對 Cache 的讀寫進行簡單說明并通過示意圖演示什么時候需要寫回(flush)緩存,什么時候需要作廢(Invalidate)緩存。
目前我所知的非常強的一款 CPU: AMD RYZEN 3970x (線程撕裂者) 32 核心 64 線程,其一級緩存 3MB, 二級緩存 16MB, 三級緩存 128MB, 有朋友用這顆芯片配置了一臺個人電腦,編譯最新的 Android S (12) 只要 20 多分鐘,絕大部分公司的服務器還做不到這個性能。
對于指令緩存的 I-Cache 和數(shù)據(jù)緩存的 D-Cache,平時 D-Cache 訪問比較多,以下主要以 D-Cache 的訪問為例說明,指令緩存 I-Cache 原理一樣。
3. Cache 數(shù)據(jù)訪問原理
Cache讀寫原理
圖一、Cache讀寫原理
寫入數(shù)據(jù)時:
第一步,CPU 將數(shù)據(jù)寫入 Cache;
第二步,將 Cache 數(shù)據(jù)傳送到 Memory 中相應的位置;
讀取數(shù)據(jù)時:
第一步,將 Memory 中的數(shù)據(jù)傳送到 Cache 中;
第二步,CPU 從 Cache 中讀取數(shù)據(jù);
在具體的硬件實現(xiàn)上,Cache 有寫操作有透寫(Write-Through)和回寫(Write-Back)兩種方式:
透寫(Write-Through)
在透寫式 Cache 中,CPU 的數(shù)據(jù)總是寫入到內存中,如果對應內存位置的數(shù)據(jù)在 Cache 中有一個備份,那么這個備份也要更新,保證內存和 Cache 中的數(shù)據(jù)永遠同步。所以每次操作總會執(zhí)行圖一中的步驟 1 和 2。
回寫(Write-Back)
在回寫式 Cache 中,把要寫的數(shù)據(jù)只寫到 Cache 中,并對 Cache 對應的位置做一個標記,只在必要的時候才會將數(shù)據(jù)更新到內存中。所以每次寫操作都會執(zhí)行步驟中的圖 1,但并不是每次執(zhí)行步驟 1 后都執(zhí)行步驟 2 操作。
透寫方式存在性能瓶頸,性能低于回寫方式,現(xiàn)在的 CPU 設計基本上都是采用 Cache 回寫方式。
通常情況下,數(shù)據(jù)只通過 CPU 進行訪問,每次訪問都會經(jīng)過 Cache,此時數(shù)據(jù)同步不會有問題。
在有設備進行 DMA 操作的情況下,設備讀寫數(shù)據(jù)不再通過 Cache,而是直接訪問內存。在設備和 CPU 讀寫同一塊內存時,所取得的數(shù)據(jù)可能會不一致,如圖二。
設備和CPU讀寫同一塊內存時數(shù)據(jù)不一致
圖二、設備和CPU讀寫同一塊內存時數(shù)據(jù)不一致
CPU 執(zhí)行步驟1將數(shù)據(jù) A 寫入 Cache,但并不是每次都會執(zhí)行步驟 2 將數(shù)據(jù) A 同步到內存,導致 Cache 中的數(shù)據(jù) A 和內存中的數(shù)據(jù) A’不一致;步驟 3 中,外部設備通過 DMA 操作時直接從內存訪問數(shù)據(jù),從而取得的是A’而不是A。
設備DMA操作完成后,通過步驟 4 將數(shù)據(jù) B 寫入內存;但是由于內存中的數(shù)據(jù)不會和 Cache 自動進行同步,步驟 5不會被執(zhí)行,所以 CPU 執(zhí)行步驟 3 讀取數(shù)據(jù)時,獲取的可能是 Cache 中的數(shù)據(jù) B’,而不是內存中的數(shù)據(jù)B;
在 CPU 和外設訪問同一片內存區(qū)域的情況下,如何操作 Cache 以確保設備和 CPU 訪問的數(shù)據(jù)一致就顯得尤為重要,見圖三。
Cache操作同步數(shù)據(jù)
圖三、Cache操作同步數(shù)據(jù)
CPU 執(zhí)行步驟 1 將數(shù)據(jù) A 寫入 Cache,由于設備也需要訪問數(shù)據(jù) A,因此執(zhí)行步驟 2 將數(shù)據(jù) A 通過 flush 操作同步到內存;步驟 3 中,外部設備通過 DMA 操作時直接從內存訪問數(shù)據(jù) A,最終 CPU 和設備訪問的都是相同的數(shù)據(jù)。
設備 DMA 操作完成后,通過步驟 4 將數(shù)據(jù) B 寫入內存;由于 CPU 也需要訪問數(shù)據(jù) B,訪問前通過 invalidate 操作作廢 Cache 中的數(shù)據(jù),從而通過 Cache 讀取數(shù)據(jù)時 Cache 會從內存取數(shù)據(jù),所以 CPU 執(zhí)行步驟 6 讀取數(shù)據(jù)時,獲取到的是從內存更新后的數(shù)據(jù);
4. Cache操作舉例
4.1 外設數(shù)據(jù) DMA 傳輸
例如,在某頂盒平臺中,內存加解密在單獨的安全芯片中進行,安全芯片訪問的數(shù)據(jù)通過 DMA 進行傳輸操作。
因此,在進行內存加解密前,需要 flush D-Cache 操作將數(shù)據(jù)同步到到內存中供安全芯片訪問;
加解密完成后需要執(zhí)行invalidate D-Cache操作,以確保CPU訪問的數(shù)據(jù)是安全芯片加解密的結果,而不是Cache之前保存的數(shù)據(jù);
DMA進行數(shù)據(jù)加解密的示例代碼:
void?mem_dma_desc( ??unsigned?long?Mode, ??unsigned?long?SrcAddr,?/*?input?data?addr?*/ ??unsigned?long?DestAddr,?/*?output?data?addr?*/ ??unsigned?long?Slot, ??unsigned?long?Size)?/*?dma?data?size?*/ ?{ ??...prepare?for?dma?encryption/decryption?operation... ??/*?flush?data?in?SrcAddr?from?D-Cache?to?memory? ?????to?ensure?dma?device?get?the?correct?data?*/ ??flush_d_cache(SrcAddr,?Size); ??...do?dma?operation,?output?will?be?redirect?to?DestAddr... ? ??/*?invalidate?D-Cache?to?ensure?fetch?data?from?memory ?????instead?of?cached?data?in?D-Cache?*/ ??invalidate_d_cache(DestAddr,?Size); ??return; ?}
4.2 外設 flash 的 I/O
某平臺的 nand flash 的控制器也支持 DMA 讀取的方式。在數(shù)據(jù)向 nand flash 寫入數(shù)據(jù)時需要先 flash dcache 確保DMA 操作的數(shù)據(jù)是真實要寫入的數(shù)據(jù),而不是內存中已經(jīng)過期的數(shù)據(jù);
從nand flash 讀取數(shù)據(jù)后需要 invalidate dcache,使 cache 中的數(shù)據(jù)失效,從而確保 cpu 讀取的是內存數(shù)據(jù),而不是上一次訪問時緩存的結果。
nand flash 通過 DMA 方式讀取數(shù)據(jù)的示例代碼:
static?int?nand_dma_read( ???struct?nand_dev?*nand, ???uint64_t?addr,?/*?read?addr?*/ ???void?*buf,?????/*?output?buffer?*/ ???size_t?len) { ?int?ret; ?...prepare?for?nand?flash?read?and?device?dma?transfer... ?/*?flush?dma?descriptor?for?nand?flash?read?operation?*/ ?flush_d_cache(descs,?ndescs?*?sizeof(*descs)); ?/*?nand?flash?dma?read?operation?*/ ?ret?=?nand_dma_run(nand,?(uintptr_t)descs); ?/*?invalidate?read?output?buffer?to?ensure?fetch?data?from?memory ????instead?of?cached?data?in?D-Cache?*/ ?invalidate_d_cache(buf,?len); ?...other?operations... ? ?return?ret; }
除了 nand flash 之外,很多硬盤也支持 DMA 方式讀取。
4.3 I-Cache 和 D-Cache 的轉換
通常 Cache 分為 I-Cache 和 D-Cache,取指令時訪問 I-Cache,讀寫數(shù)據(jù)時訪問 D-Cache。
但在代碼搬運時,外設上存放的指令會被當作數(shù)據(jù)進行處理。
例如一段代碼保存在外設(如nand ?flash或硬盤)上,CPU想執(zhí)行這段代碼,需要先將這段代碼作為數(shù)據(jù)復制到內存再將這段代碼作為指令執(zhí)行。
由于寫入數(shù)據(jù)和讀取指令分別通過 D-Cache 和 I-Cache,所以需要同步 D-Cache 和 I-Cache,即復制后需要先將 D-Cache 寫回到內存,而且還需要作廢當前的 I-Cache 以確保執(zhí)行的是 Memory 內更新的代碼,而不是 I-Cache 中緩存的數(shù)據(jù),如圖四所示:
圖四、CPU復制代碼后執(zhí)行
CPU復制代碼后執(zhí)行的示例代碼:
void?copy_code_and_execution( ??unsigned?char?*src,? ??unsigned?char?*dest,? ??size_t?len) { ?...copy?code?from?src?addr?to?dest?addr... ?/*?flush?instructions?data?in?D-Cache?to?memory?*/ ?flush_all_d_cache(); ?/*?invalidate?I-Cache?to?ensure?fetch?instructions?from?memory ????instead?of?cached?data?in?I-Cache?*/ ?invalidate_all_i_cache(); ?...jump?to?dest?address?for?execution?and?never?return... ?/*?actually?it?never?reach?here?if?it?jumps?to?dest?successfully?*/ ?printf("failed?to?jumping... "); ?return;? } 編輯:黃飛
?
?
評論
查看更多