1. 程式人生 > >計算機底層原理雜談(白話文)

計算機底層原理雜談(白話文)

  簡單說一下寫這篇文章的緣由。首先這個不是教學型別的,是我Java實在學不下去了,因為好多計算機底層原理都不是很清楚,每次學新東西都由於想不明白底層原理困惑,所以下決心停止學習Java的新東西,開始搞明白底層。一開始搞的所謂的底層是“Java虛擬機器”,然後又C語言組合語言什麼的,其實是想圖快,儘快接近現在做的事情。後來發現不行,這事快不了,所以乾脆就從物理層面用導線燈泡整合晶片開始動手做一個cpu開始吧。其實也沒多久,大概三個多月吧,從我之前寫的【從零開始自制cpu】系列的學習文章也可以看到開始時間。cpu做好之後(其實後來由於老是燒壞各種電路沒做完),開始看作業系統,現在剛開始看一點。總結起來就是我認為的學習路線應該是:

  自制個cpu--微機原理--簡單資料結構與演算法--計算機組成原理--組合語言--作業系統--C語言--編譯原理--複雜資料結構與演算法--計算機網路--Java--Java虛擬機器--原始碼研究--多執行緒linux高併發Spring負載均衡各種以這些評判一個人Java水平的東東...

  我認為Java應該在那樣一個遙遠的地方,好多人卻以它為起點了。我現在粗略地看到作業系統,當然不斷地在補前面的知識。這篇文章想借此機會跟大家說說計算機底層的理解,學的知識很碎,大家順便幫我挑挑錯誤,如果對你有幫助那就更好了。我用大白話說,想到哪說到哪,這樣更容易展現出漏洞和錯誤,希望大家在評論區裡積極吐槽~

----------------------------------------華麗的正文分割線------------------------------------------

  CPU就是由一堆導線將一堆部件連線在一塊的東西,每個部件裡面又是由一堆導線和一堆更小的部件連線在一塊。把整個cpu看成一個部件,那它和RAM,磁碟等外設又是用一堆導線連線在一塊,我感覺就像套娃似的。然後每個部件從外部看就是暴露了一堆針腳,可以用導線連上,給他們傳遞要麼高電平要麼低電平的電流,你比如說下面這款74LS173(3態輸出端的4位D型暫存器,簡單說就是一個能存4位資料的東西,可以用來做暫存器。

  

  看這麼多陣腳,我感覺分三類理解就行了,別管他們是資料輸入、資料輸出、訊號控制啥的,他們都是一類,反正要給他們要麼高要麼低的電訊號。還有一類就是接+接-的vcc和gnd,目的是讓這個部件有“電”。還有一個是時鐘clock,這你給他一個一會高一會低來回變的電訊號就行了,目的一般是讓它電平上升沿的時候這個部件“觸發”一個什麼效果,對這個暫存器來說,就是存了個數據或者讀出了資料,當然有的部件不需要時鐘訊號。

  CPU以及它關聯的其他裝置,全都是一個個這樣的“部件”,這些部件別管它多複雜,最終暴露出來的都是一個時鐘、一個接正接負、一堆其他的亂七八糟的針腳。有的部件用來存東西,有的用來做運算比如加法器,有的用來計數比如程式計數器,有的部件用來做各種翻譯呀轉換呀什麼的,比如地址譯碼什麼的。最終它們都連線在一個大boss上那就是時鐘訊號發生裝置,反正它的作用就是特別快速地輸出一個高低高低高低電平來回轉換的電訊號。所以現在我總結出cpu就是一個boss時鐘訊號,一堆小弟部件,一堆導線把部件有規律地連在一塊,最終都連在大boss上。

  那這樣一堆部件是怎麼執行的呢?首先第一個部件是程式計數器pc,它就是靠時鐘訊號不斷累加,輸出的針腳從0000,0001,0002一直往上加。當然你可以一個時鐘週期(就是高低電平來一下)就+1,但這樣除非你其他部件一個時鐘週期內就能把一條指令執行完,一般不行,那怎麼辦呢,就再弄一個計數器,比如只能從0加到5。這個計數器每次都從0開始,然後沒到5之前都把程式計數器的一個狀態針腳變成“不觸發加”的狀態,這樣程式計數器就不變了。然後各個部件在5個時鐘週期內把指令執行完,然後程式計數器再加1。這就是單指令週期,每個指令都用5個時鐘週期執行完。當然這不好,你可變化呀,讓這個計數器從0加到一個動態的值,這個值根據指令型別改變,這就多指令週期了。然後程式計數器一直往上加也不好,給他弄幾個針腳,再弄一個狀態,可以直接設定一個新值,這就實現程式跳轉了。一個指令週期可以動態改變,還可以強行設定程式計數器的值,這差不多就夠了。

  第二個部件是暫存器堆,其實就是一個存東西的地方,跟記憶體呀硬碟呀一樣,只不過離cpu近,就光用距離除以電流流速來說都能說明它比較快。所以一些運算出來的中間結果呀,甚至我從記憶體中讀出來要做加法或者要寫入記憶體的資料都先放暫存器裡面。暫存器都一樣的你存哪個都行,只不過為了統一,別一個人一個樣,把暫存器分門別類弄了些專用的功能,地址資料就放你地址資料該存的暫存器,表示狀態的資料就放在你狀態該存的暫存器,僅此而已。暫存器正因為有不同人給它賦予定義才麻煩,就比如IO介面中的埠,就是暫存器而已,只不過比如像硬碟介面,你往它3號埠寫個011101啥的,他就表示你要讀資料了,然後硬碟把資料放到4號暫存器等著你讀。

  第三個部件是算術邏輯單元,你可以先假設它就是個只能執行加法的部件,8個針腳資料1,8個針腳資料2,再來8個針腳表示這倆資料的和,完事。

  第四個部件我不想叫它控制單元,我感覺我一開始困惑的就是因為這種叫法,我感覺它更像是一種佈線的方式,只不過抽象地說出來逼格高,更容易寫成教材。簡單說就是幾個針腳接收指令,另幾個針腳輸出各種不同高低電平訊號,連線在其他部件的針腳上起到一些控制作用。你比如我輸入一個“寫入記憶體”的指令,那我輸出的針腳肯定有一個是接在記憶體的“是否寫入”這個針腳上,這不就控制了麼。

  總結起來,其實部件就那麼幾種,儲存部件:暫存器呀,RAM ROM呀;控制部件:就是聯絡所有部件的控制它們可讀可寫可加這種邏輯的;算數部件:數學運算用的;發動機部件:這我給命名成發動機部件吧,就是時鐘訊號產生,還有程式計數器,這些都是將整個部件啟用、發動的感覺,沒有它們就沒了源動力。外設部件:也可以叫IO部件,注意千萬不要把硬碟理解成儲存部件,它跟網口、滑鼠、鍵盤是一樣的,都是IO,你能從磁碟中讀資料,你也能從鍵盤中讀資料,當它們接到IO介面上時,全都視為同一個東西了。

  我拿鍵盤舉個例子,不管誰家生產的鍵盤,都要接在我一個叫“鍵盤介面”的東西上,這個鍵盤介面中有5個埠,1號2號3號4號5號,其實就是暫存器,介面上面的暫存器就叫埠。我這個鍵盤生產商可以寫個說明書,告訴大家你們聽好啦,1號埠就是我的按鍵資料,我按了鍵盤中的A,我就往1號埠中寫00100011,你cpu讀到了怎麼處理我就不管啦。當然我很好心,我給你2號埠也搞一個數據,為0的時候說明我沒按鍵,為1的時候我就按鍵了,這夠可以了吧。這時候cpu就可以處理了,我去讀這個2號埠的資料,就像我讀記憶體資料一樣,讀到了我發現它是1,那我知道鍵盤按鍵了,我接著讀1號埠的資料,然後各種處理最終給顯示器介面中的一堆埠寫上一堆奇奇怪怪的資料,顯示器讀到這些資料後又做了一堆處理最終在螢幕上亮了幾個燈泡,亮出了一個A。這裡面cpu不斷讀2號埠看鍵盤有沒有操作就叫用輪詢IO的方式檢查裝置,讀了1號埠的資料做各種處理最終給顯示器介面寫入資料,就是驅動程式。最後顯示器讀這些資料顯示到螢幕上,那這是另一個裝置的物理細節了,它裡面也有個像我們這個cpu的東西就不去細究了。

  完美,不過上面的過程又有些問題,如果io裝置很多,輪詢io的方式就很沒效率了,最好是io有動作的時候主動通知cpu。那可以這樣做,比如鍵盤有動作,我不是往我2號埠寫資料了,而是往你cpu中一個暫存器中寫一個號碼,cpu讀到這個暫存器中有資料了,通過查它的號碼找到對應驅動程式的記憶體地址,執行這個程式。這個過程就叫中斷,而查詢號碼去找程式的地方,叫做中斷向量表。這裡其實我真的也不想叫它中斷,因為又是這個詞讓我困惑好久。因為cpu是通過增加一個時鐘週期專門檢測是否有中斷訊號產生,也就是說如果沒有任何中斷訊號,這個時鐘週期也是需要空跑一次的。所以你看,從更物理的時鐘週期的層面看,這個中斷方式仍然是輪詢,只不過輪詢的單位不是指令,而是時鐘

  這說法完美,不過上面還有個問題,就是像鍵盤這樣的還好,因為它確實需要執行一段特殊的驅動程式去完成功能。但想磁碟這種,單純的就是讀出資料寫到記憶體或者讀記憶體資料寫到磁碟,這種操作很低階但是很耗時間,如果每次都是通過中斷然後資料通過cpu先傳到暫存器在一個個傳到記憶體,那就讓cpu太大材小用了。這種重複的耗時的勞動,最好別佔用cpu,直接從硬碟通過某個裝置到記憶體就好了,這個裝置就叫做IO控制器DMA。硬碟接收到cpu的讀請求後,向dma發請求訊號。dma完成了從硬碟寫入記憶體操作後,再向cpu發一箇中斷訊號,簡單執行一下資料處理完的中斷程式就行了,至於資料傳輸的過程,cpu可以做其他更高階的事情。

  完美,不過上面的又有一些問題,就是你雖然不佔用我cpu時間,但你佔用匯流排啊,我們是公用一條匯流排傳輸資料的,你傳輸資料佔用匯流排的時候,我cpu就佔不了了。或者你等我cpu不用匯流排的時候你在再用,這個叫做dma的時鐘週期竊取。但這樣也不好,我他媽就希望你離我越遠越好,別佔我cpu時間也別佔我的地方,讓專門一個可以執行簡單指令的裝置和你公用一條單獨的匯流排去完成這件事,我稱之為low版cpu,他就是io通道

  

  再說說IO埠地址問題,cpu如何指定一個埠呢,可以用一個部分表示地址,另一個部分表示是IO地址還是記憶體地址。還有一種方式是,將io埠也加入到記憶體一樣的地址範圍中,然後訪問一個埠跟訪問一個記憶體地址沒什麼差別。所以上述到就是IO埠的兩種編址方式,第一種是獨立編址,採用埠對映io,第二種是統一編制,採用記憶體對映io,現在基本都是記憶體對映。整個io這一塊大體的骨架就是這樣子的,你看剛剛所說的中斷呀,dma呀這些,我覺得可以理解為作業系統,或者說由於使用cpu的需求倒逼出的產物。當然所有這些都可以用軟體來實現,但當需求足夠大的時候可以讓cpu為作業系統做出一些改變的,這並不是cpu原本就是這個樣子。其實上面提到的中斷,是外部中斷,當然也可以是內部中斷,就是指令自己去出發一箇中斷。這是根據中斷源的不同分的。當然本質是一樣的,都是往一個暫存器或者幾個暫存器裡寫數,cpu一個時鐘週期專門檢視一下這個暫存器,然後查下中斷向量表找到對應的程式執行一下,執行完了恢復之前的pc再繼續往下進行。

  整個io差不多就是這樣的骨架,所以你看為什麼作業系統關注io,關注記憶體管理,關注多進程,因為沒啥別的東西可關注了,cpu原本能做的事情太簡單了,所謂作業系統也好,dma這些新增的硬體也好,沒有什麼技術上高階的事情,或者說在計算機底層,高階的本質就是複雜和麻煩。你說研究底層的人特別高階不如說它們對每個細節瞭解得足夠全面,這些細碎的知識聯絡在一起本身就是足夠高階的技術了,這也回答了我好久之前寫過的一篇《究竟什麼是技術》。你包括我的第一張74LS173的針腳圖,如果你看了我說的什麼“部件”巴拉巴拉明白了,你可以說你懂,當然你把cpu主要部件的針腳圖都看了,都記住了並且在麵包板或者焊接版上接過了,你也可以說你懂。但這層次就不同了,所謂理解得深不深,其實就在於細節。

  再說說記憶體地址管理,或者說定址方式,當然你可以在指令中的地址就表示絕對的地址,不經過任何轉換直接到記憶體或者相應的裝置中輸入這個地址訊號然後讀資料。你或者把倆地址拼一塊,形成一個新地址。再或者你形成新地址後再通過某種方式轉換對映一下,或者再對映一下。等等,作業系統對記憶體的管理就是這些,全都是細節。我只是簡單入了個門,最開始cpu是絕對地址定址,就是我指令中的地址直接輸入到某個部件的地址線上。第一個搞事情的是8086cpu,也就是x86架構的鼻祖cpu,它有16位資料線,但有20位地址線。當然你可以只用16位地址線但當時恰好人們覺得地址不夠用了,然後又各種原因不能弄成32位的cpu,於是乎定址的時候就把一個暫存器當作段地址資料,另一個當作段內地址,其實別管那麼多,就是應給湊成了20位地址罷了,這樣定址範圍就擴大了。但這設計好不好?美其名曰段地址和段內地址,其實這很麻煩,如果cpu位數夠沒人給自己找這種麻煩,以至於後來的32位cpu為了相容以前的拍腦門設計,即便是定址空間已經夠了但還是採用這種段方式。但後來又說作業系統變得複雜了,倒逼著cpu弄出真實模式和保護模式,每個段也有自己的許可權呀長度呀等等各種標誌了,這樣段暫存器這樣的設計就硬生生變得有用起來了,指向一張段表記錄這些標誌型的資料。

  段的長度是可以改變的,我們先假定它大小固定這樣好說明,假如我記憶體一共能容納10個段,然後我硬碟能容納1000個段,我段表就記錄我記憶體中的這10個段對應著硬碟中的哪段資料。然後呢我程式設計的時候,地址範圍寫成硬碟那樣大,然後有個專門的應將mmu用來把我程式中的地址通過查表翻譯成記憶體中的地址,如果沒有,那就把硬碟中的那個地址的資料放在記憶體中,最終翻譯成的還是記憶體中的地址。這裡就用到了虛擬地址的概念,我程式中的地址是虛擬地址,幫我查表翻譯娜硬碟到記憶體的裝置叫MMU,最終翻譯成的記憶體地址是實體地址。用段的方式來管理這個虛擬記憶體就叫做段式記憶體管理。就醬。

  段式的管理好處是長度可變比較靈活,不好的地方是你比如你A段和C段原來是B段,大小是1000,現在不用了這個地方挪出來了,你新來個從硬碟調來的資料,大小是1001,是不是很膈應人。看著放進去正好卻偏偏差一個,於是乎你只能把整個C段往前娜。要是心來個資料是900,其他地方都放不下只能放在這,那剩下的100就很尷尬。這個尷尬的100就叫做記憶體碎片。根據角度不同,如果你說這個100是由於那個900擠進來了剩下的空間,那就叫內部碎片,如果你說這個100太小新來的程式放不進來,那就叫外部碎片。這塊我覺得通過段式叫外部頁式叫內部不好,希望大家來討論下。那為了解決這個問題就有了頁式管理,頁就是個概念而已,你願意叫他不變的段也行。硬碟被分為固定大小的物理頁,作業系統邏輯上頁分為了同等數量同等大小的邏輯頁,然後同樣有個叫頁表的東西記錄了邏輯頁和物理頁的對應關係,然後和段一樣當請求的一個頁不在頁表中,準確說是頁表中標誌了這個頁不在記憶體,那就把硬碟中的頁調進來,這個過程就叫缺頁中斷。這個頁式當然頁有好有壞,然後又有個和稀泥的辦法就是段頁式管理。一句話,怎麼的都行,現在作業系統基本都是頁,完事。

  再說說程序的部分,哎不說了,這塊是在連入門都不算,完全不懂就不bb了,留在下一篇吧。

  其實上面從某個地方突然就從計算機組成原理的畫風轉成作業系統了,下面簡單說說作業系統為啥出現。當然一開始那個cpu和外設已經可以做想做的任何事了,你完全可以純手工的方式去把記憶體中的一塊區域一個個地寫入程式碼嘛,然後程式計數器搞一個初值,電一通跑起來。但這太噁心了,於是有了卡片機,再來個卡片機讀入的程式事先寫好,這樣你就不用手工操作記憶體了,你製作好卡片就好,反正是方便了一點,但本質一樣。這叫做手工作業系統,也可以叫沒有作業系統。後來發現即使是完全相同的工作,仍然需要每次取出紙片再放進去,比如有兩個卡片1和2,有個程式是執行1122121這種順序,那就需要一個人來來回回放紙,這不科學。於是有了批處理作業系統,人可以事先把1和2載入到記憶體,然後弄一個c卡片來負責呼叫這個1和2卡片,這就是排程程式,也可以叫監督程式。這就叫做批處理作業系統。再後來可以交替執行多個任務,一個任務遇到io操作就切換,這叫多道程式系統。但一個作業扔進去之後就不受使用者管了,沒有互動,於是有了分時作業系統,可以有多個終端使用cpu通過命令的方式並得到響應。但如果某些特別操作需要立刻相應可能就沒法做到,於是通過引入中斷和嚴格的中斷時間控制做到了實時作業系統。再後來就是我們現在的作業系統啦。其實對這些叫法和定義我並不是特別理解,所以你可以發現我講的其實挺亂的,這裡也希望有大佬給個好的解釋。

  今天就寫到這吧,想著這些天好像學了很多東西,但自己寫出來發現把所有肚子裡東西吐出來就這麼點,沒什麼系統就是想到哪寫到哪能串的儘量串一下了,工作之餘偷偷溜走寫的。再有這裡面的地址呀資料呀好多就是舉例,不是準確的值,為了方便理解而已,主要我也不願意花時間搞個準確的放在這樣一篇隨便寫的文章,以後會把某些地方具體拿出來講。希望各位大佬給出批評指正或者吐槽探討,感激不盡!