初識(shí)計(jì)算機(jī)組成原理-存儲(chǔ)與IO系統(tǒng)篇(一)

存儲(chǔ)器

寄存器

寄存器:我們可以把 CPU 看成計(jì)算機(jī)的“大腦”。我們思考的東西,就好比CPU中的寄存器。寄存器與其說(shuō)是存儲(chǔ)器,其實(shí)它更像是 CPU 本身的一部分,一般只能存放極其有限的信息。但是速度非常快,和 CPU 同步。

CPU Cache

CPU Cache:而我們大腦中的記憶,就好比 CPU Cache(CPU 高速緩存,我們常常簡(jiǎn)稱(chēng)為“緩存”)。CPU Cache 用的是一種叫作SRAM(靜態(tài)隨機(jī)存取存儲(chǔ)器)的芯片。

SRAM:SRAM 之所以被稱(chēng)為“靜態(tài)”存儲(chǔ)器,是因?yàn)橹灰幵谕姞顟B(tài),里面的數(shù)據(jù)就可以保持存在。而一旦斷電,里面的數(shù)據(jù)就會(huì)丟失了。在 SRAM 里面,一個(gè)比特的數(shù)據(jù),需要 6~8 個(gè)晶體管。所以 SRAM 的存儲(chǔ)密度不高。同樣的物理空間下,能夠存儲(chǔ)的數(shù)據(jù)有限。不過(guò),因?yàn)?SRAM 的電路簡(jiǎn)單,所以訪問(wèn)速度非???。

在 CPU 里,通常會(huì)有 L1、L2、L3 這樣三層高速緩存。每個(gè) CPU 核心都有一塊屬于自己的 L1高速緩存,Cpu和L1直接通信,其中L1也相對(duì)的比L2小很多,L1通常分成指令緩存和數(shù)據(jù)緩存,分開(kāi)存放 CPU 使用的指令和數(shù)據(jù),L2則不進(jìn)行區(qū)分。-> 指令和數(shù)據(jù)分開(kāi)存儲(chǔ)的這種思想也是來(lái)自于哈佛結(jié)構(gòu)。==L1的 Cache 往往就嵌在 CPU 核心的內(nèi)部。==

L2 的 Cache 同樣是每個(gè) CPU 核心都有的,不過(guò)它往往不在 CPU 核心的內(nèi)部。所以,L2 Cache 的訪問(wèn)速度會(huì)比 L1 稍微慢一些。 而 L3 Cache,則通常是多個(gè) CPU 核心共用的,尺寸會(huì)更大一些,訪問(wèn)速度自然也就更慢一些。

CPU 中的 L1 Cache 可以理解為我們的短期記憶,L2/L3 Cache 可以理解成長(zhǎng)期記憶,把內(nèi)存當(dāng)成我們擁有的書(shū)架或者書(shū)桌。 當(dāng)我們自己記憶中沒(méi)有資料的時(shí)候,可以從書(shū)桌或者書(shū)架上拿書(shū)來(lái)翻閱。這個(gè)過(guò)程中就相當(dāng)于,數(shù)據(jù)從內(nèi)存中加載到 CPU 的Cache和寄存器中,然后通過(guò)“大腦”,也就是 CPU,進(jìn)行處理和運(yùn)算。

內(nèi)存

DRAM 內(nèi)存用的是一種叫做DRAM的芯片,比起SRAM來(lái)說(shuō),它的電路更簡(jiǎn)單,密度也就更高,有更大的容量,而且它也比SRAM芯片便宜不少。它被稱(chēng)為“動(dòng)態(tài)”存儲(chǔ)器,因?yàn)?DRAM 需要靠不斷地“刷新”,才能保持?jǐn)?shù)據(jù)被存儲(chǔ)起來(lái)。DRAM 的一個(gè)比特,只需要一個(gè)晶體管和一個(gè)電容就能存儲(chǔ)。所以,DRAM 在同樣的物理空間下,能夠存儲(chǔ)的數(shù)據(jù)也就更多,也就是存儲(chǔ)的“密度”更大。但是,因?yàn)閿?shù)據(jù)是存儲(chǔ)在電容里的,電容會(huì)不斷漏電,所以需要定時(shí)刷新充電,才能保持?jǐn)?shù)據(jù)不丟失。DRAM 的數(shù)據(jù)訪問(wèn)電路和刷新電路都比 SRAM 更復(fù)雜,所以訪問(wèn)延時(shí)也就更長(zhǎng)。

硬盤(pán)

對(duì)于內(nèi)存來(lái)說(shuō),SSD(固態(tài)硬盤(pán)),HDD(硬盤(pán))這些被稱(chēng)為硬盤(pán)的外部存儲(chǔ)設(shè)備,就是公共圖書(shū)館。圖書(shū)館有更多的書(shū)(數(shù)據(jù))。

存儲(chǔ)器的層次結(jié)構(gòu)

image

整個(gè)存儲(chǔ)器的層次結(jié)構(gòu),其實(shí)都類(lèi)似于 SRAM 和 DRAM 在性能和價(jià)格上的差異。SRAM 更貴,速度更快。DRAM 更便宜,容量更大。SRAM 好像我們的大腦中的記憶,而 DRAM 就好像屬于我們自己的書(shū)桌。

L1 Cache,不僅受成本層面的限制,更受物理層面的限制。它不僅昂貴,而且它的訪問(wèn)速度和它到 CPU 的物理距離有關(guān)。芯片造得越大,總有部分離CPU的距離會(huì)變遠(yuǎn)。所以想要快,并不是靠多花錢(qián)就能解決的。

image

其中,容量越小的設(shè)備速度越快,而且,CPU 并不是直接和每一種存儲(chǔ)器設(shè)備打交道,而是每一種存儲(chǔ)器設(shè)備,只和它相鄰的存儲(chǔ)設(shè)備打交道。比如,CPU Cache 是從內(nèi)存里加載而來(lái)的,或者需要寫(xiě)回內(nèi)存,并不會(huì)直接寫(xiě)回?cái)?shù)據(jù)到硬盤(pán),也不會(huì)直接從硬盤(pán)加載數(shù)據(jù)到 CPU Cache 中,而是先加載到內(nèi)存,再?gòu)膬?nèi)存加載到 Cache 中。

image

在一臺(tái)實(shí)際的計(jì)算機(jī)里面,越是速度快的設(shè)備,容量就越小。

局部性原理

L1 Cache 一般 256K,L2 Cache 有個(gè) 1MB,L3 Cache 有 12M。

我們能不能既享受 CPU Cache 的速度,又享受內(nèi)存、硬盤(pán)巨大的容量和低廉的價(jià)格呢?因?yàn)檫@個(gè)問(wèn)題就可以引申出局部性原理,這個(gè)局部性原理包括時(shí)間局部性空間局部性這兩種策略。

  • 時(shí)間局部性:如果一個(gè)數(shù)據(jù)被訪問(wèn)了,那么推斷它在短時(shí)間內(nèi)還會(huì)被再次訪問(wèn)。那么這個(gè)數(shù)據(jù)就從在硬盤(pán)的數(shù)據(jù)庫(kù)讀取到內(nèi)存的緩存中來(lái)。這利用的就是時(shí)間局部性。
  • 空間局部性:如果一個(gè)數(shù)據(jù)被訪問(wèn)了,那么和它相鄰的數(shù)據(jù)也很快會(huì)被訪問(wèn)。這就好比我們的程序,在訪問(wèn)了數(shù)組的首項(xiàng)之后,多半會(huì)循環(huán)訪問(wèn)它的下一項(xiàng)。

有了時(shí)間局部性和空間局部性,我們不用再把所有數(shù)據(jù)都放在內(nèi)存里,也不用都放在 HDD 硬盤(pán)上,而是把訪問(wèn)次數(shù)多的數(shù)據(jù),放在貴但是快一點(diǎn)的存儲(chǔ)器里,把訪問(wèn)次數(shù)少的數(shù)據(jù),放在慢但是大一點(diǎn)的存儲(chǔ)器里。這樣組合的使用內(nèi)存、SSD 硬盤(pán)以及 HDD 硬盤(pán),使得我們可以用最低的成本提供實(shí)際所需要的數(shù)據(jù)存儲(chǔ)、管理和訪問(wèn)的需求。

局部性原理+不同層次存儲(chǔ)器組合

假設(shè)亞馬遜 6 億件商品,每件商品需要 4MB 的存儲(chǔ)空間,那么一共需要 2400TB( = 6 億 × 4MB)的數(shù)據(jù)存儲(chǔ)空間。

如果我們把數(shù)據(jù)都放在內(nèi)存里面,那就需要 3600 萬(wàn)美元( = 2400TB/1MB × 0.015 美元 = 3600 萬(wàn)美元)。但是,這 6 億件商品中,不是每一件商品都會(huì)被經(jīng)常訪問(wèn)。

如果我們只在內(nèi)存里放前 1% 的熱門(mén)商品,也就是 600 萬(wàn)件熱門(mén)商品,而把剩下的商品,放在機(jī)械式的 HDD 硬盤(pán)上,那么,我們需要的存儲(chǔ)成本就下降到 45.6 萬(wàn)美元( = 3600 萬(wàn)美元 × 1% + 2400TB / 1MB × 0.00004 美元),是原來(lái)成本的 1.3% 左右。

這里我們用的就是時(shí)間局部性。我們把有用戶(hù)訪問(wèn)過(guò)的數(shù)據(jù),加載到內(nèi)存中,一旦內(nèi)存里面放不下了,我們就把最長(zhǎng)時(shí)間沒(méi)有在內(nèi)存中被訪問(wèn)過(guò)的數(shù)據(jù),從內(nèi)存中移走,這個(gè)其實(shí)就是我們常用的LRU 緩存算法 。熱門(mén)商品被訪問(wèn)得多,就會(huì)始終被保留在內(nèi)存里,而冷門(mén)商品被訪問(wèn)得少,就只存放在 HDD 硬盤(pán)上,數(shù)據(jù)的讀取也都是直接訪問(wèn)硬盤(pán)。即使加載到內(nèi)存中,也會(huì)很快被移除。越是熱門(mén)的商品,越容易在內(nèi)存中找到,也就更好地利用了內(nèi)存的隨機(jī)訪問(wèn)性能。

那么只放 600 萬(wàn)件熱門(mén)商品是否可以滿(mǎn)足實(shí)際的線上服務(wù)請(qǐng)求則取決于LRU 緩存策略的緩存命中率了。

內(nèi)存的隨機(jī)訪問(wèn)請(qǐng)求需要 100ns。這也就意味著,在極限情況下,內(nèi)存 1 秒可以支持 1000 萬(wàn)次隨機(jī)訪問(wèn)。我們用了 24TB 內(nèi)存,如果 8G 一條的話(huà),意味著有 3000 條內(nèi)存,可以支持每秒 300 億次( = 24TB/8GB × 1s/100ns)訪問(wèn)。以亞馬遜 2017 年 3 億的用戶(hù)數(shù)來(lái)看,我們估算每天的活躍用戶(hù)為 1 億,這 1 億用戶(hù)每人平均會(huì)訪問(wèn) 100 個(gè)商品,那么平均每秒訪問(wèn)的商品數(shù)量,就是 12 萬(wàn)次。

但是如果數(shù)據(jù)沒(méi)有命中內(nèi)存,那么對(duì)應(yīng)的數(shù)據(jù)請(qǐng)求就要訪問(wèn)到 HDD 磁盤(pán)了。一塊 HDD 硬盤(pán)只能支撐每秒 100 次的隨機(jī)訪問(wèn),2400TB 的數(shù)據(jù),以 4TB 一塊磁盤(pán)來(lái)計(jì)算,有 600 塊磁盤(pán),也就是能支撐每秒 6 萬(wàn)次( = 2400TB/4TB × 1s/10ms )的隨機(jī)訪問(wèn)。

這樣算下來(lái),可以看出所有的商品訪問(wèn)請(qǐng)求,都直接到了 HDD 磁盤(pán),HDD 磁盤(pán)支撐不了這樣的壓力。我們至少要 50% 的緩存命中率,HDD 磁盤(pán)才能支撐剩余對(duì)應(yīng)的訪問(wèn)次數(shù)。不然的話(huà),我們要么選擇添加更多數(shù)量的 HDD 硬盤(pán),做到每秒 12 萬(wàn)次的隨機(jī)訪問(wèn),或者將 HDD 替換成 SSD 硬盤(pán),讓單個(gè)硬盤(pán)可以支持更多的隨機(jī)訪問(wèn)請(qǐng)求。

image

推算過(guò)程:

首選算你的貨物總共需要多少內(nèi)存,然后推算估計(jì)有多少熱點(diǎn)貨物需要訪問(wèn)加速,然后算需要加速的放到內(nèi)存中需要多少錢(qián),再加上其余貨物放到磁盤(pán)中多少錢(qián)。 -> 算出錢(qián)了

然后推算某一時(shí)刻的平均每秒訪問(wèn)量是多少,然后根據(jù)內(nèi)存隨機(jī)訪問(wèn)的請(qǐng)求時(shí)間,算1秒可以支撐多少次訪問(wèn),然后根據(jù)熱點(diǎn)數(shù)據(jù)花費(fèi)內(nèi)存的用量計(jì)算總的內(nèi)存一秒可以支撐多少次請(qǐng)求。再計(jì)算非熱點(diǎn)數(shù)據(jù)總的磁盤(pán)一秒鐘可以支撐多少次請(qǐng)求來(lái)判斷能否滿(mǎn)足某一時(shí)刻用戶(hù)的訪問(wèn)峰值。 -> 算出夠不夠

以上這個(gè)例子可以把貨物看成人,內(nèi)存看成飛機(jī),磁盤(pán)看成綠皮火車(chē)。先算多少人,再算多少人需要快速運(yùn)達(dá),然后算飛機(jī)錢(qián)多少,火車(chē)錢(qián)多少,然后算需要運(yùn)的人峰值是多少,然后算一架飛機(jī)運(yùn)一趟的時(shí)間,算一下一天可以運(yùn)多少個(gè)人,然后算所有飛機(jī)的一天可以運(yùn)多少,就很好理解了。

最后兩者綜合,在夠的情況下,省錢(qián)。

通過(guò)快速估算的方式,來(lái)判斷這個(gè)添加緩存的策略是否能夠滿(mǎn)足我們的需求,以及在估算的服務(wù)器負(fù)載的情況下,需要規(guī)劃多少硬件設(shè)備。這個(gè)“估算 + 規(guī)劃”的能力,是每一個(gè)期望成長(zhǎng)為架構(gòu)師的工程師,必須掌握的能力。

高速緩存 - 上

高速緩存 = CPU Cache = L1 Cache + L2 Cache + L3 Cache

今天來(lái)看,一次內(nèi)存的訪問(wèn),大約需要 120 個(gè) CPU Cycle

CPU 需要執(zhí)行的指令、需要訪問(wèn)的數(shù)據(jù),都在這個(gè)速度不到自己1% 的內(nèi)存里。可以如下圖看到,隨著時(shí)間變遷,CPU 和內(nèi)存之間的性能差距越來(lái)越大

image

從 CPU Cache 被加入到現(xiàn)有的 CPU 里開(kāi)始,內(nèi)存中的指令、數(shù)據(jù),會(huì)被加載到 L1-L3 Cache 中,而不是直接由 CPU 訪問(wèn)內(nèi)存去拿。在 95% 的情況下,CPU 都只需要訪問(wèn) L1-L3 Cache,從里面讀取指令和數(shù)據(jù),而無(wú)需訪問(wèn)內(nèi)存

image

現(xiàn)代 CPU 中大量的空間已經(jīng)被 SRAM 占據(jù),圖中用紅色框出的部分就是 CPU 的 L3 Cache 芯片。

int[] arr = new int[64 * 1024 * 1024];// 256MB = 4*64*2^20


// 循環(huán) 1
for (int i = 0; i < arr.length; i++) arr[i] *= 3;


// 循環(huán) 2
for (int i = 0; i < arr.length; i += 16) arr[i] *= 3
==========================================
循環(huán)1:50毫秒
循環(huán)2:46毫秒

上面這段代碼,按我們的正常的思考,第二段代碼中被執(zhí)行的數(shù)組元素是第一段的1/16,也即只進(jìn)行了第一段代碼的1/16次的乘法運(yùn)算,按理說(shuō)應(yīng)該花費(fèi)時(shí)間也應(yīng)該盡量差距在1個(gè)數(shù)量級(jí),10倍之上。但卻相差僅4毫秒。

有了上面CPU Cache和內(nèi)存訪問(wèn)速度差異的學(xué)習(xí),可以得知運(yùn)行程序的時(shí)間主要花在了將對(duì)應(yīng)的數(shù)據(jù)從內(nèi)存中讀取出來(lái),加載到 CPU Cache 里。CPU 從內(nèi)存中讀取數(shù)據(jù)到 CPU Cache 的過(guò)程中,是一小塊一小塊來(lái)讀取數(shù)據(jù)的,而不是按照單個(gè)數(shù)組元素來(lái)讀取數(shù)據(jù)的。這樣一小塊一小塊的數(shù)據(jù),在 CPU Cache 里面,我們把它叫作 Cache Line(緩存塊)。

在我們?nèi)粘J褂玫?Intel 服務(wù)器或者 PC 里,通常 Cache Line 的大小通常是 64 字節(jié)。

如果下面這段不懂可以學(xué)完橫線下面的內(nèi)容再回來(lái)看就明白了,Block中的數(shù)據(jù)在往CPU Line中存的時(shí)候是一段數(shù)據(jù)一段數(shù)據(jù)的存了,每16個(gè)數(shù)取一次,其實(shí)這16個(gè)數(shù)都會(huì)因?yàn)槟?個(gè)被讀的數(shù)而一同加載到CPU Line中。

而在上面的循環(huán) 2 里面,我們每隔 16 個(gè)整型數(shù)計(jì)算一次,16 個(gè)整型數(shù)正好是 64 個(gè)字節(jié)。于是,循環(huán) 1 和循環(huán) 2,需要把同樣數(shù)量的 Cache Line 的數(shù)據(jù)從內(nèi)存中讀取到 CPU Cache 中,最終兩個(gè)程序花費(fèi)的時(shí)間就差別不大了。


Cache 的數(shù)據(jù)結(jié)構(gòu)和讀取過(guò)程

現(xiàn)代 CPU 進(jìn)行數(shù)據(jù)讀取的時(shí)候,無(wú)論數(shù)據(jù)是否已經(jīng)存儲(chǔ)在 Cache 中,CPU 始終會(huì)首先訪問(wèn) Cache。只有當(dāng) CPU 在 Cache 中找不到數(shù)據(jù)的時(shí)候,才會(huì)去訪問(wèn)內(nèi)存,并將讀取到的數(shù)據(jù)寫(xiě)入 Cache 之中。當(dāng)時(shí)間局部性原理起作用后,這個(gè)最近剛剛被訪問(wèn)的數(shù)據(jù),會(huì)很快再次被訪問(wèn)。而 Cache 的訪問(wèn)速度遠(yuǎn)遠(yuǎn)快于內(nèi)存,這樣,CPU 花在等待內(nèi)存訪問(wèn)上的時(shí)間就大大變短了。 -> 在各類(lèi)基準(zhǔn)測(cè)試(Benchmark)和實(shí)際應(yīng)用場(chǎng)景中,CPU Cache 的命中率通常能達(dá)到 95% 以上。

image

Cache 的數(shù)據(jù)結(jié)構(gòu)和訪問(wèn)邏輯

直接映射:CPU 訪問(wèn)內(nèi)存數(shù)據(jù),是一小塊一小塊數(shù)據(jù)來(lái)讀取的。對(duì)于讀取內(nèi)存中的數(shù)據(jù),我們首先拿到的是數(shù)據(jù)所在的內(nèi)存塊的地址。 而直接映射采用的策略,就是確保任何一個(gè)內(nèi)存塊的地址,始終映射到一個(gè)固定的 CPU Cache 地址(Cache Line)。而這個(gè)映射關(guān)系,通常用 mod 運(yùn)算(求余運(yùn)算)來(lái)實(shí)現(xiàn)。

比如說(shuō),我們的主內(nèi)存被分成 0~31 號(hào)這樣 32 個(gè)塊。我們一共有 8 個(gè)緩存塊。用戶(hù)想要訪問(wèn)第 21 號(hào)內(nèi)存塊。如果 21 號(hào)內(nèi)存塊內(nèi)容在緩存塊中的話(huà),它一定在 5 號(hào)緩存塊(21 mod 8 = 5)中。

實(shí)際計(jì)算中,通常會(huì)把緩存塊的數(shù)量設(shè)置成 2 的 N 次方。這樣在計(jì)算取模的時(shí)候,可以直接取地址的低 N 位,也就是二進(jìn)制里面的后幾位。比如這里的 8 個(gè)緩存塊,就是 2 的 3 次方。那么,在對(duì) 21 取模的時(shí)候,可以對(duì) 21 的 2 進(jìn)制表示 10101 取地址的==低三位==,也就是 101,對(duì)應(yīng)的 5,就是對(duì)應(yīng)的緩存塊地址。

image

如圖,很多很內(nèi)存的數(shù)據(jù)都對(duì)應(yīng)的在被讀取的時(shí)候要往Cache Line中存,但一個(gè)Cache Line才64字節(jié),所以肯定存不下那么多Block的數(shù)據(jù),所以在Cache Line中會(huì)存儲(chǔ)一個(gè)組標(biāo)記,這個(gè)組標(biāo)記會(huì)記錄,當(dāng)前緩存塊內(nèi)存儲(chǔ)的數(shù)據(jù)對(duì)應(yīng)的內(nèi)存塊,(==到底是Block 12的數(shù)據(jù)還是Block 13的數(shù)據(jù),同一時(shí)刻一個(gè)Cache Line只對(duì)應(yīng)的取一個(gè)Block的數(shù)據(jù)==) 而緩存塊本身的地址表示的訪問(wèn)的內(nèi)存地址的低N位。就像上面的例子,21 的低 3 位 101,緩存塊本身的地址已經(jīng)涵蓋了對(duì)應(yīng)的部分信息、對(duì)應(yīng)的組標(biāo)記,只需要再記錄 21 剩余的高 2 位的信息,也就是 10 就可以了。

image

Cache Line除了組標(biāo)記還有另兩個(gè)數(shù)據(jù),一個(gè)是從內(nèi)存加載來(lái)的實(shí)際數(shù)組,另一個(gè)是有效位。它是用來(lái)標(biāo)記,對(duì)應(yīng)的緩存塊中的數(shù)據(jù)是否是有效的,確保不是機(jī)器剛剛啟動(dòng)時(shí)候的空數(shù)據(jù)。如果有效位是 0,無(wú)論其中的組標(biāo)記和 Cache Line 里的數(shù)據(jù)內(nèi)容是什么,CPU 都不會(huì)管這些數(shù)據(jù),而要直接訪問(wèn)內(nèi)存,重新加載數(shù)據(jù)。

CPU 在讀取數(shù)據(jù)的時(shí)候,并不是要讀取一整個(gè) Block,而是讀取一個(gè)他需要的整數(shù)。這樣的數(shù)據(jù),我們叫作 CPU 里的一個(gè)字。具體是哪個(gè)字,就用這個(gè)字在整個(gè)Block里面的位置來(lái)決定。這個(gè)位置,叫作偏移量(Offset)。

如果內(nèi)存中的數(shù)據(jù)已經(jīng)在 CPU Cache 里了,那一個(gè)內(nèi)存地址的訪問(wèn),就會(huì)經(jīng)歷這樣 4 個(gè)步驟:

  1. 根據(jù)內(nèi)存地址的低位,計(jì)算在 Cache 中的索引;
  2. 判斷有效位,確認(rèn) Cache 中的數(shù)據(jù)是有效的;
  3. 對(duì)比內(nèi)存訪問(wèn)地址的高位,和 Cache 中的組標(biāo)記,確認(rèn) Cache 中的數(shù)據(jù)就是我們要訪問(wèn)的內(nèi)存數(shù)據(jù),從 Cache Line 中讀取到對(duì)應(yīng)的數(shù)據(jù)塊(Data Block);
  4. 根據(jù)內(nèi)存地址的 Offset 位,從 Data Block 中,讀取希望讀取到的字。

如果在 2、3 這兩個(gè)步驟中,CPU 發(fā)現(xiàn),Cache 中的數(shù)據(jù)并不是要訪問(wèn)的內(nèi)存地址的數(shù)據(jù),那 CPU 就會(huì)訪問(wèn)內(nèi)存,并把對(duì)應(yīng)的 Block Data 更新到 Cache Line 中,同時(shí)更新對(duì)應(yīng)的有效位和組標(biāo)記的數(shù)據(jù)。

高速緩存 - 下

Java 內(nèi)存模型(JMM,Java Memory Model)

JMM 只是 Java 虛擬機(jī)這個(gè)進(jìn)程級(jí)虛擬機(jī)里的一個(gè)內(nèi)存模型,但是這個(gè)內(nèi)存模型,和計(jì)算機(jī)組成里的 CPU、高速緩存和主內(nèi)存組合在一起的硬件體系非常相似。理解了 JMM,可以讓你很容易理解計(jì)算機(jī)組成里 CPU、高速緩存和主內(nèi)存之間的關(guān)系。

volatile:它會(huì)確保我們對(duì)于這個(gè)變量的讀取和寫(xiě)入,都一定會(huì)同步到主內(nèi)存里,而不是從 Cache 里面讀取。

volatile關(guān)鍵字在用C語(yǔ)言編寫(xiě)嵌入式軟件里面用得很多,不使用volatile關(guān)鍵字的代碼比使用volatile關(guān)鍵字的代碼效率要高一些,但就無(wú)法保證數(shù)據(jù)的一致性。volatile的本意是告訴編譯器,此變量的值是易變的,每次讀寫(xiě)該變量的值時(shí)務(wù)必從該變量的內(nèi)存地址中讀取或?qū)懭耄荒転榱诵适褂脤?duì)一個(gè)“臨時(shí)”變量的讀寫(xiě)來(lái)代替對(duì)該變量的直接讀寫(xiě)。編譯器看到了volatile關(guān)鍵字,就一定會(huì)生成內(nèi)存訪問(wèn)指令,每次讀寫(xiě)該變量就一定會(huì)執(zhí)行內(nèi)存訪問(wèn)指令直接讀寫(xiě)該變量。若是沒(méi)有volatile關(guān)鍵字,編譯器為了效率,只會(huì)在循環(huán)開(kāi)始前使用讀內(nèi)存指令將該變量讀到寄存器中,之后在循環(huán)內(nèi)都是用寄存器訪問(wèn)指令來(lái)操作這個(gè)“臨時(shí)”變量,在循環(huán)結(jié)束后再使用內(nèi)存寫(xiě)指令將這個(gè)寄存器中的“臨時(shí)”變量寫(xiě)回內(nèi)存。在這個(gè)過(guò)程中,如果內(nèi)存中的這個(gè)變量被別的因素(其他線程、中斷函數(shù)、信號(hào)處理函數(shù)、DMA控制器、其他硬件設(shè)備)所改變了,就產(chǎn)生數(shù)據(jù)不一致的問(wèn)題。另外,寄存器訪問(wèn)指令的速度要比內(nèi)存訪問(wèn)指令的速度快,這里說(shuō)的內(nèi)存也包括緩存,也就是說(shuō)內(nèi)存訪問(wèn)指令實(shí)際上也有可能訪問(wèn)的是緩存里的數(shù)據(jù),但即便如此,還是不如訪問(wèn)寄存器快的。緩存對(duì)于編譯器也是透明的,編譯器使用內(nèi)存讀寫(xiě)指令時(shí)只會(huì)認(rèn)為是在讀寫(xiě)內(nèi)存,內(nèi)存和緩存間的數(shù)據(jù)同步由CPU保證。

案例

版本一代碼:

public class VolatileTest {
    private static volatile int COUNTER = 0;

    public static void main(String[] args) {
        new ChangeListener().start();
        new ChangeMaker().start();
    }

    static class ChangeListener extends Thread {
        @Override
        public void run() {
            int threadValue = COUNTER;
            while ( threadValue < 5){// 完全忙等待
                if( threadValue!= COUNTER){
                    System.out.println("Got Change for COUNTER : " + COUNTER + "");
                    threadValue= COUNTER;
                }
            }
        }
    }

    static class ChangeMaker extends Thread{
        @Override
        public void run() {
            int threadValue = COUNTER;
            while (COUNTER <5){
                System.out.println("Incrementing COUNTER to : " + (threadValue+1) + "");
                COUNTER = ++threadValue;
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) { e.printStackTrace(); }
            }
        }
    }
}
===================================
Incrementing COUNTER to : 1
Got Change for COUNTER : 1
Incrementing COUNTER to : 2
Got Change for COUNTER : 2
Incrementing COUNTER to : 3
Got Change for COUNTER : 3
Incrementing COUNTER to : 4
Got Change for COUNTER : 4
Incrementing COUNTER to : 5
Got Change for COUNTER : 5

版本一執(zhí)行:

ChangeListener 這個(gè)線程運(yùn)行的任務(wù)很簡(jiǎn)單。它先取到 COUNTER 當(dāng)前的值,然后一直監(jiān)聽(tīng)著這個(gè) COUNTER 的值。一旦 COUNTER 的值發(fā)生了變化,就把新的值通過(guò) println 打印出來(lái)。直到 COUNTER 的值達(dá)到 5 為止。這個(gè)監(jiān)聽(tīng)的過(guò)程,通過(guò)一個(gè)永不停歇的 while 循環(huán)的忙等待來(lái)實(shí)現(xiàn)。

ChangeMaker 這個(gè)線程運(yùn)行的任務(wù)同樣很簡(jiǎn)單。它同樣是取到 COUNTER 的值,在 COUNTER 小于 5 的時(shí)候,每隔 500 毫秒,就讓 COUNTER 自增 1。在自增之前,通過(guò) println 方法把自增后的值打印出來(lái)。

最后,在 main 函數(shù)里,我們分別啟動(dòng)這兩個(gè)線程,來(lái)看一看這個(gè)程序的執(zhí)行情況。程序的輸出結(jié)果并不讓人意外。ChangeMaker 函數(shù)會(huì)一次一次將 COUNTER 從 0 增加到 5。因?yàn)檫@個(gè)自增是每 500 毫秒一次,而 ChangeListener 去監(jiān)聽(tīng) COUNTER 是忙等待的,所以每一次自增都會(huì)被 ChangeListener 監(jiān)聽(tīng)到,然后對(duì)應(yīng)的結(jié)果就會(huì)被打印出來(lái)。

解釋?zhuān)?/p>

剛剛第一個(gè)使用了 volatile 關(guān)鍵字的例子里,因?yàn)樗袛?shù)據(jù)的讀和寫(xiě)都來(lái)自主內(nèi)存。那么自然地,我們的 ChangeMaker 和 ChangeListener 之間,看到的 COUNTER 值就是一樣的。


版本二代碼:

把定義的 COUNTER 這個(gè)變量的的 volatile 關(guān)鍵字給去掉。

private static int COUNTER = 0;
====================================
Incrementing COUNTER to : 1
Incrementing COUNTER to : 2
Incrementing COUNTER to : 3
Incrementing COUNTER to : 4
Incrementing COUNTER to : 5

版本二執(zhí)行:

ChangeMaker 還是能正常工作的,每隔 500ms 仍然能夠?qū)?COUNTER 自增 1。但是,奇怪的事情在 ChangeListener 上發(fā)生了,ChangeListener 不再工作了。在 ChangeListener 眼里,它似乎一直覺(jué)得 COUNTER 的值還是一開(kāi)始的 0。似乎 COUNTER 的變化,對(duì)于 ChangeListener 徹底“隱身”了。

解釋?zhuān)?/p>

去掉了 volatile 關(guān)鍵字。這個(gè)時(shí)候,ChangeListener 又是一個(gè)忙等待的循環(huán),它嘗試不停地獲取 COUNTER 的值,這樣就會(huì)從當(dāng)前線程的“Cache”里面獲取。于是,這個(gè)線程就沒(méi)有時(shí)間從主內(nèi)存里面同步更新后的 COUNTER 值。這樣,它就一直卡死在 COUNTER=0 的死循環(huán)上了。


版本三代碼:

不再讓 ChangeListener 進(jìn)行完全的忙等待,而是在 while 循環(huán)里面,小小地等待上 5 毫秒。

static class ChangeListener extends Thread {
    @Override
    public void run() {
        int threadValue = COUNTER;
        while ( threadValue < 5){
            if( threadValue!= COUNTER){
                System.out.println("Sleep 5ms, Got Change for COUNTER : " + COUNTER + "");
                threadValue= COUNTER;
            }
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) { e.printStackTrace(); }
        }
    }
}
========================================
Incrementing COUNTER to : 1
Sleep 5ms, Got Change for COUNTER : 1
Incrementing COUNTER to : 2
Sleep 5ms, Got Change for COUNTER : 2
Incrementing COUNTER to : 3
Sleep 5ms, Got Change for COUNTER : 3
Incrementing COUNTER to : 4
Sleep 5ms, Got Change for COUNTER : 4
Incrementing COUNTER to : 5
Sleep 5ms, Got Change for COUNTER : 5

版本三執(zhí)行:

雖然 COUNTER 變量,仍然沒(méi)有設(shè)置 volatile 這個(gè)關(guān)鍵字,但是 ChangeListener 似乎“睡醒了”。在通過(guò) Thread.sleep(5) 在每個(gè)循環(huán)里“睡上“5 毫秒之后,ChangeListener 又能夠正常取到 COUNTER 的值了。

解釋?zhuān)?/p>

雖然還是沒(méi)有使用 volatile關(guān)鍵字,但是短短5ms的Thead.Sleep給了這個(gè)線程喘息之機(jī)。既然這個(gè)線程沒(méi)有這么忙了,它也就有機(jī)會(huì)把最新的數(shù)據(jù)從主內(nèi)存同步到自己的高速緩存里面了(很有意思)。于是,ChangeListener 在下一次查看 COUNTER 值的時(shí)候,就能看到 ChangeMaker 造成的變化了。

寫(xiě)入策略

我們現(xiàn)在用的 Intel CPU,通常都是多核的的。每一個(gè) CPU 核(四核八線程)里面,都有獨(dú)立屬于自己的 L1、L2 的 Cache,然后再有多個(gè) CPU 核共用的 L3 的 Cache、主內(nèi)存。

因?yàn)?CPU Cache 的訪問(wèn)速度要比主內(nèi)存快很多,而在 CPU Cache 里面,L1/L2 的 Cache 也要比 L3 的 Cache 快。所以,CPU 始終都是盡可能地從CPUCache中去獲取數(shù)據(jù),而不是每一次都要從主內(nèi)存里面去讀取數(shù)據(jù)。

image

這個(gè)層級(jí)結(jié)構(gòu),就好像我們?cè)?Java 內(nèi)存模型里面,每一個(gè)線程都有屬于自己的線程棧。==線程在讀取 COUNTER 的數(shù)據(jù)的時(shí)候,其實(shí)是從本地的線程棧的 Cache 副本里面讀取數(shù)據(jù),而不是從主內(nèi)存里面讀取數(shù)據(jù)==。如果我們對(duì)于數(shù)據(jù)僅僅只是讀,問(wèn)題還不大。但如果寫(xiě)就有兩個(gè)問(wèn)題了。

  1. 寫(xiě)入 Cache 的性能也比寫(xiě)入主內(nèi)存要快,那我們寫(xiě)入的數(shù)據(jù),到底應(yīng)該寫(xiě)到 Cache 里還是主內(nèi)存呢
  2. 如果我們直接寫(xiě)入到主內(nèi)存里,Cache 里的數(shù)據(jù)是否會(huì)失效呢?

對(duì)應(yīng)解決方式有兩種寫(xiě)策略:

  1. 寫(xiě)直達(dá)
image

最簡(jiǎn)單的一種寫(xiě)入策略,叫作寫(xiě)直達(dá)。在這個(gè)策略里,每一次數(shù)據(jù)都要寫(xiě)入到主內(nèi)存里面。在寫(xiě)直達(dá)的策略里面,寫(xiě)入前,我們會(huì)先去判斷數(shù)據(jù)是否已經(jīng)在 Cache 里面了。如果數(shù)據(jù)已經(jīng)在 Cache 里面了,我們先把數(shù)據(jù)寫(xiě)入更新到 Cache 里面,再寫(xiě)入到主內(nèi)存里面;如果數(shù)據(jù)不在 Cache 里,我們就只更新主內(nèi)存。

寫(xiě)直達(dá)的這個(gè)策略很直觀,但是問(wèn)題也很明顯,那就是這個(gè)策略很慢。無(wú)論數(shù)據(jù)是不是在 Cache 里面,我們都需要把數(shù)據(jù)寫(xiě)到主內(nèi)存里面。這個(gè)方式就有點(diǎn)兒像我們上面用 volatile 關(guān)鍵字,始終都要把數(shù)據(jù)同步到主內(nèi)存里面。

  1. 寫(xiě)回
image

由于我們?nèi)プx數(shù)據(jù)也是默認(rèn)從 Cache 里面加載,所以我們不用把所有的寫(xiě)入都同步到主存中。 這個(gè)策略里,不再是每次都把數(shù)據(jù)寫(xiě)入到主內(nèi)存,而是只寫(xiě)到 CPU Cache 里。只有當(dāng) CPU Cache 里面的數(shù)據(jù)要被“替換”的時(shí)候,我們才把數(shù)據(jù)寫(xiě)入到主內(nèi)存里面去

寫(xiě)回策略的過(guò)程是這樣的:如果發(fā)現(xiàn)我們要寫(xiě)入的數(shù)據(jù),就在 CPU Cache 里面,那么我們就只是更新 CPU Cache 里面的數(shù)據(jù)。同時(shí),我們會(huì)標(biāo)記 CPU Cache 里的這個(gè) Block 是臟的。所謂臟的,就是指這個(gè)時(shí)候,我們的 CPU Cache 里面的這個(gè) Block 的數(shù)據(jù),和主內(nèi)存是不一致的。

如果我們發(fā)現(xiàn),我們要寫(xiě)入的數(shù)據(jù)所對(duì)應(yīng)的 Cache Block 里,放的是別的內(nèi)存地址的數(shù)據(jù),那么我們就要看一看,那個(gè) Cache Block 里面的數(shù)據(jù)有沒(méi)有被標(biāo)記成臟的。如果是臟的話(huà),我們要先把這個(gè) Cache Block 里面的數(shù)據(jù),寫(xiě)入到主內(nèi)存里面。然后,再把當(dāng)前要寫(xiě)入的數(shù)據(jù),寫(xiě)入到 Cache 里,同時(shí)把 Cache Block 標(biāo)記成臟的。如果 Block 里面的數(shù)據(jù)沒(méi)有被標(biāo)記成臟的,那么我們直接把數(shù)據(jù)寫(xiě)入到 Cache 里面,然后再把 Cache Block 標(biāo)記成臟的就好了。 - 臟數(shù)據(jù),和內(nèi)存數(shù)據(jù)不同

在用了寫(xiě)回這個(gè)策略之后,我們?cè)诩虞d內(nèi)存數(shù)據(jù)到 Cache 里面的時(shí)候,也要多出一步同步臟 Cache 的動(dòng)作。如果加載內(nèi)存里面的數(shù)據(jù)到 Cache 的時(shí)候,發(fā)現(xiàn) Cache Block 里面有臟標(biāo)記,我們也要先把 Cache Block 里的數(shù)據(jù)寫(xiě)回到主內(nèi)存,才能加載數(shù)據(jù)覆蓋掉 Cache。

可以看到,在寫(xiě)回這個(gè)策略里,如果我們大量的操作,都能夠命中緩存。那么大部分時(shí)間里,我們都不需要讀寫(xiě)主內(nèi)存,自然性能會(huì)比寫(xiě)直達(dá)的效果好很多。

CPU Cache - MESI協(xié)議

現(xiàn)代計(jì)算機(jī)在不能提升 CPU 的主頻之后,找到了另一種提升 CPU 吞吐率的方法就是多核 CPU 技術(shù)。但多核 CPU 里的每一個(gè) CPU 核,都有獨(dú)立的屬于自己的 L1 Cache 和 L2 Cache。多個(gè) CPU 之間,只是共用 L3 Cache 和主內(nèi)存。所以因?yàn)?CPU 的每個(gè)核各有各的緩存,互相之間的操作又是各自獨(dú)立的,就會(huì)帶來(lái)緩存一致性問(wèn)題。

image

緩存一致性:比如,iPhone 價(jià)格出現(xiàn)了浮動(dòng),我們要把 iPhone 最新的價(jià)格更新到內(nèi)存里。為了性能問(wèn)題,CPU 寫(xiě)入 CPU Cache 的策略選擇的是寫(xiě)回策略。先把數(shù)據(jù)寫(xiě)入到 L2 Cache 里面,然后把 Cache Block 標(biāo)記成臟的。這個(gè)時(shí)候,數(shù)據(jù)其實(shí)并沒(méi)有被同步到 L3 Cache 或者主內(nèi)存里。1 號(hào)核心希望在這個(gè) Cache Block 要被交換出去的時(shí)候,數(shù)據(jù)才寫(xiě)入到主內(nèi)存里。

當(dāng)計(jì)算機(jī)是單核的時(shí)候,不會(huì)出現(xiàn)這個(gè)問(wèn)題。但如果是多核的,此時(shí)其他CPU核心從內(nèi)存里面去讀取 iPhone 的價(jià)格,結(jié)果讀到的是一個(gè)錯(cuò)誤的價(jià)格,這是因?yàn)椋琲Phone 的價(jià)格剛剛被 1 號(hào)核心更新過(guò)。但是這個(gè)更新的信息,只出現(xiàn)在 1 號(hào)核心的 L2 Cache 里,而沒(méi)有出現(xiàn)在 2 號(hào)核心的 L2 Cache 或者主內(nèi)存里面。 這個(gè)問(wèn)題,就是所謂的緩存一致性問(wèn)題,1 號(hào)核心和 2 號(hào)核心的緩存,在此時(shí)是不一致的。

所以我們就需要同步兩個(gè)不同核心里面的緩存數(shù)據(jù),需要滿(mǎn)足的條件:

  1. 寫(xiě)傳播: 在一個(gè) CPU 核心里,我們的 Cache 數(shù)據(jù)更新,必須能夠傳播到其他的對(duì)應(yīng)節(jié)點(diǎn)的 Cache Line 里。
  2. 事務(wù)的串行化: 我們?cè)谝粋€(gè) CPU 核心里面的讀取和寫(xiě)入,在其他的節(jié)點(diǎn)看起來(lái),順序是一樣的。
image

如圖,就是事務(wù)的串行化未做到的例子。CPU核心3和核心4讀取到的CPU1和2的事務(wù)順序不一致,即便寫(xiě)傳播做到了,但數(shù)據(jù)仍然出錯(cuò)了。

事務(wù)的串行化,不僅僅是緩存一致性中所必須的。比如,我們平時(shí)所用到的系統(tǒng)當(dāng)中,最需要保障事務(wù)串行化的就是數(shù)據(jù)庫(kù)。多個(gè)不同的連接去訪問(wèn)數(shù)據(jù)庫(kù)的時(shí)候,我們必須保障事務(wù)的串行化。

對(duì)于事務(wù)的串行化,需要做到兩點(diǎn):

  1. 一個(gè) CPU 核心對(duì)于數(shù)據(jù)的操作,需要同步通信給到其他 CPU 核心。
  2. 如果兩個(gè) CPU 核心里有同一個(gè)數(shù)據(jù)的 Cache,那么對(duì)于這個(gè) Cache 數(shù)據(jù)的更新,需要有一個(gè)“鎖”的概念。只有拿到了對(duì)應(yīng) Cache Block 的“鎖”之后,才能進(jìn)行對(duì)應(yīng)的數(shù)據(jù)更新。

要解決緩存一致性問(wèn)題,首先要解決的是多個(gè) CPU 核心之間的數(shù)據(jù)傳播問(wèn)題。最常見(jiàn)的解決方案叫做“總線嗅探”機(jī)制。這個(gè)策略,本質(zhì)上就是把所有的讀寫(xiě)請(qǐng)求都通過(guò)總線(Bus)廣播給所有的 CPU 核心,然后讓各個(gè)核心去“嗅探”這些請(qǐng)求,再根據(jù)本地的情況進(jìn)行響應(yīng)。

MESI

寫(xiě)失效

MESI 協(xié)議,是一種叫作寫(xiě)失效的協(xié)議。在寫(xiě)失效協(xié)議里,只有一個(gè) CPU 核心負(fù)責(zé)寫(xiě)入數(shù)據(jù),其他的核心,只是同步讀取到這個(gè)寫(xiě)入。在這個(gè) CPU 核心寫(xiě)入 Cache 之后,它會(huì)去廣播一個(gè)“失效”請(qǐng)求告訴所有其他的 CPU 核心。其他的 CPU 核心,只是去判斷自己是否也有一個(gè)“失效”版本的 Cache Block,然后把這個(gè)也標(biāo)記成失效的就好了。

寫(xiě)廣播

寫(xiě)廣播:相對(duì)于寫(xiě)失效協(xié)議,還有一種叫作寫(xiě)廣播的協(xié)議,一個(gè)寫(xiě)入請(qǐng)求廣播到所有的 CPU 核心,同時(shí)更新各個(gè)核心里的Cache。 但這個(gè)協(xié)議相對(duì)寫(xiě)失效需要占用更多的總線帶寬。

MESI - 對(duì) Cache Line 的四個(gè)不同的標(biāo)記:

  1. M:代表已修改(Modified):這個(gè) Cache Block 里面的內(nèi)容我們已經(jīng)更新過(guò)了,但是還沒(méi)有寫(xiě)回到主內(nèi)存里面。
  2. E:代表==獨(dú)占==(Exclusive):無(wú)論是獨(dú)占狀態(tài)還是共享狀態(tài),Cache Block 里面的數(shù)據(jù)和主內(nèi)存里面的數(shù)據(jù)是一致的。
  3. S:代表==共享==(Shared):無(wú)論是獨(dú)占狀態(tài)還是共享狀態(tài),,Cache Block 里面的數(shù)據(jù)和主內(nèi)存里面的數(shù)據(jù)是一致的。
  4. I:代表已失效(Invalidated):這個(gè) Cache Block 里面的數(shù)據(jù)已經(jīng)失效了,我們不可以相信這個(gè) Cache Block 里面的數(shù)據(jù)。

獨(dú)占”和“共享”這兩個(gè)狀態(tài)的差別:在==獨(dú)占==狀態(tài)下,對(duì)應(yīng)的 Cache Line 只加載到了當(dāng)前 CPU 核所擁有的 Cache 里。其他的 CPU 核,并沒(méi)有加載對(duì)應(yīng)的數(shù)據(jù)到自己的 Cache 里。這個(gè)時(shí)候,如果要向獨(dú)占的 Cache Block 寫(xiě)入數(shù)據(jù),我們可以自由地寫(xiě)入數(shù)據(jù),而不需要告知其他 CPU 核。

在獨(dú)占狀態(tài)下的數(shù)據(jù),如果收到了一個(gè)來(lái)自于總線的==讀取對(duì)應(yīng)緩存的請(qǐng)求==,它就會(huì)變成共享狀態(tài)。這個(gè)共享狀態(tài)是因?yàn)?,這個(gè)時(shí)候,另外一個(gè) CPU 核心,也把對(duì)應(yīng)的 Cache Block,從內(nèi)存里面加載到了自己的 Cache 里來(lái)。

而在共享狀態(tài)下,因?yàn)橥瑯拥臄?shù)據(jù)在多個(gè) CPU 核心的 Cache 里都有。所以,當(dāng)我們想要更新 Cache 里面的數(shù)據(jù)的時(shí)候,不能直接修改,而是要先向所有的其他 CPU 核心廣播一個(gè)請(qǐng)求,要求先把其他 CPU 核心里面的 Cache,都變成無(wú)效的狀態(tài),然后再更新當(dāng)前 Cache 里面的數(shù)據(jù)。這個(gè)廣播操作,一般叫作 RFO,==也就是獲取當(dāng)前對(duì)應(yīng) Cache Block 數(shù)據(jù)的所有權(quán)==。

這個(gè)操作有點(diǎn)兒像我們?cè)诙嗑€程里面用到的讀寫(xiě)鎖。在共享狀態(tài)下,大家都可以并行去讀對(duì)應(yīng)的數(shù)據(jù)。但是如果要寫(xiě),我們就需要通過(guò)一個(gè)鎖,獲取當(dāng)前寫(xiě)入位置的所有權(quán)。

整個(gè) MESI 的狀態(tài),可以用一個(gè)有限狀態(tài)機(jī)來(lái)表示它的狀態(tài)流轉(zhuǎn)。對(duì)于不同狀態(tài)觸發(fā)的事件操作,可能來(lái)自于當(dāng)前 CPU 核心,也可能來(lái)自總線里其他 CPU 核心廣播出來(lái)的信號(hào)。

image

想要實(shí)現(xiàn)緩存一致性,關(guān)鍵是要滿(mǎn)足兩點(diǎn)。第一個(gè)是寫(xiě)傳播,也就是在一個(gè) CPU 核心寫(xiě)入的內(nèi)容,需要傳播到其他 CPU 核心里。更重要的是第二點(diǎn),保障事務(wù)的串行化,才能保障我們的數(shù)據(jù)是真正一致的,我們的程序在各個(gè)不同的核心上運(yùn)行的結(jié)果也是一致的。這個(gè)特性不僅在 CPU 的緩存層面很重要,在數(shù)據(jù)庫(kù)層面更加重要。

內(nèi)存

內(nèi)存是五大組成部分里面的存儲(chǔ)器,我們的指令和數(shù)據(jù),都需要先加載到內(nèi)存里面,才會(huì)被 CPU 拿去執(zhí)行。

我們?nèi)粘J褂玫?Linux 或者 Windows 操作系統(tǒng)下,程序并不能直接訪問(wèn)物理內(nèi)存。我們的內(nèi)存需要被分成固定大小的頁(yè),然后再通過(guò)虛擬內(nèi)存地址到物理內(nèi)存地址的地址轉(zhuǎn)換,才能到達(dá)實(shí)際存放數(shù)據(jù)的物理內(nèi)存位置。而我們的程序看到的內(nèi)存地址,都是虛擬內(nèi)存地址。

簡(jiǎn)單頁(yè)表

虛擬內(nèi)存地址,映射到物理內(nèi)存地址,是通過(guò)一張映射表來(lái)實(shí)現(xiàn)一對(duì)一對(duì)應(yīng)的。這個(gè)映射表,在計(jì)算機(jī)里面,就叫作頁(yè)表。頁(yè)表這個(gè)地址轉(zhuǎn)換的辦法,會(huì)把一個(gè)內(nèi)存地址分成頁(yè)號(hào)和偏移量?jī)蓚€(gè)部分。

以一個(gè)32位的內(nèi)存地址為例,前面的高位,就是內(nèi)存地址的頁(yè)號(hào)。后面的低位,就是內(nèi)存地址里面的偏移量。做地址轉(zhuǎn)換的頁(yè)表,只需要保留虛擬內(nèi)存地址的頁(yè)號(hào)和物理內(nèi)存地址的頁(yè)號(hào)之間的映射關(guān)系就可以了。同一個(gè)頁(yè)里面的內(nèi)存,在物理層面是連續(xù)的。以一個(gè)頁(yè)的大小是 4K 比特為例,我們需要 20 位的高位,12 位的低位。

image

內(nèi)存地址轉(zhuǎn)換步驟:

  1. 把虛擬內(nèi)存地址,切分成頁(yè)號(hào)和偏移量的組合;
  2. 從頁(yè)表里面,查詢(xún)出虛擬頁(yè)號(hào),對(duì)應(yīng)的物理頁(yè)號(hào);
  3. 直接拿物理頁(yè)號(hào),加上前面的偏移量,就得到了物理內(nèi)存地址。

這樣表示的問(wèn)題:32 位的內(nèi)存地址空間,頁(yè)表一共需要記錄 2^20 個(gè)到物理頁(yè)號(hào)的映射關(guān)系。這個(gè)存儲(chǔ)關(guān)系,就好比一個(gè) 2^20 大小的數(shù)組。一個(gè)頁(yè)號(hào)是完整的 32 位的 4 字節(jié),這樣一個(gè)頁(yè)表就需要 4MB 的空間(這只是32位)。但這個(gè)空間我們每一個(gè)進(jìn)程,都有屬于自己獨(dú)立的虛擬內(nèi)存地址空間。這也就意味著,每一個(gè)進(jìn)程都需要這樣一個(gè)頁(yè)表。不管我們這個(gè)進(jìn)程,是個(gè)本身只有幾 KB 大小的程序,還是需要幾 GB 的內(nèi)存空間,都需要這樣一個(gè)頁(yè)表。

image

多級(jí)頁(yè)表

由于大部分進(jìn)程所占用的內(nèi)存是有限的,所以需要的頁(yè)也是有限的。沒(méi)必要存下這 2^20 個(gè)物理頁(yè)表。

在整個(gè)進(jìn)程的內(nèi)存地址空間,通常是“兩頭實(shí)、中間空”。在程序運(yùn)行的時(shí)候,內(nèi)存地址從頂部往下,不斷分配占用的棧的空間。而堆的空間,內(nèi)存地址則是從底部往上,是不斷分配占用的。所以,在一個(gè)實(shí)際的程序進(jìn)程里面,虛擬內(nèi)存占用的地址空間,通常是兩段連續(xù)的空間。而不是完全散落的隨機(jī)的內(nèi)存地址。而多級(jí)頁(yè)表,就特別適合這樣的內(nèi)存地址分布。

4 級(jí)的多級(jí)頁(yè)表為例,同樣一個(gè)虛擬內(nèi)存地址,偏移量的部分和上面簡(jiǎn)單頁(yè)表一樣不變,但是原先的頁(yè)號(hào)部分,把它拆成四段,從高到低,分成 4 級(jí)到 1 級(jí)這樣 4 個(gè)頁(yè)表索引。

image

對(duì)應(yīng)的,==一個(gè)進(jìn)程會(huì)有一個(gè) 4 級(jí)頁(yè)表==。我們先通過(guò) 4 級(jí)頁(yè)表索引,找到 4 級(jí)頁(yè)表里面對(duì)應(yīng)的條目(Entry)。這個(gè)條目里存放的是一張 3 級(jí)頁(yè)表所在的位置。4 級(jí)頁(yè)面里面的每一個(gè)條目,都對(duì)應(yīng)著一張 3 級(jí)頁(yè)表,所以我們可能有多張 3 級(jí)頁(yè)表。 找到對(duì)應(yīng)這張 3 級(jí)頁(yè)表之后,我們用 3 級(jí)索引去找到對(duì)應(yīng)的 3 級(jí)索引的條目。3 級(jí)索引的條目再會(huì)指向一個(gè) 2 級(jí)頁(yè)表。同樣的,2 級(jí)頁(yè)表里我們可以用 2 級(jí)索引指向一個(gè) 1 級(jí)頁(yè)表。而最后一層的 1 級(jí)頁(yè)表里面的條目,對(duì)應(yīng)的數(shù)據(jù)內(nèi)容就是物理頁(yè)號(hào)了。在拿到了物理頁(yè)號(hào)之后,我們同樣可以用“頁(yè)號(hào) + 偏移量”的方式,來(lái)獲取最終的物理內(nèi)存地址。

我們可能有很多張 1 級(jí)頁(yè)表、2 級(jí)頁(yè)表,乃至 3 級(jí)頁(yè)表。但是,因?yàn)閷?shí)際的虛擬內(nèi)存空間通常是連續(xù)的,我們很可能只需要很少的 2 級(jí)頁(yè)表,甚至只需要 1 張 3 級(jí)頁(yè)表就夠了。

多級(jí)頁(yè)表就像一個(gè)多叉樹(shù)的數(shù)據(jù)結(jié)構(gòu),所以我們常常稱(chēng)它為頁(yè)表樹(shù)。因?yàn)樘摂M內(nèi)存地址分布的連續(xù)性,樹(shù)的第一層節(jié)點(diǎn)的指針,很多就是空的,也就不需要有對(duì)應(yīng)的子樹(shù)了。所謂不需要子樹(shù),其實(shí)就是不需要對(duì)應(yīng)的 2 級(jí)、3 級(jí)的頁(yè)表。找到最終的物理頁(yè)號(hào),就好像通過(guò)一個(gè)特定的訪問(wèn)路徑,走到樹(shù)最底層的葉子節(jié)點(diǎn)。

image

以這樣的分成 4 級(jí)的多級(jí)頁(yè)表來(lái)看,每一級(jí)如果都用 5 個(gè)比特表示。那么每一張 1 級(jí)的頁(yè)表,只需要 2^5=32 個(gè)條目。如果每個(gè)條目還是 4 個(gè)字節(jié),那么一共需要 128 個(gè)字節(jié)。而一個(gè) 1 級(jí)索引表,對(duì)應(yīng) 32 個(gè) 4KiB 的也就是 16KB 的大小。一個(gè)填滿(mǎn)的 2 級(jí)索引表,對(duì)應(yīng)的就是 32 個(gè) 1 級(jí)索引表,也就是 512KB 的大小。

我們可以一起來(lái)測(cè)算一下,一個(gè)進(jìn)程如果占用了 1MB 的內(nèi)存空間,分成了 2 個(gè) 512KB 的連續(xù)空間。那么,它一共需要 2 個(gè)獨(dú)立的、填滿(mǎn)的 2 級(jí)索引表,也就意味著 64 個(gè) 1 級(jí)索引表,2 個(gè)獨(dú)立的 3 級(jí)索引表,1 個(gè) 4 級(jí)索引表。一共需要 69 個(gè)索引表,每個(gè) 128 字節(jié),大概就是 9KB 的空間。比起 4MB 來(lái)說(shuō),只有差不多 1/500。

多級(jí)頁(yè)表就像是一顆樹(shù)。因?yàn)橐粋€(gè)進(jìn)程的內(nèi)存地址相對(duì)集中和連續(xù),所以采用這種頁(yè)表樹(shù)的方式,可以大大節(jié)省頁(yè)表所需要的空間。而因?yàn)槊總€(gè)進(jìn)程都需要一個(gè)獨(dú)立的頁(yè)表,這個(gè)空間的節(jié)省是非??捎^的

不過(guò),多級(jí)頁(yè)表雖然節(jié)約了我們的存儲(chǔ)空間,卻帶來(lái)了時(shí)間上的開(kāi)銷(xiāo),所以它其實(shí)是一個(gè)“以時(shí)間換空間”的策略。原本我們進(jìn)行一次地址轉(zhuǎn)換,只需要訪問(wèn)一次內(nèi)存就能找到物理頁(yè)號(hào),算出物理內(nèi)存地址。但是,用了 4 級(jí)頁(yè)表,我們就需要訪問(wèn) 4 次內(nèi)存,才能找到物理頁(yè)號(hào)了。

加速地址轉(zhuǎn)換:TLB

由于“地址轉(zhuǎn)換”是一個(gè)非常高頻的動(dòng)作,“地址轉(zhuǎn)換”的性能就變得至關(guān)重要了。同時(shí)CPU執(zhí)行的指令和數(shù)據(jù)也放在內(nèi)存中,所以內(nèi)存安全問(wèn)題 也需要考慮。

上述多級(jí)頁(yè)表的內(nèi)存訪問(wèn)方式,由于內(nèi)存的多次訪問(wèn),所以在性能上表現(xiàn)很差。由于程序所需要使用的指令,都順序存放在虛擬內(nèi)存里面。我們執(zhí)行的指令,也是一條條順序執(zhí)行下去的。也就是說(shuō),我們對(duì)于指令地址的訪問(wèn),存在“空間局部性”和“時(shí)間局部性”,而需要訪問(wèn)的數(shù)據(jù)也是一樣的。我們連續(xù)執(zhí)行了 5 條指令。因?yàn)閮?nèi)存地址都是連續(xù)的,所以這 5 條指令通常都在同一個(gè)“虛擬頁(yè)”里。

因此,這連續(xù) 5 次的內(nèi)存地址轉(zhuǎn)換,其實(shí)都來(lái)自于同一個(gè)虛擬頁(yè)號(hào),那我們就可以通過(guò)“加個(gè)緩存”把之前的內(nèi)存轉(zhuǎn)換地址緩存下來(lái),使得我們不需要反復(fù)去訪問(wèn)內(nèi)存來(lái)進(jìn)行內(nèi)存地址轉(zhuǎn)換。

image

于是,計(jì)算機(jī)工程師們專(zhuān)門(mén)在 CPU 里放了一塊緩存芯片。這塊緩存芯片我們稱(chēng)之為TLB,全稱(chēng)是地址變換高速緩沖。 這塊緩存存放了之前已經(jīng)進(jìn)行過(guò)地址轉(zhuǎn)換的查詢(xún)結(jié)果。這樣,當(dāng)同樣的虛擬地址需要進(jìn)行地址轉(zhuǎn)換的時(shí)候,我們可以直接在 TLB 里面查詢(xún)結(jié)果,而不需要多次訪問(wèn)內(nèi)存來(lái)完成一次轉(zhuǎn)換。

TLB 和我們前面講的 CPU 的高速緩存類(lèi)似,可以分成指令的 TLB 和數(shù)據(jù)的 TLB,也就是ITLBDTLB。同樣的,我們也可以根據(jù)大小對(duì)它進(jìn)行分級(jí),變成 L1、L2 這樣多層的 TLB。 對(duì)應(yīng)的,在寫(xiě)回?cái)?shù)據(jù)時(shí)也需要和CPU Cache 一樣需要用臟標(biāo)記這樣的標(biāo)記位,來(lái)實(shí)現(xiàn)“寫(xiě)回”這樣緩存管理策略。

image

為了性能,我們整個(gè)內(nèi)存轉(zhuǎn)換過(guò)程也要由硬件來(lái)執(zhí)行。在 CPU 芯片里面,我們封裝了內(nèi)存管理單元MMU芯片,用來(lái)完成地址轉(zhuǎn)換。和 TLB 的訪問(wèn)和交互,都是由這個(gè) MMU 控制的。

安全性與內(nèi)存保護(hù)

正常情況下,我們已經(jīng)通過(guò)虛擬內(nèi)存地址和物理內(nèi)存地址的區(qū)分,隔離了各個(gè)進(jìn)程。但由于CPU及操作系統(tǒng)執(zhí)行邏輯的復(fù)雜,仍然會(huì)有很多漏洞。

可執(zhí)行空間保護(hù)

這個(gè)機(jī)制是說(shuō),我們對(duì)于一個(gè)進(jìn)程使用的內(nèi)存,只把其中的指令部分設(shè)置成“可執(zhí)行”的,對(duì)于其他部分,比如數(shù)據(jù)部分,不給予“可執(zhí)行”的權(quán)限。因?yàn)闊o(wú)論是指令,還是數(shù)據(jù),在我們的 CPU 看來(lái),都是二進(jìn)制的數(shù)據(jù)。我們直接把數(shù)據(jù)部分拿給 CPU,如果這些數(shù)據(jù)解碼后,也能變成一條合理的指令,其實(shí)就是可執(zhí)行的。

黑客對(duì)應(yīng)方法:在程序的數(shù)據(jù)區(qū)里,放入一些要執(zhí)行的指令編碼后的數(shù)據(jù),然后找到一個(gè)辦法,讓 CPU 去把它們當(dāng)成指令去加載,那 CPU 就能執(zhí)行他們想要執(zhí)行的指令了。

對(duì)應(yīng)方法:對(duì)于進(jìn)程里內(nèi)存空間的執(zhí)行權(quán)限進(jìn)行控制,可以使得 CPU 只能執(zhí)行指令區(qū)域的代碼。 對(duì)于數(shù)據(jù)區(qū)域的內(nèi)容,即使找到了其他漏洞想要加載成指令來(lái)執(zhí)行,也會(huì)因?yàn)闆](méi)有權(quán)限而被阻擋掉。

與此類(lèi)似的典型的網(wǎng)絡(luò)攻擊SQL注入就是如此:如果服務(wù)端執(zhí)行的 SQL 腳本是通過(guò)字符串拼裝出來(lái)的,那么在 Web 請(qǐng)求里面?zhèn)鬏數(shù)膮?shù)就可以藏下一些我們想要執(zhí)行的 SQL,讓服務(wù)器執(zhí)行一些我們沒(méi)有想到過(guò)的 SQL 語(yǔ)句。這樣的結(jié)果就是,或者破壞了數(shù)據(jù)庫(kù)里的數(shù)據(jù),或者被人拖庫(kù)泄露了數(shù)據(jù)。

地址空間布局隨機(jī)化

原先我們一個(gè)進(jìn)程的內(nèi)存布局空間是固定的,所以任何第三方很容易就能知道指令在哪里,程序棧在哪里,數(shù)據(jù)在哪里,堆又在哪里。這個(gè)其實(shí)為想要搞破壞的人創(chuàng)造了很大的便利。而地址空間布局隨機(jī)化這個(gè)機(jī)制,就是讓這些區(qū)域的位置不再固定,==在內(nèi)存空間隨機(jī)去分配這些進(jìn)程里不同部分所在的內(nèi)存空間地址==,讓破壞者猜不出來(lái)。猜不出來(lái)呢,自然就沒(méi)法找到想要修改的內(nèi)容的位置。如果只是隨便做點(diǎn)修改,程序只會(huì) crash 掉,而不會(huì)去執(zhí)行計(jì)劃之外的代碼。

image

與此類(lèi)似的典型的網(wǎng)絡(luò)攻擊防護(hù)手段是:用戶(hù)賬戶(hù)密碼密文保存的時(shí)候,通過(guò)為每個(gè)用戶(hù)分配一個(gè)隨機(jī)的salt來(lái)進(jìn)行特定的密文加密的方式。

為了節(jié)約頁(yè)表所需要的內(nèi)存空間,我們采用了多級(jí)頁(yè)表這樣一個(gè)數(shù)據(jù)結(jié)構(gòu)。但是,多級(jí)頁(yè)表雖然節(jié)省空間了,卻要花費(fèi)更多的時(shí)間去多次訪問(wèn)內(nèi)存。于是,我們?cè)趯?shí)際進(jìn)行地址轉(zhuǎn)換的 MMU 旁邊放上了 TLB 這個(gè)用于地址轉(zhuǎn)換的緩存。TLB 也像 CPU Cache 一樣,分成指令和數(shù)據(jù)部分,也可以進(jìn)行 L1、L2 這樣的分層。

通過(guò)讓數(shù)據(jù)空間里面的內(nèi)容不能執(zhí)行,可以避免了類(lèi)似于“注入攻擊”的攻擊方式。通過(guò)隨機(jī)化內(nèi)存空間的分配,可以避免讓一個(gè)進(jìn)程的內(nèi)存里面的代碼,被推測(cè)出來(lái),從而不容易被攻擊。

總線

計(jì)算機(jī)里其實(shí)有很多不同的硬件設(shè)備,如果我們有 N 個(gè)不同的設(shè)備,他們之間需要各自單獨(dú)連接,那么系統(tǒng)復(fù)雜度就會(huì)變成 N^2 。為了簡(jiǎn)化系統(tǒng)的復(fù)雜度,我們就引入了總線,把這個(gè) N^2 的復(fù)雜度,變成一個(gè) N 的復(fù)雜度。所以設(shè)計(jì)出一個(gè)公用的線路,CPU 想要和什么設(shè)備通信,通信的指令是什么,對(duì)應(yīng)的數(shù)據(jù)是什么,都發(fā)送到這個(gè)線路上;設(shè)備要向 CPU 發(fā)送什么信息呢,也發(fā)送到這個(gè)線路上。 這個(gè)線路就好像一個(gè)高速公路,各個(gè)設(shè)備和其他設(shè)備之間,不需要單獨(dú)建公路,只建一條小路通向這條高速公路就好了。

image

總線,其實(shí)就是一組線路。我們的 CPU、內(nèi)存以及輸入和輸出設(shè)備,都是通過(guò)這組線路,進(jìn)行相互間通信的。其實(shí),對(duì)應(yīng)的設(shè)計(jì)思路,在軟件開(kāi)發(fā)中也是非常常見(jiàn)的。我們?cè)谧龃笮拖到y(tǒng)開(kāi)發(fā)的過(guò)程中,經(jīng)常會(huì)用到一種叫作事件總線的設(shè)計(jì)模式

在事件總線這個(gè)設(shè)計(jì)模式里,各個(gè)模塊觸發(fā)對(duì)應(yīng)的事件,并把事件對(duì)象發(fā)送到總線上。也就是說(shuō),每個(gè)模塊都是一個(gè)發(fā)布者。而各個(gè)模塊也會(huì)把自己注冊(cè)到總線上,去監(jiān)聽(tīng)總線上的事件,并根據(jù)事件的對(duì)象類(lèi)型或者是對(duì)象內(nèi)容,來(lái)決定自己是否要進(jìn)行特定的處理或者響應(yīng)。-> 我也往上發(fā),我也從上面拿來(lái)處理。

image

這樣的設(shè)計(jì)下,注冊(cè)在總線上的各個(gè)模塊就是松耦合的。模塊互相之間并沒(méi)有依賴(lài)關(guān)系。無(wú)論代碼的維護(hù),還是未來(lái)的擴(kuò)展,都會(huì)很方便。

現(xiàn)代的 Intel CPU 的體系結(jié)構(gòu)里面,通常有好幾條總線。 首先,CPU 和內(nèi)存以及高速緩存通信的總線,這里面通常有兩種總線。這種方式,我們稱(chēng)之為雙獨(dú)立總線。CPU 里,有一個(gè)快速的本地總線,以及一個(gè)速度相對(duì)較慢的前端總線。 現(xiàn)代的 CPU 里,通常有專(zhuān)門(mén)的高速緩存芯片。這里的高速本地總線,就是用來(lái)和CPU Cache通信的。而前端總線,則是用來(lái)和主內(nèi)存以及輸入輸出設(shè)備通信的。有時(shí)候,我們會(huì)把本地總線也叫作后端總線,和前面的前端總線對(duì)應(yīng)起來(lái)。而前端總線也有很多其他名字,比如處理器總線、內(nèi)存總線。

本地總線 = 后端總線,前端總線 = 處理器總線 = 內(nèi)存總線 = 系統(tǒng)總線

image

CPU 里面的北橋芯片,把我們上面說(shuō)的前端總線,一分為三,變成了三個(gè)總線。我們的前端總線,其實(shí)就是系統(tǒng)總線,CPU 里面的內(nèi)存接口,直接和系統(tǒng)總線通信,然后系統(tǒng)總線再接入一個(gè) I/O 橋接器。這個(gè) I/O 橋接器,一邊接入了我們的內(nèi)存總線,使得我們的 CPU 和內(nèi)存通信;另一邊呢,又接入了一個(gè) I/O 總線,用來(lái)連接 I/O 設(shè)備。

事實(shí)上,真實(shí)的計(jì)算機(jī)里,這個(gè)前端總線層面拆分得更細(xì)。根據(jù)不同的設(shè)備,還會(huì)分成獨(dú)立的 PCI 總線、ISA 總線等等。

image

物理層面,其實(shí)我們完全可以把總線看作一組“電線”,它通常有三類(lèi)線路:

  1. 數(shù)據(jù)線,用來(lái)傳輸實(shí)際的數(shù)據(jù)信息,也就是實(shí)際上了公交車(chē)的“人”。
  2. 地址線,用來(lái)確定到底把數(shù)據(jù)傳輸?shù)侥睦锶?,是?nèi)存的某個(gè)位置,還是某一個(gè) I/O 設(shè)備。這個(gè)其實(shí)就相當(dāng)于拿了個(gè)紙條,寫(xiě)下了上面的人要下車(chē)的站點(diǎn)。
  3. 控制線,用來(lái)控制對(duì)于總線的訪問(wèn)。雖然我們把總線比喻成了一輛公交車(chē)。那么有人想要做公交車(chē)的時(shí)候,需要告訴公交車(chē)司機(jī),這個(gè)就是我們的控制信號(hào)。

盡管總線減少了設(shè)備之間的耦合,也降低了系統(tǒng)設(shè)計(jì)的復(fù)雜度,但同時(shí)也帶來(lái)了一個(gè)新問(wèn)題,那就是總線不能同時(shí)給多個(gè)設(shè)備提供通信功能

我們的總線是很多個(gè)設(shè)備公用的,那多個(gè)設(shè)備都想要用總線,我們就需要有一個(gè)機(jī)制,去決定這種情況下,到底把總線給哪一個(gè)設(shè)備用。這個(gè)機(jī)制,就叫作總線裁決 。

輸入輸出設(shè)備

輸入輸出設(shè)備,并不只是一個(gè)設(shè)備。大部分的輸入輸出設(shè)備,都有兩個(gè)組成部分。第一個(gè)是它的==接口==,第二個(gè)才是==實(shí)際的 I/O 設(shè)備==。硬件設(shè)備并不是直接接入到總線上和 CPU 通信的,而是通過(guò)接口,用接口連接到總線上,再通過(guò)總線和 CPU 通信。

CPU - 總線 - 接口 - IO 設(shè)備

平時(shí)聽(tīng)說(shuō)的并行接口、串行接口、USB接口,都是計(jì)算機(jī)主板上內(nèi)置的各個(gè)接口。我們的實(shí)際硬件設(shè)備,比如,使用并口的打印機(jī)、使用串口的老式鼠標(biāo)或者使用 USB 接口的 U 盤(pán),都要插入到這些接口上,才能和 CPU 工作以及通信的。

image

如圖,SATA 硬盤(pán),上面的整個(gè)綠色電路板和黃色的齒狀部分就是接口電路,黃色齒狀的就是主板內(nèi)置的和IO設(shè)備對(duì)接的接口,綠色的電路板就是控制電路。

接口本身就是一塊電路板。CPU 其實(shí)不是和實(shí)際的硬件設(shè)備打交道,而是和這個(gè)接口電路板打交道。==設(shè)備里面有三類(lèi)寄存器,其實(shí)都在這個(gè)設(shè)備的接口電路上,而不在實(shí)際的設(shè)備上。==

除了內(nèi)置在主板上的接口之外,有些接口可以集成在設(shè)備上。 所以這種設(shè)備,需要通過(guò)一個(gè)線纜,把集成了接口的設(shè)備連接到主板上去。把接口和實(shí)際設(shè)備分離,這個(gè)做法實(shí)際上來(lái)自于計(jì)算機(jī)走向開(kāi)放架構(gòu) 的時(shí)代。

如果你用的是 Windows 操作系統(tǒng),你可以打開(kāi)設(shè)備管理器,里面有各種各種的 Devices(設(shè)備)、Controllers(控制器)、Adaptors(適配器)。這些,其實(shí)都是對(duì)于輸入輸出設(shè)備不同角度的描述。被叫作 Devices,看重的是實(shí)際的 I/O 設(shè)備本身。被叫作 Controllers,看重的是輸入輸出設(shè)備接口里面的控制電路。而被叫作 Adaptors,則是看重接口作為一個(gè)適配器后面可以插上不同的實(shí)際設(shè)備。

CPU是如何控制IO設(shè)備的

無(wú)論是內(nèi)置在主板上的接口,還是集成在設(shè)備上的接口,除了三類(lèi)寄存器之外,還有對(duì)應(yīng)的控制電路。正是通過(guò)這個(gè)控制電路,CPU 才能通過(guò)向這個(gè)接口電路板傳輸信號(hào),來(lái)控制實(shí)際的硬件。

image

三類(lèi)寄存器作用:

  1. 狀態(tài)寄存器:就是告訴了我們的 CPU,現(xiàn)在設(shè)備已經(jīng)在工作了,所以這個(gè)時(shí)候,CPU 你再發(fā)送數(shù)據(jù)或者命令過(guò)來(lái),都是沒(méi)有用的。直到前面的動(dòng)作已經(jīng)完成,狀態(tài)寄存器重新變成了 ready狀態(tài),我們的 CPU 才能發(fā)送下一個(gè)字符和命令。
  2. 數(shù)據(jù)寄存器:CPU 向 I/O 設(shè)備寫(xiě)入需要傳輸?shù)臄?shù)據(jù),比如要打印的內(nèi)容是“GeekTime”,我們就要先發(fā)送一個(gè)“G”給到對(duì)應(yīng)的 I/O 設(shè)備。
  3. 命令寄存器:CPU 發(fā)送一個(gè)命令,告訴打印機(jī),要進(jìn)行打印工作。這個(gè)時(shí)候,打印機(jī)里面的控制電路會(huì)做兩個(gè)動(dòng)作。第一個(gè),是去設(shè)置我們的狀態(tài)寄存器里面的狀態(tài),把狀態(tài)設(shè)置成not-ready。第二個(gè),就是實(shí)際操作打印機(jī)進(jìn)行打印。

在實(shí)際情況中,打印機(jī)里通常不只有數(shù)據(jù)寄存器,還會(huì)有數(shù)據(jù)緩沖區(qū)。CPU 不會(huì)真的一個(gè)字符一個(gè)字符這樣交給打印機(jī)去打印的,而是一次性把整個(gè)文檔傳輸?shù)酱蛴C(jī)的內(nèi)存或者數(shù)據(jù)緩沖區(qū)里面一起打印的。

信號(hào)和地址

CPU 和 I/O 設(shè)備的通信,一樣是通過(guò) CPU 支持的機(jī)器指令來(lái)執(zhí)行的。

精簡(jiǎn)指令集 MIPS,的機(jī)器指令的分類(lèi),并沒(méi)有一種專(zhuān)門(mén)的和 I/O 設(shè)備通信的指令類(lèi)型。它和IO設(shè)備的通信方式和訪問(wèn)主內(nèi)存一樣,使用“內(nèi)存地址”。為了讓已經(jīng)足夠復(fù)雜的 CPU 盡可能簡(jiǎn)單,計(jì)算機(jī)會(huì)把 I/O 設(shè)備的各個(gè)寄存器,以及 I/O 設(shè)備內(nèi)部的內(nèi)存地址,都映射到主內(nèi)存地址空間里來(lái)。主內(nèi)存的地址空間里,會(huì)給不同的 I/O 設(shè)備預(yù)留一段一段的內(nèi)存地址。CPU 想要和這些 I/O 設(shè)備通信的時(shí)候呢,就往這些地址發(fā)送數(shù)據(jù)。這些地址信息,就是通過(guò)上一講的地址線來(lái)發(fā)送的,而對(duì)應(yīng)的數(shù)據(jù)信息呢,自然就是通過(guò)數(shù)據(jù)線來(lái)發(fā)送的了。

而I/O 設(shè)備呢,就會(huì)監(jiān)控地址線,并且在 CPU 往自己地址發(fā)送數(shù)據(jù)的時(shí)候,把對(duì)應(yīng)的數(shù)據(jù)線里面?zhèn)鬏斶^(guò)來(lái)的數(shù)據(jù),接入到對(duì)應(yīng)的設(shè)備里面的寄存器和內(nèi)存里面來(lái)。CPU 無(wú)論是向 I/O 設(shè)備發(fā)送命令、查詢(xún)狀態(tài)還是傳輸數(shù)據(jù),都可以通過(guò)這樣的方式。這種方式呢,叫作內(nèi)存映射(MMIO)。

image

精簡(jiǎn)指令集 MIPS 的 CPU 特別簡(jiǎn)單,所以這里只有 MMIO。而有 2000 多個(gè)指令的 Intel X86 架構(gòu)的計(jì)算機(jī),設(shè)計(jì)了專(zhuān)門(mén)的和 I/O 設(shè)備通信的指令,也就是 in 和 out 指令。

Intel CPU 雖然也支持 MMIO,不過(guò)它還可以通過(guò)特定的指令,來(lái)支持端口映射 I/O(PMIO)或者也可以叫獨(dú)立輸入輸出。PMIO 的通信方式和 MMIO 核心的區(qū)別在于,PMIO 里面訪問(wèn)的設(shè)備地址,不再是在內(nèi)存地址空間里面,而是一個(gè)專(zhuān)門(mén)的端口。這個(gè)端口并不是指一個(gè)硬件上的插口,而是和 CPU 通信的一個(gè)抽象概念。

無(wú)論是 PMIO 還是 MMIO,CPU 都會(huì)傳送一條二進(jìn)制的數(shù)據(jù),給到 I/O 設(shè)備的對(duì)應(yīng)地址。設(shè)備自己本身的接口電路,再去解碼這個(gè)數(shù)據(jù)。解碼之后的數(shù)據(jù),就會(huì)變成設(shè)備支持的一條指令,再去通過(guò)控制電路去操作實(shí)際的硬件設(shè)備。對(duì)于 CPU 來(lái)說(shuō),它并不需要關(guān)心設(shè)備本身能夠支持哪些操作。它要做的,只是在總線上傳輸一條條數(shù)據(jù)就好了。 這個(gè),類(lèi)似設(shè)計(jì)模式里面的 Command 模式。在總線上傳輸?shù)?,是一個(gè)個(gè)數(shù)據(jù)對(duì)象,然后各個(gè)接受這些對(duì)象的設(shè)備,再去根據(jù)對(duì)象內(nèi)容,進(jìn)行實(shí)際的解碼和命令執(zhí)行。

image

這是一張我自己的顯卡,在設(shè)備管理器里面的資源信息。你可以看到,里面既有 Memory Range,這個(gè)就是設(shè)備對(duì)應(yīng)映射到的內(nèi)存地址,也就是我們上面所說(shuō)的 MMIO 的訪問(wèn)方式。同樣的,里面還有 I/O Range,這個(gè)就是我們上面所說(shuō)的 PMIO,也就是通過(guò)端口來(lái)訪問(wèn) I/O 設(shè)備的地址。最后,里面還有一個(gè) IRQ,也就是會(huì)來(lái)自于這個(gè)設(shè)備的中斷信號(hào)了。

CPU 并不是發(fā)送一個(gè)特定的操作指令來(lái)操作不同的 I/O 設(shè)備。因?yàn)槿绻悄菢拥脑?huà),隨著新的 I/O 設(shè)備的發(fā)明,我們就要去擴(kuò)展 CPU 的指令集了。

在計(jì)算機(jī)系統(tǒng)里面,CPU 和 I/O 設(shè)備之間的通信,是這么來(lái)解決的:

首先,在 I/O 設(shè)備這一側(cè),我們把 I/O 設(shè)備拆分成,能和 CPU 通信的接口電路,以及實(shí)際的 I/O 設(shè)備本身。接口電路里面有對(duì)應(yīng)的狀態(tài)寄存器、命令寄存器、數(shù)據(jù)寄存器、數(shù)據(jù)緩沖區(qū)和設(shè)備內(nèi)存等等。接口電路通過(guò)總線和 CPU 通信,接收來(lái)自 CPU 的指令和數(shù)據(jù)。而接口電路中的控制電路,再解碼接收到的指令,實(shí)際去操作對(duì)應(yīng)的硬件設(shè)備。

而在 CPU 這一側(cè),對(duì) CPU 來(lái)說(shuō),它看到的并不是一個(gè)個(gè)特定的設(shè)備,而是一個(gè)個(gè)內(nèi)存地址或者端口地址。CPU 只是向這些地址傳輸數(shù)據(jù)或者讀取數(shù)據(jù)。所需要的指令和操作內(nèi)存地址的指令其實(shí)沒(méi)有什么本質(zhì)差別。通過(guò)軟件層面對(duì)于傳輸?shù)拿顢?shù)據(jù)的定義,而不是提供特殊的新的指令,來(lái)實(shí)際操作對(duì)應(yīng)的 I/O 硬件。


同樣 CPU 和藍(lán)牙鼠標(biāo)這個(gè)輸入設(shè)備之間的通信也是如此。

對(duì)于CPU來(lái)說(shuō),這只是總線上的一個(gè)普通的USB設(shè)備,與其他的U盤(pán)、USB網(wǎng)卡之類(lèi)的USB接口設(shè)備沒(méi)什么區(qū)別,這些設(shè)備只是通過(guò)USB協(xié)議講自己的數(shù)據(jù)發(fā)送給操作系統(tǒng),對(duì)于這些數(shù)據(jù)是什么,USB是不管的,USB藍(lán)牙鼠標(biāo)接收器和普通USB的鼠標(biāo)在這一層的數(shù)據(jù)是一樣的。

對(duì)于操作系統(tǒng)來(lái)說(shuō),要使這些USB設(shè)備工作,就需要對(duì)發(fā)來(lái)的數(shù)據(jù)進(jìn)行處理,處理數(shù)據(jù)的就是驅(qū)動(dòng)程序,所以不同種類(lèi)的USB設(shè)備需要不同的驅(qū)動(dòng)程序。

回過(guò)頭來(lái)再看USB藍(lán)牙鼠標(biāo)接收器,鼠標(biāo)產(chǎn)生的事件通過(guò) 藍(lán)牙發(fā)送->藍(lán)牙接受-> USB發(fā)送-> USB接受->驅(qū)動(dòng)程序 這樣的路徑最終到達(dá)操作系統(tǒng),這里面的藍(lán)牙和USB僅僅只是傳輸數(shù)據(jù)的方式而已,換為其他的什么TCP/ IP傳輸也是一樣的,其本質(zhì)是將特定的數(shù)據(jù)傳輸給操作系統(tǒng)處理。

I/O性能 - IO_WAIT

硬盤(pán)的性能指標(biāo):響應(yīng)時(shí)間,數(shù)據(jù)傳輸率。數(shù)據(jù)傳輸率可以看做是硬盤(pán)的吞吐率。

現(xiàn)在常用的硬盤(pán)有兩種:

  1. HDD 硬盤(pán) - 機(jī)械硬盤(pán) - SATA 3.0 的接口
  2. SSD 硬盤(pán) - 固態(tài)硬盤(pán),通常會(huì)用兩種接口,各有部分使用 SATA 3.0 的接口 & PCI Express 的接口。
image

IO 順序訪問(wèn),隨機(jī)訪問(wèn)

現(xiàn)在常用的 SATA 3.0 的接口,帶寬是 6Gb/s。這里的“b”是比特。這個(gè)帶寬相當(dāng)于每秒可以傳輸 768MB 的數(shù)據(jù)。而我們?nèi)粘S玫?HDD 硬盤(pán)的數(shù)據(jù)傳輸率,差不多在 200MB/s 左右。

HDD 硬盤(pán),換成 Crucial MX500 的 SSD 硬盤(pán)。它的數(shù)據(jù)傳輸速率能到差不多 500MB/s,比 HDD 的硬盤(pán)快了一倍不止。不過(guò) SATA 接口的硬盤(pán),差不多到這個(gè)速度,性能也就到頂了。因?yàn)?SATA 接口的速度也就這么快。

image

相比于 HDD 硬盤(pán),SSD 硬盤(pán)能夠更快,PCI Express 的接口的硬盤(pán)在讀取的時(shí)候就能做到 2GB/s 左右,差不多是 HDD 硬盤(pán)的 10 倍,而在寫(xiě)入的時(shí)候也能有 1.2GB/s。

  • Acc.Time - 響應(yīng)時(shí)間:這個(gè)指標(biāo),其實(shí)就是程序發(fā)起一個(gè)硬盤(pán)的寫(xiě)入請(qǐng)求,直到這個(gè)請(qǐng)求返回的時(shí)間。
  • Seq - 順序讀寫(xiě)硬盤(pán)得到的數(shù)據(jù)傳輸率 - 吞吐率
  • 4K - 程序,去隨機(jī)讀取磁盤(pán)上某一個(gè) 4KB 大小的數(shù)據(jù),一秒之內(nèi)可以讀取到多少數(shù)據(jù)

上面的兩塊 SSD 硬盤(pán)上,Acc.Time響應(yīng)時(shí)間這個(gè)指標(biāo)大概時(shí)間都是在幾十微秒這個(gè)級(jí)別。而 HDD 的硬盤(pán),通常會(huì)在幾毫秒到十幾毫秒這個(gè)級(jí)別。這個(gè)性能的差異,就不是 10 倍了,而是在幾十倍,乃至幾百倍。

光看響應(yīng)時(shí)間和吞吐率這兩個(gè)指標(biāo),似乎我們的硬盤(pán)性能很不錯(cuò)。即使是廉價(jià)的 HDD 硬盤(pán),接收一個(gè)來(lái)自 CPU 的請(qǐng)求,也能夠在幾毫秒時(shí)間返回。一秒鐘能夠傳輸?shù)臄?shù)據(jù),也有 200MB 左右。你想一想,我們平時(shí)往數(shù)據(jù)庫(kù)里寫(xiě)入一條記錄,也就是 1KB 左右的大小。我們拿 200MB 去除以 1KB,那差不多每秒鐘可以插入 20 萬(wàn)條數(shù)據(jù)呢。- 但這個(gè)數(shù)值這么計(jì)算是不準(zhǔn)確的。

答案就來(lái)自于硬盤(pán)的讀寫(xiě)。在順序讀寫(xiě)隨機(jī)讀寫(xiě)的情況下,硬盤(pán)的性能是完全不同的。如上述的4K指標(biāo)

可以看到,在這個(gè)指標(biāo)上,SATA 3.0 接口的硬盤(pán)和 PCI Express 接口性能差異變得很小。這是因?yàn)?,在這個(gè)時(shí)候,接口本身的速度已經(jīng)不是硬盤(pán)訪問(wèn)速度的瓶頸了。可以看到即便是 PCI Express 的接口,在隨機(jī)讀寫(xiě)的時(shí)候,數(shù)據(jù)傳輸率也只能到 40MB/s 左右,是順序讀寫(xiě)情況下的幾十分之一。

按圖中數(shù)據(jù)進(jìn)行計(jì)算可得:40MB / 4KB = 10,000,即一秒之內(nèi),這塊 SSD 硬盤(pán)可以隨機(jī)讀取 1 萬(wàn)次的 4KB 的數(shù)據(jù),寫(xiě)入的話(huà)更多一些。這個(gè)每秒隨機(jī)讀寫(xiě)的次數(shù),我們稱(chēng)之為==IOPS==,也就是每秒輸入輸出操作的次數(shù)。事實(shí)上,比起響應(yīng)時(shí)間,我們更關(guān)注 IOPS 這個(gè)性能指標(biāo)。IOPS 和 DTR(數(shù)據(jù)傳輸率)才是輸入輸出性能的核心指標(biāo)。

因?yàn)?strong>在實(shí)際的應(yīng)用開(kāi)發(fā)當(dāng)中,對(duì)于數(shù)據(jù)的訪問(wèn),更多的是隨機(jī)讀寫(xiě),而不是順序讀寫(xiě)。我們平時(shí)所說(shuō)的服務(wù)器承受的“并發(fā)”,其實(shí)是在說(shuō),會(huì)有很多個(gè)不同的進(jìn)程和請(qǐng)求來(lái)訪問(wèn)服務(wù)器。自然,它們?cè)谟脖P(pán)上訪問(wèn)的數(shù)據(jù),是很難順序放在一起的。這種情況下,隨機(jī)讀寫(xiě)的 IOPS 才是服務(wù)器性能的核心指標(biāo)。 HDD 硬盤(pán)的 IOPS 通常也就在 100 左右,而不是在 20 萬(wàn)次。

定位 IO_WAIT

即使是用上了 PCI Express 接口的 SSD 硬盤(pán),IOPS 也就是在 2 萬(wàn)左右。而我們的 CPU 的主頻通常在 2GHz 以上,也就是每秒可以做 20 億次操作。即使 CPU 向硬盤(pán)發(fā)起一條讀寫(xiě)指令,需要很多個(gè)時(shí)鐘周期,一秒鐘 CPU 能夠執(zhí)行的指令數(shù),和我們硬盤(pán)能夠進(jìn)行的操作數(shù),也有好幾個(gè)數(shù)量級(jí)的差異。這也是為什么,在應(yīng)用開(kāi)發(fā)的時(shí)候往往會(huì)說(shuō)“性能瓶頸在 I/O 上”。因?yàn)楹芏鄷r(shí)候,CPU 指令發(fā)出去之后,不得不去“等”我們的 I/O 操作完成,才能進(jìn)行下一步的操作。

問(wèn)題:在實(shí)際遇到服務(wù)端程序的性能問(wèn)題的時(shí)候,我們?cè)趺粗肋@個(gè)問(wèn)題是不是來(lái)自于 CPU 等 I/O 來(lái)完成操作呢?

  1. top 命令里面,可以看到 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

%CPU 行的 wa 的指標(biāo),這個(gè)指標(biāo)就代表著 iowait,也就是 CPU 等待 IO 完成操作花費(fèi)的時(shí)間占 CPU 的百分比。

  1. 知道了 iowait 很大,就要去看一看,實(shí)際的 I/O 操作情況是什么樣的。使用 iostat 這個(gè)命令能夠看到實(shí)際的硬盤(pán)讀寫(xiě)情況。
$ 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

這個(gè)命令里,不僅有 iowait 這個(gè) CPU 等待時(shí)間的百分比,還有一些更加具體的指標(biāo),并且它還是按照機(jī)器上安裝的多塊不同的硬盤(pán)劃分的。

這里的 tps 指標(biāo),其實(shí)就對(duì)應(yīng)著我們上面所說(shuō)的硬盤(pán)的 IOPS 性能。而 kB_read/s 和 kB_wrtn/s 指標(biāo),就對(duì)應(yīng)著我們的數(shù)據(jù)傳輸率的指標(biāo)。知道實(shí)際硬盤(pán)讀寫(xiě)的 tps、kB_read/s 和 kb_wrtn/s 的指標(biāo),我們基本上可以判斷出,機(jī)器的性能是不是卡在 I/O 上了。

  1. 那么,接下來(lái),我們就是要找出到底是哪一個(gè)進(jìn)程是這些 I/O 讀寫(xiě)的來(lái)源了。這個(gè)時(shí)候,你需要“iotop”這個(gè)命令。
$ 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

通過(guò) iotop 這個(gè)命令,你可以看到具體是哪一個(gè)進(jìn)程實(shí)際占用了大量 I/O,那么你就可以有的放矢,去優(yōu)化對(duì)應(yīng)的程序了。上面的這些示例里,不管是 wa 也好,tps 也好,它們都很小。那么,接下來(lái),我就給你用 Linux 下,用 stress 命令,來(lái)模擬一個(gè)高 I/O 復(fù)雜的情況,來(lái)看看這個(gè)時(shí)候的 iowait 是怎么樣的。我在一臺(tái)云平臺(tái)上的單個(gè) CPU 核心的機(jī)器上輸入“stress -i 2”,讓 stress 這個(gè)程序模擬兩個(gè)進(jìn)程不停地從內(nèi)存里往硬盤(pán)上寫(xiě)數(shù)據(jù)。

  1. 在一臺(tái)云平臺(tái)上的單個(gè) CPU 核心的機(jī)器上輸入“stress -i 2”,讓 stress 這個(gè)程序模擬兩個(gè)進(jìn)程不停地從內(nèi)存里往硬盤(pán)上寫(xiě)數(shù)據(jù)。
$ stress -i 2

$ top
=============
top - 06:56:02 up 3 days, 19:34,  2 users,  load average: 5.99, 1.82, 0.63
Tasks:  88 total,   3 running,  85 sleeping,   0 stopped,   0 zombie
%Cpu(s):  3.0 us, 29.9 sy,  0.0 ni,  0.0 id, 67.2 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  1741304 total,  1004404 free,   307152 used,   429748 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  1245700 avail Mem 

$ iostat 2 5
==============
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           5.03    0.00   67.92   27.04    0.00    0.00
Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda           39762.26         0.00         0.00          0          0

$ iotop
==============
Total DISK READ :       0.00 B/s | Total DISK WRITE :       0.00 B/s
Actual DISK READ:       0.00 B/s | Actual DISK WRITE:       0.00 B/s
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND                                             
29161 be/4 xuwenhao    0.00 B/s    0.00 B/s  0.00 % 56.71 % stress -i 2
29162 be/4 xuwenhao    0.00 B/s    0.00 B/s  0.00 % 46.89 % stress -i 2
    1 be/4 root        0.00 B/s    0.00 B/s  0.00 %  0.00 % init


你會(huì)看到,在 top 的輸出里面,CPU 就有大量的 sy 和 wa,也就是系統(tǒng)調(diào)用和 iowait。

如果我們通過(guò) iostat,查看硬盤(pán)的 I/O,你會(huì)看到,里面的 tps 很快就到了 4 萬(wàn)左右,占滿(mǎn)了對(duì)應(yīng)硬盤(pán)的 IOPS。

如果這個(gè)時(shí)候我們?nèi)タ匆豢?iotop,你就會(huì)發(fā)現(xiàn),我們的 I/O 占用,都來(lái)自于 stress 產(chǎn)生的兩個(gè)進(jìn)程了。

總結(jié)

在順序讀取的情況下,無(wú)論是 HDD 硬盤(pán)還是 SSD 硬盤(pán),性能看起來(lái)都是很不錯(cuò)的。不過(guò),等到進(jìn)行隨機(jī)讀取測(cè)試的時(shí)候,硬盤(pán)的性能才能見(jiàn)了真章。因?yàn)樵诖蟛糠值膽?yīng)用開(kāi)發(fā)場(chǎng)景下,我們關(guān)心的并不是在順序讀寫(xiě)下的數(shù)據(jù)量,而是每秒鐘能夠進(jìn)行輸入輸出的操作次數(shù),也就是 IOPS 這個(gè)核心性能指標(biāo)。

你會(huì)發(fā)現(xiàn),即使是使用 PCI Express 接口的 SSD 硬盤(pán),IOPS 也就只是到了 2 萬(wàn)左右。這個(gè)性能,和我們 CPU 的每秒 20 億次操作的能力比起來(lái),可就差得遠(yuǎn)了。所以很多時(shí)候,我們的程序?qū)ν忭憫?yīng)慢,其實(shí)都是 CPU 在等待 I/O 操作完成。

在 Linux 下,我們可以通過(guò) top 這樣的命令,來(lái)看整個(gè)服務(wù)器的整體負(fù)載。在應(yīng)用響應(yīng)慢的時(shí)候,我們可以先通過(guò)這個(gè)指令,來(lái)看 CPU 是否在等待 I/O 完成自己的操作。進(jìn)一步地,我們可以通過(guò) iostat 這個(gè)命令,來(lái)看到各個(gè)硬盤(pán)這個(gè)時(shí)候的讀寫(xiě)情況。而 iotop 這個(gè)命令,能夠幫助我們定位到到底是哪一個(gè)進(jìn)程在進(jìn)行大量的 I/O 操作。

這些命令的組合,可以快速幫你定位到是不是我們的程序遇到了 I/O 的瓶頸,以及這些瓶頸來(lái)自于哪些程序,你就可以根據(jù)定位的結(jié)果來(lái)優(yōu)化你自己的程序了。

問(wèn)題解答

硬盤(pán)的隨機(jī)讀的性能是不如隨機(jī)寫(xiě)的。我以前一直以為是反過(guò)來(lái)的,但是為什么呢?

磁盤(pán)內(nèi)部也是有WAL隊(duì)列,只需要把write request寫(xiě)入這個(gè)隊(duì)列就可以了,但是讀如果cache不命中就沒(méi)有任何懶可偷。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容