甄建勇:五分鐘搞不定系列-打通軟硬件的任督二脈
引言
我們很多人都有下面的經(jīng)歷:
下班回家后,吃飯時,碗筷都已準備好,在吃第一口飯之前,順手點了一下旁邊的ipad或者手機(以下統(tǒng)稱為計算機),想繼續(xù)追昨天沒看完的電視劇。
你可知道,你這“順手一點”的背后,計算機內(nèi)部都發(fā)生了哪些神奇的事,才讓你看到新的劇情,新的畫面嗎?
今天,我們就虛擬一個小人兒,名叫小土孩兒,她將順著你的指尖滑下,進入到計算機內(nèi)部,看看會遇到什么你可能知道,也可能不知道的事情……
鍵盤
毫無疑問,小土孩兒離開你指尖的第一站,就是屏幕的外層,其實從技術角度看,屏幕的外層就是觸摸屏,其本質(zhì)和電腦的鍵盤并無二致,作用都是捕捉用戶操作。
對于矩陣鍵盤,其內(nèi)部有掃描電路,會隔一段時間掃描一下,根據(jù)電平的高低,來判斷是否有按鍵被按下。當小土孩兒掉落到鍵盤上時,會對按鍵(假設是空格鍵)有個壓力,這個壓力使空格鍵下面的電路導通,這樣鍵盤的掃描電路在下次掃描時就會發(fā)現(xiàn)這一情況。對于一次單擊操作,我們從宏觀上認為我們按了一次鍵,實際上,鍵盤掃描電路會有防抖機制,即在一段持續(xù)的時間段內(nèi),某個按鍵一直沒按下,才算一次單擊。如果按的的速度太快,防抖邏輯可能會認為是誤操作。如果按的時間太久,可能會被當成是“雙擊”,或者是“長按”。我們可以根據(jù)人類通常情況下的操作速度來設計合理的掃描間隔。
按完空格鍵之后,鍵盤控制芯片將空格鍵對應的編碼保存在一個寄存器中,并拉低與處理器(CPU)相連的一條線,即向處理器發(fā)送一個外部中斷信號。
中斷
CPU內(nèi)部的中斷控制器收到這個外部中斷信號之后,會把CPU內(nèi)部的一個控制寄存器“置1”(后面會提到),表示收到了一個外部中斷。在中斷控制器內(nèi)部還有另外一個控制寄存器,表示哪些中斷要被屏蔽,或者哪些中斷需要CPU進行處理。
經(jīng)過屏蔽處理的這個中斷,會附著在CPU內(nèi)部正在執(zhí)行的一條指令上,表示這條指令執(zhí)行時發(fā)生了中斷異常。
我們知道現(xiàn)代CPU中,指令處理流水線一般是多級流水,所以我們要找一個合適的中斷異常附著的時間點,又因為CPU中有亂序執(zhí)行技術,所以我們要在指令順序被打亂之前附著,所以,這個附著點一般是指令譯碼階段。
這條被異常附著的指令會隨著CPU流水線,從譯碼階段開始,依次向下一階段傳遞。傳遞過程中,被異常附著的指令不會被發(fā)送到執(zhí)行單元。比如,被附著的是一條ADD指令,這條ADD指令正常狀態(tài)下會被發(fā)往加法器執(zhí)行加法操作。
當這條指令來到ROB(Re-Order-Buffer)時,CPU將開始異常處理。
CPU的指令流水
CPU內(nèi)部,一般包括分支預測->取指->初級譯碼->保留站(reservationstation)->亂序發(fā)射->再次譯碼->指令執(zhí)行->RoB等流水級。
程序中一般包含大量的分支指令,而分支指令的后面要執(zhí)行的指令是什么,依賴于分支指令的執(zhí)行結果。而知道分支指令的結果,要在指令指令階段才可以。這時我們面臨兩個選擇:
1. 等到知道分支指令的結果之后再去讀取分支指令后面的指令。即,“不見兔子不撒鷹”
2. 可以猜一個分支指令執(zhí)行結果,根據(jù)猜的結果,提前讀取分支指令后面的指令。即,“投機執(zhí)行”。
很顯然,不見兔子不撒鷹式的處理方式,會造成分支指令前面的流水級出現(xiàn)空泡(bubble),也就意味著CPU性能下降。
而投機執(zhí)行的后果是,一旦猜錯了,必須引入猜錯處理邏輯。所以,為了提高猜中的概率,CPU引入了分支預測機制。
分支預測
基本的分支預測算法很簡單,就是增加一個飽和計數(shù)器來猜測分支結果。比如,一個2bit的飽和計數(shù)器,來預測if判斷分支指令的結果,其中0、1表示強跳轉(zhuǎn)與弱跳轉(zhuǎn),2、3表示強不跳轉(zhuǎn),弱不跳轉(zhuǎn)。其工作機制如下:
最開始,設置這個counter為1,表示弱跳轉(zhuǎn)。
如果猜中,counter減1,變成跳轉(zhuǎn)。如果沒有猜中,counter加1,變成2,表示弱不跳轉(zhuǎn)。
當counter為0之后,仍然猜中,counter將保持0,即出現(xiàn)飽和之后,持續(xù)猜中時,counter值不變。
以上機制,簡單直接,但是有個很大的缺陷,就是可能會在1、2之間顛簸,使猜中率為0%。為了解決這個問題,CPU的分支預測機制引入了其它更復雜的方法。
取值
就是根據(jù)PC(program counter)的值,將對應地址的指令讀到CPU內(nèi)部。而指令一般都存放在外部存儲器中,道阻且長。為了提高取值速度,CPU一般會引入inst_cache和MMU。關于cache和MMU,我們之前有聊到,請參考“甄建勇_五分鐘搞定”系列文章。
譯碼
讀取上來的指令,會被送往譯碼單元。即,指令的識別。對于RISC指令集的CPU來說,譯碼器比較簡單。而對于CISC指令集,譯碼就變得很麻煩,因為同一條指令在譯碼時,指令后面的內(nèi)容會依賴于指令前面譯碼的結果。比如intel的x86處理器的譯碼單元,為了提高譯碼速度,只能“全面撒網(wǎng),重點培養(yǎng)”,即,同時譯碼所有的可能,然后最后根據(jù)一部分譯碼結果,來選擇其中那個正確的。
此外,實際CPU中,最前端的譯碼單元,只需譯碼指令的一部分內(nèi)容就可以指令的去向,所以沒必要在最開始就全部譯碼。比如,只要區(qū)分出指令的類型,就可以發(fā)往下一階段。指令后續(xù)的譯碼由對應的執(zhí)行單元翻譯即可。
保留站
在保留站之前的流水級,就像一條窄窄的巷子,為了保持指令順序,指令在巷子里排著隊,慢慢的前行。我們知道,巷子里的指令,有些指令之間是有依賴關系的,而有些是沒有依賴關系的。
保留站就像巷子盡頭的一個大廣場,讓那些本來排在后面,卻和前面沒有依賴的指令先行執(zhí)行,即指令的超車。
寄存器重命名
對于WAW依賴(如下指令序列中的1和3),純粹是因為寄存器數(shù)量不夠引起的依賴,我們完全可以通過增加寄存器數(shù)量來解除指令之間的依賴,讓兩條執(zhí)行流同時執(zhí)行。就像我們?nèi)ワ堭^吃飯,結果發(fā)現(xiàn)需要排隊,而排隊的原因竟然是因為飯館的筷子只有1雙。
有依賴 |
無依賴 |
重命名 |
|
1 |
ADD R3, R1,R2 |
ADD R3, R1,R2 |
|
2 |
STORE addr0, R3 |
STORE addr0, R3 |
|
3 |
ADD R3, R4,R5 |
ADD R60, R4,R5 |
R3 -> R60 |
4 |
STORE addr0, R3 |
STORE addr0, R60 |
發(fā)射
經(jīng)過寄存器重命名處理的指令,會呆在保留站內(nèi)隨時待命,一旦指令所需的操作數(shù)全部準備好之后,就會被發(fā)射到相應的執(zhí)行單元,很顯然,這里的發(fā)射可以是亂序的,同時發(fā)射的指令數(shù)量也可能超過一條。即,亂序執(zhí)行和多發(fā)射技術。
執(zhí)行
關于指令的執(zhí)行,就是“八仙過海”了。不同的指令,執(zhí)行過程差異很大。我們之前聊過“甄建勇_五分鐘搞不定_1+1=?”系列,介紹了計算機中加法器、乘法器具體的實現(xiàn)細節(jié),感興趣的同學請參考。
ROB
“出來混,遲早要還的”,ROB就是我們要還前面欠下的“亂序”的帳。在指令進入保留站的時候,我們需要登記指令的先后順序,等亂序發(fā)射并執(zhí)行的指令完成后,將進入到ROB,也就是另外“一個廣場”。為了保持程序原本的正確順序,我們需要按照當時進入保留站的順序,依次將ROB中的指令依次移除,即指令的提交。
而最開始提到的被附著異常的指令,也是在這里處理的。ROB的另外一個原因是為了給操作系統(tǒng)一個“精確異常”, 即,處理異常前要把異常指令前面的指令都執(zhí)行完, 后面的指令都取消掉。
CPU異常處理
上面提到,ROB在收到附著異常的指令之后,會進行一系列的操作:
首先就是給CPU的其它模塊發(fā)送指令取消信號,將異常之后的所有的指令取消掉。其次要保存被附著指令對應的PC值。此外,還要修改CPU內(nèi)部的控制寄存器,將CPU設置成內(nèi)核態(tài)(CPU一般都有多個狀態(tài),比如,內(nèi)核態(tài)、用戶態(tài)、debug態(tài)等),最后,設置PC值為異常向量的入口地址,然后從入口地址取值,開始執(zhí)行入口地址處的指令。
OS異常處理
異常處理是一個復雜的過程,需要軟硬件協(xié)同完成,上面提到的是硬件的異常處理,當CPU開始從異常處理入口取值執(zhí)行后,執(zhí)行的就是OS事先設定好的操作。異常入口處的指令一般是跳轉(zhuǎn)指令,不同的異常入口處都有相應的跳轉(zhuǎn)指令,這些跳轉(zhuǎn)指令緊密的挨在一起,就像一個向量一樣,故稱“異常向量”。
異常向量是OS的一部分。所以當CPU執(zhí)行異常向量時,就開始執(zhí)行OS的代碼了。OS異常處理過程一般是先保存處理器現(xiàn)場,然后讀取CPU內(nèi)部的控制寄存器,就是前面提到的那個控制寄存器。讀回來,發(fā)現(xiàn)是外部中斷引起的異常,OS就繼續(xù)讀取外部中斷控制器的寄存器,同時將中斷清除。讀回來發(fā)現(xiàn)是鍵盤有人按下了,就繼續(xù)讀取鍵盤控制器的寄存器,發(fā)現(xiàn)被按下的是空格鍵。
OS接下來要查找這個空格鍵要發(fā)給誰,即哪個進程需要這個空格鍵。經(jīng)OS查詢發(fā)現(xiàn),是一個叫X奇藝的視頻軟件在等待按鍵,于是就將空格鍵值發(fā)給X奇藝,并喚醒X奇藝進程。
用戶態(tài)程序執(zhí)行
X奇藝被喚醒之后,發(fā)現(xiàn)OS發(fā)來的是一個空格鍵的鍵值,假設X奇藝程序的事先設定是,當處于暫停播放狀態(tài)下,按一個空格,表示視頻繼續(xù)播放。
接下來,X奇藝就會從網(wǎng)絡或者本地磁盤讀取即將顯示的數(shù)據(jù),調(diào)用視頻解碼器的驅(qū)動程序,驅(qū)動程序?qū)⒃O置解碼器的寄存器,并是能解碼器開始工作。當然,如果即將顯示的內(nèi)容可能是一些矢量數(shù)據(jù),需要GPU參與,根據(jù)矢量數(shù)據(jù)進行渲染。
GPU的渲染
GPU最重要的功能是通過給定虛擬相機、3D場景物體以及光源等場景要素來產(chǎn)生(渲染)出一幅2D的圖像,而GPU渲染一幅圖像,一般包括多個階段,分別是:頂點數(shù)據(jù)的輸入、頂點著色、曲面細分、幾何著色、圖元組裝、裁剪剔除、光柵化、像素著色以及測試與混合。
頂點數(shù)據(jù)(Vertex)一般包括頂點坐標、法線、顏色以及紋理坐標等信息。
頂點著色器(Shader)收到頂點數(shù)據(jù)之后,主要是進行坐標變換,即,將局部坐標變化到世界坐標、觀察坐標。
曲面細分(Tessellation)將輸入的較大的三角形切分成更小的三角形,使得離攝像機近的物體細節(jié)更豐富,而離攝像機遠的物體則細節(jié)較少。
幾何著色(Geometry)將輸入的獨立的點、線、等基礎圖元(primitive),擴展成多邊形。
圖元組裝(Primitive Setup)階段主要是將原始的圖元按照規(guī)則組裝成指定的圖元。比如裁剪(Clipping)窗口以外的圖元、剔除背面(Culling)等看不到的圖元,以減少后面階段的運算量。
光柵化(Rasterization)主要是將3D連續(xù)的物體轉(zhuǎn)化成離散的屏幕像素點。
像素著色會根據(jù)光照等因素,決定每一個像素的最終顏色,也會進行陰影的處理,我們看到的一些酷炫的效果主要是像素著色階段產(chǎn)生的。
GPU渲染的最后一個階段是測試,包括裁剪測試、模板測試、深度測試等。只有通過測試的像素才會進行混合,比如Alpha混合。Alpha表示的是物體的不透明度,Alpha=1表示完全不透明。
經(jīng)過以上長長的渲染管線(Rendering Pipeline),GPU就完成了一幅圖像的渲染,并將渲染好的圖像數(shù)據(jù)寫到顯示控制器可以讀取的幀緩沖里。
圖像的顯示
圖像以及圖像的顯示是個復雜的過程。光圖像的格式就多如牛毛,比如CIE的XYZ、LUV、LAB。RGB的RGB、sRGB、AdobeRGB、scRGB、DCI-P3、Rec.709、ACES等。Luma+Chroma的YUV、YIQ、YpbPr、YCbCr、xvYCC、IPT、ICtCp等。Hue+Saturation的HSV、HSL。以及在打印機領域廣泛使用的CMYK。每種格式都有對應的colorspace,下圖就是BT的兩個標準對應的colorspace。
定義這么多格式的目的都是為了跨越讓顯示器顯示的圖像和真實世界中的物體,在人眼看來是一樣的,而顯示器本身也在不斷進化中。
下一步,我們的顯示器可能是這樣的:
其實,在圖像格式和顯示器之間還需要定義數(shù)據(jù)傳輸?shù)膮f(xié)議,比如HDMI,同理CPU與GPU之間也需要數(shù)據(jù)傳輸?shù)膮f(xié)議,比如PCIe。
顯示器的控制器從幀緩沖里讀出GPU渲染好的圖像數(shù)據(jù),通過和顯示器連接的總線,傳到顯示器內(nèi)部的控制器,并最終控制顯示電路,將圖像顯示在屏幕上。
甄建勇,高級架構師(某國際大廠),十年以上半導體從業(yè)經(jīng)驗。主要研究領域:CPU/GPU/NPU架構與微架構設計。感興趣領域:經(jīng)濟學、心理學、哲學。
本文轉(zhuǎn)載自微信公眾號「Linux閱碼場」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系Linux閱碼場公眾號。