計算機原理學習(3)-- 內存工作原理


計算機原理學習(3)-- 內存工作原理  

前言

 


前面兩篇文章介紹了計算機硬件是如何工作的。而從這一章開始將逐漸的轉到軟件上面來。我們還有內存這一個很重要的部分沒有介紹。這一章不僅僅介紹內存的工作原理,還會介紹內存的編址、內存數據存放。逐漸從硬件過渡到軟件上來。為后面介紹程序運行打下基礎。



 

1. 內存工作原理


 

CPU和內存是計算機中最重要的兩個組件,前面已經知道了CPU是如何工作的,上一篇也介紹了內存采用的DRAM的存儲原理。CPU工作需要知道指令或數據的內存地址,那么這樣一個地址是如何和內存這樣一個硬件聯系起來的呢?現在就看看內存到的是怎么工作的。


 

1.1 DRAM芯片結構

上圖是DRAM芯片一個單元的結構圖。一個單元被分為了N個超單元(可以叫做cell),每個單元由M個DRAM單元組成。我們知道一個DRAM單元可以存放1bit數據, 所以描述一個DRAM芯片可以存儲N*M位數據。上圖就是一個有16個超單元,每個單元8位的存儲模塊,我們可以稱為16*8bit 的DRAM芯片。而超單元(2,1)我們可以通過如矩陣的方式訪問,比如 data = DRAM[2.1] 。這樣每個超單元都能有唯一的地址,這也是內存地址的基礎。

 

每個超單元的信息通過地址線和數據線傳輸查找和傳輸數據。如上圖有2根地址線和8根數據線連接到存儲控制器(注意這里的存儲控制器和前面講的北橋的內存控制器不是一回事),存儲控制器電路一次可以傳送M位數據到DRAM芯片或從DRAM傳出M位數據。為了讀取或寫入【i,j】超單元的數據,存儲控制器需要通過地址線傳入行地址i 和列地址j。這里我們把行地址稱為RAS(Row Access Strobe)請求, 列地址稱為(Column Access Strobe)請求。


但是我們發現地址線只有2為,也就是尋址空間是0-3。而確定一個超單元至少需要4位地址線,那么是怎么實現的呢?

解決這個問題采用的是分時傳送地址碼的方法。看上圖我們可以發現在DRAM芯片內部有一個行緩沖區,實際上獲取一個cell的數據,是傳送了2次數據,第一次發送RAS,將一行的數據放入行緩沖區,第二期發送CAS,從行緩沖區中取得數據並通過數據線傳出。這些地址線和數據線在芯片上是以管腳(PIN)與控制電路相連的。將DRAM電路設計成二維矩陣而不是一位線性數組是為了降低芯片上的管腳數量。入上圖如果使用線性數組,需要4根地址管腳,而采用二維矩陣並使用RAS\CAS兩次請求的方式只需要2個地址管腳。但這樣的缺點是增加了訪問時間。

 

 

1.2 內存模塊

 

內存模塊也就是我們常說的內存條。我們在購買內存是經常會聽到我這個內存采用的是什么顆粒,如下左圖,我們看到內存PCB上的一塊塊的就是內存顆粒。也就是我們DRAM芯片。通過管腳和PCB連接。不同廠商,不同類型的內存可以的大小,管腳,性能,封裝都不一樣,但是原理都是一樣。這里我們就不展開介紹了。而下有圖展示了一個1M*4bit的DRAM芯片的管腳圖。

 

對於一個內存顆粒來說,它的容量和字長是有限的,所以我們使用內存是會把多個顆粒組成內存模塊來對內存進行字長和容量的擴展。目前的內存一般內存條上面會有多顆內存顆粒,比如一條64M的內存可能是由8個8M*8bit 的SDRAM內存顆粒組成。

 


1.2.1 字長位數擴展 

位擴展的方法很簡單,只需將多片RAM的相應地址端、讀/寫控制端 和片選信號CS並接在一起,而各片RAM的I/O端並行輸出即可。 如上圖,我們采用了8個DRAM芯片分,別編號為0-7,每個超單元中存儲8位數據。在獲取add(row=i,col=j)地址的數據的時候,從每個DRAM芯片的【i, j】單元取出一個字節的數據,這樣傳送到CPU的一共是8*8b = 64b的數據。我們通過8個8M*8b的內存顆粒擴展為了8M*64b的內存模塊。

 

 

1.2.2 字存儲容量擴展

 

RAM的字擴展是利用譯碼器輸出控制各片RAM的片選信號CS來實現的。RAM進行字擴展時必須增加地址線,而增加的地址線作為高位地址與譯碼器的輸入相連。同時各片RAM的相應地址端、讀/寫控制端 、相應I/O端應並接在一起使用。下圖是我們通過4個2M*8b的內存顆粒,將內存容量擴展到了8M,字長為8位。

 

最后,內存通過主板上的內存插槽DIMM和內存總線相連接。對於不同內存比如SDRAM和DDR他們內存金手指的定義是不同的。這里就不需要詳細介紹了。

 


2. 內存編址

 

前面我們知道了DRAM顆粒以及內存模塊是如何擴展字長和容量的。一個內存可能是8位,也可能是64位,容量可能是1M,也可能是1G。那么內存是如何編地的呢?和地址總線,計算機字長之間又有什么關系呢?

 

 

2.1 字長

 

計算機在同一時間內處理的一組二進制數稱為一個計算機的“字”,而這組二進制數的位數就是“字長”。。通常稱處理字長為8位數據的CPU叫8位CPU,32位CPU就是在同一時間內處理字長為32位的二進制數據。 所以這里的字並不是我們理解的雙字節(Word)而是和硬件相關的一個概念。一般來說計算機的數據線的位數和字長是相同的。這樣從內存獲取數據后,只需要一次就能把數據全部傳送給CPU。

 


2.2 地址總線

 

前面我們已經介紹過地址總線的功能。地址總線的數量決定了他最大的尋址范圍。就目前來說一般地址總線先字長相同。比如32位計算機擁有32為數據線和32為地線,最大尋址范圍是4G(0x00000000 ~ 0xFFFFFFFF)。當然也有例外,Intel的8086是16為字長的CPU,采用了16位數據線和20位數據線。

 


2.3 內存編址

 

從前面我們知道一個內存的大小和它芯片擴展方式有關。比如我們內存模塊是采用 16M*8bit的內存顆粒,那么我們使用4個顆粒進行位擴展,成為16M*32bit,使用4個顆粒進行字容量擴展變為64M*32bit。那么我們內存模塊使用了16個內存顆粒,實際大小是256MB。

 

我們需要對這個256M的內存進行編址以便CPU能夠使用它,通常我們多種編址方式:

  1. 按字編址:    對於這個256M內存來說,它的尋址范圍是64M,而每個內存地址可以存儲32bit數據。
  2. 按半字編址:對於這個256M內存來說,它的尋址范圍是128M,而每個內存地址可以存儲16bit數據。
  3. 按字節編址:對於這個256M內存來說,它的尋址范圍是256M,而每個內存地址可以存儲8bit數據。


對於我們現在的計算機來說,主要都是采用按字節編址的方式。所以我們可以把內存簡單的看成一個線性數組,數組每個元素的大小為8bit,我們稱為一個存儲單元。這一點很重要,因為后面討論的所有問題內存都是以按字節編址的方式。 這也是為什么對於32位計算機來說,能使用的最多容量的內存為4GB。如果我們按字編地址,能使用的最大內存容量就是16GB了。

 

於是很容易想到一個問題,為什么我們要采用字節編址的方式呢?關於這個問題,我在網上基本沒有找到答案,甚至都找不到問這個問題的。所以這里沒法給出答案,為什么為什么呢? 麻煩知道的朋友告訴我哈。

 

另一方面的問題是,內存編址方式和DRAM芯片是否有關呢? 我認為還是有一定關系。比如我DRAM的芯片是8M*8bit,那么芯片最小的存儲單位就是8bit,那么我們內存編址就不能按照半個字節來編址。否則內存取出8bit,根本不知道你要那4bit傳給CPU。也有一種說法是現在的DRAM芯片cell都是8bit,所以采用按字節編址。另一方面應該也和數據總線位寬有關。

 

 

3. 內存數據

 

前面我們知道了,內存是按字節編址,每個地址的存儲單元可以存放8bit的數據。我們也知道CPU通過內存地址獲取一條指令和數據,而他們存在存儲單元中。現在就有一個問題。我們的數據和指令不可能剛好是8bit,如果小於8位,沒什么問題,頂多是浪費幾位(或許按字節編址是為了節省內存空間考慮)。但是當數據或指令的長度大於8bit呢?因為這種情況是很容易出現的,比如一個16bit的Int數據在內存是如何存儲的呢?

 


3.1 內存數據存放

 

其實一個簡單的辦法就是使用多個存儲單元來存放數據或指令。比如Int16使用2個內存單元,而Int32使用4個內存單元。當讀取數據時,一次讀取多個內存單元。於是這里又出現2個問題:

  1. 多個存儲單元存儲的順序?
  2. 如何確定要讀幾個內存單元?

 

3.1.1 大端和小端存儲

  1. Little-Endian 就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。
  2. Big-Endian 就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。

需要說明的是,計算機采用大端還是小端存儲是CPU來決定的, 我們常用的X86體系的CPU采用小端,一下ARM體系的CPU也是用小端,但有一些CPU卻采用大端比如PowerPC、Sun。判斷CPU采用哪種方式很簡單:

  1. bool IsBigEndian()    
  2. {    
  3.     int vlaue = 0x1234;    
  4.     char lowAdd =  *(char *)&value;     
  5.     if( lowAdd == 0x12)    
  6.     {    
  7.         return true;    
  8.     }    
  9.     return false;    
  10. }  


既然不同計算機存儲的方式不同,那么在不同計算機之間交互就可能需要進行大小端的轉換。這一點我們在Socket編程中可以看到。這里就不介紹了,對以我們單一CPU來說我們可以不需要管這個轉換的問題,另外我們目前個人PC都是采用小端方式,所以我們后面默認都是這種方式。

 


3.1.2 CPU指令


前面我們多次提到了指令的概念,也知道指令是0和1組成的,而匯編代碼提高了機器碼的可讀性。為什么突然在這里介紹CPU指令呢? 主要是解釋上面的第二個問題,當我讀取一個數據或指令時,我怎么知道需要讀取多少個內存單元。

 


3.1.2.1 CPU指令格式


首先我們來看看CPU指令的格式,我們知道CPU質量主要就是告訴CPU做什么事情,所以一條CPU指令一般包含操作碼(OP)和操作

  操作碼字段    地址碼字段

 

 

根據一條指令中有幾個操作數地址,可將該指令稱為幾操作數指令或幾地址指令。

 操作碼  A1  A2  A3

 

三地址指令: (A1) OP (A2) --> A3

 操作碼  A1  A2

 

二地址指令: (A1) OP (A2) --> A1

 操作碼   A1

 

一地址指令: (AC) OP (A) --> AC   

 操作碼  

 

    零地址指令

A1為被操作數地址,也稱源操作數地址; A2為操作數地址,也稱終點操作數地址; A3為存放結果的地址。 同樣,A1,A2,A3以是內存中的單元地址,也可以是運算器中通用寄存器的地址。所以就有一個尋址的問題。關於指令尋址后面會介紹。

CPU指令設計是十分復雜的,因為在計算機中都是0和1保存,那計算機如何區分一條指令中的操作數和操作碼呢?如何保證指令不會重復呢?這個不是我們討論的重點,有興趣的可以看看計算機體系結構的書,里面都會有介紹。從上圖來看我們知道CPU的指令長度是變長的。所以CPU並不能確定一條指令需要占用幾個內存單元,那么CPU又是如何確定一條指令是否讀取完了呢?

 


3.1.2.2 指令的獲取


 

現在的CPU多數采用可變長指令系統。關鍵是指令的第一字節。 當CPU讀指令時,並不是一下把整個指令讀近來,而是先讀入指令的第一個字節。指令譯碼器分析這個字節,就知道這是幾字節指令。接着順序讀入后面的字節。每讀一個字節,程序計數器PC加一。整個指令讀入后,PC就指向下一指令(等於為讀下一指令做好了准備)。

Sample1:

  1. MOV AL,00  機器碼是1011 0000 0000 0000  


機器碼是16位在內存中占用2個字節:

【00000000】 <- 0x0002

【10110000】 <- 0x0001


比如上面這條MOV匯編指令,把立即數00存入AL寄存器。而CPU獲取指令過程如下:

  1. 從程序計數器獲取當前指令的地址0x0001。
  2. 存儲控制器從0x0001中讀出整個字節,發送給CPU。PC+1 = 0X0002.
  3. CPU識別出【10110000】表示:操作是MOV AL,並且A2是一個立即數長度為一個字節,所以整個指令的字長為2字節。
  4. CPU從地址0x0002取出指令的最后一個字節
  5. CPU將立即數00存入AL寄存器。


這里的疑問應該是在第3步,CPU是怎么知道是MOV AL 立即數的操作呢?我們在看下面一個列子。

 

Sample2:

  1. MOV AL,[0000] 機器碼是1010 0000 0000 0000 0000 0000  

 

這里同樣是一條MOV的匯編指令,整個指令需要占用3個字節。

【00000000】 <-0x0003

【00000000】 <- 0x0002

【10100000】 <- 0x0001

 

我們可以比較一下2條指令第一個字節的區別,發現這里的MOV  AL是1010 0000,而不是Sample1中的1011 000。CPU讀取了第一個字節后識別出,操作是MOV AL [D16],表示是一個寄存器間接尋址,A3操作是存放的是一個16位就是地址偏移量(為什么是16位,后面文章會介紹),CPU就判定這條指令長度3個字節。於是從內存0x0002~0x0003讀出指令的后2個字節,進行尋址找到真正的數據內存地址,再次通過CPU讀入,並完成操作。

 

從上面我們可以看出一個指令會根據不同的尋址格式,有不同的機器碼與之對應。而每個機器碼對應的指令的長度都是在CPU設計時就規定好了。8086采用變長指令,指令長度是1-6個字節,后面可以添加8位或16位的偏移量或立即數。 下面的指令格式相比上面2個就更加復雜。

 

  • 第一個字節的高6位是操作碼,W表示傳說的數據是字(W=1)還是字節(W=0),D表示數據傳輸方向D=0數據從寄存器傳出,D=1數據傳入寄存器。
  • 第二個字節中REG表示寄存器號,3位可以表示8種寄存器,根據第一字節的W,可以表示是8位還是16位寄存器。表3-1中列出了8086寄存器編碼表
  • 第二個字節中的MOD和R/M指定了操作數的尋址方式,表3-2列出了8086的編碼

這里沒必要也無法更詳細介紹CPU指令的,只需要知道,CPU指令中已經定義了指令的長度,不會出現混亂讀取內存單元的現象。有興趣的可以查看引用中的連接。

 


3.1.3  內存數據

 

3.1.3.1 內存數據的操作

 

從上面我們可以知道,操作數可以是立即數,可以存放在寄存器,也可以存放在內存。對於第一個例子,指令已經說明,操作時是一個字節,於是CPU可以從下一個內存地址讀取操作時,而對於第二個列子,操作數只是地址偏移,所以當CPU獲得這個數據后,需要轉換成實際的內存地址,在進行一次內存訪問,把數據讀入到寄存器中。這里就出現我們前面提到的問題,這個數據我們要讀幾個存儲單元呢?

  1.     MyClass cla;  
  2. 008C3EC9  lea         ecx,[cla]    
  3. 008C3ECC  call        MyClass::MyClass (08C1050h)    
  4. 008C3ED1  mov         dword ptr [ebp-4],0    
  5.     cla.num5 = 500;  
  6. 008C3ED8  mov         dword ptr [ebp-6Ch],1F4h    
  7.     int b1 = MyClass::num1;  
  8. 008C3EDF  mov         dword ptr [b1],64h    
  9.     int b2 = MyClass::num2;  
  10. 008C3EE6  mov         dword ptr [b2],0C8h    
  11.     int b3 = MyClass::num3;  
  12. 008C3EF0  mov         eax,dword ptr ds:[008C9008h]    
  13. 008C3EF5  mov         dword ptr [b3],eax    
  14.     int b4 = cla.num4;  
  15. 008C3EFB  mov         eax,dword ptr [cla]    
  16. 008C3EFE  mov         dword ptr [b4],eax    
  17.     int b5 = cla.num5;  
  18. 008C3F04  mov         eax,dword ptr [ebp-6Ch]    
  19. 008C3F07  mov         dword ptr [b5],eax    


讓我們看一段C++代碼和對應的匯編代碼,操作很簡單,創建一個Myclass對象后,對成員變量賦值。而賦值都是試用Mov操作符。對於這些變量我們有賦值操作和取值操作,那么是如何確定要讀取或寫入數據的大小呢?

  1. cla.num5 = 500;  
  2. 08C3ED8  mov         dword ptr [ebp-6Ch],1F4h    


我看先看看賦值操作,往dword ptr [ebp-6Ch]內存存入一個立即數, [ebp-6Ch]是num5的內存地址,而前面的dword ptr 表示這是進行一個雙子操作。還記得上面指令格式中第一個字節的W字段嗎? 在8086中只能進行字節或字操作,而現在CPU都可以進行雙字操作。

  1. int b5 = cla.num5;  
  2. 08C3F04  mov         eax,dword ptr [ebp-6Ch]    


同樣,當我們要從一個內存讀取數據的時候,也要指定讀取數據的操作類型,這里也是雙字操作。這樣以來,就能從內存中正確的讀出需要的長度了。就這么一個簡單的賦值操作,獲取你從來沒想過在內存中怎么存放,又是怎么讀取的。這一切都是編譯器和CPU在背后為我們完成了。

 


3.1.3.2 內存對齊

 前面我們清楚了CPU是如何正確讀取數大小不同的數據的,最后一部分來看看有關內存對齊的問題。對於大部分程序員來說,內存對齊應該是透明的。內存對齊是編譯器的管轄范圍。編譯器為程序中的每個數據單元安排在適當的位置上。

 


3.1.3.2.1 對齊原因

從前面我們知道,目前計算機內存按照字節編址,每個地址的內存大小為1個字節。而讀取數據的大小和數據線有關。比如數據線為8位那么一次讀取一個字節,而如果數據線為32位,那么一次需要讀取32個字節,這樣是為了一次更多的獲取數據提高效率。否則讀取一個int變量就需要進行4次內存操作。對於內存訪問一般有以下兩個條件:

  1. CPU進行一次內存訪問讀取的數據和字長相同。
  2. 有些CPU只能對字長倍數的內存地址進行訪問。

對於第一個條件一般來說,目前存儲器一個cell是8bit,進行位擴展使他和字長還有數據線位數是相同,那么一次就能傳送CPU可以處理最多的數據。而前面我們說過目前是按字節編址可能是因為一個cell是8bit,所以一次內存操作讀取的數據就是和字長相同。

也正是因為和存儲器擴展有關(參考1.2.1的圖),每個DRAM位擴展芯片使用相同RAS。如果需要跨行訪問,那么需要傳遞2次RAS。所以以32位CPU為例,CPU只能對0,4,8,16這樣的地址進行尋址。而很多32位CPU禁掉了地址線中的低2位A0,A1,這樣他們的地址必須是4的倍數,否則會發送錯誤。

如上圖,當計算機數據線為32位時,一次讀入4個地址范圍的數據。當一個int變量存放在0-3的地址中時,CPU一次就內存操作就可以取得int變量的值。但是如果int變量存放在1-4的地址中呢? 根據上面條件2的解釋,這個時候CPU需要進行2次內存訪問,第一次讀取0-4的數據,並且只保存1-3的內容,第二次訪問讀取4-7的數據並且只保存4的數據,然后將1-4組合起來。如下圖:

所以內存對齊不但可以解決不同CPU的兼容性問題,還能減少內存訪問次數,提高效率。當然目前關於這個原因爭論很多,可以看看CSDN上的討論:http://bbs.csdn.net/topics/30388330

 

3.1.3.2.2 如何對齊內存

內存對齊有一個對齊系數,一般是2,4,8,16字節這樣。而不同平台上的對齊方式不同,這個主要是編譯器來決定的。

具體的規則可以參考之前轉的一篇文章,這里就不詳細寫了: http://blog.csdn.net/cc_net/article/details/2908600

 



總結



通過這一篇對內存工作的介紹,我們從內存的硬件結構,存儲方式過渡到了內存的編址方式,然后又探討了按字節編址帶來的問題和解決的辦法。這里就涉及到了CPU的指令格式,編譯器的支持。最后我們也是從硬件和軟件方面討論了內存對齊的問題。


我自己感覺,內存的訪問管理是計算機中最重要的部分,也是計算機硬件和軟件之間交互的過渡的一個地方。所以理解了內存的工作原理,對於后面理解不同的內存模型很有幫助。


 

 

參考



SDRAM DRAM工作原理

 

詳解大端模式和小端模式

 

cpu是怎樣識別一條接一條的指令的

 

8086指令與機器碼對應

 

8086/8088指令格式

 

為什么要進行內存對齊內存對齊?


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM