
這篇文章主要整理了一下計算機中的記憶體結構,以及 CPU 是如何讀寫記憶體中的資料的,如何維護 CPU 快取中的資料一致性,什么是虛擬記憶體,以及它存在的必要性,如有不對請多多指教,
概述
目前在計算機中,主要有兩大存盤器 SRAM 和 DRAM,主存盤器是由 DRAM 實作的,也就是我們常說的記憶體,在 CPU 里通常會有 L1、L2、L3 這樣三層高速快取是用 SRAM 實作的,
SRAM 被稱為“靜態”存盤器,是因為只要處在通電狀態,里面的資料就可以保持存在,而一旦斷電,里面的資料就會丟失了,
目前 SRAM 主要集成在 CPU 里面,每個 CPU 核心都有一塊屬于自己的 L1 高速快取,通常分成指令快取和資料快取,分開存放 CPU 使用的指令和資料,L2 的 Cache 同樣是每個 CPU 核心都有的,不過它往往不在 CPU 核心的內部,所以,L2 cache 的訪問速度會比 L1 稍微慢一些,而 L3 cache ,則通常是多個 CPU 核心共用的,
在 DRAM 中存盤單元使用電容保存電荷的方式來存盤資料,電容會不斷漏電,所以需要定時重繪充電,才能保持資料不丟失,這也是被稱為“動態”存盤器的原因,由于存盤 DRAM 一個 bit 只需要一個晶體管,所以存盤的資料也大很多,
我們來看一些他們的速度:
- ? L1 的存取速度:4 個 CPU 時鐘周期
- ? L2 的存取速度:11 個 CPU 時鐘周期
- ? L3 的存取速度:39 個 CPU 時鐘周期
- ? DRAM 記憶體的存取速度:107 個 CPU 時鐘周期

CPU cache
cache 結構
上面我們說了,對于 CPU 來說,SRAM 被稱為 CPU 的 cache,CPU 每次獲取資料都會先訪問 cache,如果獲取不到資料則把資料加載到 cache 中進行訪問,因為 cache 的大小是遠遠小于主存的,所以還需要在 cache 和主存之間維護一個映射關系,這樣才能正確找到資料,
一種簡單的方式是直接映射,有點像我們把資料找出來,直接放入到 map 中進行存盤一樣,映射通過地址和 cache 的數量進行取模后獲取到 cache 中主存的地址去獲取資料,
(地址)mod(cache 中的塊數)

上圖中畫了一個地址去找 cache 的情況,對于 cache 來說可以劃分為:
索引:用來取模找到對應的 cache 行;
有效位:cache 初始值一開始是空的,這個欄位標記 cache 行是否有資料;
標記:用來和地址進行比較是否是對應的資料;
資料:表示存盤的實際的資料,可以通過地址的偏移來獲取到對應的資料;
比如對于這個 cache 有 1024 個字,即 4KB,假設有一個 32 位的地址要去 cache 中查找資料,那么會取地址 10 位進行取模找到對應的 cache 行,然后取出 20 位與 cahce 標記位進行比較,如果相等,并且有效位開啟,那么對應請求地址在 cache 中命中,否則,發生缺失,
由于 CPU 在讀取資料的時候,并不是要讀取一整個 Block,而是讀取一個他需要的資料片段, cache 中命中之后會根據低兩位的偏移去資料里面索引到對應的字,
除了上面說的直接映射以外還有組相聯和全相聯,
組相聯就是使用組索引代替了原來的索引,下圖中表示每組有 2 行資料,通過組索引找到對應的資料行之后通過有效位和標記對組中每一行進行檢索,如果能匹配上就說明命中,

cache 讀寫操作
先來看看讀操作,cache 在初始狀態的時候是空的,這個時候去讀資料會產生 cache 缺失(cache miss),cache 控制器會檢測到缺失的發生,然后從主存中(或低一級 cache)中取回所需資料,如果命中,那么就會直接使用,
當 cache 缺失時,對于亂序執行處理器而言依然能執行一些其他指令,但是對于順序執行處理器,當 cache 缺失時會被阻塞,臨時暫存器和程式員可見的暫存器中的內容基本被凍結,
再來看看寫操作,因為 cache 是由多級組成,所以寫策略一般而言有兩種:寫直達(write-through)和寫回(write-back),通過這兩種策略使在 cache 中寫入的資料和主存中的資料保持一致,
寫直達就是在將資料寫入 cache 之后同時將這個資料立馬寫入到主存中,但是由于主存和 cache 本身性能差異,那么每次在寫入主存的時候都將花費大量的時間,解決辦法就是加一層寫緩沖(write buffer),這樣 CPU 在將資料寫入 cache 和緩沖之后可以繼續執行,等到緩沖寫入到主存中再釋放,
但是如果寫入速度大于緩沖釋放速度,那么還是會阻塞 CPU 執行,那么可以考慮一下寫回策略,這種機制會在每次寫入的時候僅將新值寫入 cache 中,只有當修改過的塊被替換時才需要寫到較低層存盤結構中,
一致性與 MESI 協議
由于現在都是多核 CPU,并且 cache 分了多級,并且資料存在共享的情況,所以需要一種機制保證在不同的核中看到的 cache 資料必須時一致的,最常用來處理多核 CPU 之間的快取一致性協議就是 MESI 協議,
MESI 協議,是一種叫作寫失效(Write Invalidate)的協議,在寫失效協議里,只有一個 CPU 核心負責寫入資料,其他的核心,只是同步讀取到這個寫入,在這個 CPU 核心寫入 cache 之后,它會去廣播一個“失效”請求告訴所有其他的 CPU 核心,
MESI 協議對應的四個不同的標記,分別是:
- ? M:代表已修改(Modified)
- ? E:代表獨占(Exclusive)
- ? S:代表共享(Shared)
- ? I:代表已失效(Invalidated)
“已修改”和“已失效”比較容易理解,我們來看看 獨占”和“共享” 兩個狀態,
在獨占狀態下,對應的 cache Line 只加載到了當前 CPU 核所擁有的 cache 里,其他的 CPU 核,并沒有加載對應的資料到自己的 cache 里,這個時候,如果要向獨占的 cache Block 寫入資料,我們可以自由地寫入資料,而不需要告知其他 CPU 核,
那么對應的,共享狀態就是在多核中同時加載了同一份資料,所以在共享狀態下想要修改資料要先向所有的其他 CPU 核心廣播一個請求,要求先把其他 CPU 核心里面的 cache ,都變成無效的狀態,然后再更新當前 cache 里面的資料,
虛擬記憶體
虛擬記憶體映射
在我們日常使用的 Linux 或者 Windows 作業系統下,程式并不能直接訪問物理記憶體,程式都是通過虛擬地址 VA(virtual address)用地址轉換翻譯成 PA 物理地址(physical address)才能獲取到資料,也就是說 CPU 操作的實際上是一個虛擬地址 VA,

如上圖,CPU 訪問主存的時候會將一個虛擬地址(virtual address)被記憶體管理單元(Memory Management Unint, MMU)進行翻譯成物理地址 PA(physical address) 才能訪問,
想要把虛擬記憶體地址,映射到物理記憶體地址,最直觀的辦法,就是來建一張映射表,這個映射表在計算機中叫頁表(Page Table),

在查找頁表的時候,會將虛擬地址分成頁號(Directory)和偏移量(Offset)兩個部分,前面的高位,表示記憶體地址的頁號,后面的低位,表示記憶體地址里面的偏移量,
查找方式和上面說的組相聯類似,首先使用虛擬頁號作為索引去頁表中找到對應的物理頁號,頁表中還會有 1 位表示有效位,如果該位無效就不在主存中,發生一次缺頁;如果有效,那么就可以拿到對應的物理頁號獲取到對應的物理頁位置,再根據偏移量得到物理記憶體地址,

如果有效位關閉,那么該頁就只存在磁盤上的某個指定的磁盤地址,缺頁會觸發缺頁例外,然后在閃存或磁盤中找到該頁,將其放入到主存 DRAM 中,
如果主存滿了,那么會選擇一個犧牲頁,大多數作業系統會使用 LRU 替換策略來進行頁的替換,作業系統會查找最少使用的頁,被替換的頁會寫入磁盤的交換區(swap 磁區),swap 磁區通常被稱為交換磁區,這是一塊特殊的硬碟空間,即當實際記憶體不夠用的時候,作業系統會從記憶體中取出一部分暫時不用的資料,放在交換磁區中,從而為當前運行的程式騰出足夠的記憶體空間,
在下圖中假設選擇將存放在主存中的 VP6 進行替換,將 VP6 替換為 VP3,如果被替代的 VP6 已經被修改了,那么內核會將它復制回磁盤,

由于區域性(locality)的存在,程式一般而言會在一個較小的活動頁面集合上作業,頁的切換開銷只存在于程式啟動時將頁面調度到記憶體中,接下來的程式都會頁命中,但是如果代碼的作業集太大,超過了物理記憶體大小,那么頁面就會不停地換進換出,產生抖動,
多級頁表
假設我們現在是一個 32 位的地址空間、4KB 的頁面和一個 4 位元組的 PTE,那么需要一個 4MB 的頁表常駐在記憶體中,并且這個頁表是每個行程都獨占一份,所以會造成很大的記憶體浪費,我們需要一種方式來優化我們的頁表空間存盤,
想一下虛擬記憶體空間結構,整個 4 GB 的空間,作業系統用了 1 GB,從地址 0XC0000000 到 0XFFFFFFFF, 剩余 3 GB 留給用戶空間,其實很多程式根本用不到這么大的空間,對于 64 位系統,每個行程都會擁有 256 TiB 的記憶體空間,那就更加用不上了,

那么對于用不上的空間,我們可以不可以不把它加載到頁表里面,等到用這塊空間的時候才在頁表里面給它分配一個頁表項,是不是就可以節省大量空間,
在程式運行的時候,記憶體地址從頂部往下,不斷分配占用的堆疊的空間,而堆的空間,記憶體地址則是從底部往上,是不斷分配占用的,所以,在一個實際的程式行程里面,虛擬記憶體占用的地址空間,通常是兩段連續的空間,而多級頁表,就特別適合這樣的記憶體地址分布,
假設 32 位虛擬地址空間被劃分位 4KB 每頁,每個條目都是 4 位元組,那么我們可以讓第一級頁表中的每個 PTE (頁表項 page table entry)負責映射虛擬地址空間中一個 4MB 的片,這個片由 1024 個連續頁面組成,表示二級頁表,如果地址空間是 4GB,那么 1024 個一級頁表項就可以覆寫整個空間,
如下圖所示,記憶體前 2K 個頁面給代碼和資料,接下來 6K 個頁面未分配,在接下來 1023 個頁面也未分配,接下來一個頁面分配給用戶堆疊,

這種方法從兩個方面減少了記憶體占用,第一,如果一級頁表中的一個 PTE 是空的,那么相應的二級頁表就根本不會存在,由于很多程式占用記憶體實際遠小于頁表所能表示的大小,所以可以節約很大空間的頁表項資源;第二,只有一級頁表才需要總是在主存中,二級頁表會在需要的時候創建或銷毀,只有最經常使用的二級頁表才需要快取在主存中,這就減少了主存的壓力,
Linux 在 2.6.10 中引入了四層的頁表輔助虛擬地址的轉換:

首先會找到 4 級頁表里面對應的條目(Entry),這個條目里存放的是一張 3 級頁表所在的位置,4 級頁面里面的每一個條目,都對應著一張 3 級頁表,所以我們可能有多張 3 級頁表,
找到對應這張 3 級頁表之后,我們用 3 級索引去找到對應的 3 級索引的條目,3 級索引的條目再會指向一個 2 級頁表,依次拿到 1 級頁表里面存盤的物理頁號,我們同樣可以用“頁號 + 偏移量”的方式,來獲取最終的物理記憶體地址,
TLB 加速地址轉換
對于一個頁命中的資料獲取程序通常來說,如果沒有 TLB 加速是這樣的:
- CPU 生成一個虛擬地址,并把它傳給 MMU;
- MMU 生成頁表項地址 PTEA,并從高速快取/主存請求獲取頁表項 PTE;
- 高速快取/主存向 MMU 回傳 PTE;
- MMU 構造物理地址 PA,并把它傳給高速快取/主存;
- 高速快取/主存回傳所請求的資料給 CPU,

一次簡單的資料獲取需要多次經過多次與記憶體的互動,如果是 4 級頁表,那么就需要訪問 4 次記憶體才能獲取到對應的物理頁號,如果是缺頁,還需要有一個 PTE 的置換或加載程序,在開頭也講了,訪問記憶體的性能其實很低的,實際上這嚴重影響了 CPU 處理性能,
程式所需要使用的指令,都順序存放在虛擬記憶體里面,我們執行的指令,也是一條條順序執行下去的,也就是說,我們對于指令地址的訪問,存在前面幾講所說的“空間區域性”和“時間區域性”,而需要訪問的資料也是一樣的,我們連續執行了 5 條指令,因為記憶體地址都是連續的,所以我們可以通過加快取的方法,把之前記憶體轉換的地址快取下來,減少與記憶體的互動,
加的這一層就是快取芯片 TLB (Translation Lookaside Buffer),它里面每一行保存著一個由單個 PTE 組成的塊,
那么查詢資料的程序就變成了:
- CPU 產生一個虛擬地址;
- MMU 從 TLB 中取出相應的 PTE;
- MMU 將這個虛擬地址翻譯成一個物理地址,并且將它發送到高速快取/主存;
- 高速快取/主存將所請求的資料字回傳給 CPU;

最后來看看為什么需要虛擬記憶體?
講完了什么是虛擬記憶體,我們最后講講虛擬記憶體的必要性,
由于操作虛擬記憶體實際上就是操作頁表,從上面講解我們知道,頁表的大小其實和物理記憶體沒有關系,當物理記憶體不夠用時可以通過頁缺失來將需要的資料置換到記憶體中,記憶體中只需要存放眾多程式中活躍的那部分,不需要將整個程式加載到記憶體里面,這可以讓小記憶體的機器也可以運行程式,
虛擬記憶體可以為正在運行的行程提供獨立的記憶體空間,制造一種每個行程的記憶體都是獨立的假象,虛擬記憶體空間只是作業系統中的邏輯結構,通過多層的頁表結構來轉換虛擬地址,可以讓多個行程可以通過虛擬記憶體共享物理記憶體,
并且獨立的虛擬記憶體空間也會簡化記憶體的分配程序,當用戶程式向作業系統申請堆記憶體時,作業系統可以分配幾個連續的虛擬頁,但是這些虛擬頁可以對應到物理記憶體中不連續的頁中,
再來就是提供了記憶體保護機制,任何現代計算機系統必須為作業系統提供手段來控制對記憶體系統的訪問,虛擬記憶體中頁表中頁存放了讀權限、寫權限和執行權限,記憶體管理單元可以決定當前行程是否有權限訪問目標的物理記憶體,這樣我們就最終將權限管理的功能全部收斂到虛擬記憶體系統中,減少了可能出現風險的代碼路徑,
總結
從上面我們可以知道 CPU 的快取結構一般由 L1、L2、L3 三層快取結構組成,CPU 讀取資料只與快取互動,不會直接訪問主存,所以 CPU 快取和主存之間維護了一套映射關系,當被查找的資料發生缺失時,需要等待資料從主存加載到快取中,如果快取滿了,那么還需要進行淘汰,如果被淘汰的資料是臟資料,那么還需要寫回到主存中,寫的策略有寫直達(write-through)和寫回(write-back),
由于現在計算機中的 CPU 都是多核的,并且快取資料是由多核共享的,所以就有了類似 MESI 這樣的協議來維護一個狀態機保證資料在多核之間是一致的,
為了訪問資料安全,便捷,迅速所以加了一層虛擬記憶體,每個程式在啟動的時候都會維護一個頁表,這個頁表維護了一套映射關系,CPU 操作的實際上是虛擬地址,每次需要 MMU 將虛擬地址在頁表上映射成物理地址后查找資料,并且為了節省記憶體所以設計了多級頁表,為了從頁表中查找資料更快加了一個快取芯片 TLB,
Reference
《深入理解計算機系統》
《深入淺出計算機組成原理》
《計算機組成與設計:硬體軟體介面》
https://draveness.me/whys-the-design-os-virtual-memory/
https://people.freebsd.org/~lstewart/articles/cpumemory.pdf
作者:bearluo
本文來自博客園,作者:古道輕風,轉載請注明原文鏈接:https://www.cnblogs.com/88223100/p/How-the-CPU-interacts-with-memory.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/509608.html
標籤:其他
上一篇:二、目標檢測演算法之R-CNN
