我的博客:https://www.luozhiyun.com/
內存
內存是五大組成部分里面的存儲器,我們的指令和數據,都需要先加載到內存里面,才會被CPU拿去執行。
我們的內存需要被分成固定大小的頁(Page),然后再通過虛擬內存地址(Virtual Address)到物理內存地址(Physical Address)的地址轉換(Address Translation),才能到達實際存放數據的物理內存位置。而我們的程序看到的內存地址,都是虛擬內存地址。
頁表
想要把虛擬內存地址,映射到物理內存地址,最直觀的辦法,就是來建一張映射表。虛擬內存里面的頁,到物理內存里面的頁的一一映射。這個映射表,在計算機里面,就叫作頁表(Page Table)。
頁表這個地址轉換的辦法,會把一個內存地址分成頁號(Directory)和偏移量(Offset)兩個部分。
對於一個內存地址轉換,其實就是這樣三個步驟:
- 把虛擬內存地址,切分成頁號和偏移量的組合;
- 從頁表里面,查詢出虛擬頁號,對應的物理頁號;
- 直接拿物理頁號,加上前面的偏移量,就得到了物理內存地址;
多級頁表(Multi-Level Page Table)
大部分進程所占用的內存是有限的,需要的頁也自然是很有限的。我們只需要去存那些用到的頁之間的映射關系就好了。
在整個進程的內存地址空間,通常是“兩頭實、中間空”。在程序運行的時候,內存地址從頂部往下,不斷分配占用的棧的空間。而堆的空間,內存地址則是從底部往上,是不斷分配占用的。
所以,在一個實際的程序進程里面,虛擬內存占用的地址空間,通常是兩段連續的空間。
我們以一個4級的多級頁表為例,來看一下。
對應的,一個進程會有一個4級頁表。我們先通過4級頁表索引,找到4級頁表里面對應的條目(Entry)。這個條目里存放的是一張3級頁表所在的位置。4級頁面里面的每一個條目,都對應着一張3級頁表,所以我們可能有多張3級頁表。
找到對應這張3級頁表之后,我們用3級索引去找到對應的3級索引的條目。3級索引的條目再會指向一個2級頁表。同樣的,2級頁表里我們可以用2級索引指向一個1級頁表。
而最后一層的1級頁表里面的條目,對應的數據內容就是物理頁號了。在拿到了物理頁號之后,我們同樣可以用“頁號+偏移量”的方式,來獲取最終的物理內存地址。
TLB加速地址轉換
程序里面的每一個進程,都有一個屬於自己的虛擬內存地址空間。我們可以通過地址轉換來獲得最終的實際物理地址。我們每一個指令都存放在內存里面,每一條數據都存放在內存里面。
“地址轉換”是一個非常高頻的動作,“地址轉換”的性能就變得至關重要了。
多級頁表讓原本只需要訪問一次內存的操作,變成了需要訪問4次內存,才能找到物理頁號。
程序所需要使用的指令,都順序存放在虛擬內存里面。我們執行的指令,也是一條條順序執行下去的。
於是,計算機工程師們專門在CPU里放了一塊緩存芯片。這塊緩存芯片我們稱之為TLB,全稱是地址變換高速緩沖(Translation-Lookaside Buffer)。這塊緩存存放了之前已經進行過地址轉換的查詢結果。這樣,當同樣的虛擬地址需要進行地址轉換的時候,我們可以直接在TLB里面查詢結果,而不需要多次訪問內存來完成一次轉換。
TLB和我們前面講的CPU的高速緩存類似,可以分成指令的TLB和數據的TLB,也就是ITLB和DTLB。
為了性能,我們整個內存轉換過程也要由硬件來執行。在CPU芯片里面,我們封裝了內存管理單元(MMU,Memory Management Unit)芯片,用來完成地址轉換。和TLB的訪問和交互,都是由這個MMU控制的。
I/O
我們先來看一個固態硬盤的Benchmark圖:
“4K”的指標就是我們的程序,去隨機讀取磁盤上某一個4KB大小的數據,一秒之內可以讀取到多少數據。
我們拿這個40MB/s和一次讀取4KB的數據算一下。 40MB / 4KB = 10,000 也就是說,一秒之內,這塊SSD硬盤可以隨機讀取1萬次的4KB的數據。如果是寫入的話呢,會更多一些,90MB /4KB 差不多是2萬多次。
這個每秒讀寫的次數,我們稱之為IOPS,也就是每秒輸入輸出操作的次數。
DTR(Data Transfer Rate,數據傳輸率)
我們在實際的應用開發當中,對於數據的訪問,更多的是隨機讀寫,而不是順序讀寫。
診斷 I/O瓶頸
首先看一下CPU有沒有在等待io操作。
# top
top - 06:26:30 up 4 days, 53 min, 1 user, load average: 0.79, 0.69, 0.65
Tasks: 204 total, 1 running, 203 sleeping, 0 stopped, 0 zombie
%Cpu(s): 20.0 us, 1.7 sy, 0.0 ni, 77.7 id, 0.0 wa, 0.0 hi, 0.7 si, 0.0 st
KiB Mem: 7679792 total, 6646248 used, 1033544 free, 251688 buffers
KiB Swap: 0 total, 0 used, 0 free. 4115536 cached Mem
wa的指標,這個指標就代表着iowait,也就是CPU等待IO完成操作花費的時間占CPU的百分比。
如果iowait很大,那么就可以去看看實際的I/O操作情況是什么樣的。使用iostat,就能夠看到實際的硬盤讀寫情況。
$ iostat
avg-cpu: %user %nice %system %iowait %steal %idle
17.02 0.01 2.18 0.04 0.00 80.76
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 1.81 2.02 30.87 706768 10777408
tps指標,其實就對應着我們上面所說的硬盤的IOPS性能。而kB_read/s和kB_wrtn/s指標,就對應着我們的數據傳輸率的指標。
使用iotop找出到底是哪一個進程是這些I/O讀寫的來源。
$ iotop
Total DISK READ : 0.00 B/s | Total DISK WRITE : 15.75 K/s
Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 35.44 K/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
104 be/3 root 0.00 B/s 7.88 K/s 0.00 % 0.18 % [jbd2/sda1-8]
383 be/4 root 0.00 B/s 3.94 K/s 0.00 % 0.00 % rsyslogd -n [rs:main Q:Reg]
1514 be/4 www-data 0.00 B/s 3.94 K/s 0.00 % 0.00 % nginx: worker process
硬盤
機械硬盤
一塊機械硬盤是由盤面、磁頭和懸臂三個部件組成的。
首先,自然是盤面(Disk Platter)。盤面其實就是我們實際存儲數據的盤片。
我們的硬盤有5400轉的、7200轉的,乃至10000轉的。這個多少多少轉,指的就是盤面中間電機控制的轉軸的旋轉速度,英文單位叫RPM,也就是每分鍾的旋轉圈數(Rotations Per Minute)。
磁頭:數據並不能直接從盤面傳輸到總線上,而是通過磁頭,從盤面上讀取到,然后再通過電路信號傳輸給控制電路、接口,再到總線上的。通常,我們的一個盤面上會有兩個磁頭,分別在盤面的正反面。
懸臂鏈接在磁頭上,並且在一定范圍內會去把磁頭定位到盤面的某個特定的磁道(Track)上。
一個盤面通常是圓形的,由很多個同心圓組成,每一個同心圓都是一個磁道。每個磁道都有自己的一個編號。
一個磁道,會分成一個一個扇區(Sector)。上下平行的一個一個盤面的相同扇區呢,我們叫作一個柱面(Cylinder)。
讀取數據,其實就是兩個步驟。
- 把盤面旋轉到某一個位置。在這個位置上,我們的懸臂可以定位到整個盤面的某一個子區間。
- 把我們的懸臂移動到特定磁道的特定扇區,也就在這個“幾何扇區”里面,找到我們實際的扇區。找到之后,我們的磁頭會落下,就可以讀取到正對着扇區的數據。
進行一次硬盤上的隨機訪問,需要的時間由兩個部分組成。
第一個部分,叫作平均延時(Average Latency)。這個時間,其實就是把我們的盤面旋轉,把幾何扇區對准懸臂位置的時間。這個時間很容易計算,它其實就和我們機械硬盤的轉速相關。
隨機情況下,平均找到一個幾何扇區,我們需要旋轉半圈盤面。上面7200轉的硬盤,那么一秒里面,就可以旋轉240個半圈。那么,這個平均延時就是:1s / 240 = 4.17ms
第二個部分,叫作平均尋道時間(Average Seek Time),也就是在盤面選轉之后,我們的懸臂定位到扇區的的時間。我們現在用的HDD硬盤的平均尋道時間一般在4-10ms。
SSD硬盤
現在新的大容量SSD硬盤是由很多個裸片(Die)疊在一起的,就好像我們的機械硬盤把很多個盤面(Platter)疊放再一起一樣,這樣可以在同樣的空間下放下更多的容量。
一張裸片上可以放多個平面(Plane),一般一個平面上的存儲容量大概在GB級別。一個平面上面,會划分成很多個塊(Block),一般一個塊(Block)的存儲大小, 通常幾百KB到幾MB大小。一個塊里面,還會區分很多個頁(Page),就和我們內存里面的頁一樣,一個頁的大小通常是4KB。
對於SSD硬盤來說,數據的寫入叫作Program。寫入不能像機械硬盤一樣,通過覆寫(Overwrite)來進行的,而是要先去擦除(Erase),然后再寫入。
SSD的讀取和寫入的基本單位,不是一個比特(bit)或者一個字節(byte),而是一個頁(Page)。SSD的擦除單位必須按照塊來擦除。
SSD的使用壽命,其實是每一個塊(Block)的擦除的次數。
SLC的芯片,可以擦除的次數大概在10萬次,MLC就在1萬次左右,而TLC和QLC就只在幾千次了。
SSD讀寫的生命周期
白色代表這個頁從來沒有寫入過數據,綠色代表里面寫入的是有效的數據,紅色代表里面的數據,在我們的操作系統看來已經是刪除的了。
一開始,所有塊的每一個頁都是白色的。隨着我們開始往里面寫數據,里面的有些頁就變成了綠色。
然后,因為我們刪除了硬盤上的一些文件,所以有些頁變成了紅色。但是這些紅色的頁,並不能再次寫入數據。因為SSD硬盤不能單獨擦除一個頁,必須一次性擦除整個塊,所以新的數據,我們只能往后面的白色的頁里面寫。這些散落在各個綠色空間里面的紅色空洞,就好像硬盤碎片。
如果有哪一個塊的數據一次性全部被標紅了,那我們就可以把整個塊進行擦除。它就又會變成白色,可以重新一頁一頁往里面寫數據。
在快要沒有白色的空頁去寫入數據的時候,SSD會做一次類似於Windows里面“磁盤碎片整理”或者Java里面的“內存垃圾回收”工作。找一個紅色空洞最多的塊,把里面的綠色數據,挪到另一個塊里面去,然后把整個塊擦除,變成白色,可以重新寫入數據。
DMA
為什么要發明DMA技術?
就目前而言I/O速度如何提升,比起CPU,總還是太慢。如果我們對於I/O的操作,都是由CPU發出對應的指令,然后等待I/O設備完成操作之后返回,那CPU有大量的時間其實都是在等待I/O設備完成操作。
但是,這個CPU的等待,在很多時候,其實並沒有太多的實際意義。我們對於I/O設備的大量操作,其實都只是把內存里面的數據,傳輸到I/O設備而已。
因此,計算機工程師們,就發明了DMA技術,也就是直接內存訪問(Direct Memory Access)技術,來減少CPU等待的時間。
DMA有什么用?
本質上,DMA技術就是我們在主板上放一塊獨立的芯片。在進行內存和I/O設備的數據傳輸的時候,我們不再通過CPU來控制數據傳輸,而直接通過DMA控制器(DMA Controller,簡稱DMAC)。
當傳輸大量數據的時候,DMAC可以等數據到齊了,再發送信號,給到CPU去處理,而不是讓CPU在那里忙等待。
DMAC是怎么控制數據傳輸的?
DMAC其實也是一個特殊的I/O設備,它和CPU以及其他I/O設備一樣,通過連接到總線來進行實際的數據傳輸。總線上的設備呢,其實有兩種類型。一種我們稱之為主設備(Master),另外一種,我們稱之為從設備(Slave)。
想要主動發起數據傳輸,必須要是一個主設備才可以,CPU就是主設備。而我們從設備(比如硬盤)只能接受數據傳輸。
DMAC它既是一個主設備,又是一個從設備。對於CPU來說,它是一個從設備;對於硬盤這樣的IO設備來說呢,它又變成了一個主設備。
我們下面看一張圖:
- 首先,CPU還是作為一個主設備,向DMAC設備發起請求。這個請求,其實就是在DMAC里面修改配置寄存器。
- CPU修改DMAC的配置的時候,會告訴DMAC這樣幾個信息:
- 首先是源地址的初始值以及傳輸時候的地址增減方式。
所謂源地址,就是數據要從哪里傳輸過來。如果我們要從內存里面寫入數據到硬盤上,那么就是要讀取的數據在內存里面的地址。
* 其次是目標地址初始值和傳輸時候的地址增減方式。
* 第三個是要傳輸的數據長度
- 設置完這些信息之后,DMAC就會變成一個空閑的狀態(Idle)。
- 如果我們要從硬盤上往內存里面加載數據,這個時候,硬盤就會向DMAC發起一個數據傳輸請求。這個請求並不是通過總線,而是通過一個額外的連線。
- 然后,我們的DMAC需要再通過一個額外的連線響應這個申請。
- 於是,DMAC這個芯片,就向硬盤的接口發起要總線讀的傳輸請求。數據就從硬盤里面,讀到了DMAC的控制器里面。
- 然后,DMAC再向我們的內存發起總線寫的數據傳輸請求,把數據寫入到內存里面。
- DMAC會反復進行上面第6、7步的操作,直到DMAC的寄存器里面設置的數據長度傳輸完成。
- 數據傳輸完成之后,DMAC重新回到第3步的空閑狀態。